Class: Kitchen::Driver::SfmcGoogle

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/driver/sfmc_google.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/sfmc_google.rb', line 32

def state
  @state
end

Instance Method Details

#authorizationObject



176
177
178
179
180
181
182
183
# File 'lib/kitchen/driver/sfmc_google.rb', line 176

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)


443
444
445
# File 'lib/kitchen/driver/sfmc_google.rb', line 443

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

#auto_restart?Boolean

Returns:

  • (Boolean)


447
448
449
# File 'lib/kitchen/driver/sfmc_google.rb', line 447

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

#boot_disk(server_name) ⇒ Object



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

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



360
361
362
# File 'lib/kitchen/driver/sfmc_google.rb', line 360

def boot_disk_source_image
  @boot_disk_source ||= image_url
end

#check_api_call(&block) ⇒ Object



207
208
209
210
211
212
213
214
# File 'lib/kitchen/driver/sfmc_google.rb', line 207

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

#connectionObject



163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/kitchen/driver/sfmc_google.rb', line 163

def connection
  return @connection unless @connection.nil?

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

  @connection
end

#create(state) ⇒ Object



90
91
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
# File 'lib/kitchen/driver/sfmc_google.rb', line 90

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



316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/kitchen/driver/sfmc_google.rb', line 316

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



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

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



356
357
358
# File 'lib/kitchen/driver/sfmc_google.rb', line 356

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

#env_userObject



398
399
400
# File 'lib/kitchen/driver/sfmc_google.rb', line 398

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

#find_zoneObject



282
283
284
285
286
287
# File 'lib/kitchen/driver/sfmc_google.rb', line 282

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



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

def generate_server_name
  name = "tk-#{instance.name.downcase}-#{SecureRandom.hex(3)}"

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

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

#image_exist?Boolean

Returns:

  • (Boolean)


250
251
252
# File 'lib/kitchen/driver/sfmc_google.rb', line 250

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

#image_nameObject



262
263
264
# File 'lib/kitchen/driver/sfmc_google.rb', line 262

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

#image_name_for_family(image_family) ⇒ Object



368
369
370
371
# File 'lib/kitchen/driver/sfmc_google.rb', line 368

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

#image_projectObject



266
267
268
# File 'lib/kitchen/driver/sfmc_google.rb', line 266

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

#image_urlObject



364
365
366
# File 'lib/kitchen/driver/sfmc_google.rb', line 364

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

#instance_metadataObject



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

def 
   = {
    "created-by"            => "test-kitchen",
    "test-kitchen-instance" => instance.name,
    "test-kitchen-user"     => env_user,
  }

  if config[:custom_metadata]
    puts config[:custom_metadata]
  end

  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
        item.value = v
      end
    end
  end
end

#instance_network_interfacesObject



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

def instance_network_interfaces
  interface                = Google::Apis::ComputeV1::NetworkInterface.new
  interface.network        = network_url
  interface.subnetwork     = subnet_url if subnet_url
  interface.access_configs = interface_access_configs

  Array(interface)
end

#instance_schedulingObject



431
432
433
434
435
436
437
# File 'lib/kitchen/driver/sfmc_google.rb', line 431

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



455
456
457
458
459
460
461
462
463
# File 'lib/kitchen/driver/sfmc_google.rb', line 455

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



474
475
476
# File 'lib/kitchen/driver/sfmc_google.rb', line 474

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

#interface_access_configsObject



421
422
423
424
425
426
427
428
429
# File 'lib/kitchen/driver/sfmc_google.rb', line 421

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



300
301
302
# File 'lib/kitchen/driver/sfmc_google.rb', line 300

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

#machine_type_urlObject



373
374
375
# File 'lib/kitchen/driver/sfmc_google.rb', line 373

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

#migrate_settingObject



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

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

#nameObject



86
87
88
# File 'lib/kitchen/driver/sfmc_google.rb', line 86

def name
  "Google Compute (GCE)"
end

#network_urlObject



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

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

#operation_errors(operation_name) ⇒ Object



540
541
542
543
544
545
# File 'lib/kitchen/driver/sfmc_google.rb', line 540

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

  operation.error.errors
