# frozen_string_literal: true require "chef" require "cheffish/merged_config" require "chef/provisioning/driver" require "chef/provisioning/machine/windows_machine" require "chef/provisioning/machine/unix_machine" require "chef/provisioning/vsphere_driver/clone_spec_builder" require "chef/provisioning/vsphere_driver/version" require "chef/provisioning/vsphere_driver/vsphere_helpers" require "chef/provisioning/vsphere_driver/vsphere_url" require "chef/provisioning/vsphere_driver/vm_helper" # Provisions machines in vSphere. module ChefProvisioningVsphere # Inherits the Chef::Provisioning::Driver attirbutes class VsphereDriver < Chef::Provisioning::Driver include Chef::Mixin::ShellOut # Creates the new object via the URL # # @param [String] driver_url The driver url to connect via. # @param [Object] config The config to get to the machine you are creating. def self.from_url(driver_url, config) VsphereDriver.new(driver_url, config) end # 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 def self.canonicalize_url(driver_url, config) config = symbolize_keys(config) [driver_url || URI::VsphereUrl.from_config(config).to_s, config] end # Converts the keys from strings to symbols # # @param [Object] h converts to key:value def self.symbolize_keys(h) Hash === h ? # rubocop:disable Style/MultilineTernaryOperator Hash[ h.map do |k, v| [k.respond_to?(:to_sym) ? k.to_sym : k, symbolize_keys(v)] # rubocop:disable Style/NestedTernaryOperator end ] : h end # Converts the keys from strings to symbols # # @param [Object] hash_like converts to key:value def deep_symbolize(hash_like) return {} if hash_like.nil? r = {} hash_like.each do |key, value| value = deep_symbolize(value) if value.respond_to?(:values) r[key.to_sym] = value end r end # Initialize method for this class # # @param [String] driver_url Location of where the driver needs to connect to. # @param [Object] config The complete config of the machine options. def initialize(driver_url, config) super(driver_url, config) uri = URI(driver_url) @connect_options = { provider: "vsphere", host: uri.host, port: uri.port, use_ssl: uri.use_ssl, insecure: uri.insecure, path: uri.path, } if driver_options @connect_options[:user] = driver_options[:user] @connect_options[:password] = driver_options[:password] end end attr_reader :connect_options # Creates a new vm_helper if not already there # def vm_helper @vm_helper ||= ChefProvisioningVsphere::VmHelper.new end # 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 # def allocate_machine(action_handler, machine_spec, machine_options) machine_options = deep_symbolize(machine_options) merge_options! 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_msg( machine_spec.name, machine_spec.location["server_id"], "no longer exists. Recreating ..." ) end end bootstrap_options = machine_options[:bootstrap_options] action_handler.report_progress full_description( machine_spec, bootstrap_options ) vm = find_or_create_vm(bootstrap_options, machine_spec, action_handler) add_machine_spec_location(vm, machine_spec) action_handler.performed_action(machine_msg( machine_spec.name, vm.config.instanceUuid, "created" )) vm end # Squishes the options to one large hash # # @param [Object] machine_options taken from Chef provisioning for all the `machine_options` def merge_options!(machine_options) @config = Cheffish::MergedConfig.new( { machine_options: machine_options }, @config ) @config = deep_symbolize(@config.to_h) end # Adds machine spec location from other options. # # @param [Object] machine_spec taken from Chef provisioning for all the `machine_spec`. # @param [Object] vm taken from Chef provisioning for all the vm state. def add_machine_spec_location(vm, machine_spec) 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, } end # Verify if machine is there or create it. # # @param [Object] bootstrap_options taken from Chef provisioning for all the bootstrap options. # @param [Object] machine_spec taken from Chef provisioning for all the `machine_spec`. # @param [Object] action_handler taken from Chef provisioning for TODO. def find_or_create_vm(bootstrap_options, machine_spec, action_handler) vm = vsphere_helper.find_vm( bootstrap_options[:vm_folder], machine_spec.name ) if vm Chef::Log.info machine_msg( machine_spec.name, vm.config.instanceUuid, "already created" ) else vm = clone_vm( action_handler, bootstrap_options, machine_spec ) end vm end # Creates a full description of the machine. # # @param [Object] bootstrap_options taken from Chef provisioning for all the bootstrap options. # @param [Object] machine_spec taken from Chef provisioning for all the `machine_spec`. def full_description(machine_spec, bootstrap_options) description = ["creating machine #{machine_spec.name} on #{driver_url}"] bootstrap_options.to_hash.each_pair do |key, value| if value.is_a?(Hash) temp_value = value.clone temp_value[:password] = "*********" if value.key?(:password) else temp_value = value end description << " #{key}: #{temp_value.inspect}" end description end # Creates a string of specific Machine information # # @param [String] name The name of the machine # @param [String] id The ID of the machine # @param [Object] action TODO # @return [String] "Machine - ACTION - NAME (UUID on URL)" def machine_msg(name, id, action) "Machine - #{action} - #{name} (#{id} on #{driver_url})" end # Sets the machine to ready state. (Creates the machine but does not bootstrap Chef into it.) # # @param [Object] action_handler TODO # @param [Object] machine_options taken from Chef provisioning for all the bootstrap options. # @param [Object] machine_spec taken from Chef provisioning for all the `machine_spec`. def ready_machine(action_handler, machine_spec, machine_options) machine_options = deep_symbolize(machine_options) merge_options! machine_options vm = start_machine(action_handler, machine_spec, machine_options) if vm.nil? raise "Machine #{machine_spec.name} does not have a server "\ "associated with it, or server does not exist." end bootstrap_options = machine_options[:bootstrap_options] transport_respond?( machine_options, vm, action_handler, machine_spec ) machine = machine_for(machine_spec, machine_options) setup_extra_nics(action_handler, bootstrap_options, vm, machine, machine_spec) if has_static_ip(bootstrap_options) && !is_windows?(vm) setup_ubuntu_dns(machine, bootstrap_options, machine_spec) end ## Check if true available after added nic vm_helper.open_port?(machine_spec.location["ipaddress"], vm_helper.port) unless machine_spec.location["ipaddress"].nil? machine end # Creates new NICs for the machine # # @param [Object] action_handler TODO # @param [Object] bootstrap_options taken from Chef provisioning for all the bootstrap options. # @param [Object] vm The VM object. # @param [Object] machine The machine object. def setup_extra_nics(action_handler, bootstrap_options, vm, machine, machine_spec) networks = bootstrap_options[:network_name] networks = [networks] if networks.is_a?(String) return if networks.nil? || networks.count < 2 new_nics = vsphere_helper.add_extra_nic( action_handler, vm_template_for(bootstrap_options), bootstrap_options, vm ) if is_windows?(vm) && !new_nics.nil? && vm_helper.open_port?(machine_spec.location["ipaddress"], vm_helper.port) new_nics.each do |nic| nic_label = nic.device.deviceInfo.label machine.execute_always( "Disable-Netadapter -Name '#{nic_label}' -Confirm:$false" ) end end end # Is the transport layer ready? # # @param [Object] action_handler TODO # @param [Object] machine_spec taken from Chef provisioning for all the bootstrap options. # @param [Object] vm The VM object. # @param [Object] machine_options The options required to talk to the VM. def transport_respond?( machine_options, vm, action_handler, machine_spec ) bootstrap_options = machine_options[:bootstrap_options] # this waits for vmware tools to start and the vm to presebnt an ip # This may just be the ip of a newly cloned machine # Customization below may change this to a valid ip wait_until_ready(action_handler, machine_spec, machine_options, vm) if !machine_spec.location["ipaddress"] || !has_ip?(machine_spec.location["ipaddress"], vm) # find the ip we actually want # this will be the static ip to assign # or the ip reported back by the vm if using dhcp # it *may* be nil if just cloned vm_ip = ip_to_bootstrap(bootstrap_options, vm) || vm.guest.ipAddress machine_spec.location["ipaddress"] = vm_ip transport = nil unless vm_ip.nil? transport = transport_for(machine_spec, bootstrap_options[:ssh], vm_ip) end unless !transport.nil? && transport.available? && has_ip?(vm_ip, vm) attempt_ip(machine_options, action_handler, vm, machine_spec) end machine_spec.location["ipaddress"] = vm_ip # vm.guest.ipAddress vmWare ip_address here can be 0.0.0.0 action_handler.report_progress( "IP address obtained: #{machine_spec.location['ipaddress']}" ) end wait_for_domain(bootstrap_options, vm, machine_spec, action_handler) 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_msg( machine_spec.name, vm.config.instanceUuid, "started but SSH did not come up. Rebooting..." )) restart_server(action_handler, machine_spec, machine_options) wait_until_ready(action_handler, machine_spec, machine_options, vm) wait_for_transport(action_handler, machine_spec, machine_options, vm) end end end # Test out the IP to connect to. # # @param [Object] action_handler TODO # @param [Object] machine_options taken from Chef provisioning for all the bootstrap options. # @param [Object] vm The VM object. # @param [Object] machine_spec The spec required to talk to the VM. def attempt_ip(machine_options, action_handler, vm, machine_spec) vm_ip = ip_to_bootstrap(machine_options[:bootstrap_options], vm) machine_spec.location["ipaddress"] = vm_ip wait_for_ip(vm, machine_options, machine_spec, action_handler) unless has_ip?(vm_ip, vm) action_handler.report_progress "rebooting..." if vm.guest.toolsRunningStatus != "guestToolsRunning" msg = "tools have stopped. current power state is " msg << vm.runtime.powerState msg << " and tools state is " msg << vm.guest.toolsRunningStatus msg << ". powering up server..." action_handler.report_progress(msg) vsphere_helper.start_vm(vm) else restart_server(action_handler, machine_spec, machine_options) end wait_for_ip(vm, machine_options, machine_spec, action_handler) end end # Wait for the Windows Domain. # # @param [Object] action_handler TODO # @param [Object] bootstrap_options taken from Chef provisioning for all the bootstrap options. # @param [Object] vm The VM object. # @param [Object] machine_spec The specs to connect to bootstrap machine def wait_for_domain(bootstrap_options, vm, machine_spec, action_handler) return unless bootstrap_options[:customization_spec] domain = if bootstrap_options[:customization_spec].is_a?(String) && is_windows?(vm) spec = vsphere_helper.find_customization_spec(bootstrap_options[:customization_spec]) spec.identity.identification.joinDomain elsif bootstrap_options[:customization_spec].is_a?(String) && !is_windows?(vm) spec = vsphere_helper.find_customization_spec(bootstrap_options[:customization_spec]) spec.identity.domain else bootstrap_options[:customization_spec][:domain] end return unless domain if is_windows?(vm) && domain != "local" start = 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 - start) > 30 || (vm.guest.hostName == expected_name) print "." sleep 5 end end end # Wait for another IP allocation. # # @param [Object] action_handler TODO # @param [Object] vm The VM object. # @param [Object] machine_options The machine options required to start the VM. # @param [Object] machine_spec The machine spec required to start the VM. def wait_for_ip(vm, machine_options, machine_spec, action_handler) bootstrap_options = machine_options[:bootstrap_options] ip_to_bootstrap(bootstrap_options, vm) ready_timeout = machine_options[:ready_timeout] || 300 msg1 = "waiting up to #{ready_timeout} seconds for customization" msg2 = " and find #{machine_spec.location['ipaddress']}" unless machine_spec.location["ipaddress"].nil? # unless vm_ip == vm.guest.ipAddress # RuntimeError: can't modify frozen String msg = [msg1, msg2].join action_handler.report_progress msg vm_ip = ip_to_bootstrap(bootstrap_options, vm) || vm.guest.ipAddress machine_spec.location["ipaddress"] = vm_ip until transport_for( machine_spec, machine_options[:bootstrap_options][:ssh], vm_ip ).available? || remaining_wait_time(machine_spec, machine_options) < 0 action_handler.report_progress( "IP addresses found: #{all_ips_for(vm)}" ) vm_ip = ip_to_bootstrap(bootstrap_options, vm) || vm.guest.ipAddress machine_spec.location["ipaddress"] = vm_ip if has_ip?(vm_ip, vm) transport_for( machine_spec, machine_options[:bootstrap_options][:ssh], vm_ip ).available? end sleep 5 end end # Flatten all IPs for a VM # # @param [Object] vm The VM object. def all_ips_for(vm) vm.guest.net.map(&:ipAddress).flatten end # Does the VM have this IP? # # @param [Object] vm The VM object. # @param [String] ip IP to see if it's in the map. def has_ip?(ip, vm) all_ips_for(vm).include?(ip) end # Connect to machine without acquiring it # # @param [Object] machine_options The machine options required to start the VM. # @param [Object] machine_spec The machine spec required to start the VM. def connect_to_machine(machine_spec, machine_options) machine_options = deep_symbolize(machine_options) merge_options! machine_options machine_for(machine_spec, machine_options) end # Destroy the Machine # # @param [Object] machine_options The machine options required to start the VM. # @param [Object] machine_spec The machine spec required to start the VM. # @param [Object] action_handler TODO def destroy_machine(action_handler, machine_spec, machine_options) machine_options = deep_symbolize(machine_options) merge_options! machine_options vm = vm_for(machine_spec) if vm action_handler.perform_action "Delete VM [#{vm.parent.name}/#{vm.name}]" do begin vsphere_helper.stop_vm(vm, machine_options[:stop_timeout]) 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 # Stop the Machine # # @param [Object] machine_options The machine options required to start the VM. # @param [Object] machine_spec The machine spec required to start the VM. # @param [Object] action_handler TODO def stop_machine(action_handler, machine_spec, machine_options) machine_options = deep_symbolize(machine_options) merge_options! 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 vsphere_helper.stop_vm(vm, machine_options[:stop_timeout]) end end end # Start the Machine # # @param [Object] machine_options The machine options required to start the VM. # @param [Object] machine_spec The machine spec required to start the VM. # @param [Object] action_handler TODO def start_machine(action_handler, machine_spec, machine_options) machine_options = deep_symbolize(machine_options) merge_options! machine_options vm = vm_for(machine_spec) if vm action_handler.perform_action "Power on VM [#{vm.parent.name}/#{vm.name}]" do vsphere_helper.start_vm(vm, machine_options[:bootstrap_options][:ssh][:port]) end end vm end # Restart the Machine # # @param [Object] machine_options The machine options required to start the VM. # @param [Object] machine_spec The machine spec required to start the VM. # @param [Object] action_handler TODO def restart_server(action_handler, machine_spec, machine_options) action_handler.perform_action "restart machine #{machine_spec.name} (#{driver_url})" do stop_machine(action_handler, machine_spec, machine_options) start_machine(action_handler, machine_spec, machine_options) machine_spec.location["started_at"] = Time.now.utc.to_s end end protected # Ubuntu requires some...hacking to get DNS to work. # # @param [Object] bootstrap_options The bootstrap options required to start the VM. # @param [Object] _machine_spec The machine spec required to start the VM. # @param [Object] machine Machine object to connect to. def setup_ubuntu_dns(machine, bootstrap_options, _machine_spec) host_lookup = machine.execute_always("host google.com") if host_lookup.exitstatus != 0 if host_lookup.stdout.include?("setlocale: LC_ALL") machine.execute_always("locale-gen en_US && update-locale LANG=en_US") end distro = machine.execute_always("lsb_release -i | sed -e 's/Distributor ID://g'").stdout.strip Chef::Log.info "Found distro:#{distro}" if distro == "Ubuntu" distro_version = machine.execute_always("lsb_release -r | sed -e s/[^0-9.]//g").stdout.strip.to_f Chef::Log.info "Found distro version:#{distro_version}" if distro_version >= 12.04 Chef::Log.info "Ubuntu version 12.04 or greater. Need to patch DNS." interfaces_file = "/etc/network/interfaces" nameservers = bootstrap_options[:customization_spec][:ipsettings][:dnsServerList].join(" ") machine.execute_always("if ! cat #{interfaces_file} | grep -q dns-search ; then echo 'dns-search #{bootstrap_options[:customization_spec][:domain]}' >> #{interfaces_file} ; fi") machine.execute_always("if ! cat #{interfaces_file} | grep -q dns-nameservers ; then echo 'dns-nameservers #{nameservers}' >> #{interfaces_file} ; fi") machine.execute_always("/etc/init.d/networking restart") machine.execute_always("apt-get -qq update") end end end end # Verify static IP. # # @param [Object] bootstrap_options The bootstrap options required to start the VM. def has_static_ip(bootstrap_options) if bootstrap_options.key?(:customization_spec) bootstrap_options = bootstrap_options[:customization_spec] if bootstrap_options.is_a?(String) spec = vsphere_helper.find_customization_spec(bootstrap_options) return spec.nicSettingMap[0].adapter.ip.is_a?(RbVmomi::VIM::CustomizationFixedIp) elsif bootstrap_options.key?(:ipsettings) bootstrap_options = bootstrap_options[:ipsettings] return bootstrap_options.key?(:ip) end end false end def remaining_wait_time(machine_spec, machine_options) if machine_spec.location["started_at"] (machine_options[:start_timeout] || 600) - (Time.now.utc - Time.parse(machine_spec.location["started_at"])) else (machine_options[:create_timeout] || 600) - (Time.now.utc - Time.parse(machine_spec.location["allocated_at"])) end end def wait_until_ready(action_handler, machine_spec, machine_options, vm) if vm.guest.toolsRunningStatus != "guestToolsRunning" if action_handler.should_perform_actions action_handler.report_progress "waiting for #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url}) to be ready ..." until remaining_wait_time(machine_spec, machine_options) < 0 || (vm.guest.toolsRunningStatus == "guestToolsRunning" && vm.guest.ipAddress && !vm.guest.ipAddress.empty?) print "." sleep 5 end action_handler.report_progress "#{machine_spec.name} is now ready" end end end def vm_for(machine_spec) if machine_spec.location vsphere_helper.find_vm_by_id(machine_spec.location["server_id"]) end end def clone_vm(action_handler, bootstrap_options, machine_spec) machine_name = machine_spec.name vm_template = vm_template_for(bootstrap_options) spec_builder = CloneSpecBuilder.new(vsphere_helper, action_handler) clone_spec = spec_builder.build(vm_template, machine_name, bootstrap_options) Chef::Log.debug("Clone spec: #{clone_spec.pretty_inspect}") vm_folder = vsphere_helper.find_folder(bootstrap_options[:vm_folder]) last_progress = 0 vm_template.CloneVM_Task( name: machine_name, folder: vm_folder, spec: clone_spec ).wait_for_progress do |progress| if (progress.is_a? Numeric) && (progress / 10).floor != (last_progress / 10).floor print "\n#{machine_name} progress: #{progress}%" last_progress = progress end end print "\n#{machine_name} done!\n\n" vm = vsphere_helper.find_vm(vm_folder, machine_name) add_machine_spec_location(vm, machine_spec) vsphere_helper.update_main_disk_size_for(vm, bootstrap_options[:main_disk_size_gb]) vsphere_helper.set_additional_disks_for(vm, bootstrap_options[:datastore], bootstrap_options[:additional_disk_size_gb]) vsphere_helper.set_initial_iso(vm, bootstrap_options[:initial_iso_file]) vm end def vsphere_helper @vsphere_helper ||= VsphereHelper.new( connect_options, config[:machine_options][:bootstrap_options][:datacenter] ) end def vm_template_for(bootstrap_options) template_folder = bootstrap_options[:template_folder] template_name = bootstrap_options[:template_name] vsphere_helper.find_vm(template_folder, template_name) || raise("vSphere VM Template not found [#{template_folder}/#{template_name}]") end def machine_for(machine_spec, machine_options) if machine_spec.location.nil? raise "Server for node #{machine_spec.name} has not been created!" end transport = transport_for( machine_spec, machine_options[:bootstrap_options][:ssh] ) strategy = convergence_strategy_for(machine_spec, machine_options) if machine_spec.location["is_windows"] Chef::Provisioning::Machine::WindowsMachine.new( machine_spec, transport, strategy ) else Chef::Provisioning::Machine::UnixMachine.new( machine_spec, transport, strategy ) end end def is_windows?(vm) return false if vm.nil? vm.config.guestId.start_with?("win") end def convergence_strategy_for(machine_spec, machine_options) require "chef/provisioning/convergence_strategy/install_msi" require "chef/provisioning/convergence_strategy/install_cached" require "chef/provisioning/convergence_strategy/no_converge" mopts = machine_options[:convergence_options].to_hash.dup if mopts[:chef_server] mopts[:chef_server] = mopts[:chef_server].to_hash.dup mopts[:chef_server][:options] = mopts[:chef_server][:options].to_hash.dup if mopts[:chef_server][:options] end unless machine_spec.location return Chef::Provisioning::ConvergenceStrategy::NoConverge.new( mopts, config ) end if machine_spec.location["is_windows"] Chef::Provisioning::ConvergenceStrategy::InstallMsi.new( mopts, config ) else Chef::Provisioning::ConvergenceStrategy::InstallCached.new( mopts, config ) end end # Wait for the transport connection to happen # # @param [Object] machine_spec The machine_spec from Chef-Provisioning # @param [Object] machine_options Options for remote connections # @param [Object] vm The VM object from Chef-Provisioning # @param [Object] action_handler taken from Chef provisioning for TODO. def wait_for_transport(action_handler, machine_spec, machine_options, vm) vm_helper.find_port?(vm, machine_options[:bootstrap_options]) if vm_helper.port.nil? transport = transport_for( machine_spec, machine_options[:bootstrap_options][:ssh] ) unless transport.available? if action_handler.should_perform_actions action_handler.report_progress "waiting for #{machine_spec.name} (#{vm.config.instanceUuid} on #{driver_url}) to be connectable (transport up and running) ..." until remaining_wait_time(machine_spec, machine_options) < 0 || transport.available? print "." sleep 5 end action_handler.report_progress "#{machine_spec.name} is now connectable" end end end # Figure out the transport system to send to the VM # # @param [Object] machine_spec The machine_spec from Chef-Provisioning # @param [Object] remoting_options Options for remote connections # @param [String] ip The IP to connect to. def transport_for( machine_spec, remoting_options, ip = machine_spec.location["ipaddress"] ) if machine_spec.location["is_windows"] create_winrm_transport(ip, remoting_options) else create_ssh_transport(ip, remoting_options) end end # Create a robust WinRM transport connect, so so many options to make a simple connection # # @param [String] host The host the VM is connecting to # @param [Object] options Options that are required to connect to the host from Chef-Provisioning def create_winrm_transport(host, options) require "chef/provisioning/transport/winrm" winrm_transport = if options[:port] == 5986 :ssl else options[:winrm_transport].nil? ? :negotiate : options[:winrm_transport].to_sym end port = options[:port] || vm_helper.port winrm_options = { user: (options[:user]).to_s, pass: options[:password], } if options[:winrm_opts].nil? opt = options[:user].include?('\\') ? :disable_sspi : :basic_auth_only winrm_options[opt] = true else winrm_options.merge!(options[:winrm_opts]) end scheme = winrm_transport == :ssl ? "https" : "http" endpoint = URI::Generic.build( scheme: scheme, host: host, port: port, path: "/wsman" ).to_s Chef::Provisioning::Transport::WinRM.new( endpoint, winrm_transport, winrm_options, config ) end # Return a SSH transport for the machine # # @param [String] host The host the VM is connecting to # @param [Object] options Options that are required to connect to the host from Chef-Provisioning def create_ssh_transport(host, options) require "chef/provisioning/transport/ssh" ssh_user = options[:user] options = options.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; } Chef::Provisioning::Transport::SSH.new( host, ssh_user, options.to_hash, @config[:machine_options][:sudo] ? { prefix: "sudo " } : {}, config ) end # Find the IP to bootstrap against # # @param [Object] bootstrap_options The bootstrap options from Chef-Provisioning # @param [Object] vm The VM object from Chef-Provisioning def ip_to_bootstrap(bootstrap_options, vm) start_time = Time.now.utc timeout = bootstrap_ip_ready_timeout(bootstrap_options) vm_helper.find_port?(vm, bootstrap_options) unless vm_helper.port? # First get the IP to be tested if has_static_ip(bootstrap_options) if bootstrap_options[:customization_spec].is_a?(String) spec = vsphere_helper.find_customization_spec(bootstrap_options[:customization_spec]) vm_ip = spec.nicSettingMap[0].adapter.ip.ipAddress else ## Check if true available vm_ip = bootstrap_options[:customization_spec][:ipsettings][:ip] end elsif use_ipv4_during_bootstrap?(bootstrap_options) vm_ip = wait_for_ipv4(bootstrap_ipv4_timeout(bootstrap_options), vm) else until vm_guest_ip?(vm) || Time.now.utc - start_time > timeout print "." sleep 1 end vm_ip = vm.guest.ipAddress end # Then check that it is reachable until Time.now.utc - start_time > timeout print "." return vm_ip.to_s if vm_helper.open_port?(vm_ip, vm_helper.port, 1) sleep 1 end raise "Timed out (#{timeout}s) waiting for ip #{vm_ip} to be connectable" end # Force IPv4 a bootstrap, default: false # # @param [Object] bootstrap_options The bootstrap options from Chef-Provisioning def use_ipv4_during_bootstrap?(bootstrap_options) bootstrap_options.key?(:bootstrap_ipv4) && bootstrap_options[:bootstrap_ipv4] == true end # Setting a bootstrap ipv4 timeout, default: 30 # # @param [Object] bootstrap_options The bootstrap options from Chef-Provisioning def bootstrap_ipv4_timeout(bootstrap_options) bootstrap_options.key?(:ipv4_timeout) ? bootstrap_options[:ipv4_timeout].to_i : 30 end # Setting a bootstrap ip timeout, default: 300 # # @param [Object] bootstrap_options The bootstrap options from Chef-Provisioning def bootstrap_ip_ready_timeout(bootstrap_options) bootstrap_options.key?(:ip_ready_timeout) ? bootstrap_options[:ip_ready_timeout].to_i : 300 end # Wait for IPv4 address # # @param [String] timeout_seconds A timeout in seconds, an error will be raised if it is reached # @param [Object] vm The VM object from Chef-Provisioning def wait_for_ipv4(timeout_seconds, vm) Chef::Log.info("Waiting #{timeout_seconds}s for ipv4 address.") start_time = Time.now.utc while (Time.now.utc - start_time) <= timeout_seconds # print '.' sleep 5 vm_ips = all_ips_for(vm) Chef::Log.info("Found IP address(es): #{vm_ips}") next unless vm_guest_ip?(vm) vm_ips.each do |vm_ip| if IPAddr.new(vm_ip).ipv4? Chef::Log.info("Found ipv4 address: #{vm_ip}") return vm_ip end end end raise "Timed out waiting for ipv4 address!" end # What is the VM guest IP # # @param [Object] vm The VM object from Chef-Provisioning def vm_guest_ip?(vm) vm.guest.guestState == "running" && vm.guest.toolsRunningStatus == "guestToolsRunning" && !vm.guest.ipAddress.nil? end end end