From 0fbe00f70da73e0cc58ddf7c9fdead261392778d Mon Sep 17 00:00:00 2001 From: John Griffith Date: Tue, 7 May 2013 23:11:50 -0600 Subject: [PATCH] Create an LVM utility to use for local storage. This adds a simple LVM class for performing local LVM operations. The idea is that an LVM object is instantiated based on a volume group, init can be used to instantiate and query an existing volume group, or create a new one if given a list of PV's to use in creation. See BP for information on where this is going and how it will be used in Cinder and hopefully other projects. Implements blueprint: local-lvm-storage-utils Change-Id: Iddde92af18f2317edc5f4583b2113c2b8117a4fe --- cinder/brick/__init__.py | 16 ++ cinder/brick/local_dev/__init__.py | 16 ++ cinder/brick/local_dev/lvm.py | 323 +++++++++++++++++++++++++++ cinder/tests/brick/__init__.py | 16 ++ cinder/tests/brick/test_brick_lvm.py | 126 +++++++++++ 5 files changed, 497 insertions(+) create mode 100644 cinder/brick/__init__.py create mode 100644 cinder/brick/local_dev/__init__.py create mode 100644 cinder/brick/local_dev/lvm.py create mode 100644 cinder/tests/brick/__init__.py create mode 100644 cinder/tests/brick/test_brick_lvm.py diff --git a/cinder/brick/__init__.py b/cinder/brick/__init__.py new file mode 100644 index 000000000..5e8da711f --- /dev/null +++ b/cinder/brick/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation. +# All Rights Reserved. +# +# 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. diff --git a/cinder/brick/local_dev/__init__.py b/cinder/brick/local_dev/__init__.py new file mode 100644 index 000000000..5e8da711f --- /dev/null +++ b/cinder/brick/local_dev/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation. +# All Rights Reserved. +# +# 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. diff --git a/cinder/brick/local_dev/lvm.py b/cinder/brick/local_dev/lvm.py new file mode 100644 index 000000000..f4122c28a --- /dev/null +++ b/cinder/brick/local_dev/lvm.py @@ -0,0 +1,323 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation. +# All Rights Reserved. +# +# 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. + +""" +LVM class for performing LVM operations. +""" + +import math + +from itertools import izip + +from cinder.openstack.common.gettextutils import _ +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils as putils + +LOG = logging.getLogger(__name__) + + +class VolumeGroupNotFound(Exception): + def __init__(self, vg_name): + message = (_('Unable to find Volume Group: %s') % vg_name) + super(VolumeGroupNotFound, self).__init__(message) + + +class VolumeGroupCreationFailed(Exception): + def __init__(self, vg_name): + message = (_('Failed to create Volume Group: %s') % vg_name) + super(VolumeGroupCreationFailed, self).__init__(message) + + +class LVM(object): + """LVM object to enable various LVM related operations.""" + + def __init__(self, vg_name, create_vg=False, + physical_volumes=None): + """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 create_vg: Indicates the VG doesn't exist + and we want to create it + :param physical_volumes: List of PVs to build VG on + + """ + self.vg_name = vg_name + self.pv_list = [] + self.lv_list = [] + self.vg_size = 0 + self.vg_available_space = 0 + self.vg_lv_count = 0 + self.vg_uuid = None + + if create_vg and physical_volumes is not None: + self.pv_list = physical_volumes + + try: + self._create_vg(physical_volumes) + except putils.ProcessExecutionError as err: + LOG.exception(_('Error creating Volume Group')) + LOG.error(_('Cmd :%s') % err.cmd) + LOG.error(_('StdOut :%s') % err.stdout) + LOG.error(_('StdErr :%s') % err.stderr) + raise VolumeGroupCreationFailed(vg_name=self.vg_name) + + if self._vg_exists() is False: + LOG.error(_('Unable to locate Volume Group %s') % vg_name) + raise VolumeGroupNotFound(vg_name=vg_name) + + def _size_str(self, size_in_g): + if '.00' in size_in_g: + size_in_g = size_in_g.replace('.00', '') + + if int(size_in_g) == 0: + return '100M' + + return '%sG' % size_in_g + + def _vg_exists(self): + """Simple check to see if VG exists. + + :returns: True if vg specified in object exists, else False + + """ + exists = False + cmd = ['vgs', '--noheadings', '-o', 'name'] + (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + + if out is not None: + volume_groups = out.split() + if self.vg_name in volume_groups: + exists = True + + return exists + + def _create_vg(self, pv_list): + cmd = ['vgcreate', self.vg_name, ','.join(pv_list)] + putils.execute(*cmd, root_helper='sudo', run_as_root=True) + + def _get_vg_uuid(self): + (out, err) = putils.execute('vgs', '--noheadings', + '-o uuid', self.vg_name) + if out is not None: + return out.split() + else: + return [] + + @staticmethod + def get_all_volumes(vg_name=None): + """Static method to get all LV's on a system. + + :param vg_name: optional, gathers info for only the specified VG + :returns: List of Dictionaries with LV info + + """ + cmd = ['lvs', '--noheadings', '-o', 'vg_name,name,size'] + if vg_name is not None: + cmd += [vg_name] + + (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + + lv_list = [] + if out is not None: + volumes = out.split() + for vg, name, size in izip(*[iter(volumes)] * 3): + lv_list.append({"vg": vg, "name": name, "size": size}) + + return lv_list + + def get_volumes(self): + """Get all LV's associated with this instantiation (VG). + + :returns: List of Dictionaries with LV info + + """ + self.lv_list = self.get_all_volumes(self.vg_name) + return self.lv_list + + def get_volume(self, name): + """Get reference object of volume specified by name. + + :returns: dict representation of Logical Volume if exists + + """ + ref_list = self.get_volumes() + for r in ref_list: + if r['name'] == name: + return r + + @staticmethod + def get_all_physical_volumes(vg_name=None): + """Static method to get all PVs on a system. + + :param vg_name: optional, gathers info for only the specified VG + :returns: List of Dictionaries with PV info + + """ + cmd = ['pvs', '--noheadings', + '-o', 'vg_name,name,size,free', + '--separator', ':'] + if vg_name is not None: + cmd += [vg_name] + + (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + + pv_list = [] + if out is not None: + pvs = out.split() + for pv in pvs: + fields = pv.split(':') + pv_list.append({'vg': fields[0], + 'name': fields[1], + 'size': fields[2], + 'available': fields[3]}) + + return pv_list + + def get_physical_volumes(self): + """Get all PVs associated with this instantiation (VG). + + :returns: List of Dictionaries with PV info + + """ + self.pv_list = self.get_all_physical_volumes(self.vg_name) + return self.pv_list + + @staticmethod + def get_all_volume_groups(vg_name=None): + """Static method to get all VGs on a system. + + :param vg_name: optional, gathers info for only the specified VG + :returns: List of Dictionaries with VG info + + """ + cmd = ['vgs', '--noheadings', + '-o', 'name,size,free,lv_count,uuid', + '--separator', ':'] + if vg_name is not None: + cmd += [vg_name] + + (out, err) = putils.execute(*cmd, root_helper='sudo', run_as_root=True) + + vg_list = [] + if out is not None: + vgs = out.split() + for vg in vgs: + fields = vg.split(':') + vg_list.append({'name': fields[0], + 'size': fields[1], + 'available': fields[2], + 'lv_count': fields[3], + 'uuid': fields[4]}) + + return vg_list + + def update_volume_group_info(self): + """Update VG info for this instantiation. + + Used to update member fields of object and + provide a dict of info for caller. + + :returns: Dictionaries of VG info + + """ + vg_list = self.get_all_volume_groups(self.vg_name) + + if len(vg_list) != 1: + LOG.error(_('Unable to find VG: %s') % self.vg_name) + raise VolumeGroupNotFound(vg_name=self.vg_name) + + self.vg_size = vg_list[0]['size'] + self.vg_available_space = vg_list[0]['available'] + self.vg_lv_count = vg_list[0]['lv_count'] + self.vg_uuid = vg_list[0]['uuid'] + + return vg_list[0] + + def create_volume(self, name, size_str, lv_type='default', mirror_count=0): + """Creates a logical volume on the object's VG. + + :param name: Name to use when creating Logical Volume + :param size_str: Size to use when creating Logical Volume + :param lv_type: Type of Volume (default or thin) + :param mirror_count: Use LVM mirroring with specified count + + """ + size = self._size_str(size_str) + cmd = ['lvcreate', '-n', name, self.vg_name] + if lv_type == 'thin': + cmd += ['-T', '-V', size] + else: + cmd += ['-L', size] + + if mirror_count > 0: + cmd += ['-m', mirror_count, '--nosync'] + terras = int(size[:-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 += ['-R', str(rsize)] + + putils.execute(*cmd, + root_helper='sudo', + run_as_root=True) + + def create_lv_snapshot(self, name, source_lv_name, lv_type='default'): + """Creates a snapshot of a logical volume. + + :param name: Name to assign to new snapshot + :param source_lv_name: Name of Logical Volume to snapshot + :param lv_type: Type of LV (default or thin) + + """ + source_lvref = self.get_volume(source_lv_name) + if source_lvref is None: + LOG.error(_("Unable to find LV: %s") % source_lv_name) + return False + cmd = ['lvcreate', '--name', name, + '--snapshot', '%s/%s' % (self.vg_name, source_lv_name)] + if lv_type != 'thin': + size = source_lvref['size'] + cmd += ['-L', size] + + putils.execute(*cmd, + root_helper='sudo', + run_as_root=True) + + def delete(self, name): + """Delete logical volume or snapshot. + + :param name: Name of LV to delete + + """ + putils.execute('lvremove', + '-f', + '%s/%s' % (self.vg_name, name), + root_helper='sudo', run_as_root=True) + + def revert(self, snapshot_name): + """Revert an LV from snapshot. + + :param snapshot_name: Name of snapshot to revert + + """ + putils.execute('lvconvert', '--merge', + snapshot_name, root_helper='sudo', + run_as_root=True) diff --git a/cinder/tests/brick/__init__.py b/cinder/tests/brick/__init__.py new file mode 100644 index 000000000..5e8da711f --- /dev/null +++ b/cinder/tests/brick/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation. +# All Rights Reserved. +# +# 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. diff --git a/cinder/tests/brick/test_brick_lvm.py b/cinder/tests/brick/test_brick_lvm.py new file mode 100644 index 000000000..e418227ef --- /dev/null +++ b/cinder/tests/brick/test_brick_lvm.py @@ -0,0 +1,126 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +import mox + + +from cinder.brick.local_dev import lvm as brick +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils +from cinder import test +from cinder.volume import configuration as conf + +LOG = logging.getLogger(__name__) + + +def create_configuration(): + configuration = mox.MockObject(conf.Configuration) + configuration.append_config_values(mox.IgnoreArg()) + return configuration + + +class BrickLvmTestCase(test.TestCase): + def setUp(self): + self._mox = mox.Mox() + self.configuration = mox.MockObject(conf.Configuration) + self.configuration.volume_group_name = 'fake-volumes' + super(BrickLvmTestCase, self).setUp() + self.stubs.Set(processutils, 'execute', + self.fake_execute) + self.vg = brick.LVM(self.configuration.volume_group_name) + + def failed_fake_execute(obj, *cmd, **kwargs): + return ("\n", "fake-error") + + def fake_execute(obj, *cmd, **kwargs): + cmd_string = ', '.join(cmd) + data = "\n" + + if 'vgs, --noheadings, -o, name' == cmd_string: + data = " fake-volumes\n" + elif 'vgs, --noheadings, -o uuid, fake-volumes' in cmd_string: + data = " kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n" + elif 'vgs, --noheadings, -o, name,size,free,lv_count,uuid' in\ + cmd_string: + data = " fake-volumes:10.00g:10.00g:0:"\ + "kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1\n" + if 'fake-volumes' in cmd_string: + return (data, "") + data += " fake-volumes-2:10.00g:10.00g:0:"\ + "lWyauW-dKpG-Rz7E-xtKY-jeju-QsYU-SLG7Z2\n" + data += " fake-volumes-3:10.00g:10.00g:0:"\ + "mXzbuX-dKpG-Rz7E-xtKY-jeju-QsYU-SLG8Z3\n" + elif 'lvs, --noheadings, -o, vg_name,name,size' in cmd_string: + data = " fake-volumes fake-1 1.00g\n" + data += " fake-volumes fake-2 1.00g\n" + elif 'lvs, --noheadings, -o, vg_name,name,size' in cmd_string: + data = " fake-volumes fake-1 1.00g\n" + data += " fake-volumes fake-2 1.00g\n" + elif 'pvs, --noheadings' and 'fake-volumes' in cmd_string: + data = " fake-volumes:/dev/sda:10.00g:8.99g\n" + elif 'pvs, --noheadings' in cmd_string: + data = " fake-volumes:/dev/sda:10.00g:8.99g\n" + data += " fake-volumes-2:/dev/sdb:10.00g:8.99g\n" + data += " fake-volumes-3:/dev/sdc:10.00g:8.99g\n" + else: + pass + + return (data, "") + + def test_vg_exists(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + self.assertEqual(self.vg._vg_exists(), True) + + self.stubs.Set(processutils, 'execute', self.failed_fake_execute) + self.assertEqual(self.vg._vg_exists(), False) + + def test_get_vg_uuid(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + self.assertEqual(self.vg._get_vg_uuid()[0], + 'kVxztV-dKpG-Rz7E-xtKY-jeju-QsYU-SLG6Z1') + + def test_get_all_volumes(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + out = self.vg.get_volumes() + + self.assertEqual(out[0]['name'], 'fake-1') + self.assertEqual(out[0]['size'], '1.00g') + self.assertEqual(out[0]['vg'], 'fake-volumes') + + def test_get_volume(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + self.assertEqual(self.vg.get_volume('fake-1')['name'], 'fake-1') + + def test_get_all_physical_volumes(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + pvs = self.vg.get_all_physical_volumes() + self.assertEqual(len(pvs), 3) + + def test_get_physical_volumes(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + pvs = self.vg.get_physical_volumes() + self.assertEqual(len(pvs), 1) + + def test_get_volume_groups(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + self.assertEqual(len(self.vg.get_all_volume_groups()), 3) + self.assertEqual(len(self.vg.get_all_volume_groups('fake-volumes')), 1) + + def test_update_vg_info(self): + self.stubs.Set(processutils, 'execute', self.fake_execute) + self.assertEqual(self.vg.update_volume_group_info()['name'], + 'fake-volumes') -- 2.45.2