2018-01-28 04:46:39 +08:00
|
|
|
#!/usr/bin/env python3
|
2013-03-18 05:06:52 +08:00
|
|
|
#
|
|
|
|
# Copyright(c) FUJITSU Limited 2007.
|
|
|
|
#
|
2018-04-04 21:35:41 +08:00
|
|
|
# This work is licensed under the GNU GPLv2 or later.
|
2018-03-21 03:00:02 +08:00
|
|
|
# See the COPYING file in the top-level directory.
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
|
2014-01-19 06:01:43 +08:00
|
|
|
import argparse
|
2013-07-03 06:30:46 +08:00
|
|
|
import logging
|
2013-03-18 05:06:52 +08:00
|
|
|
import sys
|
2013-03-18 06:18:22 +08:00
|
|
|
|
2018-09-30 02:27:34 +08:00
|
|
|
from virtinst import cli
|
2014-02-09 05:36:45 +08:00
|
|
|
from virtinst import Cloner
|
2013-03-18 05:06:52 +08:00
|
|
|
from virtinst.cli import fail, print_stdout, print_stderr
|
|
|
|
|
|
|
|
|
2014-05-02 22:20:59 +08:00
|
|
|
# General input gathering functions
|
2013-03-18 05:06:52 +08:00
|
|
|
def get_clone_name(new_name, auto_clone, design):
|
|
|
|
if not new_name and auto_clone:
|
|
|
|
# Generate a name to use
|
2013-07-03 06:30:46 +08:00
|
|
|
new_name = design.generate_clone_name()
|
2013-03-18 05:06:52 +08:00
|
|
|
logging.debug("Auto-generated clone name '%s'", new_name)
|
|
|
|
|
2014-02-05 05:16:39 +08:00
|
|
|
if not new_name:
|
2014-12-09 16:10:35 +08:00
|
|
|
fail(_("A name is required for the new virtual machine,"
|
2014-12-10 11:36:23 +08:00
|
|
|
" use '--name NEW_VM_NAME' to specify one."))
|
2014-02-05 05:16:39 +08:00
|
|
|
design.clone_name = new_name
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def get_original_guest(guest_name, origfile, design):
|
|
|
|
origxml = None
|
|
|
|
if origfile:
|
|
|
|
f = open(origfile, "r")
|
|
|
|
origxml = f.read()
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
try:
|
|
|
|
design.original_xml = origxml
|
|
|
|
return
|
2019-06-11 06:13:31 +08:00
|
|
|
except (ValueError, RuntimeError) as e: # pragma: no cover
|
2013-03-18 05:06:52 +08:00
|
|
|
fail(e)
|
|
|
|
|
2014-02-05 05:16:39 +08:00
|
|
|
if not guest_name:
|
2014-12-10 11:36:23 +08:00
|
|
|
fail(_("An original machine name is required,"
|
|
|
|
" use '--original ORIGINAL_GUEST' and try again."))
|
2014-02-05 05:16:39 +08:00
|
|
|
design.original_guest = guest_name
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def get_clone_macaddr(new_mac, design):
|
2013-07-03 06:30:46 +08:00
|
|
|
if new_mac is None or new_mac[0] == "RANDOM":
|
|
|
|
return
|
|
|
|
design.clone_macs = new_mac
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-02-09 05:36:45 +08:00
|
|
|
def get_clone_diskfile(new_diskfiles, design, preserve, auto_clone):
|
2013-03-18 05:06:52 +08:00
|
|
|
if new_diskfiles is None:
|
|
|
|
new_diskfiles = [None]
|
|
|
|
|
|
|
|
newidx = 0
|
2013-07-03 06:30:46 +08:00
|
|
|
clonepaths = []
|
|
|
|
for origpath in [d.path for d in design.original_disks]:
|
2013-03-18 05:06:52 +08:00
|
|
|
if len(new_diskfiles) <= newidx:
|
|
|
|
# Extend the new/passed paths list with None if it's not
|
|
|
|
# long enough
|
|
|
|
new_diskfiles.append(None)
|
2014-02-09 05:36:45 +08:00
|
|
|
newpath = new_diskfiles[newidx]
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
if origpath is None:
|
2014-02-09 05:36:45 +08:00
|
|
|
newpath = None
|
2019-03-01 00:53:58 +08:00
|
|
|
elif newpath is None and auto_clone:
|
|
|
|
newpath = design.generate_clone_disk_path(origpath)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-02-09 05:36:45 +08:00
|
|
|
clonepaths.append(newpath)
|
2013-03-18 05:06:52 +08:00
|
|
|
newidx += 1
|
2013-07-03 06:30:46 +08:00
|
|
|
design.clone_paths = clonepaths
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-02-09 05:36:45 +08:00
|
|
|
for disk in design.clone_disks:
|
|
|
|
cli.validate_disk(disk, warn_overwrite=not preserve)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def parse_args():
|
2014-02-02 04:20:37 +08:00
|
|
|
desc = _("Duplicate a virtual machine, changing all the unique "
|
2014-02-03 04:36:33 +08:00
|
|
|
"host side configuration like MAC address, name, etc. \n\n"
|
2014-02-02 04:20:37 +08:00
|
|
|
"The VM contents are NOT altered: virt-clone does not change "
|
|
|
|
"anything _inside_ the guest OS, it only duplicates disks and "
|
|
|
|
"does host side changes. So things like changing passwords, "
|
|
|
|
"changing static IP address, etc are outside the scope of "
|
|
|
|
"this tool. For these types of changes, please see virt-sysprep(1).")
|
|
|
|
parser = cli.setupParser("%(prog)s --original [NAME] ...", desc)
|
2013-03-18 05:06:52 +08:00
|
|
|
cli.add_connect_option(parser)
|
|
|
|
|
2014-01-19 06:01:43 +08:00
|
|
|
geng = parser.add_argument_group(_("General Options"))
|
|
|
|
geng.add_argument("-o", "--original", dest="original_guest",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("Name of the original guest; "
|
|
|
|
"The status must be shut off or paused."))
|
2014-01-21 07:04:23 +08:00
|
|
|
geng.add_argument("--original-xml",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("XML file to use as the original guest."))
|
2014-01-21 07:04:23 +08:00
|
|
|
geng.add_argument("--auto-clone", action="store_true",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("Auto generate clone name and storage paths from"
|
|
|
|
" the original guest configuration."))
|
2014-01-19 06:01:43 +08:00
|
|
|
geng.add_argument("-n", "--name", dest="new_name",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("Name for the new guest"))
|
2014-01-21 07:04:23 +08:00
|
|
|
geng.add_argument("-u", "--uuid", dest="new_uuid", help=argparse.SUPPRESS)
|
2018-10-12 00:53:03 +08:00
|
|
|
geng.add_argument("--reflink", action="store_true",
|
2015-02-07 10:18:05 +08:00
|
|
|
help=_("use btrfs COW lightweight copy"))
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-01-19 06:01:43 +08:00
|
|
|
stog = parser.add_argument_group(_("Storage Configuration"))
|
|
|
|
stog.add_argument("-f", "--file", dest="new_diskfile", action="append",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("New file to use as the disk image for the "
|
|
|
|
"new guest"))
|
2014-01-19 06:01:43 +08:00
|
|
|
stog.add_argument("--force-copy", dest="target", action="append",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("Force to copy devices (eg, if 'hdc' is a "
|
|
|
|
"readonly cdrom device, --force-copy=hdc)"))
|
2014-01-19 06:01:43 +08:00
|
|
|
stog.add_argument("--nonsparse", action="store_false", dest="sparse",
|
2013-03-18 05:06:52 +08:00
|
|
|
default=True,
|
|
|
|
help=_("Do not use a sparse file for the clone's "
|
|
|
|
"disk image"))
|
2014-01-19 06:01:43 +08:00
|
|
|
stog.add_argument("--preserve-data", action="store_false",
|
2013-03-18 05:06:52 +08:00
|
|
|
dest="preserve", default=True,
|
|
|
|
help=_("Do not clone storage, new disk images specified "
|
|
|
|
"via --file are preserved unchanged"))
|
2017-03-06 16:43:10 +08:00
|
|
|
stog.add_argument("--nvram", dest="new_nvram",
|
|
|
|
help=_("New file to use as storage for nvram VARS"))
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2014-01-19 06:01:43 +08:00
|
|
|
netg = parser.add_argument_group(_("Networking Configuration"))
|
|
|
|
netg.add_argument("-m", "--mac", dest="new_mac", action="append",
|
2013-03-18 05:06:52 +08:00
|
|
|
help=_("New fixed MAC address for the clone guest. "
|
|
|
|
"Default is a randomly generated MAC"))
|
|
|
|
|
2014-01-19 06:01:43 +08:00
|
|
|
misc = parser.add_argument_group(_("Miscellaneous Options"))
|
2013-09-28 23:27:26 +08:00
|
|
|
|
|
|
|
# Just used for clone tests
|
2014-01-19 06:01:43 +08:00
|
|
|
misc.add_argument("--clone-running", action="store_true",
|
2014-01-21 07:04:23 +08:00
|
|
|
default=False, help=argparse.SUPPRESS)
|
2013-09-28 23:27:26 +08:00
|
|
|
|
|
|
|
cli.add_misc_options(misc, prompt=True, replace=True, printxml=True)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2018-12-07 16:28:50 +08:00
|
|
|
cli.autocomplete(parser)
|
|
|
|
|
2014-01-19 06:01:43 +08:00
|
|
|
return parser.parse_args()
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def main(conn=None):
|
|
|
|
cli.earlyLogging()
|
2014-01-19 06:01:43 +08:00
|
|
|
options = parse_args()
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
options.quiet = options.quiet or options.xmlonly
|
|
|
|
cli.setupLogging("virt-clone", options.debug, options.quiet)
|
|
|
|
|
2015-04-12 07:25:46 +08:00
|
|
|
cli.convert_old_force(options)
|
|
|
|
cli.parse_check(options.check)
|
2013-03-18 05:06:52 +08:00
|
|
|
cli.set_prompt(options.prompt)
|
2019-06-11 06:13:31 +08:00
|
|
|
conn = cli.getConnection(options.connect, conn=conn)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2016-08-19 15:53:43 +08:00
|
|
|
if (options.new_diskfile is None and
|
|
|
|
options.auto_clone is False and
|
|
|
|
options.xmlonly is False):
|
|
|
|
fail(_("Either --auto-clone or --file is required,"
|
|
|
|
" use '--auto-clone or --file' and try again."))
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
design = Cloner(conn)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
design.clone_running = options.clone_running
|
|
|
|
design.replace = bool(options.replace)
|
|
|
|
get_original_guest(options.original_guest, options.original_xml,
|
|
|
|
design)
|
|
|
|
get_clone_name(options.new_name, options.auto_clone, design)
|
|
|
|
|
|
|
|
get_clone_macaddr(options.new_mac, design)
|
2014-02-09 05:36:45 +08:00
|
|
|
if options.new_uuid is not None:
|
|
|
|
design.clone_uuid = options.new_uuid
|
2015-02-07 10:18:05 +08:00
|
|
|
if options.reflink is True:
|
|
|
|
design.reflink = True
|
2014-02-09 05:36:45 +08:00
|
|
|
for i in options.target or []:
|
|
|
|
design.force_target = i
|
|
|
|
design.clone_sparse = options.sparse
|
|
|
|
design.preserve = options.preserve
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2017-03-06 16:43:10 +08:00
|
|
|
design.clone_nvram = options.new_nvram
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
# This determines the devices that need to be cloned, so that
|
|
|
|
# get_clone_diskfile knows how many new disk paths it needs
|
|
|
|
design.setup_original()
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
get_clone_diskfile(options.new_diskfile, design,
|
2013-03-18 05:06:52 +08:00
|
|
|
not options.preserve, options.auto_clone)
|
|
|
|
|
|
|
|
# setup design object
|
|
|
|
design.setup_clone()
|
|
|
|
|
|
|
|
if options.xmlonly:
|
|
|
|
print_stdout(design.clone_xml, do_force=True)
|
|
|
|
else:
|
2015-04-12 07:25:46 +08:00
|
|
|
design.start_duplicate(cli.get_meter())
|
2013-03-18 05:06:52 +08:00
|
|
|
|
|
|
|
print_stdout("")
|
|
|
|
print_stdout(_("Clone '%s' created successfully.") % design.clone_name)
|
|
|
|
logging.debug("end clone")
|
|
|
|
return 0
|
|
|
|
|
2019-06-11 06:13:31 +08:00
|
|
|
if __name__ == "__main__": # pragma: no cover
|
2013-03-18 05:06:52 +08:00
|
|
|
try:
|
|
|
|
sys.exit(main())
|
2017-05-06 00:47:21 +08:00
|
|
|
except SystemExit as sys_e:
|
2013-03-18 05:06:52 +08:00
|
|
|
sys.exit(sys_e.code)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print_stderr(_("Installation aborted at user request"))
|
2017-05-06 00:47:21 +08:00
|
|
|
except Exception as main_e:
|
2013-03-18 05:06:52 +08:00
|
|
|
fail(main_e)
|