Class: Kitchen::Driver::Gce

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/driver/gce.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

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.

Returns:

  • (Hash) —

    the per-disk defaults

{
  autodelete_disk: true,
  disk_size: 10,
  disk_type: "pd-standard",
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#state ⇒ Hash

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



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

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

#authorization ⇒ Google::Auth::Credentials

Application default credentials scoped for Compute Engine.

Returns:

  • (Google::Auth::Credentials) —

    the resolved credentials



419
420
421
422
423
424
425
426
# File 'lib/kitchen/driver/gce.rb', line 419

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 when preemptible, which GCE does not allow to migrate.

Returns:

  • (Boolean) —

    true if live migration is enabled



1026
1027
1028
1029
1030
# File 'lib/kitchen/driver/gce.rb', line 1026

def auto_migrate?
  return false if preemptible?

  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



1036
1037
1038
1039
1040
# File 'lib/kitchen/driver/gce.rb', line 1036

def auto_restart?
  return false if preemptible?

  config[:auto_restart] ? true : false
end

#boot_disk_source_image ⇒ String?

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

Returns:

  • (String, nil) —

    the image URL, or nil if the image is missing



852
853
854
# File 'lib/kitchen/driver/gce.rb', line 852

def boot_disk_source_image
  @boot_disk_source ||= image_url
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



465
466
467
468
469
470
471
472
# File 'lib/kitchen/driver/gce.rb', line 465

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

#connection ⇒ Google::Apis::ComputeV1::ComputeService

Memoised, authorised Compute Engine API client.

Returns:

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

    the API client



403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/kitchen/driver/gce.rb', line 403

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



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/kitchen/driver/gce.rb', line 154

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



780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
# File 'lib/kitchen/driver/gce.rb', line 780

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



725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
# File 'lib/kitchen/driver/gce.rb', line 725

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_config ⇒ Hash{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



248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/kitchen/driver/gce.rb', line 248

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



689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/kitchen/driver/gce.rb', line 689

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



749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
# File 'lib/kitchen/driver/gce.rb', line 749

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 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
    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
    params.disk_name    = unique_disk_name
  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])
    params.disk_name    = unique_disk_name
  end
  disk.initialize_params = params
  disk
end

#created_disk_names ⇒ Array<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



802
803
804
# File 'lib/kitchen/driver/gce.rb', line 802

def created_disk_names
  @created_disk_names ||= []
end

#delete_created_disks ⇒ void

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.



810
811
812
813
# File 'lib/kitchen/driver/gce.rb', line 810

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



820
821
822
823
824
825
826
827
828
829
830
831
# File 'lib/kitchen/driver/gce.rb', line 820

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



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/kitchen/driver/gce.rb', line 202

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



845
846
847
# File 'lib/kitchen/driver/gce.rb', line 845

def disk_self_link(unique_disk_name)
  "projects/#{project}/zones/#{zone}/disks/#{unique_disk_name}"
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



837
838
839
# File 'lib/kitchen/driver/gce.rb', line 837

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

#env_user ⇒ String

The username recorded in instance metadata.

Returns:

  • (String) —

    the current user, or "unknown"



957
958
959
# File 'lib/kitchen/driver/gce.rb', line 957

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

#find_zone ⇒ String

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



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

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

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



709
710
711
712
713
714
715
716
717
718
# File 'lib/kitchen/driver/gce.rb', line 709

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_accelerators ⇒ Array<Hash>

The configured guest accelerators.

Returns:

  • (Array<Hash>) —

    the accelerator configurations



883
884
885
# File 'lib/kitchen/driver/gce.rb', line 883

def guest_accelerators
  config[:guest_accelerators]
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



548
549
550
# File 'lib/kitchen/driver/gce.rb', line 548

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

#image_name ⇒ String

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

Returns:

  • (String) —

    the image name



571
572
573
# File 'lib/kitchen/driver/gce.rb', line 571

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



868
869
870
871
# File 'lib/kitchen/driver/gce.rb', line 868

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

#image_project ⇒ String

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

Returns:

  • (String) —

    the image project ID



578
579
580
# File 'lib/kitchen/driver/gce.rb', line 578

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



860
861
862
# File 'lib/kitchen/driver/gce.rb', line 860

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

#instance_guest_accelerators ⇒ Array<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



891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
# File 'lib/kitchen/driver/gce.rb', line 891

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_labels ⇒ Hash

The configured instance labels.

Returns:

  • (Hash) —

    the labels



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

def instance_labels
  config[:labels]
end

#instance_metadata ⇒ Google::Apis::ComputeV1::Metadata

The metadata in the form the GCE API expects.

Returns:

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

    the metadata object



936
937
938
939
940
941
942
943
944
945
# File 'lib/kitchen/driver/gce.rb', line 936

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_interfaces ⇒ Array<Google::Apis::ComputeV1::NetworkInterface>

Builds the instance's single network interface.

Returns:

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

    the interface



964
965
966
967
968
969
970
971
972
# File 'lib/kitchen/driver/gce.rb', line 964

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

The instance's scheduling options.

Returns:

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

    the scheduling options



1007
1008
1009
1010
1011
1012
1013
# File 'lib/kitchen/driver/gce.rb', line 1007

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_accounts ⇒ Array<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



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

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_tags ⇒ Google::Apis::ComputeV1::Tags

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

Returns:

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

    the tags object



1086
1087
1088
# File 'lib/kitchen/driver/gce.rb', line 1086

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

#interface_access_configs ⇒ Array<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



994
995
996
997
998
999
1000
1001
1002
# File 'lib/kitchen/driver/gce.rb', line 994

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



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

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

#legacy_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.

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



