kitchen-vcenter 2.8.6 → 2.9.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 +4 -4
- data/lib/kitchen-vcenter/version.rb +1 -1
- data/lib/kitchen/driver/vcenter.rb +9 -4
- data/lib/support/clone_vm.rb +40 -106
- data/lib/support/guest_customization.rb +327 -0
- metadata +24 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d91703414f7cc8c9c377a7e1ef6e322b716c90b61c77cfbd671da0053ac4e666
|
|
4
|
+
data.tar.gz: 42d6e401ffca66e898a98df2d8924243e925a28c34cef635e50885a84e8f1c76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9075327273580d4ff320138e067b5691e551bc79d45728ede0057c4bb0b57b8cbeb6e63d7cf958ef06e69fe11f3e4ff885b37887829b022a764c4e44aa15f60
|
|
7
|
+
data.tar.gz: 22ae34bfd2071b727b1ea4fec269246080021cac4dec31f83c3687faea5c1d8e650a36a5b1420c3a6e74ac081f4afc2e33e446e2e463c94ef8c462119dfdb057
|
|
@@ -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
|
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
|
|
|
@@ -438,86 +443,16 @@ class Support
|
|
|
438
443
|
false
|
|
439
444
|
end
|
|
440
445
|
|
|
441
|
-
def
|
|
442
|
-
unless
|
|
443
|
-
return false
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
if options[:guest_customization][:ip_address]
|
|
447
|
-
unless ip?(options[:guest_customization][:ip_address])
|
|
448
|
-
raise Support::CloneError.new("Guest customization error: ip_address is required to be formatted as an IPv4 address")
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
unless options[:guest_customization][:subnet_mask]
|
|
452
|
-
raise Support::CloneError.new("Guest customization error: subnet_mask is required if assigning a fixed IPv4 address")
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
unless ip?(options[:guest_customization][:subnet_mask])
|
|
456
|
-
raise Support::CloneError.new("Guest customization error: subnet_mask is required to be formatted as an IPv4 address")
|
|
457
|
-
end
|
|
458
|
-
end
|
|
446
|
+
def vm_events(event_types = [])
|
|
447
|
+
raise Support::CloneError.new("`vm_events` called before VM clone") unless vm
|
|
459
448
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
options[:guest_customization][:gateway].each do |v|
|
|
466
|
-
unless ip?(v)
|
|
467
|
-
raise Support::CloneError.new("Guest customization error: gateway is required to be formatted as an IPv4 address")
|
|
468
|
-
end
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
required = %i{dns_domain timezone dns_server_list dns_suffix_list}
|
|
473
|
-
missing = required - options[:guest_customization].keys
|
|
474
|
-
unless missing.empty?
|
|
475
|
-
raise Support::CloneError.new("Guest customization error: #{missing.join(", ")} are required to support guest customization")
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
options[:guest_customization][:dns_server_list].each do |v|
|
|
479
|
-
unless ip?(v)
|
|
480
|
-
raise Support::CloneError.new("Guest customization error: dns_server_list is required to be formatted as an IPv4 address")
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
if !options[:guest_customization][:dns_server_list].is_a?(Array)
|
|
485
|
-
raise Support::CloneError.new("Guest customization error: dns_server_list must be an array")
|
|
486
|
-
elsif !options[:guest_customization][:dns_suffix_list].is_a?(Array)
|
|
487
|
-
raise Support::CloneError.new("Guest customization error: dns_suffix_list must be an array")
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
if options[:guest_customization][:ip_address]
|
|
491
|
-
customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
|
|
492
|
-
ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: options[:guest_customization][:ip_address]),
|
|
493
|
-
gateway: options[:guest_customization][:gateway],
|
|
494
|
-
subnetMask: options[:guest_customization][:subnet_mask],
|
|
495
|
-
dnsDomain: options[:guest_customization][:dns_domain]
|
|
496
|
-
)
|
|
497
|
-
else
|
|
498
|
-
customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
|
|
499
|
-
ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new,
|
|
500
|
-
dnsDomain: options[:guest_customization][:dns_domain]
|
|
501
|
-
)
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
RbVmomi::VIM::CustomizationSpec.new(
|
|
505
|
-
identity: RbVmomi::VIM::CustomizationLinuxPrep.new(
|
|
506
|
-
domain: options[:guest_customization][:dns_domain],
|
|
507
|
-
hostName: RbVmomi::VIM::CustomizationFixedName.new(
|
|
508
|
-
name: name
|
|
509
|
-
),
|
|
510
|
-
hwClockUTC: true,
|
|
511
|
-
timeZone: options[:guest_customization][:timezone]
|
|
512
|
-
),
|
|
513
|
-
globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
|
|
514
|
-
dnsServerList: options[:guest_customization][:dns_server_list],
|
|
515
|
-
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)
|
|
516
453
|
),
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
)]
|
|
520
|
-
)
|
|
454
|
+
eventTypeId: event_types
|
|
455
|
+
))
|
|
521
456
|
end
|
|
522
457
|
|
|
523
458
|
def clone
|
|
@@ -526,12 +461,9 @@ class Support
|
|
|
526
461
|
# set the datacenter name
|
|
527
462
|
dc = find_datacenter
|
|
528
463
|
|
|
529
|
-
# get guest customization spec
|
|
530
|
-
guest_customization = customization_spec
|
|
531
|
-
|
|
532
464
|
# reference template using full inventory path
|
|
533
465
|
inventory_path = format("/%s/vm/%s", datacenter, options[:template])
|
|
534
|
-
src_vm = root_folder.findByInventoryPath(inventory_path)
|
|
466
|
+
@src_vm = root_folder.findByInventoryPath(inventory_path)
|
|
535
467
|
raise Support::CloneError.new(format("Unable to find template: %s", options[:template])) if src_vm.nil?
|
|
536
468
|
|
|
537
469
|
if src_vm.config.template && !full_clone?
|
|
@@ -544,6 +476,13 @@ class Support
|
|
|
544
476
|
options[:clone_type] = :full
|
|
545
477
|
end
|
|
546
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
|
+
|
|
547
486
|
# Specify where the machine is going to be created
|
|
548
487
|
relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec
|
|
549
488
|
|
|
@@ -642,23 +581,21 @@ class Support
|
|
|
642
581
|
]
|
|
643
582
|
|
|
644
583
|
clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec,
|
|
645
|
-
name:
|
|
584
|
+
name: vm_name)
|
|
646
585
|
|
|
647
586
|
benchmark_checkpoint("initialized") if benchmark?
|
|
648
587
|
task = src_vm.InstantClone_Task(spec: clone_spec)
|
|
649
588
|
else
|
|
650
589
|
clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(
|
|
651
590
|
location: relocate_spec,
|
|
652
|
-
powerOn: options[:poweron] && options[:
|
|
591
|
+
powerOn: options[:poweron] && options[:vm_customization].nil?,
|
|
653
592
|
template: false
|
|
654
593
|
)
|
|
655
594
|
|
|
656
|
-
if guest_customization
|
|
657
|
-
clone_spec.customization = guest_customization
|
|
658
|
-
end
|
|
595
|
+
clone_spec.customization = guest_customization_spec if options[:guest_customization]
|
|
659
596
|
|
|
660
597
|
benchmark_checkpoint("initialized") if benchmark?
|
|
661
|
-
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)
|
|
662
599
|
end
|
|
663
600
|
task.wait_for_completion
|
|
664
601
|
|
|
@@ -666,31 +603,28 @@ class Support
|
|
|
666
603
|
|
|
667
604
|
# get the IP address of the machine for bootstrapping
|
|
668
605
|
# machine name is based on the path, e.g. that includes the folder
|
|
669
|
-
path = options[:folder].nil? ?
|
|
606
|
+
path = options[:folder].nil? ? vm_name : format("%s/%s", options[:folder][:name], vm_name)
|
|
670
607
|
@vm = dc.find_vm(path)
|
|
671
608
|
raise Support::CloneError.new(format("Unable to find machine: %s", path)) if vm.nil?
|
|
672
609
|
|
|
673
|
-
if options[:vm_os].nil?
|
|
674
|
-
os = detect_os
|
|
675
|
-
Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize)
|
|
676
|
-
options[:vm_os] = os
|
|
677
|
-
end
|
|
678
|
-
|
|
679
610
|
# Reconnect network device after Instant Clone is ready
|
|
680
611
|
if instant_clone?
|
|
681
612
|
Kitchen.logger.info "Reconnecting network adapter"
|
|
682
613
|
reconnect_network_device(vm)
|
|
683
614
|
end
|
|
684
615
|
|
|
685
|
-
|
|
616
|
+
vm_customization if options[:vm_customization]
|
|
686
617
|
|
|
687
618
|
# Start only if specified or customizations wanted; no need for instant clones as they start in running state
|
|
688
|
-
if options[:poweron] && !options[:
|
|
619
|
+
if options[:poweron] && !options[:vm_customization].nil? && !instant_clone?
|
|
689
620
|
task = vm.PowerOnVM_Task
|
|
690
621
|
task.wait_for_completion
|
|
691
622
|
end
|
|
692
623
|
benchmark_checkpoint("powered_on") if benchmark?
|
|
693
624
|
|
|
625
|
+
# Windows customization takes a while, so check for its completion
|
|
626
|
+
guest_customization_wait if options[:guest_customization]
|
|
627
|
+
|
|
694
628
|
Kitchen.logger.info format("Waiting for VMware tools to become available (timeout: %d seconds)...", options[:wait_timeout])
|
|
695
629
|
wait_for_tools(options[:wait_timeout], options[:wait_interval])
|
|
696
630
|
|
|
@@ -698,7 +632,7 @@ class Support
|
|
|
698
632
|
benchmark_checkpoint("ip_detected") if benchmark?
|
|
699
633
|
|
|
700
634
|
benchmark_persist if benchmark?
|
|
701
|
-
Kitchen.logger.info format("Created machine %s with IP %s",
|
|
635
|
+
Kitchen.logger.info format("Created machine %s with IP %s", vm_name, ip)
|
|
702
636
|
end
|
|
703
637
|
end
|
|
704
638
|
end
|
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
RbVmomi::VIM::CustomizationSysprep.new(
|
|
189
|
+
guiUnattended: RbVmomi::VIM::CustomizationGuiUnattended.new(
|
|
190
|
+
timeZone: timezone.to_i || DEFAULT_WINDOWS_TIMEZONE,
|
|
191
|
+
autoLogon: false,
|
|
192
|
+
autoLogonCount: 1
|
|
193
|
+
),
|
|
194
|
+
identification: RbVmomi::VIM::CustomizationIdentification.new,
|
|
195
|
+
userData: RbVmomi::VIM::CustomizationUserData.new(
|
|
196
|
+
computerName: guest_hostname,
|
|
197
|
+
fullName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
|
|
198
|
+
orgName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
|
|
199
|
+
productId: product_id
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Check if a host is reachable
|
|
205
|
+
def up?(host)
|
|
206
|
+
check = Net::Ping::External.new(host)
|
|
207
|
+
check.ping?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Retrieve a GVLK (evaluation key) for the named OS
|
|
211
|
+
#
|
|
212
|
+
# @param [String] name Name of the OS as reported by VMware
|
|
213
|
+
# @returns [String] GVLK key, if any
|
|
214
|
+
def windows_kms_for_guest(name)
|
|
215
|
+
WINDOWS_KMS_KEYS.fetch(name, false)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Check format of Linux-specific timezone, according to VMware support
|
|
219
|
+
#
|
|
220
|
+
# @param [Integer] input Value to check for validity
|
|
221
|
+
# @returns [Boolean] if value is valid
|
|
222
|
+
def valid_linux_timezone?(input)
|
|
223
|
+
# Specific to VMware: https://kb.vmware.com/s/article/2145518
|
|
224
|
+
linux_timezone_pattern = %r{^[A-Z][A-Za-z]+\/[A-Z][-_+A-Za-z0-9]+$}
|
|
225
|
+
|
|
226
|
+
input.to_s.match? linux_timezone_pattern
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Check format of Windows-specific timezone
|
|
230
|
+
#
|
|
231
|
+
# @param [Integer] input Value to check for validity
|
|
232
|
+
# @returns [Boolean] if value is valid
|
|
233
|
+
def valid_windows_timezone?(input)
|
|
234
|
+
# Accept decimals and hex
|
|
235
|
+
# See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
|
|
236
|
+
windows_timezone_pattern = /^([0-9]+|0x[0-9a-fA-F]+)$/
|
|
237
|
+
|
|
238
|
+
input.to_s.match? windows_timezone_pattern
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Check for format of Windows Product IDs
|
|
242
|
+
#
|
|
243
|
+
# @param [String] input String to check
|
|
244
|
+
# @returns [Boolean] if value is in Windows Key format
|
|
245
|
+
def valid_windows_key?(input)
|
|
246
|
+
windows_key_pattern = /^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/
|
|
247
|
+
|
|
248
|
+
input.to_s.match? windows_key_pattern
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Return Guest hostname to be configured and check for validity.
|
|
252
|
+
#
|
|
253
|
+
# @returns [String] New hostname to assign
|
|
254
|
+
def guest_hostname
|
|
255
|
+
hostname = guest_customization[:hostname] || options[:vm_name]
|
|
256
|
+
|
|
257
|
+
hostname_pattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/
|
|
258
|
+
unless hostname.match?(hostname_pattern)
|
|
259
|
+
raise Support::GuestCustomizationError.new("Only letters, numbers or hyphens in hostnames allowed")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
RbVmomi::VIM::CustomizationFixedName.new(name: hostname)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Wait for vSphere task completion and subsequent IP address update (if any).
|
|
266
|
+
def guest_customization_wait
|
|
267
|
+
guest_customization_wait_task(guest_customization[:timeout_task] || DEFAULT_TIMEOUT_TASK)
|
|
268
|
+
guest_customization_wait_ip(guest_customization[:timeout_ip] || DEFAULT_TIMEOUT_IP)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Wait for Guest customization to finish successfully.
|
|
272
|
+
#
|
|
273
|
+
# @param [Integer] timeout Timeout in seconds
|
|
274
|
+
# @param [Integer] sleep_time Time to wait between tries
|
|
275
|
+
def guest_customization_wait_task(timeout = 600, sleep_time = 10)
|
|
276
|
+
waited_seconds = 0
|
|
277
|
+
|
|
278
|
+
Kitchen.logger.info "Waiting for guest customization (timeout: #{timeout} seconds)..."
|
|
279
|
+
|
|
280
|
+
while waited_seconds < timeout
|
|
281
|
+
events = guest_customization_events
|
|
282
|
+
|
|
283
|
+
if events.any? { |event| event.is_a? RbVmomi::VIM::CustomizationSucceeded }
|
|
284
|
+
return
|
|
285
|
+
elsif (failed = events.detect { |event| event.is_a? RbVmomi::VIM::CustomizationFailed })
|
|
286
|
+
# Only matters for Linux, as Windows won't come up at all to report a failure via VMware Tools
|
|
287
|
+
raise Support::GuestCustomizationError.new("Customization of VM failed: #{failed.fullFormattedMessage}")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
sleep(sleep_time)
|
|
291
|
+
waited_seconds += sleep_time
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
raise Support::GuestCustomizationError.new("Customization of VM did not complete within #{timeout} seconds.")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Wait for new IP to be reported, if any.
|
|
298
|
+
#
|
|
299
|
+
# @param [Integer] timeout Timeout in seconds. Tools report every 30 seconds, Default: 30 seconds
|
|
300
|
+
# @param [Integer] sleep_time Time to wait between tries
|
|
301
|
+
def guest_customization_wait_ip(timeout = 30, sleep_time = 1)
|
|
302
|
+
return unless guest_customization_ip_change?
|
|
303
|
+
|
|
304
|
+
waited_seconds = 0
|
|
305
|
+
|
|
306
|
+
Kitchen.logger.info "Waiting for guest customization IP update..."
|
|
307
|
+
|
|
308
|
+
while waited_seconds < timeout
|
|
309
|
+
found_ip = wait_for_ip(timeout, 1.0)
|
|
310
|
+
|
|
311
|
+
return if found_ip == guest_customization[:ip_address]
|
|
312
|
+
|
|
313
|
+
sleep(sleep_time)
|
|
314
|
+
waited_seconds += sleep_time
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
raise Support::GuestCustomizationError.new("Customized IP was not reported within #{timeout} seconds.")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Filter Customization events for the current VM
|
|
321
|
+
#
|
|
322
|
+
# @returns [Array<RbVmomi::VIM::CustomizationEvent>] All matching events
|
|
323
|
+
def guest_customization_events
|
|
324
|
+
vm_events %w{CustomizationSucceeded CustomizationFailed CustomizationStartedEvent}
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chef Software
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-10-
|
|
11
|
+
date: 2020-10-30 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:
|
|
@@ -98,5 +119,5 @@ requirements: []
|
|
|
98
119
|
rubygems_version: 3.0.3
|
|
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: []
|