train-core 3.4.7 → 3.5.5

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