kitchen-vcenter 2.2.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92b3382aeae44f9fd4738c426dd4671a6004ee92c3a6c3a40d9244ac07fe30ea
4
- data.tar.gz: 53bb203127b738a42d6cdccf10588778167674424c2f3f5f413334299391334b
3
+ metadata.gz: 45583a32d2c2ccf32d21491735bbe862c1cc85990ea89bc773a06086a117fc17
4
+ data.tar.gz: 872abb17f6fd2caf8bbe14f05c31fbe9c402bb065a3c6b010bc8136a944dc525
5
5
  SHA512:
6
- metadata.gz: cd1808286253bb6922355e0be07fa584bc41b2eec80c076f1cc9e81d138402e6b0de1a1f4aef9802b849ae6ba922f0c7a7278b3736ff9ec6a3bba54fbf0a4afd
7
- data.tar.gz: c8521069e9b18129ba8f2e0b0088465f65527f0b01bf351a9ab7c3282b2584a9f42c5ba738884c216988a695bd1170904648940a96e5e7ef925077401ca9aae9
6
+ metadata.gz: d523aa9288f11454ef5bffd91be68132e3ed34028566af46ddbc85daeb707e05245604477234077a836fbce5e108a1f528b89f65d01653ba62e43ddd94b18747
7
+ data.tar.gz: c5ee31885c9a12eb0cd9d7e28d2dbb99e0257a897912054ca566aad2cef05f52301ebc65a1e61b588fbbcdf0587982aadfc29e1e72addfa29f2ae9c51a589b8d
@@ -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.2.2"
23
+ VERSION = "2.4.0"
24
24
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
- #
3
1
  # Author:: Chef Partner Engineering (<partnereng@chef.io>)
4
2
  # Copyright:: Copyright (c) 2017 Chef Software, Inc.
5
3
  # License:: Apache License, Version 2.0
@@ -53,6 +51,32 @@ module Kitchen
53
51
  default_config :vm_rollback, false
54
52
  default_config :customize, nil
55
53
  default_config :interface, nil
54
+ default_config :active_discovery, false
55
+ default_config :active_discovery_command, nil
56
+ default_config :vm_os, nil
57
+ default_config :vm_username, "vagrant"
58
+ default_config :vm_password, "vagrant"
59
+ default_config :vm_win_network, "Ethernet0"
60
+
61
+ default_config :benchmark, false
62
+ default_config :benchmark_file, "kitchen-vcenter.csv"
63
+
64
+ deprecate_config_for :aggressive_mode, Util.outdent!(<<-MSG)
65
+ The 'aggressive_mode' setting was renamed to 'active_discovery' and
66
+ will be removed in future versions
67
+ MSG
68
+ deprecate_config_for :aggressive_os, Util.outdent!(<<-MSG)
69
+ The 'aggressive_os' setting was renamed to 'vm_os' and will be
70
+ removed in future versions.
71
+ MSG
72
+ deprecate_config_for :aggressive_username, Util.outdent!(<<-MSG)
73
+ The 'aggressive_username' setting was renamed to 'vm_username' and will
74
+ be removed in future versions.
75
+ MSG
76
+ deprecate_config_for :aggressive_password, Util.outdent!(<<-MSG)
77
+ The 'aggressive_password' setting was renamed to 'vm_password' and will
78
+ be removed in future versions.
79
+ MSG
56
80
 
57
81
  # The main create method
58
82
  #
@@ -117,12 +141,20 @@ module Kitchen
117
141
  datacenter: config[:datacenter],
118
142
  folder: config[:folder],
119
143
  resource_pool: config[:resource_pool],
120
- clone_type: config[:clone_type],
144
+ clone_type: config[:clone_type].to_sym,
121
145
  network_name: config[:network_name],
122
146
  interface: config[:interface],
123
147
  wait_timeout: config[:vm_wait_timeout],
124
148
  wait_interval: config[:vm_wait_interval],
125
149
  customize: config[:customize],
150
+ active_discovery: config[:active_discovery],
151
+ active_discovery_command: config[:active_discovery_command],
152
+ vm_os: config[:vm_os],
153
+ vm_username: config[:vm_username],
154
+ vm_password: config[:vm_password],
155
+ vm_win_network: config[:vm_win_network],
156
+ benchmark: config[:benchmark],
157
+ benchmark_file: config[:benchmark_file],
126
158
  }
127
159
 
128
160
  begin
@@ -226,6 +258,12 @@ module Kitchen
226
258
  # if config[:cluster].nil? && config[:resource_pool].nil?
227
259
  # warn("It is recommended to specify cluster and/or resource_pool to avoid unpredictable machine placement on large deployments")
228
260
  # end
