From 1ef0d96be333688b6139168d196028f93bdc2381 Mon Sep 17 00:00:00 2001 From: hartsocks Date: Tue, 21 May 2013 20:38:28 -0700 Subject: [PATCH] VNC console does not work with VCDriver Introduces get_vnc_console_vcenter as a way to get a modified vnc_console object that connects to the ESXi host underneath vCenter control. * adds new fake classes for testing * documents classes, methods & API logic fixes bug: #1178369 (cherry picked from commit 271fc68c1852e3764b7c64d71cd28ac3f803ecba) Conflicts: nova/virt/vmwareapi/fake.py Change-Id: I48430cb9bc9615e02ca9af235f97853f3f0bdafd --- nova/tests/test_vmwareapi.py | 8 +- nova/tests/test_vmwareapi_vm_util.py | 96 ++++++++++++++++++++++++ nova/virt/vmwareapi/driver.py | 22 ++++++ nova/virt/vmwareapi/fake.py | 79 +++++++++++++++++--- nova/virt/vmwareapi/vm_util.py | 106 +++++++++++++++++++++++++++ nova/virt/vmwareapi/vmops.py | 20 +++++ 6 files changed, 314 insertions(+), 17 deletions(-) diff --git a/nova/tests/test_vmwareapi.py b/nova/tests/test_vmwareapi.py index 205add2f630..b8039f2d22c 100644 --- a/nova/tests/test_vmwareapi.py +++ b/nova/tests/test_vmwareapi.py @@ -71,7 +71,7 @@ def setUp(self): vmwareapi_fake.reset() db_fakes.stub_out_db_instance_api(self.stubs) stubs.set_stubs(self.stubs) - self.conn = driver.VMwareESXDriver(None, False) + self.conn = driver.VMwareVCDriver(None, False) # NOTE(vish): none of the network plugging code is actually # being tested self.network_info = utils.get_test_network_info(legacy_model=False) @@ -379,14 +379,10 @@ def test_get_vnc_console_non_existent(self): self.instance) def test_get_vnc_console(self): - vm_ref = fake_vm_ref() self._create_instance_in_the_db() self._create_vm() - self.mox.StubOutWithMock(self.conn._vmops, '_get_vnc_port') - self.conn._vmops._get_vnc_port(mox.IgnoreArg()).AndReturn(5910) - self.mox.ReplayAll() vnc_dict = self.conn.get_vnc_console(self.instance) - self.assertEquals(vnc_dict['host'], "test_url") + self.assertEquals(vnc_dict['host'], "ha-host") self.assertEquals(vnc_dict['port'], 5910) def test_host_ip_addr(self): diff --git a/nova/tests/test_vmwareapi_vm_util.py b/nova/tests/test_vmwareapi_vm_util.py index eda2c25f922..b7eff0a76d4 100644 --- a/nova/tests/test_vmwareapi_vm_util.py +++ b/nova/tests/test_vmwareapi_vm_util.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import collections + from nova import exception from nova import test from nova.virt.vmwareapi import fake @@ -32,9 +34,11 @@ def _call_method(self, *args): class VMwareVMUtilTestCase(test.TestCase): def setUp(self): super(VMwareVMUtilTestCase, self).setUp() + fake.reset() def tearDown(self): super(VMwareVMUtilTestCase, self).tearDown() + fake.reset() def test_get_datastore_ref_and_name(self): result = vm_util.get_datastore_ref_and_name( @@ -53,3 +57,95 @@ def test_get_datastore_ref_and_name_without_datastore(self): self.assertRaises(exception.DatastoreNotFound, vm_util.get_datastore_ref_and_name, fake_session(), cluster="fake-cluster") + + def test_get_host_ref_from_id(self): + + fake_host_sys = fake.HostSystem( + fake.ManagedObjectReference("HostSystem", "host-123")) + + fake_host_id = fake_host_sys.obj.value + fake_host_name = "ha-host" + + ref = vm_util.get_host_ref_from_id( + fake_session([fake_host_sys]), fake_host_id, ['name']) + + self.assertIsInstance(ref, fake.HostSystem) + self.assertEqual(fake_host_id, ref.obj.value) + + host_name = vm_util.get_host_name_from_host_ref(ref) + + self.assertEquals(fake_host_name, host_name) + + def test_get_host_name_for_vm(self): + + fake_vm = fake.ManagedObject( + "VirtualMachine", fake.ManagedObjectReference( + "vm-123", "VirtualMachine")) + fake_vm.propSet.append( + fake.Property('name', 'vm-123')) + + vm_ref = vm_util.get_vm_ref_from_name( + fake_session([fake_vm]), 'vm-123') + + self.assertIsNotNone(vm_ref) + + fake_results = [ + fake.ObjectContent( + None, [ + fake.Property('runtime.host', + fake.ManagedObjectReference( + 'host-123', 'HostSystem')) + ])] + + host_id = vm_util.get_host_id_from_vm_ref( + fake_session(fake_results), vm_ref) + + self.assertEqual('host-123', host_id) + + def test_property_from_property_set(self): + + ObjectContent = collections.namedtuple('ObjectContent', ['propSet']) + DynamicProperty = collections.namedtuple('Property', ['name', 'val']) + MoRef = collections.namedtuple('Val', ['value']) + + results_good = [ + ObjectContent(propSet=[ + DynamicProperty(name='name', val=MoRef(value='vm-123'))]), + ObjectContent(propSet=[ + DynamicProperty(name='foo', val=MoRef(value='bar1')), + DynamicProperty( + name='runtime.host', val=MoRef(value='host-123')), + DynamicProperty(name='foo', val=MoRef(value='bar2')), + ]), + ObjectContent(propSet=[ + DynamicProperty( + name='something', val=MoRef(value='thing'))]), ] + + results_bad = [ + ObjectContent(propSet=[ + DynamicProperty(name='name', val=MoRef(value='vm-123'))]), + ObjectContent(propSet=[ + DynamicProperty(name='foo', val='bar1'), + DynamicProperty(name='foo', val='bar2'), ]), + ObjectContent(propSet=[ + DynamicProperty( + name='something', val=MoRef(value='thing'))]), ] + + prop = vm_util.property_from_property_set( + 'runtime.host', results_good) + self.assertIsNotNone(prop) + value = prop.val.value + self.assertEqual('host-123', value) + + prop2 = vm_util.property_from_property_set( + 'runtime.host', results_bad) + self.assertIsNone(prop2) + + prop3 = vm_util.property_from_property_set('foo', results_good) + self.assertIsNotNone(prop3) + val3 = prop3.val.value + self.assertEqual('bar1', val3) + + prop4 = vm_util.property_from_property_set('foo', results_bad) + self.assertIsNotNone(prop4) + self.assertEqual('bar1', prop4.val) diff --git a/nova/virt/vmwareapi/driver.py b/nova/virt/vmwareapi/driver.py index 9b9cb301b75..014ca0f723b 100755 --- a/nova/virt/vmwareapi/driver.py +++ b/nova/virt/vmwareapi/driver.py @@ -126,6 +126,12 @@ def __str__(self): class VMwareESXDriver(driver.ComputeDriver): """The ESX host connection object.""" + # VMwareAPI has both ESXi and vCenter API sets. + # The ESXi API are a proper sub-set of the vCenter API. + # That is to say, nearly all valid ESXi calls are + # valid vCenter calls. There are some small edge-case + # exceptions regarding VNC, CIM, User management & SSO. + def __init__(self, virtapi, read_only=False, scheme="https"): super(VMwareESXDriver, self).__init__(virtapi) @@ -335,6 +341,14 @@ def unplug_vifs(self, instance, network_info): class VMwareVCDriver(VMwareESXDriver): """The ESX host connection object.""" + # The vCenter driver includes several additional VMware vSphere + # capabilities that include API that act on hosts or groups of + # hosts in clusters or non-cluster logical-groupings. + # + # vCenter is not a hypervisor itself, it works with multiple + # hypervisor host machines and their guests. This fact can + # subtly alter how vSphere and OpenStack interoperate. + def __init__(self, virtapi, read_only=False, scheme="https"): super(VMwareVCDriver, self).__init__(virtapi) if not self._cluster_name: @@ -391,6 +405,14 @@ def live_migration(self, context, instance_ref, dest, post_method, recover_method, block_migration) + def get_vnc_console(self, instance): + """Return link to instance's VNC console using vCenter logic.""" + # In this situation, ESXi and vCenter require different + # API logic to create a valid VNC console connection object. + # In specific, vCenter does not actually run the VNC service + # itself. You must talk to the VNC host underneath vCenter. + return self._vmops.get_vnc_console_vcenter(instance) + class VMwareAPISession(object): """ diff --git a/nova/virt/vmwareapi/fake.py b/nova/virt/vmwareapi/fake.py index e088d2302aa..124952f8984 100644 --- a/nova/virt/vmwareapi/fake.py +++ b/nova/virt/vmwareapi/fake.py @@ -20,6 +20,7 @@ A fake VMware VI API implementation. """ +import collections import pprint import uuid @@ -81,22 +82,72 @@ def _get_objects(obj_type): return lst_objs -class Prop(object): +class Property(object): """Property Object base class.""" - def __init__(self): - self.name = None - self.val = None + def __init__(self, name=None, val=None): + self.name = name + self.val = val + + +class ManagedObjectReference(object): + """A managed object reference is a remote identifier.""" + + def __init__(self, value="object-123", _type="ManagedObject"): + super(ManagedObjectReference, self) + # Managed Object Reference value attributes + # typically have values like vm-123 or + # host-232 and not UUID. + self.value = value + # Managed Object Reference _type + # attributes hold the name of the type + # of the vCenter object the value + # attribute is the identifier for + self._type = _type + + +class ObjectContent(object): + """ObjectContent array holds dynamic properties.""" + + # This class is a *fake* of a class sent back to us by + # SOAP. It has its own names. These names are decided + # for us by the API we are *faking* here. + def __init__(self, obj_ref, prop_list=None, missing_list=None): + self.obj = obj_ref + + if not isinstance(prop_list, collections.Iterable): + prop_list = [] + + if not isinstance(missing_list, collections.Iterable): + missing_list = [] + + # propSet is the name your Python code will need to + # use since this is the name that the API will use + self.propSet = prop_list + + # missingSet is the name your python code will + # need to use since this is the name that the + # API we are talking to will use. + self.missingSet = missing_list class ManagedObject(object): - """Managed Data Object base class.""" + """Managed Object base class.""" - def __init__(self, name="ManagedObject", obj_ref=None): + def __init__(self, name="ManagedObject", obj_ref=None, value=None): """Sets the obj property which acts as a reference to the object.""" super(ManagedObject, self).__setattr__('objName', name) + + # A managed object is a local representation of a + # remote object that you can reference using the + # object reference. if obj_ref is None: - obj_ref = str(uuid.uuid4()) + if value is None: + value = 'obj-123' + obj_ref = ManagedObjectReference(value, name) + + # we use __setattr__ here because below the + # default setter has been altered for this class. object.__setattr__(self, 'obj', obj_ref) object.__setattr__(self, 'propSet', []) @@ -116,16 +167,20 @@ def get(self, attr): return self.__getattr__(attr) def __setattr__(self, attr, val): + # TODO(hartsocks): this is adds unnecessary complexity to the class for prop in self.propSet: if prop.name == attr: prop.val = val return - elem = Prop() + elem = Property() elem.name = attr elem.val = val self.propSet.append(elem) def __getattr__(self, attr): + # TODO(hartsocks): remove this + # in a real ManagedObject you have to iterate the propSet + # in a real ManagedObject, the propSet is a *set* not a list for elem in self.propSet: if elem.name == attr: return elem.val @@ -185,7 +240,7 @@ class VirtualMachine(ManagedObject): """Virtual Machine class.""" def __init__(self, **kwargs): - super(VirtualMachine, self).__init__("VirtualMachine") + super(VirtualMachine, self).__init__("VirtualMachine", value='vm-10') self.set("name", kwargs.get("name")) self.set("runtime.connectionState", kwargs.get("conn_state", "connected")) @@ -203,6 +258,8 @@ def __init__(self, **kwargs): self.set("summary.config.memorySizeMB", kwargs.get("mem", 1)) self.set("config.hardware.device", kwargs.get("virtual_device", None)) self.set("config.extraConfig", kwargs.get("extra_config", None)) + self.set('runtime.host', + ManagedObjectReference(value='host-123', _type="HostSystem")) self.device = kwargs.get("virtual_device") def reconfig(self, factory, val): @@ -279,8 +336,8 @@ def __init__(self): class HostSystem(ManagedObject): """Host System class.""" - def __init__(self): - super(HostSystem, self).__init__("HostSystem") + def __init__(self, obj_ref=None, value='host-123'): + super(HostSystem, self).__init__("HostSystem", obj_ref, value) self.set("name", "ha-host") if _db_content.get("HostNetworkSystem", None) is None: create_host_network_system() diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index fecac5bccf1..c6f660fd65b 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -20,6 +20,7 @@ """ import copy + from nova import exception from nova.virt.vmwareapi import vim_util @@ -522,6 +523,111 @@ def get_vm_ref(session, instance): return vm_ref +def get_host_ref_from_id(session, host_id, property_list=None): + """Get a host reference object for a host_id string.""" + + if property_list is None: + property_list = ['name'] + + host_refs = session._call_method( + vim_util, "get_objects", + "HostSystem", property_list) + + for ref in host_refs: + if ref.obj.value == host_id: + return ref + + +def get_host_id_from_vm_ref(session, vm_ref): + """ + This method allows you to find the managed object + ID of the host running a VM. Since vMotion can + change the value, you should not presume that this + is a value that you can cache for very long and + should be prepared to allow for it to change. + + :param session: a vSphere API connection + :param vm_ref: a reference object to the running VM + :return: the host_id running the virtual machine + """ + + # to prevent typographical errors below + property_name = 'runtime.host' + + # a property collector in VMware vSphere Management API + # is a set of local representations of remote values. + # property_set here, is a local representation of the + # properties we are querying for. + property_set = session._call_method( + vim_util, "get_object_properties", + None, vm_ref, vm_ref._type, [property_name]) + + prop = property_from_property_set( + property_name, property_set) + + if prop is not None: + prop = prop.val.value + else: + # reaching here represents an impossible state + raise RuntimeError( + "Virtual Machine %s exists without a runtime.host!" + % (vm_ref)) + + return prop + + +def property_from_property_set(property_name, property_set): + ''' + Use this method to filter property collector results. + + Because network traffic is expensive, multiple + VMwareAPI calls will sometimes pile-up properties + to be collected. That means results may contain + many different values for multiple purposes. + + This helper will filter a list for a single result + and filter the properties of that result to find + the single value of whatever type resides in that + result. This could be a ManagedObjectReference ID + or a complex value. + + :param property_name: name of property you want + :param property_set: all results from query + :return: the value of the property. + ''' + + for prop in property_set: + p = _property_from_propSet(prop.propSet, property_name) + if p is not None: + return p + + +def _property_from_propSet(propSet, name='name'): + for p in propSet: + if p.name == name: + return p + + +def get_host_ref_for_vm(session, instance, props): + """Get the ESXi host running a VM by its name.""" + + vm_ref = get_vm_ref(session, instance) + host_id = get_host_id_from_vm_ref(session, vm_ref) + return get_host_ref_from_id(session, host_id, props) + + +def get_host_name_for_vm(session, instance): + """Get the ESXi host running a VM by its name.""" + host_ref = get_host_ref_for_vm(session, instance, ['name']) + return get_host_name_from_host_ref(host_ref) + + +def get_host_name_from_host_ref(host_ref): + p = _property_from_propSet(host_ref.propSet) + if p is not None: + return p.val + + def get_cluster_ref_from_name(session, cluster_name): """Get reference to the cluster with the name specified.""" cls = session._call_method(vim_util, "get_objects", diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index e47c4b3a571..1c791e373b1 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -1092,6 +1092,26 @@ def get_vnc_console(self, instance): 'port': self._get_vnc_port(vm_ref), 'internal_access_path': None} + def get_vnc_console_vcenter(self, instance): + """Return connection info for a vnc console using vCenter logic.""" + + # vCenter does not run virtual machines and does not run + # a VNC proxy. Instead, you need to tell OpenStack to talk + # directly to the ESX host running the VM you are attempting + # to connect to via VNC. + + vnc_console = self.get_vnc_console(instance) + host_name = vm_util.get_host_name_for_vm( + self._session, + instance) + vnc_console['host'] = host_name + + # NOTE: VM can move hosts in some situations. Debug for admins. + LOG.debug(_("VM %(uuid)s is currently on host %(host_name)s"), + {'uuid': instance['name'], 'host_name': host_name}) + + return vnc_console + @staticmethod def _get_vnc_port(vm_ref): """Return VNC port for an VM."""