kitchen-vcenter 2.8.6 → 2.10.2

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: 1da356cf16429bae878bc2a5672f2c574c9cd82028914f4aca53d1311955eb4c
4
- data.tar.gz: ddf119be74e493604cef87b3cf3d45e2c486492a26eeac5fd5bb6c21ef58a38a
3
+ metadata.gz: 7c1f638e5648056082f4da2ce087031e468d0bb1113c480af1f04b98496fad5a
4
+ data.tar.gz: 00d9fd698d02804d4942c1be24802954d5f72ad6cbf97040eecd1d099e959631
5
5
  SHA512:
6
- metadata.gz: '07824b2bb5850ba139bea24ea65082c602e6e56ea79903a3645ce90d64ff1614e35eeee52ce1dae68670d688be6c9aaec577eab283ac4efb5fcfc4d9da23af1b'
7
- data.tar.gz: a3d4f536ad36b3bf666183a607f2ba06a8bbfe9806e2e1917db30b65ec900a808fd69977587df44622e71f9a9ebce3363581c9cc689992f0fa8aac815457f93d
6
+ metadata.gz: 66e78bad8190b260de8eebcca12051a76452110ad39136e198d1f1c9823dbadbf6698aab4d7e2fe70873fb001f28268f37c5ddec2b72ecc853f6e1d06c77b17a
7
+ data.tar.gz: 1c60d33094d436b989b503268ce68ef7bd729a0216a848cb6de6d00c65f1ecf1af3dd92fb6913f5104a2c7de225ddc6159ddbd02ee7353c6bf1d4aa4a9357ace
@@ -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.10.2"
24
24
  end
@@ -16,6 +16,7 @@
16
16
  #
17
17
 
18
18
  require "kitchen"
19
+ require "rbvmomi"
19
20
  require "vsphere-automation-cis"
20
21
  require "vsphere-automation-vcenter"
21
22
  require_relative "../../kitchen-vcenter/version"
@@ -29,8 +30,17 @@ module Kitchen
29
30
  module Driver
30
31
  # Extends the Base class for vCenter
31
32
  class Vcenter < Kitchen::Driver::Base
33
+ class UnauthenticatedError < RuntimeError; end
34
+ class ResourceMissingError < RuntimeError; end
35
+ class ResourceAmbiguousError < RuntimeError; end
36
+
32
37
  attr_accessor :connection_options, :ipaddress, :api_client
33
38
 
39
+ UNAUTH_CLASSES = [
40
+ VSphereAutomation::CIS::VapiStdErrorsUnauthenticated,
41
+ VSphereAutomation::VCenter::VapiStdErrorsUnauthenticated,
42
+ ].freeze
43
+
34
44
  required_config :vcenter_username
35
45
  required_config :vcenter_password
36
46
  required_config :vcenter_host
@@ -50,7 +60,7 @@ module Kitchen
50
60
  default_config :vm_wait_timeout, 90
51
61
  default_config :vm_wait_interval, 2.0
52
62
  default_config :vm_rollback, false
53
- default_config :customize, nil
63
+ default_config :vm_customization, nil
54
64
  default_config :guest_customization, nil
55
65
  default_config :interface, nil
56
66
  default_config :active_discovery, false
@@ -80,6 +90,10 @@ module Kitchen
80
90
  The 'aggressive_password' setting was renamed to 'vm_password' and will
81
91
  be removed in future versions.
82
92
  MSG
93
+ deprecate_config_for :customize, Util.outdent!(<<-MSG)
94
+ The `customize` setting was renamed to `vm_customization` and will
95
+ be removed in future versions.
96
+ MSG
83
97
 
84
98
  # The main create method
85
99
  #
@@ -102,6 +116,7 @@ module Kitchen
102
116
  config[:resource_pool] = root_pool
103
117
  else
104
118
  rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client)
119
+ raise_if_unauthenticated rp_api, "checking for resource pools"
105
120
 
106
121
  found_pool = nil
107
122
  pools = rp_api.get(root_pool).value.resource_pools
@@ -110,7 +125,7 @@ module Kitchen
110
125
  found_pool = pool if name == config[:resource_pool]
111
126
  end
112
127
 
113
- raise format("Pool %s not found on cluster %s", config[:resource_pool], config[:cluster]) if found_pool.nil?
128
+ raise_if_missing found_pool, format("Resource pool `%s` not found on cluster `%s`", config[:resource_pool], config[:cluster])
114
129
 
115
130
  config[:resource_pool] = found_pool
