]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Scality SRB driver
authorDavid Pineau <dav.pineau@gmail.com>
Mon, 1 Dec 2014 11:21:35 +0000 (12:21 +0100)
committerNicolas Trangez <ikke@nicolast.be>
Wed, 17 Dec 2014 12:10:43 +0000 (13:10 +0100)
This patch implements the `srb-driver` blueprint. It uses a similar
approach as Ceph's RBD in the way it exposes a block device to the
system.

The driver controls the SRB Linux kernel driver through a sysfs
interface and leverages LVM for snapshot management. The native block
driver talks to Scality's storage system (or any other vendor exposing
a compatible CDMI interface) through a HTTP-based RESTful protocol.

iSCSI export to Nova is provided.

Driver cert results are provided in bug #1400327.

Implements: blueprint srb-driver
See: https://blueprints.launchpad.net/cinder/+spec/srb-driver
Related-Bug: #1400327
See: https://bugs.launchpad.net/cinder/+bug/1400327
Co-Authored-By: Nicolas Trangez <ikke@nicolast.be>
Co-Authored-By: JordanP <jordan.pittier@scality.com>
Change-Id: Id8d00df9db4004d5aeb8c0269d114ef50e0b8f8e

cinder/tests/test_srb.py [new file with mode: 0644]
cinder/volume/drivers/srb.py [new file with mode: 0644]
etc/cinder/rootwrap.d/volume.filters

