kitchen-vcenter 2.8.6 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1da356cf16429bae878bc2a5672f2c574c9cd82028914f4aca53d1311955eb4c
4
- data.tar.gz: ddf119be74e493604cef87b3cf3d45e2c486492a26eeac5fd5bb6c21ef58a38a
3
+ metadata.gz: d91703414f7cc8c9c377a7e1ef6e322b716c90b61c77cfbd671da0053ac4e666
4
+ data.tar.gz: 42d6e401ffca66e898a98df2d8924243e925a28c34cef635e50885a84e8f1c76
5
5
  SHA512:
6
- metadata.gz: '07824b2bb5850ba139bea24ea65082c602e6e56ea79903a3645ce90d64ff1614e35eeee52ce1dae68670d688be6c9aaec577eab283ac4efb5fcfc4d9da23af1b'
7
- data.tar.gz: a3d4f536ad36b3bf666183a607f2ba06a8bbfe9806e2e1917db30b65ec900a808fd69977587df44622e71f9a9ebce3363581c9cc689992f0fa8aac815457f93d
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.8.6"
23
+ VERSION = "2.9.0"
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
@@ -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
 
@@ -438,86 +443,16 @@ class Support
438
443
  false
439
444
  end
440
445
 
441
- def customization_spec
442
- unless options[:guest_customization]
443
- return false
444
- end
445
-
446
- if options[:guest_customization][:ip_address]
447
- unless ip?(options[:guest_customization][:ip_address])
448
- raise Support::CloneError.new("Guest customization error: ip_address is required to be formatted as an IPv4 address")
449
- end
450
-
451
- unless options[:guest_customization][:subnet_mask]
452
- raise Support::CloneError.new("Guest customization error: subnet_mask is required if assigning a fixed IPv4 address")
453
- end
454
-
455
- unless ip?(options[:guest_customization][:subnet_mask])
456
- raise Support::CloneError.new("Guest customization error: subnet_mask is required to be formatted as an IPv4 address")
457
- end
458
- end
446
+ def vm_events(event_types = [])
447
+ raise Support::CloneError.new("`vm_events` called before VM clone") unless vm
459
448
 
460
- if options[:guest_customization][:gateway]
461
- unless options[:guest_customization][:gateway].is_a?(Array)
462
- raise Support::CloneError.new("Guest customization error: gateway must be an array")
463
- end
464
-
465
- options[:guest_customization][:gateway].each do |v|
466
- unless ip?(v)
467
- raise Support::CloneError.new("Guest customization error: gateway is required to be formatted as an IPv4 address")
468
- end
469
- end
470
- end
471
-
472
- required = %i{dns_domain timezone dns_server_list dns_suffix_list}
473
- missing = required - options[:guest_customization].keys
474
- unless missing.empty?
475
- raise Support::CloneError.new("Guest customization error: #{missing.join(", ")} are required to support guest customization")
476
- end
477
-
478
- options[:guest_customization][:dns_server_list].each do |v|
479
- unless ip?(v)
480
- raise Support::CloneError.new("Guest customization error: dns_server_list is required to be formatted as an IPv4 address")
481
- end
482
- end
483
-
484
- if !options[:guest_customization][:dns_server_list].is_a?(Array)
485
- raise Support::CloneError.new("Guest customization error: dns_server_list must be an array")
486
- elsif !options[:guest_customization][:dns_suffix_list].is_a?(Array)
487
- raise Support::CloneError.new("Guest customization error: dns_suffix_list must be an array")
488
- end
489
-
490
- if options[:guest_customization][:ip_address]
491
- customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
492
- ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: options[:guest_customization][:ip_address]),
493
- gateway: options[:guest_customization][:gateway],
494
- subnetMask: options[:guest_customization][:subnet_mask],
495
- dnsDomain: options[:guest_customization][:dns_domain]
496
- )
497
- else
498
- customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
499
- ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new,
500
- dnsDomain: options[:guest_customization][:dns_domain]
501
- )
502
- end
503
-
504
- RbVmomi::VIM::CustomizationSpec.new(
505
- identity: RbVmomi::VIM::CustomizationLinuxPrep.new(
506
- domain: options[:guest_customization][:dns_domain],
507
- hostName: RbVmomi::VIM::CustomizationFixedName.new(
508
- name: name
509
- ),
510
- hwClockUTC: true,
511
- timeZone: options[:guest_customization][:timezone]
512
- ),
513
- globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
514
- dnsServerList: options[:guest_customization][:dns_server_list],
515
- dnsSuffixList: options[:guest_customization][:dns_suffix_list]
449
+ vem.QueryEvents(filter: RbVmomi::VIM::EventFilterSpec(
450
+ entity: RbVmomi::VIM::EventFilterSpecByEntity(
451
+ entity: vm,
452
+ recursion: RbVmomi::VIM::EventFilterSpecRecursionOption(:self)
516
453
  ),
517
- nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new(
518
- adapter: customized_ip
519
- )]
520
- )
454
+ eventTypeId: event_types
455
+ ))
521
456
  end
