train 1.2.0 → 1.3.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 +20 -2
- data/lib/train/errors.rb +8 -5
- data/lib/train/platforms.rb +3 -2
- data/lib/train/platforms/detect/helpers/os_common.rb +55 -1
- data/lib/train/platforms/detect/helpers/os_windows.rb +41 -0
- data/lib/train/platforms/detect/specifications/os.rb +1 -0
- data/lib/train/platforms/detect/uuid.rb +34 -0
- data/lib/train/platforms/platform.rb +4 -0
- data/lib/train/transports/aws.rb +7 -0
- data/lib/train/transports/azure.rb +36 -7
- data/lib/train/transports/cisco_ios.rb +140 -0
- data/lib/train/transports/local.rb +2 -1
- data/lib/train/transports/mock.rb +2 -0
- data/lib/train/version.rb +1 -1
- data/test/unit/platforms/detect/uuid_test.rb +133 -0
- data/test/unit/transports/aws_test.rb +24 -0
- data/test/unit/transports/azure_test.rb +32 -0
- data/test/unit/transports/cisco_ios.rb +94 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '095b1ae4856669cbe224dffbfa09f555ce7c0d05'
|
4
|
+
data.tar.gz: ee6cc55a500e4466e497e853e49963a05b1f85d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea321b3f07afc791e16628f7d4f26277dc7caed537e29f8351dd8934741af77b8ee137ce35184d820c2aca2e02b335d6fdb538784577f11a06e9c7be1924ef07
|
7
|
+
data.tar.gz: d3a80455ea2819ccdb0ba1e92fdd94c7525604d6dfe49345902531e44eef9c06fd08d39b7911ac9464d44da4f762cd46fe10ab377804327237e4ba7e6405f987
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,24 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
## [1.
|
4
|
-
[Full Changelog](https://github.com/chef/train/compare/v1.
|
3
|
+
## [1.3.0](https://github.com/chef/train/tree/1.3.0) (2018-03-29)
|
4
|
+
[Full Changelog](https://github.com/chef/train/compare/v1.2.0...1.3.0)
|
5
|
+
|
6
|
+
**Implemented enhancements:**
|
7
|
+
|
8
|
+
- Update errors to have a base type of Train::Error [\#273](https://github.com/chef/train/pull/273) ([marcparadise](https://github.com/marcparadise))
|
9
|
+
|
10
|
+
**Closed issues:**
|
11
|
+
|
12
|
+
- RFC: Generate unique uuid for platforms [\#264](https://github.com/chef/train/issues/264)
|
13
|
+
|
14
|
+
**Merged pull requests:**
|
15
|
+
|
16
|
+
- Add MSI connection option for azure. [\#272](https://github.com/chef/train/pull/272) ([jquick](https://github.com/jquick))
|
17
|
+
- Add transport for Cisco IOS [\#271](https://github.com/chef/train/pull/271) ([jerryaldrichiii](https://github.com/jerryaldrichiii))
|
18
|
+
- Add platform uuid information. [\#270](https://github.com/chef/train/pull/270) ([jquick](https://github.com/jquick))
|
19
|
+
|
20
|
+
## [v1.2.0](https://github.com/chef/train/tree/v1.2.0) (2018-03-15)
|
21
|
+
[Full Changelog](https://github.com/chef/train/compare/v1.1.1...v1.2.0)
|
5
22
|
|
6
23
|
**Implemented enhancements:**
|
7
24
|
|
@@ -14,6 +31,7 @@
|
|
14
31
|
|
15
32
|
**Merged pull requests:**
|
16
33
|
|
34
|
+
- Release train 1.2.0 [\#269](https://github.com/chef/train/pull/269) ([jquick](https://github.com/jquick))
|
17
35
|
- Force 64bit powershell for 32bit ruby running on 64bit windows [\#266](https://github.com/chef/train/pull/266) ([jquick](https://github.com/jquick))
|
18
36
|
- support cisco ios xe [\#262](https://github.com/chef/train/pull/262) ([arlimus](https://github.com/arlimus))
|
19
37
|
- Create a master OS family and refactor specifications [\#261](https://github.com/chef/train/pull/261) ([jquick](https://github.com/jquick))
|
data/lib/train/errors.rb
CHANGED
@@ -9,21 +9,24 @@
|
|
9
9
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
10
10
|
|
11
11
|
module Train
|
12
|
+
# Base exception for any exception explicitly raised by the Train library.
|
13
|
+
class Error < ::StandardError; end
|
14
|
+
|
12
15
|
# Base exception class for all exceptions that are caused by user input
|
13
16
|
# errors.
|
14
|
-
class UserError <
|
17
|
+
class UserError < Error; end
|
15
18
|
|
16
19
|
# Base exception class for all exceptions that are caused by incorrect use
|
17
20
|
# of an API.
|
18
|
-
class ClientError <
|
21
|
+
class ClientError < Error; end
|
19
22
|
|
20
23
|
# Base exception class for all exceptions that are caused by other failures
|
21
24
|
# in the transport layer.
|
22
|
-
class TransportError <
|
25
|
+
class TransportError < Error; end
|
23
26
|
|
24
27
|
# Exception for when no platform can be detected.
|
25
|
-
class PlatformDetectionFailed <
|
28
|
+
class PlatformDetectionFailed < Error; end
|
26
29
|
|
27
30
|
# Exception for when a invalid cache type is passed.
|
28
|
-
class UnknownCacheType <
|
31
|
+
class UnknownCacheType < Error; end
|
29
32
|
end
|
data/lib/train/platforms.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require 'train/platforms/common'
|
4
|
-
require 'train/platforms/family'
|
5
|
-
require 'train/platforms/platform'
|
6
4
|
require 'train/platforms/detect'
|
7
5
|
require 'train/platforms/detect/scanner'
|
8
6
|
require 'train/platforms/detect/specifications/os'
|
9
7
|
require 'train/platforms/detect/specifications/api'
|
8
|
+
require 'train/platforms/detect/uuid'
|
9
|
+
require 'train/platforms/family'
|
10
|
+
require 'train/platforms/platform'
|
10
11
|
|
11
12
|
module Train::Platforms
|
12
13
|
class << self
|
@@ -5,7 +5,7 @@ require 'train/platforms/detect/helpers/os_windows'
|
|
5
5
|
require 'rbconfig'
|
6
6
|
|
7
7
|
module Train::Platforms::Detect::Helpers
|
8
|
-
module OSCommon
|
8
|
+
module OSCommon # rubocop:disable Metrics/ModuleLength
|
9
9
|
include Train::Platforms::Detect::Helpers::Linux
|
10
10
|
include Train::Platforms::Detect::Helpers::Windows
|
11
11
|
|
@@ -87,5 +87,59 @@ module Train::Platforms::Detect::Helpers
|
|
87
87
|
|
88
88
|
@cache[:cisco] = nil
|
89
89
|
end
|
90
|
+
|
91
|
+
def unix_uuid
|
92
|
+
uuid = unix_uuid_from_chef
|
93
|
+
uuid = unix_uuid_from_machine_file if uuid.nil?
|
94
|
+
uuid = uuid_from_command if uuid.nil?
|
95
|
+
raise Train::TransportError, 'Cannot find a UUID for your node.' if uuid.nil?
|
96
|
+
uuid
|
97
|
+
end
|
98
|
+
|
99
|
+
def unix_uuid_from_chef
|
100
|
+
file = @backend.file('/var/chef/cache/data_collector_metadata.json')
|
101
|
+
if file.exist? && !file.size.zero?
|
102
|
+
json = ::JSON.parse(file.content)
|
103
|
+
return json['node_uuid'] if json['node_uuid']
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def unix_uuid_from_machine_file
|
108
|
+
%W(
|
109
|
+
/etc/chef/chef_guid
|
110
|
+
#{ENV['HOME']}/.chef/chef_guid
|
111
|
+
/etc/machine-id
|
112
|
+
/var/lib/dbus/machine-id
|
113
|
+
/var/db/dbus/machine-id
|
114
|
+
).each do |path|
|
115
|
+
file = @backend.file(path)
|
116
|
+
next unless file.exist? && !file.size.zero?
|
117
|
+
return file.content.chomp if path =~ /guid/
|
118
|
+
return uuid_from_string(file.content.chomp)
|
119
|
+
end
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# This takes a command from the platform detect block to run.
|
124
|
+
# We expect the command to return a unique identifier which
|
125
|
+
# we turn into a UUID.
|
126
|
+
def uuid_from_command
|
127
|
+
return unless @platform[:uuid_command]
|
128
|
+
result = @backend.run_command(@platform[:uuid_command])
|
129
|
+
uuid_from_string(result.stdout.chomp) if result.exit_status.zero? && !result.stdout.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
# This hashes the passed string into SHA1.
|
133
|
+
# Then it downgrades the 160bit SHA1 to a 128bit
|
134
|
+
# then we format it as a valid UUIDv5.
|
135
|
+
def uuid_from_string(string)
|
136
|
+
hash = Digest::SHA1.new
|
137
|
+
hash.update(string)
|
138
|
+
ary = hash.digest.unpack('NnnnnN')
|
139
|
+
ary[2] = (ary[2] & 0x0FFF) | (5 << 12)
|
140
|
+
ary[3] = (ary[3] & 0x3FFF) | 0x8000
|
141
|
+
# rubocop:disable Style/FormatString
|
142
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % ary
|
143
|
+
end
|
90
144
|
end
|
91
145
|
end
|
@@ -75,5 +75,46 @@ module Train::Platforms::Detect::Helpers
|
|
75
75
|
arch_number = sys_info[:Architecture].to_i
|
76
76
|
arch_map[arch_number]
|
77
77
|
end
|
78
|
+
|
79
|
+
# This method scans the target os for a unique uuid to use
|
80
|
+
def windows_uuid
|
81
|
+
uuid = windows_uuid_from_chef
|
82
|
+
uuid = windows_uuid_from_machine_file if uuid.nil?
|
83
|
+
uuid = windows_uuid_from_wmic if uuid.nil?
|
84
|
+
uuid = windows_uuid_from_registry if uuid.nil?
|
85
|
+
raise Train::TransportError, 'Cannot find a UUID for your node.' if uuid.nil?
|
86
|
+
uuid
|
87
|
+
end
|
88
|
+
|
89
|
+
def windows_uuid_from_machine_file
|
90
|
+
%W(
|
91
|
+
#{ENV['SYSTEMDRIVE']}\\chef\\chef_guid
|
92
|
+
#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}\\.chef\\chef_guid
|
93
|
+
).each do |path|
|
94
|
+
file = @backend.file(path)
|
95
|
+
return file.content.chomp if file.exist? && !file.size.zero?
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def windows_uuid_from_chef
|
101
|
+
file = @backend.file("#{ENV['SYSTEMDRIVE']}\\chef\\cache\\data_collector_metadata.json")
|
102
|
+
return if !file.exist? || file.size.zero?
|
103
|
+
json = JSON.parse(file.content)
|
104
|
+
json['node_uuid'] if json['node_uuid']
|
105
|
+
end
|
106
|
+
|
107
|
+
def windows_uuid_from_wmic
|
108
|
+
result = @backend.run_command('wmic csproduct get UUID')
|
109
|
+
return unless result.exit_status.zero?
|
110
|
+
result.stdout.split("\r\n")[-1].strip
|
111
|
+
end
|
112
|
+
|
113
|
+
def windows_uuid_from_registry
|
114
|
+
cmd = '(Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography" -Name "MachineGuid")."MachineGuid"'
|
115
|
+
result = @backend.run_command(cmd)
|
116
|
+
return unless result.exit_status.zero?
|
117
|
+
result.stdout.chomp
|
118
|
+
end
|
78
119
|
end
|
79
120
|
end
|
@@ -449,6 +449,7 @@ module Train::Platforms::Detect::Specifications
|
|
449
449
|
plat.name('mac_os_x').title('macOS X').in_family('darwin')
|
450
450
|
.detect {
|
451
451
|
cmd = unix_file_contents('/System/Library/CoreServices/SystemVersion.plist')
|
452
|
+
@platform[:uuid_command] = "system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }'"
|
452
453
|
true if cmd =~ /Mac OS X/i
|
453
454
|
}
|
454
455
|
plat.name('darwin').title('Darwin').in_family('darwin')
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Train::Platforms::Detect
|
8
|
+
class UUID
|
9
|
+
include Train::Platforms::Detect::Helpers::OSCommon
|
10
|
+
|
11
|
+
def initialize(platform)
|
12
|
+
@platform = platform
|
13
|
+
@backend = @platform.backend
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_or_create_uuid
|
17
|
+
# for api transports uuid is defined on the connection
|
18
|
+
if defined?(@backend.unique_identifier)
|
19
|
+
uuid_from_string(@backend.unique_identifier)
|
20
|
+
elsif @platform.unix?
|
21
|
+
unix_uuid
|
22
|
+
elsif @platform.windows?
|
23
|
+
windows_uuid
|
24
|
+
else
|
25
|
+
if @platform[:uuid_command]
|
26
|
+
result = @backend.run_command(@platform[:uuid_command])
|
27
|
+
return uuid_from_string(result.stdout.chomp) if result.exit_status.zero? && !result.stdout.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
raise 'Could not find platform uuid! Please set a uuid_command for your platform.'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/train/transports/aws.rb
CHANGED
@@ -59,6 +59,13 @@ module Train::Transports
|
|
59
59
|
def uri
|
60
60
|
"aws://#{@options[:region]}"
|
61
61
|
end
|
62
|
+
|
63
|
+
def unique_identifier
|
64
|
+
# use aws account id
|
65
|
+
client = aws_client(::Aws::IAM::Client)
|
66
|
+
arn = client.get_user.user.arn
|
67
|
+
arn.split(':')[4]
|
68
|
+
end
|
62
69
|
end
|
63
70
|
end
|
64
71
|
end
|
@@ -4,6 +4,8 @@ require 'train/plugins'
|
|
4
4
|
require 'ms_rest_azure'
|
5
5
|
require 'azure_mgmt_resources'
|
6
6
|
require 'inifile'
|
7
|
+
require 'socket'
|
8
|
+
require 'timeout'
|
7
9
|
|
8
10
|
module Train::Transports
|
9
11
|
class Azure < Train.plugin(1)
|
@@ -12,6 +14,7 @@ module Train::Transports
|
|
12
14
|
option :client_id, default: ENV['AZURE_CLIENT_ID']
|
13
15
|
option :client_secret, default: ENV['AZURE_CLIENT_SECRET']
|
14
16
|
option :subscription_id, default: ENV['AZURE_SUBSCRIPTION_ID']
|
17
|
+
option :msi_port, default: ENV['AZURE_MSI_PORT'] || '50342'
|
15
18
|
|
16
19
|
# This can provide the client id and secret
|
17
20
|
option :credentials_file, default: ENV['AZURE_CRED_FILE']
|
@@ -21,6 +24,8 @@ module Train::Transports
|
|
21
24
|
end
|
22
25
|
|
23
26
|
class Connection < BaseConnection
|
27
|
+
attr_reader :options
|
28
|
+
|
24
29
|
def initialize(options)
|
25
30
|
@apis = {}
|
26
31
|
|
@@ -36,6 +41,8 @@ module Train::Transports
|
|
36
41
|
parse_credentials_file
|
37
42
|
end
|
38
43
|
|
44
|
+
@options[:msi_port] = @options[:msi_port].to_i unless @options[:msi_port].nil?
|
45
|
+
|
39
46
|
# additional platform details
|
40
47
|
release = Gem.loaded_specs['azure_mgmt_resources'].version
|
41
48
|
@platform_details = { release: "azure_mgmt_resources-v#{release}" }
|
@@ -54,19 +61,24 @@ module Train::Transports
|
|
54
61
|
end
|
55
62
|
|
56
63
|
def connect
|
57
|
-
|
58
|
-
|
59
|
-
@options[:
|
60
|
-
|
61
|
-
|
64
|
+
if @options[:client_id].nil? && @options[:client_secret].nil? && port_open?(@options[:msi_port])
|
65
|
+
# try using MSI connection
|
66
|
+
provider = ::MsRestAzure::MSITokenProvider.new(@options[:msi_port])
|
67
|
+
else
|
68
|
+
provider = ::MsRestAzure::ApplicationTokenProvider.new(
|
69
|
+
@options[:tenant_id],
|
70
|
+
@options[:client_id],
|
71
|
+
@options[:client_secret],
|
72
|
+
)
|
73
|
+
end
|
62
74
|
|
63
75
|
@credentials = {
|
64
76
|
credentials: ::MsRest::TokenCredentials.new(provider),
|
65
77
|
subscription_id: @options[:subscription_id],
|
66
78
|
tenant_id: @options[:tenant_id],
|
67
|
-
client_id: @options[:client_id],
|
68
|
-
client_secret: @options[:client_secret],
|
69
79
|
}
|
80
|
+
@credentials[:client_id] = @options[:client_id] unless @options[:client_id].nil?
|
81
|
+
@credentials[:client_secret] = @options[:client_secret] unless @options[:client_secret].nil?
|
70
82
|
end
|
71
83
|
|
72
84
|
def uri
|
@@ -118,8 +130,25 @@ module Train::Transports
|
|
118
130
|
@apis[resource_type]
|
119
131
|
end
|
120
132
|
|
133
|
+
def unique_identifier
|
134
|
+
options[:subscription_id] || options[:tenant_id]
|
135
|
+
end
|
136
|
+
|
121
137
|
private
|
122
138
|
|
139
|
+
def port_open?(port, seconds = 1)
|
140
|
+
Timeout.timeout(seconds) do
|
141
|
+
begin
|
142
|
+
TCPSocket.new('localhost', port).close
|
143
|
+
true
|
144
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
rescue Timeout::Error
|
149
|
+
false
|
150
|
+
end
|
151
|
+
|
123
152
|
def parse_credentials_file # rubocop:disable Metrics/AbcSize
|
124
153
|
# If an AZURE_CRED_FILE environment variable has been specified set the
|
125
154
|
# the credentials file to that, otherwise set the one in home
|
@@ -0,0 +1,140 @@
|
|
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
|
@@ -4,13 +4,14 @@
|
|
4
4
|
# author: Christoph Hartmann
|
5
5
|
|
6
6
|
require 'train/plugins'
|
7
|
+
require 'train/errors'
|
7
8
|
require 'mixlib/shellout'
|
8
9
|
|
9
10
|
module Train::Transports
|
10
11
|
class Local < Train.plugin(1)
|
11
12
|
name 'local'
|
12
13
|
|
13
|
-
class PipeError < ::
|
14
|
+
class PipeError < Train::TransportError; end
|
14
15
|
|
15
16
|
def connection(_ = nil)
|
16
17
|
@connection ||= Connection.new(@options)
|
data/lib/train/version.rb
CHANGED
@@ -0,0 +1,133 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'helper'
|
4
|
+
require 'train/transports/mock'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
class TestFile
|
8
|
+
def initialize(string)
|
9
|
+
@string = string
|
10
|
+
end
|
11
|
+
|
12
|
+
def exist?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def size
|
17
|
+
@string.length
|
18
|
+
end
|
19
|
+
|
20
|
+
def content
|
21
|
+
@string
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'uuid' do
|
26
|
+
def mock_platform(name, commands = {}, files = {}, plat_options = {})
|
27
|
+
Train::Platforms.list[name] = nil
|
28
|
+
mock = Train::Transports::Mock::Connection.new
|
29
|
+
commands.each do |command, data|
|
30
|
+
mock.mock_command(command, data)
|
31
|
+
end
|
32
|
+
|
33
|
+
file_objects = {}
|
34
|
+
files.each do |path, content|
|
35
|
+
file_objects[path] = TestFile.new(content)
|
36
|
+
end
|
37
|
+
|
38
|
+
mock.files = file_objects
|
39
|
+
mock.direct_platform(name, plat_options)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'finds a linux uuid from chef entity_uuid' do
|
43
|
+
files = { '/var/chef/cache/data_collector_metadata.json' => '{"node_uuid":"d400073f-0920-41aa-8dd3-2ea59b18f5ce"}' }
|
44
|
+
plat = mock_platform('linux', {}, files)
|
45
|
+
plat.uuid.must_equal 'd400073f-0920-41aa-8dd3-2ea59b18f5ce'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'finds a windows uuid from chef entity_uuid' do
|
49
|
+
ENV['SYSTEMDRIVE'] = 'C:'
|
50
|
+
files = { 'C:\chef\cache\data_collector_metadata.json' => '{"node_uuid":"d400073f-0920-41aa-8dd3-2ea59b18f5ce"}' }
|
51
|
+
plat = mock_platform('windows', {}, files)
|
52
|
+
plat.uuid.must_equal 'd400073f-0920-41aa-8dd3-2ea59b18f5ce'
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'finds a linux uuid from /etc/chef/chef_guid' do
|
56
|
+
files = { '/etc/chef/chef_guid' => '5e430326-b5aa-56f8-975f-c3ca1c21df91' }
|
57
|
+
plat = mock_platform('linux', {}, files)
|
58
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'finds a linux uuid from /home/testuser/.chef/chef_guid' do
|
62
|
+
ENV['HOME'] = '/home/testuser'
|
63
|
+
files = { '/home/testuser/.chef/chef_guid' => '5e430326-b5aa-56f8-975f-c3ca1c21df91' }
|
64
|
+
plat = mock_platform('linux', {}, files)
|
65
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'finds a linux uuid from /etc/machine-id' do
|
69
|
+
files = { '/etc/machine-id' => '123141dsfadf' }
|
70
|
+
plat = mock_platform('linux', {}, files)
|
71
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'finds a linux uuid from /var/lib/dbus/machine-id' do
|
75
|
+
files = {
|
76
|
+
'/etc/machine-id' => '',
|
77
|
+
'/var/lib/dbus/machine-id' => '123141dsfadf',
|
78
|
+
}
|
79
|
+
plat = mock_platform('linux', {}, files)
|
80
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'finds a linux uuid from /etc/machine-id' do
|
84
|
+
files = { '/etc/machine-id' => '123141dsfadf' }
|
85
|
+
plat = mock_platform('linux', {}, files)
|
86
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'finds a windows uuid from wmic' do
|
90
|
+
commands = { 'wmic csproduct get UUID' => "UUID\r\nd400073f-0920-41aa-8dd3-2ea59b18f5ce\r\n" }
|
91
|
+
plat = mock_platform('windows', commands)
|
92
|
+
plat.uuid.must_equal 'd400073f-0920-41aa-8dd3-2ea59b18f5ce'
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'finds a windows uuid from registry' do
|
96
|
+
commands = { '(Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography" -Name "MachineGuid")."MachineGuid"' => "d400073f-0920-41aa-8dd3-2ea59b18f5ce\r\n" }
|
97
|
+
plat = mock_platform('windows', commands)
|
98
|
+
plat.uuid.must_equal 'd400073f-0920-41aa-8dd3-2ea59b18f5ce'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'finds a windows uuid from C:\chef\chef_guid' do
|
102
|
+
ENV['SYSTEMDRIVE'] = 'C:'
|
103
|
+
files = { 'C:\chef\chef_guid' => '5e430326-b5aa-56f8-975f-c3ca1c21df91' }
|
104
|
+
plat = mock_platform('windows', {}, files)
|
105
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'finds a windows uuid from C:\Users\test\.chef\chef_guid' do
|
109
|
+
ENV['HOMEDRIVE'] = 'C:\\'
|
110
|
+
ENV['HOMEPATH'] = 'Users\test'
|
111
|
+
files = { 'C:\Users\test\.chef\chef_guid' => '5e430326-b5aa-56f8-975f-c3ca1c21df91' }
|
112
|
+
plat = mock_platform('windows', {}, files)
|
113
|
+
plat.uuid.must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'generates a uuid from a string' do
|
117
|
+
plat = mock_platform('linux')
|
118
|
+
uuid = Train::Platforms::Detect::UUID.new(plat)
|
119
|
+
uuid.uuid_from_string('123141dsfadf').must_equal '5e430326-b5aa-56f8-975f-c3ca1c21df91'
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'finds a aws uuid' do
|
123
|
+
plat = mock_platform('aws')
|
124
|
+
plat.backend.stubs(:unique_identifier).returns('158551926027')
|
125
|
+
plat.uuid.must_equal '1d74ce61-ac15-5c48-9ee3-5aa8207ac37f'
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'finds an azure uuid' do
|
129
|
+
plat = mock_platform('azure')
|
130
|
+
plat.backend.stubs(:unique_identifier).returns('1d74ce61-ac15-5c48-9ee3-5aa8207ac37f')
|
131
|
+
plat.uuid.must_equal '2c2e4fa9-7287-5dee-85a3-6527face7b7b'
|
132
|
+
end
|
133
|
+
end
|
@@ -96,4 +96,28 @@ describe 'aws transport' do
|
|
96
96
|
ENV['AWS_REGION'].must_equal 'xyz'
|
97
97
|
end
|
98
98
|
end
|
99
|
+
|
100
|
+
describe 'unique_identifier' do
|
101
|
+
class AwsArn
|
102
|
+
def arn
|
103
|
+
'arn:aws:iam::158551926027:user/test-fixture-maker'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class AwsUser
|
108
|
+
def user
|
109
|
+
AwsArn.new
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class AwsClient
|
114
|
+
def get_user
|
115
|
+
AwsUser.new
|
116
|
+
end
|
117
|
+
end
|
118
|
+
it 'returns an account id' do
|
119
|
+
connection.stubs(:aws_client).returns(AwsClient.new)
|
120
|
+
connection.unique_identifier.must_equal '158551926027'
|
121
|
+
end
|
122
|
+
end
|
99
123
|
end
|
@@ -83,12 +83,44 @@ describe 'azure transport' do
|
|
83
83
|
describe 'connect' do
|
84
84
|
it 'validate credentials' do
|
85
85
|
connection.connect
|
86
|
+
token = credentials[:credentials].instance_variable_get(:@token_provider)
|
87
|
+
token.class.must_equal MsRestAzure::ApplicationTokenProvider
|
88
|
+
|
86
89
|
credentials[:credentials].class.must_equal MsRest::TokenCredentials
|
87
90
|
credentials[:tenant_id].must_equal 'test_tenant_id'
|
88
91
|
credentials[:client_id].must_equal 'test_client_id'
|
89
92
|
credentials[:client_secret].must_equal 'test_client_secret'
|
90
93
|
credentials[:subscription_id].must_equal 'test_subscription_id'
|
91
94
|
end
|
95
|
+
|
96
|
+
it 'validate msi credentials' do
|
97
|
+
options[:client_id] = nil
|
98
|
+
options[:client_secret] = nil
|
99
|
+
Train::Transports::Azure::Connection.any_instance.stubs(:port_open?).returns(true)
|
100
|
+
|
101
|
+
connection.connect
|
102
|
+
token = credentials[:credentials].instance_variable_get(:@token_provider)
|
103
|
+
token.class.must_equal MsRestAzure::MSITokenProvider
|
104
|
+
|
105
|
+
credentials[:credentials].class.must_equal MsRest::TokenCredentials
|
106
|
+
credentials[:tenant_id].must_equal 'test_tenant_id'
|
107
|
+
credentials[:subscription_id].must_equal 'test_subscription_id'
|
108
|
+
credentials[:client_id].must_be_nil
|
109
|
+
credentials[:client_secret].must_be_nil
|
110
|
+
options[:msi_port].must_equal 50342
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'unique_identifier' do
|
115
|
+
it 'returns a subscription id' do
|
116
|
+
connection.unique_identifier.must_equal 'test_subscription_id'
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'returns a tenant id' do
|
120
|
+
options = connection.instance_variable_get(:@options)
|
121
|
+
options[:subscription_id] = nil
|
122
|
+
connection.unique_identifier.must_equal 'test_tenant_id'
|
123
|
+
end
|
92
124
|
end
|
93
125
|
|
94
126
|
describe 'parse_credentials_file' do
|
@@ -0,0 +1,94 @@
|
|
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
|
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.3.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-03-
|
11
|
+
date: 2018-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -217,6 +217,7 @@ files:
|
|
217
217
|
- lib/train/platforms/detect/scanner.rb
|
218
218
|
- lib/train/platforms/detect/specifications/api.rb
|
219
219
|
- lib/train/platforms/detect/specifications/os.rb
|
220
|
+
- lib/train/platforms/detect/uuid.rb
|
220
221
|
- lib/train/platforms/family.rb
|
221
222
|
- lib/train/platforms/platform.rb
|
222
223
|
- lib/train/plugins.rb
|
@@ -224,6 +225,7 @@ files:
|
|
224
225
|
- lib/train/plugins/transport.rb
|
225
226
|
- lib/train/transports/aws.rb
|
226
227
|
- lib/train/transports/azure.rb
|
228
|
+
- lib/train/transports/cisco_ios.rb
|
227
229
|
- lib/train/transports/docker.rb
|
228
230
|
- lib/train/transports/local.rb
|
229
231
|
- lib/train/transports/mock.rb
|
@@ -279,6 +281,7 @@ files:
|
|
279
281
|
- test/unit/platforms/detect/os_linux_test.rb
|
280
282
|
- test/unit/platforms/detect/os_windows_test.rb
|
281
283
|
- test/unit/platforms/detect/scanner_test.rb
|
284
|
+
- test/unit/platforms/detect/uuid_test.rb
|
282
285
|
- test/unit/platforms/family_test.rb
|
283
286
|
- test/unit/platforms/os_detect_test.rb
|
284
287
|
- test/unit/platforms/platform_test.rb
|
@@ -289,6 +292,7 @@ files:
|
|
289
292
|
- test/unit/train_test.rb
|
290
293
|
- test/unit/transports/aws_test.rb
|
291
294
|
- test/unit/transports/azure_test.rb
|
295
|
+
- test/unit/transports/cisco_ios.rb
|
292
296
|
- test/unit/transports/local_test.rb
|
293
297
|
- test/unit/transports/mock_test.rb
|
294
298
|
- test/unit/transports/ssh_test.rb
|
@@ -368,6 +372,7 @@ test_files:
|
|
368
372
|
- test/unit/platforms/detect/os_linux_test.rb
|
369
373
|
- test/unit/platforms/detect/os_windows_test.rb
|
370
374
|
- test/unit/platforms/detect/scanner_test.rb
|
375
|
+
- test/unit/platforms/detect/uuid_test.rb
|
371
376
|
- test/unit/platforms/family_test.rb
|
372
377
|
- test/unit/platforms/os_detect_test.rb
|
373
378
|
- test/unit/platforms/platform_test.rb
|
@@ -378,6 +383,7 @@ test_files:
|
|
378
383
|
- test/unit/train_test.rb
|
379
384
|
- test/unit/transports/aws_test.rb
|
380
385
|
- test/unit/transports/azure_test.rb
|
386
|
+
- test/unit/transports/cisco_ios.rb
|
381
387
|
- test/unit/transports/local_test.rb
|
382
388
|
- test/unit/transports/mock_test.rb
|
383
389
|
- test/unit/transports/ssh_test.rb
|