asa_console 0.1.2
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 +15 -0
- data/.yardopts +8 -0
- data/LICENSE.md +22 -0
- data/README.md +116 -0
- data/asa_console.gemspec +30 -0
- data/bin/asatest +113 -0
- data/lib/asa_console.rb +283 -0
- data/lib/asa_console/config.rb +170 -0
- data/lib/asa_console/error.rb +43 -0
- data/lib/asa_console/terminal.rb +9 -0
- data/lib/asa_console/terminal/fake_ssh.rb +145 -0
- data/lib/asa_console/terminal/ssh.rb +223 -0
- data/lib/asa_console/test.rb +134 -0
- data/lib/asa_console/test/script.rb +48 -0
- data/lib/asa_console/util.rb +151 -0
- data/script/test_clock.rb +39 -0
- data/script/test_connect.rb +10 -0
- data/script/test_error_command.rb +15 -0
- data/script/test_error_connect.rb +31 -0
- data/script/test_error_enable.rb +18 -0
- data/script/test_names.rb +33 -0
- data/script/test_object.rb +50 -0
- data/script/test_terminal.rb +25 -0
- data/script/test_version.rb +31 -0
- metadata +209 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class ASAConsole
|
5
|
+
#
|
6
|
+
# A parser for ASA running/startup config.
|
7
|
+
#
|
8
|
+
# Each instance represents a single config entry which may include nested
|
9
|
+
# lines of config that can be queried with {#select} or {#select_all}.
|
10
|
+
#
|
11
|
+
# @example Print ACEs with line numbers for each access list
|
12
|
+
# # Start by fetching a top-level Config object
|
13
|
+
# config = asa.running_config('access-list')
|
14
|
+
# config.names_of('access-list').each do |acl|
|
15
|
+
# puts "Access list #{acl}:"
|
16
|
+
# counter = 0
|
17
|
+
# config.select('access-list', acl) do |ace|
|
18
|
+
# counter += 1
|
19
|
+
# puts " Line #{counter}: #{ace.line}"
|
20
|
+
# end
|
21
|
+
# puts
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @example List all interfaces that participate in EIGRP
|
25
|
+
# # Start by fetching a top-level Config object
|
26
|
+
# config = asa.running_config('all router eigrp')
|
27
|
+
# router = config.select('router eigrp')
|
28
|
+
# router.select('passive-interface') do |passive_interface|
|
29
|
+
# ifname = passive_interface.config_data
|
30
|
+
# next if ifname == 'default'
|
31
|
+
# if passive_interface.negated?
|
32
|
+
# puts "Interface '#{ifname}' is sending routing updates"
|
33
|
+
# else
|
34
|
+
# puts "Interface '#{ifname}' is passive"
|
35
|
+
# end
|
36
|
+
# end if router
|
37
|
+
#
|
38
|
+
# @attr_reader keystr [String, nil]
|
39
|
+
# Key used to select the line of config represented by this object, or `nil`
|
40
|
+
# if the object contains nested top-level config.
|
41
|
+
# @attr_reader config_name [String, nil]
|
42
|
+
# An identifier, such as an access list name, used to distinguish between
|
43
|
+
# config entries of the same type.
|
44
|
+
# @attr_reader config_data [String, nil]
|
45
|
+
# Remainder of the config line following the {#keystr} and {#config_name}.
|
46
|
+
#
|
47
|
+
class Config
|
48
|
+
attr_reader :keystr
|
49
|
+
attr_reader :config_name
|
50
|
+
attr_reader :config_data
|
51
|
+
|
52
|
+
# @see #running_config
|
53
|
+
# @option opts [String] :keystr class attribute
|
54
|
+
# @option opts [String] :config_name class attribute
|
55
|
+
# @option opts [String] :config_data class attribute
|
56
|
+
# @option opts [Boolean] :negated
|
57
|
+
# `true` if the config line began with "no", or `false` otherwise
|
58
|
+
# @option opts [String] :nested_config
|
59
|
+
# a multiline string of config (indentation stripped)
|
60
|
+
def initialize(opts = {})
|
61
|
+
@keystr = opts[:keystr]
|
62
|
+
@config_name = opts[:config_name]
|
63
|
+
@config_data = opts[:config_data]
|
64
|
+
@negated = opts.fetch(:negated, false)
|
65
|
+
@nested_config = opts.fetch(:nested_config, '')
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [String, nil]
|
69
|
+
# the selected line, or `nil` if this is a top-level object
|
70
|
+
def line
|
71
|
+
parts = []
|
72
|
+
parts << 'no' if @negated
|
73
|
+
parts << @keystr if @keystr
|
74
|
+
parts << @config_name if @config_name
|
75
|
+
parts << @config_data if @config_data
|
76
|
+
parts.empty? ? nil : parts.join(' ')
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Boolean]
|
80
|
+
# `true` if the selected line began with "no", or `false` otherwise
|
81
|
+
def negated?
|
82
|
+
@negated ? true : false
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [Boolean]
|
86
|
+
# `true` if there is no nested config, or `false` otherwise
|
87
|
+
def empty?
|
88
|
+
@nested_config.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
# Select all lines of nested config. Equivalent to {#select} with no
|
92
|
+
# arguments.
|
93
|
+
#
|
94
|
+
# @yieldparam config [Config]
|
95
|
+
# @yieldreturn [nil] if a block is given
|
96
|
+
# @return [Array<Config>] if no block given
|
97
|
+
def select_all
|
98
|
+
result = []
|
99
|
+
select do |config|
|
100
|
+
if block_given?
|
101
|
+
yield config
|
102
|
+
else
|
103
|
+
result << config
|
104
|
+
end
|
105
|
+
end
|
106
|
+
result unless block_given?
|
107
|
+
end
|
108
|
+
|
109
|
+
# Select the first matching line of the nested config or `yield` all
|
110
|
+
# matching lines if a block is given.
|
111
|
+
#
|
112
|
+
# @param keystr [String, nil]
|
113
|
+
# @param config_name [String, nil]
|
114
|
+
# @yieldparam config [Config]
|
115
|
+
# @yieldreturn [nil] if a block is given
|
116
|
+
# @return [Config] if no block given
|
117
|
+
def select(keystr = nil, config_name = nil)
|
118
|
+
prefix = [keystr, config_name].join(' ').strip
|
119
|
+
regex = /^(?<no>no )?#{Regexp.escape(prefix)} ?(?<data>.+)?/
|
120
|
+
|
121
|
+
io = StringIO.open(@nested_config)
|
122
|
+
lines = io.readlines
|
123
|
+
io.close
|
124
|
+
|
125
|
+
loop do
|
126
|
+
break if lines.empty?
|
127
|
+
|
128
|
+
m = regex.match(lines.shift)
|
129
|
+
next unless m
|
130
|
+
|
131
|
+
nested_config = ''
|
132
|
+
loop do
|
133
|
+
break unless lines[0] && lines[0].start_with?(' ')
|
134
|
+
nested_config << lines.shift.sub(/^ /, '')
|
135
|
+
end
|
136
|
+
|
137
|
+
config = Config.new(
|
138
|
+
keystr: keystr,
|
139
|
+
config_name: config_name,
|
140
|
+
config_data: m[:data],
|
141
|
+
negated: !m[:no].nil?,
|
142
|
+
nested_config: nested_config
|
143
|
+
)
|
144
|
+
|
145
|
+
if block_given?
|
146
|
+
yield config
|
147
|
+
else
|
148
|
+
return config
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
# @see #select
|
156
|
+
# @param keystr [String]
|
157
|
+
# @return [Array<String>]
|
158
|
+
# a unique list of config element names matched by `keystr`
|
159
|
+
def names_of(keystr)
|
160
|
+
names = []
|
161
|
+
regex = /^(?:no )?#{Regexp.escape(keystr)} (?<name>\S+)/
|
162
|
+
@nested_config.each_line do |line|
|
163
|
+
m = regex.match(line)
|
164
|
+
next unless m
|
165
|
+
names << m[:name] unless names.include? m[:name]
|
166
|
+
end
|
167
|
+
names
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
class ASAConsole
|
3
|
+
# Parent class for all other {ASAConsole} exceptions.
|
4
|
+
class Exception < ::RuntimeError; end
|
5
|
+
|
6
|
+
# Container for exceptions to avoid cluttering the namespace.
|
7
|
+
module Error
|
8
|
+
# Raised when an unexpected "ERROR: [...]" message is received.
|
9
|
+
class CommandError < Exception; end
|
10
|
+
|
11
|
+
# Raised when attempting to execute a configuration command in the wrong
|
12
|
+
# config mode or submode.
|
13
|
+
class ConfigModeError < Exception; end
|
14
|
+
|
15
|
+
# Any type of connection failure.
|
16
|
+
class ConnectFailure < Exception; end
|
17
|
+
|
18
|
+
# Raised when a terminal object times out waiting for an expected prompt.
|
19
|
+
class ExpectedPromptFailure < Exception; end
|
20
|
+
|
21
|
+
# Raised when checking an ASA version against an unsupported expression.
|
22
|
+
class InvalidExpressionError < Exception; end
|
23
|
+
|
24
|
+
# Raised when a required option hash entry is missing.
|
25
|
+
class MissingOptionError < Exception; end
|
26
|
+
|
27
|
+
# Raised when attempting to execute a command on a disconnected terminal.
|
28
|
+
class NotConnectedError < Exception; end
|
29
|
+
|
30
|
+
# Raised (by default) when a configuration command generates output instead
|
31
|
+
# of just presenting the next config prompt.
|
32
|
+
class UnexpectedOutputError < Exception; end
|
33
|
+
|
34
|
+
# Raised when there is a failure parsing the appliance version string.
|
35
|
+
class VersionParseError < Exception; end
|
36
|
+
|
37
|
+
# A {ConnectFailure} caused by invalid credentials.
|
38
|
+
class AuthenticationFailure < ConnectFailure; end
|
39
|
+
|
40
|
+
# A {ConnectFailure} caused by a timeout.
|
41
|
+
class ConnectionTimeoutError < ConnectFailure; end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
|
2
|
+
require 'asa_console/terminal/ssh'
|
3
|
+
|
4
|
+
class ASAConsole
|
5
|
+
module Terminal
|
6
|
+
#
|
7
|
+
# A subclass of {SSH} that overrides the {SSH#connect} method to generate
|
8
|
+
# stub objects for RSpec testing.
|
9
|
+
#
|
10
|
+
# The constructor takes the same hash options as {SSH#initialize}, but
|
11
|
+
# requires one additional option; `:input_proc`, a proc that takes two
|
12
|
+
# arguments:
|
13
|
+
# * `input` -- A command or other input that is being passed to the
|
14
|
+
# simulated appliance.
|
15
|
+
# * `prompt` -- Last prompt displayed on the terminal when the `input`
|
16
|
+
# text was entered.
|
17
|
+
#
|
18
|
+
# The proc is expected to return an array with three elements:
|
19
|
+
# * `output` -- Result of sending `input` to the simulated appliance.
|
20
|
+
# * `prompt` -- Next prompt to present within the session.
|
21
|
+
# * `disconnect` -- `true` to indicate that the SSH connection has been
|
22
|
+
# closed (e.g. if "exit" was sent to the terminal), or `false`
|
23
|
+
# otherwise.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# input_proc = proc do |input|
|
27
|
+
# prompt = 'TEST# '
|
28
|
+
# case input
|
29
|
+
# when "terminal pager lines 0\n", nil
|
30
|
+
# output = prompt
|
31
|
+
# disconnect = false
|
32
|
+
# when "exit\n"
|
33
|
+
# output = "\nLogoff\n\n"
|
34
|
+
# disconnect = true
|
35
|
+
# else
|
36
|
+
# output = "ERROR: Command not implemented\n" + prompt
|
37
|
+
# disconnect = false
|
38
|
+
# end
|
39
|
+
# [output, prompt, disconnect]
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# asa = ASAConsole.fake_ssh(
|
43
|
+
# host: 'ignored',
|
44
|
+
# user: 'ignored',
|
45
|
+
# input_proc: input_proc
|
46
|
+
# )
|
47
|
+
# asa.connect
|
48
|
+
#
|
49
|
+
# # This will raise an error
|
50
|
+
# asa.priv_exec('write memory')
|
51
|
+
#
|
52
|
+
# @api development
|
53
|
+
class FakeSSH < SSH
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
attr_accessor :raw_buffer
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
attr_accessor :raw_session_log
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
attr_accessor :session
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
attr_accessor :channel
|
66
|
+
#
|
67
|
+
# @api private
|
68
|
+
attr_accessor :last_output_received
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
attr_accessor :input_proc
|
72
|
+
|
73
|
+
# Wrapper for the {SSH} class constructor that extracts `:input_proc` from
|
74
|
+
# the options hash and passes the rest to the parent.
|
75
|
+
#
|
76
|
+
# @option opts [Proc] :input_proc
|
77
|
+
# @option opts ... options for the {SSH} constructor
|
78
|
+
# @see SSH#initialize SSH constructor
|
79
|
+
def initialize(opts)
|
80
|
+
@input_proc = opts.delete(:input_proc)
|
81
|
+
fail Error::MissingOptionError, 'Option :input_proc is missing or invalid' \
|
82
|
+
unless @input_proc.is_a? Proc
|
83
|
+
super(opts)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Connect to a simulated appliance. This method creates anonymous stub
|
87
|
+
# classes that take the place of `Net::SSH::Connection::Session` and
|
88
|
+
# `Net::SSH::Connection::Channel`. The stub classes implement only the
|
89
|
+
# subset of methods that are used by the {SSH} terminal class.
|
90
|
+
#
|
91
|
+
# @return [void]
|
92
|
+
def connect
|
93
|
+
@session = Class.new do
|
94
|
+
def initialize
|
95
|
+
@closed = false
|
96
|
+
end
|
97
|
+
def close
|
98
|
+
@closed = true
|
99
|
+
end
|
100
|
+
def closed?
|
101
|
+
@closed ? true : false
|
102
|
+
end
|
103
|
+
end.new
|
104
|
+
|
105
|
+
@channel = Class.new do
|
106
|
+
attr_reader :terminal
|
107
|
+
def initialize(terminal)
|
108
|
+
@terminal = terminal
|
109
|
+
@input_buffer = ''
|
110
|
+
@output_buffer, @prompt = @terminal.input_proc.call
|
111
|
+
end
|
112
|
+
def connection
|
113
|
+
self
|
114
|
+
end
|
115
|
+
def process(timeout)
|
116
|
+
sleep timeout
|
117
|
+
@terminal.raw_buffer = @output_buffer
|
118
|
+
@terminal.raw_session_log << @output_buffer
|
119
|
+
@output_buffer = ''
|
120
|
+
fail Net::SSH::Disconnect if @terminal.session.closed?
|
121
|
+
end
|
122
|
+
def send_data(input)
|
123
|
+
@input_buffer << input
|
124
|
+
return unless @input_buffer.end_with? "\n"
|
125
|
+
@output_buffer << @input_buffer
|
126
|
+
output, prompt, disconnect = @terminal.input_proc.call(@input_buffer, @prompt)
|
127
|
+
@output_buffer << output
|
128
|
+
@prompt = prompt
|
129
|
+
@terminal.session.close if disconnect
|
130
|
+
@input_buffer = ''
|
131
|
+
@terminal.last_output_received = Time.now.getlocal
|
132
|
+
end
|
133
|
+
end.new(self)
|
134
|
+
|
135
|
+
@connected = true
|
136
|
+
|
137
|
+
# This is copied verbatim from the parent #connect method
|
138
|
+
expect(ANY_EXEC_PROMPT) do |success, output|
|
139
|
+
@on_output_callbacks.each { |c| c.call(nil, nil, output) }
|
140
|
+
fail Error::ConnectFailure 'Failed to parse EXEC prompt', self unless success
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
|
2
|
+
require 'net/ssh'
|
3
|
+
require 'asa_console/error'
|
4
|
+
require 'asa_console/terminal'
|
5
|
+
require 'asa_console/util'
|
6
|
+
|
7
|
+
class ASAConsole
|
8
|
+
module Terminal
|
9
|
+
#
|
10
|
+
# An SSH terminal session.
|
11
|
+
#
|
12
|
+
# @attr host [String]
|
13
|
+
# Hostname or IP address.
|
14
|
+
# @attr user [String]
|
15
|
+
# SSH username.
|
16
|
+
# @attr password [String]
|
17
|
+
# SSH password.
|
18
|
+
# @attr ssh_opts [Hash]
|
19
|
+
# Option hash passed to `Net::SSH::start`.
|
20
|
+
# @attr command_timeout [Numeric]
|
21
|
+
# Maximum time to wait for a command to execute.
|
22
|
+
# @attr connect_timeout [Numeric]
|
23
|
+
# SSH connection timeout. (`:timeout` option passed to `Net::SSH::start`)
|
24
|
+
# @attr_reader prompt [String, nil]
|
25
|
+
# Prompt currently displayed on the terminal or `nil` if not connected.
|
26
|
+
#
|
27
|
+
class SSH
|
28
|
+
DEFAULT_CONNECT_TIMEOUT = 5
|
29
|
+
DEFAULT_COMMAND_TIMEOUT = 5
|
30
|
+
|
31
|
+
attr_accessor :host
|
32
|
+
attr_accessor :user
|
33
|
+
attr_accessor :ssh_opts
|
34
|
+
attr_accessor :command_timeout
|
35
|
+
|
36
|
+
attr_reader :prompt
|
37
|
+
|
38
|
+
# @see Terminal
|
39
|
+
# @option opts [String] :host
|
40
|
+
# @option opts [String] :user
|
41
|
+
# @option opts [String] :password
|
42
|
+
# @option opts [Numeric] :connect_timeout
|
43
|
+
# @option opts [Numeric] :command_timeout
|
44
|
+
def initialize(opts)
|
45
|
+
fail Error::MissingOptionError, 'Option :host is missing' unless opts[:host]
|
46
|
+
fail Error::MissingOptionError, 'Option :user is missing' unless opts[:user]
|
47
|
+
|
48
|
+
@host = opts[:host]
|
49
|
+
@user = opts[:user]
|
50
|
+
@ssh_opts = {
|
51
|
+
timeout: opts.fetch(:connect_timeout, DEFAULT_CONNECT_TIMEOUT),
|
52
|
+
number_of_password_prompts: 0 # Avoid prompting for password on authentication failure
|
53
|
+
}
|
54
|
+
self.password = opts[:password]
|
55
|
+
@command_timeout = opts.fetch(:command_timeout, DEFAULT_COMMAND_TIMEOUT)
|
56
|
+
@prompt = nil
|
57
|
+
|
58
|
+
@raw_buffer = ''
|
59
|
+
@raw_session_log = ''
|
60
|
+
@connected = false
|
61
|
+
@channel = nil
|
62
|
+
@session = nil
|
63
|
+
@last_output_received = nil
|
64
|
+
@on_output_callbacks = []
|
65
|
+
end
|
66
|
+
|
67
|
+
def password
|
68
|
+
@ssh_opts[:password]
|
69
|
+
end
|
70
|
+
|
71
|
+
def password=(str)
|
72
|
+
@ssh_opts[:password] = str
|
73
|
+
@ssh_opts[:auth_methods] = ['password'] if str
|
74
|
+
end
|
75
|
+
|
76
|
+
def connect_timeout
|
77
|
+
@ssh_opts[:timeout]
|
78
|
+
end
|
79
|
+
|
80
|
+
def connect_timeout=(timeout)
|
81
|
+
@ssh_opts[:timeout] = timeout
|
82
|
+
end
|
83
|
+
|
84
|
+
# Start an SSH session and send a remote shell request. The method blocks
|
85
|
+
# until an EXEC prompt is received or a timeout is reached.
|
86
|
+
#
|
87
|
+
# @see https://tools.ietf.org/html/rfc4254 RFC 4254
|
88
|
+
# @raise [Error::ConnectFailure] for all error types
|
89
|
+
# @raise [Error::AuthenticationFailure]
|
90
|
+
# subclass of {Error::ConnectFailure}
|
91
|
+
# @raise [Error::ConnectionTimeoutError]
|
92
|
+
# subclass of {Error::ConnectFailure}
|
93
|
+
# @return [void]
|
94
|
+
def connect
|
95
|
+
Net::SSH.start(@host, @user, @ssh_opts) do |session|
|
96
|
+
@session = session
|
97
|
+
@session.open_channel do |channel|
|
98
|
+
channel.send_channel_request('shell') do |ch, ch_success|
|
99
|
+
fail Error::ConnectFailure, 'Failed to start remote shell' unless ch_success
|
100
|
+
@connected = true
|
101
|
+
@channel = ch
|
102
|
+
@channel.on_data do |_ch, data|
|
103
|
+
@last_output_received = Time.now.getlocal
|
104
|
+
@raw_session_log << data
|
105
|
+
@raw_buffer << data
|
106
|
+
end
|
107
|
+
@channel.on_close do
|
108
|
+
@connected = false
|
109
|
+
end
|
110
|
+
expect(ANY_EXEC_PROMPT) do |success, output|
|
111
|
+
@on_output_callbacks.each { |c| c.call(nil, nil, output) }
|
112
|
+
fail Error::ConnectFailure, 'Failed to parse EXEC prompt', self unless success
|
113
|
+
end
|
114
|
+
return # Workaround for Net::SSH limitations borrowed from Puppet
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
rescue Timeout::Error
|
119
|
+
raise Error::ConnectionTimeoutError, "Timeout connecting to #{@host}"
|
120
|
+
rescue Net::SSH::AuthenticationFailed
|
121
|
+
raise Error::AuthenticationFailure, "Authentication failed for #{@user}@#{@host}"
|
122
|
+
rescue SystemCallError, SocketError => e
|
123
|
+
raise Error::ConnectFailure, "#{e.class}: #{e.message}"
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Boolean]
|
127
|
+
def connected?
|
128
|
+
@connected = false if @session.nil? || @session.closed?
|
129
|
+
@connected
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [void]
|
133
|
+
def disconnect
|
134
|
+
@session.close if connected?
|
135
|
+
rescue Net::SSH::Disconnect
|
136
|
+
@session = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
# Send a line of text to the console and block until the expected prompt
|
140
|
+
# is seen in the output or a timeout is reached.
|
141
|
+
#
|
142
|
+
# @note
|
143
|
+
# Special characters are not escaped by this method. Use the
|
144
|
+
# {ASAConsole} wrapper for unescaped text.
|
145
|
+
#
|
146
|
+
# @see ASAConsole#send ASAConsole wrapper for this method
|
147
|
+
# @param line [String]
|
148
|
+
# @param expect_regex [Regexp]
|
149
|
+
# @param is_password [Boolean]
|
150
|
+
# @yieldparam success [Boolean]
|
151
|
+
# @yieldparam output [String]
|
152
|
+
# @return [void]
|
153
|
+
def send(line, expect_regex, is_password = false)
|
154
|
+
last_prompt = @prompt
|
155
|
+
@channel.send_data "#{line}\n" if connected?
|
156
|
+
input = (is_password ? '*' * line.length : line) + "\n"
|
157
|
+
expect(expect_regex) do |success, output|
|
158
|
+
output = output.sub(/^[^\n]*\n/m, '') # Remove echoed input
|
159
|
+
@on_output_callbacks.each { |c| c.call(last_prompt, input, output) }
|
160
|
+
yield(success, output)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Register a proc to be called whenever the {#send} method finishes
|
165
|
+
# processing a transaction (whether successful or not).
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# @command_log = []
|
169
|
+
# asa.terminal.on_output do |prompt, command, output|
|
170
|
+
# if prompt && prompt !~ ASAConsole::PASSWORD_PROMPT
|
171
|
+
# @command_log << command
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# @yieldparam prompt [String]
|
176
|
+
# @yieldparam command [String]
|
177
|
+
# @yieldparam output [String]
|
178
|
+
# @return [void]
|
179
|
+
def on_output(&block)
|
180
|
+
@on_output_callbacks << block
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return [String] a complete log of the SSH session
|
184
|
+
def session_log
|
185
|
+
Util.apply_control_chars(@raw_session_log)
|
186
|
+
end
|
187
|
+
|
188
|
+
def buffer
|
189
|
+
Util.apply_control_chars(@raw_buffer)
|
190
|
+
end
|
191
|
+
protected :buffer
|
192
|
+
|
193
|
+
def expect(prompt_regex)
|
194
|
+
@last_output_received = Time.now.getlocal
|
195
|
+
|
196
|
+
while buffer !~ prompt_regex
|
197
|
+
begin
|
198
|
+
@channel.connection.process(0.1)
|
199
|
+
break if Time.now.getlocal - @last_output_received > @command_timeout
|
200
|
+
rescue Net::SSH::Disconnect, IOError
|
201
|
+
@connected = false
|
202
|
+
break
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
matches = prompt_regex.match(buffer)
|
207
|
+
if matches.nil?
|
208
|
+
success = false
|
209
|
+
@prompt = nil
|
210
|
+
else
|
211
|
+
success = true
|
212
|
+
@prompt = matches[0]
|
213
|
+
end
|
214
|
+
|
215
|
+
output = buffer.sub(prompt_regex, '')
|
216
|
+
@raw_buffer = ''
|
217
|
+
|
218
|
+
yield(success, output)
|
219
|
+
end
|
220
|
+
protected :expect
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|