chef-provisioning-vsphere 0.8.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +272 -269
- data/chef-provisioning-vsphere.gemspec +29 -28
- data/lib/chef/provisioning/vsphere_driver/clone_spec_builder.rb +201 -205
- data/lib/chef/provisioning/vsphere_driver/driver.rb +688 -679
- data/lib/chef/provisioning/vsphere_driver/version.rb +3 -3
- data/lib/chef/provisioning/vsphere_driver/vsphere_helpers.rb +343 -341
- data/spec/integration_tests/vsphere_driver_spec.rb +158 -158
- metadata +17 -3
@@ -1,679 +1,688 @@
|
|
1
|
-
require 'chef'
|
2
|
-
require 'cheffish/merged_config'
|
3
|
-
require 'chef/provisioning/driver'
|
4
|
-
require 'chef/provisioning/machine/windows_machine'
|
5
|
-
require 'chef/provisioning/machine/unix_machine'
|
6
|
-
require 'chef/provisioning/vsphere_driver/clone_spec_builder'
|
7
|
-
require 'chef/provisioning/vsphere_driver/version'
|
8
|
-
require 'chef/provisioning/vsphere_driver/vsphere_helpers'
|
9
|
-
require 'chef/provisioning/vsphere_driver/vsphere_url'
|
10
|
-
|
11
|
-
module ChefProvisioningVsphere
|
12
|
-
# Provisions machines in vSphere.
|
13
|
-
class VsphereDriver < Chef::Provisioning::Driver
|
14
|
-
include Chef::Mixin::ShellOut
|
15
|
-
|
16
|
-
def self.from_url(driver_url, config)
|
17
|
-
VsphereDriver.new(driver_url, config)
|
18
|
-
end
|
19
|
-
|
20
|
-
# Create a new Vsphere provisioner.
|
21
|
-
#
|
22
|
-
# ## Parameters
|
23
|
-
# connect_options - hash of options to be passed to RbVmomi::VIM.connect
|
24
|
-
# :host - required - hostname of the vSphere API server
|
25
|
-
# :port - optional - port on the vSphere API server (default: 443)
|
26
|
-
# :path - optional - path on the vSphere API server (default: /sdk)
|
27
|
-
# :use_ssl - optional - true to use ssl in connection to vSphere API server (default: true)
|
28
|
-
# :insecure - optional - true to ignore ssl certificate validation errors in connection to vSphere API server (default: false)
|
29
|
-
# :user - required - user name to use in connection to vSphere API server
|
30
|
-
# :password - required - password to use in connection to vSphere API server
|
31
|
-
def self.canonicalize_url(driver_url, config)
|
32
|
-
config = symbolize_keys(config)
|
33
|
-
[ driver_url || URI::VsphereUrl.from_config(config).to_s, config ]
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.symbolize_keys(h)
|
37
|
-
Hash === h ?
|
38
|
-
Hash[
|
39
|
-
h.map do |k, v|
|
40
|
-
[k.respond_to?(:to_sym) ? k.to_sym : k, symbolize_keys(v)]
|
41
|
-
end
|
42
|
-
] : h
|
43
|
-
end
|
44
|
-
|
45
|
-
def initialize(driver_url, config)
|
46
|
-
super(driver_url, config)
|
47
|
-
|
48
|
-
uri = URI(driver_url)
|
49
|
-
@connect_options = {
|
50
|
-
provider: 'vsphere',
|
51
|
-
host: uri.host,
|
52
|
-
port: uri.port,
|
53
|
-
use_ssl: uri.use_ssl,
|
54
|
-
insecure: uri.insecure,
|
55
|
-
path: uri.path
|
56
|
-
}
|
57
|
-
|
58
|
-
if driver_options
|
59
|
-
@connect_options[:user] = driver_options[:user]
|
60
|
-
@connect_options[:password] = driver_options[:password]
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
attr_reader :connect_options
|
65
|
-
|
66
|
-
# Acquire a machine, generally by provisioning it. Returns a Machine
|
67
|
-
# object pointing at the machine, allowing useful actions like setup,
|
68
|
-
# converge, execute, file and directory. The Machine object will have a
|
69
|
-
# "node" property which must be saved to the server (if it is any
|
70
|
-
# different from the original node object).
|
71
|
-
#
|
72
|
-
# ## Parameters
|
73
|
-
# action_handler - the action_handler object that is calling this method; this
|
74
|
-
# is generally a action_handler, but could be anything that can support the
|
75
|
-
# ChefMetal::ActionHandler interface (i.e., in the case of the test
|
76
|
-
# kitchen metal driver for acquiring and destroying VMs; see the base
|
77
|
-
# class for what needs providing).
|
78
|
-
# node - node object (deserialized json) representing this machine. If
|
79
|
-
# the node has a provisioner_options hash in it, these will be used
|
80
|
-
# instead of options provided by the provisioner. TODO compare and
|
81
|
-
# fail if different?
|
82
|
-
# node will have node['normal']['provisioner_options'] in it with any options.
|
83
|
-
# It is a hash with this format:
|
84
|
-
#
|
85
|
-
# -- provisioner_url: vsphere://host:port?ssl=[true|false]&insecure=[true|false]
|
86
|
-
# -- bootstrap_options: hash of options to pass to RbVmomi::VIM::VirtualMachine::CloneTask()
|
87
|
-
# :datacenter
|
88
|
-
# :resource_pool
|
89
|
-
# :cluster
|
90
|
-
# :datastore
|
91
|
-
# :template_name
|
92
|
-
# :template_folder
|
93
|
-
# :vm_folder
|
94
|
-
# :winrm {...} (not yet implemented)
|
95
|
-
# :ssh {...}
|
96
|
-
#
|
97
|
-
# Example bootstrap_options for vSphere:
|
98
|
-
# TODO: add other CloneTask params, e.g.: datastore, annotation, resource_pool, ...
|
99
|
-
# 'bootstrap_options' => {
|
100
|
-
# 'template_name' =>'centos6.small',
|
101
|
-
# 'template_folder' =>'Templates',
|
102
|
-
# 'vm_folder' => 'MyApp'
|
103
|
-
# }
|
104
|
-
#
|
105
|
-
# node['normal']['provisioner_output'] will be populated with information
|
106
|
-
# about the created machine. For vSphere, it is a hash with this
|
107
|
-
# format:
|
108
|
-
#
|
109
|
-
# -- provisioner_url: vsphere:host:port?ssl=[true|false]&insecure=[true|false]
|
110
|
-
# -- vm_folder: name of the vSphere folder containing the VM
|
111
|
-
#
|
112
|
-
def allocate_machine(action_handler, machine_spec, machine_options)
|
113
|
-
merge_options! machine_options
|
114
|
-
|
115
|
-
if machine_spec.location
|
116
|
-
Chef::Log.warn(
|
117
|
-
"Checking to see if #{machine_spec.location} has been created...")
|
118
|
-
vm = vm_for(machine_spec)
|
119
|
-
if vm
|
120
|
-
Chef::Log.warn 'returning existing machine'
|
121
|
-
return vm
|
122
|
-
else
|
123
|
-
Chef::Log.warn machine_msg(
|
124
|
-
machine_spec.name,
|
125
|
-
machine_spec.location['server_id'],
|
126
|
-
'no longer exists. Recreating ...'
|
127
|
-
)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
bootstrap_options = machine_options[:bootstrap_options]
|
131
|
-
|
132
|
-
action_handler.report_progress full_description(
|
133
|
-
machine_spec, bootstrap_options)
|
134
|
-
|
135
|
-
vm = find_or_create_vm(bootstrap_options, machine_spec, action_handler)
|
136
|
-
|
137
|
-
add_machine_spec_location(vm, machine_spec)
|
138
|
-
|
139
|
-
action_handler.performed_action(machine_msg(
|
140
|
-
machine_spec.name,
|
141
|
-
vm.config.instanceUuid,
|
142
|
-
'created'
|
143
|
-
))
|
144
|
-
vm
|
145
|
-
end
|
146
|
-
|
147
|
-
def merge_options!(machine_options)
|
148
|
-
@config = Cheffish::MergedConfig.new(
|
149
|
-
{ machine_options: machine_options },
|
150
|
-
@config
|
151
|
-
)
|
152
|
-
end
|
153
|
-
|
154
|
-
def add_machine_spec_location(vm, machine_spec)
|
155
|
-
machine_spec.location = {
|
156
|
-
'driver_url' => driver_url,
|
157
|
-
'driver_version' => VERSION,
|
158
|
-
'server_id' => vm.config.instanceUuid,
|
159
|
-
'is_windows' => is_windows?(vm),
|
160
|
-
'allocated_at' => Time.now.utc.to_s,
|
161
|
-
'ipaddress' => vm.guest.ipAddress
|
162
|
-
}
|
163
|
-
end
|
164
|
-
|
165
|
-
def find_or_create_vm(bootstrap_options, machine_spec, action_handler)
|
166
|
-
vm = vsphere_helper.find_vm(
|
167
|
-
bootstrap_options[:vm_folder],
|
168
|
-
machine_spec.name
|
169
|
-
)
|
170
|
-
if vm
|
171
|
-
Chef::Log.info machine_msg(
|
172
|
-
machine_spec.name,
|
173
|
-
vm.config.instanceUuid,
|
174
|
-
'already created'
|
175
|
-
)
|
176
|
-
else
|
177
|
-
vm = clone_vm(
|
178
|
-
action_handler,
|
179
|
-
bootstrap_options,
|
180
|
-
machine_spec.name
|
181
|
-
)
|
182
|
-
end
|
183
|
-
vm
|
184
|
-
end
|
185
|
-
|
186
|
-
def full_description(machine_spec, bootstrap_options)
|
187
|
-
description = [ "creating machine #{machine_spec.name} on #{driver_url}" ]
|
188
|
-
bootstrap_options.to_hash.each_pair do |key,value|
|
189
|
-
description << " #{key}: #{value.inspect}"
|
190
|
-
end
|
191
|
-
description
|
192
|
-
end
|
193
|
-
|
194
|
-
def machine_msg(name, id, action)
|
195
|
-
"Machine - #{action} - #{name} (#{id} on #{driver_url})"
|
196
|
-
end
|
197
|
-
|
198
|
-
def ready_machine(action_handler, machine_spec, machine_options)
|
199
|
-
merge_options! machine_options
|
200
|
-
|
201
|
-
vm = start_machine(action_handler, machine_spec, machine_options)
|
202
|
-
if vm.nil?
|
203
|
-
raise "Machine #{machine_spec.name} does not have a server "\
|
204
|
-
'associated with it, or server does not exist.'
|
205
|
-
end
|
206
|
-
|
207
|
-
bootstrap_options = machine_options[:bootstrap_options]
|
208
|
-
|
209
|
-
transport_respond?(
|
210
|
-
machine_options,
|
211
|
-
vm,
|
212
|
-
action_handler,
|
213
|
-
machine_spec
|
214
|
-
)
|
215
|
-
|
216
|
-
machine = machine_for(machine_spec,machine_options)
|
217
|
-
|
218
|
-
setup_extra_nics(action_handler, bootstrap_options, vm, machine)
|
219
|
-
|
220
|
-
if has_static_ip(bootstrap_options) && !is_windows?(vm)
|
221
|
-
setup_ubuntu_dns(machine, bootstrap_options, machine_spec)
|
222
|
-
end
|
223
|
-
|
224
|
-
machine
|
225
|
-
end
|
226
|
-
|
227
|
-
def setup_extra_nics(action_handler, bootstrap_options, vm, machine)
|
228
|
-
networks=bootstrap_options[:network_name]
|
229
|
-
if networks.kind_of?(String)
|
230
|
-
networks=[networks]
|
231
|
-
end
|
232
|
-
return if networks.nil? || networks.count < 2
|
233
|
-
|
234
|
-
new_nics = vsphere_helper.add_extra_nic(
|
235
|
-
action_handler,
|
236
|
-
vm_template_for(bootstrap_options),
|
237
|
-
bootstrap_options,
|
238
|
-
vm
|
239
|
-
)
|
240
|
-
if is_windows?(vm) && !new_nics.nil?
|
241
|
-
new_nics.each do |nic|
|
242
|
-
nic_label = nic.device.deviceInfo.label
|
243
|
-
machine.execute_always(
|
244
|
-
"Disable-Netadapter -Name '#{nic_label}' -Confirm:$false")
|
245
|
-
end
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
def transport_respond?(
|
250
|
-
machine_options,
|
251
|
-
vm,
|
252
|
-
action_handler,
|
253
|
-
machine_spec
|
254
|
-
)
|
255
|
-
bootstrap_options = machine_options[:bootstrap_options]
|
256
|
-
|
257
|
-
# this waits for vmware tools to start and the vm to presebnt an ip
|
258
|
-
# This may just be the ip of a newly cloned machine
|
259
|
-
# Customization below may change this to a valid ip
|
260
|
-
wait_until_ready(action_handler, machine_spec, machine_options, vm)
|
261
|
-
|
262
|
-
if !machine_spec.location['ipaddress'] || !has_ip?(machine_spec.location['ipaddress'], vm)
|
263
|
-
# find the ip we actually want
|
264
|
-
# this will be the static ip to assign
|
265
|
-
# or the ip reported back by the vm if using dhcp
|
266
|
-
# it *may* be nil if just cloned
|
267
|
-
vm_ip = ip_to_bootstrap(bootstrap_options, vm)
|
268
|
-
transport = nil
|
269
|
-
unless vm_ip.nil?
|
270
|
-
transport = transport_for(machine_spec, bootstrap_options[:ssh], vm_ip)
|
271
|
-
end
|
272
|
-
|
273
|
-
unless !transport.nil? && transport.available? && has_ip?(vm_ip, vm)
|
274
|
-
attempt_ip(machine_options, action_handler, vm, machine_spec)
|
275
|
-
end
|
276
|
-
machine_spec.location['ipaddress'] = vm.guest.ipAddress
|
277
|
-
action_handler.report_progress(
|
278
|
-
"IP address obtained: #{machine_spec.location['ipaddress']}")
|
279
|
-
end
|
280
|
-
|
281
|
-
wait_for_domain(bootstrap_options, vm, machine_spec, action_handler)
|
282
|
-
|
283
|
-
begin
|
284
|
-
wait_for_transport(action_handler, machine_spec, machine_options, vm)
|
285
|
-
rescue Timeout::Error
|
286
|
-
# Only ever reboot once, and only if it's been less than 10 minutes
|
287
|
-
# since we stopped waiting
|
288
|
-
if machine_spec.location['started_at'] ||
|
289
|
-
remaining_wait_time(machine_spec, machine_options) < -(10*60)
|
290
|
-
raise
|
291
|
-
else
|
292
|
-
Chef::Log.warn(machine_msg(
|
293
|
-
machine_spec.name,
|
294
|
-
vm.config.instanceUuid,
|
295
|
-
'started but SSH did not come up. Rebooting...'
|
296
|
-
))
|
297
|
-
restart_server(action_handler, machine_spec, machine_options)
|
298
|
-
wait_until_ready(action_handler, machine_spec, machine_options, vm)
|
299
|
-
wait_for_transport(action_handler, machine_spec, machine_options, vm)
|
300
|
-
end
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
def attempt_ip(machine_options, action_handler, vm, machine_spec)
|
305
|
-
vm_ip = ip_to_bootstrap(machine_options[:bootstrap_options], vm)
|
306
|
-
|
307
|
-
wait_for_ip(vm, machine_options, machine_spec, action_handler)
|
308
|
-
|
309
|
-
unless has_ip?(vm_ip, vm)
|
310
|
-
action_handler.report_progress "rebooting..."
|
311
|
-
if vm.guest.toolsRunningStatus != "guestToolsRunning"
|
312
|
-
msg = 'tools have stopped. current power state is '
|
313
|
-
msg << vm.runtime.powerState
|
314
|
-
msg << ' and tools state is '
|
315
|
-
msg << vm.guest.toolsRunningStatus
|
316
|
-
msg << '. powering up server...'
|
317
|
-
action_handler.report_progress(msg.join)
|
318
|
-
vsphere_helper.start_vm(vm)
|
319
|
-
else
|
320
|
-
restart_server(action_handler, machine_spec, machine_options)
|
321
|
-
end
|
322
|
-
wait_for_ip(vm, machine_options, machine_spec, action_handler)
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
def wait_for_domain(bootstrap_options, vm, machine_spec, action_handler)
|
327
|
-
return unless bootstrap_options[:customization_spec]
|
328
|
-
return unless bootstrap_options[:customization_spec][:domain]
|
329
|
-
|
330
|
-
domain = bootstrap_options[:customization_spec][:domain]
|
331
|
-
if is_windows?(vm) && domain != 'local'
|
332
|
-
start = Time.now.utc
|
333
|
-
trimmed_name = machine_spec.name.byteslice(0,15)
|
334
|
-
expected_name="#{trimmed_name}.#{domain}"
|
335
|
-
action_handler.report_progress(
|
336
|
-
"waiting to domain join and be named #{expected_name}")
|
337
|
-
until (Time.now.utc - start) > 30 ||
|
338
|
-
(vm.guest.hostName == expected_name) do
|
339
|
-
print '.'
|
340
|
-
sleep 5
|
341
|
-
end
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
def wait_for_ip(vm, machine_options, machine_spec, action_handler)
|
346
|
-
bootstrap_options = machine_options[:bootstrap_options]
|
347
|
-
vm_ip = ip_to_bootstrap(bootstrap_options, vm)
|
348
|
-
ready_timeout = machine_options[:ready_timeout] || 300
|
349
|
-
msg = "waiting up to #{ready_timeout} seconds for customization"
|
350
|
-
msg << " and find #{vm_ip}" unless vm_ip == vm.guest.ipAddress
|
351
|
-
action_handler.report_progress msg
|
352
|
-
|
353
|
-
start = Time.now.utc
|
354
|
-
connectable = false
|
355
|
-
until (Time.now.utc - start) > ready_timeout || connectable do
|
356
|
-
action_handler.report_progress(
|
357
|
-
"IP addresses found: #{all_ips_for(vm)}")
|
358
|
-
vm_ip ||= ip_to_bootstrap(bootstrap_options, vm)
|
359
|
-
if has_ip?(vm_ip, vm)
|
360
|
-
connectable = transport_for(
|
361
|
-
machine_spec,
|
362
|
-
machine_options[:bootstrap_options][:ssh],
|
363
|
-
vm_ip
|
364
|
-
).available?
|
365
|
-
end
|
366
|
-
sleep 5
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
def all_ips_for(vm)
|
371
|
-
vm.guest.net.map { |net| net.ipAddress}.flatten
|
372
|
-
end
|
373
|
-
|
374
|
-
def has_ip?(ip, vm)
|
375
|
-
all_ips_for(vm).include?(ip)
|
376
|
-
end
|
377
|
-
|
378
|
-
# Connect to machine without acquiring it
|
379
|
-
def connect_to_machine(machine_spec, machine_options)
|
380
|
-
merge_options! machine_options
|
381
|
-
machine_for(machine_spec, machine_options)
|
382
|
-
end
|
383
|
-
|
384
|
-
def destroy_machine(action_handler, machine_spec, machine_options)
|
385
|
-
merge_options! machine_options
|
386
|
-
vm = vm_for(machine_spec)
|
387
|
-
if vm
|
388
|
-
action_handler.perform_action "Delete VM [#{vm.parent.name}/#{vm.name}]" do
|
389
|
-
begin
|
390
|
-
vsphere_helper.stop_vm(vm, machine_options[:stop_timeout])
|
391
|
-
vm.Destroy_Task.wait_for_completion
|
392
|
-
rescue RbVmomi::Fault => fault
|
393
|
-
raise fault unless fault.fault.class.wsdl_name == "ManagedObjectNotFound"
|
394
|
-
ensure
|
395
|
-
machine_spec.location = nil
|
396
|
-
end
|
397
|
-
end
|
398
|
-
end
|
399
|
-
strategy = convergence_strategy_for(machine_spec, machine_options)
|
400
|
-
strategy.cleanup_convergence(action_handler, machine_spec)
|
401
|
-
end
|
402
|
-
|
403
|
-
def stop_machine(action_handler, machine_spec, machine_options)
|
404
|
-
merge_options! machine_options
|
405
|
-
vm = vm_for(machine_spec)
|
406
|
-
if vm
|
407
|
-
action_handler.perform_action "Shutdown guest OS and power off VM [#{vm.parent.name}/#{vm.name}]" do
|
408
|
-
vsphere_helper.stop_vm(vm, machine_options[:stop_timeout])
|
409
|
-
end
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
def start_machine(action_handler, machine_spec, machine_options)
|
414
|
-
merge_options! machine_options
|
415
|
-
vm = vm_for(machine_spec)
|
416
|
-
if vm
|
417
|
-
action_handler.perform_action "Power on VM [#{vm.parent.name}/#{vm.name}]" do
|
418
|
-
vsphere_helper.start_vm(vm, machine_options[:bootstrap_options][:ssh][:port])
|
419
|
-
end
|
420
|
-
end
|
421
|
-
vm
|
422
|
-
end
|
423
|
-
|
424
|
-
def restart_server(action_handler, machine_spec, machine_options)
|
425
|
-
action_handler.perform_action "restart machine #{machine_spec.name} (#{driver_url})" do
|
426
|
-
stop_machine(action_handler, machine_spec, machine_options)
|
427
|
-
start_machine(action_handler, machine_spec, machine_options)
|
428
|
-
machine_spec.location['started_at'] = Time.now.utc.to_s
|
429
|
-
end
|
430
|
-
end
|
431
|
-
|
432
|
-
protected
|
433
|
-
|
434
|
-
def setup_ubuntu_dns(machine, bootstrap_options, machine_spec)
|
435
|
-
host_lookup = machine.execute_always('host google.com')
|
436
|
-
if host_lookup.exitstatus != 0
|
437
|
-
if host_lookup.stdout.include?("setlocale: LC_ALL")
|
438
|
-
machine.execute_always('locale-gen en_US && update-locale LANG=en_US')
|
439
|
-
end
|
440
|
-
distro = machine.execute_always("lsb_release -i | sed -e 's/Distributor ID://g'").stdout.strip
|
441
|
-
Chef::Log.info "Found distro:#{distro}"
|
442
|
-
if distro == 'Ubuntu'
|
443
|
-
distro_version = (machine.execute_always("lsb_release -r | sed -e s/[^0-9.]//g")).stdout.strip.to_f
|
444
|
-
Chef::Log.info "Found distro version:#{distro_version}"
|
445
|
-
if distro_version>= 12.04
|
446
|
-
Chef::Log.info "Ubuntu version 12.04 or greater. Need to patch DNS."
|
447
|
-
interfaces_file = "/etc/network/interfaces"
|
448
|
-
nameservers = bootstrap_options[:customization_spec][:ipsettings][:dnsServerList].join(' ')
|
449
|
-
machine.execute_always("if ! cat #{interfaces_file} | grep -q dns-search ; then echo 'dns-search #{bootstrap_options[:customization_spec][:domain]}' >> #{interfaces_file} ; fi")
|
450
|
-
machine.execute_always("if ! cat #{interfaces_file} | grep -q dns-nameservers ; then echo 'dns-nameservers #{nameservers}' >> #{interfaces_file} ; fi")
|
451
|
-
machine.execute_always('/etc/init.d/networking restart')
|
452
|
-
machine.execute_always('apt-get -qq update')
|
453
|
-
end
|
454
|
-
end
|
455
|
-
end
|
456
|
-
end
|
457
|
-
|
458
|
-
def has_static_ip(bootstrap_options)
|
459
|
-
if bootstrap_options.has_key?(:customization_spec)
|
460
|
-
bootstrap_options = bootstrap_options[:customization_spec]
|
461
|
-
if bootstrap_options.has_key?(:ipsettings)
|
462
|
-
bootstrap_options = bootstrap_options[:ipsettings]
|
463
|
-
if bootstrap_options.has_key?(:ip)
|
464
|
-
return true
|
465
|
-
end
|
466
|
-
end
|
467
|
-
end
|
468
|
-
false
|
469
|
-
end
|
470
|
-
|
471
|
-
def remaining_wait_time(machine_spec, machine_options)
|
472
|
-
if machine_spec.location['started_at']
|
473
|
-
(machine_options[:start_timeout] || 600) -
|
474
|
-
(Time.now.utc - Time.parse(machine_spec.location['started_at']))
|
475
|
-
else
|
476
|
-
(machine_options[:create_timeout] || 600) -
|
477
|
-
(Time.now.utc - Time.parse(machine_spec.location['allocated_at']))
|
478
|
-
end
|
479
|
-
end
|
480
|
-
|
481
|
-
def wait_until_ready(action_handler, machine_spec, machine_options, vm)
|
482
|
-
if vm.guest.toolsRunningStatus != "guestToolsRunning"
|
483
|
-
if action_handler.should_perform_actions
|
484
|
-
action_handler.report_progress "waiting for #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url}) to be ready ..."
|
485
|
-
until remaining_wait_time(machine_spec, machine_options) < 0 ||
|
486
|
-
(vm.guest.toolsRunningStatus == "guestToolsRunning" && !vm.guest.ipAddress.nil? && vm.guest.ipAddress.length > 0) do
|
487
|
-
print "."
|
488
|
-
sleep 5
|
489
|
-
end
|
490
|
-
action_handler.report_progress "#{machine_spec.name} is now ready"
|
491
|
-
end
|
492
|
-
end
|
493
|
-
end
|
494
|
-
|
495
|
-
def vm_for(machine_spec)
|
496
|
-
if machine_spec.location
|
497
|
-
vsphere_helper.find_vm_by_id(machine_spec.location['server_id'])
|
498
|
-
else
|
499
|
-
nil
|
500
|
-
end
|
501
|
-
end
|
502
|
-
|
503
|
-
def clone_vm(action_handler, bootstrap_options, machine_name)
|
504
|
-
vm_template = vm_template_for(bootstrap_options)
|
505
|
-
|
506
|
-
spec_builder = CloneSpecBuilder.new(vsphere_helper, action_handler)
|
507
|
-
clone_spec = spec_builder.build(vm_template, machine_name, bootstrap_options)
|
508
|
-
Chef::Log.debug("Clone spec: #{clone_spec.pretty_inspect}")
|
509
|
-
|
510
|
-
vm_folder = vsphere_helper.find_folder(bootstrap_options[:vm_folder])
|
511
|
-
vm_template.CloneVM_Task(
|
512
|
-
name: machine_name,
|
513
|
-
folder: vm_folder,
|
514
|
-
spec: clone_spec
|
515
|
-
).wait_for_completion
|
516
|
-
|
517
|
-
vm = vsphere_helper.find_vm(vm_folder, machine_name)
|
518
|
-
|
519
|
-
additional_disk_size_gb = bootstrap_options[:additional_disk_size_gb]
|
520
|
-
if !additional_disk_size_gb.is_a?(Array)
|
521
|
-
additional_disk_size_gb = [additional_disk_size_gb]
|
522
|
-
end
|
523
|
-
|
524
|
-
additional_disk_size_gb.each do |size|
|
525
|
-
size = size.to_i
|
526
|
-
next if size == 0
|
527
|
-
if bootstrap_options[:datastore].to_s.empty?
|
528
|
-
raise ':datastore must be specified when adding a disk to a cloned vm'
|
529
|
-
end
|
530
|
-
task = vm.ReconfigVM_Task(
|
531
|
-
spec: RbVmomi::VIM.VirtualMachineConfigSpec(
|
532
|
-
deviceChange: [
|
533
|
-
vsphere_helper.virtual_disk_for(
|
534
|
-
vm,
|
535
|
-
bootstrap_options[:datastore],
|
536
|
-
size
|
537
|
-
)
|
538
|
-
]
|
539
|
-
)
|
540
|
-
)
|
541
|
-
task.wait_for_completion
|
542
|
-
end
|
543
|
-
|
544
|
-
vm
|
545
|
-
end
|
546
|
-
|
547
|
-
def vsphere_helper
|
548
|
-
@vsphere_helper ||= VsphereHelper.new(
|
549
|
-
connect_options,
|
550
|
-
config[:machine_options][:bootstrap_options][:datacenter]
|
551
|
-
)
|
552
|
-
end
|
553
|
-
|
554
|
-
def vm_template_for(bootstrap_options)
|
555
|
-
template_folder = bootstrap_options[:template_folder]
|
556
|
-
template_name = bootstrap_options[:template_name]
|
557
|
-
vsphere_helper.find_vm(template_folder, template_name) ||
|
558
|
-
raise("vSphere VM Template not found [#{template_folder}/#{template_name}]")
|
559
|
-
end
|
560
|
-
|
561
|
-
def machine_for(machine_spec, machine_options)
|
562
|
-
if machine_spec.location.nil?
|
563
|
-
raise "Server for node #{machine_spec.name} has not been created!"
|
564
|
-
end
|
565
|
-
|
566
|
-
transport = transport_for(
|
567
|
-
machine_spec,
|
568
|
-
machine_options[:bootstrap_options][:ssh]
|
569
|
-
)
|
570
|
-
strategy = convergence_strategy_for(machine_spec, machine_options)
|
571
|
-
|
572
|
-
if machine_spec.location['is_windows']
|
573
|
-
Chef::Provisioning::Machine::WindowsMachine.new(
|
574
|
-
machine_spec, transport, strategy)
|
575
|
-
else
|
576
|
-
Chef::Provisioning::Machine::UnixMachine.new(
|
577
|
-
machine_spec, transport, strategy)
|
578
|
-
end
|
579
|
-
end
|
580
|
-
|
581
|
-
def is_windows?(vm)
|
582
|
-
return false if vm.nil?
|
583
|
-
vm.config.guestId.start_with?('win')
|
584
|
-
end
|
585
|
-
|
586
|
-
def convergence_strategy_for(machine_spec, machine_options)
|
587
|
-
require 'chef/provisioning/convergence_strategy/install_msi'
|
588
|
-
require 'chef/provisioning/convergence_strategy/install_cached'
|
589
|
-
require 'chef/provisioning/convergence_strategy/no_converge'
|
590
|
-
|
591
|
-
mopts = machine_options[:convergence_options].to_hash.dup
|
592
|
-
if mopts[:chef_server]
|
593
|
-
mopts[:chef_server] = mopts[:chef_server].to_hash.dup
|
594
|
-
mopts[:chef_server][:options] = mopts[:chef_server][:options].to_hash.dup if mopts[:chef_server][:options]
|
595
|
-
end
|
596
|
-
|
597
|
-
if !machine_spec.location
|
598
|
-
return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(
|
599
|
-
mopts, config)
|
600
|
-
end
|
601
|
-
|
602
|
-
if machine_spec.location['is_windows']
|
603
|
-
Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(
|
604
|
-
mopts, config)
|
605
|
-
else
|
606
|
-
Chef::Provisioning::ConvergenceStrategy::InstallCached.new(
|
607
|
-
mopts, config)
|
608
|
-
end
|
609
|
-
end
|
610
|
-
|
611
|
-
def wait_for_transport(action_handler, machine_spec, machine_options, vm)
|
612
|
-
transport = transport_for(
|
613
|
-
machine_spec,
|
614
|
-
machine_options[:bootstrap_options][:ssh]
|
615
|
-
)
|
616
|
-
if !transport.available?
|
617
|
-
if action_handler.should_perform_actions
|
618
|
-
action_handler.report_progress "waiting for #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url}) to be connectable (transport up and running) ..."
|
619
|
-
|
620
|
-
until remaining_wait_time(machine_spec, machine_options) < 0 || transport.available? do
|
621
|
-
print "."
|
622
|
-
sleep 5
|
623
|
-
end
|
624
|
-
|
625
|
-
action_handler.report_progress "#{machine_spec.name} is now connectable"
|
626
|
-
end
|
627
|
-
end
|
628
|
-
end
|
629
|
-
|
630
|
-
def transport_for(
|
631
|
-
machine_spec,
|
632
|
-
remoting_options,
|
633
|
-
ip = machine_spec.location['ipaddress']
|
634
|
-
)
|
635
|
-
if machine_spec.location['is_windows']
|
636
|
-
create_winrm_transport(ip, remoting_options)
|
637
|
-
else
|
638
|
-
create_ssh_transport(ip, remoting_options)
|
639
|
-
end
|
640
|
-
end
|
641
|
-
|
642
|
-
def create_winrm_transport(host, options)
|
643
|
-
require 'chef/provisioning/transport/winrm'
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
"
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
1
|
+
require 'chef'
|
2
|
+
require 'cheffish/merged_config'
|
3
|
+
require 'chef/provisioning/driver'
|
4
|
+
require 'chef/provisioning/machine/windows_machine'
|
5
|
+
require 'chef/provisioning/machine/unix_machine'
|
6
|
+
require 'chef/provisioning/vsphere_driver/clone_spec_builder'
|
7
|
+
require 'chef/provisioning/vsphere_driver/version'
|
8
|
+
require 'chef/provisioning/vsphere_driver/vsphere_helpers'
|
9
|
+
require 'chef/provisioning/vsphere_driver/vsphere_url'
|
10
|
+
|
11
|
+
module ChefProvisioningVsphere
|
12
|
+
# Provisions machines in vSphere.
|
13
|
+
class VsphereDriver < Chef::Provisioning::Driver
|
14
|
+
include Chef::Mixin::ShellOut
|
15
|
+
|
16
|
+
def self.from_url(driver_url, config)
|
17
|
+
VsphereDriver.new(driver_url, config)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create a new Vsphere provisioner.
|
21
|
+
#
|
22
|
+
# ## Parameters
|
23
|
+
# connect_options - hash of options to be passed to RbVmomi::VIM.connect
|
24
|
+
# :host - required - hostname of the vSphere API server
|
25
|
+
# :port - optional - port on the vSphere API server (default: 443)
|
26
|
+
# :path - optional - path on the vSphere API server (default: /sdk)
|
27
|
+
# :use_ssl - optional - true to use ssl in connection to vSphere API server (default: true)
|
28
|
+
# :insecure - optional - true to ignore ssl certificate validation errors in connection to vSphere API server (default: false)
|
29
|
+
# :user - required - user name to use in connection to vSphere API server
|
30
|
+
# :password - required - password to use in connection to vSphere API server
|
31
|
+
def self.canonicalize_url(driver_url, config)
|
32
|
+
config = symbolize_keys(config)
|
33
|
+
[ driver_url || URI::VsphereUrl.from_config(config).to_s, config ]
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.symbolize_keys(h)
|
37
|
+
Hash === h ?
|
38
|
+
Hash[
|
39
|
+
h.map do |k, v|
|
40
|
+
[k.respond_to?(:to_sym) ? k.to_sym : k, symbolize_keys(v)]
|
41
|
+
end
|
42
|
+
] : h
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(driver_url, config)
|
46
|
+
super(driver_url, config)
|
47
|
+
|
48
|
+
uri = URI(driver_url)
|
49
|
+
@connect_options = {
|
50
|
+
provider: 'vsphere',
|
51
|
+
host: uri.host,
|
52
|
+
port: uri.port,
|
53
|
+
use_ssl: uri.use_ssl,
|
54
|
+
insecure: uri.insecure,
|
55
|
+
path: uri.path
|
56
|
+
}
|
57
|
+
|
58
|
+
if driver_options
|
59
|
+
@connect_options[:user] = driver_options[:user]
|
60
|
+
@connect_options[:password] = driver_options[:password]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :connect_options
|
65
|
+
|
66
|
+
# Acquire a machine, generally by provisioning it. Returns a Machine
|
67
|
+
# object pointing at the machine, allowing useful actions like setup,
|
68
|
+
# converge, execute, file and directory. The Machine object will have a
|
69
|
+
# "node" property which must be saved to the server (if it is any
|
70
|
+
# different from the original node object).
|
71
|
+
#
|
72
|
+
# ## Parameters
|
73
|
+
# action_handler - the action_handler object that is calling this method; this
|
74
|
+
# is generally a action_handler, but could be anything that can support the
|
75
|
+
# ChefMetal::ActionHandler interface (i.e., in the case of the test
|
76
|
+
# kitchen metal driver for acquiring and destroying VMs; see the base
|
77
|
+
# class for what needs providing).
|
78
|
+
# node - node object (deserialized json) representing this machine. If
|
79
|
+
# the node has a provisioner_options hash in it, these will be used
|
80
|
+
# instead of options provided by the provisioner. TODO compare and
|
81
|
+
# fail if different?
|
82
|
+
# node will have node['normal']['provisioner_options'] in it with any options.
|
83
|
+
# It is a hash with this format:
|
84
|
+
#
|
85
|
+
# -- provisioner_url: vsphere://host:port?ssl=[true|false]&insecure=[true|false]
|
86
|
+
# -- bootstrap_options: hash of options to pass to RbVmomi::VIM::VirtualMachine::CloneTask()
|
87
|
+
# :datacenter
|
88
|
+
# :resource_pool
|
89
|
+
# :cluster
|
90
|
+
# :datastore
|
91
|
+
# :template_name
|
92
|
+
# :template_folder
|
93
|
+
# :vm_folder
|
94
|
+
# :winrm {...} (not yet implemented)
|
95
|
+
# :ssh {...}
|
96
|
+
#
|
97
|
+
# Example bootstrap_options for vSphere:
|
98
|
+
# TODO: add other CloneTask params, e.g.: datastore, annotation, resource_pool, ...
|
99
|
+
# 'bootstrap_options' => {
|
100
|
+
# 'template_name' =>'centos6.small',
|
101
|
+
# 'template_folder' =>'Templates',
|
102
|
+
# 'vm_folder' => 'MyApp'
|
103
|
+
# }
|
104
|
+
#
|
105
|
+
# node['normal']['provisioner_output'] will be populated with information
|
106
|
+
# about the created machine. For vSphere, it is a hash with this
|
107
|
+
# format:
|
108
|
+
#
|
109
|
+
# -- provisioner_url: vsphere:host:port?ssl=[true|false]&insecure=[true|false]
|
110
|
+
# -- vm_folder: name of the vSphere folder containing the VM
|
111
|
+
#
|
112
|
+
def allocate_machine(action_handler, machine_spec, machine_options)
|
113
|
+
merge_options! machine_options
|
114
|
+
|
115
|
+
if machine_spec.location
|
116
|
+
Chef::Log.warn(
|
117
|
+
"Checking to see if #{machine_spec.location} has been created...")
|
118
|
+
vm = vm_for(machine_spec)
|
119
|
+
if vm
|
120
|
+
Chef::Log.warn 'returning existing machine'
|
121
|
+
return vm
|
122
|
+
else
|
123
|
+
Chef::Log.warn machine_msg(
|
124
|
+
machine_spec.name,
|
125
|
+
machine_spec.location['server_id'],
|
126
|
+
'no longer exists. Recreating ...'
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
bootstrap_options = machine_options[:bootstrap_options]
|
131
|
+
|
132
|
+
action_handler.report_progress full_description(
|
133
|
+
machine_spec, bootstrap_options)
|
134
|
+
|
135
|
+
vm = find_or_create_vm(bootstrap_options, machine_spec, action_handler)
|
136
|
+
|
137
|
+
add_machine_spec_location(vm, machine_spec)
|
138
|
+
|
139
|
+
action_handler.performed_action(machine_msg(
|
140
|
+
machine_spec.name,
|
141
|
+
vm.config.instanceUuid,
|
142
|
+
'created'
|
143
|
+
))
|
144
|
+
vm
|
145
|
+
end
|
146
|
+
|
147
|
+
def merge_options!(machine_options)
|
148
|
+
@config = Cheffish::MergedConfig.new(
|
149
|
+
{ machine_options: machine_options },
|
150
|
+
@config
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_machine_spec_location(vm, machine_spec)
|
155
|
+
machine_spec.location = {
|
156
|
+
'driver_url' => driver_url,
|
157
|
+
'driver_version' => VERSION,
|
158
|
+
'server_id' => vm.config.instanceUuid,
|
159
|
+
'is_windows' => is_windows?(vm),
|
160
|
+
'allocated_at' => Time.now.utc.to_s,
|
161
|
+
'ipaddress' => vm.guest.ipAddress
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def find_or_create_vm(bootstrap_options, machine_spec, action_handler)
|
166
|
+
vm = vsphere_helper.find_vm(
|
167
|
+
bootstrap_options[:vm_folder],
|
168
|
+
machine_spec.name
|
169
|
+
)
|
170
|
+
if vm
|
171
|
+
Chef::Log.info machine_msg(
|
172
|
+
machine_spec.name,
|
173
|
+
vm.config.instanceUuid,
|
174
|
+
'already created'
|
175
|
+
)
|
176
|
+
else
|
177
|
+
vm = clone_vm(
|
178
|
+
action_handler,
|
179
|
+
bootstrap_options,
|
180
|
+
machine_spec.name
|
181
|
+
)
|
182
|
+
end
|
183
|
+
vm
|
184
|
+
end
|
185
|
+
|
186
|
+
def full_description(machine_spec, bootstrap_options)
|
187
|
+
description = [ "creating machine #{machine_spec.name} on #{driver_url}" ]
|
188
|
+
bootstrap_options.to_hash.each_pair do |key,value|
|
189
|
+
description << " #{key}: #{value.inspect}"
|
190
|
+
end
|
191
|
+
description
|
192
|
+
end
|
193
|
+
|
194
|
+
def machine_msg(name, id, action)
|
195
|
+
"Machine - #{action} - #{name} (#{id} on #{driver_url})"
|
196
|
+
end
|
197
|
+
|
198
|
+
def ready_machine(action_handler, machine_spec, machine_options)
|
199
|
+
merge_options! machine_options
|
200
|
+
|
201
|
+
vm = start_machine(action_handler, machine_spec, machine_options)
|
202
|
+
if vm.nil?
|
203
|
+
raise "Machine #{machine_spec.name} does not have a server "\
|
204
|
+
'associated with it, or server does not exist.'
|
205
|
+
end
|
206
|
+
|
207
|
+
bootstrap_options = machine_options[:bootstrap_options]
|
208
|
+
|
209
|
+
transport_respond?(
|
210
|
+
machine_options,
|
211
|
+
vm,
|
212
|
+
action_handler,
|
213
|
+
machine_spec
|
214
|
+
)
|
215
|
+
|
216
|
+
machine = machine_for(machine_spec,machine_options)
|
217
|
+
|
218
|
+
setup_extra_nics(action_handler, bootstrap_options, vm, machine)
|
219
|
+
|
220
|
+
if has_static_ip(bootstrap_options) && !is_windows?(vm)
|
221
|
+
setup_ubuntu_dns(machine, bootstrap_options, machine_spec)
|
222
|
+
end
|
223
|
+
|
224
|
+
machine
|
225
|
+
end
|
226
|
+
|
227
|
+
def setup_extra_nics(action_handler, bootstrap_options, vm, machine)
|
228
|
+
networks=bootstrap_options[:network_name]
|
229
|
+
if networks.kind_of?(String)
|
230
|
+
networks=[networks]
|
231
|
+
end
|
232
|
+
return if networks.nil? || networks.count < 2
|
233
|
+
|
234
|
+
new_nics = vsphere_helper.add_extra_nic(
|
235
|
+
action_handler,
|
236
|
+
vm_template_for(bootstrap_options),
|
237
|
+
bootstrap_options,
|
238
|
+
vm
|
239
|
+
)
|
240
|
+
if is_windows?(vm) && !new_nics.nil?
|
241
|
+
new_nics.each do |nic|
|
242
|
+
nic_label = nic.device.deviceInfo.label
|
243
|
+
machine.execute_always(
|
244
|
+
"Disable-Netadapter -Name '#{nic_label}' -Confirm:$false")
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def transport_respond?(
|
250
|
+
machine_options,
|
251
|
+
vm,
|
252
|
+
action_handler,
|
253
|
+
machine_spec
|
254
|
+
)
|
255
|
+
bootstrap_options = machine_options[:bootstrap_options]
|
256
|
+
|
257
|
+
# this waits for vmware tools to start and the vm to presebnt an ip
|
258
|
+
# This may just be the ip of a newly cloned machine
|
259
|
+
# Customization below may change this to a valid ip
|
260
|
+
wait_until_ready(action_handler, machine_spec, machine_options, vm)
|
261
|
+
|
262
|
+
if !machine_spec.location['ipaddress'] || !has_ip?(machine_spec.location['ipaddress'], vm)
|
263
|
+
# find the ip we actually want
|
264
|
+
# this will be the static ip to assign
|
265
|
+
# or the ip reported back by the vm if using dhcp
|
266
|
+
# it *may* be nil if just cloned
|
267
|
+
vm_ip = ip_to_bootstrap(bootstrap_options, vm)
|
268
|
+
transport = nil
|
269
|
+
unless vm_ip.nil?
|
270
|
+
transport = transport_for(machine_spec, bootstrap_options[:ssh], vm_ip)
|
271
|
+
end
|
272
|
+
|
273
|
+
unless !transport.nil? && transport.available? && has_ip?(vm_ip, vm)
|
274
|
+
attempt_ip(machine_options, action_handler, vm, machine_spec)
|
275
|
+
end
|
276
|
+
machine_spec.location['ipaddress'] = vm.guest.ipAddress
|
277
|
+
action_handler.report_progress(
|
278
|
+
"IP address obtained: #{machine_spec.location['ipaddress']}")
|
279
|
+
end
|
280
|
+
|
281
|
+
wait_for_domain(bootstrap_options, vm, machine_spec, action_handler)
|
282
|
+
|
283
|
+
begin
|
284
|
+
wait_for_transport(action_handler, machine_spec, machine_options, vm)
|
285
|
+
rescue Timeout::Error
|
286
|
+
# Only ever reboot once, and only if it's been less than 10 minutes
|
287
|
+
# since we stopped waiting
|
288
|
+
if machine_spec.location['started_at'] ||
|
289
|
+
remaining_wait_time(machine_spec, machine_options) < -(10*60)
|
290
|
+
raise
|
291
|
+
else
|
292
|
+
Chef::Log.warn(machine_msg(
|
293
|
+
machine_spec.name,
|
294
|
+
vm.config.instanceUuid,
|
295
|
+
'started but SSH did not come up. Rebooting...'
|
296
|
+
))
|
297
|
+
restart_server(action_handler, machine_spec, machine_options)
|
298
|
+
wait_until_ready(action_handler, machine_spec, machine_options, vm)
|
299
|
+
wait_for_transport(action_handler, machine_spec, machine_options, vm)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def attempt_ip(machine_options, action_handler, vm, machine_spec)
|
305
|
+
vm_ip = ip_to_bootstrap(machine_options[:bootstrap_options], vm)
|
306
|
+
|
307
|
+
wait_for_ip(vm, machine_options, machine_spec, action_handler)
|
308
|
+
|
309
|
+
unless has_ip?(vm_ip, vm)
|
310
|
+
action_handler.report_progress "rebooting..."
|
311
|
+
if vm.guest.toolsRunningStatus != "guestToolsRunning"
|
312
|
+
msg = 'tools have stopped. current power state is '
|
313
|
+
msg << vm.runtime.powerState
|
314
|
+
msg << ' and tools state is '
|
315
|
+
msg << vm.guest.toolsRunningStatus
|
316
|
+
msg << '. powering up server...'
|
317
|
+
action_handler.report_progress(msg.join)
|
318
|
+
vsphere_helper.start_vm(vm)
|
319
|
+
else
|
320
|
+
restart_server(action_handler, machine_spec, machine_options)
|
321
|
+
end
|
322
|
+
wait_for_ip(vm, machine_options, machine_spec, action_handler)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def wait_for_domain(bootstrap_options, vm, machine_spec, action_handler)
|
327
|
+
return unless bootstrap_options[:customization_spec]
|
328
|
+
return unless bootstrap_options[:customization_spec][:domain]
|
329
|
+
|
330
|
+
domain = bootstrap_options[:customization_spec][:domain]
|
331
|
+
if is_windows?(vm) && domain != 'local'
|
332
|
+
start = Time.now.utc
|
333
|
+
trimmed_name = machine_spec.name.byteslice(0,15)
|
334
|
+
expected_name="#{trimmed_name}.#{domain}"
|
335
|
+
action_handler.report_progress(
|
336
|
+
"waiting to domain join and be named #{expected_name}")
|
337
|
+
until (Time.now.utc - start) > 30 ||
|
338
|
+
(vm.guest.hostName == expected_name) do
|
339
|
+
print '.'
|
340
|
+
sleep 5
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def wait_for_ip(vm, machine_options, machine_spec, action_handler)
|
346
|
+
bootstrap_options = machine_options[:bootstrap_options]
|
347
|
+
vm_ip = ip_to_bootstrap(bootstrap_options, vm)
|
348
|
+
ready_timeout = machine_options[:ready_timeout] || 300
|
349
|
+
msg = "waiting up to #{ready_timeout} seconds for customization"
|
350
|
+
msg << " and find #{vm_ip}" unless vm_ip == vm.guest.ipAddress
|
351
|
+
action_handler.report_progress msg
|
352
|
+
|
353
|
+
start = Time.now.utc
|
354
|
+
connectable = false
|
355
|
+
until (Time.now.utc - start) > ready_timeout || connectable do
|
356
|
+
action_handler.report_progress(
|
357
|
+
"IP addresses found: #{all_ips_for(vm)}")
|
358
|
+
vm_ip ||= ip_to_bootstrap(bootstrap_options, vm)
|
359
|
+
if has_ip?(vm_ip, vm)
|
360
|
+
connectable = transport_for(
|
361
|
+
machine_spec,
|
362
|
+
machine_options[:bootstrap_options][:ssh],
|
363
|
+
vm_ip
|
364
|
+
).available?
|
365
|
+
end
|
366
|
+
sleep 5
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def all_ips_for(vm)
|
371
|
+
vm.guest.net.map { |net| net.ipAddress}.flatten
|
372
|
+
end
|
373
|
+
|
374
|
+
def has_ip?(ip, vm)
|
375
|
+
all_ips_for(vm).include?(ip)
|
376
|
+
end
|
377
|
+
|
378
|
+
# Connect to machine without acquiring it
|
379
|
+
def connect_to_machine(machine_spec, machine_options)
|
380
|
+
merge_options! machine_options
|
381
|
+
machine_for(machine_spec, machine_options)
|
382
|
+
end
|
383
|
+
|
384
|
+
def destroy_machine(action_handler, machine_spec, machine_options)
|
385
|
+
merge_options! machine_options
|
386
|
+
vm = vm_for(machine_spec)
|
387
|
+
if vm
|
388
|
+
action_handler.perform_action "Delete VM [#{vm.parent.name}/#{vm.name}]" do
|
389
|
+
begin
|
390
|
+
vsphere_helper.stop_vm(vm, machine_options[:stop_timeout])
|
391
|
+
vm.Destroy_Task.wait_for_completion
|
392
|
+
rescue RbVmomi::Fault => fault
|
393
|
+
raise fault unless fault.fault.class.wsdl_name == "ManagedObjectNotFound"
|
394
|
+
ensure
|
395
|
+
machine_spec.location = nil
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
strategy = convergence_strategy_for(machine_spec, machine_options)
|
400
|
+
strategy.cleanup_convergence(action_handler, machine_spec)
|
401
|
+
end
|
402
|
+
|
403
|
+
def stop_machine(action_handler, machine_spec, machine_options)
|
404
|
+
merge_options! machine_options
|
405
|
+
vm = vm_for(machine_spec)
|
406
|
+
if vm
|
407
|
+
action_handler.perform_action "Shutdown guest OS and power off VM [#{vm.parent.name}/#{vm.name}]" do
|
408
|
+
vsphere_helper.stop_vm(vm, machine_options[:stop_timeout])
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def start_machine(action_handler, machine_spec, machine_options)
|
414
|
+
merge_options! machine_options
|
415
|
+
vm = vm_for(machine_spec)
|
416
|
+
if vm
|
417
|
+
action_handler.perform_action "Power on VM [#{vm.parent.name}/#{vm.name}]" do
|
418
|
+
vsphere_helper.start_vm(vm, machine_options[:bootstrap_options][:ssh][:port])
|
419
|
+
end
|
420
|
+
end
|
421
|
+
vm
|
422
|
+
end
|
423
|
+
|
424
|
+
def restart_server(action_handler, machine_spec, machine_options)
|
425
|
+
action_handler.perform_action "restart machine #{machine_spec.name} (#{driver_url})" do
|
426
|
+
stop_machine(action_handler, machine_spec, machine_options)
|
427
|
+
start_machine(action_handler, machine_spec, machine_options)
|
428
|
+
machine_spec.location['started_at'] = Time.now.utc.to_s
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
protected
|
433
|
+
|
434
|
+
def setup_ubuntu_dns(machine, bootstrap_options, machine_spec)
|
435
|
+
host_lookup = machine.execute_always('host google.com')
|
436
|
+
if host_lookup.exitstatus != 0
|
437
|
+
if host_lookup.stdout.include?("setlocale: LC_ALL")
|
438
|
+
machine.execute_always('locale-gen en_US && update-locale LANG=en_US')
|
439
|
+
end
|
440
|
+
distro = machine.execute_always("lsb_release -i | sed -e 's/Distributor ID://g'").stdout.strip
|
441
|
+
Chef::Log.info "Found distro:#{distro}"
|
442
|
+
if distro == 'Ubuntu'
|
443
|
+
distro_version = (machine.execute_always("lsb_release -r | sed -e s/[^0-9.]//g")).stdout.strip.to_f
|
444
|
+
Chef::Log.info "Found distro version:#{distro_version}"
|
445
|
+
if distro_version>= 12.04
|
446
|
+
Chef::Log.info "Ubuntu version 12.04 or greater. Need to patch DNS."
|
447
|
+
interfaces_file = "/etc/network/interfaces"
|
448
|
+
nameservers = bootstrap_options[:customization_spec][:ipsettings][:dnsServerList].join(' ')
|
449
|
+
machine.execute_always("if ! cat #{interfaces_file} | grep -q dns-search ; then echo 'dns-search #{bootstrap_options[:customization_spec][:domain]}' >> #{interfaces_file} ; fi")
|
450
|
+
machine.execute_always("if ! cat #{interfaces_file} | grep -q dns-nameservers ; then echo 'dns-nameservers #{nameservers}' >> #{interfaces_file} ; fi")
|
451
|
+
machine.execute_always('/etc/init.d/networking restart')
|
452
|
+
machine.execute_always('apt-get -qq update')
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
def has_static_ip(bootstrap_options)
|
459
|
+
if bootstrap_options.has_key?(:customization_spec)
|
460
|
+
bootstrap_options = bootstrap_options[:customization_spec]
|
461
|
+
if bootstrap_options.has_key?(:ipsettings)
|
462
|
+
bootstrap_options = bootstrap_options[:ipsettings]
|
463
|
+
if bootstrap_options.has_key?(:ip)
|
464
|
+
return true
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
false
|
469
|
+
end
|
470
|
+
|
471
|
+
def remaining_wait_time(machine_spec, machine_options)
|
472
|
+
if machine_spec.location['started_at']
|
473
|
+
(machine_options[:start_timeout] || 600) -
|
474
|
+
(Time.now.utc - Time.parse(machine_spec.location['started_at']))
|
475
|
+
else
|
476
|
+
(machine_options[:create_timeout] || 600) -
|
477
|
+
(Time.now.utc - Time.parse(machine_spec.location['allocated_at']))
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def wait_until_ready(action_handler, machine_spec, machine_options, vm)
|
482
|
+
if vm.guest.toolsRunningStatus != "guestToolsRunning"
|
483
|
+
if action_handler.should_perform_actions
|
484
|
+
action_handler.report_progress "waiting for #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url}) to be ready ..."
|
485
|
+
until remaining_wait_time(machine_spec, machine_options) < 0 ||
|
486
|
+
(vm.guest.toolsRunningStatus == "guestToolsRunning" && !vm.guest.ipAddress.nil? && vm.guest.ipAddress.length > 0) do
|
487
|
+
print "."
|
488
|
+
sleep 5
|
489
|
+
end
|
490
|
+
action_handler.report_progress "#{machine_spec.name} is now ready"
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
def vm_for(machine_spec)
|
496
|
+
if machine_spec.location
|
497
|
+
vsphere_helper.find_vm_by_id(machine_spec.location['server_id'])
|
498
|
+
else
|
499
|
+
nil
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def clone_vm(action_handler, bootstrap_options, machine_name)
|
504
|
+
vm_template = vm_template_for(bootstrap_options)
|
505
|
+
|
506
|
+
spec_builder = CloneSpecBuilder.new(vsphere_helper, action_handler)
|
507
|
+
clone_spec = spec_builder.build(vm_template, machine_name, bootstrap_options)
|
508
|
+
Chef::Log.debug("Clone spec: #{clone_spec.pretty_inspect}")
|
509
|
+
|
510
|
+
vm_folder = vsphere_helper.find_folder(bootstrap_options[:vm_folder])
|
511
|
+
vm_template.CloneVM_Task(
|
512
|
+
name: machine_name,
|
513
|
+
folder: vm_folder,
|
514
|
+
spec: clone_spec
|
515
|
+
).wait_for_completion
|
516
|
+
|
517
|
+
vm = vsphere_helper.find_vm(vm_folder, machine_name)
|
518
|
+
|
519
|
+
additional_disk_size_gb = bootstrap_options[:additional_disk_size_gb]
|
520
|
+
if !additional_disk_size_gb.is_a?(Array)
|
521
|
+
additional_disk_size_gb = [additional_disk_size_gb]
|
522
|
+
end
|
523
|
+
|
524
|
+
additional_disk_size_gb.each do |size|
|
525
|
+
size = size.to_i
|
526
|
+
next if size == 0
|
527
|
+
if bootstrap_options[:datastore].to_s.empty?
|
528
|
+
raise ':datastore must be specified when adding a disk to a cloned vm'
|
529
|
+
end
|
530
|
+
task = vm.ReconfigVM_Task(
|
531
|
+
spec: RbVmomi::VIM.VirtualMachineConfigSpec(
|
532
|
+
deviceChange: [
|
533
|
+
vsphere_helper.virtual_disk_for(
|
534
|
+
vm,
|
535
|
+
bootstrap_options[:datastore],
|
536
|
+
size
|
537
|
+
)
|
538
|
+
]
|
539
|
+
)
|
540
|
+
)
|
541
|
+
task.wait_for_completion
|
542
|
+
end
|
543
|
+
|
544
|
+
vm
|
545
|
+
end
|
546
|
+
|
547
|
+
def vsphere_helper
|
548
|
+
@vsphere_helper ||= VsphereHelper.new(
|
549
|
+
connect_options,
|
550
|
+
config[:machine_options][:bootstrap_options][:datacenter]
|
551
|
+
)
|
552
|
+
end
|
553
|
+
|
554
|
+
def vm_template_for(bootstrap_options)
|
555
|
+
template_folder = bootstrap_options[:template_folder]
|
556
|
+
template_name = bootstrap_options[:template_name]
|
557
|
+
vsphere_helper.find_vm(template_folder, template_name) ||
|
558
|
+
raise("vSphere VM Template not found [#{template_folder}/#{template_name}]")
|
559
|
+
end
|
560
|
+
|
561
|
+
def machine_for(machine_spec, machine_options)
|
562
|
+
if machine_spec.location.nil?
|
563
|
+
raise "Server for node #{machine_spec.name} has not been created!"
|
564
|
+
end
|
565
|
+
|
566
|
+
transport = transport_for(
|
567
|
+
machine_spec,
|
568
|
+
machine_options[:bootstrap_options][:ssh]
|
569
|
+
)
|
570
|
+
strategy = convergence_strategy_for(machine_spec, machine_options)
|
571
|
+
|
572
|
+
if machine_spec.location['is_windows']
|
573
|
+
Chef::Provisioning::Machine::WindowsMachine.new(
|
574
|
+
machine_spec, transport, strategy)
|
575
|
+
else
|
576
|
+
Chef::Provisioning::Machine::UnixMachine.new(
|
577
|
+
machine_spec, transport, strategy)
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
def is_windows?(vm)
|
582
|
+
return false if vm.nil?
|
583
|
+
vm.config.guestId.start_with?('win')
|
584
|
+
end
|
585
|
+
|
586
|
+
def convergence_strategy_for(machine_spec, machine_options)
|
587
|
+
require 'chef/provisioning/convergence_strategy/install_msi'
|
588
|
+
require 'chef/provisioning/convergence_strategy/install_cached'
|
589
|
+
require 'chef/provisioning/convergence_strategy/no_converge'
|
590
|
+
|
591
|
+
mopts = machine_options[:convergence_options].to_hash.dup
|
592
|
+
if mopts[:chef_server]
|
593
|
+
mopts[:chef_server] = mopts[:chef_server].to_hash.dup
|
594
|
+
mopts[:chef_server][:options] = mopts[:chef_server][:options].to_hash.dup if mopts[:chef_server][:options]
|
595
|
+
end
|
596
|
+
|
597
|
+
if !machine_spec.location
|
598
|
+
return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(
|
599
|
+
mopts, config)
|
600
|
+
end
|
601
|
+
|
602
|
+
if machine_spec.location['is_windows']
|
603
|
+
Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(
|
604
|
+
mopts, config)
|
605
|
+
else
|
606
|
+
Chef::Provisioning::ConvergenceStrategy::InstallCached.new(
|
607
|
+
mopts, config)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
def wait_for_transport(action_handler, machine_spec, machine_options, vm)
|
612
|
+
transport = transport_for(
|
613
|
+
machine_spec,
|
614
|
+
machine_options[:bootstrap_options][:ssh]
|
615
|
+
)
|
616
|
+
if !transport.available?
|
617
|
+
if action_handler.should_perform_actions
|
618
|
+
action_handler.report_progress "waiting for #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url}) to be connectable (transport up and running) ..."
|
619
|
+
|
620
|
+
until remaining_wait_time(machine_spec, machine_options) < 0 || transport.available? do
|
621
|
+
print "."
|
622
|
+
sleep 5
|
623
|
+
end
|
624
|
+
|
625
|
+
action_handler.report_progress "#{machine_spec.name} is now connectable"
|
626
|
+
end
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
def transport_for(
|
631
|
+
machine_spec,
|
632
|
+
remoting_options,
|
633
|
+
ip = machine_spec.location['ipaddress']
|
634
|
+
)
|
635
|
+
if machine_spec.location['is_windows']
|
636
|
+
create_winrm_transport(ip, remoting_options)
|
637
|
+
else
|
638
|
+
create_ssh_transport(ip, remoting_options)
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
def create_winrm_transport(host, options)
|
643
|
+
require 'chef/provisioning/transport/winrm'
|
644
|
+
winrm_transport =
|
645
|
+
options[:winrm_transport].nil? ? :negotiate : options[:winrm_transport].to_sym
|
646
|
+
port = options[:port] || winrm_transport == :ssl ? '5986' : '5985'
|
647
|
+
winrm_options = {
|
648
|
+
user: "#{options[:user]}",
|
649
|
+
pass: options[:password]
|
650
|
+
}
|
651
|
+
if options[:winrm_opts].nil?
|
652
|
+
opt = options[:user].include?("\\") ? :disable_sspi : :basic_auth_only
|
653
|
+
winrm_options.merge!(opt => true)
|
654
|
+
else
|
655
|
+
winrm_options.merge!(options[:winrm_opts])
|
656
|
+
end
|
657
|
+
endpoint = "http#{winrm_transport == :ssl ? 's' : ''}://"\
|
658
|
+
"#{host}:#{port}/wsman"
|
659
|
+
|
660
|
+
Chef::Provisioning::Transport::WinRM.new(
|
661
|
+
endpoint,
|
662
|
+
winrm_transport,
|
663
|
+
winrm_options,
|
664
|
+
config
|
665
|
+
)
|
666
|
+
end
|
667
|
+
|
668
|
+
def create_ssh_transport(host, options)
|
669
|
+
require 'chef/provisioning/transport/ssh'
|
670
|
+
ssh_user = options[:user]
|
671
|
+
Chef::Provisioning::Transport::SSH.new(
|
672
|
+
host,
|
673
|
+
ssh_user,
|
674
|
+
options.to_hash,
|
675
|
+
@config[:machine_options][:sudo] ? {:prefix => 'sudo '} : {},
|
676
|
+
config
|
677
|
+
)
|
678
|
+
end
|
679
|
+
|
680
|
+
def ip_to_bootstrap(bootstrap_options, vm)
|
681
|
+
if has_static_ip(bootstrap_options)
|
682
|
+
bootstrap_options[:customization_spec][:ipsettings][:ip]
|
683
|
+
else
|
684
|
+
vm.guest.ipAddress
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
end
|