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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8386b994051575b7997042ebd3c818de171a8de1ca34a39e13672800b986f216
4
- data.tar.gz: 611d99f79e9d90ba850b1b0f04f126f3ca8acbb7c5000861c2d32310602f8f63
3
+ metadata.gz: d91703414f7cc8c9c377a7e1ef6e322b716c90b61c77cfbd671da0053ac4e666
4
+ data.tar.gz: 42d6e401ffca66e898a98df2d8924243e925a28c34cef635e50885a84e8f1c76
5
5
  SHA512:
6
- metadata.gz: 678e749a5da30a917227c6f057836177b0a70a7f2d3b79a5272b76ee1a26aa33191234fd31211a43d31e4137e1c6184c6b2307d7a75658bcd5e944214a4cf523
7
- data.tar.gz: adf8a44b1a88c6274e1ee7070c7055b203fb033ebffb6723476fc3db6abfd6a8660fc16693a0cae8f4419bef34e550bc75de5a3c99a903f90e34b81bc252a531
6
+ metadata.gz: e9075327273580d4ff320138e067b5691e551bc79d45728ede0057c4bb0b57b8cbeb6e63d7cf958ef06e69fe11f3e4ff885b37887829b022a764c4e44aa15f60
7
+ data.tar.gz: 22ae34bfd2071b727b1ea4fec269246080021cac4dec31f83c3687faea5c1d8e650a36a5b1420c3a6e74ac081f4afc2e33e446e2e463c94ef8c462119dfdb057
@@ -20,5 +20,5 @@
20
20
  # The main kitchen-vcenter module
21
21
  module KitchenVcenter
22
22
  # The version of this version of test-kitchen we assume enterprises want.
23
- VERSION = "2.7.9"
23
+ VERSION = "2.9.0"
24
24
  end
@@ -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 :customize, nil
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
- name: config[:vm_name],
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
- customize: config[:customize],
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.name
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
- def get_folder(name, type = "VIRTUAL_MACHINE")
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
- folders = folder_api.list({ filter_names: name, filter_type: type }).value
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", name) if folders.empty?
353
+ raise format("Unable to find folder: %s", basename) if folders.empty?
343
354
 
344
- raise format("%s returned too many folders", name) if folders.length > 1
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 nil if name.nil?
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
@@ -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, :name, :ip, :guest_auth, :username
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
- @name = options[:name]
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
- vm.config&.guestId&.match(/^win/) ? :windows : :linux
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 reconfigure_guest
322
- Kitchen.logger.info "Waiting for reconfiguration to finish"
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[:customize].select { |key, _| %i{annotation memoryMB numCPUs}.include? key }
331
+ config = options[:vm_customization].select { |key, _| %i{annotation memoryMB numCPUs}.include? key }
327
332
 
328
- add_disks = options[:customize]&.fetch(:add_disks, nil)
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).sort.last
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.childEntity.grep(RbVmomi::VIM::Datacenter).find { |x| x.name == datacenter }
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 customization_spec
439
- unless options[:guest_customization]
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::CustomizationSpec.new(
476
- identity: RbVmomi::VIM::CustomizationLinuxPrep.new(
477
- domain: options[:guest_customization][:dns_domain],
478
- hostName: RbVmomi::VIM::CustomizationFixedName.new(
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
- nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new(
489
- adapter: RbVmomi::VIM::CustomizationIPSettings.new(
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: 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[:customize].nil?,
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: 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? ? name : format("%s/%s", options[:folder][:name], name)
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
- reconfigure_guest unless options[:customize].nil?
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[:customize].nil? && !instant_clone?
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", name, ip)
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
@@ -1,5 +1,5 @@
1
1
  require "rbvmomi"
2
- require "net/http"
2
+ require "net/http" unless defined?(Net::HTTP)
3
3
 
4
4
  class Support
5
5
  # Encapsulate VMware Tools GOM interaction, inspired by github:dnuffer/raidopt
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.7.9
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-08-06 00:00:00.000000000 Z
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 VMare vCenter
122
+ summary: Test Kitchen driver for VMware vCenter
102
123
  test_files: []