Class: Kitchen::Driver::Gce

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/driver/gce.rb,
lib/kitchen/driver/gce/windows_password.rb

Overview

Google Compute Engine driver for Test Kitchen.

Creates and destroys GCE instances for Test Kitchen suites, translating kitchen.yml driver configuration into Google Compute Engine API calls.

Examples:

Minimal kitchen.yml configuration

driver:
  name: gce
  project: my-gcp-project
  zone: us-central1-a
  image_family: ubuntu-2204-lts
  image_project: ubuntu-os-cloud

Author:

Defined Under Namespace

Classes: WindowsPassword

Constant Summary collapse

SCOPE_ALIAS_MAP =

Maps the short scope aliases accepted by gcloud onto the scope segment of their fully-qualified OAuth 2.0 URL.

Returns:

  • (Hash{String => String})

    alias to scope-path mapping

{
  "bigquery" => "bigquery",
  "cloud-platform" => "cloud-platform",
  "compute-ro" => "compute.readonly",
  "compute-rw" => "compute",
  "datastore" => "datastore",
  "logging-write" => "logging.write",
  "monitoring" => "monitoring",
  "monitoring-write" => "monitoring.write",
  "service-control" => "servicecontrol",
  "service-management" => "service.management",
  "sql" => "sqlservice",
  "sql-admin" => "sqlservice.admin",
  "storage-full" => "devstorage.full_control",
  "storage-ro" => "devstorage.read_only",
  "storage-rw" => "devstorage.read_write",
  "taskqueue" => "taskqueue",
  "useraccounts-ro" => "cloud.useraccounts.readonly",
  "useraccounts-rw" => "cloud.useraccounts",
  "userinfo-email" => "userinfo.email",
}.freeze
DISK_NAME_REGEX =

Pattern a GCE disk name must match in full.

Returns:

  • (Regexp)

    the permitted disk-name pattern

/(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)/
MAX_INSTANCE_NAME_LENGTH =

Longest instance name GCE accepts.

Returns:

  • (Integer)

    the maximum instance-name length

63
LOCAL_SSD_SIZE_GB =

Fixed size, in gigabytes, of every GCE local SSD.

Returns:

  • (Integer)

    the local SSD size

See Also:

375
LOCAL_SSD_TYPE =

Disk type identifying a local SSD rather than a persistent disk.

Returns:

  • (String)

    the local SSD disk type

"local-ssd".freeze
DISK_DEFAULT_CONFIG =

Configuration applied to every disk before the user's own settings.

Deliberately sets no disk_type. GCE derives an omitted disk type from the instance's machine series -- pd-standard on first- and second-generation series such as N1 and N2, pd-balanced on C3, C3D and M3, and hyperdisk-balanced on C4, N4 and newer -- so leaving it unset is the only default that is compatible with every machine type. Naming one here would fail outright on the families that no longer accept it.

Returns:

  • (Hash)

    the per-disk defaults

{
  autodelete_disk: true,
  disk_size: 10,
}.freeze
BUILTIN_ADMINISTRATOR =

Local Windows account Test Kitchen's WinRM transport defaults to, and which Google's Windows images ship disabled.

Returns:

  • (String)

    the built-in administrator account name

"administrator".freeze
BUILTIN_ADMINISTRATOR_WARNING =

Told to the user when they are about to wait out a WinRM timeout for a reason the driver can see coming.

Returns:

  • (String)

    the warning text

"The WinRM transport is connecting as the built-in Administrator account, which is " \
"disabled on Google's Windows images. The guest agent resets its password without " \
"enabling it, so the login will be refused. Set transport.username to any other name " \
"and the agent will create that account instead.".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#stateHash

Returns the Test Kitchen state hash for the action in progress.

Returns:

  • (Hash)

    the Test Kitchen state hash for the action in progress



44
45
46
# File 'lib/kitchen/driver/gce.rb', line 44

def state
  @state
end

Instance Method Details

#assign_boot_disk(disks) ⇒ Hash{Symbol => Hash}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Ensures exactly one disk in the set is marked as the boot disk, promoting the first eligible disk when the user flagged none.

A disk is eligible unless it is a local SSD, which cannot boot, or the user explicitly set boot: false on it.

Parameters:

  • disks (Hash{Symbol => Hash})

    the normalised disk configuration

Returns:

  • (Hash{Symbol => Hash})

    the configuration with one boot disk

Raises:

  • (RuntimeError)

    if more than one boot disk is specified, no disks were given, or no disk is eligible to boot



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/kitchen/driver/gce.rb', line 372

def assign_boot_disk(disks)
  boot_disks = disks.select { |_disk_name, disk_config| disk_config[:boot] }

  raise "More than one boot disk specified" if boot_disks.size > 1
  return disks unless boot_disks.empty?

  raise "No disks specified" if disks.empty?

  bootable = disks.find do |_disk_name, disk_config|
    !local_ssd?(disk_config) && disk_config[:boot] != false
  end

  if bootable.nil?
    raise "No boot disk specified, and no disk is eligible to become one. " \
          "Local SSDs cannot boot, and disks set to 'boot: false' are excluded."
  end

  disk_name = bootable.first
  warn("No bootdisk found - Assuming #{disk_name} will be boot disk")
  disks.merge(disk_name => disks[disk_name].merge(boot: true))
end

#authorizationGoogle::Auth::Credentials

Application default credentials scoped for Compute Engine.

