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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/features/kitchen_diagnose_command.feature +32 -0
- data/lib/kitchen/cli.rb +3 -0
- data/lib/kitchen/command/diagnose.rb +6 -1
- data/lib/kitchen/configurable.rb +48 -1
- data/lib/kitchen/diagnostic.rb +29 -0
- data/lib/kitchen/driver/base.rb +25 -0
- data/lib/kitchen/driver/dummy.rb +4 -0
- data/lib/kitchen/driver/proxy.rb +3 -0
- data/lib/kitchen/instance.rb +17 -0
- data/lib/kitchen/provisioner/base.rb +30 -1
- data/lib/kitchen/provisioner/chef_base.rb +7 -3
- data/lib/kitchen/provisioner/chef_solo.rb +4 -0
- data/lib/kitchen/provisioner/chef_zero.rb +5 -1
- data/lib/kitchen/provisioner/dummy.rb +4 -0
- data/lib/kitchen/provisioner/shell.rb +5 -0
- data/lib/kitchen/shell_out.rb +6 -2
- data/lib/kitchen/transport/base.rb +25 -0
- data/lib/kitchen/transport/dummy.rb +4 -0
- data/lib/kitchen/transport/ssh.rb +22 -1
- data/lib/kitchen/transport/winrm.rb +61 -90
- data/lib/kitchen/verifier/base.rb +30 -1
- data/lib/kitchen/verifier/busser.rb +4 -0
- data/lib/kitchen/verifier/dummy.rb +4 -0
- data/lib/kitchen/version.rb +1 -1
- data/spec/kitchen/configurable_spec.rb +35 -0
- data/spec/kitchen/diagnostic_spec.rb +53 -3
- data/spec/kitchen/driver/dummy_spec.rb +8 -0
- data/spec/kitchen/driver/proxy_spec.rb +4 -0
- data/spec/kitchen/driver/ssh_base_spec.rb +4 -0
- data/spec/kitchen/instance_spec.rb +75 -0
- data/spec/kitchen/provisioner/base_spec.rb +32 -6
- data/spec/kitchen/provisioner/chef_base_spec.rb +3 -2
- data/spec/kitchen/provisioner/chef_solo_spec.rb +10 -2
- data/spec/kitchen/provisioner/chef_zero_spec.rb +24 -2
- data/spec/kitchen/provisioner/dummy_spec.rb +8 -0
- data/spec/kitchen/provisioner/shell_spec.rb +10 -0
- data/spec/kitchen/shell_out_spec.rb +7 -0
- data/spec/kitchen/transport/ssh_spec.rb +90 -1
- data/spec/kitchen/transport/winrm_spec.rb +91 -11
- data/spec/kitchen/verifier/base_spec.rb +32 -6
- data/spec/kitchen/verifier/busser_spec.rb +8 -0
- data/spec/kitchen/verifier/dummy_spec.rb +8 -0
- data/support/chef_base_install_command.sh +183 -100
- data/test-kitchen.gemspec +1 -2
- metadata +11 -48
- data/lib/kitchen/transport/winrm/command_executor.rb +0 -188
- data/lib/kitchen/transport/winrm/file_transporter.rb +0 -454
- data/lib/kitchen/transport/winrm/logging.rb +0 -50
- data/lib/kitchen/transport/winrm/template.rb +0 -74
- data/lib/kitchen/transport/winrm/tmp_zip.rb +0 -187
- data/spec/kitchen/transport/winrm/command_executor_spec.rb +0 -400
- data/spec/kitchen/transport/winrm/file_transporter_spec.rb +0 -876
- data/spec/kitchen/transport/winrm/logging_spec.rb +0 -92
- data/spec/kitchen/transport/winrm/template_spec.rb +0 -51
- data/spec/kitchen/transport/winrm/tmp_zip_spec.rb +0 -132
- data/support/check_files.ps1.erb +0 -48
- data/support/decode_files.ps1.erb +0 -62
data/test-kitchen.gemspec
CHANGED
@@ -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.
|
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-
|
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:
|
90
|
+
name: pry
|
105
91
|
requirement: !ruby/object:Gem::Requirement
|
106
92
|
requirements:
|
107
93
|
- - ">="
|
108
94
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
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:
|
120
|
-
- - "~>"
|
121
|
-
- !ruby/object:Gem::Version
|
122
|
-
version: '1.1'
|
102
|
+
version: '0'
|
123
103
|
- !ruby/object:Gem::Dependency
|
124
|
-
name:
|
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
|