train 0.30.0 → 0.31.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: 0e4971c8f699dbb772e9eb5532fa2de1771de4a5
4
- data.tar.gz: 40bf72dd2c53b33379c256b4a4f4acc42dc46efd
3
+ metadata.gz: dfe0c95ba3361d579100ba1b832fb9f01f584267
4
+ data.tar.gz: d8449a2875c73e05e5e1f188eb530e25d184a89e
5
5
  SHA512:
6
- metadata.gz: 3c5fdd0a5d53e68d3eb928d5e9ddcbcaea5a8c3fa39b57d7aa3a20f7135f4436d78e334e4d93b3ce1e8f2fd2a3310462d10ae4099899372bd4f4fbd6309af4e4
7
- data.tar.gz: db6f8f5c82f459a9f78c60cf758aaad8c4877e5adee714add3e060e5328c85a4275ef3502aae0525d80a49b0100b92fea2345f5f493fddf2ecc766954dae9c44
6
+ metadata.gz: b1239cd1ece638eb4273037034fe1ba7e3c1c864674f53d89f7a7dfdf6f4683eaf54445b5ff92ab1b2e482fbb5743143365111f516bcd1f4f0f3ae4e6f150a0b
7
+ data.tar.gz: 9c6d7296f8396a5dbac8323ef29eeb247af3c2cf4cd285ee44dafddaf7e515af921d7288360381ebfb3ac5bcca9edee08663efd0ea570353d6fa1562840bed94
data/CHANGELOG.md CHANGED
@@ -1,10 +1,22 @@
1
1
  # Change Log
2
2
 
