bosh_vsphere_cpi 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1085 @@
1
+ require "ruby_vim_sdk"
2
+ require "cloud/vsphere/client"
3
+ require "cloud/vsphere/lease_updater"
4
+ require "cloud/vsphere/resources"
5
+ require "cloud/vsphere/models/disk"
6
+
7
+ module VSphereCloud
8
+
9
+ class Cloud < Bosh::Cloud
10
+ include VimSdk
11
+
12
+ class TimeoutException < StandardError; end
13
+
14
+ attr_accessor :client
15
+
16
+ def initialize(options)
17
+ @vcenters = options["vcenters"]
18
+ raise "Invalid number of VCenters" unless @vcenters.size == 1
19
+ @vcenter = @vcenters[0]
20
+
21
+ @logger = Bosh::Clouds::Config.logger
22
+
23
+ @agent_properties = options["agent"]
24
+
25
+ @client = Client.new("https://#{@vcenter["host"]}/sdk/vimService", options)
26
+ @client.login(@vcenter["user"], @vcenter["password"], "en")
27
+
28
+ @rest_client = HTTPClient.new
29
+ @rest_client.send_timeout = 14400 # 4 hours
30
+ @rest_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
31
+
32
+ # HACK: read the session from the SOAP client so we don't leak sessions when using the REST client
33
+ cookie_str = @client.stub.cookie
34
+ @rest_client.cookie_manager.parse(cookie_str, URI.parse("https://#{@vcenter["host"]}"))
35
+
36
+ mem_ratio = 1.0
37
+ if options["mem_overcommit_ratio"]
38
+ mem_ratio = options["mem_overcommit_ratio"].to_f
39
+ end
40
+
41
+ @resources = Resources.new(@client, @vcenter, mem_ratio)
42
+
43
+ # HACK: provide a way to copy the disks instead of moving them.
44
+ # Used for extra data protection until we have proper backups
45
+ @copy_disks = options["copy_disks"] || false
46
+
47
+ @lock = Mutex.new
48
+ @locks = {}
49
+ @locks_mutex = Mutex.new
50
+
51
+ # We get disconnected if the connection is inactive for a long period.
52
+ Thread.new do
53
+ while true do
54
+ sleep(60)
55
+ @client.service_instance.current_time
56
+ end
57
+ end
58
+
59
+ # HACK: finalizer not getting called, so we'll rely on at_exit
60
+ at_exit { @client.logout }
61
+ end
62
+
63
+ def create_stemcell(image, _)
64
+ with_thread_name("create_stemcell(#{image}, _)") do
65
+ result = nil
66
+ Dir.mktmpdir do |temp_dir|
67
+ @logger.info("Extracting stemcell to: #{temp_dir}")
68
+ output = `tar -C #{temp_dir} -xzf #{image} 2>&1`
69
+ raise "Corrupt image, tar exit status: #{$?.exitstatus} output: #{output}" if $?.exitstatus != 0
70
+
71
+ ovf_file = Dir.entries(temp_dir).find { |entry| File.extname(entry) == ".ovf" }
72
+ raise "Missing OVF" if ovf_file.nil?
73
+ ovf_file = File.join(temp_dir, ovf_file)
74
+
75
+ name = "sc-#{generate_unique_name}"
76
+ @logger.info("Generated name: #{name}")
77
+
78
+ # TODO: make stemcell friendly version of the calls below
79
+ cluster, datastore = @resources.get_resources
80
+ @logger.info("Deploying to: #{cluster.mob} / #{datastore.mob}")
81
+
82
+ import_spec_result = import_ovf(name, ovf_file, cluster.resource_pool, datastore.mob)
83
+ lease = obtain_nfc_lease(cluster.resource_pool, import_spec_result.import_spec,
84
+ cluster.datacenter.template_folder)
85
+ @logger.info("Waiting for NFC lease")
86
+ state = wait_for_nfc_lease(lease)
87
+ raise "Could not acquire HTTP NFC lease" unless state == Vim::HttpNfcLease::State::READY
88
+
89
+ @logger.info("Uploading")
90
+ vm = upload_ovf(ovf_file, lease, import_spec_result.file_item)
91
+ result = name
92
+
93
+ @logger.info("Removing NICs")
94
+ devices = client.get_property(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
95
+ config = Vim::Vm::ConfigSpec.new
96
+ config.device_change = []
97
+
98
+ nics = devices.select { |device| device.kind_of?(Vim::Vm::Device::VirtualEthernetCard) }
99
+ nics.each do |nic|
100
+ nic_config = create_delete_device_spec(nic)
101
+ config.device_change << nic_config
102
+ end
103
+ client.reconfig_vm(vm, config)
104
+
105
+ @logger.info("Taking initial snapshot")
106
+ task = take_snapshot(vm, "initial")
107
+ client.wait_for_task(task)
108
+ end
109
+ result
110
+ end
111
+ end
112
+
113
+ def delete_stemcell(stemcell)
114
+ with_thread_name("delete_stemcell(#{stemcell})") do
115
+ Bosh::ThreadPool.new(:max_threads => 32, :logger => @logger).wrap do |pool|
116
+ @resources.datacenters.each_value do |datacenter|
117
+ @logger.info("Looking for stemcell replicas in: #{datacenter.name}")
118
+ templates = client.get_property(datacenter.template_folder, Vim::Folder, "childEntity", :ensure_all => true)
119
+ template_properties = client.get_properties(templates, Vim::VirtualMachine, ["name"])
120
+ template_properties.each_value do |properties|
121
+ template_name = properties["name"].gsub("%2f", "/")
122
+ if template_name.split("/").first.strip == stemcell
123
+ @logger.info("Found: #{template_name}")
124
+ pool.process do
125
+ @logger.info("Deleting: #{template_name}")
126
+ client.delete_vm(properties[:obj])
127
+ @logger.info("Deleted: #{template_name}")
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def disk_spec(vm_disk, persistent_disks)
137
+ disks = []
138
+ disks << {"size" => vm_disk, "persistent" => false}
139
+
140
+ persistent_disks ||= {}
141
+ persistent_disks.each do |disk_id|
142
+ disk = Models::Disk[disk_id]
143
+ disks << {"size" => disk.size, "persistent" => true, "datacenter" => disk.datacenter, "datastore" => disk.datastore}
144
+ end
145
+ disks
146
+ end
147
+
148
+ def create_vm(agent_id, stemcell, resource_pool, networks, disk_locality = nil, environment = nil)
149
+ with_thread_name("create_vm(#{agent_id}, ...)") do
150
+ memory = resource_pool["ram"]
151
+ disk = resource_pool["disk"]
152
+ cpu = resource_pool["cpu"]
153
+
154
+ disks = disk_spec(disk, disk_locality)
155
+ cluster, datastore = @resources.get_resources(memory, disks)
156
+
157
+ name = "vm-#{generate_unique_name}"
158
+ @logger.info("Creating vm:: #{name} on #{cluster.mob} stored in #{datastore.mob}")
159
+
160
+ replicated_stemcell_vm = replicate_stemcell(cluster, datastore, stemcell)
161
+ replicated_stemcell_properties = client.get_properties(replicated_stemcell_vm, Vim::VirtualMachine,
162
+ ["config.hardware.device", "snapshot"],
163
+ :ensure_all => true)
164
+
165
+ devices = replicated_stemcell_properties["config.hardware.device"]
166
+ snapshot = replicated_stemcell_properties["snapshot"]
167
+
168
+ config = Vim::Vm::ConfigSpec.new(:memory_mb => memory, :num_cpus => cpu)
169
+ config.device_change = []
170
+
171
+ system_disk = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualDisk) }
172
+ pci_controller = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualPCIController) }
173
+
174
+ file_name = "[#{datastore.name}] #{name}/ephemeral_disk.vmdk"
175
+ ephemeral_disk_config = create_disk_config_spec(datastore.mob, file_name, system_disk.controller_key, disk,
176
+ :create => true)
177
+ config.device_change << ephemeral_disk_config
178
+
179
+ dvs_index = {}
180
+ networks.each_value do |network|
181
+ v_network_name = network["cloud_properties"]["name"]
182
+ network_mob = client.find_by_inventory_path([cluster.datacenter.name, "network", v_network_name])
183
+ nic_config = create_nic_config_spec(v_network_name, network_mob, pci_controller.key, dvs_index)
184
+ config.device_change << nic_config
185
+ end
186
+
187
+ nics = devices.select { |device| device.kind_of?(Vim::Vm::Device::VirtualEthernetCard) }
188
+ nics.each do |nic|
189
+ nic_config = create_delete_device_spec(nic)
190
+ config.device_change << nic_config
191
+ end
192
+
193
+ fix_device_unit_numbers(devices, config.device_change)
194
+
195
+ @logger.info("Cloning vm: #{replicated_stemcell_vm} to #{name}")
196
+
197
+ task = clone_vm(replicated_stemcell_vm, name, cluster.datacenter.vm_folder, cluster.resource_pool,
198
+ :datastore => datastore.mob, :linked => true, :snapshot => snapshot.current_snapshot,
199
+ :config => config)
200
+ vm = client.wait_for_task(task)
201
+
202
+ begin
203
+ upload_file(cluster.datacenter.name, datastore.name, "#{name}/env.iso", "")
204
+
205
+ vm_properties = client.get_properties(vm, Vim::VirtualMachine, ["config.hardware.device"], :ensure_all => true)
206
+ devices = vm_properties["config.hardware.device"]
207
+
208
+ # Configure the ENV CDROM
209
+ config = Vim::Vm::ConfigSpec.new
210
+ config.device_change = []
211
+ file_name = "[#{datastore.name}] #{name}/env.iso"
212
+ cdrom_change = configure_env_cdrom(datastore.mob, devices, file_name)
213
+ config.device_change << cdrom_change
214
+ client.reconfig_vm(vm, config)
215
+
216
+ network_env = generate_network_env(devices, networks, dvs_index)
217
+ disk_env = generate_disk_env(system_disk, ephemeral_disk_config.device)
218
+ env = generate_agent_env(name, vm, agent_id, network_env, disk_env)
219
+ env["env"] = environment
220
+ @logger.info("Setting VM env: #{env.pretty_inspect}")
221
+
222
+ location = get_vm_location(vm, :datacenter => cluster.datacenter.name,
223
+ :datastore => datastore.name,
224
+ :vm => name)
225
+ set_agent_env(vm, location, env)
226
+
227
+ @logger.info("Powering on VM: #{vm} (#{name})")
228
+ client.power_on_vm(cluster.datacenter.mob, vm)
229
+ rescue => e
230
+ @logger.info("#{e} - #{e.backtrace.join("\n")}")
231
+ delete_vm(name)
232
+ raise e
233
+ end
234
+ name
235
+ end
236
+ end
237
+
238
+ def retry_block(num = 2)
239
+ result = nil
240
+ num.times do |i|
241
+ begin
242
+ result = yield
243
+ break
244
+ rescue RuntimeError
245
+ raise if i + 1 >= num
246
+ end
247
+ end
248
+ result
249
+ end
250
+
251
+ def delete_vm(vm_cid)
252
+ with_thread_name("delete_vm(#{vm_cid})") do
253
+ @logger.info("Deleting vm: #{vm_cid}")
254
+
255
+ vm = get_vm_by_cid(vm_cid)
256
+ datacenter = client.find_parent(vm, Vim::Datacenter)
257
+ properties = client.get_properties(vm, Vim::VirtualMachine, ["runtime.powerState", "runtime.question",
258
+ "config.hardware.device", "name"],
259
+ :ensure => ["config.hardware.device"])
260
+
261
+ retry_block do
262
+ question = properties["runtime.question"]
263
+ if question
264
+ choices = question.choice
265
+ @logger.info("VM is blocked on a question: #{question.text}, " +
266
+ "providing default answer: #{choices.choice_info[choices.default_index].label}")
267
+ client.answer_vm(vm, question.id, choices.choice_info[choices.default_index].key)
268
+ power_state = client.get_property(vm, Vim::VirtualMachine, "runtime.powerState")
269
+ else
270
+ power_state = properties["runtime.powerState"]
271
+ end
272
+
273
+ if power_state != Vim::VirtualMachine::PowerState::POWERED_OFF
274
+ @logger.info("Powering off vm: #{vm_cid}")
275
+ client.power_off_vm(vm)
276
+ end
277
+ end
278
+
279
+ # Detach any persistent disks in case they were not detached from the instance
280
+ devices = properties["config.hardware.device"]
281
+ persistent_disks = devices.select { |device| device.kind_of?(Vim::Vm::Device::VirtualDisk) &&
282
+ device.backing.disk_mode == Vim::Vm::Device::VirtualDiskOption::DiskMode::INDEPENDENT_PERSISTENT }
283
+
284
+ unless persistent_disks.empty?
285
+ @logger.info("Found #{persistent_disks.size} persistent disk(s)")
286
+ config = Vim::Vm::ConfigSpec.new
287
+ config.device_change = []
288
+ persistent_disks.each do |virtual_disk|
289
+ @logger.info("Detaching: #{virtual_disk.backing.file_name}")
290
+ config.device_change << create_delete_device_spec(virtual_disk)
291
+ end
292
+ retry_block { client.reconfig_vm(vm, config) }
293
+ @logger.info("Detached #{persistent_disks.size} persistent disk(s)")
294
+ end
295
+
296
+ retry_block { client.delete_vm(vm) }
297
+ @logger.info("Deleted vm: #{vm_cid}")
298
+
299
+ # Delete env.iso and VM specific files managed by the director
300
+ retry_block do
301
+ datastore = get_primary_datastore(devices)
302
+ datastore_name = client.get_property(datastore, Vim::Datastore, "name")
303
+ vm_name = properties["name"]
304
+ client.delete_path(datacenter, "[#{datastore_name}] #{vm_name}")
305
+ end
306
+ end
307
+ end
308
+
309
+ # TODO add option to force hard/soft reboot
310
+ def reboot_vm(vm_cid)
311
+ with_thread_name("reboot_vm(#{vm_cid})") do
312
+ vm = get_vm_by_cid(vm_cid)
313
+ datacenter = client.find_parent(vm, Vim::Datacenter)
314
+ power_state = client.get_property(vm, Vim::VirtualMachine, "runtime.powerState")
315
+
316
+ @logger.info("Reboot vm = #{vm_cid}")
317
+ if power_state != Vim::VirtualMachine::PowerState::POWERED_ON
318
+ @logger.info("VM not in POWERED_ON state. Current state : #{power_state}")
319
+ end
320
+ begin
321
+ vm.reboot_guest
322
+ rescue => e
323
+ @logger.error("Soft reboot failed #{e} -#{e.backtrace.join("\n")}")
324
+ @logger.info("Try hard reboot")
325
+ # if we fail to perform a soft-reboot we force a hard-reboot
326
+ if power_state == Vim::VirtualMachine::PowerState::POWERED_ON
327
+ retry_block { client.power_off_vm(vm) }
328
+ end
329
+ retry_block { client.power_on_vm(datacenter, vm) }
330
+ end
331
+ end
332
+ end
333
+
334
+ def configure_networks(vm_cid, networks)
335
+ with_thread_name("configure_networks(#{vm_cid}, ...)") do
336
+ vm = get_vm_by_cid(vm_cid)
337
+
338
+ @logger.debug("Waiting for the VM to shutdown")
339
+ state = :initial
340
+ begin
341
+ wait_until_off(vm, 30)
342
+ rescue TimeoutException
343
+ case state
344
+ when :initial
345
+ @logger.debug("The guest did not shutdown in time, requesting it to shutdown")
346
+ begin
347
+ vm.shutdown_guest
348
+ rescue => e
349
+ @logger.debug("Ignoring possible race condition when a VM has " +
350
+ "powered off by the time we ask it to shutdown: #{e.message}")
351
+ end
352
+ state = :shutdown_guest
353
+ retry
354
+ else
355
+ @logger.error("The guest did not shutdown in time, even after a request")
356
+ raise
357
+ end
358
+ end
359
+
360
+ @logger.info("Configuring: #{vm_cid} to use the following network settings: #{networks.pretty_inspect}")
361
+ vm = get_vm_by_cid(vm_cid)
362
+ devices = client.get_property(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
363
+ datacenter = client.find_parent(vm, Vim::Datacenter)
364
+ datacenter_name = client.get_property(datacenter, Vim::Datacenter, "name")
365
+ pci_controller = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualPCIController) }
366
+
367
+ config = Vim::Vm::ConfigSpec.new
368
+ config.device_change = []
369
+ nics = devices.select { |device| device.kind_of?(Vim::Vm::Device::VirtualEthernetCard) }
370
+ nics.each do |nic|
371
+ nic_config = create_delete_device_spec(nic)
372
+ config.device_change << nic_config
373
+ end
374
+
375
+ dvs_index = {}
376
+ networks.each_value do |network|
377
+ v_network_name = network["cloud_properties"]["name"]
378
+ network_mob = client.find_by_inventory_path([datacenter_name, "network", v_network_name])
379
+ nic_config = create_nic_config_spec(v_network_name, network_mob, pci_controller.key, dvs_index)
380
+ config.device_change << nic_config
381
+ end
382
+
383
+ fix_device_unit_numbers(devices, config.device_change)
384
+ @logger.debug("Reconfiguring the networks")
385
+ @client.reconfig_vm(vm, config)
386
+
387
+ location = get_vm_location(vm, :datacenter => datacenter_name)
388
+ env = get_current_agent_env(location)
389
+ @logger.debug("Reading current agent env: #{env.pretty_inspect}")
390
+
391
+ devices = client.get_property(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
392
+ env["networks"] = generate_network_env(devices, networks, dvs_index)
393
+
394
+ @logger.debug("Updating agent env to: #{env.pretty_inspect}")
395
+ set_agent_env(vm, location, env)
396
+
397
+ @logger.debug("Powering the VM back on")
398
+ client.power_on_vm(datacenter, vm)
399
+ end
400
+ end
401
+
402
+ def get_vm_host_info(vm_ref)
403
+ vm = @client.get_properties(vm_ref, Vim::VirtualMachine, "runtime")
404
+ vm_runtime = vm["runtime"]
405
+
406
+ properties = @client.get_properties(vm_runtime.host, Vim::HostSystem, ["datastore", "parent"],
407
+ :ensure_all => true)
408
+
409
+ # Get the cluster that the vm's host belongs to.
410
+ cluster = @client.get_properties(properties["parent"], Vim::ClusterComputeResource, "name")
411
+
412
+ # Get the datastores that are accessible to the vm's host.
413
+ datastores_accessible = []
414
+ properties["datastore"].each { |store|
415
+ ds = @client.get_properties(store, Vim::Datastore, "info", :ensure_all => true)
416
+ datastores_accessible << ds["info"].name
417
+ }
418
+
419
+ {"cluster" => cluster["name"], "datastores" => datastores_accessible}
420
+ end
421
+
422
+ def find_persistent_datastore(datacenter_name, host_info, disk_size)
423
+ # Find datastore
424
+ datastore = @resources.find_persistent_datastore(datacenter_name, host_info["cluster"], disk_size)
425
+
426
+ if datastore.nil?
427
+ raise Bosh::Clouds::NoDiskSpace.new(true), "Not enough persistent space on cluster #{host_info["cluster"]}, #{disk_size}"
428
+ end
429
+
430
+ # Sanity check, verify that the vm's host can access this datastore
431
+ unless host_info["datastores"].include?(datastore.name)
432
+ raise "Datastore not accessible to host, #{datastore.name}, #{host_info["datastores"]}"
433
+ end
434
+ datastore
435
+ end
436
+
437
+ def attach_disk(vm_cid, disk_cid)
438
+ with_thread_name("attach_disk(#{vm_cid}, #{disk_cid})") do
439
+ @logger.info("Attaching disk: #{disk_cid} on vm: #{vm_cid}")
440
+ disk = Models::Disk[disk_cid]
441
+ raise "Disk not found: #{disk_cid}" if disk.nil?
442
+
443
+ vm = get_vm_by_cid(vm_cid)
444
+
445
+ datacenter = client.find_parent(vm, Vim::Datacenter)
446
+ datacenter_name = client.get_property(datacenter, Vim::Datacenter, "name")
447
+
448
+ vm_properties = client.get_properties(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
449
+ host_info = get_vm_host_info(vm)
450
+ persistent_datastore = nil
451
+
452
+ create_disk = false
453
+ if disk.path
454
+ if disk.datacenter == datacenter_name &&
455
+ @resources.validate_persistent_datastore(datacenter_name, disk.datastore) &&
456
+ host_info["datastores"].include?(disk.datastore)
457
+ # Looks like we have a valid persistent data store
458
+
459
+ @logger.info("Disk already in the right datastore #{datacenter_name} #{disk.datastore}")
460
+ persistent_datastore = @resources.get_persistent_datastore(datacenter_name, host_info["cluster"],
461
+ disk.datastore)
462
+ else
463
+ @logger.info("Disk needs to move from #{datacenter_name} #{disk.datastore}")
464
+
465
+ # Find the destination datastore
466
+ persistent_datastore = find_persistent_datastore(datacenter_name, host_info, disk.size)
467
+
468
+ # Need to move disk to right datastore
469
+ source_datacenter = client.find_by_inventory_path(disk.datacenter)
470
+ source_path = disk.path
471
+ datacenter_disk_path = @resources.datacenters[disk.datacenter].disk_path
472
+
473
+ destination_path = "[#{persistent_datastore.name}] #{datacenter_disk_path}/#{disk.id}"
474
+ @logger.info("Moving #{disk.datacenter}/#{source_path} to #{datacenter_name}/#{destination_path}")
475
+
476
+ if @copy_disks
477
+ client.copy_disk(source_datacenter, source_path, datacenter, destination_path)
478
+ @logger.info("Copied disk successfully")
479
+ else
480
+ client.move_disk(source_datacenter, source_path, datacenter, destination_path)
481
+ @logger.info("Moved disk successfully")
482
+ end
483
+
484
+ disk.datacenter = datacenter_name
485
+ disk.datastore = persistent_datastore.name
486
+ disk.path = destination_path
487
+ disk.save
488
+ end
489
+ else
490
+ @logger.info("Need to create disk")
491
+
492
+ # Find the destination datastore
493
+ persistent_datastore = find_persistent_datastore(datacenter_name, host_info, disk.size)
494
+
495
+ # Need to create disk
496
+ disk.datacenter = datacenter_name
497
+ disk.datastore = persistent_datastore.name
498
+ datacenter_disk_path = @resources.datacenters[disk.datacenter].disk_path
499
+ disk.path = "[#{disk.datastore}] #{datacenter_disk_path}/#{disk.id}"
500
+ disk.save
501
+ create_disk = true
502
+ end
503
+
504
+ devices = vm_properties["config.hardware.device"]
505
+ system_disk = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualDisk) }
506
+
507
+ vmdk_path = "#{disk.path}.vmdk"
508
+ attached_disk_config = create_disk_config_spec(persistent_datastore.mob, vmdk_path,
509
+ system_disk.controller_key, disk.size.to_i,
510
+ :create => create_disk, :independent => true)
511
+ config = Vim::Vm::ConfigSpec.new
512
+ config.device_change = []
513
+ config.device_change << attached_disk_config
514
+ fix_device_unit_numbers(devices, config.device_change)
515
+
516
+ location = get_vm_location(vm, :datacenter => datacenter_name)
517
+ env = get_current_agent_env(location)
518
+ @logger.info("Reading current agent env: #{env.pretty_inspect}")
519
+ env["disks"]["persistent"][disk.id.to_s] = attached_disk_config.device.unit_number
520
+ @logger.info("Updating agent env to: #{env.pretty_inspect}")
521
+ set_agent_env(vm, location, env)
522
+ @logger.info("Attaching disk")
523
+ client.reconfig_vm(vm, config)
524
+ @logger.info("Finished attaching disk")
525
+ end
526
+ end
527
+
528
+ def detach_disk(vm_cid, disk_cid)
529
+ with_thread_name("detach_disk(#{vm_cid}, #{disk_cid})") do
530
+ @logger.info("Detaching disk: #{disk_cid} from vm: #{vm_cid}")
531
+ disk = Models::Disk[disk_cid]
532
+ raise "Disk not found: #{disk_cid}" if disk.nil?
533
+
534
+ vm = get_vm_by_cid(vm_cid)
535
+
536
+ devices = client.get_property(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
537
+
538
+ vmdk_path = "#{disk.path}.vmdk"
539
+ virtual_disk = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualDisk) &&
540
+ device.backing.file_name == vmdk_path }
541
+ raise Bosh::Clouds::DiskNotAttached.new(true), "Disk (#{disk_cid}) is not attached to VM (#{vm_cid})" if virtual_disk.nil?
542
+
543
+ config = Vim::Vm::ConfigSpec.new
544
+ config.device_change = []
545
+ config.device_change << create_delete_device_spec(virtual_disk)
546
+
547
+ location = get_vm_location(vm)
548
+ env = get_current_agent_env(location)
549
+ @logger.info("Reading current agent env: #{env.pretty_inspect}")
550
+ env["disks"]["persistent"].delete(disk.id.to_s)
551
+ @logger.info("Updating agent env to: #{env.pretty_inspect}")
552
+ set_agent_env(vm, location, env)
553
+ @logger.info("Detaching disk")
554
+ client.reconfig_vm(vm, config)
555
+
556
+ # detach-disk is async and task completion does not necessarily mean
557
+ # that changes have been applied to VC side. Query VC until we confirm
558
+ # that the change has been applied. This is a known issue for vsphere 4.
559
+ # Fixed in vsphere 5.
560
+ 5.times do
561
+ devices = client.get_property(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
562
+ virtual_disk = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualDisk) &&
563
+ device.backing.file_name == vmdk_path }
564
+ break if virtual_disk.nil?
565
+ sleep(1.0)
566
+ end
567
+ raise "Failed to detach disk: #{disk_cid} from vm: #{vm_cid}" unless virtual_disk.nil?
568
+
569
+ @logger.info("Finished detaching disk")
570
+ end
571
+ end
572
+
573
+ def create_disk(size, _ = nil)
574
+ with_thread_name("create_disk(#{size}, _)") do
575
+ @logger.info("Creating disk with size: #{size}")
576
+ disk = Models::Disk.new
577
+ disk.size = size
578
+ disk.save
579
+ @logger.info("Created disk: #{disk.pretty_inspect}")
580
+ disk.id.to_s
581
+ end
582
+ end
583
+
584
+ def delete_disk(disk_cid)
585
+ with_thread_name("delete_disk(#{disk_cid})") do
586
+ @logger.info("Deleting disk: #{disk_cid}")
587
+ disk = Models::Disk[disk_cid]
588
+ if disk
589
+ if disk.path
590
+ datacenter = client.find_by_inventory_path(disk.datacenter)
591
+ raise Bosh::Clouds::DiskNotFound.new(true), "disk #{disk_cid} not found" if datacenter.nil? || disk.path.nil?
592
+
593
+ client.delete_disk(datacenter, disk.path)
594
+ end
595
+ disk.destroy
596
+ @logger.info("Finished deleting disk")
597
+ else
598
+ raise "Could not find disk: #{disk_cid}"
599
+ end
600
+ end
601
+ end
602
+
603
+ def validate_deployment(old_manifest, new_manifest)
604
+ # TODO: still needed? what does it verify? cloud properties? should be replaced by normalize cloud properties?
605
+ end
606
+
607
+ def get_vm_by_cid(vm_cid)
608
+ # TODO: fix when we go to multiple DCs
609
+ datacenter = @resources.datacenters.values.first
610
+ vm = client.find_by_inventory_path([datacenter.name, "vm", datacenter.vm_folder_name, vm_cid])
611
+ raise Bosh::Clouds::VMNotFound, "VM `#{vm_cid}' not found" if vm.nil?
612
+ vm
613
+ end
614
+
615
+ def replicate_stemcell(cluster, datastore, stemcell)
616
+ stemcell_vm = client.find_by_inventory_path([cluster.datacenter.name, "vm",
617
+ cluster.datacenter.template_folder_name, stemcell])
618
+ raise "Could not find stemcell: #{stemcell}" if stemcell_vm.nil?
619
+ stemcell_datastore = client.get_property(stemcell_vm, Vim::VirtualMachine, "datastore", :ensure_all => true)
620
+
621
+ if stemcell_datastore != datastore.mob
622
+ @logger.info("Stemcell lives on a different datastore, looking for a local copy of: #{stemcell}.")
623
+ local_stemcell_name = "#{stemcell} / #{datastore.mob.__mo_id__}"
624
+ local_stemcell_path = [cluster.datacenter.name, "vm", cluster.datacenter.template_folder_name,
625
+ local_stemcell_name]
626
+ replicated_stemcell_vm = client.find_by_inventory_path(local_stemcell_path)
627
+
628
+ if replicated_stemcell_vm.nil?
629
+ @logger.info("Cluster doesn't have stemcell #{stemcell}, replicating")
630
+ lock = nil
631
+ @locks_mutex.synchronize do
632
+ lock = @locks[local_stemcell_name]
633
+ if lock.nil?
634
+ lock = @locks[local_stemcell_name] = Mutex.new
635
+ end
636
+ end
637
+
638
+ lock.synchronize do
639
+ replicated_stemcell_vm = client.find_by_inventory_path(local_stemcell_path)
640
+ if replicated_stemcell_vm.nil?
641
+ @logger.info("Replicating #{stemcell} (#{stemcell_vm}) to #{local_stemcell_name}")
642
+ task = clone_vm(stemcell_vm, local_stemcell_name, cluster.datacenter.template_folder,
643
+ cluster.resource_pool, :datastore => datastore.mob)
644
+ replicated_stemcell_vm = client.wait_for_task(task)
645
+ @logger.info("Replicated #{stemcell} (#{stemcell_vm}) to " +
646
+ "#{local_stemcell_name} (#{replicated_stemcell_vm})")
647
+ @logger.info("Creating initial snapshot for linked clones on #{replicated_stemcell_vm}")
648
+ task = take_snapshot(replicated_stemcell_vm, "initial")
649
+ client.wait_for_task(task)
650
+ @logger.info("Created initial snapshot for linked clones on #{replicated_stemcell_vm}")
651
+ end
652
+ end
653
+ else
654
+ @logger.info("Found local stemcell replica: #{replicated_stemcell_vm}")
655
+ end
656
+ result = replicated_stemcell_vm
657
+ else
658
+ @logger.info("Stemcell was already local: #{stemcell_vm}")
659
+ result = stemcell_vm
660
+ end
661
+
662
+ @logger.info("Using stemcell VM: #{result}")
663
+
664
+ result
665
+ end
666
+
667
+ def generate_network_env(devices, networks, dvs_index)
668
+ nics = {}
669
+
670
+ devices.each do |device|
671
+ if device.kind_of?(Vim::Vm::Device::VirtualEthernetCard)
672
+ backing = device.backing
673
+ if backing.kind_of?(Vim::Vm::Device::VirtualEthernetCard::DistributedVirtualPortBackingInfo)
674
+ v_network_name = dvs_index[device.backing.port.portgroup_key]
675
+ else
676
+ v_network_name = device.backing.device_name
677
+ end
678
+ allocated_networks = nics[v_network_name] || []
679
+ allocated_networks << device
680
+ nics[v_network_name] = allocated_networks
681
+ end
682
+ end
683
+
684
+ network_env = {}
685
+ networks.each do |network_name, network|
686
+ network_entry = network.dup
687
+ v_network_name = network["cloud_properties"]["name"]
688
+ nic = nics[v_network_name].pop
689
+ network_entry["mac"] = nic.mac_address
690
+ network_env[network_name] = network_entry
691
+ end
692
+ network_env
693
+ end
694
+
695
+ def generate_disk_env(system_disk, ephemeral_disk)
696
+ {
697
+ "system" => system_disk.unit_number,
698
+ "ephemeral" => ephemeral_disk.unit_number,
699
+ "persistent" => {}
700
+ }
701
+ end
702
+
703
+ def generate_agent_env(name, vm, agent_id, networking_env, disk_env)
704
+ vm_env = {
705
+ "name" => name,
706
+ "id" => vm.__mo_id__
707
+ }
708
+
709
+ env = {}
710
+ env["vm"] = vm_env
711
+ env["agent_id"] = agent_id
712
+ env["networks"] = networking_env
713
+ env["disks"] = disk_env
714
+ env.merge!(@agent_properties)
715
+ env
716
+ end
717
+
718
+ def get_vm_location(vm, options = {})
719
+ datacenter_name = options[:datacenter]
720
+ datastore_name = options[:datastore]
721
+ vm_name = options[:vm]
722
+
723
+ unless datacenter_name
724
+ datacenter = client.find_parent(vm, Vim::Datacenter)
725
+ datacenter_name = client.get_property(datacenter, Vim::Datacenter, "name")
726
+ end
727
+
728
+ if vm_name.nil? || datastore_name.nil?
729
+ vm_properties = client.get_properties(vm, Vim::VirtualMachine, ["config.hardware.device", "name"],
730
+ :ensure_all => true)
731
+ vm_name = vm_properties["name"]
732
+
733
+ unless datastore_name
734
+ devices = vm_properties["config.hardware.device"]
735
+ datastore = get_primary_datastore(devices)
736
+ datastore_name = client.get_property(datastore, Vim::Datastore, "name")
737
+ end
738
+ end
739
+
740
+ {:datacenter => datacenter_name, :datastore =>datastore_name, :vm =>vm_name}
741
+ end
742
+
743
+ def get_primary_datastore(devices)
744
+ ephemeral_disks = devices.select { |device| device.kind_of?(Vim::Vm::Device::VirtualDisk) &&
745
+ device.backing.disk_mode != Vim::Vm::Device::VirtualDiskOption::DiskMode::INDEPENDENT_PERSISTENT }
746
+
747
+ datastore = nil
748
+ ephemeral_disks.each do |disk|
749
+ if datastore
750
+ raise "Ephemeral disks should all be on the same datastore." unless datastore.eql?(disk.backing.datastore)
751
+ else
752
+ datastore = disk.backing.datastore
753
+ end
754
+ end
755
+
756
+ datastore
757
+ end
758
+
759
+ def get_current_agent_env(location)
760
+ contents = fetch_file(location[:datacenter], location[:datastore], "#{location[:vm]}/env.json")
761
+ contents ? Yajl::Parser.parse(contents) : nil
762
+ end
763
+
764
+ def set_agent_env(vm, location, env)
765
+ env_json = Yajl::Encoder.encode(env)
766
+
767
+ connect_cdrom(vm, false)
768
+ upload_file(location[:datacenter], location[:datastore], "#{location[:vm]}/env.json", env_json)
769
+ upload_file(location[:datacenter], location[:datastore], "#{location[:vm]}/env.iso", generate_env_iso(env_json))
770
+ connect_cdrom(vm, true)
771
+ end
772
+
773
+ def connect_cdrom(vm, connected = true)
774
+ devices = client.get_property(vm, Vim::VirtualMachine, "config.hardware.device", :ensure_all => true)
775
+ cdrom = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualCdrom) }
776
+
777
+ if cdrom.connectable.connected != connected
778
+ cdrom.connectable.connected = connected
779
+ config = Vim::Vm::ConfigSpec.new
780
+ config.device_change = [create_edit_device_spec(cdrom)]
781
+ client.reconfig_vm(vm, config)
782
+ end
783
+ end
784
+
785
+ def configure_env_cdrom(datastore, devices, file_name)
786
+ backing_info = Vim::Vm::Device::VirtualCdrom::IsoBackingInfo.new
787
+ backing_info.datastore = datastore
788
+ backing_info.file_name = file_name
789
+
790
+ connect_info = Vim::Vm::Device::VirtualDevice::ConnectInfo.new
791
+ connect_info.allow_guest_control = false
792
+ connect_info.start_connected = true
793
+ connect_info.connected = true
794
+
795
+ cdrom = devices.find { |device| device.kind_of?(Vim::Vm::Device::VirtualCdrom) }
796
+ cdrom.connectable = connect_info
797
+ cdrom.backing = backing_info
798
+
799
+ create_edit_device_spec(cdrom)
800
+ end
801
+
802
+ def which(programs)
803
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
804
+ programs.each do |bin|
805
+ exe = File.join(path, bin)
806
+ return exe if File.exists?(exe)
807
+ end
808
+ end
809
+ programs.first
810
+ end
811
+
812
+ def genisoimage
813
+ @genisoimage ||= which(%w{genisoimage mkisofs})
814
+ end
815
+
816
+ def generate_env_iso(env)
817
+ Dir.mktmpdir do |path|
818
+ env_path = File.join(path, "env")
819
+ iso_path = File.join(path, "env.iso")
820
+ File.open(env_path, "w") { |f| f.write(env) }
821
+ output = `#{genisoimage} -o #{iso_path} #{env_path} 2>&1`
822
+ raise "#{$?.exitstatus} -#{output}" if $?.exitstatus != 0
823
+ File.open(iso_path, "r") { |f| f.read }
824
+ end
825
+ end
826
+
827
+ def fetch_file(datacenter_name, datastore_name, path)
828
+ retry_block do
829
+ url = "https://#{@vcenter["host"]}/folder/#{path}?dcPath=#{URI.escape(datacenter_name)}" +
830
+ "&dsName=#{URI.escape(datastore_name)}"
831
+
832
+ response = @rest_client.get(url)
833
+
834
+ if response.code < 400
835
+ response.body
836
+ elsif response.code == 404
837
+ nil
838
+ else
839
+ raise "Could not fetch file: #{url}, status code: #{response.code}"
840
+ end
841
+ end
842
+ end
843
+
844
+ def upload_file(datacenter_name, datastore_name, path, contents)
845
+ retry_block do
846
+ url = "https://#{@vcenter["host"]}/folder/#{path}?dcPath=#{URI.escape(datacenter_name)}" +
847
+ "&dsName=#{URI.escape(datastore_name)}"
848
+ response = @rest_client.put(url, contents, {"Content-Type" => "application/octet-stream",
849
+ "Content-Length" => contents.length})
850
+
851
+ raise "Could not upload file: #{url}, status code: #{response.code}" unless response.code < 400
852
+ end
853
+ end
854
+
855
+ def clone_vm(vm, name, folder, resource_pool, options={})
856
+ relocation_spec =Vim::Vm::RelocateSpec.new
857
+ relocation_spec.datastore = options[:datastore] if options[:datastore]
858
+ if options[:linked]
859
+ relocation_spec.disk_move_type = Vim::Vm::RelocateSpec::DiskMoveOptions::CREATE_NEW_CHILD_DISK_BACKING
860
+ end
861
+ relocation_spec.pool = resource_pool
862
+
863
+ clone_spec = Vim::Vm::CloneSpec.new
864
+ clone_spec.config = options[:config] if options[:config]
865
+ clone_spec.location = relocation_spec
866
+ clone_spec.power_on = options[:power_on] ? true : false
867
+ clone_spec.snapshot = options[:snapshot] if options[:snapshot]
868
+ clone_spec.template = false
869
+
870
+ vm.clone(folder, name, clone_spec)
871
+ end
872
+
873
+ def take_snapshot(vm, name)
874
+ vm.create_snapshot(name, nil, false, false)
875
+ end
876
+
877
+ def generate_unique_name
878
+ UUIDTools::UUID.random_create.to_s
879
+ end
880
+
881
+ def create_disk_config_spec(datastore, file_name, controller_key, space, options = {})
882
+ backing_info = Vim::Vm::Device::VirtualDisk::FlatVer2BackingInfo.new
883
+ backing_info.datastore = datastore
884
+ if options[:independent]
885
+ backing_info.disk_mode = Vim::Vm::Device::VirtualDiskOption::DiskMode::INDEPENDENT_PERSISTENT
886
+ else
887
+ backing_info.disk_mode = Vim::Vm::Device::VirtualDiskOption::DiskMode::PERSISTENT
888
+ end
889
+ backing_info.file_name = file_name
890
+
891
+ virtual_disk = Vim::Vm::Device::VirtualDisk.new
892
+ virtual_disk.key = -1
893
+ virtual_disk.controller_key = controller_key
894
+ virtual_disk.backing = backing_info
895
+ virtual_disk.capacity_in_kb = space * 1024
896
+
897
+ device_config_spec = Vim::Vm::Device::VirtualDeviceSpec.new
898
+ device_config_spec.device = virtual_disk
899
+ device_config_spec.operation = Vim::Vm::Device::VirtualDeviceSpec::Operation::ADD
900
+ if options[:create]
901
+ device_config_spec.file_operation = Vim::Vm::Device::VirtualDeviceSpec::FileOperation::CREATE
902
+ end
903
+ device_config_spec
904
+ end
905
+
906
+ def create_nic_config_spec(v_network_name, network, controller_key, dvs_index)
907
+ raise "Can't find network: #{v_network_name}" if network.nil?
908
+ if network.class == Vim::Dvs::DistributedVirtualPortgroup
909
+ portgroup_properties = client.get_properties(network, Vim::DistributedVirtualPortgroup,
910
+ ["config.key", "config.distributedVirtualSwitch"],
911
+ :ensure_all => true)
912
+
913
+ switch = portgroup_properties["config.distributedVirtualSwitch"]
914
+ switch_uuid = client.get_property(switch, Vim::DistributedVirtualSwitch, "uuid", :ensure_all => true)
915
+
916
+ port = Vim::Dvs::PortConnection.new
917
+ port.switch_uuid = switch_uuid
918
+ port.portgroup_key = portgroup_properties["config.key"]
919
+
920
+ backing_info = Vim::Vm::Device::VirtualEthernetCard::DistributedVirtualPortBackingInfo.new
921
+ backing_info.port = port
922
+
923
+ dvs_index[port.portgroup_key] = v_network_name
924
+ else
925
+ backing_info = Vim::Vm::Device::VirtualEthernetCard::NetworkBackingInfo.new
926
+ backing_info.device_name = v_network_name
927
+ backing_info.network = network
928
+ end
929
+
930
+ nic = Vim::Vm::Device::VirtualVmxnet3.new
931
+ nic.key = -1
932
+ nic.controller_key = controller_key
933
+ nic.backing = backing_info
934
+
935
+ device_config_spec = Vim::Vm::Device::VirtualDeviceSpec.new
936
+ device_config_spec.device = nic
937
+ device_config_spec.operation = Vim::Vm::Device::VirtualDeviceSpec::Operation::ADD
938
+ device_config_spec
939
+ end
940
+
941
+ def create_delete_device_spec(device, options = {})
942
+ device_config_spec = Vim::Vm::Device::VirtualDeviceSpec.new
943
+ device_config_spec.device = device
944
+ device_config_spec.operation = Vim::Vm::Device::VirtualDeviceSpec::Operation::REMOVE
945
+ if options[:destroy]
946
+ device_config_spec.file_operation = Vim::Vm::Device::VirtualDeviceSpec::FileOperation::DESTROY
947
+ end
948
+ device_config_spec
949
+ end
950
+
951
+ def create_edit_device_spec(device)
952
+ device_config_spec = Vim::Vm::Device::VirtualDeviceSpec.new
953
+ device_config_spec.device = device
954
+ device_config_spec.operation = Vim::Vm::Device::VirtualDeviceSpec::Operation::EDIT
955
+ device_config_spec
956
+ end
957
+
958
+ def fix_device_unit_numbers(devices, device_changes)
959
+ max_unit_numbers = {}
960
+ devices.each do |device|
961
+ if device.controller_key
962
+ max_unit_number = max_unit_numbers[device.controller_key]
963
+ if max_unit_number.nil? || max_unit_number < device.unit_number
964
+ max_unit_numbers[device.controller_key] = device.unit_number
965
+ end
966
+ end
967
+ end
968
+
969
+ device_changes.each do |device_change|
970
+ device = device_change.device
971
+ if device.controller_key && device.unit_number.nil?
972
+ max_unit_number = max_unit_numbers[device.controller_key] || 0
973
+ device.unit_number = max_unit_number + 1
974
+ max_unit_numbers[device.controller_key] = device.unit_number
975
+ end
976
+ end
977
+ end
978
+
979
+ def import_ovf(name, ovf, resource_pool, datastore)
980
+ import_spec_params = Vim::OvfManager::CreateImportSpecParams.new
981
+ import_spec_params.entity_name = name
982
+ import_spec_params.locale = 'US'
983
+ import_spec_params.deployment_option = ''
984
+
985
+ ovf_file = File.open(ovf)
986
+ ovf_descriptor = ovf_file.read
987
+ ovf_file.close
988
+
989
+ @client.service_content.ovf_manager.create_import_spec(ovf_descriptor, resource_pool,
990
+ datastore, import_spec_params)
991
+ end
992
+
993
+ def obtain_nfc_lease(resource_pool, import_spec, folder)
994
+ resource_pool.import_vapp(import_spec, folder, nil)
995
+ end
996
+
997
+ def wait_for_nfc_lease(lease)
998
+ loop do
999
+ state = client.get_property(lease, Vim::HttpNfcLease, "state")
1000
+ unless state == Vim::HttpNfcLease::State::INITIALIZING
1001
+ return state
1002
+ end
1003
+ sleep(1.0)
1004
+ end
1005
+ end
1006
+
1007
+ def upload_ovf(ovf, lease, file_items)
1008
+ info = client.get_property(lease, Vim::HttpNfcLease, "info", :ensure_all => true)
1009
+ lease_updater = LeaseUpdater.new(client, lease)
1010
+
1011
+ info.device_url.each do |device_url|
1012
+ device_key = device_url.import_key
1013
+ file_items.each do |file_item|
1014
+ if device_key == file_item.device_id
1015
+ http_client = HTTPClient.new
1016
+ http_client.send_timeout = 14400 # 4 hours
1017
+ http_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
1018
+
1019
+ disk_file_path = File.join(File.dirname(ovf), file_item.path)
1020
+ # TODO; capture the error if file is not found a provide a more meaningful error
1021
+ disk_file = File.open(disk_file_path)
1022
+ disk_file_size = File.size(disk_file_path)
1023
+
1024
+ progress_thread = Thread.new do
1025
+ loop do
1026
+ # TODO: fix progress calculation to work across multiple disks
1027
+ lease_updater.progress = disk_file.pos * 100 / disk_file_size
1028
+ sleep(2)
1029
+ end
1030
+ end
1031
+
1032
+ @logger.info("Uploading disk to: #{device_url.url}")
1033
+
1034
+ http_client.post(device_url.url, disk_file, {"Content-Type" => "application/x-vnd.vmware-streamVmdk",
1035
+ "Content-Length" => disk_file_size})
1036
+
1037
+ progress_thread.kill
1038
+ disk_file.close
1039
+ end
1040
+ end
1041
+ end
1042
+ lease_updater.finish
1043
+ info.entity
1044
+ end
1045
+
1046
+ def wait_until_off(vm, timeout)
1047
+ started = Time.now
1048
+ loop do
1049
+ power_state = client.get_property(vm, Vim::VirtualMachine, "runtime.powerState")
1050
+ break if power_state == Vim::VirtualMachine::PowerState::POWERED_OFF
1051
+ raise TimeoutException if Time.now - started > timeout
1052
+ sleep(1.0)
1053
+ end
1054
+ end
1055
+
1056
+ def delete_all_vms
1057
+ Bosh::ThreadPool.new(:max_threads => 32, :logger => @logger).wrap do |pool|
1058
+ index = 0
1059
+
1060
+ @resources.datacenters.each_value do |datacenter|
1061
+ vm_folder_path = [datacenter.name, "vm", datacenter.vm_folder_name]
1062
+ vm_folder = client.find_by_inventory_path(vm_folder_path)
1063
+ vms = client.get_managed_objects(Vim::VirtualMachine, :root => vm_folder)
1064
+ next if vms.empty?
1065
+
1066
+ vm_properties = client.get_properties(vms, Vim::VirtualMachine, ["name"])
1067
+
1068
+ vm_properties.each do |_, properties|
1069
+ pool.process do
1070
+ @lock.synchronize {index += 1}
1071
+ vm = properties["name"]
1072
+ @logger.debug("Deleting #{index}/#{vms.size}: #{vm}")
1073
+ begin
1074
+ delete_vm(vm)
1075
+ rescue Exception => e
1076
+ @logger.info("#{e} - #{e.backtrace.join("\n")}")
1077
+ end
1078
+ end
1079
+ end
1080
+ end
1081
+ end
1082
+ end
1083
+
1084
+ end
1085
+ end