xenapi: make session calls more discoverable

Its currently hard to discover what existing calls there are to
XenAPI, and the *_utils.py are getting very large.

Also, with the move to mock, it is becoming hard work testing multiple
calls to call_xenapi within a single method. Moving to this more object
oriented way of calling XenAPI should make writing tests with mock much
easier, and give us an obvious place to move some of simpler helper
methods in vm_utils and volume_utils.

This is the very first step in this direction. Dependent patches will
show how this can be used.

Part of blueprint xenapi-driver-refactor
Change-Id: I021fbedf2e84d58f24a3d9cb8fab0b906125f7c1
changes/93/66493/15
John Garbutt 2014-01-13 08:14:07 +00:00
parent cb31014353
commit c9480634c2
7 changed files with 332 additions and 12 deletions

View File

@ -0,0 +1,91 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from nova.tests.virt.xenapi import stubs
from nova.virt.xenapi.client import objects
class XenAPISessionObjectTestCase(stubs.XenAPITestBaseNoDB):
def setUp(self):
super(XenAPISessionObjectTestCase, self).setUp()
self.session = mock.Mock()
self.obj = objects.XenAPISessionObject(self.session, "FAKE")
def test_call_method_via_attr(self):
self.session.call_xenapi.return_value = "asdf"
result = self.obj.get_X("ref")
self.assertEqual(result, "asdf")
self.session.call_xenapi.assert_called_once_with("FAKE.get_X", "ref")
class ObjectsTestCase(stubs.XenAPITestBaseNoDB):
def setUp(self):
super(ObjectsTestCase, self).setUp()
self.session = mock.Mock()
def test_VM(self):
vm = objects.VM(self.session)
vm.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VM.get_X", "ref")
def test_SR(self):
sr = objects.SR(self.session)
sr.get_X("ref")
self.session.call_xenapi.assert_called_once_with("SR.get_X", "ref")
def test_VDI(self):
vdi = objects.VDI(self.session)
vdi.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VDI.get_X", "ref")
def test_VBD(self):
vbd = objects.VBD(self.session)
vbd.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VBD.get_X", "ref")
def test_PBD(self):
pbd = objects.PBD(self.session)
pbd.get_X("ref")
self.session.call_xenapi.assert_called_once_with("PBD.get_X", "ref")
def test_PIF(self):
pif = objects.PIF(self.session)
pif.get_X("ref")
self.session.call_xenapi.assert_called_once_with("PIF.get_X", "ref")
def test_VLAN(self):
vlan = objects.VLAN(self.session)
vlan.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VLAN.get_X", "ref")
def test_host(self):
host = objects.Host(self.session)
host.get_X("ref")
self.session.call_xenapi.assert_called_once_with("host.get_X", "ref")
def test_network(self):
network = objects.Network(self.session)
network.get_X("ref")
self.session.call_xenapi.assert_called_once_with("network.get_X",
"ref")
def test_pool(self):
pool = objects.Pool(self.session)
pool.get_X("ref")
self.session.call_xenapi.assert_called_once_with("pool.get_X", "ref")

View File

@ -0,0 +1,67 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from nova.tests.virt.xenapi import stubs
from nova.virt.xenapi.client import session
class ApplySessionHelpersTestCase(stubs.XenAPITestBaseNoDB):
def setUp(self):
super(ApplySessionHelpersTestCase, self).setUp()
self.session = mock.Mock()
session.apply_session_helpers(self.session)
def test_apply_session_helpers_add_VM(self):
self.session.VM.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VM.get_X", "ref")
def test_apply_session_helpers_add_SR(self):
self.session.SR.get_X("ref")
self.session.call_xenapi.assert_called_once_with("SR.get_X", "ref")
def test_apply_session_helpers_add_VDI(self):
self.session.VDI.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VDI.get_X", "ref")
def test_apply_session_helpers_add_VBD(self):
self.session.VBD.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VBD.get_X", "ref")
def test_apply_session_helpers_add_PBD(self):
self.session.PBD.get_X("ref")
self.session.call_xenapi.assert_called_once_with("PBD.get_X", "ref")
def test_apply_session_helpers_add_PIF(self):
self.session.PIF.get_X("ref")
self.session.call_xenapi.assert_called_once_with("PIF.get_X", "ref")
def test_apply_session_helpers_add_VLAN(self):
self.session.VLAN.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VLAN.get_X", "ref")
def test_apply_session_helpers_add_host(self):
self.session.host.get_X("ref")
self.session.call_xenapi.assert_called_once_with("host.get_X", "ref")
def test_apply_session_helpers_add_host(self):
self.session.network.get_X("ref")
self.session.call_xenapi.assert_called_once_with("network.get_X",
"ref")
def test_apply_session_helpers_add_pool(self):
self.session.pool.get_X("ref")
self.session.call_xenapi.assert_called_once_with("pool.get_X", "ref")

