Class: Kitchen::Driver::Gce

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

Overview

Google Compute Engine driver for Test Kitchen

Author:

Constant Summary collapse

SCOPE_ALIAS_MAP =
{
  "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",
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#stateObject

Returns the value of attribute state.



32
33
34
# File 'lib/kitchen/driver/gce.rb', line 32

def state
  @state
end

Instance Method Details

#authorizationObject



179
180
181
182
183
184
185
186
# File 'lib/kitchen/driver/gce.rb', line 179

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

Returns:

  • (Boolean)


455
456
457
# File 'lib/kitchen/driver/gce.rb', line 455

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

#auto_restart?Boolean

Returns:

  • (Boolean)


459
460
461
# File 'lib/kitchen/driver/gce.rb', line 459

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

#boot_disk(server_name) ⇒ Object



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

def boot_disk(server_name)
  disk   = Google::Apis::ComputeV1::AttachedDisk.new
  params = Google::Apis::ComputeV1::AttachedDiskInitializeParams.new

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

  disk.initialize_params = params
  disk
end

#boot_disk_source_imageObject



375
376
377
# File 'lib/kitchen/driver/gce.rb', line 375

def boot_disk_source_image
  @boot_disk_source ||= image_url
end

#check_api_call(&block) ⇒ Object



210
211
212
213
214
215
216
217
# File 'lib/kitchen/driver/gce.rb', line 210

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

#connectionObject



166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/kitchen/driver/gce.rb', line 166

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



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/kitchen/driver/gce.rb', line 92

def create(state)
  @state = state
  return if state[:server_name]

  validate!

  server_name = generate_server_name

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

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

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

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

  update_windows_password(server_name)

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

  info("GCE instance <#{server_name}> created and ready.")
rescue => e
  error("Error encountered during server creation: #{e.class}: #{e.message}")
  destroy(state)
  raise
end

#create_instance_object(server_name) ⇒ Object



327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/kitchen/driver/gce.rb', line 327

def create_instance_object(server_name)
  inst_obj                    = Google::Apis::ComputeV1::Instance.new
  inst_obj.name               = server_name
  inst_obj.disks              = [boot_disk(server_name)]
  inst_obj.machine_type       = machine_type_url
  inst_obj.           = 
  inst_obj.network_interfaces = instance_network_interfaces
  inst_obj.scheduling         = instance_scheduling
  inst_obj.service_accounts   = instance_service_accounts unless instance_service_accounts.nil?
  inst_obj.tags               = instance_tags

  inst_obj
end

#destroy(state) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/kitchen/driver/gce.rb', line 125

def destroy(state)
  @state      = state
  server_name = state[:server_name]
  return if server_name.nil?

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

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

  state.delete(:server_name)
  state.delete(:hostname)
  state.delete(:zone)
end

#disk_type_url_for(type) ⇒ Object



371
372
373
# File 'lib/kitchen/driver/gce.rb', line 371

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

#env_userObject



411
412
413
# File 'lib/kitchen/driver/gce.rb', line 411

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

#find_zoneObject



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

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_nameObject



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

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

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

  name.gsub(/([^-a-z0-9])/, "-")
end

#image_exist?Boolean

Returns:

  • (Boolean)


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

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

#image_nameObject



265
266
267
# File 'lib/kitchen/driver/gce.rb', line 265

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

#image_name_for_family(image_family) ⇒ Object



383
384
385
386
# File 'lib/kitchen/driver/gce.rb', line 383

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

#image_projectObject



269
270
271
# File 'lib/kitchen/driver/gce.rb', line 269

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

#image_urlObject



379
380
381
# File 'lib/kitchen/driver/gce.rb', line 379

def image_url
  return "projects/#{image_project}/global/images/#{image_name}" if image_exist?
end

#instance_metadataObject



400
401
402
403
404
405
406
407
408
409
# File 'lib/kitchen/driver/gce.rb', line 400

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_interfacesObject



415
416
417
418
419
420
421
422
# File 'lib/kitchen/driver/gce.rb', line 415

def instance_network_interfaces
  interface                = Google::Apis::ComputeV1::NetworkInterface.new
  interface.network        = network_url if config[:subnet_project].nil?
  interface.subnetwork     = subnet_url if subnet_url
  interface.access_configs = interface_access_configs

  Array(interface)
end

#instance_schedulingObject



443
444
445
446
447
448
449
# File 'lib/kitchen/driver/gce.rb', line 443

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

#instance_service_accountsObject



467
468
469
470
471
472
473
474
475
# File 'lib/kitchen/driver/gce.rb', line 467

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_tagsObject



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

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

#interface_access_configsObject



433
434
435
436
437
438
439
440
441
# File 'lib/kitchen/driver/gce.rb', line 433

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



311
312
313
# File 'lib/kitchen/driver/gce.rb', line 311

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

#machine_type_urlObject



388
389
390
# File 'lib/kitchen/driver/gce.rb', line 388

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

#metadataObject



392
393
394
395
396
397
398
# File 'lib/kitchen/driver/gce.rb', line 392

def 
  config[:metadata].merge({
                              "created-by"            => "test-kitchen",
                              "test-kitchen-instance" => instance.name,
                              "test-kitchen-user"     => env_user,
                          })
end

#migrate_settingObject



463
464
465
# File 'lib/kitchen/driver/gce.rb', line 463

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

#nameObject



88
89
90
# File 'lib/kitchen/driver/gce.rb', line 88

def name
  "Google Compute (GCE)"
end

#network_projectObject



277
278
279
# File 'lib/kitchen/driver/gce.rb', line 277

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

#network_urlObject



424
425
426
# File 'lib/kitchen/driver/gce.rb', line 424

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

#operation_errors(operation_name) ⇒ Object



550
551
552
553
554
555
# File 'lib/kitchen/driver/gce.rb', line 550

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

  operation.error.errors
end

#preemptible?Boolean

Returns:

  • (Boolean)


451
452
453
# File 'lib/kitchen/driver/gce.rb', line 451

def preemptible?
  config[:preemptible]
end

#private_ip_for(server) ⇒ Object



315
316
317
318
319
# File 'lib/kitchen/driver/gce.rb', line 315

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

#projectObject



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

def project
  config[:project]
end

#public_ip_for(server) ⇒ Object



321
322
323
324
325
# File 'lib/kitchen/driver/gce.rb', line 321

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_rateObject



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

def refresh_rate
  config[:refresh_rate]
end

#regionObject



281
282
283
# File 'lib/kitchen/driver/gce.rb', line 281

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

#region_for_zoneObject



285
286
287
# File 'lib/kitchen/driver/gce.rb', line 285

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

#server_exist?(server_name) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#server_instance(server_name) ⇒ Object



307
308
309
# File 'lib/kitchen/driver/gce.rb', line 307

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

#service_account_scope_url(scope) ⇒ Object



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

def (scope)
  return scope if scope.start_with?("https://www.googleapis.com/auth/")
  "https://www.googleapis.com/auth/#{translate_scope_alias(scope)}"
end

#subnet_projectObject



273
274
275
# File 'lib/kitchen/driver/gce.rb', line 273

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

#subnet_urlObject



428
429
430
431
# File 'lib/kitchen/driver/gce.rb', line 428

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

#translate_scope_alias(scope_alias) ⇒ Object



482
483
484
# File 'lib/kitchen/driver/gce.rb', line 482

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

#update_windows_password(server_name) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/kitchen/driver/gce.rb', line 192

def update_windows_password(server_name)
  return unless winrm_transport?

  username = instance.transport[:username]

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

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

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

#valid_disk_type?Boolean

Returns:

  • (Boolean)


248
249
250
251
# File 'lib/kitchen/driver/gce.rb', line 248

def valid_disk_type?
  return false if config[:disk_type].nil?
  check_api_call { connection.get_disk_type(project, zone, config[:disk_type]) }
end

#valid_machine_type?Boolean

Returns:

  • (Boolean)


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

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

Returns:

  • (Boolean)


228
229
230
231
# File 'lib/kitchen/driver/gce.rb', line 228

def valid_network?
  return false if config[:network].nil?
  check_api_call { connection.get_network(network_project, config[:network]) }
end

#valid_project?Boolean

Returns:

  • (Boolean)


219
220
221
# File 'lib/kitchen/driver/gce.rb', line 219

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

#valid_region?Boolean

Returns:

  • (Boolean)


243
244
245
246
# File 'lib/kitchen/driver/gce.rb', line 243

def valid_region?
  return false if config[:region].nil?
  check_api_call { connection.get_region(project, config[:region]) }
end

#valid_subnet?Boolean

Returns:

  • (Boolean)


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

def valid_subnet?
  return false if config[:subnet].nil?
  check_api_call { connection.get_subnetwork(subnet_project, region, config[:subnet]) }
end

#valid_zone?Boolean

Returns:

  • (Boolean)


238
239
240
241
# File 'lib/kitchen/driver/gce.rb', line 238

def valid_zone?
  return false if config[:zone].nil?
  check_api_call { connection.get_zone(project, config[:zone]) }
end

#validate!Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/kitchen/driver/gce.rb', line 144

def validate!
  raise "Project #{config[:project]} is not a valid project" unless valid_project?
  raise "Either zone or region must be specified" unless config[:zone] || config[:region]
  raise "'any' is no longer a valid region" if config[:region] == "any"
  raise "Zone #{config[:zone]} is not a valid zone" if config[:zone] && !valid_zone?
  raise "Region #{config[:region]} is not a valid region" if config[:region] && !valid_region?
  raise "Machine type #{config[:machine_type]} is not valid" unless valid_machine_type?
  raise "Disk type #{config[:disk_type]} is not valid" unless valid_disk_type?
  raise "Either image family or name must be specified" unless config[:image_family] || config[:image_name]
  raise "Disk image #{config[:image_name]} is not valid - check your image name and image project" if boot_disk_source_image.nil?
  raise "Network #{config[:network]} is not valid" unless valid_network?
  raise "Subnet #{config[:subnet]} is not valid" if config[:subnet] && !valid_subnet?
  raise "Email address of GCE user is not set" if winrm_transport? && config[:email].nil?

  warn("Both zone and region specified - region will be ignored.") if config[:zone] && config[:region]
  warn("Both image family and name specified - image family will be ignored") if config[:image_family] && config[:image_name]
  warn("Image project not specified - searching current project only") unless config[:image_project]
  warn("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]
end

#wait_for_operation(operation) ⇒ Object



523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'lib/kitchen/driver/gce.rb', line 523

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_serverObject



538
539
540
541
542
543
544
# File 'lib/kitchen/driver/gce.rb', line 538

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



498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/kitchen/driver/gce.rb', line 498

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_timeObject



490
491
492
# File 'lib/kitchen/driver/gce.rb', line 490

def wait_time
  config[:wait_time]
end

#winrm_transport?Boolean

Returns:

  • (Boolean)


188
189
190
# File 'lib/kitchen/driver/gce.rb', line 188

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

#zoneObject



289
290
291
# File 'lib/kitchen/driver/gce.rb', line 289

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

#zone_operation(operation_name) ⇒ Object



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

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

#zones_in_regionObject



300
301
302
303
304
305
# File 'lib/kitchen/driver/gce.rb', line 300

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