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,95 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed/dialect'
7
+
8
+ module Glazed
9
+ module Dialect
10
+ # Edits that interactions and runbook steps apply to a text buffer.
11
+ #
12
+ # A String is shorthand for {Append} and nil does nothing.
13
+ module Actions
14
+ # Action applying content to a buffer.
15
+ class Content
16
+ # @param content [String] text the action carries
17
+ #: (content: String) -> void
18
+ def initialize(content:)
19
+ @content = content.dup.freeze #: String
20
+ end
21
+
22
+ # @return [String] text the action carries
23
+ #: String
24
+ attr_reader :content
25
+
26
+ # @param other [Object] object to compare
27
+ # @return [Boolean] whether other is the same action with the same content
28
+ #: (untyped other) -> bool
29
+ def ==(other) = other.class == self.class && other.content == content
30
+
31
+ alias eql? ==
32
+
33
+ # @return [Integer] hash consistent with {#==}
34
+ #: () -> Integer
35
+ def hash = [self.class, content].hash
36
+ end
37
+
38
+ # Appends content to the buffer.
39
+ class Append < Content; end
40
+
41
+ # Replaces the whole buffer with content.
42
+ class Replace < Content; end
43
+
44
+ # Applies several actions in order.
45
+ class Multi
46
+ # @param actions [Array<Append, Replace, Multi, String, nil>] actions, flattened and normalised
47
+ #: (*Actions::action? actions) -> void
48
+ def initialize(*actions)
49
+ @actions = actions.flat_map { Multi.normalize(it) }.freeze #: Array[Append | Replace]
50
+ end
51
+
52
+ # @return [Array<Append, Replace>] flattened actions
53
+ #: Array[Append | Replace]
54
+ attr_reader :actions
55
+
56
+ # @param action [Append, Replace, Multi, String, nil] action to normalise
57
+ # @return [Array<Append, Replace>] equivalent plain actions
58
+ #: (Actions::action? action) -> Array[Append | Replace]
59
+ def self.normalize(action)
60
+ case action
61
+ when nil then []
62
+ when String then [Append.new(content: action)]
63
+ when Multi then action.actions
64
+ else [action]
65
+ end
66
+ end
67
+
68
+ # @param other [Object] object to compare
69
+ # @return [Boolean] whether other holds the same actions
70
+ #: (untyped other) -> bool
71
+ def ==(other) = other.is_a?(Multi) ? other.actions == actions : false
72
+ end
73
+
74
+ #: type action = Append | Replace | Multi | String
75
+
76
+ # Applies an action to a buffer.
77
+ #
78
+ # @param buffer [String] text to edit
79
+ # @param action [Append, Replace, Multi, String, nil] action to apply
80
+ # @return [String] the edited text, a new string
81
+ # @example
82
+ # Actions.apply("a", Actions::Append.new(content: "b")) #=> "ab"
83
+ #: (String buffer, Actions::action? action) -> String
84
+ def self.apply(buffer, action)
85
+ Multi.normalize(action).inject(buffer.dup) do |result, step|
86
+ case step
87
+ when Append then result << step.content
88
+ when Replace then step.content.dup
89
+ else step #: absurd
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,63 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed/dialect'
7
+
8
+ module Glazed
9
+ module Dialect
10
+ # How a dialect talks to a device over one transport.
11
+ class CommunicationConfig
12
+ #: type hook = ^(Run run) -> void
13
+
14
+ #: () -> void
15
+ def initialize
16
+ @exec = false #: bool
17
+ @pty_options = {} #: Hash[Symbol, untyped]
18
+ @username_prompt = nil #: Regexp?
19
+ @password_prompt = nil #: Regexp?
20
+ @post_login_hooks = [] #: Array[CommunicationConfig::hook]
21
+ @pre_logout_hooks = [] #: Array[CommunicationConfig::hook]
22
+ end
23
+
24
+ # @return [Boolean] run each command on its own exec channel when the transport supports it
25
+ #: bool
26
+ attr_accessor :exec
27
+ # @return [Hash{Symbol => Object}] PTY settings such as :term and :chars_wide, over the transport's defaults
28
+ #: Hash[Symbol, untyped]
29
+ attr_reader :pty_options
30
+
31
+ # @return [Regexp, nil] prompt for the user name; when set, the session logs in by answering it
32
+ #: Regexp?
33
+ attr_accessor :username_prompt
34
+ # @return [Regexp, nil] prompt for the password
35
+ #: Regexp?
36
+ attr_accessor :password_prompt
37
+ # @return [Array<Proc>] hooks run after login, in order
38
+ #: Array[CommunicationConfig::hook]
39
+ attr_reader :post_login_hooks
40
+ # @return [Array<Proc>] hooks run before logout, in order
41
+ #: Array[CommunicationConfig::hook]
42
+ attr_reader :pre_logout_hooks
43
+
44
+ # Adds a hook run after login and before the runbook.
45
+ #
46
+ # @yieldparam run [Run] the run in progress
47
+ # @return [void]
48
+ #: () { (Run run) -> void } -> void
49
+ def post_login(&hook)
50
+ @post_login_hooks << hook
51
+ end
52
+
53
+ # Adds a hook run after the runbook and before the connection closes.
54
+ #
55
+ # @yieldparam run [Run] the run in progress
56
+ # @return [void]
57
+ #: () { (Run run) -> void } -> void
58
+ def pre_logout(&hook)
59
+ @pre_logout_hooks << hook
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
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
+
9
+ module Glazed
10
+ module Dialect
11
+ #: type responder = ^(CliSession session, String buffer, MatchData match) -> Actions::action?
12
+ #: type data_filter = ^(String fragment) -> String
13
+
14
+ # Responds to a pattern appearing in the session buffer, such as a pager prompt.
15
+ class Interaction
16
+ # @param pattern [Regexp] pattern matched against the session buffer
17
+ # @param responder [Proc] called with the session, a copy of the buffer and the match; returns an action applied to the buffer
18
+ #: (pattern: Regexp, responder: Dialect::responder) -> void
19
+ def initialize(pattern:, responder:)
20
+ @pattern = pattern #: Regexp
21
+ @responder = responder #: Dialect::responder
22
+ end
23
+
24
+ # @return [Regexp] pattern matched against the session buffer
25
+ #: Regexp
26
+ attr_reader :pattern
27
+ # @return [Proc] responder returning an action applied to the buffer
28
+ #: Dialect::responder
29
+ attr_reader :responder
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,84 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed/cli_session'
7
+ require 'glazed/command_result'
8
+ require 'glazed/dialect'
9
+ require 'glazed/dialect/actions'
10
+ require 'glazed/proto'
11
+ require 'glazed/secret'
12
+
13
+ module Glazed
14
+ module Dialect
15
+ # State of one runbook execution against one node.
16
+ class Run
17
+ # @param cli [CliSession] session to the node
18
+ # @param node [Proto::V1::Node] node being run against
19
+ # @param dialect [Class<Dialects::Base>] dialect of the node
20
+ #: (cli: CliSession, node: Proto::V1::Node, dialect: singleton(Dialects::Base)) -> void
21
+ def initialize(cli:, node:, dialect:)
22
+ @cli = cli #: CliSession
23
+ @node = node #: Proto::V1::Node
24
+ @dialect = dialect #: singleton(Dialects::Base)
25
+ @vars = {} #: Hash[Symbol, untyped]
26
+ @output = +'' #: String
27
+ end
28
+
29
+ # @return [CliSession] session to the node
30
+ #: CliSession
31
+ attr_reader :cli
32
+ # @return [Proto::V1::Node] node being run against
33
+ #: Proto::V1::Node
34
+ attr_reader :node
35
+ # @return [Class<Dialects::Base>] dialect of the node
36
+ #: singleton(Dialects::Base)
37
+ attr_reader :dialect
38
+ # @return [Hash{Symbol => Object}] scratch space shared by the steps of this run
39
+ #: Hash[Symbol, untyped]
40
+ attr_reader :vars
41
+ # @return [String] configuration collected so far
42
+ #: String
43
+ attr_reader :output
44
+
45
+ # Runs a command and passes its output, as bytes, through the dialect's
46
+ # output filters. Collected configuration must be UTF-8, so a dialect for
47
+ # a device using another encoding converts it in an output filter.
48
+ #
49
+ # @param line [String] command line
50
+ # @return [CommandResult] the filtered result, in UTF-8
51
+ # @raise [InvalidEncoding] when the filtered output is not valid UTF-8
52
+ #: (String line) -> CommandResult
53
+ def command(line)
54
+ result = cli.command(line)
55
+ filtered = dialect.output_filters.inject(result.output) { |text, output_filter| output_filter.call(text) }
56
+ output = filtered.dup.force_encoding(Encoding::UTF_8)
57
+ unless output.valid_encoding?
58
+ raise InvalidEncoding, "output of #{line.inspect} is not valid UTF-8; convert it in the dialect's filter_output"
59
+ end
60
+
61
+ CommandResult.new(command: result.command, output:)
62
+ end
63
+
64
+ # Applies an action to the collected configuration.
65
+ #
66
+ # @param action [Actions::Append, Actions::Replace, Actions::Multi, String, nil] action to apply
67
+ # @return [void]
68
+ #: (Actions::action? action) -> void
69
+ def emit(action)
70
+ @output = Actions.apply(@output, action)
71
+ end
72
+
73
+ # @param name [String] parameter name
74
+ # @return [String, nil] the node's parameter value
75
+ #: (String name) -> String?
76
+ def param(name) = node.params[name]
77
+
78
+ # @param name [String] secret name
79
+ # @return [Secret, nil] the node's secret
80
+ #: (String name) -> Secret?
81
+ def secret(name) = Secret.maybe(node.secrets[name])
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,97 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed/command_result'
7
+ require 'glazed/dialect'
8
+ require 'glazed/dialect/actions'
9
+ require 'glazed/dialect/run'
10
+
11
+ module Glazed
12
+ module Dialect
13
+ # Ordered steps that collect configuration from a node.
14
+ class Runbook
15
+ #: type step_block = ^(Run run) -> void
16
+
17
+ # One step of a runbook.
18
+ class Step
19
+ # @param description [String] what the step does, such as its command line
20
+ # @param block [Proc] called with the run
21
+ #: (description: String, block: Runbook::step_block) -> void
22
+ def initialize(description:, block:)
23
+ @description = description #: String
24
+ @block = block #: Runbook::step_block
25
+ end
26
+
27
+ # @return [String] what the step does
28
+ #: String
29
+ attr_reader :description
30
+ # @return [Proc] called with the run
31
+ #: Runbook::step_block
32
+ attr_reader :block
33
+ end
34
+
35
+ # Collects steps for a runbook.
36
+ #
37
+ # @example
38
+ # builder.command('show version') { |result| comment(result.output) }
39
+ # builder.raw { |run| run.vars[:model] = run.command('show model').output }
40
+ class Builder
41
+ #: () -> void
42
+ def initialize
43
+ @steps = [] #: Array[Step]
44
+ end
45
+
46
+ # Adds a step that runs a command and emits what the block returns, or
47
+ # the output itself without a block.
48
+ #
49
+ # @param line [String] command line
50
+ # @yieldparam result [CommandResult] filtered command result
51
+ # @yieldreturn [Actions::Append, Actions::Replace, Actions::Multi, String, nil] action to emit
52
+ # @return [void]
53
+ #: (String line) ?{ (CommandResult result) -> Actions::action? } -> void
54
+ def command(line, &callback)
55
+ raw(line) do |run|
56
+ result = run.command(line)
57
+ run.emit(callback ? callback.call(result) : result.output)
58
+ end
59
+ end
60
+
61
+ # Adds a step that drives the run directly.
62
+ #
63
+ # @param description [String] what the step does
64
+ # @yieldparam run [Run] the run in progress
65
+ # @return [void]
66
+ #: (?String description) { (Run run) -> void } -> void
67
+ def raw(description = '', &block)
68
+ @steps << Step.new(description:, block:)
69
+ end
70
+
71
+ # @return [Runbook] runbook of the collected steps
72
+ #: () -> Runbook
73
+ def build = Runbook.new(steps: @steps.dup)
74
+ end
75
+
76
+ # @param steps [Array<Step>] steps in order
77
+ #: (steps: Array[Step]) -> void
78
+ def initialize(steps:)
79
+ @steps = steps.freeze #: Array[Step]
80
+ end
81
+
82
+ # @return [Array<Step>] steps in order
83
+ #: Array[Step]
84
+ attr_reader :steps
85
+
86
+ # Runs every step.
87
+ #
88
+ # @param run [Run] run to execute against
89
+ # @return [String] the collected configuration
90
+ #: (Run run) -> String
91
+ def execute(run)
92
+ steps.each { it.block.call(run) }
93
+ run.output
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,31 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed/dialect'
7
+
8
+ module Glazed
9
+ module Dialect
10
+ # Locates secrets in configuration text.
11
+ class SensitivePattern
12
+ #: type determiner = ^(MatchData match) -> (String | Range[Integer])?
13
+
14
+ # @param pattern [Regexp] pattern matched against the whole configuration
15
+ # @param determiner [Proc] returns the secret within a match, or its offsets in the configuration,
16
+ # or nil when the match holds none
17
+ #: (pattern: Regexp, determiner: SensitivePattern::determiner) -> void
18
+ def initialize(pattern:, determiner:)
19
+ @pattern = pattern #: Regexp
20
+ @determiner = determiner #: SensitivePattern::determiner
21
+ end
22
+
23
+ # @return [Regexp] pattern matched against the whole configuration
24
+ #: Regexp
25
+ attr_reader :pattern
26
+ # @return [Proc] returns the secret within a match, or its offsets
27
+ #: SensitivePattern::determiner
28
+ attr_reader :determiner
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ require 'glazed'
7
+
8
+ module Glazed
9
+ # Building blocks of the dialect DSL. Dialects themselves live in {Dialects}.
10
+ module Dialect
11
+ end
12
+ end
@@ -0,0 +1,84 @@
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/awplus.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
+ # Allied Telesis AlliedWare Plus.
15
+ class Awplus < Base
16
+ prompt(/^(\r?[\w.@:\/-]+[#>]\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
+ filter_data { |fragment| fragment.gsub("\e[7m\e[m", '').delete("\r") }
25
+
26
+ sensitive_pattern(/^snmp-server community\s+(\S+)/) { it[1] }
27
+ sensitive_pattern(/^snmp-server user .+auth \S+ (\S+)$/) { it[1] }
28
+ sensitive_pattern(/^snmp-server user .+priv \S+ (\S+)$/) { it[1] }
29
+ sensitive_pattern(/^username \S+ privilege \d+ (?:password|secret) (?:\d+ )?(\S+)/) { it[1] }
30
+ sensitive_pattern(/^username .+? password \d+\s+(\S+).*/) { it[1] }
31
+ sensitive_pattern(/^username .+? secret \d+\s+(\S+).*/) { it[1] }
32
+ sensitive_pattern(/^enable (?:password|secret) (?:\d )?\s*(\S+)/) { it[1] }
33
+ sensitive_pattern(/\s+(?:password|secret) (?:\d )?(\S+)/) { it[1] }
34
+ sensitive_pattern(/^tacacs-server key (?:\d )?(\S+)/) { it[1] }
35
+ sensitive_pattern(/^radius-server key (\S+)/) { it[1] }
36
+ sensitive_pattern(/^auth-mac (?:encrypted )?password (\S+)/) { it[1] }
37
+ sensitive_pattern(/^\s*ppp password (?:encrypted )?(.+)$/) { it[1] }
38
+ sensitive_pattern(/^\s*key-string (?:encrypted )?(.+)$/) { it[1] }
39
+ sensitive_pattern(/^\s+password (?:encrypted )?(.+)$/) { it[1] }
40
+ sensitive_pattern(/^\s+key (?:encrypted )?(.+)$/) { it[1] }
41
+ sensitive_pattern(/^\s+neighbor \S+ password (.+?)(?: encrypted)?$/) { it[1] }
42
+ sensitive_pattern(/^\s+ip ospf \S+ message-digest-key \d+ md5 (?:encrypted )?(.+)$/) { it[1] }
43
+ sensitive_pattern(/^\s+ip ospf \S+ authentication-key (?:encrypted )?(.+)$/) { it[1] }
44
+ sensitive_pattern(/^\s*aes-key (?:encrypted )?(.+)$/) { it[1] }
45
+
46
+ runbook :get do |book|
47
+ book.command('show system') do |result|
48
+ stable = result.output.each_line.grep_v(
49
+ /\A\s*\n\z
50
+ |System\ Status
51
+ |RAM\s*:
52
+ |Uptime\s*:
53
+ |Flash\s*:
54
+ |Current\ software\s*:
55
+ |Software\ version\s*:
56
+ |Build\ date\s*:
57
+ /x,
58
+ )
59
+ comment("-- show system #{'-' * 66}\n#{stable.join}#{'-' * 80}\n")
60
+ end
61
+ book.command('show running-config')
62
+ end
63
+
64
+ communication :telnet do |config|
65
+ config.username_prompt = /^login:\s/i
66
+ config.password_prompt = /^password:\s/i
67
+ end
68
+
69
+ communication :ssh, :telnet do |config|
70
+ config.post_login do |run|
71
+ if (enable = run.secret('enable'))
72
+ run.cli.write("enable\n")
73
+ run.cli.expect(/^Password:\s/)
74
+ run.cli.write("#{enable.value}\n")
75
+ run.cli.expect(run.cli.prompt)
76
+ else
77
+ run.cli.command('enable')
78
+ end
79
+ end
80
+ config.pre_logout { |run| run.cli.write("exit\n") }
81
+ end
82
+ end
83
+ end
84
+ end