3
- ## [0.30.0](https://github.com/chef/train/tree/0.30.0) (2017-12-04)
4
- [Full Changelog](https://github.com/chef/train/compare/v0.29.2...0.30.0)
3
+ ## [0.31.0](https://github.com/chef/train/tree/0.31.0) (2017-12-05)
4
+ [Full Changelog](https://github.com/chef/train/compare/v0.30.0...0.31.0)
5
+
6
+ **Fixed bugs:**
7
+
8
+ - Add release detect for failback debian [\#223](https://github.com/chef/train/pull/223) ([jquick](https://github.com/jquick))
9
+
10
+ **Merged pull requests:**
11
+
12
+ - Use named pipe to decrease local Windows runtime [\#220](https://github.com/chef/train/pull/220) ([jerryaldrichiii](https://github.com/jerryaldrichiii))
13
+
14
+ ## [v0.30.0](https://github.com/chef/train/tree/v0.30.0) (2017-12-04)
15
+ [Full Changelog](https://github.com/chef/train/compare/v0.29.2...v0.30.0)
5
16
 
6
17
  **Merged pull requests:**
7
18
 
19
+ - Release 0.30.0 [\#222](https://github.com/chef/train/pull/222) ([adamleff](https://github.com/adamleff))
8
20
  - Change the mock transport name to be 'mock' [\#221](https://github.com/chef/train/pull/221) ([jquick](https://github.com/jquick))
9
21
  - Enable caching on connections [\#214](https://github.com/chef/train/pull/214) ([jquick](https://github.com/jquick))
10
22
 
@@ -121,43 +121,6 @@ module Train::Extras
121
121
  end
122
122
  end
123
123
 
124
- # this is required if you run locally on windows,
125
- # winrm connections provide a PowerShell shell automatically
126
- # TODO: only activate in local mode
127
- class PowerShellCommand < CommandWrapperBase
128
- Train::Options.attach(self)
129
-
130
- def initialize(backend, options)
131
- @backend = backend
132
- validate_options(options)
133
- end
134
-
135
- def run(script)
136
- # wrap the script to ensure we always run it via powershell
137
- # especially in local mode, we cannot be sure that we get a Powershell
138
- # we may just get a `cmd`.
139
- # TODO: we may want to opt for powershell.exe -command instead of `encodeCommand`
140
- "powershell -NoProfile -encodedCommand #{encoded(safe_script(script))}"
141
- end
142
-
143
- # suppress the progress stream from leaking to stderr
144
- def safe_script(script)
145
- "$ProgressPreference='SilentlyContinue';" + script
146
- end
147
-
148
- # Encodes the script so that it can be passed to the PowerShell
149
- # --EncodedCommand argument.
150
- # @return [String] The UTF-16LE base64 encoded script
151
- def encoded(script)
152
- encoded_script = safe_script(script).encode('UTF-16LE', 'UTF-8')
153
- Base64.strict_encode64(encoded_script)
154
- end
155
-
156
- def to_s
157
- 'PowerShell CommandWrapper'
158
- end
159
- end
160
-
161
124
  class CommandWrapper
162
125
  include_options LinuxCommand
163
126
 
@@ -168,10 +131,6 @@ module Train::Extras
168
131
  msg = res.verify
169
132
  fail Train::UserError, "Sudo failed: #{msg}" unless msg.nil?
170
133
  res
171
- # only use powershell command for local transport. winrm transport
172
- # uses powershell as default
173
- elsif transport.os.windows? && transport.class == Train::Transports::Local::Connection
174
- PowerShellCommand.new(transport, options)
175
134
  end
176
135
  end
177
136
  end
@@ -110,6 +110,7 @@ module Train::Platforms::Detect::Specifications
110
110
  end
111
111
 
112
112
  # if we get this far we have to be some type of debian
113
+ @platform[:release] = unix_file_contents('/etc/debian_version').chomp
113
114
  true
114
115
  }
115
116
 
@@ -12,6 +12,8 @@ module Train::Transports
12
12
 
13
13
  include_options Train::Extras::CommandWrapper
14
14
 
15
+ class PipeError < ::StandardError; end
16
+
15
17
  def connection(_ = nil)
16
18
  @connection ||= Connection.new(@options)
17
19
  end
@@ -19,8 +21,23 @@ module Train::Transports
19
21
  class Connection < BaseConnection
20
22
  def initialize(options)
21
23
  super(options)
22
- @cmd_wrapper = nil
23
- @cmd_wrapper = CommandWrapper.load(self, options)
24
+
25
+ # While OS is being discovered, use the GenericRunner
26
+ @runner = GenericRunner.new
27
+ @runner.cmd_wrapper = CommandWrapper.load(self, options)
28
+
29
+ if os.windows?
30
+ # Attempt to use a named pipe but fallback to ShellOut if that fails
31
+ begin
32
+ @runner = WindowsPipeRunner.new
33
+ rescue PipeError
34
+ @runner = WindowsShellRunner.new
35
+ end
36
+ end
37
+ end
38
+
39
+ def local?
40
+ true
24
41
  end
25
42
 
26
43
  def login_command
@@ -31,17 +48,10 @@ module Train::Transports
31
48
  'local://'
32
49
  end
33
50
 
34
- def local?
35
- true
36
- end
37
-
38
51
  private
39
52
 
40
53
  def run_command_via_connection(cmd)
41
- cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
42
- res = Mixlib::ShellOut.new(cmd)
43
- res.run_command
44
- CommandResult.new(res.stdout, res.stderr, res.exitstatus)
54
+ @runner.run_command(cmd)
45
55
  rescue Errno::ENOENT => _
46
56
  CommandResult.new('', '', 1)
47
57
  end
@@ -53,6 +63,128 @@ module Train::Transports
53
63
  Train::File::Local::Unix.new(self, path)
54
64
  end
55
65
  end
66
+
67
+ class GenericRunner
68
+ attr_writer :cmd_wrapper
69
+
70
+ def run_command(cmd)
71
+ if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
72
+ cmd = @cmd_wrapper.run(cmd)
73
+ end
74
+
75
+ res = Mixlib::ShellOut.new(cmd)
76
+ res.run_command
77
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
78
+ end
79
+ end
80
+
81
+ class WindowsShellRunner
82
+ require 'json'
83
+ require 'base64'
84
+
85
+ def run_command(script)
86
+ # Prevent progress stream from leaking into stderr
87
+ script = "$ProgressPreference='SilentlyContinue';" + script
88
+
89
+ # Encode script so PowerShell can use it
90
+ script = script.encode('UTF-16LE', 'UTF-8')
91
+ base64_script = Base64.strict_encode64(script)
92
+
93
+ cmd = "powershell -NoProfile -EncodedCommand #{base64_script}"
94
+
95
+ res = Mixlib::ShellOut.new(cmd)
96
+ res.run_command
97
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
98
+ end
99
+ end
100
+
101
+ class WindowsPipeRunner
102
+ require 'json'
103
+ require 'base64'
104
+ require 'securerandom'
105
+
106
+ def initialize
107
+ @pipe = acquire_pipe
108
+ fail PipeError if @pipe.nil?
109
+ end
110
+
111
+ def run_command(cmd)
112
+ script = "$ProgressPreference='SilentlyContinue';" + cmd
113
+ encoded_script = Base64.strict_encode64(script)
114
+ @pipe.puts(encoded_script)
115
+ @pipe.flush
116
+ res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
117
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
118
+ end
119
+
120
+ private
121
+
122
+ def acquire_pipe
123
+ pipe_name = "inspec_#{SecureRandom.hex}"
124
+
125
+ start_pipe_server(pipe_name)
126
+
127
+ pipe = nil
128
+
129
+ # PowerShell needs time to create pipe.
130
+ 100.times do
131
+ begin
132
+ pipe = open("//./pipe/#{pipe_name}", 'r+')
133
+ break
134
+ rescue
135
+ sleep 0.1
136
+ end
137
+ end
138
+
139
+ pipe
140
+ end
141
+
142
+ def start_pipe_server(pipe_name)
143
+ require 'win32/process'
144
+
145
+ script = <<-EOF
146
+ $ErrorActionPreference = 'Stop'
147
+
148
+ $pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}')
149
+ $pipeReader = New-Object System.IO.StreamReader($pipeServer)
150
+ $pipeWriter = New-Object System.IO.StreamWriter($pipeServer)
151
+
152
+ $pipeServer.WaitForConnection()
153
+
154
+ # Create loop to receive and process user commands/scripts
155
+ $clientConnected = $true
156
+ while($clientConnected) {
157
+ $input = $pipeReader.ReadLine()
158
+ $command = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($input))
159
+
160
+ # Execute user command/script and convert result to JSON
161
+ $scriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($command)
162
+ try {
163
+ $stdout = & $scriptBlock | Out-String
164
+ $result = @{ 'stdout' = $stdout ; 'stderr' = ''; 'exitstatus' = 0 }
165
+ } catch {
166
+ $stderr = $_ | Out-String
167
+ $result = @{ 'stdout' = ''; 'stderr' = $_; 'exitstatus' = 1 }
168
+ }
169
+ $resultJSON = $result | ConvertTo-JSON
170
+
171
+ # Encode JSON in Base64 and write to pipe
172
+ $encodedResult = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($resultJSON))
173
+ $pipeWriter.WriteLine($encodedResult)
174
+ $pipeWriter.Flush()
175
+ }
176
+ EOF
177
+
178
+ utf8_script = script.encode('UTF-16LE', 'UTF-8')
179
+ base64_script = Base64.strict_encode64(utf8_script)
180
+ cmd = "powershell -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
181
+
182
+ server_pid = Process.create(command_line: cmd).process_id
183
+
184
+ # Ensure process is killed when the Train process exits
185
+ at_exit { Process.kill('KILL', server_pid) }
186
+ end
187
+ end
56
188
  end
57
189
  end
58
190
  end
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 = '0.30.0'.freeze
6
+ VERSION = '0.31.0'.freeze
7
7
  end
@@ -79,7 +79,7 @@ describe 'os_detect' do
79
79
  lsb_release = "DISTRIB_ID=#{id}\nDISTRIB_RELEASE=#{version}"
80
80
  files = {
81
81
  '/etc/lsb-release' => lsb_release,
82
- '/etc/debian_version' => 'data',
82
+ '/etc/debian_version' => '11',
83
83
  }
84
84
  scan_with_files('linux', files)
85
85
  end
@@ -124,7 +124,7 @@ describe 'os_detect' do
124
124
 
125
125
  platform[:name].must_equal('debian')
126
126
  platform[:family].must_equal('debian')
127
- platform[:release].must_equal('12.99')
127
+ platform[:release].must_equal('11')
128
128
  end
129
129
  end
130
130
  end
@@ -6,9 +6,11 @@ require 'train/transports/local'
6
6
  class TransportHelper
7
7
  attr_accessor :transport
8
8
 
9
- def initialize
9
+ def initialize(user_opts = {})
10
+ opts = {platform_name: 'mock', family_hierarchy: ['mock']}.merge(user_opts)
10
11
  Train::Platforms::Detect::Specifications::OS.load
11
- plat = Train::Platforms.name('mock').in_family('linux')
12
+ plat = Train::Platforms.name(opts[:platform_name])
13
+ plat.family_hierarchy = opts[:family_hierarchy]
12
14
  plat.add_platform_methods
13
15
  Train::Platforms::Detect.stubs(:scan).returns(plat)
14
16
  @transport = Train::Transports::Local.new
@@ -93,4 +95,37 @@ describe 'local transport' do
93
95
  end
94
96
  end
95
97
  end
98
+
99
+ describe 'when running on Windows' do
100
+ let(:connection) do
101
+ TransportHelper.new(family_hierarchy: ['windows']).transport.connection
102
+ end
103
+ let(:runner) { mock }
104
+
105
+ it 'uses `WindowsPipeRunner` by default' do
106
+ Train::Transports::Local::Connection::WindowsPipeRunner
107
+ .expects(:new)
108
+ .returns(runner)
109
+
110
+ Train::Transports::Local::Connection::WindowsShellRunner
111
+ .expects(:new)
112
+ .never
113
+
114
+ runner.expects(:run_command).with('not actually executed')
115
+ connection.run_command('not actually executed')
116
+ end
117
+
118
+ it 'uses `WindowsShellRunner` when a named pipe is not available' do
119
+ Train::Transports::Local::Connection::WindowsPipeRunner
120
+ .expects(:new)
121
+ .raises(Train::Transports::Local::PipeError)
122
+
123
+ Train::Transports::Local::Connection::WindowsShellRunner
124
+ .expects(:new)
125
+ .returns(runner)
126
+
127
+ runner.expects(:run_command).with('not actually executed')
128
+ connection.run_command('not actually executed')
129
+ end
130
+ end
96
131
  end
@@ -8,6 +8,9 @@ require 'mocha/setup'
8
8
  require 'train'
9
9
  require 'tempfile'
10
10
 
11
+ # Loading here to ensure methods exist to be stubbed
12
+ require 'train/transports/local'
13
+
11
14
  describe 'windows local command' do
12
15
  let(:conn) {
13
16
  # get final config
@@ -40,6 +43,38 @@ describe 'windows local command' do
40
43
  cmd.stderr.must_equal ''
41
44
  end
42
45
 
46
+ it 'can execute a command using a named pipe' do
47
+ SecureRandom.expects(:hex).returns('via_pipe')
48
+
49
+ Train::Transports::Local::Connection::WindowsShellRunner
50
+ .any_instance
51
+ .expects(:new)
52
+ .never
53
+
54
+ cmd = conn.run_command('Write-Output "Create pipe"')
55
+ File.exist?('//./pipe/inspec_via_pipe').must_equal true
56
+ cmd.stdout.must_equal "Create pipe\r\n"
57
+ cmd.stderr.must_equal ''
58
+ end
59
+
60
+ it 'can execute a command via ShellRunner if pipe creation fails' do
61
+ # By forcing `acquire_pipe` to fail to return a pipe, any attempts to create
62
+ # a `WindowsPipeRunner` object should fail. If we can still run a command,
63
+ # then we know that it was successfully executed by `Mixlib::ShellOut`.
64
+ Train::Transports::Local::Connection::WindowsPipeRunner
65
+ .any_instance
66
+ .expects(:acquire_pipe)
67
+ .at_least_once
68
+ .returns(nil)
69
+
70
+ proc { Train::Transports::Local::Connection::WindowsPipeRunner.new }
71
+ .must_raise(Train::Transports::Local::PipeError)
72
+
73
+ cmd = conn.run_command('Write-Output "test"')
74
+ cmd.stdout.must_equal "test\r\n"
75
+ cmd.stderr.must_equal ''
76
+ end
77
+
43
78
  describe 'file' do
44
79
  before do
45
80
  @temp = Tempfile.new('foo')
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: 0.30.0
4
+ version: 0.31.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: 2017-12-04 00:00:00.000000000 Z
11
+ date: 2017-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json