diff --git a/nova/tests/fake_imagebackend.py b/nova/tests/fake_imagebackend.py index b6a052f2a997..978c879fdf41 100644 --- a/nova/tests/fake_imagebackend.py +++ b/nova/tests/fake_imagebackend.py @@ -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) diff --git a/nova/tests/fake_libvirt_utils.py b/nova/tests/fake_libvirt_utils.py index 1862521c13c8..4f7c96d4f9fb 100644 --- a/nova/tests/fake_libvirt_utils.py +++ b/nova/tests/fake_libvirt_utils.py @@ -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 diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 7af877f2282f..7de72266bfc4 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -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() diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 50227ae6d54f..763a98e80142 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -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) diff --git a/nova/virt/libvirt/imagebackend.py b/nova/virt/libvirt/imagebackend.py index 0f2f044d7f39..040884e17a48 100644 --- a/nova/virt/libvirt/imagebackend.py +++ b/nova/virt/libvirt/imagebackend.py @@ -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) diff --git a/nova/virt/libvirt/snapshots.py b/nova/virt/libvirt/snapshots.py new file mode 100644 index 000000000000..37933876d3c4 --- /dev/null +++ b/nova/virt/libvirt/snapshots.py @@ -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")) diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index 2d1b5558c09a..17c9efddaf93 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -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: