chef-provisioning-vsphere 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module ChefProvisioningVsphere
2
+ VERSION = '0.4.2'
3
+ end
@@ -0,0 +1,506 @@
1
+ require 'rbvmomi'
2
+
3
+ module ChefProvisioningVsphere
4
+ module Helpers
5
+
6
+ if !$guest_op_managers
7
+ $guest_op_managers = {}
8
+ end
9
+
10
+ def vim(options = connect_options)
11
+ if @current_connection.nil? or @current_connection.serviceContent.sessionManager.currentSession.nil?
12
+ puts "establishing connection to #{options[:host]}"
13
+ @current_connection = RbVmomi::VIM.connect options
14
+ str_conn = @current_connection.pretty_inspect # a string in the format of VIM(host ip)
15
+
16
+ # we are caching guest operation managers in a global variable...terrible i know
17
+ # this object is available from the serviceContent object on API version 5 forward
18
+ # Its a singleton and if another connection is made for the same host and user
19
+ # that object is not available on any subsequent connection
20
+ # I could find no documentation that discusses this
21
+ if !$guest_op_managers.has_key?(str_conn)
22
+ $guest_op_managers[str_conn] = @current_connection.serviceContent.guestOperationsManager
23
+ end
24
+ end
25
+
26
+ @current_connection
27
+ end
28
+
29
+ def find_vm(dc_name, vm_folder, vm_name)
30
+ folder = find_folder(dc_name, vm_folder) or raise("vSphere Folder not found [#{vm_folder}] for vm #{vm_name}")
31
+ vm = folder.find(vm_name, RbVmomi::VIM::VirtualMachine)
32
+ end
33
+
34
+ def find_vm_by_id(uuid, connection = vim)
35
+ vm = connection.searchIndex.FindByUuid({:uuid => uuid, :vmSearch => true, :instanceUuid => true})
36
+ end
37
+
38
+ def vm_started?(vm, wait_on_port = 22)
39
+ return false if vm.nil?
40
+ state = vm.runtime.powerState
41
+ return false unless state == 'poweredOn'
42
+ return false unless port_ready?(vm, wait_on_port)
43
+ return true
44
+ end
45
+
46
+ def vm_stopped?(vm)
47
+ return true if vm.nil?
48
+ state = vm.runtime.powerState
49
+ return false unless state == 'poweredOff'
50
+ return false
51
+ end
52
+
53
+ def start_vm(vm, wait_on_port = 22)
54
+ state = vm.runtime.powerState
55
+ unless state == 'poweredOn'
56
+ vm.PowerOnVM_Task.wait_for_completion
57
+ end
58
+ end
59
+
60
+ def stop_vm(vm)
61
+ begin
62
+ vm.ShutdownGuest
63
+ sleep 2 until vm.runtime.powerState == 'poweredOff'
64
+ rescue
65
+ vm.PowerOffVM_Task.wait_for_completion
66
+ end
67
+ end
68
+
69
+ def port_ready?(vm, port)
70
+ vm_ip = vm.guest.ipAddress
71
+ return false if vm_ip.nil?
72
+
73
+ begin
74
+ tcp_socket = TCPSocket.new(vm_ip, port)
75
+ readable = IO.select([tcp_socket], nil, nil, 5)
76
+ if readable
77
+ true
78
+ else
79
+ false
80
+ end
81
+ rescue Errno::ETIMEDOUT
82
+ false
83
+ rescue Errno::EPERM
84
+ false
85
+ rescue Errno::ECONNREFUSED
86
+ false
87
+ rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
88
+ false
89
+ ensure
90
+ tcp_socket && tcp_socket.close
91
+ end
92
+ end
93
+
94
+ #folder could be like: /Level1/Level2/folder_name
95
+ def find_folder(dc_name, folder_name)
96
+ baseEntity = dc(dc_name).vmFolder
97
+ if folder_name && folder_name.length > 0
98
+ entityArray = folder_name.split('/')
99
+ entityArray.each do |entityArrItem|
100
+ if entityArrItem != ''
101
+ baseEntity = baseEntity.childEntity.grep(RbVmomi::VIM::Folder).find { |f| f.name == entityArrItem }
102
+ end
103
+ end
104
+ end
105
+ baseEntity
106
+ end
107
+
108
+ def dc(dc_name)
109
+ vim.serviceInstance.find_datacenter(dc_name) or raise("vSphere Datacenter not found [#{dc_name}]")
110
+ end
111
+
112
+ def network_adapter_for(operation, network_name, network_label, device_key, backing_info)
113
+ connectable = RbVmomi::VIM::VirtualDeviceConnectInfo(
114
+ :allowGuestControl => true,
115
+ :connected => true,
116
+ :startConnected => true)
117
+ device = RbVmomi::VIM::VirtualVmxnet3(
118
+ :backing => backing_info,
119
+ :deviceInfo => RbVmomi::VIM::Description(:label => network_label, :summary => network_name),
120
+ :key => device_key,
121
+ :connectable => connectable)
122
+ RbVmomi::VIM::VirtualDeviceConfigSpec(
123
+ :operation => operation,
124
+ :device => device)
125
+ end
126
+
127
+ def find_ethernet_cards_for(vm)
128
+ vm.config.hardware.device.select {|d| d.is_a?(RbVmomi::VIM::VirtualEthernetCard)}
129
+ end
130
+
131
+ def do_vm_clone(action_handler, dc_name, vm_template, vm_name, options)
132
+ deviceAdditions = []
133
+
134
+ clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(
135
+ location: relocate_spec_for(dc_name, vm_template, options),
136
+ powerOn: false,
137
+ template: false,
138
+ config: RbVmomi::VIM.VirtualMachineConfigSpec(
139
+ :cpuHotAddEnabled => true,
140
+ :memoryHotAddEnabled => true,
141
+ :cpuHotRemoveEnabled => true,
142
+ :deviceChange => Array.new)
143
+ )
144
+
145
+ clone_spec.customization = customization_options_from(action_handler, vm_template, vm_name, options)
146
+
147
+ unless options[:annotation].to_s.nil?
148
+ clone_spec.config.annotation = options[:annotation]
149
+ end
150
+
151
+ unless options[:num_cpus].to_s.nil?
152
+ clone_spec.config.numCPUs = options[:num_cpus]
153
+ end
154
+
155
+ unless options[:memory_mb].to_s.nil?
156
+ clone_spec.config.memoryMB = options[:memory_mb]
157
+ end
158
+
159
+ unless options[:network_name].nil?
160
+ deviceAdditions, changes = network_device_changes(action_handler, vm_template, options)
161
+ clone_spec.config.deviceChange = changes
162
+ end
163
+
164
+ vm_template.CloneVM_Task(
165
+ name: vm_name,
166
+ folder: find_folder(dc_name, options[:vm_folder]),
167
+ spec: clone_spec
168
+ ).wait_for_completion
169
+
170
+ vm = find_vm(dc_name, options[:vm_folder], vm_name)
171
+
172
+ if options[:additional_disk_size_gb].to_i > 0
173
+ task = vm.ReconfigVM_Task(:spec => RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => [virtual_disk_for(vm, options)]))
174
+ task.wait_for_completion
175
+ end
176
+
177
+ vm
178
+ end
179
+
180
+ def add_extra_nic(action_handler, vm_template, options, vm)
181
+ deviceAdditions, changes = network_device_changes(action_handler, vm_template, options)
182
+
183
+ if deviceAdditions.count > 0
184
+ current_networks = find_ethernet_cards_for(vm).map{|card| network_id_for(card.backing)}
185
+ new_devices = deviceAdditions.select { |device| !current_networks.include?(network_id_for(device.device.backing))}
186
+
187
+ if new_devices.count > 0
188
+ action_handler.report_progress "Adding extra NICs"
189
+ task = vm.ReconfigVM_Task(:spec => RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => new_devices))
190
+ task.wait_for_completion
191
+ new_devices
192
+ end
193
+ end
194
+ end
195
+
196
+ def network_id_for(backing_info)
197
+ if backing_info.is_a?(RbVmomi::VIM::VirtualEthernetCardDistributedVirtualPortBackingInfo)
198
+ backing_info.port.portgroupKey
199
+ else
200
+ backing_info.network
201
+ end
202
+ end
203
+
204
+ def relocate_spec_for(dc_name, vm_template, options)
205
+ datacenter = dc(dc_name)
206
+ if options.has_key?(:host)
207
+ host = find_host(datacenter, options[:host])
208
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(host: host)
209
+ else
210
+ pool = options[:resource_pool] ? find_pool(datacenter, options[:resource_pool]) : vm_template.resourcePool
211
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(pool: pool)
212
+ raise 'either :host or :resource_pool must be specified when cloning from a VM Template' if pool.nil?
213
+ end
214
+
215
+ if options.has_key?(:use_linked_clone)
216
+ create_delta_disk(vm_template)
217
+ rspec.diskMoveType = :moveChildMostDiskBacking
218
+ end
219
+
220
+ unless options[:datastore].to_s.empty?
221
+ rspec.datastore = find_datastore(datacenter, options[:datastore])
222
+ end
223
+
224
+ rspec
225
+ end
226
+
227
+ def create_delta_disk(vm_template)
228
+ disks = vm_template.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk)
229
+ disks.select { |disk| disk.backing.parent == nil }.each do |disk|
230
+ spec = {
231
+ :deviceChange => [
232
+ {
233
+ :operation => :remove,
234
+ :device => disk
235
+ },
236
+ {
237
+ :operation => :add,
238
+ :fileOperation => :create,
239
+ :device => disk.dup.tap { |new_disk|
240
+ new_disk.backing = new_disk.backing.dup
241
+ new_disk.backing.fileName = "[#{disk.backing.datastore.name}]"
242
+ new_disk.backing.parent = disk.backing
243
+ },
244
+ }
245
+ ]
246
+ }
247
+ vm_template.ReconfigVM_Task(:spec => spec).wait_for_completion
248
+ end
249
+ end
250
+
251
+ def virtual_disk_for(vm, options)
252
+ if options[:datastore].to_s.empty?
253
+ raise ":datastore must be specified when adding a disk to a cloned vm"
254
+ end
255
+ idx = vm.disks.count
256
+ RbVmomi::VIM::VirtualDeviceConfigSpec(
257
+ :operation => :add,
258
+ :fileOperation => :create,
259
+ :device => RbVmomi::VIM.VirtualDisk(
260
+ :key => idx,
261
+ :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo(
262
+ :fileName => "[#{options[:datastore]}]",
263
+ :diskMode => 'persistent',
264
+ :thinProvisioned => true
265
+ ),
266
+ :capacityInKB => options[:additional_disk_size_gb] * 1024 * 1024,
267
+ :controllerKey => 1000,
268
+ :unitNumber => idx
269
+ )
270
+ )
271
+ end
272
+
273
+ def network_device_changes(action_handler, vm_template, options)
274
+ additions = []
275
+ changes = []
276
+ networks=options[:network_name]
277
+ if networks.kind_of?(String)
278
+ networks=[networks]
279
+ end
280
+
281
+ cards = find_ethernet_cards_for(vm_template)
282
+
283
+ key = 4000
284
+ networks.each_index do | i |
285
+ label = "Ethernet #{i+1}"
286
+ backing_info = backing_info_for(action_handler, dc(options[:datacenter]), networks[i])
287
+ if card = cards.shift
288
+ key = card.key
289
+ operation = RbVmomi::VIM::VirtualDeviceConfigSpecOperation('edit')
290
+ action_handler.report_progress "changing template nic for #{networks[i]}"
291
+ changes.push(
292
+ network_adapter_for(operation, networks[i], label, key, backing_info))
293
+ else
294
+ key = key + 1
295
+ operation = RbVmomi::VIM::VirtualDeviceConfigSpecOperation('add')
296
+ action_handler.report_progress "will be adding nic for #{networks[i]}"
297
+ additions.push(
298
+ network_adapter_for(operation, networks[i], label, key, backing_info))
299
+ end
300
+ end
301
+ [additions, changes]
302
+ end
303
+
304
+ def backing_info_for(action_handler, datacenter, network_name)
305
+ networks = datacenter.network.select {|net| net.name == network_name}
306
+ if networks.count > 0
307
+ dpg = networks.select { |net| net.is_a?(RbVmomi::VIM::DistributedVirtualPortgroup) }
308
+ networks = dpg unless dpg.empty?
309
+ end
310
+ action_handler.report_progress "network: #{network_name} is a #{networks[0].class}}"
311
+ if networks[0].is_a?(RbVmomi::VIM::DistributedVirtualPortgroup)
312
+ port = RbVmomi::VIM::DistributedVirtualSwitchPortConnection(
313
+ :switchUuid => networks[0].config.distributedVirtualSwitch.uuid,
314
+ :portgroupKey => networks[0].key
315
+ )
316
+ RbVmomi::VIM::VirtualEthernetCardDistributedVirtualPortBackingInfo(:port => port)
317
+ else
318
+ RbVmomi::VIM::VirtualEthernetCardNetworkBackingInfo(:deviceName => network_name)
319
+ end
320
+ end
321
+
322
+ def find_datastore(dc, datastore_name)
323
+ baseEntity = dc.datastore
324
+ baseEntity.find { |f| f.info.name == datastore_name } or raise "no such datastore #{datastore_name}"
325
+ end
326
+
327
+ def customization_options_from(action_handler, vm_template, vm_name, options)
328
+ if options.has_key?(:customization_spec)
329
+ if(options[:customization_spec].is_a?(Hash))
330
+ cust_options = options[:customization_spec]
331
+ raise ArgumentError, "domain is required" unless cust_options.key?(:domain)
332
+ cust_ip_settings = nil
333
+ if cust_options.key?(:ipsettings) and cust_options[:ipsettings].key?(:ip)
334
+ raise ArgumentError, "ip and subnetMask is required for static ip" unless cust_options[:ipsettings].key?(:ip) and
335
+ cust_options[:ipsettings].key?(:subnetMask)
336
+ cust_ip_settings = RbVmomi::VIM::CustomizationIPSettings.new(cust_options[:ipsettings])
337
+ action_handler.report_progress "customizing #{vm_name} with static IP #{cust_options[:ipsettings][:ip]}"
338
+ cust_ip_settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cust_options[:ipsettings][:ip])
339
+ end
340
+ cust_domain = cust_options[:domain]
341
+ if cust_ip_settings.nil?
342
+ cust_ip_settings= RbVmomi::VIM::CustomizationIPSettings.new(:ip => RbVmomi::VIM::CustomizationDhcpIpGenerator.new())
343
+ cust_ip_settings.dnsServerList = cust_options[:ipsettings][:dnsServerList]
344
+ action_handler.report_progress "customizing #{vm_name} with dynamic IP and DNS: #{cust_options[:ipsettings][:dnsServerList]}"
345
+ end
346
+
347
+ cust_ip_settings.dnsDomain = cust_domain
348
+ cust_global_ip_settings = RbVmomi::VIM::CustomizationGlobalIPSettings.new
349
+ cust_global_ip_settings.dnsServerList = cust_ip_settings.dnsServerList
350
+ cust_global_ip_settings.dnsSuffixList = [cust_domain]
351
+ cust_hostname = hostname_from(options[:customization_spec], vm_name)
352
+ cust_hwclockutc = cust_options[:hw_clock_utc]
353
+ cust_timezone = cust_options[:time_zone]
354
+
355
+ if vm_template.config.guestId.start_with?('win')
356
+ cust_prep = windows_prep_for(action_handler, options, vm_name)
357
+ else
358
+ cust_prep = RbVmomi::VIM::CustomizationLinuxPrep.new(
359
+ :domain => cust_domain,
360
+ :hostName => cust_hostname,
361
+ :hwClockUTC => cust_hwclockutc,
362
+ :timeZone => cust_timezone)
363
+ end
364
+ cust_adapter_mapping = [RbVmomi::VIM::CustomizationAdapterMapping.new(:adapter => cust_ip_settings)]
365
+ RbVmomi::VIM::CustomizationSpec.new(
366
+ :identity => cust_prep,
367
+ :globalIPSettings => cust_global_ip_settings,
368
+ :nicSettingMap => cust_adapter_mapping)
369
+ else
370
+ find_customization_spec(options[:customization_spec])
371
+ end
372
+ end
373
+ end
374
+
375
+ def windows_prep_for(action_handler, options, vm_name)
376
+ cust_options = options[:customization_spec]
377
+ cust_runonce = RbVmomi::VIM::CustomizationGuiRunOnce.new(
378
+ :commandList => [
379
+ 'winrm set winrm/config/client/auth @{Basic="true"}',
380
+ 'winrm set winrm/config/service/auth @{Basic="true"}',
381
+ 'winrm set winrm/config/service @{AllowUnencrypted="true"}',
382
+ 'shutdown -l'])
383
+
384
+ cust_login_password = RbVmomi::VIM::CustomizationPassword(
385
+ :plainText => true,
386
+ :value => options[:ssh][:password])
387
+ if cust_options.has_key?(:domain) and cust_options[:domain] != 'local'
388
+ cust_domain_password = RbVmomi::VIM::CustomizationPassword(
389
+ :plainText => true,
390
+ :value => ENV['domainAdminPassword'] || cust_options[:domainAdminPassword])
391
+ cust_id = RbVmomi::VIM::CustomizationIdentification.new(
392
+ :joinDomain => cust_options[:domain],
393
+ :domainAdmin => cust_options[:domainAdmin],
394
+ :domainAdminPassword => cust_domain_password)
395
+ #puts "my env passwd is: #{ENV['domainAdminPassword']}"
396
+ action_handler.report_progress "joining domain #{cust_options[:domain]} with user: #{cust_options[:domainAdmin]}"
397
+ else
398
+ cust_id = RbVmomi::VIM::CustomizationIdentification.new(
399
+ :joinWorkgroup => 'WORKGROUP')
400
+ end
401
+ cust_gui_unattended = RbVmomi::VIM::CustomizationGuiUnattended.new(
402
+ :autoLogon => true,
403
+ :autoLogonCount => 1,
404
+ :password => cust_login_password,
405
+ :timeZone => cust_options[:win_time_zone])
406
+ cust_userdata = RbVmomi::VIM::CustomizationUserData.new(
407
+ :computerName => hostname_from(cust_options, vm_name),
408
+ :fullName => cust_options[:org_name],
409
+ :orgName => cust_options[:org_name],
410
+ :productId => cust_options[:product_id])
411
+ RbVmomi::VIM::CustomizationSysprep.new(
412
+ :guiRunOnce => cust_runonce,
413
+ :identification => cust_id,
414
+ :guiUnattended => cust_gui_unattended,
415
+ :userData => cust_userdata)
416
+ end
417
+
418
+ def hostname_from(options, vm_name)
419
+ if options.key?(:hostname)
420
+ RbVmomi::VIM::CustomizationFixedName.new(:name => options[:hostname])
421
+ else
422
+ RbVmomi::VIM::CustomizationFixedName.new(:name => vm_name)
423
+ end
424
+ end
425
+
426
+ def find_host(dc, host_name)
427
+ baseEntity = dc.hostFolder
428
+ entityArray = host_name.split('/')
429
+ entityArray.each do |entityArrItem|
430
+ if entityArrItem != ''
431
+ if baseEntity.is_a? RbVmomi::VIM::Folder
432
+ baseEntity = baseEntity.childEntity.find { |f| f.name == entityArrItem } or nil
433
+ elsif baseEntity.is_a? RbVmomi::VIM::ClusterComputeResource or baseEntity.is_a? RbVmomi::VIM::ComputeResource
434
+ baseEntity = baseEntity.host.find { |f| f.name == entityArrItem } or nil
435
+ elsif baseEntity.is_a? RbVmomi::VIM::HostSystem
436
+ baseEntity = baseEntity.host.find { |f| f.name == entityArrItem } or nil
437
+ else
438
+ baseEntity = nil
439
+ end
440
+ end
441
+ end
442
+
443
+ raise "vSphere Host not found [#{host_name}]" if baseEntity.nil?
444
+
445
+ baseEntity = baseEntity.host if not baseEntity.is_a?(RbVmomi::VIM::HostSystem) and baseEntity.respond_to?(:host)
446
+ baseEntity
447
+ end
448
+
449
+ def find_pool(dc, pool_name)
450
+ baseEntity = dc.hostFolder
451
+ entityArray = pool_name.split('/')
452
+ entityArray.each do |entityArrItem|
453
+ if entityArrItem != ''
454
+ if baseEntity.is_a? RbVmomi::VIM::Folder
455
+ baseEntity = baseEntity.childEntity.find { |f| f.name == entityArrItem } or nil
456
+ elsif baseEntity.is_a? RbVmomi::VIM::ClusterComputeResource or baseEntity.is_a? RbVmomi::VIM::ComputeResource
457
+ baseEntity = baseEntity.resourcePool.resourcePool.find { |f| f.name == entityArrItem } or nil
458
+ elsif baseEntity.is_a? RbVmomi::VIM::ResourcePool
459
+ baseEntity = baseEntity.resourcePool.find { |f| f.name == entityArrItem } or nil
460
+ else
461
+ baseEntity = nil
462
+ end
463
+ end
464
+ end
465
+
466
+ raise "vSphere ResourcePool not found [#{pool_name}]" if baseEntity.nil?
467
+
468
+ baseEntity = baseEntity.resourcePool if not baseEntity.is_a?(RbVmomi::VIM::ResourcePool) and baseEntity.respond_to?(:resourcePool)
469
+ baseEntity
470
+ end
471
+
472
+ def find_customization_spec(customization_spec)
473
+ csm = vim.serviceContent.customizationSpecManager
474
+ csi = csm.GetCustomizationSpec(:name => customization_spec)
475
+ spec = csi.spec
476
+ raise "Customization Spec not found [#{customization_spec}]" if spec.nil?
477
+ spec
478
+ end
479
+
480
+ def upload_file_to_vm(vm, username, password, local, remote)
481
+ auth = RbVmomi::VIM::NamePasswordAuthentication({:username => username, :password => password, :interactiveSession => false})
482
+ size = File.size(local)
483
+ endpoint = $guest_op_managers[vim.pretty_inspect].fileManager.InitiateFileTransferToGuest(
484
+ :vm => vm,
485
+ :auth => auth,
486
+ :guestFilePath => remote,
487
+ :overwrite => true,
488
+ :fileAttributes => RbVmomi::VIM::GuestWindowsFileAttributes.new,
489
+ :fileSize => size)
490
+
491
+ uri = URI.parse(endpoint)
492
+ http = Net::HTTP.new(uri.host, uri.port)
493
+ http.use_ssl = true
494
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
495
+
496
+ req = Net::HTTP::Put.new("#{uri.path}?#{uri.query}")
497
+ req.body_stream = File.open(local)
498
+ req["Content-Type"] = "application/octet-stream"
499
+ req["Content-Length"] = size
500
+ res = http.request(req)
501
+ unless res.kind_of?(Net::HTTPSuccess)
502
+ raise "Error: #{res.inspect} :: #{res.body} :: sending #{local} to #{remote} at #{vm.name} via #{endpoint} with a size of #{size}"
503
+ end
504
+ end
505
+ end
506
+ end