Returns:

  • (Google::Auth::Credentials)

    the resolved credentials



453
454
455
456
457
458
459
460
# File 'lib/kitchen/driver/gce.rb', line 453

def authorization
  @authorization ||= Google::Auth.get_application_default(
    [
      "https://www.googleapis.com/auth/cloud-platform",
      "https://www.googleapis.com/auth/compute",
    ]
  )
end

#auto_migrate?Boolean

Whether the instance may live-migrate. Always false for preemptible instances and for instances with guest accelerators attached, neither of which GCE will migrate.

Returns:

  • (Boolean)

    true if live migration is enabled



1118
1119
1120
1121
1122
# File 'lib/kitchen/driver/gce.rb', line 1118

def auto_migrate?
  return false if preemptible? || guest_accelerators?

  config[:auto_migrate] ? true : false
end

#auto_restart?Boolean

Whether the instance should restart automatically. Always false when preemptible, which GCE does not allow to auto-restart.

Returns:

  • (Boolean)

    true if auto-restart is enabled



1135
1136
1137
1138
1139
# File 'lib/kitchen/driver/gce.rb', line 1135

def auto_restart?
  return false if preemptible?

  config[:auto_restart] ? true : false
end

#boot_disk_source_imageString?

Memoised URL of the image the boot disk is created from.

Returns:

  • (String, nil)

    the image URL, or nil if the image is missing



943
944
945
# File 'lib/kitchen/driver/gce.rb', line 943

def boot_disk_source_image
  @boot_disk_source ||= image_url
end

#builtin_administrator?Boolean

Whether the WinRM transport is configured to log in as the built-in Administrator account.

Returns:

  • (Boolean)

    true if the transport username is administrator



466
467
468
# File 'lib/kitchen/driver/gce.rb', line 466

def builtin_administrator?
  instance.transport[:username].to_s.casecmp?(BUILTIN_ADMINISTRATOR)
end

#check_api_call { ... } ⇒ Boolean

Runs an API call and reports whether it succeeded, swallowing client errors so callers can use it as a validity predicate.

Yields:

  • the API call to attempt

Returns:

  • (Boolean)

    true if the call succeeded, false on a client error



507
508
509
510
511
512
513
514
# File 'lib/kitchen/driver/gce.rb', line 507

def check_api_call(&block)
  yield
rescue Google::Apis::ClientError => e
  debug("API error: #{e.message}")
  false
else
  true
end

#connectionGoogle::Apis::ComputeV1::ComputeService

Memoised, authorised Compute Engine API client.

Returns:

  • (Google::Apis::ComputeV1::ComputeService)

    the API client



437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/kitchen/driver/gce.rb', line 437

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) ⇒ void

This method returns an undefined value.

Creates a GCE instance for the Test Kitchen suite and waits until its transport is reachable.

Returns immediately if the state file already records a server, making the action idempotent. If any step fails, the partially-created instance and any standalone disks created along the way are torn down before the error is re-raised.

Parameters:

  • state (Hash)

    the Test Kitchen state hash, mutated in place with :server_name, :hostname and :zone

Raises:

  • (StandardError)

    if instance creation fails for any reason



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/gce.rb', line 179

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

  # GCE starts billing for the instance as soon as the insert is
  # accepted, so record it before waiting on the operation. Anything that
  # goes wrong from here on can then be torn down by the rescue below,
  # and by `kitchen destroy` if the process does not survive to run it.
  state[:server_name] = server_name
  state[:zone]        = zone

  wait_for_operation(operation)

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

  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}")
  begin
    # The instance must go first: its disks cannot be deleted while it
    # still holds them.
    destroy(state)
  ensure
    delete_created_disks
  end
  raise
end

#create_attached_disk(unique_disk_name, disk_config) ⇒ Google::Apis::ComputeV1::AttachedDisk

Creates a standalone persistent disk, waits for it to become ready, and returns a reference attaching it to the instance.

Parameters:

  • unique_disk_name (String)

    the disk's name

  • disk_config (Hash)

    the normalised disk configuration

Returns:

  • (Google::Apis::ComputeV1::AttachedDisk)

    the attachment



871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
# File 'lib/kitchen/driver/gce.rb', line 871

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]) if 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))
  created_disk_names << unique_disk_name
  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) ⇒ Array<Google::Apis::ComputeV1::AttachedDisk>

Builds every disk for the instance, creating standalone persistent disks up front where required. The boot disk is always listed first.

Parameters:

  • server_name (String)

    the instance name, used to derive disk names

Returns:

  • (Array<Google::Apis::ComputeV1::AttachedDisk>)

    the disks



770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
# File 'lib/kitchen/driver/gce.rb', line 770

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 local_ssd?(disk_config) || 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_configHash{Symbol => Hash}

Normalises whichever disk configuration style the user supplied into the canonical disks hash the rest of the driver consumes.

Deprecated single-disk options are converted to a one-entry disks hash; an explicit disks hash has defaults applied, is validated, and has a boot disk chosen when none was flagged. When neither is present a single default boot disk is configured.

Returns:

  • (Hash{Symbol => Hash})

    the normalised disk configuration, also written back to config[:disks]

Raises:

  • (RuntimeError)

    if a disk name, disk type or boot-disk arrangement is invalid



277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/kitchen/driver/gce.rb', line 277

