kitchen-vcenter 2.9.8 → 2.11.4

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: a7424e04d466fa8ed2aea4fbc36f1454ee71e4e5b2bf383490e8f1ec5a1668d5
4
- data.tar.gz: 00b915156f7dd91b71d656d24f129015a28e6349a82a06dcb50d4c2a43c38e06
3
+ metadata.gz: 28119d22d4b763e0ddbe3bcacc0fdf119e7d477a78aaab050dd0433a6656935e
4
+ data.tar.gz: 0d4ebe88e1dd7c970729c426c4b5eb19d5319a8c048ef5db1c705835efbb4a1a
5
5
  SHA512:
6
- metadata.gz: b9b8f896730e171635e298173469651183cf2e785bde217e8f49d98870ec9d21606bf3b4b5b4ea2eec762458e616a7705c8ff93471e2986fe01d108a52d5309a
7
- data.tar.gz: 72ff746c626c7e479f2e154ef7c2d8f05485cfc95726fd8673a6a27313ff89241307913a3574e77bfd1fd6304406ccc3c506bd636171b03e7ba8aa0294ffb6eb
6
+ metadata.gz: f5bf290eee4065039cd41a3b2fb74f6cdf3d6b3caf1b82b78353c84bf1e28fb69c04e029ec22327f8bc2ae0b3a60a01d60f14801c4dbf14f54b6d291b468774b
7
+ data.tar.gz: 54a3d86d39d3aa303097eab94f898d2ff015bf33af1ee2af18b2c75c8f7064f46e5b44fe13d040529321f30f44c08cb9bf64a0305c9bdace988fa6394b6ddc6d
@@ -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
@@ -106,6 +116,7 @@ module Kitchen
106
116
  config[:resource_pool] = root_pool
107
117
  else
108
118
  rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client)
119
+ raise_if_unauthenticated rp_api, "checking for resource pools"
109
120
 
110
121
  found_pool = nil
111
122
  pools = rp_api.get(root_pool).value.resource_pools
@@ -114,7 +125,7 @@ module Kitchen
114
125
  found_pool = pool if name == config[:resource_pool]
115
126
  end
116
127
 
117
- 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])
118
129
 
119
130
  config[:resource_pool] = found_pool
120
131
  end
@@ -130,7 +141,6 @@ module Kitchen
130
141
  datacenter = get_datacenter(dc_folder, dc_name)
131
142
  cluster_id = get_cluster_id(config[:cluster])
132
143
 
133
- # Using the clone class, create a machine for TK
134
144
  # Find the identifier for the targethost to pass to rbvmomi
135
145
  config[:targethost] = get_host(config[:targethost], datacenter, cluster_id)
136
146
 
@@ -145,6 +155,9 @@ module Kitchen
145
155
  }
146
156
  end
147
157
 
158
+ # Check for valid tags before cloning
159
+ vm_tags = map_tags(config[:tags])
160
+
148
161
  # Allow different clone types
149
162
  config[:clone_type] = :linked if config[:clone_type] == "linked"
150
163
  config[:clone_type] = :instant if config[:clone_type] == "instant"
@@ -186,7 +199,7 @@ module Kitchen
186
199
 
187
200
  rescue # Kitchen::ActionFailed => e
188
201
  if config[:vm_rollback] == true
189
- 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])
190
203
 
191
204
  # Inject name of failed VM for destroy to work
192
205
  state[:vm_name] = config[:vm_name]
@@ -197,31 +210,18 @@ module Kitchen
197
210
  raise
198
211
  end
199
212
 
200
- unless config[:tags].nil? || config[:tags].empty?
201
- tag_api = VSphereAutomation::CIS::TaggingTagApi.new(api_client)
202
- vm_tags = tag_api.list.value
203
- raise format("No configured tags found on VCenter, but %s specified", config[:tags].to_s) if vm_tags.empty?
204
-
205
- valid_tags = {}
206
- vm_tags.each do |uid|
207
- tag = tag_api.get(uid)
208
-
209
- valid_tags[tag.value.name] = tag.value.id if tag.is_a? VSphereAutomation::CIS::CisTaggingTagResult
210
- end
211
-
212
- # Error out on undefined tags
213
- invalid = config[:tags] - valid_tags.keys
214
- 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("`, `"))
215
215
 
216
216
  tag_service = VSphereAutomation::CIS::TaggingTagAssociationApi.new(api_client)
217
- tag_ids = config[:tags].map { |name| valid_tags[name] }
217
+ raise_if_unauthenticated tag_service, "connecting to tagging service"
218
218
 
219
219
  request_body = {
220
220
  object_id: {
221
221
  id: get_vm(config[:vm_name]).vm,
222
222
  type: "VirtualMachine",
223
223
  },
224
- tag_ids: tag_ids,
224
+ tag_ids: vm_tags.values,
225
225
  }
226
226
  tag_service.attach_multiple_tags_to_object(request_body)
227
227
  end
@@ -243,6 +243,7 @@ module Kitchen
243
243
  vm = get_vm(state[:vm_name])
244
244
  unless vm.nil?
245
245
  vm_api = VSphereAutomation::VCenter::VMApi.new(api_client)
246
+ raise_if_unauthenticated vm_api, "connecting to VM API"
246
247
 
247
248
  # shut the machine down if it is running
248
249
  if vm.power_state == "POWERED_ON"
@@ -296,17 +297,69 @@ module Kitchen
296
297
  state.key?(property) && !state[property].nil?
297
298
  end
298
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
+
299
350
  # Sees in the datacenter exists or not
300
351
  #
301
352
  # @param [folder] folder is the name of the folder in which the Datacenter is stored in inventory, possibly nil
302
353
  # @param [name] name is the name of the datacenter
303
354
  def datacenter_exists?(folder, name)
304
355
  dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client)
356
+ raise_if_unauthenticated dc_api, "checking for datacenter `#{name}`"
357
+
305
358
  opts = { filter_names: name }
306
359
  opts[:filter_folders] = get_folder(folder, "DATACENTER") if folder
307
360
  dcs = dc_api.list(opts).value
308
361
 
309
- 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)
310
363
  end
311
364
 
312
365
  # Checks if a network exists or not
@@ -314,9 +367,43 @@ module Kitchen
314
367
  # @param [name] name is the name of the Network
315
368
  def network_exists?(name)
316
369
  net_api = VSphereAutomation::VCenter::NetworkApi.new(api_client)
370
+ raise_if_unauthenticated net_api, "checking for VM network `#{name}`"
371
+
317
372
  nets = net_api.list({ filter_names: name }).value
318
373
 
319
- 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 }
320
407
  end
321
408
 
322
409
  # Validates the host name of the server you can connect to
@@ -325,15 +412,31 @@ module Kitchen
325
412
  def get_host(name, datacenter, cluster = nil)
326
413
  # create a host object to work with
327
414
  host_api = VSphereAutomation::VCenter::HostApi.new(api_client)
415
+ raise_if_unauthenticated host_api, "checking for target host `#{name || "(any)"}`"
328
416
 
329
417
  hosts = host_api.list({ filter_names: name,
330
418
  filter_datacenters: datacenter,
331
419
  filter_clusters: cluster,
332
420
  filter_connection_states: ["CONNECTED"] }).value
333
421
 
334
- 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)
335
430
 
336
- 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
337
440
  end
338
441
 
339
442
  # Gets the folder you want to create the VM
@@ -343,6 +446,8 @@ module Kitchen
343
446
  # @param [datacenter] datacenter is the datacenter of the folder
344
447
  def get_folder(name, type = "VIRTUAL_MACHINE", datacenter = nil)
345
448
  folder_api = VSphereAutomation::VCenter::FolderApi.new(api_client)
449
+ raise_if_unauthenticated folder_api, "checking for folder `#{name}`"
450
+
346
451
  parent_path, basename = File.split(name)
347
452
  filter = { filter_names: basename, filter_type: type }
348
453
  filter[:filter_datacenters] = datacenter if datacenter
@@ -350,9 +455,8 @@ module Kitchen
350
455
 
351
456
  folders = folder_api.list(filter).value
352
457
 
353
- raise format("Unable to find folder: %s", basename) if folders.empty?
354
-
355
- 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)
356
460
 
357
461
  folders.first.folder
358
462
  end
@@ -362,8 +466,13 @@ module Kitchen
362
466
  # @param [name] name is the name of the VM
363
467
  def get_vm(name)
364
468
  vm_api = VSphereAutomation::VCenter::VMApi.new(api_client)
469
+ raise_if_unauthenticated vm_api, "checking for VM `#{name}`"
470
+
365
471
  vms = vm_api.list({ filter_names: name }).value
366
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
+
367
476
  vms.first
368
477
  end
369
478
 
@@ -373,13 +482,14 @@ module Kitchen
373
482
  # @param [name] name is the name of the Datacenter
374
483
  def get_datacenter(folder, name)
375
484
  dc_api = VSphereAutomation::VCenter::DatacenterApi.new(api_client)
485
+ raise_if_unauthenticated dc_api, "checking for datacenter `#{name}` in folder `#{folder}`"
486
+
376
487
  opts = { filter_names: name }
377
488
  opts[:filter_folders] = get_folder(folder, "DATACENTER") if folder
378
489
  dcs = dc_api.list(opts).value
379
490
 
380
- raise format("Unable to find data center: %s", name) if dcs.empty?
381
-
382
- 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)
383
493
 
384
494
  dcs.first.datacenter
385
495
  end
@@ -391,11 +501,12 @@ module Kitchen
391
501
  return if name.nil?
392
502
 
393
503
  cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
394
- clusters = cluster_api.list({ filter_names: name }).value
504
+ raise_if_unauthenticated cluster_api, "checking for ID of cluster `#{name}`"
395
505
 
396
- raise format("Unable to find Cluster: %s", name) if clusters.empty?
506
+ clusters = cluster_api.list({ filter_names: name }).value
397
507
 
398
- 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)
399
510
 
400
511
  clusters.first.cluster
401
512
  end
@@ -407,9 +518,11 @@ module Kitchen
407
518
  cluster_id = get_cluster_id(name)
408
519
 