267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/kitchen/driver/gce.rb', line 267

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]),
    disk_type: config.fetch(:disk_type, DISK_DEFAULT_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



367
368
369
# File 'lib/kitchen/driver/gce.rb', line 367

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

#machine_type_url ⇒ String

Partial URL identifying the machine type in the target zone.

Returns:

  • (String) —

    the machine type URL



876
877
878
# File 'lib/kitchen/driver/gce.rb', line 876

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

#metadata ⇒ Hash{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



916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
# File 'lib/kitchen/driver/gce.rb', line 916

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

The host maintenance behaviour implied by #auto_migrate?.

Returns:

  • (String) —

    "MIGRATE" or "TERMINATE"



1045
1046
1047
# File 'lib/kitchen/driver/gce.rb', line 1045

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

#name ⇒ String

Human-readable driver name shown in Test Kitchen output.

Returns:

  • (String) —

    the driver's display name



138
139
140
# File 'lib/kitchen/driver/gce.rb', line 138

def name
  "Google Compute (GCE)"
end

#network_ip ⇒ String?

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

Returns:

  • (String, nil) —

    the internal IP address



599
600
601
# File 'lib/kitchen/driver/gce.rb', line 599

def network_ip
  config[:network_ip]
end

#network_project ⇒ String

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

Returns:

  • (String) —

    the network project ID



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

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

#network_url ⇒ String

Partial URL identifying the configured network.

Returns:

  • (String) —

    the network URL



977
978
979
# File 'lib/kitchen/driver/gce.rb', line 977

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



232
233
234
# File 'lib/kitchen/driver/gce.rb', line 232

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



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/kitchen/driver/gce.rb', line 309

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



291
292
293
294
295
296
297
298
299
# File 'lib/kitchen/driver/gce.rb', line 291

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



225
226
227
# File 'lib/kitchen/driver/gce.rb', line 225

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



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

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



1018
1019
1020
# File 'lib/kitchen/driver/gce.rb', line 1018

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



668
669
670
671
672
# File 'lib/kitchen/driver/gce.rb', line 668

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

#project ⇒ String

The configured GCP project.

Returns:

  • (String) —

    the project ID



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

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



679
680
681
682
683
# File 'lib/kitchen/driver/gce.rb', line 679

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 ⇒ Integer

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

Returns:

  • (Integer) —

    the poll interval



1100
1101
1102
# File 'lib/kitchen/driver/gce.rb', line 1100

def refresh_rate
  config[:refresh_rate]
end

#region ⇒ String

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

Returns:

  • (String) —

    the region name



606
607
608
# File 'lib/kitchen/driver/gce.rb', line 606

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

#region_for_zone ⇒ String

Looks up which region the target zone belongs to.

Returns:

  • (String) —

    the region name



613
614
615
# File 'lib/kitchen/driver/gce.rb', line 613

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



556
557
558
# File 'lib/kitchen/driver/gce.rb', line 556

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



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

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



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

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

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

#subnet_project ⇒ String

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

Returns:

  • (String) —

    the subnet project ID



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

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

#subnet_url ⇒ String?

Partial URL identifying the configured subnet.

Returns:

  • (String, nil) —

    the subnet URL, or nil when no subnet is set



984
985
986
987
988
# File 'lib/kitchen/driver/gce.rb', line 984

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



1079
1080
1081
# File 'lib/kitchen/driver/gce.rb', line 1079

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



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/kitchen/driver/gce.rb', line 440

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}...")

  opts = {
    project: project,
    zone: zone,
    instance_name: server_name,
    email: config[:email],
    username: username,
  }
  opts[:timeout] = config[:winpass_timeout] unless config[:winpass_timeout].nil?
  state[:password] = GoogleComputeWindowsPassword.new(**opts).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



540
541
542
# File 'lib/kitchen/driver/gce.rb', line 540

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.

Parameters:

  • disk_type (String, nil) —

    the disk type to check

Returns:

  • (Boolean) —

    true if the disk type is valid



530
531
532
533
534
# File 'lib/kitchen/driver/gce.rb', line 530

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

Whether the configured machine type exists in the target zone.

Returns:

  • (Boolean) —

    true if the machine type is valid



484
485
486
487
488
# File 'lib/kitchen/driver/gce.rb', line 484

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



493
494
495
496
497
# File 'lib/kitchen/driver/gce.rb', line 493

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



477
478
479
# File 'lib/kitchen/driver/gce.rb', line 477

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



520
521
522
523
524
# File 'lib/kitchen/driver/gce.rb', line 520

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



502
503
504
505
506
# File 'lib/kitchen/driver/gce.rb', line 502

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



511
512
513
514
515
# File 'lib/kitchen/driver/gce.rb', line 511

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



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/kitchen/driver/gce.rb', line 377

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



1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
# File 'lib/kitchen/driver/gce.rb', line 1142

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

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



1162
1163
1164
1165
1166
1167
1168
# File 'lib/kitchen/driver/gce.rb', line 1162

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



1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
# File 'lib/kitchen/driver/gce.rb', line 1111

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 ⇒ Integer

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

Returns:

  • (Integer) —

    the wait timeout



1093
1094
1095
# File 'lib/kitchen/driver/gce.rb', line 1093

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



431
432
433
# File 'lib/kitchen/driver/gce.rb', line 431

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

#zone ⇒ String

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

Returns:

  • (String) —

    the zone name



621
622
623
# File 'lib/kitchen/driver/gce.rb', line 621

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



1174
1175
1176
# File 'lib/kitchen/driver/gce.rb', line 1174

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

#zones_in_region ⇒ Array<Google::Apis::ComputeV1::Zone>

All zones in the configured region whose status is UP.

Returns:

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

    the available zones



639
640
641
642
643
644
# File 'lib/kitchen/driver/gce.rb', line 639

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