class Kitchen::Driver::Gce

Google Compute Engine driver for Test Kitchen

@author Andrew Leonard <andy@hurricane-ridge.com>

Constants

SCOPE_ALIAS_MAP

Attributes

state[RW]

Public Instance Methods

authorization() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 174
def authorization
  @authorization ||= Google::Auth.get_application_default(
    [
      "https://www.googleapis.com/auth/cloud-platform",
      "https://www.googleapis.com/auth/compute",
    ]
  )
end
auto_migrate?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 437
def auto_migrate?
  preemptible? ? false : config[:auto_migrate]
end
auto_restart?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 441
def auto_restart?
  preemptible? ? false : config[:auto_restart]
end
boot_disk(server_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 339
def boot_disk(server_name)
  disk   = Google::Apis::ComputeV1::AttachedDisk.new
  params = Google::Apis::ComputeV1::AttachedDiskInitializeParams.new

  disk.boot           = true
  disk.auto_delete    = config[:autodelete_disk]
  params.disk_name    = server_name
  params.disk_size_gb = config[:disk_size]
  params.disk_type    = disk_type_url_for(config[:disk_type])
  params.source_image = boot_disk_source_image

  disk.initialize_params = params
  disk
end
boot_disk_source_image() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 358
def boot_disk_source_image
  @boot_disk_source ||= image_url
end
check_api_call() { || ... } click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 205
def check_api_call(&block)
  yield
rescue Google::Apis::ClientError => e
  debug("API error: #{e.message}")
  false
else
  true
end
connection() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 161
def connection
  return @connection unless @connection.nil?

  @connection = Google::Apis::ComputeV1::ComputeService.new
  @connection.authorization = authorization
  @connection.client_options = Google::Apis::ClientOptions.new.tap do |opts|
    opts.application_name    = "kitchen-google"
    opts.application_version = Kitchen::Driver::GCE_VERSION
  end

  @connection
end
create(state) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 88
def create(state)
  @state = state
  return if state[:server_name]

  validate!

  server_name = generate_server_name

  info("Creating GCE instance <#{server_name}> in project #{project}, zone #{zone}...")
  operation = connection.insert_instance(project, zone, create_instance_object(server_name))

  info("Zone operation #{operation.name} created. Waiting for it to complete...")
  wait_for_operation(operation)

  server              = server_instance(server_name)
  state[:server_name] = server_name
  state[:hostname]    = ip_address_for(server)
  state[:zone]        = zone

  info("Server <#{server_name}> created.")

  update_windows_password(server_name)

  info("Waiting for server <#{server_name}> to be ready...")
  wait_for_server

  info("GCE instance <#{server_name}> created and ready.")
rescue => e
  error("Error encountered during server creation: #{e.class}: #{e.message}")
  destroy(state)
  raise
end
create_instance_object(server_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 314
def create_instance_object(server_name)
  inst_obj                    = Google::Apis::ComputeV1::Instance.new
  inst_obj.name               = server_name
  inst_obj.disks              = [boot_disk(server_name)]
  inst_obj.machine_type       = machine_type_url
  inst_obj.metadata           = instance_metadata
  inst_obj.network_interfaces = instance_network_interfaces
  inst_obj.scheduling         = instance_scheduling
  inst_obj.service_accounts   = instance_service_accounts unless instance_service_accounts.nil?
  inst_obj.tags               = instance_tags

  inst_obj
end
destroy(state) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 121
def destroy(state)
  @state      = state
  server_name = state[:server_name]
  return if server_name.nil?

  unless server_exist?(server_name)
    info("GCE instance <#{server_name}> does not exist - assuming it has been already destroyed.")
    return
  end

  info("Destroying GCE instance <#{server_name}>...")
  wait_for_operation(connection.delete_instance(project, zone, server_name))
  info("GCE instance <#{server_name}> destroyed.")

  state.delete(:server_name)
  state.delete(:hostname)
  state.delete(:zone)
end
disk_type_url_for(type) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 354
def disk_type_url_for(type)
  "zones/#{zone}/diskTypes/#{type}"
end
env_user() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 392
def env_user
  ENV["USER"] || "unknown"
end
find_zone() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 280
def find_zone
  zone = zones_in_region.sample
  raise "Unable to find a suitable zone in #{region}" if zone.nil?

  zone.name
end
generate_server_name() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 328
def generate_server_name
  name = "tk-#{instance.name.downcase}-#{SecureRandom.hex(3)}"

  if name.length > 63
    warn("The TK instance name (#{instance.name}) has been removed from the GCE instance name due to size limitations. Consider setting shorter platform or suite names.")
    name = "tk-#{SecureRandom.uuid}"
  end

  name.gsub(/([^-a-z0-9])/, "-")
end
image_exist?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 248
def image_exist?
  check_api_call { connection.get_image(image_project, image_name) }
end
image_name() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 260
def image_name
  @image_name ||= config[:image_name] || image_name_for_family(config[:image_family])
end
image_name_for_family(image_family) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 366
def image_name_for_family(image_family)
  image = connection.get_image_from_family(image_project, image_family)
  image.name
end
image_project() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 264
def image_project
  config[:image_project].nil? ? project : config[:image_project]
end
image_url() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 362
def image_url
  return "projects/#{image_project}/global/images/#{image_name}" if image_exist?
end
instance_metadata() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 375
def instance_metadata
  metadata = {
    "created-by"            => "test-kitchen",
    "test-kitchen-instance" => instance.name,
    "test-kitchen-user"     => env_user,
  }

  Google::Apis::ComputeV1::Metadata.new.tap do |metadata_obj|
    metadata_obj.items = metadata.each_with_object([]) do |(k, v), memo|
      memo << Google::Apis::ComputeV1::Metadata::Item.new.tap do |item|
        item.key   = k
        item.value = v
      end
    end
  end
end
instance_network_interfaces() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 396
def instance_network_interfaces
  interface                = Google::Apis::ComputeV1::NetworkInterface.new
  interface.network        = network_url
  interface.subnetwork     = subnet_url if subnet_url
  interface.access_configs = interface_access_configs

  Array(interface)
end
instance_scheduling() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 425
def instance_scheduling
  Google::Apis::ComputeV1::Scheduling.new.tap do |scheduling|
    scheduling.automatic_restart   = auto_restart?.to_s
    scheduling.preemptible         = preemptible?.to_s
    scheduling.on_host_maintenance = migrate_setting
  end
end
instance_service_accounts() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 449
def instance_service_accounts
  return if config[:service_account_scopes].nil? || config[:service_account_scopes].empty?

  service_account        = Google::Apis::ComputeV1::ServiceAccount.new
  service_account.email  = config[:service_account_name]
  service_account.scopes = config[:service_account_scopes].map { |scope| service_account_scope_url(scope) }

  Array(service_account)
end
instance_tags() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 468
def instance_tags
  Google::Apis::ComputeV1::Tags.new.tap { |tag_obj| tag_obj.items = config[:tags] }
end
interface_access_configs() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 415
def interface_access_configs
  return [] if config[:use_private_ip]

  access_config        = Google::Apis::ComputeV1::AccessConfig.new
  access_config.name   = "External NAT"
  access_config.type   = "ONE_TO_ONE_NAT"

  Array(access_config)
end
ip_address_for(server) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 298
def ip_address_for(server)
  config[:use_private_ip] ? private_ip_for(server) : public_ip_for(server)
end
machine_type_url() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 371
def machine_type_url
  "zones/#{zone}/machineTypes/#{config[:machine_type]}"
end
migrate_setting() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 445
def migrate_setting
  auto_migrate? ? "MIGRATE" : "TERMINATE"
end
name() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 84
def name
  "Google Compute (GCE)"
end
network_url() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 405
def network_url
  "projects/#{project}/global/networks/#{config[:network]}"
end
operation_errors(operation_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 534
def operation_errors(operation_name)
  operation = zone_operation(operation_name)
  return [] if operation.error.nil?

  operation.error.errors
end
preemptible?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 433
def preemptible?
  config[:preemptible]
end
private_ip_for(server) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 302
def private_ip_for(server)
  server.network_interfaces.first.network_ip
rescue NoMethodError
  raise "Unable to determine private IP for instance"
end
project() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 256
def project
  config[:project]
end
public_ip_for(server) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 308
def public_ip_for(server)
  server.network_interfaces.first.access_configs.first.nat_ip
rescue NoMethodError
  raise "Unable to determine public IP for instance"
end
refresh_rate() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 476
def refresh_rate
  config[:refresh_rate]
end
region() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 268
def region
  config[:region].nil? ? region_for_zone : config[:region]
end
region_for_zone() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 272
def region_for_zone
  @region_for_zone ||= connection.get_zone(project, zone).region.split("/").last
end
server_exist?(server_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 252
def server_exist?(server_name)
  check_api_call { server_instance(server_name) }
end
server_instance(server_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 294
def server_instance(server_name)
  connection.get_instance(project, zone, server_name)
end
service_account_scope_url(scope) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 459
def service_account_scope_url(scope)
  return scope if scope.start_with?("https://www.googleapis.com/auth/")
  "https://www.googleapis.com/auth/#{translate_scope_alias(scope)}"
end
subnet_url() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 409
def subnet_url
  return unless config[:subnet]

  "projects/#{project}/regions/#{region}/subnetworks/#{config[:subnet]}"
end
translate_scope_alias(scope_alias) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 464
def translate_scope_alias(scope_alias)
  SCOPE_ALIAS_MAP.fetch(scope_alias, scope_alias)
end
update_windows_password(server_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 187
def update_windows_password(server_name)
  return unless winrm_transport?

  username = instance.transport[:username]

  info("Resetting the Windows password for user #{username} on #{server_name}...")

  state[:password] = GoogleComputeWindowsPassword.new(
    project:       project,
    zone:          zone,
    instance_name: server_name,
    email:         config[:email],
    username:      username
  ).new_password

  info("Password reset complete on #{server_name} complete.")
end
valid_disk_type?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 243
def valid_disk_type?
  return false if config[:disk_type].nil?
  check_api_call { connection.get_disk_type(project, zone, config[:disk_type]) }
end
valid_machine_type?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 218
def valid_machine_type?
  return false if config[:machine_type].nil?
  check_api_call { connection.get_machine_type(project, zone, config[:machine_type]) }
end
valid_network?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 223
def valid_network?
  return false if config[:network].nil?
  check_api_call { connection.get_network(project, config[:network]) }
end
valid_project?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 214
def valid_project?
  check_api_call { connection.get_project(project) }
end
valid_region?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 238
def valid_region?
  return false if config[:region].nil?
  check_api_call { connection.get_region(project, config[:region]) }
end
valid_subnet?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 228
def valid_subnet?
  return false if config[:subnet].nil?
  check_api_call { connection.get_subnetwork(project, region, config[:subnet]) }
end
valid_zone?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 233
def valid_zone?
  return false if config[:zone].nil?
  check_api_call { connection.get_zone(project, config[:zone]) }
end
validate!() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 140
def validate!
  raise "Project #{config[:project]} is not a valid project" unless valid_project?
  raise "Either zone or region must be specified" unless config[:zone] || config[:region]
  raise "'any' is no longer a valid region" if config[:region] == "any"
  raise "Zone #{config[:zone]} is not a valid zone" if config[:zone] && !valid_zone?
  raise "Region #{config[:region]} is not a valid region" if config[:region] && !valid_region?
  raise "Machine type #{config[:machine_type]} is not valid" unless valid_machine_type?
  raise "Disk type #{config[:disk_type]} is not valid" unless valid_disk_type?
  raise "Either image family or name must be specified" unless config[:image_family] || config[:image_name]
  raise "Disk image #{config[:image_name]} is not valid - check your image name and image project" if boot_disk_source_image.nil?
  raise "Network #{config[:network]} is not valid" unless valid_network?
  raise "Subnet #{config[:subnet]} is not valid" if config[:subnet] && !valid_subnet?
  raise "Email address of GCE user is not set" if winrm_transport? && config[:email].nil?

  warn("Both zone and region specified - region will be ignored.") if config[:zone] && config[:region]
  warn("Both image family and name specified - image family will be ignored") if config[:image_family] && config[:image_name]
  warn("Image project not specified - searching current project only") unless config[:image_project]
  warn("Auto-migrate disabled for preemptible instance") if preemptible? && config[:auto_migrate]
  warn("Auto-restart disabled for preemptible instance") if preemptible? && config[:auto_restart]
end
wait_for_operation(operation) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 505
def wait_for_operation(operation)
  operation_name = operation.name

  wait_for_status("DONE") { zone_operation(operation_name) }

  errors = operation_errors(operation_name)
  return if errors.empty?

  errors.each do |error|
    error("#{error.code}: #{error.message}")
  end

  raise "Operation #{operation_name} failed."
end
wait_for_server() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 520
def wait_for_server
  begin
    instance.transport.connection(state).wait_until_ready
  rescue
    error("Server not reachable. Destroying server...")
    destroy(state)
    raise
  end
end
wait_for_status(requested_status) { || ... } click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 480
def wait_for_status(requested_status, &block)
  last_status = ""

  begin
    Timeout.timeout(wait_time) do
      loop do
        item = yield
        current_status = item.status

        unless last_status == current_status
          last_status = current_status
          info("Current status: #{current_status}")
        end

        break if current_status == requested_status

        sleep refresh_rate
      end
    end
  rescue Timeout::Error
    error("Request did not complete in #{wait_time} seconds. Check the Google Cloud Console for more info.")
    raise
  end
end
wait_time() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 472
def wait_time
  config[:wait_time]
end
winrm_transport?() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 183
def winrm_transport?
  instance.transport.name.casecmp("winrm") == 0
end
zone() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 276
def zone
  @zone ||= state[:zone] || config[:zone] || find_zone
end
zone_operation(operation_name) click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 530
def zone_operation(operation_name)
  connection.get_zone_operation(project, zone, operation_name)
end
zones_in_region() click to toggle source
# File lib/kitchen/driver/gce_as.rb, line 287
def zones_in_region
  connection.list_zones(project).items.select do |zone|
    zone.status == "UP" &&
      zone.region.split("/").last == region
  end
end