train 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|