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
This commit is contained in:
Boris Filippov
2012-05-16 15:17:53 +04:00
parent b13c27d634
commit 1fe1f66428
7 changed files with 482 additions and 13 deletions

View File

@@ -23,6 +23,7 @@ Asbjørn Sannes <asbjorn.sannes@interhost.no>
Ben McGraw <ben@pistoncloud.com> Ben McGraw <ben@pistoncloud.com>
Ben Swartzlander <bswartz@netapp.com> Ben Swartzlander <bswartz@netapp.com>
Bilal Akhtar <bilalakhtar@ubuntu.com> Bilal Akhtar <bilalakhtar@ubuntu.com>
Boris Filippov <bfilippov@griddynamics.com>
Brad Hall <brad@nicira.com> Brad Hall <brad@nicira.com>
Brad McConnell <bmcconne@rackspace.com> Brad McConnell <bmcconne@rackspace.com>
Brendan Maguire <B_Maguire@Dell.com> Brendan Maguire <B_Maguire@Dell.com>

View File

@@ -188,4 +188,13 @@ filterlist = [
# nova/virt/libvirt/connection.py: # nova/virt/libvirt/connection.py:
filters.ReadFileFilter("/etc/iscsi/initiatorname.iscsi"), 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")
] ]

View File

@@ -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)

View File

@@ -50,6 +50,22 @@ def mkfs(fs, path):
pass 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): def ensure_tree(path):
pass pass

View File

@@ -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)

View File

@@ -47,6 +47,7 @@ from nova.virt import images
from nova.virt.libvirt import config from nova.virt.libvirt import config
from nova.virt.libvirt import connection from nova.virt.libvirt import connection
from nova.virt.libvirt import firewall 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 utils as libvirt_utils
from nova.virt.libvirt import volume from nova.virt.libvirt import volume
from nova.volume import driver as volume_driver from nova.volume import driver as volume_driver
@@ -342,24 +343,24 @@ class CacheConcurrencyTestCase(test.TestCase):
self.stubs.Set(os.path, 'exists', fake_exists) self.stubs.Set(os.path, 'exists', fake_exists)
self.stubs.Set(utils, 'execute', fake_execute) self.stubs.Set(utils, 'execute', fake_execute)
self.stubs.Set(connection.disk, 'extend', fake_extend) self.stubs.Set(imagebackend.disk, 'extend', fake_extend)
connection.libvirt_utils = fake_libvirt_utils imagebackend.libvirt_utils = fake_libvirt_utils
def tearDown(self): def tearDown(self):
connection.libvirt_utils = libvirt_utils imagebackend.libvirt_utils = libvirt_utils
super(CacheConcurrencyTestCase, self).tearDown() super(CacheConcurrencyTestCase, self).tearDown()
def test_same_fname_concurrency(self): def test_same_fname_concurrency(self):
"""Ensures that the same fname cache runs at a sequentially""" """Ensures that the same fname cache runs at a sequentially"""
conn = connection.LibvirtDriver backend = imagebackend.Backend(False)
wait1 = eventlet.event.Event() wait1 = eventlet.event.Event()
done1 = eventlet.event.Event() done1 = eventlet.event.Event()
eventlet.spawn(conn._cache_image, _concurrency, eventlet.spawn(backend.image('instance', 'name').cache,
'target', 'fname', False, None, wait1, done1) _concurrency, 'fname', None, wait=wait1, done=done1)
wait2 = eventlet.event.Event() wait2 = eventlet.event.Event()
done2 = eventlet.event.Event() done2 = eventlet.event.Event()
eventlet.spawn(conn._cache_image, _concurrency, eventlet.spawn(backend.image('instance', 'name').cache,
'target', 'fname', False, None, wait2, done2) _concurrency, 'fname', None, wait=wait2, done=done2)
wait2.send() wait2.send()
eventlet.sleep(0) eventlet.sleep(0)
try: try:
@@ -372,15 +373,15 @@ class CacheConcurrencyTestCase(test.TestCase):
def test_different_fname_concurrency(self): def test_different_fname_concurrency(self):
"""Ensures that two different fname caches are concurrent""" """Ensures that two different fname caches are concurrent"""
conn = connection.LibvirtDriver backend = imagebackend.Backend(False)
wait1 = eventlet.event.Event() wait1 = eventlet.event.Event()
done1 = eventlet.event.Event() done1 = eventlet.event.Event()
eventlet.spawn(conn._cache_image, _concurrency, eventlet.spawn(backend.image('instance', 'name').cache,
'target', 'fname2', False, None, wait1, done1) _concurrency, 'fname2', None, wait=wait1, done=done1)
wait2 = eventlet.event.Event() wait2 = eventlet.event.Event()
done2 = eventlet.event.Event() done2 = eventlet.event.Event()
eventlet.spawn(conn._cache_image, _concurrency, eventlet.spawn(backend.image('instance', 'name').cache,
'target', 'fname1', False, None, wait2, done2) _concurrency, 'fname1', None, wait=wait2, done=done2)
wait2.send() wait2.send()
eventlet.sleep(0) eventlet.sleep(0)
try: try:

View File

@@ -63,6 +63,7 @@ class _FakeDriverBackendTestCase(test.TestCase):
else: else:
self.saved_libvirt = None self.saved_libvirt = None
import fake_imagebackend
import fake_libvirt_utils import fake_libvirt_utils
import fakelibvirt import fakelibvirt
@@ -70,6 +71,7 @@ class _FakeDriverBackendTestCase(test.TestCase):
import nova.virt.libvirt.connection import nova.virt.libvirt.connection
import nova.virt.libvirt.firewall import nova.virt.libvirt.firewall
nova.virt.libvirt.connection.imagebackend = fake_imagebackend
nova.virt.libvirt.connection.libvirt = fakelibvirt nova.virt.libvirt.connection.libvirt = fakelibvirt
nova.virt.libvirt.connection.libvirt_utils = fake_libvirt_utils nova.virt.libvirt.connection.libvirt_utils = fake_libvirt_utils
nova.virt.libvirt.firewall.libvirt = fakelibvirt nova.virt.libvirt.firewall.libvirt = fakelibvirt