From e0540dfed1c1276106105aea8d5765356961ef3d Mon Sep 17 00:00:00 2001 From: Boris Filippov Date: Wed, 16 May 2012 15:17:53 +0400 Subject: [PATCH] blueprint lvm-disk-images Add ability to use LVM volumes for VM disks. Implements LVM disks support for libvirt driver. VM disks will be stored on LVM volumes in volume group specified by `libvirt_images_volume_group` option. Another option `libvirt_local_images_type` specify which storage type will be used. Supported values are `raw`, `lvm`, `qcow2`, `default`. If `libvirt_local_images_type` = `default`, usual logic with `use_cow_images` flag is used. Boolean option `libvirt_sparse_logical_volumes` controls which type of logical volumes will be created (sparsed with virtualsize or usual logical volumes with full space allocation). Default value for this option is `False`. Commit introduce three classes: `Raw`, `Qcow2` and `Lvm`. They contain image creation logic, that was stored in `LibvirtConnection._cache_image` and `libvirt_info` methods, that produce right `LibvirtGuestConfigDisk` configurations for libvirt. `Backend` class choose which image type to use. Change-Id: I0d01cb7d2fd67de2565b8d45d34f7846ad4112c2 --- Authors | 1 + nova/rootwrap/compute.py | 9 + nova/tests/fake_imagebackend.py | 48 ++++ nova/tests/fake_libvirt_utils.py | 16 ++ nova/tests/test_imagebackend.py | 392 ++++++++++++++++++++++++++++++ nova/tests/test_libvirt.py | 27 +- nova/tests/test_virt_drivers.py | 2 + nova/virt/disk/api.py | 8 +- nova/virt/libvirt/connection.py | 269 ++++++++------------ nova/virt/libvirt/imagebackend.py | 255 +++++++++++++++++++ nova/virt/libvirt/utils.py | 55 +++++ 11 files changed, 900 insertions(+), 182 deletions(-) create mode 100644 nova/tests/fake_imagebackend.py create mode 100644 nova/tests/test_imagebackend.py create mode 100644 nova/virt/libvirt/imagebackend.py diff --git a/Authors b/Authors index 3448eb05527..967dc6bebeb 100644 --- a/Authors +++ b/Authors @@ -23,6 +23,7 @@ Asbjørn Sannes Ben McGraw Ben Swartzlander Bilal Akhtar +Boris Filippov Brad Hall Brad McConnell Brendan Maguire diff --git a/nova/rootwrap/compute.py b/nova/rootwrap/compute.py index bb53abc7c7e..a28e79706be 100755 --- a/nova/rootwrap/compute.py +++ b/nova/rootwrap/compute.py @@ -188,4 +188,13 @@ # nova/virt/libvirt/connection.py: filters.ReadFileFilter("/etc/iscsi/initiatorname.iscsi"), + # nova/virt/libvirt/connection.py: + filters.CommandFilter("/sbin/lvremove", "root"), + + # nova/virt/libvirt/utils.py: + filters.CommandFilter("/sbin/lvcreate", "root"), + + # nova/virt/libvirt/utils.py: + filters.CommandFilter("/sbin/vgs", "root") + ] diff --git a/nova/tests/fake_imagebackend.py b/nova/tests/fake_imagebackend.py new file mode 100644 index 00000000000..425fd037ea4 --- /dev/null +++ b/nova/tests/fake_imagebackend.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Grid Dynamics +# 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 os + +from nova.virt.libvirt import config +from nova.virt.libvirt import imagebackend + + +class Backend(object): + def __init__(self, use_cow): + pass + + def image(self, instance, name, suffix='', image_type=''): + class FakeImage(imagebackend.Image): + def __init__(self, instance, name, suffix=''): + self.path = os.path.join(instance, name + suffix) + + def create_image(self, prepare_template, base, + size, *args, **kwargs): + pass + + def cache(self, fn, fname, size=None, *args, **kwargs): + pass + + def libvirt_info(self, device_type): + info = config.LibvirtConfigGuestDisk() + info.source_type = 'file' + info.source_device = device_type + info.driver_format = 'raw' + info.source_path = self.path + return info + + return FakeImage(instance, name, suffix) diff --git a/nova/tests/fake_libvirt_utils.py b/nova/tests/fake_libvirt_utils.py index f1d1cb73e71..35b3b683c9e 100644 --- a/nova/tests/fake_libvirt_utils.py +++ b/nova/tests/fake_libvirt_utils.py @@ -50,6 +50,22 @@ def mkfs(fs, path): pass +def resize2fs(path): + pass + + +def create_lvm_image(vg, lv, size, sparse=False): + pass + + +def volume_group_free_space(vg): + pass + + +def remove_logical_volumes(*paths): + pass + + def ensure_tree(path): pass diff --git a/nova/tests/test_imagebackend.py b/nova/tests/test_imagebackend.py new file mode 100644 index 00000000000..ec925e4f2ce --- /dev/null +++ b/nova/tests/test_imagebackend.py @@ -0,0 +1,392 @@ +#!/usr/bin/python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Grid Dynamics +# 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 os + +from nova import flags +from nova import test +from nova.tests import fake_libvirt_utils +from nova.virt.libvirt import imagebackend + +FLAGS = flags.FLAGS + + +class _ImageTestCase(test.TestCase): + INSTANCES_PATH = '/fake' + + def mock_create_image(self, image): + def create_image(fn, base, size, *args, **kwargs): + fn(target=base, *args, **kwargs) + image.create_image = create_image + + def setUp(self): + super(_ImageTestCase, self).setUp() + self.flags(instances_path=self.INSTANCES_PATH) + self.INSTANCE = 'instance' + self.NAME = 'fake' + self.SUFFIX = 'vm' + self.TEMPLATE = 'template' + + self.PATH = os.path.join(FLAGS.instances_path, self.INSTANCE, + self.NAME + self.SUFFIX) + self.TEMPLATE_DIR = os.path.join(FLAGS.instances_path, + '_base') + self.TEMPLATE_PATH = os.path.join(self.TEMPLATE_DIR, 'template') + + imagebackend.libvirt_utils = fake_libvirt_utils + + def test_cache(self): + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.PATH).AndReturn(False) + os.path.exists(self.TEMPLATE_DIR).AndReturn(False) + os.path.exists(self.TEMPLATE_PATH).AndReturn(False) + fn = self.mox.CreateMockAnything() + fn(target=self.TEMPLATE_PATH) + self.mox.StubOutWithMock(imagebackend.libvirt_utils, 'ensure_tree') + imagebackend.libvirt_utils.ensure_tree(self.TEMPLATE_DIR) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + self.mock_create_image(image) + image.cache(fn, self.TEMPLATE) + + self.mox.VerifyAll() + + def test_cache_image_exists(self): + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.PATH).AndReturn(True) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.cache(None, self.TEMPLATE) + + self.mox.VerifyAll() + + def test_cache_base_dir_exists(self): + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.PATH).AndReturn(False) + os.path.exists(self.TEMPLATE_DIR).AndReturn(True) + os.path.exists(self.TEMPLATE_PATH).AndReturn(False) + fn = self.mox.CreateMockAnything() + fn(target=self.TEMPLATE_PATH) + self.mox.StubOutWithMock(imagebackend.libvirt_utils, 'ensure_tree') + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + self.mock_create_image(image) + image.cache(fn, self.TEMPLATE) + + self.mox.VerifyAll() + + def test_cache_template_exists(self): + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.PATH).AndReturn(False) + os.path.exists(self.TEMPLATE_DIR).AndReturn(True) + os.path.exists(self.TEMPLATE_PATH).AndReturn(True) + fn = self.mox.CreateMockAnything() + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + self.mock_create_image(image) + image.cache(fn, self.TEMPLATE) + + self.mox.VerifyAll() + + +class RawTestCase(_ImageTestCase): + + SIZE = 1024 + + def setUp(self): + self.image_class = imagebackend.Raw + super(RawTestCase, self).setUp() + + def prepare_mocks(self): + fn = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(imagebackend.utils.synchronized, '__call__') + self.mox.StubOutWithMock(imagebackend.libvirt_utils, 'copy_image') + self.mox.StubOutWithMock(imagebackend.disk, 'extend') + return fn + + def test_create_image(self): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH, image_id=None) + imagebackend.libvirt_utils.copy_image(self.TEMPLATE_PATH, self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, None, image_id=None) + + self.mox.VerifyAll() + + def test_create_image_generated(self): + fn = self.prepare_mocks() + fn(target=self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, None) + + self.mox.VerifyAll() + + def test_create_image_extend(self): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH, image_id=None) + imagebackend.libvirt_utils.copy_image(self.TEMPLATE_PATH, self.PATH) + imagebackend.disk.extend(self.PATH, self.SIZE) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, self.SIZE, image_id=None) + + self.mox.VerifyAll() + + +class Qcow2TestCase(_ImageTestCase): + SIZE = 1024 * 1024 * 1024 + + def setUp(self): + self.image_class = imagebackend.Qcow2 + super(Qcow2TestCase, self).setUp() + self.QCOW2_BASE = (self.TEMPLATE_PATH + + '_%d' % (self.SIZE / (1024 * 1024 * 1024))) + + def prepare_mocks(self): + fn = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(imagebackend.utils.synchronized, '__call__') + self.mox.StubOutWithMock(imagebackend.libvirt_utils, + 'create_cow_image') + self.mox.StubOutWithMock(imagebackend.libvirt_utils, 'copy_image') + self.mox.StubOutWithMock(imagebackend.disk, 'extend') + return fn + + def test_create_image(self): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH) + imagebackend.libvirt_utils.create_cow_image(self.TEMPLATE_PATH, + self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, None) + + self.mox.VerifyAll() + + def test_create_image_with_size(self): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH) + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.QCOW2_BASE).AndReturn(False) + imagebackend.libvirt_utils.copy_image(self.TEMPLATE_PATH, + self.QCOW2_BASE) + imagebackend.disk.extend(self.QCOW2_BASE, self.SIZE) + imagebackend.libvirt_utils.create_cow_image(self.QCOW2_BASE, + self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, self.SIZE) + + self.mox.VerifyAll() + + def test_create_image_with_size_template_exists(self): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH) + self.mox.StubOutWithMock(os.path, 'exists') + os.path.exists(self.QCOW2_BASE).AndReturn(True) + imagebackend.libvirt_utils.create_cow_image(self.QCOW2_BASE, + self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, self.SIZE) + + self.mox.VerifyAll() + + +class LvmTestCase(_ImageTestCase): + VG = 'FakeVG' + TEMPLATE_SIZE = 512 + SIZE = 1024 + + def setUp(self): + self.image_class = imagebackend.Lvm + super(LvmTestCase, self).setUp() + self.flags(libvirt_images_volume_group=self.VG) + self.LV = '%s_%s' % (self.INSTANCE, self.NAME + self.SUFFIX) + self.PATH = os.path.join('/dev', self.VG, self.LV) + + self.disk = imagebackend.disk + self.utils = imagebackend.utils + self.libvirt_utils = imagebackend.libvirt_utils + + def prepare_mocks(self): + fn = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(self.disk, 'get_image_virtual_size') + self.mox.StubOutWithMock(self.disk, 'resize2fs') + self.mox.StubOutWithMock(self.libvirt_utils, 'create_lvm_image') + self.mox.StubOutWithMock(self.utils, 'execute') + return fn + + def _create_image(self, sparse): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH) + self.disk.get_image_virtual_size(self.TEMPLATE_PATH + ).AndReturn(self.TEMPLATE_SIZE) + self.libvirt_utils.create_lvm_image(self.VG, + self.LV, + self.TEMPLATE_SIZE, + sparse=sparse) + cmd = ('dd', 'if=%s' % self.TEMPLATE_PATH, + 'of=%s' % self.PATH, 'bs=4M') + self.utils.execute(*cmd, run_as_root=True) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, None) + + self.mox.VerifyAll() + + def _create_image_generated(self, sparse): + fn = self.prepare_mocks() + self.libvirt_utils.create_lvm_image(self.VG, self.LV, + self.SIZE, sparse=sparse) + fn(target=self.PATH, ephemeral_size=None) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, + self.SIZE, ephemeral_size=None) + + self.mox.VerifyAll() + + def _create_image_resize(self, sparse): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH) + self.disk.get_image_virtual_size(self.TEMPLATE_PATH + ).AndReturn(self.TEMPLATE_SIZE) + self.libvirt_utils.create_lvm_image(self.VG, self.LV, + self.SIZE, sparse=sparse) + cmd = ('dd', 'if=%s' % self.TEMPLATE_PATH, + 'of=%s' % self.PATH, 'bs=4M') + self.utils.execute(*cmd, run_as_root=True) + self.disk.resize2fs(self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + image.create_image(fn, self.TEMPLATE_PATH, self.SIZE) + + self.mox.VerifyAll() + + def test_create_image(self): + self._create_image(False) + + def test_create_image_sparsed(self): + self.flags(libvirt_sparse_logical_volumes=True) + self._create_image(True) + + def test_create_image_generated(self): + self._create_image_generated(False) + + def test_create_image_generated_sparsed(self): + self.flags(libvirt_sparse_logical_volumes=True) + self._create_image_generated(True) + + def test_create_image_resize(self): + self._create_image_resize(False) + + def test_create_image_resize_sparsed(self): + self.flags(libvirt_sparse_logical_volumes=True) + self._create_image_resize(True) + + def test_create_image_negative(self): + fn = self.prepare_mocks() + fn(target=self.TEMPLATE_PATH) + self.disk.get_image_virtual_size(self.TEMPLATE_PATH + ).AndReturn(self.TEMPLATE_SIZE) + self.libvirt_utils.create_lvm_image(self.VG, + self.LV, + self.SIZE, + sparse=False + ).AndRaise(RuntimeError()) + self.mox.StubOutWithMock(self.libvirt_utils, 'remove_logical_volumes') + self.libvirt_utils.remove_logical_volumes(self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + + self.assertRaises(RuntimeError, image.create_image, fn, + self.TEMPLATE_PATH, self.SIZE) + self.mox.VerifyAll() + + def test_create_image_generated_negative(self): + fn = self.prepare_mocks() + fn(target=self.PATH, + ephemeral_size=None).AndRaise(RuntimeError()) + self.libvirt_utils.create_lvm_image(self.VG, + self.LV, + self.SIZE, + sparse=False) + self.mox.StubOutWithMock(self.libvirt_utils, 'remove_logical_volumes') + self.libvirt_utils.remove_logical_volumes(self.PATH) + self.mox.ReplayAll() + + image = self.image_class(self.INSTANCE, self.NAME, self.SUFFIX) + + self.assertRaises(RuntimeError, image.create_image, fn, + self.TEMPLATE_PATH, self.SIZE, + ephemeral_size=None) + self.mox.VerifyAll() + + +class BackendTestCase(test.TestCase): + INSTANCE = 'fake-instance' + NAME = 'fake-name' + SUFFIX = 'suffix' + + def get_image(self, use_cow, image_type): + return imagebackend.Backend(use_cow).image(self.INSTANCE, + self.NAME, + self.SUFFIX, + image_type) + + def _test_image(self, image_type, image_not_cow, image_cow): + image1 = self.get_image(False, image_type) + image2 = self.get_image(True, image_type) + + def assertIsInstance(instance, class_object): + failure = ('Expected %s,' + + ' but got %s.') % (class_object.__name__, + instance.__class__.__name__) + self.assertTrue(isinstance(instance, class_object), failure) + + assertIsInstance(image1, image_not_cow) + assertIsInstance(image2, image_cow) + + def test_image_raw(self): + self._test_image('raw', imagebackend.Raw, imagebackend.Raw) + + def test_image_qcow2(self): + self._test_image('qcow2', imagebackend.Qcow2, imagebackend.Qcow2) + + def test_image_lvm(self): + self.flags(libvirt_images_volume_group='FakeVG') + self._test_image('lvm', imagebackend.Lvm, imagebackend.Lvm) + + def test_image_default(self): + self._test_image('default', imagebackend.Raw, imagebackend.Qcow2) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index a0ef0060546..c77a2a79738 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -47,6 +47,7 @@ from nova.virt.libvirt import config from nova.virt.libvirt import connection from nova.virt.libvirt import firewall +from nova.virt.libvirt import imagebackend from nova.virt.libvirt import utils as libvirt_utils from nova.virt.libvirt import volume from nova.volume import driver as volume_driver @@ -342,24 +343,24 @@ def fake_extend(image, size): self.stubs.Set(os.path, 'exists', fake_exists) self.stubs.Set(utils, 'execute', fake_execute) - self.stubs.Set(connection.disk, 'extend', fake_extend) - connection.libvirt_utils = fake_libvirt_utils + self.stubs.Set(imagebackend.disk, 'extend', fake_extend) + imagebackend.libvirt_utils = fake_libvirt_utils def tearDown(self): - connection.libvirt_utils = libvirt_utils + imagebackend.libvirt_utils = libvirt_utils super(CacheConcurrencyTestCase, self).tearDown() def test_same_fname_concurrency(self): """Ensures that the same fname cache runs at a sequentially""" - conn = connection.LibvirtDriver + backend = imagebackend.Backend(False) wait1 = eventlet.event.Event() done1 = eventlet.event.Event() - eventlet.spawn(conn._cache_image, _concurrency, - 'target', 'fname', False, None, wait1, done1) + eventlet.spawn(backend.image('instance', 'name').cache, + _concurrency, 'fname', None, wait=wait1, done=done1) wait2 = eventlet.event.Event() done2 = eventlet.event.Event() - eventlet.spawn(conn._cache_image, _concurrency, - 'target', 'fname', False, None, wait2, done2) + eventlet.spawn(backend.image('instance', 'name').cache, + _concurrency, 'fname', None, wait=wait2, done=done2) wait2.send() eventlet.sleep(0) try: @@ -372,15 +373,15 @@ def test_same_fname_concurrency(self): def test_different_fname_concurrency(self): """Ensures that two different fname caches are concurrent""" - conn = connection.LibvirtDriver + backend = imagebackend.Backend(False) wait1 = eventlet.event.Event() done1 = eventlet.event.Event() - eventlet.spawn(conn._cache_image, _concurrency, - 'target', 'fname2', False, None, wait1, done1) + eventlet.spawn(backend.image('instance', 'name').cache, + _concurrency, 'fname2', None, wait=wait1, done=done1) wait2 = eventlet.event.Event() done2 = eventlet.event.Event() - eventlet.spawn(conn._cache_image, _concurrency, - 'target', 'fname1', False, None, wait2, done2) + eventlet.spawn(backend.image('instance', 'name').cache, + _concurrency, 'fname1', None, wait=wait2, done=done2) wait2.send() eventlet.sleep(0) try: diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index f5ecd344050..3457a77e4ae 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -63,6 +63,7 @@ def _setup_fakelibvirt(self): else: self.saved_libvirt = None + import fake_imagebackend import fake_libvirt_utils import fakelibvirt @@ -70,6 +71,7 @@ def _setup_fakelibvirt(self): import nova.virt.libvirt.connection import nova.virt.libvirt.firewall + nova.virt.libvirt.connection.imagebackend = fake_imagebackend nova.virt.libvirt.connection.libvirt = fakelibvirt nova.virt.libvirt.connection.libvirt_utils = fake_libvirt_utils nova.virt.libvirt.firewall.libvirt = fakelibvirt diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py index 9e2e9ffa177..672b5d50ed8 100644 --- a/nova/virt/disk/api.py +++ b/nova/virt/disk/api.py @@ -108,6 +108,11 @@ def get_image_virtual_size(image): return int(m.group(2)) +def resize2fs(image, check_exit_code=False): + utils.execute('e2fsck', '-fp', image, check_exit_code=check_exit_code) + utils.execute('resize2fs', image, check_exit_code=check_exit_code) + + def extend(image, size): """Increase image to size""" # NOTE(MotoKen): check image virtual size before resize @@ -116,8 +121,7 @@ def extend(image, size): return utils.execute('qemu-img', 'resize', image, size) # NOTE(vish): attempts to resize filesystem - utils.execute('e2fsck', '-fp', image, check_exit_code=False) - utils.execute('resize2fs', image, check_exit_code=False) + resize2fs(image) def bind(src, target, instance_name): diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index fe117159bf7..d890cffb20d 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -71,6 +71,7 @@ from nova.virt import driver from nova.virt.libvirt import config from nova.virt.libvirt import firewall +from nova.virt.libvirt import imagebackend from nova.virt.libvirt import imagecache from nova.virt.libvirt import utils as libvirt_utils @@ -271,6 +272,7 @@ def __init__(self, read_only=False): self._disk_cachemode = None self.image_cache_manager = imagecache.ImageCacheManager() + self.image_backend = imagebackend.Backend(FLAGS.use_cow_images) @property def disk_cachemode(self): @@ -506,6 +508,34 @@ def _cleanup(self, instance, network_info, block_device_info): if os.path.exists(target): shutil.rmtree(target) + #NOTE(bfilippov): destroy all LVM disks for this instance + self._cleanup_lvm(instance) + + def _cleanup_lvm(self, instance): + """Delete all LVM disks for given instance object""" + disks = self._lvm_disks(instance) + if disks: + libvirt_utils.remove_logical_volumes(*disks) + + def _lvm_disks(self, instance): + """Returns all LVM disks for given instance object""" + if FLAGS.libvirt_images_volume_group: + vg = os.path.join('/dev', FLAGS.libvirt_images_volume_group) + if not os.path.exists(vg): + return [] + pattern = '%s_' % instance['name'] + + def belongs_to_instance(disk): + return disk.startswith(pattern) + + def fullpath(name): + return os.path.join(vg, name) + + disk_names = filter(belongs_to_instance, os.listdir(vg)) + disks = map(fullpath, disk_names) + return disks + return [] + def get_volume_connector(self, instance): if not self._initiator: self._initiator = libvirt_utils.get_iscsi_initiator() @@ -681,7 +711,10 @@ def snapshot(self, context, instance, image_href): # NOTE(vish): assume amis are raw source_format = 'raw' image_format = FLAGS.snapshot_image_format or source_format - if FLAGS.use_cow_images: + use_qcow2 = ((FLAGS.libvirt_images_type == 'default' and + FLAGS.use_cow_images) or + FLAGS.libvirt_images_type == 'qcow2') + if use_qcow2: source_format = 'qcow2' # NOTE(vish): glance forces ami disk format to be ami if base.get('disk_format') == 'ami': @@ -957,8 +990,8 @@ def _append_to_file(self, data, fpath): return fpath def _inject_files(self, instance, files, partition): - disk_path = os.path.join(FLAGS.instances_path, - instance['name'], 'disk') + disk_path = self.image_backend.image(instance['name'], + 'disk').path disk.inject_files(disk_path, files, partition=partition, use_cow=FLAGS.use_cow_images) @@ -1070,72 +1103,6 @@ def _supports_direct_io(dirpath): return hasDirectIO - @staticmethod - def _cache_image(fn, target, fname, cow=False, size=None, *args, **kwargs): - """Wrapper for a method that creates an image that caches the image. - - This wrapper will save the image into a common store and create a - copy for use by the hypervisor. - - The underlying method should specify a kwarg of target representing - where the image will be saved. - - fname is used as the filename of the base image. The filename needs - to be unique to a given image. - - If cow is True, it will make a CoW image instead of a copy. - - If size is specified, we attempt to resize up to that size. - """ - - # NOTE(mikal): Checksums aren't created here, even if the image cache - # manager is enabled, as that would slow down VM startup. If both - # cache management and checksumming are enabled, then the checksum - # will be created on the first pass of the image cache manager. - - generating = 'image_id' not in kwargs - if not os.path.exists(target): - base_dir = os.path.join(FLAGS.instances_path, FLAGS.base_dir_name) - - if not os.path.exists(base_dir): - libvirt_utils.ensure_tree(base_dir) - base = os.path.join(base_dir, fname) - - @utils.synchronized(fname) - def call_if_not_exists(base, fn, *args, **kwargs): - if not os.path.exists(base): - with utils.remove_path_on_error(base): - fn(target=base, *args, **kwargs) - - if cow or not generating: - call_if_not_exists(base, fn, *args, **kwargs) - elif generating: - # For raw it's quicker to just generate outside the cache - call_if_not_exists(target, fn, *args, **kwargs) - - @utils.synchronized(base) - def copy_and_extend(cow, generating, base, target, size): - if cow: - cow_base = base - if size: - size_gb = size / (1024 * 1024 * 1024) - cow_base += "_%d" % size_gb - if not os.path.exists(cow_base): - with utils.remove_path_on_error(cow_base): - libvirt_utils.copy_image(base, cow_base) - disk.extend(cow_base, size) - libvirt_utils.create_cow_image(cow_base, target) - elif not generating: - libvirt_utils.copy_image(base, target) - # Resize after the copy, as it's usually much faster - # to make sparse updates, rather than potentially - # naively copying the whole image file. - if size: - disk.extend(target, size) - - with utils.remove_path_on_error(target): - copy_and_extend(cow, generating, base, target, size) - @staticmethod def _create_local(target, local_size, unit='G', fs_format=None, label=None): @@ -1181,6 +1148,13 @@ def basepath(fname='', suffix=suffix): instance['name'], fname + suffix) + def image(fname, image_type=FLAGS.libvirt_images_type): + return self.image_backend.image(instance['name'], + fname, suffix, image_type) + + def raw(fname): + return image(fname, image_type='raw') + # ensure directories exist and are writable libvirt_utils.ensure_tree(basepath(suffix='')) @@ -1204,22 +1178,20 @@ def basepath(fname='', suffix=suffix): if disk_images['kernel_id']: fname = disk_images['kernel_id'] - self._cache_image(fn=libvirt_utils.fetch_image, - context=context, - target=basepath('kernel'), - fname=fname, - image_id=disk_images['kernel_id'], - user_id=instance['user_id'], - project_id=instance['project_id']) + raw('kernel').cache(fn=libvirt_utils.fetch_image, + context=context, + fname=fname, + image_id=disk_images['kernel_id'], + user_id=instance['user_id'], + project_id=instance['project_id']) if disk_images['ramdisk_id']: fname = disk_images['ramdisk_id'] - self._cache_image(fn=libvirt_utils.fetch_image, - context=context, - target=basepath('ramdisk'), - fname=fname, - image_id=disk_images['ramdisk_id'], - user_id=instance['user_id'], - project_id=instance['project_id']) + raw('ramdisk').cache(fn=libvirt_utils.fetch_image, + context=context, + fname=fname, + image_id=disk_images['ramdisk_id'], + user_id=instance['user_id'], + project_id=instance['project_id']) root_fname = hashlib.sha1(str(disk_images['image_id'])).hexdigest() size = instance['root_gb'] * 1024 * 1024 * 1024 @@ -1231,15 +1203,13 @@ def basepath(fname='', suffix=suffix): if not self._volume_in_mapping(self.default_root_device, block_device_info): - self._cache_image(fn=libvirt_utils.fetch_image, - context=context, - target=basepath('disk'), - fname=root_fname, - cow=FLAGS.use_cow_images, - image_id=disk_images['image_id'], - user_id=instance['user_id'], - project_id=instance['project_id'], - size=size) + image('disk').cache(fn=libvirt_utils.fetch_image, + context=context, + fname=root_fname, + size=size, + image_id=disk_images['image_id'], + user_id=instance['user_id'], + project_id=instance['project_id']) ephemeral_gb = instance['ephemeral_gb'] if ephemeral_gb and not self._volume_in_mapping( @@ -1248,12 +1218,14 @@ def basepath(fname='', suffix=suffix): fn = functools.partial(self._create_ephemeral, fs_label='ephemeral0', os_type=instance.os_type) - self._cache_image(fn=fn, - target=basepath('disk.local'), - fname="ephemeral_%s_%s_%s" % - ("0", ephemeral_gb, instance.os_type), - cow=FLAGS.use_cow_images, - ephemeral_size=ephemeral_gb) + fname = "ephemeral_%s_%s_%s" % ("0", + ephemeral_gb, + instance.os_type) + size = ephemeral_gb * 1024 * 1024 * 1024 + image('disk.local').cache(fn=fn, + fname=fname, + size=size, + ephemeral_size=ephemeral_gb) else: swap_device = self.default_second_device @@ -1261,12 +1233,14 @@ def basepath(fname='', suffix=suffix): fn = functools.partial(self._create_ephemeral, fs_label='ephemeral%d' % eph['num'], os_type=instance.os_type) - self._cache_image(fn=fn, - target=basepath(_get_eph_disk(eph)), - fname="ephemeral_%s_%s_%s" % - (eph['num'], eph['size'], instance.os_type), - cow=FLAGS.use_cow_images, - ephemeral_size=eph['size']) + size = eph['size'] * 1024 * 1024 * 1024 + fname = "ephemeral_%s_%s_%s" % (eph['num'], + eph['size'], + instance.os_type) + image(_get_eph_disk(eph)).cache(fn=fn, + fname=fname, + size=size, + ephemeral_size=eph['size']) swap_mb = 0 @@ -1278,11 +1252,11 @@ def basepath(fname='', suffix=suffix): swap_mb = inst_type['swap'] if swap_mb > 0: - self._cache_image(fn=self._create_swap, - target=basepath('disk.swap'), - fname="swap_%s" % swap_mb, - cow=FLAGS.use_cow_images, - swap_mb=swap_mb) + size = swap_mb * 1024 * 1024 + image('disk.swap').cache(fn=self._create_swap, + fname="swap_%s" % swap_mb, + size=size, + swap_mb=swap_mb) target_partition = None if not instance['kernel_id']: @@ -1297,12 +1271,11 @@ def basepath(fname='', suffix=suffix): if config_drive_id: fname = config_drive_id - self._cache_image(fn=libvirt_utils.fetch_image, - target=basepath('disk.config'), - fname=fname, - image_id=config_drive_id, - user_id=instance['user_id'], - project_id=instance['project_id'],) + raw('disk.config').cache(fn=libvirt_utils.fetch_image, + fname=fname, + image_id=config_drive_id, + user_id=instance['user_id'], + project_id=instance['project_id']) elif config_drive: label = 'config' with utils.remove_path_on_error(basepath('disk.config')): @@ -1360,10 +1333,10 @@ def basepath(fname='', suffix=suffix): if any((key, net, metadata, admin_password)): if config_drive: # Should be True or None by now. - injection_path = basepath('disk.config') + injection_path = raw('disk.config').path img_id = 'config-drive' else: - injection_path = basepath('disk') + injection_path = image('disk').path img_id = instance.image_ref for injection in ('metadata', 'key', 'net', 'admin_password'): @@ -1511,16 +1484,16 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, "rootfs") guest.add_device(fs) else: - if FLAGS.use_cow_images: - driver_type = 'qcow2' - else: - driver_type = 'raw' - if image_meta and image_meta.get('disk_format') == 'iso': root_device_type = 'cdrom' else: root_device_type = 'disk' + def disk_info(name, suffix=''): + image = self.image_backend.image(instance['name'], + name, suffix) + return image.libvirt_info(root_device_type) + if FLAGS.libvirt_type == "uml": ephemeral_disk_bus = "uml" elif FLAGS.libvirt_type == "xen": @@ -1529,23 +1502,13 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, ephemeral_disk_bus = "virtio" if rescue: - diskrescue = config.LibvirtConfigGuestDisk() - diskrescue.source_type = "file" - diskrescue.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - "disk.rescue") - diskrescue.driver_format = driver_type + diskrescue = disk_info('disk', '.rescue') diskrescue.driver_cache = self.disk_cachemode diskrescue.target_dev = self.default_root_device diskrescue.target_bus = ephemeral_disk_bus guest.add_device(diskrescue) - diskos = config.LibvirtConfigGuestDisk() - diskos.source_type = "file" - diskos.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - "disk") - diskos.driver_format = driver_type + diskos = disk_info('disk') diskos.driver_cache = self.disk_cachemode diskos.target_dev = self.default_second_device diskos.target_bus = ephemeral_disk_bus @@ -1555,14 +1518,8 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, block_device_info) if not ebs_root: - diskos = config.LibvirtConfigGuestDisk() - diskos.source_type = "file" - diskos.source_device = root_device_type - diskos.driver_format = driver_type + diskos = disk_info('disk') diskos.driver_cache = self.disk_cachemode - diskos.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - "disk") diskos.target_dev = root_device if root_device_type == "cdrom": diskos.target_bus = "ide" @@ -1580,14 +1537,8 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, ephemeral_device = self.default_second_device if ephemeral_device is not None: - disklocal = config.LibvirtConfigGuestDisk() - disklocal.source_type = "file" - disklocal.source_device = root_device_type - disklocal.driver_format = driver_type + disklocal = disk_info('disk.local') disklocal.driver_cache = self.disk_cachemode - disklocal.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - "disk.local") disklocal.target_dev = ephemeral_device disklocal.target_bus = ephemeral_disk_bus guest.add_device(disklocal) @@ -1603,14 +1554,8 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, for eph in driver.block_device_info_get_ephemerals( block_device_info): - diskeph = config.LibvirtConfigGuestDisk() - diskeph.source_type = "block" - diskeph.source_device = root_device_type - diskeph.driver_format = driver_type + diskeph = disk_info(_get_eph_disk(eph)) diskeph.driver_cache = self.disk_cachemode - diskeph.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - _get_eph_disk(eph)) diskeph.target_dev = block_device.strip_dev( eph['device_name']) diskeph.target_bus = ephemeral_disk_bus @@ -1618,13 +1563,8 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, swap = driver.block_device_info_get_swap(block_device_info) if driver.swap_is_usable(swap): - diskswap = config.LibvirtConfigGuestDisk() - diskswap.disk_type = "file" - diskswap.driver_format = driver_type + diskswap = disk_info('disk.swap') diskswap.driver_cache = self.disk_cachemode - diskswap.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - "disk.swap") diskswap.target_dev = block_device.strip_dev( swap['device_name']) diskswap.target_bus = ephemeral_disk_bus @@ -1632,13 +1572,8 @@ def get_guest_config(self, instance, network_info, image_meta, rescue=None, elif (inst_type['swap'] > 0 and not self._volume_in_mapping(swap_device, block_device_info)): - diskswap = config.LibvirtConfigGuestDisk() - diskswap.disk_type = "file" - diskswap.driver_format = driver_type + diskswap = disk_info('disk.swap') diskswap.driver_cache = self.disk_cachemode - diskswap.source_path = os.path.join(FLAGS.instances_path, - instance['name'], - "disk.swap") diskswap.target_dev = swap_device diskswap.target_bus = ephemeral_disk_bus guest.add_device(diskswap) diff --git a/nova/virt/libvirt/imagebackend.py b/nova/virt/libvirt/imagebackend.py new file mode 100644 index 00000000000..d1a6afe6a09 --- /dev/null +++ b/nova/virt/libvirt/imagebackend.py @@ -0,0 +1,255 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Grid Dynamics +# 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 abc +import contextlib +import os + +from nova import flags +from nova.openstack.common import cfg +from nova.openstack.common import excutils +from nova import utils +from nova.virt.disk import api as disk +from nova.virt.libvirt import config +from nova.virt.libvirt import utils as libvirt_utils + +__imagebackend_opts = [ + cfg.StrOpt('libvirt_images_type', + default='default', + help='VM Images format. Acceptable values are: raw, qcow2, lvm,' + ' default. If default is specified,' + ' then use_cow_images flag is used instead of this one.'), + cfg.StrOpt('libvirt_images_volume_group', + default=None, + help='LVM Volume Group that is used for VM images, when you' + ' specify libvirt_images_type=lvm.'), + cfg.BoolOpt('libvirt_sparse_logical_volumes', + default=False, + help='Create sparse logical volumes (with virtualsize)' + ' if this flag is set to True.'), + ] + +FLAGS = flags.FLAGS +FLAGS.register_opts(__imagebackend_opts) + + +class Image(object): + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __init__(self, instance, name, suffix): + """Image initialization. + + :instance: Instance name. + :name: Image name. + :suffix: Suffix for image name. + """ + pass + + @abc.abstractmethod + def create_image(self, prepare_template, base, size, *args, **kwargs): + """Create image from template. + + Contains specific behavior for each image type. + + :prepare_template: function, that creates template. + Should accept `target` argument. + :base: Template name + :size: Size of created image in bytes + """ + pass + + @abc.abstractmethod + def libvirt_info(self, device_type): + """Get `LibvirtConfigGuestDisk` filled for this image. + + :device_type: Device type for this image. + """ + pass + + def cache(self, fn, fname, size=None, *args, **kwargs): + """Creates image from template. + + Ensures that template and image not already exists. + Ensures that base directory exists. + Synchronizes on template fetching. + + :fn: function, that creates template. + Should accept `target` argument. + :fname: Template name + :size: Size of created image in bytes (optional) + """ + @utils.synchronized(fname) + def call_if_not_exists(target, *args, **kwargs): + if not os.path.exists(target): + fn(target=target, *args, **kwargs) + + if not os.path.exists(self.path): + base_dir = os.path.join(FLAGS.instances_path, '_base') + if not os.path.exists(base_dir): + libvirt_utils.ensure_tree(base_dir) + base = os.path.join(base_dir, fname) + + self.create_image(call_if_not_exists, base, size, + *args, **kwargs) + + +class Raw(Image): + def __init__(self, instance, name, suffix): + if not suffix: + suffix = '' + self.path = os.path.join(FLAGS.instances_path, + instance, name + suffix) + + def libvirt_info(self, device_type): + info = config.LibvirtConfigGuestDisk() + info.source_type = 'file' + info.source_device = device_type + info.driver_format = 'raw' + info.source_path = self.path + return info + + def create_image(self, prepare_template, base, size, *args, **kwargs): + @utils.synchronized(base) + def copy_raw_image(base, target, size): + libvirt_utils.copy_image(base, target) + if size: + disk.extend(target, size) + + generating = 'image_id' not in kwargs + if generating: + #Generating image in place + prepare_template(target=self.path, *args, **kwargs) + else: + prepare_template(target=base, *args, **kwargs) + with utils.remove_path_on_error(self.path): + copy_raw_image(base, self.path, size) + + +class Qcow2(Raw): + def libvirt_info(self, device_type): + info = config.LibvirtConfigGuestDisk() + info.source_type = 'file' + info.source_device = device_type + info.driver_format = 'qcow2' + info.source_path = self.path + return info + + def create_image(self, prepare_template, base, size, *args, **kwargs): + @utils.synchronized(base) + def copy_qcow2_image(base, target, size): + qcow2_base = base + if size: + size_gb = size / (1024 * 1024 * 1024) + qcow2_base += '_%d' % size_gb + if not os.path.exists(qcow2_base): + with utils.remove_path_on_error(qcow2_base): + libvirt_utils.copy_image(base, qcow2_base) + disk.extend(qcow2_base, size) + libvirt_utils.create_cow_image(qcow2_base, target) + + prepare_template(target=base, *args, **kwargs) + with utils.remove_path_on_error(self.path): + copy_qcow2_image(base, self.path, size) + + +class Lvm(Image): + @staticmethod + def escape(fname): + return fname.replace('_', '__') + + def libvirt_info(self, device_type): + info = config.LibvirtConfigGuestDisk() + info.source_type = 'block' + info.source_device = device_type + info.driver_format = 'raw' + info.source_path = self.path + return info + + def __init__(self, instance, name, suffix): + if not suffix: + suffix = '' + if not FLAGS.libvirt_images_volume_group: + raise RuntimeError(_('You should specify' + ' libvirt_images_volume_group' + ' flag to use LVM images.')) + self.vg = FLAGS.libvirt_images_volume_group + self.lv = '%s_%s' % (self.escape(instance), + self.escape(name + suffix)) + self.path = os.path.join('/dev', self.vg, self.lv) + self.sparse = FLAGS.libvirt_sparse_logical_volumes + + def create_image(self, prepare_template, base, size, *args, **kwargs): + @utils.synchronized(base) + def create_lvm_image(base, size): + base_size = disk.get_image_virtual_size(base) + resize = size > base_size + size = size if resize else base_size + libvirt_utils.create_lvm_image(self.vg, self.lv, + size, sparse=self.sparse) + cmd = ('dd', 'if=%s' % base, 'of=%s' % self.path, 'bs=4M') + utils.execute(*cmd, run_as_root=True) + if resize: + disk.resize2fs(self.path) + + generated = 'ephemeral_size' in kwargs + + #Generate images with specified size right on volume + if generated and size: + libvirt_utils.create_lvm_image(self.vg, self.lv, + size, sparse=self.sparse) + with self.remove_volume_on_error(self.path): + prepare_template(target=self.path, *args, **kwargs) + else: + prepare_template(target=base, *args, **kwargs) + with self.remove_volume_on_error(self.path): + create_lvm_image(base, size) + + @contextlib.contextmanager + def remove_volume_on_error(self, path): + try: + yield + except Exception: + with excutils.save_and_reraise_exception(): + libvirt_utils.remove_logical_volumes(path) + + +class Backend(object): + def __init__(self, use_cow): + self.BACKEND = { + 'raw': Raw, + 'qcow2': Qcow2, + 'lvm': Lvm, + 'default': Qcow2 if use_cow else Raw + } + + def image(self, instance, name, + suffix=None, image_type=None): + """Constructs image for selected backend + + :instance: Instance name. + :name: Image name. + :suffix: Suffix for image name (optional). + :image_type: Image type. + Optional, is FLAGS.libvirt_images_type by default. + """ + if not image_type: + image_type = FLAGS.libvirt_images_type + image = self.BACKEND.get(image_type) + if not image: + raise RuntimeError(_('Unknown image_type=%s') % image_type) + return image(instance, name, suffix) diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index 735957b291a..363c692aa02 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -90,6 +90,61 @@ def create_cow_image(backing_file, path): 'backing_file=%s' % backing_file, path) +def create_lvm_image(vg, lv, size, sparse=False): + """Create LVM image. + + Creates a LVM image with given size. + + :param vg: existing volume group which should hold this image + :param lv: name for this image (logical volume) + :size: size of image in bytes + :sparse: create sparse logical volume + """ + free_space = volume_group_free_space(vg) + + def check_size(size): + if size > free_space: + raise RuntimeError(_('Insufficient Space on Volume Group %(vg)s.' + ' Only %(free_space)db available,' + ' but %(size)db required' + ' by volume %(lv)s.') % locals()) + + if sparse: + preallocated_space = 64 * 1024 * 1024 + check_size(preallocated_space) + if free_space < size: + LOG.warning(_('Volume group %(vg)s will not be able' + ' to hold sparse volume %(lv)s.' + ' Virtual volume size is %(size)db,' + ' but free space on volume group is' + ' only %(free_space)db.') % locals()) + + cmd = ('lvcreate', '-L', '%db' % preallocated_space, + '--virtualsize', '%db' % size, '-n', lv, vg) + else: + check_size(size) + cmd = ('lvcreate', '-L', '%db' % size, '-n', lv, vg) + execute(*cmd, run_as_root=True, attempts=3) + + +def volume_group_free_space(vg): + """Return available space on volume group in bytes. + + :param vg: volume group name + """ + out, err = execute('vgs', '--noheadings', '--nosuffix', + '--units', 'b', '-o', 'vg_free', vg, + run_as_root=True) + return int(out.strip()) + + +def remove_logical_volumes(*paths): + """Remove one or more logical volume.""" + if paths: + lvremove = ('lvremove', '-f') + paths + execute(*lvremove, attempts=3, run_as_root=True) + + def get_disk_size(path): """Get the (virtual) size of a disk image