vmpooler-provider-gce 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1f22f8c5aea6e2a9f2a6cb2d635c3433ccb90c983919d3a33b30a41cb1ab80a
4
+ data.tar.gz: 84310c0950ebf6ba4fd87457e8750aaf689a64eb6b79f4c1490878294e27785a
5
+ SHA512:
6
+ metadata.gz: 24d390125d902e57a1ef0b48e53b207b386e97cb0439f8a411cf0a9eb64b754ce31842c96fa66a51601590e47e4548613844dac3d8515e22068713c10795c128
7
+ data.tar.gz: 8f9166867e5258c310c8b1f2ab0e2d51bb32d2d93dff85b4b96e5a69dc9d8ae8adc07c769ad1e3987019b03a94552e3709cdfc3952596b01a6344436a460fad9
@@ -0,0 +1,739 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'googleauth'
4
+ require 'google/apis/compute_v1'
5
+ require 'google/cloud/dns'
6
+ require 'bigdecimal'
7
+ require 'bigdecimal/util'
8
+ require 'vmpooler/providers/base'
9
+
10
+ module Vmpooler
11
+ class PoolManager
12
+ class Provider
13
+ # This class represent a GCE provider to CRUD resources in a gce cloud.
14
+ class Gce < Vmpooler::PoolManager::Provider::Base
15
+ # The connection_pool method is normally used only for testing
16
+ attr_reader :connection_pool
17
+
18
+ def initialize(config, logger, metrics, redis_connection_pool, name, options)
19
+ super(config, logger, metrics, redis_connection_pool, name, options)
20
+
21
+ task_limit = global_config[:config].nil? || global_config[:config]['task_limit'].nil? ? 10 : global_config[:config]['task_limit'].to_i
22
+ # The default connection pool size is:
23
+ # Whatever is biggest from:
24
+ # - How many pools this provider services
25
+ # - Maximum number of cloning tasks allowed
26
+ # - Need at least 2 connections so that a pool can have inventory functions performed while cloning etc.
27
+ default_connpool_size = [provided_pools.count, task_limit, 2].max
28
+ connpool_size = provider_config['connection_pool_size'].nil? ? default_connpool_size : provider_config['connection_pool_size'].to_i
29
+ # The default connection pool timeout should be quite large - 60 seconds
30
+ connpool_timeout = provider_config['connection_pool_timeout'].nil? ? 60 : provider_config['connection_pool_timeout'].to_i
31
+ logger.log('d', "[#{name}] ConnPool - Creating a connection pool of size #{connpool_size} with timeout #{connpool_timeout}")
32
+ @connection_pool = Vmpooler::PoolManager::GenericConnectionPool.new(
33
+ metrics: metrics,
34
+ connpool_type: 'provider_connection_pool',
35
+ connpool_provider: name,
36
+ size: connpool_size,
37
+ timeout: connpool_timeout
38
+ ) do
39
+ logger.log('d', "[#{name}] Connection Pool - Creating a connection object")
40
+ # Need to wrap the vSphere connection object in another object. The generic connection pooler will preserve
41
+ # the object reference for the connection, which means it cannot "reconnect" by creating an entirely new connection
42
+ # object. Instead by wrapping it in a Hash, the Hash object reference itself never changes but the content of the
43
+ # Hash can change, and is preserved across invocations.
44
+ new_conn = connect_to_gce
45
+ { connection: new_conn }
46
+ end
47
+ @redis = redis_connection_pool
48
+ end
49
+
50
+ # name of the provider class
51
+ def name
52
+ 'gce'
53
+ end
54
+
55
+ def connection
56
+ @connection_pool.with_metrics do |pool_object|
57
+ return ensured_gce_connection(pool_object)
58
+ end
59
+ end
60
+
61
+ def dns
62
+ @dns ||= Google::Cloud::Dns.new(project_id: project)
63
+ @dns
64
+ end
65
+
66
+ # main configuration options
67
+ def project
68
+ provider_config['project']
69
+ end
70
+
71
+ def network_name
72
+ provider_config['network_name']
73
+ end
74
+
75
+ def subnetwork_name(pool_name)
76
+ return pool_config(pool_name)['subnetwork_name'] if pool_config(pool_name)['subnetwork_name']
77
+ end
78
+
79
+ # main configuration options, overridable for each pool
80
+ def zone(pool_name)
81
+ return pool_config(pool_name)['zone'] if pool_config(pool_name)['zone']
82
+ return provider_config['zone'] if provider_config['zone']
83
+ end
84
+
85
+ def machine_type(pool_name)
86
+ return pool_config(pool_name)['machine_type'] if pool_config(pool_name)['machine_type']
87
+ return provider_config['machine_type'] if provider_config['machine_type']
88
+ end
89
+
90
+ def domain
91
+ provider_config['domain']
92
+ end
93
+
94
+ def dns_zone_resource_name
95
+ provider_config['dns_zone_resource_name']
96
+ end
97
+
98
+ # Base methods that are implemented:
99
+
100
+ # vms_in_pool lists all the VM names in a pool, which is based on the VMs
101
+ # having a label "pool" that match a pool config name.
102
+ # inputs
103
+ # [String] pool_name : Name of the pool
104
+ # returns
105
+ # empty array [] if no VMs found in the pool
106
+ # [Array]
107
+ # [Hashtable]
108
+ # [String] name : the name of the VM instance (unique for whole project)
109
+ def vms_in_pool(pool_name)
110
+ debug_logger('vms_in_pool')
111
+ vms = []
112
+ pool = pool_config(pool_name)
113
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
114
+
115
+ zone = zone(pool_name)
116
+ filter = "(labels.pool = #{pool_name})"
117
+ instance_list = connection.list_instances(project, zone, filter: filter)
118
+
119
+ return vms if instance_list.items.nil?
120
+
121
+ instance_list.items.each do |vm|
122
+ vms << { 'name' => vm.name }
123
+ end
124
+ debug_logger(vms)
125
+ vms
126
+ end
127
+
128
+ # inputs
129
+ # [String] pool_name : Name of the pool
130
+ # [String] vm_name : Name of the VM to find
131
+ # returns
132
+ # nil if VM doesn't exist
133
+ # [Hastable] of the VM
134
+ # [String] name : The name of the resource, provided by the client when initially creating the resource
135
+ # [String] hostname : Specifies the hostname of the instance. The specified hostname must be RFC1035 compliant. If hostname is not specified,
136
+ # the default hostname is [ INSTANCE_NAME].c.[PROJECT_ID].internal when using the global DNS, and
137
+ # [ INSTANCE_NAME].[ZONE].c.[PROJECT_ID].internal when using zonal DNS
138
+ # [String] template : This is the name of template
139
+ # [String] poolname : Name of the pool the VM as per labels
140
+ # [Time] boottime : Time when the VM was created/booted
141
+ # [String] status : One of the following values: PROVISIONING, STAGING, RUNNING, STOPPING, SUSPENDING, SUSPENDED, REPAIRING, and TERMINATED
142
+ # [String] zone : URL of the zone where the instance resides.
143
+ # [String] machine_type : Full or partial URL of the machine type resource to use for this instance, in the format: zones/zone/machineTypes/machine-type.
144
+ def get_vm(pool_name, vm_name)
145
+ debug_logger('get_vm')
146
+ vm_hash = nil
147
+ begin
148
+ vm_object = connection.get_instance(project, zone(pool_name), vm_name)
149
+ rescue ::Google::Apis::ClientError => e
150
+ raise e unless e.status_code == 404
151
+
152
+ # swallow the ClientError error 404 and return nil when the VM was not found
153
+ return nil
154
+ end
155
+
156
+ return vm_hash if vm_object.nil?
157
+
158
+ vm_hash = generate_vm_hash(vm_object, pool_name)
159
+ debug_logger("vm_hash #{vm_hash}")
160
+ vm_hash
161
+ end
162
+
163
+ # create_vm creates a new VM with a default network from the config,
164
+ # a initial disk named #{new_vmname}-disk0 that uses the 'template' as its source image
165
+ # and labels added for vm and pool
166
+ # and an instance configuration for machine_type from the config and
167
+ # labels vm and pool
168
+ # having a label "pool" that match a pool config name.
169
+ # inputs
170
+ # [String] pool : Name of the pool
171
+ # [String] new_vmname : Name to give the new VM
172
+ # returns
173
+ # [Hashtable] of the VM as per get_vm(pool_name, vm_name)
174
+ def create_vm(pool_name, new_vmname)
175
+ debug_logger('create_vm')
176
+ pool = pool_config(pool_name)
177
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
178
+
179
+ # harcoded network info
180
+ network_interfaces = Google::Apis::ComputeV1::NetworkInterface.new(
181
+ network: network_name
182
+ )
183
+ network_interfaces.subnetwork = subnetwork_name(pool_name) if subnetwork_name(pool_name)
184
+ init_params = {
185
+ source_image: pool['template'], # The source image to create this disk.
186
+ labels: { 'vm' => new_vmname, 'pool' => pool_name },
187
+ disk_name: "#{new_vmname}-disk0"
188
+ }
189
+ disk = Google::Apis::ComputeV1::AttachedDisk.new(
190
+ auto_delete: true,
191
+ boot: true,
192
+ initialize_params: Google::Apis::ComputeV1::AttachedDiskInitializeParams.new(init_params)
193
+ )
194
+ # Assume all pool config is valid i.e. not missing
195
+ client = ::Google::Apis::ComputeV1::Instance.new(
196
+ name: new_vmname,
197
+ machine_type: pool['machine_type'],
198
+ disks: [disk],
199
+ network_interfaces: [network_interfaces],
200
+ labels: { 'vm' => new_vmname, 'pool' => pool_name },
201
+ tags: Google::Apis::ComputeV1::Tags.new(items: [project])
202
+ )
203
+
204
+ debug_logger('trigger insert_instance')
205
+ result = connection.insert_instance(project, zone(pool_name), client)
206
+ wait_for_operation(project, pool_name, result)
207
+ created_instance = get_vm(pool_name, new_vmname)
208
+ dns_setup(created_instance)
209
+ created_instance
210
+ end
211
+
212
+ # create_disk creates an additional disk for an existing VM. It will name the new
213
+ # disk #{vm_name}-disk#{number_disk} where number_disk is the next logical disk number
214
+ # starting with 1 when adding an additional disk to a VM with only the boot disk:
215
+ # #{vm_name}-disk0 == boot disk
216
+ # #{vm_name}-disk1 == additional disk added via create_disk
217
+ # #{vm_name}-disk2 == additional disk added via create_disk if run a second time etc
218
+ # the new disk has labels added for vm and pool
219
+ # The GCE lifecycle is to create a new disk (lives independently of the instance) then to attach
220
+ # it to the existing instance.
221
+ # inputs
222
+ # [String] pool_name : Name of the pool
223
+ # [String] vm_name : Name of the existing VM
224
+ # [String] disk_size : The new disk size in GB
225
+ # returns
226
+ # [boolean] true : once the operations are finished
227
+ def create_disk(pool_name, vm_name, disk_size)
228
+ debug_logger('create_disk')
229
+ pool = pool_config(pool_name)
230
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
231
+
232
+ begin
233
+ vm_object = connection.get_instance(project, zone(pool_name), vm_name)
234
+ rescue ::Google::Apis::ClientError => e
235
+ raise e unless e.status_code == 404
236
+
237
+ # if it does not exist
238
+ raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}")
239
+ end
240
+ # this number should start at 1 when there is only the boot disk,
241
+ # eg the new disk will be named spicy-proton-disk1
242
+ number_disk = vm_object.disks.length
243
+
244
+ disk_name = "#{vm_name}-disk#{number_disk}"
245
+ disk = Google::Apis::ComputeV1::Disk.new(
246
+ name: disk_name,
247
+ size_gb: disk_size,
248
+ labels: { 'pool' => pool_name, 'vm' => vm_name }
249
+ )
250
+ debug_logger("trigger insert_disk #{disk_name} for #{vm_name}")
251
+ result = connection.insert_disk(project, zone(pool_name), disk)
252
+ wait_for_operation(project, pool_name, result)
253
+ debug_logger("trigger get_disk #{disk_name} for #{vm_name}")
254
+ new_disk = connection.get_disk(project, zone(pool_name), disk_name)
255
+
256
+ attached_disk = Google::Apis::ComputeV1::AttachedDisk.new(
257
+ auto_delete: true,
258
+ boot: false,
259
+ source: new_disk.self_link
260
+ )
261
+ debug_logger("trigger attach_disk #{disk_name} for #{vm_name}")
262
+ result = connection.attach_disk(project, zone(pool_name), vm_object.name, attached_disk)
263
+ wait_for_operation(project, pool_name, result)
264
+ true
265
+ end
266
+
267
+ # create_snapshot creates new snapshots with the unique name {new_snapshot_name}-#{disk.name}
268
+ # for one vm, and one create_snapshot() there could be multiple snapshots created, one for each drive.
269
+ # since the snapshot resource needs a unique name in the gce project,
270
+ # we create a unique name by concatenating {new_snapshot_name}-#{disk.name}
271
+ # the disk name is based on vm_name which makes it unique.
272
+ # The snapshot is added labels snapshot_name, vm, pool, diskname and boot
273
+ # inputs
274
+ # [String] pool_name : Name of the pool
275
+ # [String] vm_name : Name of the existing VM
276
+ # [String] new_snapshot_name : a unique name for this snapshot, which would be used to refer to it when reverting
277
+ # returns
278
+ # [boolean] true : once the operations are finished
279
+ # raises
280
+ # RuntimeError if the vm_name cannot be found
281
+ # RuntimeError if the snapshot_name already exists for this VM
282
+ def create_snapshot(pool_name, vm_name, new_snapshot_name)
283
+ debug_logger('create_snapshot')
284
+ begin
285
+ vm_object = connection.get_instance(project, zone(pool_name), vm_name)
286
+ rescue ::Google::Apis::ClientError => e
287
+ raise e unless e.status_code == 404
288
+
289
+ # if it does not exist
290
+ raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}")
291
+ end
292
+
293
+ old_snap = find_snapshot(vm_name, new_snapshot_name)
294
+ raise("Snapshot #{new_snapshot_name} for VM #{vm_name} in pool #{pool_name} already exists for the provider #{name}") unless old_snap.nil?
295
+
296
+ result_list = []
297
+ vm_object.disks.each do |attached_disk|
298
+ disk_name = disk_name_from_source(attached_disk)
299
+ snapshot_obj = ::Google::Apis::ComputeV1::Snapshot.new(
300
+ name: "#{new_snapshot_name}-#{disk_name}",
301
+ labels: {
302
+ 'snapshot_name' => new_snapshot_name,
303
+ 'vm' => vm_name,
304
+ 'pool' => pool_name,
305
+ 'diskname' => disk_name,
306
+ 'boot' => attached_disk.boot.to_s
307
+ }
308
+ )
309
+ debug_logger("trigger async create_disk_snapshot #{vm_name}: #{new_snapshot_name}-#{disk_name}")
310
+ result = connection.create_disk_snapshot(project, zone(pool_name), disk_name, snapshot_obj)
311
+ # do them all async, keep a list, check later
312
+ result_list << result
313
+ end
314
+ # now check they are done
315
+ result_list.each do |result|
316
+ wait_for_operation(project, pool_name, result)
317
+ end
318
+ true
319
+ end
320
+
321
+ # revert_snapshot reverts an existing VM's disks to an existing snapshot_name
322
+ # reverting in gce entails
323
+ # 1. shutting down the VM,
324
+ # 2. detaching and deleting the drives,
325
+ # 3. creating new disks with the same name from the snapshot for each disk
326
+ # 4. attach disks and start instance
327
+ # for one vm, there might be multiple snapshots in time. We select the ones referred to by the
328
+ # snapshot_name, but that may be multiple snapshots, one for each disks
329
+ # The new disk is added labels vm and pool
330
+ # inputs
331
+ # [String] pool_name : Name of the pool
332
+ # [String] vm_name : Name of the existing VM
333
+ # [String] snapshot_name : Name of an existing snapshot
334
+ # returns
335
+ # [boolean] true : once the operations are finished
336
+ # raises
337
+ # RuntimeError if the vm_name cannot be found
338
+ # RuntimeError if the snapshot_name already exists for this VM
339
+ def revert_snapshot(pool_name, vm_name, snapshot_name)
340
+ debug_logger('revert_snapshot')
341
+ begin
342
+ vm_object = connection.get_instance(project, zone(pool_name), vm_name)
343
+ rescue ::Google::Apis::ClientError => e
344
+ raise e unless e.status_code == 404
345
+
346
+ # if it does not exist
347
+ raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}")
348
+ end
349
+
350
+ snapshot_object = find_snapshot(vm_name, snapshot_name)
351
+ raise("Snapshot #{snapshot_name} for VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if snapshot_object.nil?
352
+
353
+ # Shutdown instance
354
+ debug_logger("trigger stop_instance #{vm_name}")
355
+ result = connection.stop_instance(project, zone(pool_name), vm_name)
356
+ wait_for_operation(project, pool_name, result)
357
+
358
+ # Delete existing disks
359
+ vm_object.disks&.each do |attached_disk|
360
+ debug_logger("trigger detach_disk #{vm_name}: #{attached_disk.device_name}")
361
+ result = connection.detach_disk(project, zone(pool_name), vm_name, attached_disk.device_name)
362
+ wait_for_operation(project, pool_name, result)
363
+ current_disk_name = disk_name_from_source(attached_disk)
364
+ debug_logger("trigger delete_disk #{vm_name}: #{current_disk_name}")
365
+ result = connection.delete_disk(project, zone(pool_name), current_disk_name)
366
+ wait_for_operation(project, pool_name, result)
367
+ end
368
+
369
+ # this block is sensitive to disruptions, for example if vmpooler is stopped while this is running
370
+ snapshot_object.each do |snapshot|
371
+ current_disk_name = snapshot.labels['diskname']
372
+ bootable = (snapshot.labels['boot'] == 'true')
373
+ disk = Google::Apis::ComputeV1::Disk.new(
374
+ name: current_disk_name,
375
+ labels: { 'pool' => pool_name, 'vm' => vm_name },
376
+ source_snapshot: snapshot.self_link
377
+ )
378
+ # create disk in GCE as a separate resource
379
+ debug_logger("trigger insert_disk #{vm_name}: #{current_disk_name} based on #{snapshot.self_link}")
380
+ result = connection.insert_disk(project, zone(pool_name), disk)
381
+ wait_for_operation(project, pool_name, result)
382
+ # read the new disk info
383
+ new_disk_info = connection.get_disk(project, zone(pool_name), current_disk_name)
384
+ new_attached_disk = Google::Apis::ComputeV1::AttachedDisk.new(
385
+ auto_delete: true,
386
+ boot: bootable,
387
+ source: new_disk_info.self_link
388
+ )
389
+ # attach the new disk to existing instance
390
+ debug_logger("trigger attach_disk #{vm_name}: #{current_disk_name}")
391
+ result = connection.attach_disk(project, zone(pool_name), vm_name, new_attached_disk)
392
+ wait_for_operation(project, pool_name, result)
393
+ end
394
+
395
+ debug_logger("trigger start_instance #{vm_name}")
396
+ result = connection.start_instance(project, zone(pool_name), vm_name)
397
+ wait_for_operation(project, pool_name, result)
398
+ true
399
+ end
400
+
401
+ # destroy_vm deletes an existing VM instance and any disks and snapshots via the labels
402
+ # in gce instances, disks and snapshots are resources that can exist independent of each other
403
+ # inputs
404
+ # [String] pool_name : Name of the pool
405
+ # [String] vm_name : Name of the existing VM
406
+ # returns
407
+ # [boolean] true : once the operations are finished
408
+ def destroy_vm(pool_name, vm_name)
409
+ debug_logger('destroy_vm')
410
+ deleted = false
411
+ begin
412
+ connection.get_instance(project, zone(pool_name), vm_name)
413
+ rescue ::Google::Apis::ClientError => e
414
+ raise e unless e.status_code == 404
415
+
416
+ # If a VM doesn't exist then it is effectively deleted
417
+ deleted = true
418
+ debug_logger("instance #{vm_name} already deleted")
419
+ end
420
+
421
+ unless deleted
422
+ debug_logger("trigger delete_instance #{vm_name}")
423
+ vm_hash = get_vm(pool_name, vm_name)
424
+ result = connection.delete_instance(project, zone(pool_name), vm_name)
425
+ wait_for_operation(project, pool_name, result, 10)
426
+ dns_teardown(vm_hash)
427
+ end
428
+
429
+ # list and delete any leftover disk, for instance if they were detached from the instance
430
+ filter = "(labels.vm = #{vm_name})"
431
+ disk_list = connection.list_disks(project, zone(pool_name), filter: filter)
432
+ result_list = []
433
+ disk_list.items&.each do |disk|
434
+ debug_logger("trigger delete_disk #{disk.name}")
435
+ result = connection.delete_disk(project, zone(pool_name), disk.name)
436
+ # do them all async, keep a list, check later
437
+ result_list << result
438
+ end
439
+ # now check they are done
440
+ result_list.each do |r|
441
+ wait_for_operation(project, pool_name, r)
442
+ end
443
+
444
+ # list and delete leftover snapshots, this could happen if snapshots were taken,
445
+ # as they are not removed when the original disk is deleted or the instance is detroyed
446
+ snapshot_list = find_all_snapshots(vm_name)
447
+ result_list = []
448
+ snapshot_list&.each do |snapshot|
449
+ debug_logger("trigger delete_snapshot #{snapshot.name}")
450
+ result = connection.delete_snapshot(project, snapshot.name)
451
+ # do them all async, keep a list, check later
452
+ result_list << result
453
+ end
454
+ # now check they are done
455
+ result_list.each do |r|
456
+ wait_for_operation(project, pool_name, r)
457
+ end
458
+ true
459
+ end
460
+
461
+ def vm_ready?(_pool_name, vm_name)
462
+ begin
463
+ # TODO: we could use a healthcheck resource attached to instance
464
+ open_socket(vm_name, domain || global_config[:config]['domain'])
465
+ rescue StandardError => _e
466
+ return false
467
+ end
468
+ true
469
+ end
470
+
471
+ # Scans zones that are configured for list of resources (VM, disks, snapshots) that do not have the label.pool set
472
+ # to one of the configured pools. If it is also not in the allowlist, the resource is destroyed
473
+ def purge_unconfigured_resources(allowlist)
474
+ debug_logger('purge_unconfigured_resources')
475
+ pools_array = provided_pools
476
+ filter = {}
477
+ # we have to group things by zone, because the API search feature is done against a zone and not global
478
+ # so we will do the searches in each configured zone
479
+ pools_array.each do |pool|
480
+ filter[zone(pool)] = [] if filter[zone(pool)].nil?
481
+ filter[zone(pool)] << "(labels.pool != #{pool})"
482
+ end
483
+ filter.each_key do |zone|
484
+ # this filter should return any item that have a labels.pool that is not in the config OR
485
+ # do not have a pool label at all
486
+ filter_string = "#{filter[zone].join(' AND ')} OR -labels.pool:*"
487
+ # VMs
488
+ instance_list = connection.list_instances(project, zone, filter: filter_string)
489
+
490
+ result_list = []
491
+ instance_list.items&.each do |vm|
492
+ next if should_be_ignored(vm, allowlist)
493
+
494
+ debug_logger("trigger async delete_instance #{vm.name}")
495
+ result = connection.delete_instance(project, zone, vm.name)
496
+ vm_pool = vm.labels&.key?('pool') ? vm.labels['pool'] : nil
497
+ existing_vm = generate_vm_hash(vm, vm_pool)
498
+ dns_teardown(existing_vm)
499
+ result_list << result
500
+ end
501
+ # now check they are done
502
+ result_list.each do |result|
503
+ wait_for_zone_operation(project, zone, result)
504
+ end
505
+
506
+ # Disks
507
+ disks_list = connection.list_disks(project, zone, filter: filter_string)
508
+ disks_list.items&.each do |disk|
509
+ next if should_be_ignored(disk, allowlist)
510
+
511
+ debug_logger("trigger async no wait delete_disk #{disk.name}")
512
+ connection.delete_disk(project, zone, disk.name)
513
+ end
514
+
515
+ # Snapshots
516
+ snapshot_list = connection.list_snapshots(project, filter: filter_string)
517
+ next if snapshot_list.items.nil?
518
+
519
+ snapshot_list.items.each do |sn|
520
+ next if should_be_ignored(sn, allowlist)
521
+
522
+ debug_logger("trigger async no wait delete_snapshot #{sn.name}")
523
+ connection.delete_snapshot(project, sn.name)
524
+ end
525
+ end
526
+ end
527
+
528
+ # tag_vm_user This method is called once we know who is using the VM (it is running). This method enables seeing
529
+ # who is using what in the provider pools.
530
+ #
531
+ # inputs
532
+ # [String] pool_name : Name of the pool
533
+ # [String] vm_name : Name of the VM to check if ready
534
+ # returns
535
+ # [Boolean] : true if successful, false if an error occurred and it should retry
536
+ def tag_vm_user(pool, vm_name)
537
+ user = get_current_user(vm_name)
538
+ vm_hash = get_vm(pool, vm_name)
539
+ return false if vm_hash.nil?
540
+
541
+ new_labels = vm_hash['labels']
542
+ # bailing in this case since labels should exist, and continuing would mean losing them
543
+ return false if new_labels.nil?
544
+
545
+ # add new label called token-user, with value as user
546
+ new_labels['token-user'] = user
547
+ begin
548
+ instances_set_labels_request_object = Google::Apis::ComputeV1::InstancesSetLabelsRequest.new(label_fingerprint: vm_hash['label_fingerprint'], labels: new_labels)
549
+ result = connection.set_instance_labels(project, zone(pool), vm_name, instances_set_labels_request_object)
550
+ wait_for_zone_operation(project, zone(pool), result)
551
+ rescue StandardError => _e
552
+ return false
553
+ end
554
+ true
555
+ end
556
+
557
+ # END BASE METHODS
558
+
559
+ def dns_setup(created_instance)
560
+ dns_zone = dns.zone(dns_zone_resource_name) if dns_zone_resource_name
561
+ return unless dns_zone && created_instance && created_instance['name'] && created_instance['ip']
562
+
563
+ name = created_instance['name']
564
+ begin
565
+ change = dns_zone.add(name, 'A', 60, [created_instance['ip']])
566
+ debug_logger("#{change.id} - #{change.started_at} - #{change.status} DNS address added") if change
567
+ rescue Google::Cloud::AlreadyExistsError => _e
568
+ # DNS setup is done only for new instances, so in the rare case where a DNS record already exists (it is stale) and we replace it.
569
+ # the error is Google::Cloud::AlreadyExistsError: alreadyExists: The resource 'entity.change.additions[0]' named 'instance-8.test.vmpooler.net. (A)' already exists
570
+ change = dns_zone.replace(name, 'A', 60, [created_instance['ip']])
571
+ debug_logger("#{change.id} - #{change.started_at} - #{change.status} DNS address previously existed and was replaced") if change
572
+ end
573
+ end
574
+
575
+ def dns_teardown(created_instance)
576
+ dns_zone = dns.zone(dns_zone_resource_name) if dns_zone_resource_name
577
+ return unless dns_zone && created_instance
578
+
579
+ name = created_instance['name']
580
+ change = dns_zone.remove(name, 'A')
581
+ debug_logger("#{change.id} - #{change.started_at} - #{change.status} DNS address removed") if change
582
+ end
583
+
584
+ def should_be_ignored(item, allowlist)
585
+ return false if allowlist.nil?
586
+
587
+ allowlist.map!(&:downcase) # remove uppercase from configured values because its not valid as resource label
588
+ array_flattened_labels = []
589
+ item.labels&.each do |k, v|
590
+ array_flattened_labels << "#{k}=#{v}"
591
+ end
592
+ (!item.labels.nil? && allowlist&.include?(item.labels['pool'])) || # the allow list specifies the value within the pool label
593
+ (allowlist&.include?('') && !item.labels&.keys&.include?('pool')) || # the allow list specifies "" string and the pool label is not set
594
+ !(allowlist & array_flattened_labels).empty? # the allow list specify a fully qualified label eg user=Bob and the item has it
595
+ end
596
+
597
+ def get_current_user(vm_name)
598
+ @redis.with_metrics do |redis|
599
+ user = redis.hget("vmpooler__vm__#{vm_name}", 'token:user')
600
+ return '' if user.nil?
601
+
602
+ # cleanup so it's a valid label value
603
+ # can't have upercase
604
+ user = user.downcase
605
+ # replace invalid chars with dash
606
+ user = user.gsub(/[^0-9a-z_-]/, '-')
607
+ return user
608
+ end
609
+ end
610
+
611
+ # Compute resource wait for operation to be DONE (synchronous operation)
612
+ def wait_for_zone_operation(project, zone, result, retries = 5)
613
+ while result.status != 'DONE'
614
+ result = connection.wait_zone_operation(project, zone, result.name)
615
+ debug_logger(" -> wait_for_zone_operation status #{result.status} (#{result.name})")
616
+ end
617
+ if result.error # unsure what kind of error can be stored here
618
+ error_message = ''
619
+ # array of errors, combine them all
620
+ result.error.errors.each do |error|
621
+ error_message = "#{error_message} #{error.code}:#{error.message}"
622
+ end
623
+ raise "Operation: #{result.description} failed with error: #{error_message}"
624
+ end
625
+ result
626
+ rescue Google::Apis::TransmissionError => e
627
+ # Error returned once timeout reached, each retry typically about 1 minute.
628
+ if retries.positive?
629
+ retries -= 1
630
+ retry
631
+ end
632
+ raise
633
+ rescue Google::Apis::ClientError => e
634
+ raise e unless e.status_code == 404
635
+
636
+ # if the operation is not found, and we are 'waiting' on it, it might be because it
637
+ # is already finished
638
+ puts "waited on #{result.name} but was not found, so skipping"
639
+ end
640
+
641
+ def wait_for_operation(project, pool_name, result, retries = 5)
642
+ wait_for_zone_operation(project, zone(pool_name), result, retries)
643
+ end
644
+
645
+ # Return a hash of VM data
646
+ # Provides vmname, hostname, template, poolname, boottime, status, zone, machine_type, labels, label_fingerprint, ip information
647
+ def generate_vm_hash(vm_object, pool_name)
648
+ pool_configuration = pool_config(pool_name)
649
+ return nil if pool_configuration.nil?
650
+
651
+ {
652
+ 'name' => vm_object.name,
653
+ 'hostname' => vm_object.hostname,
654
+ 'template' => pool_configuration&.key?('template') ? pool_configuration['template'] : nil, # was expecting to get it from API, not from config, but this is what vSphere does too!
655
+ 'poolname' => vm_object.labels&.key?('pool') ? vm_object.labels['pool'] : nil,
656
+ 'boottime' => vm_object.creation_timestamp,
657
+ 'status' => vm_object.status, # One of the following values: PROVISIONING, STAGING, RUNNING, STOPPING, SUSPENDING, SUSPENDED, REPAIRING, and TERMINATED
658
+ 'zone' => vm_object.zone,
659
+ 'machine_type' => vm_object.machine_type,
660
+ 'labels' => vm_object.labels,
661
+ 'label_fingerprint' => vm_object.label_fingerprint,
662
+ 'ip' => vm_object.network_interfaces ? vm_object.network_interfaces.first.network_ip : nil
663
+ }
664
+ end
665
+
666
+ def ensured_gce_connection(connection_pool_object)
667
+ connection_pool_object[:connection] = connect_to_gce unless connection_pool_object[:connection]
668
+ connection_pool_object[:connection]
669
+ end
670
+
671
+ def connect_to_gce
672
+ max_tries = global_config[:config]['max_tries'] || 3
673
+ retry_factor = global_config[:config]['retry_factor'] || 10
674
+ try = 1
675
+ begin
676
+ scopes = ['https://www.googleapis.com/auth/compute', 'https://www.googleapis.com/auth/cloud-platform']
677
+
678
+ authorization = Google::Auth.get_application_default(scopes)
679
+
680
+ compute = ::Google::Apis::ComputeV1::ComputeService.new
681
+ compute.authorization = authorization
682
+
683
+ metrics.increment('connect.open')
684
+ compute
685
+ rescue StandardError => e # is that even a thing?
686
+ metrics.increment('connect.fail')
687
+ raise e if try >= max_tries
688
+
689
+ sleep(try * retry_factor)
690
+ try += 1
691
+ retry
692
+ end
693
+ end
694
+
695
+ # This should supercede the open_socket method in the Pool Manager
696
+ def open_socket(host, domain = nil, timeout = 5, port = 22, &_block)
697
+ Timeout.timeout(timeout) do
698
+ target_host = host
699
+ target_host = "#{host}.#{domain}" if domain
700
+ sock = TCPSocket.new target_host, port
701
+ begin
702
+ yield sock if block_given?
703
+ ensure
704
+ sock.close
705
+ end
706
+ end
707
+ end
708
+
709
+ # this is used because for one vm, with the same snapshot name there could be multiple snapshots,
710
+ # one for each disk
711
+ def find_snapshot(vm_name, snapshotname)
712
+ filter = "(labels.vm = #{vm_name}) AND (labels.snapshot_name = #{snapshotname})"
713
+ snapshot_list = connection.list_snapshots(project, filter: filter)
714
+ snapshot_list.items # array of snapshot objects
715
+ end
716
+
717
+ # find all snapshots ever created for one vm,
718
+ # regardless of snapshot name, for example when deleting it all
719
+ def find_all_snapshots(vm_name)
720
+ filter = "(labels.vm = #{vm_name})"
721
+ snapshot_list = connection.list_snapshots(project, filter: filter)
722
+ snapshot_list.items # array of snapshot objects
723
+ end
724
+
725
+ def disk_name_from_source(attached_disk)
726
+ attached_disk.source.split('/')[-1] # disk name is after the last / of the full source URL
727
+ end
728
+
729
+ # used in local dev environment, set DEBUG_FLAG=true
730
+ # this way the upstream vmpooler manager does not get polluted with logs
731
+ def debug_logger(message, send_to_upstream: false)
732
+ # the default logger is simple and does not enforce debug levels (the first argument)
733
+ puts message if ENV['DEBUG_FLAG']
734
+ logger.log('[g]', message) if send_to_upstream
735
+ end
736
+ end
737
+ end
738
+ end
739
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VmpoolerProviderGce
4
+ VERSION = '0.1.2'
5
+ end
metadata ADDED
@@ -0,0 +1,240 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vmpooler-provider-gce
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Puppet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-01-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: google-apis-compute_v1
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.14'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: googleauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.16.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.16.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: google-cloud-dns
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.35.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.35.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: vmpooler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 1.3.0
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '1.3'
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.3.0
75
+ - !ruby/object:Gem::Dependency
76
+ name: climate_control
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 0.2.0
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 0.2.0
89
+ - !ruby/object:Gem::Dependency
90
+ name: mock_redis
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 0.17.0
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 0.17.0
103
+ - !ruby/object:Gem::Dependency
104
+ name: pry
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rack-test
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0.6'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0.6'
131
+ - !ruby/object:Gem::Dependency
132
+ name: rspec
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '3.2'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '3.2'
145
+ - !ruby/object:Gem::Dependency
146
+ name: rubocop
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 1.1.0
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 1.1.0
159
+ - !ruby/object:Gem::Dependency
160
+ name: simplecov
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: 0.11.2
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 0.11.2
173
+ - !ruby/object:Gem::Dependency
174
+ name: thor
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '1.0'
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: 1.0.1
183
+ type: :development
184
+ prerelease: false
185
+ version_requirements: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - "~>"
188
+ - !ruby/object:Gem::Version
189
+ version: '1.0'
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: 1.0.1
193
+ - !ruby/object:Gem::Dependency
194
+ name: yarjuf
195
+ requirement: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: '2.0'
200
+ type: :development
201
+ prerelease: false
202
+ version_requirements: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '2.0'
207
+ description:
208
+ email:
209
+ - support@puppet.com
210
+ executables: []
211
+ extensions: []
212
+ extra_rdoc_files: []
213
+ files:
214
+ - lib/vmpooler-provider-gce/version.rb
215
+ - lib/vmpooler/providers/gce.rb
216
+ homepage: https://github.com/puppetlabs/vmpooler-provider-gce
217
+ licenses:
218
+ - Apache-2.0
219
+ metadata: {}
220
+ post_install_message:
221
+ rdoc_options: []
222
+ require_paths:
223
+ - lib
224
+ required_ruby_version: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: 2.3.0
229
+ required_rubygems_version: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: '0'
234
+ requirements: []
235
+ rubyforge_project:
236
+ rubygems_version: 2.7.6.2
237
+ signing_key:
238
+ specification_version: 4
239
+ summary: GCE provider for VMPooler
240
+ test_files: []