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 +4 -4
- data/lib/kitchen-vcenter/version.rb +1 -1
- data/lib/kitchen/driver/vcenter.rb +41 -3
- data/lib/support/clone_vm.rb +352 -57
- data/lib/support/guest_operations.rb +151 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45583a32d2c2ccf32d21491735bbe862c1cc85990ea89bc773a06086a117fc17
|
4
|
+
data.tar.gz: 872abb17f6fd2caf8bbe14f05c31fbe9c402bb065a3c6b010bc8136a944dc525
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d523aa9288f11454ef5bffd91be68132e3ed34028566af46ddbc85daeb707e05245604477234077a836fbce5e108a1f528b89f65d01653ba62e43ddd94b18747
|
7
|
+
data.tar.gz: c5ee31885c9a12eb0cd9d7e28d2dbb99e0257a897912054ca566aad2cef05f52301ebc65a1e61b588fbbcdf0587982aadfc29e1e72addfa29f2ae9c51a589b8d
|
@@ -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
|
data/lib/support/clone_vm.rb
CHANGED
@@ -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, :
|
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
|
17
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
53
|
+
loop do
|
54
|
+
if vm.guest.toolsRunningStatus == "guestToolsRunning"
|
55
|
+
benchmark_checkpoint("tools_detected") if benchmark?
|
36
56
|
|
37
|
-
|
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
|
-
|
64
|
+
raise Support::CloneError.new("Timeout waiting for VMware Tools")
|
41
65
|
end
|
42
66
|
|
43
|
-
def wait_for_ip(
|
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 =
|
49
|
-
|
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
|
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 =
|
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
|
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
|
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
|
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
|
-
#
|
154
|
-
|
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
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
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
|
-
|
484
|
+
active_ip_discovery || standard_ip_discovery
|
485
|
+
benchmark_checkpoint("ip_detected") if benchmark?
|
191
486
|
|
192
|
-
|
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.
|
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-
|
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.
|
134
|
+
rubygems_version: 3.0.3
|
134
135
|
signing_key:
|
135
136
|
specification_version: 4
|
136
137
|
summary: Test Kitchen driver for VMare vCenter
|