522
457
 
523
458
  def clone
@@ -526,12 +461,9 @@ class Support
526
461
  # set the datacenter name
527
462
  dc = find_datacenter
528
463
 
529
- # get guest customization spec
530
- guest_customization = customization_spec
531
-
532
464
  # reference template using full inventory path
533
465
  inventory_path = format("/%s/vm/%s", datacenter, options[:template])
534
- src_vm = root_folder.findByInventoryPath(inventory_path)
466
+ @src_vm = root_folder.findByInventoryPath(inventory_path)
535
467
  raise Support::CloneError.new(format("Unable to find template: %s", options[:template])) if src_vm.nil?
536
468
 
537
469
  if src_vm.config.template && !full_clone?
@@ -544,6 +476,13 @@ class Support
544
476
  options[:clone_type] = :full
545
477
  end
546
478
 
479
+ # Autodetect OS, if none given
480
+ if options[:vm_os].nil?
481
+ os = detect_os(src_vm)
482
+ Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize)
483
+ options[:vm_os] = os
484
+ end
485
+
547
486
  # Specify where the machine is going to be created
548
487
  relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec
549
488
 
@@ -642,23 +581,21 @@ class Support
642
581
  ]
643
582
 
644
583
  clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec,
645
- name: name)
584
+ name: vm_name)
646
585
 
647
586
  benchmark_checkpoint("initialized") if benchmark?
648
587
  task = src_vm.InstantClone_Task(spec: clone_spec)
649
588
  else
650
589
  clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(
651
590
  location: relocate_spec,
652
- powerOn: options[:poweron] && options[:customize].nil?,
591
+ powerOn: options[:poweron] && options[:vm_customization].nil?,
653
592
  template: false
654
593
  )
655
594
 
656
- if guest_customization
657
- clone_spec.customization = guest_customization
658
- end
595
+ clone_spec.customization = guest_customization_spec if options[:guest_customization]
659
596
 
660
597
  benchmark_checkpoint("initialized") if benchmark?
661
- task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name: name)
598
+ task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name: vm_name)
662
599
  end
663
600
  task.wait_for_completion
664
601
 
@@ -666,31 +603,28 @@ class Support
666
603
 
667
604
  # get the IP address of the machine for bootstrapping
668
605
  # machine name is based on the path, e.g. that includes the folder
669
- path = options[:folder].nil? ? name : format("%s/%s", options[:folder][:name], name)
606
+ path = options[:folder].nil? ? vm_name : format("%s/%s", options[:folder][:name], vm_name)
670
607
  @vm = dc.find_vm(path)
671
608
  raise Support::CloneError.new(format("Unable to find machine: %s", path)) if vm.nil?
672
609
 
673
- if options[:vm_os].nil?
674
- os = detect_os
675
- Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize)
676
- options[:vm_os] = os
677
- end
678
-
679
610
  # Reconnect network device after Instant Clone is ready
680
611
  if instant_clone?
681
612
  Kitchen.logger.info "Reconnecting network adapter"
682
613
  reconnect_network_device(vm)
683
614
  end
684
615
 
685
- reconfigure_guest unless options[:customize].nil?
616
+ vm_customization if options[:vm_customization]
686
617
 
687
618
  # Start only if specified or customizations wanted; no need for instant clones as they start in running state
688
- if options[:poweron] && !options[:customize].nil? && !instant_clone?
619
+ if options[:poweron] && !options[:vm_customization].nil? && !instant_clone?
689
620
  task = vm.PowerOnVM_Task
690
621
  task.wait_for_completion
691
622
  end
692
623
  benchmark_checkpoint("powered_on") if benchmark?
693
624
 
625
+ # Windows customization takes a while, so check for its completion
626
+ guest_customization_wait if options[:guest_customization]
627
+
694
628
  Kitchen.logger.info format("Waiting for VMware tools to become available (timeout: %d seconds)...", options[:wait_timeout])
695
629
  wait_for_tools(options[:wait_timeout], options[:wait_interval])
696
630
 
@@ -698,7 +632,7 @@ class Support
698
632
  benchmark_checkpoint("ip_detected") if benchmark?
699
633
 
700
634
  benchmark_persist if benchmark?
701
- Kitchen.logger.info format("Created machine %s with IP %s", name, ip)
635
+ Kitchen.logger.info format("Created machine %s with IP %s", vm_name, ip)
702
636
  end
703
637
  end
704
638
  end
