--- /dev/null
+# 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
+ )
--- /dev/null
+# 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)
--- /dev/null
+# 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()