409
520
  host_api = VSphereAutomation::VCenter::HostApi.new(api_client)
410
- hosts = host_api.list({ filter_clusters: cluster_id, connection_states: "CONNECTED" }).value
521
+ raise_if_unauthenticated host_api, "checking for cluster `#{name}`"
411
522
 
412
- 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)
413
526
 
414
527
  cluster_api = VSphereAutomation::VCenter::ClusterApi.new(api_client)
415
528
  cluster_api.get(cluster_id).value
@@ -422,6 +535,7 @@ module Kitchen
422
535
  def get_resource_pool(name)
423
536
  # Create a resource pool object
424
537
  rp_api = VSphereAutomation::VCenter::ResourcePoolApi.new(api_client)
538
+ raise_if_unauthenticated rp_api, "checking for resource pool `#{name || "(default)"}`"
425
539
 
426
540
  # If no name has been set, use the first resource pool that can be found,
427
541
  # otherwise try to find by given name
@@ -446,7 +560,7 @@ module Kitchen
446
560
  debug("Search for resource pools found: " + resource_pools.map(&:name).to_s)
447
561
  end
448
562
 
449
- 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)")
450
564
 
451
565
  resource_pools.first.resource_pool
452
566
  end
@@ -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.9.8"
23
+ VERSION = "2.11.4"
24
24
  end
@@ -184,6 +184,8 @@ class Support
184
184
 
185
185
  def reconnect_network_device(vm)
186
186
  network_device = network_device(vm)
187
+ return unless network_device
188
+
187
189
  network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo(
188
190
  allowGuestControl: true,
189
191
  startConnected: true,
@@ -393,6 +395,12 @@ class Support
393
395
  end
394
396
  end
395
397
 
398
+ guestinfo = options[:vm_customization].select { |key, _| key =~ /^guestinfo\..*/ }
399
+ unless guestinfo.empty?
400
+ gi = guestinfo.map { |k, v| { key: k, value: v } }
401
+ config[:extraConfig] = gi
402
+ end
403
+
396
404
  config_spec = RbVmomi::VIM.VirtualMachineConfigSpec(config)
397
405
 
398
406
  task = vm.ReconfigVM_Task(spec: config_spec)
@@ -496,13 +504,15 @@ class Support
496
504
  relocate_spec.pool = options[:resource_pool]
497
505
 
498
506
  # Change network, if wanted
499
- unless options[:network_name].nil?
507
+ network_device = network_device(src_vm)
508
+ Kitchen.logger.warn format("Source VM/template does not have any network device (use VMware IPPools and vsphere-gom transport or govc to access)") unless network_device
509
+
510
+ unless network_device.nil? || options[:network_name].nil?
500
511
  networks = dc.network.select { |n| n.name == options[:network_name] }
501
512
  raise Support::CloneError.new(format("Could not find network named %s", options[:network_name])) if networks.empty?
502
513
 
503
514
  Kitchen.logger.warn format("Found %d networks named %s, picking first one", networks.count, options[:network_name]) if networks.count > 1
504
515
  network_obj = networks.first
505
- network_device = network_device(src_vm)
506
516
 
507
517
  if network_obj.is_a? RbVmomi::VIM::DistributedVirtualPortgroup
508
518
  Kitchen.logger.info format("Assigning network %s...", network_obj.pretty_path)
@@ -566,19 +576,20 @@ class Support
566
576
  # MAC at least with 6.7.0 build 9433931
567
577
 
568
578
  # Disconnect network device, so wo don't get IP collisions on start
569
- network_device = network_device(src_vm)
570
- network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo(
571
- allowGuestControl: true,
572
- startConnected: true,
573
- connected: false,
574
- migrateConnect: "disconnect"
575
- )
576
- relocate_spec.deviceChange = [
577
- RbVmomi::VIM.VirtualDeviceConfigSpec(
578
- operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("edit"),
579
- device: network_device
580
- ),
581
- ]
579
+ if network_device
580
+ network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo(
581
+ allowGuestControl: true,
582
+ startConnected: true,
583
+ connected: false,
584
+ migrateConnect: "disconnect"
585
+ )
586
+ relocate_spec.deviceChange = [
587
+ RbVmomi::VIM.VirtualDeviceConfigSpec(
588
+ operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("edit"),
589
+ device: network_device
590
+ ),
591
+ ]
592
+ end
582
593
 
583
594
  clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec,
584
595
  name: vm_name)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-vcenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.8
4
+ version: 2.11.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-04 00:00:00.000000000 Z
11
+ date: 2021-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-ping
@@ -59,7 +59,7 @@ dependencies:
59
59
  version: '1.16'
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
- version: '3.0'
62
+ version: '4'
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '1.16'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '3.0'
72
+ version: '4'
73
73
  - !ruby/object:Gem::Dependency
74
74
  name: vsphere-automation-sdk
75
75
  requirement: !ruby/object:Gem::Requirement
@@ -109,7 +109,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
109
  requirements:
110
110
  - - ">="
111
111
  - !ruby/object:Gem::Version
112
- version: '2.4'
112
+ version: '2.6'
113
113
  required_rubygems_version: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="