class Dopv::Infrastructure::Vsphere

Public Class Methods

new(node_config, data_disks_db, wait_params={}) click to toggle source
Calls superclass method Dopv::Infrastructure::Base::new
# File lib/dopv/infrastructure/providers/vsphere.rb, line 14
def initialize(node_config, data_disks_db, wait_params={})
  super(node_config, data_disks_db)

  @compute_connection_opts = {
      :provider                     => 'vsphere',
      :vsphere_username             => provider_username,
      :vsphere_password             => provider_password,
      :vsphere_server               => provider_host,
      :vsphere_port                 => provider_port,
      :vsphere_expected_pubkey_hash => provider_pubkey_hash
  }

  @node_creation_opts = {
      'name'          => nodename,
      'datacenter'    => datacenter.name,
      'template_path' => image,
      'dest_folder'   => dest_folder || '',
      'numCPUs'       => cores,
      'memoryMB'      => memory.mebibytes.to_i
  }

  if infrastructure_properties.cluster
    @node_creation_opts = {
        'resource_pool' => [infrastructure_properties.cluster, '']
    }.merge(@node_creation_opts)
  end

  @wait_params = {
      :maxtime        => 300,
      :delay          => 10
  }.merge(wait_params)
end

Private Instance Methods

add_node_nics(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 222
def add_node_nics(node_instance)
  ::Dopv::log.info("Node #{nodename}: Trying to add interfaces.")

  # Remove all interfaces defined by the template
  remove_node_nics(node_instance) do |node, interface|
    Dopv::log.debug("Node #{nodename}: Remove #{interface.name} (#{interface.key}, #{interface.type}, #{interface.network}).")
    deviceChange = compute_provider.send(:create_interface, interface, interface.key, :remove, :datacenter => datacenter.name)
    # workaround in case the network of the interface is nil
    if deviceChange[:device][:backing].respond_to?(:deviceName) and deviceChange[:device][:backing][:deviceName].nil?
      deviceChange[:device][:backing][:deviceName] = ''
    end
    compute_provider.vm_reconfig_hardware('instance_uuid' => interface.server_id,
                                          'hardware_spec' => {
                                              'deviceChange'=>[deviceChange]
                                          })
  end

  # Create interfaces from scratch
  interfaces_config.each do |i|
    log_msg = i.virtual_switch.nil? ?
                  "Node #{nodename}: Creating interface #{i.name} in #{i.network}." :
                  "Node #{nodename}: Creating interface #{i.name} in #{i.network} (#{i.virtual_switch})."
    ::Dopv::log.debug(log_msg)
    attrs = {
        :name => i.name,
        :datacenter => node_instance.datacenter,
        :network => i.network,
        :virtualswitch => i.virtual_switch,
        :type => 'VirtualVmxnet3'
    }
    add_node_nic(node_instance, attrs)
  end
end
add_node_volume(node_instance, config) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 256
def add_node_volume(node_instance, config)
  volume = node_instance.volumes.create(
      :datastore => config.pool,
      :size => config.size.kibibytes.to_i,
      :mode => 'persistent',
      :thin => config.thin?
  )
  node_instance.volumes.reload
  volume.name = config.name
  volume
end
attach_node_volume(node_instance, volume) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 274
def attach_node_volume(node_instance, volume)
  backing_info = RbVmomi::VIM::VirtualDiskFlatVer2BackingInfo.new(
      :datastore => volume.pool,
      :fileName => volume.id,
      :diskMode => 'persistent'
  )

  virtual_disk = RbVmomi::VIM::VirtualDisk.new(
      :controllerKey => node_instance.scsi_controller.key,
      :unitNumber => node_instance.volumes.collect { |v| v.unit_number }.max + 1,
      :key => -1,
      :backing => backing_info,
      :capacityInKB => volume.size * 1048576
  )

  device_spec = RbVmomi::VIM::VirtualDeviceConfigSpec.new(
      :operation => :add,
      :device => virtual_disk
  )

  config_spec = RbVmomi::VIM::VirtualMachineConfigSpec.new(:deviceChange => [device_spec])

  reconfig_node_task(node_instance, config_spec)

  node_instance.volumes.reload
end
create_node_instance() click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 75
def create_node_instance
  ::Dopv::log.info("Node #{nodename}: Creating node instance.")
  @node_creation_opts['datastore'] = infrastructure_properties.default_pool unless infrastructure_properties.default_pool.nil?
  @node_creation_opts['transform'] = thin_clone ? :sparse : :flat unless thin_clone.nil?

  vm = compute_provider.vm_clone(
      @node_creation_opts.merge(
          'power_on'  => false,
          'wait'      => true
      )
  )

  compute_provider.servers.get(vm['new_vm']['id'])
end
customization_domain?(domain) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 94
def customization_domain?(domain)
  cred = customization_domain_credential(domain)
  !cred.nil?
end
customization_domain_credential(domain) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 90
def customization_domain_credential(domain)
  credentials.find { |c| c.type == :username_password && c.username.start_with?(domain) }
end
customization_domain_password(domain) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 99
def customization_domain_password(domain)
  cred = customization_domain_credential(domain)
  cred.nil? ? nil : cred.password
end
customization_domain_username(domain) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 104
def customization_domain_username(domain)
  cred = customization_domain_credential(domain)
  cred.nil? ? nil : cred.username.split('\\').last
end
customize_node_instance(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 109
def customize_node_instance(node_instance)
  ::Dopv::log.info("Node #{nodename}: Customizing node.")
  # Settings related to each network interface
  ip_settings = interfaces_config.collect do |i|
    ip_setting = ::RbVmomi::VIM::CustomizationIPSettings.new
    if i.ip == :dhcp
      ip_setting.ip = ::RbVmomi::VIM::CustomizationDhcpIpGenerator.new
    else
      ip_setting.ip = ::RbVmomi::VIM::CustomizationFixedIp('ipAddress' => i.ip)
      ip_setting.subnetMask = i.netmask
      ip_setting.gateway = [i.gateway] if i.set_gateway?
    end
    ip_setting
  end

  # Adapters mapping
  nic_setting_map = ip_settings.collect { |s| RbVmomi::VIM::CustomizationAdapterMapping.new('adapter' => s) }

  # Global network settings
  global_ip_settings = RbVmomi::VIM::CustomizationGlobalIPSettings.new(
      :dnsServerList => dns.name_servers,
      :dnsSuffixList => dns.search_domains
  )

  # Identity settings
  identity_settings = case guest_id_to_os_family(node_instance)

                      when :linux

                        RbVmomi::VIM::CustomizationLinuxPrep.new(
                            :domain => domainname,
                            :hostName => RbVmomi::VIM::CustomizationFixedName.new(:name => hostname)
                        )

                      when :windows

                        raise ProviderError, "credentials 'Administrator' is missing in plan" unless administrator_password
                        raise ProviderError, "'organization_name' is missing in plan" unless organization_name

                        password_settings = (RbVmomi::VIM::CustomizationPassword.new(
                            :plainText => true,
                            :value => administrator_password
                        ) rescue nil)

                        # Declare identification
                        domain ||= customization_domain?(domainname) ? domainname : nil
                        workgroup ||= nil
                        if domain
                          customization_domain_password_settings = (RbVmomi::VIM::CustomizationPassword.new(
                              :plainText => true,
                              :value => customization_domain_password(domain)
                          ) rescue nil)
                          customization_id = RbVmomi::VIM::CustomizationIdentification.new(
                              :joinDomain => domain,
                              :domainAdmin => customization_domain_username(domain),
                              :domainAdminPassword => customization_domain_password_settings
                          )
                        elsif workgroup
                          customization_id = RbVmomi::VIM::CustomizationIdentification.new(
                              :joinWorkgroup => workgroup
                          )
                        else
                          customization_id = RbVmomi::VIM::CustomizationIdentification.new(
                              :domainAdmin => nil,
                              :domainAdminPassword => nil,
                              :joinDomain => nil
                          )
                        end

                        RbVmomi::VIM::CustomizationSysprep.new(
                            :guiRunOnce => nil,
                            :guiUnattended => RbVmomi::VIM::CustomizationGuiUnattended.new(
                                :autoLogon => false,
                                :autoLogonCount => 1,
                                :password => password_settings,
                                :timeZone => timezone_to_index(timezone)
                            ),
                            :identification => customization_id,
                            :userData => RbVmomi::VIM::CustomizationUserData.new(
                                :computerName => RbVmomi::VIM::CustomizationFixedName.new(:name => hostname),
                                :fullName => administrator_fullname,
                                :orgName => organization_name,
                                :productId => (!product_id ? '' : product_id)
                            )
                        )
                      else
                        raise ProviderError, "Unsupported guest OS type '#{node_instance.guest_id.to_sym}'"
                      end

  custom_spec = RbVmomi::VIM::CustomizationSpec.new(
      :identity => identity_settings,
      :globalIPSettings => global_ip_settings,
      :nicSettingMap => nic_setting_map
  )
  custom_spec.options = RbVmomi::VIM::CustomizationWinOptions.new(
      :changeSID => true,
      :deleteAccounts => false
  ) if guest_id_to_os_family(node_instance) == :windows

  custom_spec
end
customize_node_task(node_instance, customization_spec) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 395
def customize_node_task(node_instance, customization_spec)
  node_ref = compute_provider.send(:get_vm_ref, node_instance.id)
  node_ref.CustomizeVM_Task(:spec => customization_spec).wait_for_completion
end
dest_folder() click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 49
def dest_folder
  @dest_folder ||= infrastructure_properties.dest_folder
end
destroy_node_volume(node_instance, volume) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 268
def destroy_node_volume(node_instance, volume)
  volume_instance = node_instance.volumes.find { |v| v.filename == volume.id }
  volume_instance.destroy
  node_instance.volumes.reload
end
detach_node_volume(node_instance, volume) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 301
def detach_node_volume(node_instance, volume)
  volume = node_instance.volumes.all.find { |v| v.filename == volume.id }

  virtual_disk = RbVmomi::VIM::VirtualDisk.new(
      :controllerKey => volume.server.scsi_controller.key,
      :unitNumber => volume.unit_number,
      :key => volume.key,
      :capacityInKB => volume.size
  )

  device_spec = RbVmomi::VIM::VirtualDeviceConfigSpec.new(
      :operation => :remove,
      :device => virtual_disk
  )

  config_spec = RbVmomi::VIM::VirtualMachineConfigSpec.new(:deviceChange => [device_spec])

  reconfig_node_task(node_instance, config_spec)

  node_instance.volumes.reload
end
get_node_ip_addresses(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 416
def get_node_ip_addresses(node_instance)
  begin
    is_windows = guest_id_to_os_family(node_instance) == :windows
    raise ProviderError, "VMware Tools not installed" unless node_instance.tools_installed?

    ::Dopv::log.debug("Node #{nodename}: Waiting on VMware Tools for #{@wait_params[:maxtime]} seconds.")
    reload_node_instance(node_instance)
    node_instance.wait_for(@wait_params[:maxtime]){ready?}
    node_instance.wait_for(@wait_params[:maxtime]){tools_running?}
    # raise ProviderError, "VMware Tools Version not supported" if node_instance.tools_version.to_sym == :guestToolsUnmanaged

    node_ref = compute_provider.send(:get_vm_ref, node_instance.id)
    node_ref_guest_net = nil
    start_time = Time.now.to_f
    is_connected = false
    node_ref.guest.net.each do |i| is_connected ||= i.connected end
    raise ProviderError, "No connected network interface available" unless is_connected

    while (Time.now.to_f - start_time) < @wait_params[:maxtime]
      unless node_ref.guest_ip
        sleep @wait_params[:delay]
      else
        node_ref_guest_net = node_ref.guest.net.map(&:ipAddress).flatten.uniq.compact.select{|i| i.match(Resolv::IPv4::Regex) && !(i.start_with?('169.254.') && is_windows)}
        unless node_ref_guest_net.any?
          sleep @wait_params[:delay]
        else
          break
        end
      end
    end
    raise ProviderError, "VMware Tools not ready yet" unless node_ref_guest_net
    node_ref_guest_net

  rescue Exception => e
    ::Dopv::log.debug("Node #{nodename}: Unable to obtain IP Addresses, Error: #{e.message}.")
    [node_instance.public_ip_address].compact.select{|i| i.match(Resolv::IPv4::Regex) && !(i.start_with?('169.254.') && is_windows)}
  end
end
guest_id_to_os_family(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 334
def guest_id_to_os_family(node_instance)
  # Based on http://pubs.vmware.com/vsphere-50/index.jsp?topic=/com.vmware.wssdk.apiref.doc_50/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html
  lookup_table = {
      :asianux8_64Guest       => :linux,
      :centos8_64Guest        => :linux,
      :darwin17_64Guest       => :linux,
      :darwin18_64Guest       => :linux,
      :debian6_64Guest        => :linux,
      :debian6_Guest          => :linux,
      :freebsd11Guest         => :linux,
      :freebsd11_64Guest      => :linux,
      :freebsd12Guest         => :linux,
      :freebsd12_64Guest      => :linux,
      :rhel4_64Guest          => :linux,
      :rhel4Guest             => :linux,
      :rhel5_64Guest          => :linux,
      :rhel5Guest             => :linux,
      :rhel6_64Guest          => :linux,
      :rhel6Guest             => :linux,
      :rhel7_64Guest          => :linux,
      :rhel7Guest             => :linux,
      :rhel8_64Guest          => :linux,
      :sles11_64Guest         => :linux,
      :sles12_64Guest         => :linux,
      :sles13_64Guest         => :linux,
      :sles14_64Guest         => :linux,
      :sles15_64Guest         => :linux,
      :oracleLinuxGuest       => :linux,
      :oracleLinux64Guest     => :linux,
      :oracleLinux8_64Guest   => :linux,
      :other4xLinux64Guest    => :linux,
      :other4xLinuxGuest      => :linux,
      :ubuntu64Guest          => :linux,
      :ubuntuGuest            => :linux,
      :windows7_64Guest       => :windows,
      :windows7Guest          => :windows,
      :windows7Server64Guest  => :windows,
      :windows8_64Guest       => :windows,
      :windows8Guest          => :windows,
      :windows8Server64Guest  => :windows,
      :windows9_64Guest       => :windows,
      :windows9Guest          => :windows,
      :windows9Server64Guest  => :windows,
      :windows10_64Guest      => :windows,
      :windows10Guest         => :windows,
      :windows10Server64Guest => :windows,
      :'windows7srv-64'       => :windows,
      :'windows8srv-64'       => :windows,
      :'windows9srv-64'       => :windows,
      :'windows10srv-64'      => :windows
  }

  node_instance.guest_id ?
      lookup_table[node_instance.guest_id.to_sym] : nil
end
node_instance_stopped?(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 71
def node_instance_stopped?(node_instance)
  !node_instance.ready?
end
provider_pubkey_hash() click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 400
def provider_pubkey_hash
  unless @provider_pubkey_hash
    connection = ::RbVmomi::VIM.new(
        :host     => provider_host,
        :port     => provider_port,
        :ssl      => provider_ssl?,
        :ns       => 'urn:vim25',
        :rev      => '4.0',
        :insecure => true
    )
    @provider_pubkey_hash ||= ::Digest::SHA2.hexdigest(connection.http.peer_cert.public_key.to_s)
    connection.close
  end
  @provider_pubkey_hash
end
reconfig_node_task(node_instance, reconfig_spec) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 390
def reconfig_node_task(node_instance, reconfig_spec)
  node_ref = compute_provider.send(:get_vm_ref, node_instance.id)
  node_ref.ReconfigVM_Task(:spec => reconfig_spec).wait_for_completion
end
record_node_data_volume(volume) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 323
def record_node_data_volume(volume)
  ::Dopv::log.debug("Node #{nodename}: Recording volume #{volume.name} into DB.")
  volume = {
      :name => volume.name,
      :id   => volume.filename,
      :pool => volume.datastore,
      :size => volume.size * 1048576 # Size must be in gibibytes
  }
  super(volume)
end
refresh_node_instance(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 455
def refresh_node_instance(node_instance)
  return unless tags

  begin
    require 'json'
    require 'rest-client'

    ::Dopv::log.debug("Node #{nodename}: Trying to associate tags: #{tags} (requires minimum VMware vSphere 6.0).")

    parse_json_response_block = Proc.new do |response, request, result|
      JSON.parse(response).fetch('value')
    end

    base_uri = "https://#{provider_host}:#{provider_port}/rest/com/vmware/cis"
    session_header = {'vmware-use-header-authn' => ('0'..'z').to_a.shuffle.first(32).join, 'Content-Type' => 'application/json', 'Accept' => 'application/json'}

    session_token = RestClient::Request.execute method: :post, url: "#{base_uri}/session", user: provider_username, password: provider_password, headers: session_header, verify_ssl: false, &parse_json_response_block
    session_header.merge!({'vmware-api-session-id' => session_token})
    session_header.merge!({:cookies => {'vmware-api-session-id' => session_header.fetch('vmware-api-session-id')}})

    # search tags
    found_tags = []
    all_tags = RestClient::Request.execute method: :get, url: "#{base_uri}/tagging/tag", headers: session_header, verify_ssl: false, &parse_json_response_block

    all_tags.each do |tag_id|
      tag = RestClient::Request.execute method: :get, url: "#{base_uri}/tagging/tag/id:#{tag_id}", headers: session_header, verify_ssl: false, &parse_json_response_block
      if tags.include?(tag.fetch('name'))
        ::Dopv::log.debug("Node #{nodename}: Tag '#{tag.fetch('name')}' found.")
        found_tags << tag
      end
    end

    # ensure all tags found
    tags.each do |tag_name|
      result = found_tags.select { |tag| tag.fetch('name') == tag_name }
      ::Dopv::log.warn("Node #{nodename}: Tag '#{tag_name}' not found! (Tag created in '#{provider_host}' and User '#{provider_username}' authorized?)") if result.empty?
    end

    found_tags.each do |tag|
      payload = {'object_id' => {'type' => 'VirtualMachine', 'id' => node_instance.mo_ref}}.to_json
      ::Dopv::log.debug("Node #{nodename}: Associate tag '#{tag.fetch('name')}' to '#{node_instance.mo_ref}'.")
      _assign = RestClient::Request.execute method: :post, url: "#{base_uri}/tagging/tag-association/id:#{tag.fetch('id')}?~action=attach", headers: session_header, verify_ssl: false, payload: payload
    end

  rescue Exception => e
    ::Dopv::log.debug("Node #{nodename}: Unable to assign tags, Error: #{e.message}.")
  end
end
start_node_instance(node_instance) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 211
def start_node_instance(node_instance)
  customization_spec = super(node_instance)
  customize_node_task(node_instance, customization_spec)
  node_instance.start
end
stop_node_instance(node_instance, options={}) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 217
def stop_node_instance(node_instance, options={})
  super(node_instance, options)
  node_instance.wait_for(@wait_params[:maxtime]){power_state.to_sym == :poweredOff}
end
timezone() click to toggle source
Calls superclass method
# File lib/dopv/infrastructure/providers/vsphere.rb, line 53
def timezone
  super || '085'
end
timezone_to_index(timezone) click to toggle source
# File lib/dopv/infrastructure/providers/vsphere.rb, line 57
def timezone_to_index(timezone)
  lookup_table = {
      'Europe/Zurich'         => 110,
      :default                => 110
  }

  unless timezone.match(/^[1-9][0-9][0-9]$/)
    (lookup_table.key? timezone) ?
        lookup_table[timezone].to_s : lookup_table[:default].to_s
  else
    timezone
  end
end