clc-chef-provisioning-vsphere 0.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.
@@ -0,0 +1,3 @@
1
+ module ChefProvisioningVsphere
2
+ VERSION = '0.4.0'
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