Implement snapshots for raw backend

blueprint snapshots-for-everyone

Use simple qemu-img convert for raw snapshots.
Polymorphically select snapshot behavior for
raw and qcow2.
Allow images to be constructed from actual device/file path.

Change-Id: I6a57e43c6775c144c41a53382dcc7504ce6d4c43
This commit is contained in:
Boris Filippov 2012-09-26 04:55:03 +04:00 committed by Gerrit Code Review
parent 4f3930632c
commit 0df84e9bba
7 changed files with 199 additions and 28 deletions

View File

@ -37,6 +37,9 @@ class Backend(object):
def cache(self, fetch_func, filename, size=None, *args, **kwargs):
pass
def snapshot(self, name):
pass
def libvirt_info(self, disk_bus, disk_dev, device_type,
cache_mode):
info = config.LibvirtConfigGuestDisk()
@ -50,3 +53,8 @@ class Backend(object):
return info
return FakeImage(instance, name)
def snapshot(self, path, name, image_type=''):
#NOTE(bfilippov): this is done in favor for
# snapshot tests in test_libvirt.LibvirtConnTestCase
return imagebackend.Backend(True).snapshot(path, name, image_type)

View File

@ -104,7 +104,7 @@ def file_open(path, mode=None):
def find_disk(virt_dom):
return "some/path"
return "filename"
def load_file(path):
@ -115,6 +115,10 @@ def load_file(path):
return ''
def logical_volume_info(path):
return {}
def file_delete(path):
return True

View File

@ -53,6 +53,7 @@ from nova.virt.libvirt import config
from nova.virt.libvirt import driver as libvirt_driver
from nova.virt.libvirt import firewall
from nova.virt.libvirt import imagebackend
from nova.virt.libvirt import snapshots
from nova.virt.libvirt import utils as libvirt_utils
from nova.virt.libvirt import volume
from nova.virt.libvirt import volume_nfs
@ -484,6 +485,7 @@ class LibvirtConnTestCase(test.TestCase):
self.flags(libvirt_snapshots_directory='')
self.call_libvirt_dependant_setup = False
libvirt_driver.libvirt_utils = fake_libvirt_utils
snapshots.libvirt_utils = fake_libvirt_utils
def fake_extend(image, size):
pass
@ -532,7 +534,7 @@ class LibvirtConnTestCase(test.TestCase):
def fake_lookup(self, instance_name):
return FakeVirtDomain()
def fake_execute(self, *args):
def fake_execute(self, *args, **kwargs):
open(args[-1], "a").close()
def create_service(self, **kwargs):
@ -1129,6 +1131,7 @@ class LibvirtConnTestCase(test.TestCase):
libvirt_driver.LibvirtDriver._conn.lookupByName = self.fake_lookup
self.mox.StubOutWithMock(libvirt_driver.utils, 'execute')
libvirt_driver.utils.execute = self.fake_execute
libvirt_driver.libvirt_utils.disk_type = "qcow2"
self.mox.ReplayAll()
@ -1164,6 +1167,11 @@ class LibvirtConnTestCase(test.TestCase):
libvirt_driver.utils.execute = self.fake_execute
self.stubs.Set(libvirt_driver.libvirt_utils, 'disk_type', 'raw')
def convert_image(source, dest, out_format):
libvirt_driver.libvirt_utils.files[dest] = ''
images.convert_image = convert_image
self.mox.ReplayAll()
conn = libvirt_driver.LibvirtDriver(False)
@ -1197,6 +1205,7 @@ class LibvirtConnTestCase(test.TestCase):
libvirt_driver.LibvirtDriver._conn.lookupByName = self.fake_lookup
self.mox.StubOutWithMock(libvirt_driver.utils, 'execute')
libvirt_driver.utils.execute = self.fake_execute
libvirt_driver.libvirt_utils.disk_type = "qcow2"
self.mox.ReplayAll()

View File

@ -813,6 +813,10 @@ class LibvirtDriver(driver.ComputeDriver):
image_format = FLAGS.snapshot_image_format or source_format
# NOTE(bfilippov): save lvm as raw
if image_format == 'lvm':
image_format = 'raw'
# NOTE(vish): glance forces ami disk format to be ami
if base.get('disk_format') == 'ami':
metadata['disk_format'] = 'ami'
@ -828,8 +832,12 @@ class LibvirtDriver(driver.ComputeDriver):
if state == power_state.RUNNING:
virt_dom.managedSave(0)
# Make the snapshot
libvirt_utils.create_snapshot(disk_path, snapshot_name)
snapshot = self.image_backend.snapshot(disk_path, snapshot_name,
image_type=source_format)
snapshot.create()
# Export the snapshot to a raw image
snapshot_directory = FLAGS.libvirt_snapshots_directory
@ -837,11 +845,9 @@ class LibvirtDriver(driver.ComputeDriver):
with utils.tempdir(dir=snapshot_directory) as tmpdir:
try:
out_path = os.path.join(tmpdir, snapshot_name)
libvirt_utils.extract_snapshot(disk_path, source_format,
snapshot_name, out_path,
image_format)
snapshot.extract(out_path, image_format)
finally:
libvirt_utils.delete_snapshot(disk_path, snapshot_name)
snapshot.delete()
if state == power_state.RUNNING:
self._create_domain(domain=virt_dom)

