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
This commit is contained in:
0
nova/tests/virt/xenapi/client/__init__.py
Normal file
0
nova/tests/virt/xenapi/client/__init__.py
Normal file
91
nova/tests/virt/xenapi/client/test_objects.py
Normal file
91
nova/tests/virt/xenapi/client/test_objects.py
Normal 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")
|
67
nova/tests/virt/xenapi/client/test_session.py
Normal file
67
nova/tests/virt/xenapi/client/test_session.py
Normal 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")
|
@@ -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
|
||||
|
||||
|
120
nova/virt/xenapi/client/objects.py
Normal file
120
nova/virt/xenapi/client/objects.py
Normal 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")
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user