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