kitchen-vcenter 2.7.12 → 2.9.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7085ec77febee812284fdba85d1002f2f1d6bae3ee1e02a7f264efb761217a30
4
- data.tar.gz: df5e365f5821f623f7d8328ed7381ad710d9a62534970102fce3a4600195917b
3
+ metadata.gz: a7424e04d466fa8ed2aea4fbc36f1454ee71e4e5b2bf383490e8f1ec5a1668d5
4
+ data.tar.gz: 00b915156f7dd91b71d656d24f129015a28e6349a82a06dcb50d4c2a43c38e06
5
5
  SHA512:
6
- metadata.gz: 744634197d4ba4c19fb461226f42afbbca219e77054829ba5cb3a7a2b1b7cd10c29d94a3bb1416b840b04dc26bd5a6ba3a1dffa2827c3af9009f9f67130a3eb3
7
- data.tar.gz: 389178a1c61df997e790cc06cbb3a3d5ae7aa7d6303e946f5cf8ef7499fe65003a6419d7f1a120d6c0da99d3f524b68cdf4dae6e1ee2beab5929a2d47ae5f945
6
+ metadata.gz: b9b8f896730e171635e298173469651183cf2e785bde217e8f49d98870ec9d21606bf3b4b5b4ea2eec762458e616a7705c8ff93471e2986fe01d108a52d5309a
7
+ data.tar.gz: 72ff746c626c7e479f2e154ef7c2d8f05485cfc95726fd8673a6a27313ff89241307913a3574e77bfd1fd6304406ccc3c506bd636171b03e7ba8aa0294ffb6eb
@@ -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.12"
23
+ VERSION = "2.9.8"
24
24
  end
@@ -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
  #
@@ -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
@@ -383,7 +388,7 @@ module Kitchen
383
388
  #
384
389
  # @param [name] name is the name of the Cluster
385
390
  def get_cluster_id(name)
386
- return nil if name.nil?
391
+ return if name.nil?
387
392
 
388
393
  cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
389
394
  clusters = cluster_api.list({ filter_names: name }).value