def create_disks_config
  # These defaults cannot live in default_config: their absence is what
  # tells us which of the two configuration styles the user chose.
  config[:disks] =
    if old_disk_configuration_present?
      { disk1: legacy_disk_config }
    elsif new_disk_configuration_present?
      normalize_disks(config[:disks])
    else
      { disk1: DISK_DEFAULT_CONFIG.merge(boot: true) }
    end
end

#create_instance_object(server_name) ⇒ Google::Apis::ComputeV1::Instance

Assembles the full instance definition sent to the GCE API.

Parameters:

  • server_name (String)

    the instance name

Returns:

  • (Google::Apis::ComputeV1::Instance)

    the instance to create



734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
# File 'lib/kitchen/driver/gce.rb', line 734

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.guest_accelerators = instance_guest_accelerators
  inst_obj.           = 
  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) ⇒ Google::Apis::ComputeV1::AttachedDisk

Builds a disk created inline with the instance, from either the boot image, a custom image, or as local SSD scratch space.

Parameters:

  • unique_disk_name (String)

    the disk's name

  • disk_config (Hash)

    the normalised disk configuration

Returns:

  • (Google::Apis::ComputeV1::AttachedDisk)

    the disk



794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
# File 'lib/kitchen/driver/gce.rb', line 794

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

  if local_ssd?(disk_config)
    info("Creating a #{LOCAL_SSD_SIZE_GB} GB local ssd as scratch disk (https://cloud.google.com/compute/docs/disks/#localssds).")
    disk.type = "SCRATCH"
  elsif disk.boot
    params.disk_size_gb = disk_size_for_image(disk_config[:disk_size], image_name)
    info("Creating a #{params.disk_size_gb} GB boot disk named #{unique_disk_name} from image #{image_name}...")
    params.source_image = boot_disk_source_image
    params.disk_name    = unique_disk_name
  else
    params.disk_size_gb = disk_size_for_image(disk_config[:disk_size], disk_config[:custom_image])
    info("Creating a #{params.disk_size_gb} GB extra disk named #{unique_disk_name} from image #{disk_config[:custom_image]}...")
    params.source_image = image_url(disk_config[:custom_image])
    params.disk_name    = unique_disk_name
  end
  disk.initialize_params = params
  disk
end

#created_disk_namesArray<String>

Names of the standalone disks this driver created during the current action, tracked so they can be cleaned up if creation fails.

Returns:

  • (Array<String>)

    the created disk names



893
894
895
# File 'lib/kitchen/driver/gce.rb', line 893

def created_disk_names
  @created_disk_names ||= []
end

#delete_created_disksvoid

This method returns an undefined value.

Deletes every standalone disk created during a failed create, so a partial run does not leave billable disks behind.



901
902
903
904
# File 'lib/kitchen/driver/gce.rb', line 901

def delete_created_disks
  created_disk_names.each { |disk_name| delete_disk(disk_name) }
  created_disk_names.clear
end

#delete_disk(unique_disk_name) ⇒ void

This method returns an undefined value.

Deletes a standalone persistent disk, tolerating one that is already gone.

Parameters:

  • unique_disk_name (String)

    the disk's name



911
912
913
914
915
916
917
918
919
920
921
922
# File 'lib/kitchen/driver/gce.rb', line 911

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) ⇒ void

This method returns an undefined value.

Destroys the GCE instance recorded in the state file.

Does nothing when the state file records no server, or when the instance no longer exists in GCE.

Parameters:

  • state (Hash)

    the Test Kitchen state hash, mutated in place to remove :server_name, :hostname and :zone



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/kitchen/driver/gce.rb', line 231

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

Partial URL identifying a disk in the target project and zone.

Parameters:

  • unique_disk_name (String)

    the disk's name

Returns:

  • (String)

    the disk's self link



936
937
938
# File 'lib/kitchen/driver/gce.rb', line 936

def disk_self_link(unique_disk_name)
  "projects/#{project}/zones/#{zone}/disks/#{unique_disk_name}"
end

#disk_size_for_image(requested, image) ⇒ Integer?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The size to request for a disk cloned from an image.

GCE refuses to create a disk smaller than the image it is cloned from, and the driver's own 10 GB default is smaller than many stock images - every Windows image is 50 GB, and Rocky and CentOS are 20 GB. Rather than fail the run over a size the user never chose, raise the request to what the image needs and say so.

Parameters:

  • requested (Integer, nil)

    the configured size in gigabytes

  • image (String, nil)

    the image the disk is cloned from

Returns:

  • (Integer, nil)

    the size to request



833
834
835
836
837
838
839
840
# File 'lib/kitchen/driver/gce.rb', line 833

def disk_size_for_image(requested, image)
  image_size = image_disk_size_gb(image)
  return requested if image_size.nil? || (!requested.nil? && requested >= image_size)

  warn("Requested disk size of #{requested} GB is smaller than image #{image} " \
       "(#{image_size} GB) - creating a #{image_size} GB disk instead.")
  image_size
end

#disk_type_url_for(type) ⇒ String

Partial URL identifying a disk type in the target zone.

Parameters:

  • type (String)

    the disk type

Returns:

  • (String)

    the disk type URL



928
929
930
# File 'lib/kitchen/driver/gce.rb', line 928

def disk_type_url_for(type)
  "zones/#{zone}/diskTypes/#{type}"
end

#env_userString

The username recorded in instance metadata.

Returns:

  • (String)

    the current user, or "unknown"



1048
1049
1050
# File 'lib/kitchen/driver/gce.rb', line 1048

