Class: ChefProvisioningVsphere::VsphereDriver

Inherits:
Chef::Provisioning::Driver
  • Object
show all
Includes:
Chef::Mixin::ShellOut, Helpers
Defined in:
lib/chef/provisioning/vsphere_driver/driver.rb

Overview

Provisions machines in vSphere.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers

#add_extra_nic, #backing_info_for, #create_delta_disk, #customization_options_from, #dc, #do_vm_clone, #find_customization_spec, #find_datastore, #find_ethernet_cards_for, #find_folder, #find_host, #find_pool, #find_vm, #find_vm_by_id, #hostname_from, #network_adapter_for, #network_device_changes, #network_id_for, #port_ready?, #relocate_spec_for, #start_vm, #stop_vm, #upload_file_to_vm, #vim, #virtual_disk_for, #vm_started?, #vm_stopped?, #windows_prep_for

Constructor Details

#initialize(driver_url, config) ⇒ VsphereDriver

Create a new Vsphere provisioner.

## Parameters connect_options - hash of options to be passed to RbVmomi::VIM.connect

:host       - required - hostname of the vSphere API server
:port       - optional - port on the vSphere API server (default: 443)
:path        - optional - path on the vSphere API server (default: /sdk)
:use_ssl        - optional - true to use ssl in connection to vSphere API server (default: true)
:insecure   - optional - true to ignore ssl certificate validation errors in connection to vSphere API server (default: false)
:user       - required - user name to use in connection to vSphere API server
:password   - required - password to use in connection to vSphere API server
:proxy_host         - optional - http proxy host to use in connection to vSphere API server (default: none)
:proxy_port         - optional - http proxy port to use in connection to vSphere API server (default: none)


89
90
91
92
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 89

def initialize(driver_url, config)
  super(driver_url, config)
  @connect_options = config[:driver_options][:connect_options].to_hash
end

Instance Attribute Details

#connect_optionsObject (readonly)

Returns the value of attribute connect_options.



94
95
96
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 94

def connect_options
  @connect_options
end

Class Method Details

.canonicalize_url(driver_url, config) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 20

def self.canonicalize_url(driver_url, config)
  config = symbolize_keys(config)
  new_defaults = {
   :driver_options => { :connect_options => { :port     => 443,
                                              :use_ssl      => true,
                                              :insecure => false,
                                              :path     => '/sdk'
    } },
                   :machine_options => { :start_timeout => 600, 
                                         :create_timeout => 600, 
                                         :ready_timeout => 300,
                                         :bootstrap_options => { :ssh => { :port => 22,
                                                                           :user => 'root' },
                                                                 :key_name => 'metal_default',
                                                                 :tags => {} } }
  }

  new_connect_options = {}
  new_connect_options[:provider] = 'vsphere'
  if !driver_url.nil?
    uri = URI(driver_url)
    new_connect_options[:host] = uri.host
    new_connect_options[:port] = uri.port
    if uri.path && uri.path.length > 0
      new_connect_options[:path] = uri.path
    end
    new_connect_options[:use_ssl] = uri.use_ssl
    new_connect_options[:insecure] = uri.insecure
  end
  new_connect_options = new_connect_options.merge(config[:driver_options])

  new_config = { :driver_options => { :connect_options => new_connect_options }}
  config = Cheffish::MergedConfig.new(new_config, config, new_defaults)

  required_options = [:host, :user, :password]
  missing_options = []
  required_options.each do |opt|
    missing_options << opt unless config[:driver_options][:connect_options].has_key?(opt)
  end
  unless missing_options.empty?
    raise "missing required options: #{missing_options.join(', ')}"
  end

  url = URI::VsphereUrl.from_config(config[:driver_options][:connect_options]).to_s
  [ url, config ]
end

.from_url(driver_url, config) ⇒ Object



16
17
18
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 16

def self.from_url(driver_url, config)
  VsphereDriver.new(driver_url, config)
end

.symbolize_keys(h) ⇒ Object



67
68
69
70
71
72
73
74
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 67

def self.symbolize_keys(h)
  Hash === h ?
    Hash[
      h.map do |k, v|
        [k.respond_to?(:to_sym) ? k.to_sym : k, symbolize_keys(v)]
      end
    ] : h
end

Instance Method Details

#allocate_machine(action_handler, machine_spec, machine_options) ⇒ Object

Acquire a machine, generally by provisioning it. Returns a Machine object pointing at the machine, allowing useful actions like setup, converge, execute, file and directory. The Machine object will have a “node” property which must be saved to the server (if it is any different from the original node object).

## Parameters action_handler - the action_handler object that is calling this method; this

is generally a action_handler, but could be anything that can support the
ChefMetal::ActionHandler interface (i.e., in the case of the test
kitchen metal driver for acquiring and destroying VMs; see the base
class for what needs providing).