View File

@ -35,6 +35,7 @@ from nova import test
from nova.tests.virt.xenapi import stubs
from nova.tests.virt.xenapi import test_xenapi
from nova import utils
from nova.virt.xenapi.client import session as xenapi_session
from nova.virt.xenapi import driver as xenapi_conn
from nova.virt.xenapi import fake
from nova.virt.xenapi import vm_utils
@ -64,14 +65,16 @@ def get_fake_connection_data(sr_type):
return fakes[sr_type]
def _get_fake_session_and_exception(error):
def _get_fake_session(error=None):
session = mock.Mock()
xenapi_session.apply_session_helpers(session)
class FakeException(Exception):
details = [error, "a", "b", "c"]
if error is not None:
class FakeException(Exception):
details = [error, "a", "b", "c"]
session.XenAPI.Failure = FakeException
session.call_xenapi.side_effect = FakeException
session.XenAPI.Failure = FakeException
session.call_xenapi.side_effect = FakeException
return session
@ -892,7 +895,7 @@ class UnplugVbdTestCase(VMUtilsTestBase):
def test_unplug_vbd_already_detached_works(self):
error = "DEVICE_ALREADY_DETACHED"
session = _get_fake_session_and_exception(error)
session = _get_fake_session(error)
vbd_ref = "vbd_ref"
vm_ref = 'vm_ref'
@ -900,7 +903,7 @@ class UnplugVbdTestCase(VMUtilsTestBase):
self.assertEqual(1, session.call_xenapi.call_count)
def test_unplug_vbd_already_raises_unexpected_xenapi_error(self):
session = _get_fake_session_and_exception("")
session = _get_fake_session("")
vbd_ref = "vbd_ref"
vm_ref = 'vm_ref'
@ -909,7 +912,7 @@ class UnplugVbdTestCase(VMUtilsTestBase):
self.assertEqual(1, session.call_xenapi.call_count)
def _test_uplug_vbd_retries(self, mock_sleep, error):
session = _get_fake_session_and_exception(error)
session = _get_fake_session(error)
vbd_ref = "vbd_ref"
vm_ref = 'vm_ref'
@ -1435,7 +1438,7 @@ class ScanSrTestCase(VMUtilsTestBase):
class CreateVmTestCase(VMUtilsTestBase):
def test_vss_provider(self, mock_extract):
self.flags(vcpu_pin_set="2,3")
session = mock.Mock()
session = _get_fake_session()
instance = {
"uuid": "uuid", "os_type": "windows"
}
@ -1492,6 +1495,29 @@ class CreateVmTestCase(VMUtilsTestBase):
session, instance, "label",
"kernel", "ramdisk")
def test_destroy_vm(self, mock_extract):
session = mock.Mock()
instance = {
"uuid": "uuid",
}
vm_utils.destroy_vm(session, instance, "vm_ref")
session.VM.destroy.assert_called_once_with("vm_ref")
def test_destroy_vm_silently_fails(self, mock_extract):
session = mock.Mock()
exc = test.TestingException()
session.XenAPI.Failure = test.TestingException
session.VM.destroy.side_effect = exc
instance = {
"uuid": "uuid",
}
vm_utils.destroy_vm(session, instance, "vm_ref")
session.VM.destroy.assert_called_once_with("vm_ref")
class DetermineVmModeTestCase(VMUtilsTestBase):
def test_determine_vm_mode_returns_xen_mode(self):
@ -2028,7 +2054,7 @@ class CreateVmRecordTestCase(VMUtilsTestBase):
def _test_create_vm_record(self, mock_extract_flavor, instance,
is_viridian):
session = mock.Mock()
session = _get_fake_session()
flavor = {"memory_mb": 1024, "vcpus": 1, "vcpu_weight": 2}
mock_extract_flavor.return_value = flavor

View File