def env_user
  ENV["USER"] || "unknown"
end

#find_zoneString

Picks a random zone that is up in the configured region.

Returns:

  • (String)

    the chosen zone name

Raises:

  • (RuntimeError)

    if no zone in the region is available



674
675
676
677
678
679
# File 'lib/kitchen/driver/gce.rb', line 674

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_nameString

Builds a unique, GCE-legal instance name, falling back to a UUID when the Test Kitchen instance name would make it too long.

Returns:

  • (String)

    the instance name



754
755
756
757
758
759
760
761
762
763
# File 'lib/kitchen/driver/gce.rb', line 754

def generate_server_name
  name = config[:inst_name] || "tk-#{instance.name.downcase}-#{SecureRandom.hex(3)}"

  if name.length > MAX_INSTANCE_NAME_LENGTH
    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

#guest_acceleratorsArray<Hash>

The configured guest accelerators.

Returns:

  • (Array<Hash>)

    the accelerator configurations



974
975
976
# File 'lib/kitchen/driver/gce.rb', line 974

def guest_accelerators
  config[:guest_accelerators]
end

#guest_accelerators?Boolean

Whether any guest accelerators are attached.

Returns:

  • (Boolean)

    true if the instance has at least one accelerator



1127
1128
1129
# File 'lib/kitchen/driver/gce.rb', line 1127

def guest_accelerators?
  !Array(config[:guest_accelerators]).empty?
end

#image_disk_size_gb(image) ⇒ Integer?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The size, in gigabytes, of an image in the image project.

Memoised per image name, since the boot image is looked up more than once during a single action.

Parameters:

  • image (String, nil)

    the image name

Returns:

  • (Integer, nil)

    the image's size, or nil if it cannot be read



850
851
852
853
854
855
856
857
858
859
860
861
862
863
# File 'lib/kitchen/driver/gce.rb', line 850

def image_disk_size_gb(image)
  return if image.nil?

  @image_disk_sizes ||= {}
  return @image_disk_sizes[image] if @image_disk_sizes.key?(image)

  @image_disk_sizes[image] =
    begin
      connection.get_image(image_project, image).disk_size_gb
    rescue Google::Apis::ClientError => e
      debug("Unable to read the size of image #{image}: #{e.message}")
      nil
    end
end

#image_exist?(image = image_name) ⇒ Boolean

Whether an image exists in the image project.

Parameters:

  • image (String) (defaults to: image_name)

    the image name, defaulting to the configured one

Returns:

  • (Boolean)

    true if the image exists



593
594
595
# File 'lib/kitchen/driver/gce.rb', line 593

def image_exist?(image = image_name)
  check_api_call { connection.get_image(image_project, image) }
end

#image_nameString

Name of the boot image, resolved from the image family when only a family was configured.

Returns:

  • (String)

    the image name



616
617
618
# File 'lib/kitchen/driver/gce.rb', line 616

def image_name
  @image_name ||= config[:image_name] || image_name_for_family(config[:image_family])
end

#image_name_for_family(image_family) ⇒ String

Resolves the current image name for an image family.

Parameters:

  • image_family (String)

    the image family

Returns:

  • (String)

    the image name



959
960
961
962
# File 'lib/kitchen/driver/gce.rb', line 959

def image_name_for_family(image_family)
  image = connection.get_image_from_family(image_project, image_family)
  image.name
end

#image_projectString

Project searched for images, defaulting to the instance's own project.

Returns:

  • (String)

    the image project ID



623
624
625
# File 'lib/kitchen/driver/gce.rb', line 623

def image_project
  config[:image_project].nil? ? project : config[:image_project]
end

#image_url(image = image_name) ⇒ String?

URL of an image, provided it exists in the image project.

Parameters:

  • image (String) (defaults to: image_name)

    the image name, defaulting to the configured one

Returns:

  • (String, nil)

    the image URL, or nil if the image is missing



951
952
953
# File 'lib/kitchen/driver/gce.rb', line 951

def image_url(image = image_name)
  "projects/#{image_project}/global/images/#{image}" if image_exist?(image)
end

#instance_guest_acceleratorsArray<Google::Apis::ComputeV1::AcceleratorConfig>

Builds accelerator definitions for the instance, skipping any entry that does not name a type and defaulting the count to one.

Returns:

  • (Array<Google::Apis::ComputeV1::AcceleratorConfig>)

    the accelerators



982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
# File 'lib/kitchen/driver/gce.rb', line 982

def instance_guest_accelerators
  guest_accelerator_configs = []

  guest_accelerators.each do |guest_accelerator|
    next unless guest_accelerator.key?(:type)

    guest_accelerator_obj = Google::Apis::ComputeV1::AcceleratorConfig.new
    guest_accelerator_obj.accelerator_type = "zones/#{zone}/acceleratorTypes/#{guest_accelerator[:type]}"

    count = 1

    count = guest_accelerator[:count] if guest_accelerator.key?(:count)

    guest_accelerator_obj.accelerator_count = count

    guest_accelerator_configs << guest_accelerator_obj
  end

  guest_accelerator_configs
end

#instance_labelsHash

The configured instance labels.

Returns:

  • (Hash)

    the labels



1041
1042
1043
# File 'lib/kitchen/driver/gce.rb', line 1041

def instance_labels
  config[:labels]
end

#instance_metadataGoogle::Apis::ComputeV1::Metadata

