2013-03-18 01:06:52 +04:00
#
2015-03-31 18:59:06 +03:00
# Copyright 2013, 2015 Red Hat, Inc.
2013-03-18 01:06:52 +04:00
# Copyright(c) FUJITSU Limited 2007.
#
# Cloning a virtual machine module.
#
2018-04-04 16:35:41 +03:00
# This work is licensed under the GNU GPLv2 or later.
2018-03-20 22:00:02 +03:00
# See the COPYING file in the top-level directory.
2013-03-18 01:06:52 +04:00
import re
import os
import libvirt
2019-06-08 01:16:53 +03:00
from . import generatename
2019-06-08 00:32:51 +03:00
from . import progress
2014-09-12 23:59:22 +04:00
from . guest import Guest
2018-03-20 19:27:37 +03:00
from . devices import DeviceInterface
from . devices import DeviceDisk
2019-06-17 04:12:39 +03:00
from . logger import log
2018-03-20 19:27:37 +03:00
from . devices import DeviceChannel
2013-03-18 01:06:52 +04:00
2013-04-13 22:34:52 +04:00
2020-01-27 20:13:23 +03:00
def _replace_vm ( conn , name ) :
"""
Remove the existing VM with the same name if requested
"""
try :
vm = conn . lookupByName ( name )
except libvirt . libvirtError :
return
try :
log . debug ( " Explicitly replacing guest ' %s ' " , name )
if vm . ID ( ) != - 1 :
log . debug ( " Destroying guest ' %s ' " , name )
vm . destroy ( )
log . debug ( " Undefining guest ' %s ' " , name )
vm . undefine ( )
except libvirt . libvirtError as e : # pragma: no cover
2020-07-12 00:31:40 +03:00
raise RuntimeError (
2020-07-14 10:41:49 +03:00
_ ( " Could not remove old vm ' %(vm)s ' : %(error)s " ) % {
" vm " : name ,
" error " : str ( e ) ,
} )
2020-01-27 20:13:23 +03:00
2020-09-03 19:03:38 +03:00
def _generate_clone_name ( conn , basename ) :
"""
If the orig name is " foo-clone " , we don ' t want the clone to be
" foo-clone-clone " , we want " foo-clone1 "
"""
match = re . search ( " -clone[1-9]*$ " , basename )
start_num = 1
force_num = False
if match :
num_match = re . search ( " [1-9]+$ " , match . group ( ) )
if num_match :
start_num = int ( str ( num_match . group ( ) ) ) + 1
force_num = True
basename = basename . replace ( match . group ( ) , " " )
def cb ( n ) :
return generatename . check_libvirt_collision (
conn . lookupByName , n )
basename = basename + " -clone "
return generatename . generate_name ( basename , cb ,
sep = " " , start_num = start_num , force_num = force_num )
def _generate_clone_disk_path ( conn , origname , newname , origpath ) :
"""
Generate desired cloned disk path name , derived from the
original path , original VM name , and proposed new VM name
"""
if origpath is None :
return None
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 )
def cb ( p ) :
return DeviceDisk . path_definitely_exists ( conn , p )
return generatename . generate_name ( clonebase , cb , suffix = suffix )
def _lookup_vm ( conn , name ) :
try :
return conn . lookupByName ( name )
except libvirt . libvirtError :
e = ValueError ( _ ( " Domain ' %s ' was not found. " ) % str ( name ) )
raise e from None
2020-09-04 20:54:54 +03:00
def _build_clone_vol_install ( orig_disk , new_disk ) :
2020-09-04 17:41:17 +03:00
# We set a stub size for initial creation
# set_input_vol will overwrite it
size = .000001
sparse = False
2020-09-03 19:03:38 +03:00
vol_install = DeviceDisk . build_vol_install (
2020-09-04 20:54:54 +03:00
orig_disk . conn , os . path . basename ( new_disk . path ) ,
new_disk . get_parent_pool ( ) , size , sparse )
2020-09-04 17:41:17 +03:00
vol_install . set_input_vol ( orig_disk . get_vol_object ( ) )
2020-09-03 19:03:38 +03:00
return vol_install
def _build_clone_disk ( orig_disk , clonepath , allow_create , sparse ) :
conn = orig_disk . conn
device = DeviceDisk . DEVICE_DISK
if not clonepath :
device = DeviceDisk . DEVICE_CDROM
2020-09-04 20:54:54 +03:00
new_disk = DeviceDisk ( conn )
new_disk . path = clonepath
new_disk . device = device
2020-09-03 19:03:38 +03:00
if not allow_create :
2020-09-04 20:54:54 +03:00
new_disk . validate ( )
return new_disk
2020-09-03 19:03:38 +03:00
2020-09-04 20:54:54 +03:00
if new_disk . get_vol_object ( ) :
2020-09-03 19:03:38 +03:00
# 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 ( conn . is_remote ( ) or
2020-09-04 20:54:54 +03:00
new_disk . type != new_disk . TYPE_BLOCK or
2020-09-03 19:03:38 +03:00
not orig_disk . path or
not os . access ( orig_disk . path , os . R_OK ) or
2020-09-04 20:54:54 +03:00
not new_disk . path or
not os . access ( new_disk . path , os . W_OK ) ) :
2020-09-03 19:03:38 +03:00
raise RuntimeError (
_ ( " Clone onto existing storage volume is not "
2020-09-04 20:54:54 +03:00
" currently supported: ' %s ' " ) % new_disk . path )
2020-09-03 19:03:38 +03:00
if ( orig_disk . get_vol_object ( ) and
2020-09-04 20:54:54 +03:00
new_disk . wants_storage_creation ( ) ) :
vol_install = _build_clone_vol_install ( orig_disk , new_disk )
2020-09-03 19:03:38 +03:00
if not sparse :
vol_install . allocation = vol_install . capacity
2020-09-04 20:54:54 +03:00
new_disk . set_vol_install ( vol_install )
2020-09-03 19:03:38 +03:00
elif orig_disk . path :
2020-09-04 20:54:54 +03:00
new_disk . set_local_disk_to_clone ( orig_disk , sparse )
2020-09-03 19:03:38 +03:00
2020-09-04 20:54:54 +03:00
new_disk . validate ( )
return new_disk
2020-09-03 19:03:38 +03:00
class _CloneDiskInfo :
"""
Class that tracks some additional information about how we want
to default handle each disk of the source VM
"""
def __init__ ( self , srcdisk ) :
self . disk = DeviceDisk ( srcdisk . conn , parsexml = srcdisk . get_xml ( ) )
self . _do_clone = self . _do_we_clone_default ( )
2020-09-04 20:54:54 +03:00
self . new_disk = None
2020-09-03 19:03:38 +03:00
def is_clone_requested ( self ) :
return self . _do_clone
def set_clone_requested ( self , val ) :
self . _do_clone = val
def _do_we_clone_default ( self ) :
if not self . disk . path :
return False
if self . disk . read_only :
return False
if self . disk . shareable :
return False
return True
def check_clonable ( self ) :
try :
2020-09-04 20:07:02 +03:00
self . disk . set_backend_for_existing_path ( )
2020-09-03 19:03:38 +03:00
if self . disk . wants_storage_creation ( ) :
raise ValueError (
_ ( " Disk path ' %s ' does not exist. " ) % self . disk . path )
except Exception as e :
log . debug ( " Exception processing clone original path " , exc_info = True )
err = _ ( " Could not determine original disk information: %s " % str ( e ) )
raise ValueError ( err ) from None
2020-09-04 20:54:54 +03:00
def set_new_path ( self , path , allow_create , sparse ) :
2020-09-03 19:03:38 +03:00
if allow_create :
self . check_clonable ( )
try :
2020-09-04 20:54:54 +03:00
self . new_disk = Cloner . build_clone_disk (
2020-09-03 19:03:38 +03:00
self . disk , path , allow_create , sparse )
except Exception as e :
log . debug ( " Error setting clone path. " , exc_info = True )
raise ValueError (
_ ( " Could not use path ' %(path)s ' for cloning: %(error)s " ) % {
" path " : path ,
" error " : str ( e ) ,
} )
2013-07-03 02:30:46 +04:00
class Cloner ( object ) :
2020-09-03 19:03:38 +03:00
@staticmethod
def generate_clone_name ( conn , basename ) :
return _generate_clone_name ( conn , basename )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
@staticmethod
def generate_clone_disk_path ( conn , origname , newname , origpath ) :
return _generate_clone_disk_path ( conn , origname , newname , origpath )
2013-03-18 01:06:52 +04:00
2020-09-04 17:41:17 +03:00
@staticmethod
def build_clone_disk ( orig_disk , clonepath , allow_create , sparse ) :
return _build_clone_disk ( orig_disk , clonepath , allow_create , sparse )
2020-09-03 19:03:38 +03:00
def __init__ ( self , conn , src_name = None , src_xml = None ) :
2013-07-03 02:30:46 +04:00
self . conn = conn
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
self . _src_guest = None
self . _new_guest = None
self . _diskinfos = [ ]
self . _init_src ( src_name , src_xml )
self . _new_nvram_path = None
2017-03-06 11:43:10 +03:00
self . _nvram_disk = None
2013-07-03 02:30:46 +04:00
2020-09-03 19:03:38 +03:00
self . _sparse = True
self . _overwrite = True
2013-08-09 22:16:59 +04:00
self . _replace = False
2015-02-07 05:18:05 +03:00
self . _reflink = False
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
#################
# Init routines #
#################
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def _init_src ( self , src_name , src_xml ) :
"""
Set up the source VM info we are cloning , from passed in VM name
or full XML
"""
if not src_xml :
dom = _lookup_vm ( self . conn , src_name )
status = dom . info ( ) [ 0 ]
if status not in [ libvirt . VIR_DOMAIN_SHUTOFF ] :
raise RuntimeError ( _ ( " Domain to clone must be shutoff. " ) )
flags = libvirt . VIR_DOMAIN_XML_SECURE
src_xml = dom . XMLDesc ( flags )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
log . debug ( " Original XML: \n %s " , src_xml )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
self . _src_guest = Guest ( self . conn , parsexml = src_xml )
self . _new_guest = Guest ( self . conn , parsexml = src_xml )
self . _init_new_guest ( )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
# Collect disk info for every disk to determine if we will
# default to cloning or not
for disk in self . _src_guest . devices . disk :
self . _diskinfos . append ( _CloneDiskInfo ( disk ) )
for diskinfo in [ d for d in self . _diskinfos if d . is_clone_requested ( ) ] :
disk = diskinfo . disk
log . debug ( " Wants cloning: size= %s path= %s " ,
disk . get_size ( ) , disk . path )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def _init_new_guest ( self ) :
"""
Perform the series of unconditional new VM changes we always make
"""
self . _new_guest . id = None
self . _new_guest . title = None
self . _new_guest . uuid = None
self . _new_guest . uuid = Guest . generate_uuid ( self . conn )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
for dev in self . _new_guest . devices . graphics :
if dev . port and dev . port != - 1 :
log . warning ( _ ( " Setting the graphics device port to autoport, "
" in order to avoid conflicting. " ) )
dev . port = - 1
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
for iface in self . _new_guest . devices . interface :
iface . target_dev = None
iface . macaddr = DeviceInterface . generate_mac ( self . conn )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
# For guest agent channel, remove a path to generate a new one with
# new guest name
for channel in self . _new_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
2013-07-03 02:30:46 +04:00
2020-09-03 19:03:38 +03:00
new_name = Cloner . generate_clone_name ( self . conn , self . src_name )
log . debug ( " Auto-generated clone name ' %s ' " , new_name )
self . set_clone_name ( new_name )
2018-02-23 04:44:09 +03:00
2020-09-03 19:03:38 +03:00
##############
# Properties #
##############
2015-02-07 05:18:05 +03:00
2020-09-03 19:03:38 +03:00
@property
def src_name ( self ) :
"""
The name of the original VM we are cloning
"""
return self . _src_guest . name
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
@property
def new_guest ( self ) :
2013-03-18 01:06:52 +04:00
"""
2020-09-03 19:03:38 +03:00
The Guest instance of the new XML we will create
2013-03-18 01:06:52 +04:00
"""
2020-09-03 19:03:38 +03:00
return self . _new_guest
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def set_clone_name ( self , name ) :
self . _new_guest . name = name
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def set_clone_uuid ( self , uuid ) :
"""
Override the new VMs generated UUId
"""
self . _new_guest . uuid = uuid
def set_replace ( self , val ) :
"""
If True , don ' t check for clone name collision, simply undefine
any conflicting guest .
"""
self . _replace = bool ( val )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def set_reflink ( self , reflink ) :
"""
If true , use COW lightweight copy
"""
self . _reflink = reflink
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def set_sparse ( self , flg ) :
"""
If True , attempt sparse allocation during cloning
"""
self . _sparse = flg
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def get_diskinfos ( self ) :
"""
Return the list of _CloneDiskInfo instances
"""
return self . _diskinfos [ : ]
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def get_diskinfos_to_clone ( self ) :
"""
Return a list of _CloneDiskInfo that are tagged for cloning
"""
return [ di for di in self . get_diskinfos ( ) if di . is_clone_requested ( ) ]
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def set_nvram_path ( self , val ) :
"""
If the VM needs to have nvram content cloned , this overrides the
destination path
"""
self . _new_nvram_path = val
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
def set_overwrite ( self , flg ) :
2013-04-24 18:47:56 +04:00
"""
2020-09-03 19:03:38 +03:00
If False , no data is copied to the destination disks by default .
Storage may be created , but it is empty .
2013-04-24 18:47:56 +04:00
"""
2020-09-03 19:03:38 +03:00
self . _overwrite = flg
2014-12-06 05:09:26 +03:00
2013-04-24 18:47:56 +04:00
2020-09-03 19:03:38 +03:00
######################
# Functional methods #
######################
2013-03-18 01:06:52 +04:00
2017-03-06 11:43:10 +03:00
def _prepare_nvram ( self ) :
2020-09-03 19:03:38 +03:00
new_nvram_path = self . _new_nvram_path
if new_nvram_path is None :
nvram_dir = os . path . dirname ( self . _new_guest . os . nvram )
new_nvram_path = os . path . join (
nvram_dir , " %s _VARS.fd " % self . _new_guest . name )
2017-03-06 11:43:10 +03:00
2019-03-28 16:27:04 +03:00
old_nvram = DeviceDisk ( self . conn )
2020-09-03 19:03:38 +03:00
old_nvram . path = self . _new_guest . os . nvram
2018-03-20 19:18:35 +03:00
nvram = DeviceDisk ( self . conn )
2020-09-03 19:03:38 +03:00
nvram . path = new_nvram_path
diskinfo = _CloneDiskInfo ( old_nvram )
allow_create = self . _overwrite
2017-03-06 11:43:10 +03:00
2020-09-03 19:03:38 +03:00
if ( allow_create and
2019-03-28 16:27:04 +03:00
nvram . wants_storage_creation ( ) and
old_nvram . get_vol_object ( ) ) :
2020-09-03 19:03:38 +03:00
# We only run validation if there's some existing nvram we
# can copy. It's valid for nvram to not exist at VM define
# time, libvirt will create it for us
2020-09-04 20:54:54 +03:00
diskinfo . set_new_path ( new_nvram_path , allow_create , self . _sparse )
self . _nvram_disk = diskinfo . new_disk
2020-09-03 19:03:38 +03:00
self . _nvram_disk . get_vol_install ( ) . reflink = self . _reflink
2017-03-06 11:43:10 +03:00
2020-09-03 19:03:38 +03:00
self . _new_guest . os . nvram = nvram . path
2017-03-06 11:43:10 +03:00
2019-03-28 16:27:04 +03:00
2020-09-03 19:03:38 +03:00
def prepare ( self ) :
2013-03-18 01:06:52 +04:00
"""
Validate and set up all parameters needed for the new ( clone ) VM
"""
2020-09-03 19:03:38 +03:00
try :
Guest . validate_name ( self . conn , self . _new_guest . name ,
check_collision = not self . _replace ,
validate = False )
except ValueError as e :
raise ValueError ( _ ( " Invalid name for new guest: %s " ) % e )
2020-01-29 23:31:21 +03:00
2020-09-03 19:03:38 +03:00
for diskinfo in self . get_diskinfos_to_clone ( ) :
orig_disk = diskinfo . disk
2014-07-21 15:54:38 +04:00
2020-09-04 20:54:54 +03:00
if not diskinfo . new_disk :
2020-09-03 19:03:38 +03:00
# User didn't set a path, generate one
newpath = Cloner . generate_clone_disk_path (
self . conn , self . src_name ,
self . new_guest . name ,
orig_disk . path )
2020-09-04 20:54:54 +03:00
diskinfo . set_new_path ( newpath ,
2020-09-03 19:03:38 +03:00
self . _overwrite , self . _sparse )
2013-03-18 01:06:52 +04:00
2020-09-04 20:54:54 +03:00
new_disk = diskinfo . new_disk
assert new_disk
2020-09-03 19:03:38 +03:00
log . debug ( " Cloning srcpath= %s dstpath= %s " ,
2020-09-04 20:54:54 +03:00
orig_disk . path , new_disk . path )
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
if self . _reflink :
2020-09-04 20:54:54 +03:00
vol_install = new_disk . get_vol_install ( )
2020-09-03 19:03:38 +03:00
vol_install . reflink = self . _reflink
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
for disk in self . _new_guest . devices . disk :
2013-03-18 01:06:52 +04:00
if disk . target == orig_disk . target :
xmldisk = disk
# Change the XML
xmldisk . path = None
2020-09-04 20:54:54 +03:00
xmldisk . type = new_disk . type
2013-07-13 18:09:00 +04:00
xmldisk . driver_name = orig_disk . driver_name
2013-03-18 01:06:52 +04:00
xmldisk . driver_type = orig_disk . driver_type
2020-09-04 20:54:54 +03:00
xmldisk . path = new_disk . path
2013-03-18 01:06:52 +04:00
2020-09-03 19:03:38 +03:00
if self . _new_guest . os . nvram :
2017-03-06 11:43:10 +03:00
self . _prepare_nvram ( )
2013-03-18 01:06:52 +04:00
# Save altered clone xml
2020-09-03 19:03:38 +03:00
log . debug ( " Clone guest xml is \n %s " , self . _new_guest . get_xml ( ) )
2013-03-18 01:06:52 +04:00
2013-07-03 02:30:46 +04:00
def start_duplicate ( self , meter = None ) :
"""
Actually perform the duplication : cloning disks if needed and defining
the new clone xml .
"""
2019-06-17 04:12:39 +03:00
log . debug ( " Starting duplicate. " )
2019-06-08 00:32:51 +03:00
meter = progress . ensure_meter ( meter )
2013-07-03 02:30:46 +04:00
dom = None
try :
# Replace orig VM if required
2020-09-03 19:03:38 +03:00
if self . _replace :
_replace_vm ( self . conn , self . _new_guest . name )
2013-07-03 02:30:46 +04:00
# Define domain early to catch any xml errors before duping storage
2020-09-03 19:03:38 +03:00
dom = self . conn . defineXML ( self . _new_guest . get_xml ( ) )
2013-07-03 02:30:46 +04:00
2020-09-03 19:03:38 +03:00
if self . _overwrite :
diskinfos = self . get_diskinfos_to_clone ( )
2020-09-04 20:54:54 +03:00
for dst_dev in [ d . new_disk for d in diskinfos ] :
2018-09-03 23:44:38 +03:00
dst_dev . build_storage ( meter )
2017-03-06 11:43:10 +03:00
if self . _nvram_disk :
2018-09-03 23:44:38 +03:00
self . _nvram_disk . build_storage ( meter )
2017-05-05 19:47:21 +03:00
except Exception as e :
2019-06-17 04:12:39 +03:00
log . debug ( " Duplicate failed: %s " , str ( e ) )
2013-07-03 02:30:46 +04:00
if dom :
dom . undefine ( )
raise
2019-06-17 04:12:39 +03:00
log . debug ( " Duplicating finished. " )