train 1.2.0 → 1.3.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 +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
|