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



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

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



330
331
332
333
334
335
336
# File 'lib/kitchen/driver/ec2.rb', line 330

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



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/kitchen/driver/ec2.rb', line 266

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



170
171
172
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
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/kitchen/driver/ec2.rb', line 170

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

  info(Kitchen::Util.outdent!("    Creating <\#{state[:server_id]}>...\n    If you are not using an account that qualifies under the AWS\n    free-tier, you may be charged to run these suites. The charge\n    should be minimal, but neither Test Kitchen nor its maintainers\n    are responsible for your incurred costs.\n  END\n\n  if config[:price]\n    # Spot instance when a price is set\n    server = submit_spot(state)\n  else\n    # On-demand instance\n    server = submit_server\n  end\n  info(\"Instance <\#{server.id}> requested.\")\n  tag_server(server)\n\n  state[:server_id] = server.id\n  info(\"EC2 instance <\#{state[:server_id]}> created.\")\n  wait_log = proc do |attempts|\n    c = attempts * config[:retryable_sleep]\n    t = config[:retryable_tries] * config[:retryable_sleep]\n    info \"Waited \#{c}/\#{t}s for instance <\#{state[:server_id]}> to become ready.\"\n  end\n  begin\n    server = server.wait_until(\n      :max_attempts => config[:retryable_tries],\n      :delay => config[:retryable_sleep],\n      :before_attempt => wait_log\n    ) do |s|\n      hostname = hostname(s, config[:interface])\n      # Euca instances often report ready before they have an IP\n      s.exists? && s.state.name == \"running\" && !hostname.nil? && hostname != \"0.0.0.0\"\n    end\n  rescue ::Aws::Waiters::Errors::WaiterFailed\n    error(\"Ran out of time waiting for the server with id [\#{state[:server_id]}]\" \\\n      \" to become ready, attempting to destroy it\")\n    destroy(state)\n    raise\n  end\n\n  info(\"EC2 instance <\#{state[:server_id]}> ready.\")\n  state[:hostname] = hostname(server)\n  instance.transport.connection(state).wait_until_ready\n  create_ec2_json(state)\n  debug(\"ec2:create '\#{state[:hostname]}'\")\nend\n"))

#create_ec2_json(state) ⇒ Object



371
372
373
374
375
# File 'lib/kitchen/driver/ec2.rb', line 371

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



242
243
244
245
# File 'lib/kitchen/driver/ec2.rb', line 242

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

#destroy(state) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/kitchen/driver/ec2.rb', line 223

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]]
    )
  end
  info("EC2 instance <#{state[:server_id]}> destroyed.")
  state.delete(:server_id)
  state.delete(:hostname)
end

#ec2Object



247
248
249
250
251
252
253
254
255
# File 'lib/kitchen/driver/ec2.rb', line 247

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]
  )
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



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

def finalize_config!(instance)
  super

  if config[:availability_zone].nil?
    config[:availability_zone] = config[:region] + "b"
  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.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/kitchen/driver/ec2.rb', line 354

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



257
258
259
# File 'lib/kitchen/driver/ec2.rb', line 257

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

#submit_serverObject

Fog AWS helper for creating the instance



288
289
290
291
292
293
294
# File 'lib/kitchen/driver/ec2.rb', line 288

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



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/kitchen/driver/ec2.rb', line 296

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



322
323
324
325
326
327
328
# File 'lib/kitchen/driver/ec2.rb', line 322

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