test-kitchen 1.2.1 → 1.3.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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +1 -1
  3. data/.rubocop.yml +3 -0
  4. data/.travis.yml +20 -9
  5. data/CHANGELOG.md +219 -108
  6. data/Gemfile +10 -6
  7. data/Guardfile +38 -9
  8. data/README.md +11 -1
  9. data/Rakefile +21 -37
  10. data/bin/kitchen +4 -4
  11. data/features/kitchen_action_commands.feature +161 -0
  12. data/features/kitchen_console_command.feature +34 -0
  13. data/features/kitchen_diagnose_command.feature +64 -0
  14. data/features/kitchen_init_command.feature +29 -17
  15. data/features/kitchen_list_command.feature +2 -2
  16. data/features/kitchen_login_command.feature +56 -0
  17. data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
  18. data/features/kitchen_test_command.feature +88 -0
  19. data/features/step_definitions/gem_steps.rb +8 -6
  20. data/features/step_definitions/git_steps.rb +4 -2
  21. data/features/step_definitions/output_steps.rb +5 -0
  22. data/features/support/env.rb +12 -9
  23. data/lib/kitchen.rb +60 -38
  24. data/lib/kitchen/base64_stream.rb +55 -0
  25. data/lib/kitchen/busser.rb +124 -58
  26. data/lib/kitchen/cli.rb +121 -38
  27. data/lib/kitchen/collection.rb +3 -3
  28. data/lib/kitchen/color.rb +4 -4
  29. data/lib/kitchen/command.rb +78 -11
  30. data/lib/kitchen/command/action.rb +3 -2
  31. data/lib/kitchen/command/console.rb +12 -5
  32. data/lib/kitchen/command/diagnose.rb +17 -3
  33. data/lib/kitchen/command/driver_discover.rb +26 -7
  34. data/lib/kitchen/command/exec.rb +41 -0
  35. data/lib/kitchen/command/list.rb +44 -14
  36. data/lib/kitchen/command/login.rb +2 -1
  37. data/lib/kitchen/command/sink.rb +2 -1
  38. data/lib/kitchen/command/test.rb +5 -4
  39. data/lib/kitchen/config.rb +146 -14
  40. data/lib/kitchen/configurable.rb +314 -0
  41. data/lib/kitchen/data_munger.rb +522 -18
  42. data/lib/kitchen/diagnostic.rb +43 -4
  43. data/lib/kitchen/driver.rb +4 -4
  44. data/lib/kitchen/driver/base.rb +80 -115
  45. data/lib/kitchen/driver/dummy.rb +34 -6
  46. data/lib/kitchen/driver/proxy.rb +14 -3
  47. data/lib/kitchen/driver/ssh_base.rb +61 -7
  48. data/lib/kitchen/errors.rb +109 -9
  49. data/lib/kitchen/generator/driver_create.rb +39 -5
  50. data/lib/kitchen/generator/init.rb +130 -45
  51. data/lib/kitchen/instance.rb +162 -28
  52. data/lib/kitchen/lazy_hash.rb +79 -7
  53. data/lib/kitchen/loader/yaml.rb +159 -27
  54. data/lib/kitchen/logger.rb +267 -21
  55. data/lib/kitchen/logging.rb +30 -3
  56. data/lib/kitchen/login_command.rb +11 -2
  57. data/lib/kitchen/metadata_chopper.rb +2 -2
  58. data/lib/kitchen/provisioner.rb +4 -4
  59. data/lib/kitchen/provisioner/base.rb +107 -103
  60. data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
  61. data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
  62. data/lib/kitchen/provisioner/chef_base.rb +206 -167
  63. data/lib/kitchen/provisioner/chef_solo.rb +25 -7
  64. data/lib/kitchen/provisioner/chef_zero.rb +105 -29
  65. data/lib/kitchen/provisioner/dummy.rb +1 -1
  66. data/lib/kitchen/provisioner/shell.rb +21 -6
  67. data/lib/kitchen/rake_tasks.rb +8 -3
  68. data/lib/kitchen/shell_out.rb +15 -18
  69. data/lib/kitchen/ssh.rb +122 -27
  70. data/lib/kitchen/state_file.rb +24 -7
  71. data/lib/kitchen/thor_tasks.rb +9 -4
  72. data/lib/kitchen/util.rb +43 -118
  73. data/lib/kitchen/version.rb +1 -1
  74. data/lib/vendor/hash_recursive_merge.rb +10 -2
  75. data/spec/kitchen/base64_stream_spec.rb +77 -0
  76. data/spec/kitchen/busser_spec.rb +490 -0
  77. data/spec/kitchen/collection_spec.rb +10 -10
  78. data/spec/kitchen/color_spec.rb +2 -2
  79. data/spec/kitchen/config_spec.rb +234 -62
  80. data/spec/kitchen/configurable_spec.rb +490 -0
  81. data/spec/kitchen/data_munger_spec.rb +1070 -862
  82. data/spec/kitchen/diagnostic_spec.rb +79 -0
  83. data/spec/kitchen/driver/base_spec.rb +80 -85
  84. data/spec/kitchen/driver/dummy_spec.rb +43 -14
  85. data/spec/kitchen/driver/proxy_spec.rb +134 -0
  86. data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
  87. data/spec/kitchen/driver_spec.rb +15 -15
  88. data/spec/kitchen/errors_spec.rb +309 -0
  89. data/spec/kitchen/instance_spec.rb +143 -46
  90. data/spec/kitchen/lazy_hash_spec.rb +36 -9
  91. data/spec/kitchen/loader/yaml_spec.rb +237 -226
  92. data/spec/kitchen/logger_spec.rb +419 -0
  93. data/spec/kitchen/logging_spec.rb +59 -0
  94. data/spec/kitchen/login_command_spec.rb +49 -0
  95. data/spec/kitchen/metadata_chopper_spec.rb +82 -0
  96. data/spec/kitchen/platform_spec.rb +4 -4
  97. data/spec/kitchen/provisioner/base_spec.rb +65 -125
  98. data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
  99. data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
  100. data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
  101. data/spec/kitchen/provisioner/shell_spec.rb +269 -0
  102. data/spec/kitchen/provisioner_spec.rb +6 -6
  103. data/spec/kitchen/shell_out_spec.rb +143 -0
  104. data/spec/kitchen/ssh_spec.rb +683 -0
  105. data/spec/kitchen/state_file_spec.rb +28 -21
  106. data/spec/kitchen/suite_spec.rb +7 -7
  107. data/spec/kitchen/util_spec.rb +68 -10
  108. data/spec/kitchen_spec.rb +107 -0
  109. data/spec/spec_helper.rb +18 -13
  110. data/support/chef-client-zero.rb +10 -9
  111. data/support/chef_helpers.sh +16 -0
  112. data/support/download_helpers.sh +109 -0
  113. data/test-kitchen.gemspec +42 -33
  114. metadata +107 -33
