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