glazed-oxidized 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,708 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ # Derived from Oxidized (https://github.com/ytti/oxidized) lib/refinements.rb,
7
+ # lib/oxidized/model/model.rb, lib/oxidized/model/outputs.rb and
8
+ # lib/oxidized/input/{cli,ssh,telnet}.rb,
9
+ # Copyright 2013-2015 Saku Ytti, Samer Abdel-Hafez and Oxidized contributors,
10
+ # licensed under the Apache License, Version 2.0. Modified to run on glazed.
11
+
12
+ # The part of Oxidized's API that model files use. Loaded inside the
13
+ # Ruby::Box of each model, never into the main box; input classes reach the
14
+ # device through a {Glazed::Oxidized::Bridge}.
15
+
16
+ # String helpers that models activate with `using Refinements`.
17
+ module Refinements
18
+ refine String do
19
+ attr_accessor :type, :cmd, :name
20
+
21
+ # @param lines [Integer] lines to remove
22
+ # @return [String] copy of self without the last lines
23
+ #: (?Integer lines) -> String
24
+ def cut_tail(lines = 1)
25
+ return "" if length.zero?
26
+
27
+ each_line.to_a[0..(-1 - lines)].join
28
+ end
29
+
30
+ # @param lines [Integer] lines to remove
31
+ # @return [String] copy of self without the first lines
32
+ #: (?Integer lines) -> String
33
+ def cut_head(lines = 1)
34
+ return "" if length.zero?
35
+
36
+ each_line.to_a[lines..].join
37
+ end
38
+
39
+ # @param head [Integer] leading lines to remove
40
+ # @param tail [Integer] trailing lines to remove
41
+ # @return [String] copy of self without the first and last lines
42
+ #: (?Integer head, ?Integer tail) -> String
43
+ def cut_both(head = 1, tail = 1)
44
+ return "" if length.zero?
45
+
46
+ each_line.to_a[head..(-1 - tail)].join
47
+ end
48
+
49
+ # Records the command that produced this output.
50
+ #
51
+ # @param command [String] command line
52
+ # @return [String] the output name
53
+ #: (String command) -> String
54
+ def process_cmd(command)
55
+ @cmd = command
56
+ @name ||= @cmd.to_s.strip.gsub(/\s+/, '_')
57
+ end
58
+
59
+ # Copies command, name and type from another output.
60
+ #
61
+ # @param str [String] output to copy from
62
+ # @return [void]
63
+ # @raise [TypeError] when str is not a String
64
+ #: (?String str) -> void
65
+ def init_from_string(str = '')
66
+ raise TypeError unless str.instance_of?(String)
67
+
68
+ @cmd = str.instance_variable_get(:@cmd)
69
+ @name = str.instance_variable_get(:@name)
70
+ @type = str.instance_variable_get(:@type)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Oxidized's namespace as model files see it.
76
+ module Oxidized
77
+ # Not at the top level: a box requiring this from a non-main thread runs it with a self lacking main's #using.
78
+ using Refinements
79
+
80
+ # Base class of errors models raise.
81
+ class OxidizedError < StandardError; end
82
+ # Raised when the device prompt cannot be detected.
83
+ class PromptUndetect < OxidizedError; end
84
+ # Raised for features this compatibility layer lacks.
85
+ class NotSupported < OxidizedError; end
86
+
87
+ # Logger handed to models, writing warnings and errors to standard error.
88
+ class Logger
89
+ %i[debug info].each do |level|
90
+ define_method(level) { |*| nil }
91
+ end
92
+
93
+ %i[warn error fatal].each do |level|
94
+ define_method(level) { |message = nil, &block| $stderr.puts("oxidized model #{level}: #{message || block&.call}") }
95
+ end
96
+ end
97
+
98
+ # @return [Logger] logger for models
99
+ #: () -> Logger
100
+ def self.logger
101
+ @logger ||= Logger.new
102
+ end
103
+
104
+ # Configuration access, limited to variables.
105
+ class Config
106
+ # Variable lookup shared by models and inputs.
107
+ module Vars
108
+ # @param name [Symbol, String] variable name
109
+ # @return [Object, nil] the node's variable
110
+ #: (Symbol | String name) -> untyped
111
+ def vars(name)
112
+ @node&.vars&.[](name.to_sym)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Stands in for command output to record the substitutions of a model's
118
+ # `cmd :secret` blocks, so glazed can turn them into sensitive patterns.
119
+ class SecretRecorder < BasicObject
120
+ # Raised when a block does anything but substitute a string for a pattern.
121
+ class Unrecordable < ::StandardError; end
122
+
123
+ # @param model_class [Class] model whose blocks to record
124
+ # @return [Array<Array(Regexp, String)>] patterns and replacements, in order
125
+ # @raise [Unrecordable] when a block does anything else
126
+ #: (untyped model_class) -> Array[[Regexp, String]]
127
+ def self.record(model_class)
128
+ recorder = new
129
+ model = model_class.new
130
+ model_class.cmds[:secret].each { model.instance_exec(recorder, '', &it) }
131
+ recorder.__substitutions
132
+ end
133
+
134
+ #: () -> void
135
+ def initialize
136
+ @substitutions = []
137
+ end
138
+
139
+ # @return [Array<Array(Regexp, String)>] recorded patterns and replacements
140
+ #: () -> Array[[Regexp, String]]
141
+ def __substitutions = @substitutions
142
+
143
+ %i[sub sub! gsub gsub!].each do |name|
144
+ define_method(name) do |pattern, replacement = nil, &block|
145
+ unless replacement.instance_of?(::String) && block.nil?
146
+ ::Kernel.raise Unrecordable, "calls #{name} without a string replacement"
147
+ end
148
+
149
+ @substitutions << [pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern)), replacement]
150
+ self
151
+ end
152
+ end
153
+
154
+ # @param name [Symbol] method called
155
+ # @raise [Unrecordable] always
156
+ #: (Symbol name, *untyped _args) -> bot
157
+ def method_missing(name, *_args)
158
+ ::Kernel.raise Unrecordable, "calls #{name}"
159
+ end
160
+
161
+ # @return [Boolean] false
162
+ #: (Symbol _name, ?bool _include_private) -> bool
163
+ def respond_to_missing?(_name, _include_private = false) = false
164
+ end
165
+
166
+ # Base class of device models.
167
+ class Model
168
+ include Oxidized::Config::Vars
169
+
170
+ class << self
171
+ # Starts or inherits declarations for a new model.
172
+ #
173
+ # @param klass [Class] the new model
174
+ # @return [void]
175
+ #: (untyped klass) -> void
176
+ def inherited(klass)
177
+ super
178
+ if klass.superclass == Oxidized::Model
179
+ klass.instance_variable_set '@cmd', Hash.new { |h, k| h[k] = [] }
180
+ klass.instance_variable_set '@cfg', Hash.new { |h, k| h[k] = [] }
181
+ klass.instance_variable_set '@procs', Hash.new { |h, k| h[k] = [] }
182
+ klass.instance_variable_set '@expect', []
183
+ klass.instance_variable_set '@comment', nil
184
+ klass.instance_variable_set '@prompt', nil
185
+ else
186
+ instance_variables.each do |var|
187
+ iv = instance_variable_get(var)
188
+ klass.instance_variable_set var, iv.dup
189
+ @cmd[:cmd] = iv[:cmd].dup if var.to_s == "@cmd"
190
+ end
191
+ end
192
+ end
193
+
194
+ # Declares or reads the comment prefix.
195
+ #
196
+ # @param str [String] prefix used when none is declared yet
197
+ # @return [String] the comment prefix
198
+ #: (?String str) ?{ () -> String } -> String
199
+ def comment(str = "# ")
200
+ @comment = if block_given?
201
+ yield
202
+ elsif !@comment
203
+ str
204
+ else
205
+ @comment
206
+ end
207
+ end
208
+
209
+ # Declares or reads the prompt pattern.
210
+ #
211
+ # @param regex [Regexp, nil] prompt pattern
212
+ # @return [Regexp, nil] the prompt pattern
213
+ #: (?Regexp? regex) -> Regexp?
214
+ def prompt(regex = nil)
215
+ @prompt = regex || @prompt
216
+ end
217
+
218
+ # Declares blocks evaluated on the input for transports.
219
+ #
220
+ # @param methods [Array<Symbol>] transport names
221
+ # @param args [Hash] :clear or :prepend
222
+ # @return [void]
223
+ #: (*Symbol methods, **untyped args) { () -> void } -> void
224
+ def cfg(*methods, **args, &block)
225
+ [methods].flatten.each do |method|
226
+ process_args_block(@cfg[method.to_s], args, block)
227
+ end
228
+ end
229
+
230
+ # @return [Hash{String => Array<Proc>}] cfg blocks by transport name
231
+ #: () -> Hash[String, Array[Proc]]
232
+ def cfgs
233
+ @cfg
234
+ end
235
+
236
+ # Declares a command, or a block for :all or :secret outputs.
237
+ #
238
+ # @param cmd_arg [String, Symbol, Proc] command line, or :all or :secret
239
+ # @param args [Hash] :clear or :prepend
240
+ # @return [void]
241
+ #: (?untyped cmd_arg, **untyped args) ?{ (String) -> untyped } -> void
242
+ def cmd(cmd_arg = nil, **args, &block)
243
+ if cmd_arg.instance_of?(Symbol)
244
+ process_args_block(@cmd[cmd_arg], args, block)
245
+ else
246
+ process_args_block(@cmd[:cmd], args, [cmd_arg, block])
247
+ end
248
+ end
249
+
250
+ # @return [Hash{Symbol => Array}] declared commands and blocks
251
+ #: () -> Hash[Symbol, Array[untyped]]
252
+ def cmds
253
+ @cmd
254
+ end
255
+
256
+ # Declares a response to a pattern in received data.
257
+ #
258
+ # @param regex [Regexp] pattern to watch for
259
+ # @param args [Hash] :clear or :prepend
260
+ # @return [void]
261
+ #: (Regexp regex, **untyped args) { (String) -> String } -> void
262
+ def expect(regex, **args, &block)
263
+ process_args_block(@expect, args, [regex, block])
264
+ end
265
+
266
+ # @return [Array<Array(Regexp, Proc)>] declared responses
267
+ #: () -> Array[[Regexp, Proc]]
268
+ def expects
269
+ @expect
270
+ end
271
+
272
+ # Declares a block whose output is prepended.
273
+ #
274
+ # @param args [Hash] :clear or :prepend
275
+ # @return [void]
276
+ #: (**untyped args) { () -> String } -> void
277
+ def pre(**args, &block)
278
+ process_args_block(@procs[:pre], args, block)
279
+ end
280
+
281
+ # Declares a block whose output is appended.
282
+ #
283
+ # @param args [Hash] :clear or :prepend
284
+ # @return [void]
285
+ #: (**untyped args) { () -> String } -> void
286
+ def post(**args, &block)
287
+ process_args_block(@procs[:post], args, block)
288
+ end
289
+
290
+ # @return [Hash{Symbol => Array<Proc>}] :pre and :post blocks
291
+ #: Hash[Symbol, Array[Proc]]
292
+ attr_reader :procs
293
+
294
+ # @param target [Array] list to add to
295
+ # @param args [Hash] :clear or :prepend
296
+ # @param block [Proc, Array] entry to add
297
+ # @return [void]
298
+ #: (Array[untyped] target, Hash[Symbol, untyped] args, untyped block) -> void
299
+ private def process_args_block(target, args, block)
300
+ if args[:clear]
301
+ if block.instance_of?(Array)
302
+ target.reject! { |k, _| k == block[0] }
303
+ target.push(block)
304
+ else
305
+ target.replace([block])
306
+ end
307
+ else
308
+ method = args[:prepend] ? :unshift : :push
309
+ target.send(method, block)
310
+ end
311
+ end
312
+ end
313
+
314
+ # @return [Input] input talking to the device
315
+ #: Input
316
+ attr_accessor :input
317
+ # @return [Glazed::Oxidized::NodeView] node being collected
318
+ #: untyped
319
+ attr_accessor :node
320
+
321
+ # Runs a command through the :all, :secret and given blocks.
322
+ #
323
+ # @param string [String] command line
324
+ # @return [String, false] processed output, or false when the input returned none
325
+ #: (String string) ?{ (String) -> untyped } -> (String | false)
326
+ def cmd(string, &block)
327
+ out = @input.cmd(string)
328
+ return false unless out
329
+
330
+ self.class.cmds[:all].each do |all_block|
331
+ out = instance_exec out, string, &all_block
332
+ end
333
+ if vars :remove_secret
334
+ self.class.cmds[:secret].each do |all_block|
335
+ out = instance_exec out, string, &all_block
336
+ end
337
+ end
338
+ out = instance_exec out, &block if block
339
+ process_cmd_output out, string
340
+ end
341
+
342
+ # @return [String] data the input received last
343
+ #: () -> String
344
+ def output
345
+ @input.output
346
+ end
347
+
348
+ # Sends raw data.
349
+ #
350
+ # @param data [String] data to send
351
+ # @return [void]
352
+ #: (String data) -> void
353
+ def send(data)
354
+ @input.send data
355
+ end
356
+
357
+ # @return [void]
358
+ #: (*untyped) -> void
359
+ def expect(...)
360
+ self.class.expect(...)
361
+ end
362
+
363
+ # @return [Hash{String => Array<Proc>}] cfg blocks by transport name
364
+ #: () -> Hash[String, Array[Proc]]
365
+ def cfg
366
+ self.class.cfgs
367
+ end
368
+
369
+ # @return [Regexp, nil] the prompt pattern
370
+ #: () -> Regexp?
371
+ def prompt
372
+ self.class.prompt
373
+ end
374
+
375
+ # Applies every declared response whose pattern matches.
376
+ #
377
+ # @param data [String] received data
378
+ # @return [String] data after the responses
379
+ #: (String data) -> String
380
+ def expects(data)
381
+ self.class.expects.each do |re, cb|
382
+ data = run_expect(re, cb, data) if data.match re
383
+ end
384
+ data
385
+ end
386
+
387
+ # Collects the outputs of every command and the pre and post blocks.
388
+ #
389
+ # @return [Outputs, false] the outputs, or false when a command returned none
390
+ #: () -> (Outputs | false)
391
+ def get
392
+ outputs = Outputs.new
393
+ procs = self.class.procs
394
+ self.class.cmds[:cmd].each do |command, block|
395
+ out = cmd command, &block
396
+ return false unless out
397
+
398
+ outputs << out
399
+ end
400
+ procs[:pre].each do |pre_proc|
401
+ outputs.unshift process_cmd_output(instance_eval(&pre_proc), '')
402
+ end
403
+ procs[:post].each do |post_proc|
404
+ outputs << process_cmd_output(instance_eval(&post_proc), '')
405
+ end
406
+ outputs
407
+ end
408
+
409
+ # @param str [String] text to comment out
410
+ # @return [String] every line prefixed with the comment prefix
411
+ #: (String str) -> String
412
+ def comment(str)
413
+ data = String.new('')
414
+ str.each_line do |line|
415
+ data << self.class.comment << line
416
+ end
417
+ data
418
+ end
419
+
420
+ # @param str [String] text to comment out
421
+ # @return [String] the text as XML comments
422
+ #: (String str) -> String
423
+ def xmlcomment(str)
424
+ data = String.new('')
425
+ str.each_line do |_line|
426
+ data << '<!-- ' << str.gsub(/-(?=-)/, '- ').chomp << " -->\n"
427
+ end
428
+ data
429
+ end
430
+
431
+ # @return [Boolean] whether commands run on an interactive shell
432
+ #: () -> untyped
433
+ def screenscrape
434
+ @input.class.to_s.match(/Telnet/) || vars(:ssh_no_exec)
435
+ end
436
+
437
+ # Applies one declared response.
438
+ #
439
+ # @param index [Integer] position of the response
440
+ # @param data [String] received data
441
+ # @return [String] data after the response
442
+ #: (Integer index, String data) -> String
443
+ def glazed_expect(index, data)
444
+ re, cb = self.class.expects.fetch(index)
445
+ run_expect(re, cb, data)
446
+ end
447
+
448
+ # @return [String, nil] the configuration, or nil when a command returned none
449
+ #: () -> String?
450
+ def glazed_get
451
+ outputs = get
452
+ outputs ? outputs.to_cfg : nil
453
+ end
454
+
455
+ # @param re [Regexp] pattern of the response
456
+ # @param cb [Proc] the response
457
+ # @param data [String] received data
458
+ # @return [String] data after the response
459
+ #: (Regexp re, Proc cb, String data) -> String
460
+ private def run_expect(re, cb, data)
461
+ cb.arity == 2 ? instance_exec([data, re], &cb) : instance_exec(data, &cb)
462
+ end
463
+
464
+ # @param output [Object] block result
465
+ # @param name [String] command line
466
+ # @return [String] the output, an empty String unless it is one
467
+ #: (untyped output, String name) -> String
468
+ private def process_cmd_output(output, name)
469
+ output = String.new('') unless output.instance_of?(String)
470
+ output.process_cmd(name)
471
+ output
472
+ end
473
+
474
+ # Outputs of a model run.
475
+ class Outputs
476
+ #: () -> void
477
+ def initialize
478
+ @outputs = []
479
+ end
480
+
481
+ # @return [String] outputs without a type, joined
482
+ #: () -> String
483
+ def to_cfg
484
+ type_to_str(nil)
485
+ end
486
+
487
+ # @param want_type [String, nil] output type
488
+ # @return [String] outputs of the type, joined
489
+ #: (String? want_type) -> String
490
+ def type_to_str(want_type)
491
+ type(want_type).join
492
+ end
493
+
494
+ # @param output [String] output to append
495
+ # @return [void]
496
+ #: (String output) -> void
497
+ def <<(output)
498
+ @outputs << output
499
+ end
500
+
501
+ # @param output [String] output to prepend
502
+ # @return [void]
503
+ #: (String output) -> void
504
+ def unshift(output)
505
+ @outputs.unshift output
506
+ end
507
+
508
+ # @return [Array<String>] every output
509
+ #: () -> Array[String]
510
+ def all
511
+ @outputs
512
+ end
513
+
514
+ # @param type [String, nil] output type
515
+ # @return [Array<String>] outputs of the type
516
+ #: (String? type) -> Array[String]
517
+ def type(type)
518
+ @outputs.select { |out| out.type == type }
519
+ end
520
+
521
+ # @return [Array<String>] output types
522
+ #: () -> Array[String]
523
+ def types
524
+ @outputs.map(&:type).uniq.compact
525
+ end
526
+ end
527
+ end
528
+
529
+ # Base class of inputs, which carry commands to the device through a bridge.
530
+ class Input
531
+ include Oxidized::Config::Vars
532
+
533
+ # Connects the input to a bridge and a node.
534
+ #
535
+ # @param bridge [Glazed::Oxidized::Bridge] session to the device
536
+ # @param node [Glazed::Oxidized::NodeView, nil] node being collected
537
+ # @return [self]
538
+ #: (untyped bridge, untyped node) -> self
539
+ def glazed_attach(bridge, node)
540
+ @glazed_bridge = bridge
541
+ @node = node
542
+ self
543
+ end
544
+
545
+ # Sends raw data.
546
+ #
547
+ # @param data [String] data to send
548
+ # @return [void]
549
+ #: (String data) -> void
550
+ def send(data)
551
+ @glazed_bridge.write(data)
552
+ end
553
+
554
+ # @return [String] data received but not consumed yet
555
+ #: () -> String
556
+ def output
557
+ @glazed_bridge.output
558
+ end
559
+
560
+ # Login and logout handling of command-line inputs.
561
+ module CLI
562
+ # @return [Glazed::Oxidized::NodeView, nil] node being collected
563
+ #: untyped
564
+ attr_reader :node
565
+
566
+ # Starts with no login handling declared.
567
+ #: () -> void
568
+ def initialize
569
+ @post_login = []
570
+ @pre_logout = []
571
+ @username, @password, @exec = nil
572
+ end
573
+
574
+ # Runs the post_login commands and blocks.
575
+ #
576
+ # @return [void]
577
+ #: () -> void
578
+ def connect_cli
579
+ @post_login.each { |command, block| block ? block.call : cmd(command) }
580
+ end
581
+
582
+ # Runs the pre_logout commands and blocks.
583
+ #
584
+ # @return [void]
585
+ #: () -> void
586
+ def disconnect_cli
587
+ @pre_logout.each { |command, block| block ? block.call : cmd(command, nil) }
588
+ end
589
+
590
+ # Declares a command or block run after login.
591
+ #
592
+ # @param cmd [String, nil] command line
593
+ # @return [void]
594
+ #: (?String? cmd) ?{ () -> void } -> void
595
+ def post_login(cmd = nil, &block)
596
+ return if @exec
597
+
598
+ @post_login << [cmd, block]
599
+ end
600
+
601
+ # Declares a command or block run before logout.
602
+ #
603
+ # @param cmd [String, nil] command line
604
+ # @return [void]
605
+ #: (?String? cmd) ?{ () -> void } -> void
606
+ def pre_logout(cmd = nil, &block)
607
+ return if @exec
608
+
609
+ @pre_logout << [cmd, block]
610
+ end
611
+
612
+ # Declares the user name prompt.
613
+ #
614
+ # @param regex [Regexp] prompt pattern
615
+ # @return [Regexp] the declared pattern
616
+ #: (?Regexp regex) -> Regexp
617
+ def username(regex = /^(Username|login)/)
618
+ @username || (@username = regex)
619
+ end
620
+
621
+ # Declares the password prompt.
622
+ #
623
+ # @param regex [Regexp] prompt pattern
624
+ # @return [Regexp] the declared pattern
625
+ #: (?Regexp regex) -> Regexp
626
+ def password(regex = /^Password/)
627
+ @password || (@password = regex)
628
+ end
629
+
630
+ # @return [Regexp, nil] the declared user name prompt
631
+ #: () -> Regexp?
632
+ def glazed_username_prompt = @username
633
+
634
+ # @return [Regexp, nil] the declared password prompt
635
+ #: () -> Regexp?
636
+ def glazed_password_prompt = @password
637
+
638
+ # @return [Boolean] whether commands run on exec channels
639
+ #: () -> bool
640
+ def glazed_exec? = @exec ? true : false
641
+
642
+ # Waits for one of the patterns.
643
+ #
644
+ # @param regexps [Array<Regexp>] patterns
645
+ # @return [Regexp] the pattern that matched
646
+ #: (*Regexp regexps) -> Regexp
647
+ private def expect(*regexps)
648
+ @glazed_bridge.expect(regexps.flatten)
649
+ end
650
+ end
651
+ end
652
+
653
+ # Input over SSH.
654
+ class SSH < Input
655
+ include Input::CLI
656
+
657
+ # Raised when no shell can be opened.
658
+ class NoShell < OxidizedError; end
659
+
660
+ # Runs a command.
661
+ #
662
+ # @param cmd [String] command line
663
+ # @param expect [Regexp, Array<Regexp>, nil] patterns ending the output; nil only sends the line
664
+ # @return [String] output including the echoed command and the prompt, unless on an exec channel
665
+ #: (String cmd, ?untyped expect) -> String
666
+ def cmd(cmd, expect = node.prompt)
667
+ @glazed_bridge.cmd(cmd, expect, exec: glazed_exec?)
668
+ end
669
+
670
+ # PTY options are left to the glazed transport.
671
+ #
672
+ # @param _hash [Hash] PTY options
673
+ # @return [void]
674
+ #: (Hash[Symbol, untyped] _hash) -> void
675
+ def pty_options(_hash); end
676
+
677
+ # Declares or reads whether commands run on exec channels.
678
+ #
679
+ # @param state [Boolean, nil] true to use exec channels
680
+ # @return [Boolean, nil] the setting
681
+ #: (?bool? state) -> bool?
682
+ private def exec(state = nil)
683
+ return nil if vars(:ssh_no_exec)
684
+
685
+ state.nil? ? @exec : (@exec = state)
686
+ end
687
+ end
688
+
689
+ # Input over Telnet.
690
+ class Telnet < Input
691
+ include Input::CLI
692
+
693
+ # Runs a command.
694
+ #
695
+ # @param cmd_str [String] command line
696
+ # @param expect [Regexp, Array<Regexp>, nil] patterns ending the output; nil only sends the line
697
+ # @return [String] output including the echoed command and the prompt
698
+ #: (String cmd_str, ?untyped expect) -> String
699
+ def cmd(cmd_str, expect = @node.prompt)
700
+ unless expect
701
+ send(cmd_str + "\r\n")
702
+ return String.new('')
703
+ end
704
+
705
+ @glazed_bridge.cmd(cmd_str, expect, exec: false)
706
+ end
707
+ end
708
+ end