@ -0,0 +1,120 @@
# Copyright 2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class XenAPISessionObject(object):
"""Wrapper to make calling and mocking the session easier
The XenAPI protocol is an XML RPC API that is based around the
XenAPI database, and operations you can do on each of the objects
stored in the database, such as VM, SR, VDI, etc.
For more details see the XenAPI docs:
http://docs.vmd.citrix.com/XenServer/6.2.0/1.0/en_gb/api/
Most, objects like VM, SR, VDI, etc, share a common set of methods:
* vm_ref = session.VM.create(vm_rec)
* vm_ref = session.VM.get_by_uuid(uuid)
* session.VM.destroy(vm_ref)
* vm_refs = session.VM.get_all()
Each object also has specific messages, or functions, such as:
* session.VM.clean_reboot(vm_ref)
Each object has fields, like "VBDs" that can be fetched like this:
* vbd_refs = session.VM.get_VBDs(vm_ref)
You can get all the fields by fetching the full record.
However please note this is much more expensive than just
fetching the field you require:
* vm_rec = session.VM.get_record(vm_ref)
When searching for particular objects, you may be tempted
to use get_all(), but this often leads to races as objects
get deleted under your feet. It is preferable to use the undocumented:
* vms = session.VM.get_all_records_where(
'field "is_control_domain"="true"')
"""
def __init__(self, session, name):
self.session = session
self.name = name
def _call_method(self, method_name, *args):
call = "%s.%s" % (self.name, method_name)
return self.session.call_xenapi(call, *args)
def __getattr__(self, method_name):
return lambda *params: self._call_method(method_name, *params)
class VM(XenAPISessionObject):
"""Virtual Machine."""
def __init__(self, session):
super(VM, self).__init__(session, "VM")
class VBD(XenAPISessionObject):
"""Virtual block device."""
def __init__(self, session):
super(VBD, self).__init__(session, "VBD")
class VDI(XenAPISessionObject):
"""Virtual disk image."""
def __init__(self, session):
super(VDI, self).__init__(session, "VDI")
class SR(XenAPISessionObject):
"""Storage Repository."""
def __init__(self, session):
super(SR, self).__init__(session, "SR")
class PBD(XenAPISessionObject):
"""Physical block device."""
def __init__(self, session):
super(PBD, self).__init__(session, "PBD")
class PIF(XenAPISessionObject):
"""Physical Network Interface."""
def __init__(self, session):
super(PIF, self).__init__(session, "PIF")
class VLAN(XenAPISessionObject):
"""VLAN."""
def __init__(self, session):
super(VLAN, self).__init__(session, "VLAN")
class Host(XenAPISessionObject):
"""XenServer hosts."""
def __init__(self, session):
super(Host, self).__init__(session, "host")
class Network(XenAPISessionObject):
"""Networks that VIFs are attached to."""
def __init__(self, session):
super(Network, self).__init__(session, "network")
class Pool(XenAPISessionObject):
"""Pool of hosts."""
def __init__(self, session):
super(Pool, self).__init__(session, "pool")

View File

@ -28,6 +28,7 @@ from nova.openstack.common.gettextutils import _
from nova.openstack.common import log as logging
from nova.openstack.common import versionutils
from nova import utils
from nova.virt.xenapi.client import objects
from nova.virt.xenapi import pool
from nova.virt.xenapi import pool_states
@ -52,6 +53,19 @@ CONF.register_opts(xenapi_session_opts, 'xenserver')
CONF.import_opt('host', 'nova.netconf')
def apply_session_helpers(session):
session.VM = objects.VM(session)
session.SR = objects.SR(session)
session.VDI = objects.VDI(session)
session.VBD = objects.VBD(session)
session.PBD = objects.PBD(session)
session.PIF = objects.PIF(session)
session.VLAN = objects.VLAN(session)
session.host = objects.Host(session)
session.network = objects.Network(session)
session.pool = objects.Pool(session)
class XenAPISession(object):
"""The session to invoke XenAPI SDK calls."""
@ -77,6 +91,8 @@ class XenAPISession(object):
self._verify_plugin_version()
apply_session_helpers(self)
def _verify_plugin_version(self):
requested_version = self.PLUGIN_REQUIRED_VERSION
current_version = self.call_plugin_serialized(

View File

@ -330,7 +330,7 @@ def create_vm(session, instance, name_label, kernel, ramdisk,
if device_id:
rec['platform']['device_id'] = device_id
vm_ref = session.call_xenapi('VM.create', rec)
vm_ref = session.VM.create(rec)
LOG.debug(_('Created VM'), instance=instance)
return vm_ref
@ -338,7 +338,7 @@ def create_vm(session, instance, name_label, kernel, ramdisk,
def destroy_vm(session, instance, vm_ref):
"""Destroys a VM record."""
try:
session.call_xenapi('VM.destroy', vm_ref)
session.VM.destroy(vm_ref)
except session.XenAPI.Failure as exc:
LOG.exception(exc)
return