Class: Kitchen::Driver::Ec2

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/driver/ec2.rb

Overview

Amazon EC2 driver for Test Kitchen.

Author:

Constant Summary collapse

INTERFACE_TYPES =

Ordered mapping from config name to Fog name. Ordered by preference when looking up hostname.

{
  "dns" => "public_dns_name",
  "public" => "public_ip_address",
  "private" => "private_ip_address"
}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.validation_warn(driver, old_key, new_key) ⇒ Object



70
71
72
73
# File 'lib/kitchen/driver/ec2.rb', line 70

def self.validation_warn(driver, old_key, new_key)
  driver.warn "WARN: The driver[#{driver.class.name}] config key `#{old_key}` " \
    "is deprecated, please use `#{new_key}`"
end

Instance Method Details

#amisObject



343
344
345
346
347
348
349
# File 'lib/kitchen/driver/ec2.rb', line 343

def amis
  @amis ||= begin
    json_file = File.join(File.dirname(__FILE__),
      %w[.. .. .. data amis.json])
    JSON.load(IO.read(json_file))
  end
end

#copy_deprecated_configs(state) ⇒ Object

This copies transport config from the current config object into the state. This relies on logic in the transport that merges the transport config with the current state object, so its a bad coupling. But we can get rid of this when we get rid of these deprecated configs! rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/kitchen/driver/ec2.rb', line 255

def copy_deprecated_configs(state)
  if config[:ssh_timeout]
    state[:connection_timeout] = config[:ssh_timeout]
  end
  if config[:ssh_retries]
    state[:connection_retries] = config[:ssh_retries]
  end
  if config[:username]
    state[:username] = config[:username]
  elsif instance.transport[:username] == instance.transport.class.defaults[:username]
    # If the transport has the default username, copy it from amis.json
    # This duplicated old behavior but I hate amis.json
    ami_username = amis["usernames"][instance.platform.name]
    state[:username] = ami_username if ami_username
  end
  if config[:ssh_key]
    state[:ssh_key] = config[:ssh_key]
  end
end

#create(state) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/MethodLength



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/kitchen/driver/ec2.rb', line 173

def create(state) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  copy_deprecated_configs(state)
  return if state[:server_id]

  info(Kitchen::Util.outdent!(<<-END))
    Creating <#{state[:server_id]}>...
    If you are not using an account that qualifies under the AWS
    free-tier, you may be charged to run these suites. The charge
    should be minimal, but neither Test Kitchen nor its maintainers
    are responsible for your incurred costs.
  END

  if config[:price]
    # Spot instance when a price is set
    server = submit_spot(state)
  else
    # On-demand instance
    server = submit_server
  end
  info("Instance <#{server.id}> requested.")
  ec2.client.wait_until(
    :instance_exists,
    :instance_ids => [server.id]
  )
  tag_server(server)

  state[:server_id] = server.id
  info("EC2 instance <#{state[:server_id]}> created.")
  wait_until_ready(server, state)

  info("EC2 instance <#{state[:server_id]}> ready.")
  state[:hostname] = hostname(server)
  instance.transport.connection(state).wait_until_ready
  create_ec2_json(state)
  debug("ec2:create '#{state[:hostname]}'")
end

#create_ec2_json(state) ⇒ Object



384
385
386
387
388
# File 'lib/kitchen/driver/ec2.rb', line 384

def create_ec2_json(state)
  instance.transport.connection(state).execute(
    "sudo mkdir -p /etc/chef/ohai/hints;sudo touch /etc/chef/ohai/hints/ec2.json"
  )
end

#default_amiObject



230
231
232
233
# File 'lib/kitchen/driver/ec2.rb', line 230

def default_ami
  region = amis["regions"][config[:region]]
  region && region[instance.platform.name]
end

#destroy(state) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/kitchen/driver/ec2.rb', line 210

def destroy(state)
  return if state[:server_id].nil?

  server = ec2.get_instance(state[:server_id])
  unless server.nil?
    instance.transport.connection(state).close
    server.terminate
  end
  if state[:spot_request_id]
    debug("Deleting spot request <#{state[:server_id]}>")
    ec2.client.cancel_spot_instance_requests(
      :spot_instance_request_ids => [state[:spot_request_id]]
    )
    state.delete(:spot_request_id)
  end
  info("EC2 instance <#{state[:server_id]}> destroyed.")
  state.delete(:server_id)
  state.delete(:hostname)
end

#ec2Object



235
236
237
238
239
240
241
242
243
244
# File 'lib/kitchen/driver/ec2.rb', line 235

def ec2
  @ec2 ||= Aws::Client.new(
    config[:region],
    config[:shared_credentials_profile],
    config[:aws_access_key_id],
    config[:aws_secret_access_key],
    config[:aws_session_token],
    config[:http_proxy]
  )
end

#finalize_config!(instance) ⇒ self

