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:
parent
4f3930632c
commit
0df84e9bba
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue