require 'digest/sha2' module Fog module Compute class Vsphere < Fog::Service requires :vsphere_username, :vsphere_password, :vsphere_server recognizes :vsphere_port, :vsphere_path, :vsphere_ns recognizes :vsphere_rev, :vsphere_ssl, :vsphere_expected_pubkey_hash model_path 'fog/vsphere/models/compute' model :server collection :servers model :servertype collection :servertypes model :datacenter collection :datacenters model :interface collection :interfaces model :interfacetype collection :interfacetypes model :volume collection :volumes model :template collection :templates model :cluster collection :clusters model :resource_pool collection :resource_pools model :network collection :networks model :datastore collection :datastores model :folder collection :folders model :customvalue collection :customvalues model :customfield collection :customfields request_path 'fog/vsphere/requests/compute' request :current_time request :list_virtual_machines request :vm_power_off request :vm_power_on request :vm_reboot request :vm_clone request :vm_destroy request :vm_migrate request :list_datacenters request :get_datacenter request :list_clusters request :get_cluster request :list_resource_pools request :get_resource_pool request :list_networks request :get_network request :list_datastores request :get_datastore request :list_templates request :get_template request :get_folder request :list_folders request :create_vm request :list_vm_interfaces request :modify_vm_interface request :list_vm_volumes request :get_virtual_machine request :vm_reconfig_hardware request :vm_reconfig_memory request :vm_reconfig_cpus request :vm_config_vnc request :create_folder request :list_server_types request :get_server_type request :list_interface_types request :get_interface_type request :list_vm_customvalues request :list_customfields module Shared attr_reader :vsphere_is_vcenter attr_reader :vsphere_rev attr_reader :vsphere_server attr_reader :vsphere_username protected ATTR_TO_PROP = { :id => 'config.instanceUuid', :name => 'name', :uuid => 'config.uuid', :template => 'config.template', :parent => 'parent', :hostname => 'summary.guest.hostName', :operatingsystem => 'summary.guest.guestFullName', :ipaddress => 'guest.ipAddress', :power_state => 'runtime.powerState', :connection_state => 'runtime.connectionState', :hypervisor => 'runtime.host', :tools_state => 'guest.toolsStatus', :tools_version => 'guest.toolsVersionStatus', :memory_mb => 'config.hardware.memoryMB', :cpus => 'config.hardware.numCPU', :corespersocket => 'config.hardware.numCoresPerSocket', :overall_status => 'overallStatus', :guest_id => 'config.guestId', } def convert_vm_view_to_attr_hash(vms) vms = @connection.serviceContent.propertyCollector.collectMultiple(vms,*ATTR_TO_PROP.values.uniq) vms.map { |vm| props_to_attr_hash(*vm) } end # Utility method to convert a VMware managed object into an attribute hash. # This should only really be necessary for the real class. # This method is expected to be called by the request methods # in order to massage VMware Managed Object References into Attribute Hashes. def convert_vm_mob_ref_to_attr_hash(vm_mob_ref) return nil unless vm_mob_ref props = vm_mob_ref.collect!(*ATTR_TO_PROP.values.uniq) props_to_attr_hash vm_mob_ref, props end def props_to_attr_hash vm_mob_ref, props # NOTE: Object.tap is in 1.8.7 and later. # Here we create the hash object that this method returns, but first we need # to add a few more attributes that require additional calls to the vSphere # API. The hypervisor name and mac_addresses attributes may not be available # so we need catch any exceptions thrown during lookup and set them to nil. # # The use of the "tap" method here is a convenience, it allows us to update the # hash object without explicitly returning the hash at the end of the method. Hash[ATTR_TO_PROP.map { |k,v| [k.to_s, props[v]] }].tap do |attrs| attrs['id'] ||= vm_mob_ref._ref attrs['mo_ref'] = vm_mob_ref._ref # The name method "magically" appears after a VM is ready and # finished cloning. if attrs['hypervisor'].kind_of?(RbVmomi::VIM::HostSystem) host = attrs['hypervisor'] attrs['datacenter'] = Proc.new { parent_attribute(host.path, :datacenter)[1] rescue nil } attrs['cluster'] = Proc.new { parent_attribute(host.path, :cluster)[1] rescue nil } attrs['hypervisor'] = Proc.new { host.name rescue nil } attrs['resource_pool'] = Proc.new {(vm_mob_ref.resourcePool || host.resourcePool).name rescue nil} end # This inline rescue catches any standard error. While a VM is # cloning, a call to the macs method will throw and NoMethodError attrs['mac_addresses'] = Proc.new {vm_mob_ref.macs rescue nil} # Rescue nil to catch testing while vm_mob_ref isn't reaL?? attrs['path'] = "/"+attrs['parent'].path.map(&:last).join('/') rescue nil attrs['relative_path'] = (attrs['path'].split('/').reject {|e| e.empty?} - ["Datacenters", attrs['datacenter'], "vm"]).join("/") rescue nil end end # returns the parent object based on a type # provides both real RbVmomi object and its name. # e.g. #[Datacenter("datacenter-2"), "dc-name"] def parent_attribute path, type element = case type when :datacenter RbVmomi::VIM::Datacenter when :cluster RbVmomi::VIM::ClusterComputeResource when :host RbVmomi::VIM::HostSystem else raise "Unknown type" end path.select {|x| x[0].is_a? element}.flatten rescue nil end # returns vmware managed obj id string def managed_obj_id obj obj.to_s.match(/\("([^"]+)"\)/)[1] end def is_uuid?(id) !(id =~ /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/).nil? end end class Mock include Shared def self.data @data ||= Hash.new do |hash, key| hash[key] = { :servers => { "5032c8a5-9c5e-ba7a-3804-832a03e16381" => { "resource_pool" => "Resources", "memory_mb" => 2196, "mac_addresses" => { "Network adapter 1" => "00:50:56:a9:00:28" }, "power_state" => "poweredOn", "cpus" => 1, "hostname" => "dhcp75-197.virt.bos.redhat.com", "mo_ref" => "vm-562", "connection_state" => "connected", "overall_status" => "green", "datacenter" => "Solutions", "volumes" => [{ "id" => "6000C29c-a47d-4cd9-5249-c371de775f06", "datastore" => "Storage1", "mode" => "persistent", "size" => 8388608, "thin" => true, "name" => "Hard disk 1", "filename" => "[Storage1] rhel6-mfojtik/rhel6-mfojtik.vmdk", "size_gb" => 8 }], "interfaces" => [{"mac" => "00:50:56:a9:00:28", "network" => "VM Network", "name" => "Network adapter 1", "status" => "ok", "summary" => "VM Network", }], "hypervisor" => "gunab.puppetlabs.lan", "guest_id" => "rhel6_64Guest", "tools_state" => "toolsOk", "cluster" => "Solutionscluster", "name" => "rhel64", "operatingsystem" => "Red Hat Enterprise Linux 6 (64-bit)", "path" => "/Datacenters/Solutions/vm", "uuid" => "4229f0e9-bfdc-d9a7-7bac-12070772e6dc", "instance_uuid" => "5032c8a5-9c5e-ba7a-3804-832a03e16381", "id" => "5032c8a5-9c5e-ba7a-3804-832a03e16381", "tools_version" => "guestToolsUnmanaged", "ipaddress" => "192.168.100.184", "template" => false }, "502916a3-b42e-17c7-43ce-b3206e9524dc" => { "resource_pool" => "Resources", "memory_mb" => 512, "power_state" => "poweredOn", "mac_addresses" => { "Network adapter 1" => "00:50:56:a9:00:00" }, "hostname" => nil, "cpus" => 1, "connection_state" => "connected", "mo_ref" => "vm-621", "overall_status" => "green", "datacenter" => "Solutions", "volumes" => [{"thin" => false, "size_gb" => 10, "datastore" => "datastore1", "filename" => "[datastore1] i-1342439683/i-1342439683.vmdk", "size" => 10485762, "name" => "Hard disk 1", "mode" => "persistent", "id" => "6000C29b-f364-d073-8316-8e98ac0a0eae" }], "interfaces" => [{ "summary" => "VM Network", "mac" => "00:50:56:a9:00:00", "status" => "ok", "network" => "VM Network", "name" => "Network adapter 1" }], "hypervisor" => "gunab.puppetlabs.lan", "guest_id" => nil, "cluster" => "Solutionscluster", "tools_state" => "toolsNotInstalled", "name" => "i-1342439683", "operatingsystem" => nil, "path" => "/", "tools_version" => "guestToolsNotInstalled", "uuid" => "4229e0de-30cb-ceb2-21f9-4d8d8beabb52", "instance_uuid" => "502916a3-b42e-17c7-43ce-b3206e9524dc", "id" => "502916a3-b42e-17c7-43ce-b3206e9524dc", "ipaddress" => nil, "template" => false }, "5029c440-85ee-c2a1-e9dd-b63e39364603" => { "resource_pool" => "Resources", "memory_mb" => 2196, "power_state" => "poweredOn", "mac_addresses" => { "Network adapter 1" => "00:50:56:b2:00:af" }, "hostname" => "centos56gm.localdomain", "cpus" => 1, "connection_state" => "connected", "mo_ref" => "vm-715", "overall_status" => "green", "datacenter" => "Solutions", "hypervisor" => "gunab.puppetlabs.lan", "guest_id" => "rhel6_64Guest", "cluster" => "Solutionscluster", "tools_state" => "toolsOk", "name" => "jefftest", "operatingsystem" => "Red Hat Enterprise Linux 6 (64-bit)", "path" => "/", "tools_version" => "guestToolsUnmanaged", "ipaddress" => "192.168.100.187", "uuid" => "42329da7-e8ab-29ec-1892-d6a4a964912a", "instance_uuid" => "5029c440-85ee-c2a1-e9dd-b63e39364603", "id" => "5029c440-85ee-c2a1-e9dd-b63e39364603", "template" => false } }, :datacenters => { "Solutions" => {:name => "Solutions", :status => "grey"} } } end end def initialize(options={}) require 'rbvmomi' @vsphere_username = options[:vsphere_username] @vsphere_password = 'REDACTED' @vsphere_server = options[:vsphere_server] @vsphere_expected_pubkey_hash = options[:vsphere_expected_pubkey_hash] @vsphere_is_vcenter = true @vsphere_rev = '4.0' end def data self.class.data[@vsphere_username] end def reset_data self.class.data.delete(@vsphere_username) end end class Real include Shared def initialize(options={}) require 'rbvmomi' @vsphere_username = options[:vsphere_username] @vsphere_password = options[:vsphere_password] @vsphere_server = options[:vsphere_server] @vsphere_port = options[:vsphere_port] || 443 @vsphere_path = options[:vsphere_path] || '/sdk' @vsphere_ns = options[:vsphere_ns] || 'urn:vim25' @vsphere_rev = options[:vsphere_rev] || '4.0' @vsphere_ssl = options[:vsphere_ssl] || true @vsphere_expected_pubkey_hash = options[:vsphere_expected_pubkey_hash] @vsphere_must_reauthenticate = false @vsphere_is_vcenter = nil @connection = nil connect negotiate_revision(options[:vsphere_rev]) authenticate end def reload connect # Check if the negotiation was ever run if @vsphere_is_vcenter.nil? negotiate end authenticate end private def negotiate_revision(revision = nil) # Negotiate the API revision if not revision rev = @connection.serviceContent.about.apiVersion @connection.rev = [ rev, ENV['FOG_VSPHERE_REV'] || '4.1' ].min end @vsphere_is_vcenter = @connection.serviceContent.about.apiType == "VirtualCenter" @vsphere_rev = @connection.rev end def connect # This is a state variable to allow digest validation of the SSL cert bad_cert = false loop do begin @connection = RbVmomi::VIM.new :host => @vsphere_server, :port => @vsphere_port, :path => @vsphere_path, :ns => @vsphere_ns, :rev => @vsphere_rev, :ssl => @vsphere_ssl, :insecure => bad_cert break rescue OpenSSL::SSL::SSLError raise if bad_cert bad_cert = true end end if bad_cert then validate_ssl_connection end end def authenticate begin @connection.serviceContent.sessionManager.Login :userName => @vsphere_username, :password => @vsphere_password rescue RbVmomi::VIM::InvalidLogin => e raise Fog::Vsphere::Errors::ServiceError, e.message end end # Verify a SSL certificate based on the hashed public key def validate_ssl_connection pubkey = @connection.http.peer_cert.public_key pubkey_hash = Digest::SHA2.hexdigest(pubkey.to_s) expected_pubkey_hash = @vsphere_expected_pubkey_hash if pubkey_hash != expected_pubkey_hash then raise Fog::Vsphere::Errors::SecurityError, "The remote system presented a public key with hash #{pubkey_hash} but we're expecting a hash of #{expected_pubkey_hash || ''}. If you are sure the remote system is authentic set vsphere_expected_pubkey_hash: in ~/.fog" end end end end end end