train-core 3.4.7 → 3.5.5

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
  SHA256:
3
- metadata.gz: e85ed8cc37da145f7dabcff7855b85b9c2a6bc8f216945aeaa693843c07f8b95
4
- data.tar.gz: a4ac06f8b1c2ced955d933ba1a9553d9434c9d255d11ec5d8cc43e14e1cc0469
3
+ metadata.gz: dbe3fad014697c3f0a1d5fb5d3f4ff422f9e552362179f72019f7c2f9784f975
4
+ data.tar.gz: 337a74095aa5e41b4dd4f24ba2e41857190161a035c2adaf809617205525e442
5
5
  SHA512:
6
- metadata.gz: 0b920c8d01dbdd94eae308fcca501cf9dc5f24619f8e023e4c5cedbdd11eebcfc4b4bcb41683435325f3ede4afa5b6a7981164e1cf8242aa472e2e801282c284
7
- data.tar.gz: e3d5e835b15d0cc84f86066fe66aafc7456544f53fbe91d2a66c7341ed4db3b2f6a3d890e98aab26d0fe294af587fc246a89a10c7d56b9f4ac1cf09a99fae9d8
6
+ metadata.gz: a066352b95be252da1de517176e98fe67804484bfb7f9371a23762b04a5050e7a3a7ca814eb27c773c5b23e7eb85a6eb26fd251df67bfd25c500e34a1075f7dc
7
+ data.tar.gz: fdfa95f6c1278dd04760e5dface6b4feacac3d0dd2b77f8844215d6f9b17760967afa482181415368fd8c3ca869e697e205c5abd45913e6cbfb12dcfc93044c1
@@ -15,6 +15,12 @@ module Train
15
15
  nil
16
16
  end
17
17
 
18
+ def content=(new_content)
19
+ ::File.open(@path, "w", encoding: "UTF-8") { |fp| fp.write(new_content) }
20
+
21
+ @content = new_content
22
+ end
23
+
18
24
  def link_path
19
25
  return nil unless symlink?
20
26
 
@@ -1,3 +1,4 @@
1
+ require "base64" unless defined?(Base64)
1
2
  require "shellwords" unless defined?(Shellwords)
2
3
 
3
4
  module Train
@@ -19,6 +20,21 @@ module Train
19
20
  end
20
21
  end
21
22
 
23
+ def content=(new_content)
24
+ execute_result = @backend.run_command("base64 --help")
25
+ if execute_result.exit_status != 0
26
+ raise TransportError, "#{self.class} found no base64 binary for file writes"
27
+ end
28
+
29
+ unix_cmd = format("echo '%<base64>s' | base64 --decode > %<file>s",
30
+ base64: Base64.strict_encode64(new_content),
31
+ file: @spath)
32
+
33
+ @backend.run_command(unix_cmd)
34
+
35
+ @content = new_content
36
+ end
37
+
22
38
  def exist?
23
39
  @exist ||= begin
24
40
  f = @follow_symlink ? "" : " || test -L #{@spath}"
@@ -26,6 +26,16 @@ module Train
26
26
  @content
27
27
  end
28
28
 
29
+ def content=(new_content)
30
+ win_cmd = format('[IO.File]::WriteAllBytes("%<file>s", [Convert]::FromBase64String("%<base64>s"))',
31
+ base64: Base64.strict_encode64(new_content),
32
+ file: @spath)
33
+
34
+ @backend.run_command(win_cmd)
35
+
36
+ @content = new_content
37
+ end
38
+
29
39
  def exist?
30
40
  return @exist if defined?(@exist)
31
41
 
@@ -1,6 +1,7 @@
1
1
  require_relative "../errors"
2
2
  require_relative "../extras"
3
3
  require_relative "../file"
4
+ require "fileutils" unless defined?(FileUtils)
4
5
  require "logger"
5
6
 
6
7
  class Train::Plugins::Transport
@@ -161,6 +162,48 @@ class Train::Plugins::Transport
161
162
  @cache[:file][path] ||= file_via_connection(path, *args)
162
163
  end
163
164
 
