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.
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