test-kitchen 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.cane +1 -1
- data/.rubocop.yml +3 -0
- data/.travis.yml +20 -9
- data/CHANGELOG.md +219 -108
- data/Gemfile +10 -6
- data/Guardfile +38 -9
- data/README.md +11 -1
- data/Rakefile +21 -37
- data/bin/kitchen +4 -4
- data/features/kitchen_action_commands.feature +161 -0
- data/features/kitchen_console_command.feature +34 -0
- data/features/kitchen_diagnose_command.feature +64 -0
- data/features/kitchen_init_command.feature +29 -17
- data/features/kitchen_list_command.feature +2 -2
- data/features/kitchen_login_command.feature +56 -0
- data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
- data/features/kitchen_test_command.feature +88 -0
- data/features/step_definitions/gem_steps.rb +8 -6
- data/features/step_definitions/git_steps.rb +4 -2
- data/features/step_definitions/output_steps.rb +5 -0
- data/features/support/env.rb +12 -9
- data/lib/kitchen.rb +60 -38
- data/lib/kitchen/base64_stream.rb +55 -0
- data/lib/kitchen/busser.rb +124 -58
- data/lib/kitchen/cli.rb +121 -38
- data/lib/kitchen/collection.rb +3 -3
- data/lib/kitchen/color.rb +4 -4
- data/lib/kitchen/command.rb +78 -11
- data/lib/kitchen/command/action.rb +3 -2
- data/lib/kitchen/command/console.rb +12 -5
- data/lib/kitchen/command/diagnose.rb +17 -3
- data/lib/kitchen/command/driver_discover.rb +26 -7
- data/lib/kitchen/command/exec.rb +41 -0
- data/lib/kitchen/command/list.rb +44 -14
- data/lib/kitchen/command/login.rb +2 -1
- data/lib/kitchen/command/sink.rb +2 -1
- data/lib/kitchen/command/test.rb +5 -4
- data/lib/kitchen/config.rb +146 -14
- data/lib/kitchen/configurable.rb +314 -0
- data/lib/kitchen/data_munger.rb +522 -18
- data/lib/kitchen/diagnostic.rb +43 -4
- data/lib/kitchen/driver.rb +4 -4
- data/lib/kitchen/driver/base.rb +80 -115
- data/lib/kitchen/driver/dummy.rb +34 -6
- data/lib/kitchen/driver/proxy.rb +14 -3
- data/lib/kitchen/driver/ssh_base.rb +61 -7
- data/lib/kitchen/errors.rb +109 -9
- data/lib/kitchen/generator/driver_create.rb +39 -5
- data/lib/kitchen/generator/init.rb +130 -45
- data/lib/kitchen/instance.rb +162 -28
- data/lib/kitchen/lazy_hash.rb +79 -7
- data/lib/kitchen/loader/yaml.rb +159 -27
- data/lib/kitchen/logger.rb +267 -21
- data/lib/kitchen/logging.rb +30 -3
- data/lib/kitchen/login_command.rb +11 -2
- data/lib/kitchen/metadata_chopper.rb +2 -2
- data/lib/kitchen/provisioner.rb +4 -4
- data/lib/kitchen/provisioner/base.rb +107 -103
- data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
- data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
- data/lib/kitchen/provisioner/chef_base.rb +206 -167
- data/lib/kitchen/provisioner/chef_solo.rb +25 -7
- data/lib/kitchen/provisioner/chef_zero.rb +105 -29
- data/lib/kitchen/provisioner/dummy.rb +1 -1
- data/lib/kitchen/provisioner/shell.rb +21 -6
- data/lib/kitchen/rake_tasks.rb +8 -3
- data/lib/kitchen/shell_out.rb +15 -18
- data/lib/kitchen/ssh.rb +122 -27
- data/lib/kitchen/state_file.rb +24 -7
- data/lib/kitchen/thor_tasks.rb +9 -4
- data/lib/kitchen/util.rb +43 -118
- data/lib/kitchen/version.rb +1 -1
- data/lib/vendor/hash_recursive_merge.rb +10 -2
- data/spec/kitchen/base64_stream_spec.rb +77 -0
- data/spec/kitchen/busser_spec.rb +490 -0
- data/spec/kitchen/collection_spec.rb +10 -10
- data/spec/kitchen/color_spec.rb +2 -2
- data/spec/kitchen/config_spec.rb +234 -62
- data/spec/kitchen/configurable_spec.rb +490 -0
- data/spec/kitchen/data_munger_spec.rb +1070 -862
- data/spec/kitchen/diagnostic_spec.rb +79 -0
- data/spec/kitchen/driver/base_spec.rb +80 -85
- data/spec/kitchen/driver/dummy_spec.rb +43 -14
- data/spec/kitchen/driver/proxy_spec.rb +134 -0
- data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
- data/spec/kitchen/driver_spec.rb +15 -15
- data/spec/kitchen/errors_spec.rb +309 -0
- data/spec/kitchen/instance_spec.rb +143 -46
- data/spec/kitchen/lazy_hash_spec.rb +36 -9
- data/spec/kitchen/loader/yaml_spec.rb +237 -226
- data/spec/kitchen/logger_spec.rb +419 -0
- data/spec/kitchen/logging_spec.rb +59 -0
- data/spec/kitchen/login_command_spec.rb +49 -0
- data/spec/kitchen/metadata_chopper_spec.rb +82 -0
- data/spec/kitchen/platform_spec.rb +4 -4
- data/spec/kitchen/provisioner/base_spec.rb +65 -125
- data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
- data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
- data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
- data/spec/kitchen/provisioner/shell_spec.rb +269 -0
- data/spec/kitchen/provisioner_spec.rb +6 -6
- data/spec/kitchen/shell_out_spec.rb +143 -0
- data/spec/kitchen/ssh_spec.rb +683 -0
- data/spec/kitchen/state_file_spec.rb +28 -21
- data/spec/kitchen/suite_spec.rb +7 -7
- data/spec/kitchen/util_spec.rb +68 -10
- data/spec/kitchen_spec.rb +107 -0
- data/spec/spec_helper.rb +18 -13
- data/support/chef-client-zero.rb +10 -9
- data/support/chef_helpers.sh +16 -0
- data/support/download_helpers.sh +109 -0
- data/test-kitchen.gemspec +42 -33
- metadata +107 -33
data/lib/kitchen/ssh.rb
CHANGED
@@ -16,20 +16,20 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
19
|
+
require "logger"
|
20
|
+
require "net/ssh"
|
21
|
+
require "net/scp"
|
22
|
+
require "socket"
|
23
23
|
|
24
|
-
require
|
25
|
-
require
|
24
|
+
require "kitchen/errors"
|
25
|
+
require "kitchen/login_command"
|
26
26
|
|
27
27
|
module Kitchen
|
28
28
|
|
29
29
|
# Wrapped exception for any internally raised SSH-related errors.
|
30
30
|
#
|
31
31
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
32
|
-
class SSHFailed < TransientFailure
|
32
|
+
class SSHFailed < TransientFailure; end
|
33
33
|
|
34
34
|
# Class to help establish SSH connections, issue remote commands, and
|
35
35
|
# transfer files between a local system and remote node.
|
@@ -37,6 +37,29 @@ module Kitchen
|
|
37
37
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
38
38
|
class SSH
|
39
39
|
|
40
|
+
# Constructs a new SSH object.
|
41
|
+
#
|
42
|
+
# @example basic usage
|
43
|
+
#
|
44
|
+
# ssh = Kitchen::SSH.new("remote.example.com", "root")
|
45
|
+
# ssh.exec("sudo apt-get update")
|
46
|
+
# ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
|
47
|
+
# ssh.shutdown
|
48
|
+
#
|
49
|
+
# @example block usage
|
50
|
+
#
|
51
|
+
# Kitchen::SSH.new("remote.example.com", "root") do |ssh|
|
52
|
+
# ssh.exec("sudo apt-get update")
|
53
|
+
# ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# @param hostname [String] the remote hostname (IP address, FQDN, etc.)
|
57
|
+
# @param username [String] the username for the remote host
|
58
|
+
# @param options [Hash] configuration options
|
59
|
+
# @option options [Logger] :logger the logger to use
|
60
|
+
# (default: `::Logger.new(STDOUT)`)
|
61
|
+
# @yield [self] if a block is given then the constructed object yields
|
62
|
+
# itself and calls `#shutdown` at the end, closing the remote connection
|
40
63
|
def initialize(hostname, username, options = {})
|
41
64
|
@hostname = hostname
|
42
65
|
@username = username
|
@@ -49,6 +72,10 @@ module Kitchen
|
|
49
72
|
end
|
50
73
|
end
|
51
74
|
|
75
|
+
# Execute a command on the remote host.
|
76
|
+
#
|
77
|
+
# @param cmd [String] command string to execute
|
78
|
+
# @raise [SSHFailed] if the command does not exit with a 0 code
|
52
79
|
def exec(cmd)
|
53
80
|
logger.debug("[SSH] #{self} (#{cmd})")
|
54
81
|
exit_code = exec_with_exit(cmd)
|
@@ -58,9 +85,16 @@ module Kitchen
|
|
58
85
|
end
|
59
86
|
end
|
60
87
|
|
88
|
+
# Uploads a local file to remote host.
|
89
|
+
#
|
90
|
+
# @param local [String] path to local file
|
91
|
+
# @param remote [String] path to remote file destination
|
92
|
+
# @param options [Hash] configuration options that are passed to
|
93
|
+
# `Net::SCP.upload`
|
94
|
+
# @see http://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
|
61
95
|
def upload!(local, remote, options = {}, &progress)
|
62
96
|
if progress.nil?
|
63
|
-
progress = lambda { |
|
97
|
+
progress = lambda { |_ch, name, sent, total|
|
64
98
|
if sent == total
|
65
99
|
logger.debug("Uploaded #{name} (#{total} bytes)")
|
66
100
|
end
|
@@ -70,12 +104,21 @@ module Kitchen
|
|
70
104
|
session.scp.upload!(local, remote, options, &progress)
|
71
105
|
end
|
72
106
|
|
107
|
+
# Uploads a recursive directory to remote host.
|
108
|
+
#
|
109
|
+
# @param local [String] path to local file or directory
|
110
|
+
# @param remote [String] path to remote file destination
|
111
|
+
# @param options [Hash] configuration options that are passed to
|
112
|
+
# `Net::SCP.upload`
|
113
|
+
# @option options [true,false] :recursive recursive copy (default: `true`)
|
114
|
+
# @see http://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
|
73
115
|
def upload_path!(local, remote, options = {}, &progress)
|
74
116
|
options = { :recursive => true }.merge(options)
|
75
117
|
|
76
118
|
upload!(local, remote, options, &progress)
|
77
119
|
end
|
78
120
|
|
121
|
+
# Shuts down the session connection, if it is still active.
|
79
122
|
def shutdown
|
80
123
|
return if @session.nil?
|
81
124
|
|
@@ -85,45 +128,82 @@ module Kitchen
|
|
85
128
|
@session = nil
|
86
129
|
end
|
87
130
|
|
131
|
+
# Blocks until the remote host's SSH TCP port is listening.
|
88
132
|
def wait
|
89
133
|
logger.info("Waiting for #{hostname}:#{port}...") until test_ssh
|
90
134
|
end
|
91
135
|
|
136
|
+
# Builds a LoginCommand which can be used to open an interactive session
|
137
|
+
# on the remote host.
|
138
|
+
#
|
139
|
+
# @return [LoginCommand] the login command
|
92
140
|
def login_command
|
93
|
-
args = %W
|
94
|
-
args += %W
|
95
|
-
args += %W
|
96
|
-
args += %W
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
args += %W
|
141
|
+
args = %W[ -o UserKnownHostsFile=/dev/null ]
|
142
|
+
args += %W[ -o StrictHostKeyChecking=no ]
|
143
|
+
args += %W[ -o IdentitiesOnly=yes ] if options[:keys]
|
144
|
+
args += %W[ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} ]
|
145
|
+
if options.key?(:forward_agent)
|
146
|
+
args += %W[ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} ]
|
147
|
+
end
|
148
|
+
Array(options[:keys]).each { |ssh_key| args += %W[ -i #{ssh_key} ] }
|
149
|
+
args += %W[ -p #{port} ]
|
150
|
+
args += %W[ #{username}@#{hostname} ]
|
101
151
|
|
102
152
|
LoginCommand.new(["ssh", *args])
|
103
153
|
end
|
104
154
|
|
105
155
|
private
|
106
156
|
|
107
|
-
|
157
|
+
# TCP socket exceptions
|
158
|
+
SOCKET_EXCEPTIONS = [
|
159
|
+
SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
160
|
+
Errno::ENETUNREACH, IOError
|
161
|
+
]
|
162
|
+
|
163
|
+
# @return [String] the remote hostname
|
164
|
+
# @api private
|
165
|
+
attr_reader :hostname
|
166
|
+
|
167
|
+
# @return [String] the username for the remote host
|
168
|
+
# @api private
|
169
|
+
attr_reader :username
|
170
|
+
|
171
|
+
# @return [Hash] SSH options, passed to `Net::SSH.start`
|
172
|
+
attr_reader :options
|
173
|
+
|
174
|
+
# @return [Logger] the logger to use
|
175
|
+
# @api private
|
176
|
+
attr_reader :logger
|
108
177
|
|
178
|
+
# Builds the Net::SSH session connection or returns the existing one if
|
179
|
+
# built.
|
180
|
+
#
|
181
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
182
|
+
# @api private
|
109
183
|
def session
|
110
184
|
@session ||= establish_connection
|
111
185
|
end
|
112
186
|
|
187
|
+
# Establish a connection session to the remote host.
|
188
|
+
#
|
189
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
190
|
+
# @api private
|
113
191
|
def establish_connection
|
114
192
|
rescue_exceptions = [
|
115
193
|
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
|
116
194
|
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
|
117
|
-
Net::SSH::Disconnect
|
195
|
+
Net::SSH::Disconnect, Net::SSH::AuthenticationFailed
|
118
196
|
]
|
119
|
-
retries = 3
|
197
|
+
retries = options[:ssh_retries] || 3
|
120
198
|
|
121
199
|
begin
|
122
200
|
logger.debug("[SSH] opening connection to #{self}")
|
123
201
|
Net::SSH.start(hostname, username, options)
|
124
202
|
rescue *rescue_exceptions => e
|
125
|
-
|
203
|
+
retries -= 1
|
204
|
+
if retries > 0
|
126
205
|
logger.info("[SSH] connection failed, retrying (#{e.inspect})")
|
206
|
+
sleep options[:ssh_timeout] || 1
|
127
207
|
retry
|
128
208
|
else
|
129
209
|
logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
|
@@ -132,31 +212,42 @@ module Kitchen
|
|
132
212
|
end
|
133
213
|
end
|
134
214
|
|
215
|
+
# String representation of object, reporting its connection details and
|
216
|
+
# configuration.
|
217
|
+
#
|
218
|
+
# @api private
|
135
219
|
def to_s
|
136
220
|
"#{username}@#{hostname}:#{port}<#{options.inspect}>"
|
137
221
|
end
|
138
222
|
|
223
|
+
# @return [Integer] SSH port (default: 22)
|
224
|
+
# @api private
|
139
225
|
def port
|
140
226
|
options.fetch(:port, 22)
|
141
227
|
end
|
142
228
|
|
229
|
+
# Execute a remote command and return the command's exit code.
|
230
|
+
#
|
231
|
+
# @param cmd [String] command string to execute
|
232
|
+
# @return [Integer] the exit code of the command
|
233
|
+
# @api private
|
143
234
|
def exec_with_exit(cmd)
|
144
235
|
exit_code = nil
|
145
236
|
session.open_channel do |channel|
|
146
237
|
|
147
238
|
channel.request_pty
|
148
239
|
|
149
|
-
channel.exec(cmd) do |
|
240
|
+
channel.exec(cmd) do |_ch, _success|
|
150
241
|
|
151
|
-
channel.on_data do |
|
242
|
+
channel.on_data do |_ch, data|
|
152
243
|
logger << data
|
153
244
|
end
|
154
245
|
|
155
|
-
channel.on_extended_data do |
|
246
|
+
channel.on_extended_data do |_ch, _type, data|
|
156
247
|
logger << data
|
157
248
|
end
|
158
249
|
|
159
|
-
channel.on_request("exit-status") do |
|
250
|
+
channel.on_request("exit-status") do |_ch, data|
|
160
251
|
exit_code = data.read_long
|
161
252
|
end
|
162
253
|
end
|
@@ -165,12 +256,16 @@ module Kitchen
|
|
165
256
|
exit_code
|
166
257
|
end
|
167
258
|
|
259
|
+
# Test a remote TCP socket (presumably SSH) for connectivity.
|
260
|
+
#
|
261
|
+
# @return [true,false] a truthy value if the socket is ready and false
|
262
|
+
# otherwise
|
263
|
+
# @api private
|
168
264
|
def test_ssh
|
169
265
|
socket = TCPSocket.new(hostname, port)
|
170
266
|
IO.select([socket], nil, nil, 5)
|
171
|
-
rescue
|
172
|
-
|
173
|
-
sleep 2
|
267
|
+
rescue *SOCKET_EXCEPTIONS
|
268
|
+
sleep options[:ssh_timeout] || 2
|
174
269
|
false
|
175
270
|
rescue Errno::EPERM, Errno::ETIMEDOUT
|
176
271
|
false
|
data/lib/kitchen/state_file.rb
CHANGED
@@ -18,18 +18,20 @@
|
|
18
18
|
|
19
19
|
if RUBY_VERSION <= "1.9.3"
|
20
20
|
# ensure that Psych and not Syck is used for Ruby 1.9.2
|
21
|
-
require
|
22
|
-
YAML::ENGINE.yamler =
|
21
|
+
require "yaml"
|
22
|
+
YAML::ENGINE.yamler = "psych"
|
23
23
|
end
|
24
|
-
require
|
24
|
+
require "safe_yaml/load"
|
25
25
|
|
26
26
|
module Kitchen
|
27
27
|
|
28
28
|
# Exception class for any exceptions raised when reading and parsing a state
|
29
29
|
# file from disk
|
30
|
-
class StateFileLoadError < StandardError
|
30
|
+
class StateFileLoadError < StandardError; end
|
31
31
|
|
32
32
|
# State persistence manager for instances between actions and invocations.
|
33
|
+
#
|
34
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
33
35
|
class StateFile
|
34
36
|
|
35
37
|
# Constructs an new instance taking the kitchen root and instance name.
|
@@ -49,7 +51,7 @@ module Kitchen
|
|
49
51
|
# @raise [StateFileLoadError] if there is a problem loading the state file
|
50
52
|
# from disk and loading it into a Hash
|
51
53
|
def read
|
52
|
-
if File.
|
54
|
+
if File.exist?(file_name) && !File.zero?(file_name)
|
53
55
|
Util.symbolized_hash(deserialize_string(read_file))
|
54
56
|
else
|
55
57
|
Hash.new
|
@@ -63,13 +65,13 @@ module Kitchen
|
|
63
65
|
dir = File.dirname(file_name)
|
64
66
|
serialized_string = serialize_hash(Util.stringified_hash(state))
|
65
67
|
|
66
|
-
FileUtils.mkdir_p(dir) if !
|
68
|
+
FileUtils.mkdir_p(dir) if !File.directory?(dir)
|
67
69
|
File.open(file_name, "wb") { |f| f.write(serialized_string) }
|
68
70
|
end
|
69
71
|
|
70
72
|
# Destroys a state file on disk if it exists.
|
71
73
|
def destroy
|
72
|
-
FileUtils.rm_f(file_name) if File.
|
74
|
+
FileUtils.rm_f(file_name) if File.exist?(file_name)
|
73
75
|
end
|
74
76
|
|
75
77
|
# Returns a Hash of configuration and other useful diagnostic information.
|
@@ -84,18 +86,33 @@ module Kitchen
|
|
84
86
|
|
85
87
|
private
|
86
88
|
|
89
|
+
# @return [String] absolute path to the yaml state file on disk
|
90
|
+
# @api private
|
87
91
|
attr_reader :file_name
|
88
92
|
|
93
|
+
# @return [String] a string representation of the yaml state file
|
94
|
+
# @api private
|
89
95
|
def read_file
|
90
96
|
IO.read(file_name)
|
91
97
|
end
|
92
98
|
|
99
|
+
# Parses a YAML string and returns a Hash.
|
100
|
+
#
|
101
|
+
# @param string [String] a yaml document as a string
|
102
|
+
# @return [Hash] a hash
|
103
|
+
# @raise [StateFileLoadError] if the string document cannot be parsed
|
104
|
+
# @api private
|
93
105
|
def deserialize_string(string)
|
94
106
|
SafeYAML.load(string)
|
95
107
|
rescue SyntaxError, Psych::SyntaxError => ex
|
96
108
|
raise StateFileLoadError, "Error parsing #{file_name} (#{ex.message})"
|
97
109
|
end
|
98
110
|
|
111
|
+
# Serializes a Hash into a YAML string.
|
112
|
+
#
|
113
|
+
# @param hash [Hash] a hash
|
114
|
+
# @return [String] a yaml document as a string
|
115
|
+
# @api private
|
99
116
|
def serialize_hash(hash)
|
100
117
|
::YAML.dump(hash)
|
101
118
|
end
|
data/lib/kitchen/thor_tasks.rb
CHANGED
@@ -16,9 +16,9 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require
|
19
|
+
require "thor"
|
20
20
|
|
21
|
-
require
|
21
|
+
require "kitchen"
|
22
22
|
|
23
23
|
module Kitchen
|
24
24
|
|
@@ -42,19 +42,24 @@ module Kitchen
|
|
42
42
|
|
43
43
|
private
|
44
44
|
|
45
|
+
# @return [Config] a Kitchen::Config
|
45
46
|
attr_reader :config
|
46
47
|
|
48
|
+
# Generates a test Thor task for each instance and one to test all
|
49
|
+
# instances in serial.
|
50
|
+
#
|
51
|
+
# @api private
|
47
52
|
def define
|
48
53
|
config.instances.each do |instance|
|
49
54
|
self.class.desc instance.name, "Run #{instance.name} test instance"
|
50
|
-
self.class.send(:define_method, instance.name.gsub(/-/,
|
55
|
+
self.class.send(:define_method, instance.name.gsub(/-/, "_")) do
|
51
56
|
instance.test(:always)
|
52
57
|
end
|
53
58
|
end
|
54
59
|
|
55
60
|
self.class.desc "all", "Run all test instances"
|
56
61
|
self.class.send(:define_method, :all) do
|
57
|
-
config.instances.each { |i| invoke i.name.gsub(/-/,
|
62
|
+
config.instances.each { |i| invoke i.name.gsub(/-/, "_") }
|
58
63
|
end
|
59
64
|
end
|
60
65
|
end
|
data/lib/kitchen/util.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
4
|
#
|
5
|
-
# Copyright (C) 2012, Fletcher Nichol
|
5
|
+
# Copyright (C) 2012, 2013, 2014, Fletcher Nichol
|
6
6
|
#
|
7
7
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
8
|
# you may not use this file except in compliance with the License.
|
@@ -63,9 +63,9 @@ module Kitchen
|
|
63
63
|
# @return [Object] a converted hash with all keys as symbols
|
64
64
|
def self.symbolized_hash(obj)
|
65
65
|
if obj.is_a?(Hash)
|
66
|
-
obj.inject({}) { |h, (k, v)| h[k.to_sym] = symbolized_hash(v)
|
66
|
+
obj.inject({}) { |h, (k, v)| h[k.to_sym] = symbolized_hash(v); h }
|
67
67
|
elsif obj.is_a?(Array)
|
68
|
-
obj.inject([]) { |a,
|
68
|
+
obj.inject([]) { |a, e| a << symbolized_hash(e); a }
|
69
69
|
else
|
70
70
|
obj
|
71
71
|
end
|
@@ -80,9 +80,9 @@ module Kitchen
|
|
80
80
|
# @return [Object] a converted hash with all keys as strings
|
81
81
|
def self.stringified_hash(obj)
|
82
82
|
if obj.is_a?(Hash)
|
83
|
-
obj.inject({}) { |h, (k, v)| h[k.to_s] = stringified_hash(v)
|
83
|
+
obj.inject({}) { |h, (k, v)| h[k.to_s] = stringified_hash(v); h }
|
84
84
|
elsif obj.is_a?(Array)
|
85
|
-
obj.inject([]) { |a,
|
85
|
+
obj.inject([]) { |a, e| a << stringified_hash(e); a }
|
86
86
|
else
|
87
87
|
obj
|
88
88
|
end
|
@@ -96,7 +96,41 @@ module Kitchen
|
|
96
96
|
total = 0 if total.nil?
|
97
97
|
minutes = (total / 60).to_i
|
98
98
|
seconds = (total - (minutes * 60))
|
99
|
-
"(%dm%.2fs)"
|
99
|
+
format("(%dm%.2fs)", minutes, seconds)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Generates a command (or series of commands) wrapped so that it can be
|
103
|
+
# invoked on a remote instance or locally.
|
104
|
+
#
|
105
|
+
# This method uses the Bourne shell (/bin/sh) to maximize the chance of
|
106
|
+
# cross platform portability on Unixlike systems.
|
107
|
+
#
|
108
|
+
# @param [String] the command
|
109
|
+
# @return [String] a wrapped command string
|
110
|
+
def self.wrap_command(cmd)
|
111
|
+
cmd = "false" if cmd.nil?
|
112
|
+
cmd = "true" if cmd.to_s.empty?
|
113
|
+
cmd = cmd.sub(/\n\Z/, "") if cmd =~ /\n\Z/
|
114
|
+
|
115
|
+
"sh -c '\n#{cmd}\n'"
|
116
|
+
end
|
117
|
+
|
118
|
+
# Modifes the given string to strip leading whitespace on each line, the
|
119
|
+
# amount which is calculated by using the first line of text.
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
#
|
123
|
+
# string = <<-STRING
|
124
|
+
# a
|
125
|
+
# b
|
126
|
+
# c
|
127
|
+
# STRING
|
128
|
+
# Util.outdent!(string) # => "a\n b\nc\n"
|
129
|
+
#
|
130
|
+
# @param string [String] the string that will be modified
|
131
|
+
# @return [String] the modified string
|
132
|
+
def self.outdent!(string)
|
133
|
+
string.gsub!(/^ {#{string.index(/[^ ]/)}}/, "")
|
100
134
|
end
|
101
135
|
|
102
136
|
# Returns a set of Bourne Shell (AKA /bin/sh) compatible helper
|
@@ -105,118 +139,9 @@ module Kitchen
|
|
105
139
|
#
|
106
140
|
# @return [String] a string representation of useful helper functions
|
107
141
|
def self.shell_helpers
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
exists() {
|
112
|
-
if command -v $1 >/dev/null 2>&1
|
113
|
-
then
|
114
|
-
return 0
|
115
|
-
else
|
116
|
-
return 1
|
117
|
-
fi
|
118
|
-
}
|
119
|
-
|
120
|
-
# do_wget URL FILENAME
|
121
|
-
do_wget() {
|
122
|
-
echo "trying wget..."
|
123
|
-
wget -O "$2" "$1" 2>/tmp/stderr
|
124
|
-
# check for bad return status
|
125
|
-
test $? -ne 0 && return 1
|
126
|
-
# check for 404 or empty file
|
127
|
-
grep "ERROR 404" /tmp/stderr 2>&1 >/dev/null
|
128
|
-
if test $? -eq 0 || test ! -s "$2"; then
|
129
|
-
return 1
|
130
|
-
fi
|
131
|
-
return 0
|
132
|
-
}
|
133
|
-
|
134
|
-
# do_curl URL FILENAME
|
135
|
-
do_curl() {
|
136
|
-
echo "trying curl..."
|
137
|
-
curl -L "$1" > "$2"
|
138
|
-
# check for bad return status
|
139
|
-
[ $? -ne 0 ] && return 1
|
140
|
-
# check for bad output or empty file
|
141
|
-
grep "The specified key does not exist." "$2" 2>&1 >/dev/null
|
142
|
-
if test $? -eq 0 || test ! -s "$2"; then
|
143
|
-
return 1
|
144
|
-
fi
|
145
|
-
return 0
|
146
|
-
}
|
147
|
-
|
148
|
-
# do_fetch URL FILENAME
|
149
|
-
do_fetch() {
|
150
|
-
echo "trying fetch..."
|
151
|
-
fetch -o "$2" "$1" 2>/tmp/stderr
|
152
|
-
# check for bad return status
|
153
|
-
test $? -ne 0 && return 1
|
154
|
-
return 0
|
155
|
-
}
|
156
|
-
|
157
|
-
# do_perl URL FILENAME
|
158
|
-
do_perl() {
|
159
|
-
echo "trying perl..."
|
160
|
-
perl -e "use LWP::Simple; getprint($ARGV[0]);" "$1" > "$2"
|
161
|
-
# check for bad return status
|
162
|
-
test $? -ne 0 && return 1
|
163
|
-
# check for bad output or empty file
|
164
|
-
# grep "The specified key does not exist." "$2" 2>&1 >/dev/null
|
165
|
-
# if test $? -eq 0 || test ! -s "$2"; then
|
166
|
-
# unable_to_retrieve_package
|
167
|
-
# fi
|
168
|
-
return 0
|
169
|
-
}
|
170
|
-
|
171
|
-
# do_python URL FILENAME
|
172
|
-
do_python() {
|
173
|
-
echo "trying python..."
|
174
|
-
python -c "import sys,urllib2 ; sys.stdout.write(urllib2.urlopen(sys.argv[1]).read())" "$1" > "$2"
|
175
|
-
# check for bad return status
|
176
|
-
test $? -ne 0 && return 1
|
177
|
-
# check for bad output or empty file
|
178
|
-
#grep "The specified key does not exist." "$2" 2>&1 >/dev/null
|
179
|
-
#if test $? -eq 0 || test ! -s "$2"; then
|
180
|
-
# unable_to_retrieve_package
|
181
|
-
#fi
|
182
|
-
return 0
|
183
|
-
}
|
184
|
-
|
185
|
-
# do_download URL FILENAME
|
186
|
-
do_download() {
|
187
|
-
PATH=/opt/local/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
188
|
-
export PATH
|
189
|
-
|
190
|
-
echo "downloading $1"
|
191
|
-
echo " to file $2"
|
192
|
-
|
193
|
-
# we try all of these until we get success.
|
194
|
-
# perl, in particular may be present but LWP::Simple may not be installed
|
195
|
-
|
196
|
-
if exists wget; then
|
197
|
-
do_wget $1 $2 && return 0
|
198
|
-
fi
|
199
|
-
|
200
|
-
if exists curl; then
|
201
|
-
do_curl $1 $2 && return 0
|
202
|
-
fi
|
203
|
-
|
204
|
-
if exists fetch; then
|
205
|
-
do_fetch $1 $2 && return 0
|
206
|
-
fi
|
207
|
-
|
208
|
-
if exists perl; then
|
209
|
-
do_perl $1 $2 && return 0
|
210
|
-
fi
|
211
|
-
|
212
|
-
if exists python; then
|
213
|
-
do_python $1 $2 && return 0
|
214
|
-
fi
|
215
|
-
|
216
|
-
echo ">>>>>> wget, curl, fetch, perl or python not found on this instance."
|
217
|
-
return 16
|
218
|
-
}
|
219
|
-
HELPERS
|
142
|
+
IO.read(File.join(
|
143
|
+
File.dirname(__FILE__), %w[.. .. support download_helpers.sh]
|
144
|
+
))
|
220
145
|
end
|
221
146
|
end
|
222
147
|
end
|