A lifecycle method that should be invoked when the object is about ready to be used. A reference to an Instance is required as configuration dependant data may be access through an Instance. This also acts as a hook point where the object may wish to perform other last minute checks, validations, or configuration expansions.

Parameters:

  • instance (Instance)

    an associated instance

Returns:

  • (self)

    itself, for use in chaining

Raises:

  • (ClientError)

    if instance parameter is nil



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/kitchen/driver/ec2.rb', line 157

def finalize_config!(instance)
  super

  if config[:availability_zone].nil?
    config[:availability_zone] = config[:region] + "b"
  elsif config[:availability_zone] =~ /^[a-z]$/
    config[:availability_zone] = config[:region] + config[:availability_zone]
  end
  # TODO: when we get rid of flavor_id, move this to a default
  if config[:instance_type].nil?
    config[:instance_type] = config[:flavor_id] || "m1.small"
  end

  self
end

#hostname(server, interface_type = nil) ⇒ Object

Lookup hostname of provided server. If interface_type is provided use that interface to lookup hostname. Otherwise, try ordered list of options.



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/kitchen/driver/ec2.rb', line 367

def hostname(server, interface_type = nil)
  if interface_type
    interface_type = INTERFACE_TYPES.fetch(interface_type) do
      raise Kitchen::UserError, "Invalid interface [#{interface_type}]"
    end
    server.send(interface_type)
  else
    potential_hostname = nil
    INTERFACE_TYPES.values.each do |type|
      potential_hostname ||= server.send(type)
      # AWS returns an empty string if the dns name isn't populated yet
      potential_hostname = nil if potential_hostname == ""
    end
    potential_hostname
  end
end

#instance_generatorObject



246
247
248
# File 'lib/kitchen/driver/ec2.rb', line 246

def instance_generator
  @instance_generator ||= Aws::InstanceGenerator.new(config, ec2, instance.logger)
end

#submit_serverObject

Fog AWS helper for creating the instance



277
278
279
280
281
282
283
# File 'lib/kitchen/driver/ec2.rb', line 277

def submit_server
  debug("Creating EC2 Instance..")
  instance_data = instance_generator.ec2_instance_data
  instance_data[:min_count] = 1
  instance_data[:max_count] = 1
  ec2.create_instance(instance_data)
end

#submit_spot(state) ⇒ Object

rubocop:disable Metrics/AbcSize



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/kitchen/driver/ec2.rb', line 285

def submit_spot(state) # rubocop:disable Metrics/AbcSize
  debug("Creating EC2 Spot Instance..")
  request_data = {}
  request_data[:spot_price] = config[:price].to_s
  request_data[:launch_specification] = instance_generator.ec2_instance_data

  response = ec2.client.request_spot_instances(request_data)
  spot_request_id = response[:spot_instance_requests][0][:spot_instance_request_id]
  # deleting the instance cancels the request, but deleting the request
  # does not affect the instance
  state[:spot_request_id] = spot_request_id
  ec2.client.wait_until(
    :spot_instance_request_fulfilled,
    :spot_instance_request_ids => [spot_request_id]
  ) do |w|
    w.max_attempts = config[:retryable_tries]
    w.delay = config[:retryable_sleep]
    w.before_attempt do |attempts|
      c = attempts * config[:retryable_sleep]
      t = config[:retryable_tries] * config[:retryable_sleep]
      info "Waited #{c}/#{t}s for spot request <#{spot_request_id}> to become fulfilled."
    end
  end
  ec2.get_instance_from_spot_request(spot_request_id)
end

#tag_server(server) ⇒ Object



311
312
313
314
315
316
317
# File 'lib/kitchen/driver/ec2.rb', line 311

def tag_server(server)
  tags = []
  config[:tags].each do |k, v|
    tags << { :key => k, :value => v }
  end
  server.create_tags(:tags => tags)
end

#wait_until_ready(server, state) ⇒ Object



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/kitchen/driver/ec2.rb', line 319

def wait_until_ready(server, state)
  wait_log = proc do |attempts|
    c = attempts * config[:retryable_sleep]
    t = config[:retryable_tries] * config[:retryable_sleep]
    info "Waited #{c}/#{t}s for instance <#{state[:server_id]}> to become ready."
  end
  begin
    server.wait_until(
      :max_attempts => config[:retryable_tries],
      :delay => config[:retryable_sleep],
      :before_attempt => wait_log
    ) do |s|
      hostname = hostname(s, config[:interface])
      # Euca instances often report ready before they have an IP
      s.exists? && s.state.name == "running" && !hostname.nil? && hostname != "0.0.0.0"
    end
  rescue ::Aws::Waiters::Errors::WaiterFailed
    error("Ran out of time waiting for the server with id [#{state[:server_id]}]" \
      " to become ready, attempting to destroy it")
    destroy(state)
    raise
  end
end