165
+ # Uploads local files or directories to remote host.
166
+ #
167
+ # @param locals [Array<String>] paths to local files or directories
168
+ # @param remote [String] path to remote destination
169
+ # @raise [TransportFailed] if the files could not all be uploaded
170
+ # successfully, which may vary by implementation
171
+ def upload(locals, remote)
172
+ unless file(remote).directory?
173
+ raise TransportError, "#{self.class} expects remote directory as second upload parameter"
174
+ end
175
+
176
+ Array(locals).each do |local|
177
+ new_content = File.read(local)
178
+ remote_file = File.join(remote, File.basename(local))
179
+
180
+ logger.debug("Attempting to upload '#{local}' as file #{remote_file}")
181
+
182
+ file(remote_file).content = new_content
183
+ end
184
+ end
185
+
186
+ # Download remote files or directories to local host.
187
+ #
188
+ # @param remotes [Array<String>] paths to remote files or directories
189
+ # @param local [String] path to local destination. If `local` is an
190
+ # existing directory, `remote` will be downloaded into the directory
191
+ # using its original name
192
+ # @raise [TransportFailed] if the files could not all be downloaded
193
+ # successfully, which may vary by implementation
194
+ def download(remotes, local)
195
+ FileUtils.mkdir_p(File.dirname(local))
196
+
197
+ Array(remotes).each do |remote|
198
+ new_content = file(remote).content
199
+ local_file = File.join(local, File.basename(remote))
200
+
201
+ logger.debug("Attempting to download '#{remote}' as file #{local_file}")
202
+
203
+ File.open(local_file, "w") { |fp| fp.write(new_content) }
204
+ end
205
+ end
206
+
164
207
  # Builds a LoginCommand which can be used to open an interactive
165
208
  # session on the remote host.
166
209
  #
@@ -29,6 +29,14 @@ class Train::Transports::SSH
29
29
  result.stdout.split(" ")[-1]
30
30
  end
31
31
 
32
+ def upload(locals, remote)
33
+ raise NotImplementedError, "#{self.class} does not implement #upload()"
34
+ end
35
+
36
+ def download(remotes, local)
37
+ raise NotImplementedError, "#{self.class} does not implement #download()"
38
+ end
39
+
32
40
  private
33
41
 
34
42
  def establish_connection
@@ -31,10 +31,26 @@ module Train::Transports
31
31
  nil # none, open your shell
32
32
  end
33
33
 
34
+ def close
35
+ @runner.close
36
+ end
37
+
34
38
  def uri
35
39
  "local://"
36
40
  end
37
41
 
42
+ def upload(locals, remote)
43
+ FileUtils.mkdir_p(remote)
44
+
45
+ Array(locals).each do |local|
46
+ FileUtils.cp_r(local, remote)
47
+ end
48
+ end
49
+
50
+ def download(remotes, local)
51
+ upload(remotes, local)
52
+ end
53
+
38
54
  private
39
55
 
40
56
  def select_runner(options)
@@ -70,9 +86,9 @@ module Train::Transports
70
86
  end
71
87
  end
72
88
 
73
- def run_command_via_connection(cmd, &_data_handler)
89
+ def run_command_via_connection(cmd, opts, &_data_handler)
74
90
  # Use the runner if it is available
75
- return @runner.run_command(cmd) if defined?(@runner)
91
+ return @runner.run_command(cmd, opts) if defined?(@runner)
76
92
 
77
93
  # If we don't have a runner, such as at the beginning of setting up the
78
94
  # transport and performing the first few steps of OS detection, fall
@@ -99,15 +115,24 @@ module Train::Transports
99
115
  @cmd_wrapper = Local::CommandWrapper.load(connection, options)
100
116
  end
101
117
 
102
- def run_command(cmd)
118
+ def run_command(cmd, opts = {})
103
119
  if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
104
120
  cmd = @cmd_wrapper.run(cmd)
105
121
  end
106
122
 
107
123
  res = Mixlib::ShellOut.new(cmd)
108
- res.run_command
124
+ res.timeout = opts[:timeout]
125
+ begin
126
+ res.run_command
127
+ rescue Mixlib::ShellOut::CommandTimeout
128
+ raise Train::CommandTimeoutReached
129
+ end
109
130
  Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
110
131
  end
132
+
133
+ def close
134
+ # nothing to do at the moment
135
+ end
111
136
  end
112
137
 
113
138
  class WindowsShellRunner
@@ -118,7 +143,7 @@ module Train::Transports
118
143
  @powershell_cmd = powershell_cmd
119
144
  end
120
145
 
121
- def run_command(script)
146
+ def run_command(script, opts)
122
147
  # Prevent progress stream from leaking into stderr
123
148
  script = "$ProgressPreference='SilentlyContinue';" + script
124
149
 
@@ -129,9 +154,18 @@ module Train::Transports
129
154
  cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}"
130
155
 
131
156
  res = Mixlib::ShellOut.new(cmd)
132
- res.run_command
157
+ res.timeout = opts[:timeout]
158
+ begin
159
+ res.run_command
160
+ rescue Mixlib::ShellOut::CommandTimeout
161
+ raise Train::CommandTimeoutReached
162
+ end
133
163
  Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
134
164
  end
