glazed 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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +5 -0
  4. data/CHANGELOG.md +18 -0
  5. data/LICENSE.txt +202 -0
  6. data/NOTICE +16 -0
  7. data/README.md +3 -0
  8. data/Rakefile +12 -0
  9. data/exe/glazed +8 -0
  10. data/lib/glazed/cli_session.rb +178 -0
  11. data/lib/glazed/collector.rb +113 -0
  12. data/lib/glazed/command/base.rb +146 -0
  13. data/lib/glazed/command/orchestrate.rb +78 -0
  14. data/lib/glazed/command/run.rb +51 -0
  15. data/lib/glazed/command/send_signal.rb +69 -0
  16. data/lib/glazed/command/status.rb +179 -0
  17. data/lib/glazed/command/version.rb +23 -0
  18. data/lib/glazed/command.rb +39 -0
  19. data/lib/glazed/command_result.rb +26 -0
  20. data/lib/glazed/dialect/actions.rb +95 -0
  21. data/lib/glazed/dialect/communication_config.rb +63 -0
  22. data/lib/glazed/dialect/interaction.rb +32 -0
  23. data/lib/glazed/dialect/run.rb +84 -0
  24. data/lib/glazed/dialect/runbook.rb +97 -0
  25. data/lib/glazed/dialect/sensitive_pattern.rb +31 -0
  26. data/lib/glazed/dialect.rb +12 -0
  27. data/lib/glazed/dialects/awplus.rb +84 -0
  28. data/lib/glazed/dialects/base.rb +246 -0
  29. data/lib/glazed/dialects/ios.rb +173 -0
  30. data/lib/glazed/dialects/junos.rb +78 -0
  31. data/lib/glazed/dialects/nec_ix.rb +71 -0
  32. data/lib/glazed/dialects/wlc.rb +56 -0
  33. data/lib/glazed/dialects.rb +65 -0
  34. data/lib/glazed/finder.rb +62 -0
  35. data/lib/glazed/must.rb +54 -0
  36. data/lib/glazed/orchestrator/policy.rb +184 -0
  37. data/lib/glazed/orchestrator.rb +288 -0
  38. data/lib/glazed/plugin_registry.rb +70 -0
  39. data/lib/glazed/proto.rb +20 -0
  40. data/lib/glazed/proto_json.rb +34 -0
  41. data/lib/glazed/redactor.rb +158 -0
  42. data/lib/glazed/runner.rb +176 -0
  43. data/lib/glazed/secret.rb +74 -0
  44. data/lib/glazed/secrets.rb +59 -0
  45. data/lib/glazed/shell_command.rb +90 -0
  46. data/lib/glazed/sources/base.rb +79 -0
  47. data/lib/glazed/sources/command.rb +67 -0
  48. data/lib/glazed/sources/inline.rb +56 -0
  49. data/lib/glazed/sources/json_files.rb +87 -0
  50. data/lib/glazed/sources.rb +105 -0
  51. data/lib/glazed/state_backends/base.rb +72 -0
  52. data/lib/glazed/state_backends/sqlite.rb +299 -0
  53. data/lib/glazed/state_backends.rb +75 -0
  54. data/lib/glazed/step_context.rb +65 -0
  55. data/lib/glazed/steps/base.rb +89 -0
  56. data/lib/glazed/steps/batch_shell.rb +57 -0
  57. data/lib/glazed/steps/fetch.rb +67 -0
  58. data/lib/glazed/steps/publish.rb +94 -0
  59. data/lib/glazed/steps/redact.rb +77 -0
  60. data/lib/glazed/steps/shell.rb +107 -0
  61. data/lib/glazed/steps.rb +83 -0
  62. data/lib/glazed/storages/base.rb +167 -0
  63. data/lib/glazed/storages/filesystem.rb +144 -0
  64. data/lib/glazed/storages/git.rb +181 -0
  65. data/lib/glazed/storages/github.rb +269 -0
  66. data/lib/glazed/storages/memory.rb +125 -0
  67. data/lib/glazed/storages.rb +88 -0
  68. data/lib/glazed/transports/base.rb +103 -0
  69. data/lib/glazed/transports/ssh.rb +177 -0
  70. data/lib/glazed/transports/telnet.rb +100 -0
  71. data/lib/glazed/transports.rb +28 -0
  72. data/lib/glazed/v1/config_pb.rb +25 -0
  73. data/lib/glazed/v1/inventory_pb.rb +27 -0
  74. data/lib/glazed/v1/orchestrator_pb.rb +27 -0
  75. data/lib/glazed/v1/redaction_pb.rb +23 -0
  76. data/lib/glazed/v1/secret_pb.rb +22 -0
  77. data/lib/glazed/v1/state_pb.rb +25 -0
  78. data/lib/glazed/v1/steps_pb.rb +25 -0
  79. data/lib/glazed/v1/storage_pb.rb +34 -0
  80. data/lib/glazed/v1/workflow_pb.rb +29 -0
  81. data/lib/glazed/validation.rb +31 -0
  82. data/lib/glazed/version.rb +9 -0
  83. data/lib/glazed/workflow.rb +70 -0
  84. data/lib/glazed.rb +18 -0
  85. metadata +245 -0