end

#preemptible?Boolean

Returns:

  • (Boolean)


439
440
441
# File 'lib/kitchen/driver/sfmc_google.rb', line 439

def preemptible?
  config[:preemptible]
end

#private_ip_for(server) ⇒ Object



304
305
306
307
308
# File 'lib/kitchen/driver/sfmc_google.rb', line 304

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

#projectObject



258
259
260
# File 'lib/kitchen/driver/sfmc_google.rb', line 258

def project
  config[:project]
end

#public_ip_for(server) ⇒ Object



310
311
312
313
314
# File 'lib/kitchen/driver/sfmc_google.rb', line 310

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



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

def refresh_rate
  config[:refresh_rate]
end

#regionObject



270
271
272
# File 'lib/kitchen/driver/sfmc_google.rb', line 270

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

#region_for_zoneObject



274
275
276
# File 'lib/kitchen/driver/sfmc_google.rb', line 274

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

#server_exist?(server_name) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#server_instance(server_name) ⇒ Object



296
297
298
# File 'lib/kitchen/driver/sfmc_google.rb', line 296

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

#service_account_scope_url(scope) ⇒ Object



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

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

#subnet_urlObject



415
416
417
418
419
# File 'lib/kitchen/driver/sfmc_google.rb', line 415

def subnet_url
  return unless config[:subnet]

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

#translate_scope_alias(scope_alias) ⇒ Object



470
471
472
# File 'lib/kitchen/driver/sfmc_google.rb', line 470

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

#update_windows_password(server_name) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/kitchen/driver/sfmc_google.rb', line 189

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)


245
246
247
248
# File 'lib/kitchen/driver/sfmc_google.rb', line 245

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)


220
221
222
223
# File 'lib/kitchen/driver/sfmc_google.rb', line 220

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)


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

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

#valid_project?Boolean

Returns:

  • (Boolean)


216
217
218
# File 'lib/kitchen/driver/sfmc_google.rb', line 216

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

#valid_region?Boolean

Returns:

  • (Boolean)


240
241
242
243
# File 'lib/kitchen/driver/sfmc_google.rb', line 240

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

#valid_subnet?Boolean

Returns:

  • (Boolean)


230
231
232
233
# File 'lib/kitchen/driver/sfmc_google.rb', line 230

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

#valid_zone?Boolean

Returns:

  • (Boolean)


235
236
237
238
# File 'lib/kitchen/driver/sfmc_google.rb', line 235

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

#validate!Object



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

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

  warn("Both zone and region specified - region will be ignored.") if config[:zone] && config[:region]
  warn("Both image family and name specified - image family will be ignored") if config[:image_family] && config[:image_name]
  warn("Image project not specified - searching current project only") unless config[:image_project]
  warn("Auto-migrate disabled for preemptible instance") if preemptible? && config[:auto_migrate]
  warn("Auto-restart disabled for preemptible instance") if preemptible? && config[:auto_restart]
end

#wait_for_operation(operation) ⇒ Object



511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/kitchen/driver/sfmc_google.rb', line 511

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



526
527
528
529
530
531
532
533
534
# File 'lib/kitchen/driver/sfmc_google.rb', line 526

def wait_for_server
  begin
    instance.transport.connection(state).wait_until_ready
  rescue
    error("Server not reachable. Destroying server...")
    destroy(state)
    raise
  end
end

#wait_for_status(requested_status, &block) ⇒ Object



486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/kitchen/driver/sfmc_google.rb', line 486

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



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

def wait_time
  config[:wait_time]
end

#winrm_transport?Boolean

Returns:

  • (Boolean)


185
186
187
# File 'lib/kitchen/driver/sfmc_google.rb', line 185

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

#zoneObject



278
279
280
# File 'lib/kitchen/driver/sfmc_google.rb', line 278

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

#zone_operation(operation_name) ⇒ Object



536
537
538
# File 'lib/kitchen/driver/sfmc_google.rb', line 536

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

#zones_in_regionObject



289
290
291
292
293
294
# File 'lib/kitchen/driver/sfmc_google.rb', line 289

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