glazed 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/NOTICE +2 -2
- data/lib/glazed/cli_session.rb +15 -10
- data/lib/glazed/collector.rb +11 -70
- data/lib/glazed/command/base.rb +12 -2
- data/lib/glazed/command/orchestrate.rb +3 -3
- data/lib/glazed/command/run.rb +3 -4
- data/lib/glazed/coordinator.rb +9 -4
- data/lib/glazed/dialects/{wlc.rb → aireos.rb} +22 -6
- data/lib/glazed/dialects/aruba_instant.rb +90 -0
- data/lib/glazed/dialects/eos.rb +66 -0
- data/lib/glazed/dialects/nec_ix.rb +2 -1
- data/lib/glazed/dialects/routeros.rb +92 -0
- data/lib/glazed/node_session.rb +152 -0
- data/lib/glazed/orchestrator/policy.rb +8 -4
- data/lib/glazed/steps/publish.rb +3 -15
- data/lib/glazed/storages/base.rb +7 -6
- data/lib/glazed/storages/git.rb +8 -7
- data/lib/glazed/storages/github.rb +20 -17
- data/lib/glazed/transports/base.rb +1 -1
- data/lib/glazed/transports/ssh.rb +13 -6
- data/lib/glazed/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e764c4f949d82239b29b22b0d38b300f260e31818bf41adc5fb47ad6c091468a
|
|
4
|
+
data.tar.gz: 5788a04e0b8da61d1c46e436520ad9e3f78641310b4e0f66c210c524d0041e64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f456a6b2116655636c1c19e3c8253131aecc266e2d72201ecc6d531faa7ee2e8d64d4ca92147bed91f2153a7e6810c5db8268ca31c257d2cbb08db1160731a02
|
|
7
|
+
data.tar.gz: 9d4d631c639884e667c90b718476d1c99a2d37c49085bdf9c48eb7c9a4f2d96626b2681cd5cd12a62abc20e1169b5371ce313d29d6ddffcfea430e8f65cd2776
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.3.0] - 2026-10-17
|
|
2
|
+
|
|
3
|
+
### Breaking changes
|
|
4
|
+
|
|
5
|
+
- The `wlc` dialect is renamed `aireos` (`Glazed::Dialects::Aireos`), as Catalyst 9800 controllers run IOS XE
|
|
6
|
+
|
|
7
|
+
### Enhancements
|
|
8
|
+
|
|
9
|
+
- `aruba_instant` dialect for Aruba Instant (IAP and virtual controller), ported from oxidized's arubainstant model, with secret redaction for management users, SNMP, RADIUS/TACACS keys and passphrases
|
|
10
|
+
- `eos` dialect for Arista EOS, ported from oxidized, redacting hashed and typed secrets, SNMP, AAA, routing, key chain and TerminAttr keys
|
|
11
|
+
- `routeros` dialect for MikroTik RouterOS (SSH exec channel or Telnet), ported from Oxidized, redacting passwords, pre-shared keys, private keys, RADIUS and IPsec secrets, and SNMP community names. Param `routeros_hide_sensitive` runs `/export hide-sensitive`
|
|
12
|
+
- `Glazed::Coordinator` takes `waiter_alive:`, telling whether the execution holding the waiter record still runs, so `arm` takes over from one that ended without giving its record up
|
|
13
|
+
- `aireos` redacts the encrypted secrets AireOS prints in its configuration, along with mgmtuser, netuser, AP mgmtuser and SNMP passwords, and SNMP community names wherever they appear
|
|
14
|
+
- `Glazed::NodeSession` connects to a node, logs in and runs the dialect's post-login and pre-logout hooks, for callers that keep an interactive session open. `exec: false` keeps commands on the shell where the dialect would use exec channels, and `interactions:` adds responders after the dialect's own. `Collector` is built on it
|
|
15
|
+
|
|
1
16
|
## [0.2.0] - 2026-10-15
|
|
2
17
|
|
|
3
18
|
### Breaking changes
|
|
@@ -8,6 +23,8 @@
|
|
|
8
23
|
|
|
9
24
|
### Enhancements
|
|
10
25
|
|
|
26
|
+
- New `routeros` dialect for MikroTik RouterOS (SSH exec channel or Telnet), ported from Oxidized, redacting passwords, pre-shared keys, private keys, RADIUS and IPsec secrets, and SNMP community names. Param `routeros_hide_sensitive` runs `/export hide-sensitive`
|
|
27
|
+
|
|
11
28
|
- Redaction hides additional secrets given as secret references, as literal text or regular expressions and optionally split by a delimiter, in a `redact` step or in the top-level `redaction` shared by all of them
|
|
12
29
|
- Storages: `subdirectory` for `git` and `github` places files in a directory of the repository instead of its root
|
|
13
30
|
- `glazed signal --all`, and `all_nodes` and `workflows` on `glazed.v1.Signal`, signal every node of chosen workflows, for scheduled runs
|
|
@@ -23,6 +40,8 @@ Initial release.
|
|
|
23
40
|
|
|
24
41
|
### Enhancements
|
|
25
42
|
|
|
43
|
+
- New `routeros` dialect for MikroTik RouterOS (SSH exec channel or Telnet), ported from Oxidized, redacting passwords, pre-shared keys, private keys, RADIUS and IPsec secrets, and SNMP community names. Param `routeros_hide_sensitive` runs `/export hide-sensitive`
|
|
44
|
+
|
|
26
45
|
- Collect configuration over SSH or Telnet, with dialects for Cisco IOS/IOS XE, Cisco WLC (AireOS), Juniper Junos, NEC UNIVERGE IX and Allied Telesis AlliedWare Plus, ported from Oxidized
|
|
27
46
|
- Class-based dialect DSL with inheritance ([docs/dialect-api.md](../docs/dialect-api.md))
|
|
28
47
|
- Redaction of the secrets a dialect locates: replaced with `[~REDACTED~]`, or with `[~REDACTED:dgst=…~]` carrying a keyed HMAC so changed secrets still show in diffs
|
data/NOTICE
CHANGED
|
@@ -11,6 +11,6 @@ Version 2.0:
|
|
|
11
11
|
|
|
12
12
|
Derived from Oxidized as of commit d7f51c2, and modified for glazed:
|
|
13
13
|
|
|
14
|
-
- lib/glazed/dialects/{awplus,ios,junos,nec_ix,
|
|
14
|
+
- lib/glazed/dialects/{aireos,aruba_instant,awplus,eos,ios,junos,nec_ix,routeros}.rb
|
|
15
15
|
Prompts, command sequences, output cleanup and secret patterns ported
|
|
16
|
-
from lib/oxidized/model/{awplus,ios,junos,necix,aireos}.rb.
|
|
16
|
+
from lib/oxidized/model/{awplus,ios,junos,necix,aireos,eos,arubainstant,routeros}.rb.
|
data/lib/glazed/cli_session.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Glazed
|
|
|
22
22
|
# session = Glazed::CliSession.new(transport:, prompt: /^\S+#\s?$/)
|
|
23
23
|
# session.command('show version').output
|
|
24
24
|
class CliSession
|
|
25
|
-
# Default seconds
|
|
25
|
+
# Default seconds the device may stay silent while expected output is awaited.
|
|
26
26
|
DEFAULT_TIMEOUT = 60 #: Integer
|
|
27
27
|
# Longest single wait on the transport, so a deadline is checked regularly.
|
|
28
28
|
READ_SLICE = 1.0 #: Float
|
|
@@ -32,7 +32,7 @@ module Glazed
|
|
|
32
32
|
# @param data_filters [Array<Proc>] filters applied to each received fragment, in order
|
|
33
33
|
# @param interactions [Array<Dialect::Interaction>] responders to patterns in the buffer
|
|
34
34
|
# @param exec [Boolean] run {#command} through {Transports::Base#exec} when supported
|
|
35
|
-
# @param timeout [Numeric] default seconds
|
|
35
|
+
# @param timeout [Numeric] default seconds the device may stay silent while output is awaited
|
|
36
36
|
# @param progress [Proc] receives a message whenever the session starts waiting for the device
|
|
37
37
|
#: (transport: Transports::Base, prompt: Regexp, ?data_filters: Array[Dialect::data_filter], ?interactions: Array[Dialect::Interaction], ?exec: bool, ?timeout: Numeric, ?progress: ^(String) -> void) -> void
|
|
38
38
|
def initialize(transport:, prompt:, data_filters: [], interactions: [], exec: false, timeout: DEFAULT_TIMEOUT, progress: ->(_message) {})
|
|
@@ -72,9 +72,9 @@ module Glazed
|
|
|
72
72
|
# the end of the match.
|
|
73
73
|
#
|
|
74
74
|
# @param patterns [Array<Regexp>] patterns tried in order
|
|
75
|
-
# @param timeout [Numeric] seconds
|
|
75
|
+
# @param timeout [Numeric] seconds the device may stay silent
|
|
76
76
|
# @return [MatchData] the first match
|
|
77
|
-
# @raise [TimeoutError] when
|
|
77
|
+
# @raise [TimeoutError] when the device stays silent for timeout seconds before a match
|
|
78
78
|
# @raise [ConnectionClosed] when the device closes the connection first
|
|
79
79
|
#: (*Regexp patterns, ?timeout: Numeric) -> MatchData
|
|
80
80
|
def expect(*patterns, timeout: self.timeout)
|
|
@@ -87,9 +87,9 @@ module Glazed
|
|
|
87
87
|
# Runs a command and waits for the prompt.
|
|
88
88
|
#
|
|
89
89
|
# @param line [String] command line, without newline
|
|
90
|
-
# @param timeout [Numeric] seconds
|
|
90
|
+
# @param timeout [Numeric] seconds the device may stay silent before the prompt
|
|
91
91
|
# @return [CommandResult] output without the echoed command and the prompt
|
|
92
|
-
# @raise [TimeoutError] when the
|
|
92
|
+
# @raise [TimeoutError] when the device stays silent for timeout seconds before the prompt
|
|
93
93
|
# @raise [ConnectionClosed] when the device closes the connection first
|
|
94
94
|
#: (String line, ?timeout: Numeric) -> CommandResult
|
|
95
95
|
def command(line, timeout: self.timeout)
|
|
@@ -122,11 +122,14 @@ module Glazed
|
|
|
122
122
|
discarded
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# Waits for a match while the device keeps sending. The timeout restarts
|
|
126
|
+
# whenever data arrives, so slow but steady output does not time out.
|
|
127
|
+
#
|
|
125
128
|
# @param description [String] what is awaited, for error messages
|
|
126
|
-
# @param timeout [Numeric] seconds
|
|
129
|
+
# @param timeout [Numeric] seconds the device may stay silent
|
|
127
130
|
# @yieldreturn [MatchData, nil] match against the current buffer, or nil to keep waiting
|
|
128
131
|
# @return [MatchData] the first non-nil match
|
|
129
|
-
# @raise [TimeoutError] when
|
|
132
|
+
# @raise [TimeoutError] when the device stays silent for timeout seconds before a match
|
|
130
133
|
# @raise [ConnectionClosed] when the device closes the connection first
|
|
131
134
|
#: (String description, timeout: Numeric) { () -> MatchData? } -> MatchData
|
|
132
135
|
private def wait_until(description, timeout:, &matcher)
|
|
@@ -137,12 +140,14 @@ module Glazed
|
|
|
137
140
|
return match if match
|
|
138
141
|
|
|
139
142
|
remaining = deadline - now
|
|
140
|
-
raise TimeoutError, "
|
|
143
|
+
raise TimeoutError, "no output for #{timeout}s while waiting for #{description}; buffer tail: #{(buffer[-200..] || buffer).dump}" unless remaining.positive?
|
|
141
144
|
|
|
142
145
|
data = transport.read(timeout: [remaining, READ_SLICE].min)
|
|
143
146
|
raise ConnectionClosed, "connection closed while waiting for #{description}" if data.nil?
|
|
147
|
+
next if data.empty?
|
|
144
148
|
|
|
145
|
-
|
|
149
|
+
deadline = now + timeout.to_f
|
|
150
|
+
feed(data)
|
|
146
151
|
end
|
|
147
152
|
end
|
|
148
153
|
|
data/lib/glazed/collector.rb
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
require 'glazed'
|
|
7
7
|
require 'glazed/cli_session'
|
|
8
|
-
require 'glazed/
|
|
9
|
-
require 'glazed/dialects'
|
|
8
|
+
require 'glazed/node_session'
|
|
10
9
|
require 'glazed/proto'
|
|
11
10
|
require 'glazed/secrets_resolver'
|
|
12
11
|
require 'glazed/transports'
|
|
@@ -18,9 +17,6 @@ module Glazed
|
|
|
18
17
|
# @example
|
|
19
18
|
# config = Glazed::Collector.new(node:).call
|
|
20
19
|
class Collector
|
|
21
|
-
# Transport used when a node names none.
|
|
22
|
-
DEFAULT_TRANSPORT = 'ssh' #: String
|
|
23
|
-
|
|
24
20
|
# @param node [Proto::V1::Node] node to collect from
|
|
25
21
|
# @param transport [Transports::Base, nil] transport to use instead of building one from the node
|
|
26
22
|
# @param timeout [Numeric] seconds to wait for each expected output
|
|
@@ -28,22 +24,16 @@ module Glazed
|
|
|
28
24
|
# @param secrets [SecretsResolver] reads the node's password and secrets
|
|
29
25
|
#: (node: Proto::V1::Node, ?transport: Transports::Base?, ?timeout: Numeric, ?progress: ^(String) -> void, ?secrets: SecretsResolver) -> void
|
|
30
26
|
def initialize(node:, transport: nil, timeout: CliSession::DEFAULT_TIMEOUT, progress: ->(_message) {}, secrets: SecretsResolver.new)
|
|
31
|
-
@
|
|
32
|
-
@node = node #: Proto::V1::Node
|
|
33
|
-
@dialect = Dialects.find(node.dialect) #: singleton(Dialects::Base)
|
|
34
|
-
@kind = (node.transport.empty? ? DEFAULT_TRANSPORT : node.transport) #: String
|
|
35
|
-
@transport = transport || build_transport #: Transports::Base
|
|
36
|
-
@config = @dialect.communication_config(@kind.to_sym) #: Dialect::CommunicationConfig
|
|
37
|
-
@timeout = timeout #: Numeric
|
|
38
|
-
@progress = progress #: ^(String) -> void
|
|
27
|
+
@session = NodeSession.new(node:, transport:, timeout:, progress:, secrets:) #: NodeSession
|
|
39
28
|
end
|
|
40
29
|
|
|
41
30
|
# @return [Proto::V1::Node] node to collect from
|
|
42
|
-
#: Proto::V1::Node
|
|
43
|
-
|
|
31
|
+
#: () -> Proto::V1::Node
|
|
32
|
+
def node = @session.node
|
|
33
|
+
|
|
44
34
|
# @return [Class<Dialects::Base>] dialect of the node
|
|
45
|
-
#: singleton(Dialects::Base)
|
|
46
|
-
|
|
35
|
+
#: () -> singleton(Dialects::Base)
|
|
36
|
+
def dialect = @session.dialect
|
|
47
37
|
|
|
48
38
|
# Collects the configuration. The connection is closed even on failure.
|
|
49
39
|
#
|
|
@@ -53,65 +43,16 @@ module Glazed
|
|
|
53
43
|
# @raise [InvalidEncoding] when the configuration is not valid UTF-8
|
|
54
44
|
#: () -> String
|
|
55
45
|
def call
|
|
56
|
-
session
|
|
57
|
-
transport: @transport,
|
|
58
|
-
prompt: dialect.prompt_pattern,
|
|
59
|
-
data_filters: dialect.data_filters,
|
|
60
|
-
interactions: dialect.interactions,
|
|
61
|
-
exec: @config.exec,
|
|
62
|
-
timeout: @timeout,
|
|
63
|
-
progress: @progress,
|
|
64
|
-
)
|
|
65
|
-
run = Dialect::Run.new(cli: session, node:, dialect:, secrets: @secrets)
|
|
66
|
-
@transport.pty_options = @config.pty_options
|
|
67
|
-
@progress.call("connecting to #{@transport.host} over #{@kind}")
|
|
68
|
-
@transport.open
|
|
46
|
+
@session.open
|
|
69
47
|
begin
|
|
70
|
-
|
|
71
|
-
@config.post_login_hooks.each { it.call(run) }
|
|
72
|
-
output = dialect.runbook_for(:get).execute(run).dup.force_encoding(Encoding::UTF_8)
|
|
48
|
+
output = dialect.runbook_for(:get).execute(@session.run).dup.force_encoding(Encoding::UTF_8)
|
|
73
49
|
raise InvalidEncoding, "#{node.name}: collected configuration is not valid UTF-8" unless output.valid_encoding?
|
|
74
50
|
|
|
75
|
-
@
|
|
51
|
+
@session.logout
|
|
76
52
|
output
|
|
77
53
|
ensure
|
|
78
|
-
@
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# @return [Boolean] whether every command runs on exec channels, so the shell is never used
|
|
83
|
-
#: () -> bool
|
|
84
|
-
private def exec_only? = @config.exec && @transport.exec?
|
|
85
|
-
|
|
86
|
-
# Answers login prompts the dialect declares, then waits for the first prompt.
|
|
87
|
-
#
|
|
88
|
-
# @param session [CliSession] session to log in through
|
|
89
|
-
# @return [void]
|
|
90
|
-
#: (CliSession session) -> void
|
|
91
|
-
private def login(session)
|
|
92
|
-
if (username_prompt = @config.username_prompt)
|
|
93
|
-
session.expect(username_prompt)
|
|
94
|
-
session.write("#{node.username}\n")
|
|
95
|
-
end
|
|
96
|
-
if (password_prompt = @config.password_prompt)
|
|
97
|
-
session.expect(password_prompt)
|
|
98
|
-
session.write("#{@secrets.maybe(node.password)&.value}\n")
|
|
54
|
+
@session.close
|
|
99
55
|
end
|
|
100
|
-
session.expect(session.prompt)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# @return [Transports::Base] transport built from the node
|
|
104
|
-
# @raise [Finder::NotFound] when the transport is unknown
|
|
105
|
-
#: () -> Transports::Base
|
|
106
|
-
private def build_transport
|
|
107
|
-
Transports.find(@kind).new(
|
|
108
|
-
host: node.host.empty? ? node.name : node.host,
|
|
109
|
-
port: node.port.zero? ? nil : node.port,
|
|
110
|
-
username: node.username,
|
|
111
|
-
password: @secrets.maybe(node.password),
|
|
112
|
-
transport_options: node.transport_options,
|
|
113
|
-
secrets: @secrets,
|
|
114
|
-
)
|
|
115
56
|
end
|
|
116
57
|
end
|
|
117
58
|
end
|
data/lib/glazed/command/base.rb
CHANGED
|
@@ -85,10 +85,20 @@ module Glazed
|
|
|
85
85
|
StateBackends.build(Must.because(orchestration.state) { 'validation requires orchestrator.state' }, context:)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# dry-cli passes integer options as strings.
|
|
89
|
+
#
|
|
90
|
+
# @param option [String, nil] the --jobs option
|
|
88
91
|
# @param config [Proto::V1::Config] the config
|
|
89
92
|
# @return [Integer] nodes processed in parallel
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
# @raise [Glazed::Error] when --jobs is not a positive integer
|
|
94
|
+
#: (String? option, Proto::V1::Config config) -> Integer
|
|
95
|
+
private def resolve_jobs(option, config)
|
|
96
|
+
return config.jobs.zero? ? DEFAULT_JOBS : config.jobs unless option
|
|
97
|
+
|
|
98
|
+
raise Glazed::Error, "--jobs must be a positive integer, got #{option.inspect}" unless option.match?(/\A[1-9]\d*\z/)
|
|
99
|
+
|
|
100
|
+
option.to_i
|
|
101
|
+
end
|
|
92
102
|
|
|
93
103
|
# Reads the config with {Glazed::Config.load}.
|
|
94
104
|
#
|
|
@@ -26,12 +26,12 @@ module Glazed
|
|
|
26
26
|
# Runs the orchestrator until a signal stops it.
|
|
27
27
|
#
|
|
28
28
|
# @param config [String, nil] path of the config file; {Base::CONFIG_FILE_ENV} when nil
|
|
29
|
-
# @param jobs [
|
|
29
|
+
# @param jobs [String, nil] parallelism overriding the config
|
|
30
30
|
# @param _options [Hash] other options dry-cli passes, unused
|
|
31
31
|
# @return [void]
|
|
32
32
|
# @raise [Glazed::Error, Google::Protobuf::ParseError] when the config or inventory is invalid, or the state backend fails
|
|
33
33
|
# @raise [Interrupt] when a second TERM or INT interrupts a batch
|
|
34
|
-
#: (?config: String?, ?jobs:
|
|
34
|
+
#: (?config: String?, ?jobs: String?, **untyped _options) -> void
|
|
35
35
|
def call(config: nil, jobs: nil, **_options)
|
|
36
36
|
config = config_path(config, 'orchestrate')
|
|
37
37
|
|
|
@@ -43,7 +43,7 @@ module Glazed
|
|
|
43
43
|
backend = state_backend(orchestration, context)
|
|
44
44
|
orchestrator = Orchestrator.new(
|
|
45
45
|
settings: orchestration, workflows:, backend:, inventory: -> { Sources.nodes(settings.inventory.to_a) },
|
|
46
|
-
jobs: jobs
|
|
46
|
+
jobs: resolve_jobs(jobs, settings), listener:,
|
|
47
47
|
)
|
|
48
48
|
with_traps(orchestrator) { orchestrator.run }
|
|
49
49
|
ensure
|
data/lib/glazed/command/run.rb
CHANGED
|
@@ -27,11 +27,11 @@ module Glazed
|
|
|
27
27
|
# @param config [String, nil] path of the config file; {Base::CONFIG_FILE_ENV} when nil
|
|
28
28
|
# @param workflow [String, nil] workflow name; the only one when nil
|
|
29
29
|
# @param only [Array<String>, nil] node names to run for; all when nil
|
|
30
|
-
# @param jobs [
|
|
30
|
+
# @param jobs [String, nil] parallelism overriding the config
|
|
31
31
|
# @param _options [Hash] other options dry-cli passes, unused
|
|
32
32
|
# @return [void]
|
|
33
33
|
# @raise [Glazed::Error, Google::Protobuf::ParseError] when the config, inventory or a batch step fails
|
|
34
|
-
#: (?config: String?, ?workflow: String?, ?only: Array[String]?, ?jobs:
|
|
34
|
+
#: (?config: String?, ?workflow: String?, ?only: Array[String]?, ?jobs: String?, **untyped _options) -> void
|
|
35
35
|
def call(config: nil, workflow: nil, only: nil, jobs: nil, **_options)
|
|
36
36
|
config = config_path(config, 'run')
|
|
37
37
|
|
|
@@ -41,8 +41,7 @@ module Glazed
|
|
|
41
41
|
steps = Workflow.from_proto(select_workflow(settings, workflow), context:)
|
|
42
42
|
nodes = select_nodes(Sources.nodes(settings.inventory.to_a), only || [])
|
|
43
43
|
|
|
44
|
-
jobs
|
|
45
|
-
result = Runner.new(workflow: steps, jobs:, listener:).call(nodes)
|
|
44
|
+
result = Runner.new(workflow: steps, jobs: resolve_jobs(jobs, settings), listener:).call(nodes)
|
|
46
45
|
out.puts "completed #{result.states.size} of #{nodes.size} nodes, #{result.changed.size} changed, #{result.failures.size} failed"
|
|
47
46
|
exit 1 unless result.failures.empty?
|
|
48
47
|
end
|
data/lib/glazed/coordinator.rb
CHANGED
|
@@ -36,15 +36,17 @@ module Glazed
|
|
|
36
36
|
# @param clock [Proc] returns the current time
|
|
37
37
|
# @param random [Random] source of jitter
|
|
38
38
|
# @param owner [String] identifies this process in leases
|
|
39
|
-
|
|
39
|
+
# @param waiter_alive [Proc] tells whether the execution of a waiter's token still runs; see {#arm}
|
|
40
|
+
#: (settings: Proto::V1::Orchestrator, backend: StateBackends::Base, ?listener: Runner::listener, ?clock: ^() -> Time, ?random: Random, ?owner: String, ?waiter_alive: ^(String) -> bool) -> void
|
|
40
41
|
def initialize(settings:, backend:, listener: ->(_event) {}, clock: -> { Time.now }, random: Random.new,
|
|
41
|
-
owner: "#{Socket.gethostname}:#{Process.pid}:#{SecureRandom.hex(4)}")
|
|
42
|
+
owner: "#{Socket.gethostname}:#{Process.pid}:#{SecureRandom.hex(4)}", waiter_alive: ->(_token) { true })
|
|
42
43
|
@settings = settings #: Proto::V1::Orchestrator
|
|
43
44
|
@backend = backend #: StateBackends::Base
|
|
44
45
|
@listener = listener #: Runner::listener
|
|
45
46
|
@clock = clock #: ^() -> Time
|
|
46
47
|
@random = random #: Random
|
|
47
48
|
@owner = owner #: String
|
|
49
|
+
@waiter_alive = waiter_alive #: ^(String) -> bool
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# @return [Proto::V1::Orchestrator] orchestrator settings
|
|
@@ -202,7 +204,8 @@ module Glazed
|
|
|
202
204
|
|
|
203
205
|
# Decides whether a coordinator execution waits for the next due node,
|
|
204
206
|
# recording it as the waiter, or ends because another execution already
|
|
205
|
-
# waits until then.
|
|
207
|
+
# waits until then. A waiter whose execution waiter_alive reports ended
|
|
208
|
+
# is taken over before its record expires. See {Orchestrator::Policy.arm}.
|
|
206
209
|
#
|
|
207
210
|
# @param token [String] identifies the execution
|
|
208
211
|
# @param cycle [Integer] times the execution has run due nodes already
|
|
@@ -215,7 +218,9 @@ module Glazed
|
|
|
215
218
|
now = @clock.call
|
|
216
219
|
wake = nil #: Time?
|
|
217
220
|
@backend.update_wakeup do |record|
|
|
218
|
-
kept, wake = Orchestrator::Policy.arm(
|
|
221
|
+
kept, wake = Orchestrator::Policy.arm(
|
|
222
|
+
record, token:, next_due:, now:, gap: poll_seconds, grace: grace || longest_lease, cycle:, max_cycles:, alive: @waiter_alive,
|
|
223
|
+
)
|
|
219
224
|
kept
|
|
220
225
|
end
|
|
221
226
|
if wake
|
|
@@ -13,15 +13,31 @@ module Glazed
|
|
|
13
13
|
module Dialects
|
|
14
14
|
# Cisco Wireless LAN Controller on AireOS. Ported from oxidized's aireos model.
|
|
15
15
|
#
|
|
16
|
-
# AireOS asks for the user name and
|
|
17
|
-
#
|
|
18
|
-
class
|
|
16
|
+
# Depending on version and settings, AireOS asks for the user name and
|
|
17
|
+
# password again inside the session after SSH authentication.
|
|
18
|
+
class Aireos < Base
|
|
19
|
+
# Encrypted secret in the running configuration: key index, IV, HMAC, optional length and ciphertext.
|
|
20
|
+
ENCRYPTED = /1 \h{32} \h{40}(?: \d+)? \h+/ #: Regexp
|
|
21
|
+
|
|
19
22
|
prompt(/^\([^)]+\)\s>/)
|
|
20
23
|
comment_prefix '# '
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# Parts of `show run-config commands` page even after `config paging disable`.
|
|
26
|
+
interact(/--More-- or \(q\)uit/) do |cli, _buffer, match|
|
|
27
|
+
cli.write(' ')
|
|
28
|
+
Actions::Replace.new(content: match.pre_match + match.post_match)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sensitive_pattern(/(?<= )#{ENCRYPTED}/o) { it[0] }
|
|
32
|
+
# Some releases print run-config commands without the leading `config`.
|
|
33
|
+
sensitive_pattern(/^(?:config )?wlan security wpa akm \S+ set-key (?:ascii|hex) (?!encrypt )(\S+)/) { it[1] }
|
|
34
|
+
sensitive_pattern(/^(?:config )?(?:radius|tacacs) (?:auth|acct|athr) add \d+ \S+ \d+ (?:ascii|hex) (\S+)/) { it[1] }
|
|
35
|
+
sensitive_pattern(/^(?:config )?mgmtuser (?:add|password) (?!encrypt)\S+ (\S+)/) { it[1] }
|
|
36
|
+
sensitive_pattern(/^(?:config )?netuser (?:add|password) (?!encrypt)\S+ (\S+)/) { it[1] }
|
|
37
|
+
# Covers the password, its crypt hash and the enable secret.
|
|
38
|
+
sensitive_pattern(/^(?:config )?ap mgmtuser add (?:encrypt )?username \S+ password (.+) \S+$/) { it[1] }
|
|
39
|
+
sensitive_pattern(/^(?:config )?snmp community (?:create|mode \S+|accessmode \S+|ipaddr \S+ \S+|ipv6addr \S+ \S+) (\S+)/) { it[1] }
|
|
40
|
+
sensitive_pattern(/^(?:config )?snmp v3user create \S+ (?:ro|rw) \S+ \S+ (\S.*)$/) { it[1] }
|
|
25
41
|
|
|
26
42
|
runbook :get do |book|
|
|
27
43
|
book.command('show udi') { comment("#{clean(it.output)}\n") }
|
|
@@ -0,0 +1,90 @@
|
|
|
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/arubainstant.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
|
+
# Aruba Instant (IAP and Instant virtual controller). Ported from oxidized's arubainstant model.
|
|
15
|
+
#
|
|
16
|
+
# Node secrets: "enable" is sent as the enable password; without it, param
|
|
17
|
+
# "enable" set to "true" runs `enable` without one.
|
|
18
|
+
class ArubaInstant < Base
|
|
19
|
+
# Access levels of `mgmt-user` on Instant virtual controllers and Mobility Access Switches.
|
|
20
|
+
MGMT_ROLES = 'root|guest-provisioning|network-operations|read-only|location-api-mgmt' #: String
|
|
21
|
+
|
|
22
|
+
# The prompt gains a leading space while a core file is present.
|
|
23
|
+
prompt(/^ ?[\w:.@-]+[#>] $/)
|
|
24
|
+
comment_prefix '# '
|
|
25
|
+
|
|
26
|
+
sensitive_pattern(/ipsec (\S+)$/) { it[1] }
|
|
27
|
+
sensitive_pattern(/community (\S+)$/) { it[1] }
|
|
28
|
+
sensitive_pattern(/^\s*snmp-server host [\d.]+ version 2c (\S+)/) { it[1] }
|
|
29
|
+
sensitive_pattern(/^\s*snmpv3-user .*?auth-pass (\S+)/) { it[1] }
|
|
30
|
+
sensitive_pattern(/^\s*snmpv3-user .*?priv-pass (\S+)/) { it[1] }
|
|
31
|
+
sensitive_pattern(/^\s*mgmt-user \S+ (?:#{MGMT_ROLES}) (\S+)$/) { it[1] }
|
|
32
|
+
sensitive_pattern(/^\s*mgmt-user \S+ (?!(?:#{MGMT_ROLES}) )(\S+)(?: (?:read-only|guest-mgmt))?$/) { it[1] }
|
|
33
|
+
sensitive_pattern(/key (\S+)$/) { it[1] }
|
|
34
|
+
sensitive_pattern(/^\s*(?:\S+-)?(?:passwd|passphrase|psk|secret|password|passwords) (\S+)$/) { it[1] }
|
|
35
|
+
sensitive_pattern(/^\s*user \S+ (\S+) \S+$/) { it[1] }
|
|
36
|
+
sensitive_pattern(/^\s*hash-mgmt-user \S+ password \S+ (\S+)(?: usertype .*)?$/) { it[1] }
|
|
37
|
+
sensitive_pattern(/^\s*hash-mgmt-user \S+ password (\S+)(?: usertype .*)?$/) { it[1] }
|
|
38
|
+
|
|
39
|
+
runbook :get do |book|
|
|
40
|
+
book.command('show version') do |result|
|
|
41
|
+
comment(result.output.each_line.grep_v(/^(?:Switch|AP) uptime is |^Reboot Time and Cause/).join)
|
|
42
|
+
end
|
|
43
|
+
book.command('show activate status') do |result|
|
|
44
|
+
comment("#{result.output.each_line.grep_v(/^(?:Activate |Provision interval|Cloud Activation Key)/).join}\n")
|
|
45
|
+
end
|
|
46
|
+
book.command('show aps') do |result|
|
|
47
|
+
comment("#{result.output.each_line.map { aps_line(it) }.join}\n")
|
|
48
|
+
end
|
|
49
|
+
book.command('show running-config no-encrypt')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
communication :telnet do |config|
|
|
53
|
+
config.username_prompt = /^User:\s*/
|
|
54
|
+
config.password_prompt = /^Password:\s*/
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
communication :ssh, :telnet do |config|
|
|
58
|
+
config.post_login do |run|
|
|
59
|
+
if (enable = run.secret('enable'))
|
|
60
|
+
run.cli.write("enable\n")
|
|
61
|
+
run.cli.expect(/^[pP]assword:/)
|
|
62
|
+
run.cli.write("#{enable.value}\n")
|
|
63
|
+
run.cli.expect(run.cli.prompt)
|
|
64
|
+
elsif run.param('enable') == 'true'
|
|
65
|
+
run.cli.command('enable')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
config.pre_logout do |run|
|
|
69
|
+
run.cli.write("exit\n") if run.secret('enable') || run.param('enable') == 'true'
|
|
70
|
+
run.cli.write("exit\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# Drops the per-radio columns of `show aps`, which change on their own.
|
|
76
|
+
#
|
|
77
|
+
# @param line [String] a line of `show aps`
|
|
78
|
+
# @return [String] the line with the volatile columns removed
|
|
79
|
+
#: (String line) -> String
|
|
80
|
+
def aps_line(line)
|
|
81
|
+
if line.match?(/^Name/)
|
|
82
|
+
line.sub(/^(Name +IP Address +).*(Type +IPv6 Address +).*(Serial #).*$/, '\1\2\3')
|
|
83
|
+
else
|
|
84
|
+
line.sub(/^(\S+ +\S+ +)(?:\S+ +){3}(\S+ +\S+ +)(?:\S+ +){2}(\S+) +.*$/, '\1\2\3')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
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/eos.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
|
+
# Arista EOS. Ported from oxidized's eos model.
|
|
15
|
+
#
|
|
16
|
+
# Node secrets: "enable" is sent as the enable password; without it, param
|
|
17
|
+
# "enable" set to "true" runs `enable` without one.
|
|
18
|
+
class Eos < Base
|
|
19
|
+
# Optional encoding prefix of a secret (0 plaintext, 5 and sha512 hashes, 7 and 8a reversible).
|
|
20
|
+
SECRET_TYPE = '(?:(?:0|5|7|8a|sha512) )?'
|
|
21
|
+
|
|
22
|
+
prompt(/^.+[#>]$/)
|
|
23
|
+
comment_prefix '! '
|
|
24
|
+
|
|
25
|
+
sensitive_pattern(/^snmp-server community (?:(?:0|7|8a) (?=\S+ (?:ro|rw)\b))?(\S+)/) { it[1] }
|
|
26
|
+
# SNMPv3 hosts carry a user name, not a community.
|
|
27
|
+
sensitive_pattern(/^snmp-server host (?!.* version 3 )\S+(?> vrf \S+)?(?> informs?| traps)?(?> version (?:1|2c))? +(\S+)/) { it[1] }
|
|
28
|
+
sensitive_pattern(/^snmp-server user .*? auth (?:md5|sha\d*) (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
29
|
+
sensitive_pattern(/^snmp-server user .*? priv \w+ (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
30
|
+
sensitive_pattern(/^enable (?:secret|password)(?: level \d+)? #{SECRET_TYPE}(\S+)/) { it[1] }
|
|
31
|
+
sensitive_pattern(/^username \S+ .*?(?:secret|password) #{SECRET_TYPE}(\S+)/) { it[1] }
|
|
32
|
+
sensitive_pattern(/^\s+(?:secret|password) #{SECRET_TYPE}(\S+)/) { it[1] }
|
|
33
|
+
sensitive_pattern(/^(?:tacacs|radius)-server (?:.+ )?key (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
34
|
+
sensitive_pattern(/^\s+server \S+(?: .+)? key (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
35
|
+
sensitive_pattern(/^\s*key (?:\h+ )?(?:0|7|8a) (\S+)/) { it[1] }
|
|
36
|
+
sensitive_pattern(/^\s*(?:encryption-key|key-string|pre-shared-key) (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
37
|
+
sensitive_pattern(/^\s*(?:isis |ip ospf )?authentication(?:-key| key) (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
38
|
+
sensitive_pattern(/^\s*ip ospf message-digest-key \d+ \S+ (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
39
|
+
sensitive_pattern(/^\s*neighbor \S+ password (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
40
|
+
sensitive_pattern(/^\s*vrrp \d+ authentication text (\S+)/) { it[1] }
|
|
41
|
+
sensitive_pattern(/^ntp authentication-key \d+ \S+ (?:(?:0|7|8a) )?(\S+)/) { it[1] }
|
|
42
|
+
sensitive_pattern(/^service unsupported-transceiver \S+ (\S+)/) { it[1] }
|
|
43
|
+
sensitive_pattern(/-(?:cv|ingest)auth=key,(\S+)/) { it[1] }
|
|
44
|
+
|
|
45
|
+
runbook :get do |book|
|
|
46
|
+
book.command('show inventory | no-more') { comment(it.output) }
|
|
47
|
+
book.command('show running-config | no-more | exclude ! Time:')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
communication :ssh, :telnet do |config|
|
|
51
|
+
config.post_login do |run|
|
|
52
|
+
if (enable = run.secret('enable'))
|
|
53
|
+
run.cli.write("enable\n")
|
|
54
|
+
run.cli.expect(/[pP]assword:\s?$/)
|
|
55
|
+
run.cli.write("#{enable.value}\n")
|
|
56
|
+
run.cli.expect(run.cli.prompt)
|
|
57
|
+
elsif run.param('enable') == 'true'
|
|
58
|
+
run.cli.command('enable')
|
|
59
|
+
end
|
|
60
|
+
run.cli.command('terminal length 0')
|
|
61
|
+
end
|
|
62
|
+
config.pre_logout { |run| run.cli.write("exit\n") }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -26,7 +26,8 @@ module Glazed
|
|
|
26
26
|
sensitive_pattern(/^ikev2 authentication psk id .+ key \S+ (\S+).*/) { it[1] }
|
|
27
27
|
sensitive_pattern(/^ike policy ike-policy .+ key \S+ (\S+).*/) { it[1] }
|
|
28
28
|
sensitive_pattern(/^radius host .+ key \S+ (\S+).*/) { it[1] }
|
|
29
|
-
sensitive_pattern(/^\s*authentication
|
|
29
|
+
sensitive_pattern(/^\s*authentication password \S+ (\S+)/) { it[1] }
|
|
30
|
+
sensitive_pattern(/^\s*authentication secret-password \d+ (.+?)\s*$/) { it[1] }
|
|
30
31
|
sensitive_pattern(/^\s*ngn radius-auth password \S+ (\S+)/) { it[1] }
|
|
31
32
|
sensitive_pattern(/^\s*usbmem authentication .+ password-file \S+ \S+ (\S+)/) { it[1] }
|
|
32
33
|
sensitive_pattern(/^web-auth username \S+ (?:secret-)?password (\S+)/) { it[1] }
|
|
@@ -0,0 +1,92 @@
|
|
|
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/routeros.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
|
+
# MikroTik RouterOS. Ported from oxidized's routeros model.
|
|
15
|
+
#
|
|
16
|
+
# Node params: "routeros_hide_sensitive" set to "true" collects `/export hide-sensitive`.
|
|
17
|
+
# Otherwise RouterOS 7 and later run `/export show-sensitive`, and older versions `/export`.
|
|
18
|
+
class Routeros < Base
|
|
19
|
+
# A key=value value, either quoted or bare.
|
|
20
|
+
VALUE = /(?:"((?:[^"\\]|\\.)*)"|([^\s"]\S*))/ #: Regexp
|
|
21
|
+
|
|
22
|
+
prompt(/\[\w+@\S+(\s+\S+)*\]\s?>\s?$/)
|
|
23
|
+
comment_prefix '# '
|
|
24
|
+
|
|
25
|
+
# Cursor movements also appear, such as before the prompt.
|
|
26
|
+
filter_output { it.gsub(/\x1B\[[0-9;?]*[A-Za-z]/, '') }
|
|
27
|
+
filter_output { it.gsub(/^\r+(.+)/, '\1').gsub(/([^\r]*)\r+$/, '\1') }
|
|
28
|
+
|
|
29
|
+
sensitive_pattern(
|
|
30
|
+
/(?<![\w.-])[\w.-]*(?:password|passphrase|secret|pre-shared-key|preshared-key|private-key|psk|md5-key|auth-key|authentication-key|protection-key|static-key-\d|trap-community)=#{VALUE}/,
|
|
31
|
+
) { it[1] || it[2] }
|
|
32
|
+
# Community names follow `name=`, one section at a time. \G continues from the previous match.
|
|
33
|
+
sensitive_pattern(
|
|
34
|
+
/(?:^\/snmp community\n|\G(?<=\S)[^\n]*\n)(?:(?:add|set)[^\n]*\n)*?(?:add|set) [^\n]*?(?<=\s)name=#{VALUE}/,
|
|
35
|
+
) { it[1] || it[2] }
|
|
36
|
+
|
|
37
|
+
runbook :get do |book|
|
|
38
|
+
book.command('/system resource print') do |result|
|
|
39
|
+
comment(result.output.each_line.grep(/(version|factory-software|total-memory|cpu|cpu-count|total-hdd-space|architecture-name|board-name|platform):/).join)
|
|
40
|
+
end
|
|
41
|
+
book.raw('/system package update print') do |run|
|
|
42
|
+
line = run.command('/system package update print').output.each_line.grep(/installed-version:\s|current-version:\s/).first
|
|
43
|
+
next unless line
|
|
44
|
+
|
|
45
|
+
run.vars[:ros_version] = line[/:\s*(\d+)/, 1]&.to_i
|
|
46
|
+
run.emit(comment(line))
|
|
47
|
+
end
|
|
48
|
+
book.command('/system history print without-paging') { comment(it.output) }
|
|
49
|
+
book.raw('/export') do |run|
|
|
50
|
+
line = if run.param('routeros_hide_sensitive') == 'true'
|
|
51
|
+
'/export hide-sensitive'
|
|
52
|
+
elsif (version = run.vars[:ros_version]).is_a?(Integer) && version >= 7
|
|
53
|
+
'/export show-sensitive'
|
|
54
|
+
else
|
|
55
|
+
'/export'
|
|
56
|
+
end
|
|
57
|
+
run.emit(export(run.command(line).output))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
communication :telnet do |config|
|
|
62
|
+
config.username_prompt = /^Login:/
|
|
63
|
+
config.password_prompt = /^Password:/
|
|
64
|
+
config.pre_logout { |run| run.cli.write("quit\n") }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
communication :ssh do |config|
|
|
68
|
+
config.exec = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
# Drops the lines of `/export` that change without configuration changes
|
|
73
|
+
# and joins the lines RouterOS wraps with a trailing backslash.
|
|
74
|
+
#
|
|
75
|
+
# @param output [String] output of `/export`
|
|
76
|
+
# @return [String] the configuration, ending with a newline
|
|
77
|
+
#: (String output) -> String
|
|
78
|
+
def export(output)
|
|
79
|
+
config = output
|
|
80
|
+
.gsub(/\\\r?\n\s+/, '')
|
|
81
|
+
.gsub("# inactive time\n", '')
|
|
82
|
+
.gsub(/# received packet from \S+ bad format\n/, '')
|
|
83
|
+
.gsub("# poe-out status: short_circuit\n", '')
|
|
84
|
+
.gsub("# Firmware upgraded successfully, please reboot for changes to take effect!\n", '')
|
|
85
|
+
.gsub(/# \S+ not ready\n/, '')
|
|
86
|
+
lines = config.split("\n").reject { it.match?(%r{^#\s\w{3}/\d{2}/\d{4}.*$}) || it.match?(/^#\s\d{4}-\d{2}-\d{2}.*$/) }
|
|
87
|
+
"#{lines.join("\n")}\n"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed'
|
|
7
|
+
require 'glazed/cli_session'
|
|
8
|
+
require 'glazed/dialect/run'
|
|
9
|
+
require 'glazed/dialects'
|
|
10
|
+
require 'glazed/proto'
|
|
11
|
+
require 'glazed/secrets_resolver'
|
|
12
|
+
require 'glazed/transports'
|
|
13
|
+
|
|
14
|
+
module Glazed
|
|
15
|
+
# Logged-in session to one node: connects, answers the dialect's login
|
|
16
|
+
# prompts, runs its post-login hooks, and later its pre-logout hooks.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# session = Glazed::NodeSession.new(node:)
|
|
20
|
+
# session.open
|
|
21
|
+
# begin
|
|
22
|
+
# session.run.command('show version')
|
|
23
|
+
# session.logout
|
|
24
|
+
# ensure
|
|
25
|
+
# session.close
|
|
26
|
+
# end
|
|
27
|
+
class NodeSession
|
|
28
|
+
# Transport used when a node names none.
|
|
29
|
+
DEFAULT_TRANSPORT = 'ssh' #: String
|
|
30
|
+
|
|
31
|
+
# @param node [Proto::V1::Node] node to connect to
|
|
32
|
+
# @param transport [Transports::Base, nil] transport to use instead of building one from the node
|
|
33
|
+
# @param timeout [Numeric] seconds to wait for each expected output
|
|
34
|
+
# @param exec [Boolean, nil] run commands on exec channels when the transport supports it; the dialect decides when nil
|
|
35
|
+
# @param interactions [Array<Dialect::Interaction>] responders tried after the dialect's own
|
|
36
|
+
# @param progress [Proc] receives a message when connecting and whenever waiting for the device
|
|
37
|
+
# @param secrets [SecretsResolver] reads the node's password and secrets
|
|
38
|
+
#: (node: Proto::V1::Node, ?transport: Transports::Base?, ?timeout: Numeric, ?exec: bool?, ?interactions: Array[Dialect::Interaction], ?progress: ^(String) -> void, ?secrets: SecretsResolver) -> void
|
|
39
|
+
def initialize(node:, transport: nil, timeout: CliSession::DEFAULT_TIMEOUT, exec: nil, interactions: [], progress: ->(_message) {}, secrets: SecretsResolver.new)
|
|
40
|
+
@node = node #: Proto::V1::Node
|
|
41
|
+
@secrets = secrets #: SecretsResolver
|
|
42
|
+
@progress = progress #: ^(String) -> void
|
|
43
|
+
@dialect = Dialects.find(node.dialect) #: singleton(Dialects::Base)
|
|
44
|
+
@kind = (node.transport.empty? ? DEFAULT_TRANSPORT : node.transport) #: String
|
|
45
|
+
@transport = transport || build_transport #: Transports::Base
|
|
46
|
+
@config = @dialect.communication_config(@kind.to_sym) #: Dialect::CommunicationConfig
|
|
47
|
+
@exec = (exec.nil? ? @config.exec : exec) #: bool
|
|
48
|
+
@cli = CliSession.new(
|
|
49
|
+
transport: @transport,
|
|
50
|
+
prompt: @dialect.prompt_pattern,
|
|
51
|
+
data_filters: @dialect.data_filters,
|
|
52
|
+
interactions: @dialect.interactions + interactions,
|
|
53
|
+
exec: @exec,
|
|
54
|
+
timeout:,
|
|
55
|
+
progress:,
|
|
56
|
+
) #: CliSession
|
|
57
|
+
@run = Dialect::Run.new(cli: @cli, node:, dialect: @dialect, secrets:) #: Dialect::Run
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: Proto::V1::Node
|
|
61
|
+
attr_reader :node
|
|
62
|
+
#: singleton(Dialects::Base)
|
|
63
|
+
attr_reader :dialect
|
|
64
|
+
# @return [Transports::Base] underlying transport
|
|
65
|
+
#: Transports::Base
|
|
66
|
+
attr_reader :transport
|
|
67
|
+
# @return [CliSession] session to the node
|
|
68
|
+
#: CliSession
|
|
69
|
+
attr_reader :cli
|
|
70
|
+
# @return [Dialect::Run] run state passed to runbooks and hooks
|
|
71
|
+
#: Dialect::Run
|
|
72
|
+
attr_reader :run
|
|
73
|
+
|
|
74
|
+
# Connects, logs in and runs the post-login hooks. The connection is
|
|
75
|
+
# closed when any of them fails.
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
# @raise [TimeoutError] when the device does not answer as the dialect expects
|
|
79
|
+
# @raise [ConnectionClosed] when the device drops the connection
|
|
80
|
+
#: () -> void
|
|
81
|
+
def open
|
|
82
|
+
@transport.pty_options = @config.pty_options
|
|
83
|
+
@progress.call("connecting to #{@transport.host} over #{@kind}")
|
|
84
|
+
@transport.open
|
|
85
|
+
begin
|
|
86
|
+
login unless exec_only?
|
|
87
|
+
@config.post_login_hooks.each { it.call(run) }
|
|
88
|
+
rescue StandardError
|
|
89
|
+
close
|
|
90
|
+
raise
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Runs the pre-logout hooks.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
#: () -> void
|
|
98
|
+
def logout
|
|
99
|
+
@config.pre_logout_hooks.each { it.call(run) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Closes the connection without logging out. Safe to call more than once.
|
|
103
|
+
#
|
|
104
|
+
# @return [void]
|
|
105
|
+
#: () -> void
|
|
106
|
+
def close
|
|
107
|
+
@transport.close
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Boolean] whether every command runs on exec channels, so the shell is never used
|
|
111
|
+
#: () -> bool
|
|
112
|
+
private def exec_only? = @exec && @transport.exec?
|
|
113
|
+
|
|
114
|
+
# Answers login prompts the dialect declares until the first prompt appears.
|
|
115
|
+
# Each login prompt is answered at most once, and none has to appear, as
|
|
116
|
+
# some devices ask for credentials only under certain settings.
|
|
117
|
+
#
|
|
118
|
+
# @return [void]
|
|
119
|
+
#: () -> void
|
|
120
|
+
private def login
|
|
121
|
+
answers = {} #: Hash[Regexp, ^() -> String?]
|
|
122
|
+
if (username_prompt = @config.username_prompt)
|
|
123
|
+
answers[username_prompt] = -> { node.username }
|
|
124
|
+
end
|
|
125
|
+
if (password_prompt = @config.password_prompt)
|
|
126
|
+
answers[password_prompt] = -> { @secrets.maybe(node.password)&.value }
|
|
127
|
+
end
|
|
128
|
+
# Sorbet cannot check a splat against expect's keyword options (7019).
|
|
129
|
+
cli = @cli #: as untyped
|
|
130
|
+
loop do
|
|
131
|
+
answer = answers.delete(cli.expect(*answers.keys, @cli.prompt).regexp)
|
|
132
|
+
return unless answer
|
|
133
|
+
|
|
134
|
+
@cli.write("#{answer.call}\n")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [Transports::Base] transport built from the node
|
|
139
|
+
# @raise [Finder::NotFound] when the transport is unknown
|
|
140
|
+
#: () -> Transports::Base
|
|
141
|
+
private def build_transport
|
|
142
|
+
Transports.find(@kind).new(
|
|
143
|
+
host: node.host.empty? ? node.name : node.host,
|
|
144
|
+
port: node.port.zero? ? nil : node.port,
|
|
145
|
+
username: node.username,
|
|
146
|
+
password: @secrets.maybe(node.password),
|
|
147
|
+
transport_options: node.transport_options,
|
|
148
|
+
secrets: @secrets,
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -149,7 +149,8 @@ module Glazed
|
|
|
149
149
|
# Decides whether a coordinator execution waits for the next due node.
|
|
150
150
|
# It does unless another live execution already waits until then, or
|
|
151
151
|
# it has looped max_cycles times; a waiter that stops waiting gives up
|
|
152
|
-
# its record.
|
|
152
|
+
# its record. A waiter that ended without giving it up, such as by
|
|
153
|
+
# failing, is live until its record expires unless alive tells otherwise.
|
|
153
154
|
#
|
|
154
155
|
# @param record [Proto::V1::Wakeup, nil] the current waiter
|
|
155
156
|
# @param token [String] identifies the deciding execution
|
|
@@ -159,17 +160,20 @@ module Glazed
|
|
|
159
160
|
# @param grace [Integer] seconds after waking until another execution may take over
|
|
160
161
|
# @param cycle [Integer] times the execution has run due nodes already
|
|
161
162
|
# @param max_cycles [Integer] cycles after which the execution ends
|
|
163
|
+
# @param alive [Proc] tells whether the execution of a waiter's token still runs
|
|
162
164
|
# @return [Array(Proto::V1::Wakeup, Time), Array(Proto::V1::Wakeup, nil), Array(nil, nil)] the waiter
|
|
163
165
|
# record to keep, and when to wake; nil when the execution ends
|
|
164
|
-
#: (Proto::V1::Wakeup? record, token: String, next_due: Time?, now: Time, gap: Integer, grace: Integer, cycle: Integer, max_cycles: Integer) -> [Proto::V1::Wakeup?, Time?]
|
|
165
|
-
def self.arm(record, token:, next_due:, now:, gap:, grace:, cycle:, max_cycles:)
|
|
166
|
+
#: (Proto::V1::Wakeup? record, token: String, next_due: Time?, now: Time, gap: Integer, grace: Integer, cycle: Integer, max_cycles: Integer, ?alive: ^(String) -> bool) -> [Proto::V1::Wakeup?, Time?]
|
|
167
|
+
def self.arm(record, token:, next_due:, now:, gap:, grace:, cycle:, max_cycles:, alive: ->(_token) { true })
|
|
166
168
|
own = record&.owner == token
|
|
167
169
|
return [own ? nil : record, nil] if next_due.nil? || cycle >= max_cycles
|
|
168
170
|
|
|
169
171
|
wake = [next_due, cycle.zero? ? now : now + gap].max
|
|
170
172
|
expires_at = time(record&.expires_at)
|
|
171
173
|
wake_at = time(record&.wake_at)
|
|
172
|
-
|
|
174
|
+
if record && !own && expires_at && expires_at > now && wake_at && wake_at <= wake && alive.call(record.owner)
|
|
175
|
+
return [record, nil]
|
|
176
|
+
end
|
|
173
177
|
|
|
174
178
|
[Proto::V1::Wakeup.new(owner: token, wake_at: timestamp(wake), expires_at: timestamp(wake + grace)), wake]
|
|
175
179
|
end
|
data/lib/glazed/steps/publish.rb
CHANGED
|
@@ -10,15 +10,12 @@ require 'glazed/storages'
|
|
|
10
10
|
|
|
11
11
|
module Glazed
|
|
12
12
|
module Steps
|
|
13
|
-
# Batch: writes an artifact of every node into a storage as one change
|
|
14
|
-
# such as
|
|
13
|
+
# Batch: writes an artifact of every node into a storage, as one change
|
|
14
|
+
# per changed node, such as a commit titled "glazed: rt1".
|
|
15
15
|
#
|
|
16
16
|
# @example Step definition
|
|
17
17
|
# {"name": "publish", "publish": {"input": "redacted", "storage": "team"}}
|
|
18
18
|
class Publish < BatchStep
|
|
19
|
-
# Node names listed in a commit message before it is shortened.
|
|
20
|
-
MESSAGE_NAMES = 5 #: Integer
|
|
21
|
-
|
|
22
19
|
# @param name [String] step name
|
|
23
20
|
# @param input [String] artifact to read from each node
|
|
24
21
|
# @param storage_name [String] storage to write to, by name
|
|
@@ -71,7 +68,7 @@ module Glazed
|
|
|
71
68
|
names[key] = node.name
|
|
72
69
|
[key, @context.read(artifact(state, @input))] #: Storages::Base::file
|
|
73
70
|
end
|
|
74
|
-
results = @storage.write_all(files, message: ->(
|
|
71
|
+
results = @storage.write_all(files, message: ->(key) { "glazed: #{names.fetch(key, key)}" })
|
|
75
72
|
output = @output
|
|
76
73
|
return batch unless output
|
|
77
74
|
|
|
@@ -80,15 +77,6 @@ module Glazed
|
|
|
80
77
|
end
|
|
81
78
|
Proto::V1::BatchState.new(nodes:)
|
|
82
79
|
end
|
|
83
|
-
|
|
84
|
-
# @param names [Array<String>] names of the nodes whose content changed
|
|
85
|
-
# @return [String] commit message naming the nodes
|
|
86
|
-
#: (Array[String] names) -> String
|
|
87
|
-
private def message(names)
|
|
88
|
-
listed = names.first(MESSAGE_NAMES).join(', ')
|
|
89
|
-
rest = names.size - MESSAGE_NAMES
|
|
90
|
-
rest.positive? ? "#{name}: update #{listed} and #{rest} more" : "#{name}: update #{listed}"
|
|
91
|
-
end
|
|
92
80
|
end
|
|
93
81
|
end
|
|
94
82
|
end
|
data/lib/glazed/storages/base.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Glazed
|
|
|
21
21
|
class Conflict < Glazed::Error; end
|
|
22
22
|
|
|
23
23
|
#: type file = [String, String]
|
|
24
|
-
#: type message_builder = ^(
|
|
24
|
+
#: type message_builder = ^(String changed_key) -> String
|
|
25
25
|
|
|
26
26
|
# Outcome of writing one file.
|
|
27
27
|
class WriteResult
|
|
@@ -78,15 +78,16 @@ module Glazed
|
|
|
78
78
|
# @raise [InvalidKey] for an unsafe key
|
|
79
79
|
#: (key: String, content: String, message: String) -> WriteResult
|
|
80
80
|
def write(key:, content:, message:)
|
|
81
|
-
write_all([[key, content]], message: ->(
|
|
81
|
+
write_all([[key, content]], message: ->(_changed_key) { message }).fetch(0)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Writes several files as one change where the storage
|
|
85
|
-
#
|
|
84
|
+
# Writes several files, as one change per changed file where the storage
|
|
85
|
+
# records changes, such as one commit each. Files whose content equals
|
|
86
|
+
# what their key holds are not written again.
|
|
86
87
|
#
|
|
87
88
|
# @param files [Array<Array(String, String)>] keys and contents
|
|
88
|
-
# @param message [Proc] builds the description of the change
|
|
89
|
-
# commit message
|
|
89
|
+
# @param message [Proc] builds the description of the change to one key,
|
|
90
|
+
# such as a commit message; called only for keys whose content changed
|
|
90
91
|
# @return [Array<WriteResult>] results, in the order of files
|
|
91
92
|
# @raise [InvalidKey] for an unsafe key
|
|
92
93
|
#: (Array[Base::file] files, message: Base::message_builder) -> Array[WriteResult]
|
data/lib/glazed/storages/git.rb
CHANGED
|
@@ -12,8 +12,8 @@ require 'glazed/storages/base'
|
|
|
12
12
|
|
|
13
13
|
module Glazed
|
|
14
14
|
module Storages
|
|
15
|
-
# Local git repository driven by the git command. Each
|
|
16
|
-
# on the configured branch, optionally pulled before and pushed after.
|
|
15
|
+
# Local git repository driven by the git command. Each changed file is one
|
|
16
|
+
# commit on the configured branch, optionally pulled before and pushed after.
|
|
17
17
|
#
|
|
18
18
|
# @example
|
|
19
19
|
# storage = Glazed::Storages::Git.new(path: '/var/lib/glazed/raw')
|
|
@@ -81,12 +81,13 @@ module Glazed
|
|
|
81
81
|
#: String?
|
|
82
82
|
attr_reader :subdirectory
|
|
83
83
|
|
|
84
|
-
# Writes
|
|
85
|
-
# changed, and the pointers then name the
|
|
84
|
+
# Writes each changed file in its own commit and pushes once. Nothing is
|
|
85
|
+
# committed when no content changed, and the pointers then name the
|
|
86
|
+
# existing HEAD.
|
|
86
87
|
#
|
|
87
88
|
# @param files [Array<Array(String, String)>] keys and contents
|
|
88
|
-
# @param message [Proc] builds the commit message
|
|
89
|
-
# @return [Array<WriteResult>] the commit
|
|
89
|
+
# @param message [Proc] builds the commit message for a changed key
|
|
90
|
+
# @return [Array<WriteResult>] the last commit, which holds every file, and whether each file changed
|
|
90
91
|
# @raise [InvalidKey] for an unsafe key
|
|
91
92
|
# @raise [CommandFailed] when a git command fails
|
|
92
93
|
#: (Array[Base::file] files, message: Base::message_builder) -> Array[WriteResult]
|
|
@@ -104,7 +105,7 @@ module Glazed
|
|
|
104
105
|
changed_paths = git(['diff', '--cached', '--name-only', '-z', '--', *paths]).split("\0")
|
|
105
106
|
changed = keys.select { changed_paths.include?(repository_path(it)) }
|
|
106
107
|
unless changed.empty?
|
|
107
|
-
git(['commit', '--quiet', '--no-verify', '-m', message.call(
|
|
108
|
+
changed.each { git(['commit', '--quiet', '--no-verify', '-m', message.call(it), '--', repository_path(it)]) }
|
|
108
109
|
git(['push', '--quiet', Must.expect(remote), "HEAD:refs/heads/#{branch}"]) if remote
|
|
109
110
|
end
|
|
110
111
|
commit = git(%w[rev-parse HEAD]).strip
|
|
@@ -15,8 +15,8 @@ require 'glazed/version'
|
|
|
15
15
|
|
|
16
16
|
module Glazed
|
|
17
17
|
module Storages
|
|
18
|
-
# GitHub repository written through the Git Data API. Each
|
|
19
|
-
# commit on the branch, which is created from the base branch when missing.
|
|
18
|
+
# GitHub repository written through the Git Data API. Each changed file is
|
|
19
|
+
# one commit on the branch, which is created from the base branch when missing.
|
|
20
20
|
#
|
|
21
21
|
# @example
|
|
22
22
|
# storage = Glazed::Storages::Github.new(repository: 'org/netconf', branch: 'glazed/auto', token:)
|
|
@@ -99,12 +99,13 @@ module Glazed
|
|
|
99
99
|
#: String?
|
|
100
100
|
attr_reader :subdirectory
|
|
101
101
|
|
|
102
|
-
# Writes
|
|
103
|
-
# content changed, and the pointers then
|
|
102
|
+
# Writes each changed file in its own commit and moves the branch once.
|
|
103
|
+
# Nothing is committed when no content changed, and the pointers then
|
|
104
|
+
# name the existing head.
|
|
104
105
|
#
|
|
105
106
|
# @param files [Array<Array(String, String)>] keys and contents
|
|
106
|
-
# @param message [Proc] builds the commit message
|
|
107
|
-
# @return [Array<WriteResult>] the commit
|
|
107
|
+
# @param message [Proc] builds the commit message for a changed key
|
|
108
|
+
# @return [Array<WriteResult>] the last commit, which holds every file, and whether each file changed
|
|
108
109
|
# @raise [InvalidKey] for an unsafe key
|
|
109
110
|
# @raise [ApiError] when the API fails, or the branch keeps moving under concurrent writers
|
|
110
111
|
#: (Array[Base::file] files, message: Base::message_builder) -> Array[WriteResult]
|
|
@@ -147,8 +148,8 @@ module Glazed
|
|
|
147
148
|
end
|
|
148
149
|
|
|
149
150
|
# @param files [Array<Array(String, String)>] keys and contents
|
|
150
|
-
# @param message [Proc] builds the commit message
|
|
151
|
-
# @return [Array(String, Array<String>)] SHA of the commit
|
|
151
|
+
# @param message [Proc] builds the commit message for a changed key
|
|
152
|
+
# @return [Array(String, Array<String>)] SHA of the last commit, and the changed keys
|
|
152
153
|
# @raise [ApiError] when the API fails
|
|
153
154
|
#: (Array[Base::file] files, Base::message_builder message) -> [String, Array[String]]
|
|
154
155
|
private def commit_files(files, message)
|
|
@@ -158,16 +159,18 @@ module Glazed
|
|
|
158
159
|
existing = blob_shas(base_tree)
|
|
159
160
|
candidates = existing ? files.reject { |key, content| existing[repository_path(key)] == blob_sha(content) } : files
|
|
160
161
|
|
|
161
|
-
commit = parent
|
|
162
|
+
commit = parent #: String
|
|
163
|
+
tree = base_tree
|
|
162
164
|
changed = [] #: Array[String]
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
candidates.each do |key, content|
|
|
166
|
+
entry = {path: repository_path(key), mode: '100644', type: 'blob', content:}
|
|
167
|
+
new_tree = json(post("/repos/#{repository}/git/trees", {base_tree: tree, tree: [entry]})).fetch('sha')
|
|
168
|
+
next if new_tree == tree
|
|
169
|
+
|
|
170
|
+
body = {message: message.call(key), tree: new_tree, parents: [commit], author: @author}.compact
|
|
171
|
+
commit = json(post("/repos/#{repository}/git/commits", body)).fetch('sha')
|
|
172
|
+
tree = new_tree
|
|
173
|
+
changed << key
|
|
171
174
|
end
|
|
172
175
|
|
|
173
176
|
if head.nil?
|
|
@@ -101,7 +101,7 @@ module Glazed
|
|
|
101
101
|
# Runs one command outside the interactive session.
|
|
102
102
|
#
|
|
103
103
|
# @param command [String] command line
|
|
104
|
-
# @param timeout [Numeric] seconds
|
|
104
|
+
# @param timeout [Numeric] seconds the command may stay silent before it finishes
|
|
105
105
|
# @return [String] command output
|
|
106
106
|
# @raise [NotImplementedError] when the transport has no exec
|
|
107
107
|
#: (String command, timeout: Numeric) -> String
|
|
@@ -58,6 +58,9 @@ module Glazed
|
|
|
58
58
|
password: password&.value,
|
|
59
59
|
timeout: connect_timeout,
|
|
60
60
|
non_interactive: true,
|
|
61
|
+
# Some devices leave keepalives unanswered while busy, and the idle
|
|
62
|
+
# timeout of CliSession and #exec already detects a dead connection.
|
|
63
|
+
keepalive_maxcount: 0,
|
|
61
64
|
)
|
|
62
65
|
end
|
|
63
66
|
|
|
@@ -140,25 +143,29 @@ module Glazed
|
|
|
140
143
|
# Runs a command on its own exec channel.
|
|
141
144
|
#
|
|
142
145
|
# @param command [String] command line
|
|
143
|
-
# @param timeout [Numeric] seconds
|
|
146
|
+
# @param timeout [Numeric] seconds the channel may stay silent before it closes
|
|
144
147
|
# @return [String] combined stdout and stderr
|
|
145
148
|
# @raise [Glazed::Error] when the device refuses the exec request
|
|
146
|
-
# @raise [Glazed::TimeoutError] when the
|
|
149
|
+
# @raise [Glazed::TimeoutError] when the channel stays silent for timeout seconds
|
|
147
150
|
#: (String command, timeout: Numeric) -> String
|
|
148
151
|
def exec(command, timeout:)
|
|
149
152
|
output = +''
|
|
150
153
|
closed = false #: bool
|
|
154
|
+
deadline = now + timeout.to_f
|
|
155
|
+
receive = lambda do |data|
|
|
156
|
+
output << data
|
|
157
|
+
deadline = now + timeout.to_f
|
|
158
|
+
end
|
|
151
159
|
session.open_channel do |channel|
|
|
152
160
|
channel.exec(command) do |_, success|
|
|
153
161
|
raise Glazed::Error, "#{host}: exec request refused for #{command.inspect}" unless success
|
|
154
162
|
end
|
|
155
|
-
channel.on_data { |_, data|
|
|
156
|
-
channel.on_extended_data { |_, _, data|
|
|
163
|
+
channel.on_data { |_, data| receive.call(data) }
|
|
164
|
+
channel.on_extended_data { |_, _, data| receive.call(data) }
|
|
157
165
|
channel.on_close { closed = true }
|
|
158
166
|
end
|
|
159
|
-
deadline = now + timeout.to_f
|
|
160
167
|
session.loop(0.1) do
|
|
161
|
-
raise Glazed::TimeoutError, "#{host}: #{command.inspect}
|
|
168
|
+
raise Glazed::TimeoutError, "#{host}: #{command.inspect} sent no output for #{timeout}s" if !closed && now > deadline
|
|
162
169
|
|
|
163
170
|
!closed
|
|
164
171
|
end
|
data/lib/glazed/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glazed
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sorah Fukumori
|
|
@@ -164,14 +164,18 @@ files:
|
|
|
164
164
|
- lib/glazed/dialect/runbook.rb
|
|
165
165
|
- lib/glazed/dialect/sensitive_pattern.rb
|
|
166
166
|
- lib/glazed/dialects.rb
|
|
167
|
+
- lib/glazed/dialects/aireos.rb
|
|
168
|
+
- lib/glazed/dialects/aruba_instant.rb
|
|
167
169
|
- lib/glazed/dialects/awplus.rb
|
|
168
170
|
- lib/glazed/dialects/base.rb
|
|
171
|
+
- lib/glazed/dialects/eos.rb
|
|
169
172
|
- lib/glazed/dialects/ios.rb
|
|
170
173
|
- lib/glazed/dialects/junos.rb
|
|
171
174
|
- lib/glazed/dialects/nec_ix.rb
|
|
172
|
-
- lib/glazed/dialects/
|
|
175
|
+
- lib/glazed/dialects/routeros.rb
|
|
173
176
|
- lib/glazed/finder.rb
|
|
174
177
|
- lib/glazed/must.rb
|
|
178
|
+
- lib/glazed/node_session.rb
|
|
175
179
|
- lib/glazed/orchestrator.rb
|
|
176
180
|
- lib/glazed/orchestrator/policy.rb
|
|
177
181
|
- lib/glazed/plugin_registry.rb
|