Splitting out volume drivers in driver.py

Moved RBDDriver, SheepdogDriver into their own respective driver
files under cinder/volume/drivers/*. Moved FakeISCSIDriver and
LoggingVolumeDriver into a fake driver file under the tests,
since that's the only place where those are used.

Implements bp driver-cleanup

Change-Id: I39d91ac8e498a9e42c5443db16706203b988444b
This commit is contained in:
Nirmal Ranganathan 2012-10-21 14:07:13 -05:00 committed by john-griffith
parent 5aa1e72911
commit bad23ceff8
11 changed files with 558 additions and 385 deletions

113
cinder/tests/fake_driver.py Normal file
View File

@ -0,0 +1,113 @@
# Copyright 2012 OpenStack LLC
#
# 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.openstack.common import log as logging
from cinder.volume import driver
LOG = logging.getLogger(__name__)
class FakeISCSIDriver(driver.ISCSIDriver):
"""Logs calls instead of executing."""
def __init__(self, *args, **kwargs):
super(FakeISCSIDriver, self).__init__(execute=self.fake_execute,
*args, **kwargs)
def check_for_setup_error(self):
"""No setup necessary in fake mode."""
pass
def initialize_connection(self, volume, connector):
return {
'driver_volume_type': 'iscsi',
'data': {}
}
def terminate_connection(self, volume, connector):
pass
@staticmethod
def fake_execute(cmd, *_args, **_kwargs):
"""Execute that simply logs the command."""
LOG.debug(_("FAKE ISCSI: %s"), cmd)
return (None, None)
class LoggingVolumeDriver(driver.VolumeDriver):
"""Logs and records calls, for unit tests."""
def check_for_setup_error(self):
pass
def create_volume(self, volume):
self.log_action('create_volume', volume)
def delete_volume(self, volume):
self.log_action('delete_volume', volume)
def local_path(self, volume):
print "local_path not implemented"
raise NotImplementedError()
def ensure_export(self, context, volume):
self.log_action('ensure_export', volume)
def create_export(self, context, volume):
self.log_action('create_export', volume)
def remove_export(self, context, volume):
self.log_action('remove_export', volume)
def initialize_connection(self, volume, connector):
self.log_action('initialize_connection', volume)
def terminate_connection(self, volume, connector):
self.log_action('terminate_connection', volume)
_LOGS = []
@staticmethod
def clear_logs():
LoggingVolumeDriver._LOGS = []
@staticmethod
def log_action(action, parameters):
"""Logs the command."""
LOG.debug(_("LoggingVolumeDriver: %s") % (action))
log_dictionary = {}
if parameters:
log_dictionary = dict(parameters)
log_dictionary['action'] = action
LOG.debug(_("LoggingVolumeDriver: %s") % (log_dictionary))
LoggingVolumeDriver._LOGS.append(log_dictionary)
@staticmethod
def all_logs():
return LoggingVolumeDriver._LOGS
@staticmethod
def logs_like(action, **kwargs):
matches = []
for entry in LoggingVolumeDriver._LOGS:
if entry['action'] != action:
continue
match = True
for k, v in kwargs.iteritems():
if entry.get(k) != v:
match = False
break
if match:
matches.append(entry)
return matches

View File

@ -30,7 +30,8 @@ def_vol_type = 'fake_vol_type'
def set_defaults(conf):
conf.set_default('default_volume_type', def_vol_type)
conf.set_default('volume_driver', 'cinder.volume.driver.FakeISCSIDriver')
conf.set_default('volume_driver',
'cinder.tests.fake_driver.FakeISCSIDriver')
conf.set_default('connection_type', 'fake')
conf.set_default('fake_rabbit', True)
conf.set_default('rpc_backend', 'cinder.openstack.common.rpc.impl_fake')

View File

@ -22,6 +22,7 @@ from cinder import service
from cinder.openstack.common import log as logging
from cinder.tests.integrated import integrated_helpers
from cinder.tests.integrated.api import client
from cinder.tests import fake_driver
from cinder.volume import driver
@ -31,7 +32,7 @@ LOG = logging.getLogger(__name__)
class VolumesTest(integrated_helpers._IntegratedTestBase):
def setUp(self):
super(VolumesTest, self).setUp()
driver.LoggingVolumeDriver.clear_logs()
fake_driver.LoggingVolumeDriver.clear_logs()
def _start_api_service(self):
self.osapi = service.WSGIService("osapi_volume")
@ -42,7 +43,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase):
def _get_flags(self):
f = super(VolumesTest, self)._get_flags()
f['use_local_volumes'] = False # Avoids calling local_path
f['volume_driver'] = 'cinder.volume.driver.LoggingVolumeDriver'
f['volume_driver'] = 'cinder.tests.fake_driver.LoggingVolumeDriver'
return f
def test_get_volumes_summary(self):
@ -114,9 +115,9 @@ class VolumesTest(integrated_helpers._IntegratedTestBase):
# Should be gone
self.assertFalse(found_volume)
LOG.debug("Logs: %s" % driver.LoggingVolumeDriver.all_logs())
LOG.debug("Logs: %s" % fake_driver.LoggingVolumeDriver.all_logs())
create_actions = driver.LoggingVolumeDriver.logs_like(
create_actions = fake_driver.LoggingVolumeDriver.logs_like(
'create_volume',
id=created_volume_id)
LOG.debug("Create_Actions: %s" % create_actions)
@ -127,7 +128,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase):
self.assertEquals(create_action['availability_zone'], 'nova')
self.assertEquals(create_action['size'], 1)
export_actions = driver.LoggingVolumeDriver.logs_like(
export_actions = fake_driver.LoggingVolumeDriver.logs_like(
'create_export',
id=created_volume_id)
self.assertEquals(1, len(export_actions))
@ -135,7 +136,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase):
self.assertEquals(export_action['id'], created_volume_id)
self.assertEquals(export_action['availability_zone'], 'nova')
delete_actions = driver.LoggingVolumeDriver.logs_like(
delete_actions = fake_driver.LoggingVolumeDriver.logs_like(
'delete_volume',
id=created_volume_id)
self.assertEquals(1, len(delete_actions))

View File

@ -0,0 +1,58 @@
# Copyright 2012 OpenStack LLC
#
# 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 context
from cinder import flags
from cinder.openstack.common import importutils
from cinder import test
FLAGS = flags.FLAGS
RBD_MODULE = "cinder.volume.drivers.rbd.RBDDriver"
SHEEPDOG_MODULE = "cinder.volume.drivers.sheepdog.SheepdogDriver"
class VolumeDriverCompatibility(test.TestCase):
"""Test backwards compatibility for volume drivers."""
def setUp(self):
super(VolumeDriverCompatibility, self).setUp()
self.manager = importutils.import_object(FLAGS.volume_manager)
self.context = context.get_admin_context()
def tearDown(self):
super(VolumeDriverCompatibility, self).tearDown()
def _load_driver(self, driver):
self.manager.__init__(volume_driver=driver)
def _driver_module_name(self):
return "%s.%s" % (self.manager.driver.__class__.__module__,
self.manager.driver.__class__.__name__)
def test_rbd_old(self):
self._load_driver('cinder.volume.driver.RBDDriver')
self.assertEquals(self._driver_module_name(), RBD_MODULE)
def test_rbd_new(self):
self._load_driver(RBD_MODULE)
self.assertEquals(self._driver_module_name(), RBD_MODULE)
def test_sheepdog_old(self):
self._load_driver('cinder.volume.driver.SheepdogDriver')
self.assertEquals(self._driver_module_name(), SHEEPDOG_MODULE)
def test_sheepdog_new(self):
self._load_driver('cinder.volume.drivers.sheepdog.SheepdogDriver')
self.assertEquals(self._driver_module_name(), SHEEPDOG_MODULE)

View File

@ -26,7 +26,7 @@ from cinder.openstack.common import timeutils
from cinder import test
from cinder.tests.image import fake as fake_image
from cinder.tests.test_volume import DriverTestCase
from cinder.volume.driver import RBDDriver
from cinder.volume.drivers.rbd import RBDDriver
LOG = logging.getLogger(__name__)

View File

@ -22,9 +22,7 @@ Drivers for volumes.
import os
import re
import tempfile
import time
import urllib
from cinder import exception
from cinder import flags
@ -58,20 +56,6 @@ volume_opts = [
cfg.IntOpt('iscsi_port',
default=3260,
help='The port that the iSCSI daemon is listening on'),
cfg.StrOpt('rbd_pool',
default='rbd',
help='the RADOS pool in which rbd volumes are stored'),
cfg.StrOpt('rbd_user',
default=None,
help='the RADOS client name for accessing rbd volumes'),
cfg.StrOpt('rbd_secret_uuid',
default=None,
help='the libvirt uuid of the secret for the rbd_user'
'volumes'),
cfg.StrOpt('volume_tmp_dir',
default=None,
help='where to store temporary image files if the volume '
'driver does not write them directly to the volume'),
]
FLAGS = flags.FLAGS
@ -149,9 +133,9 @@ class VolumeDriver(object):
# zero out old volumes to prevent data leaking between users
# TODO(ja): reclaiming space should be done lazy and low priority
dev_path = self.local_path(volume)
if os.path.exists(dev_path):
if FLAGS.secure_delete:
self._copy_volume('/dev/zero', dev_path, size_in_g)
if FLAGS.secure_delete and os.path.exists(dev_path):
self._copy_volume('/dev/zero', dev_path, size_in_g)
self._try_execute('lvremove', '-f', "%s/%s" %
(FLAGS.volume_group,
self._escape_snapshot(volume['name'])),
@ -614,363 +598,6 @@ class ISCSIDriver(VolumeDriver):
image_service.update(context, image_id, {}, volume_file)
class FakeISCSIDriver(ISCSIDriver):
"""Logs calls instead of executing."""
def __init__(self, *args, **kwargs):
super(FakeISCSIDriver, self).__init__(execute=self.fake_execute,
*args, **kwargs)
def check_for_setup_error(self):
"""No setup necessary in fake mode."""
pass
def initialize_connection(self, volume, connector):
return {
'driver_volume_type': 'iscsi',
'data': {}
}
def terminate_connection(self, volume, connector):
pass
@staticmethod
def fake_execute(cmd, *_args, **_kwargs):
"""Execute that simply logs the command."""
LOG.debug(_("FAKE ISCSI: %s"), cmd)
return (None, None)
class RBDDriver(VolumeDriver):
"""Implements RADOS block device (RBD) volume commands"""
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met"""
(stdout, stderr) = self._execute('rados', 'lspools')
pools = stdout.split("\n")
if not FLAGS.rbd_pool in pools:
exception_message = (_("rbd has no pool %s") %
FLAGS.rbd_pool)
raise exception.VolumeBackendAPIException(data=exception_message)
def _supports_layering(self):
stdout, _ = self._execute('rbd', '--help')
return 'clone' in stdout
def create_volume(self, volume):
"""Creates a logical volume."""
if int(volume['size']) == 0:
size = 100
else:
size = int(volume['size']) * 1024
args = ['rbd', 'create',
'--pool', FLAGS.rbd_pool,
'--size', size,
volume['name']]
if self._supports_layering():
args += ['--new-format']
self._try_execute(*args)
def _clone(self, volume, src_pool, src_image, src_snap):
self._try_execute('rbd', 'clone',
'--pool', src_pool,
'--image', src_image,
'--snap', src_snap,
'--dest-pool', FLAGS.rbd_pool,
'--dest', volume['name'])
def _resize(self, volume):
size = int(volume['size']) * 1024
self._try_execute('rbd', 'resize',
'--pool', FLAGS.rbd_pool,
'--image', volume['name'],
'--size', size)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
self._clone(volume, FLAGS.rbd_pool,
snapshot['volume_name'], snapshot['name'])
if int(volume['size']):
self._resize(volume)
def delete_volume(self, volume):
"""Deletes a logical volume."""
stdout, _ = self._execute('rbd', 'snap', 'ls',
'--pool', FLAGS.rbd_pool,
volume['name'])
if stdout.count('\n') > 1:
raise exception.VolumeIsBusy(volume_name=volume['name'])
self._try_execute('rbd', 'rm',
'--pool', FLAGS.rbd_pool,
volume['name'])
def create_snapshot(self, snapshot):
"""Creates an rbd snapshot"""
self._try_execute('rbd', 'snap', 'create',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
if self._supports_layering():
self._try_execute('rbd', 'snap', 'protect',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
def delete_snapshot(self, snapshot):
"""Deletes an rbd snapshot"""
if self._supports_layering():
try:
self._try_execute('rbd', 'snap', 'unprotect',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
except exception.ProcessExecutionError:
raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
self._try_execute('rbd', 'snap', 'rm',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
def local_path(self, volume):
"""Returns the path of the rbd volume."""
# This is the same as the remote path
# since qemu accesses it directly.
return "rbd:%s/%s" % (FLAGS.rbd_pool, volume['name'])
def ensure_export(self, context, volume):
"""Synchronously recreates an export for a logical volume."""
pass
def create_export(self, context, volume):
"""Exports the volume"""
pass
def remove_export(self, context, volume):
"""Removes an export for a logical volume"""
pass
def initialize_connection(self, volume, connector):
return {
'driver_volume_type': 'rbd',
'data': {
'name': '%s/%s' % (FLAGS.rbd_pool, volume['name']),
'auth_enabled': FLAGS.rbd_secret_uuid is not None,
'auth_username': FLAGS.rbd_user,
'secret_type': 'ceph',
'secret_uuid': FLAGS.rbd_secret_uuid,
}
}
def terminate_connection(self, volume, connector):
pass
def _parse_location(self, location):
prefix = 'rbd://'
if not location.startswith(prefix):
reason = _('Image %s is not stored in rbd') % location
raise exception.ImageUnacceptable(reason)
pieces = map(urllib.unquote, location[len(prefix):].split('/'))
if any(map(lambda p: p == '', pieces)):
reason = _('Image %s has blank components') % location
raise exception.ImageUnacceptable(reason)
if len(pieces) != 4:
reason = _('Image %s is not an rbd snapshot') % location
raise exception.ImageUnacceptable(reason)
return pieces
def _get_fsid(self):
stdout, _ = self._execute('ceph', 'fsid')
return stdout.rstrip('\n')
def _is_cloneable(self, image_location):
try:
fsid, pool, image, snapshot = self._parse_location(image_location)
except exception.ImageUnacceptable:
return False
if self._get_fsid() != fsid:
reason = _('%s is in a different ceph cluster') % image_location
LOG.debug(reason)
return False
# check that we can read the image
try:
self._execute('rbd', 'info',
'--pool', pool,
'--image', image,
'--snap', snapshot)
except exception.ProcessExecutionError:
LOG.debug(_('Unable to read image %s') % image_location)
return False
return True
def clone_image(self, volume, image_location):
if image_location is None or not self._is_cloneable(image_location):
return False
_, pool, image, snapshot = self._parse_location(image_location)
self._clone(volume, pool, image, snapshot)
self._resize(volume)
return True
def copy_image_to_volume(self, context, volume, image_service, image_id):
# TODO(jdurgin): replace with librbd
# this is a temporary hack, since rewriting this driver
# to use librbd would take too long
if FLAGS.volume_tmp_dir and not os.path.exists(FLAGS.volume_tmp_dir):
os.makedirs(FLAGS.volume_tmp_dir)
with tempfile.NamedTemporaryFile(dir=FLAGS.volume_tmp_dir) as tmp:
image_service.download(context, image_id, tmp)
# import creates the image, so we must remove it first
self._try_execute('rbd', 'rm',
'--pool', FLAGS.rbd_pool,
volume['name'])
self._try_execute('rbd', 'import',
'--pool', FLAGS.rbd_pool,
tmp.name, volume['name'])
class SheepdogDriver(VolumeDriver):
"""Executes commands relating to Sheepdog Volumes"""
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met"""
try:
#NOTE(francois-charlier) Since 0.24 'collie cluster info -r'
# gives short output, but for compatibility reason we won't
# use it and just check if 'running' is in the output.
(out, err) = self._execute('collie', 'cluster', 'info')
if not 'running' in out.split():
exception_message = (_("Sheepdog is not working: %s") % out)
raise exception.VolumeBackendAPIException(
data=exception_message)
except exception.ProcessExecutionError:
exception_message = _("Sheepdog is not working")
raise exception.VolumeBackendAPIException(data=exception_message)
def create_volume(self, volume):
"""Creates a sheepdog volume"""
self._try_execute('qemu-img', 'create',
"sheepdog:%s" % volume['name'],
self._sizestr(volume['size']))
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a sheepdog volume from a snapshot."""
self._try_execute('qemu-img', 'create', '-b',
"sheepdog:%s:%s" % (snapshot['volume_name'],
snapshot['name']),
"sheepdog:%s" % volume['name'])
def delete_volume(self, volume):
"""Deletes a logical volume"""
self._try_execute('collie', 'vdi', 'delete', volume['name'])
def create_snapshot(self, snapshot):
"""Creates a sheepdog snapshot"""
self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'],
"sheepdog:%s" % snapshot['volume_name'])
def delete_snapshot(self, snapshot):
"""Deletes a sheepdog snapshot"""
self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'],
'-s', snapshot['name'])
def local_path(self, volume):
return "sheepdog:%s" % volume['name']
def ensure_export(self, context, volume):
"""Safely and synchronously recreates an export for a logical volume"""
pass
def create_export(self, context, volume):
"""Exports the volume"""
pass
def remove_export(self, context, volume):
"""Removes an export for a logical volume"""
pass
def initialize_connection(self, volume, connector):
return {
'driver_volume_type': 'sheepdog',
'data': {
'name': volume['name']
}
}
def terminate_connection(self, volume, connector):
pass
class LoggingVolumeDriver(VolumeDriver):
"""Logs and records calls, for unit tests."""
def check_for_setup_error(self):
pass
def create_volume(self, volume):
self.log_action('create_volume', volume)
def delete_volume(self, volume):
self.log_action('delete_volume', volume)
def local_path(self, volume):
print "local_path not implemented"
raise NotImplementedError()
def ensure_export(self, context, volume):
self.log_action('ensure_export', volume)
def create_export(self, context, volume):
self.log_action('create_export', volume)
def remove_export(self, context, volume):
self.log_action('remove_export', volume)
def initialize_connection(self, volume, connector):
self.log_action('initialize_connection', volume)
def terminate_connection(self, volume, connector):
self.log_action('terminate_connection', volume)
_LOGS = []
@staticmethod
def clear_logs():
LoggingVolumeDriver._LOGS = []
@staticmethod
def log_action(action, parameters):
"""Logs the command."""
LOG.debug(_("LoggingVolumeDriver: %s") % (action))
log_dictionary = {}
if parameters:
log_dictionary = dict(parameters)
log_dictionary['action'] = action
LOG.debug(_("LoggingVolumeDriver: %s") % (log_dictionary))
LoggingVolumeDriver._LOGS.append(log_dictionary)
@staticmethod
def all_logs():
return LoggingVolumeDriver._LOGS
@staticmethod
def logs_like(action, **kwargs):
matches = []
for entry in LoggingVolumeDriver._LOGS:
if entry['action'] != action:
continue
match = True
for k, v in kwargs.iteritems():
if entry.get(k) != v:
match = False
break
if match:
matches.append(entry)
return matches
def _iscsi_location(ip, target, iqn, lun=None):
return "%s:%s,%s %s %s" % (ip, FLAGS.iscsi_port, target, iqn, lun)

View File

@ -0,0 +1,22 @@
# Copyright 2012 OpenStack LLC
#
# 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.
"""
:mod:`cinder.volume.driver` -- Cinder Drivers
=====================================================
.. automodule:: cinder.volume.driver
:platform: Unix
:synopsis: Module containing all the Cinder drivers.
"""

View File

@ -0,0 +1,239 @@
# Copyright 2012 OpenStack LLC
#
# 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.
"""
RADOS Block Device Driver
"""
import os
import tempfile
import urllib
from cinder import exception
from cinder import flags
from cinder.openstack.common import log as logging
from cinder.openstack.common import cfg
from cinder.volume import driver
LOG = logging.getLogger(__name__)
rbd_opts = [
cfg.StrOpt('rbd_pool',
default='rbd',
help='the RADOS pool in which rbd volumes are stored'),
cfg.StrOpt('rbd_user',
default=None,
help='the RADOS client name for accessing rbd volumes'),
cfg.StrOpt('rbd_secret_uuid',
default=None,
help='the libvirt uuid of the secret for the rbd_user'
'volumes'),
cfg.StrOpt('volume_tmp_dir',
default=None,
help='where to store temporary image files if the volume '
'driver does not write them directly to the volume'),
]
FLAGS = flags.FLAGS
FLAGS.register_opts(rbd_opts)
class RBDDriver(driver.VolumeDriver):
"""Implements RADOS block device (RBD) volume commands"""
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met"""
(stdout, stderr) = self._execute('rados', 'lspools')
pools = stdout.split("\n")
if not FLAGS.rbd_pool in pools:
exception_message = (_("rbd has no pool %s") %
FLAGS.rbd_pool)
raise exception.VolumeBackendAPIException(data=exception_message)
def _supports_layering(self):
stdout, _ = self._execute('rbd', '--help')
return 'clone' in stdout
def create_volume(self, volume):
"""Creates a logical volume."""
if int(volume['size']) == 0:
size = 100
else:
size = int(volume['size']) * 1024
args = ['rbd', 'create',
'--pool', FLAGS.rbd_pool,
'--size', size,
volume['name']]
if self._supports_layering():
args += ['--new-format']
self._try_execute(*args)
def _clone(self, volume, src_pool, src_image, src_snap):
self._try_execute('rbd', 'clone',
'--pool', src_pool,
'--image', src_image,
'--snap', src_snap,
'--dest-pool', FLAGS.rbd_pool,
'--dest', volume['name'])
def _resize(self, volume):
size = int(volume['size']) * 1024
self._try_execute('rbd', 'resize',
'--pool', FLAGS.rbd_pool,
'--image', volume['name'],
'--size', size)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
self._clone(volume, FLAGS.rbd_pool,
snapshot['volume_name'], snapshot['name'])
if int(volume['size']):
self._resize(volume)
def delete_volume(self, volume):
"""Deletes a logical volume."""
stdout, _ = self._execute('rbd', 'snap', 'ls',
'--pool', FLAGS.rbd_pool,
volume['name'])
if stdout.count('\n') > 1:
raise exception.VolumeIsBusy(volume_name=volume['name'])
self._try_execute('rbd', 'rm',
'--pool', FLAGS.rbd_pool,
volume['name'])
def create_snapshot(self, snapshot):
"""Creates an rbd snapshot"""
self._try_execute('rbd', 'snap', 'create',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
if self._supports_layering():
self._try_execute('rbd', 'snap', 'protect',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
def delete_snapshot(self, snapshot):
"""Deletes an rbd snapshot"""
if self._supports_layering():
try:
self._try_execute('rbd', 'snap', 'unprotect',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
except exception.ProcessExecutionError:
raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
self._try_execute('rbd', 'snap', 'rm',
'--pool', FLAGS.rbd_pool,
'--snap', snapshot['name'],
snapshot['volume_name'])
def local_path(self, volume):
"""Returns the path of the rbd volume."""
# This is the same as the remote path
# since qemu accesses it directly.
return "rbd:%s/%s" % (FLAGS.rbd_pool, volume['name'])
def ensure_export(self, context, volume):
"""Synchronously recreates an export for a logical volume."""
pass
def create_export(self, context, volume):
"""Exports the volume"""
pass
def remove_export(self, context, volume):
"""Removes an export for a logical volume"""
pass
def initialize_connection(self, volume, connector):
return {
'driver_volume_type': 'rbd',
'data': {
'name': '%s/%s' % (FLAGS.rbd_pool, volume['name']),
'auth_enabled': FLAGS.rbd_secret_uuid is not None,
'auth_username': FLAGS.rbd_user,
'secret_type': 'ceph',
'secret_uuid': FLAGS.rbd_secret_uuid,
}
}
def terminate_connection(self, volume, connector):
pass
def _parse_location(self, location):
prefix = 'rbd://'
if not location.startswith(prefix):
reason = _('Image %s is not stored in rbd') % location
raise exception.ImageUnacceptable(reason)
pieces = map(urllib.unquote, location[len(prefix):].split('/'))
if any(map(lambda p: p == '', pieces)):
reason = _('Image %s has blank components') % location
raise exception.ImageUnacceptable(reason)
if len(pieces) != 4:
reason = _('Image %s is not an rbd snapshot') % location
raise exception.ImageUnacceptable(reason)
return pieces
def _get_fsid(self):
stdout, _ = self._execute('ceph', 'fsid')
return stdout.rstrip('\n')
def _is_cloneable(self, image_location):
try:
fsid, pool, image, snapshot = self._parse_location(image_location)
except exception.ImageUnacceptable:
return False
if self._get_fsid() != fsid:
reason = _('%s is in a different ceph cluster') % image_location
LOG.debug(reason)
return False
# check that we can read the image
try:
self._execute('rbd', 'info',
'--pool', pool,
'--image', image,
'--snap', snapshot)
except exception.ProcessExecutionError:
LOG.debug(_('Unable to read image %s') % image_location)
return False
return True
def clone_image(self, volume, image_location):
if image_location is None or not self._is_cloneable(image_location):
return False
_, pool, image, snapshot = self._parse_location(image_location)
self._clone(volume, pool, image, snapshot)
self._resize(volume)
return True
def copy_image_to_volume(self, context, volume, image_service, image_id):
# TODO(jdurgin): replace with librbd
# this is a temporary hack, since rewriting this driver
# to use librbd would take too long
if FLAGS.volume_tmp_dir and not os.path.exists(FLAGS.volume_tmp_dir):
os.makedirs(FLAGS.volume_tmp_dir)
with tempfile.NamedTemporaryFile(dir=FLAGS.volume_tmp_dir) as tmp:
image_service.download(context, image_id, tmp)
# import creates the image, so we must remove it first
self._try_execute('rbd', 'rm',
'--pool', FLAGS.rbd_pool,
volume['name'])
self._try_execute('rbd', 'import',
'--pool', FLAGS.rbd_pool,
tmp.name, volume['name'])

View File

@ -0,0 +1,100 @@
# Copyright 2012 OpenStack LLC
#
# 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.
"""
SheepDog Volume Driver.
"""
from cinder import exception
from cinder import flags
from cinder.openstack.common import log as logging
from cinder.volume import driver
LOG = logging.getLogger(__name__)
FLAGS = flags.FLAGS
class SheepdogDriver(driver.VolumeDriver):
"""Executes commands relating to Sheepdog Volumes"""
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met"""
try:
#NOTE(francois-charlier) Since 0.24 'collie cluster info -r'
# gives short output, but for compatibility reason we won't
# use it and just check if 'running' is in the output.
(out, err) = self._execute('collie', 'cluster', 'info')
if not 'running' in out.split():
exception_message = (_("Sheepdog is not working: %s") % out)
raise exception.VolumeBackendAPIException(
data=exception_message)
except exception.ProcessExecutionError:
exception_message = _("Sheepdog is not working")
raise exception.VolumeBackendAPIException(data=exception_message)
def create_volume(self, volume):
"""Creates a sheepdog volume"""
self._try_execute('qemu-img', 'create',
"sheepdog:%s" % volume['name'],
self._sizestr(volume['size']))
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a sheepdog volume from a snapshot."""
self._try_execute('qemu-img', 'create', '-b',
"sheepdog:%s:%s" % (snapshot['volume_name'],
snapshot['name']),
"sheepdog:%s" % volume['name'])
def delete_volume(self, volume):
"""Deletes a logical volume"""
self._try_execute('collie', 'vdi', 'delete', volume['name'])
def create_snapshot(self, snapshot):
"""Creates a sheepdog snapshot"""
self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'],
"sheepdog:%s" % snapshot['volume_name'])
def delete_snapshot(self, snapshot):
"""Deletes a sheepdog snapshot"""
self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'],
'-s', snapshot['name'])
def local_path(self, volume):
return "sheepdog:%s" % volume['name']
def ensure_export(self, context, volume):
"""Safely and synchronously recreates an export for a logical volume"""
pass
def create_export(self, context, volume):
"""Exports the volume"""
pass
def remove_export(self, context, volume):
"""Removes an export for a logical volume"""
pass
def initialize_connection(self, volume, connector):
return {
'driver_volume_type': 'sheepdog',
'data': {
'name': volume['name']
}
}
def terminate_connection(self, volume, connector):
pass

View File

@ -71,6 +71,12 @@ volume_manager_opts = [
FLAGS = flags.FLAGS
FLAGS.register_opts(volume_manager_opts)
MAPPING = {
'cinder.volume.driver.RBDDriver': 'cinder.volume.drivers.rbd.RBDDriver',
'cinder.volume.driver.SheepdogDriver':
'cinder.volume.drivers.sheepdog.SheepdogDriver',
}
class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
@ -78,7 +84,10 @@ class VolumeManager(manager.SchedulerDependentManager):
"""Load the driver from the one specified in args, or from flags."""
if not volume_driver:
volume_driver = FLAGS.volume_driver
self.driver = importutils.import_object(volume_driver)
if volume_driver in MAPPING:
self.driver = importutils.import_object(MAPPING[volume_driver])
else:
self.driver = importutils.import_object(volume_driver)
super(VolumeManager, self).__init__(service_name='volume',
*args, **kwargs)
# NOTE(vish): Implementation specific db handling is done

View File

@ -535,6 +535,9 @@
# iscsi_port=3260
#### (IntOpt) The port that the iSCSI daemon is listening on
######## defined in cinder.volume.drivers.rbd ########
# rbd_pool=rbd
#### (StrOpt) the RADOS pool in which rbd volumes are stored