116
131
  end
@@ -126,7 +141,6 @@ module Kitchen
126
141
  datacenter = get_datacenter(dc_folder, dc_name)
127
142
  cluster_id = get_cluster_id(config[:cluster])
128
143
 
129
- # Using the clone class, create a machine for TK
130
144
  # Find the identifier for the targethost to pass to rbvmomi
131
145
  config[:targethost] = get_host(config[:targethost], datacenter, cluster_id)
132
146
 
@@ -141,13 +155,16 @@ module Kitchen
141
155
  }
142
156
  end
143
157
 
158
+ # Check for valid tags before cloning
159
+ vm_tags = map_tags(config[:tags])
160
+
144
161
  # Allow different clone types
145
162
  config[:clone_type] = :linked if config[:clone_type] == "linked"
146
163
  config[:clone_type] = :instant if config[:clone_type] == "instant"
147
164
 
148
165
  # Create a hash of options that the clone requires
149
166
  options = {
150
- name: config[:vm_name],
167
+ vm_name: config[:vm_name],
151
168
  targethost: config[:targethost],
152
169
  poweron: config[:poweron],
153
170
  template: config[:template],
@@ -159,7 +176,7 @@ module Kitchen
159
176
  interface: config[:interface],
160
177
  wait_timeout: config[:vm_wait_timeout],
161
178
  wait_interval: config[:vm_wait_interval],
162
- customize: config[:customize],
179
+ vm_customization: config[:vm_customization],
163
180
  guest_customization: config[:guest_customization],
164
181
  active_discovery: config[:active_discovery],
165
182
  active_discovery_command: config[:active_discovery_command],
@@ -178,11 +195,11 @@ module Kitchen
178
195
  new_vm.clone
179
196
 
180
197
  state[:hostname] = new_vm.ip
181
- state[:vm_name] = new_vm.name
198
+ state[:vm_name] = new_vm.vm_name
182
199
 
183
200
  rescue # Kitchen::ActionFailed => e
184
201
  if config[:vm_rollback] == true
185
- error format("Rolling back VM %s after critical error", config[:vm_name])
202
+ error format("Rolling back VM `%s` after critical error", config[:vm_name])
186
203
 
187
204
  # Inject name of failed VM for destroy to work
188
205
  state[:vm_name] = config[:vm_name]
@@ -193,31 +210,18 @@ module Kitchen
193
210
  raise
194
211
  end
195
212
 
196
- unless config[:tags].nil? || config[:tags].empty?
197
- tag_api = VSphereAutomation::CIS::TaggingTagApi.new(api_client)
198
- vm_tags = tag_api.list.value
199
- raise format("No configured tags found on VCenter, but %s specified", config[:tags].to_s) if vm_tags.empty?
200
-
201
- valid_tags = {}
202
- vm_tags.each do |uid|
203
- tag = tag_api.get(uid)
204
-
205
- valid_tags[tag.value.name] = tag.value.id if tag.is_a? VSphereAutomation::CIS::CisTaggingTagResult
206
- end
207
-
208
- # Error out on undefined tags
209
- invalid = config[:tags] - valid_tags.keys
210
- raise format("Specified tag(s) %s not valid", invalid.join(",")) unless invalid.empty?
213
+ if vm_tags
214
+ debug format("Setting tags on machine: `%s`", vm_tags.keys.join("`, `"))
211
215
 
212
216
  tag_service = VSphereAutomation::CIS::TaggingTagAssociationApi.new(api_client)
213
- tag_ids = config[:tags].map { |name| valid_tags[name] }
217
+ raise_if_unauthenticated tag_service, "connecting to tagging service"
214
218
 
215
219
  request_body = {
216
220
  object_id: {
217
221
  id: get_vm(config[:vm_name]).vm,
218
222
  type: "VirtualMachine",
219
223
  },
220
- tag_ids: tag_ids,
224
+ tag_ids: vm_tags.values,
221
225
  }
222
226
  tag_service.attach_multiple_tags_to_object(request_body)
223
227
  end
@@ -239,6 +243,7 @@ module Kitchen
239
243
  vm = get_vm(state[:vm_name])
240
244
  unless vm.nil?
241
245
  vm_api = VSphereAutomation::VCenter::VMApi.new(api_client)
246
+ raise_if_unauthenticated vm_api, "connecting to VM API"
242
247
 
243
248
  # shut the machine down if it is running
244
249
  if vm.power_state == "POWERED_ON"
@@ -280,6 +285,7 @@ module Kitchen
280
285
  config[:vm_os] = config[:aggressive_os] unless config[:aggressive_os].nil?
281
286
  config[:vm_username] = config[:aggressive_username] unless config[:aggressive_username].nil?
282
287
  config[:vm_password] = config[:aggressive_password] unless config[:aggressive_password].nil?
288
+ config[:vm_customization] = config[:customize] unless config[:customize].nil?
283
289
  end
284
290
 
285
291
  # A helper method to validate the state
@@ -291,17 +297,69 @@ module Kitchen
291
297
  state.key?(property) && !state[property].nil?
292
298
  end
293
299
 
300
+ # Handle the non-ruby way of the SDK to report errors.
301
+ #
302
+ # @param api_response [Object] a generic API response class, which might include an error type
303
+ # @param message [String] description to output in case of error
304
+ # @raise UnauthenticatedError
305
+ def raise_if_unauthenticated(api_response, message)
306
+ session_id = api_response.api_client.default_headers["vmware-api-session-id"]
307
+ return unless UNAUTH_CLASSES.include? session_id.class
308
+
309
+ message = format("Authentication or permissions error on %s", message)
310
+ raise UnauthenticatedError.new(message)
311
+ end
312
+
313
+ # Handle missing resources in a query.
314
+ #
315
+ # @param collection [Enumerable] list which is supposed to have at least one entry
316
+ # @param message [String] description to output in case of error
317
+ # @raise ResourceMissingError
318
+ def raise_if_missing(collection, message)
319
+ return unless collection.nil? || collection.empty?
320
+
321
+ raise ResourceMissingError.new(message)
322
+ end
323
+
324
+ # Handle ambiguous resources in a query.
325
+ #
326
+ # @param collection [Enumerable] list which is supposed to one entry at most
327
+ # @param message [String] description to output in case of error
328
+ # @raise ResourceAmbiguousError
329
+ def raise_if_ambiguous(collection, message)
330
+ return unless collection.length > 1
331
+
332
+ raise ResourceAmbiguousError.new(message)
333
+ end
334
+
335
+ # Access to legacy SOAP based vMOMI API for some functionality
336
+ #
337
+ # @return [RbVmomi::VIM] VIM instance
338
+ def vim
339
+ @vim ||= RbVmomi::VIM.connect(connection_options)
340
+ end
341
+
342
+ # Search host data via vMOMI
343
+ #
344
+ # @param moref [String] identifier of a host system ("host-xxxx")
345
+ # @return [RbVmomi::VIM::HostSystem]
346
+ def host_by_moref(moref)
347
+ vim.serviceInstance.content.hostSpecManager.RetrieveHostSpecification(host: moref, fromHost: false).host
348
+ end
349
+
294
350
  # Sees in the datacenter exists or not
295
351
  #
296
352
  # @param [folder] folder is the name of the folder in which the Datacenter is stored in inventory, possibly nil
297
353
  # @param [name] name is the name of the datacenter
298
354
  def datacenter_exists?(folder, name)
299
355
  dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client)