The metadata in the form the GCE API expects.

Returns:

  • (Google::Apis::ComputeV1::Metadata)

    the metadata object



1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
# File 'lib/kitchen/driver/gce.rb', line 1027

def 
  Google::Apis::ComputeV1::Metadata.new.tap do ||
    .items = .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_interfacesArray<Google::Apis::ComputeV1::NetworkInterface>

Builds the instance's single network interface.

Returns:

  • (Array<Google::Apis::ComputeV1::NetworkInterface>)

    the interface



1055
1056
1057
1058
1059
1060
1061
1062
1063
# File 'lib/kitchen/driver/gce.rb', line 1055

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_schedulingGoogle::Apis::ComputeV1::Scheduling

The instance's scheduling options.

Returns:

  • (Google::Apis::ComputeV1::Scheduling)

    the scheduling options



1098
1099
1100
1101
1102
1103
1104
# File 'lib/kitchen/driver/gce.rb', line 1098

def instance_scheduling
  Google::Apis::ComputeV1::Scheduling.new.tap do |scheduling|
    scheduling.automatic_restart   = auto_restart?
    scheduling.preemptible         = preemptible?
    scheduling.on_host_maintenance = migrate_setting
  end
end

#instance_service_accountsArray<Google::Apis::ComputeV1::ServiceAccount>?

The service account and scopes attached to the instance.

Returns:

  • (Array<Google::Apis::ComputeV1::ServiceAccount>, nil)

    the service accounts, or nil when no scopes are configured



1152
1153
1154
1155
1156
1157
1158
1159
1160
# File 'lib/kitchen/driver/gce.rb', line 1152

def instance_service_accounts
  return if config[:service_account_scopes].nil? || config[:service_account_scopes].empty?

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

  Array()
end

#instance_tagsGoogle::Apis::ComputeV1::Tags

The configured network tags in the form the GCE API expects.

Returns:

  • (Google::Apis::ComputeV1::Tags)

    the tags object



1185
1186
1187
# File 'lib/kitchen/driver/gce.rb', line 1185

def instance_tags
  Google::Apis::ComputeV1::Tags.new.tap { |tag_obj| tag_obj.items = config[:tags] }
end

#interface_access_configsArray<Google::Apis::ComputeV1::AccessConfig>

The interface's external access configuration, omitted entirely when use_private_ip is set.

Returns:

  • (Array<Google::Apis::ComputeV1::AccessConfig>)

    the access configs



1085
1086
1087
1088
1089
1090
1091
1092
1093
# File 'lib/kitchen/driver/gce.rb', line 1085

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) ⇒ String

The IP address Test Kitchen should connect to, honouring use_private_ip.

Parameters:

  • server (Google::Apis::ComputeV1::Instance)

    the instance

Returns:

  • (String)

    the IP address



704
705
706
# File 'lib/kitchen/driver/gce.rb', line 704

def ip_address_for(server)
  config[:use_private_ip] ? private_ip_for(server) : public_ip_for(server)
end

#legacy_disk_configHash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds the single boot disk described by the deprecated autodelete_disk, disk_size and disk_type options.

Returns:

  • (Hash)

    the normalised boot disk configuration

Raises:

  • (RuntimeError)

    if the configured disk type is not valid



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/kitchen/driver/gce.rb', line 296

def legacy_disk_config
  disk_config = {
    boot: true,
    autodelete_disk: config.fetch(:autodelete_disk, DISK_DEFAULT_CONFIG[:autodelete_disk]),
    disk_size: config.fetch(:disk_size, DISK_DEFAULT_CONFIG[:disk_size]),
  }

  # Carry the key only when the user set it, so that an unset type stays
  # absent rather than becoming an explicit nil. See DISK_DEFAULT_CONFIG.
  disk_config[:disk_type] = config[:disk_type] if config[:disk_type]

  raise "Disk type #{disk_config[:disk_type]} is not valid" unless valid_disk_type?(disk_config[:disk_type])

  disk_config
end

#local_ssd?(disk_config) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether a disk configuration describes a local SSD.

Parameters:

  • disk_config (Hash)

    a disk configuration

Returns:

  • (Boolean)

    true if the disk type is local-ssd



399
400
401
# File 'lib/kitchen/driver/gce.rb', line 399

def local_ssd?(disk_config)
  disk_config[:disk_type] == LOCAL_SSD_TYPE
end

#machine_type_urlString

Partial URL identifying the machine type in the target zone.

Returns:

  • (String)

    the machine type URL



967
968
969
# File 'lib/kitchen/driver/gce.rb', line 967

def machine_type_url
  "zones/#{zone}/machineTypes/#{config[:machine_type]}"
end

#metadataHash{String => String}

The instance metadata, merging the driver's own keys over any the user configured and adding a WinRM bootstrap script for Windows guests.

Returns:

  • (Hash{String => String})

    the metadata



1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
# File 'lib/kitchen/driver/gce.rb', line 1007