@@ -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,336 @@
1
+ require "net/ping"
2
+ require "rbvmomi"
3
+
4
+ class Support
5
+ class GuestCustomizationError < RuntimeError; end
6
+ class GuestCustomizationOptionsError < RuntimeError; end
7
+
8
+ module GuestCustomization
9
+ DEFAULT_LINUX_TIMEZONE = "Etc/UTC".freeze
10
+ DEFAULT_WINDOWS_ORG = "TestKitchen".freeze
11
+ DEFAULT_WINDOWS_TIMEZONE = 0x80000050 # Etc/UTC
12
+ DEFAULT_TIMEOUT_TASK = 600
13
+ DEFAULT_TIMEOUT_IP = 60
14
+
15
+ # Generic Volume License Keys for temporary Windows Server setup.
16
+ #
17
+ # @see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys
18
+ WINDOWS_KMS_KEYS = {
19
+ "Microsoft Windows Server 2019 (64-bit)" => "N69G4-B89J2-4G8F4-WWYCC-J464C",
20
+ "Microsoft Windows Server 2016 (64-bit)" => "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY",
21
+ "Microsoft Windows Server 2012R2 (64-bit)" => "D2N9P-3P6X9-2R39C-7RTCD-MDVJX",
22
+ "Microsoft Windows Server 2012 (64-bit)" => "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
23
+ }.freeze
24
+
25
+ # Configuration values for Guest Customization
26
+ #
27
+ # @returns [Hash] Configuration values from file
28
+ def guest_customization
29
+ options[:guest_customization]
30
+ end
31
+
32
+ # Build CustomizationSpec for Guest OS Customization
33
+ #
34
+ # @returns [RbVmomi::VIM::CustomizationSpec] Customization Spec for guest adjustments
35
+ def guest_customization_spec
36
+ return unless guest_customization
37
+
38
+ guest_customization_validate_options
39
+
40
+ if guest_customization[:ip_address]
41
+ customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
42
+ ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: guest_customization[:ip_address]),
43
+ gateway: guest_customization[:gateway],
44
+ subnetMask: guest_customization[:subnet_mask],
45
+ dnsDomain: guest_customization[:dns_domain]
46
+ )
47
+ else
48
+ customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
49
+ ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new,
50
+ dnsDomain: guest_customization[:dns_domain]
51
+ )
52
+ end
53
+
54
+ RbVmomi::VIM::CustomizationSpec.new(
55
+ identity: guest_customization_identity,
56
+ globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
57
+ dnsServerList: guest_customization[:dns_server_list],
58
+ dnsSuffixList: guest_customization[:dns_suffix_list]
59
+ ),
60
+ nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new(
61
+ adapter: customized_ip
62
+ )]
63
+ )
64
+ end
65
+
66
+ # Check options for existance and format
67
+ #
68
+ # @raise [Support::GuestCustomizationOptionsError] For any violation
69
+ def guest_customization_validate_options
70
+ if guest_customization_ip_change?
71
+ unless ip?(guest_customization[:ip_address])
72
+ raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` is required to be formatted as an IPv4 address")
73
+ end
74
+
75
+ unless guest_customization[:subnet_mask]
76
+ raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required if assigning a fixed IPv4 address")
77
+ end
78
+
79
+ unless ip?(guest_customization[:subnet_mask])
80
+ raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required to be formatted as an IPv4 address")
81
+ end
82
+
83
+ if up?(guest_customization[:ip_address])
84
+ raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` points to a host reachable via ICMP") unless guest_customization[:continue_on_ip_conflict]
85
+
86
+ Kitchen.logger.warn("Continuing customization despite `ip_address` conflicting with a reachable host per user request")
87
+ end
88
+ end
89
+
90
+ if guest_customization[:gateway]
91
+ unless guest_customization[:gateway].is_a?(Array)
92
+ raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` must be an array")
93
+ end
94
+
95
+ guest_customization[:gateway].each do |v|
96
+ unless ip?(v)
97
+ raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` is required to be formatted as an IPv4 address")
98
+ end
99
+ end
100
+ end
101
+
102
+ required = %i{dns_domain dns_server_list dns_suffix_list}
103
+ missing = required - guest_customization.keys
104
+ unless missing.empty?
105
+ raise Support::GuestCustomizationOptionsError.new("Parameters `#{missing.join("`, `")}` are required to support guest customization")
106
+ end
107
+
108
+ guest_customization[:dns_server_list].each do |v|
109
+ unless ip?(v)
110
+ raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` is required to be formatted as an IPv4 address")
111
+ end
112
+ end
113
+
114
+ if !guest_customization[:dns_server_list].is_a?(Array)
115
+ raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` must be an array")
116
+ elsif !guest_customization[:dns_suffix_list].is_a?(Array)
117
+ raise Support::GuestCustomizationOptionsError.new("Parameter `dns_suffix_list` must be an array")
118
+ end
119
+ end
120
+
121
+ # Check if an IP change is requested
122
+ #
123
+ # @returns [Boolean] If `ip_address` is to be changed
124
+ def guest_customization_ip_change?
125
+ guest_customization[:ip_address]
126
+ end
127
+
128
+ # Return OS-specific CustomizationIdentity object
129
+ def guest_customization_identity
130
+ if linux?
131
+ guest_customization_identity_linux
132
+ elsif windows?
133
+ guest_customization_identity_windows
134
+ else
135
+ raise Support::GuestCustomizationError.new("Unknown OS, no valid customization found")
136
+ end
137
+ end
138
+
139
+ # Construct Linux-specific customization information
140
+ def guest_customization_identity_linux
141
+ timezone = guest_customization[:timezone]
142
+ if timezone && !valid_linux_timezone?(timezone)
143
+ raise Support::GuestCustomizationError.new <<~ERROR
144
+ Linux customization requires `timezone` in `Area/Location` format.
145
+ See https://kb.vmware.com/s/article/2145518
146
+ ERROR
147
+ end
148
+
149
+ Kitchen.logger.warn("Linux guest customization: No timezone passed, assuming UTC") unless timezone
150
+
151
+ RbVmomi::VIM::CustomizationLinuxPrep.new(
152
+ domain: guest_customization[:dns_domain],
153
+ hostName: guest_hostname,
154
+ hwClockUTC: true,
155
+ timeZone: timezone || DEFAULT_LINUX_TIMEZONE
156
+ )
157
+ end
158
+
159
+ # Construct Windows-specific customization information
160
+ def guest_customization_identity_windows
161
+ timezone = guest_customization[:timezone]
162
+ if timezone && !valid_windows_timezone?(timezone)
163
+ raise Support::GuestCustomizationOptionsError.new <<~ERROR
164
+ Windows customization requires `timezone` as decimal number or hex number (0x55).
165
+ See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
166
+ ERROR
167
+ end
168
+
169
+ Kitchen.logger.warn("Windows guest customization: No timezone passed, assuming UTC") unless timezone
170
+
171
+ product_id = guest_customization[:product_id]
172
+
173
+ # Try to look up and use a known, documented 120-day trial key
174
+ unless product_id
175
+ guest_os = src_vm.guest&.guestFullName
176
+ product_id = windows_kms_for_guest(guest_os)
177
+
178
+ Kitchen.logger.warn format("Windows guest customization:: Using KMS Key `%<key>s` for %<os>s", key: product_id, os: guest_os) if product_id
179
+ end
180
+
181
+ unless valid_windows_key? product_id
182
+ raise Support::GuestCustomizationOptionsError.new <<~ERROR
183
+ Windows customization requires `product_id` to work. Add a valid product key or
184
+ see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys for KMS trial keys
185
+ ERROR
186
+ end
187
+
188
+ customization_pass = nil
189
+ if guest_customization[:administrator_password]
190
+ customization_pass = RbVmomi::VIM::CustomizationPassword.new(
191
+ plainText: true,
192
+ value: guest_customization[:administrator_password]
193
+ )
194
+ end
195
+
196
+ RbVmomi::VIM::CustomizationSysprep.new(
197
+ guiUnattended: RbVmomi::VIM::CustomizationGuiUnattended.new(
198
+ timeZone: timezone.to_i || DEFAULT_WINDOWS_TIMEZONE,
199
+ autoLogon: false,
200
+ autoLogonCount: 1,
201
+ password: customization_pass
202
+ ),
203
+ identification: RbVmomi::VIM::CustomizationIdentification.new,
204
+ userData: RbVmomi::VIM::CustomizationUserData.new(
205
+ computerName: guest_hostname,
206
+ fullName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
207
+ orgName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
208
+ productId: product_id
209
+ )
210
+ )
211
+ end
212
+
213
+ # Check if a host is reachable
214
+ def up?(host)
215
+ check = Net::Ping::External.new(host)
216
+ check.ping?
217
+ end
218
+
219
+ # Retrieve a GVLK (evaluation key) for the named OS
220
+ #
221
+ # @param [String] name Name of the OS as reported by VMware
222
+ # @returns [String] GVLK key, if any
223
+ def windows_kms_for_guest(name)
224
+ WINDOWS_KMS_KEYS.fetch(name, false)
225
+ end
226
+
227
+ # Check format of Linux-specific timezone, according to VMware support
228
+ #
229
+ # @param [Integer] input Value to check for validity
230
+ # @returns [Boolean] if value is valid
231
+ def valid_linux_timezone?(input)
232
+ # Specific to VMware: https://kb.vmware.com/s/article/2145518
233
+ linux_timezone_pattern = %r{^[A-Z][A-Za-z]+\/[A-Z][-_+A-Za-z0-9]+$}
234
+
235
+ input.to_s.match? linux_timezone_pattern
236
+ end
237
+
238
+ # Check format of Windows-specific timezone
239
+ #
240
+ # @param [Integer] input Value to check for validity
241
+ # @returns [Boolean] if value is valid
242
+ def valid_windows_timezone?(input)
243
+ # Accept decimals and hex
244
+ # See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
245
+ windows_timezone_pattern = /^([0-9]+|0x[0-9a-fA-F]+)$/
246
+
247
+ input.to_s.match? windows_timezone_pattern
248
+ end
249
+
250
+ # Check for format of Windows Product IDs
251
+ #
252
+ # @param [String] input String to check
253
+ # @returns [Boolean] if value is in Windows Key format
254
+ def valid_windows_key?(input)
255
+ windows_key_pattern = /^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/
256
+
257
+ input.to_s.match? windows_key_pattern
258
+ end
259
+
260
+ # Return Guest hostname to be configured and check for validity.
261
+ #
262
+ # @returns [String] New hostname to assign
263
+ def guest_hostname
264
+ hostname = guest_customization[:hostname] || options[:vm_name]
265
+
266
+ hostname_pattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/
267
+ unless hostname.match?(hostname_pattern)
268
+ raise Support::GuestCustomizationError.new("Only letters, numbers or hyphens in hostnames allowed")
269
+ end
270
+
271
+ RbVmomi::VIM::CustomizationFixedName.new(name: hostname)
272
+ end
273
+
274
+ # Wait for vSphere task completion and subsequent IP address update (if any).
275
+ def guest_customization_wait
276
+ guest_customization_wait_task(guest_customization[:timeout_task] || DEFAULT_TIMEOUT_TASK)
277
+ guest_customization_wait_ip(guest_customization[:timeout_ip] || DEFAULT_TIMEOUT_IP)
278
+ end
279
+
280
+ # Wait for Guest customization to finish successfully.
281
+ #
282
+ # @param [Integer] timeout Timeout in seconds
283
+ # @param [Integer] sleep_time Time to wait between tries
284
+ def guest_customization_wait_task(timeout = 600, sleep_time = 10)
285
+ waited_seconds = 0
286
+
287
+ Kitchen.logger.info "Waiting for guest customization (timeout: #{timeout} seconds)..."
288
+
289
+ while waited_seconds < timeout
290
+ events = guest_customization_events
291
+
292
+ if events.any? { |event| event.is_a? RbVmomi::VIM::CustomizationSucceeded }
293
+ return
294
+ elsif (failed = events.detect { |event| event.is_a? RbVmomi::VIM::CustomizationFailed })
295
+ # Only matters for Linux, as Windows won't come up at all to report a failure via VMware Tools
296
+ raise Support::GuestCustomizationError.new("Customization of VM failed: #{failed.fullFormattedMessage}")
297
+ end
298
+
299
+ sleep(sleep_time)
300
+ waited_seconds += sleep_time
301
+ end
302
+
303
+ raise Support::GuestCustomizationError.new("Customization of VM did not complete within #{timeout} seconds.")
304
+ end
305
+
306
+ # Wait for new IP to be reported, if any.
307
+ #
308
+ # @param [Integer] timeout Timeout in seconds. Tools report every 30 seconds, Default: 30 seconds
309
+ # @param [Integer] sleep_time Time to wait between tries
310
+ def guest_customization_wait_ip(timeout = 30, sleep_time = 1)
311
+ return unless guest_customization_ip_change?
312
+
313
+ waited_seconds = 0
314
+
315
+ Kitchen.logger.info "Waiting for guest customization IP update..."
316
+
317
+ while waited_seconds < timeout
318
+ found_ip = wait_for_ip(timeout, 1.0)
319
+
320
+ return if found_ip == guest_customization[:ip_address]
321
+
322
+ sleep(sleep_time)
323
+ waited_seconds += sleep_time
324
+ end
325
+
326
+ raise Support::GuestCustomizationError.new("Customized IP was not reported within #{timeout} seconds.")
327
+ end
328
+
329
+ # Filter Customization events for the current VM
330
+ #
331
+ # @returns [Array<RbVmomi::VIM::CustomizationEvent>] All matching events
332
+ def guest_customization_events
333
+ vm_events %w{CustomizationSucceeded CustomizationFailed CustomizationStartedEvent}
334
+ end
335
+ end
336
+ end
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-vcenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.12
4
+ version: 2.9.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-21 00:00:00.000000000 Z
11
+ date: 2021-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ping
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 2.0.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: rbvmomi
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +95,7 @@ files:
75
95
  - lib/kitchen-vcenter/version.rb
76
96
  - lib/kitchen/driver/vcenter.rb
77
97
  - lib/support/clone_vm.rb
98
+ - lib/support/guest_customization.rb
78
99
  - lib/support/guest_operations.rb
79
100
  homepage: https://github.com/chef/kitchen-vcenter
80
101
  licenses:
@@ -95,8 +116,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
116
  - !ruby/object:Gem::Version
96
117
  version: '0'
97
118
  requirements: []
98
- rubygems_version: 3.0.3
119
+ rubygems_version: 3.1.4
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: []