vmpooler-provider-vsphere 1.4.0

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: c8065ff117266c13945c80a47fd98b0b7a8a4fe3ec8018b4ea2edd384cc733de
4
+ data.tar.gz: 7f6ef47c263526a44208b36dbbaed4fca5b9c6985d68ab968d9bf258a133c96e
5
+ SHA512:
6
+ metadata.gz: 4c774b0ffd019c106b1d69cb11aafd0c8acddb566e339b195d8a3bad373844855c9943823268b6ac34b5ea312d39b237093c707aaa46b72d22208fa25005ecb7
7
+ data.tar.gz: aaaa1c54f356a1039a13c6c2b59b509f7a070349eeb02fe900acf527ae2b9bbd13f7819ec9a9283d53429137ec279c3f2dbcb24719fcbce058d3a71894283372
@@ -0,0 +1,1173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+ require 'bigdecimal/util'
5
+ require 'rbvmomi'
6
+ require 'vmpooler/providers/base'
7
+
8
+ module Vmpooler
9
+ class PoolManager
10
+ class Provider
11
+ class VSphere < Vmpooler::PoolManager::Provider::Base
12
+ # The connection_pool method is normally used only for testing
13
+ attr_reader :connection_pool
14
+
15
+ def initialize(config, logger, metrics, redis_connection_pool, name, options)
16
+ super(config, logger, metrics, redis_connection_pool, name, options)
17
+
18
+ task_limit = global_config[:config].nil? || global_config[:config]['task_limit'].nil? ? 10 : global_config[:config]['task_limit'].to_i
19
+ # The default connection pool size is:
20
+ # Whatever is biggest from:
21
+ # - How many pools this provider services
22
+ # - Maximum number of cloning tasks allowed
23
+ # - Need at least 2 connections so that a pool can have inventory functions performed while cloning etc.
24
+ default_connpool_size = [provided_pools.count, task_limit, 2].max
25
+ connpool_size = provider_config['connection_pool_size'].nil? ? default_connpool_size : provider_config['connection_pool_size'].to_i
26
+ # The default connection pool timeout should be quite large - 60 seconds
27
+ connpool_timeout = provider_config['connection_pool_timeout'].nil? ? 60 : provider_config['connection_pool_timeout'].to_i
28
+ logger.log('d', "[#{name}] ConnPool - Creating a connection pool of size #{connpool_size} with timeout #{connpool_timeout}")
29
+ @connection_pool = Vmpooler::PoolManager::GenericConnectionPool.new(
30
+ metrics: metrics,
31
+ connpool_type: 'provider_connection_pool',
32
+ connpool_provider: name,
33
+ size: connpool_size,
34
+ timeout: connpool_timeout
35
+ ) do
36
+ logger.log('d', "[#{name}] Connection Pool - Creating a connection object")
37
+ # Need to wrap the vSphere connection object in another object. The generic connection pooler will preserve
38
+ # the object reference for the connection, which means it cannot "reconnect" by creating an entirely new connection
39
+ # object. Instead by wrapping it in a Hash, the Hash object reference itself never changes but the content of the
40
+ # Hash can change, and is preserved across invocations.
41
+ new_conn = connect_to_vsphere
42
+ { connection: new_conn }
43
+ end
44
+ @provider_hosts = {}
45
+ @provider_hosts_lock = Mutex.new
46
+ @redis = redis_connection_pool
47
+ end
48
+
49
+ # name of the provider class
50
+ def name
51
+ 'vsphere'
52
+ end
53
+
54
+ def folder_configured?(folder_title, base_folder, configured_folders, whitelist)
55
+ return true if whitelist&.include?(folder_title)
56
+ return false unless configured_folders.keys.include?(folder_title)
57
+ return false unless configured_folders[folder_title] == base_folder
58
+
59
+ true
60
+ end
61
+
62
+ def destroy_vm_and_log(vm_name, vm_object, pool, data_ttl)
63
+ try = 0 if try.nil?
64
+ max_tries = 3
65
+ @redis.with_metrics do |redis|
66
+ redis.multi
67
+ redis.srem("vmpooler__completed__#{pool}", vm_name)
68
+ redis.hdel("vmpooler__active__#{pool}", vm_name)
69
+ redis.hset("vmpooler__vm__#{vm_name}", 'destroy', Time.now)
70
+
71
+ # Auto-expire metadata key
72
+ redis.expire("vmpooler__vm__#{vm_name}", (data_ttl * 60 * 60))
73
+ redis.exec
74
+ end
75
+
76
+ start = Time.now
77
+
78
+ if vm_object.is_a? RbVmomi::VIM::Folder
79
+ logger.log('s', "[!] [#{pool}] '#{vm_name}' is a folder, bailing on destroying")
80
+ raise('Expected VM, but received a folder object')
81
+ end
82
+ vm_object.PowerOffVM_Task.wait_for_completion if vm_object.runtime&.powerState && vm_object.runtime.powerState == 'poweredOn'
83
+ vm_object.Destroy_Task.wait_for_completion
84
+
85
+ finish = format('%<time>.2f', time: Time.now - start)
86
+ logger.log('s', "[-] [#{pool}] '#{vm_name}' destroyed in #{finish} seconds")
87
+ metrics.timing("destroy.#{pool}", finish)
88
+ rescue RuntimeError
89
+ raise
90
+ rescue StandardError => e
91
+ try += 1
92
+ logger.log('s', "[!] [#{pool}] failed to destroy '#{vm_name}' with an error: #{e}")
93
+ try >= max_tries ? raise : retry
94
+ end
95
+
96
+ def destroy_folder_and_children(folder_object)
97
+ vms = {}
98
+ data_ttl = $config[:redis]['data_ttl'].to_i
99
+ folder_name = folder_object.name
100
+ unless folder_object.childEntity.count == 0
101
+ folder_object.childEntity.each do |vm|
102
+ vms[vm.name] = vm
103
+ end
104
+
105
+ vms.each do |vm_name, vm_object|
106
+ destroy_vm_and_log(vm_name, vm_object, folder_name, data_ttl)
107
+ end
108
+ end
109
+ destroy_folder(folder_object)
110
+ end
111
+
112
+ def destroy_folder(folder_object)
113
+ try = 0 if try.nil?
114
+ max_tries = 3
115
+ logger.log('s', "[-] [#{folder_object.name}] removing unconfigured folder")
116
+ folder_object.Destroy_Task.wait_for_completion
117
+ rescue StandardError
118
+ try += 1
119
+ try >= max_tries ? raise : retry
120
+ end
121
+
122
+ def purge_unconfigured_folders(base_folders, configured_folders, whitelist)
123
+ @connection_pool.with_metrics do |pool_object|
124
+ connection = ensured_vsphere_connection(pool_object)
125
+
126
+ base_folders.each do |base_folder|
127
+ folder_children = get_folder_children(base_folder, connection)
128
+ next if folder_children.empty?
129
+
130
+ folder_children.each do |folder_hash|
131
+ folder_hash.each do |folder_title, folder_object|
132
+ destroy_folder_and_children(folder_object) unless folder_configured?(folder_title, base_folder, configured_folders, whitelist)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def get_folder_children(folder_name, connection)
140
+ folders = []
141
+
142
+ propSpecs = { # rubocop:disable Naming/VariableName
143
+ entity: self,
144
+ inventoryPath: folder_name
145
+ }
146
+ folder_object = connection.searchIndex.FindByInventoryPath(propSpecs) # rubocop:disable Naming/VariableName
147
+
148
+ return folders if folder_object.nil?
149
+
150
+ folder_object.childEntity.each do |folder|
151
+ next unless folder.is_a? RbVmomi::VIM::Folder
152
+
153
+ folders << { folder.name => folder }
154
+ end
155
+
156
+ folders
157
+ end
158
+
159
+ def vms_in_pool(pool_name)
160
+ vms = []
161
+ @connection_pool.with_metrics do |pool_object|
162
+ connection = ensured_vsphere_connection(pool_object)
163
+ folder_object = find_vm_folder(pool_name, connection)
164
+
165
+ return vms if folder_object.nil?
166
+
167
+ folder_object.childEntity.each do |vm|
168
+ vms << { 'name' => vm.name } if vm.is_a? RbVmomi::VIM::VirtualMachine
169
+ end
170
+ end
171
+ vms
172
+ end
173
+
174
+ def select_target_hosts(target, cluster, datacenter)
175
+ percentage = 100
176
+ dc = "#{datacenter}_#{cluster}"
177
+ @provider_hosts_lock.synchronize do
178
+ begin
179
+ target[dc] = {} unless target.key?(dc)
180
+ target[dc]['checking'] = true
181
+ hosts_hash = find_least_used_hosts(cluster, datacenter, percentage)
182
+ target[dc] = hosts_hash
183
+ rescue StandardError
184
+ target[dc] = {}
185
+ raise
186
+ ensure
187
+ target[dc]['check_time_finished'] = Time.now
188
+ end
189
+ end
190
+ end
191
+
192
+ def run_select_hosts(pool_name, target)
193
+ now = Time.now
194
+ max_age = @config[:config]['host_selection_max_age'] || 60
195
+ loop_delay = 5
196
+ datacenter = get_target_datacenter_from_config(pool_name)
197
+ cluster = get_target_cluster_from_config(pool_name)
198
+ raise("cluster for pool #{pool_name} cannot be identified") if cluster.nil?
199
+ raise("datacenter for pool #{pool_name} cannot be identified") if datacenter.nil?
200
+
201
+ dc = "#{datacenter}_#{cluster}"
202
+ unless target.key?(dc)
203
+ select_target_hosts(target, cluster, datacenter)
204
+ return
205
+ end
206
+ wait_for_host_selection(dc, target, loop_delay, max_age) if target[dc].key?('checking')
207
+ select_target_hosts(target, cluster, datacenter) if target[dc].key?('check_time_finished') && now - target[dc]['check_time_finished'] > max_age
208
+ end
209
+
210
+ def wait_for_host_selection(dc, target, maxloop = 0, loop_delay = 1, max_age = 60)
211
+ loop_count = 1
212
+ until target.key?(dc) && target[dc].key?('check_time_finished')
213
+ sleep(loop_delay)
214
+ unless maxloop == 0
215
+ break if loop_count >= maxloop
216
+
217
+ loop_count += 1
218
+ end
219
+ end
220
+ return unless target[dc].key?('check_time_finished')
221
+
222
+ loop_count = 1
223
+ while Time.now - target[dc]['check_time_finished'] > max_age
224
+ sleep(loop_delay)
225
+ unless maxloop == 0
226
+ break if loop_count >= maxloop
227
+
228
+ loop_count += 1
229
+ end
230
+ end
231
+ end
232
+
233
+ def select_next_host(pool_name, target, architecture = nil)
234
+ datacenter = get_target_datacenter_from_config(pool_name)
235
+ cluster = get_target_cluster_from_config(pool_name)
236
+ raise("cluster for pool #{pool_name} cannot be identified") if cluster.nil?
237
+ raise("datacenter for pool #{pool_name} cannot be identified") if datacenter.nil?
238
+
239
+ dc = "#{datacenter}_#{cluster}"
240
+ @provider_hosts_lock.synchronize do
241
+ if architecture
242
+ raise("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory") unless target[dc].key?('architectures')
243
+
244
+ host = target[dc]['architectures'][architecture].shift
245
+ target[dc]['architectures'][architecture] << host
246
+ if target[dc]['hosts'].include?(host)
247
+ target[dc]['hosts'].delete(host)
248
+ target[dc]['hosts'] << host
249
+ end
250
+ else
251
+ raise("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory") unless target[dc].key?('hosts')
252
+
253
+ host = target[dc]['hosts'].shift
254
+ target[dc]['hosts'] << host
255
+ target[dc]['architectures'].each do |arch|
256
+ target[dc]['architectures'][arch] = arch.partition { |v| v != host }.flatten if arch.include?(host)
257
+ end
258
+ end
259
+
260
+ return host
261
+ end
262
+ end
263
+
264
+ def vm_in_target?(pool_name, parent_host, architecture, target)
265
+ datacenter = get_target_datacenter_from_config(pool_name)
266
+ cluster = get_target_cluster_from_config(pool_name)
267
+ raise("cluster for pool #{pool_name} cannot be identified") if cluster.nil?
268
+ raise("datacenter for pool #{pool_name} cannot be identified") if datacenter.nil?
269
+
270
+ dc = "#{datacenter}_#{cluster}"
271
+ raise("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory") unless target[dc].key?('hosts')
272
+ return true if target[dc]['hosts'].include?(parent_host)
273
+ return true if target[dc]['architectures'][architecture].include?(parent_host)
274
+
275
+ false
276
+ end
277
+
278
+ def get_vm(pool_name, vm_name)
279
+ vm_hash = nil
280
+ @connection_pool.with_metrics do |pool_object|
281
+ connection = ensured_vsphere_connection(pool_object)
282
+ vm_object = find_vm(pool_name, vm_name, connection)
283
+ return vm_hash if vm_object.nil?
284
+
285
+ vm_hash = generate_vm_hash(vm_object, pool_name)
286
+ end
287
+ vm_hash
288
+ end
289
+
290
+ def create_vm(pool_name, new_vmname)
291
+ pool = pool_config(pool_name)
292
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
293
+
294
+ vm_hash = nil
295
+ @connection_pool.with_metrics do |pool_object|
296
+ connection = ensured_vsphere_connection(pool_object)
297
+ # Assume all pool config is valid i.e. not missing
298
+ template_path = pool['template']
299
+ target_folder_path = pool['folder']
300
+ target_datastore = pool['datastore']
301
+ target_datacenter_name = get_target_datacenter_from_config(pool_name)
302
+
303
+ # Get the template VM object
304
+ raise("Pool #{pool_name} did not specify a full path for the template for the provider #{name}") unless valid_template_path? template_path
305
+
306
+ template_vm_object = find_template_vm(pool, connection)
307
+
308
+ extra_config = [
309
+ { key: 'guestinfo.hostname', value: new_vmname }
310
+ ]
311
+
312
+ if pool.key?('snapshot_mainMem_ioBlockPages')
313
+ ioblockpages = pool['snapshot_mainMem_ioBlockPages']
314
+ extra_config.push(
315
+ { key: 'mainMem.ioBlockPages', value: ioblockpages }
316
+ )
317
+ end
318
+ if pool.key?('snapshot_mainMem_iowait')
319
+ iowait = pool['snapshot_mainMem_iowait']
320
+ extra_config.push(
321
+ { key: 'mainMem.iowait', value: iowait }
322
+ )
323
+ end
324
+
325
+ # Annotate with creation time, origin template, etc.
326
+ # Add extraconfig options that can be queried by vmtools
327
+ config_spec = create_config_spec(new_vmname, template_path, extra_config)
328
+
329
+ # Check if alternate network configuration is specified and add configuration
330
+ if pool.key?('network')
331
+ template_vm_network_device = template_vm_object.config.hardware.device.grep(RbVmomi::VIM::VirtualEthernetCard).first
332
+ network_name = pool['network']
333
+ network_device = set_network_device(target_datacenter_name, template_vm_network_device, network_name, connection)
334
+ config_spec.deviceChange = [{ operation: 'edit', device: network_device }]
335
+ end
336
+
337
+ # Put the VM in the specified folder and resource pool
338
+ relocate_spec = create_relocate_spec(target_datastore, target_datacenter_name, pool_name, connection)
339
+
340
+ # Create a clone spec
341
+ clone_spec = create_clone_spec(relocate_spec, config_spec)
342
+
343
+ begin
344
+ vm_target_folder = find_vm_folder(pool_name, connection)
345
+ vm_target_folder ||= create_folder(connection, target_folder_path, target_datacenter_name) if @config[:config].key?('create_folders') && (@config[:config]['create_folders'] == true)
346
+ rescue StandardError
347
+ if @config[:config].key?('create_folders') && (@config[:config]['create_folders'] == true)
348
+ vm_target_folder = create_folder(connection, target_folder_path, target_datacenter_name)
349
+ else
350
+ raise
351
+ end
352
+ end
353
+ raise ArgumentError, "Cannot find the configured folder for #{pool_name} #{target_folder_path}" unless vm_target_folder
354
+
355
+ # Create the new VM
356
+ new_vm_object = template_vm_object.CloneVM_Task(
357
+ folder: vm_target_folder,
358
+ name: new_vmname,
359
+ spec: clone_spec
360
+ ).wait_for_completion
361
+
362
+ vm_hash = generate_vm_hash(new_vm_object, pool_name)
363
+ end
364
+ vm_hash
365
+ end
366
+
367
+ def create_config_spec(vm_name, template_name, extra_config)
368
+ RbVmomi::VIM.VirtualMachineConfigSpec(
369
+ annotation: JSON.pretty_generate(
370
+ name: vm_name,
371
+ created_by: provider_config['username'],
372
+ base_template: template_name,
373
+ creation_timestamp: Time.now.utc
374
+ ),
375
+ extraConfig: extra_config
376
+ )
377
+ end
378
+
379
+ def create_relocate_spec(target_datastore, target_datacenter_name, pool_name, connection)
380
+ pool = pool_config(pool_name)
381
+ target_cluster_name = get_target_cluster_from_config(pool_name)
382
+
383
+ relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec(
384
+ datastore: find_datastore(target_datastore, connection, target_datacenter_name),
385
+ diskMoveType: get_disk_backing(pool)
386
+ )
387
+ manage_host_selection = @config[:config]['manage_host_selection'] if @config[:config].key?('manage_host_selection')
388
+ if manage_host_selection
389
+ run_select_hosts(pool_name, @provider_hosts)
390
+ target_host = select_next_host(pool_name, @provider_hosts)
391
+ host_object = find_host_by_dnsname(connection, target_host)
392
+ relocate_spec.host = host_object
393
+ else
394
+ # Choose a cluster/host to place the new VM on
395
+ target_cluster_object = find_cluster(target_cluster_name, connection, target_datacenter_name)
396
+ relocate_spec.pool = target_cluster_object.resourcePool
397
+ end
398
+ relocate_spec
399
+ end
400
+
401
+ def create_clone_spec(relocate_spec, config_spec)
402
+ RbVmomi::VIM.VirtualMachineCloneSpec(
403
+ location: relocate_spec,
404
+ config: config_spec,
405
+ powerOn: true,
406
+ template: false
407
+ )
408
+ end
409
+
410
+ def set_network_device(datacenter_name, template_vm_network_device, network_name, connection)
411
+ # Retrieve network object
412
+ datacenter = connection.serviceInstance.find_datacenter(datacenter_name)
413
+ new_network = datacenter.network.find { |n| n.name == network_name }
414
+
415
+ raise("Cannot find network #{network_name} in datacenter #{datacenter_name}") unless new_network
416
+
417
+ # Determine network device type
418
+ # All possible device type options here: https://vdc-download.vmware.com/vmwb-repository/dcr-public/98d63b35-d822-47fe-a87a-ddefd469df06/2e3c7b58-f2bd-486e-8bb1-a75eb0640bee/doc/vim.vm.device.VirtualEthernetCard.html
419
+ network_device =
420
+ if template_vm_network_device.instance_of? RbVmomi::VIM::VirtualVmxnet2
421
+ RbVmomi::VIM.VirtualVmxnet2
422
+ elsif template_vm_network_device.instance_of? RbVmomi::VIM::VirtualVmxnet3
423
+ RbVmomi::VIM.VirtualVmxnet3
424
+ elsif template_vm_network_device.instance_of? RbVmomi::VIM::VirtualE1000
425
+ RbVmomi::VIM.VirtualE1000
426
+ elsif template_vm_network_device.instance_of? RbVmomi::VIM::VirtualE1000e
427
+ RbVmomi::VIM.VirtualE1000e
428
+ elsif template_vm_network_device.instance_of? RbVmomi::VIM::VirtualSriovEthernetCard
429
+ RbVmomi::VIM.VirtualSriovEthernetCard
430
+ else
431
+ RbVmomi::VIM.VirtualPCNet32
432
+ end
433
+
434
+ # Set up new network device attributes
435
+ network_device.key = template_vm_network_device.key
436
+ network_device.deviceInfo = RbVmomi::VIM.Description(
437
+ label: template_vm_network_device.deviceInfo.label,
438
+ summary: network_name
439
+ )
440
+ network_device.backing = RbVmomi::VIM.VirtualEthernetCardNetworkBackingInfo(
441
+ deviceName: network_name,
442
+ network: new_network,
443
+ useAutoDetect: false
444
+ )
445
+ network_device.addressType = 'assigned'
446
+ network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo(
447
+ allowGuestControl: true,
448
+ startConnected: true,
449
+ connected: true
450
+ )
451
+ network_device
452
+ end
453
+
454
+ def create_disk(pool_name, vm_name, disk_size)
455
+ pool = pool_config(pool_name)
456
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
457
+
458
+ datastore_name = pool['datastore']
459
+ raise("Pool #{pool_name} does not have a datastore defined for the provider #{name}") if datastore_name.nil?
460
+
461
+ @connection_pool.with_metrics do |pool_object|
462
+ connection = ensured_vsphere_connection(pool_object)
463
+ vm_object = find_vm(pool_name, vm_name, connection)
464
+ raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if vm_object.nil?
465
+
466
+ add_disk(vm_object, disk_size, datastore_name, connection, get_target_datacenter_from_config(pool_name))
467
+ end
468
+ true
469
+ end
470
+
471
+ def create_snapshot(pool_name, vm_name, new_snapshot_name)
472
+ @connection_pool.with_metrics do |pool_object|
473
+ connection = ensured_vsphere_connection(pool_object)
474
+ vm_object = find_vm(pool_name, vm_name, connection)
475
+ raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if vm_object.nil?
476
+
477
+ old_snap = find_snapshot(vm_object, new_snapshot_name)
478
+ raise("Snapshot #{new_snapshot_name} for VM #{vm_name} in pool #{pool_name} already exists for the provider #{name}") unless old_snap.nil?
479
+
480
+ vm_object.CreateSnapshot_Task(
481
+ name: new_snapshot_name,
482
+ description: 'vmpooler',
483
+ memory: true,
484
+ quiesce: true
485
+ ).wait_for_completion
486
+ end
487
+ true
488
+ end
489
+
490
+ def revert_snapshot(pool_name, vm_name, snapshot_name)
491
+ @connection_pool.with_metrics do |pool_object|
492
+ connection = ensured_vsphere_connection(pool_object)
493
+ vm_object = find_vm(pool_name, vm_name, connection)
494
+ raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if vm_object.nil?
495
+
496
+ snapshot_object = find_snapshot(vm_object, snapshot_name)
497
+ raise("Snapshot #{snapshot_name} for VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if snapshot_object.nil?
498
+
499
+ snapshot_object.RevertToSnapshot_Task.wait_for_completion
500
+ end
501
+ true
502
+ end
503
+
504
+ def destroy_vm(pool_name, vm_name)
505
+ @connection_pool.with_metrics do |pool_object|
506
+ connection = ensured_vsphere_connection(pool_object)
507
+ vm_object = find_vm(pool_name, vm_name, connection)
508
+ # If a VM doesn't exist then it is effectively deleted
509
+ return true if vm_object.nil?
510
+
511
+ # Poweroff the VM if it's running
512
+ vm_object.PowerOffVM_Task.wait_for_completion if vm_object.runtime&.powerState && vm_object.runtime.powerState == 'poweredOn'
513
+
514
+ # Kill it with fire
515
+ vm_object.Destroy_Task.wait_for_completion
516
+ end
517
+ true
518
+ end
519
+
520
+ def vm_ready?(_pool_name, vm_name)
521
+ begin
522
+ open_socket(vm_name, global_config[:config]['domain'])
523
+ rescue StandardError => _e
524
+ return false
525
+ end
526
+
527
+ true
528
+ end
529
+
530
+ # VSphere Helper methods
531
+
532
+ def get_target_cluster_from_config(pool_name)
533
+ pool = pool_config(pool_name)
534
+ return nil if pool.nil?
535
+
536
+ return pool['clone_target'] unless pool['clone_target'].nil?
537
+ return global_config[:config]['clone_target'] unless global_config[:config]['clone_target'].nil?
538
+
539
+ nil
540
+ end
541
+
542
+ def get_target_datacenter_from_config(pool_name)
543
+ pool = pool_config(pool_name)
544
+ return nil if pool.nil?
545
+
546
+ return pool['datacenter'] unless pool['datacenter'].nil?
547
+ return provider_config['datacenter'] unless provider_config['datacenter'].nil?
548
+
549
+ nil
550
+ end
551
+
552
+ # Return a hash of VM data
553
+ # Provides vmname, hostname, template, poolname, boottime and powerstate information
554
+ def generate_vm_hash(vm_object, pool_name)
555
+ pool_configuration = pool_config(pool_name)
556
+ return nil if pool_configuration.nil?
557
+
558
+ hostname = vm_object.summary.guest.hostName if vm_object.summary&.guest && vm_object.summary.guest.hostName
559
+ boottime = vm_object.runtime.bootTime if vm_object.runtime&.bootTime
560
+ powerstate = vm_object.runtime.powerState if vm_object.runtime&.powerState
561
+
562
+ {
563
+ 'name' => vm_object.name,
564
+ 'hostname' => hostname,
565
+ 'template' => pool_configuration['template'],
566
+ 'poolname' => pool_name,
567
+ 'boottime' => boottime,
568
+ 'powerstate' => powerstate
569
+ }
570
+ end
571
+
572
+ # vSphere helper methods
573
+ ADAPTER_TYPE = 'lsiLogic'
574
+ DISK_TYPE = 'thin'
575
+ DISK_MODE = 'persistent'
576
+
577
+ def ensured_vsphere_connection(connection_pool_object)
578
+ connection_pool_object[:connection] = connect_to_vsphere unless vsphere_connection_ok?(connection_pool_object[:connection])
579
+ connection_pool_object[:connection]
580
+ end
581
+
582
+ def vsphere_connection_ok?(connection)
583
+ _result = connection.serviceInstance.CurrentTime
584
+ true
585
+ rescue StandardError
586
+ false
587
+ end
588
+
589
+ def connect_to_vsphere
590
+ max_tries = global_config[:config]['max_tries'] || 3
591
+ retry_factor = global_config[:config]['retry_factor'] || 10
592
+ try = 1
593
+ begin
594
+ connection = RbVmomi::VIM.connect host: provider_config['server'],
595
+ user: provider_config['username'],
596
+ password: provider_config['password'],
597
+ insecure: provider_config['insecure'] || false
598
+ metrics.increment('connect.open')
599
+ connection
600
+ rescue StandardError => e
601
+ metrics.increment('connect.fail')
602
+ raise e if try >= max_tries
603
+
604
+ sleep(try * retry_factor)
605
+ try += 1
606
+ retry
607
+ end
608
+ end
609
+
610
+ # This should supercede the open_socket method in the Pool Manager
611
+ def open_socket(host, domain = nil, timeout = 5, port = 22, &_block)
612
+ Timeout.timeout(timeout) do
613
+ target_host = host
614
+ target_host = "#{host}.#{domain}" if domain
615
+ sock = TCPSocket.new target_host, port
616
+ begin
617
+ yield sock if block_given?
618
+ ensure
619
+ sock.close
620
+ end
621
+ end
622
+ end
623
+
624
+ def get_vm_folder_path(vm_object)
625
+ # This gives an array starting from the root Datacenters folder all the way to the VM
626
+ # [ [Object, String], [Object, String ] ... ]
627
+ # It's then reversed so that it now goes from the VM to the Datacenter
628
+ full_path = vm_object.path.reverse
629
+
630
+ # Find the Datacenter object
631
+ dc_index = full_path.index { |p| p[0].is_a?(RbVmomi::VIM::Datacenter) }
632
+ return nil if dc_index.nil?
633
+ # The Datacenter should be at least 2 otherwise there's something
634
+ # wrong with the array passed in
635
+ # This is the minimum:
636
+ # [ VM (0), VM ROOT FOLDER (1), DC (2)]
637
+ return nil if dc_index <= 1
638
+
639
+ # Remove the VM name (Starting position of 1 in the slice)
640
+ # Up until the Root VM Folder of DataCenter Node (dc_index - 2)
641
+ full_path = full_path.slice(1..dc_index - 2)
642
+
643
+ # Reverse the array back to normal and
644
+ # then convert the array of paths into a '/' seperated string
645
+ (full_path.reverse.map { |p| p[1] }).join('/')
646
+ end
647
+
648
+ def add_disk(vm, size, datastore, connection, datacentername)
649
+ return false unless size.to_i > 0
650
+
651
+ vmdk_datastore = find_datastore(datastore, connection, datacentername)
652
+ raise("Datastore '#{datastore}' does not exist in datacenter '#{datacentername}'") if vmdk_datastore.nil?
653
+
654
+ datacenter = connection.serviceInstance.find_datacenter(datacentername)
655
+ controller = find_disk_controller(vm)
656
+ disk_unit_number = find_disk_unit_number(vm, controller)
657
+ disk_count = vm.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk).count
658
+ vmdk_file_name = "#{vm['name']}/#{vm['name']}_#{disk_count}.vmdk"
659
+
660
+ vmdk_spec = RbVmomi::VIM::FileBackedVirtualDiskSpec(
661
+ capacityKb: size.to_i * 1024 * 1024,
662
+ adapterType: ADAPTER_TYPE,
663
+ diskType: DISK_TYPE
664
+ )
665
+
666
+ vmdk_backing = RbVmomi::VIM::VirtualDiskFlatVer2BackingInfo(
667
+ datastore: vmdk_datastore,
668
+ diskMode: DISK_MODE,
669
+ fileName: "[#{datastore}] #{vmdk_file_name}"
670
+ )
671
+
672
+ device = RbVmomi::VIM::VirtualDisk(
673
+ backing: vmdk_backing,
674
+ capacityInKB: size.to_i * 1024 * 1024,
675
+ controllerKey: controller.key,
676
+ key: -1,
677
+ unitNumber: disk_unit_number
678
+ )
679
+
680
+ device_config_spec = RbVmomi::VIM::VirtualDeviceConfigSpec(
681
+ device: device,
682
+ operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation('add')
683
+ )
684
+
685
+ vm_config_spec = RbVmomi::VIM::VirtualMachineConfigSpec(
686
+ deviceChange: [device_config_spec]
687
+ )
688
+
689
+ connection.serviceContent.virtualDiskManager.CreateVirtualDisk_Task(
690
+ datacenter: datacenter,
691
+ name: "[#{datastore}] #{vmdk_file_name}",
692
+ spec: vmdk_spec
693
+ ).wait_for_completion
694
+
695
+ vm.ReconfigVM_Task(spec: vm_config_spec).wait_for_completion
696
+
697
+ true
698
+ end
699
+
700
+ def find_datastore(datastorename, connection, datacentername)
701
+ datacenter = connection.serviceInstance.find_datacenter(datacentername)
702
+ raise("Datacenter #{datacentername} does not exist") if datacenter.nil?
703
+
704
+ datacenter.find_datastore(datastorename)
705
+ end
706
+
707
+ def find_device(vm, device_name)
708
+ vm.config.hardware.device.each do |device|
709
+ return device if device.deviceInfo.label == device_name
710
+ end
711
+
712
+ nil
713
+ end
714
+
715
+ def find_disk_controller(vm)
716
+ devices = find_disk_devices(vm)
717
+
718
+ devices.keys.sort.each do |device|
719
+ return find_device(vm, devices[device]['device'].deviceInfo.label) if devices[device]['children'].length < 15
720
+ end
721
+
722
+ nil
723
+ end
724
+
725
+ def find_disk_devices(vm)
726
+ devices = {}
727
+
728
+ vm.config.hardware.device.each do |device|
729
+ if device.is_a? RbVmomi::VIM::VirtualSCSIController
730
+ if devices[device.controllerKey].nil?
731
+ devices[device.key] = {}
732
+ devices[device.key]['children'] = []
733
+ end
734
+
735
+ devices[device.key]['device'] = device
736
+ end
737
+
738
+ if device.is_a? RbVmomi::VIM::VirtualDisk
739
+ if devices[device.controllerKey].nil?
740
+ devices[device.controllerKey] = {}
741
+ devices[device.controllerKey]['children'] = []
742
+ end
743
+
744
+ devices[device.controllerKey]['children'].push(device)
745
+ end
746
+ end
747
+
748
+ devices
749
+ end
750
+
751
+ def find_disk_unit_number(vm, controller)
752
+ used_unit_numbers = []
753
+ available_unit_numbers = []
754
+
755
+ devices = find_disk_devices(vm)
756
+
757
+ devices.keys.sort.each do |c|
758
+ next unless controller.key == devices[c]['device'].key
759
+
760
+ used_unit_numbers.push(devices[c]['device'].scsiCtlrUnitNumber)
761
+ devices[c]['children'].each do |disk|
762
+ used_unit_numbers.push(disk.unitNumber)
763
+ end
764
+ end
765
+
766
+ (0..15).each do |scsi_id|
767
+ available_unit_numbers.push(scsi_id) if used_unit_numbers.grep(scsi_id).length <= 0
768
+ end
769
+
770
+ available_unit_numbers.min
771
+ end
772
+
773
+ # Finds a folder object by inventory path
774
+ # Params:
775
+ # +pool_name+:: the pool to find the folder for
776
+ # +connection+:: the vsphere connection object
777
+ # returns a ManagedObjectReference for the folder found or nil if not found
778
+ def find_vm_folder(pool_name, connection)
779
+ # Find a folder by its inventory path and return the object
780
+ # Returns nil when the object found is not a folder
781
+ pool_configuration = pool_config(pool_name)
782
+ return nil if pool_configuration.nil?
783
+
784
+ folder = pool_configuration['folder']
785
+ datacenter = get_target_datacenter_from_config(pool_name)
786
+ return nil if datacenter.nil?
787
+
788
+ propSpecs = { # rubocop:disable Naming/VariableName
789
+ entity: self,
790
+ inventoryPath: "#{datacenter}/vm/#{folder}"
791
+ }
792
+
793
+ folder_object = connection.searchIndex.FindByInventoryPath(propSpecs) # rubocop:disable Naming/VariableName
794
+ return nil unless folder_object.instance_of? RbVmomi::VIM::Folder
795
+
796
+ folder_object
797
+ end
798
+
799
+ # Returns an array containing cumulative CPU and memory utilization of a host, and its object reference
800
+ # Params:
801
+ # +model+:: CPU arch version to match on
802
+ # +limit+:: Hard limit for CPU or memory utilization beyond which a host is excluded for deployments
803
+ # returns nil if one on these conditions is true:
804
+ # the model param is defined and cannot be found
805
+ # the host is in maintenance mode
806
+ # the host status is not 'green'
807
+ # the cpu or memory utilization is bigger than the limit param
808
+ def get_host_utilization(host, model = nil, limit = 90)
809
+ limit = @config[:config]['utilization_limit'] if @config[:config].key?('utilization_limit')
810
+ return nil if model && !host_has_cpu_model?(host, model)
811
+ return nil if host.runtime.inMaintenanceMode
812
+ return nil unless host.overallStatus == 'green'
813
+ return nil unless host.configIssue.empty?
814
+
815
+ cpu_utilization = cpu_utilization_for host
816
+ memory_utilization = memory_utilization_for host
817
+
818
+ return nil if cpu_utilization.nil?
819
+ return nil if cpu_utilization.to_d == 0.0.to_d
820
+ return nil if memory_utilization.nil?
821
+ return nil if memory_utilization.to_d == 0.0.to_d
822
+
823
+ return nil if cpu_utilization > limit
824
+ return nil if memory_utilization > limit
825
+
826
+ [cpu_utilization, host]
827
+ end
828
+
829
+ def host_has_cpu_model?(host, model)
830
+ get_host_cpu_arch_version(host) == model
831
+ end
832
+
833
+ def get_host_cpu_arch_version(host)
834
+ cpu_model = host.hardware.cpuPkg[0].description
835
+ cpu_model_parts = cpu_model.split
836
+ cpu_model_parts[4]
837
+ end
838
+
839
+ def cpu_utilization_for(host)
840
+ cpu_usage = host.summary.quickStats.overallCpuUsage
841
+ return nil if cpu_usage.nil?
842
+
843
+ cpu_size = host.summary.hardware.cpuMhz * host.summary.hardware.numCpuCores
844
+ cpu_usage.fdiv(cpu_size) * 100
845
+ end
846
+
847
+ def memory_utilization_for(host)
848
+ memory_usage = host.summary.quickStats.overallMemoryUsage
849
+ return nil if memory_usage.nil?
850
+
851
+ memory_size = host.summary.hardware.memorySize / 1024 / 1024
852
+ memory_usage.fdiv(memory_size) * 100
853
+ end
854
+
855
+ def get_average_cluster_utilization(hosts)
856
+ utilization_counts = hosts.map { |host| host[0] }
857
+ utilization_counts.inject(:+) / hosts.count
858
+ end
859
+
860
+ def build_compatible_hosts_lists(hosts, percentage)
861
+ hosts_with_arch_versions = hosts.map do |h|
862
+ {
863
+ 'utilization' => h[0],
864
+ 'host_object' => h[1],
865
+ 'architecture' => get_host_cpu_arch_version(h[1])
866
+ }
867
+ end
868
+ versions = hosts_with_arch_versions.map { |host| host['architecture'] }.uniq
869
+ architectures = {}
870
+ versions.each do |version|
871
+ architectures[version] = []
872
+ end
873
+
874
+ hosts_with_arch_versions.each do |h|
875
+ architectures[h['architecture']] << [h['utilization'], h['host_object'], h['architecture']]
876
+ end
877
+
878
+ versions.each do |version|
879
+ targets = select_least_used_hosts(architectures[version], percentage)
880
+ architectures[version] = targets
881
+ end
882
+ architectures
883
+ end
884
+
885
+ def select_least_used_hosts(hosts, percentage)
886
+ raise('Provided hosts list to select_least_used_hosts is empty') if hosts.empty?
887
+
888
+ average_utilization = get_average_cluster_utilization(hosts)
889
+ least_used_hosts = []
890
+ hosts.each do |host|
891
+ least_used_hosts << host if host[0] <= average_utilization
892
+ end
893
+ hosts_to_select = (hosts.count * (percentage / 100.0)).to_int
894
+ hosts_to_select = hosts.count - 1 if percentage == 100
895
+ least_used_hosts.sort[0..hosts_to_select].map { |host| host[1].name }
896
+ end
897
+
898
+ def find_least_used_hosts(cluster, datacentername, percentage)
899
+ @connection_pool.with_metrics do |pool_object|
900
+ connection = ensured_vsphere_connection(pool_object)
901
+ cluster_object = find_cluster(cluster, connection, datacentername)
902
+ raise("Cluster #{cluster} cannot be found") if cluster_object.nil?
903
+
904
+ target_hosts = get_cluster_host_utilization(cluster_object)
905
+ raise("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory'") if target_hosts.empty?
906
+
907
+ architectures = build_compatible_hosts_lists(target_hosts, percentage)
908
+ least_used_hosts = select_least_used_hosts(target_hosts, percentage)
909
+ {
910
+ 'hosts' => least_used_hosts,
911
+ 'architectures' => architectures
912
+ }
913
+ end
914
+ end
915
+
916
+ def find_host_by_dnsname(connection, dnsname)
917
+ host_object = connection.searchIndex.FindByDnsName(dnsName: dnsname, vmSearch: false)
918
+ return nil if host_object.nil?
919
+
920
+ host_object
921
+ end
922
+
923
+ def find_least_used_host(cluster, connection, datacentername)
924
+ cluster_object = find_cluster(cluster, connection, datacentername)
925
+ target_hosts = get_cluster_host_utilization(cluster_object)
926
+ raise("There is no host candidate in vcenter that meets all the required conditions, check that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory'") if target_hosts.empty?
927
+
928
+ target_hosts.min[1]
929
+ end
930
+
931
+ def find_cluster(cluster, connection, datacentername)
932
+ datacenter = connection.serviceInstance.find_datacenter(datacentername)
933
+ raise("Datacenter #{datacentername} does not exist") if datacenter.nil?
934
+
935
+ # In the event the cluster is not a direct descendent of the
936
+ # datacenter, we use a ContainerView to leverage its recursive
937
+ # search. This will find clusters which are, for example, in
938
+ # folders under the datacenter. This will also find standalone
939
+ # hosts which are not part of a cluster.
940
+ cv = connection.serviceContent.viewManager.CreateContainerView(
941
+ container: datacenter.hostFolder,
942
+ type: ['ComputeResource', 'ClusterComputeResource'],
943
+ recursive: true
944
+ )
945
+ cluster = cv.view.find { |cluster_object| cluster_object.name == cluster }
946
+ cv.DestroyView
947
+ cluster
948
+ end
949
+
950
+ def get_cluster_host_utilization(cluster, model = nil)
951
+ cluster_hosts = []
952
+ cluster.host.each do |host|
953
+ host_usage = get_host_utilization(host, model)
954
+ cluster_hosts << host_usage if host_usage
955
+ end
956
+ cluster_hosts
957
+ end
958
+
959
+ def find_least_used_vpshere_compatible_host(vm)
960
+ source_host = vm.summary.runtime.host
961
+ model = get_host_cpu_arch_version(source_host)
962
+ cluster = source_host.parent
963
+ target_hosts = get_cluster_host_utilization(cluster, model)
964
+ raise("There is no host candidate in vcenter that meets all the required conditions, check that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory'") if target_hosts.empty?
965
+
966
+ target_host = target_hosts.min[1]
967
+ [target_host, target_host.name]
968
+ end
969
+
970
+ def find_snapshot(vm, snapshotname)
971
+ get_snapshot_list(vm.snapshot.rootSnapshotList, snapshotname) if vm.snapshot
972
+ end
973
+
974
+ def build_propSpecs(datacenter, folder, vmname) # rubocop:disable Naming/MethodName
975
+ {
976
+ entity => self,
977
+ :inventoryPath => "#{datacenter}/vm/#{folder}/#{vmname}"
978
+ }
979
+ end
980
+
981
+ def find_vm(pool_name, vmname, connection)
982
+ # Find a VM by its inventory path and return the VM object
983
+ # Returns nil when a VM, or pool configuration, cannot be found
984
+ pool_configuration = pool_config(pool_name)
985
+ return nil if pool_configuration.nil?
986
+
987
+ folder = pool_configuration['folder']
988
+ datacenter = get_target_datacenter_from_config(pool_name)
989
+ return nil if datacenter.nil?
990
+
991
+ propSpecs = { # rubocop:disable Naming/VariableName
992
+ entity: self,
993
+ inventoryPath: "#{datacenter}/vm/#{folder}/#{vmname}"
994
+ }
995
+
996
+ connection.searchIndex.FindByInventoryPath(propSpecs) # rubocop:disable Naming/VariableName
997
+ end
998
+
999
+ def get_base_vm_container_from(connection)
1000
+ view_manager = connection.serviceContent.viewManager
1001
+ view_manager.CreateContainerView(
1002
+ container: connection.serviceContent.rootFolder,
1003
+ recursive: true,
1004
+ type: ['VirtualMachine']
1005
+ )
1006
+ end
1007
+
1008
+ def get_snapshot_list(tree, snapshotname)
1009
+ snapshot = nil
1010
+
1011
+ tree.each do |child|
1012
+ if child.name == snapshotname
1013
+ snapshot ||= child.snapshot
1014
+ else
1015
+ snapshot ||= get_snapshot_list(child.childSnapshotList, snapshotname)
1016
+ end
1017
+ end
1018
+
1019
+ snapshot
1020
+ end
1021
+
1022
+ def get_vm_details(pool_name, vm_name, connection)
1023
+ vm_object = find_vm(pool_name, vm_name, connection)
1024
+ return nil if vm_object.nil?
1025
+
1026
+ parent_host_object = vm_object.summary.runtime.host if vm_object.summary&.runtime && vm_object.summary.runtime.host
1027
+ raise('Unable to determine which host the VM is running on') if parent_host_object.nil?
1028
+
1029
+ parent_host = parent_host_object.name
1030
+ architecture = get_host_cpu_arch_version(parent_host_object)
1031
+ {
1032
+ 'host_name' => parent_host,
1033
+ 'object' => vm_object,
1034
+ 'architecture' => architecture
1035
+ }
1036
+ end
1037
+
1038
+ def migration_enabled?(config)
1039
+ migration_limit = config[:config]['migration_limit']
1040
+ return false unless migration_limit.is_a? Integer
1041
+ return true if migration_limit > 0
1042
+
1043
+ false
1044
+ end
1045
+
1046
+ def migrate_vm(pool_name, vm_name)
1047
+ @connection_pool.with_metrics do |pool_object|
1048
+ begin
1049
+ connection = ensured_vsphere_connection(pool_object)
1050
+ vm_hash = get_vm_details(pool_name, vm_name, connection)
1051
+ @redis.with_metrics do |redis|
1052
+ redis.hset("vmpooler__vm__#{vm_name}", 'host', vm_hash['host_name'])
1053
+ migration_count = redis.scard('vmpooler__migration')
1054
+ migration_limit = @config[:config]['migration_limit'] if @config[:config].key?('migration_limit')
1055
+ if migration_enabled? @config
1056
+ if migration_count >= migration_limit
1057
+ logger.log('s', "[ ] [#{pool_name}] '#{vm_name}' is running on #{vm_hash['host_name']}. No migration will be evaluated since the migration_limit has been reached")
1058
+ break
1059
+ end
1060
+ run_select_hosts(pool_name, @provider_hosts)
1061
+ if vm_in_target?(pool_name, vm_hash['host_name'], vm_hash['architecture'], @provider_hosts)
1062
+ logger.log('s', "[ ] [#{pool_name}] No migration required for '#{vm_name}' running on #{vm_hash['host_name']}")
1063
+ else
1064
+ migrate_vm_to_new_host(pool_name, vm_name, vm_hash, connection)
1065
+ end
1066
+ else
1067
+ logger.log('s', "[ ] [#{pool_name}] '#{vm_name}' is running on #{vm_hash['host_name']}")
1068
+ end
1069
+ end
1070
+ rescue StandardError
1071
+ logger.log('s', "[!] [#{pool_name}] '#{vm_name}' is running on #{vm_hash['host_name']}")
1072
+ raise
1073
+ end
1074
+ end
1075
+ end
1076
+
1077
+ def migrate_vm_to_new_host(pool_name, vm_name, vm_hash, connection)
1078
+ @redis.with_metrics do |redis|
1079
+ redis.sadd('vmpooler__migration', vm_name)
1080
+ end
1081
+ target_host_name = select_next_host(pool_name, @provider_hosts, vm_hash['architecture'])
1082
+ target_host_object = find_host_by_dnsname(connection, target_host_name)
1083
+ finish = migrate_vm_and_record_timing(pool_name, vm_name, vm_hash, target_host_object, target_host_name)
1084
+ @redis.with_metrics do |redis|
1085
+ redis.multi
1086
+ redis.hset("vmpooler__vm__#{vm_name}", 'host', target_host_name)
1087
+ redis.hset("vmpooler__vm__#{vm_name}", 'migrated', true)
1088
+ redis.exec
1089
+ end
1090
+ logger.log('s', "[>] [#{pool_name}] '#{vm_name}' migrated from #{vm_hash['host_name']} to #{target_host_name} in #{finish} seconds")
1091
+ ensure
1092
+ @redis.with_metrics do |redis|
1093
+ redis.srem('vmpooler__migration', vm_name)
1094
+ end
1095
+ end
1096
+
1097
+ def migrate_vm_and_record_timing(pool_name, vm_name, vm_hash, target_host_object, dest_host_name)
1098
+ start = Time.now
1099
+ migrate_vm_host(vm_hash['object'], target_host_object)
1100
+ finish = format('%<time>.2f', time: Time.now - start)
1101
+ metrics.timing("migrate.#{pool_name}", finish)
1102
+ metrics.increment("migrate_from.#{vm_hash['host_name']}")
1103
+ metrics.increment("migrate_to.#{dest_host_name}")
1104
+ @redis.with_metrics do |redis|
1105
+ checkout_to_migration = format('%<time>.2f', time: Time.now - Time.parse(redis.hget("vmpooler__vm__#{vm_name}", 'checkout')))
1106
+ redis.multi
1107
+ redis.hset("vmpooler__vm__#{vm_name}", 'migration_time', finish)
1108
+ redis.hset("vmpooler__vm__#{vm_name}", 'checkout_to_migration', checkout_to_migration)
1109
+ redis.exec
1110
+ end
1111
+ finish
1112
+ end
1113
+
1114
+ def migrate_vm_host(vm_object, host)
1115
+ relospec = RbVmomi::VIM.VirtualMachineRelocateSpec(host: host)
1116
+ vm_object.RelocateVM_Task(spec: relospec).wait_for_completion
1117
+ end
1118
+
1119
+ def create_folder(connection, new_folder, datacenter)
1120
+ dc = connection.serviceInstance.find_datacenter(datacenter)
1121
+ folder_object = dc.vmFolder.traverse(new_folder, RbVmomi::VIM::Folder, true)
1122
+ raise("Cannot create folder #{new_folder}") if folder_object.nil?
1123
+
1124
+ folder_object
1125
+ end
1126
+
1127
+ def find_template_vm(pool, connection)
1128
+ datacenter = get_target_datacenter_from_config(pool['name'])
1129
+ raise('cannot find datacenter') if datacenter.nil?
1130
+
1131
+ propSpecs = { # rubocop:disable Naming/VariableName
1132
+ entity: self,
1133
+ inventoryPath: "#{datacenter}/vm/#{pool['template']}"
1134
+ }
1135
+
1136
+ template_vm_object = connection.searchIndex.FindByInventoryPath(propSpecs) # rubocop:disable Naming/VariableName
1137
+ raise("Pool #{pool['name']} specifies a template VM of #{pool['template']} which does not exist for the provider #{name}") if template_vm_object.nil?
1138
+
1139
+ template_vm_object
1140
+ end
1141
+
1142
+ def create_template_delta_disks(pool)
1143
+ @connection_pool.with_metrics do |pool_object|
1144
+ connection = ensured_vsphere_connection(pool_object)
1145
+ template_vm_object = find_template_vm(pool, connection)
1146
+
1147
+ template_vm_object.add_delta_disk_layer_on_all_disks
1148
+ end
1149
+ end
1150
+
1151
+ def valid_template_path?(template)
1152
+ return false unless template.include?('/')
1153
+ return false if template[0] == '/'
1154
+ return false if template[-1] == '/'
1155
+
1156
+ true
1157
+ end
1158
+
1159
+ def get_disk_backing(pool)
1160
+ return :moveChildMostDiskBacking if linked_clone?(pool)
1161
+
1162
+ :moveAllDiskBackingsAndConsolidate
1163
+ end
1164
+
1165
+ def linked_clone?(pool)
1166
+ return if pool['create_linked_clone'] == false
1167
+ return true if pool['create_linked_clone']
1168
+ return true if @config[:config]['create_linked_clones']
1169
+ end
1170
+ end
1171
+ end
1172
+ end
1173
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VmpoolerProviderVsphere
4
+ VERSION = '1.4.0'
5
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vmpooler-provider-vsphere
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Puppet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rbvmomi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: vmpooler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: climate_control
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.2.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.2.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: mock_redis
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.17.0
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 0.17.0
75
+ - !ruby/object:Gem::Dependency
76
+ name: pry
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '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'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rack-test
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0.6'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0.6'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '3.2'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '3.2'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 1.1.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 1.1.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: simplecov
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 0.11.2
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 0.11.2
145
+ - !ruby/object:Gem::Dependency
146
+ name: thor
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '1.0'
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: 1.0.1
155
+ type: :development
156
+ prerelease: false
157
+ version_requirements: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '1.0'
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: 1.0.1
165
+ - !ruby/object:Gem::Dependency
166
+ name: yarjuf
167
+ requirement: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '2.0'
172
+ type: :development
173
+ prerelease: false
174
+ version_requirements: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '2.0'
179
+ description:
180
+ email:
181
+ - support@puppet.com
182
+ executables: []
183
+ extensions: []
184
+ extra_rdoc_files: []
185
+ files:
186
+ - lib/vmpooler-provider-vsphere/version.rb
187
+ - lib/vmpooler/providers/vsphere.rb
188
+ homepage: https://github.com/puppetlabs/vmpooler-provider-vsphere
189
+ licenses:
190
+ - Apache-2.0
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: 2.3.0
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 2.7.6.2
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: VMware provider for VMPooler
212
+ test_files: []