train 0.30.0 → 0.31.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: 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