356
+ raise_if_unauthenticated dc_api, "checking for datacenter `#{name}`"
357
+
300
358
  opts = { filter_names: name }
301
359
  opts[:filter_folders] = get_folder(folder, "DATACENTER") if folder
302
360
  dcs = dc_api.list(opts).value
303
361
 
304
- raise format("Unable to find data center: %s", name) if dcs.empty?
362
+ raise_if_missing dcs, format("Unable to find data center `%s`", name)
305
363
  end
306
364
 
307
365
  # Checks if a network exists or not
@@ -309,9 +367,43 @@ module Kitchen
309
367
  # @param [name] name is the name of the Network
310
368
  def network_exists?(name)
311
369
  net_api = VSphereAutomation::VCenter::NetworkApi.new(api_client)
370
+ raise_if_unauthenticated net_api, "checking for VM network `#{name}`"
371
+
312
372
  nets = net_api.list({ filter_names: name }).value
313
373
 
314
- raise format("Unable to find target network: %s", name) if nets.empty?
374
+ raise_if_missing nets, format("Unable to find target network: `%s`", name)
375
+ end
376
+
377
+ # Map VCenter tag names to URNs (VCenter needs tags to be predefined)
378
+ #
379
+ # @param tags [tags] tags is the list of tags to associate
380
+ # @return [Hash] mapping of VCenter tag name to URN
381
+ # @raise UnauthenticatedError
382
+ # @raise ResourceMissingError
383
+ def map_tags(tags)
384
+ return nil if tags.nil? || tags.empty?
385
+
386
+ tag_api = VSphereAutomation::CIS::TaggingTagApi.new(api_client)
387
+ raise_if_unauthenticated tag_api, "checking for tags"
388
+
389
+ vm_tags = tag_api.list.value
390
+ raise_if_missing vm_tags, format("No configured tags found on VCenter, but `%s` specified", config[:tags].to_s)
391
+
392
+ # Create list of all VCenter defined tags, associated with their internal ID
393
+ valid_tags = {}
394
+ vm_tags.each do |uid|
395
+ tag = tag_api.get(uid)
396
+
397
+ valid_tags[tag.value.name] = tag.value.id if tag.is_a? VSphereAutomation::CIS::CisTaggingTagResult
398
+ end
399
+
400
+ invalid = config[:tags] - valid_tags.keys
401
+ unless invalid.empty?
402
+ message = format("Specified tag(s) `%s` not preconfigured on VCenter", invalid.join("`, `"))
403
+ raise ResourceMissingError.new(message)
404
+ end
405
+
406
+ valid_tags.select { |tag, _urn| config[:tags].include? tag }
315
407
  end
316
408
 
317
409
  # Validates the host name of the server you can connect to
@@ -320,15 +412,31 @@ module Kitchen
320
412
  def get_host(name, datacenter, cluster = nil)
321
413
  # create a host object to work with
322
414
  host_api = VSphereAutomation::VCenter::HostApi.new(api_client)
415
+ raise_if_unauthenticated host_api, "checking for target host `#{name || "(any)"}`"
323
416
 
324
417
  hosts = host_api.list({ filter_names: name,
325
418
  filter_datacenters: datacenter,
326
419
  filter_clusters: cluster,
327
420
  filter_connection_states: ["CONNECTED"] }).value
328
421
 
329
- raise format("Unable to find target host: %s", name) if hosts.empty?
422
+ raise_if_missing hosts, format("Unable to find target host `%s`", name || "(any)")
423
+
424
+ filter_maintenance!(hosts)
425
+ raise_if_missing hosts, "Unable to find active target host in datacenter (check maintenance mode?)"
426
+
427
+ # Randomize returned hosts
428
+ host = hosts.sample
429
+ debug format("Selected host `%s` randomly for deployment", host.name)
330
430
 
331
- hosts.sample
431
+ host
432
+ end
433
+
434
+ def filter_maintenance!(hosts)
435
+ # Exclude hosts which are in maintenance mode (via SOAP API only)
436
+ hosts.reject! do |hostinfo|
437
+ host = host_by_moref(hostinfo.host)
438
+ host.runtime.inMaintenanceMode
439
+ end
332
440
  end
333
441
 
334
442
  # Gets the folder you want to create the VM
@@ -338,6 +446,8 @@ module Kitchen
338
446
  # @param [datacenter] datacenter is the datacenter of the folder
339
447
  def get_folder(name, type = "VIRTUAL_MACHINE", datacenter = nil)
340
448
  folder_api = VSphereAutomation::VCenter::FolderApi.new(api_client)
449
+ raise_if_unauthenticated folder_api, "checking for folder `#{name}`"
450
+
341
451
  parent_path, basename = File.split(name)
342
452
  filter = { filter_names: basename, filter_type: type }
343
453
  filter[:filter_datacenters] = datacenter if datacenter
@@ -345,9 +455,8 @@ module Kitchen
345
455
 
346
456
  folders = folder_api.list(filter).value
347
457
 
348
- raise format("Unable to find folder: %s", basename) if folders.empty?
349
-
350
- raise format("`%s` returned too many folders", basename) if folders.length > 1
458
+ raise_if_missing folders, format("Unable to find VM/template folder: `%s`", basename)
459
+ raise_if_ambiguous folders, format("`%s` returned too many VM/template folders", basename)
351
460
 
352
461
  folders.first.folder
353
462
  end
@@ -357,8 +466,13 @@ module Kitchen
357
466
  # @param [name] name is the name of the VM
358
467
  def get_vm(name)
359
468
  vm_api = VSphereAutomation::VCenter::VMApi.new(api_client)
469
+ raise_if_unauthenticated vm_api, "checking for VM `#{name}`"
470
+
360
471
  vms = vm_api.list({ filter_names: name }).value
361
472
 
473
+ raise_if_missing vms, format("Unable to find VM `%s`", name)
474
+ raise_if_ambiguous vms, format("`%s` returned too many VMs", name)
475
+
362
476
  vms.first