def 
   = {
    "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]
    ["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")
      ["windows-startup-script-ps1"] += "winrm quickconfig -q"
    end
  end

  config[:metadata].merge()
end

#migrate_settingString

The host maintenance behaviour implied by #auto_migrate?.

Returns:

  • (String)

    "MIGRATE" or "TERMINATE"



1144
1145
1146
# File 'lib/kitchen/driver/gce.rb', line 1144

def migrate_setting
  auto_migrate? ? "MIGRATE" : "TERMINATE"
end

#nameString

Human-readable driver name shown in Test Kitchen output.

Returns:

  • (String)

    the driver's display name



163
164
165
# File 'lib/kitchen/driver/gce.rb', line 163

def name
  "Google Compute (GCE)"
end

#network_ipString?

The static internal IP to assign, if one was configured.

Returns:

  • (String, nil)

    the internal IP address



644
645
646
# File 'lib/kitchen/driver/gce.rb', line 644

def network_ip
  config[:network_ip]
end

#network_projectString

Project searched for networks, defaulting to the instance's own project.

Returns:

  • (String)

    the network project ID



637
638
639
# File 'lib/kitchen/driver/gce.rb', line 637

def network_project
  config[:network_project].nil? ? project : config[:network_project]
end

#network_urlString

Partial URL identifying the configured network.

Returns:

  • (String)

    the network URL



1068
1069
1070
# File 'lib/kitchen/driver/gce.rb', line 1068

def network_url
  "projects/#{network_project}/global/networks/#{config[:network]}"
end

#new_disk_configuration_present?Boolean

Whether the multi-disk disks option is configured.

Returns:

  • (Boolean)

    true if disks is set



261
262
263
# File 'lib/kitchen/driver/gce.rb', line 261

def new_disk_configuration_present?
  !config[:disks].nil?
end

#normalize_disk(disk_name, disk_config) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Applies the disk defaults to one disk entry and validates the result.

Parameters:

  • disk_name (String, Symbol)

    the disk's name, used in error messages

  • disk_config (Hash)

    the user-supplied configuration for this disk

Returns:

  • (Hash)

    the disk configuration with defaults applied

Raises:

  • (RuntimeError)

    if the disk type is invalid, a local SSD is marked bootable, or a size is given for a local SSD



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/kitchen/driver/gce.rb', line 341

def normalize_disk(disk_name, disk_config)
  normalized = DISK_DEFAULT_CONFIG.merge(disk_config)

  unless valid_disk_type?(normalized[:disk_type])
    raise "Disk type #{normalized[:disk_type]} for disk #{disk_name} is not valid"
  end

  return normalized unless local_ssd?(normalized)

  raise "Boot disk cannot be local SSD." if normalized[:boot]

  unless disk_config[:disk_size].nil?
    raise "#{disk_name}: Cannot use 'disk_size' with local SSD. They always have " \
          "#{LOCAL_SSD_SIZE_GB} GB (https://cloud.google.com/compute/docs/disks/#localssds)."
  end

  # disk_size defaults to 10 above, which must not be sent for a local SSD.
  normalized.merge(disk_size: nil)
end

#normalize_disks(disks) ⇒ Hash{Symbol => Hash}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Applies defaults to and validates every entry of a user-supplied disks hash, then ensures exactly one disk is marked bootable.

Builds a new hash rather than mutating the one being iterated, so that string keys from kitchen.yml can be symbolised safely.

Parameters:

  • disks (Hash)

    the raw disks configuration, keyed by disk name

Returns:

  • (Hash{Symbol => Hash})

    the normalised disk configuration

Raises:

  • (RuntimeError)

    if a disk name or type is invalid, or more than one boot disk is specified



323
324
325
326
327
328
329
330
331
# File 'lib/kitchen/driver/gce.rb', line 323

def normalize_disks(disks)
  normalized = disks.each_with_object({}) do |(disk_name, disk_config), memo|
    raise "Disk name invalid. Must match #{DISK_NAME_REGEX}." unless valid_disk_name?(disk_name)

    memo[disk_name.to_sym] = normalize_disk(disk_name, disk_config)
  end

  assign_boot_disk(normalized)
end

#old_disk_configuration_present?Boolean

Whether the deprecated single-boot-disk options are configured.

Returns:

  • (Boolean)

    true if any of autodelete_disk, disk_size or disk_type is set



254
255
256
# File 'lib/kitchen/driver/gce.rb', line 254

def old_disk_configuration_present?
  !config[:autodelete_disk].nil? || !config[:disk_size].nil? || !config[:disk_type].nil?
end

#operation_errors(operation_name) ⇒ Array<Google::Apis::ComputeV1::Operation::Error::Error>

The errors a zone operation reported, if any.

Parameters:

  • operation_name (String)

    the operation name

Returns:

  • (Array<Google::Apis::ComputeV1::Operation::Error::Error>)

    the errors



1281
1282
1283
1284
1285
1286
# File 'lib/kitchen/driver/gce.rb', line 1281

def operation_errors(operation_name)
  operation = zone_operation(operation_name)
  return [] if operation.error.nil?

  operation.error.errors
end

#preemptible?Boolean

Whether the instance should be preemptible.

Returns:

  • (Boolean)

    true if preemptible



1109
1110
1111
# File 'lib/kitchen/driver/gce.rb', line 1109

def preemptible?
  config[:preemptible] ? true : false
end

#private_ip_for(server) ⇒ String

The instance's internal IP address.

Parameters:

  • server (Google::Apis::ComputeV1::Instance)

    the instance

Returns:

  • (String)

    the private IP address

Raises:

  • (RuntimeError)

    if the instance has no network interface



713
714
715
716
717
# File 'lib/kitchen/driver/gce.rb', line 713

def private_ip_for(server)
  server.network_interfaces.first.network_ip
rescue NoMethodError
  raise "Unable to determine private IP for instance"
end

#projectString

The configured GCP project.

Returns:

  • (String)

    the project ID



608
609
610
# File 'lib/kitchen/driver/gce.rb', line 608

def project
  config[:project]
end

#public_ip_for(server) ⇒ String

The instance's external NAT IP address.

Parameters:

  • server (Google::Apis::ComputeV1::Instance)

    the instance

Returns:

  • (String)

    the public IP address

Raises:

  • (RuntimeError)

    if the instance has no external access config



724
725
726
727
728
# File 'lib/kitchen/driver/gce.rb', line 724

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_rateInteger

How long, in seconds, to sleep between status polls.

Returns:

  • (Integer)

    the poll interval



1199
1200
1201
# File 'lib/kitchen/driver/gce.rb', line 1199

def refresh_rate
  config[:refresh_rate]
end

#regionString

The target region, derived from the zone when not configured directly.

Returns:

  • (String)

    the region name



651
652
653
# File 'lib/kitchen/driver/gce.rb', line 651

def region
  config[:region].nil? ? region_for_zone : config[:region]
end

#region_for_zoneString

Looks up which region the target zone belongs to.

Returns:

  • (String)

    the region name



658
659
660
# File 'lib/kitchen/driver/gce.rb', line 658

def region_for_zone
  @region_for_zone ||= connection.get_zone(project, zone).region.split("/").last
end

#server_exist?(server_name) ⇒ Boolean

Whether a GCE instance exists in the target project and zone.

Parameters:

  • server_name (String)

    the instance name

Returns:

  • (Boolean)

    true if the instance exists



601
602
603
# File 'lib/kitchen/driver/gce.rb', line 601

def server_exist?(server_name)
  check_api_call { server_instance(server_name) }
end

#server_instance(server_name) ⇒ Google::Apis::ComputeV1::Instance

Fetches a GCE instance.

Parameters:

  • server_name (String)

    the instance name

Returns:

  • (Google::Apis::ComputeV1::Instance)

    the instance



695
696
697
# File 'lib/kitchen/driver/gce.rb', line 695

def server_instance(server_name)
  connection.get_instance(project, zone, server_name)
end

#service_account_scope_url(scope) ⇒ String

Expands a scope alias or bare scope name into a full OAuth 2.0 URL, passing through anything that is already one.

Parameters:

  • scope (String)

    the scope, alias or URL

Returns:

  • (String)

    the fully-qualified scope URL



1167
1168
1169
1170
1171
# File 'lib/kitchen/driver/gce.rb', line 1167

def (scope)
  return scope if scope.start_with?("https://www.googleapis.com/auth/")

  "https://www.googleapis.com/auth/#{translate_scope_alias(scope)}"
end

#subnet_projectString

Project searched for subnets, defaulting to the instance's own project.

Returns:

  • (String)

    the subnet project ID



630
631
632
# File 'lib/kitchen/driver/gce.rb', line 630

def subnet_project
  config[:subnet_project].nil? ? project : config[:subnet_project]
end

#subnet_urlString?

Partial URL identifying the configured subnet.

Returns:

  • (String, nil)

    the subnet URL, or nil when no subnet is set



1075
1076
1077
1078
1079
# File 'lib/kitchen/driver/gce.rb', line 1075

def subnet_url
  return unless config[:subnet]

  "projects/#{subnet_project}/regions/#{region}/subnetworks/#{config[:subnet]}"
end

#translate_scope_alias(scope_alias) ⇒ String

Translates a gcloud scope alias into its scope path, returning the input unchanged when it is not a known alias.

Parameters:

  • scope_alias (String)

    the alias to translate

Returns:

  • (String)

    the scope path



1178
1179
1180
# File 'lib/kitchen/driver/gce.rb', line 1178

def translate_scope_alias(scope_alias)
  SCOPE_ALIAS_MAP.fetch(scope_alias, scope_alias)
end

#update_windows_password(server_name) ⇒ void

This method returns an undefined value.

Resets the Windows password for the transport's user and stores it in the state file. A no-op for non-WinRM transports.

Parameters:

  • server_name (String)

    the GCE instance name

Raises:

  • (RuntimeError)

    if the in-guest agent could not reset the password

  • (Timeout::Error)

    if the agent does not respond in time



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/kitchen/driver/gce.rb', line 484

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] = WindowsPassword.new(
    self,
    instance_name: server_name,
    email: config[:email],
    username: username,
    timeout: config[:winpass_timeout]
  ).new_password

  info("Password reset complete on #{server_name}.")