View File

@ -25,6 +25,7 @@ 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 snapshots
from nova.virt.libvirt import utils as libvirt_utils
__imagebackend_opts = [
@ -125,13 +126,21 @@ class Image(object):
self.create_image(call_if_not_exists, base, size,
*args, **kwargs)
@abc.abstractmethod
def snapshot(self, name):
"""Create snapshot object for this image
:name: snapshot name
"""
pass
class Raw(Image):
def __init__(self, instance, name):
def __init__(self, instance=None, name=None, path=None):
super(Raw, self).__init__("file", "raw", is_block_dev=False)
self.path = os.path.join(FLAGS.instances_path,
instance, name)
self.path = path or os.path.join(FLAGS.instances_path,
instance, name)
def create_image(self, prepare_template, base, size, *args, **kwargs):
@utils.synchronized(base, external=True, lock_path=self.lock_path)
@ -149,13 +158,16 @@ class Raw(Image):
with utils.remove_path_on_error(self.path):
copy_raw_image(base, self.path, size)
def snapshot(self, name):
return snapshots.RawSnapshot(self.path, name)
class Qcow2(Image):
def __init__(self, instance, name):
def __init__(self, instance=None, name=None, path=None):
super(Qcow2, self).__init__("file", "qcow2", is_block_dev=False)
self.path = os.path.join(FLAGS.instances_path,
instance, name)
self.path = path or os.path.join(FLAGS.instances_path,
instance, name)
def create_image(self, prepare_template, base, size, *args, **kwargs):
@utils.synchronized(base, external=True, lock_path=self.lock_path)
@ -174,23 +186,33 @@ class Qcow2(Image):
with utils.remove_path_on_error(self.path):
copy_qcow2_image(base, self.path, size)
def snapshot(self, name):
return snapshots.Qcow2Snapshot(self.path, name)
class Lvm(Image):
@staticmethod
def escape(filename):
return filename.replace('_', '__')
def __init__(self, instance, name):
def __init__(self, instance=None, name=None, path=None):
super(Lvm, self).__init__("block", "raw", is_block_dev=True)
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))
self.path = os.path.join('/dev', self.vg, self.lv)
if path:
info = libvirt_utils.logical_volume_info(path)
self.vg = info['VG']
self.lv = info['LV']
self.path = path
else:
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))
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):
@ -227,6 +249,9 @@ class Lvm(Image):
with excutils.save_and_reraise_exception():
libvirt_utils.remove_logical_volumes(path)
def snapshot(self, name):
return snapshots.LvmSnapshot(self.path, name)
class Backend(object):
def __init__(self, use_cow):
@ -237,6 +262,14 @@ class Backend(object):
'default': Qcow2 if use_cow else Raw
}
def backend(self, image_type=None):
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
def image(self, instance, name, image_type=None):
"""Constructs image for selected backend
@ -245,9 +278,15 @@ class Backend(object):
: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)
backend = self.backend(image_type)
return backend(instance=instance, name=name)
def snapshot(self, path, snapshot_name, image_type=None):
"""Returns snapshot for given image
:path: path to image
:snapshot_name: snapshot name
:image_type: type of image
"""
backend = self.backend(image_type)
return backend(path=path).snapshot(snapshot_name)

View File

@ -0,0 +1,89 @@
# 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
from nova.virt import images
from nova.virt.libvirt import utils as libvirt_utils
class Snapshot(object):
@abc.abstractmethod
def create(self):
"""Create new snapshot"""
pass
@abc.abstractmethod
def extract(self, target, out_format):
"""Extract snapshot content to file
:target: path to extraction
:out_format: format of extraction (raw, qcow2, ...)
"""
pass
@abc.abstractmethod
def delete(self):
"""Delete snapshot"""
pass
class RawSnapshot(object):
def __init__(self, path, name):
self.path = path
self.name = name
def create(self):
pass
def extract(self, target, out_format):
images.convert_image(self.path, target, out_format)
def delete(self):
pass
class Qcow2Snapshot(object):
def __init__(self, path, name):
self.path = path
self.name = name
def create(self):
libvirt_utils.create_snapshot(self.path, self.name)
def extract(self, target, out_format):
libvirt_utils.extract_snapshot(self.path, 'qcow2',
self.name, target,
out_format)
def delete(self):
libvirt_utils.delete_snapshot(self.path, self.name)
class LvmSnapshot(object):
def __init__(self, path, name):
self.path = path
self.name = name
def create(self):
raise NotImplementedError(_("LVM snapshots not implemented"))
def extract(self, target, out_format):
raise NotImplementedError(_("LVM snapshots not implemented"))
def delete(self):
raise NotImplementedError(_("LVM snapshots not implemented"))

View File

@ -172,6 +172,22 @@ def list_logical_volumes(vg):
return [line.strip() for line in out.splitlines()]
def logical_volume_info(path):
"""Get logical volume info.
:param path: logical volume path
"""
out, err = execute('lvs', '-o', 'vg_all,lv_all',
'--separator', '|', path, run_as_root=True)
info = [line.split('|') for line in out.splitlines()]
if len(info) != 2:
raise RuntimeError(_("Path %s must be LVM logical volume") % path)
return dict(zip(*info))
def remove_logical_volumes(*paths):
"""Remove one or more logical volume."""
if paths: