kitchen-ec2 0.10.0 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ require "kitchen/driver/aws/standard_platform"
2
+
3
+ module Kitchen
4
+ module Driver
5
+ class Aws
6
+ class StandardPlatform
7
+ # https://docs.fedoraproject.org/en-US/Fedora_Draft_Documentation/0.1/html/Cloud_Guide/ch02.html#id697643
8
+ class Fedora < StandardPlatform
9
+ StandardPlatform.platforms["fedora"] = self
10
+
11
+ def username
12
+ "fedora"
13
+ end
14
+
15
+ def image_search
16
+ search = {
17
+ "owner-id" => "125523088429",
18
+ "name" => version ? "Fedora-Cloud-Base-#{version}-*" : "Fedora-Cloud-Base-*"
19
+ }
20
+ search["architecture"] = architecture if architecture
21
+ search
22
+ end
23
+
24
+ def self.from_image(driver, image)
25
+ if image.name =~ /fedora/i
26
+ image.name =~ /\b(\d+(\.\d+)?)\b/i
27
+ new(driver, "fedora", (Regexp.last_match || [])[1], image.architecture)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ require "kitchen/driver/aws/standard_platform"
2
+
3
+ module Kitchen
4
+ module Driver
5
+ class Aws
6
+ class StandardPlatform
7
+ # http://www.daemonology.net/freebsd-on-ec2/
8
+ class Freebsd < StandardPlatform
9
+ StandardPlatform.platforms["freebsd"] = self
10
+
11
+ def username
12
+ (version && version.to_f < 9.1) ? "root" : "ec2-user"
13
+ end
14
+
15
+ def sudo_command
16
+ end
17
+
18
+ def image_search
19
+ search = {
20
+ "owner-id" => "118940168514",
21
+ "name" => ["FreeBSD #{version}*-RELEASE*", "FreeBSD/EC2 #{version}*-RELEASE*"]
22
+ }
23
+ search["architecture"] = architecture if architecture
24
+ search
25
+ end
26
+
27
+ def self.from_image(driver, image)
28
+ if image.name =~ /freebsd/i
29
+ image.name =~ /\b(\d+(\.\d+)?)\b/i
30
+ new(driver, "freebsd", (Regexp.last_match || [])[1], image.architecture)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ require "kitchen/driver/aws/standard_platform"
2
+
3
+ module Kitchen
4
+ module Driver
5
+ class Aws
6
+ class StandardPlatform
7
+ # https://aws.amazon.com/blogs/aws/now-available-red-hat-enterprise-linux-64-amis/
8
+ class El < StandardPlatform
9
+ StandardPlatform.platforms["rhel"] = self
10
+ StandardPlatform.platforms["el"] = self
11
+
12
+ def initialize(driver, _name, version, architecture)
13
+ # rhel = el
14
+ super(driver, "rhel", version, architecture)
15
+ end
16
+
17
+ def username
18
+ (version && version.to_f < 6.4) ? "root" : "ec2-user"
19
+ end
20
+
21
+ def image_search
22
+ search = {
23
+ "owner-id" => "309956199498",
24
+ "name" => "RHEL-#{version}*"
25
+ }
26
+ search["architecture"] = architecture if architecture
27
+ search
28
+ end
29
+
30
+ def self.from_image(driver, image)
31
+ if image.name =~ /rhel/i
32
+ image.name =~ /\b(\d+(\.\d+)?)/i
33
+ new(driver, "rhel", (Regexp.last_match || [])[1], image.architecture)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ require "kitchen/driver/aws/standard_platform"
2
+
3
+ module Kitchen
4
+ module Driver
5
+ class Aws
6
+ class StandardPlatform
7
+ # https://help.ubuntu.com/community/EC2StartersGuide#Official_Ubuntu_Cloud_Guest_Amazon_Machine_Images_.28AMIs.29
8
+ class Ubuntu < StandardPlatform
9
+ StandardPlatform.platforms["ubuntu"] = self
10
+
11
+ def username
12
+ "ubuntu"
13
+ end
14
+
15
+ def image_search
16
+ search = {
17
+ "owner-id" => "099720109477",
18
+ "name" => "ubuntu/images/*/ubuntu-*-#{version}*"
19
+ }
20
+ search["architecture"] = architecture if architecture
21
+ search
22
+ end
23
+
24
+ def self.from_image(driver, image)
25
+ if image.name =~ /ubuntu/i
26
+ image.name =~ /\b(\d+(\.\d+)?)\b/i
27
+ new(driver, "ubuntu", (Regexp.last_match || [])[1], image.architecture)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,138 @@
1
+ require "kitchen/driver/aws/standard_platform"
2
+
3
+ module Kitchen
4
+ module Driver
5
+ class Aws
6
+ class StandardPlatform
7
+ # http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/finding-an-ami.html
8
+ class Windows < StandardPlatform
9
+ StandardPlatform.platforms["windows"] = self
10
+
11
+ def username
12
+ "administrator"
13
+ end
14
+
15
+ # Figure out the right set of names to search for:
16
+ #
17
+ # "windows" -> [nil, nil, nil]
18
+ # Windows_Server-*-R*_RTM-, Windows_Server-*-R*_SP*-,
19
+ # Windows_Server-*-RTM-, Windows_Server-*-SP*-
20
+ # "windows-2012" -> [2012, 0, nil]
21
+ # Windows_Server-2012-RTM-, Windows_Server-2012-SP*-
22
+ # "windows-2012r2" -> [2012, 2, nil]
23
+ # Windows_Server-2012-R2_RTM-, Windows_Server-2012-R2_SP*-
24
+ # "windows-2012sp1" -> [2012, 0, 1]
25
+ # Windows_Server-2012-SP1-
26
+ # "windows-2012rtm" -> [2012, 0, 0]
27
+ # Windows_Server-2012-RTM-
28
+ # "windows-2012r2sp1" -> [2012, 2, 1]
29
+ # Windows_Server-2012-R2_SP1-
30
+ # "windows-2012r2rtm" -> [2012, 2, 0]
31
+ # Windows_Server-2012-R2_RTM-
32
+ def image_search
33
+ search = {
34
+ "owner-alias" => "amazon",
35
+ "name" => windows_name_filter
36
+ }
37
+ search["architecture"] = architecture if architecture
38
+ search
39
+ end
40
+
41
+ def sort_by_version(images)
42
+ # 2008r2rtm -> [ img1, img2, img3 ]
43
+ # 2012r2sp1 -> [ img4, img5 ]
44
+ # ...
45
+ images.group_by { |image| self.class.from_image(driver, image).windows_version_parts }.
46
+ sort_by { |version, _platform_images| version }.
47
+ reverse.map { |_version, platform_images| platform_images }.flatten(1)
48
+ end
49
+
50
+ def self.from_image(driver, image)
51
+ if image.name =~ /Windows/i
52
+ # 2008 R2 SP2
53
+ if image.name =~ /(\b\d+)\W*(r\d+)?/i
54
+ major, revision = (Regexp.last_match || [])[1], (Regexp.last_match || [])[2]
55
+ if image.name =~ /(sp\d+|rtm)/i
56
+ service_pack = (Regexp.last_match || [])[1]
57
+ end
58
+ revision = revision.downcase if revision
59
+ service_pack ||= "rtm"
60
+ service_pack = service_pack.downcase
61
+ version = "#{major}#{revision}#{service_pack}"
62
+ end
63
+
64
+ new(driver, "windows", version, image.architecture)
65
+ end
66
+ end
67
+
68
+ protected
69
+
70
+ # Turn windows version into [ major, revision, service_pack ]
71
+ #
72
+ # nil -> [ nil, nil, nil ]
73
+ # 2012 -> [ 2012, 0, nil ]
74
+ # 2012r2 -> [ 2012, 2, nil ]
75
+ # 2012r2sp4 -> [ 2012, 2, 4 ]
76
+ # 2012sp4 -> [ 2012, 0, 4 ]
77
+ # 2012rtm -> [ 2012, 0, 0 ]
78
+ def windows_version_parts
79
+ version = self.version
80
+ if version
81
+ # windows-server-* -> windows-*
82
+ if version.split("-", 2)[0] == "server"
83
+ version = version.split("-", 2)[1]
84
+ end
85
+
86
+ if version =~ /^(\d+)(r\d+)?(sp\d+|rtm)?$/i
87
+ major, revision, service_pack = Regexp.last_match[1..3]
88
+ end
89
+ end
90
+
91
+ if major
92
+ # Get major as an integer (2008 -> 2008, 7 -> 7)
93
+ major = major.to_i
94
+
95
+ # Get revision as an integer (no revision -> 0, R1 -> 1).
96
+ revision = revision ? revision[1..-1].to_i : 0
97
+
98
+ # Turn service_pack into an integer. rtm = 0, spN = N.
99
+ if service_pack
100
+ service_pack = (service_pack.downcase == "rtm") ? 0 : service_pack[2..-1].to_i
101
+ end
102
+ end
103
+
104
+ [major, revision, service_pack]
105
+ end
106
+
107
+ private
108
+
109
+ def windows_name_filter
110
+ major, revision, service_pack = windows_version_parts
111
+
112
+ case revision
113
+ when nil
114
+ revision_strings = ["", "R*_"]
115
+ when 0
116
+ revision_strings = [""]
117
+ else
118
+ revision_strings = ["R#{revision}_"]
119
+ end
120
+
121
+ case service_pack
122
+ when nil
123
+ revision_strings = revision_strings.flat_map { |r| ["#{r}RTM", "#{r}SP*"] }
124
+ when 0
125
+ revision_strings = revision_strings.map { |r| "#{r}RTM" }
126
+ else
127
+ revision_strings = revision_strings.map { |r| "#{r}SP#{service_pack}" }
128
+ end
129
+
130
+ revision_strings.map do |r|
131
+ "Windows_Server-#{major || "*"}-#{r}-English-*-Base-*"
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -22,7 +22,16 @@ require "kitchen"
22
22
  require_relative "ec2_version"
