2013-03-18 05:06:52 +08:00
|
|
|
#!/usr/bin/python -tt
|
|
|
|
#
|
|
|
|
# Copyright(c) FUJITSU Limited 2007.
|
|
|
|
#
|
|
|
|
# Script to set up an cloning guest configuration and kick off an cloning
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
|
|
# MA 02110-1301 USA.
|
|
|
|
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
import logging
|
2013-03-18 06:18:22 +08:00
|
|
|
import optparse
|
2013-03-18 05:06:52 +08:00
|
|
|
import sys
|
2013-03-18 06:18:22 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
import urlgrabber.progress as progress
|
|
|
|
|
|
|
|
import virtinst.cli as cli
|
2013-07-03 06:30:46 +08:00
|
|
|
from virtinst import Cloner
|
2013-03-18 05:06:52 +08:00
|
|
|
from virtinst.cli import fail, print_stdout, print_stderr
|
|
|
|
|
|
|
|
|
|
|
|
### General input gathering functions
|
|
|
|
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)
|
|
|
|
|
|
|
|
prompt_txt = _("What is the name for the cloned virtual machine?")
|
|
|
|
err_txt = _("A name is required for the new virtual machine.")
|
|
|
|
cli.prompt_loop(prompt_txt, err_txt, new_name, design, "clone_name")
|
|
|
|
|
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
|
|
|
|
except (ValueError, RuntimeError), e:
|
|
|
|
fail(e)
|
|
|
|
|
|
|
|
prompt_txt = _("What is the name of the original virtual machine?")
|
|
|
|
err_txt = _("An original machine name or xml file is required.")
|
|
|
|
cli.prompt_loop(prompt_txt, err_txt,
|
|
|
|
guest_name, design, "original_guest")
|
|
|
|
|
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
|
|
|
def get_clone_uuid(new_uuid, design):
|
|
|
|
if new_uuid is not None:
|
|
|
|
design.clone_uuid = new_uuid
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
def get_clone_diskfile(new_diskfiles, design, preserve=False,
|
2013-03-18 05:06:52 +08:00
|
|
|
auto_clone=False):
|
|
|
|
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)
|
|
|
|
disk = new_diskfiles[newidx]
|
|
|
|
|
|
|
|
if disk is None and auto_clone:
|
2013-07-03 06:30:46 +08:00
|
|
|
disk = design.generate_clone_disk_path(origpath)
|
2013-03-18 05:06:52 +08:00
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
if origpath is None:
|
2013-03-18 05:06:52 +08:00
|
|
|
devpath = None
|
|
|
|
else:
|
2013-07-03 06:30:46 +08:00
|
|
|
dev = _check_disk(design.conn, disk, origpath, preserve)
|
2013-03-18 05:06:52 +08:00
|
|
|
devpath = dev.path
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
clonepaths.append(devpath)
|
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
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def _check_disk(conn, clone_path, orig_path, preserve):
|
|
|
|
|
|
|
|
prompt_txt = (_("What would you like to use as the cloned disk "
|
|
|
|
"(file path) for '%s'?") % orig_path)
|
|
|
|
|
|
|
|
return cli.disk_prompt(conn, clone_path, .00001, False,
|
|
|
|
prompt_txt,
|
|
|
|
warn_overwrite=not preserve,
|
|
|
|
check_size=False,
|
|
|
|
path_to_clone=orig_path)
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def get_clone_sparse(sparse, design):
|
|
|
|
design.clone_sparse = sparse
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def get_preserve(preserve, design):
|
|
|
|
design.preserve = preserve
|
|
|
|
|
|
|
|
|
|
|
|
def get_force_target(target, design):
|
|
|
|
for i in target or []:
|
|
|
|
design.force_target = i
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def parse_args():
|
2013-07-01 03:03:53 +08:00
|
|
|
parser = cli.setupParser(
|
|
|
|
"%prog --original [NAME] ...",
|
|
|
|
_("Duplicate a virtual machine, changing all unique configuration "
|
|
|
|
"like MAC address, name, etc. The VM contents are not altered."))
|
2013-03-18 05:06:52 +08:00
|
|
|
cli.add_connect_option(parser)
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
geng = optparse.OptionGroup(parser, _("General Options"))
|
2013-03-18 05:06:52 +08:00
|
|
|
geng.add_option("-o", "--original", dest="original_guest",
|
|
|
|
help=_("Name of the original guest; "
|
|
|
|
"The status must be shut off or paused."))
|
|
|
|
geng.add_option("", "--original-xml", dest="original_xml",
|
|
|
|
help=_("XML file to use as the original guest."))
|
|
|
|
geng.add_option("", "--auto-clone", dest="auto_clone", action="store_true",
|
|
|
|
help=_("Auto generate clone name and storage paths from"
|
|
|
|
" the original guest configuration."))
|
|
|
|
geng.add_option("-n", "--name", dest="new_name",
|
|
|
|
help=_("Name for the new guest"))
|
|
|
|
geng.add_option("-u", "--uuid", dest="new_uuid",
|
|
|
|
help=_("New UUID for the clone guest; Default is a "
|
|
|
|
"randomly generated UUID"))
|
|
|
|
parser.add_option_group(geng)
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
stog = optparse.OptionGroup(parser, _("Storage Configuration"))
|
2013-03-18 05:06:52 +08:00
|
|
|
stog.add_option("-f", "--file", dest="new_diskfile", action="append",
|
|
|
|
help=_("New file to use as the disk image for the "
|
|
|
|
"new guest"))
|
|
|
|
stog.add_option("", "--force-copy", dest="target", action="append",
|
|
|
|
help=_("Force to copy devices (eg, if 'hdc' is a "
|
|
|
|
"readonly cdrom device, --force-copy=hdc)"))
|
|
|
|
stog.add_option("", "--nonsparse", action="store_false", dest="sparse",
|
|
|
|
default=True,
|
|
|
|
help=_("Do not use a sparse file for the clone's "
|
|
|
|
"disk image"))
|
|
|
|
stog.add_option("", "--preserve-data", action="store_false",
|
|
|
|
dest="preserve", default=True,
|
|
|
|
help=_("Do not clone storage, new disk images specified "
|
|
|
|
"via --file are preserved unchanged"))
|
|
|
|
parser.add_option_group(stog)
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
netg = optparse.OptionGroup(parser, _("Networking Configuration"))
|
2013-03-18 05:06:52 +08:00
|
|
|
netg.add_option("-m", "--mac", dest="new_mac", action="append",
|
|
|
|
help=_("New fixed MAC address for the clone guest. "
|
|
|
|
"Default is a randomly generated MAC"))
|
|
|
|
parser.add_option_group(netg)
|
|
|
|
|
2013-07-03 06:30:46 +08:00
|
|
|
misc = optparse.OptionGroup(parser, _("Miscellaneous Options"))
|
2013-03-18 05:06:52 +08:00
|
|
|
misc.add_option("", "--print-xml", action="store_true", dest="xmlonly",
|
|
|
|
help=_("Print the generated domain XML rather than define "
|
|
|
|
"and clone the guest."))
|
|
|
|
misc.add_option("", "--replace", action="store_true", dest="replace",
|
|
|
|
help=_("Don't check for name collision. Allows replacing "
|
|
|
|
"an existing guest with the new clone"))
|
|
|
|
misc.add_option("-d", "--debug", action="store_true", dest="debug",
|
|
|
|
help=_("Print debugging information"))
|
|
|
|
misc.add_option("", "--prompt", action="store_true", dest="prompt",
|
|
|
|
default=False,
|
|
|
|
help=_("Request user input for ambiguous situations or "
|
|
|
|
"required options."))
|
|
|
|
misc.add_option("", "--force", action="store_true", dest="force",
|
|
|
|
default=False,
|
|
|
|
help=_("Do not prompt for input. Answers yes where "
|
|
|
|
"applicable, terminates for all other prompts"))
|
|
|
|
misc.add_option("-q", "--quiet", action="store_true", dest="quiet",
|
|
|
|
help=_("Suppress non-error output"))
|
|
|
|
misc.add_option("", "--clone-running", action="store_true",
|
|
|
|
dest="clone_running", default=False,
|
|
|
|
help=optparse.SUPPRESS_HELP)
|
|
|
|
parser.add_option_group(misc)
|
|
|
|
|
|
|
|
(options, parseargs) = parser.parse_args()
|
|
|
|
return options, parseargs
|
|
|
|
|
2013-04-14 02:34:52 +08:00
|
|
|
|
|
|
|
|
2013-03-18 05:06:52 +08:00
|
|
|
def main(conn=None):
|
|
|
|
cli.earlyLogging()
|
|
|
|
options, parseargs = parse_args()
|
|
|
|
|
|
|
|
options.quiet = options.quiet or options.xmlonly
|
|
|
|
cli.setupLogging("virt-clone", options.debug, options.quiet)
|
|
|
|
if parseargs:
|
|
|
|
fail(_("Unknown argument '%s'") % parseargs[0])
|
|
|
|
|
|
|
|
cli.set_prompt(options.prompt)
|
|
|
|
cli.set_force(options.force)
|
|
|
|
|
|
|
|
if conn is None:
|
|
|
|
conn = cli.getConnection(options.connect)
|
|
|
|
|
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)
|
|
|
|
get_clone_uuid(options.new_uuid, design)
|
|
|
|
get_clone_sparse(options.sparse, design)
|
|
|
|
get_force_target(options.target, design)
|
|
|
|
get_preserve(options.preserve, design)
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
# start cloning
|
|
|
|
meter = progress.TextMeter(fo=sys.stdout)
|
2013-07-03 06:30:46 +08:00
|
|
|
design.start_duplicate(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
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
try:
|
|
|
|
sys.exit(main())
|
|
|
|
except SystemExit, sys_e:
|
|
|
|
sys.exit(sys_e.code)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print_stderr(_("Installation aborted at user request"))
|
|
|
|
except Exception, main_e:
|
|
|
|
fail(main_e)
|