node - node object (deserialized json) representing this machine. If

the node has a provisioner_options hash in it, these will be used
instead of options provided by the provisioner.  TODO compare and
fail if different?
node will have node['normal']['provisioner_options'] in it with any options.
It is a hash with this format:

   -- provisioner_url: vsphere://host:port?ssl=[true|false]&insecure=[true|false]
   -- bootstrap_options: hash of options to pass to RbVmomi::VIM::VirtualMachine::CloneTask()
        :datacenter
        :resource_pool
        :cluster
        :datastore
        :template_name
        :template_folder
        :vm_folder
        :winrm {...} (not yet implemented)
        :ssh {...}

Example bootstrap_options for vSphere:
  TODO: add other CloneTask params, e.g.: datastore, annotation, resource_pool, ...
  'bootstrap_options' => {
    'template_name' =>'centos6.small',
    'template_folder' =>'Templates',
    'vm_folder' => 'MyApp'
  }

node['normal']['provisioner_output'] will be populated with information
about the created machine.  For vSphere, it is a hash with this
format:

   -- provisioner_url: vsphere:host:port?ssl=[true|false]&insecure=[true|false]
   -- vm_folder: name of the vSphere folder containing the VM


142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 142

def allocate_machine(action_handler, machine_spec, machine_options)
  if machine_spec.location
    Chef::Log.warn "Checking to see if #{machine_spec.location} has been created..."
    vm = vm_for(machine_spec)
    if vm
      Chef::Log.warn "returning existing machine"
      return vm
    else
      Chef::Log.warn "Machine #{machine_spec.name} (#{machine_spec.location['server_id']} on #{driver_url}) no longer exists.  Recreating ..."
    end
  end
  bootstrap_options = bootstrap_options_for(machine_spec, machine_options)
  vm = nil

  if bootstrap_options[:ssh]
    wait_on_port = bootstrap_options[:ssh][:port]
    raise "Must specify bootstrap_options[:ssh][:port]" if wait_on_port.nil?
  else
    raise 'bootstrapping is currently supported for ssh only'
    # wait_on_port = bootstrap_options['winrm']['port']
  end

  description = [ "creating machine #{machine_spec.name} on #{driver_url}" ]
  bootstrap_options.each_pair { |key,value| description << "  #{key}: #{value.inspect}" }
  action_handler.report_progress description

  vm = find_vm(bootstrap_options[:datacenter], bootstrap_options[:vm_folder], machine_spec.name)
  server_id = nil
  if vm
    Chef::Log.info "machine already created: #{bootstrap_options[:vm_folder]}/#{machine_spec.name}"
  else
    vm = clone_vm(action_handler, bootstrap_options)
  end

  machine_spec.location = {
    'driver_url' => driver_url,
    'driver_version' => VERSION,
    'server_id' => vm.config.instanceUuid,
    'is_windows' => is_windows?(vm),
    'allocated_at' => Time.now.utc.to_s,
    'ipaddress' => vm.guest.ipAddress
  }
  machine_spec.location['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
  %w(ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
    machine_spec.location[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
  end

  action_handler.performed_action "machine #{machine_spec.name} created as #{machine_spec.location['server_id']} on #{driver_url}"
  vm
end

#connect_to_machine(machine_spec, machine_options) ⇒ Object

Connect to machine without acquiring it



281
282
283
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 281

def connect_to_machine(machine_spec, machine_options)
  machine_for(machine_spec, machine_options)
end

#destroy_machine(action_handler, machine_spec, machine_options) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 285

def destroy_machine(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  if vm
    action_handler.perform_action "Delete VM [#{vm.parent.name}/#{vm.name}]" do
      begin
        vm.PowerOffVM_Task.wait_for_completion unless vm.runtime.powerState == 'poweredOff'
        vm.Destroy_Task.wait_for_completion
      rescue RbVmomi::Fault => fault
        raise fault unless fault.fault.class.wsdl_name == "ManagedObjectNotFound"
      ensure
        machine_spec.location = nil
      end
    end
  end
  strategy = convergence_strategy_for(machine_spec, machine_options)
  strategy.cleanup_convergence(action_handler, machine_spec)
end

#ready_machine(action_handler, machine_spec, machine_options) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 193

def ready_machine(action_handler, machine_spec, machine_options)
  start_machine(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  if vm.nil?
    raise "Machine #{machine_spec.name} does not have a server associated with it, or server does not exist."
  end

  wait_until_ready(action_handler, machine_spec, machine_options, vm)

  bootstrap_options = bootstrap_options_for(machine_spec, machine_options)

  transport = nil
  vm_ip = ip_for(bootstrap_options, vm)
  if !vm_ip.nil?
    transport = transport_for(machine_spec, machine_options, vm)
  end

  if transport.nil? || !transport.available? || !(vm.guest.net.map { |net| net.ipAddress}.flatten).include?(vm_ip)
    action_handler.report_progress "waiting up to #{machine_options[:ready_timeout]} seconds for customizations to complete and find #{vm_ip}"
    now = Time.now.utc

    until (Time.now.utc - now) > machine_options[:ready_timeout] || (vm.guest.net.map { |net| net.ipAddress}.flatten).include?(vm_ip) do
      action_handler.report_progress "IP addresses on #{machine_spec.name} are #{vm.guest.net.map { |net| net.ipAddress}.flatten}"
      vm_ip = ip_for(bootstrap_options, vm) if vm_ip.nil?
      sleep 5
    end
    if !(vm.guest.net.map { |net| net.ipAddress}.flatten).include?(vm_ip)
      action_handler.report_progress "rebooting..."
      if vm.guest.toolsRunningStatus != "guestToolsRunning"
        action_handler.report_progress "tools have stopped. current power state is #{vm.runtime.powerState} and tools state is #{vm.guest.toolsRunningStatus}. powering up server..."
        start_vm(vm)
      else
        restart_server(action_handler, machine_spec, vm)
      end
      now = Time.now.utc
      until (Time.now.utc - now) > 90 || (vm.guest.net.map { |net| net.ipAddress}.flatten).include?(vm_ip) do
        vm_ip = ip_for(bootstrap_options, vm) if vm_ip.nil?
        print "-"
        sleep 5
      end
    end
    machine_spec.location['ipaddress'] = vm.guest.ipAddress
    action_handler.report_progress "IP address obtained: #{machine_spec.location['ipaddress']}"
  end

  domain = bootstrap_options[:customization_spec][:domain]
  if vm.config.guestId.start_with?('win') && domain != 'local'
    now = Time.now.utc
    trimmed_name = machine_spec.name.byteslice(0,15)
    expected_name="#{trimmed_name}.#{domain}"
    action_handler.report_progress "waiting to domain join and be named #{expected_name}"
    until (Time.now.utc - now) > 30 || (vm.guest.hostName == expected_name) do
      print "."
      sleep 5
    end
  end

  begin
    wait_for_transport(action_handler, machine_spec, machine_options, vm)
  rescue Timeout::Error
    # Only ever reboot once, and only if it's been less than 10 minutes since we stopped waiting
    if machine_spec.location['started_at'] || remaining_wait_time(machine_spec, machine_options) < -(10*60)
      raise
    else
      Chef::Log.warn "Machine #{machine_spec.name} (#{server.config.instanceUuid} on #{driver_url}) was started but SSH did not come up.  Rebooting machine in an attempt to unstick it ..."
      restart_server(action_handler, machine_spec, vm)
      wait_until_ready(action_handler, machine_spec, machine_options, vm)
      wait_for_transport(action_handler, machine_spec, machine_options, vm)
    end
  end

  machine = machine_for(machine_spec, machine_options, vm)

  new_nics = add_extra_nic(action_handler, vm_template_for(bootstrap_options), bootstrap_options, vm)
  if is_windows?(vm) && !new_nics.nil?
    new_nics.each do |nic|
      machine.execute_always("Disable-Netadapter -Name '#{nic.device.deviceInfo.label}' -Confirm:$false")
    end
  end

  if has_static_ip(bootstrap_options) && !is_windows?(vm)
    setup_ubuntu_dns(machine, bootstrap_options, machine_spec)
  end

  machine
end

#restart_server(action_handler, machine_spec, vm) ⇒ Object



322
323
324
325
326
327
328
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 322

def restart_server(action_handler, machine_spec, vm)
  action_handler.perform_action "restart machine #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url})" do
    stop_machine(action_handler, machine_spec, vm)
    start_vm(vm)
    machine_spec.location['started_at'] = Time.now.utc.to_s
  end
end

#start_machine(action_handler, machine_spec, machine_options) ⇒ Object



312
313
314
315
316
317
318
319
320
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 312

def start_machine(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  if vm
    action_handler.perform_action "Power on VM [#{vm.parent.name}/#{vm.name}]" do
      bootstrap_options = bootstrap_options_for(machine_spec, machine_options)
      start_vm(vm, bootstrap_options[:ssh][:port])
    end
  end
end

#stop_machine(action_handler, machine_spec, machine_options) ⇒ Object



303
304
305
306
307
308
309
310
# File 'lib/chef/provisioning/vsphere_driver/driver.rb', line 303

def stop_machine(action_handler, machine_spec, machine_options)
  vm = vm_for(machine_spec)
  if vm
    action_handler.perform_action "Shutdown guest OS and power off VM [#{vm.parent.name}/#{vm.name}]" do
      stop_vm(vm)
    end
  end
end