asa_console 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Yzc1YjUzZDY2YWUwMjY2Y2IyZTM2YTcyMDQ2M2M2MjE4MDVmYmNkYw==
5
+ data.tar.gz: !binary |-
6
+ ZGFkZjI4YjZlNTc3OTExYmY0YTA2MGFmYjc2YWVlZDk3Nzk3NzFlMw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MTRhYjE5YzE5ZDhiYjQzZWJmNGM2NTM0ZDA3YTdjNzczM2VkZjYyYTc4ZDJm
10
+ ZTFiN2I2MjAzZTUxNTYzNDUxYzU1N2I5MGJlNTZhNThjNWJkYmRlNzk4NzQz
11
+ M2I1YWExNGExZWE3YzZhZjM2YTQ2MmQyNWYzZDQ2NjQyMTU0OWU=
12
+ data.tar.gz: !binary |-
13
+ MTE2YTQ4NTJjZGUzMTU2MzkyZWY4YTVhMGVlOGM3YzhhMTQzMDUzOGViZTIz
14
+ YzY0MzMzNzFhMjJjOTY2M2JkMGIzYmZiOWRhZGZlYWRjYTI4NDlhYjQ5NmQy
15
+ MWM1MjFmNDkwYmUwMjMzNGQ0ODZkMmY4ZDU1M2Y4ZTgzODVmN2Y=
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --default-return void
2
+ --markup markdown
3
+ --no-api
4
+ --no-cache
5
+ --no-private
6
+ -
7
+ LICENSE.md
8
+ script/*
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright (c) 2015 Henry Goodman
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ [![Build Status](https://travis-ci.org/hgoodman/asa-console.svg?branch=master)](https://travis-ci.org/hgoodman/asa-console)
2
+ [![Code Climate](https://codeclimate.com/github/hgoodman/asa-console/badges/gpa.svg)](https://codeclimate.com/github/hgoodman/asa-console)
3
+ [![Test Coverage](https://codeclimate.com/github/hgoodman/asa-console/badges/coverage.svg)](https://codeclimate.com/github/hgoodman/asa-console/coverage)
4
+ [![Gem Version](https://badge.fury.io/rb/asa_console.svg)](https://badge.fury.io/rb/asa_console)
5
+ [![YARD Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/hgoodman/asa-console/)
6
+
7
+ ASAConsole
8
+ ==========
9
+
10
+ A ruby gem for Cisco ASA management via an interactive terminal session.
11
+
12
+ This gem lets a program interact with a Cisco ASA using CLI commands. It includes a minimal set of functions for issuing commands and parsing the results.
13
+
14
+ Caveats
15
+ =======
16
+
17
+ Most people would be better off using [Cisco's official REST API](http://www.cisco.com/c/en/us/td/docs/security/asa/api/qsg-asa-api.html) plugin for the ASA platform. This gem does not use the supported API. It was developed as an academic pursuit and may not be suitable for your environment. It is distributed under the [MIT License](LICENSE.md), which is to say that it comes with no warranty of any kind.
18
+
19
+ That being said, you might find it useful if you are working with older hardware or if you have other special requirements. The official REST API plugin is only supported on 5500-X, ASAv and newer platforms.
20
+
21
+ For the time being, direct SSH is the only transport method implemented by this gem although it could easily be extended to support alternatives like using a serial console or a jump box.
22
+
23
+ Getting Started
24
+ ===============
25
+
26
+ The easiest way to get started is to browse through the test files in the [script](script/) folder. To supplement its automated tests, this gem provides a framework for live testing against devices in a lab environment. There are several canned test scripts that demonstrate different features of the library.
27
+
28
+ Each script executes a series of commands declared in a block as show below. The test runner displays output as it would appear in an SSH session and adds color to indicate how the output is being parsed. Informational messages can be added to the output with the `log` method.
29
+
30
+ ```ruby
31
+ ASAConsole::Test.script do |asa|
32
+ log 'Connecting...'
33
+ asa.connect
34
+ if asa.version? '>= 9.4(1)'
35
+ asa.priv_exec 'no terminal interactive'
36
+ else
37
+ log 'The "no terminal interactive" command is not supported'
38
+ end
39
+ log 'Disconnecting...'
40
+ asa.disconnect
41
+ end
42
+ ```
43
+
44
+ The included test scripts are designed to be non-invasive and to leave the device configuration in its original state. Nevertheless, running them in a production environment is not recommended.
45
+
46
+ Command Line Utility
47
+ --------------------
48
+
49
+ **Usage**
50
+
51
+ asatest [options] <testname> [asaname]
52
+
53
+ **Examples**
54
+
55
+ List command line options and available tests:
56
+
57
+ asatest --help
58
+
59
+ Execute a canned test:
60
+
61
+ asatest connect
62
+
63
+ Load a custom test file:
64
+
65
+ asatest -f ./my_test_file.rb
66
+
67
+ Configuration Files
68
+ -------------------
69
+
70
+ The `asatest` executable will read a list of default command line options from the file `~/.asa-console/test_options.yaml`. Here is an example of the file format:
71
+
72
+ ```yaml
73
+ ---
74
+ show-session-log: true
75
+ color: light
76
+ ```
77
+
78
+ Each key matches a long-form command line option with the leading "--" removed. Run the program with "--help" for a complete list.
79
+
80
+ Device information is needed for running live tests. By default, the program will look for appliance information in `~/.asa-console/test_appliances.yaml`. Here is an example of the file format:
81
+
82
+ ```yaml
83
+ ---
84
+ default_appliance: firewall002
85
+ appliances:
86
+ firewall001:
87
+ terminal_opts:
88
+ host: 10.7.7.1
89
+ connect_timeout: 20
90
+ firewall002:
91
+ terminal_opts:
92
+ host: 10.7.7.254
93
+ user: testuser
94
+ password: execpass
95
+ enable_password: enablepass
96
+ ```
97
+
98
+ If any of the following options are not found in this file, the user will be prompted to enter values for them.
99
+
100
+ - `terminal_opts[host]`
101
+ - `terminal_opts[user]`
102
+ - `terminal_opts[password]`
103
+ - `enable_password`
104
+
105
+ The enable password can be omitted if it is the same as the terminal password or if it is otherwise not needed.
106
+
107
+ API Documentation
108
+ =================
109
+
110
+ You can view online documentation at [rubydoc.info](http://www.rubydoc.info/github/hgoodman/asa-console/) or generate it yourself with:
111
+
112
+ rake yardoc
113
+
114
+ To include documentation for objects used in testing and development:
115
+
116
+ rake yardoc_dev
@@ -0,0 +1,30 @@
1
+
2
+ Gem::Specification.new do |s|
3
+ s.name = 'asa_console'
4
+ s.version = '0.1.2'
5
+ s.summary = 'Cisco ASA management via an interactive terminal session'
6
+ s.description = 'This gem lets a program interact with a Cisco ASA using CLI
7
+ commands. It includes a minimal set of functions for issuing commands and
8
+ parsing the results.'.gsub(/\s+/, ' ')
9
+ s.author = 'Henry Goodman'
10
+ s.email = 'github@henrygoodman.com'
11
+ s.homepage = 'https://github.com/hgoodman/asa-console/'
12
+ s.license = 'MIT'
13
+ s.has_rdoc = 'yard'
14
+ s.files = Dir[__FILE__, 'lib/**/*', 'script/*', '*.md', '.yardopts']
15
+ s.executables = ['asatest']
16
+
17
+ s.add_runtime_dependency('net-ssh', '~> 2.9.2')
18
+ s.add_runtime_dependency('highline', '~> 1.7')
19
+
20
+ s.add_development_dependency('bundler', '~> 1.7')
21
+ s.add_development_dependency('rake', '~> 10.4')
22
+ s.add_development_dependency('redcarpet', '~> 3.3')
23
+ s.add_development_dependency('rspec', '~> 3.4')
24
+ s.add_development_dependency('rubocop', '~> 0.35')
25
+ s.add_development_dependency('simplecov', '~> 0.11')
26
+ s.add_development_dependency('thor', '~> 0.19')
27
+ s.add_development_dependency('yard', '~> 0.8')
28
+
29
+ s.required_ruby_version = '>=1.9.3'
30
+ end
data/bin/asatest ADDED
@@ -0,0 +1,113 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'getoptlong'
5
+ require 'highline/import'
6
+ require 'asa_console/test'
7
+
8
+ DEFAULT_OPTION_FILE = File.join(Dir.home, '.asa-console', 'test_options.yaml')
9
+ DEFAULT_APPLIANCE_FILE = File.join(Dir.home, '.asa-console', 'test_appliances.yaml')
10
+ DEFAULT_COLOR = :dark
11
+
12
+ def usage
13
+ puts %(
14
+ Usage:
15
+ #{$PROGRAM_NAME} [options] <testname> [asaname]
16
+
17
+ Options:
18
+ -h, --help Display this message
19
+ -f, --file Interpret <testname> as an explicit file name
20
+ -o, --option-file <file> YAML file with command line defaults for this utility
21
+ -a, --appliance-file <file> YAML file with ASA device list and terminal options
22
+ -s, --show-session-log Include the full session log in the command output
23
+ -c, --color (light|dark|off) Colorize output using light or dark colors
24
+
25
+ Defaults:
26
+ --option-file #{DEFAULT_OPTION_FILE}
27
+ --appliance-file #{DEFAULT_APPLIANCE_FILE}
28
+ --color #{DEFAULT_COLOR}
29
+
30
+ Available Tests:
31
+ #{ASAConsole::Test.test_names.join("\n ")}
32
+ ).gsub(/^ /, '')
33
+ puts
34
+ exit 1
35
+ end
36
+
37
+ cli_opts = {}
38
+
39
+ begin
40
+ cli = GetoptLong.new(
41
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
42
+ [ '--file', '-f', GetoptLong::NO_ARGUMENT ],
43
+ [ '--option-file', '-o', GetoptLong::REQUIRED_ARGUMENT ],
44
+ [ '--appliance-file', '-a', GetoptLong::REQUIRED_ARGUMENT ],
45
+ [ '--show-session-log', '-s', GetoptLong::NO_ARGUMENT ],
46
+ [ '--color', '-c', GetoptLong::REQUIRED_ARGUMENT ]
47
+ )
48
+ cli.each do |opt, arg|
49
+ cli_opts[opt.sub(/^--/, '')] = (arg == '' ? true : arg)
50
+ end
51
+ rescue GetoptLong::MissingArgument
52
+ usage
53
+ end
54
+
55
+ help = false
56
+ is_file = false
57
+ option_file = DEFAULT_OPTION_FILE
58
+ appliance_file = DEFAULT_APPLIANCE_FILE
59
+ show_session_log = false
60
+ color = DEFAULT_COLOR
61
+
62
+ option_file = cli_opts['option-file'] if cli_opts['option-file']
63
+ if File.readable? option_file
64
+ cli_opts = YAML.load_file(option_file).merge(cli_opts)
65
+ end
66
+
67
+ cli_opts.each do |opt, arg|
68
+ case opt
69
+ when 'help' then help = arg
70
+ when 'file' then is_file = arg
71
+ when 'appliance-file' then appliance_file = arg
72
+ when 'show-session-log' then show_session_log = arg
73
+ when 'color' then color = arg.to_sym
74
+ end
75
+ end
76
+
77
+ test_name = ARGV.shift if ARGV.length > 0
78
+ asa_name = ARGV.shift if ARGV.length > 0
79
+
80
+ ASAConsole::Test.color_scheme = color
81
+
82
+ enable_password = nil
83
+
84
+ if File.readable? appliance_file
85
+ config = YAML.load_file(appliance_file)
86
+ asa_name ||= config['default_appliance']
87
+ if asa_name
88
+ if config['appliances'][asa_name].is_a?(Hash)
89
+ terminal_opts = config['appliances'][asa_name]['terminal_opts']
90
+ enable_password = config['appliances'][asa_name]['enable_password']
91
+ else
92
+ puts "Error: Configuration missing for #{asa_name.dump}"
93
+ exit 1
94
+ end
95
+ end
96
+ end
97
+
98
+ usage if help || test_name.nil?
99
+
100
+ test_file = is_file ? File.expand_path(test_name) : ASAConsole::Test.test_path(test_name)
101
+
102
+ terminal_opts ||= {}
103
+ terminal_opts.keys.each { |key| terminal_opts[key.to_sym] = terminal_opts.delete(key) }
104
+ terminal_opts[:host] = ask 'Host Name: ' unless terminal_opts[:host]
105
+ terminal_opts[:user] = ask 'User Name: ' unless terminal_opts[:user]
106
+ terminal_opts[:password] = ask('Password: ') { |q| q.echo = '*' } unless terminal_opts[:password]
107
+
108
+ unless enable_password
109
+ pw = ask('Enable Password [ENTER for none]: ') { |q| q.echo = '*' }
110
+ enable_password = pw if pw.length > 0
111
+ end
112
+
113
+ exit 1 unless ASAConsole::Test.start(test_file, terminal_opts, enable_password, show_session_log)
@@ -0,0 +1,283 @@
1
+
2
+ require 'asa_console/config'
3
+ require 'asa_console/error'
4
+ require 'asa_console/terminal/fake_ssh'
5
+ require 'asa_console/terminal/ssh'
6
+
7
+ #
8
+ # Command line interface to a Cisco ASA.
9
+ #
10
+ # @example
11
+ # # Create an instance of ASAConsole for an SSH connection
12
+ # asa = ASAConsole.ssh(
13
+ # host: 'fw01.example.com', # Required by Net::SSH
14
+ # user: 'admin', # Required by Net::SSH
15
+ # password: 'mypass', # Optional in case you want to use SSH keys
16
+ # connect_timeout: 3, # Converted to the Net::SSH :timeout value
17
+ # command_timeout: 10, # How long to wait for a command to execute?
18
+ # enable_password: 'secret' # Optional if it's the same as :password
19
+ # )
20
+ #
21
+ # # Other Net::SSH options can be set before connecting
22
+ # asa.terminal.ssh_opts[:port] = 2022
23
+ #
24
+ # asa.connect
25
+ # puts asa.show('version')
26
+ # asa.disconnect
27
+ #
28
+ # @attr_reader config_mode [String, nil]
29
+ # Configuration submode string (e.g. "config", "config-if") or `nil` if not
30
+ # in a configuration mode
31
+ # @attr terminal [Object]
32
+ # An instance of a class from the {Terminal} module
33
+ # @attr enable_password [String, nil]
34
+ # Enable password or `nil` if the enble password is not required
35
+ #
36
+ class ASAConsole
37
+ attr_reader :config_mode
38
+ attr_accessor :terminal
39
+ attr_accessor :enable_password
40
+
41
+ PASSWORD_PROMPT = /^Password: +\z/
42
+ EXEC_PROMPT = /^[\w\.\/-]+> +\z/
43
+ PRIV_EXEC_PROMPT = /^[\w\.\/-]+(?:\(config[\s\w-]*\))?# +\z/
44
+ ANY_EXEC_PROMPT = /^[\w\.\/-]+(?:(?:\(config[\s\w-]*\))?#|>) +\z/
45
+ CONFIG_PROMPT = /^[\w\.\/-]+\(config[\s\w-]*\)# +\z/
46
+ INVALID_CMD_CHAR = /[^\x20-\x3e\x40-\x7e]/
47
+ CONFIG_MODE_REGEX = /^[\w\.\-\/]+\((config[\s\w\-]*)\)# $/
48
+ CMD_ERROR_REGEX = /^ERROR: (?:% )?(.*)/
49
+
50
+ class << self
51
+ # Factory method for a fake SSH console session.
52
+ #
53
+ # @api development
54
+ # @see Terminal::FakeSSH#initialize Terminal::FakeSSH constructor
55
+ # @option opts ... options for the {Terminal::FakeSSH} constructor
56
+ # @option opts [String] :enable_password
57
+ # @return [ASAConsole]
58
+ def fake_ssh(opts)
59
+ enable_password = opts.delete(:enable_password)
60
+ terminal = Terminal::FakeSSH.new(opts)
61
+ new terminal, enable_password
62
+ end
63
+
64
+ # Factory method for an SSH console session.
65
+ #
66
+ # @see Terminal::SSH#initialize Terminal::SSH constructor
67
+ # @option opts ... options for the {Terminal::SSH} constructor
68
+ # @option opts [String] :enable_password
69
+ # @return [ASAConsole]
70
+ def ssh(opts)
71
+ enable_password = opts.delete(:enable_password)
72
+ terminal = Terminal::SSH.new(opts)
73
+ new terminal, enable_password
74
+ end
75
+
76
+ private :new
77
+ end
78
+
79
+ # @private
80
+ def initialize(terminal, enable_password = nil)
81
+ @config_mode = nil
82
+ @terminal = terminal
83
+ @terminal.on_output do
84
+ matches = CONFIG_MODE_REGEX.match(@terminal.prompt)
85
+ @config_mode = matches ? matches[1] : nil
86
+ end
87
+ @enable_password = enable_password
88
+ @version = nil
89
+ @running_config = {}
90
+ end
91
+
92
+ # @return [void]
93
+ def connect
94
+ @terminal.connect
95
+ priv_exec! 'terminal pager lines 0'
96
+ end
97
+
98
+ # @return [Boolean]
99
+ def connected?
100
+ @terminal.connected?
101
+ end
102
+
103
+ # @return [void]
104
+ def disconnect
105
+ while @terminal.connected? && @terminal.prompt =~ ANY_EXEC_PROMPT
106
+ @terminal.send('exit', ANY_EXEC_PROMPT) { |success| break unless success }
107
+ end
108
+ @terminal.disconnect
109
+ end
110
+
111
+ # Send a line of text to the console and block until the expected prompt is
112
+ # seen in the output or a timeout is reached. Raises an exception if the
113
+ # expected prompt has not been received after waiting for `command_timeout`
114
+ # seconds following the last data transfer.
115
+ #
116
+ # @param line [String]
117
+ # @param expect_regex [Regexp]
118
+ # @param is_password [Boolean]
119
+ # if `true`, `line` will be masked with asterisks in the session log
120
+ # @raise [Error::ExpectedPromptFailure]
121
+ # @return [String] output from the command, if any
122
+ def send(line, expect_regex, is_password = false)
123
+ must_be_connected!
124
+ line = line.gsub(INVALID_CMD_CHAR, "\x16\\0") unless is_password
125
+ @terminal.send(line, expect_regex, is_password) do |success, output|
126
+ fail Error::ExpectedPromptFailure, "Expected prompt not found in output: #{output}" unless success
127
+ output
128
+ end
129
+ end
130
+
131
+ # Execute a command in any configuration mode.
132
+ #
133
+ # @param command [String]
134
+ # @option opts [Regexp] :expect_prompt
135
+ # prompt expected after executing the command
136
+ # @option opts [Boolean] :ignore_output
137
+ # if `true`, do not raise an error when the command generates output (unless
138
+ # the output is an error message)
139
+ # @option opts [Boolean] :ignore_errors
140
+ # if `true`, ignore error messages in the output (implies `:ignore_output`)
141
+ # @option opts [Boolean] :require_config_mode
142
+ # a specific configuration submode required to execute the command
143
+ # @return [String] output from the command, if any
144
+ def config_exec(command, opts = {})
145
+ must_be_connected!
146
+ enable! if @terminal.prompt =~ EXEC_PROMPT
147
+
148
+ expect_prompt = opts.fetch(:expect_prompt, CONFIG_PROMPT)
149
+ ignore_output = opts.fetch(:ignore_output, false)
150
+ ignore_errors = opts.fetch(:ignore_errors, false)
151
+ require_config_mode = opts[:require_config_mode]
152
+
153
+ unless @terminal.prompt =~ CONFIG_PROMPT
154
+ send('configure terminal', CONFIG_PROMPT)
155
+ end
156
+
157
+ if require_config_mode && @config_mode != require_config_mode
158
+ message = "Will not execute command in '%s' mode (expected '%s')"
159
+ fail Error::ConfigModeError, message % [ @config_mode, require_config_mode ]
160
+ end
161
+
162
+ # Any part of the config may change, so clear the cache.
163
+ @running_config = {}
164
+
165
+ output = send(command, expect_prompt)
166
+
167
+ unless ignore_errors
168
+ error = CMD_ERROR_REGEX.match(output)
169
+ fail Error::CommandError, "Error output after executing '#{command}': #{error[1]}" if error
170
+ unless ignore_output || output.empty?
171
+ fail Error::UnexpectedOutputError, "Unexpected output after executing '#{command}': #{output}"
172
+ end
173
+ end
174
+
175
+ output
176
+ end
177
+
178
+ # Execute a command from top-level configuration mode. This method is a
179
+ # wrapper for {#config_exec}.
180
+ #
181
+ # @see #config_exec
182
+ # @param command [String]
183
+ # @param opts [Hash] options for {#config_exec}
184
+ # @return [String] output from the command, if any
185
+ def config_exec!(command, opts = {})
186
+ must_be_connected!
187
+ send('exit', CONFIG_PROMPT) while @config_mode && @config_mode != 'config'
188
+ config_exec(command, opts)
189
+ end
190
+
191
+ # Execute a command in any privileged EXEC mode (includes config EXEC modes).
192
+ #
193
+ # @param command [String]
194
+ # @return [String] output from the command, if any
195
+ def priv_exec(command)
196
+ must_be_connected!
197
+ enable! if @terminal.prompt =~ EXEC_PROMPT
198
+ output = send(command, PRIV_EXEC_PROMPT)
199
+ error = CMD_ERROR_REGEX.match(output)
200
+ fail Error::CommandError, "Error output after executing '#{command}': #{error[1]}" if error
201
+ output
202
+ end
203
+
204
+ # Execute a command in privileged EXEC mode (excludes config EXEC modes). This
205
+ # method is a wrapper for {#priv_exec}.
206
+ #
207
+ # @see #priv_exec
208
+ # @param command [String]
209
+ # @return [String] output from the command, if any
210
+ def priv_exec!(command)
211
+ must_be_connected!
212
+ send('exit', PRIV_EXEC_PROMPT) while @config_mode
213
+ priv_exec(command)
214
+ end
215
+
216
+ # A shortcut for running "show" commands.
217
+ #
218
+ # @param subcmd [String]
219
+ # @return [String]
220
+ def show(subcmd)
221
+ priv_exec('show ' + subcmd)
222
+ end
223
+
224
+ # Execute a "show running-conifg [...]" command and load the results into a
225
+ # {Config} object.
226
+ #
227
+ # @see Config
228
+ # @param subcmd [String, nil]
229
+ # @return [Config] a top-level {Config} node with nested config
230
+ def running_config(subcmd = nil)
231
+ unless @running_config.key? subcmd
232
+ output = subcmd ? show('running-config ' + subcmd) : show('running-config')
233
+ @running_config[subcmd] = Config.new(nested_config: output)
234
+ end
235
+ @running_config[subcmd]
236
+ rescue Error::CommandError
237
+ Config.new
238
+ end
239
+
240
+ # ASA software version in `x.x(x)` format.
241
+ #
242
+ # @return [String]
243
+ def version
244
+ unless @version
245
+ # Reassemble the version string on the off chance that an interim release
246
+ # is reported in the format "x.x(x.x)" versus "x.x(x)x".
247
+ regex = /^Cisco Adaptive Security Appliance Software Version (\d+)\.(\d+)\((\d+).*?\)/
248
+ matches = regex.match(show('version'))
249
+ fail Error::VersionParseError, 'Unable to determine appliance version' unless matches
250
+ @version = '%d.%d(%d)' % matches[1..3]
251
+ end
252
+ @version
253
+ end
254
+
255
+ # Return the result of comparing the ASA software version with a list of
256
+ # expressions. Will `yield` once on success if a block is given.
257
+ #
258
+ # @example
259
+ # asa.version? '9.x', '< 9.3' do
260
+ # puts 'Running version 9.0, 9.1 or 9.2'
261
+ # end
262
+ #
263
+ # @see Util.version_match? The utility function called by this method
264
+ # @param exprs [Array<String>]
265
+ # @return [Boolean] `true` if _all_ expressions match, or `false` otherwise
266
+ def version?(*exprs)
267
+ success = Util.version_match?(version, exprs)
268
+ yield if success && block_given?
269
+ success
270
+ end
271
+
272
+ def enable!
273
+ send('enable', PASSWORD_PROMPT)
274
+ password = @enable_password ? @enable_password : @terminal.password
275
+ send(password, PRIV_EXEC_PROMPT, true)
276
+ end
277
+ private :enable!
278
+
279
+ def must_be_connected!
280
+ fail Error::NotConnectedError, 'Terminal is not connected' unless @terminal.connected?
281
+ end
282
+ private :must_be_connected!
283
+ end