diff options
-rw-r--r-- | nova/virt/libvirt/driver.py | 145 | ||||
-rw-r--r-- | nova/virt/libvirt/utils.py | 28 |
2 files changed, 144 insertions, 29 deletions
diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index c865e4b3a..9ed7a054c 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -47,6 +47,7 @@ import os import shutil import sys import tempfile +import time import uuid from eventlet import greenthread @@ -254,6 +255,10 @@ MIN_LIBVIRT_VERSION = (0, 9, 6) # When the above version matches/exceeds this version # delete it & corresponding code using it MIN_LIBVIRT_HOST_CPU_VERSION = (0, 9, 10) +# Live snapshot requirements +REQ_HYPERVISOR_LIVESNAPSHOT = "QEMU" +MIN_LIBVIRT_LIVESNAPSHOT_VERSION = (1, 0, 0) +MIN_QEMU_LIVESNAPSHOT_VERSION = (1, 3, 0) def _get_eph_disk(ephemeral): @@ -325,16 +330,29 @@ class LibvirtDriver(driver.ComputeDriver): self._host_state = HostState(self.virtapi, self.read_only) return self._host_state - def has_min_version(self, ver): - libvirt_version = self._conn.getLibVersion() - + def has_min_version(self, lv_ver=None, hv_ver=None, hv_type=None): def _munge_version(ver): return ver[0] * 1000000 + ver[1] * 1000 + ver[2] - if libvirt_version < _munge_version(ver): - return False + try: + if lv_ver is not None: + libvirt_version = self._conn.getLibVersion() + if libvirt_version < _munge_version(lv_ver): + return False - return True + if hv_ver is not None: + hypervisor_version = self._conn.getVersion() + if hypervisor_version < _munge_version(hv_ver): + return False + + if hv_type is not None: + hypervisor_type = self._conn.getType() + if hypervisor_type != hv_type: + return False + + return True + except Exception: + return False def init_host(self, host): if not self.has_min_version(MIN_LIBVIRT_VERSION): @@ -806,35 +824,64 @@ class LibvirtDriver(driver.ComputeDriver): (state, _max_mem, _mem, _cpus, _t) = virt_dom.info() state = LIBVIRT_POWER_STATE[state] + # NOTE(rmk): Live snapshots require QEMU 1.3 and Libvirt 1.0.0. + # These restrictions can be relaxed as other configurations + # can be validated. + if self.has_min_version(MIN_LIBVIRT_LIVESNAPSHOT_VERSION, + MIN_QEMU_LIVESNAPSHOT_VERSION, + REQ_HYPERVISOR_LIVESNAPSHOT): + live_snapshot = True + else: + live_snapshot = False + + # NOTE(rmk): We cannot perform live snapshots when a managedSave + # file is present, so we will use the cold/legacy method + # for instances which are shutdown. + if state == power_state.SHUTDOWN: + live_snapshot = False + # NOTE(dkang): managedSave does not work for LXC - if CONF.libvirt_type != 'lxc': + if CONF.libvirt_type != 'lxc' and not live_snapshot: if state == power_state.RUNNING or state == power_state.PAUSED: virt_dom.managedSave(0) - # Make the snapshot - snapshot = self.image_backend.snapshot(disk_path, snapshot_name, - image_type=source_format) + if live_snapshot: + LOG.info(_("Beginning live snapshot process"), + instance=instance) + else: + LOG.info(_("Beginning cold snapshot process"), + instance=instance) + snapshot = self.image_backend.snapshot(disk_path, snapshot_name, + image_type=source_format) + snapshot.create() - snapshot.create() update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) - - # Export the snapshot to a raw image snapshot_directory = CONF.libvirt_snapshots_directory fileutils.ensure_tree(snapshot_directory) with utils.tempdir(dir=snapshot_directory) as tmpdir: try: out_path = os.path.join(tmpdir, snapshot_name) - snapshot.extract(out_path, image_format) + if live_snapshot: + # NOTE (rmk): libvirt needs to be able to write to the + # temp directory, which is owned nova. + utils.execute('chmod', '777', tmpdir, run_as_root=True) + self._live_snapshot(virt_dom, disk_path, out_path, + image_format) + else: + snapshot.extract(out_path, image_format) finally: - snapshot.delete() + if not live_snapshot: + snapshot.delete() # NOTE(dkang): because previous managedSave is not called # for LXC, _create_domain must not be called. - if CONF.libvirt_type != 'lxc': + if CONF.libvirt_type != 'lxc' and not live_snapshot: if state == power_state.RUNNING: self._create_domain(domain=virt_dom) elif state == power_state.PAUSED: self._create_domain(domain=virt_dom, launch_flags=libvirt.VIR_DOMAIN_START_PAUSED) + LOG.info(_("Snapshot extracted, beginning image upload"), + instance=instance) # Upload that image to the image service @@ -845,6 +892,72 @@ class LibvirtDriver(driver.ComputeDriver): image_href, metadata, image_file) + LOG.info(_("Snapshot image upload complete"), + instance=instance) + + def _live_snapshot(self, domain, disk_path, out_path, image_format): + """Snapshot an instance without downtime.""" + # Save a copy of the domain's running XML file + xml = domain.XMLDesc(0) + + # Abort is an idempotent operation, so make sure any block + # jobs which may have failed are ended. + try: + domain.blockJobAbort(disk_path, 0) + except Exception: + pass + + def _wait_for_block_job(domain, disk_path): + status = domain.blockJobInfo(disk_path, 0) + try: + cur = status.get('cur', 0) + end = status.get('end', 0) + except Exception: + return False + + if cur == end and cur != 0 and end != 0: + return False + else: + return True + + # NOTE (rmk): We are using shallow rebases as a workaround to a bug + # in QEMU 1.3. In order to do this, we need to create + # a destination image with the original backing file + # and matching size of the instance root disk. + src_disk_size = libvirt_utils.get_disk_size(disk_path) + src_back_path = libvirt_utils.get_disk_backing_file(disk_path, + basename=False) + disk_delta = out_path + '.delta' + libvirt_utils.create_cow_image(src_back_path, disk_delta, + src_disk_size) + + try: + # NOTE (rmk): blockRebase cannot be executed on persistent + # domains, so we need to temporarily undefine it. + # If any part of this block fails, the domain is + # re-defined regardless. + if domain.isPersistent(): + domain.undefine() + + # NOTE (rmk): Establish a temporary mirror of our root disk and + # issue an abort once we have a complete copy. + domain.blockRebase(disk_path, disk_delta, 0, + libvirt.VIR_DOMAIN_BLOCK_REBASE_COPY | + libvirt.VIR_DOMAIN_BLOCK_REBASE_REUSE_EXT | + libvirt.VIR_DOMAIN_BLOCK_REBASE_SHALLOW) + + while _wait_for_block_job(domain, disk_path): + time.sleep(0.5) + + domain.blockJobAbort(disk_path, 0) + libvirt_utils.chown(disk_delta, os.getuid()) + finally: + self._conn.defineXML(xml) + + # Convert the delta (CoW) image with a backing file to a flat + # image with no backing file. + libvirt_utils.extract_snapshot(disk_delta, 'qcow2', None, + out_path, image_format) def reboot(self, instance, network_info, reboot_type='SOFT', block_device_info=None): diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index 4b3517da7..bd4ec685c 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -63,7 +63,7 @@ def create_image(disk_format, path, size): execute('qemu-img', 'create', '-f', disk_format, path, size) -def create_cow_image(backing_file, path): +def create_cow_image(backing_file, path, size=None): """Create COW image Creates a COW image with the given backing file @@ -89,6 +89,8 @@ def create_cow_image(backing_file, path): # cow_opts += ['preallocation=%s' % base_details['preallocation']] if base_details and base_details.encryption: cow_opts += ['encryption=%s' % base_details.encryption] + if size is not None: + cow_opts += ['size=%s' % size] if cow_opts: # Format as a comma separated list csv_opts = ",".join(cow_opts) @@ -292,14 +294,14 @@ def get_disk_size(path): return int(size) -def get_disk_backing_file(path): +def get_disk_backing_file(path, basename=True): """Get the backing file of a disk image :param path: Path to the disk image :returns: a path to the image's backing store """ backing_file = images.qemu_img_info(path).backing_file - if backing_file: + if backing_file and basename: backing_file = os.path.basename(backing_file) return backing_file @@ -403,16 +405,16 @@ def extract_snapshot(disk_path, source_fmt, snapshot_name, out_path, dest_fmt): # NOTE(markmc): ISO is just raw to qemu-img if dest_fmt == 'iso': dest_fmt = 'raw' - qemu_img_cmd = ('qemu-img', - 'convert', - '-f', - source_fmt, - '-O', - dest_fmt, - '-s', - snapshot_name, - disk_path, - out_path) + + qemu_img_cmd = ('qemu-img', 'convert', '-f', source_fmt, '-O', + dest_fmt, '-s', snapshot_name, disk_path, out_path) + + # When snapshot name is omitted we do a basic convert, which + # is used by live snapshots. + if snapshot_name is None: + qemu_img_cmd = ('qemu-img', 'convert', '-f', source_fmt, '-O', + dest_fmt, disk_path, out_path) + execute(*qemu_img_cmd) |