kitchen-vcenter 2.2.2 → 2.4.0

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: 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