kitchen-vcenter 2.7.12 → 2.9.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/kitchen-vcenter/version.rb +1 -1
- data/lib/kitchen/driver/vcenter.rb +10 -5
- data/lib/support/clone_vm.rb +45 -89
- data/lib/support/guest_customization.rb +336 -0
- metadata +25 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7424e04d466fa8ed2aea4fbc36f1454ee71e4e5b2bf383490e8f1ec5a1668d5
|
4
|
+
data.tar.gz: 00b915156f7dd91b71d656d24f129015a28e6349a82a06dcb50d4c2a43c38e06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9b8f896730e171635e298173469651183cf2e785bde217e8f49d98870ec9d21606bf3b4b5b4ea2eec762458e616a7705c8ff93471e2986fe01d108a52d5309a
|
7
|
+
data.tar.gz: 72ff746c626c7e479f2e154ef7c2d8f05485cfc95726fd8673a6a27313ff89241307913a3574e77bfd1fd6304406ccc3c506bd636171b03e7ba8aa0294ffb6eb
|
@@ -50,7 +50,7 @@ module Kitchen
|
|
50
50
|
default_config :vm_wait_timeout, 90
|
51
51
|
default_config :vm_wait_interval, 2.0
|
52
52
|
default_config :vm_rollback, false
|
53
|
-
default_config :
|
53
|
+
default_config :vm_customization, nil
|
54
54
|
default_config :guest_customization, nil
|
55
55
|
default_config :interface, nil
|
56
56
|
default_config :active_discovery, false
|
@@ -80,6 +80,10 @@ module Kitchen
|
|
80
80
|
The 'aggressive_password' setting was renamed to 'vm_password' and will
|
81
81
|
be removed in future versions.
|
82
82
|
MSG
|
83
|
+
deprecate_config_for :customize, Util.outdent!(<<-MSG)
|
84
|
+
The `customize` setting was renamed to `vm_customization` and will
|
85
|
+
be removed in future versions.
|
86
|
+
MSG
|
83
87
|
|
84
88
|
# The main create method
|
85
89
|
#
|
@@ -147,7 +151,7 @@ module Kitchen
|
|
147
151
|
|
148
152
|
# Create a hash of options that the clone requires
|
149
153
|
options = {
|
150
|
-
|
154
|
+
vm_name: config[:vm_name],
|
151
155
|
targethost: config[:targethost],
|
152
156
|
poweron: config[:poweron],
|
153
157
|
template: config[:template],
|
@@ -159,7 +163,7 @@ module Kitchen
|
|
159
163
|
interface: config[:interface],
|
160
164
|
wait_timeout: config[:vm_wait_timeout],
|
161
165
|
wait_interval: config[:vm_wait_interval],
|
162
|
-
|
166
|
+
vm_customization: config[:vm_customization],
|
163
167
|
guest_customization: config[:guest_customization],
|
164
168
|
active_discovery: config[:active_discovery],
|
165
169
|
active_discovery_command: config[:active_discovery_command],
|
@@ -178,7 +182,7 @@ module Kitchen
|
|
178
182
|
new_vm.clone
|
179
183
|
|
180
184
|
state[:hostname] = new_vm.ip
|
181
|
-
state[:vm_name] = new_vm.
|
185
|
+
state[:vm_name] = new_vm.vm_name
|
182
186
|
|
183
187
|
rescue # Kitchen::ActionFailed => e
|
184
188
|
if config[:vm_rollback] == true
|
@@ -280,6 +284,7 @@ module Kitchen
|
|
280
284
|
config[:vm_os] = config[:aggressive_os] unless config[:aggressive_os].nil?
|
281
285
|
config[:vm_username] = config[:aggressive_username] unless config[:aggressive_username].nil?
|
282
286
|
config[:vm_password] = config[:aggressive_password] unless config[:aggressive_password].nil?
|
287
|
+
config[:vm_customization] = config[:customize] unless config[:customize].nil?
|
283
288
|
end
|
284
289
|
|
285
290
|
# A helper method to validate the state
|
@@ -383,7 +388,7 @@ module Kitchen
|
|
383
388
|
#
|
384
389
|
# @param [name] name is the name of the Cluster
|
385
390
|
def get_cluster_id(name)
|
386
|
-
return
|
391
|
+
return if name.nil?
|
387
392
|
|
388
393
|
cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
|
389
394
|
clusters = cluster_api.list({ filter_names: name }).value
|
data/lib/support/clone_vm.rb
CHANGED
@@ -1,20 +1,25 @@
|
|
1
1
|
require "kitchen"
|
2
2
|
require "rbvmomi"
|
3
|
+
|
4
|
+
require_relative "guest_customization"
|
3
5
|
require_relative "guest_operations"
|
4
6
|
|
5
7
|
class Support
|
6
8
|
class CloneError < RuntimeError; end
|
7
9
|
|
8
10
|
class CloneVm
|
9
|
-
attr_reader :vim, :options, :ssl_verify, :vm, :
|
11
|
+
attr_reader :vim, :vem, :options, :ssl_verify, :src_vm, :vm, :vm_name, :ip, :guest_auth, :username
|
12
|
+
|
13
|
+
include GuestCustomization
|
10
14
|
|
11
15
|
def initialize(conn_opts, options)
|
12
16
|
@options = options
|
13
|
-
@
|
17
|
+
@vm_name = options[:vm_name]
|
14
18
|
@ssl_verify = !conn_opts[:insecure]
|
15
19
|
|
16
20
|
# Connect to vSphere
|
17
21
|
@vim ||= RbVmomi::VIM.connect conn_opts
|
22
|
+
@vem ||= vim.serviceContent.eventManager
|
18
23
|
|
19
24
|
@username = options[:vm_username]
|
20
25
|
password = options[:vm_password]
|
@@ -156,8 +161,8 @@ class Support
|
|
156
161
|
Kitchen.logger.debug format("Benchmark: Appended data to file %s", benchmark_file)
|
157
162
|
end
|
158
163
|
|
159
|
-
def detect_os
|
160
|
-
|
164
|
+
def detect_os(vm_or_template)
|
165
|
+
vm_or_template.config&.guestId&.match(/^win/) ? :windows : :linux
|
161
166
|
end
|
162
167
|
|
163
168
|
def windows?
|
@@ -318,14 +323,14 @@ class Support
|
|
318
323
|
end
|
319
324
|
end
|
320
325
|
|
321
|
-
def
|
322
|
-
Kitchen.logger.info "Waiting for
|
326
|
+
def vm_customization
|
327
|
+
Kitchen.logger.info "Waiting for VM customization..."
|
323
328
|
|
324
329
|
# Pass some contents right through
|
325
330
|
# https://pubs.vmware.com/vsphere-6-5/index.jsp?topic=%2Fcom.vmware.wssdk.smssdk.doc%2Fvim.vm.ConfigSpec.html
|
326
|
-
config = options[:
|
331
|
+
config = options[:vm_customization].select { |key, _| %i{annotation memoryMB numCPUs}.include? key }
|
327
332
|
|
328
|
-
add_disks = options[:
|
333
|
+
add_disks = options[:vm_customization]&.fetch(:add_disks, nil)
|
329
334
|
unless add_disks.nil?
|
330
335
|
config[:deviceChange] = []
|
331
336
|
|
@@ -365,7 +370,7 @@ class Support
|
|
365
370
|
|
366
371
|
disk_spec.device.controllerKey = controller.key
|
367
372
|
|
368
|
-
highest_id = vm.disks.map(&:unitNumber).
|
373
|
+
highest_id = vm.disks.map(&:unitNumber).max
|
369
374
|
next_id = highest_id + idx + 1
|
370
375
|
|
371
376
|
# Avoid the SCSI controller ID
|
@@ -425,7 +430,10 @@ class Support
|
|
425
430
|
def find_datacenter
|
426
431
|
vim.serviceInstance.find_datacenter(datacenter)
|
427
432
|
rescue RbVmomi::Fault
|
428
|
-
root_folder.
|
433
|
+
dc = root_folder.findByInventoryPath(datacenter)
|
434
|
+
return dc if dc.is_a?(RbVmomi::VIM::Datacenter)
|
435
|
+
|
436
|
+
raise Support::CloneError.new("Unable to locate datacenter at '#{datacenter}'")
|
429
437
|
end
|
430
438
|
|
431
439
|
def ip?(string)
|
@@ -435,67 +443,16 @@ class Support
|
|
435
443
|
false
|
436
444
|
end
|
437
445
|
|
438
|
-
def
|
439
|
-
unless
|
440
|
-
return false
|
441
|
-
end
|
442
|
-
|
443
|
-
unless ip?(options[:guest_customization][:ip_address])
|
444
|
-
raise Support::CloneError.new("Guest customization error: ip_address is required to be formatted as an IPv4 address")
|
445
|
-
end
|
446
|
-
|
447
|
-
unless ip?(options[:guest_customization][:subnet_mask])
|
448
|
-
raise Support::CloneError.new("Guest customization error: subnet_mask is required to be formatted as an IPv4 address")
|
449
|
-
end
|
450
|
-
|
451
|
-
options[:guest_customization][:gateway].each do |v|
|
452
|
-
unless ip?(v)
|
453
|
-
raise Support::CloneError.new("Guest customization error: gateway is required to be formatted as an IPv4 address")
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
options[:guest_customization][:dns_server_list].each do |v|
|
458
|
-
unless ip?(v)
|
459
|
-
raise Support::CloneError.new("Guest customization error: dns_server_list is required to be formatted as an IPv4 address")
|
460
|
-
end
|
461
|
-
end
|
462
|
-
|
463
|
-
unless %i{dns_domain timezone dns_server_list dns_suffix_list ip_address gateway subnet_mask}.all? { |k| options[:guest_customization].key? k }
|
464
|
-
raise Support::CloneError.new("Guest customization error: currently all options are required to support guest customization")
|
465
|
-
end
|
466
|
-
|
467
|
-
if !options[:guest_customization][:dns_server_list].is_a?(Array)
|
468
|
-
raise Support::CloneError.new("Guest customization error: dns_server_list must be an array")
|
469
|
-
elsif !options[:guest_customization][:dns_suffix_list].is_a?(Array)
|
470
|
-
raise Support::CloneError.new("Guest customization error: dns_suffix_list must be an array")
|
471
|
-
elsif !options[:guest_customization][:gateway].is_a?(Array)
|
472
|
-
raise Support::CloneError.new("Guest customization error: gateway must be an array")
|
473
|
-
end
|
446
|
+
def vm_events(event_types = [])
|
447
|
+
raise Support::CloneError.new("`vm_events` called before VM clone") unless vm
|
474
448
|
|
475
|
-
RbVmomi::VIM::
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
name: name
|
480
|
-
),
|
481
|
-
hwClockUTC: true,
|
482
|
-
timeZone: options[:guest_customization][:timezone]
|
483
|
-
),
|
484
|
-
globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
|
485
|
-
dnsServerList: options[:guest_customization][:dns_server_list],
|
486
|
-
dnsSuffixList: options[:guest_customization][:dns_suffix_list]
|
449
|
+
vem.QueryEvents(filter: RbVmomi::VIM::EventFilterSpec(
|
450
|
+
entity: RbVmomi::VIM::EventFilterSpecByEntity(
|
451
|
+
entity: vm,
|
452
|
+
recursion: RbVmomi::VIM::EventFilterSpecRecursionOption(:self)
|
487
453
|
),
|
488
|
-
|
489
|
-
|
490
|
-
ip: RbVmomi::VIM::CustomizationFixedIp(
|
491
|
-
ipAddress: options[:guest_customization][:ip_address]
|
492
|
-
),
|
493
|
-
gateway: options[:guest_customization][:gateway],
|
494
|
-
subnetMask: options[:guest_customization][:subnet_mask],
|
495
|
-
dnsDomain: options[:guest_customization][:dns_domain]
|
496
|
-
)
|
497
|
-
)]
|
498
|
-
)
|
454
|
+
eventTypeId: event_types
|
455
|
+
))
|
499
456
|
end
|
500
457
|
|
501
458
|
def clone
|
@@ -504,12 +461,9 @@ class Support
|
|
504
461
|
# set the datacenter name
|
505
462
|
dc = find_datacenter
|
506
463
|
|
507
|
-
# get guest customization spec
|
508
|
-
guest_customization = customization_spec
|
509
|
-
|
510
464
|
# reference template using full inventory path
|
511
465
|
inventory_path = format("/%s/vm/%s", datacenter, options[:template])
|
512
|
-
src_vm = root_folder.findByInventoryPath(inventory_path)
|
466
|
+
@src_vm = root_folder.findByInventoryPath(inventory_path)
|
513
467
|
raise Support::CloneError.new(format("Unable to find template: %s", options[:template])) if src_vm.nil?
|
514
468
|
|
515
469
|
if src_vm.config.template && !full_clone?
|
@@ -522,6 +476,13 @@ class Support
|
|
522
476
|
options[:clone_type] = :full
|
523
477
|
end
|
524
478
|
|
479
|
+
# Autodetect OS, if none given
|
480
|
+
if options[:vm_os].nil?
|
481
|
+
os = detect_os(src_vm)
|
482
|
+
Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize)
|
483
|
+
options[:vm_os] = os
|
484
|
+
end
|
485
|
+
|
525
486
|
# Specify where the machine is going to be created
|
526
487
|
relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec
|
527
488
|
|
@@ -620,23 +581,21 @@ class Support
|
|
620
581
|
]
|
621
582
|
|
622
583
|
clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec,
|
623
|
-
name:
|
584
|
+
name: vm_name)
|
624
585
|
|
625
586
|
benchmark_checkpoint("initialized") if benchmark?
|
626
587
|
task = src_vm.InstantClone_Task(spec: clone_spec)
|
627
588
|
else
|
628
589
|
clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(
|
629
590
|
location: relocate_spec,
|
630
|
-
powerOn: options[:poweron] && options[:
|
591
|
+
powerOn: options[:poweron] && options[:vm_customization].nil?,
|
631
592
|
template: false
|
632
593
|
)
|
633
594
|
|
634
|
-
if guest_customization
|
635
|
-
clone_spec.customization = guest_customization
|
636
|
-
end
|
595
|
+
clone_spec.customization = guest_customization_spec if options[:guest_customization]
|
637
596
|
|
638
597
|
benchmark_checkpoint("initialized") if benchmark?
|
639
|
-
task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name:
|
598
|
+
task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name: vm_name)
|
640
599
|
end
|
641
600
|
task.wait_for_completion
|
642
601
|
|
@@ -644,31 +603,28 @@ class Support
|
|
644
603
|
|
645
604
|
# get the IP address of the machine for bootstrapping
|
646
605
|
# machine name is based on the path, e.g. that includes the folder
|
647
|
-
path = options[:folder].nil? ?
|
606
|
+
path = options[:folder].nil? ? vm_name : format("%s/%s", options[:folder][:name], vm_name)
|
648
607
|
@vm = dc.find_vm(path)
|
649
608
|
raise Support::CloneError.new(format("Unable to find machine: %s", path)) if vm.nil?
|
650
609
|
|
651
|
-
if options[:vm_os].nil?
|
652
|
-
os = detect_os
|
653
|
-
Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize)
|
654
|
-
options[:vm_os] = os
|
655
|
-
end
|
656
|
-
|
657
610
|
# Reconnect network device after Instant Clone is ready
|
658
611
|
if instant_clone?
|
659
612
|
Kitchen.logger.info "Reconnecting network adapter"
|
660
613
|
reconnect_network_device(vm)
|
661
614
|
end
|
662
615
|
|
663
|
-
|
616
|
+
vm_customization if options[:vm_customization]
|
664
617
|
|
665
618
|
# Start only if specified or customizations wanted; no need for instant clones as they start in running state
|
666
|
-
if options[:poweron] && !options[:
|
619
|
+
if options[:poweron] && !options[:vm_customization].nil? && !instant_clone?
|
667
620
|
task = vm.PowerOnVM_Task
|
668
621
|
task.wait_for_completion
|
669
622
|
end
|
670
623
|
benchmark_checkpoint("powered_on") if benchmark?
|
671
624
|
|
625
|
+
# Windows customization takes a while, so check for its completion
|
626
|
+
guest_customization_wait if options[:guest_customization]
|
627
|
+
|
672
628
|
Kitchen.logger.info format("Waiting for VMware tools to become available (timeout: %d seconds)...", options[:wait_timeout])
|
673
629
|
wait_for_tools(options[:wait_timeout], options[:wait_interval])
|
674
630
|
|
@@ -676,7 +632,7 @@ class Support
|
|
676
632
|
benchmark_checkpoint("ip_detected") if benchmark?
|
677
633
|
|
678
634
|
benchmark_persist if benchmark?
|
679
|
-
Kitchen.logger.info format("Created machine %s with IP %s",
|
635
|
+
Kitchen.logger.info format("Created machine %s with IP %s", vm_name, ip)
|
680
636
|
end
|
681
637
|
end
|
682
638
|
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
require "net/ping"
|
2
|
+
require "rbvmomi"
|
3
|
+
|
4
|
+
class Support
|
5
|
+
class GuestCustomizationError < RuntimeError; end
|
6
|
+
class GuestCustomizationOptionsError < RuntimeError; end
|
7
|
+
|
8
|
+
module GuestCustomization
|
9
|
+
DEFAULT_LINUX_TIMEZONE = "Etc/UTC".freeze
|
10
|
+
DEFAULT_WINDOWS_ORG = "TestKitchen".freeze
|
11
|
+
DEFAULT_WINDOWS_TIMEZONE = 0x80000050 # Etc/UTC
|
12
|
+
DEFAULT_TIMEOUT_TASK = 600
|
13
|
+
DEFAULT_TIMEOUT_IP = 60
|
14
|
+
|
15
|
+
# Generic Volume License Keys for temporary Windows Server setup.
|
16
|
+
#
|
17
|
+
# @see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys
|
18
|
+
WINDOWS_KMS_KEYS = {
|
19
|
+
"Microsoft Windows Server 2019 (64-bit)" => "N69G4-B89J2-4G8F4-WWYCC-J464C",
|
20
|
+
"Microsoft Windows Server 2016 (64-bit)" => "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY",
|
21
|
+
"Microsoft Windows Server 2012R2 (64-bit)" => "D2N9P-3P6X9-2R39C-7RTCD-MDVJX",
|
22
|
+
"Microsoft Windows Server 2012 (64-bit)" => "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
# Configuration values for Guest Customization
|
26
|
+
#
|
27
|
+
# @returns [Hash] Configuration values from file
|
28
|
+
def guest_customization
|
29
|
+
options[:guest_customization]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Build CustomizationSpec for Guest OS Customization
|
33
|
+
#
|
34
|
+
# @returns [RbVmomi::VIM::CustomizationSpec] Customization Spec for guest adjustments
|
35
|
+
def guest_customization_spec
|
36
|
+
return unless guest_customization
|
37
|
+
|
38
|
+
guest_customization_validate_options
|
39
|
+
|
40
|
+
if guest_customization[:ip_address]
|
41
|
+
customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
|
42
|
+
ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: guest_customization[:ip_address]),
|
43
|
+
gateway: guest_customization[:gateway],
|
44
|
+
subnetMask: guest_customization[:subnet_mask],
|
45
|
+
dnsDomain: guest_customization[:dns_domain]
|
46
|
+
)
|
47
|
+
else
|
48
|
+
customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
|
49
|
+
ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new,
|
50
|
+
dnsDomain: guest_customization[:dns_domain]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
RbVmomi::VIM::CustomizationSpec.new(
|
55
|
+
identity: guest_customization_identity,
|
56
|
+
globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
|
57
|
+
dnsServerList: guest_customization[:dns_server_list],
|
58
|
+
dnsSuffixList: guest_customization[:dns_suffix_list]
|
59
|
+
),
|
60
|
+
nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new(
|
61
|
+
adapter: customized_ip
|
62
|
+
)]
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check options for existance and format
|
67
|
+
#
|
68
|
+
# @raise [Support::GuestCustomizationOptionsError] For any violation
|
69
|
+
def guest_customization_validate_options
|
70
|
+
if guest_customization_ip_change?
|
71
|
+
unless ip?(guest_customization[:ip_address])
|
72
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` is required to be formatted as an IPv4 address")
|
73
|
+
end
|
74
|
+
|
75
|
+
unless guest_customization[:subnet_mask]
|
76
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required if assigning a fixed IPv4 address")
|
77
|
+
end
|
78
|
+
|
79
|
+
unless ip?(guest_customization[:subnet_mask])
|
80
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required to be formatted as an IPv4 address")
|
81
|
+
end
|
82
|
+
|
83
|
+
if up?(guest_customization[:ip_address])
|
84
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` points to a host reachable via ICMP") unless guest_customization[:continue_on_ip_conflict]
|
85
|
+
|
86
|
+
Kitchen.logger.warn("Continuing customization despite `ip_address` conflicting with a reachable host per user request")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if guest_customization[:gateway]
|
91
|
+
unless guest_customization[:gateway].is_a?(Array)
|
92
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` must be an array")
|
93
|
+
end
|
94
|
+
|
95
|
+
guest_customization[:gateway].each do |v|
|
96
|
+
unless ip?(v)
|
97
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` is required to be formatted as an IPv4 address")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
required = %i{dns_domain dns_server_list dns_suffix_list}
|
103
|
+
missing = required - guest_customization.keys
|
104
|
+
unless missing.empty?
|
105
|
+
raise Support::GuestCustomizationOptionsError.new("Parameters `#{missing.join("`, `")}` are required to support guest customization")
|
106
|
+
end
|
107
|
+
|
108
|
+
guest_customization[:dns_server_list].each do |v|
|
109
|
+
unless ip?(v)
|
110
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` is required to be formatted as an IPv4 address")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if !guest_customization[:dns_server_list].is_a?(Array)
|
115
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` must be an array")
|
116
|
+
elsif !guest_customization[:dns_suffix_list].is_a?(Array)
|
117
|
+
raise Support::GuestCustomizationOptionsError.new("Parameter `dns_suffix_list` must be an array")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Check if an IP change is requested
|
122
|
+
#
|
123
|
+
# @returns [Boolean] If `ip_address` is to be changed
|
124
|
+
def guest_customization_ip_change?
|
125
|
+
guest_customization[:ip_address]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return OS-specific CustomizationIdentity object
|
129
|
+
def guest_customization_identity
|
130
|
+
if linux?
|
131
|
+
guest_customization_identity_linux
|
132
|
+
elsif windows?
|
133
|
+
guest_customization_identity_windows
|
134
|
+
else
|
135
|
+
raise Support::GuestCustomizationError.new("Unknown OS, no valid customization found")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Construct Linux-specific customization information
|
140
|
+
def guest_customization_identity_linux
|
141
|
+
timezone = guest_customization[:timezone]
|
142
|
+
if timezone && !valid_linux_timezone?(timezone)
|
143
|
+
raise Support::GuestCustomizationError.new <<~ERROR
|
144
|
+
Linux customization requires `timezone` in `Area/Location` format.
|
145
|
+
See https://kb.vmware.com/s/article/2145518
|
146
|
+
ERROR
|
147
|
+
end
|
148
|
+
|
149
|
+
Kitchen.logger.warn("Linux guest customization: No timezone passed, assuming UTC") unless timezone
|
150
|
+
|
151
|
+
RbVmomi::VIM::CustomizationLinuxPrep.new(
|
152
|
+
domain: guest_customization[:dns_domain],
|
153
|
+
hostName: guest_hostname,
|
154
|
+
hwClockUTC: true,
|
155
|
+
timeZone: timezone || DEFAULT_LINUX_TIMEZONE
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Construct Windows-specific customization information
|
160
|
+
def guest_customization_identity_windows
|
161
|
+
timezone = guest_customization[:timezone]
|
162
|
+
if timezone && !valid_windows_timezone?(timezone)
|
163
|
+
raise Support::GuestCustomizationOptionsError.new <<~ERROR
|
164
|
+
Windows customization requires `timezone` as decimal number or hex number (0x55).
|
165
|
+
See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
|
166
|
+
ERROR
|
167
|
+
end
|
168
|
+
|
169
|
+
Kitchen.logger.warn("Windows guest customization: No timezone passed, assuming UTC") unless timezone
|
170
|
+
|
171
|
+
product_id = guest_customization[:product_id]
|
172
|
+
|
173
|
+
# Try to look up and use a known, documented 120-day trial key
|
174
|
+
unless product_id
|
175
|
+
guest_os = src_vm.guest&.guestFullName
|
176
|
+
product_id = windows_kms_for_guest(guest_os)
|
177
|
+
|
178
|
+
Kitchen.logger.warn format("Windows guest customization:: Using KMS Key `%<key>s` for %<os>s", key: product_id, os: guest_os) if product_id
|
179
|
+
end
|
180
|
+
|
181
|
+
unless valid_windows_key? product_id
|
182
|
+
raise Support::GuestCustomizationOptionsError.new <<~ERROR
|
183
|
+
Windows customization requires `product_id` to work. Add a valid product key or
|
184
|
+
see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys for KMS trial keys
|
185
|
+
ERROR
|
186
|
+
end
|
187
|
+
|
188
|
+
customization_pass = nil
|
189
|
+
if guest_customization[:administrator_password]
|
190
|
+
customization_pass = RbVmomi::VIM::CustomizationPassword.new(
|
191
|
+
plainText: true,
|
192
|
+
value: guest_customization[:administrator_password]
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
RbVmomi::VIM::CustomizationSysprep.new(
|
197
|
+
guiUnattended: RbVmomi::VIM::CustomizationGuiUnattended.new(
|
198
|
+
timeZone: timezone.to_i || DEFAULT_WINDOWS_TIMEZONE,
|
199
|
+
autoLogon: false,
|
200
|
+
autoLogonCount: 1,
|
201
|
+
password: customization_pass
|
202
|
+
),
|
203
|
+
identification: RbVmomi::VIM::CustomizationIdentification.new,
|
204
|
+
userData: RbVmomi::VIM::CustomizationUserData.new(
|
205
|
+
computerName: guest_hostname,
|
206
|
+
fullName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
|
207
|
+
orgName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
|
208
|
+
productId: product_id
|
209
|
+
)
|
210
|
+
)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Check if a host is reachable
|
214
|
+
def up?(host)
|
215
|
+
check = Net::Ping::External.new(host)
|
216
|
+
check.ping?
|
217
|
+
end
|
218
|
+
|
219
|
+
# Retrieve a GVLK (evaluation key) for the named OS
|
220
|
+
#
|
221
|
+
# @param [String] name Name of the OS as reported by VMware
|
222
|
+
# @returns [String] GVLK key, if any
|
223
|
+
def windows_kms_for_guest(name)
|
224
|
+
WINDOWS_KMS_KEYS.fetch(name, false)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Check format of Linux-specific timezone, according to VMware support
|
228
|
+
#
|
229
|
+
# @param [Integer] input Value to check for validity
|
230
|
+
# @returns [Boolean] if value is valid
|
231
|
+
def valid_linux_timezone?(input)
|
232
|
+
# Specific to VMware: https://kb.vmware.com/s/article/2145518
|
233
|
+
linux_timezone_pattern = %r{^[A-Z][A-Za-z]+\/[A-Z][-_+A-Za-z0-9]+$}
|
234
|
+
|
235
|
+
input.to_s.match? linux_timezone_pattern
|
236
|
+
end
|
237
|
+
|
238
|
+
# Check format of Windows-specific timezone
|
239
|
+
#
|
240
|
+
# @param [Integer] input Value to check for validity
|
241
|
+
# @returns [Boolean] if value is valid
|
242
|
+
def valid_windows_timezone?(input)
|
243
|
+
# Accept decimals and hex
|
244
|
+
# See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
|
245
|
+
windows_timezone_pattern = /^([0-9]+|0x[0-9a-fA-F]+)$/
|
246
|
+
|
247
|
+
input.to_s.match? windows_timezone_pattern
|
248
|
+
end
|
249
|
+
|
250
|
+
# Check for format of Windows Product IDs
|
251
|
+
#
|
252
|
+
# @param [String] input String to check
|
253
|
+
# @returns [Boolean] if value is in Windows Key format
|
254
|
+
def valid_windows_key?(input)
|
255
|
+
windows_key_pattern = /^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/
|
256
|
+
|
257
|
+
input.to_s.match? windows_key_pattern
|
258
|
+
end
|
259
|
+
|
260
|
+
# Return Guest hostname to be configured and check for validity.
|
261
|
+
#
|
262
|
+
# @returns [String] New hostname to assign
|
263
|
+
def guest_hostname
|
264
|
+
hostname = guest_customization[:hostname] || options[:vm_name]
|
265
|
+
|
266
|
+
hostname_pattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/
|
267
|
+
unless hostname.match?(hostname_pattern)
|
268
|
+
raise Support::GuestCustomizationError.new("Only letters, numbers or hyphens in hostnames allowed")
|
269
|
+
end
|
270
|
+
|
271
|
+
RbVmomi::VIM::CustomizationFixedName.new(name: hostname)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Wait for vSphere task completion and subsequent IP address update (if any).
|
275
|
+
def guest_customization_wait
|
276
|
+
guest_customization_wait_task(guest_customization[:timeout_task] || DEFAULT_TIMEOUT_TASK)
|
277
|
+
guest_customization_wait_ip(guest_customization[:timeout_ip] || DEFAULT_TIMEOUT_IP)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Wait for Guest customization to finish successfully.
|
281
|
+
#
|
282
|
+
# @param [Integer] timeout Timeout in seconds
|
283
|
+
# @param [Integer] sleep_time Time to wait between tries
|
284
|
+
def guest_customization_wait_task(timeout = 600, sleep_time = 10)
|
285
|
+
waited_seconds = 0
|
286
|
+
|
287
|
+
Kitchen.logger.info "Waiting for guest customization (timeout: #{timeout} seconds)..."
|
288
|
+
|
289
|
+
while waited_seconds < timeout
|
290
|
+
events = guest_customization_events
|
291
|
+
|
292
|
+
if events.any? { |event| event.is_a? RbVmomi::VIM::CustomizationSucceeded }
|
293
|
+
return
|
294
|
+
elsif (failed = events.detect { |event| event.is_a? RbVmomi::VIM::CustomizationFailed })
|
295
|
+
# Only matters for Linux, as Windows won't come up at all to report a failure via VMware Tools
|
296
|
+
raise Support::GuestCustomizationError.new("Customization of VM failed: #{failed.fullFormattedMessage}")
|
297
|
+
end
|
298
|
+
|
299
|
+
sleep(sleep_time)
|
300
|
+
waited_seconds += sleep_time
|
301
|
+
end
|
302
|
+
|
303
|
+
raise Support::GuestCustomizationError.new("Customization of VM did not complete within #{timeout} seconds.")
|
304
|
+
end
|
305
|
+
|
306
|
+
# Wait for new IP to be reported, if any.
|
307
|
+
#
|
308
|
+
# @param [Integer] timeout Timeout in seconds. Tools report every 30 seconds, Default: 30 seconds
|
309
|
+
# @param [Integer] sleep_time Time to wait between tries
|
310
|
+
def guest_customization_wait_ip(timeout = 30, sleep_time = 1)
|
311
|
+
return unless guest_customization_ip_change?
|
312
|
+
|
313
|
+
waited_seconds = 0
|
314
|
+
|
315
|
+
Kitchen.logger.info "Waiting for guest customization IP update..."
|
316
|
+
|
317
|
+
while waited_seconds < timeout
|
318
|
+
found_ip = wait_for_ip(timeout, 1.0)
|
319
|
+
|
320
|
+
return if found_ip == guest_customization[:ip_address]
|
321
|
+
|
322
|
+
sleep(sleep_time)
|
323
|
+
waited_seconds += sleep_time
|
324
|
+
end
|
325
|
+
|
326
|
+
raise Support::GuestCustomizationError.new("Customized IP was not reported within #{timeout} seconds.")
|
327
|
+
end
|
328
|
+
|
329
|
+
# Filter Customization events for the current VM
|
330
|
+
#
|
331
|
+
# @returns [Array<RbVmomi::VIM::CustomizationEvent>] All matching events
|
332
|
+
def guest_customization_events
|
333
|
+
vm_events %w{CustomizationSucceeded CustomizationFailed CustomizationStartedEvent}
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kitchen-vcenter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.9.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chef Software
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ping
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.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.0.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: rbvmomi
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -75,6 +95,7 @@ files:
|
|
75
95
|
- lib/kitchen-vcenter/version.rb
|
76
96
|
- lib/kitchen/driver/vcenter.rb
|
77
97
|
- lib/support/clone_vm.rb
|
98
|
+
- lib/support/guest_customization.rb
|
78
99
|
- lib/support/guest_operations.rb
|
79
100
|
homepage: https://github.com/chef/kitchen-vcenter
|
80
101
|
licenses:
|
@@ -95,8 +116,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
116
|
- !ruby/object:Gem::Version
|
96
117
|
version: '0'
|
97
118
|
requirements: []
|
98
|
-
rubygems_version: 3.
|
119
|
+
rubygems_version: 3.1.4
|
99
120
|
signing_key:
|
100
121
|
specification_version: 4
|
101
|
-
summary: Test Kitchen driver for
|
122
|
+
summary: Test Kitchen driver for VMware vCenter
|
102
123
|
test_files: []
|