@@ -0,0 +1,246 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed/dialect'
7
+ require 'glazed/dialect/actions'
8
+ require 'glazed/dialect/communication_config'
9
+ require 'glazed/dialect/interaction'
10
+ require 'glazed/dialect/runbook'
11
+ require 'glazed/dialect/sensitive_pattern'
12
+
13
+ module Glazed
14
+ module Dialects
15
+ # Base class of dialects. A dialect describes how to talk to one kind of
16
+ # device and how to collect its configuration, through class-level
17
+ # declarations that subclasses inherit.
18
+ #
19
+ # Lists (data and output filters, interactions, sensitive patterns,
20
+ # communication blocks) accumulate from parent to child. The prompt, comment
21
+ # prefix and each runbook are replaced by a child's own declaration.
22
+ #
23
+ # @example
24
+ # class Rootware < Glazed::Dialects::Base
25
+ # prompt(/^\S+[#>]\s?$/)
26
+ # comment_prefix '! '
27
+ # runbook :get do |book|
28
+ # book.command('show version') { |result| comment(result.output) }
29
+ # book.command('show running-config')
30
+ # end
31
+ # end
32
+ class Base
33
+ # Shorthand for {Dialect::Actions} inside dialect definitions.
34
+ Actions = Glazed::Dialect::Actions
35
+
36
+ # Comment prefix used when a dialect declares none.
37
+ DEFAULT_COMMENT_PREFIX = '# ' #: String
38
+
39
+ @parent = nil #: singleton(Base)?
40
+ @prompt = nil #: Regexp?
41
+ @comment_prefix = nil #: String?
42
+ @data_filters = nil #: Array[Dialect::data_filter]?
43
+ @output_filters = nil #: Array[Dialect::data_filter]?
44
+ @interactions = nil #: Array[Dialect::Interaction]?
45
+ @sensitive_patterns = nil #: Array[Dialect::SensitivePattern]?
46
+ @communications = nil #: Array[[Array[Symbol], ^(Dialect::CommunicationConfig) -> void]]?
47
+ @runbooks = nil #: Hash[Symbol, Dialect::Runbook]?
48
+
49
+
50
+ class << self
51
+ # Records the parent so subclasses inherit declarations.
52
+ #
53
+ # @param subclass [Class<Base>] the new dialect
54
+ # @return [void]
55
+ #: (singleton(Base) subclass) -> void
56
+ def inherited(subclass)
57
+ super
58
+ subclass.parent = self
59
+ end
60
+
61
+ # Declares the command prompt pattern.
62
+ #
63
+ # @param pattern [Regexp] prompt pattern, matched against the session buffer
64
+ # @return [void]
65
+ # @example
66
+ # prompt(/^\S+[#>]\s?$/)
67
+ #: (Regexp pattern) -> void
68
+ def prompt(pattern)
69
+ @prompt = pattern
70
+ end
71
+
72
+ # @return [Regexp] the declared or inherited prompt pattern
73
+ # @raise [NotImplementedError] when no prompt is declared
74
+ #: () -> Regexp
75
+ def prompt_pattern
76
+ @prompt || @parent&.prompt_pattern || raise(NotImplementedError, "#{name} declares no prompt")
77
+ end
78
+
79
+ # Declares the prefix of comment lines in collected configuration.
80
+ #
81
+ # @param prefix [String] such as "! " or "# "
82
+ # @return [void]
83
+ # @example
84
+ # comment_prefix '! '
85
+ #: (String prefix) -> void
86
+ def comment_prefix(prefix)
87
+ @comment_prefix = prefix
88
+ end
89
+
90
+ # @return [String] the declared or inherited comment prefix
91
+ #: () -> String
92
+ def comment_prefix_value
93
+ @comment_prefix || @parent&.comment_prefix_value || DEFAULT_COMMENT_PREFIX
94
+ end
95
+
96
+ # Turns text into comment lines.
97
+ #
98
+ # @param text [String] text to comment out
99
+ # @return [String] every line prefixed, ending with a newline
100
+ #: (String text) -> String
101
+ def comment(text)
102
+ prefix = comment_prefix_value
103
+ text.each_line.map { |line| "#{prefix}#{line.chomp}".rstrip + "\n" }.join
104
+ end
105
+
106
+ # Declares a filter for each fragment of received data, such as one
107
+ # removing ANSI sequences. Fragments split at arbitrary points.
108
+ #
109
+ # @yieldparam fragment [String] received data
110
+ # @yieldreturn [String] filtered data
111
+ # @return [void]
112
+ # @example
113
+ # filter_data { |fragment| fragment.delete("\r") }
114
+ #: () { (String fragment) -> String } -> void
115
+ def filter_data(&filter)
116
+ (@data_filters ||= []) << filter
117
+ end
118
+
119
+ # @return [Array<Proc>] inherited then own data filters
120
+ #: () -> Array[Dialect::data_filter]
121
+ def data_filters = (@parent&.data_filters || []) + (@data_filters || [])
122
+
123
+ # Declares a filter for the whole output of each runbook command. The
124
+ # output arrives as bytes and must be valid UTF-8 once every filter ran,
125
+ # so a dialect for a device using another encoding converts it here.
126
+ #
127
+ # @yieldparam output [String] command output, as bytes (ASCII-8BIT)
128
+ # @yieldreturn [String] filtered output
129
+ # @return [void]
130
+ # @example
131
+ # filter_output { |output| output.gsub(/ +$/, '') }
132
+ #: () { (String output) -> String } -> void
133
+ def filter_output(&filter)
134
+ (@output_filters ||= []) << filter
135
+ end
136
+
137
+ # @return [Array<Proc>] inherited then own output filters
138
+ #: () -> Array[Dialect::data_filter]
139
+ def output_filters = (@parent&.output_filters || []) + (@output_filters || [])
140
+
141
+ # Declares a response to a pattern appearing in the session buffer.
142
+ # The responder must remove the pattern from the buffer, or it fires
143
+ # again on the next received data.
144
+ #
145
+ # @param pattern [Regexp] pattern to watch for
146
+ # @yieldparam session [CliSession] the session
147
+ # @yieldparam buffer [String] copy of the buffer
148
+ # @yieldparam match [MatchData] the match in the buffer
149
+ # @yieldreturn [Actions::Append, Actions::Replace, Actions::Multi, String, nil] action applied to the buffer
150
+ # @return [void]
151
+ # @example
152
+ # interact(/--More--/) do |cli, _buffer, match|
153
+ # cli.write(' ')
154
+ # Actions::Replace.new(content: match.pre_match + match.post_match)
155
+ # end
156
+ #: (Regexp pattern) { (CliSession session, String buffer, MatchData match) -> Dialect::Actions::action? } -> void
157
+ def interact(pattern, &responder)
158
+ (@interactions ||= []) << Dialect::Interaction.new(pattern:, responder:)
159
+ end
160
+
161
+ # @return [Array<Dialect::Interaction>] inherited then own interactions
162
+ #: () -> Array[Dialect::Interaction]
163
+ def interactions = (@parent&.interactions || []) + (@interactions || [])
164
+
165
+ # Declares where secrets appear in collected configuration.
166
+ #
167
+ # @param pattern [Regexp] pattern matched against the whole configuration
168
+ # @yieldparam match [MatchData] a match
169
+ # @yieldreturn [String, Range<Integer>, nil] the secret within the match, or its offsets in the configuration
170
+ # @return [void]
171
+ # @example
172
+ # sensitive_pattern(/^enable secret \d (\S+)/) { it[1] }
173
+ #: (Regexp pattern) { (MatchData match) -> (String | Range[Integer])? } -> void
174
+ def sensitive_pattern(pattern, &determiner)
175
+ (@sensitive_patterns ||= []) << Dialect::SensitivePattern.new(pattern:, determiner:)
176
+ end
177
+
178
+ # @return [Array<Dialect::SensitivePattern>] inherited then own sensitive patterns
179
+ #: () -> Array[Dialect::SensitivePattern]
180
+ def sensitive_patterns = (@parent&.sensitive_patterns || []) + (@sensitive_patterns || [])
181
+
182
+ # Declares communication settings for transports.
183
+ #
184
+ # @param kinds [Array<Symbol>] transport names, such as :ssh and :telnet
185
+ # @yieldparam config [Dialect::CommunicationConfig] config to adjust
186
+ # @return [void]
187
+ # @example
188
+ # communication :ssh, :telnet do |config|
189
+ # config.post_login { |run| run.cli.command('terminal length 0') }
190
+ # end
191
+ #: (*Symbol kinds) { (Dialect::CommunicationConfig config) -> void } -> void
192
+ def communication(*kinds, &configure)
193
+ (@communications ||= []) << [kinds, configure]
194
+ end
195
+
196
+ # Builds the communication settings for a transport.
197
+ #
198
+ # @param kind [Symbol] transport name
199
+ # @return [Dialect::CommunicationConfig] config with inherited then own blocks applied
200
+ #: (Symbol kind) -> Dialect::CommunicationConfig
201
+ def communication_config(kind)
202
+ config = @parent&.communication_config(kind) || Dialect::CommunicationConfig.new
203
+ (@communications || []).each do |kinds, configure|
204
+ configure.call(config) if kinds.include?(kind)
205
+ end
206
+ config
207
+ end
208
+
209
+ # Declares the runbook for an operation, replacing any inherited one.
210
+ #
211
+ # @param operation [Symbol] operation name; :get collects the configuration
212
+ # @yieldparam book [Dialect::Runbook::Builder] builder to add steps to
213
+ # @return [void]
214
+ # @example
215
+ # runbook :get do |book|
216
+ # book.command('show running-config')
217
+ # end
218
+ #: (Symbol operation) { (Dialect::Runbook::Builder book) -> void } -> void
219
+ def runbook(operation, &definition)
220
+ builder = Dialect::Runbook::Builder.new
221
+ definition.call(builder)
222
+ (@runbooks ||= {})[operation] = builder.build
223
+ end
224
+
225
+ # @param operation [Symbol] operation name
226
+ # @return [Dialect::Runbook] the declared or inherited runbook
227
+ # @raise [NotImplementedError] when no runbook is declared for the operation
228
+ #: (Symbol operation) -> Dialect::Runbook
229
+ def runbook_for(operation)
230
+ @runbooks&.[](operation) || @parent&.runbook_for(operation) ||
231
+ raise(NotImplementedError, "#{name} declares no #{operation} runbook")
232
+ end
233
+
234
+ # @param parent [Class<Base>, nil] dialect to inherit declarations from
235
+ # @return [void]
236
+ #: (singleton(Base)? parent) -> void
237
+ protected def parent=(parent)
238
+ @parent = parent
239
+ end
240
+ end
241
+
242
+ # A PTY ends lines with CRLF; normalised on whole output, as a fragment may split the pair.
243
+ filter_output { it.gsub(/\r+\n/, "\n") }
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,173 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ # Derived from Oxidized (https://github.com/ytti/oxidized) lib/oxidized/model/ios.rb,
7
+ # Copyright 2013-2015 Saku Ytti, Samer Abdel-Hafez and Oxidized contributors,
8
+ # licensed under the Apache License, Version 2.0. Modified for the glazed dialect API.
9
+
10
+ require 'glazed/dialects/base'
11
+
12
+ module Glazed
13
+ module Dialects
14
+ # Cisco IOS and IOS XE. Ported from oxidized's ios model.
15
+ #
16
+ # Node params: "ios_rbac" set to "true" collects `show running-config view full`.
17
+ # Node secrets: "enable" is sent as the enable password; without it, param
18
+ # "enable" set to "true" runs `enable` without one.
19
+ class Ios < Base
20
+ prompt(/^([\w.@()-]+[#>]\s?)$/)
21
+ comment_prefix '! '
22
+
23
+ # Commands some platforms lack leave an error behind.
24
+ filter_output { it.gsub(/^% Invalid input detected at '\^' marker\.$|^\s+\^$/, '') }
25
+
26
+ sensitive_pattern(/^snmp-server community (\S+)/) { it[1] }
27
+ # SNMPv3 hosts carry a user name, not a community.
28
+ sensitive_pattern(/^snmp-server host (?!.* version 3 )\S+(?> vrf \S+)?(?> informs?| traps)?(?> version (?:1|2c))? +(\S+)/) { it[1] }
29
+ sensitive_pattern(/^username .+ (?:password|secret) \d (.+)$/) { it[1] }
30
+ sensitive_pattern(/^enable (?:password|secret)(?: level \d+)? \d (.+)$/) { it[1] }
31
+ sensitive_pattern(/^ +(?:password|secret) (?:\d )?(\S+)/) { it[1] }
32
+ sensitive_pattern(/^.*wpa-psk ascii \d (\S+)/) { it[1] }
33
+ sensitive_pattern(/^.*key 7 (\d.+)$/) { it[1] }
34
+ sensitive_pattern(/^tacacs-server (?:.+ )?key (?:\d )?(.+)$/) { it[1] }
35
+ sensitive_pattern(/^crypto isakmp key (\S+) /) { it[1] }
36
+ sensitive_pattern(/^ +ip ospf message-digest-key \d+ md5 (?:\d )?(.+)$/) { it[1] }
37
+ sensitive_pattern(/^ +ip ospf authentication-key (?:\d )?(.+)$/) { it[1] }
38
+ sensitive_pattern(/^ +neighbor \S+ password (?:\d )?(.+)$/) { it[1] }
39
+ sensitive_pattern(/^ +vrrp \d+ authentication text (.+)$/) { it[1] }
40
+ sensitive_pattern(/^ +standby \d+ authentication (?!md5 )(.{1,8})$/) { it[1] }
41
+ sensitive_pattern(/^ +standby \d+ authentication md5 key-string (?:\d )?(.+?)(?: timeout \d+)?$/) { it[1] }
42
+ sensitive_pattern(/^ +key-string (?:\d )?(.+)$/) { it[1] }
43
+ sensitive_pattern(/^(?:tacacs|radius) server [^\n]+\n(?: +[^\n]+\n)*? +key (?:\d+ )?(\S+)/) { it[1] }
44
+ sensitive_pattern(/^ +ppp (?:chap|pap) password \d (.+)$/) { it[1] }
45
+ sensitive_pattern(/^ +security wpa psk set-key (?:ascii|hex) \d (.+)$/) { it[1] }
46
+ sensitive_pattern(/^ +dot1x username \S+ password \d (.+)$/) { it[1] }
47
+ sensitive_pattern(/^ +mgmtuser username \S+ password \d (.*) secret \d /) { it[1] }
48
+ sensitive_pattern(/^ +mgmtuser username \S+ password \d .* secret \d (.*)$/) { it[1] }
49
+ sensitive_pattern(/^ +client \S+ server-key \d (.+)$/) { it[1] }
50
+ sensitive_pattern(/^ +domain-password (\S+)/) { it[1] }
51
+ sensitive_pattern(/^ +pre-shared-key (.+)$/) { it[1] }
52
+
53
+ runbook :get do |book|
54
+ book.command('show version') { comment(version_summary(it.output)) }
55
+ book.command('show vtp status') do |result|
56
+ vtp = result.output.gsub(/^\s*\n/, '').gsub(/Configuration last modified by.*\n/, '')
57
+ comment("#{vtp.gsub(/^/, 'VTP: ')}\n")
58
+ end
59
+ book.command('show inventory') { comment(it.output) }
60
+ book.raw('show running-config') do |run|
61
+ line = run.param('ios_rbac') == 'true' ? 'show running-config view full' : 'show running-config'
62
+ run.emit(running_config(run.command(line).output))
63
+ end
64
+ end
65
+
66
+ communication :telnet do |config|
67
+ config.username_prompt = /^Username:/i
68
+ config.password_prompt = /^Password:/i
69
+ end
70
+
71
+ communication :ssh, :telnet do |config|
72
+ config.post_login do |run|
73
+ if (enable = run.secret('enable'))
74
+ run.cli.write("enable\n")
75
+ run.cli.expect(/^[pP]assword:/)
76
+ run.cli.write("#{enable.value}\n")
77
+ run.cli.expect(run.cli.prompt)
78
+ elsif run.param('enable') == 'true'
79
+ run.cli.command('enable')
80
+ end
81
+ run.cli.command('terminal length 0')
82
+ run.cli.command('terminal width 0')
83
+ end
84
+ config.pre_logout { |run| run.cli.write("exit\n") }
85
+ end
86
+
87
+ class << self
88
+ # Summarises `show version` into stable facts, as oxidized does.
89
+ #
90
+ # @param output [String] output of `show version`
91
+ # @return [String] summary lines, ending with a blank line
92
+ #: (String output) -> String
93
+ def version_summary(output)
94
+ lines = output.lines
95
+ facts = lines.first(1)
96
+ lines.each_with_index do |line, index|
97
+ facts.concat(version_facts(line, lines[index + 1], lines[index + 2]))
98
+ end
99
+ facts << "\n"
100
+ facts.join("\n")
101
+ end
102
+
103
+ # Cleans `show running-config` of lines that change without configuration changes.
104
+ #
105
+ # @param output [String] output of `show running-config`
106
+ # @return [String] the configuration
107
+ #: (String output) -> String
108
+ def running_config(output)
109
+ config = output
110
+ .sub(/\A\s*Building configuration\.\.\.\n\s*\n?/, '')
111
+ .sub(/^Current configuration : [^\n]*\n/, '')
112
+ config = config.each_line.reject do |line|
113
+ line.start_with?('ntp clock-period ') ||
114
+ (line.match?(/^! (Last|No) configuration change (at|since).*/) && !line.match?(/\d+\sby\s\S+$/))
115
+ end.join
116
+ config
117
+ .gsub(/^ tunnel mpls traffic-eng bandwidth[^\n]*\n*((?: [^\n]*\n*)*tunnel mpls traffic-eng auto-bw)/m, '\1')
118
+ .gsub(/^(\s+expression) \d+$/, '\1 <value removed>')
119
+ end
120
+
121
+ # @param line [String] a line of `show version`
122
+ # @param next_line [String, nil] the line after it
123
+ # @param line_after [String, nil] the line after that
124
+ # @return [Array<String>] facts found on the line
125
+ #: (String line, String? next_line, String? line_after) -> Array[String]
126
+ private def version_facts(line, next_line, line_after)
127
+ slave = (slot = line[/^Slave in slot (\d+) is running/, 1]) ? ' Slave:' : ''
128
+ slave_slot = slot ? ", slot #{slot}" : ''
129
+ facts = [] #: Array[String]
130
+ if (m = line.match(/^Compiled (.*)$/))
131
+ facts << "Image:#{slave} Compiled: #{m[1]}"
132
+ end
133
+ if (m = line.match(/^(?:Cisco )?IOS .* Software,? \(([A-Za-z0-9_-]*)\), .*Version\s+(.*)$/))
134
+ facts << "Image:#{slave} Software: #{m[1]}, #{m[2]}"
135
+ end
136
+ if (m = line.match(/^ROM: (IOS \S+ )?(System )?Bootstrap.*(Version.*)$/))
137
+ facts << "ROM Bootstrap: #{m[3]}"
138
+ end
139
+ if (m = line.match(/^BOOTFLASH: .*(Version.*)$/))
140
+ facts << "BOOTFLASH: #{m[1]}"
141
+ end
142
+ if (m = line.match(/^(\d+[kK]) bytes of (non-volatile|NVRAM)/))
143
+ facts << "Memory: nvram #{m[1]}"
144
+ end
145
+ if (m = line.match(/^(\d+[kK]) bytes of (flash memory|flash internal|processor board System flash|ATA CompactFlash)/i))
146
+ facts << "Memory: flash #{m[1]}"
147
+ end
148
+ if (m = line.match(/^(\d+[kK]) bytes of (Flash|ATA)?.*PCMCIA .*(slot|disk) ?(\d)/i))
149
+ facts << "Memory: pcmcia #{m[2]} #{m[3]}#{m[4]} #{m[1]}"
150
+ end
151
+ if (m = line.match(/(\S+(?:\sseries)?)\s+(?:\(([\S ]+)\)\s+processor|\(revision[^)]+\)).*\s+with (\S+k) bytes/i))
152
+ cpu = m[2]
153
+ facts << "Chassis type:#{slave} #{m[1]}"
154
+ facts << "Memory:#{slave} main #{m[3]}"
155
+ if (id = next_line&.match(/processor board id (\S+)/i))
156
+ facts << "Processor ID: #{id[1]}"
157
+ end
158
+ cpu_extra = if line_after&.match?(/(cpu at |processor: |#{cpu} processor,)/i)
159
+ line_after.gsub('implementation', 'impl').gsub(/^/, ', ').chomp
160
+ else
161
+ ''
162
+ end
163
+ facts << "CPU:#{slave} #{cpu}#{cpu_extra}#{slave_slot}"
164
+ end
165
+ if (m = line.match(/^System image file is "([^"]*)"$/))
166
+ facts << "Image: #{m[1]}"
167
+ end
168
+ facts
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,78 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ # Derived from Oxidized (https://github.com/ytti/oxidized) lib/oxidized/model/junos.rb,
7
+ # Copyright 2013-2015 Saku Ytti, Samer Abdel-Hafez and Oxidized contributors,
8
+ # licensed under the Apache License, Version 2.0. Modified for the glazed dialect API.
9
+
10
+ require 'glazed/dialects/base'
11
+
12
+ module Glazed
13
+ module Dialects
14
+ # Juniper Junos OS.
15
+ #
16
+ # Node params: "junos_license" set to "false" skips the license commands,
17
+ # whose usage counts change without configuration changes.
18
+ class Junos < Base
19
+ prompt(/^(\r?[\w.@:\/-]+[#>]\s?)$/)
20
+ comment_prefix '# '
21
+
22
+ filter_output { |output| output.gsub(/[ \t\r]+$/, '') }
23
+ # Counters that change without configuration changes.
24
+ filter_output do |output|
25
+ output
26
+ .gsub(/ scale-subscriber (\s+)(\d+)/, ' scale-subscriber <count>')
27
+ .gsub(/VMX-BANDWIDTH\s+(\d+) (.*)/, 'VMX-BANDWIDTH <count> \2')
28
+ end
29
+
30
+ sensitive_pattern(/ community (.+) \{$/) { it[1] }
31
+ sensitive_pattern(/"(.+)".*; ## SECRET-DATA$/) { it[1] }
32
+
33
+ runbook :get do |book|
34
+ book.raw('show version') do |run|
35
+ output = run.command('show version').output
36
+ run.vars[:model] = output[/^Model: (\S+)/, 1]
37
+ run.emit(comment(output))
38
+ end
39
+ book.command('show chassis hardware') { comment(it.output) }
40
+ book.raw('licenses') do |run|
41
+ next if run.param('junos_license') == 'false'
42
+
43
+ run.emit(
44
+ comment(
45
+ run.command('show system license').output
46
+ .gsub(/ fib-scale\s+(\d+)/, ' fib-scale <count>')
47
+ .gsub(/ rib-scale\s+(\d+)/, ' rib-scale <count>'),
48
+ ),
49
+ )
50
+ run.emit(comment(run.command('show system license keys').output))
51
+ end
52
+ book.command('show configuration | display omit')
53
+ book.raw('model-specific status') do |run|
54
+ case run.vars[:model]
55
+ when 'mx960'
56
+ run.emit(comment(run.command('show chassis fabric reachability').output))
57
+ when /^(ex22|ex3[34]|ex4|ex8|qfx)/
58
+ run.emit(comment(run.command('show virtual-chassis').output))
59
+ end
60
+ end
61
+ end
62
+
63
+ communication :telnet do |config|
64
+ config.username_prompt = /^login:/i
65
+ config.password_prompt = /^password:/i
66
+ config.post_login do |run|
67
+ run.cli.command('set cli screen-length 0')
68
+ run.cli.command('set cli screen-width 0')
69
+ end
70
+ config.pre_logout { |run| run.cli.write("exit\n") }
71
+ end
72
+
73
+ communication :ssh do |config|
74
+ config.exec = true
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ # Derived from Oxidized (https://github.com/ytti/oxidized) lib/oxidized/model/necix.rb,
7
+ # Copyright 2013-2015 Saku Ytti, Samer Abdel-Hafez and Oxidized contributors,
8
+ # licensed under the Apache License, Version 2.0. Modified for the glazed dialect API.
9
+
10
+ require 'glazed/dialects/base'
11
+
12
+ module Glazed
13
+ module Dialects
14
+ # NEC UNIVERGE IX series.
15
+ class NecIx < Base
16
+ prompt(/^(\([\w.-]*\)\s[#$]|^\S+[$#]\s?)$/)
17
+ comment_prefix '! '
18
+
19
+ interact(/^--More--$/) do |cli, _buffer, match|
20
+ cli.write(' ')
21
+ Actions::Replace.new(content: match.pre_match + match.post_match)
22
+ end
23
+
24
+ sensitive_pattern(/^username \S+ password \S+ (\S+)/) { it[1] }
25
+ sensitive_pattern(/^snmp-agent \S+ community (\S+).*/) { it[1] }
26
+ sensitive_pattern(/^ikev2 authentication psk id .+ key \S+ (\S+).*/) { it[1] }
27
+ sensitive_pattern(/^ike policy ike-policy .+ key \S+ (\S+).*/) { it[1] }
28
+ sensitive_pattern(/^radius host .+ key \S+ (\S+).*/) { it[1] }
29
+ sensitive_pattern(/^\s*authentication (?:secret-)?password \S+ (\S+)/) { it[1] }
30
+ sensitive_pattern(/^\s*ngn radius-auth password \S+ (\S+)/) { it[1] }
31
+ sensitive_pattern(/^\s*usbmem authentication .+ password-file \S+ \S+ (\S+)/) { it[1] }
32
+ sensitive_pattern(/^web-auth username \S+ (?:secret-)?password (\S+)/) { it[1] }
33
+ sensitive_pattern(/^\s*area \S+ virtual-link .* authentication-key (\S+)/) { it[1] }
34
+ sensitive_pattern(/^\s*ip ospf authentication-key (\S+)/) { it[1] }
35
+ sensitive_pattern(/^\s*ip ospf message-digest-key (\S+)/) { it[1] }
36
+ sensitive_pattern(/^\s*neighbor \S+ password (\S+)/) { it[1] }
37
+ sensitive_pattern(/^\s*password (?:plain |secret )?(\S+)/) { it[1] }
38
+ sensitive_pattern(/^\s*l2tp password \S+ (\S+)/) { it[1] }
39
+ sensitive_pattern(/^\s*vrrp \S+ authentication (\S+)/) { it[1] }
40
+ sensitive_pattern(/^\s*nm account \S+ password \S+ (\S+)/) { it[1] }
41
+ sensitive_pattern(/^snmpv3 user \S+ .+ priv \S+ (?:plain|secret) (\S+)/) { it[1] }
42
+ sensitive_pattern(/^snmpv3 user \S+ .+? (?:plain|secret) (\S+)/) { it[1] }
43
+ sensitive_pattern(/^http-server cross-site (?:secret-)?password (\S+)/) { it[1] }
44
+ sensitive_pattern(/^http-server (?:guest-|monitor-)?username \S+ (?:secret-)?password (\S+)/) { it[1] }
45
+ sensitive_pattern(/^startup (?:config-download|software-update) .+ (?:secret-)?password (\S+)/) { it[1] }
46
+
47
+ runbook :get do |book|
48
+ book.command('show running-config') do |result|
49
+ result.output.each_line.grep_v(/^!.*Current time.*$/).join
50
+ end
51
+ end
52
+
53
+ communication :telnet do |config|
54
+ config.username_prompt = /^Username:\s/i
55
+ config.password_prompt = /^Password:\s/i
56
+ end
57
+
58
+ communication :ssh, :telnet do |config|
59
+ config.post_login do |run|
60
+ raise Glazed::Error, 'CONFIG process is occupied' if run.cli.command('configure').output.match?(/CONFIG process is occupied/)
61
+
62
+ run.cli.command('terminal length 0')
63
+ end
64
+ config.pre_logout do |run|
65
+ run.cli.write("\cZ")
66
+ run.cli.write("exit\n")
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ # Derived from Oxidized (https://github.com/ytti/oxidized) lib/oxidized/model/aireos.rb,
7
+ # Copyright 2013-2015 Saku Ytti, Samer Abdel-Hafez and Oxidized contributors,
8
+ # licensed under the Apache License, Version 2.0. Modified for the glazed dialect API.
9
+
10
+ require 'glazed/dialects/base'
11
+
12
+ module Glazed
13
+ module Dialects
14
+ # Cisco Wireless LAN Controller on AireOS. Ported from oxidized's aireos model.
15
+ #
16
+ # AireOS asks for the user name and password inside the session, after SSH
17
+ # authentication as well.
18
+ class Wlc < Base
19
+ prompt(/^\([^)]+\)\s>/)
20
+ comment_prefix '# '
21
+
22
+ sensitive_pattern(/^config snmp community create (\S+)/) { it[1] }
23
+ sensitive_pattern(/^config wlan security wpa akm psk set-key (?:ascii|hex) (\S+)/) { it[1] }
24
+ sensitive_pattern(/^config (?:radius|tacacs) (?:auth|acct|athr) add \d+ \S+ \d+ (?:ascii|hex) (\S+)/) { it[1] }
25
+
26
+ runbook :get do |book|
27
+ book.command('show udi') { comment("#{clean(it.output)}\n") }
28
+ book.command('show boot') { comment("#{clean(it.output)}\n") }
29
+ book.command('show run-config commands') { clean(it.output) }
30
+ end
31
+
32
+ communication :ssh, :telnet do |config|
33
+ config.username_prompt = /^User:\s*/
34
+ config.password_prompt = /^Password:\s*/
35
+ config.post_login { |run| run.cli.command('config paging disable') }
36
+ # Logout asks whether to save unsaved changes.
37
+ config.pre_logout do |run|
38
+ run.cli.write("logout\n")
39
+ run.cli.write('n')
40
+ end
41
+ end
42
+
43
+ # Drops blank lines, rogue alerts and surrounding spaces.
44
+ #
45
+ # @param output [String] command output
46
+ # @return [String] cleaned lines, ending with a newline
47
+ #: (String output) -> String
48
+ def self.clean(output)
49
+ lines = output.each_line.map(&:strip).reject do |line|
50
+ line.empty? || line.match?(/rogue (adhoc|client) (alert|Unknown) [\da-f]{2}:/)
51
+ end
52
+ "#{lines.join("\n")}\n"
53
+ end
54
+ end
55
+ end
56
+ end