From: David Pineau Date: Mon, 1 Dec 2014 11:21:35 +0000 (+0100) Subject: Add Scality SRB driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=a23f17f8cebe5e1e57f675aedf6272257257d1b7;p=openstack-build%2Fcinder-build.git Add Scality SRB driver 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 Co-Authored-By: JordanP Change-Id: Id8d00df9db4004d5aeb8c0269d114ef50e0b8f8e --- diff --git a/cinder/tests/test_srb.py b/cinder/tests/test_srb.py new file mode 100644 index 000000000..4599b2a32 --- /dev/null +++ b/cinder/tests/test_srb.py @@ -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 index 000000000..a14402de0 --- /dev/null +++ b/cinder/volume/drivers/srb.py @@ -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) diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 2d23743f3..a90f8ded1 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -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 '