261
+
262
+ # Process deprecated parameters
263
+ config[:active_discovery] = config[:aggressive_mode] unless config[:aggressive_mode].nil?
264
+ config[:vm_os] = config[:aggressive_os] unless config[:aggressive_os].nil?
265
+ config[:vm_username] = config[:aggressive_username] unless config[:aggressive_username].nil?
266
+ config[:vm_password] = config[:aggressive_password] unless config[:aggressive_password].nil?
229
267
  end
230
268
 
231
269
  # A helper method to validate the state
@@ -1,92 +1,360 @@
1
1
  require "kitchen"
2
2
  require "rbvmomi"
3
+ require "support/guest_operations"
3
4
 
4
5
  class Support
6
+ class CloneError < RuntimeError; end
7
+
5
8
  class CloneVm
6
- attr_reader :vim, :options, :vm, :name, :path, :ip
9
+ attr_reader :vim, :options, :ssl_verify, :vm, :name, :ip, :guest_auth, :username
7
10
 
8
11
  def initialize(conn_opts, options)
9
12
  @options = options
10
13
  @name = options[:name]
14
+ @ssl_verify = !conn_opts[:insecure]
11
15
 
12
16
  # Connect to vSphere
13
17
  @vim ||= RbVmomi::VIM.connect conn_opts
18
+
19
+ @username = options[:vm_username]
20
+ password = options[:vm_password]
21
+ @guest_auth = RbVmomi::VIM::NamePasswordAuthentication(interactiveSession: false, username: username, password: password)
22
+
23
+ @benchmark_data = {}
24
+ end
25
+
26
+ def active_discovery?
27
+ options[:active_discovery] == true
14
28
  end
15
29
 
16
- def get_ip(vm)
17
- @ip = nil
30
+ def ip_from_tools
31
+ return if vm.guest.net.empty?
18
32
 
19
33
  # Don't simply use vm.guest.ipAddress to allow specifying a different interface
20
- unless vm.guest.net.empty? || !vm.guest.ipAddress
21
- nics = vm.guest.net
22
- if options[:interface]
23
- nics.select! { |nic| nic.network == options[:interface] }
34
+ nics = vm.guest.net
35
+ if options[:interface]
36
+ nics.select! { |nic| nic.network == options[:interface] }
24
37
 
25
- raise format("No interfaces found on VM which are attached to network '%s'", options[:interface]) if nics.empty?
26
- end
38
+ raise Support::CloneError.new(format("No interfaces found on VM which are attached to network '%s'", options[:interface])) if nics.empty?
39
+ end
27
40
 
28
- vm_ip = nil
29
- nics.each do |net|
30
- vm_ip = net.ipConfig.ipAddress.detect { |addr| addr.origin != "linklayer" }
31
- break unless vm_ip.nil?
32
- end
41
+ vm_ip = nil
42
+ nics.each do |net|
43
+ vm_ip = net.ipConfig.ipAddress.detect { |addr| addr.origin != "linklayer" }
44
+ break unless vm_ip.nil?
45
+ end
46
+
47
+ vm_ip&.ipAddress
48
+ end
49
+
50
+ def wait_for_tools(timeout = 30.0, interval = 2.0)
51
+ start = Time.new
33
52
 
34
- extended_msg = options[:interface] ? "Network #{options[:interface]}" : ""
35
- raise format("No valid IP found on VM %s", extended_msg) if vm_ip.nil?
53
+ loop do
54
+ if vm.guest.toolsRunningStatus == "guestToolsRunning"
55
+ benchmark_checkpoint("tools_detected") if benchmark?
36
56
 
37
- @ip = vm_ip.ipAddress
57
+ Kitchen.logger.debug format("Tools detected after %.1f seconds", Time.new - start)
58
+ return
59
+ end
60
+ break if (Time.new - start) >= timeout
61
+ sleep interval
38
62
  end
39
63
 
40
- ip
64
+ raise Support::CloneError.new("Timeout waiting for VMware Tools")
41
65
  end
42
66
 
43
- def wait_for_ip(vm, timeout = 30.0, interval = 2.0)
67
+ def wait_for_ip(timeout = 60.0, interval = 2.0)
44
68
  start = Time.new
45
69
 
46
70
  ip = nil
47
71
  loop do
48
- ip = get_ip(vm)
49
- break if ip || (Time.new - start) >= timeout
72
+ ip = ip_from_tools
73
+ if ip || (Time.new - start) >= timeout
74
+ Kitchen.logger.debug format("IP retrieved after %.1f seconds", Time.new - start) if ip
75
+ break
76
+ end
50
77
  sleep interval
51
78
  end
52
79
 
