class Kitchen::Driver::Gce
Google Compute Engine driver for Test Kitchen
@author Andrew Leonard <andy@hurricane-ridge.com>
Constants
- DISK_NAME_REGEX
- SCOPE_ALIAS_MAP
Attributes
state[RW]
Public Instance Methods
auto_migrate?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 618 def auto_migrate? preemptible? ? false : config[:auto_migrate] end
auto_restart?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 622 def auto_restart? preemptible? ? false : config[:auto_restart] end
boot_disk_source_image()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 523 def boot_disk_source_image @boot_disk_source ||= image_url end
check_api_call() { || ... }
click to toggle source
# File lib/kitchen/driver/gce.rb, line 284 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.rb, line 240 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 = "GoogleChefTestKitchen" opts.application_version = Kitchen::Driver::GCE_VERSION end @connection end
create(state)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 92 def create(state) @state = state return if state[:server_name] validate! server_name = generate_server_name create_disks_config info("Creating GCE instance <#{server_name}> in project #{project}, zone #{zone}...") operation = connection.insert_instance(project, zone, create_instance_object(server_name)) 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_attached_disk(unique_disk_name, disk_config)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 485 def create_attached_disk(unique_disk_name, disk_config) disk = Google::Apis::ComputeV1::Disk.new disk.name = unique_disk_name disk.size_gb = disk_config[:disk_size] disk.type = disk_type_url_for(disk_config[:disk_type]) info("Creating a #{disk_config[:disk_size]} GB disk named #{unique_disk_name}...") wait_for_operation(connection.insert_disk(project, zone, disk)) info("Waiting for disk to be ready...") wait_for_status("READY") { connection.get_disk(project, zone, unique_disk_name) } info("Disk created successfully.") attached_disk = Google::Apis::ComputeV1::AttachedDisk.new attached_disk.source = disk_self_link(unique_disk_name) attached_disk.auto_delete = disk_config[:autodelete_disk] attached_disk end
create_disks(server_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 441 def create_disks(server_name) disks = [] config[:disks].each do |disk_name, disk_config| unique_disk_name = "#{server_name}-#{disk_name}" if disk_config[:boot] disk = create_local_disk(unique_disk_name, disk_config) disks.unshift(disk) elsif (disk_config[:disk_type] == "local-ssd") || disk_config[:custom_image] disk = create_local_disk(unique_disk_name, disk_config) disks.push(disk) else disk = create_attached_disk(unique_disk_name, disk_config) disks.push(disk) end end disks end
create_disks_config()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 153 def create_disks_config # This can't be present in default_config because we couldn't # determine which disk configuration the user used otherwise disk_default_config = { autodelete_disk: true, disk_size: 10, disk_type: "pd-standard", } if old_disk_configuration_present? # If the old disk configuration is used, # we'll convert it to the new one config[:disks] = { disk1: { boot: true, autodelete_disk: config.fetch(:autodelete_disk, disk_default_config[:autodelete_disk]), disk_size: config.fetch(:disk_size, disk_default_config[:disk_size]), disk_type: config.fetch(:disk_type, disk_default_config[:disk_type]), }, } raise "Disk type #{config[:disks][:disk1][:disk_type]} is not valid" unless valid_disk_type?(config[:disks][:disk1][:disk_type]) elsif new_disk_configuration_present? # If the new disk configuration is present, ensure that for # every disk the needed configuration is set boot_disk_counter = 0 config[:disks].each do |disk_name, disk_config| # te&/ => te raise "Disk name invalid. Must match #{DISK_NAME_REGEX}." unless valid_disk_name?(disk_name) # Update the config for the disk with the fixed config config[:disks][disk_name.to_sym] = disk_default_config.merge(disk_config) # Since the config was altered, we can't use disk_config (as it will be different or keys will not be present) raise "Disk type #{config[:disks][disk_name.to_sym][:disk_type]} for disk #{disk_name} is not valid" unless valid_disk_type?(config[:disks][disk_name.to_sym][:disk_type]) unless disk_config[:boot].nil? boot_disk_counter += 1 raise "Boot disk cannot be local SSD." if disk_config[:disk_type] == "local-ssd" end if disk_config[:disk_type] == "local-ssd" raise "#{disk_name}: Cannot use 'disk_size' with local SSD. They always have 375 GB (https://cloud.google.com/compute/docs/disks/#localssds)." unless disk_config[:disk_size].nil? # Since disk_size is set to 10 in default_config, it needs to be adjusted for local SSDs config[:disks][disk_name.to_sym][:disk_size] = nil end end if boot_disk_counter == 0 first_disk = config[:disks].first[0] first_config = config[:disks].first[1] config[:disks][first_disk] = first_config.merge({ boot: true }) warn("No bootdisk found - Assuming first disk will be boot disk") elsif boot_disk_counter > 1 raise "More than one boot disk specified" end elsif !new_disk_configuration_present? # If no new disk configuration is present, # we'll set up the default configuration for the new style config[:disks] = { "disk1": disk_default_config.merge({ boot: true }), } end end
create_instance_object(server_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 415 def create_instance_object(server_name) inst_obj = Google::Apis::ComputeV1::Instance.new inst_obj.name = server_name inst_obj.disks = create_disks(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.labels = instance_labels inst_obj end
create_local_disk(unique_disk_name, disk_config)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 459 def create_local_disk(unique_disk_name, disk_config) disk = Google::Apis::ComputeV1::AttachedDisk.new # Specifies the parameters for a new disk that will be created alongside the new instance. params = Google::Apis::ComputeV1::AttachedDiskInitializeParams.new disk.boot = true if !disk_config[:boot].nil? && disk_config[:boot].to_s == "true" disk.auto_delete = disk_config[:autodelete_disk] params.disk_size_gb = disk_config[:disk_size] params.disk_type = disk_type_url_for(disk_config[:disk_type]) if disk_config[:disk_type] == "local-ssd" info("Creating a 375 GB local ssd as scratch disk (https://cloud.google.com/compute/docs/disks/#localssds).") disk.type = "SCRATCH" elsif disk.boot info("Creating a #{disk_config[:disk_size]} GB boot disk named #{unique_disk_name} from image #{image_name}...") params.source_image = boot_disk_source_image unless disk_config[:disk_type] == "local-ssd" params.disk_name = unique_disk_name unless disk_config[:disk_type] == "local-ssd" else info("Creating a #{disk_config[:disk_size]} GB extra disk named #{unique_disk_name} from image #{disk_config[:custom_image]}...") params.source_image = image_url(disk_config[:custom_image]) unless disk_config[:disk_type] == "local-ssd" params.disk_name = unique_disk_name unless disk_config[:disk_type] == "local-ssd" end disk.initialize_params = params disk end
delete_disk(unique_disk_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 502 def delete_disk(unique_disk_name) begin connection.get_disk(project, zone, unique_disk_name) rescue Google::Apis::ClientError info("Unable to locate disk #{unique_disk_name} in project #{project}, zone #{zone}") return end info("Waiting for disk #{unique_disk_name} to be deleted...") wait_for_operation(connection.delete_disk(project, zone, unique_disk_name)) info("Disk #{unique_disk_name} deleted successfully.") end
destroy(state)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 126 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_self_link(unique_disk_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 519 def disk_self_link(unique_disk_name) "projects/#{project}/zones/#{zone}/disks/#{unique_disk_name}" end
disk_type_url_for(type)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 515 def disk_type_url_for(type) "zones/#{zone}/diskTypes/#{type}" end
env_user()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 572 def env_user ENV["USER"] || "unknown" end
find_zone()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 381 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.rb, line 430 def generate_server_name name = config[:inst_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?(image = image_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 337 def image_exist?(image = image_name) check_api_call { connection.get_image(image_project, image) } end
image_name()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 349 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.rb, line 531 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.rb, line 353 def image_project config[:image_project].nil? ? project : config[:image_project] end
image_url(image = image_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 527 def image_url(image = image_name) return "projects/#{image_project}/global/images/#{image}" if image_exist?(image) end
instance_labels()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 568 def instance_labels config[:labels] end
instance_metadata()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 557 def instance_metadata 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.to_s item.value = v.to_s end end end end
instance_network_interfaces()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 576 def instance_network_interfaces interface = Google::Apis::ComputeV1::NetworkInterface.new interface.network = network_url if config[:subnet_project].nil? interface.network_ip = network_ip unless network_ip.nil? 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.rb, line 606 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.rb, line 630 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
interface_access_configs()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 596 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.rb, line 399 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.rb, line 536 def machine_type_url "zones/#{zone}/machineTypes/#{config[:machine_type]}" end
metadata()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 540 def metadata default_metadata = { "created-by" => "test-kitchen", "test-kitchen-instance" => instance.name, "test-kitchen-user" => env_user, } if winrm_transport? image_identifier = config[:image_family] || config[:image_name] default_metadata["windows-startup-script-ps1"] = 'netsh advfirewall firewall add rule name="winrm" dir=in action=allow protocol=TCP localport=5985;' if !image_identifier.nil? && image_identifier.include?("2008") default_metadata["windows-startup-script-ps1"] += "winrm quickconfig -q" end end config[:metadata].merge(default_metadata) end
migrate_setting()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 626 def migrate_setting auto_migrate? ? "MIGRATE" : "TERMINATE" end
name()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 88 def name "Google Compute (GCE)" end
network_ip()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 365 def network_ip config[:network_ip] end
network_project()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 361 def network_project config[:network_project].nil? ? project : config[:network_project] end
network_url()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 586 def network_url "projects/#{network_project}/global/networks/#{config[:network]}" end
new_disk_configuration_present?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 149 def new_disk_configuration_present? !config[:disks].nil? end
old_disk_configuration_present?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 145 def old_disk_configuration_present? !config[:autodelete_disk].nil? || !config[:disk_size].nil? || !config[:disk_type].nil? end
operation_errors(operation_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 714 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.rb, line 614 def preemptible? config[:preemptible] end
private_ip_for(server)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 403 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.rb, line 345 def project config[:project] end
public_ip_for(server)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 409 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.rb, line 658 def refresh_rate config[:refresh_rate] end
region()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 369 def region config[:region].nil? ? region_for_zone : config[:region] end
region_for_zone()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 373 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.rb, line 341 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.rb, line 395 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.rb, line 640 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_project()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 357 def subnet_project config[:subnet_project].nil? ? project : config[:subnet_project] end
subnet_url()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 590 def subnet_url return unless config[:subnet] "projects/#{subnet_project}/regions/#{region}/subnetworks/#{config[:subnet]}" end
translate_scope_alias(scope_alias)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 646 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.rb, line 266 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_name?(disk_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 333 def valid_disk_name?(disk_name) disk_name.to_s.match(DISK_NAME_REGEX).to_s.length == disk_name.length end
valid_disk_type?(disk_type)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 327 def valid_disk_type?(disk_type) return false if disk_type.nil? check_api_call { connection.get_disk_type(project, zone, disk_type) } end
valid_machine_type?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 297 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.rb, line 303 def valid_network? return false if config[:network].nil? check_api_call { connection.get_network(network_project, config[:network]) } end
valid_project?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 293 def valid_project? check_api_call { connection.get_project(project) } end
valid_region?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 321 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.rb, line 309 def valid_subnet? return false if config[:subnet].nil? check_api_call { connection.get_subnetwork(subnet_project, region, config[:subnet]) } end
valid_zone?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 315 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.rb, line 217 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 "Either image family or name must be specified" unless config[:image_family] || config[:image_name] 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? raise "You cannot use autodelete_disk, disk_size or disk_type with the new disks configuration" if old_disk_configuration_present? && new_disk_configuration_present? raise "Disk image #{config[:image_name]} is not valid - check your image name and image project" if boot_disk_source_image.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("Subnet project not specified - searching current project only") if config[:subnet] && !config[:subnet_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] warn("These configs are deprecated - consider using new disks configuration") if old_disk_configuration_present? end
wait_for_operation(operation)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 687 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.rb, line 702 def wait_for_server instance.transport.connection(state).wait_until_ready rescue error("Server not reachable. Destroying server...") destroy(state) raise end
wait_for_status(requested_status) { || ... }
click to toggle source
# File lib/kitchen/driver/gce.rb, line 662 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.rb, line 654 def wait_time config[:wait_time] end
winrm_transport?()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 262 def winrm_transport? instance.transport.name.casecmp("winrm") == 0 end
zone()
click to toggle source
# File lib/kitchen/driver/gce.rb, line 377 def zone @zone ||= state[:zone] || config[:zone] || find_zone end
zone_operation(operation_name)
click to toggle source
# File lib/kitchen/driver/gce.rb, line 710 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.rb, line 388 def zones_in_region connection.list_zones(project).items.select do |zone| zone.status == "UP" && zone.region.split("/").last == region end end