kitchen-vcenter 2.7.9 → 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 +23 -12
- data/lib/support/clone_vm.rb +45 -89
- data/lib/support/guest_customization.rb +327 -0
- data/lib/support/guest_operations.rb +1 -1
- 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
|
@@ -20,8 +20,8 @@ require "vsphere-automation-cis"
|
|
20
20
|
require "vsphere-automation-vcenter"
|
21
21
|
require_relative "../../kitchen-vcenter/version"
|
22
22
|
require_relative "../../support/clone_vm"
|
23
|
-
require "securerandom"
|
24
|
-
require "uri"
|
23
|
+
require "securerandom" unless defined?(SecureRandom)
|
24
|
+
require "uri" unless defined?(URI)
|
25
25
|
|
26
26
|
# The main kitchen module
|
27
27
|
module Kitchen
|
@@ -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
|
#
|
@@ -137,7 +141,7 @@ module Kitchen
|
|
137
141
|
unless config[:folder].nil?
|
138
142
|
config[:folder] = {
|
139
143
|
name: config[:folder],
|
140
|
-
id: get_folder(config[:folder]),
|
144
|
+
id: get_folder(config[:folder], "VIRTUAL_MACHINE", datacenter),
|
141
145
|
}
|
142
146
|
end
|
143
147
|
|
@@ -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
|
@@ -335,13 +340,19 @@ module Kitchen
|
|
335
340
|
#
|
336
341
|
# @param [name] name is the name of the folder
|
337
342
|
# @param [type] type is the type of the folder, one of VIRTUAL_MACHINE, DATACENTER, possibly other values
|
338
|
-
|
343
|
+
# @param [datacenter] datacenter is the datacenter of the folder
|
344
|
+
def get_folder(name, type = "VIRTUAL_MACHINE", datacenter = nil)
|
339
345
|
folder_api = VSphereAutomation::VCenter::FolderApi.new(api_client)
|
340
|
-
|
346
|
+
parent_path, basename = File.split(name)
|
347
|
+
filter = { filter_names: basename, filter_type: type }
|
348
|
+
filter[:filter_datacenters] = datacenter if datacenter
|
349
|
+
filter[:filter_parent_folders] = get_folder(parent_path, type, datacenter) unless parent_path == "."
|
350
|
+
|
351
|
+
folders = folder_api.list(filter).value
|
341
352
|
|
342
|
-
raise format("Unable to find folder: %s",
|
353
|
+
raise format("Unable to find folder: %s", basename) if folders.empty?
|
343
354
|
|
344
|
-
raise format("
|
355
|
+
raise format("`%s` returned too many folders", basename) if folders.length > 1
|
345
356
|
|
346
357
|
folders.first.folder
|
347
358
|
end
|
@@ -377,7 +388,7 @@ module Kitchen
|
|
377
388
|
#
|
378
389
|
# @param [name] name is the name of the Cluster
|
379
390
|
def get_cluster_id(name)
|
380
|
-
return
|
391
|
+
return if name.nil?
|
381
392
|
|
382
393
|
cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
|
383
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,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-
|
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: []
|