53
- raise "Timeout waiting for IP address or no VMware Tools installed on guest" if ip.nil?
54
- raise format("Error getting accessible IP address, got %s. Check DHCP server and scope exhaustion", ip) if ip =~ /^169\.254\./
80
+ raise Support::CloneError.new("Timeout waiting for IP address") if ip.nil?
81
+ raise Support::CloneError.new(format("Error getting accessible IP address, got %s. Check DHCP server and scope exhaustion", ip)) if ip =~ /^169\.254\./
82
+
83
+ @ip = ip
84
+ end
85
+
86
+ def benchmark?
87
+ options[:benchmark] == true
88
+ end
89
+
90
+ def benchmark_file
91
+ options[:benchmark_file]
92
+ end
93
+
94
+ def benchmark_start
95
+ Kitchen.logger.debug("Starting benchmark data collection.")
96
+
97
+ @benchmark_data = {
98
+ template: options[:template],
99
+ clonetype: options[:clone_type],
100
+ checkpoints: [
101
+ { title: "timestamp", value: Time.new.to_f },
102
+ ],
103
+ }
104
+ end
105
+
106
+ def benchmark_checkpoint(title)
107
+ timestamp = Time.new
108
+ checkpoints = @benchmark_data[:checkpoints]
109
+
110
+ total = timestamp - checkpoints.first.fetch(:value)
111
+ Kitchen.logger.debug format(
112
+ 'Benchmark: Step "%s" at %d (%.1f since start)',
113
+ title, timestamp, total.to_f
114
+ )
115
+
116
+ @benchmark_data[:checkpoints] << {
117
+ title: title.to_sym,
118
+ value: total,
119
+ }
120
+ end
121
+
122
+ def benchmark_persist
123
+ # Add total time spent as well
124
+ checkpoints = @benchmark_data[:checkpoints]
125
+ checkpoints << {
126
+ title: :total,
127
+ value: Time.new - checkpoints.first.fetch(:value),
128
+ }
129
+
130
+ # Include CSV headers
131
+ unless File.exist?(benchmark_file)
132
+ header = "template, clonetype, active_discovery, "
133
+ header += checkpoints.map { |entry| entry[:title] }.join(", ") + "\n"
134
+ File.write(benchmark_file, header)
135
+ end
136
+
137
+ active_discovery = options[:active_discovery] || instant_clone?
138
+ data = [@benchmark_data[:template], @benchmark_data[:clonetype], active_discovery.to_s]
139
+ data << checkpoints.map { |entry| format("%.1f", entry[:value]) }
140
+
141
+ file = File.new(benchmark_file, "a")
142
+ file.puts(data.join(", ") + "\n")
143
+
144
+ Kitchen.logger.debug format("Benchmark: Appended data to file %s", benchmark_file)
145
+ end
146
+
147
+ def detect_os
148
+ vm.config&.guestId&.match(/^win/) ? :windows : :linux
149
+ end
150
+
151
+ def windows?
152
+ options[:vm_os].downcase.to_sym == :windows
153
+ end
154
+
155
+ def linux?
156
+ options[:vm_os].downcase.to_sym == :linux
157
+ end
158
+
159
+ def network_device(vm)
160
+ all_network_devices = vm.config.hardware.device.select do |device|
161
+ device.is_a?(RbVmomi::VIM::VirtualEthernetCard)
162
+ end
163
+
164
+ # Only support for first NIC so far
165
+ all_network_devices.first
166
+ end
167
+
168
+ def reconnect_network_device(vm)
169
+ network_device = network_device(vm)
170
+ network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo(
171
+ allowGuestControl: true,
172
+ startConnected: true,
173
+ connected: true
174
+ )
175
+
176
+ config_spec = RbVmomi::VIM.VirtualMachineConfigSpec(
177
+ deviceChange: [
178
+ RbVmomi::VIM.VirtualDeviceConfigSpec(
179
+ operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("edit"),
180
+ device: network_device
181
+ )
182
+ ]
183
+ )
184
+
185
+ task = vm.ReconfigVM_Task(spec: config_spec)
186
+ task.wait_for_completion
187
+
188
+ benchmark_checkpoint("nic_reconfigured") if benchmark?
189
+ end
190
+
191
+ def standard_ip_discovery
192
+ Kitchen.logger.info format("Waiting for IP (timeout: %d seconds)...", options[:wait_timeout])
193
+ wait_for_ip(options[:wait_timeout], options[:wait_interval])
194
+ end
195
+
196
+ def command_separator
197
+ case options[:vm_os].downcase.to_sym
198
+ when :linux
199
+ " && "
200
+ when :windows
201
+ " & "
202
+ end
203
+ end
204
+
205
+ # Rescan network adapters for MAC/IP changes
206
+ def rescan_commands
207
+ Kitchen.logger.info "Refreshing network interfaces in OS"
208
+
209
+ case options[:vm_os].downcase.to_sym
210
+ when :linux
211
+ # @todo: allow override if no dhclient
212
+ return [
213
+ "/sbin/modprobe -r vmxnet3",
214
+ "/sbin/modprobe vmxnet3",
215
+ "/sbin/dhclient"
216
+ ]
217
+ when :windows
218
+ return [
219
+ "netsh interface set Interface #{options[:vm_win_network]} disable",
220
+ "netsh interface set Interface #{options[:vm_win_network]} enable",
221
+ "ipconfig /renew",
222
+ ]
223
+ end
224
+ end
225
+
226
+ # Available from VMware Tools 10.1.0 this pushes the IP instead of the standard 30 second poll
227
+ # This will be used to provide a quick fallback, if active discovery fails.
228
+ def trigger_tools
229
+ case options[:vm_os].downcase.to_sym
230
+ when :linux
231
+ [
232
+ "/usr/bin/vmware-toolbox-cmd info update network"
233
+ ]
234
+ when :windows
235
+ [
236
+ '"C:\Program Files\VMware\VMware Tools\VMwareToolboxCmd.exe" info update network',
237
+ ]
238
+ end
239
+ end
240
+
241
+ # Retrieve IP via OS commands
242
+ def discovery_commands
243
+ if options[:active_discovery_command].nil?
244
+ case options[:vm_os].downcase.to_sym
245
+ when :linux
246
+ "ip address show scope global | grep global | cut -b10- | cut -d/ -f1"
247
+ when :windows
248
+ ["sleep 5", "ipconfig"]
249
+ # "ipconfig /renew"
250
+ # "wmic nicconfig get IPAddress",
251
+ # "netsh interface ip show ipaddress #{options[:vm_win_network]}"
252
+ end
253
+ end
254
+ end
255
+
256
+ def active_ip_discovery(prefix_commands = [])
257
+ # Instant clone needs this to have synchronous reply on the new IP
258
+ return unless active_discovery? || instant_clone?
259
+
260
+ Kitchen.logger.info "Attempting active IP discovery"
261
+ begin
262
+ tools = Support::GuestOperations.new(vim, vm, guest_auth, ssl_verify)
263
+
264
+ commands = []
265
+ commands << rescan_commands if instant_clone?
266
+ # commands << trigger_tools # deactivated for now, as benefit is doubtful
267
+ commands << discovery_commands
268
+ script = commands.flatten.join(command_separator)
269
+
270
+ stdout = tools.run_shell_capture_output(script, :auto, 20)
271
+
272
+ # Windows returns wrongly encoded UTF-8 for some reason
273
+ stdout = stdout.bytes.map { |b| (32..126).cover?(b.ord) ? b.chr : nil }.join unless stdout.ascii_only?
274
+ @ip = stdout.match(/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/m)&.captures&.first
275
+
276
+ Kitchen.logger.debug format("Script output: %s", stdout)
277
+ raise Support::CloneError.new(format("Could not find IP in script output, fallback to standard discovery")) if ip.nil?
278
+ raise Support::CloneError.new(format("Error getting accessible IP address, got %s. Check DHCP server, scope exhaustion or timing issues", ip)) if ip =~ /^169\.254\./
279
+ rescue RbVmomi::Fault => e
280
+ if e.fault.class.wsdl_name == "InvalidGuestLogin"
281
+ message = format('Error authenticating to guest OS as "%s", check configuration of "vm_username"/"vm_password"', username)
282
+ else
283
+ message = e.message
284
+ end
285
+
286
+ raise Support::CloneError.new(message)
287
+ rescue ::StandardError => e
288
+ Kitchen.logger.info format("Active discovery failed: %s", e.message)
289
+ return false
290
+ end
291
+
292
+ true
293
+ end
294
+
295
+ def reconfigure_guest
296
+ Kitchen.logger.info "Waiting for reconfiguration to finish"
297
+
298
+ # Pass contents of the customization option/Hash through to allow full customization
299
+ # https://pubs.vmware.com/vsphere-6-5/index.jsp?topic=%2Fcom.vmware.wssdk.smssdk.doc%2Fvim.vm.ConfigSpec.html
300
+ config_spec = RbVmomi::VIM.VirtualMachineConfigSpec(options[:customize])
301
+
302
+ task = vm.ReconfigVM_Task(spec: config_spec)
303
+ task.wait_for_completion
304
+
305
+ benchmark_checkpoint("reconfigured") if benchmark?
306
+ end
307
+
308
+ def instant_clone?
309
+ options[:clone_type] == :instant
310
+ end
311
+
312
+ def linked_clone?
313
+ options[:clone_type] == :linked
314
+ end
315
+
316
+ def full_clone?
317
+ options[:clone_type] == :full
55
318
  end
