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.
- checksums.yaml +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +202 -0
- data/README.md +16 -0
- data/Rakefile +12 -0
- data/lib/glazed/oxidized/bridge.rb +148 -0
- data/lib/glazed/oxidized/errors.rb +21 -0
- data/lib/glazed/oxidized/model_box.rb +115 -0
- data/lib/glazed/oxidized/model_dialect.rb +119 -0
- data/lib/glazed/oxidized/node_view.rb +69 -0
- data/lib/glazed/oxidized/prelude.rb +708 -0
- data/lib/glazed/oxidized/secret_substitution.rb +105 -0
- data/lib/glazed/oxidized/version.rb +11 -0
- data/lib/glazed/oxidized.rb +93 -0
- metadata +71 -0
|
@@ -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
|