From b69990682b4aaa4eefd1e806e9ca85745acdc98d Mon Sep 17 00:00:00 2001 From: John Griffith Date: Mon, 22 Jul 2013 10:39:54 -0600 Subject: [PATCH] Refactor LVM driver to use Brick VG utility Refactor the LVM driver to utilize bricks LVM module. This includes significant modification to tests and fake objects related to testing. implements blueprint: refactor-lvm-and-iscsi-driver Change-Id: I94e41abf90d0c5b77e732d40ed8b81b0a2f5d7dd --- cinder/brick/local_dev/lvm.py | 80 ++-- .../tests/api/contrib/test_admin_actions.py | 3 +- cinder/tests/brick/fake_lvm.py | 69 +++ cinder/tests/brick/test_brick_lvm.py | 14 +- cinder/tests/fake_driver.py | 4 + cinder/tests/test_volume.py | 69 ++- cinder/volume/drivers/lvm.py | 406 ++++++++---------- cinder/volume/utils.py | 24 ++ 8 files changed, 397 insertions(+), 272 deletions(-) create mode 100644 cinder/tests/brick/fake_lvm.py diff --git a/cinder/brick/local_dev/lvm.py b/cinder/brick/local_dev/lvm.py index b67fab649..2543a8735 100644 --- a/cinder/brick/local_dev/lvm.py +++ b/cinder/brick/local_dev/lvm.py @@ -35,21 +35,22 @@ LOG = logging.getLogger(__name__) class LVM(object): """LVM object to enable various LVM related operations.""" - def __init__(self, - vg_name, - create_vg=False, - physical_volumes=None, - lvm_type='default', + def __init__(self, vg_name, root_helper, create_vg=False, + physical_volumes=None, lvm_type='default', executor=putils.execute): + """Initialize the LVM object. The LVM object is based on an LVM VolumeGroup, one instantiation for each VolumeGroup you have/use. :param vg_name: Name of existing VG or VG to create + :param root_helper: Execution root_helper method to use :param create_vg: Indicates the VG doesn't exist and we want to create it :param physical_volumes: List of PVs to build VG on + :param lvm_type: VG and Volume type (default, or thin) + :param executor: Execute method to use, None uses common/processutils """ self.vg_name = vg_name @@ -61,7 +62,8 @@ class LVM(object): self.vg_uuid = None self.vg_thin_pool = None self.vg_thin_pool_size = 0 - self._execute = executor + self.root_helper = root_helper + self._set_execute(executor) if create_vg and physical_volumes is not None: self.pv_list = physical_volumes @@ -86,6 +88,9 @@ class LVM(object): else: self.vg_thin_pool = pool_name + def _set_execute(self, execute): + self._execute = execute + def _size_str(self, size_in_g): if '.00' in size_in_g: size_in_g = size_in_g.replace('.00', '') @@ -103,7 +108,9 @@ class LVM(object): """ exists = False cmd = ['vgs', '--noheadings', '-o', 'name'] - (out, err) = self._execute(*cmd, root_helper='sudo', run_as_root=True) + (out, err) = self._execute(*cmd, + root_helper=self.root_helper, + run_as_root=True) if out is not None: volume_groups = out.split() @@ -114,7 +121,7 @@ class LVM(object): def _create_vg(self, pv_list): cmd = ['vgcreate', self.vg_name, ','.join(pv_list)] - self._execute(*cmd, root_helper='sudo', run_as_root=True) + self._execute(*cmd, root_helper=self.root_helper, run_as_root=True) def _get_vg_uuid(self): (out, err) = self._execute('vgs', '--noheadings', @@ -125,14 +132,17 @@ class LVM(object): return [] @staticmethod - def supports_thin_provisioning(): + def supports_thin_provisioning(root_helper): """Static method to check for thin LVM support on a system. + :param root_helper: root_helper to use for execute :returns: True if supported, False otherwise """ cmd = ['vgs', '--version'] - (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + (out, err) = putils.execute(*cmd, + root_helper=root_helper, + run_as_root=True) lines = out.split('\n') for line in lines: @@ -147,9 +157,10 @@ class LVM(object): return False @staticmethod - def get_all_volumes(vg_name=None, no_suffix=True): + def get_all_volumes(root_helper, vg_name=None, no_suffix=True): """Static method to get all LV's on a system. + :param root_helper: root_helper to use for execute :param vg_name: optional, gathers info for only the specified VG :param no_suffix: optional, reports sizes in g with no suffix :returns: List of Dictionaries with LV info @@ -163,7 +174,9 @@ class LVM(object): if vg_name is not None: cmd.append(vg_name) - (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + (out, err) = putils.execute(*cmd, + root_helper=root_helper, + run_as_root=True) lv_list = [] if out is not None: @@ -179,7 +192,7 @@ class LVM(object): :returns: List of Dictionaries with LV info """ - self.lv_list = self.get_all_volumes(self.vg_name) + self.lv_list = self.get_all_volumes(self.root_helper, self.vg_name) return self.lv_list def get_volume(self, name): @@ -194,9 +207,10 @@ class LVM(object): return r @staticmethod - def get_all_physical_volumes(vg_name=None, no_suffix=True): + def get_all_physical_volumes(root_helper, vg_name=None, no_suffix=True): """Static method to get all PVs on a system. + :param root_helper: root_helper to use for execute :param vg_name: optional, gathers info for only the specified VG :param no_suffix: optional, reports sizes in g with no suffix :returns: List of Dictionaries with PV info @@ -212,7 +226,9 @@ class LVM(object): if vg_name is not None: cmd.append(vg_name) - (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + (out, err) = putils.execute(*cmd, + root_helper=root_helper, + run_as_root=True) pv_list = [] if out is not None: @@ -232,13 +248,15 @@ class LVM(object): :returns: List of Dictionaries with PV info """ - self.pv_list = self.get_all_physical_volumes(self.vg_name) + self.pv_list = self.get_all_physical_volumes(self.root_helper, + self.vg_name) return self.pv_list @staticmethod - def get_all_volume_groups(vg_name=None, no_suffix=True): + def get_all_volume_groups(root_helper, vg_name=None, no_suffix=True): """Static method to get all VGs on a system. + :param root_helper: root_helper to use for execute :param vg_name: optional, gathers info for only the specified VG :param no_suffix: optional, reports sizes in g with no suffix :returns: List of Dictionaries with VG info @@ -255,7 +273,9 @@ class LVM(object): if vg_name is not None: cmd.append(vg_name) - (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + (out, err) = putils.execute(*cmd, + root_helper=root_helper, + run_as_root=True) vg_list = [] if out is not None: @@ -279,7 +299,7 @@ class LVM(object): :returns: Dictionaries of VG info """ - vg_list = self.get_all_volume_groups(self.vg_name) + vg_list = self.get_all_volume_groups(self.root_helper, self.vg_name) if len(vg_list) != 1: LOG.error(_('Unable to find VG: %s') % self.vg_name) @@ -291,9 +311,9 @@ class LVM(object): self.vg_uuid = vg_list[0]['uuid'] if self.vg_thin_pool is not None: - for lv in self.get_all_volumes(self.vg_name): - if lv[1] == self.vg_thin_pool: - self.vg_thin_pool_size = lv[2] + for lv in self.get_all_volumes(self.root_helper, self.vg_name): + if lv['name'] == self.vg_thin_pool: + self.vg_thin_pool_size = lv['size'] def create_thin_pool(self, name=None, size_str=0): """Creates a thin provisioning pool for this VG. @@ -307,7 +327,7 @@ class LVM(object): """ - if not self.supports_thin_provisioning(): + if not self.supports_thin_provisioning(self.root_helper): LOG.error(_('Requested to setup thin provisioning, ' 'however current LVM version does not ' 'support it.')) @@ -327,7 +347,7 @@ class LVM(object): cmd = ['lvcreate', '-T', '-L', size_str, pool_path] self._execute(*cmd, - root_helper='sudo', + root_helper=self.root_helper, run_as_root=True) self.vg_thin_pool = pool_path @@ -358,7 +378,7 @@ class LVM(object): try: self._execute(*cmd, - root_helper='sudo', + root_helper=self.root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_('Error creating Volume')) @@ -387,7 +407,7 @@ class LVM(object): try: self._execute(*cmd, - root_helper='sudo', + root_helper=self.root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_('Error creating snapshot')) @@ -405,7 +425,7 @@ class LVM(object): self._execute('lvremove', '-f', '%s/%s' % (self.vg_name, name), - root_helper='sudo', run_as_root=True) + root_helper=self.root_helper, run_as_root=True) def revert(self, snapshot_name): """Revert an LV from snapshot. @@ -414,13 +434,15 @@ class LVM(object): """ self._execute('lvconvert', '--merge', - snapshot_name, root_helper='sudo', + snapshot_name, root_helper=self.root_helper, run_as_root=True) def lv_has_snapshot(self, name): out, err = self._execute('lvdisplay', '--noheading', '-C', '-o', 'Attr', - '%s/%s' % (self.vg_name, name)) + '%s/%s' % (self.vg_name, name), + root_helper=self.root_helper, + run_as_root=True) if out: out = out.strip() if (out[0] == 'o') or (out[0] == 'O'): diff --git a/cinder/tests/api/contrib/test_admin_actions.py b/cinder/tests/api/contrib/test_admin_actions.py index 31132550d..942f41686 100644 --- a/cinder/tests/api/contrib/test_admin_actions.py +++ b/cinder/tests/api/contrib/test_admin_actions.py @@ -16,6 +16,7 @@ import webob from oslo.config import cfg +from cinder.brick.local_dev import lvm as brick_lvm from cinder import context from cinder import db from cinder import exception @@ -45,6 +46,7 @@ class AdminActionsTest(test.TestCase): self.flags(rpc_backend='cinder.openstack.common.rpc.impl_fake') self.flags(lock_path=self.tempdir) self.volume_api = volume_api.API() + self.stubs.Set(brick_lvm.LVM, '_vg_exists', lambda x: True) def tearDown(self): shutil.rmtree(self.tempdir) @@ -432,7 +434,6 @@ class AdminActionsTest(test.TestCase): def test_attach_attaching_volume_with_different_instance(self): """Test that attaching volume reserved for another instance fails.""" - # admin context ctx = context.RequestContext('admin', 'fake', True) # current status is available volume = db.volume_create(ctx, {'status': 'available', 'host': 'test', diff --git a/cinder/tests/brick/fake_lvm.py b/cinder/tests/brick/fake_lvm.py new file mode 100644 index 000000000..a6f76ab9b --- /dev/null +++ b/cinder/tests/brick/fake_lvm.py @@ -0,0 +1,69 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinder.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +class FakeBrickLVM(object): + """Logs and records calls, for unit tests.""" + def __init__(self, vg_name, create, pv_list, vtype, execute=None): + super(FakeBrickLVM, self).__init__() + self.vg_size = '5.00' + self.vg_free_space = '5.00' + self.vg_name = vg_name + + def supports_thin_provisioning(): + return False + + def get_all_volumes(vg_name=None, no_suffix=True): + if vg_name is not None: + return [vg_name] + return ['cinder-volumes', 'fake-vg-1'] + + def get_volumes(self): + return ['fake-volume'] + + def get_volume(self, name): + return ['name'] + + def get_all_physical_volumes(vg_name=None, no_suffix=True): + return [] + + def get_physical_volumes(self): + return [] + + def get_all_volume_groups(vg_name=None, no_suffix=True): + return ['cinder-volumes', 'fake-vg'] + + def update_volume_group_info(self): + pass + + def create_thin_pool(self, name=None, size_str=0): + pass + + def create_volume(self, name, size_str, lv_type='default', mirror_count=0): + pass + + def create_lv_snapshot(self, name, source_lv_name, lv_type='default'): + pass + + def delete(self, name): + pass + + def revert(self, snapshot_name): + pass + + def lv_has_snapshot(self, name): + return False diff --git a/cinder/tests/brick/test_brick_lvm.py b/cinder/tests/brick/test_brick_lvm.py index 1751ece7d..264be4859 100644 --- a/cinder/tests/brick/test_brick_lvm.py +++ b/cinder/tests/brick/test_brick_lvm.py @@ -44,6 +44,7 @@ class BrickLvmTestCase(test.TestCase): self.stubs.Set(processutils, 'execute', self.fake_execute) self.vg = brick.LVM(self.configuration.volume_group_name, + 'sudo', False, None, 'default', self.fake_execute) @@ -117,7 +118,7 @@ class BrickLvmTestCase(test.TestCase): self.assertEqual(self.vg.get_volume('fake-1')['name'], 'fake-1') def test_get_all_physical_volumes(self): - pvs = self.vg.get_all_physical_volumes() + pvs = self.vg.get_all_physical_volumes('sudo') self.assertEqual(len(pvs), 3) def test_get_physical_volumes(self): @@ -125,8 +126,9 @@ class BrickLvmTestCase(test.TestCase): self.assertEqual(len(pvs), 1) def test_get_volume_groups(self): - self.assertEqual(len(self.vg.get_all_volume_groups()), 3) - self.assertEqual(len(self.vg.get_all_volume_groups('fake-volumes')), 1) + self.assertEqual(len(self.vg.get_all_volume_groups('sudo')), 3) + self.assertEqual(len(self.vg.get_all_volume_groups('sudo', + 'fake-volumes')), 1) def test_thin_support(self): # lvm.supports_thin() is a static method and doesn't @@ -134,13 +136,13 @@ class BrickLvmTestCase(test.TestCase): # so we need to stub proessutils.execute appropriately self.stubs.Set(processutils, 'execute', self.fake_execute) - self.assertTrue(self.vg.supports_thin_provisioning()) + self.assertTrue(self.vg.supports_thin_provisioning('sudo')) self.stubs.Set(processutils, 'execute', self.fake_pretend_lvm_version) - self.assertTrue(self.vg.supports_thin_provisioning()) + self.assertTrue(self.vg.supports_thin_provisioning('sudo')) self.stubs.Set(processutils, 'execute', self.fake_old_lvm_version) - self.assertFalse(self.vg.supports_thin_provisioning()) + self.assertFalse(self.vg.supports_thin_provisioning('sudo')) def test_lv_has_snapshot(self): self.assertTrue(self.vg.lv_has_snapshot('fake-volumes')) diff --git a/cinder/tests/fake_driver.py b/cinder/tests/fake_driver.py index 6f352a7c7..37f5621bd 100644 --- a/cinder/tests/fake_driver.py +++ b/cinder/tests/fake_driver.py @@ -13,6 +13,7 @@ # under the License. from cinder.openstack.common import log as logging +from cinder.tests.brick.fake_lvm import FakeBrickLVM from cinder.volume import driver from cinder.volume.drivers import lvm @@ -25,6 +26,9 @@ class FakeISCSIDriver(lvm.LVMISCSIDriver): def __init__(self, *args, **kwargs): super(FakeISCSIDriver, self).__init__(execute=self.fake_execute, *args, **kwargs) + self.vg = FakeBrickLVM('cinder-volumes', False, + None, 'default', + self.fake_execute) def check_for_setup_error(self): """No setup necessary in fake mode.""" diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index c000e01ed..aa91f3d9e 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -22,7 +22,6 @@ Tests for Volume Code. import datetime import os -import re import shutil import socket import tempfile @@ -30,9 +29,8 @@ import tempfile import mox from oslo.config import cfg -from cinder.brick.initiator import connector as brick_conn from cinder.brick.iscsi import iscsi -from cinder.brick.iser import iser +from cinder.brick.local_dev import lvm as brick_lvm from cinder import context from cinder import db from cinder import exception @@ -45,6 +43,7 @@ from cinder.openstack.common import rpc import cinder.policy from cinder import quota from cinder import test +from cinder.tests.brick.fake_lvm import FakeBrickLVM from cinder.tests import conf_fixture from cinder.tests.image import fake as fake_image from cinder.tests.keymgr import fake as fake_keymgr @@ -78,8 +77,12 @@ class VolumeTestCase(test.TestCase): self.volume = importutils.import_object(CONF.volume_manager) self.context = context.get_admin_context() self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target) + self.stubs.Set(brick_lvm.LVM, + 'get_all_volume_groups', + self.fake_get_all_volume_groups) fake_image.stub_out_image_service(self.stubs) test_notifier.NOTIFICATIONS = [] + self.stubs.Set(brick_lvm.LVM, '_vg_exists', lambda x: True) def tearDown(self): try: @@ -92,6 +95,13 @@ class VolumeTestCase(test.TestCase): def fake_get_target(obj, iqn): return 1 + def fake_get_all_volume_groups(obj, vg_name=None, no_suffix=True): + return [{'name': 'cinder-volumes', + 'size': '5.00', + 'available': '2.50', + 'lv_count': '2', + 'uuid': 'vR1JU3-FAKE-C4A9-PQFh-Mctm-9FwA-Xwzc1m'}] + @staticmethod def _create_volume(size=0, snapshot_id=None, image_id=None, source_volid=None, metadata=None, status="creating", @@ -707,7 +717,6 @@ class VolumeTestCase(test.TestCase): """Test snapshot can be created with metadata and deleted.""" test_meta = {'fake_key': 'fake_value'} volume = self._create_volume(0, None) - volume_id = volume['id'] snapshot = self._create_snapshot(volume['id'], metadata=test_meta) snapshot_id = snapshot['id'] @@ -877,6 +886,12 @@ class VolumeTestCase(test.TestCase): def test_delete_busy_snapshot(self): """Test snapshot can be created and deleted.""" + + self.volume.driver.vg = FakeBrickLVM('cinder-volumes', + False, + None, + 'default') + volume = self._create_volume() volume_id = volume['id'] self.volume.create_volume(self.context, volume_id) @@ -884,6 +899,7 @@ class VolumeTestCase(test.TestCase): self.volume.create_snapshot(self.context, volume_id, snapshot_id) self.mox.StubOutWithMock(self.volume.driver, 'delete_snapshot') + self.volume.driver.delete_snapshot( mox.IgnoreArg()).AndRaise( exception.SnapshotIsBusy(snapshot_name='fake')) @@ -1099,8 +1115,6 @@ class VolumeTestCase(test.TestCase): 'disk_format': 'raw', 'container_format': 'bare'} - image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - try: volume_id = None volume_api = cinder.volume.api.API( @@ -1125,8 +1139,6 @@ class VolumeTestCase(test.TestCase): 'disk_format': 'raw', 'container_format': 'bare'} - image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - volume_api = cinder.volume.api.API(image_service=_FakeImageService()) self.assertRaises(exception.InvalidInput, @@ -1146,8 +1158,6 @@ class VolumeTestCase(test.TestCase): 'container_format': 'bare', 'min_disk': 5} - image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - volume_api = cinder.volume.api.API(image_service=_FakeImageService()) self.assertRaises(exception.InvalidInput, @@ -1317,8 +1327,6 @@ class VolumeTestCase(test.TestCase): ctx = context.get_admin_context(read_deleted="yes") # Find all all snapshots valid within a timeframe window. - vol = db.volume_create(self.context, {'id': 1}) - # Not in window db.snapshot_create( ctx, @@ -1699,6 +1707,7 @@ class DriverTestCase(test.TestCase): self.context = context.get_admin_context() self.output = "" self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target) + self.stubs.Set(brick_lvm.LVM, '_vg_exists', lambda x: True) def _fake_execute(_command, *_args, **_kwargs): """Fake _execute.""" @@ -1736,14 +1745,20 @@ class LVMISCSIVolumeDriverTestCase(DriverTestCase): lambda x: False) self.stubs.Set(self.volume.driver, '_delete_volume', lambda x: False) - # Want DriverTestCase._fake_execute to return 'o' so that - # volume.driver.delete_volume() raises the VolumeIsBusy exception. - self.output = 'o' + + self.volume.driver.vg = FakeBrickLVM('cinder-volumes', + False, + None, + 'default') + + self.stubs.Set(self.volume.driver.vg, 'lv_has_snapshot', + lambda x: True) self.assertRaises(exception.VolumeIsBusy, self.volume.driver.delete_volume, {'name': 'test1', 'size': 1024}) - # when DriverTestCase._fake_execute returns something other than - # 'o' volume.driver.delete_volume() does not raise an exception. + + self.stubs.Set(self.volume.driver.vg, 'lv_has_snapshot', + lambda x: False) self.output = 'x' self.volume.driver.delete_volume({'name': 'test1', 'size': 1024}) @@ -1784,7 +1799,8 @@ class LVMISCSIVolumeDriverTestCase(DriverTestCase): def test_lvm_migrate_volume_proceed(self): hostname = socket.gethostname() - capabilities = {'location_info': 'LVMVolumeDriver:%s:bar' % hostname} + capabilities = {'location_info': 'LVMVolumeDriver:%s:' + 'cinder-volumes:default:0' % hostname} host = {'capabilities': capabilities} vol = {'name': 'test', 'id': 1, 'size': 1} self.stubs.Set(self.volume.driver, 'remove_export', @@ -1797,6 +1813,11 @@ class LVMISCSIVolumeDriverTestCase(DriverTestCase): lambda x: None) self.stubs.Set(self.volume.driver, '_create_export', lambda x, y, vg='vg': None) + + self.volume.driver.vg = FakeBrickLVM('cinder-volumes', + False, + None, + 'default') moved, model_update = self.volume.driver.migrate_volume(self.context, vol, host) self.assertEqual(moved, True) @@ -1884,7 +1905,18 @@ class ISCSITestCase(DriverTestCase): out += " test2-volumes 5.52 0.52" return out, None + def _fake_get_all_volume_groups(obj, vg_name=None, no_suffix=True): + return [{'name': 'cinder-volumes', + 'size': '5.52', + 'available': '0.52', + 'lv_count': '2', + 'uuid': 'vR1JU3-FAKE-C4A9-PQFh-Mctm-9FwA-Xwzc1m'}] + + self.stubs.Set(brick_lvm.LVM, + 'get_all_volume_groups', + _fake_get_all_volume_groups) self.volume.driver.set_execute(_emulate_vgs_execute) + self.volume.driver.vg = brick_lvm.LVM('cinder-volumes', 'sudo') self.volume.driver._update_volume_stats() @@ -1955,6 +1987,7 @@ class VolumePolicyTestCase(test.TestCase): cinder.policy.init() self.context = context.get_admin_context() + self.stubs.Set(brick_lvm.LVM, '_vg_exists', lambda x: True) def tearDown(self): super(VolumePolicyTestCase, self).tearDown() diff --git a/cinder/volume/drivers/lvm.py b/cinder/volume/drivers/lvm.py index 7c596554a..2b01cc031 100644 --- a/cinder/volume/drivers/lvm.py +++ b/cinder/volume/drivers/lvm.py @@ -20,7 +20,6 @@ Driver for Linux servers running LVM. """ -import math import os import re import socket @@ -29,6 +28,7 @@ from oslo.config import cfg from cinder.brick.iscsi import iscsi from cinder.brick.iser import iser +from cinder.brick.local_dev import lvm as lvm from cinder import exception from cinder.image import image_utils from cinder.openstack.common import fileutils @@ -51,6 +51,10 @@ volume_opts = [ default=0, help='If set, create lvms with multiple mirrors. Note that ' 'this requires lvm_mirrors + 2 pvs with available space'), + cfg.StrOpt('lvm_type', + default='default', + help='Type of LVM volumes to deploy; (default or thin)'), + ] CONF = cfg.CONF @@ -60,84 +64,115 @@ CONF.register_opts(volume_opts) class LVMVolumeDriver(driver.VolumeDriver): """Executes commands relating to Volumes.""" - VERSION = '1.0.0' + VERSION = '2.0.0' - def __init__(self, *args, **kwargs): + def __init__(self, vg_obj=None, *args, **kwargs): super(LVMVolumeDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(volume_opts) self.hostname = socket.gethostname() + self.vg = vg_obj - def check_for_setup_error(self): - """Returns an error if prerequisites aren't met""" - out, err = self._execute('vgs', '--noheadings', '-o', 'name', - run_as_root=True) - volume_groups = out.split() - if self.configuration.volume_group not in volume_groups: - exception_message = (_("volume group %s doesn't exist") - % self.configuration.volume_group) - raise exception.VolumeBackendAPIException(data=exception_message) - - def _create_volume(self, volume_name, sizestr, vg=None): - if vg is None: - vg = self.configuration.volume_group - no_retry_list = ['Insufficient free extents', - 'One or more specified logical volume(s) not found'] + def set_execute(self, execute): + self._execute = execute - cmd = ['lvcreate', '-L', sizestr, '-n', volume_name, vg] - if self.configuration.lvm_mirrors: - cmd.extend(['-m', self.configuration.lvm_mirrors, '--nosync']) - terras = int(sizestr[:-1]) / 1024.0 - if terras >= 1.5: - rsize = int(2 ** math.ceil(math.log(terras) / math.log(2))) - # NOTE(vish): Next power of two for region size. See: - # http://red.ht/U2BPOD - cmd.extend(['-R', str(rsize)]) + def check_for_setup_error(self): + """Verify that requirements are in place to use LVM driver.""" + if self.vg is None: + root_helper = 'sudo cinder-rootwrap %s' % CONF.rootwrap_config + try: + self.vg = lvm.LVM(self.configuration.volume_group, + root_helper, + lvm_type=self.configuration.lvm_type, + executor=self._execute) + except lvm.VolumeGroupNotFound: + message = ("Volume Group %s does not exist" % + self.configuration.volume_group) + raise exception.VolumeBackendAPIException(data=message) + + vg_list = volutils.get_all_volume_groups( + self.configuration.volume_group) + vg_dict = \ + (vg for vg in vg_list if vg['name'] == self.vg.vg_name).next() + if vg_dict is None: + message = ("Volume Group %s does not exist" % + self.configuration.volume_group) + raise exception.VolumeBackendAPIException(data=message) + + if self.configuration.lvm_type == 'thin': + # Specific checks for using Thin provisioned LV's + if not volutils.supports_thin_provisioning(): + message = ("Thin provisioning not supported " + "on this version of LVM.") + raise exception.VolumeBackendAPIException(data=message) + + pool_name = "%s-pool" % self.configuration.volume_group + if self.vg.get_volume(pool_name) is None: + try: + self.vg.create_thin_pool(pool_name) + except exception.ProcessExecutionError as exc: + exception_message = ("Failed to create thin pool, " + "error message was: %s" + % exc.stderr) + raise exception.VolumeBackendAPIException( + data=exception_message) - self._try_execute(*cmd, run_as_root=True, no_retry_list=no_retry_list) + def _sizestr(self, size_in_g): + if int(size_in_g) == 0: + return '100m' + return '%sg' % size_in_g def _volume_not_present(self, volume_name): - path_name = '%s/%s' % (self.configuration.volume_group, volume_name) - try: - self._try_execute('lvdisplay', path_name, run_as_root=True) - except Exception as e: - # If the volume isn't present - return True - return False + return self.vg.get_volume(volume_name) is None - def _delete_volume(self, volume): + def _delete_volume(self, volume, is_snapshot=False): """Deletes a logical volume.""" + # zero out old volumes to prevent data leaking between users # TODO(ja): reclaiming space should be done lazy and low priority dev_path = self.local_path(volume) - if os.path.exists(dev_path): - self.clear_volume(volume) - self._try_execute('lvremove', '-f', "%s/%s" % - (self.configuration.volume_group, - self._escape_snapshot(volume['name'])), - run_as_root=True) - - def _sizestr(self, size_in_g): - if int(size_in_g) == 0: - return '100M' - return '%sG' % size_in_g + # TODO(jdg): Maybe we could optimize this for snaps by looking at + # the cow table and only overwriting what's necessary? + # for now we're still skipping on snaps due to hang issue + if os.path.exists(dev_path) and not is_snapshot: + self.clear_volume(volume) + name = volume['name'] + if is_snapshot: + name = self._escape_snapshot(volume['name']) + self.vg.delete(name) - # Linux LVM reserves name that starts with snapshot, so that - # such volume name can't be created. Mangle it. def _escape_snapshot(self, 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('snapshot'): return snapshot_name return '_' + snapshot_name + def _create_volume(self, name, size, lvm_type, mirror_count, vg=None): + vg_ref = self.vg + if vg is not None: + vg_ref = vg + + vg_ref.create_volume(name, size, lvm_type, mirror_count) + def create_volume(self, volume): - """Creates a logical volume. Can optionally return a Dictionary of - changes to the volume object to be persisted. - """ - self._create_volume(volume['name'], self._sizestr(volume['size'])) + """Creates a logical volume.""" + mirror_count = 0 + if self.configuration.lvm_mirrors: + mirror_count = self.configuration.lvm_mirrors + + self._create_volume(volume['name'], + self._sizestr(volume['size']), + self.configuration.lvm_type, + mirror_count) def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" - self._create_volume(volume['name'], self._sizestr(volume['size'])) + self._create_volume(volume['name'], + self._sizestr(volume['size']), + self.configuration.lvm_type, + self.configuration.lvm_mirrors) + volutils.copy_volume(self.local_path(snapshot), self.local_path(volume), snapshot['volume_size'] * 1024, @@ -149,18 +184,8 @@ class LVMVolumeDriver(driver.VolumeDriver): # If the volume isn't present, then don't attempt to delete return True - # TODO(yamahata): lvm can't delete origin volume only without - # deleting derived snapshots. Can we do something fancy? - out, err = self._execute('lvdisplay', '--noheading', - '-C', '-o', 'Attr', - '%s/%s' % (self.configuration.volume_group, - volume['name']), - run_as_root=True) - # fake_execute returns None resulting unit test error - if out: - out = out.strip() - if (out[0] == 'o') or (out[0] == 'O'): - raise exception.VolumeIsBusy(volume_name=volume['name']) + if self.vg.lv_has_snapshot(volume['name']): + raise exception.VolumeIsBusy(volume_name=volume['name']) self._delete_volume(volume) @@ -202,12 +227,10 @@ class LVMVolumeDriver(driver.VolumeDriver): def create_snapshot(self, snapshot): """Creates a snapshot.""" - orig_lv_name = "%s/%s" % (self.configuration.volume_group, - snapshot['volume_name']) - self._try_execute('lvcreate', '-L', - self._sizestr(snapshot['volume_size']), - '--name', self._escape_snapshot(snapshot['name']), - '--snapshot', orig_lv_name, run_as_root=True) + + self.vg.create_lv_snapshot(self._escape_snapshot(snapshot['name']), + snapshot['volume_name'], + self.configuration.lvm_type) def delete_snapshot(self, snapshot): """Deletes a snapshot.""" @@ -219,7 +242,7 @@ class LVMVolumeDriver(driver.VolumeDriver): # TODO(yamahata): zeroing out the whole snapshot triggers COW. # it's quite slow. - self._delete_volume(snapshot) + self._delete_volume(snapshot, is_snapshot=True) def local_path(self, volume, vg=None): if vg is None: @@ -245,6 +268,10 @@ class LVMVolumeDriver(driver.VolumeDriver): def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" + + mirror_count = 0 + if self.configuration.lvm_mirrors: + mirror_count = self.configuration.lvm_mirrors LOG.info(_('Creating clone of volume: %s') % src_vref['id']) volume_name = src_vref['name'] temp_id = 'tmp-snap-%s' % volume['id'] @@ -253,8 +280,13 @@ class LVMVolumeDriver(driver.VolumeDriver): 'volume_size': src_vref['size'], 'name': 'clone-snap-%s' % volume['id'], 'id': temp_id} + self.create_snapshot(temp_snapshot) - self._create_volume(volume['name'], self._sizestr(volume['size'])) + self._create_volume(volume['name'], + self._sizestr(volume['size']), + self.configuration.lvm_type, + mirror_count) + try: volutils.copy_volume(self.local_path(temp_snapshot), self.local_path(volume), @@ -281,6 +313,47 @@ class LVMVolumeDriver(driver.VolumeDriver): with fileutils.file_open(volume_path, 'wb') as volume_file: backup_service.restore(backup, volume['id'], volume_file) + def get_volume_stats(self, refresh=False): + """Get volume status. + + If 'refresh' is True, run update the stats first. + """ + + if refresh: + self._update_volume_status() + self._update_volume_status() + + return self._stats + + def _update_volume_status(self): + """Retrieve status info from volume group.""" + + # FIXME(jdg): Fix up the duplicate code between + # LVM, LVMISCSI and ISER starting with this section + LOG.debug(_("Updating volume status")) + data = {} + + backend_name = self.configuration.safe_get('volume_backend_name') + data["volume_backend_name"] = backend_name or 'LVM' + data["vendor_name"] = 'Open Source' + data["driver_version"] = self.VERSION + data["storage_protocol"] = 'local' + + data['total_capacity_gb'] = float(self.vg.vg_size.replace(',', '.')) + data['free_capacity_gb'] =\ + float(self.vg.vg_free_space.replace(',', '.')) + data['reserved_percentage'] = self.configuration.reserved_percentage + data['QoS_support'] = False + data['location_info'] =\ + ('LVMVolumeDriver:%(hostname)s:%(vg)s' + ':%(lvm_type)s:%(lvm_mirrors)s' % + {'hostname': self.hostname, + 'vg': self.configuration.volume_group, + 'lvm_type': self.configuration.lvm_type, + 'lvm_mirrors': self.configuration.lvm_mirrors}) + + self._stats = data + class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): """Executes commands relating to ISCSI volumes. @@ -322,7 +395,7 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): auth_user, auth_pass) except exception.NotFound: - LOG.debug("volume_info:", volume_info) + LOG.debug(_("volume_info:%s"), volume_info) LOG.info(_("Skipping ensure_export. No iscsi_target " "provision for volume: %s"), volume['id']) return @@ -359,7 +432,7 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): volume['name'] not in volume['provider_location']): msg = _('Detected inconsistency in provider_location id') - LOG.debug(msg) + LOG.debug(_('%s'), msg) old_name = self._fix_id_migration(context, volume) if 'in-use' in volume['status']: volume_name = old_name @@ -525,35 +598,52 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): # this export has already been removed self.tgtadm.show_target(iscsi_target, iqn=iqn) - except Exception as e: + except Exception: LOG.info(_("Skipping remove_export. No iscsi_target " "is presently exported for volume: %s"), volume['id']) return self.tgtadm.remove_iscsi_target(iscsi_target, 0, volume['id']) - def migrate_volume(self, ctxt, volume, host): + def migrate_volume(self, ctxt, volume, host, thin=False, mirror_count=0): """Optimize the migration if the destination is on the same server. If the specified host is another back-end on the same server, and the volume is not attached, we can do the migration locally without going through iSCSI. """ + false_ret = (False, None) if 'location_info' not in host['capabilities']: return false_ret info = host['capabilities']['location_info'] try: - (dest_type, dest_hostname, dest_vg) = info.split(':') + (dest_type, dest_hostname, dest_vg, lvm_type, lvm_mirrors) =\ + info.split(':') except ValueError: return false_ret if (dest_type != 'LVMVolumeDriver' or dest_hostname != self.hostname): return false_ret - self.remove_export(ctxt, volume) - self._create_volume(volume['name'], - self._sizestr(volume['size']), - dest_vg) + if dest_vg != self.vg.vg_name: + vg_list = volutils.get_all_volume_groups() + vg_dict = \ + (vg for vg in vg_list if vg['name'] == self.vg.vg_name).next() + if vg_dict is None: + message = ("Destination Volume Group %s does not exist" % + dest_vg) + LOG.error(_('%s'), message) + return false_ret + + helper = 'sudo cinder-rootwrap %s' % CONF.rootwrap_config + dest_vg_ref = lvm.LVM(dest_vg, helper, lvm_type, self._execute) + self.remove_export(ctxt, volume) + self._create_volume(volume['name'], + self._sizestr(volume['size']), + lvm_type, + lvm_mirrors, + dest_vg_ref) + volutils.copy_volume(self.local_path(volume), self.local_path(volume, vg=dest_vg), volume['size'], @@ -582,6 +672,7 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): """Retrieve stats info from volume group.""" LOG.debug(_("Updating volume stats")) + self.vg.update_volume_group_info() data = {} # Note(zhiteng): These information are driver/backend specific, @@ -593,27 +684,17 @@ class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver): data["driver_version"] = self.VERSION data["storage_protocol"] = 'iSCSI' - data['total_capacity_gb'] = 0 - data['free_capacity_gb'] = 0 + data['total_capacity_gb'] = float(self.vg.vg_size.replace(',', '.')) + data['free_capacity_gb'] = float(self.vg.vg_free_space) data['reserved_percentage'] = self.configuration.reserved_percentage data['QoS_support'] = False - data['location_info'] = ('LVMVolumeDriver:%(hostname)s:%(vg)s' % - {'hostname': self.hostname, - 'vg': self.configuration.volume_group}) - - try: - out, err = self._execute('vgs', '--noheadings', '--nosuffix', - '--unit=G', '-o', 'name,size,free', - self.configuration.volume_group, - run_as_root=True) - except exception.ProcessExecutionError as exc: - LOG.error(_("Error retrieving volume stats: %s"), exc.stderr) - out = False - - if out: - volume = out.split() - data['total_capacity_gb'] = float(volume[1].replace(',', '.')) - data['free_capacity_gb'] = float(volume[2].replace(',', '.')) + data['location_info'] =\ + ('LVMVolumeDriver:%(hostname)s:%(vg)s' + ':%(lvm_type)s:%(lvm_mirrors)s' % + {'hostname': self.hostname, + 'vg': self.configuration.volume_group, + 'lvm_type': self.configuration.lvm_type, + 'lvm_mirrors': self.configuration.lvm_mirrors}) self._stats = data @@ -764,7 +845,7 @@ class LVMISERDriver(LVMISCSIDriver, driver.ISERDriver): self.tgtadm.show_target(iser_target, iqn=iqn) - except Exception as e: + except Exception: LOG.info(_("Skipping remove_export. No iser_target " "is presently exported for volume: %s"), volume['id']) return @@ -775,6 +856,7 @@ class LVMISERDriver(LVMISCSIDriver, driver.ISERDriver): """Retrieve status info from volume group.""" LOG.debug(_("Updating volume status")) + self.vg.update_volume_group_info() data = {} # Note(zhiteng): These information are driver/backend specific, @@ -785,26 +867,13 @@ class LVMISERDriver(LVMISCSIDriver, driver.ISERDriver): data["vendor_name"] = 'Open Source' data["driver_version"] = self.VERSION data["storage_protocol"] = 'iSER' + data['total_capacity_gb'] = float(self.vg.vg_size.replace(',', '.')) + data['free_capacity_gb'] =\ + float(self.vg.vg_free_space.replace(',', '.')) - data['total_capacity_gb'] = 0 - data['free_capacity_gb'] = 0 data['reserved_percentage'] = self.configuration.reserved_percentage data['QoS_support'] = False - try: - out, err = self._execute('vgs', '--noheadings', '--nosuffix', - '--unit=G', '-o', 'name,size,free', - self.configuration.volume_group, - run_as_root=True) - except exception.ProcessExecutionError as exc: - LOG.error(_("Error retrieving volume status: %s"), exc.stderr) - out = False - - if out: - volume = out.split() - data['total_capacity_gb'] = float(volume[1].replace(',', '.')) - data['free_capacity_gb'] = float(volume[2].replace(',', '.')) - self._stats = data def _iser_location(self, ip, target, iqn, lun=None): @@ -813,102 +882,3 @@ class LVMISERDriver(LVMISCSIDriver, driver.ISERDriver): def _iser_authentication(self, chap, name, password): return "%s %s %s" % (chap, name, password) - - -class ThinLVMVolumeDriver(LVMISCSIDriver): - """Subclass for thin provisioned LVM's.""" - - VERSION = '1.0' - - def __init__(self, *args, **kwargs): - super(ThinLVMVolumeDriver, self).__init__(*args, **kwargs) - - def check_for_setup_error(self): - """Returns an error if prerequisites aren't met""" - out, err = self._execute('lvs', '--option', - 'name', '--noheadings', - run_as_root=True) - pool_name = "%s-pool" % self.configuration.volume_group - if pool_name not in out: - if not self.configuration.pool_size: - out, err = self._execute('vgs', - self.configuration.volume_group, - '--noheadings', - '--options', - 'name,size', - run_as_root=True) - - size = re.sub(r'[\.][\d][\d]', '', out.split()[1]) - else: - size = "%s" % self.configuration.pool_size - - pool_path = '%s/%s' % (self.configuration.volume_group, - pool_name) - out, err = self._execute('lvcreate', '-T', '-L', size, - pool_path, run_as_root=True) - - def _do_lvm_snapshot(self, src_lvm_name, dest_vref, is_cinder_snap=True): - if is_cinder_snap: - new_name = self._escape_snapshot(dest_vref['name']) - else: - new_name = dest_vref['name'] - - self._try_execute('lvcreate', '-s', '-n', new_name, - src_lvm_name, run_as_root=True) - - def _create_volume(self, volume_name, sizestr): - vg_name = ("%s/%s-pool" % (self.configuration.volume_group, - self.configuration.volume_group)) - self._try_execute('lvcreate', '-T', '-V', sizestr, '-n', - volume_name, vg_name, run_as_root=True) - - def delete_volume(self, volume): - """Deletes a logical volume.""" - if self._volume_not_present(volume['name']): - return True - self._try_execute('lvremove', '-f', "%s/%s" % - (self.configuration.volume_group, - self._escape_snapshot(volume['name'])), - run_as_root=True) - - def create_cloned_volume(self, volume, src_vref): - """Creates a clone of the specified volume.""" - LOG.info(_('Creating clone of volume: %s') % src_vref['id']) - orig_lv_name = "%s/%s" % (self.configuration.volume_group, - src_vref['name']) - self._do_lvm_snapshot(orig_lv_name, volume, False) - - def create_snapshot(self, snapshot): - """Creates a snapshot of a volume.""" - orig_lv_name = "%s/%s" % (self.configuration.volume_group, - snapshot['volume_name']) - self._do_lvm_snapshot(orig_lv_name, snapshot) - - def get_volume_stats(self, refresh=False): - """Get volume stats. - If 'refresh' is True, run update the stats first. - """ - if refresh: - self._update_volume_stats() - - return self._stats - - def _update_volume_stats(self): - """Retrieve stats info from volume group.""" - - LOG.debug(_("Updating volume stats")) - data = {} - - backend_name = self.configuration.safe_get('volume_backend_name') - data["volume_backend_name"] = backend_name or self.__class__.__name__ - data["vendor_name"] = 'Open Source' - data["driver_version"] = self.VERSION - data["storage_protocol"] = 'iSCSI' - data['reserved_percentage'] = self.configuration.reserved_percentage - data['QoS_support'] = False - data['total_capacity_gb'] = 'infinite' - data['free_capacity_gb'] = 'infinite' - data['location_info'] = ('LVMVolumeDriver:%(hostname)s:%(vg)s' % - {'hostname': self.hostname, - 'vg': self.configuration.volume_group}) - self._stats = data diff --git a/cinder/volume/utils.py b/cinder/volume/utils.py index d29e76491..41a2e19ec 100644 --- a/cinder/volume/utils.py +++ b/cinder/volume/utils.py @@ -23,6 +23,7 @@ import stat from oslo.config import cfg +from cinder.brick.local_dev import lvm as brick_lvm from cinder import exception from cinder.openstack.common import log as logging from cinder.openstack.common.notifier import api as notifier_api @@ -192,3 +193,26 @@ def copy_volume(srcstr, deststr, size_in_m, sync=False, 'count=%d' % count, 'bs=%s' % blocksize, *extra_flags, run_as_root=True) + + +def supports_thin_provisioning(): + return brick_lvm.LVM.supports_thin_provisioning( + 'sudo cinder-rootwrap %s' % CONF.rootwrap_config) + + +def get_all_volumes(vg_name=None, no_suffix=True): + return brick_lvm.LVM.get_all_volumes( + 'sudo cinder-rootwrap %s' % CONF.rootwrap_config, + vg_name, no_suffix) + + +def get_all_physical_volumes(vg_name=None, no_suffix=True): + return brick_lvm.LVM.get_all_physical_volumes( + 'sudo cinder-rootwrap %s' % CONF.rootwrap_config, + vg_name, no_suffix) + + +def get_all_volume_groups(vg_name=None, no_suffix=True): + return brick_lvm.LVM.get_all_volume_groups( + 'sudo cinder-rootwrap %s' % CONF.rootwrap_config, + vg_name, no_suffix) -- 2.45.2