diff --git a/cinder/tests/fake_driver.py b/cinder/tests/fake_driver.py new file mode 100644 index 00000000000..261bfafb4bf --- /dev/null +++ b/cinder/tests/fake_driver.py @@ -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 diff --git a/cinder/tests/fake_flags.py b/cinder/tests/fake_flags.py index e2147c63c58..eb245073d9c 100644 --- a/cinder/tests/fake_flags.py +++ b/cinder/tests/fake_flags.py @@ -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') diff --git a/cinder/tests/integrated/test_volumes.py b/cinder/tests/integrated/test_volumes.py index 0ec851113d1..e08b9adada8 100644 --- a/cinder/tests/integrated/test_volumes.py +++ b/cinder/tests/integrated/test_volumes.py @@ -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)) diff --git a/cinder/tests/test_drivers_compatibility.py b/cinder/tests/test_drivers_compatibility.py new file mode 100644 index 00000000000..263433f9c8c --- /dev/null +++ b/cinder/tests/test_drivers_compatibility.py @@ -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) diff --git a/cinder/tests/test_rbd.py b/cinder/tests/test_rbd.py index 4ce9cfa9737..5aa011a326d 100644 --- a/cinder/tests/test_rbd.py +++ b/cinder/tests/test_rbd.py @@ -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__) diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index f894a1c6154..4eb45eaddd7 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -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) diff --git a/cinder/volume/drivers/__init__.py b/cinder/volume/drivers/__init__.py new file mode 100644 index 00000000000..815a442c69f --- /dev/null +++ b/cinder/volume/drivers/__init__.py @@ -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. +""" diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py new file mode 100644 index 00000000000..3e02dfc4cca --- /dev/null +++ b/cinder/volume/drivers/rbd.py @@ -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']) diff --git a/cinder/volume/drivers/sheepdog.py b/cinder/volume/drivers/sheepdog.py new file mode 100644 index 00000000000..b256e975b52 --- /dev/null +++ b/cinder/volume/drivers/sheepdog.py @@ -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 diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 915e5cf0168..441d9ddaff7 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -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 diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index f976d05ea06..e7979783ada 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -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