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 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