56
319
 
57
320
  def clone
321
+ benchmark_start if benchmark?
322
+
58
323
  # set the datacenter name
59
324
  dc = vim.serviceInstance.find_datacenter(options[:datacenter])
60
325
 
61
326
  # reference template using full inventory path
62
- root_folder = @vim.serviceInstance.content.rootFolder
327
+ root_folder = vim.serviceInstance.content.rootFolder
63
328
  inventory_path = format("/%s/vm/%s", options[:datacenter], options[:template])
64
329
  src_vm = root_folder.findByInventoryPath(inventory_path)
65
- raise format("Unable to find template: %s", options[:template]) if src_vm.nil?
330
+ raise Support::CloneError.new(format("Unable to find template: %s", options[:template])) if src_vm.nil?
331
+
332
+ if src_vm.config.template && !full_clone?
333
+ Kitchen.logger.warn "Source is a template, thus falling back to full clone. Reference a VM for linked/instant clones."
334
+ options[:clone_type] = :full
335
+ end
336
+
337
+ if src_vm.snapshot.nil? && !full_clone?
338
+ Kitchen.logger.warn "Source VM has no snapshot available, thus falling back to full clone. Create a snapshot for linked/instant clones."
339
+ options[:clone_type] = :full
340
+ end
66
341
 