diff --git a/cinder/tests/test_srb.py b/cinder/tests/test_srb.py
new file mode 100644 (file)
index 0000000..4599b2a
--- /dev/null
@@ -0,0 +1,946 @@
+# Copyright (c) 2014 Scality
+#
+#    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.
+
+"""
+Unit tests for the Scality Rest Block Volume Driver.
+"""
+
+import mock
+from oslo.concurrency import processutils
+from oslo.utils import units
+
+from cinder import context
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder.tests.brick import test_brick_lvm
+from cinder.volume import configuration as conf
+from cinder.volume.drivers import srb
+
+LOG = logging.getLogger(__name__)
+
+
+class SRBLvmTestCase(test_brick_lvm.BrickLvmTestCase):
+    def setUp(self):
+        super(SRBLvmTestCase, self).setUp()
+
+        self.vg = srb.LVM(self.configuration.volume_group_name,
+                          'sudo',
+                          False, None,
+                          'default',
+                          self.fake_execute)
+
+    def fake_execute(self, *cmd, **kwargs):
+        try:
+            return super(SRBLvmTestCase, self).fake_execute(*cmd, **kwargs)
+        except AssertionError:
+            pass
+
+        cmd_string = ', '.join(cmd)
+
+        if 'vgremove, -f, ' in cmd_string:
+            pass
+        elif 'pvresize, ' in cmd_string:
+            pass
+        elif 'lvextend, ' in cmd_string:
+            pass
+        elif 'lvchange, ' in cmd_string:
+            pass
+        else:
+            raise AssertionError('unexpected command called: %s' % cmd_string)
+
+    def test_activate_vg(self):
+        executor = mock.MagicMock()
+        self.vg.set_execute(executor)
+        self.vg.activate_vg()
+        executor.assert_called_once_with('vgchange', '-ay',
+                                         self.configuration.volume_group_name,
+                                         root_helper=self.vg._root_helper,
+                                         run_as_root=True)
+
+    def test_deactivate_vg(self):
+        executor = mock.MagicMock()
+        self.vg.set_execute(executor)
+        self.vg.deactivate_vg()
+        executor.assert_called_once_with('vgchange', '-an',
+                                         self.configuration.volume_group_name,
+                                         root_helper=self.vg._root_helper,
+                                         run_as_root=True)
+
+    def test_destroy_vg(self):
+        executor = mock.MagicMock()
+        self.vg.set_execute(executor)
+        self.vg.destroy_vg()
+        executor.assert_called_once_with('vgremove', '-f',
+                                         self.configuration.volume_group_name,
+                                         root_helper=self.vg._root_helper,
+                                         run_as_root=True)
+
+    def test_pv_resize(self):
+        executor = mock.MagicMock()
+        self.vg.set_execute(executor)
+        self.vg.pv_resize('fake-pv', '50G')
+        executor.assert_called_once_with('pvresize',
+                                         '--setphysicalvolumesize',
+                                         '50G', 'fake-pv',
+                                         root_helper=self.vg._root_helper,
+                                         run_as_root=True)
+
+    def test_extend_thin_pool_nothin(self):
+        executor =\
+            mock.MagicMock(side_effect=Exception('Unexpected call to execute'))
+        self.vg.set_execute(executor)
+        thin_calc =\
+            mock.MagicMock(
+                side_effect=
+                Exception('Unexpected call to _calculate_thin_pool_size'))
+        self.vg._calculate_thin_pool_size = thin_calc
+        self.vg.extend_thin_pool()
+
+    def test_extend_thin_pool_thin(self):
+        self.stubs.Set(processutils, 'execute', self.fake_execute)
+        self.thin_vg = srb.LVM(self.configuration.volume_group_name,
+                               'sudo',
+                               False, None,
+                               'thin',
+                               self.fake_execute)
+        self.assertTrue(self.thin_vg.supports_thin_provisioning('sudo'))
+        self.thin_vg.update_volume_group_info = mock.MagicMock()
+        with mock.patch('oslo.concurrency.processutils.execute'):
+            executor = mock.MagicMock()
+            self.thin_vg._execute = executor
+            self.thin_vg.extend_thin_pool()
+            executor.assert_called_once_with('lvextend',
+                                             '-L', '9.5g',
+                                             'fake-vg/fake-vg-pool',
+                                             root_helper=self.vg._root_helper,
+                                             run_as_root=True)
+            self.thin_vg.update_volume_group_info.assert_called_once_with()
+
+
+class SRBRetryTestCase(test.TestCase):
+
+    def __init__(self, *args, **kwargs):
+        super(SRBRetryTestCase, self).__init__(*args, **kwargs)
+        self.attempts = 0
+
+    def setUp(self):
+        super(SRBRetryTestCase, self).setUp()
+        self.attempts = 0
+
+    def test_retry_no_failure(self):
+        expected_attempts = 1
+
+        @srb.retry(exceptions=(), count=expected_attempts)
+        def _try_failing(self):
+            self.attempts = self.attempts + 1
+            return True
+
+        ret = _try_failing(self)
+
+        self.assertTrue(ret)
+        self.assertEqual(expected_attempts, self.attempts)
+
+    def test_retry_fail_by_exception(self):
+        expected_attempts = 2
+        ret = None
+
+        @srb.retry(count=expected_attempts,
+                   exceptions=(processutils.ProcessExecutionError))
+        def _try_failing(self):
+            self.attempts = self.attempts + 1
+            raise processutils.ProcessExecutionError("Fail everytime")
+            return True
+
+        try:
+            ret = _try_failing(self)
+        except processutils.ProcessExecutionError:
+            pass
+
+        self.assertIsNone(ret)
+        self.assertEqual(expected_attempts, self.attempts)
+
+    def test_retry_fail_and_succeed_mixed(self):
+
+        @srb.retry(count=4, exceptions=(Exception))
+        def _try_failing(self):
+            attempted = self.attempts
+            self.attempts = self.attempts + 1
+            if attempted == 0:
+                raise IOError(0, 'Oops')
+            if attempted == 1:
+                raise Exception("Second try shall except")
+            if attempted == 2:
+                assert False
+            return 34
+
+        ret = _try_failing(self)
+
+        self.assertEqual(34, ret)
+        self.assertEqual(4, self.attempts)
+
+
+class TestHandleProcessExecutionError(test.TestCase):
+    def test_no_exception(self):
+        with srb.handle_process_execution_error(
+                message='', info_message='', reraise=True):
+            pass
+
+    def test_other_exception(self):
+        def f():
+            with srb.handle_process_execution_error(
+                    message='', info_message='', reraise=True):
+                1 / 0
+
+        self.assertRaises(ZeroDivisionError, f)
+
+    def test_reraise_true(self):
+        def f():
+            with srb.handle_process_execution_error(
+                    message='', info_message='', reraise=True):
+                raise processutils.ProcessExecutionError(description='Oops')
+
+        self.assertRaisesRegex(processutils.ProcessExecutionError, r'^Oops', f)
+
+    def test_reraise_false(self):
+        with srb.handle_process_execution_error(
+                message='', info_message='', reraise=False):
+            raise processutils.ProcessExecutionError(description='Oops')
+
+    def test_reraise_exception(self):
+        def f():
+            with srb.handle_process_execution_error(
+                    message='', info_message='', reraise=RuntimeError('Oops')):
+                raise processutils.ProcessExecutionError
+
+        self.assertRaisesRegex(RuntimeError, r'^Oops', f)
+
+
+class SRBDriverTestCase(test.TestCase):
+    """Test case for the Scality Rest Block driver."""
+
+    def __init__(self, *args, **kwargs):
+        super(SRBDriverTestCase, self).__init__(*args, **kwargs)
+        self._urls = []
+        self._volumes = {
+            "fake-old-volume": {
+                "name": "fake-old-volume",
+                "size": 4 * units.Gi,
+                "vgs": {
+                    "fake-old-volume": {
+                        "lvs": {"vol1": 4 * units.Gi},
+                        "snaps": ["snap1", "snap2", "snap3"],
+                    },
+                },
+            },
+            "volume-extend": {
+                "name": "volume-extend",
+                "size": 4 * units.Gi,
+                "vgs": {
+                    "volume-extend": {
+                        "lvs": {"volume-extend-pool": 0.95 * 4 * units.Gi,
+                                "volume-extend": 4 * units.Gi},
+                        "snaps": [],
+                    },
+                },
+            },
+            "volume-SnapBase": {
+                "name": "volume-SnapBase",
+                "size": 4 * units.Gi,
+                "vgs": {
+                    "volume-SnapBase": {
+                        "lvs": {"volume-SnapBase-pool": 0.95 * 4 * units.Gi,
+                                "volume-SnapBase": 4 * units.Gi},
+                        "snaps": ['snapshot-SnappedBase', 'snapshot-delSnap'],
+                    },
+                },
+            },
+        }
+
+    @staticmethod
+    def _convert_size(s):
+        if isinstance(s, (int, long)):
+            return s
+
+        try:
+            return int(s)
+        except ValueError:
+            pass
+
+        conv_map = {
+            'g': units.Gi,
+            'G': units.Gi,
+            'm': units.Mi,
+            'M': units.Mi,
+            'k': units.Ki,
+            'K': units.Ki,
+        }
+
+        if s[-1] in conv_map:
+            return int(s[:-1]) * conv_map[s[-1]]
+
+        raise ValueError('Unknown size: %r' % s)
+
+    def _fake_add_urls(self):
+        def check(cmd_string):
+            return '> /sys/class/srb/add_urls' in cmd_string
+
+        def act(cmd):
+            self._urls.append(cmd[2].split()[1])
+
+        return check, act
+
+    def _fake_create(self):
+        def check(cmd_string):
+            return '> /sys/class/srb/create' in cmd_string
+
+        def act(cmd):
+            volname = cmd[2].split()[1]
+            volsize = cmd[2].split()[2]
+            self._volumes[volname] = {
+                "name": volname,
+                "size": self._convert_size(volsize),
+                "vgs": {
+                },
+            }
+
+        return check, act
+
+    def _fake_destroy(self):
+        def check(cmd_string):
+            return '> /sys/class/srb/destroy' in cmd_string
+
+        def act(cmd):
+            volname = cmd[2].split()[1]
+            del self._volumes[volname]
+
+        return check, act
+
+    def _fake_extend(self):
+        def check(cmd_string):
+            return '> /sys/class/srb/extend' in cmd_string
+
+        def act(cmd):
+            volname = cmd[2].split()[1]
+            volsize = cmd[2].split()[2]
+            self._volumes[volname]["size"] = self._convert_size(volsize)
+
+        return check, act
+
+    def _fake_attach(self):
+        def check(cmd_string):
+            return '> /sys/class/srb/attach' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_detach(self):
+        def check(cmd_string):
+            return '> /sys/class/srb/detach' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_vg_list(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, vgs, --noheadings, -o, name' in cmd_string
+
+        def act(cmd):
+            # vg exists
+            data = "  fake-outer-vg\n"
+            for vname in self._volumes:
+                vol = self._volumes[vname]
+                for vgname in vol['vgs']:
+                    data += "  " + vgname + "\n"
+
+            return data
+
+        return check, act
+
+    def _fake_thinpool_free_space(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, lvs, --noheadings, --unit=g, '\
+                '-o, size,data_percent, --separator, :, --nosuffix'\
+                in cmd_string
+
+        def act(cmd):
+            data = ''
+
+            groupname, poolname = cmd[10].split('/')[2:4]
+            for vname in self._volumes:
+                vol = self._volumes[vname]
+                for vgname in vol['vgs']:
+                    if vgname != groupname:
+                        continue
+                    vg = vol['vgs'][vgname]
+                    for lvname in vg['lvs']:
+                        if poolname != lvname:
+                            continue
+                        lv_size = vg['lvs'][lvname]
+                        data += "  %.2f:0.00\n" % (lv_size / units.Gi)
+
+            return data
+
+        return check, act
+
+    def _fake_vgs_version(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, vgs, --version' in cmd_string
+
+        def act(cmd):
+            return "  LVM version:     2.02.95(2) (2012-03-06)\n"
+
+        return check, act
+
+    def _fake_get_all_volumes(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, lvs, --noheadings, --unit=g, ' \
+                '-o, vg_name,name,size, --nosuffix' in cmd_string
+
+        def act(cmd):
+            # get_all_volumes
+            data = "  fake-outer-vg fake-1 1.00g\n"
+            for vname in self._volumes:
+                vol = self._volumes[vname]
+                for vgname in vol['vgs']:
+                    vg = vol['vgs'][vgname]
+                    for lvname in vg['lvs']:
+                        lv_size = vg['lvs'][lvname]
+                        data += "  %s %s %.2fg\n" %\
+                            (vgname, lvname, lv_size)
+
+            return data
+
+        return check, act
+
+    def _fake_get_all_physical_volumes(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, pvs, --noheadings, --unit=g, ' \
+                '-o, vg_name,name,size,free, --separator, :, --nosuffix' \
+                in cmd_string
+
+        def act(cmd):
+            data = "  fake-outer-vg:/dev/fake1:10.00:1.00\n"
+            for vname in self._volumes:
+                vol = self._volumes[vname]
+                for vgname in vol['vgs']:
+                    vg = vol['vgs'][vgname]
+                    for lvname in vg['lvs']:
+                        lv_size = vg['lvs'][lvname]
+                        data += "  %s:/dev/srb/%s/device:%.2f:%.2f\n" %\
+                            (vgname, vol['name'],
+                             lv_size / units.Gi, lv_size / units.Gi)
+
+            return data
+
+        return check, act
+
+    def _fake_get_all_volume_groups(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, vgs, --noheadings, --unit=g, ' \
+                '-o, name,size,free,lv_count,uuid, --separator, :, ' \
+                '--nosuffix' in cmd_string
+
+        def act(cmd):
+            data = ''
+
+            search_vgname = None
+            if len(cmd) == 11:
+                search_vgname = cmd[10]
+            # get_all_volume_groups
+            if search_vgname is None:
+                data = "  fake-outer-vg:10.00:10.00:0:"\
+                       "kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n"
+            for vname in self._volumes:
+                vol = self._volumes[vname]
+                for vgname in vol['vgs']:
+                    if search_vgname is None or search_vgname == vgname:
+                        vg = vol['vgs'][vgname]
+                        data += "  %s:%.2f:%.2f:%i:%s\n" %\
+                            (vgname,
+                             vol['size'] / units.Gi, vol['size'] / units.Gi,
+                             len(vg['lvs']) + len(vg['snaps']), vgname)
+
+            return data
+
+        return check, act
+
+    def _fake_udevadm_settle(self):
+        def check(cmd_string):
+            return 'udevadm, settle, ' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_vgcreate(self):
+        def check(cmd_string):
+            return 'vgcreate, ' in cmd_string
+
+        def act(cmd):
+            volname = "volume-%s" % (cmd[2].split('/')[2].split('-')[1])
+            vgname = cmd[1]
+            self._volumes[volname]['vgs'][vgname] = {
+                "lvs": {},
+                "snaps": []
+            }
+
+        return check, act
+
+    def _fake_vgremove(self):
+        def check(cmd_string):
+            return 'vgremove, -f, ' in cmd_string
+
+        def act(cmd):
+            volname = cmd[2]
+            del self._volumes[volname]['vgs'][volname]
+
+        return check, act
+
+    def _fake_vgchange_ay(self):
+        def check(cmd_string):
+            return 'vgchange, -ay, ' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_vgchange_an(self):
+        def check(cmd_string):
+            return 'vgchange, -an, ' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_lvcreate_T_L(self):
+        def check(cmd_string):
+            return 'lvcreate, -T, -L, ' in cmd_string
+
+        def act(cmd):
+            vgname = cmd[4].split('/')[0]
+            lvname = cmd[4].split('/')[1]
+            if cmd[3][-1] == 'g':
+                lv_size = int(float(cmd[3][0:-1]) * units.Gi)
+            elif cmd[3][-1] == 'B':
+                lv_size = int(cmd[3][0:-1])
+            else:
+                lv_size = int(cmd[3])
+            self._volumes[vgname]['vgs'][vgname]['lvs'][lvname] = lv_size
+
+        return check, act
+
+    def _fake_lvcreate_T_V(self):
+        def check(cmd_string):
+            return 'lvcreate, -T, -V, ' in cmd_string
+
+        def act(cmd):
+            cmd_string = ', '.join(cmd)
+
+            vgname = cmd[6].split('/')[0]
+            poolname = cmd[6].split('/')[1]
+            lvname = cmd[5]
+            if poolname not in self._volumes[vgname]['vgs'][vgname]['lvs']:
+                raise AssertionError('thin-lv creation attempted before '
+                                     'thin-pool creation: %s'
+                                     % cmd_string)
+            if cmd[3][-1] == 'g':
+                lv_size = int(float(cmd[3][0:-1]) * units.Gi)
+            elif cmd[3][-1] == 'B':
+                lv_size = int(cmd[3][0:-1])
+            else:
+                lv_size = int(cmd[3])
+            self._volumes[vgname]['vgs'][vgname]['lvs'][lvname] = lv_size
+
+        return check, act
+
+    def _fake_lvcreate_name(self):
+        def check(cmd_string):
+            return 'lvcreate, --name, ' in cmd_string
+
+        def act(cmd):
+            cmd_string = ', '.join(cmd)
+
+            vgname = cmd[4].split('/')[0]
+            lvname = cmd[4].split('/')[1]
+            snapname = cmd[2]
+            if lvname not in self._volumes[vgname]['vgs'][vgname]['lvs']:
+                raise AssertionError('snap creation attempted on non-existant '
+                                     'thin-lv: %s' % cmd_string)
+            if snapname[1:] in self._volumes[vgname]['vgs'][vgname]['snaps']:
+                raise AssertionError('snap creation attempted on existing '
+                                     'snapshot: %s' % cmd_string)
+            self._volumes[vgname]['vgs'][vgname]['snaps'].append(snapname[1:])
+
+        return check, act
+
+    def _fake_lvchange(self):
+        def check(cmd_string):
+            return 'lvchange, -a, y, --yes' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_lvremove(self):
+
+        def check(cmd_string):
+            return 'lvremove, --config, activation ' \
+                '{ retry_deactivation = 1}, -f, ' in cmd_string
+
+        def act(cmd):
+            cmd_string = ', '.join(cmd)
+
+            vgname = cmd[4].split('/')[0]
+            lvname = cmd[4].split('/')[1]
+            if lvname in self._volumes[vgname]['vgs'][vgname]['lvs']:
+                del self._volumes[vgname]['vgs'][vgname]['lvs'][lvname]
+            elif lvname in self._volumes[vgname]['vgs'][vgname]['snaps']:
+                self._volumes[vgname]['vgs'][vgname]['snaps'].remove(lvname)
+            else:
+                raise AssertionError('Cannot delete inexistant lv or snap'
+                                     'thin-lv: %s' % cmd_string)
+
+        return check, act
+
+    def _fake_lvdisplay(self):
+        def check(cmd_string):
+            return 'env, LC_ALL=C, lvdisplay, --noheading, -C, -o, Attr, ' \
+                in cmd_string
+
+        def act(cmd):
+            data = ''
+            cmd_string = ', '.join(cmd)
+
+            vgname = cmd[7].split('/')[0]
+            lvname = cmd[7].split('/')[1]
+            if lvname not in self._volumes[vgname]['vgs'][vgname]['lvs']:
+                raise AssertionError('Cannot check snaps for inexistant lv'
+                                     ': %s' % cmd_string)
+            if len(self._volumes[vgname]['vgs'][vgname]['snaps']):
+                data = '  owi-a-\n'
+            else:
+                data = '  wi-a-\n'
+
+            return data
+
+        return check, act
+
+    def _fake_lvextend(self):
+        def check(cmd_string):
+            return 'lvextend, -L, ' in cmd_string
+
+        def act(cmd):
+            cmd_string = ', '.join(cmd)
+            vgname = cmd[3].split('/')[0]
+            lvname = cmd[3].split('/')[1]
+            if cmd[2][-1] == 'g':
+                size = int(float(cmd[2][0:-1]) * units.Gi)
+            elif cmd[2][-1] == 'B':
+                size = int(cmd[2][0:-1])
+            else:
+                size = int(cmd[2])
+            if vgname not in self._volumes:
+                raise AssertionError('Cannot extend inexistant volume'
+                                     ': %s' % cmd_string)
+            if lvname not in self._volumes[vgname]['vgs'][vgname]['lvs']:
+                raise AssertionError('Cannot extend inexistant lv'
+                                     ': %s' % cmd_string)
+            self._volumes[vgname]['vgs'][vgname]['lvs'][lvname] = size
+
+        return check, act
+
+    def _fake_pvresize(self):
+        def check(cmd_string):
+            return 'pvresize, ' in cmd_string
+
+        def act(_):
+            pass
+
+        return check, act
+
+    def _fake_execute(self, *cmd, **kwargs):
+        cmd_string = ', '.join(cmd)
+        ##
+        #  To test behavior, we need to stub part of the brick/local_dev/lvm
+        #  functions too, because we want to check the state between calls,
+        #  not only if the calls were done
+        ##
+
+        handlers = [
+            self._fake_add_urls(),
+            self._fake_attach(),
+            self._fake_create(),
+            self._fake_destroy(),
+            self._fake_detach(),
+            self._fake_extend(),
+            self._fake_get_all_physical_volumes(),
+            self._fake_get_all_volume_groups(),
+            self._fake_get_all_volumes(),
+            self._fake_lvchange(),
+            self._fake_lvcreate_T_L(),
+            self._fake_lvcreate_T_V(),
+            self._fake_lvcreate_name(),
+            self._fake_lvdisplay(),
+            self._fake_lvextend(),
+            self._fake_lvremove(),
+            self._fake_pvresize(),
+            self._fake_thinpool_free_space(),
+            self._fake_udevadm_settle(),
+            self._fake_vg_list(),
+            self._fake_vgchange_an(),
+            self._fake_vgchange_ay(),
+            self._fake_vgcreate(),
+            self._fake_vgremove(),
+            self._fake_vgs_version(),
+        ]
+
+        for (check, act) in handlers:
+            if check(cmd_string):
+                out = act(cmd)
+                return (out, '')
+
+        self.fail('Unexpected command: %s' % cmd_string)
+
+    def _configure_driver(self):
+        srb.CONF.srb_base_urls = "http://localhost/volumes"
+
+    def setUp(self):
+        super(SRBDriverTestCase, self).setUp()
+
+        self.configuration = conf.Configuration(None)
+        self._driver = srb.SRBDriver(configuration=self.configuration)
+        # Stub processutils.execute for static methods
+        self.stubs.Set(processutils, 'execute', self._fake_execute)
+        self._driver.set_execute(self._fake_execute)
+        self._configure_driver()
+
+    def test_setup(self):
+        """The url shall be added automatically"""
+        self._driver.do_setup(None)
+        self.assertEqual('http://localhost/volumes', self._urls[0])
+        self._driver.check_for_setup_error()
+
+    def test_setup_no_config(self):
+        """The driver shall not start without any url configured"""
+        srb.CONF.srb_base_urls = None
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self._driver.do_setup, None)
+
+    def test_volume_create(self):
+        """The volume shall be added in the internal
+            state through fake_execute
+        """
+        volume = {'name': 'volume-test', 'id': 'test', 'size': 4 * units.Gi}
+        old_vols = self._volumes
+        updates = self._driver.create_volume(volume)
+        self.assertEqual({'provider_location': volume['name']}, updates)
+        new_vols = self._volumes
+        old_vols['volume-test'] = {
+            'name': 'volume-test',
+            'size': 4 * units.Gi,
+            'vgs': {
+                'volume-test': {
+                    'lvs': {'volume-test-pool': 0.95 * 4 * units.Gi,
+                            'volume-test': 4 * units.Gi},
+                    'snaps': [],
+                },
+            },
+        }
+        self.assertDictMatch(old_vols, new_vols)
+
+    def test_volume_delete(self):
+        vol = {'name': 'volume-delete', 'id': 'delete', 'size': units.Gi}
+
+        old_vols = self._volumes
+        self._volumes['volume-delete'] = {
+            'name': 'volume-delete',
+            'size': units.Gi,
+            'vgs': {
+                'volume-delete': {
+                    'lvs': {'volume-delete-pool': 0.95 * units.Gi,
+                            'volume-delete': units.Gi},
+                    'snaps': [],
+                },
+            },
+        }
+        self._driver.delete_volume(vol)
+        new_vols = self._volumes
+        self.assertDictMatch(old_vols, new_vols)
+
+    def test_volume_create_and_delete(self):
+        volume = {'name': 'volume-autoDelete', 'id': 'autoDelete',
+                  'size': 4 * units.Gi}
+        old_vols = self._volumes
+        updates = self._driver.create_volume(volume)
+        self.assertEqual({'provider_location': volume['name']}, updates)
+        self._driver.delete_volume(volume)
+        new_vols = self._volumes
+        self.assertDictMatch(old_vols, new_vols)
+
+    def test_volume_create_cloned(self):
+        with mock.patch('cinder.volume.utils.copy_volume'):
+            new = {'name': 'volume-cloned', 'size': 4 * units.Gi,
+                   'id': 'cloned'}
+            old = {'name': 'volume-old', 'size': 4 * units.Gi, 'id': 'old'}
+            old_vols = self._volumes
+            self._volumes['volume-old'] = {
+                'name': 'volume-old',
+                'size': 4 * units.Gi,
+                'vgs': {
+                    'volume-old': {
+                        'name': 'volume-old',
+                        'lvs': {'volume-old-pool': 0.95 * 4 * units.Gi,
+                                'volume-old': 4 * units.Gi},
+                        'snaps': [],
+                    },
+                },
+            }
+            self._driver.create_cloned_volume(new, old)
+            new_vols = self._volumes
+            old_vols['volume-cloned'] = {
+                'name': 'volume-cloned',
+                'size': 4 * units.Gi,
+                'vgs': {
+                    'volume-cloned': {
+                        'name': 'volume-cloned',
+                        'lvs': {'volume-cloned-pool': 0.95 * 4 * units.Gi,
+                                'volume-cloned': 4 * units.Gi},
+                        'snaps': [],
+                    },
+                },
+            }
+            self.assertDictMatch(old_vols, new_vols)
+
+    def test_volume_create_from_snapshot(self):
+        cp_vol_patch = mock.patch('cinder.volume.utils.copy_volume')
+        lv_activ_patch = mock.patch(
+            'cinder.brick.local_dev.lvm.LVM.activate_lv')
+
+        with cp_vol_patch as cp_vol, lv_activ_patch as lv_activ:
+            old_vols = self._volumes
+            newvol = {"name": "volume-SnapClone", "id": "SnapClone",
+                      "size": 4 * units.Gi}
+            srcsnap = {"name": "snapshot-SnappedBase", "id": "SnappedBase",
+                       "volume_id": "SnapBase", "volume_size": 4,
+                       "volume_name": "volume-SnapBase"}
+
+            self._driver.create_volume_from_snapshot(newvol, srcsnap)
+
+            expected_lv_activ_calls = [
+                mock.call(srcsnap['volume_name'] + "-pool"),
+                mock.call(srcsnap['name'], True)
+            ]
+            lv_activ.assertEqual(expected_lv_activ_calls,
+                                 lv_activ.call_args_list)
+            cp_vol.assert_called_with(
+                '/dev/mapper/volume--SnapBase-_snapshot--SnappedBase',
+                '/dev/mapper/volume--SnapClone-volume--SnapClone',
+                srcsnap['volume_size'] * units.Ki,
+                self._driver.configuration.volume_dd_blocksize,
+                execute=self._fake_execute)
+
+            new_vols = self._volumes
+            old_vols['volume-SnapClone'] = {
+                "name": 'volume-SnapClone',
+                "id": 'SnapClone',
+                "size": 30,
+                "vgs": {
+                    "name": 'volume-SnapClone',
+                    "lvs": {'volume-SnapClone-pool': 0.95 * 30 * units.Gi,
+                            'volume-SnapClone': 30 * units.Gi},
+                    "snaps": [],
+                },
+            }
+            self.assertDictMatch(old_vols, new_vols)
+
+    def test_volume_snapshot_create(self):
+        old_vols = self._volumes
+        snap = {"name": "snapshot-SnapBase1",
+                "id": "SnapBase1",
+                "volume_name": "volume-SnapBase",
+                "volume_id": "SnapBase",
+                "volume_size": 4}
+        self._driver.create_snapshot(snap)
+        new_vols = self._volumes
+        old_vols['volume-SnapBase']["vgs"]['volume-SnapBase']["snaps"].\
+            append('snapshot-SnapBase1')
+        self.assertDictMatch(old_vols, new_vols)
+
+    def test_volume_snapshot_delete(self):
+        old_vols = self._volumes
+        snap = {"name": "snapshot-delSnap",
+                "id": "delSnap",
+                "volume_name": "volume-SnapBase",
+                "volume_id": "SnapBase",
+                "volume_size": 4}
+        self._driver.delete_snapshot(snap)
+        new_vols = self._volumes
+        old_vols['volume-SnapBase']["vgs"]['volume-SnapBase']["snaps"].\
+            remove(snap['name'])
+        self.assertDictMatch(old_vols, new_vols)
+        self.assertEqual(
+            set(old_vols['volume-SnapBase']['vgs']
+                ['volume-SnapBase']['snaps']),
+            set(new_vols['volume-SnapBase']['vgs']
+                ['volume-SnapBase']['snaps']))
+
+    def test_volume_copy_from_image(self):
+        with (mock.patch('cinder.image.image_utils.fetch_to_volume_format'))\
+                as fetch:
+            vol = {'name': 'volume-SnapBase', 'id': 'SnapBase',
+                   'size': 5 * units.Gi}
+            self._driver.copy_image_to_volume(context,
+                                              vol,
+                                              'image_service',
+                                              'image_id')
+            fetch.assert_called_once_with(context,
+                                          'image_service',
+                                          'image_id',
+                                          self._driver._mapper_path(vol),
+                                          'qcow2',
+                                          self._driver.
+                                          configuration.volume_dd_blocksize,
+                                          size=vol['size'])
+
+    def test_volume_copy_to_image(self):
+        with mock.patch('cinder.image.image_utils.upload_volume') as upload:
+            vol = {'name': 'volume-SnapBase', 'id': 'SnapBase',
+                   'size': 5 * units.Gi}
+            self._driver.copy_volume_to_image(context,
+                                              vol,
+                                              'image_service',
+                                              'image_meta')
+            upload.assert_called_once_with(context,
+                                           'image_service',
+                                           'image_meta',
+                                           self._driver._mapper_path(vol))
+
+    def test_volume_extend(self):
+        vol = {'name': 'volume-extend', 'id': 'extend', 'size': 4 * units.Gi}
+        new_size = 5
+
+        self._driver.extend_volume(vol, new_size)
+
+        new_vols = self._volumes
+        self.assertEqual(srb.SRBDriver.OVER_ALLOC_RATIO * new_size * units.Gi,
+                         new_vols['volume-extend']['size'])
diff --git a/cinder/volume/drivers/srb.py b/cinder/volume/drivers/srb.py
new file mode 100644 (file)
index 0000000..a14402d
--- /dev/null
@@ -0,0 +1,878 @@
+# Copyright (c) 2014 Scality
+#
+#    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.
+
+"""
+Volume driver for the Scality REST Block storage system
+
+This driver provisions Linux SRB volumes leveraging RESTful storage platforms
+(e.g. Scality CDMI).
+"""
+
+import contextlib
+import functools
+import sys
+import time
+
+from oslo.concurrency import lockutils
+from oslo.concurrency import processutils as putils
+from oslo.config import cfg
+from oslo.utils import excutils
+from oslo.utils import units
+import six
+
+from cinder.brick.local_dev import lvm
+from cinder import exception
+from cinder.i18n import _, _LI, _LE, _LW
+from cinder.image import image_utils
+from cinder.openstack.common import log as logging
+from cinder import utils
+from cinder.volume import driver
+from cinder.volume import utils as volutils
+
+
+LOG = logging.getLogger(__name__)
+
+srb_opts = [
+    cfg.StrOpt('srb_base_urls',
+               default=None,
+               help='Comma-separated list of REST servers to connect to'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(srb_opts)
+
+
+class retry:
+    SLEEP_NONE = 'none'
+    SLEEP_DOUBLE = 'double'
+    SLEEP_INCREMENT = 'increment'
+
+    def __init__(self, exceptions, count, sleep_mechanism=SLEEP_INCREMENT,
+                 sleep_factor=1):
+        if sleep_mechanism not in [self.SLEEP_NONE,
+                                   self.SLEEP_DOUBLE,
+                                   self.SLEEP_INCREMENT]:
+            raise ValueError('Invalid value for `sleep_mechanism` argument')
+
+        self._exceptions = exceptions
+        self._count = count
+        self._sleep_mechanism = sleep_mechanism
+        self._sleep_factor = sleep_factor
+
+    def __call__(self, fun):
+        func_name = fun.func_name
+
+        @functools.wraps(fun)
+        def wrapped(*args, **kwargs):
+            sleep_time = self._sleep_factor
+            exc_info = None
+
+            for attempt in xrange(self._count):
+                if attempt != 0:
+                    LOG.warning(_LW('Retrying failed call to %(func)s, '
+                                    'attempt %(attempt)i.')
+                                % {'func': func_name,
+                                   'attempt': attempt})
+                try:
+                    return fun(*args, **kwargs)
+                except self._exceptions:
+                    exc_info = sys.exc_info()
+
+                if attempt != self._count - 1:
+                    if self._sleep_mechanism == self.SLEEP_NONE:
+                        continue
+                    elif self._sleep_mechanism == self.SLEEP_INCREMENT:
+                        time.sleep(sleep_time)
+                        sleep_time += self._sleep_factor
+                    elif self._sleep_mechanism == self.SLEEP_DOUBLE:
+                        time.sleep(sleep_time)
+                        sleep_time *= 2
+                    else:
+                        raise ValueError('Unknown sleep mechanism: %r'
+                                         % self._sleep_mechanism)
+
+            six.reraise(exc_info[0], exc_info[1], exc_info[2])
+
+        return wrapped
+
+
+class LVM(lvm.LVM):
+    def activate_vg(self):
+        """Activate the Volume Group associated with this instantiation.
+
+        :raises: putils.ProcessExecutionError
+        """
+
+        cmd = ['vgchange', '-ay', self.vg_name]
+        try:
+            self._execute(*cmd,
+                          root_helper=self._root_helper,
+                          run_as_root=True)
+        except putils.ProcessExecutionError as err:
+            LOG.exception(_LE('Error activating Volume Group'))
+            LOG.error(_LE('Cmd     :%s') % err.cmd)
+            LOG.error(_LE('StdOut  :%s') % err.stdout)
+            LOG.error(_LE('StdErr  :%s') % err.stderr)
+            raise
+
+    def deactivate_vg(self):
+        """Deactivate the Volume Group associated with this instantiation.
+
+        This forces LVM to release any reference to the device.
+
+        :raises: putils.ProcessExecutionError
+        """
+
+        cmd = ['vgchange', '-an', self.vg_name]
+        try:
+            self._execute(*cmd,
+                          root_helper=self._root_helper,
+                          run_as_root=True)
+        except putils.ProcessExecutionError as err:
+            LOG.exception(_LE('Error deactivating Volume Group'))
+            LOG.error(_LE('Cmd     :%s') % err.cmd)
+            LOG.error(_LE('StdOut  :%s') % err.stdout)
+            LOG.error(_LE('StdErr  :%s') % err.stderr)
+            raise
+
+    def destroy_vg(self):
+        """Destroy the Volume Group associated with this instantiation.
+
+        :raises: putils.ProcessExecutionError
+        """
+
+        cmd = ['vgremove', '-f', self.vg_name]
+        try:
+            self._execute(*cmd,
+                          root_helper=self._root_helper,
+                          run_as_root=True)
+        except putils.ProcessExecutionError as err:
+            LOG.exception(_LE('Error destroying Volume Group'))
+            LOG.error(_LE('Cmd     :%s') % err.cmd)
+            LOG.error(_LE('StdOut  :%s') % err.stdout)
+            LOG.error(_LE('StdErr  :%s') % err.stderr)
+            raise
+
+    def pv_resize(self, pv_name, new_size_str):
+        """Extend the size of an existing PV (for virtual PVs).
+
+        :raises: putils.ProcessExecutionError
+        """
+        try:
+            self._execute('pvresize',
+                          '--setphysicalvolumesize', new_size_str,
+                          pv_name,
+                          root_helper=self._root_helper,
+                          run_as_root=True)
+        except putils.ProcessExecutionError as err:
+            LOG.exception(_LE('Error resizing Physical Volume'))
+            LOG.error(_LE('Cmd     :%s') % err.cmd)
+            LOG.error(_LE('StdOut  :%s') % err.stdout)
+            LOG.error(_LE('StdErr  :%s') % err.stderr)
+            raise
+
+    def extend_thin_pool(self):
+        """Extend the size of the thin provisioning pool.
+
+        This method extends the size of a thin provisioning pool to 95% of the
+        size of the VG, if the VG is configured as thin and owns a thin
+        provisioning pool.
+
+        :raises: putils.ProcessExecutionError
+        """
+        if self.vg_thin_pool is None:
+            return
+
+        new_size_str = self._calculate_thin_pool_size()
+        try:
+            self._execute('lvextend',
+                          '-L', new_size_str,
+                          "%s/%s-pool" % (self.vg_name, self.vg_name),
+                          root_helper=self._root_helper,
+                          run_as_root=True)
+        except putils.ProcessExecutionError as err:
+            LOG.exception(_LE('Error extending thin provisioning pool'))
+            LOG.error(_LE('Cmd     :%s') % err.cmd)
+            LOG.error(_LE('StdOut  :%s') % err.stdout)
+            LOG.error(_LE('StdErr  :%s') % err.stderr)
+            raise
+
+
+@contextlib.contextmanager
+def patched(obj, attr, fun):
+    '''Context manager to locally patch a method.
+
+    Within the managed context, the `attr` method of `obj` will be replaced by
+    a method which calls `fun` passing in the original `attr` attribute of
+    `obj` as well as any positional and keyword arguments.
+
+    At the end of the context, the original method is restored.
+    '''
+
+    orig = getattr(obj, attr)
+
+    def patch(*args, **kwargs):
+        return fun(orig, *args, **kwargs)
+
+    setattr(obj, attr, patch)
+
+    try:
+        yield
+    finally:
+        setattr(obj, attr, orig)
+
+
+@contextlib.contextmanager
+def handle_process_execution_error(message, info_message, reraise=True):
+    '''Consistently handle `putils.ProcessExecutionError` exceptions
+
+    This context-manager will catch any `putils.ProcessExecutionError`
+    exceptions raised in the managed block, and generate logging output
+    accordingly.
+
+    The value of the `message` argument will be logged at `logging.ERROR`
+    level, and the `info_message` argument at `logging.INFO` level. Finally
+    the command string, exit code, standard output and error output of the
+    process will be logged at `logging.DEBUG` level.
+
+    The `reraise` argument specifies what should happend when a
+    `putils.ProcessExecutionError` is caught. If it's equal to `True`, the
+    exception will be re-raised. If it's some other non-`False` object, this
+    object will be raised instead (so you most likely want it to be some
+    `Exception`). Any `False` value will result in the exception to be
+    swallowed.
+    '''
+
+    try:
+        yield
+    except putils.ProcessExecutionError as exc:
+        LOG.error(message)
+
+        LOG.info(info_message)
+        LOG.debug('Command   : %s', exc.cmd)
+        LOG.debug('Exit Code : %r', exc.exit_code)
+        LOG.debug('StdOut    : %s', exc.stdout)
+        LOG.debug('StdErr    : %s', exc.stderr)
+
+        if reraise is True:
+            raise
+        elif reraise:
+            raise reraise  # pylint: disable=E0702
+
+
+@contextlib.contextmanager
+def temp_snapshot(driver, volume, src_vref):
+    snapshot = {'volume_name': src_vref['name'],
+                'volume_id': src_vref['id'],
+                'volume_size': src_vref['size'],
+                'name': 'snapshot-clone-%s' % volume['id'],
+                'id': 'tmp-snap-%s' % volume['id'],
+                'size': src_vref['size']}
+
+    driver.create_snapshot(snapshot)
+
+    try:
+        yield snapshot
+    finally:
+        driver.delete_snapshot(snapshot)
+
+
+@contextlib.contextmanager
+def temp_raw_device(driver, volume):
+    driver._attach_file(volume)
+
+    try:
+        yield
+    finally:
+        driver._detach_file(volume)
+
+
+@contextlib.contextmanager
+def temp_lvm_device(driver, volume):
+    with temp_raw_device(driver, volume):
+        vg = driver._get_lvm_vg(volume)
+        vg.activate_vg()
+
+        yield vg
+
+
+class SRBDriver(driver.VolumeDriver):
+    """Scality SRB volume driver
+
+    This driver manages volumes provisioned by the Scality REST Block driver
+    Linux kernel module, backed by RESTful storage providers (e.g. Scality
+    CDMI).
+    """
+
+    VERSION = '1.0.0'
+
+    # Over-allocation ratio (multiplied with requested size) for thin
+    # provisioning
+    OVER_ALLOC_RATIO = 2
+    SNAPSHOT_PREFIX = 'snapshot'
+
+    def __init__(self, *args, **kwargs):
+        super(SRBDriver, self).__init__(*args, **kwargs)
+        self.configuration.append_config_values(srb_opts)
+        self.urls_setup = False
+        self.backend_name = None
+        self.base_urls = None
+        self._attached_devices = {}
+
+    def _setup_urls(self):
+        if not self.base_urls:
+            message = _("No url configured")
+            raise exception.VolumeBackendAPIException(data=message)
+
+        with handle_process_execution_error(
+                message=_LE('Cound not setup urls on the Block Driver.'),
+                info_message=_LI('Error creating Volume'),
+                reraise=False):
+            cmd = 'echo ' + self.base_urls + ' > /sys/class/srb/add_urls'
+            putils.execute('sh', '-c', cmd,
+                           root_helper='sudo', run_as_root=True)
+            self.urls_setup = True
+
+    def do_setup(self, context):
+        """Any initialization the volume driver does while starting."""
+        self.backend_name = self.configuration.safe_get('volume_backend_name')
+
+        base_urls = self.configuration.safe_get('srb_base_urls')
+        if base_urls:
+            base_urls = ','.join(s.strip() for s in base_urls.split(','))
+
+        self.base_urls = base_urls
+
+        self._setup_urls()
+
+    def check_for_setup_error(self):
+        """Returns an error if prerequisites aren't met."""
+        if not self.base_urls:
+            LOG.warning(_LW("Configuration variable srb_base_urls"
+                            " not set or empty."))
+
+        if self.urls_setup is False:
+            message = _("Could not setup urls properly")
+            raise exception.VolumeBackendAPIException(data=message)
+
+    @classmethod
+    def _is_snapshot(cls, volume):
+        return volume['name'].startswith(cls.SNAPSHOT_PREFIX)
+
+    @classmethod
+    def _get_volname(cls, volume):
+        """Returns the name of the actual volume
+
+        If the volume is a snapshot, it returns the name of the parent volume.
+        otherwise, returns the volume's name.
+        """
+        name = volume['name']
+        if cls._is_snapshot(volume):
+            name = "volume-%s" % (volume['volume_id'])
+        return name
+
+    @classmethod
+    def _get_volid(cls, volume):
+        """Returns the ID of the actual volume
+
+        If the volume is a snapshot, it returns the ID of the parent volume.
+        otherwise, returns the volume's id.
+        """
+        volid = volume['id']
+        if cls._is_snapshot(volume):
+            volid = volume['volume_id']
+        return volid
+
+    @classmethod
+    def _device_name(cls, volume):
+        volume_id = cls._get_volid(volume)
+        name = 'cinder-%s' % volume_id
+
+        # Device names can't be longer than 32 bytes (incl. \0)
+        return name[:31]
+
+    @classmethod
+    def _device_path(cls, volume):
+        return "/dev/" + cls._device_name(volume)
+
+    @classmethod
+    def _escape_snapshot(cls, snapshot_name):
+        # Linux LVM reserves name that starts with snapshot, so that
+        # such volume name can't be created. Mangle it.
+        if not snapshot_name.startswith(cls.SNAPSHOT_PREFIX):
+            return snapshot_name
+        return '_' + snapshot_name
+
+    @classmethod
+    def _mapper_path(cls, volume):
+        groupname = cls._get_volname(volume)
+        name = volume['name']
+        if cls._is_snapshot(volume):
+            name = cls._escape_snapshot(name)
+        # NOTE(vish): stops deprecation warning
+        groupname = groupname.replace('-', '--')
+        name = name.replace('-', '--')
+        return "/dev/mapper/%s-%s" % (groupname, name)
+
+    @staticmethod
+    def _size_int(size_in_g):
+        try:
+            return max(int(size_in_g), 1)
+        except ValueError:
+            message = (_("Invalid size parameter '%s': Cannot be interpreted"
+                         " as an integer value.")
+                       % size_in_g)
+            LOG.error(message)
+            raise exception.VolumeBackendAPIException(data=message)
+
+    @classmethod
+    def _set_device_path(cls, volume):
+        volume['provider_location'] = cls._get_volname(volume)
+        return {
+            'provider_location': volume['provider_location'],
+        }
+
+    @staticmethod
+    def _activate_lv(orig, *args, **kwargs):
+        '''Use with `patched` to patch `lvm.LVM.activate_lv` to ignore `EEXIST`
+        '''
+        try:
+            orig(*args, **kwargs)
+        except putils.ProcessExecutionError as exc:
+            if exc.exit_code != 5:
+                raise
+            else:
+                LOG.debug('`activate_lv` returned 5, ignored')
+
+    def _get_lvm_vg(self, volume, create_vg=False):
+        # NOTE(joachim): One-device volume group to manage thin snapshots
+        # Get origin volume name even for snapshots
+        volume_name = self._get_volname(volume)
+        physical_volumes = [self._device_path(volume)]
+
+        with patched(lvm.LVM, 'activate_lv', self._activate_lv):
+            return LVM(volume_name, utils.get_root_helper(),
+                       create_vg=create_vg,
+                       physical_volumes=physical_volumes,
+                       lvm_type='thin', executor=self._execute)
+
+    @staticmethod
+    def _volume_not_present(vg, volume_name):
+        # Used to avoid failing to delete a volume for which
+        # the create operation partly failed
+        return vg.get_volume(volume_name) is None
+
+    def _create_file(self, volume):
+        message = _('Could not create volume on any configured REST server.')
+
+        with handle_process_execution_error(
+                message=message,
+                info_message=_LI('Error creating Volume %s.') % volume['name'],
+                reraise=exception.VolumeBackendAPIException(data=message)):
+            size = self._size_int(volume['size']) * self.OVER_ALLOC_RATIO
+
+            cmd = 'echo ' + volume['name'] + ' '
+            cmd += '%dG' % size
+            cmd += ' > /sys/class/srb/create'
+            putils.execute('/bin/sh', '-c', cmd,
+                           root_helper='sudo', run_as_root=True)
+
+        return self._set_device_path(volume)
+
+    def _extend_file(self, volume, new_size):
+        message = _('Could not extend volume on any configured REST server.')
+
+        with handle_process_execution_error(
+                message=message,
+                info_message=(_LI('Error extending Volume %s.')
+                              % volume['name']),
+                reraise=exception.VolumeBackendAPIException(data=message)):
+            size = self._size_int(new_size) * self.OVER_ALLOC_RATIO
+
+            cmd = 'echo ' + volume['name'] + ' '
+            cmd += '%dG' % size
+            cmd += ' > /sys/class/srb/extend'
+            putils.execute('/bin/sh', '-c', cmd,
+                           root_helper='sudo', run_as_root=True)
+
+    @staticmethod
+    def _destroy_file(volume):
+        message = _('Could not destroy volume on any configured REST server.')
+
+        volname = volume['name']
+        with handle_process_execution_error(
+                message=message,
+                info_message=_LI('Error destroying Volume %s.') % volname,
+                reraise=exception.VolumeBackendAPIException(data=message)):
+            cmd = 'echo ' + volume['name'] + ' > /sys/class/srb/destroy'
+            putils.execute('/bin/sh', '-c', cmd,
+                           root_helper='sudo', run_as_root=True)
+
+    # NOTE(joachim): Must only be called within a function decorated by:
+    # @lockutils.synchronized('devices', 'cinder-srb-')
+    def _increment_attached_count(self, volume):
+        """Increments the attach count of the device"""
+        volid = self._get_volid(volume)
+        if volid not in self._attached_devices:
+            self._attached_devices[volid] = 1
+        else:
+            self._attached_devices[volid] += 1
+
+    # NOTE(joachim): Must only be called within a function decorated by:
+    # @lockutils.synchronized('devices', 'cinder-srb-')
+    def _decrement_attached_count(self, volume):
+        """Decrements the attach count of the device"""
+        volid = self._get_volid(volume)
+        if volid not in self._attached_devices:
+            raise exception.VolumeBackendAPIException(
+                (_("Internal error in srb driver: "
+                   "Trying to detach detached volume %s."))
+                % (self._get_volname(volume))
+            )
+
+        self._attached_devices[volid] -= 1
+
+        if self._attached_devices[volid] == 0:
+            del self._attached_devices[volid]
+
+    # NOTE(joachim): Must only be called within a function decorated by:
+    # @lockutils.synchronized('devices', 'cinder-srb-')
+    def _get_attached_count(self, volume):
+        volid = self._get_volid(volume)
+
+        return self._attached_devices.get(volid, 0)
+
+    @lockutils.synchronized('devices', 'cinder-srb-')
+    def _is_attached(self, volume):
+        return self._get_attached_count(volume) > 0
+
+    @lockutils.synchronized('devices', 'cinder-srb-')
+    def _attach_file(self, volume):
+        name = self._get_volname(volume)
+        devname = self._device_name(volume)
+        LOG.debug('Attaching volume %s as %s', name, devname)
+
+        count = self._get_attached_count(volume)
+        if count == 0:
+            message = (_('Could not attach volume %(vol)s as %(dev)s '
+                         'on system.')
+                       % {'vol': name, 'dev': devname})
+            with handle_process_execution_error(
+                    message=message,
+                    info_message=_LI('Error attaching Volume'),
+                    reraise=exception.VolumeBackendAPIException(data=message)):
+                cmd = 'echo ' + name + ' ' + devname
+                cmd += ' > /sys/class/srb/attach'
+                putils.execute('/bin/sh', '-c', cmd,
+                               root_helper='sudo', run_as_root=True)
+        else:
+            LOG.debug('Volume %s already attached', name)
+
+        self._increment_attached_count(volume)
+
+    @retry(exceptions=(putils.ProcessExecutionError, ),
+           count=3, sleep_mechanism=retry.SLEEP_INCREMENT, sleep_factor=5)
+    def _do_deactivate(self, volume, vg):
+        vg.deactivate_vg()
+
+    @retry(exceptions=(putils.ProcessExecutionError, ),
+           count=5, sleep_mechanism=retry.SLEEP_DOUBLE, sleep_factor=1)
+    def _do_detach(self, volume, vg):
+        devname = self._device_name(volume)
+        volname = self._get_volname(volume)
+        cmd = 'echo ' + devname + ' > /sys/class/srb/detach'
+        try:
+            putils.execute('/bin/sh', '-c', cmd,
+                           root_helper='sudo', run_as_root=True)
+        except putils.ProcessExecutionError:
+            with excutils.save_and_reraise_exception(reraise=True):
+                try:
+                    with patched(lvm.LVM, 'activate_lv', self._activate_lv):
+                        vg.activate_lv(volname)
+
+                    self._do_deactivate(volume, vg)
+                except putils.ProcessExecutionError:
+                    LOG.warning(_LW('All attempts to recover failed detach '
+                                    'of %(volume)s failed.')
+                                % {'volume': volname})
+
+    @lockutils.synchronized('devices', 'cinder-srb-')
+    def _detach_file(self, volume):
+        name = self._get_volname(volume)
+        devname = self._device_name(volume)
+        vg = self._get_lvm_vg(volume)
+        LOG.debug('Detaching device %s', devname)
+
+        count = self._get_attached_count(volume)
+        if count > 1:
+            LOG.info(_LI('Reference count of %(volume)s is %(count)d, '
+                         'not detaching.')
+                     % {'volume': volume['name'],
+                        'count': count})
+            return
+
+        message = (_('Could not detach volume %(vol)s from device %(dev)s.')
+                   % {'vol': name, 'dev': devname})
+        with handle_process_execution_error(
+                message=message,
+                info_message=_LI('Error detaching Volume'),
+                reraise=exception.VolumeBackendAPIException(data=message)):
+            try:
+                if vg is not None:
+                    self._do_deactivate(volume, vg)
+            except putils.ProcessExecutionError:
+                msg = _LE('Could not deactivate volume groupe %s')\
+                    % (self._get_volname(volume))
+                LOG.error(msg)
+                raise
+
+            try:
+                self._do_detach(volume, vg=vg)
+            except putils.ProcessExecutionError:
+                msg = _LE('Could not detach volume '
+                          '%(vol)s from device %(dev)s.') \
+                    % {'vol': name, 'dev': devname}
+                LOG.error(msg)
+                raise
+
+            self._decrement_attached_count(volume)
+
+    def _setup_lvm(self, volume):
+        # NOTE(joachim): One-device volume group to manage thin snapshots
+        size = self._size_int(volume['size']) * self.OVER_ALLOC_RATIO
+        size_str = '%dg' % size
+        vg = self._get_lvm_vg(volume, create_vg=True)
+        vg.create_volume(volume['name'], size_str, lv_type='thin')
+
+    def _destroy_lvm(self, volume):
+        vg = self._get_lvm_vg(volume)
+        if vg.lv_has_snapshot(volume['name']):
+            LOG.error(_LE('Unable to delete due to existing snapshot '
+                          'for volume: %s.'),
+                      volume['name'])
+            raise exception.VolumeIsBusy(volume_name=volume['name'])
+        vg.destroy_vg()
+        # NOTE(joachim) Force lvm vg flush through a vgs command
+        vgs = vg.get_all_volume_groups(root_helper='sudo', vg_name=vg.vg_name)
+        if len(vgs) != 0:
+            LOG.warning(_LW('Removed volume group %s still appears in vgs.'),
+                        vg.vg_name)
+
+    def _create_and_copy_volume(self, dstvol, srcvol):
+        """Creates a volume from a volume or a snapshot."""
+        updates = self._create_file(dstvol)
+
+        # We need devices attached for IO operations.
+        with temp_lvm_device(self, srcvol) as vg, \
+                temp_raw_device(self, dstvol):
+            self._setup_lvm(dstvol)
+
+            # Some configurations of LVM do not automatically activate
+            # ThinLVM snapshot LVs.
+            with patched(lvm.LVM, 'activate_lv', self._activate_lv):
+                vg.activate_lv(srcvol['name'], True)
+
+            # copy_volume expects sizes in MiB, we store integer GiB
+            # be sure to convert before passing in
+            volutils.copy_volume(self._mapper_path(srcvol),
+                                 self._mapper_path(dstvol),
+                                 srcvol['volume_size'] * units.Ki,
+                                 self.configuration.volume_dd_blocksize,
+                                 execute=self._execute)
+
+        return updates
+
+    def create_volume(self, volume):
+        """Creates a volume.
+
+        Can optionally return a Dictionary of changes to the volume object to
+        be persisted.
+        """
+        updates = self._create_file(volume)
+        # We need devices attached for LVM operations.
+        with temp_raw_device(self, volume):
+            self._setup_lvm(volume)
+        return updates
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Creates a volume from a snapshot."""
+
+        return self._create_and_copy_volume(volume, snapshot)
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Creates a clone of the specified volume."""
+        LOG.info(_LI('Creating clone of volume: %s'), src_vref['id'])
+
+        updates = None
+        with temp_lvm_device(self, src_vref):
+            with temp_snapshot(self, volume, src_vref) as snapshot:
+                updates = self._create_and_copy_volume(volume, snapshot)
+
+        return updates
+
+    def delete_volume(self, volume):
+        """Deletes a volume."""
+        attached = False
+        if self._is_attached(volume):
+            attached = True
+            with temp_lvm_device(self, volume):
+                self._destroy_lvm(volume)
+            self._detach_file(volume)
+
+        LOG.debug('Deleting volume %s, attached=%s',
+                  volume['name'], attached)
+
+        self._destroy_file(volume)
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        with temp_lvm_device(self, snapshot) as vg:
+            # NOTE(joachim) we only want to support thin lvm_types
+            vg.create_lv_snapshot(self._escape_snapshot(snapshot['name']),
+                                  snapshot['volume_name'],
+                                  lv_type='thin')
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        with temp_lvm_device(self, snapshot) as vg:
+            if self._volume_not_present(
+                    vg, self._escape_snapshot(snapshot['name'])):
+                # If the snapshot isn't present, then don't attempt to delete
+                LOG.warning(_LW("snapshot: %s not found, "
+                                "skipping delete operations"),
+                            snapshot['name'])
+                return
+
+            vg.delete(self._escape_snapshot(snapshot['name']))
+
+    def get_volume_stats(self, refresh=False):
+        """Return the current state of the volume service."""
+        stats = {
+            'vendor_name': 'Scality',
+            'driver_version': self.VERSION,
+            'storage_protocol': 'Scality Rest Block Device',
+            'total_capacity_gb': 'infinite',
+            'free_capacity_gb': 'infinite',
+            'reserved_percentage': 0,
+            'volume_backend_name': self.backend_name,
+        }
+        return stats
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume."""
+        with temp_lvm_device(self, volume):
+            image_utils.fetch_to_volume_format(context,
+                                               image_service,
+                                               image_id,
+                                               self._mapper_path(volume),
+                                               'qcow2',
+                                               self.configuration.
+                                               volume_dd_blocksize,
+                                               size=volume['size'])
+
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
+        """Copy the volume to the specified image."""
+        with temp_lvm_device(self, volume):
+            image_utils.upload_volume(context,
+                                      image_service,
+                                      image_meta,
+                                      self._mapper_path(volume))
+
+    def extend_volume(self, volume, new_size):
+        new_alloc_size = self._size_int(new_size) * self.OVER_ALLOC_RATIO
+        new_size_str = '%dg' % new_alloc_size
+        self._extend_file(volume, new_size)
+        with temp_lvm_device(self, volume) as vg:
+            vg.pv_resize(self._device_path(volume), new_size_str)
+            vg.extend_thin_pool()
+            vg.extend_volume(volume['name'], new_size_str)
+
+
+class SRBISCSIDriver(SRBDriver, driver.ISCSIDriver):
+    """Scality SRB volume driver with ISCSI support
+
+    This driver manages volumes provisioned by the Scality REST Block driver
+    Linux kernel module, backed by RESTful storage providers (e.g. Scality
+    CDMI), and exports them through ISCSI to Nova.
+    """
+
+    VERSION = '1.0.0'
+
+    def __init__(self, *args, **kwargs):
+        self.db = kwargs.get('db')
+        self.target_helper = self.get_target_helper(self.db)
+        super(SRBISCSIDriver, self).__init__(*args, **kwargs)
+        self.backend_name =\
+            self.configuration.safe_get('volume_backend_name') or 'SRB_iSCSI'
+        self.protocol = 'iSCSI'
+
+    def set_execute(self, execute):
+        super(SRBISCSIDriver, self).set_execute(execute)
+        if self.target_helper is not None:
+            self.target_helper.set_execute(execute)
+
+    def ensure_export(self, context, volume):
+        volume_name = volume['name']
+        iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,
+                               volume_name)
+        device_path = self._mapper_path(volume)
+        # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
+        # should clean this all up at some point in the future
+        model_update = self.target_helper.ensure_export(
+            context, volume,
+            iscsi_name,
+            device_path,
+            None,
+            self.configuration)
+        if model_update:
+            self.db.volume_update(context, volume['id'], model_update)
+
+    def create_export(self, context, volume):
+        """Creates an export for a logical volume."""
+        self._attach_file(volume)
+        vg = self._get_lvm_vg(volume)
+        vg.activate_vg()
+
+        # SRB uses the same name as the volume for the VG
+        volume_path = self._mapper_path(volume)
+
+        data = self.target_helper.create_export(context,
+                                                volume,
+                                                volume_path,
+                                                self.configuration)
+        return {
+            'provider_location': data['location'],
+            'provider_auth': data['auth'],
+        }
+
+    def remove_export(self, context, volume):
+        # NOTE(joachim) Taken from iscsi._ExportMixin.remove_export
+        # This allows us to avoid "detaching" a device not attached by
+        # an export, and avoid screwing up the device attach refcount.
+        try:
+            # Raises exception.NotFound if export not provisioned
+            iscsi_target = self.target_helper._get_iscsi_target(context,
+                                                                volume['id'])
+            # Raises an Exception if currently not exported
+            location = volume['provider_location'].split(' ')
+            iqn = location[1]
+            self.target_helper.show_target(iscsi_target, iqn=iqn)
+
+            self.target_helper.remove_export(context, volume)
+            self._detach_file(volume)
+        except exception.NotFound:
+            LOG.warning(_LW('Volume %r not found while trying to remove.'),
+                        volume['id'])
+        except Exception as exc:
+            LOG.warning(_LW('Error while removing export: %r'), exc)
index 2d23743f3a97027f08d3615d745e760b2d74bf6c..a90f8ded16b7ec5637f0e735d19369b3075e8fae 100644 (file)
@@ -14,6 +14,19 @@ vgs: EnvFilter, env, root, LC_ALL=C, vgs
 lvs: EnvFilter, env, root, LC_ALL=C, lvs
 lvdisplay: EnvFilter, env, root, LC_ALL=C, lvdisplay
 
+# cinder/volumes/drivers/srb.py: 'pvresize', '--setphysicalvolumesize', sizestr, pvname
+pvresize: CommandFilter, pvresize, root
+
+# cinder/brick/local_dev/lvm.py: 'vgcreate', vg_name, pv_list
+vgcreate: CommandFilter, vgcreate, root
+
+# cinder/volumes/drivers/srb.py: 'vgremove', '-f', vgname
+vgremove: CommandFilter, vgremove, root
+
+# cinder/volumes/drivers/srb.py: 'vgchange', '-an', vgname
+# cinder/volumes/drivers/srb.py: 'vgchange', '-ay', vgname
+vgchange: CommandFilter, vgchange, root
+
 # cinder/volume/driver.py: 'lvcreate', '-L', sizestr, '-n', volume_name,..
 # cinder/volume/driver.py: 'lvcreate', '-L', ...
 lvcreate: CommandFilter, lvcreate, root
@@ -28,6 +41,7 @@ lvremove: CommandFilter, lvremove, root
 lvrename: CommandFilter, lvrename, root
 
 # cinder/volume/driver.py: 'lvextend', '-L' '%(new_size)s', '%(lv_name)s' ...
+# cinder/volume/driver.py: 'lvextend', '-L' '%(new_size)s', '%(thin_pool)s' ...
 lvextend: CommandFilter, lvextend, root
 
 # cinder/brick/local_dev/lvm.py: 'lvchange -a y -K <lv>'