23
23
  require_relative "aws/client"
24
24
  require_relative "aws/instance_generator"
25
+ require_relative "aws/standard_platform"
26
+ require_relative "aws/standard_platform/centos"
27
+ require_relative "aws/standard_platform/debian"
28
+ require_relative "aws/standard_platform/rhel"
29
+ require_relative "aws/standard_platform/fedora"
30
+ require_relative "aws/standard_platform/freebsd"
31
+ require_relative "aws/standard_platform/ubuntu"
32
+ require_relative "aws/standard_platform/windows"
25
33
  require "aws-sdk-core/waiters/errors"
34
+ require "retryable"
26
35
 
27
36
  module Kitchen
28
37
 
@@ -40,8 +49,9 @@ module Kitchen
40
49
  default_config :region, ENV["AWS_REGION"] || "us-east-1"
41
50
  default_config :shared_credentials_profile, nil
42
51
  default_config :availability_zone, nil
43
- default_config :flavor_id, nil
44
- default_config :instance_type, nil
52
+ default_config :instance_type do |driver|
53
+ driver.default_instance_type
54
+ end
45
55
  default_config :ebs_optimized, false
46
56
  default_config :security_group_ids, nil
47
57
  default_config :tags, "created-by" => "test-kitchen"
@@ -62,120 +72,87 @@ module Kitchen
62
72
  default_config :image_id do |driver|