@@ -0,0 +1,327 @@
1
+ require "net/ping"
2
+ require "rbvmomi"
3
+
4
+ class Support
5
+ class GuestCustomizationError < RuntimeError; end
6
+ class GuestCustomizationOptionsError < RuntimeError; end
7
+
8
+ module GuestCustomization
9
+ DEFAULT_LINUX_TIMEZONE = "Etc/UTC".freeze
10
+ DEFAULT_WINDOWS_ORG = "TestKitchen".freeze
11
+ DEFAULT_WINDOWS_TIMEZONE = 0x80000050 # Etc/UTC
12
+ DEFAULT_TIMEOUT_TASK = 600
13
+ DEFAULT_TIMEOUT_IP = 60
14
+
15
+ # Generic Volume License Keys for temporary Windows Server setup.
16
+ #
17
+ # @see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys
18
+ WINDOWS_KMS_KEYS = {
19
+ "Microsoft Windows Server 2019 (64-bit)" => "N69G4-B89J2-4G8F4-WWYCC-J464C",
20
+ "Microsoft Windows Server 2016 (64-bit)" => "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY",
21
+ "Microsoft Windows Server 2012R2 (64-bit)" => "D2N9P-3P6X9-2R39C-7RTCD-MDVJX",
22
+ "Microsoft Windows Server 2012 (64-bit)" => "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
23
+ }.freeze
24
+
25
+ # Configuration values for Guest Customization
26
+ #
27
+ # @returns [Hash] Configuration values from file
28
+ def guest_customization
29
+ options[:guest_customization]
30
+ end
31
+
32
+ # Build CustomizationSpec for Guest OS Customization
33
+ #
34
+ # @returns [RbVmomi::VIM::CustomizationSpec] Customization Spec for guest adjustments
35
+ def guest_customization_spec
36
+ return unless guest_customization
37
+
38
+ guest_customization_validate_options
39
+
40
+ if guest_customization[:ip_address]
41
+ customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
42
+ ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: guest_customization[:ip_address]),
43
+ gateway: guest_customization[:gateway],
44
+ subnetMask: guest_customization[:subnet_mask],
45
+ dnsDomain: guest_customization[:dns_domain]
46
+ )
47
+ else
48
+ customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
49
+ ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new,
50
+ dnsDomain: guest_customization[:dns_domain]
51
+ )
52
+ end
53
+
54
+ RbVmomi::VIM::CustomizationSpec.new(
55
+ identity: guest_customization_identity,
56
+ globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
57
+ dnsServerList: guest_customization[:dns_server_list],
58
+ dnsSuffixList: guest_customization[:dns_suffix_list]
59
+ ),
60
+ nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new(
61
+ adapter: customized_ip
62
+ )]
63
+ )
64
+ end
65
+
66
+ # Check options for existance and format
67
+ #
68
+ # @raise [Support::GuestCustomizationOptionsError] For any violation
69
+ def guest_customization_validate_options
70
+ if guest_customization_ip_change?
71
+ unless ip?(guest_customization[:ip_address])
72
+ raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` is required to be formatted as an IPv4 address")
73
+ end
74
+
75
+ unless guest_customization[:subnet_mask]
76
+ raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required if assigning a fixed IPv4 address")
77
+ end
78
+
79
+ unless ip?(guest_customization[:subnet_mask])
80
+ raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required to be formatted as an IPv4 address")
81
+ end
82
+
83
+ if up?(guest_customization[:ip_address])
84
+ raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` points to a host reachable via ICMP") unless guest_customization[:continue_on_ip_conflict]
85
+
86
+ Kitchen.logger.warn("Continuing customization despite `ip_address` conflicting with a reachable host per user request")
87
+ end
88
+ end
89
+
90
+ if guest_customization[:gateway]
91
+ unless guest_customization[:gateway].is_a?(Array)
92
+ raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` must be an array")
93
+ end
94
+
95
+ guest_customization[:gateway].each do |v|
96
+ unless ip?(v)
97
+ raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` is required to be formatted as an IPv4 address")
98
+ end
99
+ end
100
+ end
101
+
102
+ required = %i{dns_domain dns_server_list dns_suffix_list}
103
+ missing = required - guest_customization.keys
104
+ unless missing.empty?
105
+ raise Support::GuestCustomizationOptionsError.new("Parameters `#{missing.join("`, `")}` are required to support guest customization")
106
+ end
107
+
108
+ guest_customization[:dns_server_list].each do |v|
109
+ unless ip?(v)
110
+ raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` is required to be formatted as an IPv4 address")
111
+ end
112
+ end
113
+
114
+ if !guest_customization[:dns_server_list].is_a?(Array)
115
+ raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` must be an array")
116
+ elsif !guest_customization[:dns_suffix_list].is_a?(Array)
117
+ raise Support::GuestCustomizationOptionsError.new("Parameter `dns_suffix_list` must be an array")
118
+ end
119
+ end
120
+
121
+ # Check if an IP change is requested
122
+ #
123
+ # @returns [Boolean] If `ip_address` is to be changed
124
+ def guest_customization_ip_change?
125
+ guest_customization[:ip_address]
126
+ end
127
+
128
+ # Return OS-specific CustomizationIdentity object
129
+ def guest_customization_identity
130
+ if linux?
131
+ guest_customization_identity_linux
132
+ elsif windows?
133
+ guest_customization_identity_windows
134
+ else
135
+ raise Support::GuestCustomizationError.new("Unknown OS, no valid customization found")
136
+ end
137
+ end
138
+
139
+ # Construct Linux-specific customization information
140
+ def guest_customization_identity_linux
141
+ timezone = guest_customization[:timezone]
142
+ if timezone && !valid_linux_timezone?(timezone)
143
+ raise Support::GuestCustomizationError.new <<~ERROR
144
+ Linux customization requires `timezone` in `Area/Location` format.
145
+ See https://kb.vmware.com/s/article/2145518
146
+ ERROR
147
+ end
148
+
149
+ Kitchen.logger.warn("Linux guest customization: No timezone passed, assuming UTC") unless timezone
150
+
151
+ RbVmomi::VIM::CustomizationLinuxPrep.new(
152
+ domain: guest_customization[:dns_domain],
153
+ hostName: guest_hostname,
154
+ hwClockUTC: true,
155
+ timeZone: timezone || DEFAULT_LINUX_TIMEZONE
156
+ )
157
+ end
158
+
159
+ # Construct Windows-specific customization information
160
+ def guest_customization_identity_windows
161
+ timezone = guest_customization[:timezone]
162
+ if timezone && !valid_windows_timezone?(timezone)
163
+ raise Support::GuestCustomizationOptionsError.new <<~ERROR
164
+ Windows customization requires `timezone` as decimal number or hex number (0x55).
165
+ See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
166
+ ERROR
167
+ end
168
+
169
+ Kitchen.logger.warn("Windows guest customization: No timezone passed, assuming UTC") unless timezone
170
+
171
+ product_id = guest_customization[:product_id]
172
+
173
+ # Try to look up and use a known, documented 120-day trial key
174
+ unless product_id
175
+ guest_os = src_vm.guest&.guestFullName
176
+ product_id = windows_kms_for_guest(guest_os)
177
+
178
+ Kitchen.logger.warn format("Windows guest customization:: Using KMS Key `%<key>s` for %<os>s", key: product_id, os: guest_os) if product_id
179
+ end
180
+
181
+ unless valid_windows_key? product_id
182
+ raise Support::GuestCustomizationOptionsError.new <<~ERROR
183
+ Windows customization requires `product_id` to work. Add a valid product key or
184
+ see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys for KMS trial keys
185
+ ERROR
186
+ end
187
+
188
+ RbVmomi::VIM::CustomizationSysprep.new(
189
+ guiUnattended: RbVmomi::VIM::CustomizationGuiUnattended.new(
190
+ timeZone: timezone.to_i || DEFAULT_WINDOWS_TIMEZONE,
191
+ autoLogon: false,
192
+ autoLogonCount: 1
193
+ ),
194
+ identification: RbVmomi::VIM::CustomizationIdentification.new,
195
+ userData: RbVmomi::VIM::CustomizationUserData.new(
196
+ computerName: guest_hostname,
197
+ fullName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
198
+ orgName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
199
+ productId: product_id
200
+ )
201
+ )
202
+ end
203
+
204
+ # Check if a host is reachable
205
+ def up?(host)
206
+ check = Net::Ping::External.new(host)
207
+ check.ping?
208
+ end
209
+
210
+ # Retrieve a GVLK (evaluation key) for the named OS
211
+ #
212
+ # @param [String] name Name of the OS as reported by VMware
213
+ # @returns [String] GVLK key, if any
214
+ def windows_kms_for_guest(name)
215
+ WINDOWS_KMS_KEYS.fetch(name, false)
216
+ end
217
+
218
+ # Check format of Linux-specific timezone, according to VMware support
219
+ #
220
+ # @param [Integer] input Value to check for validity
221
+ # @returns [Boolean] if value is valid
222
+ def valid_linux_timezone?(input)
223
+ # Specific to VMware: https://kb.vmware.com/s/article/2145518
224
+ linux_timezone_pattern = %r{^[A-Z][A-Za-z]+\/[A-Z][-_+A-Za-z0-9]+$}
225
+
226
+ input.to_s.match? linux_timezone_pattern
227
+ end
228
+
229
+ # Check format of Windows-specific timezone
230
+ #
231
+ # @param [Integer] input Value to check for validity
232
+ # @returns [Boolean] if value is valid
233
+ def valid_windows_timezone?(input)
234
+ # Accept decimals and hex
235
+ # See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
236
+ windows_timezone_pattern = /^([0-9]+|0x[0-9a-fA-F]+)$/
237
+
238
+ input.to_s.match? windows_timezone_pattern
239
+ end
240
+
241
+ # Check for format of Windows Product IDs
242
+ #
243
+ # @param [String] input String to check
244
+ # @returns [Boolean] if value is in Windows Key format
245
+ def valid_windows_key?(input)
246
+ windows_key_pattern = /^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/
247
+
248
+ input.to_s.match? windows_key_pattern
249
+ end
250
+
251
+ # Return Guest hostname to be configured and check for validity.
252
+ #
253
+ # @returns [String] New hostname to assign
254
+ def guest_hostname
255
+ hostname = guest_customization[:hostname] || options[:vm_name]
256
+
257
+ hostname_pattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/
258
+ unless hostname.match?(hostname_pattern)
259
+ raise Support::GuestCustomizationError.new("Only letters, numbers or hyphens in hostnames allowed")
260
+ end
261
+
262
+ RbVmomi::VIM::CustomizationFixedName.new(name: hostname)
263
+ end
264
+
265
+ # Wait for vSphere task completion and subsequent IP address update (if any).
266
+ def guest_customization_wait
267
+ guest_customization_wait_task(guest_customization[:timeout_task] || DEFAULT_TIMEOUT_TASK)
268
+ guest_customization_wait_ip(guest_customization[:timeout_ip] || DEFAULT_TIMEOUT_IP)
269
+ end
270
+
271
+ # Wait for Guest customization to finish successfully.
272
+ #
273
+ # @param [Integer] timeout Timeout in seconds
274
+ # @param [Integer] sleep_time Time to wait between tries
275
+ def guest_customization_wait_task(timeout = 600, sleep_time = 10)
276
+ waited_seconds = 0
277
+
278
+ Kitchen.logger.info "Waiting for guest customization (timeout: #{timeout} seconds)..."
279
+
280
+ while waited_seconds < timeout
281
+ events = guest_customization_events
282
+
283
+ if events.any? { |event| event.is_a? RbVmomi::VIM::CustomizationSucceeded }
284
+ return
285
+ elsif (failed = events.detect { |event| event.is_a? RbVmomi::VIM::CustomizationFailed })
286
+ # Only matters for Linux, as Windows won't come up at all to report a failure via VMware Tools
287
+ raise Support::GuestCustomizationError.new("Customization of VM failed: #{failed.fullFormattedMessage}")
288
+ end
289
+
290
+ sleep(sleep_time)
291
+ waited_seconds += sleep_time
292
+ end
293
+
294
+ raise Support::GuestCustomizationError.new("Customization of VM did not complete within #{timeout} seconds.")
295
+ end
296
+
297
+ # Wait for new IP to be reported, if any.
298
+ #
299
+ # @param [Integer] timeout Timeout in seconds. Tools report every 30 seconds, Default: 30 seconds
300
+ # @param [Integer] sleep_time Time to wait between tries
301
+ def guest_customization_wait_ip(timeout = 30, sleep_time = 1)
302
+ return unless guest_customization_ip_change?
303
+
304
+ waited_seconds = 0
305
+
306
+ Kitchen.logger.info "Waiting for guest customization IP update..."
307
+
308
+ while waited_seconds < timeout
309
+ found_ip = wait_for_ip(timeout, 1.0)
310
+
311
+ return if found_ip == guest_customization[:ip_address]
312
+
313
+ sleep(sleep_time)
314
+ waited_seconds += sleep_time
315
+ end
316
+
317
+ raise Support::GuestCustomizationError.new("Customized IP was not reported within #{timeout} seconds.")
318
+ end
319
+
320
+ # Filter Customization events for the current VM
321
+ #
322
+ # @returns [Array<RbVmomi::VIM::CustomizationEvent>] All matching events
323
+ def guest_customization_events
324
+ vm_events %w{CustomizationSucceeded CustomizationFailed CustomizationStartedEvent}
325
+ end
326
+ end
327
+ end
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-vcenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.6
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-22 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: []