test-kitchen 3.9.1 → 4.0.0
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 +4 -4
- data/Gemfile +5 -4
- data/lib/kitchen/collection.rb +1 -1
- data/lib/kitchen/configurable.rb +4 -4
- data/lib/kitchen/data_munger.rb +7 -9
- data/lib/kitchen/driver/proxy.rb +11 -6
- data/lib/kitchen/generator/init.rb +1 -1
- data/lib/kitchen/instance.rb +6 -103
- data/lib/kitchen/lazy_hash.rb +1 -1
- data/lib/kitchen/logger.rb +1 -1
- data/lib/kitchen/metadata_chopper.rb +1 -1
- data/lib/kitchen/provisioner/base.rb +0 -18
- data/lib/kitchen/provisioner.rb +1 -2
- data/lib/kitchen/transport/winrm.rb +8 -8
- data/lib/kitchen/util.rb +1 -1
- data/lib/kitchen/version.rb +1 -1
- data/lib/kitchen.rb +0 -2
- data/lib/vendor/hash_recursive_merge.rb +2 -2
- data/templates/driver/driver.rb.erb +1 -1
- data/test-kitchen.gemspec +4 -6
- metadata +26 -39
- data/lib/kitchen/driver/ssh_base.rb +0 -346
- data/lib/kitchen/provisioner/chef/berkshelf.rb +0 -116
- data/lib/kitchen/provisioner/chef/common_sandbox.rb +0 -352
- data/lib/kitchen/provisioner/chef/policyfile.rb +0 -173
- data/lib/kitchen/provisioner/chef_apply.rb +0 -121
- data/lib/kitchen/provisioner/chef_base.rb +0 -723
- data/lib/kitchen/provisioner/chef_infra.rb +0 -167
- data/lib/kitchen/provisioner/chef_solo.rb +0 -82
- data/lib/kitchen/provisioner/chef_target.rb +0 -130
- data/lib/kitchen/provisioner/chef_zero.rb +0 -12
- data/lib/kitchen/ssh.rb +0 -296
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
|
3
|
-
#
|
|
4
|
-
# Copyright (C) 2013, Fletcher Nichol
|
|
5
|
-
#
|
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
# you may not use this file except in compliance with the License.
|
|
8
|
-
# You may obtain a copy of the License at
|
|
9
|
-
#
|
|
10
|
-
# https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
#
|
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
# See the License for the specific language governing permissions and
|
|
16
|
-
# limitations under the License.
|
|
17
|
-
|
|
18
|
-
require_relative "chef_base"
|
|
19
|
-
|
|
20
|
-
module Kitchen
|
|
21
|
-
module Provisioner
|
|
22
|
-
# Chef Zero provisioner.
|
|
23
|
-
#
|
|
24
|
-
# @author Fletcher Nichol <fnichol@nichol.ca>
|
|
25
|
-
class ChefInfra < ChefBase
|
|
26
|
-
kitchen_provisioner_api_version 2
|
|
27
|
-
|
|
28
|
-
plugin_version Kitchen::VERSION
|
|
29
|
-
|
|
30
|
-
default_config :client_rb, {}
|
|
31
|
-
default_config :named_run_list, {}
|
|
32
|
-
default_config :json_attributes, true
|
|
33
|
-
default_config :chef_zero_host, nil
|
|
34
|
-
default_config :chef_zero_port, 8889
|
|
35
|
-
|
|
36
|
-
default_config :chef_client_path do |provisioner|
|
|
37
|
-
provisioner
|
|
38
|
-
.remote_path_join(%W{#{provisioner[:chef_omnibus_root]} bin chef-client})
|
|
39
|
-
.tap { |path| path.concat(".bat") if provisioner.windows_os? }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
default_config :ruby_bindir do |provisioner|
|
|
43
|
-
provisioner
|
|
44
|
-
.remote_path_join(%W{#{provisioner[:chef_omnibus_root]} embedded bin})
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# (see Base#create_sandbox)
|
|
48
|
-
def create_sandbox
|
|
49
|
-
super
|
|
50
|
-
prepare_validation_pem
|
|
51
|
-
prepare_config_rb
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def run_command
|
|
55
|
-
cmd = "#{sudo(config[:chef_client_path])} --local-mode".tap { |str| str.insert(0, "& ") if powershell_shell? }
|
|
56
|
-
|
|
57
|
-
chef_cmd(cmd)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
# Adds optional flags to a chef-client command, depending on
|
|
63
|
-
# configuration data. Note that this method mutates the incoming Array.
|
|
64
|
-
#
|
|
65
|
-
# @param args [Array<String>] array of flags
|
|
66
|
-
# @api private
|
|
67
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
68
|
-
def add_optional_chef_client_args!(args)
|
|
69
|
-
if config[:json_attributes]
|
|
70
|
-
json = remote_path_join(config[:root_path], "dna.json")
|
|
71
|
-
args << "--json-attributes #{json}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
args << "--logfile #{config[:log_file]}" if config[:log_file]
|
|
75
|
-
|
|
76
|
-
# these flags are chef-client local mode only and will not work
|
|
77
|
-
# on older versions of chef-client
|
|
78
|
-
if config[:chef_zero_host]
|
|
79
|
-
args << "--chef-zero-host #{config[:chef_zero_host]}"
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
if config[:chef_zero_port]
|
|
83
|
-
args << "--chef-zero-port #{config[:chef_zero_port]}"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
args << "--profile-ruby" if config[:profile_ruby]
|
|
87
|
-
|
|
88
|
-
if config[:slow_resource_report]
|
|
89
|
-
if config[:slow_resource_report].is_a?(Integer)
|
|
90
|
-
args << "--slow-report #{config[:slow_resource_report]}"
|
|
91
|
-
else
|
|
92
|
-
args << "--slow-report"
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
97
|
-
|
|
98
|
-
# Returns an Array of command line arguments for the chef client.
|
|
99
|
-
#
|
|
100
|
-
# @return [Array<String>] an array of command line arguments
|
|
101
|
-
# @api private
|
|
102
|
-
def chef_args(client_rb_filename)
|
|
103
|
-
level = config[:log_level]
|
|
104
|
-
args = [
|
|
105
|
-
"--config #{remote_path_join(config[:root_path], client_rb_filename)}",
|
|
106
|
-
"--log_level #{level}",
|
|
107
|
-
"--force-formatter",
|
|
108
|
-
"--no-color",
|
|
109
|
-
]
|
|
110
|
-
add_optional_chef_client_args!(args)
|
|
111
|
-
|
|
112
|
-
args
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Generates a string of shell environment variables needed for the
|
|
116
|
-
# chef-client-zero.rb shim script to properly function.
|
|
117
|
-
#
|
|
118
|
-
# @return [String] a shell script string
|
|
119
|
-
# @api private
|
|
120
|
-
def chef_client_zero_env
|
|
121
|
-
root = config[:root_path]
|
|
122
|
-
gem_home = gem_path = remote_path_join(root, "chef-client-zero-gems")
|
|
123
|
-
gem_cache = remote_path_join(gem_home, "cache")
|
|
124
|
-
|
|
125
|
-
[
|
|
126
|
-
shell_env_var("CHEF_REPO_PATH", root),
|
|
127
|
-
shell_env_var("GEM_HOME", gem_home),
|
|
128
|
-
shell_env_var("GEM_PATH", gem_path),
|
|
129
|
-
shell_env_var("GEM_CACHE", gem_cache),
|
|
130
|
-
].join("\n").concat("\n")
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Writes a fake (but valid) validation.pem into the sandbox directory.
|
|
134
|
-
#
|
|
135
|
-
# @api private
|
|
136
|
-
def prepare_validation_pem
|
|
137
|
-
info("Preparing validation.pem")
|
|
138
|
-
debug("Using a dummy validation.pem")
|
|
139
|
-
|
|
140
|
-
source = File.join(File.dirname(__FILE__),
|
|
141
|
-
%w{.. .. .. support dummy-validation.pem})
|
|
142
|
-
FileUtils.cp(source, File.join(sandbox_path, "validation.pem"))
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Returns the command that will run a backwards compatible shim script
|
|
146
|
-
# that approximates local mode in a modern chef-client run.
|
|
147
|
-
#
|
|
148
|
-
# @return [String] the command string
|
|
149
|
-
# @api private
|
|
150
|
-
def shim_command
|
|
151
|
-
ruby = remote_path_join(config[:ruby_bindir], "ruby")
|
|
152
|
-
.tap { |path| path.concat(".exe") if windows_os? }
|
|
153
|
-
shim = remote_path_join(config[:root_path], "chef-client-zero.rb")
|
|
154
|
-
|
|
155
|
-
"#{chef_client_zero_env}\n#{sudo(ruby)} #{shim}"
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# This provisioner supports policyfiles, so override the default (which
|
|
159
|
-
# is false)
|
|
160
|
-
# @return [true] always returns true
|
|
161
|
-
# @api private
|
|
162
|
-
def supports_policyfile?
|
|
163
|
-
true
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
|
3
|
-
#
|
|
4
|
-
# Copyright (C) 2013, Fletcher Nichol
|
|
5
|
-
#
|
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
# you may not use this file except in compliance with the License.
|
|
8
|
-
# You may obtain a copy of the License at
|
|
9
|
-
#
|
|
10
|
-
# https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
#
|
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
# See the License for the specific language governing permissions and
|
|
16
|
-
# limitations under the License.
|
|
17
|
-
|
|
18
|
-
require_relative "chef_base"
|
|
19
|
-
|
|
20
|
-
module Kitchen
|
|
21
|
-
module Provisioner
|
|
22
|
-
# Chef Solo provisioner.
|
|
23
|
-
#
|
|
24
|
-
# @author Fletcher Nichol <fnichol@nichol.ca>
|
|
25
|
-
class ChefSolo < ChefBase
|
|
26
|
-
kitchen_provisioner_api_version 2
|
|
27
|
-
|
|
28
|
-
plugin_version Kitchen::VERSION
|
|
29
|
-
|
|
30
|
-
# ChefSolo is dependent on Berkshelf, which is not thread-safe.
|
|
31
|
-
# See discussion on https://github.com/test-kitchen/test-kitchen/issues/1307
|
|
32
|
-
no_parallel_for :converge
|
|
33
|
-
|
|
34
|
-
default_config :solo_rb, {}
|
|
35
|
-
|
|
36
|
-
default_config :chef_solo_path do |provisioner|
|
|
37
|
-
provisioner
|
|
38
|
-
.remote_path_join(%W{#{provisioner[:chef_omnibus_root]} bin chef-solo})
|
|
39
|
-
.tap { |path| path.concat(".bat") if provisioner.windows_os? }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# (see Base#config_filename)
|
|
43
|
-
def config_filename
|
|
44
|
-
"solo.rb"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# (see Base#create_sandbox)
|
|
48
|
-
def create_sandbox
|
|
49
|
-
super
|
|
50
|
-
prepare_config_rb
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# (see Base#run_command)
|
|
54
|
-
def run_command
|
|
55
|
-
cmd = sudo(config[:chef_solo_path]).dup
|
|
56
|
-
.tap { |str| str.insert(0, "& ") if powershell_shell? }
|
|
57
|
-
|
|
58
|
-
chef_cmd(cmd)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
# Returns an Array of command line arguments for the chef client.
|
|
64
|
-
#
|
|
65
|
-
# @return [Array<String>] an array of command line arguments
|
|
66
|
-
# @api private
|
|
67
|
-
def chef_args(solo_rb_filename)
|
|
68
|
-
args = [
|
|
69
|
-
"--config #{remote_path_join(config[:root_path], solo_rb_filename)}",
|
|
70
|
-
"--log_level #{config[:log_level]}",
|
|
71
|
-
"--force-formatter",
|
|
72
|
-
"--no-color",
|
|
73
|
-
"--json-attributes #{remote_path_join(config[:root_path], "dna.json")}",
|
|
74
|
-
]
|
|
75
|
-
args << "--logfile #{config[:log_file]}" if config[:log_file]
|
|
76
|
-
args << "--profile-ruby" if config[:profile_ruby]
|
|
77
|
-
args << "--legacy-mode" if config[:legacy_mode]
|
|
78
|
-
args
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Author:: Thomas Heinen (<thomas.heinen@gmail.com>)
|
|
3
|
-
#
|
|
4
|
-
# Copyright (C) 2023, Thomas Heinen
|
|
5
|
-
#
|
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
# you may not use this file except in compliance with the License.
|
|
8
|
-
# You may obtain a copy of the License at
|
|
9
|
-
#
|
|
10
|
-
# https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
#
|
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
# See the License for the specific language governing permissions and
|
|
16
|
-
# limitations under the License.
|
|
17
|
-
|
|
18
|
-
require_relative "chef_infra"
|
|
19
|
-
|
|
20
|
-
module Kitchen
|
|
21
|
-
module Provisioner
|
|
22
|
-
# Chef Target provisioner.
|
|
23
|
-
#
|
|
24
|
-
# @author Thomas Heinen <thomas.heinen@gmail.com>
|
|
25
|
-
class ChefTarget < ChefInfra
|
|
26
|
-
MIN_VERSION_REQUIRED = "19.0.0".freeze
|
|
27
|
-
class ChefVersionTooLow < UserError; end
|
|
28
|
-
class ChefClientNotFound < UserError; end
|
|
29
|
-
class RequireTrainTransport < UserError; end
|
|
30
|
-
|
|
31
|
-
default_config :install_strategy, "none"
|
|
32
|
-
default_config :sudo, true
|
|
33
|
-
|
|
34
|
-
def install_command; ""; end
|
|
35
|
-
def init_command; ""; end
|
|
36
|
-
def prepare_command; ""; end
|
|
37
|
-
|
|
38
|
-
def chef_args(client_rb_filename)
|
|
39
|
-
# Dummy execution to initialize and test remote connection
|
|
40
|
-
connection = instance.remote_exec("echo Connection established")
|
|
41
|
-
|
|
42
|
-
check_transport(connection)
|
|
43
|
-
check_local_chef_client
|
|
44
|
-
|
|
45
|
-
instance_name = instance.name
|
|
46
|
-
credentials_file = File.join(kitchen_basepath, ".kitchen", instance_name + ".ini")
|
|
47
|
-
File.write(credentials_file, connection.credentials_file)
|
|
48
|
-
|
|
49
|
-
super.push(
|
|
50
|
-
"--target #{instance_name}",
|
|
51
|
-
"--credentials #{credentials_file}"
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def check_transport(connection)
|
|
56
|
-
debug("Checking for active transport")
|
|
57
|
-
|
|
58
|
-
unless connection.respond_to? "train_uri"
|
|
59
|
-
error("Chef Target Mode provisioner requires a Train-based transport like kitchen-transport-train")
|
|
60
|
-
raise RequireTrainTransport.new("No Train transport")
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
debug("Kitchen transport responds to train_uri function call, as required")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def check_local_chef_client
|
|
67
|
-
debug("Checking for chef-client version")
|
|
68
|
-
|
|
69
|
-
begin
|
|
70
|
-
client_version = `chef-client -v`.chop.split(":")[-1]
|
|
71
|
-
rescue Errno::ENOENT => e
|
|
72
|
-
error("Error determining Chef Infra version: #{e.exception.message}")
|
|
73
|
-
raise ChefClientNotFound.new("Need chef-client installed locally")
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
minimum_version = Gem::Version.new(MIN_VERSION_REQUIRED)
|
|
77
|
-
installed_version = Gem::Version.new(client_version)
|
|
78
|
-
|
|
79
|
-
if installed_version < minimum_version
|
|
80
|
-
error("Found Chef Infra version #{installed_version}, but require #{minimum_version} for Target Mode")
|
|
81
|
-
raise ChefVersionTooLow.new("Need version #{MIN_VERSION_REQUIRED} or higher")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
debug("Chef Infra found and version constraints match")
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def kitchen_basepath
|
|
88
|
-
instance.driver.config[:kitchen_root]
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def create_sandbox
|
|
92
|
-
super
|
|
93
|
-
|
|
94
|
-
# Change config.rb to point to the local sandbox path, not to /tmp/kitchen
|
|
95
|
-
config[:root_path] = sandbox_path
|
|
96
|
-
prepare_config_rb
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def call(state)
|
|
100
|
-
remote_connection = instance.transport.connection(state)
|
|
101
|
-
|
|
102
|
-
config[:uploads].to_h.each do |locals, remote|
|
|
103
|
-
debug("Uploading #{Array(locals).join(", ")} to #{remote}")
|
|
104
|
-
remote_connection.upload(locals.to_s, remote)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# no installation
|
|
108
|
-
create_sandbox
|
|
109
|
-
# no prepare command
|
|
110
|
-
|
|
111
|
-
# Stream output to logger
|
|
112
|
-
require "open3"
|
|
113
|
-
Open3.popen2e(run_command) do |_stdin, output, _thread|
|
|
114
|
-
output.each { |line| logger << line }
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
info("Downloading files from #{instance.to_str}")
|
|
118
|
-
config[:downloads].to_h.each do |remotes, local|
|
|
119
|
-
debug("Downloading #{Array(remotes).join(", ")} to #{local}")
|
|
120
|
-
remote_connection.download(remotes, local)
|
|
121
|
-
end
|
|
122
|
-
debug("Download complete")
|
|
123
|
-
rescue Kitchen::Transport::TransportFailed => ex
|
|
124
|
-
raise ActionFailed, ex.message
|
|
125
|
-
ensure
|
|
126
|
-
cleanup_sandbox
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Deprecated AS PER THE PR - https://github.com/test-kitchen/test-kitchen/pull/1730
|
|
2
|
-
require_relative "chef_infra"
|
|
3
|
-
|
|
4
|
-
module Kitchen
|
|
5
|
-
module Provisioner
|
|
6
|
-
# Chef Zero provisioner.
|
|
7
|
-
#
|
|
8
|
-
# @author Fletcher Nichol <fnichol@nichol.ca>
|
|
9
|
-
class ChefZero < ChefInfra
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
data/lib/kitchen/ssh.rb
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
|
3
|
-
#
|
|
4
|
-
# Copyright (C) 2013, Fletcher Nichol
|
|
5
|
-
#
|
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
# you may not use this file except in compliance with the License.
|
|
8
|
-
# You may obtain a copy of the License at
|
|
9
|
-
#
|
|
10
|
-
# https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
#
|
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
# See the License for the specific language governing permissions and
|
|
16
|
-
# limitations under the License.
|
|
17
|
-
|
|
18
|
-
require "logger"
|
|
19
|
-
module Net
|
|
20
|
-
autoload :SSH, "net/ssh"
|
|
21
|
-
end
|
|
22
|
-
require "socket" unless defined?(Socket)
|
|
23
|
-
|
|
24
|
-
require_relative "errors"
|
|
25
|
-
require_relative "login_command"
|
|
26
|
-
require_relative "util"
|
|
27
|
-
|
|
28
|
-
module Kitchen
|
|
29
|
-
# Wrapped exception for any internally raised SSH-related errors.
|
|
30
|
-
#
|
|
31
|
-
# @author Fletcher Nichol <fnichol@nichol.ca>
|
|
32
|
-
class SSHFailed < TransientFailure; end
|
|
33
|
-
|
|
34
|
-
# Class to help establish SSH connections, issue remote commands, and
|
|
35
|
-
# transfer files between a local system and remote node.
|
|
36
|
-
#
|
|
37
|
-
# @author Fletcher Nichol <fnichol@nichol.ca>
|
|
38
|
-
class SSH
|
|
39
|
-
# Constructs a new SSH object.
|
|
40
|
-
#
|
|
41
|
-
# @example basic usage
|
|
42
|
-
#
|
|
43
|
-
# ssh = Kitchen::SSH.new("remote.example.com", "root")
|
|
44
|
-
# ssh.exec("sudo apt-get update")
|
|
45
|
-
# ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
|
|
46
|
-
# ssh.shutdown
|
|
47
|
-
#
|
|
48
|
-
# @example block usage
|
|
49
|
-
#
|
|
50
|
-
# Kitchen::SSH.new("remote.example.com", "root") do |ssh|
|
|
51
|
-
# ssh.exec("sudo apt-get update")
|
|
52
|
-
# ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
|
|
53
|
-
# end
|
|
54
|
-
#
|
|
55
|
-
# @param hostname [String] the remote hostname (IP address, FQDN, etc.)
|
|
56
|
-
# @param username [String] the username for the remote host
|
|
57
|
-
# @param options [Hash] configuration options
|
|
58
|
-
# @option options [Logger] :logger the logger to use
|
|
59
|
-
# (default: `::Logger.new(STDOUT)`)
|
|
60
|
-
# @yield [self] if a block is given then the constructed object yields
|
|
61
|
-
# itself and calls `#shutdown` at the end, closing the remote connection
|
|
62
|
-
def initialize(hostname, username, options = {})
|
|
63
|
-
@hostname = hostname
|
|
64
|
-
@username = username
|
|
65
|
-
@options = options.dup
|
|
66
|
-
@logger = @options.delete(:logger) || ::Logger.new(STDOUT)
|
|
67
|
-
|
|
68
|
-
if block_given?
|
|
69
|
-
yield self
|
|
70
|
-
shutdown
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Execute a command on the remote host.
|
|
75
|
-
#
|
|
76
|
-
# @param cmd [String] command string to execute
|
|
77
|
-
# @raise [SSHFailed] if the command does not exit with a 0 code
|
|
78
|
-
def exec(cmd)
|
|
79
|
-
string_to_mask = "[SSH] #{self} (#{cmd})"
|
|
80
|
-
masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
|
|
81
|
-
logger.debug(masked_string)
|
|
82
|
-
exit_code = exec_with_exit(cmd)
|
|
83
|
-
|
|
84
|
-
if exit_code != 0
|
|
85
|
-
raise SSHFailed, "SSH exited (#{exit_code}) for command: [#{cmd}]"
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Uploads a local file to remote host.
|
|
90
|
-
#
|
|
91
|
-
# @param local [String] path to local file
|
|
92
|
-
# @param remote [String] path to remote file destination
|
|
93
|
-
# @param options [Hash] configuration options that are passed to
|
|
94
|
-
# `Net::SCP.upload`
|
|
95
|
-
# @see https://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
|
|
96
|
-
def upload!(local, remote, options = {}, &progress)
|
|
97
|
-
require "net/scp" unless defined?(Net::SCP)
|
|
98
|
-
if progress.nil?
|
|
99
|
-
progress = lambda do |_ch, name, sent, total|
|
|
100
|
-
logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
session.scp.upload!(local, remote, options, &progress)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def upload(local, remote, options = {}, &progress)
|
|
108
|
-
require "net/scp" unless defined?(Net::SCP)
|
|
109
|
-
if progress.nil?
|
|
110
|
-
progress = lambda do |_ch, name, sent, total|
|
|
111
|
-
if sent == total
|
|
112
|
-
logger.debug("Async Uploaded #{name} (#{total} bytes)")
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
session.scp.upload(local, remote, options, &progress)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Uploads a recursive directory to remote host.
|
|
121
|
-
#
|
|
122
|
-
# @param local [String] path to local file or directory
|
|
123
|
-
# @param remote [String] path to remote file destination
|
|
124
|
-
# @param options [Hash] configuration options that are passed to
|
|
125
|
-
# `Net::SCP.upload`
|
|
126
|
-
# @option options [true,false] :recursive recursive copy (default: `true`)
|
|
127
|
-
# @see https://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
|
|
128
|
-
def upload_path!(local, remote, options = {}, &progress)
|
|
129
|
-
options = { recursive: true }.merge(options)
|
|
130
|
-
|
|
131
|
-
upload!(local, remote, options, &progress)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def upload_path(local, remote, options = {}, &progress)
|
|
135
|
-
options = { recursive: true }.merge(options)
|
|
136
|
-
upload(local, remote, options, &progress)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Shuts down the session connection, if it is still active.
|
|
140
|
-
def shutdown
|
|
141
|
-
return if @session.nil?
|
|
142
|
-
|
|
143
|
-
string_to_mask = "[SSH] closing connection to #{self}"
|
|
144
|
-
masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
|
|
145
|
-
logger.debug(masked_string)
|
|
146
|
-
session.shutdown!
|
|
147
|
-
ensure
|
|
148
|
-
@session = nil
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Blocks until the remote host's SSH TCP port is listening.
|
|
152
|
-
def wait
|
|
153
|
-
logger.info("Waiting for #{hostname}:#{port}...") until test_ssh
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Builds a LoginCommand which can be used to open an interactive session
|
|
157
|
-
# on the remote host.
|
|
158
|
-
#
|
|
159
|
-
# @return [LoginCommand] the login command
|
|
160
|
-
def login_command
|
|
161
|
-
args = %w{ -o UserKnownHostsFile=/dev/null }
|
|
162
|
-
args += %w{ -o StrictHostKeyChecking=no }
|
|
163
|
-
args += %w{ -o IdentitiesOnly=yes } if options[:keys]
|
|
164
|
-
args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
|
|
165
|
-
if options.key?(:forward_agent)
|
|
166
|
-
args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} }
|
|
167
|
-
end
|
|
168
|
-
Array(options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key} } }
|
|
169
|
-
args += %W{ -p #{port} }
|
|
170
|
-
args += %W{ #{username}@#{hostname} }
|
|
171
|
-
|
|
172
|
-
LoginCommand.new("ssh", args)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
private
|
|
176
|
-
|
|
177
|
-
# TCP socket exceptions
|
|
178
|
-
SOCKET_EXCEPTIONS = [
|
|
179
|
-
SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
|
180
|
-
Errno::ENETUNREACH, IOError
|
|
181
|
-
].freeze
|
|
182
|
-
|
|
183
|
-
# @return [String] the remote hostname
|
|
184
|
-
# @api private
|
|
185
|
-
attr_reader :hostname
|
|
186
|
-
|
|
187
|
-
# @return [String] the username for the remote host
|
|
188
|
-
# @api private
|
|
189
|
-
attr_reader :username
|
|
190
|
-
|
|
191
|
-
# @return [Hash] SSH options, passed to `Net::SSH.start`
|
|
192
|
-
attr_reader :options
|
|
193
|
-
|
|
194
|
-
# @return [Logger] the logger to use
|
|
195
|
-
# @api private
|
|
196
|
-
attr_reader :logger
|
|
197
|
-
|
|
198
|
-
# Builds the Net::SSH session connection or returns the existing one if
|
|
199
|
-
# built.
|
|
200
|
-
#
|
|
201
|
-
# @return [Net::SSH::Connection::Session] the SSH connection session
|
|
202
|
-
# @api private
|
|
203
|
-
def session
|
|
204
|
-
@session ||= establish_connection
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Establish a connection session to the remote host.
|
|
208
|
-
#
|
|
209
|
-
# @return [Net::SSH::Connection::Session] the SSH connection session
|
|
210
|
-
# @api private
|
|
211
|
-
def establish_connection
|
|
212
|
-
rescue_exceptions = [
|
|
213
|
-
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
|
|
214
|
-
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
|
|
215
|
-
Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout
|
|
216
|
-
]
|
|
217
|
-
retries = options[:ssh_retries] || 3
|
|
218
|
-
|
|
219
|
-
begin
|
|
220
|
-
string_to_mask = "[SSH] opening connection to #{self}"
|
|
221
|
-
masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
|
|
222
|
-
logger.debug(masked_string)
|
|
223
|
-
Net::SSH.start(hostname, username, options)
|
|
224
|
-
rescue *rescue_exceptions => e
|
|
225
|
-
retries -= 1
|
|
226
|
-
if retries > 0
|
|
227
|
-
logger.info("[SSH] connection failed, retrying (#{e.inspect})")
|
|
228
|
-
sleep options[:ssh_timeout] || 1
|
|
229
|
-
retry
|
|
230
|
-
else
|
|
231
|
-
logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
|
|
232
|
-
raise
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# String representation of object, reporting its connection details and
|
|
238
|
-
# configuration.
|
|
239
|
-
#
|
|
240
|
-
# @api private
|
|
241
|
-
def to_s
|
|
242
|
-
"#{username}@#{hostname}:#{port}<#{options.inspect}>"
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# @return [Integer] SSH port (default: 22)
|
|
246
|
-
# @api private
|
|
247
|
-
def port
|
|
248
|
-
options.fetch(:port, 22)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Execute a remote command and return the command's exit code.
|
|
252
|
-
#
|
|
253
|
-
# @param cmd [String] command string to execute
|
|
254
|
-
# @return [Integer] the exit code of the command
|
|
255
|
-
# @api private
|
|
256
|
-
def exec_with_exit(cmd)
|
|
257
|
-
exit_code = nil
|
|
258
|
-
session.open_channel do |channel|
|
|
259
|
-
channel.request_pty
|
|
260
|
-
|
|
261
|
-
channel.exec(cmd) do |_ch, _success|
|
|
262
|
-
channel.on_data do |_ch, data|
|
|
263
|
-
logger << data
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
channel.on_extended_data do |_ch, _type, data|
|
|
267
|
-
logger << data
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
channel.on_request("exit-status") do |_ch, data|
|
|
271
|
-
exit_code = data.read_long
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
session.loop
|
|
276
|
-
exit_code
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Test a remote TCP socket (presumably SSH) for connectivity.
|
|
280
|
-
#
|
|
281
|
-
# @return [true,false] a truthy value if the socket is ready and false
|
|
282
|
-
# otherwise
|
|
283
|
-
# @api private
|
|
284
|
-
def test_ssh
|
|
285
|
-
socket = TCPSocket.new(hostname, port)
|
|
286
|
-
IO.select([socket], nil, nil, 5)
|
|
287
|
-
rescue *SOCKET_EXCEPTIONS
|
|
288
|
-
sleep options[:ssh_timeout] || 2
|
|
289
|
-
false
|
|
290
|
-
rescue Errno::EPERM, Errno::ETIMEDOUT
|
|
291
|
-
false
|
|
292
|
-
ensure
|
|
293
|
-
socket && socket.close
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
end
|