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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 124fd4ed4c208f576481fb0b295fb8db93d9dd21
4
- data.tar.gz: '0680e4d022b89b9e94abcd2f76ec830c69936ec9'
3
+ metadata.gz: '095b1ae4856669cbe224dffbfa09f555ce7c0d05'
4
+ data.tar.gz: ee6cc55a500e4466e497e853e49963a05b1f85d2
5
5
  SHA512:
6
- metadata.gz: 7a82299c5aea2a11cbaf596a8aaa6bee503e7a169c86934b7af9a379ae70fc72fddbde527dcea1b717b08795d270a8092a223f72d7b582e8ded18b5c99180d56
7
- data.tar.gz: db232796a60f74458d26ceff2273e72a233fcb14f794b03c6c9e45dfda7ba84e0632005a2c3c6df2ef9b2d7e816044f9589003cea87c890ea0e369fdc9f68838
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.2.0](https://github.com/chef/train/tree/1.2.0) (2018-03-15)
4
- [Full Changelog](https://github.com/chef/train/compare/v1.1.1...1.2.0)
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 < ::StandardError; end
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 < ::StandardError; end
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 < ::StandardError; end
25
+ class TransportError < Error; end
23
26
 
24
27
  # Exception for when no platform can be detected.
25
- class PlatformDetectionFailed < ::StandardError; end
28
+ class PlatformDetectionFailed < Error; end
26
29
 
27
30
  # Exception for when a invalid cache type is passed.
28
- class UnknownCacheType < ::StandardError; end
31
+ class UnknownCacheType < Error; end
29
32
  end
@@ -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
@@ -41,6 +41,10 @@ module Train::Platforms
41
41
  end
42
42
  end
43
43
 
44
+ def uuid
45
+ @uuid ||= Train::Platforms::Detect::UUID.new(self).find_or_create_uuid.downcase
46
+ end
47
+
44
48
  # This is for backwords compatability with
45
49
  # the current inspec os resource.
46
50
  def[](name)
@@ -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
- provider = ::MsRestAzure::ApplicationTokenProvider.new(
58
- @options[:tenant_id],
59
- @options[:client_id],
60
- @options[:client_secret],
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 < ::StandardError; end
14
+ class PipeError < Train::TransportError; end
14
15
 
15
16
  def connection(_ = nil)
16
17
  @connection ||= Connection.new(@options)
@@ -57,6 +57,8 @@ end
57
57
 
58
58
  class Train::Transports::Mock
59
59
  class Connection < BaseConnection
60
+ attr_reader :options
61
+
60
62
  def initialize(conf = nil)
61
63
  super(conf)
62
64
  mock_os
data/lib/train/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
4
 
5
5
  module Train
6
- VERSION = '1.2.0'.freeze
6
+ VERSION = '1.3.0'.freeze
7
7
  end
@@ -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.2.0
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-15 00:00:00.000000000 Z
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