--- /dev/null
+# 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'])
--- /dev/null
+# 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)