end

#valid_disk_name?(disk_name) ⇒ Boolean

Whether a disk name matches DISK_NAME_REGEX in full.

Parameters:

  • disk_name (String, Symbol)

    the disk name to check

Returns:

  • (Boolean)

    true if the whole name matches the pattern



585
586
587
# File 'lib/kitchen/driver/gce.rb', line 585

def valid_disk_name?(disk_name)
  disk_name.to_s.match?(/\A#{DISK_NAME_REGEX}\z/)
end

#valid_disk_type?(disk_type) ⇒ Boolean

Whether a disk type exists in the target zone.

An unset type is valid: the driver sends no diskType at all and GCE substitutes the default for the instance's machine series.

Parameters:

  • disk_type (String, nil)

    the disk type to check

Returns:

  • (Boolean)

    true if the disk type is valid or unset



575
576
577
578
579
# File 'lib/kitchen/driver/gce.rb', line 575

def valid_disk_type?(disk_type)
  return true if disk_type.nil?

  check_api_call { connection.get_disk_type(project, zone, disk_type) }
end

#valid_machine_type?Boolean

Whether the configured machine type exists in the target zone.

Returns:

  • (Boolean)

    true if the machine type is valid



526
527
528
529
530
# File 'lib/kitchen/driver/gce.rb', line 526

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?Boolean

Whether the configured network exists in the network project.

Returns:

  • (Boolean)

    true if the network is valid



535
536
537
538
539
# File 'lib/kitchen/driver/gce.rb', line 535

def valid_network?
  return false if config[:network].nil?

  check_api_call { connection.get_network(network_project, config[:network]) }
end

#valid_project?Boolean

Whether the configured project exists and is reachable.

Returns:

  • (Boolean)

    true if the project is valid



519
520
521
# File 'lib/kitchen/driver/gce.rb', line 519

def valid_project?
  check_api_call { connection.get_project(project) }
end

#valid_region?Boolean

Whether the configured region exists in the project.

Returns:

  • (Boolean)

    true if the region is valid



562
563
564
565
566
# File 'lib/kitchen/driver/gce.rb', line 562

def valid_region?
  return false if config[:region].nil?

  check_api_call { connection.get_region(project, config[:region]) }
end

#valid_subnet?Boolean

Whether the configured subnet exists in the subnet project and region.

Returns:

  • (Boolean)

    true if the subnet is valid



544
545
546
547
548
# File 'lib/kitchen/driver/gce.rb', line 544

def valid_subnet?
  return false if config[:subnet].nil?

  check_api_call { connection.get_subnetwork(subnet_project, region, config[:subnet]) }
end

#valid_zone?Boolean

Whether the configured zone exists in the project.

Returns:

  • (Boolean)

    true if the zone is valid



553
554
555
556
557
# File 'lib/kitchen/driver/gce.rb', line 553

def valid_zone?
  return false if config[:zone].nil?

  check_api_call { connection.get_zone(project, config[:zone]) }
end

#validate!void

This method returns an undefined value.

Validates the driver configuration against the GCE API, raising on the first problem found and warning about ambiguous or deprecated settings.

Raises:

  • (RuntimeError)

    if any configured project, zone, region, machine type, network, subnet, image or disk setting is invalid



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/kitchen/driver/gce.rb', line 409

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(BUILTIN_ADMINISTRATOR_WARNING) if winrm_transport? && builtin_administrator?
  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-migrate disabled for instance with guest accelerators") if guest_accelerators? && 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) ⇒ void

This method returns an undefined value.

Waits for a zone operation to finish and raises if it reported errors.

Parameters:

  • operation (Google::Apis::ComputeV1::Operation)

    the operation

Raises:

  • (RuntimeError)

    if the operation completed with errors

  • (Timeout::Error)

    if the operation did not finish in time



1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
# File 'lib/kitchen/driver/gce.rb', line 1241

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_servervoid

This method returns an undefined value.

Waits until the suite's transport can reach the instance, destroying it if it never becomes reachable.

Raises:

  • (StandardError)

    if the server cannot be reached



1261
1262
1263
1264
1265
1266
1267
# File 'lib/kitchen/driver/gce.rb', line 1261

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, &block) ⇒ void

