From 09f98c9caa495bc648f624ba3c46b912a88e6e2f Mon Sep 17 00:00:00 2001 From: Mate Lakat Date: Mon, 5 Nov 2012 09:57:52 +0000 Subject: [PATCH] Add XenAPINFSDriver Related to blueprint xenapi-storage-manager-nfs As xensm was not pulled to Cinder, this change shows an implementation example for using an NFS export for volumes. This change contains the volume create and delete functions, as a minimal set of functionality. The connection information structure is compatible with the previous xensm implementation. Activate the driver with the following configuration entries in cinder.conf: volume_driver=cinder.volume.xenapi_sm.XenAPINFSDriver xenapi_connection_url=http:// xenapi_connection_username=root xenapi_connection_password= xenapi_nfs_server= xenapi_nfs_serverpath=/exported_catalog Change-Id: Ieaf077f540fc026c5bc37f2e3eb9d48fb96d0b74 --- cinder/tests/test_xenapi_sm.py | 186 +++++++++++++++++ cinder/tests/xenapi/__init__.py | 0 cinder/volume/xenapi/__init__.py | 0 cinder/volume/xenapi/lib.py | 333 +++++++++++++++++++++++++++++++ cinder/volume/xenapi_sm.py | 127 ++++++++++++ 5 files changed, 646 insertions(+) create mode 100644 cinder/tests/test_xenapi_sm.py create mode 100644 cinder/tests/xenapi/__init__.py create mode 100644 cinder/volume/xenapi/__init__.py create mode 100644 cinder/volume/xenapi/lib.py create mode 100644 cinder/volume/xenapi_sm.py diff --git a/cinder/tests/test_xenapi_sm.py b/cinder/tests/test_xenapi_sm.py new file mode 100644 index 000000000..bc7752643 --- /dev/null +++ b/cinder/tests/test_xenapi_sm.py @@ -0,0 +1,186 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +from cinder.volume.xenapi import lib +from cinder.volume import xenapi_sm as driver +import mox +import unittest + + +class DriverTestCase(unittest.TestCase): + + def assert_flag(self, flagname): + self.assertTrue(hasattr(driver.FLAGS, flagname)) + + def test_config_options(self): + self.assert_flag('xenapi_connection_url') + self.assert_flag('xenapi_connection_username') + self.assert_flag('xenapi_connection_password') + self.assert_flag('xenapi_nfs_server') + self.assert_flag('xenapi_nfs_serverpath') + + def test_do_setup(self): + mock = mox.Mox() + mock.StubOutWithMock(driver, 'xenapi_lib') + mock.StubOutWithMock(driver, 'FLAGS') + + driver.FLAGS.xenapi_connection_url = 'url' + driver.FLAGS.xenapi_connection_username = 'user' + driver.FLAGS.xenapi_connection_password = 'pass' + + session_factory = object() + nfsops = object() + + driver.xenapi_lib.SessionFactory('url', 'user', 'pass').AndReturn( + session_factory) + + driver.xenapi_lib.NFSBasedVolumeOperations( + session_factory).AndReturn(nfsops) + + drv = driver.XenAPINFSDriver() + + mock.ReplayAll() + drv.do_setup('context') + mock.VerifyAll() + + self.assertEquals(nfsops, drv.nfs_ops) + + def test_create_volume(self): + mock = mox.Mox() + + mock.StubOutWithMock(driver, 'FLAGS') + driver.FLAGS.xenapi_nfs_server = 'server' + driver.FLAGS.xenapi_nfs_serverpath = 'path' + + ops = mock.CreateMock(lib.NFSBasedVolumeOperations) + drv = driver.XenAPINFSDriver() + drv.nfs_ops = ops + + volume_details = dict( + sr_uuid='sr_uuid', + vdi_uuid='vdi_uuid' + ) + ops.create_volume( + 'server', 'path', 1, 'name', 'desc').AndReturn(volume_details) + + mock.ReplayAll() + result = drv.create_volume(dict( + size=1, display_name='name', display_description='desc')) + mock.VerifyAll() + + self.assertEquals(dict( + provider_location='sr_uuid/vdi_uuid' + ), result) + + def test_delete_volume(self): + mock = mox.Mox() + + mock.StubOutWithMock(driver, 'FLAGS') + driver.FLAGS.xenapi_nfs_server = 'server' + driver.FLAGS.xenapi_nfs_serverpath = 'path' + + ops = mock.CreateMock(lib.NFSBasedVolumeOperations) + drv = driver.XenAPINFSDriver() + drv.nfs_ops = ops + + ops.delete_volume('server', 'path', 'sr_uuid', 'vdi_uuid') + + mock.ReplayAll() + result = drv.delete_volume(dict( + provider_location='sr_uuid/vdi_uuid')) + mock.VerifyAll() + + def test_create_export_does_not_raise_exception(self): + drv = driver.XenAPINFSDriver() + drv.create_export('context', 'volume') + + def test_remove_export_does_not_raise_exception(self): + drv = driver.XenAPINFSDriver() + drv.remove_export('context', 'volume') + + def test_initialize_connection(self): + mock = mox.Mox() + + mock.StubOutWithMock(driver, 'FLAGS') + driver.FLAGS.xenapi_nfs_server = 'server' + driver.FLAGS.xenapi_nfs_serverpath = 'path' + + drv = driver.XenAPINFSDriver() + + mock.ReplayAll() + result = drv.initialize_connection( + dict( + display_name='name', + display_description='desc', + provider_location='sr_uuid/vdi_uuid'), + 'connector' + ) + mock.VerifyAll() + + self.assertEquals( + dict( + driver_volume_type='xensm', + data=dict( + name_label='name', + name_description='desc', + sr_uuid='sr_uuid', + vdi_uuid='vdi_uuid', + sr_type='nfs', + server='server', + serverpath='path', + introduce_sr_keys=['sr_type', 'server', 'serverpath'] + ) + ), + result + ) + + def test_initialize_connection_null_values(self): + mock = mox.Mox() + + mock.StubOutWithMock(driver, 'FLAGS') + driver.FLAGS.xenapi_nfs_server = 'server' + driver.FLAGS.xenapi_nfs_serverpath = 'path' + + drv = driver.XenAPINFSDriver() + + mock.ReplayAll() + result = drv.initialize_connection( + dict( + display_name=None, + display_description=None, + provider_location='sr_uuid/vdi_uuid'), + 'connector' + ) + mock.VerifyAll() + + self.assertEquals( + dict( + driver_volume_type='xensm', + data=dict( + name_label='', + name_description='', + sr_uuid='sr_uuid', + vdi_uuid='vdi_uuid', + sr_type='nfs', + server='server', + serverpath='path', + introduce_sr_keys=['sr_type', 'server', 'serverpath'] + ) + ), + result + ) diff --git a/cinder/tests/xenapi/__init__.py b/cinder/tests/xenapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/volume/xenapi/__init__.py b/cinder/volume/xenapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/volume/xenapi/lib.py b/cinder/volume/xenapi/lib.py new file mode 100644 index 000000000..eb5fd4f06 --- /dev/null +++ b/cinder/volume/xenapi/lib.py @@ -0,0 +1,333 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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 contextlib + + +class XenAPIException(Exception): + def __init__(self, original_exception): + super(XenAPIException, self).__init__(str(original_exception)) + self.original_exception = original_exception + + +class OperationsBase(object): + def __init__(self, xenapi_session): + self.session = xenapi_session + + def call_xenapi(self, method, *args): + return self.session.call_xenapi(method, *args) + + +class PbdOperations(OperationsBase): + def get_all(self): + return self.call_xenapi('PBD.get_all') + + def unplug(self, pbd_ref): + self.call_xenapi('PBD.unplug', pbd_ref) + + def create(self, host_ref, sr_ref, device_config): + return self.call_xenapi( + 'PBD.create', + dict( + host=host_ref, + SR=sr_ref, + device_config=device_config + ) + ) + + def plug(self, pbd_ref): + self.call_xenapi('PBD.plug', pbd_ref) + + +class SrOperations(OperationsBase): + def get_all(self): + return self.call_xenapi('SR.get_all') + + def get_record(self, sr_ref): + return self.call_xenapi('SR.get_record', sr_ref) + + def forget(self, sr_ref): + self.call_xenapi('SR.forget', sr_ref) + + def scan(self, sr_ref): + self.call_xenapi('SR.scan', sr_ref) + + def create(self, host_ref, device_config, name_label, name_description, + sr_type, physical_size=None, content_type=None, + shared=False, sm_config=None): + return self.call_xenapi( + 'SR.create', + host_ref, + device_config, + physical_size or '0', + name_label or '', + name_description or '', + sr_type, + content_type or '', + shared, + sm_config or dict() + ) + + def introduce(self, sr_uuid, name_label, name_description, sr_type, + content_type=None, shared=False, sm_config=None): + return self.call_xenapi( + 'SR.introduce', + sr_uuid, + name_label or '', + name_description or '', + sr_type, + content_type or '', + shared, + sm_config or dict() + ) + + def get_uuid(self, sr_ref): + return self.get_record(sr_ref)['uuid'] + + def get_name_label(self, sr_ref): + return self.get_record(sr_ref)['name_label'] + + def get_name_description(self, sr_ref): + return self.get_record(sr_ref)['name_description'] + + def destroy(self, sr_ref): + self.call_xenapi('SR.destroy', sr_ref) + + +class VdiOperations(OperationsBase): + def get_all(self): + return self.call_xenapi('VDI.get_all') + + def get_record(self, vdi_ref): + return self.call_xenapi('VDI.get_record', vdi_ref) + + def get_by_uuid(self, vdi_uuid): + return self.call_xenapi('VDI.get_by_uuid', vdi_uuid) + + def get_uuid(self, vdi_ref): + return self.get_record(vdi_ref)['uuid'] + + def create(self, sr_ref, size, vdi_type, + sharable=False, read_only=False, other_config=None): + return self.call_xenapi('VDI.create', + dict( + SR=sr_ref, + virtual_size=str(size), + type=vdi_type, + sharable=sharable, + read_only=read_only, + other_config=other_config or dict() + ) + ) + + def destroy(self, vdi_ref): + self.call_xenapi('VDI.destroy', vdi_ref) + + +class HostOperations(OperationsBase): + def get_record(self, host_ref): + return self.call_xenapi('host.get_record', host_ref) + + def get_uuid(self, host_ref): + return self.get_record(host_ref)['uuid'] + + +class XenAPISession(object): + def __init__(self, session, exception_to_convert): + self._session = session + self._exception_to_convert = exception_to_convert + self.handle = self._session.handle + self.PBD = PbdOperations(self) + self.SR = SrOperations(self) + self.VDI = VdiOperations(self) + self.host = HostOperations(self) + + def close(self): + return self.call_xenapi('logout') + + def call_xenapi(self, method, *args): + try: + return self._session.xenapi_request(method, args) + except self._exception_to_convert as e: + raise XenAPIException(e) + + def get_pool(self): + return self.call_xenapi('session.get_pool', self.handle) + + def get_this_host(self): + return self.call_xenapi('session.get_this_host', self.handle) + + +class CompoundOperations(object): + def unplug_pbds_from_sr(self, sr_ref): + sr_rec = self.SR.get_record(sr_ref) + for pbd_ref in sr_rec.get('PBDs', []): + self.PBD.unplug(pbd_ref) + + def unplug_pbds_and_forget_sr(self, sr_ref): + self.unplug_pbds_from_sr(sr_ref) + self.SR.forget(sr_ref) + + def create_new_vdi(self, sr_ref, size_in_gigabytes): + return self.VDI.create( + sr_ref, + to_bytes(size_in_gigabytes), + 'User', + ) + + +def to_bytes(size_in_gigs): + return size_in_gigs * 1024 * 1024 * 1024 + + +class NFSOperationsMixIn(CompoundOperations): + def is_nfs_sr(self, sr_ref): + return self.SR.get_record(sr_ref).get('type') == 'nfs' + + @contextlib.contextmanager + def new_sr_on_nfs(self, host_ref, server, serverpath, + name_label=None, name_description=None): + + device_config = dict( + server=server, + serverpath=serverpath + ) + name_label = name_label or '' + name_description = name_description or '' + sr_type = 'nfs' + + sr_ref = self.SR.create( + host_ref, + device_config, + name_label, + name_description, + sr_type, + ) + yield sr_ref + + self.unplug_pbds_and_forget_sr(sr_ref) + + def plug_nfs_sr(self, host_ref, server, serverpath, sr_uuid, + name_label=None, name_description=None): + + device_config = dict( + server=server, + serverpath=serverpath + ) + sr_type = 'nfs' + + sr_ref = self.SR.introduce( + sr_uuid, + name_label, + name_description, + sr_type, + ) + + pbd_ref = self.PBD.create( + host_ref, + sr_ref, + device_config + ) + + self.PBD.plug(pbd_ref) + + return sr_ref + + def connect_volume(self, server, serverpath, sr_uuid, vdi_uuid): + host_ref = self.get_this_host() + sr_ref = self.plug_nfs_sr( + host_ref, + server, + serverpath, + sr_uuid + ) + self.SR.scan(sr_ref) + vdi_ref = self.VDI.get_by_uuid(vdi_uuid) + return dict(sr_ref=sr_ref, vdi_ref=vdi_ref) + + +class ContextAwareSession(XenAPISession): + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +class OpenStackXenAPISession(ContextAwareSession, + NFSOperationsMixIn): + pass + + +def connect(url, user, password): + import XenAPI + session = XenAPI.Session(url) + session.login_with_password(user, password) + return OpenStackXenAPISession(session, XenAPI.Failure) + + +class SessionFactory(object): + def __init__(self, url, user, password): + self.url = url + self.user = user + self.password = password + + def get_session(self): + return connect(self.url, self.user, self.password) + + +class NFSBasedVolumeOperations(object): + def __init__(self, session_factory): + self._session_factory = session_factory + + def create_volume(self, server, serverpath, size, + name=None, description=None): + with self._session_factory.get_session() as session: + host_ref = session.get_this_host() + with session.new_sr_on_nfs(host_ref, server, serverpath, + name, description) as sr_ref: + vdi_ref = session.create_new_vdi(sr_ref, size) + + return dict( + sr_uuid=session.SR.get_uuid(sr_ref), + vdi_uuid=session.VDI.get_uuid(vdi_ref) + ) + + def delete_volume(self, server, serverpath, sr_uuid, vdi_uuid): + with self._session_factory.get_session() as session: + refs = session.connect_volume( + server, serverpath, sr_uuid, vdi_uuid) + + session.VDI.destroy(refs['vdi_ref']) + sr_ref = refs['sr_ref'] + session.unplug_pbds_from_sr(sr_ref) + session.SR.destroy(sr_ref) + + def connect_volume(self, server, serverpath, sr_uuid, vdi_uuid): + with self._session_factory.get_session() as session: + refs = session.connect_volume( + server, serverpath, sr_uuid, vdi_uuid) + + return session.VDI.get_uuid(refs['vdi_ref']) + + def disconnect_volume(self, vdi_uuid): + with self._session_factory.get_session() as session: + vdi_ref = session.VDI.get_by_uuid(vdi_uuid) + vdi_rec = session.VDI.get_record(vdi_ref) + sr_ref = vdi_rec['SR'] + session.unplug_pbds_and_forget_sr(sr_ref) diff --git a/cinder/volume/xenapi_sm.py b/cinder/volume/xenapi_sm.py new file mode 100644 index 000000000..362e572d6 --- /dev/null +++ b/cinder/volume/xenapi_sm.py @@ -0,0 +1,127 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +from cinder import flags +from cinder.openstack.common import cfg +from cinder.volume import driver +from cinder.volume.xenapi import lib as xenapi_lib + + +xenapi_opts = [ + cfg.StrOpt('xenapi_connection_url', + default=None, + help='URL for XenAPI connection'), + cfg.StrOpt('xenapi_connection_username', + default='root', + help='Username for XenAPI connection'), + cfg.StrOpt('xenapi_connection_password', + default=None, + help='Password for XenAPI connection'), +] + +xenapi_nfs_opts = [ + cfg.StrOpt('xenapi_nfs_server', + default=None, + help='NFS server to be used by XenAPINFSDriver'), + cfg.StrOpt('xenapi_nfs_serverpath', + default=None, + help='Path of exported NFS, used by XenAPINFSDriver'), +] + +FLAGS = flags.FLAGS +FLAGS.register_opts(xenapi_opts) +FLAGS.register_opts(xenapi_nfs_opts) + + +class XenAPINFSDriver(driver.VolumeDriver): + + def do_setup(self, context): + session_factory = xenapi_lib.SessionFactory( + FLAGS.xenapi_connection_url, + FLAGS.xenapi_connection_username, + FLAGS.xenapi_connection_password + ) + self.nfs_ops = xenapi_lib.NFSBasedVolumeOperations(session_factory) + + def create_volume(self, volume): + volume_details = self.nfs_ops.create_volume( + FLAGS.xenapi_nfs_server, + FLAGS.xenapi_nfs_serverpath, + volume['size'], + volume['display_name'], + volume['display_description'] + ) + location = "%(sr_uuid)s/%(vdi_uuid)s" % volume_details + return dict(provider_location=location) + + def create_export(self, context, volume): + pass + + def delete_volume(self, volume): + sr_uuid, vdi_uuid = volume['provider_location'].split('/') + + self.nfs_ops.delete_volume( + FLAGS.xenapi_nfs_server, + FLAGS.xenapi_nfs_serverpath, + sr_uuid, + vdi_uuid + ) + + def remove_export(self, context, volume): + pass + + def initialize_connection(self, volume, connector): + sr_uuid, vdi_uuid = volume['provider_location'].split('/') + + return dict( + driver_volume_type='xensm', + data=dict( + name_label=volume['display_name'] or '', + name_description=volume['display_description'] or '', + sr_uuid=sr_uuid, + vdi_uuid=vdi_uuid, + sr_type='nfs', + server=FLAGS.xenapi_nfs_server, + serverpath=FLAGS.xenapi_nfs_serverpath, + introduce_sr_keys=['sr_type', 'server', 'serverpath'] + ) + ) + + def terminate_connection(self, volume, connector, force=False, **kwargs): + pass + + def check_for_setup_error(self): + """To override superclass' method""" + + def create_volume_from_snapshot(self, volume, snapshot): + raise NotImplementedError() + + def create_snapshot(self, snapshot): + raise NotImplementedError() + + def delete_snapshot(self, snapshot): + raise NotImplementedError() + + def ensure_export(self, context, volume): + pass + + def copy_image_to_volume(self, context, volume, image_service, image_id): + raise NotImplementedError() + + def copy_volume_to_image(self, context, volume, image_service, image_id): + raise NotImplementedError() -- 2.45.2