]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Create an LVM utility to use for local storage.
authorJohn Griffith <john.griffith@solidfire.com>
Wed, 8 May 2013 05:11:50 +0000 (23:11 -0600)
committerJohn Griffith <john.griffith@solidfire.com>
Mon, 13 May 2013 15:18:17 +0000 (09:18 -0600)
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 [new file with mode: 0644]
cinder/brick/local_dev/__init__.py [new file with mode: 0644]
cinder/brick/local_dev/lvm.py [new file with mode: 0644]
cinder/tests/brick/__init__.py [new file with mode: 0644]
cinder/tests/brick/test_brick_lvm.py [new file with mode: 0644]

diff --git a/cinder/brick/__init__.py b/cinder/brick/__init__.py
new file mode 100644 (file)
index 0000000..5e8da71
--- /dev/null
@@ -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 (file)
index 0000000..5e8da71
--- /dev/null
@@ -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 (file)
index 0000000..f4122c2
--- /dev/null
@@ -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 (file)
index 0000000..5e8da71
--- /dev/null
@@ -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 (file)
index 0000000..e418227
--- /dev/null
@@ -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')