67
342
  # Specify where the machine is going to be created
68
343
  relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec
69
344
 
70
345
  # Setting the host is not allowed for instant clone due to VM memory sharing
71
- relocate_spec.host = options[:targethost].host unless options[:clone_type] == :instant
346
+ relocate_spec.host = options[:targethost].host unless instant_clone?
72
347
 
73
348
  # Change to delta disks for linked clones
74
- relocate_spec.diskMoveType = :moveChildMostDiskBacking if options[:clone_type] == :linked
349
+ relocate_spec.diskMoveType = :moveChildMostDiskBacking if linked_clone?
75
350
 
76
351
  # Set the resource pool
77
352
  relocate_spec.pool = options[:resource_pool]
78
353
 
79
354
  # Change network, if wanted
80
355
  unless options[:network_name].nil?
81
- all_network_devices = src_vm.config.hardware.device.select do |device|
82
- device.is_a?(RbVmomi::VIM::VirtualEthernetCard)
83
- end
84
-
85
- # Only support for first NIC so far
86
- network_device = all_network_devices.first
87
-
88
356
  networks = dc.network.select { |n| n.name == options[:network_name] }
89
- raise format("Could not find network named %s", option[:network_name]) if networks.empty?
357
+ raise Support::CloneError.new(format("Could not find network named %s", option[:network_name])) if networks.empty?
90
358
 
91
359
  Kitchen.logger.warn format("Found %d networks named %s, picking first one", networks.count, options[:network_name]) if networks.count > 1
92
360
  network_obj = networks.first
@@ -97,6 +365,7 @@ class Support
97
365
  vds_obj = network_obj.config.distributedVirtualSwitch
98
366
  Kitchen.logger.info format("Using vDS '%s' for network connectivity...", vds_obj.name)
99
367
 