@@ -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 'logger'
20
- require 'net/ssh'
21
- require 'net/scp'
22
- require 'socket'
19
+ require "logger"
20
+ require "net/ssh"
21
+ require "net/scp"
22
+ require "socket"
23
23
 
24
- require 'kitchen/errors'
25
- require 'kitchen/login_command'
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 ; end
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 { |ch, name, sent, total|
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{ -o UserKnownHostsFile=/dev/null }
94
- args += %W{ -o StrictHostKeyChecking=no }
95
- args += %W{ -o IdentitiesOnly=yes } if options[:keys]
96
- args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
97
- args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} } if options.key? :forward_agent
98
- Array(options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key}} }
99
- args += %W{ -p #{port}}
100
- args += %W{ #{username}@#{hostname}}
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
- attr_reader :hostname, :username, :options, :logger
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
- if (retries -= 1) > 0
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 |ch, success|
240
+ channel.exec(cmd) do |_ch, _success|
150
241
 
151
- channel.on_data do |ch, data|
242
+ channel.on_data do |_ch, data|
152
243
  logger << data
153
244
  end
154
245
 
155
- channel.on_extended_data do |ch, type, data|
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 |ch, data|
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 SocketError, Errno::ECONNREFUSED,
172
- Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
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
@@ -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 'yaml'
22
- YAML::ENGINE.yamler = 'psych'
21
+ require "yaml"
22
+ YAML::ENGINE.yamler = "psych"
23
23
  end
24
- require 'safe_yaml/load'
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 ; end
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.exists?(file_name)
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 ! File.directory?(dir)
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.exists?(file_name)
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
@@ -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 'thor'
19
+ require "thor"
20
20
 
21
- require 'kitchen'
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(/-/, '_')) do
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
@@ -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) ; h }
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, v| a << symbolized_hash(v) ; 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) ; h }
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, v| a << stringified_hash(v) ; 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)" % [minutes, seconds]
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
- # use Bourne (/bin/sh) as Bash does not exist on all Unix flavors
109
- <<-HELPERS
110
- # Check whether a command exists - returns 0 if it does, 1 if it does not
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