--- /dev/null
+# 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
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')
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
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")
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):
# 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)
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))
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))
--- /dev/null
+# 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)
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__)
import os
import re
-import tempfile
import time
-import urllib
from cinder import exception
from cinder import flags
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
# 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'])),
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)
--- /dev/null
+# 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.
+"""
--- /dev/null
+# 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'])
--- /dev/null
+# 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
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."""
"""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
# 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