165
+
166
+ def close
167
+ # nothing to do at the moment
168
+ end
135
169
  end
136
170
 
137
171
  class WindowsPipeRunner
@@ -141,6 +175,7 @@ module Train::Transports
141
175
 
142
176
  def initialize(powershell_cmd = "powershell")
143
177
  @powershell_cmd = powershell_cmd
178
+ @server_pid = nil
144
179
  @pipe = acquire_pipe
145
180
  raise PipeError if @pipe.nil?
146
181
  end
@@ -151,21 +186,31 @@ module Train::Transports
151
186
  # A command that succeeds without setting an exit code will have exitstatus 0
152
187
  # A command that exits with an exit code will have that value as exitstatus
153
188
  # A command that fails (e.g. throws exception) before setting an exit code will have exitstatus 1
154
- def run_command(cmd)
189
+ def run_command(cmd, _opts)
155
190
  script = "$ProgressPreference='SilentlyContinue';" + cmd
156
191
  encoded_script = Base64.strict_encode64(script)
192
+ # TODO: no way to safely implement timeouts here.
157
193
  @pipe.puts(encoded_script)
158
194
  @pipe.flush
159
195
  res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
160
196
  Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
161
197
  end
162
198
 
199
+ def close
200
+ Process.kill("KILL", @server_pid) unless @server_pid.nil?
201
+ @server_pid = nil
202
+ end
203
+
163
204
  private
164
205
 
165
206
  def acquire_pipe
207
+ require "win32/process"
166
208
  pipe_name = "inspec_#{SecureRandom.hex}"
167
209
 
168
- start_pipe_server(pipe_name)
210
+ @server_pid = start_pipe_server(pipe_name)
211
+
212
+ # Ensure process is killed when the Train process exits
213
+ at_exit { close rescue Errno::EIO }
169
214
 
170
215
  pipe = nil
171
216
 
@@ -227,11 +272,7 @@ module Train::Transports
227
272
  utf8_script = script.encode("UTF-16LE", "UTF-8")
228
273
  base64_script = Base64.strict_encode64(utf8_script)
229
274
  cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
230
-
231
- server_pid = Process.create(command_line: cmd).process_id
232
-
233
- # Ensure process is killed when the Train process exits
234
- at_exit { Process.kill("KILL", server_pid) }
275
+ Process.create(command_line: cmd).process_id
235
276
  end
236
277
  end
237
278
  end
@@ -29,8 +29,8 @@ class Train::Transports::SSH
29
29
  #
30
30
  # @author Fletcher Nichol <fnichol@nichol.ca>
31
31
  class Connection < BaseConnection # rubocop:disable Metrics/ClassLength
32
- attr_reader :hostname
33
- attr_reader :transport_options
32
+ attr_reader :hostname
33
+ attr_accessor :transport_options
34
34
 
35
35
  def initialize(options)
36
36
  # Track IOS command retries to prevent infinite loop on IOError. This must
@@ -247,6 +247,16 @@ class Train::Transports::SSH
247
247
 
248
248
  exit_status, stdout, stderr = execute_on_channel(cmd, opts, &data_handler)
249
249
 
250
+ # An interactive console might contain the STDERR in STDOUT
251
+ # concat both outputs for non-zero exit status. 
252
+ output = "#{stdout} #{stderr}".strip if exit_status != 0
253
+
254
+ # Abstract the su - USER authentication failure
255
+ # raise the Train::UserError and passes message & reason
256
+ if output && output.match?("su: Authentication failure")
257
+ raise Train::UserError.new(output, :bad_su_user_password)
258
+ end
259
+
250
260
  # Since `@session.loop` succeeded, reset the IOS command retry counter
251
261
  @ios_cmd_retries = 0
252
262
 
@@ -322,12 +332,12 @@ class Train::Transports::SSH
322
332
  channel.exec(cmd) do |_, success|
323
333
  abort "Couldn't execute command on SSH." unless success
324
334
  channel.on_data do |_, data|
325
- yield(data) if block_given?
335
+ yield(data, channel) if block_given?
326
336
  stdout += data
327
337
  end
328
338
 
329
339
  channel.on_extended_data do |_, _type, data|
330
- yield(data) if block_given?
340
+ yield(data, channel) if block_given?
331
341
  stderr += data
332
342
  end
333
343
 
data/lib/train/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Author:: Dominik Richter (<dominik.richter@gmail.com>)
3
3
 
4
4
  module Train
5
- VERSION = "3.4.7".freeze
5
+ VERSION = "3.5.5".freeze
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: train-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.7
4
+ version: 3.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef InSpec Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-11 00:00:00.000000000 Z
11
+ date: 2021-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable