test-kitchen 1.4.0.beta.2 → 1.4.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/features/kitchen_diagnose_command.feature +32 -0
  4. data/lib/kitchen/cli.rb +3 -0
  5. data/lib/kitchen/command/diagnose.rb +6 -1
  6. data/lib/kitchen/configurable.rb +48 -1
  7. data/lib/kitchen/diagnostic.rb +29 -0
  8. data/lib/kitchen/driver/base.rb +25 -0
  9. data/lib/kitchen/driver/dummy.rb +4 -0
  10. data/lib/kitchen/driver/proxy.rb +3 -0
  11. data/lib/kitchen/instance.rb +17 -0
  12. data/lib/kitchen/provisioner/base.rb +30 -1
  13. data/lib/kitchen/provisioner/chef_base.rb +7 -3
  14. data/lib/kitchen/provisioner/chef_solo.rb +4 -0
  15. data/lib/kitchen/provisioner/chef_zero.rb +5 -1
  16. data/lib/kitchen/provisioner/dummy.rb +4 -0
  17. data/lib/kitchen/provisioner/shell.rb +5 -0
  18. data/lib/kitchen/shell_out.rb +6 -2
  19. data/lib/kitchen/transport/base.rb +25 -0
  20. data/lib/kitchen/transport/dummy.rb +4 -0
  21. data/lib/kitchen/transport/ssh.rb +22 -1
  22. data/lib/kitchen/transport/winrm.rb +61 -90
  23. data/lib/kitchen/verifier/base.rb +30 -1
  24. data/lib/kitchen/verifier/busser.rb +4 -0
  25. data/lib/kitchen/verifier/dummy.rb +4 -0
  26. data/lib/kitchen/version.rb +1 -1
  27. data/spec/kitchen/configurable_spec.rb +35 -0
  28. data/spec/kitchen/diagnostic_spec.rb +53 -3
  29. data/spec/kitchen/driver/dummy_spec.rb +8 -0
  30. data/spec/kitchen/driver/proxy_spec.rb +4 -0
  31. data/spec/kitchen/driver/ssh_base_spec.rb +4 -0
  32. data/spec/kitchen/instance_spec.rb +75 -0
  33. data/spec/kitchen/provisioner/base_spec.rb +32 -6
  34. data/spec/kitchen/provisioner/chef_base_spec.rb +3 -2
  35. data/spec/kitchen/provisioner/chef_solo_spec.rb +10 -2
  36. data/spec/kitchen/provisioner/chef_zero_spec.rb +24 -2
  37. data/spec/kitchen/provisioner/dummy_spec.rb +8 -0
  38. data/spec/kitchen/provisioner/shell_spec.rb +10 -0
  39. data/spec/kitchen/shell_out_spec.rb +7 -0
  40. data/spec/kitchen/transport/ssh_spec.rb +90 -1
  41. data/spec/kitchen/transport/winrm_spec.rb +91 -11
  42. data/spec/kitchen/verifier/base_spec.rb +32 -6
  43. data/spec/kitchen/verifier/busser_spec.rb +8 -0
  44. data/spec/kitchen/verifier/dummy_spec.rb +8 -0
  45. data/support/chef_base_install_command.sh +183 -100
  46. data/test-kitchen.gemspec +1 -2
  47. metadata +11 -48
  48. data/lib/kitchen/transport/winrm/command_executor.rb +0 -188
  49. data/lib/kitchen/transport/winrm/file_transporter.rb +0 -454
  50. data/lib/kitchen/transport/winrm/logging.rb +0 -50
  51. data/lib/kitchen/transport/winrm/template.rb +0 -74
  52. data/lib/kitchen/transport/winrm/tmp_zip.rb +0 -187
  53. data/spec/kitchen/transport/winrm/command_executor_spec.rb +0 -400
  54. data/spec/kitchen/transport/winrm/file_transporter_spec.rb +0 -876
  55. data/spec/kitchen/transport/winrm/logging_spec.rb +0 -92
  56. data/spec/kitchen/transport/winrm/template_spec.rb +0 -51
  57. data/spec/kitchen/transport/winrm/tmp_zip_spec.rb +0 -132
  58. data/support/check_files.ps1.erb +0 -48
  59. data/support/decode_files.ps1.erb +0 -62
@@ -26,12 +26,11 @@ Gem::Specification.new do |gem|
26
26
  gem.add_dependency "mixlib-shellout", ">= 1.2", "< 3.0"
27
27
  gem.add_dependency "net-scp", "~> 1.1"
28
28
  gem.add_dependency "net-ssh", "~> 2.7"
29
- gem.add_dependency "winrm", "~> 1.3"
30
29
  gem.add_dependency "safe_yaml", "~> 1.0"
31
30
  gem.add_dependency "thor", "~> 0.18"
32
- gem.add_dependency "rubyzip", ">= 1.1.7", "~> 1.1"
33
31
 
34
32
  gem.add_development_dependency "pry"
33
+ gem.add_development_dependency "winrm-transport", "~> 1.0"
35
34
 
36
35
  gem.add_development_dependency "bundler", "~> 1.3"
37
36
  gem.add_development_dependency "rake"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-kitchen
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0.beta.2
4
+ version: 1.4.0.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fletcher Nichol
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-25 00:00:00.000000000 Z
11
+ date: 2015-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-shellout
@@ -58,20 +58,6 @@ dependencies:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '2.7'
61
- - !ruby/object:Gem::Dependency
62
- name: winrm
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '1.3'
68
- type: :runtime
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '1.3'
75
61
  - !ruby/object:Gem::Dependency
76
62
  name: safe_yaml
77
63
  requirement: !ruby/object:Gem::Requirement
@@ -101,39 +87,33 @@ dependencies:
101
87
  - !ruby/object:Gem::Version
102
88
  version: '0.18'
103
89
  - !ruby/object:Gem::Dependency
104
- name: rubyzip
90
+ name: pry
105
91
  requirement: !ruby/object:Gem::Requirement
106
92
  requirements:
107
93
  - - ">="
108
94
  - !ruby/object:Gem::Version
109
- version: 1.1.7
110
- - - "~>"
111
- - !ruby/object:Gem::Version
112
- version: '1.1'
113
- type: :runtime
95
+ version: '0'
96
+ type: :development
114
97
  prerelease: false
115
98
  version_requirements: !ruby/object:Gem::Requirement
116
99
  requirements:
117
100
  - - ">="
118
101
  - !ruby/object:Gem::Version
119
- version: 1.1.7
120
- - - "~>"
121
- - !ruby/object:Gem::Version
122
- version: '1.1'
102
+ version: '0'
123
103
  - !ruby/object:Gem::Dependency
124
- name: pry
104
+ name: winrm-transport
125
105
  requirement: !ruby/object:Gem::Requirement
126
106
  requirements:
127
- - - ">="
107
+ - - "~>"
128
108
  - !ruby/object:Gem::Version
129
- version: '0'
109
+ version: '1.0'
130
110
  type: :development
131
111
  prerelease: false
132
112
  version_requirements: !ruby/object:Gem::Requirement
133
113
  requirements:
134
- - - ">="
114
+ - - "~>"
135
115
  - !ruby/object:Gem::Version
136
- version: '0'
116
+ version: '1.0'
137
117
  - !ruby/object:Gem::Dependency
138
118
  name: bundler
139
119
  requirement: !ruby/object:Gem::Requirement
@@ -396,11 +376,6 @@ files:
396
376
  - lib/kitchen/transport/dummy.rb
397
377
  - lib/kitchen/transport/ssh.rb
398
378
  - lib/kitchen/transport/winrm.rb
399
- - lib/kitchen/transport/winrm/command_executor.rb
400
- - lib/kitchen/transport/winrm/file_transporter.rb
401
- - lib/kitchen/transport/winrm/logging.rb
402
- - lib/kitchen/transport/winrm/template.rb
403
- - lib/kitchen/transport/winrm/tmp_zip.rb
404
379
  - lib/kitchen/util.rb
405
380
  - lib/kitchen/verifier.rb
406
381
  - lib/kitchen/verifier/base.rb
@@ -443,11 +418,6 @@ files:
443
418
  - spec/kitchen/suite_spec.rb
444
419
  - spec/kitchen/transport/base_spec.rb
445
420
  - spec/kitchen/transport/ssh_spec.rb
446
- - spec/kitchen/transport/winrm/command_executor_spec.rb
447
- - spec/kitchen/transport/winrm/file_transporter_spec.rb
448
- - spec/kitchen/transport/winrm/logging_spec.rb
449
- - spec/kitchen/transport/winrm/template_spec.rb
450
- - spec/kitchen/transport/winrm/tmp_zip_spec.rb
451
421
  - spec/kitchen/transport/winrm_spec.rb
452
422
  - spec/kitchen/transport_spec.rb
453
423
  - spec/kitchen/util_spec.rb
@@ -460,7 +430,6 @@ files:
460
430
  - spec/support/powershell_max_size_spec.rb
461
431
  - support/busser_install_command.ps1
462
432
  - support/busser_install_command.sh
463
- - support/check_files.ps1.erb
464
433
  - support/chef-client-zero.rb
465
434
  - support/chef_base_init_command.ps1
466
435
  - support/chef_base_init_command.sh
@@ -468,7 +437,6 @@ files:
468
437
  - support/chef_base_install_command.sh
469
438
  - support/chef_zero_prepare_command_legacy.ps1
470
439
  - support/chef_zero_prepare_command_legacy.sh
471
- - support/decode_files.ps1.erb
472
440
  - support/download_helpers.sh
473
441
  - support/dummy-validation.pem
474
442
  - templates/driver/CHANGELOG.md.erb
@@ -565,11 +533,6 @@ test_files:
565
533
  - spec/kitchen/suite_spec.rb
566
534
  - spec/kitchen/transport/base_spec.rb
567
535
  - spec/kitchen/transport/ssh_spec.rb
568
- - spec/kitchen/transport/winrm/command_executor_spec.rb
569
- - spec/kitchen/transport/winrm/file_transporter_spec.rb
570
- - spec/kitchen/transport/winrm/logging_spec.rb
571
- - spec/kitchen/transport/winrm/template_spec.rb
572
- - spec/kitchen/transport/winrm/tmp_zip_spec.rb
573
536
  - spec/kitchen/transport/winrm_spec.rb
574
537
  - spec/kitchen/transport_spec.rb
575
538
  - spec/kitchen/util_spec.rb
@@ -1,188 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Matt Wrock (<matt@mattwrock.com>)
4
- #
5
- # Copyright (C) 2014, Matt Wrock
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require "kitchen/transport/winrm/logging"
20
-
21
- module Kitchen
22
-
23
- module Transport
24
-
25
- class Winrm < Kitchen::Transport::Base
26
-
27
- # Object which can execute multiple commands and Powershell scripts in
28
- # one shared remote shell session. The maximum number of commands per
29
- # shell is determined by interrogating the remote host when the session
30
- # is opened and the remote shell is automatically recycled before the
31
- # threshold is reached.
32
- #
33
- # @author Matt Wrock <matt@mattwrock.com>
34
- # @author Fletcher Nichol <fnichol@nichol.ca>
35
- class CommandExecutor
36
-
37
- include Logging
38
-
39
- # @return [Integer,nil] the safe maximum number of commands that can
40
- # be executed in one remote shell session, or `nil` if the
41
- # threshold has not yet been determined
42
- attr_reader :max_commands
43
-
44
- # @return [String,nil] the identifier for the current open remote
45
- # shell session, or `nil` if the session is not open
46
- attr_reader :shell
47
-
48
- # Creates a CommandExecutor given a `WinRM::WinRMWebService` object.
49
- #
50
- # @param service [WinRM::WinRMWebService] a winrm web service object
51
- # @param logger [#debug,#info] an optional logger/ui object that
52
- # responds to `#debug` and `#info` (default: `nil`)
53
- def initialize(service, logger = nil)
54
- @service = service
55
- @logger = logger
56
- @command_count = 0
57
- end
58
-
59
- # Closes the open remote shell session. This method can be called
60
- # multiple times, even if there is no open session.
61
- def close
62
- return if shell.nil?
63
-
64
- service.close_shell(shell)
65
- @shell = nil
66
- end
67
-
68
- # Opens a remote shell session for reuse. The maxiumum
69
- # command-per-shell threshold is also determined the first time this
70
- # method is invoked and cached for later invocations.
71
- #
72
- # @return [String] the remote shell session indentifier
73
- def open
74
- close
75
- @shell = service.open_shell
76
- @command_count = 0
77
- determine_max_commands unless max_commands
78
- shell
79
- end
80
-
81
- # Runs a CMD command.
82
- #
83
- # @param command [String] the command to run on the remote system
84
- # @param arguments [Array<String>] arguments to the command
85
- # @yield [stdout, stderr] yields more live access the standard
86
- # output and standard error streams as they are returns, if
87
- # streaming behavior is desired
88
- # @return [WinRM::Output] output object with stdout, stderr, and
89
- # exit code
90
- def run_cmd(command, arguments = [], &block)
91
- reset if command_count_exceeded?
92
- ensure_open_shell!
93
-
94
- @command_count += 1
95
- result = nil
96
- service.run_command(shell, command, arguments) do |command_id|
97
- result = service.get_command_output(shell, command_id, &block)
98
- end
99
- result
100
- end
101
-
102
- # Run a Powershell script that resides on the local box.
103
- #
104
- # @param script_file [IO,String] an IO reference for reading the
105
- # Powershell script or the actual file contents
106
- # @yield [stdout, stderr] yields more live access the standard
107
- # output and standard error streams as they are returns, if
108
- # streaming behavior is desired
109
- # @return [WinRM::Output] output object with stdout, stderr, and
110
- # exit code
111
- def run_powershell_script(script_file, &block)
112
- # this code looks overly compact in an attempt to limit local
113
- # variable assignments that may contain large strings and
114
- # consequently bloat the Ruby VM
115
- run_cmd(
116
- "powershell",
117
- [
118
- "-encodedCommand",
119
- WinRM::PowershellScript.new(
120
- script_file.is_a?(IO) ? script_file.read : script_file
121
- ).encoded
122
- ],
123
- &block
124
- )
125
- end
126
-
127
- private
128
-
129
- LEGACY_LIMIT = 15
130
-
131
- MODERN_LIMIT = 1500
132
-
133
- PS1_OS_VERSION = "[environment]::OSVersion.Version.tostring()".freeze
134
-
135
- # @return [Integer] the number of executed commands on the remote
136
- # shell session
137
- # @api private
138
- attr_accessor :command_count
139
-
140
- # @return [#debug,#info] the logger
141
- # @api private
142
- attr_reader :logger
143
-
144
- # @return [WinRM::WinRMWebService] a WinRM web service object
145
- # @api private
146
- attr_reader :service
147
-
148
- # @return [true,false] whether or not the number of exeecuted commands
149
- # have exceeded the maxiumum threshold
150
- # @api private
151
- def command_count_exceeded?
152
- command_count > max_commands.to_i
153
- end
154
-
155
- # Ensures that there is an open remote shell session.
156
- #
157
- # @raise [WinRM::WinRMError] if there is no open shell
158
- # @api private
159
- def ensure_open_shell!
160
- if shell.nil?
161
- raise WinRM::WinRMError, "#{self.class}#open must be called " \
162
- "before any run methods are invoked"
163
- end
164
- end
165
-
166
- # Determines the safe maximum number of commands that can be executed
167
- # on a remote shell session by interrogating the remote host.
168
- #
169
- # @api private
170
- def determine_max_commands
171
- os_version = run_powershell_script(PS1_OS_VERSION).stdout.chomp
172
- @max_commands = os_version < "6.2" ? LEGACY_LIMIT : MODERN_LIMIT
173
- @max_commands -= 2 # to be safe
174
- end
175
-
176
- # Closes the remote shell session and opens a new one.
177
- #
178
- # @api private
179
- def reset
180
- debug {
181
- "Resetting WinRM shell (Max command limit is #{max_commands})"
182
- }
183
- open
184
- end
185
- end
186
- end
187
- end
188
- end
@@ -1,454 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2015, Fletcher Nichol
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require "benchmark"
20
- require "csv"
21
- require "digest"
22
- require "securerandom"
23
- require "stringio"
24
-
25
- require "kitchen/transport/winrm/logging"
26
- require "kitchen/transport/winrm/template"
27
- require "kitchen/transport/winrm/tmp_zip"
28
-
29
- module Kitchen
30
-
31
- module Transport
32
-
33
- class Winrm < Kitchen::Transport::Base
34
-
35
- # Wrapped exception for any internally raised WinRM-related errors.
36
- #
37
- # @author Fletcher Nichol <fnichol@nichol.ca>
38
- class FileTransporterFailed < ::WinRM::WinRMError; end
39
-
40
- # Object which can upload one or more files or directories to a remote
41
- # host over WinRM using PowerShell scripts and CMD commands. Note that
42
- # this form of file transfer is *not* ideal and extremely costly on both
43
- # the local and remote sides. Great pains are made to minimize round
44
- # trips to the remote host and to minimize the number of PowerShell
45
- # sessions being invoked which can be 2 orders of magnitude more
46
- # expensive than vanilla CMD commands.
47
- #
48
- # This object is supported by either a `WinRM::WinRMWebService` or
49
- # `Winrm::CommandExecutor` instance as it depends on the `#run_cmd` and
50
- # `#run_powershell_script` API contracts.
51
- #
52
- # An optional logger can be supplied, assuming it can respond to the
53
- # `#debug` and `#debug?` messages.
54
- #
55
- # @author Fletcher Nichol <fnichol@nichol.ca>
56
- # @author Matt Wrock <matt@mattwrock.com>
57
- class FileTransporter
58
-
59
- include Logging
60
-
61
- # Creates a FileTransporter given a service object and optional logger.
62
- # The service object may be a `WinRM::WinRMWebService` or
63
- # `Winrm::CommandExecutor` instance.
64
- #
65
- # @param service [WinRM::WinRMWebService,Winrm::CommandExecutor] a
66
- # winrm web service object
67
- # @param logger [#debug,#debug?] an optional logger/ui object that
68
- # responds to `#debug` and `#debug?` (default: `nil`)
69
- def initialize(service, logger = nil, opts = {})
70
- @service = service
71
- @logger = logger
72
- @id_generator = opts.fetch(:id_generator) { -> { SecureRandom.uuid } }
73
- end
74
-
75
- # Uploads a collection of files and/or directories to the remote host.
76
- #
77
- # **TODO Notes:**
78
- # * options could specify zip mode, zip options, etc.
79
- # * maybe option to set tmpfile base dir to override $env:PATH?
80
- # * progress yields block like net-scp progress
81
- # * final API: def upload(locals, remote, _options = {}, &_progress)
82
- #
83
- # @param locals [Array<String>,String] one or more local file or
84
- # directory paths
85
- # @param remote [String] the base destination path on the remote host
86
- # @return [Hash] report hash, keyed by the local MD5 digest
87
- def upload(locals, remote)
88
- files = nil
89
-
90
- elapsed = Benchmark.measure do
91
- files = make_files_hash(Array(locals), remote)
92
-
93
- report = check_files(files)
94
- merge_with_report!(files, report)
95
-
96
- report = stream_upload_files(files)
97
- merge_with_report!(files, report)
98
-
99
- report = decode_files(files)
100
- merge_with_report!(files, report)
101
-
102
- cleanup(files)
103
- end
104
-
105
- debug {
106
- "Uploaded #{files.keys.size} items " \
107
- "in #{Util.duration(elapsed.real)}"
108
- }
109
-
110
- files
111
- end
112
-
113
- private
114
-
115
- MAX_ENCODED_WRITE = 8000
116
-
117
- BASE64_PACK = "m0".freeze
118
-
119
- # @return [#debug,#debug?] the logger
120
- # @api private
121
- attr_reader :logger
122
-
123
- # @return [WinRM::WinRMWebService,Winrm::CommandExecutor] a WinRM web
124
- # service object
125
- # @api private
126
- attr_reader :service
127
-
128
- # Adds an entry to a files Hash (keyed by local MD5 digest) for a
129
- # directory. When a directory is added, a temporary Zip file is created
130
- # containing the contents of the directory and any file-related data
131
- # such as MD5 digest, size, etc. will be referring to the Zip file.
132
- #
133
- # @param hash [Hash] hash to be mutated
134
- # @param dir [String] directory path to be Zipped and added
135
- # @param remote [String] path to destination on remote host
136
- # @api private
137
- def add_directory_hash!(hash, dir, remote)
138
- zip_io = TmpZip.new(dir, logger)
139
- zip_md5 = md5sum(zip_io.path)
140
-
141
- hash[zip_md5] = {
142
- "src" => dir,
143
- "src_zip" => zip_io.path.to_s,
144
- "zip_io" => zip_io,
145
- "tmpzip" => "$env:TEMP\\tmpzip-#{zip_md5}.zip",
146
- "dst" => remote,
147
- "size" => File.size(zip_io.path)
148
- }
149
- end
150
-
151
- # Adds an entry to a files Hash (keyed by local MD5 digest) for a file.
152
- #
153
- # @param hash [Hash] hash to be mutated
154
- # @param local [String] file path
155
- # @param remote [String] path to destination on remote host
156
- # @api private
157
- def add_file_hash!(hash, local, remote)
158
- hash[md5sum(local)] = {
159
- "src" => local,
160
- "dst" => "#{remote}\\#{File.basename(local)}",
161
- "size" => File.size(local)
162
- }
163
- end
164
-
165
- # Runs the check_files PowerShell script against a collection of
166
- # destination path/MD5 checksum pairs. The PowerShell script returns
167
- # its results as a CSV-formatted report which is converted into a Ruby
168
- # Hash.
169
- #
170
- # @param files [Hash] files hash, keyed by the local MD5 digest
171
- # @return [Hash] a report hash, keyed by the local MD5 digest
172
- # @api private
173
- def check_files(files)
174
- debug { "Running check_files.ps1" }
175
- hash_file = create_remote_hash_file(check_files_ps_hash(files))
176
- output = service.run_powershell_script(
177
- check_files_template % { :hash_file => hash_file }
178
- )
179
- parse_response(output)
180
- end
181
-
182
- # Constructs a collection of destination path/MD5 checksum pairs as a
183
- # String representation of the contents of a PowerShell Hash Table.
184
- #
185
- # @param files [Hash] files hash, keyed by the local MD5 digest
186
- # @return [String] the inner contents of a PowerShell Hash Table
187
- # @api private
188
- def check_files_ps_hash(files)
189
- ps_hash(Hash[
190
- files.map { |md5, data| [data.fetch("tmpzip", data["dst"]), md5] }
191
- ])
192
- end
193
-
194
- # @return [Template] an un-rendered template of the check_files
195
- # PowerShell script
196
- # @api private
197
- def check_files_template
198
- @check_files_template ||= Template.new(File.join(
199
- File.dirname(__FILE__),
200
- %W[.. .. .. .. support check_files.ps1.erb]
201
- ))
202
- end
203
-
204
- # Performs any final cleanup on the report Hash and removes any
205
- # temporary files/resources used in the upload task.
206
- #
207
- # @param files [Hash] a files hash
208
- # @api private
209
- def cleanup(files)
210
- files.select { |_, data| data.key?("zip_io") }.each do |md5, data|
211
- data.fetch("zip_io").unlink
212
- files.fetch(md5).delete("zip_io")
213
- debug { "Cleaned up src_zip #{data["src_zip"]}" }
214
- end
215
- end
216
-
217
- # Creates a remote Base64-encoded temporary file containing a
218
- # PowerShell hash table.
219
- #
220
- # @param hash [String] a String representation of a PowerShell hash
221
- # table
222
- # @return [String] the remote path to the temporary file
223
- # @api private
224
- def create_remote_hash_file(hash)
225
- hash_file = "$env:TEMP\\hash-#{@id_generator.call}.txt"
226
- hash.lines.each { |line| debug { line.chomp } }
227
- StringIO.open(hash) { |io| stream_upload(io, hash_file) }
228
- hash_file
229
- end
230
-
231
- # Runs the decode_files PowerShell script against a collection of
232
- # temporary file/destination path pairs. The PowerShell script returns
233
- # its results as a CSV-formatted report which is converted into a Ruby
234
- # Hash. The script will not be invoked if there are no "dirty" files
235
- # present in the incoming files Hash.
236
- #
237
- # @param files [Hash] files hash, keyed by the local MD5 digest
238
- # @return [Hash] a report hash, keyed by the local MD5 digest
239
- # @api private
240
- def decode_files(files)
241
- decoded_files = decode_files_ps_hash(files)
242
-
243
- if decoded_files == ps_hash(Hash.new)
244
- debug { "No remote files to decode, skipping" }
245
- Hash.new
246
- else
247
- debug { "Running decode_files.ps1" }
248
- hash_file = create_remote_hash_file(decoded_files)
249
- output = service.run_powershell_script(
250
- decode_files_template % { :hash_file => hash_file }
251
- )
252
- parse_response(output)
253
- end
254
- end
255
-
256
- # Constructs a collection of temporary file/destination path pairs for
257
- # all "dirty" files as a String representation of the contents of a
258
- # PowerShell Hash Table. A "dirty" file is one which has the
259
- # `"chk_dirty"` option set to `"True"` in the incoming files Hash.
260
- #
261
- # @param files [Hash] files hash, keyed by the local MD5 digest
262
- # @return [String] the inner contents of a PowerShell Hash Table
263
- # @api private
264
- def decode_files_ps_hash(files)
265
- result = files.select { |_, data| data["chk_dirty"] == "True" }.map { |_, data|
266
- val = { "dst" => data["dst"] }
267
- val["tmpzip"] = data["tmpzip"] if data["tmpzip"]
268
-
269
- [data["tmpfile"], val]
270
- }
271
-
272
- ps_hash(Hash[result])
273
- end
274
-
275
- # @return [Template] an un-rendered template of the decode_files
276
- # PowerShell script
277
- # @api private
278
- def decode_files_template
279
- @decode_files_template ||= Template.new(File.join(
280
- File.dirname(__FILE__),
281
- %W[.. .. .. .. support decode_files.ps1.erb]
282
- ))
283
- end
284
-
285
- # Contructs a Hash of files or directories, keyed by the local MD5
286
- # digest. Each file entry has a source and destination set, at a
287
- # minimum.
288
- #
289
- # @param locals [Array<String>] a collection of local files or
290
- # directories
291
- # @param remote [String] the base destination path on the remote host
292
- # @return [Hash] files hash, keyed by the local MD5 digest
293
- # @api private
294
- def make_files_hash(locals, remote)
295
- hash = Hash.new
296
- locals.each do |local|
297
- expanded = File.expand_path(local)
298
- expanded += local[-1] if local.end_with?("/", "\\")
299
-
300
- if File.file?(expanded)
301
- add_file_hash!(hash, expanded, remote)
302
- elsif File.directory?(expanded)
303
- add_directory_hash!(hash, expanded, remote)
304
- else
305
- raise Errno::ENOENT, "No such file or directory #{expanded}"
306
- end
307
- end
308
- hash
309
- end
310
-
311
- # @return [String] the MD5 digest of a local file
312
- # @api private
313
- def md5sum(local)
314
- Digest::MD5.file(local).hexdigest
315
- end
316
-
317
- # Destructively merges a report Hash into an existing files Hash.
318
- # **Note:** this method mutates the files Hash.
319
- #
320
- # @param files [Hash] files hash, keyed by the local MD5 digest
321
- # @param report [Hash] report hash, keyed by the local MD5 digest
322
- # @api private
323
- def merge_with_report!(files, report)
324
- files.merge!(report) { |_, oldval, newval| oldval.merge(newval) }
325
- end
326
-
327
- # @param depth [Integer] number of padding characters (default: `0`)
328
- # @return [String] a whitespace padded string of the given length
329
- # @api private
330
- def pad(depth = 0)
331
- " " * depth
332
- end
333
-
334
- # Parses response of a PowerShell script or CMD command which contains
335
- # a CSV-formatted document in the standard output stream.
336
- #
337
- # @param output [WinRM::Output] output object with stdout, stderr, and
338
- # exit code
339
- # @return [Hash] report hash, keyed by the local MD5 digest
340
- # @api private
341
- def parse_response(output)
342
- if output[:exitcode] != 0
343
- raise FileTransporterFailed, "[#{self.class}] Upload failed " \
344
- "(exitcode: #{output[:exitcode]})\n#{output.stderr}"
345
- end
346
- array = CSV.parse(output.stdout, :headers => true).map(&:to_hash)
347
- array.each { |h| h.each { |key, value| h[key] = nil if value == "" } }
348
- Hash[array.map { |entry| [entry.fetch("src_md5"), entry] }]
349
- end
350
-
351
- # Converts a Ruby hash into a PowerShell hash table, represented in a
352
- # String.
353
- #
354
- # @param obj [Object] source Hash or object when used in recursive
355
- # calls
356
- # @param depth [Integer] padding depth, used in recursive calls
357
- # (default: `0`)
358
- # @return [String] a PowerShell hash table
359
- # @api private
360
- def ps_hash(obj, depth = 0)
361
- if obj.is_a?(Hash)
362
- obj.map { |k, v|
363
- %{#{pad(depth + 2)}#{ps_hash(k)} = #{ps_hash(v, depth + 2)}}
364
- }.join("\n").insert(0, "@{\n").insert(-1, "\n#{pad(depth)}}")
365
- else
366
- %{"#{obj}"}
367
- end
368
- end
369
-
370
- # Uploads an IO stream to a Base64-encoded destination file.
371
- #
372
- # **Implementation Note:** Some of the code in this method may appear
373
- # slightly too dense and while adding additional variables would help,
374
- # the code is written very precisely to avoid unwanted allocations
375
- # which will bloat the Ruby VM's object space (and memory footprint).
376
- # The goal here is to stream potentially large files to a remote host
377
- # while not loading the entire file into memory first, then Base64
378
- # encoding it--duplicating the file in memory again.
379
- #
380
- # @param input_io [#read] a readable stream or object to be uploaded
381
- # @param dest [String] path to the destination file on the remote host
382
- # @return [Integer,Integer] the number of resulting upload chunks and
383
- # the number of bytes transferred to the remote host
384
- # @api private
385
- def stream_upload(input_io, dest)
386
- dest_cmd = dest.sub("$env:TEMP", "%TEMP%")
387
- read_size = (MAX_ENCODED_WRITE.to_i / 4) * 3
388
- chunk, bytes = 1, 0
389
- buffer = ""
390
- service.run_cmd(%{echo|set /p=>"#{dest_cmd}"}) # truncate empty file
391
- while input_io.read(read_size, buffer)
392
- bytes += (buffer.bytesize / 3 * 4)
393
- service.run_cmd([buffer].pack(BASE64_PACK).
394
- insert(0, "echo ").concat(%{ >> "#{dest_cmd}"}))
395
- debug { "Wrote chunk #{chunk} for #{dest}" } if chunk % 25 == 0
396
- chunk += 1
397
- end
398
- buffer = nil # rubocop:disable Lint/UselessAssignment
399
-
400
- [chunk - 1, bytes]
401
- end
402
-
403
- # Uploads a local file to a Base64-encoded temporary file.
404
- #
405
- # @param src [String] path to a local file
406
- # @param tmpfile [String] path to the temporary file on the remote
407
- # host
408
- # @return [Integer,Integer] the number of resulting upload chunks and
409
- # the number of bytes transferred to the remote host
410
- # @api private
411
- def stream_upload_file(src, tmpfile)
412
- debug { "Uploading #{src} to encoded tmpfile #{tmpfile}" }
413
- chunks, bytes = 0, 0
414
- elapsed = Benchmark.measure do
415
- File.open(src, "rb") do |io|
416
- chunks, bytes = stream_upload(io, tmpfile)
417
- end
418
- end
419
- debug {
420
- "Finished uploading #{src} to encoded tmpfile #{tmpfile} " \
421
- "(#{bytes.to_f / 1000} KB over #{chunks} chunks) " \
422
- "in #{Util.duration(elapsed.real)}"
423
- }
424
-
425
- [chunks, bytes]
426
- end
427
-
428
- # Uploads a collection of "dirty" files to the remote host as
429
- # Base64-encoded temporary files. A "dirty" file is one which has the
430
- # `"chk_dirty"` option set to `"True"` in the incoming files Hash.
431
- #
432
- # @param files [Hash] files hash, keyed by the local MD5 digest
433
- # @return [Hash] a report hash, keyed by the local MD5 digest
434
- # @api private
435
- def stream_upload_files(files)
436
- response = Hash.new
437
- files.each do |md5, data|
438
- src = data.fetch("src_zip", data["src"])
439
- if data["chk_dirty"] == "True"
440
- tmpfile = "$env:TEMP\\b64-#{md5}.txt"
441
- response[md5] = { "tmpfile" => tmpfile }
442
- chunks, bytes = stream_upload_file(src, tmpfile)
443
- response[md5]["chunks"] = chunks
444
- response[md5]["xfered"] = bytes
445
- else
446
- debug { "File #{data["dst"]} is up to date, skipping" }
447
- end
448
- end
449
- response
450
- end
451
- end
452
- end
453
- end
454
- end