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 +4 -4
- data/CHANGELOG.md +14 -2
- data/lib/train/extras/command_wrapper.rb +0 -41
- data/lib/train/platforms/detect/specifications/os.rb +1 -0
- data/lib/train/transports/local.rb +142 -10
- data/lib/train/version.rb +1 -1
- data/test/unit/platforms/os_detect_test.rb +2 -2
- data/test/unit/transports/local_test.rb +37 -2
- data/test/windows/local_test.rb +35 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dfe0c95ba3361d579100ba1b832fb9f01f584267
|
4
|
+
data.tar.gz: d8449a2875c73e05e5e1f188eb530e25d184a89e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
4
|
-
[Full Changelog](https://github.com/chef/train/compare/v0.
|
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
|
@@ -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
|
-
|
23
|
-
|
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
|
-
|
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
@@ -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' => '
|
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('
|
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(
|
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
|
data/test/windows/local_test.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
date: 2017-12-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|