368
+ network_device = network_device(src_vm)
100
369
  network_device.backing = RbVmomi::VIM.VirtualEthernetCardDistributedVirtualPortBackingInfo(
101
370
  port: RbVmomi::VIM.DistributedVirtualSwitchPortConnection(
102
371
  portgroupKey: network_obj.key,
@@ -110,7 +379,7 @@ class Support
110
379
  deviceName: options[:network_name]
111
380
  )
112
381
  else
113
- raise format("Unknown network type %s for network name %s", network_obj.class.to_s, options[:network_name])
382
+ raise Support::CloneError.new(format("Unknown network type %s for network name %s", network_obj.class.to_s, options[:network_name]))
114
383
  end
115
384
 
116
385
  relocate_spec.deviceChange = [
@@ -125,71 +394,97 @@ class Support
125
394
  dest_folder = options[:folder].nil? ? dc.vmFolder : options[:folder][:id]
126
395
 
127
396
  Kitchen.logger.info format("Cloning '%s' to create the VM...", options[:template])
128
- if options[:clone_type] == :instant
397
+ if instant_clone?
129
398
  vcenter_data = vim.serviceInstance.content.about
130
- raise "Instant clones only supported with vCenter 6.7 or higher" unless vcenter_data.version.to_f >= 6.7
399
+ raise Support::CloneError.new("Instant clones only supported with vCenter 6.7 or higher") unless vcenter_data.version.to_f >= 6.7
131
400
  Kitchen.logger.debug format("Detected %s", vcenter_data.fullName)
132
401
 
133
402
  resources = dc.hostFolder.children
134
403
  hosts = resources.select { |resource| resource.class.to_s =~ /ComputeResource$/ }.map { |c| c.host }.flatten
135
404
  targethost = hosts.select { |host| host.summary.config.name == options[:targethost].name }.first
136
- raise "No matching ComputeResource found in host folder" if targethost.nil?
405
+ raise Support::CloneError.new("No matching ComputeResource found in host folder") if targethost.nil?
137
406
 
138
407
  esx_data = targethost.summary.config.product
139
- raise "Instant clones only supported with ESX 6.7 or higher" unless esx_data.version.to_f >= 6.7
408
+ raise Support::CloneError.new("Instant clones only supported with ESX 6.7 or higher") unless esx_data.version.to_f >= 6.7
140
409
  Kitchen.logger.debug format("Detected %s", esx_data.fullName)
141
410
 
142
411
  # Other tools check for VMWare Tools status, but that will be toolsNotRunning on frozen VMs
143
- raise "Need a running VM for instant clones" unless src_vm.runtime.powerState == "poweredOn"
412
+ raise Support::CloneError.new("Need a running VM for instant clones") unless src_vm.runtime.powerState == "poweredOn"
144
413
 
145
414
  # In first iterations, only support the Frozen Source VM workflow. This is more efficient
146
415
  # but needs preparations (freezing the source VM). Running Source VM support is to be
147
416
  # added later
148
- raise "Need a frozen VM for instant clones, running source VM not supported yet" unless src_vm.runtime.instantCloneFrozen
417
+ raise Support::CloneError.new("Need a frozen VM for instant clones, running source VM not supported yet") unless src_vm.runtime.instantCloneFrozen
149
418
 
150
419
  # Swapping NICs not needed anymore (blog posts mention this), instant clones get a new
151
420
  # MAC at least with 6.7.0 build 9433931
152
421
 
153
- # @todo not working yet
154
- # relocate_spec.folder = dest_folder
422
+ # Disconnect network device, so wo don't get IP collisions on start
423
+ network_device = network_device(src_vm)
424
+ network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo(
425
+ allowGuestControl: true,
426
+ startConnected: true,
427
+ connected: false,
428
+ migrateConnect: "disconnect"
429
+ )
430
+ relocate_spec.deviceChange = [
431
+ RbVmomi::VIM.VirtualDeviceConfigSpec(
432
+ operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation("edit"),
433
+ device: network_device
434
+ )
435
+ ]
436
+
155
437
  clone_spec = RbVmomi::VIM.VirtualMachineInstantCloneSpec(location: relocate_spec,
156
438
  name: name)
157
439
 
440
+ benchmark_checkpoint("initialized") if benchmark?
158
441
  task = src_vm.InstantClone_Task(spec: clone_spec)
159
442
  else
160
443
  clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(location: relocate_spec,
161
444
  powerOn: options[:poweron] && options[:customize].nil?,
162
445
  template: false)
163
446
 
447
+ benchmark_checkpoint("initialized") if benchmark?
164
448
  task = src_vm.CloneVM_Task(spec: clone_spec, folder: dest_folder, name: name)
165
449
  end
166
450
  task.wait_for_completion
167
451
 
452
+ benchmark_checkpoint("cloned") if benchmark?
453
+
168
454
  # get the IP address of the machine for bootstrapping
169
455
  # machine name is based on the path, e.g. that includes the folder
170
- @path = options[:folder].nil? ? name : format("%s/%s", options[:folder][:name], name)
456
+ path = options[:folder].nil? ? name : format("%s/%s", options[:folder][:name], name)
171
457
  @vm = dc.find_vm(path)
458
+ raise Support::CloneError.new(format("Unable to find machine: %s", path)) if vm.nil?
172
459
 
173
- raise format("Unable to find machine: %s", path) if vm.nil?
174
-
175
- # Pass contents of the customization option/Hash through to allow full customization
176
- # https://pubs.vmware.com/vsphere-6-5/index.jsp?topic=%2Fcom.vmware.wssdk.smssdk.doc%2Fvim.vm.ConfigSpec.html
177
- unless options[:customize].nil?
178
- Kitchen.logger.info "Waiting for reconfiguration to finish"
460
+ if options[:vm_os].nil?
461
+ os = detect_os
462
+ Kitchen.logger.debug format('OS for VM not configured, got "%s" from VMware', os.to_s.capitalize)
463
+ options[:vm_os] = os
464
+ end
179
465
 
180
- config_spec = RbVmomi::VIM.VirtualMachineConfigSpec(options[:customize])
181
- task = vm.ReconfigVM_Task(spec: config_spec)
182
- task.wait_for_completion
466
+ # Reconnect network device after Instant Clone is ready
467
+ if instant_clone?
468
+ Kitchen.logger.info "Reconnecting network adapter"
469
+ reconnect_network_device(vm)
183
470
  end
184
471
 
185
- if options[:poweron] && !options[:customize].nil? && options[:clone_type] != :instant
472
+ reconfigure_guest unless options[:customize].nil?
473
+
474
+ # Start only if specified or customizations wanted; no need for instant clones as they start in running state
475
+ if options[:poweron] && !options[:customize].nil? && !instant_clone?
186
476
  task = vm.PowerOnVM_Task
187
477
  task.wait_for_completion
188
478
  end
479
+ benchmark_checkpoint("powered_on") if benchmark?
480
+
481
+ Kitchen.logger.info format("Waiting for VMware tools to become available (timeout: %d seconds)...", options[:wait_timeout])
482
+ wait_for_tools(options[:wait_timeout], options[:wait_interval])
189
483
 
190
- Kitchen.logger.info format("Waiting for VMware tools/network interfaces to become available (timeout: %d seconds)...", options[:wait_timeout])
484
+ active_ip_discovery || standard_ip_discovery
485
+ benchmark_checkpoint("ip_detected") if benchmark?
191
486
 
192
- wait_for_ip(vm, options[:wait_timeout], options[:wait_interval])
487
+ benchmark_persist if benchmark?
193
488
  Kitchen.logger.info format("Created machine %s with IP %s", name, ip)
194
489
  end
195
490
  end
@@ -0,0 +1,151 @@
1
+ require "rbvmomi"
2
+ require "net/http"
3
+
4
+ class Support
5
+ # Encapsulate VMware Tools GOM interaction, inspired by github:dnuffer/raidopt
6
+ class GuestOperations
7
+ attr_reader :gom, :vm, :guest_auth, :ssl_verify
8
+
9
+ def initialize(vim, vm, guest_auth, ssl_verify = true)
10
+ @gom = vim.serviceContent.guestOperationsManager
11
+ @vm = vm
12
+ @guest_auth = guest_auth
13
+ @ssl_verify = ssl_verify
14
+ end
15
+
16
+ def os_family
17
+ return vm.guest.guestFamily == "windowsGuest" ? :windows : :linux if vm.guest.guestFamily
18
+
19
+ # VMware tools are not initialized or missing, infer from Guest Id
20
+ vm.config&.guestId&.match(/^win/) ? :windows : :linux
21
+ end
22
+
23
+ def linux?
24
+ os_family == :linux
25
+ end
26
+
27
+ def windows?
28
+ os_family == :windows
29
+ end
30
+
31
+ def delete_dir(dir)
32
+ gom.fileManager.DeleteDirectoryInGuest(vm: vm, auth: guest_auth, directoryPath: dir, recursive: true)
33
+ end
34
+
35
+ def process_is_running(pid)
36
+ procs = gom.processManager.ListProcessesInGuest(vm: vm, auth: guest_auth, pids: [pid])
37
+ procs.empty? || procs.any? { |gpi| gpi.exitCode.nil? }
38
+ end
39
+
40
+ def process_exit_code(pid)
41
+ gom.processManager.ListProcessesInGuest(vm: vm, auth: guest_auth, pids: [pid])&.first&.exitCode
42
+ end
43
+
44
+ def wait_for_process_exit(pid, timeout = 60.0, interval = 1.0)
45
+ start = Time.new
46
+
47
+ loop do
48
+ return unless process_is_running(pid)
49
+ break if (Time.new - start) >= timeout
50
+ sleep interval
51
+ end
52
+
53
+ raise format("Timeout waiting for process %d to exit after %d seconds", pid, timeout) if (Time.new - start) >= timeout
54
+ end
55
+
56
+ def run_program(path, args = "", timeout = 60.0)
57
+ Kitchen.logger.debug format("Running %s %s", path, args)
58
+
59
+ pid = gom.processManager.StartProgramInGuest(vm: vm, auth: guest_auth, spec: RbVmomi::VIM::GuestProgramSpec.new(programPath: path, arguments: args))
60
+ wait_for_process_exit(pid, timeout)
61
+
62
+ exit_code = process_exit_code(pid)
63
+ raise format("Failed to run '%s %s'. Exit code: %d", path, args, exit_code) if exit_code != 0
64
+
65
+ exit_code
66
+ end
67
+
68
+ def run_shell_capture_output(command, shell = :auto, timeout = 60.0)
69
+ if shell == :auto
70
+ shell = :linux if linux?
71
+ shell = :cmd if windows?
72
+ end
73
+
74
+ if shell == :linux
75
+ tmp_out_fname = format("/tmp/vm_utils_run_out_%s", Random.rand)
76
+ tmp_err_fname = format("/tmp/vm_utils_run_err_%s", Random.rand)
77
+ shell = "/bin/sh"
78
+ args = format("-c '(%s) > %s 2> %s'", command.gsub("'", %q{\\\'}), tmp_out_fname, tmp_err_fname)
79
+ elsif shell == :cmd
80
+ tmp_out_fname = format('C:\Windows\TEMP\vm_utils_run_out_%s', Random.rand)
81
+ tmp_err_fname = format('C:\Windows\TEMP\vm_utils_run_err_%s', Random.rand)
82
+ shell = "cmd.exe"
83
+ args = format('/c "%s > %s 2> %s"', command.gsub("\"", %q{\\\"}), tmp_out_fname, tmp_err_fname)
84
+ elsif shell == :powershell
85
+ tmp_out_fname = format('C:\Windows\TEMP\vm_utils_run_out_%s', Random.rand)
86
+ tmp_err_fname = format('C:\Windows\TEMP\vm_utils_run_err_%s', Random.rand)
87
+ shell = 'C:\Windows\System32\WindowsPowershell\v1.0\powershell.exe'
88
+ args = format('-Command "%s > %s 2> %s"', command.gsub("\"", %q{\\\"}), tmp_out_fname, tmp_err_fname)
89
+ end
90
+
91
+ begin
92
+ exit_code = run_program(shell, args, timeout)
93
+ rescue StandardError
94
+ proc_err = "" # read_file(tmp_err_fname)
95
+ raise format("Error executing command %s. Exit code: %d. StdErr %s", command, exit_code, proc_err)
96
+ end
97
+
98
+ read_file(tmp_out_fname)
99
+ end
100
+
101
+ def write_file(remote_file, contents)
102
+ # Required privilege: VirtualMachine.GuestOperations.Modify
103
+ put_url = gom.fileManager.InitiateFileTransferToGuest(
104
+ vm: vm,
105
+ auth: guest_auth,
106
+ guestFilePath: remote_file,
107
+ fileAttributes: RbVmomi::VIM::GuestFileAttributes(),
108
+ fileSize: contents.size,
109
+ overwrite: true
110
+ )
111
+ put_url = put_url.gsub(%r{^https://\*:}, format("https://%s:%s", vm._connection.host, put_url))
112
+ uri = URI.parse(put_url)
113
+
114
+ request = Net::HTTP::Put.new(uri.request_uri)
115
+ request["Transfer-Encoding"] = "chunked"
116
+ request["Content-Length"] = contents.size
117
+ request.body = contents
118
+
119
+ http = Net::HTTP.new(uri.host, uri.port)
120
+ http.use_ssl = (uri.scheme == "https")
121
+ http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
122
+ http.request(request)
123
+ end
124
+
125
+ def read_file(remote_file)
126
+ download_file(remote_file, nil)
127
+ end
128
+
129
+ def upload_file(local_file, remote_file)
130
+ Kitchen.logger.debug format("Copy %s to %s", local_file, remote_file)
131
+ write_file(remote_file, File.open(local_file, "rb").read)
132
+ end
133
+
134
+ def download_file(remote_file, local_file)
135
+ info = gom.fileManager.InitiateFileTransferFromGuest(vm: vm, auth: guest_auth, guestFilePath: remote_file)
136
+ uri = URI.parse(info.url)
137
+
138
+ request = Net::HTTP::Get.new(uri.request_uri)
139
+ http = Net::HTTP.new(uri.host, uri.port)
140
+ http.use_ssl = (uri.scheme == "https")
141
+ http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
142
+ response = http.request(request)
143
+
144
+ if response.body.size != info.size
145
+ raise format("Downloaded file has different size than reported: %s (%d bytes instead of %d bytes)", remote_file, response.body.size, info.size)
146
+ end
147
+
148
+ local_file.nil? ? response.body : File.open(local_file, "w") { |file| file.write(response.body) }
149
+ end
150
+ end
151
+ end
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.2.2
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-20 00:00:00.000000000 Z
11
+ date: 2019-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbvmomi
@@ -111,6 +111,7 @@ files:
111
111
  - lib/kitchen-vcenter/version.rb
112
112
  - lib/kitchen/driver/vcenter.rb
113
113
  - lib/support/clone_vm.rb
114
+ - lib/support/guest_operations.rb
114
115
  homepage: https://github.com/chef/kitchen-vcenter
115
116
  licenses:
116
117
  - Apache-2.0
@@ -130,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
131
  - !ruby/object:Gem::Version
131
132
  version: '0'
132
133
  requirements: []
133
- rubygems_version: 3.0.1
134
+ rubygems_version: 3.0.3
134
135
  signing_key:
135
136
  specification_version: 4
136
137
  summary: Test Kitchen driver for VMare vCenter