train 1.3.0 → 1.4.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 +17 -2
- data/lib/train/file/remote/windows.rb +1 -1
- data/lib/train/platforms/detect/specifications/os.rb +12 -0
- data/lib/train/transports/cisco_ios_connection.rb +119 -0
- data/lib/train/transports/ssh.rb +14 -0
- data/lib/train/transports/winrm_connection.rb +6 -2
- data/lib/train/version.rb +1 -1
- data/test/unit/platforms/os_detect_test.rb +10 -0
- data/test/unit/platforms/platform_test.rb +9 -0
- data/test/unit/transports/cisco_ios_connection.rb +81 -0
- data/test/unit/transports/ssh_test.rb +4 -4
- metadata +5 -5
- data/lib/train/transports/cisco_ios.rb +0 -140
- data/test/unit/transports/cisco_ios.rb +0 -94
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f666a5aa2bb040f3bc662ef2881d2a3778764cc9
|
4
|
+
data.tar.gz: 6e84e3102772ea858d8f4827f959e74f0d7a396f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 376156ec17342f41fb82f5a1e8888e1c19b31f8760fd0ceaed4b664f68acf54c759129d3a458c0343c19d0925c70a09f715d1c8f18171d82b506c6ab331267f9
|
7
|
+
data.tar.gz: 5bce1e60af8264a419c68898e8efca072e0f5004313a9aabb1b83a978956618e21d6d3bc9de9ee848a93b482bfe6ec751d713bda18035074814b15294d5502da
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,21 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [1.
|
4
|
-
[Full Changelog](https://github.com/chef/train/compare/v1.
|
3
|
+
## [1.4.0](https://github.com/chef/train/tree/1.4.0) (2018-04-12)
|
4
|
+
[Full Changelog](https://github.com/chef/train/compare/v1.3.0...1.4.0)
|
5
|
+
|
6
|
+
**Closed issues:**
|
7
|
+
|
8
|
+
- Train reports directories with the archive bit set as files on the windows platform [\#274](https://github.com/chef/train/issues/274)
|
9
|
+
|
10
|
+
**Merged pull requests:**
|
11
|
+
|
12
|
+
- Add CloudLinux as a detected platform [\#281](https://github.com/chef/train/pull/281) ([tarcinil](https://github.com/tarcinil))
|
13
|
+
- Move Cisco IOS connection under SSH transport [\#279](https://github.com/chef/train/pull/279) ([jerryaldrichiii](https://github.com/jerryaldrichiii))
|
14
|
+
- Initialize FileManager using '@service' [\#278](https://github.com/chef/train/pull/278) ([marcparadise](https://github.com/marcparadise))
|
15
|
+
- small fix to make sure windows directories with the archive bit set a… [\#275](https://github.com/chef/train/pull/275) ([devoptimist](https://github.com/devoptimist))
|
16
|
+
|
17
|
+
## [v1.3.0](https://github.com/chef/train/tree/v1.3.0) (2018-03-29)
|
18
|
+
[Full Changelog](https://github.com/chef/train/compare/v1.2.0...v1.3.0)
|
5
19
|
|
6
20
|
**Implemented enhancements:**
|
7
21
|
|
@@ -13,6 +27,7 @@
|
|
13
27
|
|
14
28
|
**Merged pull requests:**
|
15
29
|
|
30
|
+
- Release Train 1.3.0 [\#276](https://github.com/chef/train/pull/276) ([jquick](https://github.com/jquick))
|
16
31
|
- Add MSI connection option for azure. [\#272](https://github.com/chef/train/pull/272) ([jquick](https://github.com/jquick))
|
17
32
|
- Add transport for Cisco IOS [\#271](https://github.com/chef/train/pull/271) ([jerryaldrichiii](https://github.com/jerryaldrichiii))
|
18
33
|
- Add platform uuid information. [\#270](https://github.com/chef/train/pull/270) ([jquick](https://github.com/jquick))
|
@@ -195,6 +195,18 @@ module Train::Platforms::Detect::Specifications
|
|
195
195
|
true
|
196
196
|
end
|
197
197
|
}
|
198
|
+
plat.name('cloudlinux').title('CloudLinux').in_family('redhat')
|
199
|
+
.detect {
|
200
|
+
lsb = read_linux_lsb
|
201
|
+
if lsb && lsb[:id] =~ /cloudlinux/i
|
202
|
+
@platform[:release] = lsb[:release]
|
203
|
+
true
|
204
|
+
elsif (raw = unix_file_contents('/etc/redhat-release')) =~ /cloudlinux/i
|
205
|
+
@platform[:name] = redhatish_platform(raw)
|
206
|
+
@platform[:release] = redhatish_version(raw)
|
207
|
+
true
|
208
|
+
end
|
209
|
+
}
|
198
210
|
# keep redhat at the end as a fallback for anything with a redhat-release
|
199
211
|
plat.name('redhat').title('Red Hat Linux').in_family('redhat')
|
200
212
|
.detect {
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Train::Transports::SSH
|
4
|
+
class CiscoIOSConnection < BaseConnection
|
5
|
+
class BadEnablePassword < Train::TransportError; end
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
super(options)
|
9
|
+
|
10
|
+
# Extract options to avoid passing them in to `Net::SSH.start` later
|
11
|
+
@host = options.delete(:host)
|
12
|
+
@user = options.delete(:user)
|
13
|
+
@port = options.delete(:port)
|
14
|
+
@enable_password = options.delete(:enable_password)
|
15
|
+
|
16
|
+
# Use all options left that are not `nil` for `Net::SSH.start` later
|
17
|
+
@ssh_options = options.reject { |_key, value| value.nil? }
|
18
|
+
|
19
|
+
@prompt = /^\S+[>#]\r\n.*$/
|
20
|
+
end
|
21
|
+
|
22
|
+
def uri
|
23
|
+
"ssh://#{@user}@#{@host}:#{@port}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def establish_connection
|
29
|
+
logger.debug("[SSH] opening connection to #{self}")
|
30
|
+
|
31
|
+
Net::SSH.start(@host, @user, @ssh_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def session
|
35
|
+
return @session unless @session.nil?
|
36
|
+
|
37
|
+
@session = open_channel(establish_connection)
|
38
|
+
|
39
|
+
# Escalate privilege to enable mode if password is given
|
40
|
+
if @enable_password
|
41
|
+
run_command_via_connection("enable\r\n#{@enable_password}")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Prevent `--MORE--` by removing terminal length limit
|
45
|
+
run_command_via_connection('terminal length 0')
|
46
|
+
|
47
|
+
@session
|
48
|
+
end
|
49
|
+
|
50
|
+
def run_command_via_connection(cmd)
|
51
|
+
# Ensure buffer is empty before sending data
|
52
|
+
@buf = ''
|
53
|
+
|
54
|
+
logger.debug("[SSH] Running `#{cmd}` on #{self}")
|
55
|
+
session.send_data(cmd + "\r\n")
|
56
|
+
|
57
|
+
logger.debug('[SSH] waiting for prompt')
|
58
|
+
until @buf =~ @prompt
|
59
|
+
raise BadEnablePassword if @buf =~ /Bad secrets/
|
60
|
+
session.connection.process(0)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Save the buffer and clear it for the next command
|
64
|
+
output = @buf.dup
|
65
|
+
@buf = ''
|
66
|
+
|
67
|
+
format_result(format_output(output, cmd))
|
68
|
+
end
|
69
|
+
|
70
|
+
ERROR_MATCHERS = [
|
71
|
+
'Bad IP address',
|
72
|
+
'Incomplete command',
|
73
|
+
'Invalid input detected',
|
74
|
+
'Unrecognized host',
|
75
|
+
].freeze
|
76
|
+
|
77
|
+
# IOS commands do not have an exit code so we must compare the command
|
78
|
+
# output with partial segments of known errors. Then, we return a
|
79
|
+
# `CommandResult` with arguments in the correct position based on the
|
80
|
+
# result.
|
81
|
+
def format_result(result)
|
82
|
+
if ERROR_MATCHERS.none? { |e| result.include?(e) }
|
83
|
+
CommandResult.new(result, '', 0)
|
84
|
+
else
|
85
|
+
CommandResult.new('', result, 1)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# The buffer (@buf) contains all data sent/received on the SSH channel so
|
90
|
+
# we need to format the data to match what we would expect from Train
|
91
|
+
def format_output(output, cmd)
|
92
|
+
leading_prompt = /(\r\n|^)\S+[>#]/
|
93
|
+
command_string = /#{Regexp.quote(cmd)}\r\n/
|
94
|
+
trailing_prompt = /\S+[>#](\r\n|$)/
|
95
|
+
trailing_line_endings = /(\r\n)+$/
|
96
|
+
|
97
|
+
output
|
98
|
+
.sub(leading_prompt, '')
|
99
|
+
.sub(command_string, '')
|
100
|
+
.gsub(trailing_prompt, '')
|
101
|
+
.gsub(trailing_line_endings, '')
|
102
|
+
end
|
103
|
+
|
104
|
+
# Create an SSH channel that writes to @buf when data is received
|
105
|
+
def open_channel(ssh)
|
106
|
+
logger.debug("[SSH] opening SSH channel to #{self}")
|
107
|
+
ssh.open_channel do |ch|
|
108
|
+
ch.on_data do |_, data|
|
109
|
+
@buf += data
|
110
|
+
end
|
111
|
+
|
112
|
+
ch.send_channel_request('shell') do |_, success|
|
113
|
+
raise 'Failed to open SSH shell' unless success
|
114
|
+
logger.debug('[SSH] shell opened')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/train/transports/ssh.rb
CHANGED
@@ -37,6 +37,7 @@ module Train::Transports
|
|
37
37
|
name 'ssh'
|
38
38
|
|
39
39
|
require 'train/transports/ssh_connection'
|
40
|
+
require 'train/transports/cisco_ios_connection'
|
40
41
|
|
41
42
|
# add options for submodules
|
42
43
|
include_options Train::Extras::CommandWrapper
|
@@ -193,6 +194,19 @@ module Train::Transports
|
|
193
194
|
|
194
195
|
@connection_options = options
|
195
196
|
conn = Connection.new(options, &block)
|
197
|
+
|
198
|
+
# Cisco IOS requires a special implementation of `Net:SSH`. This uses the
|
199
|
+
# SSH transport to identify the platform, but then replaces SSHConnection
|
200
|
+
# with a CiscoIOSConnection in order to behave as expected for the user.
|
201
|
+
if defined?(conn.platform.cisco_ios?) && conn.platform.cisco_ios?
|
202
|
+
ios_options = {}
|
203
|
+
ios_options[:host] = @options[:host]
|
204
|
+
ios_options[:user] = @options[:user]
|
205
|
+
ios_options[:enable_password] = @options[:enable_password]
|
206
|
+
ios_options.merge!(@connection_options)
|
207
|
+
conn = CiscoIOSConnection.new(ios_options)
|
208
|
+
end
|
209
|
+
|
196
210
|
@connection = conn unless conn.nil?
|
197
211
|
end
|
198
212
|
|
@@ -80,7 +80,7 @@ class Train::Transports::WinRM
|
|
80
80
|
retry_limit: @max_wait_until_ready / delay,
|
81
81
|
retry_delay: delay,
|
82
82
|
)
|
83
|
-
|
83
|
+
run_command_via_connection(PING_COMMAND.dup)
|
84
84
|
end
|
85
85
|
|
86
86
|
def uri
|
@@ -129,7 +129,11 @@ class Train::Transports::WinRM
|
|
129
129
|
# @return [Winrm::FileManager] a file transporter
|
130
130
|
# @api private
|
131
131
|
def file_manager
|
132
|
-
@file_manager ||=
|
132
|
+
@file_manager ||= begin
|
133
|
+
# Ensure @service is available:
|
134
|
+
wait_until_ready
|
135
|
+
WinRM::FS::FileManager.new(@service)
|
136
|
+
end
|
133
137
|
end
|
134
138
|
|
135
139
|
# Builds a `LoginCommand` for use by Linux-based platforms.
|
data/lib/train/version.rb
CHANGED
@@ -54,6 +54,16 @@ describe 'os_detect' do
|
|
54
54
|
platform[:family].must_equal('redhat')
|
55
55
|
platform[:release].must_equal('7.4')
|
56
56
|
end
|
57
|
+
it 'sets the correct family, name, and release on CloudLinux' do
|
58
|
+
files = {
|
59
|
+
'/etc/redhat-release' => "CloudLinux release 7.4 (Georgy Grechko)\n",
|
60
|
+
'/etc/os-release' => "NAME=\"CloudLinux\"\nVERSION=\"7.4 (Georgy Grechko)\"\nID=\"cloudlinux\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"7.4\"\nPRETTY_NAME=\"CloudLinux 7.4 (Georgy Grechko)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:cloudlinux:cloudlinux:7.4:GA:server\"\nHOME_URL=\"https://www.cloudlinux.com//\"\nBUG_REPORT_URL=\"https://www.cloudlinux.com/support\"\n",
|
61
|
+
}
|
62
|
+
platform = scan_with_files('linux', files)
|
63
|
+
platform[:name].must_equal('cloudlinux')
|
64
|
+
platform[:family].must_equal('redhat')
|
65
|
+
platform[:release].must_equal('7.4')
|
66
|
+
end
|
57
67
|
end
|
58
68
|
end
|
59
69
|
|
@@ -133,6 +133,15 @@ describe 'platform' do
|
|
133
133
|
it { os.unix?.must_equal(true) }
|
134
134
|
end
|
135
135
|
|
136
|
+
describe 'with platform set to cloudlinux' do
|
137
|
+
let(:os) { mock_platform('cloudlinux') }
|
138
|
+
it { os.redhat?.must_equal(true) }
|
139
|
+
it { os.debian?.must_equal(false) }
|
140
|
+
it { os.suse?.must_equal(false) }
|
141
|
+
it { os.linux?.must_equal(true) }
|
142
|
+
it { os.unix?.must_equal(true) }
|
143
|
+
end
|
144
|
+
|
136
145
|
describe 'with platform set to fedora' do
|
137
146
|
let(:os) { mock_platform('fedora') }
|
138
147
|
it { os.fedora?.must_equal(true) }
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
require 'train/transports/ssh'
|
5
|
+
|
6
|
+
describe 'CiscoIOSConnection' do
|
7
|
+
let(:cls) do
|
8
|
+
Train::Platforms::Detect::Specifications::OS.load
|
9
|
+
plat = Train::Platforms.name('mock').in_family('cisco_ios')
|
10
|
+
plat.add_platform_methods
|
11
|
+
plat.stubs(:cisco_ios?).returns(true)
|
12
|
+
Train::Platforms::Detect.stubs(:scan).returns(plat)
|
13
|
+
Train::Transports::SSH
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:opts) do
|
17
|
+
{
|
18
|
+
host: 'fakehost',
|
19
|
+
user: 'fakeuser',
|
20
|
+
password: 'fakepassword',
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:connection) do
|
25
|
+
cls.new(opts).connection
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#initialize' do
|
29
|
+
it 'provides a uri' do
|
30
|
+
connection.uri.must_equal 'ssh://fakeuser@fakehost:22'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#format_result' do
|
35
|
+
it 'returns correctly when result is `good`' do
|
36
|
+
output = 'good'
|
37
|
+
Train::Extras::CommandResult.expects(:new).with(output, '', 0)
|
38
|
+
connection.send(:format_result, 'good')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns correctly when result matches /Bad IP address/' do
|
42
|
+
output = "Translating \"nope\"\r\n\r\nTranslating \"nope\"\r\n\r\n% Bad IP address or host name\r\n% Unknown command or computer name, or unable to find computer address\r\n"
|
43
|
+
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
44
|
+
connection.send(:format_result, output)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns correctly when result matches /Incomplete command/' do
|
48
|
+
output = "% Incomplete command.\r\n\r\n"
|
49
|
+
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
50
|
+
connection.send(:format_result, output)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns correctly when result matches /Invalid input detected/' do
|
54
|
+
output = " ^\r\n% Invalid input detected at '^' marker.\r\n\r\n"
|
55
|
+
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
56
|
+
connection.send(:format_result, output)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'returns correctly when result matches /Unrecognized host/' do
|
60
|
+
output = "Translating \"nope\"\r\n% Unrecognized host or address, or protocol not running.\r\n\r\n"
|
61
|
+
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
62
|
+
connection.send(:format_result, output)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#format_output' do
|
67
|
+
it 'returns the correct output' do
|
68
|
+
cmd = 'show calendar'
|
69
|
+
output = "show calendar\r\n10:35:50 UTC Fri Mar 23 2018\r\n7200_ios_12#\r\n7200_ios_12#"
|
70
|
+
result = connection.send(:format_output, output, cmd)
|
71
|
+
result.must_equal '10:35:50 UTC Fri Mar 23 2018'
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns the correct output when a pipe is used' do
|
75
|
+
cmd = 'show running-config | section line con 0'
|
76
|
+
output = "show running-config | section line con 0\r\nline con 0\r\n exec-timeout 0 0\r\n privilege level 15\r\n logging synchronous\r\n stopbits 1\r\n7200_ios_12#\r\n7200_ios_12#"
|
77
|
+
result = connection.send(:format_output, output, cmd)
|
78
|
+
result.must_equal "line con 0\r\n exec-timeout 0 0\r\n privilege level 15\r\n logging synchronous\r\n stopbits 1"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -5,10 +5,10 @@ require 'train/transports/ssh'
|
|
5
5
|
|
6
6
|
describe 'ssh transport' do
|
7
7
|
let(:cls) do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
plat = Train::Platforms.name('mock').in_family('linux')
|
9
|
+
plat.add_platform_methods
|
10
|
+
Train::Platforms::Detect.stubs(:scan).returns(plat)
|
11
|
+
Train::Transports::SSH
|
12
12
|
end
|
13
13
|
let(:conf) {{
|
14
14
|
host: rand.to_s,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: train
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dominik Richter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-04-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -225,7 +225,7 @@ files:
|
|
225
225
|
- lib/train/plugins/transport.rb
|
226
226
|
- lib/train/transports/aws.rb
|
227
227
|
- lib/train/transports/azure.rb
|
228
|
-
- lib/train/transports/
|
228
|
+
- lib/train/transports/cisco_ios_connection.rb
|
229
229
|
- lib/train/transports/docker.rb
|
230
230
|
- lib/train/transports/local.rb
|
231
231
|
- lib/train/transports/mock.rb
|
@@ -292,7 +292,7 @@ files:
|
|
292
292
|
- test/unit/train_test.rb
|
293
293
|
- test/unit/transports/aws_test.rb
|
294
294
|
- test/unit/transports/azure_test.rb
|
295
|
-
- test/unit/transports/
|
295
|
+
- test/unit/transports/cisco_ios_connection.rb
|
296
296
|
- test/unit/transports/local_test.rb
|
297
297
|
- test/unit/transports/mock_test.rb
|
298
298
|
- test/unit/transports/ssh_test.rb
|
@@ -383,7 +383,7 @@ test_files:
|
|
383
383
|
- test/unit/train_test.rb
|
384
384
|
- test/unit/transports/aws_test.rb
|
385
385
|
- test/unit/transports/azure_test.rb
|
386
|
-
- test/unit/transports/
|
386
|
+
- test/unit/transports/cisco_ios_connection.rb
|
387
387
|
- test/unit/transports/local_test.rb
|
388
388
|
- test/unit/transports/mock_test.rb
|
389
389
|
- test/unit/transports/ssh_test.rb
|
@@ -1,140 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
require 'train/plugins'
|
4
|
-
require 'train/transports/ssh'
|
5
|
-
|
6
|
-
module Train::Transports
|
7
|
-
class BadEnablePassword < Train::TransportError; end
|
8
|
-
|
9
|
-
class CiscoIOS < SSH
|
10
|
-
name 'cisco_ios'
|
11
|
-
|
12
|
-
option :host, required: true
|
13
|
-
option :user, required: true
|
14
|
-
option :port, default: 22, required: true
|
15
|
-
|
16
|
-
option :password, required: true
|
17
|
-
|
18
|
-
# Used to elevate to enable mode (similar to `sudo su` in Linux)
|
19
|
-
option :enable_password
|
20
|
-
|
21
|
-
def connection
|
22
|
-
@connection ||= Connection.new(validate_options(@options).options)
|
23
|
-
end
|
24
|
-
|
25
|
-
class Connection < BaseConnection
|
26
|
-
def initialize(options)
|
27
|
-
super(options)
|
28
|
-
|
29
|
-
# Delete options to avoid passing them in to `Net::SSH.start` later
|
30
|
-
@host = @options.delete(:host)
|
31
|
-
@user = @options.delete(:user)
|
32
|
-
@port = @options.delete(:port)
|
33
|
-
@enable_password = @options.delete(:enable_password)
|
34
|
-
|
35
|
-
@prompt = /^\S+[>#]\r\n.*$/
|
36
|
-
end
|
37
|
-
|
38
|
-
def uri
|
39
|
-
"ssh://#{@user}@#{@host}:#{@port}"
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def establish_connection
|
45
|
-
logger.debug("[SSH] opening connection to #{self}")
|
46
|
-
|
47
|
-
Net::SSH.start(
|
48
|
-
@host,
|
49
|
-
@user,
|
50
|
-
@options.reject { |_key, value| value.nil? },
|
51
|
-
)
|
52
|
-
end
|
53
|
-
|
54
|
-
def session
|
55
|
-
return @session unless @session.nil?
|
56
|
-
|
57
|
-
@session = open_channel(establish_connection)
|
58
|
-
|
59
|
-
# Escalate privilege to enable mode if password is given
|
60
|
-
if @enable_password
|
61
|
-
run_command_via_connection("enable\r\n#{@enable_password}")
|
62
|
-
end
|
63
|
-
|
64
|
-
# Prevent `--MORE--` by removing terminal length limit
|
65
|
-
run_command_via_connection('terminal length 0')
|
66
|
-
|
67
|
-
@session
|
68
|
-
end
|
69
|
-
|
70
|
-
def run_command_via_connection(cmd)
|
71
|
-
# Ensure buffer is empty before sending data
|
72
|
-
@buf = ''
|
73
|
-
|
74
|
-
logger.debug("[SSH] Running `#{cmd}` on #{self}")
|
75
|
-
session.send_data(cmd + "\r\n")
|
76
|
-
|
77
|
-
logger.debug('[SSH] waiting for prompt')
|
78
|
-
until @buf =~ @prompt
|
79
|
-
raise BadEnablePassword if @buf =~ /Bad secrets/
|
80
|
-
session.connection.process(0)
|
81
|
-
end
|
82
|
-
|
83
|
-
# Save the buffer and clear it for the next command
|
84
|
-
output = @buf.dup
|
85
|
-
@buf = ''
|
86
|
-
|
87
|
-
format_result(format_output(output, cmd))
|
88
|
-
end
|
89
|
-
|
90
|
-
ERROR_MATCHERS = [
|
91
|
-
'Bad IP address',
|
92
|
-
'Incomplete command',
|
93
|
-
'Invalid input detected',
|
94
|
-
'Unrecognized host',
|
95
|
-
].freeze
|
96
|
-
|
97
|
-
# IOS commands do not have an exit code so we must compare the command
|
98
|
-
# output with partial segments of known errors. Then, we return a
|
99
|
-
# `CommandResult` with arguments in the correct position based on the
|
100
|
-
# result.
|
101
|
-
def format_result(result)
|
102
|
-
if ERROR_MATCHERS.none? { |e| result.include?(e) }
|
103
|
-
CommandResult.new(result, '', 0)
|
104
|
-
else
|
105
|
-
CommandResult.new('', result, 1)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# The buffer (@buf) contains all data sent/received on the SSH channel so
|
110
|
-
# we need to format the data to match what we would expect from Train
|
111
|
-
def format_output(output, cmd)
|
112
|
-
leading_prompt = /(\r\n|^)\S+[>#]/
|
113
|
-
command_string = /#{cmd}\r\n/
|
114
|
-
trailing_prompt = /\S+[>#](\r\n|$)/
|
115
|
-
trailing_line_endings = /(\r\n)+$/
|
116
|
-
|
117
|
-
output
|
118
|
-
.sub(leading_prompt, '')
|
119
|
-
.sub(command_string, '')
|
120
|
-
.gsub(trailing_prompt, '')
|
121
|
-
.gsub(trailing_line_endings, '')
|
122
|
-
end
|
123
|
-
|
124
|
-
# Create an SSH channel that writes to @buf when data is received
|
125
|
-
def open_channel(ssh)
|
126
|
-
logger.debug("[SSH] opening SSH channel to #{self}")
|
127
|
-
ssh.open_channel do |ch|
|
128
|
-
ch.on_data do |_, data|
|
129
|
-
@buf += data
|
130
|
-
end
|
131
|
-
|
132
|
-
ch.send_channel_request('shell') do |_, success|
|
133
|
-
raise 'Failed to open SSH shell' unless success
|
134
|
-
logger.debug('[SSH] shell opened')
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
@@ -1,94 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
require 'helper'
|
4
|
-
require 'train/transports/cisco_ios'
|
5
|
-
|
6
|
-
describe 'Train::Transports::CiscoIOS' do
|
7
|
-
let(:cls) do
|
8
|
-
plat = Train::Platforms.name('mock').in_family('cisco_ios')
|
9
|
-
plat.add_platform_methods
|
10
|
-
Train::Platforms::Detect.stubs(:scan).returns(plat)
|
11
|
-
Train::Transports::CiscoIOS
|
12
|
-
end
|
13
|
-
|
14
|
-
let(:opts) do
|
15
|
-
{
|
16
|
-
host: 'fakehost',
|
17
|
-
user: 'fakeuser',
|
18
|
-
password: 'fakepassword',
|
19
|
-
}
|
20
|
-
end
|
21
|
-
|
22
|
-
let(:cisco_ios) do
|
23
|
-
cls.new(opts)
|
24
|
-
end
|
25
|
-
|
26
|
-
describe 'CiscoIOS::Connection' do
|
27
|
-
let(:connection) { cls.new(opts).connection }
|
28
|
-
|
29
|
-
describe '#initialize' do
|
30
|
-
it 'raises an error when user is missing' do
|
31
|
-
opts.delete(:user)
|
32
|
-
err = proc { cls.new(opts).connection }.must_raise(Train::ClientError)
|
33
|
-
err.message.must_match(/must provide.*user/)
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'raises an error when host is missing' do
|
37
|
-
opts.delete(:host)
|
38
|
-
err = proc { cls.new(opts).connection }.must_raise(Train::ClientError)
|
39
|
-
err.message.must_match(/must provide.*host/)
|
40
|
-
end
|
41
|
-
|
42
|
-
it 'raises an error when password is missing' do
|
43
|
-
opts.delete(:password)
|
44
|
-
err = proc { cls.new(opts).connection }.must_raise(Train::ClientError)
|
45
|
-
err.message.must_match(/must provide.*password/)
|
46
|
-
end
|
47
|
-
|
48
|
-
it 'provides a uri' do
|
49
|
-
connection.uri.must_equal 'ssh://fakeuser@fakehost:22'
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
describe '#format_result' do
|
54
|
-
it 'returns correctly when result is `good`' do
|
55
|
-
output = 'good'
|
56
|
-
Train::Extras::CommandResult.expects(:new).with(output, '', 0)
|
57
|
-
connection.send(:format_result, 'good')
|
58
|
-
end
|
59
|
-
|
60
|
-
it 'returns correctly when result matches /Bad IP address/' do
|
61
|
-
output = "Translating \"nope\"\r\n\r\nTranslating \"nope\"\r\n\r\n% Bad IP address or host name\r\n% Unknown command or computer name, or unable to find computer address\r\n"
|
62
|
-
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
63
|
-
connection.send(:format_result, output)
|
64
|
-
end
|
65
|
-
|
66
|
-
it 'returns correctly when result matches /Incomplete command/' do
|
67
|
-
output = "% Incomplete command.\r\n\r\n"
|
68
|
-
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
69
|
-
connection.send(:format_result, output)
|
70
|
-
end
|
71
|
-
|
72
|
-
it 'returns correctly when result matches /Invalid input detected/' do
|
73
|
-
output = " ^\r\n% Invalid input detected at '^' marker.\r\n\r\n"
|
74
|
-
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
75
|
-
connection.send(:format_result, output)
|
76
|
-
end
|
77
|
-
|
78
|
-
it 'returns correctly when result matches /Unrecognized host/' do
|
79
|
-
output = "Translating \"nope\"\r\n% Unrecognized host or address, or protocol not running.\r\n\r\n"
|
80
|
-
Train::Extras::CommandResult.expects(:new).with('', output, 1)
|
81
|
-
connection.send(:format_result, output)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
describe '#format_output' do
|
86
|
-
it 'returns output containing only the output of the command executed' do
|
87
|
-
cmd = 'show calendar'
|
88
|
-
output = "show calendar\r\n10:35:50 UTC Fri Mar 23 2018\r\n7200_ios_12#\r\n7200_ios_12#"
|
89
|
-
result = connection.send(:format_output, output, cmd)
|
90
|
-
result.must_equal '10:35:50 UTC Fri Mar 23 2018'
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|