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 +4 -4
- data/lib/kitchen-vcenter/version.rb +1 -1
- data/lib/kitchen/driver/vcenter.rb +159 -40
- data/lib/support/clone_vm.rb +40 -106
- data/lib/support/guest_customization.rb +336 -0
- metadata +28 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c1f638e5648056082f4da2ce087031e468d0bb1113c480af1f04b98496fad5a
|
4
|
+
data.tar.gz: 00d9fd698d02804d4942c1be24802954d5f72ad6cbf97040eecd1d099e959631
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66e78bad8190b260de8eebcca12051a76452110ad39136e198d1f1c9823dbadbf6698aab4d7e2fe70873fb001f28268f37c5ddec2b72ecc853f6e1d06c77b17a
|
7
|
+
data.tar.gz: 1c60d33094d436b989b503268ce68ef7bd729a0216a848cb6de6d00c65f1ecf1af3dd92fb6913f5104a2c7de225ddc6159ddbd02ee7353c6bf1d4aa4a9357ace
|
@@ -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 :
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
197
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
504
|
+
raise_if_unauthenticated cluster_api, "checking for ID of cluster `#{name}`"
|
390
505
|
|
391
|
-
|
506
|
+
clusters = cluster_api.list({ filter_names: name }).value
|
392
507
|
|
393
|
-
|
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
|
-
|
521
|
+
raise_if_unauthenticated host_api, "checking for cluster `#{name}`"
|
406
522
|
|
407
|
-
|
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
|
-
|
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
|
data/lib/support/clone_vm.rb
CHANGED
@@ -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, :
|
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
|
-
@
|
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
|
-
|
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
|
322
|
-
Kitchen.logger.info "Waiting for
|
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[:
|
331
|
+
config = options[:vm_customization].select { |key, _| %i{annotation memoryMB numCPUs}.include? key }
|
327
332
|
|
328
|
-
add_disks = options[:
|
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
|
442
|
-
unless
|
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
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
518
|
-
|
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:
|
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[:
|
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:
|
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? ?
|
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
|
-
|
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[:
|
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",
|
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.
|
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:
|
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: '
|
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: '
|
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.
|
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.
|
119
|
+
rubygems_version: 3.1.4
|
99
120
|
signing_key:
|
100
121
|
specification_version: 4
|
101
|
-
summary: Test Kitchen driver for
|
122
|
+
summary: Test Kitchen driver for VMware vCenter
|
102
123
|
test_files: []
|