kitchen-ec2 0.10.0 → 1.0.0.beta.1

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