63
73
  driver.default_ami
64
74
  end
65
- default_config :username, nil
75
+ default_config :image_search, nil
76
+ default_config :username, nil
66
77
  default_config :associate_public_ip, nil
67
78
  default_config :interface, nil
68
79
  default_config :http_proxy, ENV["HTTPS_PROXY"] || ENV["HTTP_PROXY"]
80
+ default_config :retry_limit, 3
69
81
 
70
82
  required_config :aws_ssh_key_id
71
- required_config :image_id
72
83
 
73
84
  def self.validation_warn(driver, old_key, new_key)
74
85
  driver.warn "WARN: The driver[#{driver.class.name}] config key `#{old_key}` " \
75
86
  "is deprecated, please use `#{new_key}`"
76
87
  end
77
88
 
78
- # TODO: remove these in the next major version of TK
89
+ def self.validation_error(driver, old_key, new_key)
90
+ raise "ERROR: The driver[#{driver.class.name}] config key `#{old_key}` " \
91
+ "has been removed, please use `#{new_key}`"
92
+ end
93
+
94
+ # TODO: remove these in 1.1
79
95
  deprecated_configs = [:ebs_volume_size, :ebs_delete_on_termination, :ebs_device_name]
80
96
  deprecated_configs.each do |d|
81
97
  validations[d] = lambda do |attr, val, driver|
82
98
  unless val.nil?
83
- validation_warn(driver, attr, "block_device_mappings")
99
+ validation_error(driver, attr, "block_device_mappings")
84
100
  end
85
101
  end
86
102
  end
87
103
  validations[:ssh_key] = lambda do |attr, val, driver|
88
104
  unless val.nil?
89
- validation_warn(driver, attr, "transport.ssh_key")
105
+ validation_error(driver, attr, "transport.ssh_key")
90
106
  end
91
107
  end
92
108
  validations[:ssh_timeout] = lambda do |attr, val, driver|
93
109
  unless val.nil?
94
- validation_warn(driver, attr, "transport.connection_timeout")
110
+ validation_error(driver, attr, "transport.connection_timeout")
95
111
  end
96
112
  end
97
113
  validations[:ssh_retries] = lambda do |attr, val, driver|
98
114
  unless val.nil?
99
- validation_warn(driver, attr, "transport.connection_retries")
115
+ validation_error(driver, attr, "transport.connection_retries")
100
116
  end
101
117
  end
102
118
  validations[:username] = lambda do |attr, val, driver|
103
119
  unless val.nil?
104
- validation_warn(driver, attr, "transport.username")
120
+ validation_error(driver, attr, "transport.username")
105
121
  end
106
122
  end
107
123
  validations[:flavor_id] = lambda do |attr, val, driver|
108
124
  unless val.nil?
109
- validation_warn(driver, attr, "instance_type")
110
- end
111
- end
112
-
113
- default_config :block_device_mappings, nil
114
- validations[:block_device_mappings] = lambda do |_attr, val, _driver|
115
- unless val.nil?
116
- val.each do |bdm|
117
- unless bdm.keys.include?(:ebs_volume_size) &&
118
- bdm.keys.include?(:ebs_delete_on_termination) &&
119
- bdm.keys.include?(:ebs_device_name)
120
- raise "Every :block_device_mapping must include the keys :ebs_volume_size, " \
121
- ":ebs_delete_on_termination and :ebs_device_name"
122
- end
123
- end
125
+ validation_error(driver, attr, "instance_type")
124
126
  end
125
127
  end
126
128
 
127
129
  # The access key/secret are now using the priority list AWS uses
128
130
  # Providing these inside the .kitchen.yml is no longer recommended
129
- validations[:aws_access_key_id] = lambda do |attr, val, driver|
131
+ validations[:aws_access_key_id] = lambda do |attr, val, _driver|
130
132
  unless val.nil?
131
- driver.warn "WARN: #{attr} has been deprecated, please use " \
133
+ raise "#{attr} is no longer valid, please use " \
132
134
  "ENV['AWS_ACCESS_KEY_ID'] or ~/.aws/credentials. See " \
133
135
  "the README for more details"
134
136
  end
135
137
  end
136
- validations[:aws_secret_access_key] = lambda do |attr, val, driver|
138
+ validations[:aws_secret_access_key] = lambda do |attr, val, _driver|
137
139
  unless val.nil?
138
- driver.warn "WARN: #{attr} has been deprecated, please use " \
140
+ raise "#{attr} is no longer valid, please use " \
139
141
  "ENV['AWS_SECRET_ACCESS_KEY'] or ~/.aws/credentials. See " \
140
142
  "the README for more details"
141
143
  end
142
144
  end
143
- validations[:aws_session_token] = lambda do |attr, val, driver|
145
+ validations[:aws_session_token] = lambda do |attr, val, _driver|
144
146
  unless val.nil?
145
- driver.warn "WARN: #{attr} has been deprecated, please use " \
147
+ raise "#{attr} is no longer valid, please use " \
146
148
  "ENV['AWS_SESSION_TOKEN'] or ~/.aws/credentials. See " \
147
149
  "the README for more details"
148
150
  end
149
151
  end
150
152
 
151
- # A lifecycle method that should be invoked when the object is about
152
- # ready to be used. A reference to an Instance is required as
153
- # configuration dependant data may be access through an Instance. This
154
- # also acts as a hook point where the object may wish to perform other
155
- # last minute checks, validations, or configuration expansions.
156
- #
157
- # @param instance [Instance] an associated instance
158
- # @return [self] itself, for use in chaining
159
- # @raise [ClientError] if instance parameter is nil
160
- def finalize_config!(instance)
161
- super
162
-
163
- if config[:availability_zone].nil?
164
- config[:availability_zone] = config[:region] + "b"
165
- elsif config[:availability_zone] =~ /^[a-z]$/
166
- config[:availability_zone] = config[:region] + config[:availability_zone]
167
- end
168
- # TODO: when we get rid of flavor_id, move this to a default
169
- if config[:instance_type].nil?
170
- config[:instance_type] = config[:flavor_id] || "m1.small"
171
- end
172
-
173
- self
174
- end
175
-
176
153
  def create(state) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
177
- copy_deprecated_configs(state)
178
154
  return if state[:server_id]
155
+ update_username(state)
179
156
 
180
157
  info(Kitchen::Util.outdent!(<<-END))
181
158
  If you are not using an account that qualifies under the AWS
@@ -192,15 +169,28 @@ module Kitchen
192
169
  server = submit_server
193
170
  end
194
171
  info("Instance <#{server.id}> requested.")
195
- ec2.client.wait_until(
196
- :instance_exists,
197
- :instance_ids => [server.id]
198
- )
199
- tag_server(server)
172
+ server.wait_until_exists do |w|
173
+ w.before_attempt do |attempts|
174
+ info("Polling AWS for existence, attempt #{attempts}...")
175
+ end
176
+ end
200
177
 
201
- state[:server_id] = server.id
202
- info("EC2 instance <#{state[:server_id]}> created.")
203
- wait_until_ready(server, state)
178
+ # See https://github.com/aws/aws-sdk-ruby/issues/859
179
+ # Tagging can fail with a NotFound error even though we waited until the server exists
180
+ # Waiting can also fail, so we have to also retry on that. If it means we re-tag the
181
+ # instance, so be it.
182
+ Retryable.retryable(
183
+ :tries => 10,
184
+ :sleep => lambda { |n| [2**n, 30].min },
185
+ :on => ::Aws::EC2::Errors::InvalidInstanceIDNotFound
186
+ ) do |r, _|
187
+ info("Attempting to tag the instance, #{r} retries")
188
+ tag_server(server)
189
+
190
+ state[:server_id] = server.id
191
+ info("EC2 instance <#{state[:server_id]}> created.")
192
+ wait_until_ready(server, state)
193
+ end
204
194
 
205
195
  if windows_os? &&
206
196
  instance.transport[:username] =~ /administrator/i &&
@@ -236,9 +226,68 @@ module Kitchen
236
226
  state.delete(:hostname)
237
227
  end
238
228
 
229
+ def image
230
+ return @image if defined?(@image)
231
+
232
+ if config[:image_id]
233
+ @image = ec2.resource.image(config[:image_id])
234
+ show_chosen_image
235
+
236
+ else
237
+ raise "Neither image_id nor an image_search specified for instance #{instance.name}!" \
238
+ " Please specify one or the other."
239
+ end
240
+
241
+ @image
242
+ end
243
+
244
+ def default_instance_type
245
+ @instance_type ||= begin
246
+ # We default to the free tier (t2.micro for hvm, t1.micro for paravirtual)
247
+ if image && image.virtualization_type == "hvm"
248
+ info("instance_type not specified. Using free tier t2.micro instance ...")
249
+ "t2.micro"
250
+ else
251
+ info("instance_type not specified. Using free tier t1.micro instance since" \
252
+ " image is paravirtual (pick an hvm image to use the superior t2.micro!) ...")
253
+ "t1.micro"
254
+ end
255
+ end
256
+ end
257
+
258
+ # The actual platform is the platform detected from the image
259
+ def actual_platform
260
+ @actual_platform ||= Aws::StandardPlatform.from_image(self, image) if image
261
+ end
262
+
263
+ def desired_platform
264
+ @desired_platform ||= begin
265
+ platform = Aws::StandardPlatform.from_platform_string(self, instance.platform.name)
266
+ if platform
267
+ debug("platform name #{instance.platform.name} appears to be a standard platform." \
268
+ " Searching for #{platform} ...")
269
+ end
270
+ platform
271
+ end
272
+ end
273
+
239
274
  def default_ami
240
- region = amis["regions"][config[:region]]
241
- region && region[instance.platform.name]
275
+ @default_ami ||= begin
276
+ search_platform = desired_platform ||
277
+ Aws::StandardPlatform.from_platform_string(self, "ubuntu")
278
+ image_search = config[:image_search] || search_platform.image_search
279
+ search_platform.find_image(image_search)
280
+ end
281
+ end
282
+
283
+ def update_username(state)
284
+ # TODO: if the user explicitly specified the transport's default username,
285
+ # do NOT overwrite it!
286
+ if instance.transport[:username] == instance.transport.class.defaults[:username]
287
+ debug("No SSH username specified: using default username #{actual_platform.username} " \
288
+ " for image #{config[:image_id]}, which we detected as #{actual_platform}.")
289
+ state[:username] = actual_platform.username
290
+ end
242
291
  end
243
292
 
244
293
  def ec2
@@ -248,7 +297,8 @@ module Kitchen
248
297
  config[:aws_access_key_id],
249
298
  config[:aws_secret_access_key],
250
299
  config[:aws_session_token],
251
- config[:http_proxy]
300
+ config[:http_proxy],
301
+ config[:retry_limit]
252
302
  )
253
303
  end
254
304
 
@@ -256,36 +306,13 @@ module Kitchen
256
306
  @instance_generator ||= Aws::InstanceGenerator.new(config, ec2, instance.logger)
257
307
  end
258
308
 
259
- # This copies transport config from the current config object into the
260
- # state. This relies on logic in the transport that merges the transport
261
- # config with the current state object, so its a bad coupling. But we
262
- # can get rid of this when we get rid of these deprecated configs!
263
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
264
- def copy_deprecated_configs(state)
265
- if config[:ssh_timeout]
266
- state[:connection_timeout] = config[:ssh_timeout]
267
- end
268
- if config[:ssh_retries]
269
- state[:connection_retries] = config[:ssh_retries]
270
- end
271
- if config[:username]
272
- state[:username] = config[:username]
273
- elsif instance.transport[:username] == instance.transport.class.defaults[:username]
274
- # If the transport has the default username, copy it from amis.json
275
- # This duplicated old behavior but I hate amis.json
276
- ami_username = amis["usernames"][instance.platform.name]
277
- state[:username] = ami_username if ami_username
278
- end
279
- if config[:ssh_key]
280
- state[:ssh_key] = config[:ssh_key]
281
- end
282
- end
283
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
284
-
285
309
  # Fog AWS helper for creating the instance
286
310
  def submit_server
287
- debug("Creating EC2 Instance..")
288
311
  instance_data = instance_generator.ec2_instance_data
312
+ debug("Creating EC2 instance in region #{config[:region]} with properties:")
313
+ instance_data.each do |key, value|
314
+ debug("- #{key} = #{value.inspect}")
315
+ end
289
316
  instance_data[:min_count] = 1
290
317
  instance_data[:max_count] = 1
291
318
  ec2.create_instance(instance_data)
@@ -325,6 +352,8 @@ module Kitchen
325
352
  server.create_tags(:tags => tags)
326
353
  end
327
354
 
355
+ # Normally we could use `server.wait_until_running` but we actually need
356
+ # to check more than just the instance state
328
357
  def wait_until_ready(server, state)
329
358
  wait_with_destroy(server, state, "to become ready") do |aws_instance|
330
359
  hostname = hostname(aws_instance, config[:interface])
@@ -347,21 +376,8 @@ module Kitchen
347
376
  end
348
377
  end
349
378
 
350
- # rubocop:disable Lint/UnusedBlockArgument
351
- def fetch_windows_admin_password(server, state)
352
- wait_with_destroy(server, state, "to fetch windows admin password") do |aws_instance|
353
- enc = server.client.get_password_data(
354
- :instance_id => state[:server_id]
355
- ).password_data
356
- # Password data is blank until password is available
357
- !enc.nil? && !enc.empty?
358
- end
359
- pass = server.decrypt_windows_password(instance.transport[:ssh_key])
360
- state[:password] = pass
361
- info("Retrieved Windows password for instance <#{state[:server_id]}>.")
362
- end
363
- # rubocop:enable Lint/UnusedBlockArgument
364
-
379
+ # Poll a block, waiting for it to return true. If it does not succeed
380
+ # within the configured time we destroy the instance to save people money
365
381
  def wait_with_destroy(server, state, status_msg, &block)
366
382
  wait_log = proc do |attempts|
367
383
  c = attempts * config[:retryable_sleep]
@@ -383,13 +399,20 @@ module Kitchen
383
399
  end
384
400
  end
385
401
 
386
- def amis
387
- @amis ||= begin
388
- json_file = File.join(File.dirname(__FILE__),
389
- %w[.. .. .. data amis.json])
390
- JSON.load(IO.read(json_file))
402
+ # rubocop:disable Lint/UnusedBlockArgument
403
+ def fetch_windows_admin_password(server, state)
404
+ wait_with_destroy(server, state, "to fetch windows admin password") do |aws_instance|
405
+ enc = server.client.get_password_data(
406
+ :instance_id => state[:server_id]
407
+ ).password_data
408
+ # Password data is blank until password is available
409
+ !enc.nil? && !enc.empty?
391
410
  end
411
+ pass = server.decrypt_windows_password(File.expand_path(instance.transport[:ssh_key]))
412
+ state[:password] = pass
413
+ info("Retrieved Windows password for instance <#{state[:server_id]}>.")
392
414
  end
415
+ # rubocop:enable Lint/UnusedBlockArgument
393
416
 
394
417
  #
395
418
  # Ordered mapping from config name to Fog name. Ordered by preference
@@ -424,16 +447,24 @@ module Kitchen
424
447
  end
425
448
  end
426
449
 
450
+ #
451
+ # Returns the sudo command to use or empty string if sudo is not configured
452
+ #
453
+ def sudo_command
454
+ instance.provisioner[:sudo] ? instance.provisioner[:sudo_command].to_s : ""
455
+ end
456
+
457
+ # rubocop:disable Metrics/MethodLength, Metrics/LineLength
427
458
  def create_ec2_json(state)
428
459
  if windows_os?
429
460
  cmd = "New-Item -Force C:\\chef\\ohai\\hints\\ec2.json -ItemType File"
430
461
  else
431
- cmd = "sudo mkdir -p /etc/chef/ohai/hints;sudo touch /etc/chef/ohai/hints/ec2.json"
462
+ debug "Using sudo_command='#{sudo_command}' for ohai hints"
463
+ cmd = "#{sudo_command} mkdir -p /etc/chef/ohai/hints; #{sudo_command} touch /etc/chef/ohai/hints/ec2.json"
432
464
  end
433
465
  instance.transport.connection(state).execute(cmd)
434
466
  end
435
467
 
436
- # rubocop:disable Metrics/MethodLength, Metrics/LineLength
437
468
  def default_windows_user_data
438
469
  # Preparing custom static admin user if we defined something other than Administrator
439
470
  custom_admin_script = ""
@@ -476,6 +507,29 @@ module Kitchen
476
507
  end
477
508
  # rubocop:enable Metrics/MethodLength, Metrics/LineLength
478
509
 
510
+ def show_chosen_image
511
+ # Print some debug stuff
512
+ debug("Image for #{instance.name}: #{image.name}. #{image_info(image)}")
513
+ if actual_platform
514
+ info("Detected platform: #{actual_platform.name} version #{actual_platform.version}" \
515
+ " on #{actual_platform.architecture}. Instance Type: #{config[:instance_type]}." \
516
+ " Default username: #{actual_platform.username} (default).")
517
+ else
518
+ debug("No platform detected for #{image.name}.")
519
+ end
520
+ end
521
+
522
+ def image_info(image)
523
+ root_device = image.block_device_mappings.
524
+ find { |b| b.device_name == image.root_device_name }
525
+ volume_type = " #{root_device.ebs.volume_type}" if root_device && root_device.ebs
526
+
527
+ " Architecture: #{image.architecture}," \
528
+ " Virtualization: #{image.virtualization_type}," \
529
+ " Storage: #{image.root_device_type}#{volume_type}," \
530
+ " Created: #{image.creation_date}"
531
+ end
532
+
479
533
  end
480
534
  end
481
535
  end