This method returns an undefined value.

Polls the yielded resource until it reports the requested status, logging each status change.

Parameters:

  • requested_status (String)

    the status to wait for

Yield Returns:

  • (#status)

    the resource to poll

Raises:

  • (Timeout::Error)

    if the status is not reached within #wait_time



1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
# File 'lib/kitchen/driver/gce.rb', line 1210

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_timeInteger

How long, in seconds, to wait for an operation or status change.

Returns:

  • (Integer)

    the wait timeout



1192
1193
1194
# File 'lib/kitchen/driver/gce.rb', line 1192

def wait_time
  config[:wait_time]
end

#winrm_transport?Boolean

Whether the suite's transport is WinRM, implying a Windows guest.

Returns:

  • (Boolean)

    true when the transport is WinRM



473
474
475
# File 'lib/kitchen/driver/gce.rb', line 473

def winrm_transport?
  instance.transport.name.casecmp("winrm") == 0
end

#zoneString

The target zone, taken from the state file or configuration, or chosen at random from the configured region.

Returns:

  • (String)

    the zone name



666
667
668
# File 'lib/kitchen/driver/gce.rb', line 666

def zone
  @zone ||= state[:zone] || config[:zone] || find_zone
end

#zone_operation(operation_name) ⇒ Google::Apis::ComputeV1::Operation

Fetches the current state of a zone operation.

Parameters:

  • operation_name (String)

    the operation name

Returns:

  • (Google::Apis::ComputeV1::Operation)

    the operation



1273
1274
1275
# File 'lib/kitchen/driver/gce.rb', line 1273

def zone_operation(operation_name)
  connection.get_zone_operation(project, zone, operation_name)
end

#zones_in_regionArray<Google::Apis::ComputeV1::Zone>

All zones in the configured region whose status is UP.

Returns:

  • (Array<Google::Apis::ComputeV1::Zone>)

    the available zones



684
685
686
687
688
689
# File 'lib/kitchen/driver/gce.rb', line 684

def zones_in_region
  connection.list_zones(project).items.select do |zone|
    zone.status == "UP" &&
      zone.region.split("/").last == region
  end
end