virt-manager/virtinst/cloner.py
2019-06-07 18:16:53 -04:00

603 lines
21 KiB
Python

#
# Copyright 2013, 2015 Red Hat, Inc.
# Copyright(c) FUJITSU Limited 2007.
#
# Cloning a virtual machine module.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
import logging
import re
import os
import libvirt
from . import generatename
from . import progress
from . import util
from .guest import Guest
from .devices import DeviceInterface
from .devices import DeviceDisk
from .storage import StorageVolume
from .devices import DeviceChannel
class Cloner(object):
# Reasons why we don't default to cloning.
CLONE_POLICY_NO_READONLY = 1
CLONE_POLICY_NO_SHAREABLE = 2
CLONE_POLICY_NO_EMPTYMEDIA = 3
def __init__(self, conn):
self.conn = conn
# original guest name or uuid
self._original_guest = None
self.original_dom = None
self._original_disks = []
self._original_xml = None
self._guest = None
# clone guest
self._clone_name = None
self._clone_disks = []
self._clone_macs = []
self._clone_uuid = None
self._clone_sparse = True
self._clone_xml = None
self.clone_nvram = None
self._nvram_disk = None
self._force_target = []
self._skip_target = []
self._preserve = True
self._clone_running = False
self._replace = False
self._reflink = False
# Default clone policy for back compat: don't clone readonly,
# shareable, or empty disks
self._clone_policy = [self.CLONE_POLICY_NO_READONLY,
self.CLONE_POLICY_NO_SHAREABLE,
self.CLONE_POLICY_NO_EMPTYMEDIA]
# Generate a random UUID at the start
self.clone_uuid = Guest.generate_uuid(conn)
##############
# Properties #
##############
# Original guest name
def get_original_guest(self):
return self._original_guest
def set_original_guest(self, original_guest):
if self._lookup_vm(original_guest):
self._original_guest = original_guest
original_guest = property(get_original_guest, set_original_guest)
# XML of the original guest
def set_original_xml(self, val):
if not isinstance(val, str):
raise ValueError(_("Original xml must be a string."))
self._original_xml = val
self._original_guest = Guest(self.conn,
parsexml=self._original_xml).name
def get_original_xml(self):
return self._original_xml
original_xml = property(get_original_xml, set_original_xml)
# Name to use for the new guest clone
def get_clone_name(self):
return self._clone_name
def set_clone_name(self, name):
try:
Guest.validate_name(self.conn, name,
check_collision=not self.replace,
validate=False)
except ValueError as e:
raise ValueError(_("Invalid name for new guest: %s") % e)
self._clone_name = name
clone_name = property(get_clone_name, set_clone_name)
# UUID to use for the new guest clone
def set_clone_uuid(self, uuid):
self._clone_uuid = uuid
def get_clone_uuid(self):
return self._clone_uuid
clone_uuid = property(get_clone_uuid, set_clone_uuid)
# Paths to use for the new disk locations
def set_clone_paths(self, paths):
disklist = []
for path in util.listify(paths):
try:
device = DeviceDisk.DEVICE_DISK
if not path:
device = DeviceDisk.DEVICE_CDROM
disk = DeviceDisk(self.conn)
disk.path = path
disk.device = device
if (not self.preserve_dest_disks and
disk.wants_storage_creation()):
vol_install = DeviceDisk.build_vol_install(
self.conn, os.path.basename(disk.path),
disk.get_parent_pool(), .000001, False)
disk.set_vol_install(vol_install)
disk.validate()
disklist.append(disk)
except Exception as e:
logging.debug("Error setting clone path.", exc_info=True)
raise ValueError(_("Could not use path '%s' for cloning: %s") %
(path, str(e)))
self._clone_disks = disklist
def get_clone_paths(self):
return [d.path for d in self.clone_disks]
clone_paths = property(get_clone_paths, set_clone_paths)
# DeviceDisk instances for the new disk paths
@property
def clone_disks(self):
return self._clone_disks
# MAC address for the new guest clone
def set_clone_macs(self, mac):
maclist = util.listify(mac)
for m in maclist:
DeviceInterface.is_conflict_net(self.conn, m)
self._clone_macs = maclist
def get_clone_macs(self):
return self._clone_macs
clone_macs = property(get_clone_macs, set_clone_macs)
# DeviceDisk instances of the original disks being cloned
@property
def original_disks(self):
return self._original_disks
# Generated XML for the guest clone
def get_clone_xml(self):
return self._clone_xml
def set_clone_xml(self, clone_xml):
self._clone_xml = clone_xml
clone_xml = property(get_clone_xml, set_clone_xml)
# Whether to attempt sparse allocation during cloning
def get_clone_sparse(self):
return self._clone_sparse
def set_clone_sparse(self, flg):
self._clone_sparse = flg
clone_sparse = property(get_clone_sparse, set_clone_sparse)
# If true, preserve ALL original disk devices
def get_preserve(self):
return self._preserve
def set_preserve(self, flg):
self._preserve = flg
preserve = property(get_preserve, set_preserve)
# If true, preserve ALL disk devices for the NEW guest.
# This means no storage cloning.
# This is a convenience access for not Cloner.preserve
@property
def preserve_dest_disks(self):
return not self.preserve
# List of disk targets that we force cloning despite
# Cloner's recommendation
def set_force_target(self, dev):
if isinstance(dev, list):
self._force_target = dev[:]
else:
self._force_target.append(dev)
def get_force_target(self):
return self._force_target
force_target = property(get_force_target, set_force_target)
# List of disk targets that we skip cloning despite Cloner's
# recommendation. This takes precedence over force_target.")
def set_skip_target(self, dev):
if isinstance(dev, list):
self._skip_target = dev[:]
else:
self._skip_target.append(dev)
def get_skip_target(self):
return self._skip_target
skip_target = property(get_skip_target, set_skip_target)
# List of policy rules for determining which vm disks to clone.
# See CLONE_POLICY_*
def set_clone_policy(self, policy_list):
if not isinstance(policy_list, list):
raise ValueError(_("Cloning policy must be a list of rules."))
self._clone_policy = policy_list
def get_clone_policy(self):
return self._clone_policy
clone_policy = property(get_clone_policy, set_clone_policy)
# Allow cloning a running VM. If enabled, domain state is not
# checked before cloning.
def get_clone_running(self):
return self._clone_running
def set_clone_running(self, val):
self._clone_running = bool(val)
clone_running = property(get_clone_running, set_clone_running)
# If enabled, don't check for clone name collision, simply undefine
# any conflicting guest.
def _get_replace(self):
return self._replace
def _set_replace(self, val):
self._replace = bool(val)
replace = property(_get_replace, _set_replace)
# If true, use COW lightweight copy
def _get_reflink(self):
return self._reflink
def _set_reflink(self, reflink):
self._reflink = reflink
reflink = property(_get_reflink, _set_reflink)
######################
# Functional methods #
######################
def setup_original(self):
"""
Validate and setup all parameters needed for the original (cloned) VM
"""
logging.debug("Validating original guest parameters")
if self.original_guest is None and self.original_xml is None:
raise RuntimeError(_("Original guest name or xml is required."))
if self.original_guest is not None and not self.original_xml:
self.original_dom = self._lookup_vm(self.original_guest)
flags = libvirt.VIR_DOMAIN_XML_SECURE
self.original_xml = self.original_dom.XMLDesc(flags)
logging.debug("Original XML:\n%s", self.original_xml)
self._guest = Guest(self.conn, parsexml=self.original_xml)
self._guest.id = None
# Pull clonable storage info from the original xml
self._original_disks = self._get_original_disks_info()
logging.debug("Original paths: %s",
[d.path for d in self.original_disks])
logging.debug("Original sizes: %s",
[d.get_size() for d in self.original_disks])
# If domain has devices to clone, it must be 'off' or 'paused'
if (not self.clone_running and
(self.original_dom and len(self.original_disks) != 0)):
status = self.original_dom.info()[0]
if status not in [libvirt.VIR_DOMAIN_SHUTOFF,
libvirt.VIR_DOMAIN_PAUSED]:
raise RuntimeError(_("Domain with devices to clone must be "
"paused or shutoff."))
def _setup_disk_clone_destination(self, orig_disk, clone_disk):
"""
Helper that validates the new path location
"""
if self.preserve_dest_disks:
return
if clone_disk.get_vol_object():
# Special case: non remote cloning of a guest using
# managed block devices: fall back to local cloning if
# we have permissions to do so. This validation check
# caused a few bug reports in a short period of time,
# so must be a common case.
if (self.conn.is_remote() or
clone_disk.type != clone_disk.TYPE_BLOCK or
not orig_disk.path or
not os.access(orig_disk.path, os.R_OK) or
not clone_disk.path or
not os.access(clone_disk.path, os.W_OK)):
raise RuntimeError(
_("Clone onto existing storage volume is not "
"currently supported: '%s'") % clone_disk.path)
# Setup proper cloning inputs for the new virtual disks
if (orig_disk.get_vol_object() and
clone_disk.get_vol_install()):
clone_vol_install = clone_disk.get_vol_install()
# Source and dest are managed. If they share the same pool,
# replace vol_install with a CloneVolume instance, otherwise
# simply set input_vol on the dest vol_install
if (clone_vol_install.pool.name() ==
orig_disk.get_parent_pool().name()):
vol_install = StorageVolume(self.conn)
vol_install.input_vol = orig_disk.get_vol_object()
vol_install.sync_input_vol()
vol_install.name = clone_vol_install.name
else:
# Cross pool cloning
# Sync only the format of the image.
clone_vol_install.input_vol = orig_disk.get_vol_object()
vol_install = clone_vol_install
vol_install.input_vol = orig_disk.get_vol_object()
vol_install.sync_input_vol(only_format=True)
if not self.clone_sparse:
vol_install.allocation = vol_install.capacity
vol_install.reflink = self.reflink
clone_disk.set_vol_install(vol_install)
elif orig_disk.path:
clone_disk.set_local_disk_to_clone(orig_disk, self.clone_sparse)
clone_disk.validate()
def _prepare_nvram(self):
if self.clone_nvram is None:
nvram_dir = os.path.dirname(self._guest.os.nvram)
self.clone_nvram = os.path.join(nvram_dir,
"%s_VARS.fd" % self._clone_name)
old_nvram = DeviceDisk(self.conn)
old_nvram.path = self._guest.os.nvram
nvram = DeviceDisk(self.conn)
nvram.path = self.clone_nvram
if (not self.preserve_dest_disks and
nvram.wants_storage_creation() and
old_nvram.get_vol_object()):
nvram_install = DeviceDisk.build_vol_install(
self.conn, os.path.basename(nvram.path),
nvram.get_parent_pool(), nvram.get_size(), False)
nvram_install.input_vol = old_nvram.get_vol_object()
nvram_install.sync_input_vol(only_format=True)
nvram_install.reflink = self.reflink
nvram.set_vol_install(nvram_install)
nvram.validate()
self._nvram_disk = nvram
self._guest.os.nvram = nvram.path
def setup_clone(self):
"""
Validate and set up all parameters needed for the new (clone) VM
"""
logging.debug("Validating clone parameters.")
self._clone_xml = self.original_xml
if len(self.clone_disks) < len(self.original_disks):
raise ValueError(_("More disks to clone than new paths specified. "
"(%(passed)d specified, %(need)d needed") %
{"passed": len(self.clone_disks),
"need": len(self.original_disks)})
logging.debug("Clone paths: %s", [d.path for d in self.clone_disks])
self._guest.name = self._clone_name
self._guest.uuid = self._clone_uuid
self._clone_macs.reverse()
for dev in self._guest.devices.graphics:
if dev.port and dev.port != -1:
logging.warning(_("Setting the graphics device port to autoport, "
"in order to avoid conflicting."))
dev.port = -1
clone_macs = self._clone_macs[:]
for iface in self._guest.devices.interface:
iface.target_dev = None
if clone_macs:
mac = clone_macs.pop()
else:
mac = DeviceInterface.generate_mac(self.conn)
iface.macaddr = mac
# Changing storage XML
for i, orig_disk in enumerate(self._original_disks):
clone_disk = self._clone_disks[i]
for disk in self._guest.devices.disk:
if disk.target == orig_disk.target:
xmldisk = disk
self._setup_disk_clone_destination(orig_disk, clone_disk)
# Change the XML
xmldisk.path = None
xmldisk.type = clone_disk.type
xmldisk.driver_name = orig_disk.driver_name
xmldisk.driver_type = orig_disk.driver_type
xmldisk.path = clone_disk.path
# For guest agent channel, remove a path to generate a new one with
# new guest name
for channel in self._guest.devices.channel:
if (channel.type == DeviceChannel.TYPE_UNIX and
channel.target_name and channel.source.path and
channel.target_name in channel.source.path):
channel.source.path = None
if self._guest.os.nvram:
self._prepare_nvram()
# Save altered clone xml
self._clone_xml = self._guest.get_xml()
logging.debug("Clone guest xml is\n%s", self._clone_xml)
def start_duplicate(self, meter=None):
"""
Actually perform the duplication: cloning disks if needed and defining
the new clone xml.
"""
logging.debug("Starting duplicate.")
meter = progress.ensure_meter(meter)
dom = None
try:
# Replace orig VM if required
Guest.check_vm_collision(self.conn, self.clone_name,
do_remove=self.replace)
# Define domain early to catch any xml errors before duping storage
dom = self.conn.defineXML(self.clone_xml)
if self.preserve:
for dst_dev in self.clone_disks:
dst_dev.build_storage(meter)
if self._nvram_disk:
self._nvram_disk.build_storage(meter)
except Exception as e:
logging.debug("Duplicate failed: %s", str(e))
if dom:
dom.undefine()
raise
logging.debug("Duplicating finished.")
def generate_clone_disk_path(self, origpath, newname=None):
origname = self.original_guest
newname = newname or self.clone_name
path = origpath
suffix = ""
# Try to split the suffix off the existing disk name. Ex.
# foobar.img -> foobar-clone.img
#
# If the suffix is greater than 7 characters, assume it isn't
# a file extension and is part of the disk name, at which point
# just stick '-clone' on the end.
if "." in origpath and len(origpath.rsplit(".", 1)[1]) <= 7:
path, suffix = origpath.rsplit(".", 1)
suffix = "." + suffix
dirname = os.path.dirname(path)
basename = os.path.basename(path)
clonebase = basename + "-clone"
if origname and basename == origname:
clonebase = newname
clonebase = os.path.join(dirname, clonebase)
return generatename.generate_name(
clonebase,
lambda p: DeviceDisk.path_definitely_exists(self.conn, p),
suffix,
lib_collision=False)
def generate_clone_name(self):
# If the orig name is "foo-clone", we don't want the clone to be
# "foo-clone-clone", we want "foo-clone1"
basename = self.original_guest
match = re.search("-clone[1-9]*$", basename)
start_num = 1
if match:
num_match = re.search("[1-9]+$", match.group())
if num_match:
start_num = int(str(num_match.group()))
basename = basename.replace(match.group(), "")
basename = basename + "-clone"
return generatename.generate_name(basename,
self.conn.lookupByName,
sep="", start_num=start_num)
############################
# Private helper functions #
############################
# Parse disk paths that need to be cloned from the original guest's xml
# Return a list of DeviceDisk instances pointing to the original
# storage
def _get_original_disks_info(self):
clonelist = []
retdisks = []
for disk in self._guest.devices.disk:
if self._do_we_clone_device(disk):
clonelist.append(disk)
continue
# Set up virtual disk to encapsulate all relevant path info
for disk in clonelist:
validate = not self.preserve_dest_disks
try:
device = DeviceDisk.DEVICE_DISK
if not disk.path:
# Tell DeviceDisk we are a cdrom to allow empty media
device = DeviceDisk.DEVICE_CDROM
newd = DeviceDisk(self.conn)
newd.path = disk.path
newd.device = device
newd.driver_name = disk.driver_name
newd.driver_type = disk.driver_type
newd.target = disk.target
if validate:
if newd.wants_storage_creation():
raise ValueError(_("Disk path '%s' does not exist.") %
newd.path)
except Exception as e:
logging.debug("Exception creating clone disk objects",
exc_info=True)
raise ValueError(_("Could not determine original disk "
"information: %s" % str(e)))
retdisks.append(newd)
return retdisks
# Pull disk #i from the original guest xml, return it's source path
# if it should be cloned
# Cloning policy based on 'clone_policy', 'force_target' and 'skip_target'
def _do_we_clone_device(self, disk):
if not disk.target:
raise ValueError(_("XML has no 'dev' attribute in disk target"))
if disk.target in self.skip_target:
return False
if disk.target in self.force_target:
return True
# No media path
if (not disk.path and
self.CLONE_POLICY_NO_EMPTYMEDIA in self.clone_policy):
return False
# Readonly disks
if (disk.read_only and
self.CLONE_POLICY_NO_READONLY in self.clone_policy):
return False
# Shareable disks
if (disk.shareable and
self.CLONE_POLICY_NO_SHAREABLE in self.clone_policy):
return False
return True
# Simple wrapper for checking a vm exists and returning the domain
def _lookup_vm(self, name):
try:
return self.conn.lookupByName(name)
except libvirt.libvirtError:
raise ValueError(_("Domain '%s' was not found.") % str(name))