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://<yourxenserver>
xenapi_connection_username=root
xenapi_connection_password=<secret>
xenapi_nfs_server=<nfs_server_address>
xenapi_nfs_serverpath=/exported_catalog

Change-Id: Ieaf077f540fc026c5bc37f2e3eb9d48fb96d0b74
This commit is contained in:
Mate Lakat 2012-11-05 09:57:52 +00:00
parent ceee1fdaf2
commit 09f98c9caa
5 changed files with 646 additions and 0 deletions

View File

@ -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
)

View File

View File

333
cinder/volume/xenapi/lib.py Normal file
View File

@ -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)

127
cinder/volume/xenapi_sm.py Normal file
View File

@ -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()