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