]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Splitting out volume drivers in driver.py
authorNirmal Ranganathan <rnirmal@gmail.com>
Sun, 21 Oct 2012 19:07:13 +0000 (14:07 -0500)
committerjohn-griffith <john.griffith@solidfire.com>
Wed, 7 Nov 2012 03:22:45 +0000 (20:22 -0700)
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

cinder/tests/fake_driver.py [new file with mode: 0644]
cinder/tests/fake_flags.py
cinder/tests/integrated/test_volumes.py
cinder/tests/test_drivers_compatibility.py [new file with mode: 0644]
cinder/tests/test_rbd.py
cinder/volume/driver.py
cinder/volume/drivers/__init__.py [new file with mode: 0644]
cinder/volume/drivers/rbd.py [new file with mode: 0644]
cinder/volume/drivers/sheepdog.py [new file with mode: 0644]
cinder/volume/manager.py
etc/cinder/cinder.conf.sample

diff --git a/cinder/tests/fake_driver.py b/cinder/tests/fake_driver.py
new file mode 100644 (file)
index 0000000..261bfaf
--- /dev/null
@@ -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
index e2147c63c582d3708bf54d8ebef0dfaf32437e25..eb245073d9c197f3096038b783a9a5dfc788982d 100644 (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')
index 0ec851113d15b555d1a9e13b95fbf0ec2994a1dd..e08b9adada8e5df2c5148820f2ab5f30a6074c78 100644 (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))
diff --git a/cinder/tests/test_drivers_compatibility.py b/cinder/tests/test_drivers_compatibility.py
new file mode 100644 (file)
index 0000000..263433f
--- /dev/null
@@ -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)
index 4ce9cfa973766322d3a9b1dbcc03f240974ec2e1..5aa011a326db174f4e96bb5fb95defa60efc5fea 100644 (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__)
 
index f894a1c61547f9be9103ecd2f14e0432dac07701..4eb45eaddd7f39e31df817dedfc24159efa9b4bb 100644 (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)
 
diff --git a/cinder/volume/drivers/__init__.py b/cinder/volume/drivers/__init__.py
new file mode 100644 (file)
index 0000000..815a442
--- /dev/null
@@ -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 (file)
index 0000000..3e02dfc
--- /dev/null
@@ -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 (file)
index 0000000..b256e97
--- /dev/null
@@ -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
index 915e5cf016826a6ff247125b50f1f8bc4e0c0cdb..441d9ddaff7c20e1ba2bb22bcb0a84d9e9339bb2 100644 (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
index f976d05ea06ed175fe13c15753768660280bab59..e7979783ada3c36aedd91d1ac9095643711a24bc 100644 (file)
 # 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