363
477
  end
364
478
 
@@ -368,13 +482,14 @@ module Kitchen
368
482
  # @param [name] name is the name of the Datacenter
369
483
  def get_datacenter(folder, name)
370
484
  dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client)
485
+ raise_if_unauthenticated dc_api, "checking for datacenter `#{name}` in folder `#{folder}`"
486
+
371
487
  opts = { filter_names: name }
372
488
  opts[:filter_folders] = get_folder(folder, "DATACENTER") if folder
373
489
  dcs = dc_api.list(opts).value
374
490
 
375
- raise format("Unable to find data center: %s", name) if dcs.empty?
376
-
377
- raise format("%s returned too many data centers", name) if dcs.length > 1
491
+ raise_if_missing dcs, format("Unable to find data center: `%s`", name)
492
+ raise_if_ambiguous dcs, format("`%s` returned too many data centers", name)
378
493
 
379
494
  dcs.first.datacenter
380
495
  end
@@ -386,11 +501,12 @@ module Kitchen
386
501
  return if name.nil?
387
502
 
388
503
  cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
389
- clusters = cluster_api.list({ filter_names: name }).value
504
+ raise_if_unauthenticated cluster_api, "checking for ID of cluster `#{name}`"
390
505
 
391
- raise format("Unable to find Cluster: %s", name) if clusters.empty?
506
+ clusters = cluster_api.list({ filter_names: name }).value
392
507
 
393
- raise format("%s returned too many clusters", name) if clusters.length > 1
508
+ raise_if_missing clusters, format("Unable to find Cluster: `%s`", name)
509
+ raise_if_ambiguous clusters, format("`%s` returned too many clusters", name)
394
510
 
395
511
  clusters.first.cluster
396
512
  end
@@ -402,9 +518,11 @@ module Kitchen
402
518
  cluster_id = get_cluster_id(name)
403
519
 
404
520
  host_api = VSphereAutomation::VCenter::HostApi.new(api_client)
405
- hosts = host_api.list({ filter_clusters: cluster_id, connection_states: "CONNECTED" }).value
521
+ raise_if_unauthenticated host_api, "checking for cluster `#{name}`"
406
522
 
407
- raise format("Unable to find active host in cluster %s", name) if hosts.empty?
523
+ hosts = host_api.list({ filter_clusters: cluster_id, connection_states: "CONNECTED" }).value
524
+ filter_maintenance!(hosts)
525
+ raise_if_missing hosts, format("Unable to find active hosts in cluster `%s`", name)
408
526
 
409
527
  cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
410
528
  cluster_api.get(cluster_id).value
@@ -417,6 +535,7 @@ module Kitchen
417
535
  def get_resource_pool(name)
418
536
  # Create a resource pool object
419
537
  rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client)
538
+ raise_if_unauthenticated rp_api, "checking for resource pool `#{name || "(default)"}`"
420
539
 
421
540
  # If no name has been set, use the first resource pool that can be found,
422
541
  # otherwise try to find by given name
@@ -441,7 +560,7 @@ module Kitchen
441
560
  debug("Search for resource pools found: " + resource_pools.map(&:name).to_s)
442
561
  end
443
562
 
444
- raise format("Unable to find Resource Pool: %s", name) if resource_pools.empty?
563
+ raise_if_missing resource_pools, format("Unable to find resource pool `%s`", name || "(default)")
445
564
 
446
565
  resource_pools.first.resource_pool
447
566
  end
@@ -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,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.8.6
4
+ version: 2.10.2
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: 2021-08-16 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
@@ -39,7 +59,7 @@ dependencies:
39
59
  version: '1.16'
40
60
  - - "<"
41
61
  - !ruby/object:Gem::Version
42
- version: '3.0'
62
+ version: '4'
43
63
  type: :runtime
44
64
  prerelease: false
45
65
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +69,7 @@ dependencies:
49
69
  version: '1.16'
50
70
  - - "<"
51
71
  - !ruby/object:Gem::Version
52
- version: '3.0'
72
+ version: '4'
53
73
  - !ruby/object:Gem::Dependency
54
74
  name: vsphere-automation-sdk
55
75
  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:
@@ -88,15 +109,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
109
  requirements:
89
110
  - - ">="
90
111
  - !ruby/object:Gem::Version
91
- version: '2.4'
112
+ version: '2.5'
92
113
  required_rubygems_version: !ruby/object:Gem::Requirement
93
114
  requirements:
94
115
  - - ">="
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: []