mirror of
https://github.com/virt-manager/virt-manager.git
synced 2025-01-03 01:18:00 +03:00
Move virt-* code into their modules
This layout is closer to what most python modules have nowadays. It also simplifies testing and static analysis setup. Keep virt-* wrappers locally, for ease of running these commands from a git checkout. Adjust the wrapper binaries we install on via packaging to be pure python, which makes things like running gdb easier. Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
parent
3538a8df79
commit
8fe0a208db
50
setup.py
50
setup.py
@ -68,10 +68,7 @@ def _generate_potfiles_in():
|
|||||||
ret.sort(key=lambda s: s.lower())
|
ret.sort(key=lambda s: s.lower())
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
scripts = ["virt-manager", "virt-install",
|
potfiles = ""
|
||||||
"virt-clone", "virt-xml"]
|
|
||||||
|
|
||||||
potfiles = "\n".join(scripts) + "\n\n"
|
|
||||||
potfiles += "\n".join(find("virtManager", "*.py")) + "\n\n"
|
potfiles += "\n".join(find("virtManager", "*.py")) + "\n\n"
|
||||||
potfiles += "\n".join(find("virtinst", "*.py")) + "\n\n"
|
potfiles += "\n".join(find("virtinst", "*.py")) + "\n\n"
|
||||||
|
|
||||||
@ -179,21 +176,34 @@ class my_build(distutils.command.build.build):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def _make_bin_wrappers(self):
|
def _make_bin_wrappers(self):
|
||||||
cmds = ["virt-manager", "virt-install", "virt-clone",
|
template = """#!/usr/bin/env python3
|
||||||
"virt-xml"]
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "%(sharepath)s")
|
||||||
|
from %(pkgname)s import %(filename)s
|
||||||
|
|
||||||
|
%(filename)s.runcli()
|
||||||
|
"""
|
||||||
if not os.path.exists("build"):
|
if not os.path.exists("build"):
|
||||||
os.mkdir("build")
|
os.mkdir("build")
|
||||||
|
sharepath = os.path.join(BuildConfig.prefix, "share", "virt-manager")
|
||||||
|
|
||||||
for app in cmds:
|
def make_script(pkgname, filename, toolname):
|
||||||
sharepath = os.path.join(BuildConfig.prefix,
|
assert os.path.exists(pkgname + "/" + filename + ".py")
|
||||||
"share", "virt-manager", app)
|
content = template % {
|
||||||
|
"sharepath": sharepath,
|
||||||
|
"pkgname": pkgname,
|
||||||
|
"filename": filename}
|
||||||
|
|
||||||
wrapper = "#!/bin/sh\n\n"
|
newpath = os.path.abspath(os.path.join("build", toolname))
|
||||||
wrapper += "exec \"%s\" \"$@\"" % (sharepath)
|
|
||||||
|
|
||||||
newpath = os.path.abspath(os.path.join("build", app))
|
|
||||||
print("Generating %s" % newpath)
|
print("Generating %s" % newpath)
|
||||||
open(newpath, "w").write(wrapper)
|
open(newpath, "w").write(content)
|
||||||
|
|
||||||
|
make_script("virtinst", "virtinstall", "virt-install")
|
||||||
|
make_script("virtinst", "virtclone", "virt-clone")
|
||||||
|
make_script("virtinst", "virtxml", "virt-xml")
|
||||||
|
make_script("virtManager", "virtmanager", "virt-manager")
|
||||||
|
|
||||||
|
|
||||||
def _make_man_pages(self):
|
def _make_man_pages(self):
|
||||||
@ -452,7 +462,6 @@ class TestBaseCommand(distutils.core.Command):
|
|||||||
for key, val in self._clistate.items():
|
for key, val in self._clistate.items():
|
||||||
setattr(testsmodule.utils.clistate, key, val)
|
setattr(testsmodule.utils.clistate, key, val)
|
||||||
testsmodule.setup_logging()
|
testsmodule.setup_logging()
|
||||||
testsmodule.setup_cli_imports()
|
|
||||||
|
|
||||||
# This makes the test runner report results before exiting from ctrl-c
|
# This makes the test runner report results before exiting from ctrl-c
|
||||||
unittest.installHandler()
|
unittest.installHandler()
|
||||||
@ -605,10 +614,7 @@ class CheckPylint(distutils.core.Command):
|
|||||||
import pylint.lint
|
import pylint.lint
|
||||||
import pycodestyle
|
import pycodestyle
|
||||||
|
|
||||||
files = ["setup.py", "virt-install", "virt-clone",
|
files = ["setup.py", "virtinst", "virtManager", "tests"]
|
||||||
"virt-xml", "virt-manager",
|
|
||||||
"virtinst", "virtManager",
|
|
||||||
"tests"]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import codespell_lib
|
import codespell_lib
|
||||||
@ -673,12 +679,6 @@ distutils.core.setup(
|
|||||||
"build/virt-xml"]),
|
"build/virt-xml"]),
|
||||||
|
|
||||||
data_files=[
|
data_files=[
|
||||||
("share/virt-manager/", [
|
|
||||||
"virt-manager",
|
|
||||||
"virt-install",
|
|
||||||
"virt-clone",
|
|
||||||
"virt-xml",
|
|
||||||
]),
|
|
||||||
("share/glib-2.0/schemas",
|
("share/glib-2.0/schemas",
|
||||||
["data/org.virt-manager.virt-manager.gschema.xml"]),
|
["data/org.virt-manager.virt-manager.gschema.xml"]),
|
||||||
("share/virt-manager/ui", glob.glob("ui/*.ui")),
|
("share/virt-manager/ui", glob.glob("ui/*.ui")),
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
# This work is licensed under the GNU GPLv2 or later.
|
# This work is licensed under the GNU GPLv2 or later.
|
||||||
# See the COPYING file in the top-level directory.
|
# See the COPYING file in the top-level directory.
|
||||||
|
|
||||||
import atexit
|
|
||||||
import imp
|
import imp
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -20,9 +19,10 @@ imp.reload(buildconfig)
|
|||||||
|
|
||||||
from tests import utils
|
from tests import utils
|
||||||
|
|
||||||
virtinstall = None
|
# pylint: disable=ungrouped-imports
|
||||||
virtclone = None
|
from virtinst import virtinstall
|
||||||
virtxml = None
|
from virtinst import virtclone
|
||||||
|
from virtinst import virtxml
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
@ -38,24 +38,3 @@ def setup_logging():
|
|||||||
streamHandler.setLevel(logging.ERROR)
|
streamHandler.setLevel(logging.ERROR)
|
||||||
log.addHandler(streamHandler)
|
log.addHandler(streamHandler)
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
def setup_cli_imports():
|
|
||||||
_cleanup_imports = []
|
|
||||||
|
|
||||||
def _cleanup_imports_cb():
|
|
||||||
for f in _cleanup_imports:
|
|
||||||
if os.path.exists(f):
|
|
||||||
os.unlink(f)
|
|
||||||
|
|
||||||
def _import(name, path):
|
|
||||||
_cleanup_imports.append(path + "c")
|
|
||||||
return imp.load_source(name, path)
|
|
||||||
|
|
||||||
global virtinstall
|
|
||||||
global virtclone
|
|
||||||
global virtxml
|
|
||||||
atexit.register(_cleanup_imports_cb)
|
|
||||||
virtinstall = _import("virtinstall", "virt-install")
|
|
||||||
virtclone = _import("virtclone", "virt-clone")
|
|
||||||
virtxml = _import("virtxml", "virt-xml")
|
|
||||||
|
@ -11,7 +11,7 @@ import unittest
|
|||||||
import libvirt
|
import libvirt
|
||||||
|
|
||||||
import virtinst
|
import virtinst
|
||||||
import virtinst.cli
|
from virtinst import cli
|
||||||
import virtinst.uri
|
import virtinst.uri
|
||||||
|
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ class _URIs(object):
|
|||||||
|
|
||||||
if not (is_testdriver_xml and self._testdriver_error):
|
if not (is_testdriver_xml and self._testdriver_error):
|
||||||
try:
|
try:
|
||||||
conn = virtinst.cli.getConnection(uri)
|
conn = cli.getConnection(uri)
|
||||||
except libvirt.libvirtError as e:
|
except libvirt.libvirtError as e:
|
||||||
if not is_testdriver_xml:
|
if not is_testdriver_xml:
|
||||||
raise
|
raise
|
||||||
|
@ -8,6 +8,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import virtinst
|
import virtinst
|
||||||
|
from virtinst import cli
|
||||||
from virtinst import DeviceDisk
|
from virtinst import DeviceDisk
|
||||||
|
|
||||||
from tests import utils
|
from tests import utils
|
||||||
@ -214,8 +215,8 @@ class TestXMLMisc(unittest.TestCase):
|
|||||||
|
|
||||||
def test_nonpredicatble_generate(self):
|
def test_nonpredicatble_generate(self):
|
||||||
kvm_uri = utils.URIs.kvm.replace(",predictable", "")
|
kvm_uri = utils.URIs.kvm.replace(",predictable", "")
|
||||||
kvmconn = virtinst.cli.getConnection(kvm_uri)
|
kvmconn = cli.getConnection(kvm_uri)
|
||||||
testconn = virtinst.cli.getConnection("test:///default")
|
testconn = cli.getConnection("test:///default")
|
||||||
|
|
||||||
testuuid = virtinst.Guest.generate_uuid(self.conn)
|
testuuid = virtinst.Guest.generate_uuid(self.conn)
|
||||||
randomuuid = virtinst.Guest.generate_uuid(testconn)
|
randomuuid = virtinst.Guest.generate_uuid(testconn)
|
||||||
|
220
virt-clone
220
virt-clone
@ -1,217 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3
|
||||||
#
|
# Convenience wrapper for easily running virt-clone from the git tree
|
||||||
# Copyright(c) FUJITSU Limited 2007.
|
|
||||||
#
|
|
||||||
# This work is licensed under the GNU GPLv2 or later.
|
|
||||||
# See the COPYING file in the top-level directory.
|
|
||||||
|
|
||||||
import argparse
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
from virtinst import cli
|
from virtinst import virtclone
|
||||||
from virtinst import Cloner
|
virtclone.runcli()
|
||||||
from virtinst import log
|
|
||||||
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
|
|
||||||
new_name = design.generate_clone_name()
|
|
||||||
log.debug("Auto-generated clone name '%s'", new_name)
|
|
||||||
|
|
||||||
if not new_name:
|
|
||||||
fail(_("A name is required for the new virtual machine,"
|
|
||||||
" use '--name NEW_VM_NAME' to specify one."))
|
|
||||||
design.clone_name = new_name
|
|
||||||
|
|
||||||
|
|
||||||
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) as e: # pragma: no cover
|
|
||||||
fail(e)
|
|
||||||
|
|
||||||
if not guest_name:
|
|
||||||
fail(_("An original machine name is required,"
|
|
||||||
" use '--original ORIGINAL_GUEST' and try again."))
|
|
||||||
design.original_guest = guest_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_clone_macaddr(new_mac, design):
|
|
||||||
if new_mac is None or new_mac[0] == "RANDOM":
|
|
||||||
return
|
|
||||||
design.clone_macs = new_mac
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_clone_diskfile(new_diskfiles, design, preserve, auto_clone):
|
|
||||||
if new_diskfiles is None:
|
|
||||||
new_diskfiles = [None]
|
|
||||||
|
|
||||||
newidx = 0
|
|
||||||
clonepaths = []
|
|
||||||
for origpath in [d.path for d in design.original_disks]:
|
|
||||||
if len(new_diskfiles) <= newidx:
|
|
||||||
# Extend the new/passed paths list with None if it's not
|
|
||||||
# long enough
|
|
||||||
new_diskfiles.append(None)
|
|
||||||
newpath = new_diskfiles[newidx]
|
|
||||||
|
|
||||||
if origpath is None:
|
|
||||||
newpath = None
|
|
||||||
elif newpath is None and auto_clone:
|
|
||||||
newpath = design.generate_clone_disk_path(origpath)
|
|
||||||
|
|
||||||
clonepaths.append(newpath)
|
|
||||||
newidx += 1
|
|
||||||
design.clone_paths = clonepaths
|
|
||||||
|
|
||||||
for disk in design.clone_disks:
|
|
||||||
cli.validate_disk(disk, warn_overwrite=not preserve)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
desc = _("Duplicate a virtual machine, changing all the unique "
|
|
||||||
"host side configuration like MAC address, name, etc. \n\n"
|
|
||||||
"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)
|
|
||||||
cli.add_connect_option(parser)
|
|
||||||
|
|
||||||
geng = parser.add_argument_group(_("General Options"))
|
|
||||||
geng.add_argument("-o", "--original", dest="original_guest",
|
|
||||||
help=_("Name of the original guest; "
|
|
||||||
"The status must be shut off or paused."))
|
|
||||||
geng.add_argument("--original-xml",
|
|
||||||
help=_("XML file to use as the original guest."))
|
|
||||||
geng.add_argument("--auto-clone", action="store_true",
|
|
||||||
help=_("Auto generate clone name and storage paths from"
|
|
||||||
" the original guest configuration."))
|
|
||||||
geng.add_argument("-n", "--name", dest="new_name",
|
|
||||||
help=_("Name for the new guest"))
|
|
||||||
geng.add_argument("-u", "--uuid", dest="new_uuid", help=argparse.SUPPRESS)
|
|
||||||
geng.add_argument("--reflink", action="store_true",
|
|
||||||
help=_("use btrfs COW lightweight copy"))
|
|
||||||
|
|
||||||
stog = parser.add_argument_group(_("Storage Configuration"))
|
|
||||||
stog.add_argument("-f", "--file", dest="new_diskfile", action="append",
|
|
||||||
help=_("New file to use as the disk image for the "
|
|
||||||
"new guest"))
|
|
||||||
stog.add_argument("--force-copy", dest="target", action="append",
|
|
||||||
help=_("Force to copy devices (eg, if 'hdc' is a "
|
|
||||||
"readonly cdrom device, --force-copy=hdc)"))
|
|
||||||
stog.add_argument("--skip-copy", action="append",
|
|
||||||
help=_("Skip copy of the device target. (eg, if 'vda' is a "
|
|
||||||
"disk you don't want to copy and use the same path "
|
|
||||||
"in the new VM, use --skip-copy=vda)"))
|
|
||||||
stog.add_argument("--nonsparse", action="store_false", dest="sparse",
|
|
||||||
default=True,
|
|
||||||
help=_("Do not use a sparse file for the clone's "
|
|
||||||
"disk image"))
|
|
||||||
stog.add_argument("--preserve-data", action="store_false",
|
|
||||||
dest="preserve", default=True,
|
|
||||||
help=_("Do not clone storage, new disk images specified "
|
|
||||||
"via --file are preserved unchanged"))
|
|
||||||
stog.add_argument("--nvram", dest="new_nvram",
|
|
||||||
help=_("New file to use as storage for nvram VARS"))
|
|
||||||
|
|
||||||
netg = parser.add_argument_group(_("Networking Configuration"))
|
|
||||||
netg.add_argument("-m", "--mac", dest="new_mac", action="append",
|
|
||||||
help=_("New fixed MAC address for the clone guest. "
|
|
||||||
"Default is a randomly generated MAC"))
|
|
||||||
|
|
||||||
misc = parser.add_argument_group(_("Miscellaneous Options"))
|
|
||||||
|
|
||||||
# Just used for clone tests
|
|
||||||
misc.add_argument("--clone-running", action="store_true",
|
|
||||||
default=False, help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
cli.add_misc_options(misc, prompt=True, replace=True, printxml=True)
|
|
||||||
|
|
||||||
cli.autocomplete(parser)
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(conn=None):
|
|
||||||
cli.earlyLogging()
|
|
||||||
options = parse_args()
|
|
||||||
|
|
||||||
options.quiet = options.quiet or options.xmlonly
|
|
||||||
cli.setupLogging("virt-clone", options.debug, options.quiet)
|
|
||||||
|
|
||||||
cli.convert_old_force(options)
|
|
||||||
cli.parse_check(options.check)
|
|
||||||
cli.set_prompt(options.prompt)
|
|
||||||
conn = cli.getConnection(options.connect, conn=conn)
|
|
||||||
|
|
||||||
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."))
|
|
||||||
|
|
||||||
design = Cloner(conn)
|
|
||||||
|
|
||||||
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)
|
|
||||||
if options.new_uuid is not None:
|
|
||||||
design.clone_uuid = options.new_uuid
|
|
||||||
if options.reflink is True:
|
|
||||||
design.reflink = True
|
|
||||||
for i in options.target or []:
|
|
||||||
design.force_target = i
|
|
||||||
for i in options.skip_copy or []:
|
|
||||||
design.skip_target = i
|
|
||||||
design.clone_sparse = options.sparse
|
|
||||||
design.preserve = options.preserve
|
|
||||||
|
|
||||||
design.clone_nvram = options.new_nvram
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
get_clone_diskfile(options.new_diskfile, design,
|
|
||||||
not options.preserve, options.auto_clone)
|
|
||||||
|
|
||||||
# setup design object
|
|
||||||
design.setup_clone()
|
|
||||||
|
|
||||||
if options.xmlonly:
|
|
||||||
print_stdout(design.clone_xml, do_force=True)
|
|
||||||
else:
|
|
||||||
design.start_duplicate(cli.get_meter())
|
|
||||||
|
|
||||||
print_stdout("")
|
|
||||||
print_stdout(_("Clone '%s' created successfully.") % design.clone_name)
|
|
||||||
log.debug("end clone")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
|
||||||
try:
|
|
||||||
sys.exit(main())
|
|
||||||
except SystemExit as sys_e:
|
|
||||||
sys.exit(sys_e.code)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print_stderr(_("Installation aborted at user request"))
|
|
||||||
except Exception as main_e:
|
|
||||||
fail(main_e)
|
|
||||||
|
1049
virt-install
1049
virt-install
File diff suppressed because it is too large
Load Diff
344
virt-manager
344
virt-manager
@ -1,343 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3
|
||||||
#
|
# Convenience wrapper for easily running virt-manager from the git tree
|
||||||
# Copyright (C) 2006, 2014 Red Hat, Inc.
|
|
||||||
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
|
|
||||||
#
|
|
||||||
# This work is licensed under the GNU GPLv2 or later.
|
|
||||||
# See the COPYING file in the top-level directory.
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
import gi
|
from virtManager import virtmanager
|
||||||
gi.require_version('LibvirtGLib', '1.0')
|
virtmanager.runcli()
|
||||||
from gi.repository import LibvirtGLib
|
|
||||||
|
|
||||||
from virtinst import BuildConfig
|
|
||||||
from virtinst import VirtinstConnection
|
|
||||||
from virtinst import cli
|
|
||||||
from virtinst import log
|
|
||||||
|
|
||||||
# This is massively heavy handed, but I can't figure out any way to shut
|
|
||||||
# up the slew of gtk deprecation warnings that clog up our very useful
|
|
||||||
# stdout --debug output. Of course we could drop use of deprecated APIs,
|
|
||||||
# but it's a serious quantity of churn
|
|
||||||
import warnings # pylint: disable=wrong-import-order
|
|
||||||
warnings.simplefilter("ignore")
|
|
||||||
|
|
||||||
try:
|
|
||||||
gi.check_version("3.22.0")
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
print("pygobject3 3.22.0 or later is required.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _show_startup_error(msg, details):
|
|
||||||
log.debug("Error starting virt-manager: %s\n%s", msg, details,
|
|
||||||
exc_info=True)
|
|
||||||
from virtManager.error import vmmErrorDialog
|
|
||||||
err = vmmErrorDialog.get_instance()
|
|
||||||
title = _("Error starting Virtual Machine Manager")
|
|
||||||
err.show_err(title + ": " + msg,
|
|
||||||
details=details,
|
|
||||||
title=title,
|
|
||||||
modal=True,
|
|
||||||
debug=False)
|
|
||||||
|
|
||||||
|
|
||||||
def _import_gtk(leftovers):
|
|
||||||
# The never ending fork+gsettings problems now require us to
|
|
||||||
# import Gtk _after_ the fork. This creates a funny race, since we
|
|
||||||
# need to parse the command line arguments to know if we need to
|
|
||||||
# fork, but need to import Gtk before cli processing so it can
|
|
||||||
# handle --g-fatal-args. We strip out our flags first and pass the
|
|
||||||
# left overs to gtk
|
|
||||||
origargv = sys.argv
|
|
||||||
try:
|
|
||||||
sys.argv = origargv[:1] + leftovers[:]
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
leftovers = sys.argv[1:]
|
|
||||||
|
|
||||||
if Gtk.check_version(3, 22, 0):
|
|
||||||
print("gtk3 3.22.0 or later is required.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# This will error if Gtk wasn't correctly initialized
|
|
||||||
Gtk.init()
|
|
||||||
globals()["Gtk"] = Gtk
|
|
||||||
|
|
||||||
# This ensures we can init gsettings correctly
|
|
||||||
import virtManager.config
|
|
||||||
ignore = virtManager.config
|
|
||||||
except Exception as e:
|
|
||||||
# Don't just let the exception raise here. abrt reports bugs
|
|
||||||
# when users mess up su/sudo and DISPLAY isn't set. Printing
|
|
||||||
# it avoids the issue
|
|
||||||
display = os.environ.get("DISPLAY", "")
|
|
||||||
msg = str(e)
|
|
||||||
if display:
|
|
||||||
msg += ": Could not open display: %s" % display
|
|
||||||
log.debug("".join(traceback.format_exc()))
|
|
||||||
print(msg)
|
|
||||||
sys.exit(1)
|
|
||||||
finally:
|
|
||||||
sys.argv = origargv
|
|
||||||
|
|
||||||
return leftovers
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_gsettings_path(schemadir):
|
|
||||||
"""
|
|
||||||
If running from the virt-manager.git srcdir, compile our gsettings
|
|
||||||
schema and use it directly
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
exe = shutil.which("glib-compile-schemas")
|
|
||||||
if not exe: # pragma: no cover
|
|
||||||
raise RuntimeError("You must install glib-compile-schemas to run "
|
|
||||||
"virt-manager from git.")
|
|
||||||
subprocess.check_call([exe, "--strict", schemadir])
|
|
||||||
|
|
||||||
|
|
||||||
def drop_tty():
|
|
||||||
# We fork and setsid so that we drop the controlling
|
|
||||||
# tty. This prevents libvirt's SSH tunnels from prompting
|
|
||||||
# for user input if SSH keys/agent aren't configured.
|
|
||||||
if os.fork() != 0:
|
|
||||||
os._exit(0) # pylint: disable=protected-access
|
|
||||||
|
|
||||||
os.setsid()
|
|
||||||
|
|
||||||
|
|
||||||
def drop_stdio():
|
|
||||||
# This is part of the fork process described in drop_tty()
|
|
||||||
for fd in range(0, 2):
|
|
||||||
try:
|
|
||||||
os.close(fd)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
os.open(os.devnull, os.O_RDWR)
|
|
||||||
os.dup2(0, 1)
|
|
||||||
os.dup2(0, 2)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_commandline():
|
|
||||||
epilog = ("Also accepts standard GTK arguments like --g-fatal-warnings")
|
|
||||||
parser = argparse.ArgumentParser(usage="virt-manager [options]",
|
|
||||||
epilog=epilog)
|
|
||||||
parser.add_argument('--version', action='version',
|
|
||||||
version=BuildConfig.version)
|
|
||||||
parser.set_defaults(domain=None)
|
|
||||||
|
|
||||||
# Trace every libvirt API call to debug output
|
|
||||||
parser.add_argument("--trace-libvirt", choices=["all", "mainloop"],
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
# Don't load any connections on startup to test first run
|
|
||||||
# PackageKit integration
|
|
||||||
parser.add_argument("--test-first-run",
|
|
||||||
help=argparse.SUPPRESS, action="store_true")
|
|
||||||
# Force disable use of libvirt object events
|
|
||||||
parser.add_argument("--test-no-events",
|
|
||||||
help=argparse.SUPPRESS, action="store_true")
|
|
||||||
# Enabling this will tell us, at app exit time, which vmmGObjects were not
|
|
||||||
# garbage collected. This is caused by circular references to other objects,
|
|
||||||
# like a signal that wasn't disconnected. It's not a big deal, but if we
|
|
||||||
# have objects that can be created and destroyed a lot over the course of
|
|
||||||
# the app lifecycle, every non-garbage collected class is a memory leak.
|
|
||||||
# So it's nice to poke at this every now and then and try to track down
|
|
||||||
# what we need to add to class _cleanup handling.
|
|
||||||
parser.add_argument("--test-leak-debug",
|
|
||||||
help=argparse.SUPPRESS, action="store_true")
|
|
||||||
|
|
||||||
# comma separated string of options to tweak app behavior,
|
|
||||||
# for manual and automated testing config
|
|
||||||
parser.add_argument("--test-options", help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
parser.add_argument("-c", "--connect", dest="uri",
|
|
||||||
help="Connect to hypervisor at URI", metavar="URI")
|
|
||||||
parser.add_argument("--debug", action="store_true",
|
|
||||||
help="Print debug output to stdout (implies --no-fork)",
|
|
||||||
default=False)
|
|
||||||
parser.add_argument("--no-fork", action="store_true",
|
|
||||||
help="Don't fork into background on startup")
|
|
||||||
|
|
||||||
parser.add_argument("--show-domain-creator", action="store_true",
|
|
||||||
help="Show 'New VM' wizard")
|
|
||||||
parser.add_argument("--show-domain-editor", metavar="NAME|ID|UUID",
|
|
||||||
help="Show domain details window")
|
|
||||||
parser.add_argument("--show-domain-performance", metavar="NAME|ID|UUID",
|
|
||||||
help="Show domain performance window")
|
|
||||||
parser.add_argument("--show-domain-console", metavar="NAME|ID|UUID",
|
|
||||||
help="Show domain graphical console window")
|
|
||||||
parser.add_argument("--show-domain-delete", metavar="NAME|ID|UUID",
|
|
||||||
help="Show domain delete window")
|
|
||||||
parser.add_argument("--show-host-summary", action="store_true",
|
|
||||||
help="Show connection details window")
|
|
||||||
|
|
||||||
return parser.parse_known_args()
|
|
||||||
|
|
||||||
|
|
||||||
class CLITestOptionsClass:
|
|
||||||
"""
|
|
||||||
Helper class for parsing and tracking --test-* options
|
|
||||||
"""
|
|
||||||
def __init__(self, test_options_str):
|
|
||||||
opts = []
|
|
||||||
if test_options_str:
|
|
||||||
opts = test_options_str.split(",")
|
|
||||||
|
|
||||||
def _get(optname):
|
|
||||||
if optname not in opts:
|
|
||||||
return False
|
|
||||||
opts.remove(optname)
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.first_run = _get("first-run")
|
|
||||||
self.leak_debug = _get("leak-debug")
|
|
||||||
self.no_events = _get("no-events")
|
|
||||||
self.xmleditor_enabled = _get("xmleditor-enabled")
|
|
||||||
|
|
||||||
if opts:
|
|
||||||
print("Unknown --test-options keys: %s" % opts)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
(options, leftovers) = parse_commandline()
|
|
||||||
|
|
||||||
cli.setupLogging("virt-manager", options.debug, False, False)
|
|
||||||
|
|
||||||
import virtManager
|
|
||||||
log.debug("virt-manager version: %s", BuildConfig.version)
|
|
||||||
log.debug("virtManager import: %s", str(virtManager))
|
|
||||||
|
|
||||||
if BuildConfig.running_from_srcdir:
|
|
||||||
_setup_gsettings_path(BuildConfig.gsettings_dir)
|
|
||||||
|
|
||||||
if options.trace_libvirt:
|
|
||||||
log.debug("Libvirt tracing requested")
|
|
||||||
from virtManager.lib import module_trace
|
|
||||||
import libvirt
|
|
||||||
module_trace.wrap_module(libvirt,
|
|
||||||
mainloop=(options.trace_libvirt == "mainloop"),
|
|
||||||
regex=None)
|
|
||||||
|
|
||||||
CLITestOptions = CLITestOptionsClass(options.test_options)
|
|
||||||
if options.test_first_run:
|
|
||||||
CLITestOptions.first_run = True
|
|
||||||
if options.test_leak_debug:
|
|
||||||
CLITestOptions.leak_debug = True
|
|
||||||
if options.test_no_events:
|
|
||||||
CLITestOptions.no_events = True
|
|
||||||
|
|
||||||
# With F27 gnome+wayland we need to set these before GTK import
|
|
||||||
os.environ["GSETTINGS_SCHEMA_DIR"] = BuildConfig.gsettings_dir
|
|
||||||
if CLITestOptions.first_run:
|
|
||||||
os.environ["GSETTINGS_BACKEND"] = "memory"
|
|
||||||
|
|
||||||
# Now we've got basic environment up & running we can fork
|
|
||||||
do_drop_stdio = False
|
|
||||||
if not options.no_fork and not options.debug:
|
|
||||||
drop_tty()
|
|
||||||
do_drop_stdio = True
|
|
||||||
|
|
||||||
# Ignore SIGHUP, otherwise a serial console closing drops the whole app
|
|
||||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
|
||||||
|
|
||||||
leftovers = _import_gtk(leftovers)
|
|
||||||
Gtk = globals()["Gtk"]
|
|
||||||
|
|
||||||
# Do this after the Gtk import so the user has a chance of seeing any error
|
|
||||||
if do_drop_stdio:
|
|
||||||
drop_stdio()
|
|
||||||
|
|
||||||
if leftovers:
|
|
||||||
raise RuntimeError("Unhandled command line options '%s'" % leftovers)
|
|
||||||
|
|
||||||
log.debug("PyGObject version: %d.%d.%d",
|
|
||||||
gi.version_info[0],
|
|
||||||
gi.version_info[1],
|
|
||||||
gi.version_info[2])
|
|
||||||
log.debug("GTK version: %d.%d.%d",
|
|
||||||
Gtk.get_major_version(),
|
|
||||||
Gtk.get_minor_version(),
|
|
||||||
Gtk.get_micro_version())
|
|
||||||
|
|
||||||
if not VirtinstConnection.libvirt_new_enough_for_virtmanager(6000):
|
|
||||||
# We need this version for threaded virConnect access
|
|
||||||
_show_startup_error(
|
|
||||||
_("virt-manager requires libvirt 0.6.0 or later."), "")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prime the vmmConfig cache
|
|
||||||
virtManager.config.vmmConfig.get_instance(BuildConfig, CLITestOptions)
|
|
||||||
|
|
||||||
# Add our icon dir to icon theme
|
|
||||||
icon_theme = Gtk.IconTheme.get_default()
|
|
||||||
icon_theme.prepend_search_path(BuildConfig.icon_dir)
|
|
||||||
|
|
||||||
from virtManager.engine import vmmEngine
|
|
||||||
|
|
||||||
Gtk.Window.set_default_icon_name("virt-manager")
|
|
||||||
|
|
||||||
show_window = None
|
|
||||||
domain = None
|
|
||||||
if options.show_domain_creator:
|
|
||||||
show_window = vmmEngine.CLI_SHOW_DOMAIN_CREATOR
|
|
||||||
elif options.show_host_summary:
|
|
||||||
show_window = vmmEngine.CLI_SHOW_HOST_SUMMARY
|
|
||||||
elif options.show_domain_editor:
|
|
||||||
show_window = vmmEngine.CLI_SHOW_DOMAIN_EDITOR
|
|
||||||
domain = options.show_domain_editor
|
|
||||||
elif options.show_domain_performance:
|
|
||||||
show_window = vmmEngine.CLI_SHOW_DOMAIN_PERFORMANCE
|
|
||||||
domain = options.show_domain_performance
|
|
||||||
elif options.show_domain_console:
|
|
||||||
show_window = vmmEngine.CLI_SHOW_DOMAIN_CONSOLE
|
|
||||||
domain = options.show_domain_console
|
|
||||||
elif options.show_domain_delete:
|
|
||||||
show_window = vmmEngine.CLI_SHOW_DOMAIN_DELETE
|
|
||||||
domain = options.show_domain_delete
|
|
||||||
|
|
||||||
if show_window and options.uri is None:
|
|
||||||
raise RuntimeError("can't use --show-* options without --connect")
|
|
||||||
|
|
||||||
skip_autostart = False
|
|
||||||
if show_window:
|
|
||||||
skip_autostart = True
|
|
||||||
|
|
||||||
# Hook libvirt events into glib main loop
|
|
||||||
LibvirtGLib.init(None)
|
|
||||||
LibvirtGLib.event_register()
|
|
||||||
|
|
||||||
engine = vmmEngine.get_instance()
|
|
||||||
|
|
||||||
# Actually exit when we receive ctrl-c
|
|
||||||
from gi.repository import GLib
|
|
||||||
def _sigint_handler(user_data):
|
|
||||||
ignore = user_data
|
|
||||||
log.debug("Received KeyboardInterrupt. Exiting application.")
|
|
||||||
engine.exit_app()
|
|
||||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
|
|
||||||
_sigint_handler, None)
|
|
||||||
|
|
||||||
engine.start(options.uri, show_window, domain, skip_autostart)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
log.debug("Received KeyboardInterrupt. Exiting application.")
|
|
||||||
except Exception as run_e:
|
|
||||||
if "Gtk" not in globals():
|
|
||||||
raise
|
|
||||||
_show_startup_error(str(run_e), "".join(traceback.format_exc()))
|
|
||||||
|
@ -139,7 +139,6 @@ done
|
|||||||
%{_mandir}/man1/%{name}.1*
|
%{_mandir}/man1/%{name}.1*
|
||||||
|
|
||||||
%{_datadir}/%{name}/ui/*.ui
|
%{_datadir}/%{name}/ui/*.ui
|
||||||
%{_datadir}/%{name}/virt-manager
|
|
||||||
%{_datadir}/%{name}/virtManager
|
%{_datadir}/%{name}/virtManager
|
||||||
|
|
||||||
%{_datadir}/%{name}/icons
|
%{_datadir}/%{name}/icons
|
||||||
@ -161,10 +160,6 @@ done
|
|||||||
%{_mandir}/man1/virt-clone.1*
|
%{_mandir}/man1/virt-clone.1*
|
||||||
%{_mandir}/man1/virt-xml.1*
|
%{_mandir}/man1/virt-xml.1*
|
||||||
|
|
||||||
%{_datadir}/%{name}/virt-install
|
|
||||||
%{_datadir}/%{name}/virt-clone
|
|
||||||
%{_datadir}/%{name}/virt-xml
|
|
||||||
|
|
||||||
%{_datadir}/bash-completion/completions/virt-install
|
%{_datadir}/bash-completion/completions/virt-install
|
||||||
%{_datadir}/bash-completion/completions/virt-clone
|
%{_datadir}/bash-completion/completions/virt-clone
|
||||||
%{_datadir}/bash-completion/completions/virt-xml
|
%{_datadir}/bash-completion/completions/virt-xml
|
||||||
|
576
virt-xml
576
virt-xml
@ -1,573 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/python3
|
||||||
#
|
# Convenience wrapper for easily running virt-xml from the git tree
|
||||||
# Copyright 2013-2014 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# This work is licensed under the GNU GPLv2 or later.
|
|
||||||
# See the COPYING file in the top-level directory.
|
|
||||||
|
|
||||||
import difflib
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
import libvirt
|
from virtinst import virtxml
|
||||||
|
virtxml.runcli()
|
||||||
import virtinst
|
|
||||||
from virtinst import cli
|
|
||||||
from virtinst import log
|
|
||||||
from virtinst import xmlutil
|
|
||||||
from virtinst.cli import fail, print_stdout, print_stderr
|
|
||||||
|
|
||||||
|
|
||||||
###################
|
|
||||||
# Utility helpers #
|
|
||||||
###################
|
|
||||||
|
|
||||||
def prompt_yes_or_no(msg):
|
|
||||||
while 1:
|
|
||||||
printmsg = msg + " (y/n): "
|
|
||||||
sys.stdout.write(printmsg)
|
|
||||||
sys.stdout.flush()
|
|
||||||
inp = sys.stdin.readline().lower().strip()
|
|
||||||
|
|
||||||
if inp in ["y", "yes"]:
|
|
||||||
return True
|
|
||||||
elif inp in ["n", "no"]:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print_stdout(_("Please enter 'yes' or 'no'."))
|
|
||||||
|
|
||||||
|
|
||||||
def get_diff(origxml, newxml):
|
|
||||||
ret = "".join(difflib.unified_diff(origxml.splitlines(1),
|
|
||||||
newxml.splitlines(1),
|
|
||||||
fromfile="Original XML",
|
|
||||||
tofile="Altered XML"))
|
|
||||||
|
|
||||||
if ret:
|
|
||||||
log.debug("XML diff:\n%s", ret)
|
|
||||||
else:
|
|
||||||
log.debug("No XML diff, didn't generate any change.")
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def set_os_variant(options, guest):
|
|
||||||
if options.os_variant is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
osdata = cli.parse_os_variant(options.os_variant)
|
|
||||||
if osdata.name:
|
|
||||||
guest.set_os_name(osdata.name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_xmldesc(domain, inactive=False):
|
|
||||||
flags = libvirt.VIR_DOMAIN_XML_SECURE
|
|
||||||
if inactive:
|
|
||||||
flags |= libvirt.VIR_DOMAIN_XML_INACTIVE
|
|
||||||
return domain.XMLDesc(flags)
|
|
||||||
|
|
||||||
|
|
||||||
def get_domain_and_guest(conn, domstr):
|
|
||||||
try:
|
|
||||||
int(domstr)
|
|
||||||
isint = True
|
|
||||||
except ValueError:
|
|
||||||
isint = False
|
|
||||||
|
|
||||||
uuidre = "[a-fA-F0-9]{8}[-]([a-fA-F0-9]{4}[-]){3}[a-fA-F0-9]{12}$"
|
|
||||||
isuuid = bool(re.match(uuidre, domstr))
|
|
||||||
|
|
||||||
try:
|
|
||||||
domain = None
|
|
||||||
try:
|
|
||||||
domain = conn.lookupByName(domstr)
|
|
||||||
except Exception:
|
|
||||||
# In case the VM has a UUID or ID for a name
|
|
||||||
log.debug("Error looking up domain by name", exc_info=True)
|
|
||||||
if isint:
|
|
||||||
domain = conn.lookupByID(int(domstr))
|
|
||||||
elif isuuid:
|
|
||||||
domain = conn.lookupByUUIDString(domstr)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
except libvirt.libvirtError as e:
|
|
||||||
fail(_("Could not find domain '%s': %s") % (domstr, e))
|
|
||||||
|
|
||||||
state = domain.info()[0]
|
|
||||||
active_xmlobj = None
|
|
||||||
inactive_xmlobj = virtinst.Guest(conn, parsexml=get_xmldesc(domain))
|
|
||||||
if state != libvirt.VIR_DOMAIN_SHUTOFF:
|
|
||||||
active_xmlobj = inactive_xmlobj
|
|
||||||
inactive_xmlobj = virtinst.Guest(conn,
|
|
||||||
parsexml=get_xmldesc(domain, inactive=True))
|
|
||||||
|
|
||||||
return (domain, inactive_xmlobj, active_xmlobj)
|
|
||||||
|
|
||||||
|
|
||||||
def defined_xml_is_unchanged(conn, domain, original_xml):
|
|
||||||
rawxml = get_xmldesc(domain, inactive=True)
|
|
||||||
new_xml = virtinst.Guest(conn, parsexml=rawxml).get_xml()
|
|
||||||
return new_xml == original_xml
|
|
||||||
|
|
||||||
|
|
||||||
################
|
|
||||||
# Change logic #
|
|
||||||
################
|
|
||||||
|
|
||||||
def _find_objects_to_edit(guest, action_name, editval, parserclass):
|
|
||||||
objlist = xmlutil.listify(parserclass.lookup_prop(guest))
|
|
||||||
idx = None
|
|
||||||
|
|
||||||
if editval is None:
|
|
||||||
idx = 1
|
|
||||||
elif (editval.isdigit() or
|
|
||||||
editval.startswith("-") and editval[1:].isdigit()):
|
|
||||||
idx = int(editval)
|
|
||||||
|
|
||||||
if idx is not None:
|
|
||||||
# Edit device by index
|
|
||||||
if idx == 0:
|
|
||||||
fail(_("Invalid --edit option '%s'") % editval)
|
|
||||||
|
|
||||||
if not objlist:
|
|
||||||
fail(_("No --%s objects found in the XML") %
|
|
||||||
parserclass.cli_arg_name)
|
|
||||||
if len(objlist) < abs(idx):
|
|
||||||
fail(_("--edit %s requested but there's only %s "
|
|
||||||
"--%s object in the XML") %
|
|
||||||
(idx, len(objlist), parserclass.cli_arg_name))
|
|
||||||
|
|
||||||
if idx > 0:
|
|
||||||
idx -= 1
|
|
||||||
inst = objlist[idx]
|
|
||||||
|
|
||||||
elif editval == "all":
|
|
||||||
# Edit 'all' devices
|
|
||||||
inst = objlist[:]
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Lookup device by the passed prop string
|
|
||||||
parserobj = parserclass(editval, guest=guest)
|
|
||||||
inst = parserobj.lookup_child_from_option_string()
|
|
||||||
if not inst:
|
|
||||||
fail(_("No matching objects found for --%s %s") %
|
|
||||||
(action_name, editval))
|
|
||||||
|
|
||||||
return inst
|
|
||||||
|
|
||||||
|
|
||||||
def check_action_collision(options):
|
|
||||||
actions = ["edit", "add-device", "remove-device", "build-xml"]
|
|
||||||
|
|
||||||
collisions = []
|
|
||||||
for cliname in actions:
|
|
||||||
optname = cliname.replace("-", "_")
|
|
||||||
if getattr(options, optname) not in [False, -1]:
|
|
||||||
collisions.append(cliname)
|
|
||||||
|
|
||||||
if len(collisions) == 0:
|
|
||||||
fail(_("One of %s must be specified.") %
|
|
||||||
", ".join(["--" + c for c in actions]))
|
|
||||||
if len(collisions) > 1:
|
|
||||||
fail(_("Conflicting options %s") %
|
|
||||||
", ".join(["--" + c for c in collisions]))
|
|
||||||
|
|
||||||
|
|
||||||
def check_xmlopt_collision(options):
|
|
||||||
collisions = []
|
|
||||||
for parserclass in cli.VIRT_PARSERS:
|
|
||||||
if getattr(options, parserclass.cli_arg_name):
|
|
||||||
collisions.append(parserclass)
|
|
||||||
|
|
||||||
if len(collisions) == 0:
|
|
||||||
fail(_("No change specified."))
|
|
||||||
if len(collisions) != 1:
|
|
||||||
fail(_("Only one change operation may be specified "
|
|
||||||
"(conflicting options %s)") %
|
|
||||||
[c.cli_flag_name() for c in collisions])
|
|
||||||
|
|
||||||
return collisions[0]
|
|
||||||
|
|
||||||
|
|
||||||
def action_edit(guest, options, parserclass):
|
|
||||||
if parserclass.guest_propname:
|
|
||||||
inst = _find_objects_to_edit(guest, "edit", options.edit, parserclass)
|
|
||||||
else:
|
|
||||||
inst = guest
|
|
||||||
if options.edit and options.edit != '1' and options.edit != 'all':
|
|
||||||
fail(_("'--edit %s' doesn't make sense with --%s, "
|
|
||||||
"just use empty '--edit'") %
|
|
||||||
(options.edit, parserclass.cli_arg_name))
|
|
||||||
if options.os_variant is not None:
|
|
||||||
fail(_("--os-variant is not supported with --edit"))
|
|
||||||
|
|
||||||
return cli.parse_option_strings(options, guest, inst, editing=True)
|
|
||||||
|
|
||||||
|
|
||||||
def action_add_device(guest, options, parserclass):
|
|
||||||
if not parserclass.prop_is_list(guest):
|
|
||||||
fail(_("Cannot use --add-device with --%s") % parserclass.cli_arg_name)
|
|
||||||
set_os_variant(options, guest)
|
|
||||||
devs = cli.parse_option_strings(options, guest, None)
|
|
||||||
devs = xmlutil.listify(devs)
|
|
||||||
for dev in devs:
|
|
||||||
dev.set_defaults(guest)
|
|
||||||
return devs
|
|
||||||
|
|
||||||
|
|
||||||
def action_remove_device(guest, options, parserclass):
|
|
||||||
if not parserclass.prop_is_list(guest):
|
|
||||||
fail(_("Cannot use --remove-device with --%s") %
|
|
||||||
parserclass.cli_arg_name)
|
|
||||||
if options.os_variant is not None:
|
|
||||||
fail(_("--os-variant is not supported with --remove-device"))
|
|
||||||
|
|
||||||
devs = _find_objects_to_edit(guest, "remove-device",
|
|
||||||
getattr(options, parserclass.cli_arg_name)[-1], parserclass)
|
|
||||||
|
|
||||||
devs = xmlutil.listify(devs)
|
|
||||||
for dev in devs:
|
|
||||||
guest.remove_device(dev)
|
|
||||||
return devs
|
|
||||||
|
|
||||||
|
|
||||||
def action_build_xml(conn, options, parserclass, guest):
|
|
||||||
if not parserclass.guest_propname:
|
|
||||||
fail(_("--build-xml not supported for --%s") %
|
|
||||||
parserclass.cli_arg_name)
|
|
||||||
if options.os_variant is not None:
|
|
||||||
fail(_("--os-variant is not supported with --build-xml"))
|
|
||||||
|
|
||||||
inst = parserclass.lookup_prop(guest)
|
|
||||||
if parserclass.prop_is_list(guest):
|
|
||||||
inst = inst.new()
|
|
||||||
else:
|
|
||||||
inst = inst.__class__(conn)
|
|
||||||
|
|
||||||
devs = cli.parse_option_strings(options, guest, inst)
|
|
||||||
devs = xmlutil.listify(devs)
|
|
||||||
for dev in devs:
|
|
||||||
dev.set_defaults(guest)
|
|
||||||
return devs
|
|
||||||
|
|
||||||
|
|
||||||
def setup_device(dev):
|
|
||||||
if getattr(dev, "DEVICE_TYPE", None) != "disk":
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug("Doing setup for disk=%s", dev)
|
|
||||||
dev.build_storage(cli.get_meter())
|
|
||||||
|
|
||||||
|
|
||||||
def define_changes(conn, inactive_xmlobj, devs, action, confirm):
|
|
||||||
if confirm:
|
|
||||||
if not prompt_yes_or_no(
|
|
||||||
_("Define '%s' with the changed XML?") % inactive_xmlobj.name):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if action == "hotplug":
|
|
||||||
for dev in devs:
|
|
||||||
setup_device(dev)
|
|
||||||
|
|
||||||
dom = conn.defineXML(inactive_xmlobj.get_xml())
|
|
||||||
print_stdout(_("Domain '%s' defined successfully.") % inactive_xmlobj.name)
|
|
||||||
return dom
|
|
||||||
|
|
||||||
|
|
||||||
def start_domain_transient(conn, xmlobj, devs, action, confirm):
|
|
||||||
if confirm:
|
|
||||||
if not prompt_yes_or_no(
|
|
||||||
_("Start '%s' with the changed XML?") % xmlobj.name):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if action == "hotplug":
|
|
||||||
for dev in devs:
|
|
||||||
setup_device(dev)
|
|
||||||
|
|
||||||
try:
|
|
||||||
dom = conn.createXML(xmlobj.get_xml())
|
|
||||||
except libvirt.libvirtError as e:
|
|
||||||
fail(_("Failed starting domain '%s': %s") % (xmlobj.name, e))
|
|
||||||
else:
|
|
||||||
print_stdout(_("Domain '%s' started successfully.") % xmlobj.name)
|
|
||||||
return dom
|
|
||||||
|
|
||||||
|
|
||||||
def update_changes(domain, devs, action, confirm):
|
|
||||||
for dev in devs:
|
|
||||||
xml = dev.get_xml()
|
|
||||||
|
|
||||||
if confirm:
|
|
||||||
if action == "hotplug":
|
|
||||||
prep = "to"
|
|
||||||
elif action == "hotunplug":
|
|
||||||
prep = "from"
|
|
||||||
else:
|
|
||||||
prep = "for"
|
|
||||||
|
|
||||||
msg = ("%s\n\n%s this device %s guest '%s'?" %
|
|
||||||
(xml, action.capitalize(), prep, domain.name()))
|
|
||||||
if not prompt_yes_or_no(msg):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if action == "hotplug":
|
|
||||||
setup_device(dev)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if action == "hotplug":
|
|
||||||
domain.attachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
|
||||||
elif action == "hotunplug":
|
|
||||||
domain.detachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
|
||||||
elif action == "update":
|
|
||||||
domain.updateDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
|
||||||
except libvirt.libvirtError as e:
|
|
||||||
fail(_("Error attempting device %s: %s") % (action, e))
|
|
||||||
|
|
||||||
# Test driver doesn't support device hotplug so we can't reach this
|
|
||||||
print_stdout(_("Device %s successful.") % action) # pragma: no cover
|
|
||||||
if confirm: # pragma: no cover
|
|
||||||
print_stdout("")
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_changes(xmlobj, options, parserclass):
|
|
||||||
origxml = xmlobj.get_xml()
|
|
||||||
|
|
||||||
if options.edit != -1:
|
|
||||||
devs = action_edit(xmlobj, options, parserclass)
|
|
||||||
action = "update"
|
|
||||||
|
|
||||||
elif options.add_device:
|
|
||||||
devs = action_add_device(xmlobj, options, parserclass)
|
|
||||||
action = "hotplug"
|
|
||||||
|
|
||||||
elif options.remove_device:
|
|
||||||
devs = action_remove_device(xmlobj, options, parserclass)
|
|
||||||
action = "hotunplug"
|
|
||||||
|
|
||||||
newxml = xmlobj.get_xml()
|
|
||||||
diff = get_diff(origxml, newxml)
|
|
||||||
|
|
||||||
if not diff:
|
|
||||||
log.warning(_("No XML diff was generated. The requested "
|
|
||||||
"changes will have no effect."))
|
|
||||||
|
|
||||||
if options.print_diff:
|
|
||||||
if diff:
|
|
||||||
print_stdout(diff)
|
|
||||||
elif options.print_xml:
|
|
||||||
print_stdout(newxml)
|
|
||||||
|
|
||||||
return devs, action
|
|
||||||
|
|
||||||
|
|
||||||
#######################
|
|
||||||
# CLI option handling #
|
|
||||||
#######################
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
parser = cli.setupParser(
|
|
||||||
"%(prog)s [options]",
|
|
||||||
_("Edit libvirt XML using command line options."),
|
|
||||||
introspection_epilog=True)
|
|
||||||
|
|
||||||
cli.add_connect_option(parser, "virt-xml")
|
|
||||||
|
|
||||||
parser.add_argument("domain", nargs='?',
|
|
||||||
help=_("Domain name, id, or uuid"))
|
|
||||||
|
|
||||||
actg = parser.add_argument_group(_("XML actions"))
|
|
||||||
actg.add_argument("--edit", nargs='?', default=-1,
|
|
||||||
help=_("Edit VM XML. Examples:\n"
|
|
||||||
"--edit --disk ... (edit first disk device)\n"
|
|
||||||
"--edit 2 --disk ... (edit second disk device)\n"
|
|
||||||
"--edit all --disk ... (edit all disk devices)\n"
|
|
||||||
"--edit target=hda --disk ... (edit disk 'hda')\n"))
|
|
||||||
actg.add_argument("--remove-device", action="store_true",
|
|
||||||
help=_("Remove specified device. Examples:\n"
|
|
||||||
"--remove-device --disk 1 (remove first disk)\n"
|
|
||||||
"--remove-device --disk all (remove all disks)\n"
|
|
||||||
"--remove-device --disk /some/path"))
|
|
||||||
actg.add_argument("--add-device", action="store_true",
|
|
||||||
help=_("Add specified device. Example:\n"
|
|
||||||
"--add-device --disk ..."))
|
|
||||||
actg.add_argument("--build-xml", action="store_true",
|
|
||||||
help=_("Output built device XML. Domain is optional but "
|
|
||||||
"recommended to ensure optimal defaults."))
|
|
||||||
|
|
||||||
outg = parser.add_argument_group(_("Output options"))
|
|
||||||
outg.add_argument("--update", action="store_true",
|
|
||||||
help=_("Apply changes to the running VM.\n"
|
|
||||||
"With --add-device, this is a hotplug operation.\n"
|
|
||||||
"With --remove-device, this is a hotunplug operation.\n"
|
|
||||||
"With --edit, this is an update device operation."))
|
|
||||||
define_g = outg.add_mutually_exclusive_group()
|
|
||||||
define_g.add_argument("--define", action="store_true",
|
|
||||||
help=_("Force defining the domain. Only required if a --print "
|
|
||||||
"option was specified."))
|
|
||||||
define_g.add_argument("--no-define", dest='define', action="store_false",
|
|
||||||
help=_("Force not defining the domain."))
|
|
||||||
define_g.set_defaults(define=None)
|
|
||||||
outg.add_argument("--start", action="store_true",
|
|
||||||
help=_("Start the domain."))
|
|
||||||
outg.add_argument("--print-diff", action="store_true",
|
|
||||||
help=_("Only print the requested change, in diff format"))
|
|
||||||
outg.add_argument("--print-xml", action="store_true",
|
|
||||||
help=_("Only print the requested change, in full XML format"))
|
|
||||||
outg.add_argument("--confirm", action="store_true",
|
|
||||||
help=_("Require confirmation before saving any results."))
|
|
||||||
|
|
||||||
cli.add_os_variant_option(parser, virtinstall=False)
|
|
||||||
|
|
||||||
g = parser.add_argument_group(_("XML options"))
|
|
||||||
cli.add_disk_option(g, editexample=True)
|
|
||||||
cli.add_net_option(g)
|
|
||||||
cli.add_gfx_option(g)
|
|
||||||
cli.add_metadata_option(g)
|
|
||||||
cli.add_memory_option(g)
|
|
||||||
cli.vcpu_cli_options(g, editexample=True)
|
|
||||||
cli.add_guest_xml_options(g)
|
|
||||||
cli.add_boot_options(g)
|
|
||||||
cli.add_device_options(g)
|
|
||||||
|
|
||||||
misc = parser.add_argument_group(_("Miscellaneous Options"))
|
|
||||||
cli.add_misc_options(misc, prompt=False, printxml=False, dryrun=False)
|
|
||||||
|
|
||||||
cli.autocomplete(parser)
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
###################
|
|
||||||
# main() handling #
|
|
||||||
###################
|
|
||||||
|
|
||||||
def main(conn=None):
|
|
||||||
cli.earlyLogging()
|
|
||||||
options = parse_args()
|
|
||||||
|
|
||||||
if (options.confirm or options.print_xml or
|
|
||||||
options.print_diff or options.build_xml):
|
|
||||||
options.quiet = False
|
|
||||||
cli.setupLogging("virt-xml", options.debug, options.quiet)
|
|
||||||
|
|
||||||
if cli.check_option_introspection(options):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
options.stdinxml = None
|
|
||||||
if not options.domain and not options.build_xml:
|
|
||||||
if not sys.stdin.closed and not sys.stdin.isatty():
|
|
||||||
if options.confirm:
|
|
||||||
fail(_("Can't use --confirm with stdin input."))
|
|
||||||
if options.update:
|
|
||||||
fail(_("Can't use --update with stdin input."))
|
|
||||||
options.stdinxml = sys.stdin.read()
|
|
||||||
else:
|
|
||||||
fail(_("A domain must be specified"))
|
|
||||||
|
|
||||||
# Default to --define, unless:
|
|
||||||
# --no-define explicitly specified
|
|
||||||
# --print-* option is used
|
|
||||||
# XML input came from stdin
|
|
||||||
if not options.print_xml and not options.print_diff:
|
|
||||||
if options.stdinxml:
|
|
||||||
if not options.define:
|
|
||||||
options.print_xml = True
|
|
||||||
else:
|
|
||||||
if options.define is None:
|
|
||||||
options.define = True
|
|
||||||
if options.confirm and not options.print_xml:
|
|
||||||
options.print_diff = True
|
|
||||||
|
|
||||||
# Ensure only one of these actions wash specified
|
|
||||||
# --edit
|
|
||||||
# --remove-device
|
|
||||||
# --add-device
|
|
||||||
# --build-xml
|
|
||||||
check_action_collision(options)
|
|
||||||
|
|
||||||
# Ensure there wasn't more than one device/xml config option
|
|
||||||
# specified. So reject '--disk X --network X'
|
|
||||||
parserclass = check_xmlopt_collision(options)
|
|
||||||
|
|
||||||
if options.update and not parserclass.guest_propname:
|
|
||||||
fail(_("Don't know how to --update for --%s") %
|
|
||||||
(parserclass.cli_arg_name))
|
|
||||||
|
|
||||||
conn = cli.getConnection(options.connect, conn)
|
|
||||||
|
|
||||||
domain = None
|
|
||||||
active_xmlobj = None
|
|
||||||
inactive_xmlobj = None
|
|
||||||
if options.domain:
|
|
||||||
domain, inactive_xmlobj, active_xmlobj = get_domain_and_guest(
|
|
||||||
conn, options.domain)
|
|
||||||
else:
|
|
||||||
inactive_xmlobj = virtinst.Guest(conn, parsexml=options.stdinxml)
|
|
||||||
vm_is_running = bool(active_xmlobj)
|
|
||||||
|
|
||||||
if options.build_xml:
|
|
||||||
devs = action_build_xml(conn, options, parserclass, inactive_xmlobj)
|
|
||||||
for dev in devs:
|
|
||||||
# pylint: disable=no-member
|
|
||||||
print_stdout(dev.get_xml())
|
|
||||||
return 0
|
|
||||||
|
|
||||||
performed_update = False
|
|
||||||
if options.update:
|
|
||||||
if options.update and options.start:
|
|
||||||
fail(_("Cannot mix --update and --start"))
|
|
||||||
|
|
||||||
if vm_is_running:
|
|
||||||
devs, action = prepare_changes(active_xmlobj, options, parserclass)
|
|
||||||
update_changes(domain, devs, action, options.confirm)
|
|
||||||
performed_update = True
|
|
||||||
else:
|
|
||||||
log.warning(
|
|
||||||
_("The VM is not running, --update is inapplicable."))
|
|
||||||
if not options.define:
|
|
||||||
# --update and --no-define passed, so we are done
|
|
||||||
# It's hard to hit this case with the test suite
|
|
||||||
return 0 # pragma: no cover
|
|
||||||
|
|
||||||
original_xml = inactive_xmlobj.get_xml()
|
|
||||||
devs, action = prepare_changes(inactive_xmlobj, options, parserclass)
|
|
||||||
if not options.define:
|
|
||||||
if options.start:
|
|
||||||
start_domain_transient(conn, inactive_xmlobj, devs,
|
|
||||||
action, options.confirm)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
dom = define_changes(conn, inactive_xmlobj,
|
|
||||||
devs, action, options.confirm)
|
|
||||||
if not dom:
|
|
||||||
# --confirm user said 'no'
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if options.start:
|
|
||||||
try:
|
|
||||||
dom.create()
|
|
||||||
except libvirt.libvirtError as e: # pragma: no cover
|
|
||||||
fail(_("Failed starting domain '%s': %s") % (
|
|
||||||
inactive_xmlobj.name, e))
|
|
||||||
print_stdout(_("Domain '%s' started successfully.") %
|
|
||||||
inactive_xmlobj.name)
|
|
||||||
|
|
||||||
elif vm_is_running and not performed_update:
|
|
||||||
print_stdout(
|
|
||||||
_("Changes will take effect after the domain is fully powered off."))
|
|
||||||
elif defined_xml_is_unchanged(conn, domain, original_xml):
|
|
||||||
log.warning(_("XML did not change after domain define. You may "
|
|
||||||
"have changed a value that libvirt is setting by default."))
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
|
||||||
try:
|
|
||||||
sys.exit(main())
|
|
||||||
except SystemExit as sys_e:
|
|
||||||
sys.exit(sys_e.code)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
log.debug("", exc_info=True)
|
|
||||||
print_stderr(_("Aborted at user request"))
|
|
||||||
except Exception as main_e:
|
|
||||||
fail(main_e)
|
|
||||||
|
342
virtManager/virtmanager.py
Executable file
342
virtManager/virtmanager.py
Executable file
@ -0,0 +1,342 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (C) 2006, 2014 Red Hat, Inc.
|
||||||
|
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
|
||||||
|
#
|
||||||
|
# This work is licensed under the GNU GPLv2 or later.
|
||||||
|
# See the COPYING file in the top-level directory.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('LibvirtGLib', '1.0')
|
||||||
|
from gi.repository import LibvirtGLib
|
||||||
|
|
||||||
|
from virtinst import BuildConfig
|
||||||
|
from virtinst import VirtinstConnection
|
||||||
|
from virtinst import cli
|
||||||
|
from virtinst import log
|
||||||
|
|
||||||
|
# This is massively heavy handed, but I can't figure out any way to shut
|
||||||
|
# up the slew of gtk deprecation warnings that clog up our very useful
|
||||||
|
# stdout --debug output. Of course we could drop use of deprecated APIs,
|
||||||
|
# but it's a serious quantity of churn
|
||||||
|
import warnings # pylint: disable=wrong-import-order
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
|
||||||
|
try:
|
||||||
|
gi.check_version("3.22.0")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
print("pygobject3 3.22.0 or later is required.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_startup_error(msg, details):
|
||||||
|
log.debug("Error starting virt-manager: %s\n%s", msg, details,
|
||||||
|
exc_info=True)
|
||||||
|
from .error import vmmErrorDialog
|
||||||
|
err = vmmErrorDialog.get_instance()
|
||||||
|
title = _("Error starting Virtual Machine Manager")
|
||||||
|
err.show_err(title + ": " + msg,
|
||||||
|
details=details,
|
||||||
|
title=title,
|
||||||
|
modal=True,
|
||||||
|
debug=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_gtk(leftovers):
|
||||||
|
# The never ending fork+gsettings problems now require us to
|
||||||
|
# import Gtk _after_ the fork. This creates a funny race, since we
|
||||||
|
# need to parse the command line arguments to know if we need to
|
||||||
|
# fork, but need to import Gtk before cli processing so it can
|
||||||
|
# handle --g-fatal-args. We strip out our flags first and pass the
|
||||||
|
# left overs to gtk
|
||||||
|
origargv = sys.argv
|
||||||
|
try:
|
||||||
|
sys.argv = origargv[:1] + leftovers[:]
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
leftovers = sys.argv[1:]
|
||||||
|
|
||||||
|
if Gtk.check_version(3, 22, 0):
|
||||||
|
print("gtk3 3.22.0 or later is required.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# This will error if Gtk wasn't correctly initialized
|
||||||
|
Gtk.init()
|
||||||
|
globals()["Gtk"] = Gtk
|
||||||
|
|
||||||
|
# This ensures we can init gsettings correctly
|
||||||
|
from . import config
|
||||||
|
ignore = config
|
||||||
|
except Exception as e:
|
||||||
|
# Don't just let the exception raise here. abrt reports bugs
|
||||||
|
# when users mess up su/sudo and DISPLAY isn't set. Printing
|
||||||
|
# it avoids the issue
|
||||||
|
display = os.environ.get("DISPLAY", "")
|
||||||
|
msg = str(e)
|
||||||
|
if display:
|
||||||
|
msg += ": Could not open display: %s" % display
|
||||||
|
log.debug("".join(traceback.format_exc()))
|
||||||
|
print(msg)
|
||||||
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
sys.argv = origargv
|
||||||
|
|
||||||
|
return leftovers
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_gsettings_path(schemadir):
|
||||||
|
"""
|
||||||
|
If running from the virt-manager.git srcdir, compile our gsettings
|
||||||
|
schema and use it directly
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
exe = shutil.which("glib-compile-schemas")
|
||||||
|
if not exe: # pragma: no cover
|
||||||
|
raise RuntimeError("You must install glib-compile-schemas to run "
|
||||||
|
"virt-manager from git.")
|
||||||
|
subprocess.check_call([exe, "--strict", schemadir])
|
||||||
|
|
||||||
|
|
||||||
|
def drop_tty():
|
||||||
|
# We fork and setsid so that we drop the controlling
|
||||||
|
# tty. This prevents libvirt's SSH tunnels from prompting
|
||||||
|
# for user input if SSH keys/agent aren't configured.
|
||||||
|
if os.fork() != 0:
|
||||||
|
os._exit(0) # pylint: disable=protected-access
|
||||||
|
|
||||||
|
os.setsid()
|
||||||
|
|
||||||
|
|
||||||
|
def drop_stdio():
|
||||||
|
# This is part of the fork process described in drop_tty()
|
||||||
|
for fd in range(0, 2):
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
os.open(os.devnull, os.O_RDWR)
|
||||||
|
os.dup2(0, 1)
|
||||||
|
os.dup2(0, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_commandline():
|
||||||
|
epilog = ("Also accepts standard GTK arguments like --g-fatal-warnings")
|
||||||
|
parser = argparse.ArgumentParser(usage="virt-manager [options]",
|
||||||
|
epilog=epilog)
|
||||||
|
parser.add_argument('--version', action='version',
|
||||||
|
version=BuildConfig.version)
|
||||||
|
parser.set_defaults(domain=None)
|
||||||
|
|
||||||
|
# Trace every libvirt API call to debug output
|
||||||
|
parser.add_argument("--trace-libvirt", choices=["all", "mainloop"],
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
# Don't load any connections on startup to test first run
|
||||||
|
# PackageKit integration
|
||||||
|
parser.add_argument("--test-first-run",
|
||||||
|
help=argparse.SUPPRESS, action="store_true")
|
||||||
|
# Force disable use of libvirt object events
|
||||||
|
parser.add_argument("--test-no-events",
|
||||||
|
help=argparse.SUPPRESS, action="store_true")
|
||||||
|
# Enabling this will tell us, at app exit time, which vmmGObjects were not
|
||||||
|
# garbage collected. This is caused by circular references to other objects,
|
||||||
|
# like a signal that wasn't disconnected. It's not a big deal, but if we
|
||||||
|
# have objects that can be created and destroyed a lot over the course of
|
||||||
|
# the app lifecycle, every non-garbage collected class is a memory leak.
|
||||||
|
# So it's nice to poke at this every now and then and try to track down
|
||||||
|
# what we need to add to class _cleanup handling.
|
||||||
|
parser.add_argument("--test-leak-debug",
|
||||||
|
help=argparse.SUPPRESS, action="store_true")
|
||||||
|
|
||||||
|
# comma separated string of options to tweak app behavior,
|
||||||
|
# for manual and automated testing config
|
||||||
|
parser.add_argument("--test-options", help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument("-c", "--connect", dest="uri",
|
||||||
|
help="Connect to hypervisor at URI", metavar="URI")
|
||||||
|
parser.add_argument("--debug", action="store_true",
|
||||||
|
help="Print debug output to stdout (implies --no-fork)",
|
||||||
|
default=False)
|
||||||
|
parser.add_argument("--no-fork", action="store_true",
|
||||||
|
help="Don't fork into background on startup")
|
||||||
|
|
||||||
|
parser.add_argument("--show-domain-creator", action="store_true",
|
||||||
|
help="Show 'New VM' wizard")
|
||||||
|
parser.add_argument("--show-domain-editor", metavar="NAME|ID|UUID",
|
||||||
|
help="Show domain details window")
|
||||||
|
parser.add_argument("--show-domain-performance", metavar="NAME|ID|UUID",
|
||||||
|
help="Show domain performance window")
|
||||||
|
parser.add_argument("--show-domain-console", metavar="NAME|ID|UUID",
|
||||||
|
help="Show domain graphical console window")
|
||||||
|
parser.add_argument("--show-domain-delete", metavar="NAME|ID|UUID",
|
||||||
|
help="Show domain delete window")
|
||||||
|
parser.add_argument("--show-host-summary", action="store_true",
|
||||||
|
help="Show connection details window")
|
||||||
|
|
||||||
|
return parser.parse_known_args()
|
||||||
|
|
||||||
|
|
||||||
|
class CLITestOptionsClass:
|
||||||
|
"""
|
||||||
|
Helper class for parsing and tracking --test-* options
|
||||||
|
"""
|
||||||
|
def __init__(self, test_options_str):
|
||||||
|
opts = []
|
||||||
|
if test_options_str:
|
||||||
|
opts = test_options_str.split(",")
|
||||||
|
|
||||||
|
def _get(optname):
|
||||||
|
if optname not in opts:
|
||||||
|
return False
|
||||||
|
opts.remove(optname)
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.first_run = _get("first-run")
|
||||||
|
self.leak_debug = _get("leak-debug")
|
||||||
|
self.no_events = _get("no-events")
|
||||||
|
self.xmleditor_enabled = _get("xmleditor-enabled")
|
||||||
|
|
||||||
|
if opts:
|
||||||
|
print("Unknown --test-options keys: %s" % opts)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(options, leftovers) = parse_commandline()
|
||||||
|
|
||||||
|
cli.setupLogging("virt-manager", options.debug, False, False)
|
||||||
|
|
||||||
|
log.debug("virt-manager version: %s", BuildConfig.version)
|
||||||
|
log.debug("virtManager import: %s", os.path.dirname(__file__))
|
||||||
|
|
||||||
|
if BuildConfig.running_from_srcdir:
|
||||||
|
_setup_gsettings_path(BuildConfig.gsettings_dir)
|
||||||
|
|
||||||
|
if options.trace_libvirt:
|
||||||
|
log.debug("Libvirt tracing requested")
|
||||||
|
from .lib import module_trace
|
||||||
|
import libvirt
|
||||||
|
module_trace.wrap_module(libvirt,
|
||||||
|
mainloop=(options.trace_libvirt == "mainloop"),
|
||||||
|
regex=None)
|
||||||
|
|
||||||
|
CLITestOptions = CLITestOptionsClass(options.test_options)
|
||||||
|
if options.test_first_run:
|
||||||
|
CLITestOptions.first_run = True
|
||||||
|
if options.test_leak_debug:
|
||||||
|
CLITestOptions.leak_debug = True
|
||||||
|
if options.test_no_events:
|
||||||
|
CLITestOptions.no_events = True
|
||||||
|
|
||||||
|
# With F27 gnome+wayland we need to set these before GTK import
|
||||||
|
os.environ["GSETTINGS_SCHEMA_DIR"] = BuildConfig.gsettings_dir
|
||||||
|
if CLITestOptions.first_run:
|
||||||
|
os.environ["GSETTINGS_BACKEND"] = "memory"
|
||||||
|
|
||||||
|
# Now we've got basic environment up & running we can fork
|
||||||
|
do_drop_stdio = False
|
||||||
|
if not options.no_fork and not options.debug:
|
||||||
|
drop_tty()
|
||||||
|
do_drop_stdio = True
|
||||||
|
|
||||||
|
# Ignore SIGHUP, otherwise a serial console closing drops the whole app
|
||||||
|
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
|
|
||||||
|
leftovers = _import_gtk(leftovers)
|
||||||
|
Gtk = globals()["Gtk"]
|
||||||
|
|
||||||
|
# Do this after the Gtk import so the user has a chance of seeing any error
|
||||||
|
if do_drop_stdio:
|
||||||
|
drop_stdio()
|
||||||
|
|
||||||
|
if leftovers:
|
||||||
|
raise RuntimeError("Unhandled command line options '%s'" % leftovers)
|
||||||
|
|
||||||
|
log.debug("PyGObject version: %d.%d.%d",
|
||||||
|
gi.version_info[0],
|
||||||
|
gi.version_info[1],
|
||||||
|
gi.version_info[2])
|
||||||
|
log.debug("GTK version: %d.%d.%d",
|
||||||
|
Gtk.get_major_version(),
|
||||||
|
Gtk.get_minor_version(),
|
||||||
|
Gtk.get_micro_version())
|
||||||
|
|
||||||
|
if not VirtinstConnection.libvirt_new_enough_for_virtmanager(6000):
|
||||||
|
# We need this version for threaded virConnect access
|
||||||
|
_show_startup_error(
|
||||||
|
_("virt-manager requires libvirt 0.6.0 or later."), "")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prime the vmmConfig cache
|
||||||
|
from . import config
|
||||||
|
config.vmmConfig.get_instance(BuildConfig, CLITestOptions)
|
||||||
|
|
||||||
|
# Add our icon dir to icon theme
|
||||||
|
icon_theme = Gtk.IconTheme.get_default()
|
||||||
|
icon_theme.prepend_search_path(BuildConfig.icon_dir)
|
||||||
|
|
||||||
|
from .engine import vmmEngine
|
||||||
|
Gtk.Window.set_default_icon_name("virt-manager")
|
||||||
|
|
||||||
|
show_window = None
|
||||||
|
domain = None
|
||||||
|
if options.show_domain_creator:
|
||||||
|
show_window = vmmEngine.CLI_SHOW_DOMAIN_CREATOR
|
||||||
|
elif options.show_host_summary:
|
||||||
|
show_window = vmmEngine.CLI_SHOW_HOST_SUMMARY
|
||||||
|
elif options.show_domain_editor:
|
||||||
|
show_window = vmmEngine.CLI_SHOW_DOMAIN_EDITOR
|
||||||
|
domain = options.show_domain_editor
|
||||||
|
elif options.show_domain_performance:
|
||||||
|
show_window = vmmEngine.CLI_SHOW_DOMAIN_PERFORMANCE
|
||||||
|
domain = options.show_domain_performance
|
||||||
|
elif options.show_domain_console:
|
||||||
|
show_window = vmmEngine.CLI_SHOW_DOMAIN_CONSOLE
|
||||||
|
domain = options.show_domain_console
|
||||||
|
elif options.show_domain_delete:
|
||||||
|
show_window = vmmEngine.CLI_SHOW_DOMAIN_DELETE
|
||||||
|
domain = options.show_domain_delete
|
||||||
|
|
||||||
|
if show_window and options.uri is None:
|
||||||
|
raise RuntimeError("can't use --show-* options without --connect")
|
||||||
|
|
||||||
|
skip_autostart = False
|
||||||
|
if show_window:
|
||||||
|
skip_autostart = True
|
||||||
|
|
||||||
|
# Hook libvirt events into glib main loop
|
||||||
|
LibvirtGLib.init(None)
|
||||||
|
LibvirtGLib.event_register()
|
||||||
|
|
||||||
|
engine = vmmEngine.get_instance()
|
||||||
|
|
||||||
|
# Actually exit when we receive ctrl-c
|
||||||
|
from gi.repository import GLib
|
||||||
|
def _sigint_handler(user_data):
|
||||||
|
ignore = user_data
|
||||||
|
log.debug("Received KeyboardInterrupt. Exiting application.")
|
||||||
|
engine.exit_app()
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
|
||||||
|
_sigint_handler, None)
|
||||||
|
|
||||||
|
engine.start(options.uri, show_window, domain, skip_autostart)
|
||||||
|
|
||||||
|
|
||||||
|
def runcli():
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.debug("Received KeyboardInterrupt. Exiting application.")
|
||||||
|
except Exception as run_e:
|
||||||
|
if "Gtk" not in globals():
|
||||||
|
raise
|
||||||
|
_show_startup_error(str(run_e), "".join(traceback.format_exc()))
|
218
virtinst/virtclone.py
Normal file
218
virtinst/virtclone.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright(c) FUJITSU Limited 2007.
|
||||||
|
#
|
||||||
|
# This work is licensed under the GNU GPLv2 or later.
|
||||||
|
# See the COPYING file in the top-level directory.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import cli
|
||||||
|
from .cli import fail, print_stdout, print_stderr
|
||||||
|
from .cloner import Cloner
|
||||||
|
from .logger import log
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
new_name = design.generate_clone_name()
|
||||||
|
log.debug("Auto-generated clone name '%s'", new_name)
|
||||||
|
|
||||||
|
if not new_name:
|
||||||
|
fail(_("A name is required for the new virtual machine,"
|
||||||
|
" use '--name NEW_VM_NAME' to specify one."))
|
||||||
|
design.clone_name = new_name
|
||||||
|
|
||||||
|
|
||||||
|
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) as e: # pragma: no cover
|
||||||
|
fail(e)
|
||||||
|
|
||||||
|
if not guest_name:
|
||||||
|
fail(_("An original machine name is required,"
|
||||||
|
" use '--original ORIGINAL_GUEST' and try again."))
|
||||||
|
design.original_guest = guest_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_clone_macaddr(new_mac, design):
|
||||||
|
if new_mac is None or new_mac[0] == "RANDOM":
|
||||||
|
return
|
||||||
|
design.clone_macs = new_mac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_clone_diskfile(new_diskfiles, design, preserve, auto_clone):
|
||||||
|
if new_diskfiles is None:
|
||||||
|
new_diskfiles = [None]
|
||||||
|
|
||||||
|
newidx = 0
|
||||||
|
clonepaths = []
|
||||||
|
for origpath in [d.path for d in design.original_disks]:
|
||||||
|
if len(new_diskfiles) <= newidx:
|
||||||
|
# Extend the new/passed paths list with None if it's not
|
||||||
|
# long enough
|
||||||
|
new_diskfiles.append(None)
|
||||||
|
newpath = new_diskfiles[newidx]
|
||||||
|
|
||||||
|
if origpath is None:
|
||||||
|
newpath = None
|
||||||
|
elif newpath is None and auto_clone:
|
||||||
|
newpath = design.generate_clone_disk_path(origpath)
|
||||||
|
|
||||||
|
clonepaths.append(newpath)
|
||||||
|
newidx += 1
|
||||||
|
design.clone_paths = clonepaths
|
||||||
|
|
||||||
|
for disk in design.clone_disks:
|
||||||
|
cli.validate_disk(disk, warn_overwrite=not preserve)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
desc = _("Duplicate a virtual machine, changing all the unique "
|
||||||
|
"host side configuration like MAC address, name, etc. \n\n"
|
||||||
|
"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)
|
||||||
|
cli.add_connect_option(parser)
|
||||||
|
|
||||||
|
geng = parser.add_argument_group(_("General Options"))
|
||||||
|
geng.add_argument("-o", "--original", dest="original_guest",
|
||||||
|
help=_("Name of the original guest; "
|
||||||
|
"The status must be shut off or paused."))
|
||||||
|
geng.add_argument("--original-xml",
|
||||||
|
help=_("XML file to use as the original guest."))
|
||||||
|
geng.add_argument("--auto-clone", action="store_true",
|
||||||
|
help=_("Auto generate clone name and storage paths from"
|
||||||
|
" the original guest configuration."))
|
||||||
|
geng.add_argument("-n", "--name", dest="new_name",
|
||||||
|
help=_("Name for the new guest"))
|
||||||
|
geng.add_argument("-u", "--uuid", dest="new_uuid", help=argparse.SUPPRESS)
|
||||||
|
geng.add_argument("--reflink", action="store_true",
|
||||||
|
help=_("use btrfs COW lightweight copy"))
|
||||||
|
|
||||||
|
stog = parser.add_argument_group(_("Storage Configuration"))
|
||||||
|
stog.add_argument("-f", "--file", dest="new_diskfile", action="append",
|
||||||
|
help=_("New file to use as the disk image for the "
|
||||||
|
"new guest"))
|
||||||
|
stog.add_argument("--force-copy", dest="target", action="append",
|
||||||
|
help=_("Force to copy devices (eg, if 'hdc' is a "
|
||||||
|
"readonly cdrom device, --force-copy=hdc)"))
|
||||||
|
stog.add_argument("--skip-copy", action="append",
|
||||||
|
help=_("Skip copy of the device target. (eg, if 'vda' is a "
|
||||||
|
"disk you don't want to copy and use the same path "
|
||||||
|
"in the new VM, use --skip-copy=vda)"))
|
||||||
|
stog.add_argument("--nonsparse", action="store_false", dest="sparse",
|
||||||
|
default=True,
|
||||||
|
help=_("Do not use a sparse file for the clone's "
|
||||||
|
"disk image"))
|
||||||
|
stog.add_argument("--preserve-data", action="store_false",
|
||||||
|
dest="preserve", default=True,
|
||||||
|
help=_("Do not clone storage, new disk images specified "
|
||||||
|
"via --file are preserved unchanged"))
|
||||||
|
stog.add_argument("--nvram", dest="new_nvram",
|
||||||
|
help=_("New file to use as storage for nvram VARS"))
|
||||||
|
|
||||||
|
netg = parser.add_argument_group(_("Networking Configuration"))
|
||||||
|
netg.add_argument("-m", "--mac", dest="new_mac", action="append",
|
||||||
|
help=_("New fixed MAC address for the clone guest. "
|
||||||
|
"Default is a randomly generated MAC"))
|
||||||
|
|
||||||
|
misc = parser.add_argument_group(_("Miscellaneous Options"))
|
||||||
|
|
||||||
|
# Just used for clone tests
|
||||||
|
misc.add_argument("--clone-running", action="store_true",
|
||||||
|
default=False, help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
cli.add_misc_options(misc, prompt=True, replace=True, printxml=True)
|
||||||
|
|
||||||
|
cli.autocomplete(parser)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main(conn=None):
|
||||||
|
cli.earlyLogging()
|
||||||
|
options = parse_args()
|
||||||
|
|
||||||
|
options.quiet = options.quiet or options.xmlonly
|
||||||
|
cli.setupLogging("virt-clone", options.debug, options.quiet)
|
||||||
|
|
||||||
|
cli.convert_old_force(options)
|
||||||
|
cli.parse_check(options.check)
|
||||||
|
cli.set_prompt(options.prompt)
|
||||||
|
conn = cli.getConnection(options.connect, conn=conn)
|
||||||
|
|
||||||
|
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."))
|
||||||
|
|
||||||
|
design = Cloner(conn)
|
||||||
|
|
||||||
|
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)
|
||||||
|
if options.new_uuid is not None:
|
||||||
|
design.clone_uuid = options.new_uuid
|
||||||
|
if options.reflink is True:
|
||||||
|
design.reflink = True
|
||||||
|
for i in options.target or []:
|
||||||
|
design.force_target = i
|
||||||
|
for i in options.skip_copy or []:
|
||||||
|
design.skip_target = i
|
||||||
|
design.clone_sparse = options.sparse
|
||||||
|
design.preserve = options.preserve
|
||||||
|
|
||||||
|
design.clone_nvram = options.new_nvram
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
get_clone_diskfile(options.new_diskfile, design,
|
||||||
|
not options.preserve, options.auto_clone)
|
||||||
|
|
||||||
|
# setup design object
|
||||||
|
design.setup_clone()
|
||||||
|
|
||||||
|
if options.xmlonly:
|
||||||
|
print_stdout(design.clone_xml, do_force=True)
|
||||||
|
else:
|
||||||
|
design.start_duplicate(cli.get_meter())
|
||||||
|
|
||||||
|
print_stdout("")
|
||||||
|
print_stdout(_("Clone '%s' created successfully.") % design.clone_name)
|
||||||
|
log.debug("end clone")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def runcli(): # pragma: no cover
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except SystemExit as sys_e:
|
||||||
|
sys.exit(sys_e.code)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print_stderr(_("Installation aborted at user request"))
|
||||||
|
except Exception as main_e:
|
||||||
|
fail(main_e)
|
1049
virtinst/virtinstall.py
Normal file
1049
virtinst/virtinstall.py
Normal file
File diff suppressed because it is too large
Load Diff
573
virtinst/virtxml.py
Normal file
573
virtinst/virtxml.py
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright 2013-2014 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# This work is licensed under the GNU GPLv2 or later.
|
||||||
|
# See the COPYING file in the top-level directory.
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import libvirt
|
||||||
|
|
||||||
|
from . import cli
|
||||||
|
from .cli import fail, print_stdout, print_stderr
|
||||||
|
from .guest import Guest
|
||||||
|
from .logger import log
|
||||||
|
from . import xmlutil
|
||||||
|
|
||||||
|
|
||||||
|
###################
|
||||||
|
# Utility helpers #
|
||||||
|
###################
|
||||||
|
|
||||||
|
def prompt_yes_or_no(msg):
|
||||||
|
while 1:
|
||||||
|
printmsg = msg + " (y/n): "
|
||||||
|
sys.stdout.write(printmsg)
|
||||||
|
sys.stdout.flush()
|
||||||
|
inp = sys.stdin.readline().lower().strip()
|
||||||
|
|
||||||
|
if inp in ["y", "yes"]:
|
||||||
|
return True
|
||||||
|
elif inp in ["n", "no"]:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print_stdout(_("Please enter 'yes' or 'no'."))
|
||||||
|
|
||||||
|
|
||||||
|
def get_diff(origxml, newxml):
|
||||||
|
ret = "".join(difflib.unified_diff(origxml.splitlines(1),
|
||||||
|
newxml.splitlines(1),
|
||||||
|
fromfile="Original XML",
|
||||||
|
tofile="Altered XML"))
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
log.debug("XML diff:\n%s", ret)
|
||||||
|
else:
|
||||||
|
log.debug("No XML diff, didn't generate any change.")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def set_os_variant(options, guest):
|
||||||
|
if options.os_variant is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
osdata = cli.parse_os_variant(options.os_variant)
|
||||||
|
if osdata.name:
|
||||||
|
guest.set_os_name(osdata.name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_xmldesc(domain, inactive=False):
|
||||||
|
flags = libvirt.VIR_DOMAIN_XML_SECURE
|
||||||
|
if inactive:
|
||||||
|
flags |= libvirt.VIR_DOMAIN_XML_INACTIVE
|
||||||
|
return domain.XMLDesc(flags)
|
||||||
|
|
||||||
|
|
||||||
|
def get_domain_and_guest(conn, domstr):
|
||||||
|
try:
|
||||||
|
int(domstr)
|
||||||
|
isint = True
|
||||||
|
except ValueError:
|
||||||
|
isint = False
|
||||||
|
|
||||||
|
uuidre = "[a-fA-F0-9]{8}[-]([a-fA-F0-9]{4}[-]){3}[a-fA-F0-9]{12}$"
|
||||||
|
isuuid = bool(re.match(uuidre, domstr))
|
||||||
|
|
||||||
|
try:
|
||||||
|
domain = None
|
||||||
|
try:
|
||||||
|
domain = conn.lookupByName(domstr)
|
||||||
|
except Exception:
|
||||||
|
# In case the VM has a UUID or ID for a name
|
||||||
|
log.debug("Error looking up domain by name", exc_info=True)
|
||||||
|
if isint:
|
||||||
|
domain = conn.lookupByID(int(domstr))
|
||||||
|
elif isuuid:
|
||||||
|
domain = conn.lookupByUUIDString(domstr)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
fail(_("Could not find domain '%s': %s") % (domstr, e))
|
||||||
|
|
||||||
|
state = domain.info()[0]
|
||||||
|
active_xmlobj = None
|
||||||
|
inactive_xmlobj = Guest(conn, parsexml=get_xmldesc(domain))
|
||||||
|
if state != libvirt.VIR_DOMAIN_SHUTOFF:
|
||||||
|
active_xmlobj = inactive_xmlobj
|
||||||
|
inactive_xmlobj = Guest(conn,
|
||||||
|
parsexml=get_xmldesc(domain, inactive=True))
|
||||||
|
|
||||||
|
return (domain, inactive_xmlobj, active_xmlobj)
|
||||||
|
|
||||||
|
|
||||||
|
def defined_xml_is_unchanged(conn, domain, original_xml):
|
||||||
|
rawxml = get_xmldesc(domain, inactive=True)
|
||||||
|
new_xml = Guest(conn, parsexml=rawxml).get_xml()
|
||||||
|
return new_xml == original_xml
|
||||||
|
|
||||||
|
|
||||||
|
################
|
||||||
|
# Change logic #
|
||||||
|
################
|
||||||
|
|
||||||
|
def _find_objects_to_edit(guest, action_name, editval, parserclass):
|
||||||
|
objlist = xmlutil.listify(parserclass.lookup_prop(guest))
|
||||||
|
idx = None
|
||||||
|
|
||||||
|
if editval is None:
|
||||||
|
idx = 1
|
||||||
|
elif (editval.isdigit() or
|
||||||
|
editval.startswith("-") and editval[1:].isdigit()):
|
||||||
|
idx = int(editval)
|
||||||
|
|
||||||
|
if idx is not None:
|
||||||
|
# Edit device by index
|
||||||
|
if idx == 0:
|
||||||
|
fail(_("Invalid --edit option '%s'") % editval)
|
||||||
|
|
||||||
|
if not objlist:
|
||||||
|
fail(_("No --%s objects found in the XML") %
|
||||||
|
parserclass.cli_arg_name)
|
||||||
|
if len(objlist) < abs(idx):
|
||||||
|
fail(_("--edit %s requested but there's only %s "
|
||||||
|
"--%s object in the XML") %
|
||||||
|
(idx, len(objlist), parserclass.cli_arg_name))
|
||||||
|
|
||||||
|
if idx > 0:
|
||||||
|
idx -= 1
|
||||||
|
inst = objlist[idx]
|
||||||
|
|
||||||
|
elif editval == "all":
|
||||||
|
# Edit 'all' devices
|
||||||
|
inst = objlist[:]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Lookup device by the passed prop string
|
||||||
|
parserobj = parserclass(editval, guest=guest)
|
||||||
|
inst = parserobj.lookup_child_from_option_string()
|
||||||
|
if not inst:
|
||||||
|
fail(_("No matching objects found for --%s %s") %
|
||||||
|
(action_name, editval))
|
||||||
|
|
||||||
|
return inst
|
||||||
|
|
||||||
|
|
||||||
|
def check_action_collision(options):
|
||||||
|
actions = ["edit", "add-device", "remove-device", "build-xml"]
|
||||||
|
|
||||||
|
collisions = []
|
||||||
|
for cliname in actions:
|
||||||
|
optname = cliname.replace("-", "_")
|
||||||
|
if getattr(options, optname) not in [False, -1]:
|
||||||
|
collisions.append(cliname)
|
||||||
|
|
||||||
|
if len(collisions) == 0:
|
||||||
|
fail(_("One of %s must be specified.") %
|
||||||
|
", ".join(["--" + c for c in actions]))
|
||||||
|
if len(collisions) > 1:
|
||||||
|
fail(_("Conflicting options %s") %
|
||||||
|
", ".join(["--" + c for c in collisions]))
|
||||||
|
|
||||||
|
|
||||||
|
def check_xmlopt_collision(options):
|
||||||
|
collisions = []
|
||||||
|
for parserclass in cli.VIRT_PARSERS:
|
||||||
|
if getattr(options, parserclass.cli_arg_name):
|
||||||
|
collisions.append(parserclass)
|
||||||
|
|
||||||
|
if len(collisions) == 0:
|
||||||
|
fail(_("No change specified."))
|
||||||
|
if len(collisions) != 1:
|
||||||
|
fail(_("Only one change operation may be specified "
|
||||||
|
"(conflicting options %s)") %
|
||||||
|
[c.cli_flag_name() for c in collisions])
|
||||||
|
|
||||||
|
return collisions[0]
|
||||||
|
|
||||||
|
|
||||||
|
def action_edit(guest, options, parserclass):
|
||||||
|
if parserclass.guest_propname:
|
||||||
|
inst = _find_objects_to_edit(guest, "edit", options.edit, parserclass)
|
||||||
|
else:
|
||||||
|
inst = guest
|
||||||
|
if options.edit and options.edit != '1' and options.edit != 'all':
|
||||||
|
fail(_("'--edit %s' doesn't make sense with --%s, "
|
||||||
|
"just use empty '--edit'") %
|
||||||
|
(options.edit, parserclass.cli_arg_name))
|
||||||
|
if options.os_variant is not None:
|
||||||
|
fail(_("--os-variant is not supported with --edit"))
|
||||||
|
|
||||||
|
return cli.parse_option_strings(options, guest, inst, editing=True)
|
||||||
|
|
||||||
|
|
||||||
|
def action_add_device(guest, options, parserclass):
|
||||||
|
if not parserclass.prop_is_list(guest):
|
||||||
|
fail(_("Cannot use --add-device with --%s") % parserclass.cli_arg_name)
|
||||||
|
set_os_variant(options, guest)
|
||||||
|
devs = cli.parse_option_strings(options, guest, None)
|
||||||
|
devs = xmlutil.listify(devs)
|
||||||
|
for dev in devs:
|
||||||
|
dev.set_defaults(guest)
|
||||||
|
return devs
|
||||||
|
|
||||||
|
|
||||||
|
def action_remove_device(guest, options, parserclass):
|
||||||
|
if not parserclass.prop_is_list(guest):
|
||||||
|
fail(_("Cannot use --remove-device with --%s") %
|
||||||
|
parserclass.cli_arg_name)
|
||||||
|
if options.os_variant is not None:
|
||||||
|
fail(_("--os-variant is not supported with --remove-device"))
|
||||||
|
|
||||||
|
devs = _find_objects_to_edit(guest, "remove-device",
|
||||||
|
getattr(options, parserclass.cli_arg_name)[-1], parserclass)
|
||||||
|
|
||||||
|
devs = xmlutil.listify(devs)
|
||||||
|
for dev in devs:
|
||||||
|
guest.remove_device(dev)
|
||||||
|
return devs
|
||||||
|
|
||||||
|
|
||||||
|
def action_build_xml(conn, options, parserclass, guest):
|
||||||
|
if not parserclass.guest_propname:
|
||||||
|
fail(_("--build-xml not supported for --%s") %
|
||||||
|
parserclass.cli_arg_name)
|
||||||
|
if options.os_variant is not None:
|
||||||
|
fail(_("--os-variant is not supported with --build-xml"))
|
||||||
|
|
||||||
|
inst = parserclass.lookup_prop(guest)
|
||||||
|
if parserclass.prop_is_list(guest):
|
||||||
|
inst = inst.new()
|
||||||
|
else:
|
||||||
|
inst = inst.__class__(conn)
|
||||||
|
|
||||||
|
devs = cli.parse_option_strings(options, guest, inst)
|
||||||
|
devs = xmlutil.listify(devs)
|
||||||
|
for dev in devs:
|
||||||
|
dev.set_defaults(guest)
|
||||||
|
return devs
|
||||||
|
|
||||||
|
|
||||||
|
def setup_device(dev):
|
||||||
|
if getattr(dev, "DEVICE_TYPE", None) != "disk":
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug("Doing setup for disk=%s", dev)
|
||||||
|
dev.build_storage(cli.get_meter())
|
||||||
|
|
||||||
|
|
||||||
|
def define_changes(conn, inactive_xmlobj, devs, action, confirm):
|
||||||
|
if confirm:
|
||||||
|
if not prompt_yes_or_no(
|
||||||
|
_("Define '%s' with the changed XML?") % inactive_xmlobj.name):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if action == "hotplug":
|
||||||
|
for dev in devs:
|
||||||
|
setup_device(dev)
|
||||||
|
|
||||||
|
dom = conn.defineXML(inactive_xmlobj.get_xml())
|
||||||
|
print_stdout(_("Domain '%s' defined successfully.") % inactive_xmlobj.name)
|
||||||
|
return dom
|
||||||
|
|
||||||
|
|
||||||
|
def start_domain_transient(conn, xmlobj, devs, action, confirm):
|
||||||
|
if confirm:
|
||||||
|
if not prompt_yes_or_no(
|
||||||
|
_("Start '%s' with the changed XML?") % xmlobj.name):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if action == "hotplug":
|
||||||
|
for dev in devs:
|
||||||
|
setup_device(dev)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dom = conn.createXML(xmlobj.get_xml())
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
fail(_("Failed starting domain '%s': %s") % (xmlobj.name, e))
|
||||||
|
else:
|
||||||
|
print_stdout(_("Domain '%s' started successfully.") % xmlobj.name)
|
||||||
|
return dom
|
||||||
|
|
||||||
|
|
||||||
|
def update_changes(domain, devs, action, confirm):
|
||||||
|
for dev in devs:
|
||||||
|
xml = dev.get_xml()
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
if action == "hotplug":
|
||||||
|
prep = "to"
|
||||||
|
elif action == "hotunplug":
|
||||||
|
prep = "from"
|
||||||
|
else:
|
||||||
|
prep = "for"
|
||||||
|
|
||||||
|
msg = ("%s\n\n%s this device %s guest '%s'?" %
|
||||||
|
(xml, action.capitalize(), prep, domain.name()))
|
||||||
|
if not prompt_yes_or_no(msg):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if action == "hotplug":
|
||||||
|
setup_device(dev)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if action == "hotplug":
|
||||||
|
domain.attachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
||||||
|
elif action == "hotunplug":
|
||||||
|
domain.detachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
||||||
|
elif action == "update":
|
||||||
|
domain.updateDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
||||||
|
except libvirt.libvirtError as e:
|
||||||
|
fail(_("Error attempting device %s: %s") % (action, e))
|
||||||
|
|
||||||
|
# Test driver doesn't support device hotplug so we can't reach this
|
||||||
|
print_stdout(_("Device %s successful.") % action) # pragma: no cover
|
||||||
|
if confirm: # pragma: no cover
|
||||||
|
print_stdout("")
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_changes(xmlobj, options, parserclass):
|
||||||
|
origxml = xmlobj.get_xml()
|
||||||
|
|
||||||
|
if options.edit != -1:
|
||||||
|
devs = action_edit(xmlobj, options, parserclass)
|
||||||
|
action = "update"
|
||||||
|
|
||||||
|
elif options.add_device:
|
||||||
|
devs = action_add_device(xmlobj, options, parserclass)
|
||||||
|
action = "hotplug"
|
||||||
|
|
||||||
|
elif options.remove_device:
|
||||||
|
devs = action_remove_device(xmlobj, options, parserclass)
|
||||||
|
action = "hotunplug"
|
||||||
|
|
||||||
|
newxml = xmlobj.get_xml()
|
||||||
|
diff = get_diff(origxml, newxml)
|
||||||
|
|
||||||
|
if not diff:
|
||||||
|
log.warning(_("No XML diff was generated. The requested "
|
||||||
|
"changes will have no effect."))
|
||||||
|
|
||||||
|
if options.print_diff:
|
||||||
|
if diff:
|
||||||
|
print_stdout(diff)
|
||||||
|
elif options.print_xml:
|
||||||
|
print_stdout(newxml)
|
||||||
|
|
||||||
|
return devs, action
|
||||||
|
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# CLI option handling #
|
||||||
|
#######################
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = cli.setupParser(
|
||||||
|
"%(prog)s [options]",
|
||||||
|
_("Edit libvirt XML using command line options."),
|
||||||
|
introspection_epilog=True)
|
||||||
|
|
||||||
|
cli.add_connect_option(parser, "virt-xml")
|
||||||
|
|
||||||
|
parser.add_argument("domain", nargs='?',
|
||||||
|
help=_("Domain name, id, or uuid"))
|
||||||
|
|
||||||
|
actg = parser.add_argument_group(_("XML actions"))
|
||||||
|
actg.add_argument("--edit", nargs='?', default=-1,
|
||||||
|
help=_("Edit VM XML. Examples:\n"
|
||||||
|
"--edit --disk ... (edit first disk device)\n"
|
||||||
|
"--edit 2 --disk ... (edit second disk device)\n"
|
||||||
|
"--edit all --disk ... (edit all disk devices)\n"
|
||||||
|
"--edit target=hda --disk ... (edit disk 'hda')\n"))
|
||||||
|
actg.add_argument("--remove-device", action="store_true",
|
||||||
|
help=_("Remove specified device. Examples:\n"
|
||||||
|
"--remove-device --disk 1 (remove first disk)\n"
|
||||||
|
"--remove-device --disk all (remove all disks)\n"
|
||||||
|
"--remove-device --disk /some/path"))
|
||||||
|
actg.add_argument("--add-device", action="store_true",
|
||||||
|
help=_("Add specified device. Example:\n"
|
||||||
|
"--add-device --disk ..."))
|
||||||
|
actg.add_argument("--build-xml", action="store_true",
|
||||||
|
help=_("Output built device XML. Domain is optional but "
|
||||||
|
"recommended to ensure optimal defaults."))
|
||||||
|
|
||||||
|
outg = parser.add_argument_group(_("Output options"))
|
||||||
|
outg.add_argument("--update", action="store_true",
|
||||||
|
help=_("Apply changes to the running VM.\n"
|
||||||
|
"With --add-device, this is a hotplug operation.\n"
|
||||||
|
"With --remove-device, this is a hotunplug operation.\n"
|
||||||
|
"With --edit, this is an update device operation."))
|
||||||
|
define_g = outg.add_mutually_exclusive_group()
|
||||||
|
define_g.add_argument("--define", action="store_true",
|
||||||
|
help=_("Force defining the domain. Only required if a --print "
|
||||||
|
"option was specified."))
|
||||||
|
define_g.add_argument("--no-define", dest='define', action="store_false",
|
||||||
|
help=_("Force not defining the domain."))
|
||||||
|
define_g.set_defaults(define=None)
|
||||||
|
outg.add_argument("--start", action="store_true",
|
||||||
|
help=_("Start the domain."))
|
||||||
|
outg.add_argument("--print-diff", action="store_true",
|
||||||
|
help=_("Only print the requested change, in diff format"))
|
||||||
|
outg.add_argument("--print-xml", action="store_true",
|
||||||
|
help=_("Only print the requested change, in full XML format"))
|
||||||
|
outg.add_argument("--confirm", action="store_true",
|
||||||
|
help=_("Require confirmation before saving any results."))
|
||||||
|
|
||||||
|
cli.add_os_variant_option(parser, virtinstall=False)
|
||||||
|
|
||||||
|
g = parser.add_argument_group(_("XML options"))
|
||||||
|
cli.add_disk_option(g, editexample=True)
|
||||||
|
cli.add_net_option(g)
|
||||||
|
cli.add_gfx_option(g)
|
||||||
|
cli.add_metadata_option(g)
|
||||||
|
cli.add_memory_option(g)
|
||||||
|
cli.vcpu_cli_options(g, editexample=True)
|
||||||
|
cli.add_guest_xml_options(g)
|
||||||
|
cli.add_boot_options(g)
|
||||||
|
cli.add_device_options(g)
|
||||||
|
|
||||||
|
misc = parser.add_argument_group(_("Miscellaneous Options"))
|
||||||
|
cli.add_misc_options(misc, prompt=False, printxml=False, dryrun=False)
|
||||||
|
|
||||||
|
cli.autocomplete(parser)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
###################
|
||||||
|
# main() handling #
|
||||||
|
###################
|
||||||
|
|
||||||
|
def main(conn=None):
|
||||||
|
cli.earlyLogging()
|
||||||
|
options = parse_args()
|
||||||
|
|
||||||
|
if (options.confirm or options.print_xml or
|
||||||
|
options.print_diff or options.build_xml):
|
||||||
|
options.quiet = False
|
||||||
|
cli.setupLogging("virt-xml", options.debug, options.quiet)
|
||||||
|
|
||||||
|
if cli.check_option_introspection(options):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
options.stdinxml = None
|
||||||
|
if not options.domain and not options.build_xml:
|
||||||
|
if not sys.stdin.closed and not sys.stdin.isatty():
|
||||||
|
if options.confirm:
|
||||||
|
fail(_("Can't use --confirm with stdin input."))
|
||||||
|
if options.update:
|
||||||
|
fail(_("Can't use --update with stdin input."))
|
||||||
|
options.stdinxml = sys.stdin.read()
|
||||||
|
else:
|
||||||
|
fail(_("A domain must be specified"))
|
||||||
|
|
||||||
|
# Default to --define, unless:
|
||||||
|
# --no-define explicitly specified
|
||||||
|
# --print-* option is used
|
||||||
|
# XML input came from stdin
|
||||||
|
if not options.print_xml and not options.print_diff:
|
||||||
|
if options.stdinxml:
|
||||||
|
if not options.define:
|
||||||
|
options.print_xml = True
|
||||||
|
else:
|
||||||
|
if options.define is None:
|
||||||
|
options.define = True
|
||||||
|
if options.confirm and not options.print_xml:
|
||||||
|
options.print_diff = True
|
||||||
|
|
||||||
|
# Ensure only one of these actions wash specified
|
||||||
|
# --edit
|
||||||
|
# --remove-device
|
||||||
|
# --add-device
|
||||||
|
# --build-xml
|
||||||
|
check_action_collision(options)
|
||||||
|
|
||||||
|
# Ensure there wasn't more than one device/xml config option
|
||||||
|
# specified. So reject '--disk X --network X'
|
||||||
|
parserclass = check_xmlopt_collision(options)
|
||||||
|
|
||||||
|
if options.update and not parserclass.guest_propname:
|
||||||
|
fail(_("Don't know how to --update for --%s") %
|
||||||
|
(parserclass.cli_arg_name))
|
||||||
|
|
||||||
|
conn = cli.getConnection(options.connect, conn)
|
||||||
|
|
||||||
|
domain = None
|
||||||
|
active_xmlobj = None
|
||||||
|
inactive_xmlobj = None
|
||||||
|
if options.domain:
|
||||||
|
domain, inactive_xmlobj, active_xmlobj = get_domain_and_guest(
|
||||||
|
conn, options.domain)
|
||||||
|
else:
|
||||||
|
inactive_xmlobj = Guest(conn, parsexml=options.stdinxml)
|
||||||
|
vm_is_running = bool(active_xmlobj)
|
||||||
|
|
||||||
|
if options.build_xml:
|
||||||
|
devs = action_build_xml(conn, options, parserclass, inactive_xmlobj)
|
||||||
|
for dev in devs:
|
||||||
|
# pylint: disable=no-member
|
||||||
|
print_stdout(dev.get_xml())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
performed_update = False
|
||||||
|
if options.update:
|
||||||
|
if options.update and options.start:
|
||||||
|
fail(_("Cannot mix --update and --start"))
|
||||||
|
|
||||||
|
if vm_is_running:
|
||||||
|
devs, action = prepare_changes(active_xmlobj, options, parserclass)
|
||||||
|
update_changes(domain, devs, action, options.confirm)
|
||||||
|
performed_update = True
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
_("The VM is not running, --update is inapplicable."))
|
||||||
|
if not options.define:
|
||||||
|
# --update and --no-define passed, so we are done
|
||||||
|
# It's hard to hit this case with the test suite
|
||||||
|
return 0 # pragma: no cover
|
||||||
|
|
||||||
|
original_xml = inactive_xmlobj.get_xml()
|
||||||
|
devs, action = prepare_changes(inactive_xmlobj, options, parserclass)
|
||||||
|
if not options.define:
|
||||||
|
if options.start:
|
||||||
|
start_domain_transient(conn, inactive_xmlobj, devs,
|
||||||
|
action, options.confirm)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
dom = define_changes(conn, inactive_xmlobj,
|
||||||
|
devs, action, options.confirm)
|
||||||
|
if not dom:
|
||||||
|
# --confirm user said 'no'
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if options.start:
|
||||||
|
try:
|
||||||
|
dom.create()
|
||||||
|
except libvirt.libvirtError as e: # pragma: no cover
|
||||||
|
fail(_("Failed starting domain '%s': %s") % (
|
||||||
|
inactive_xmlobj.name, e))
|
||||||
|
print_stdout(_("Domain '%s' started successfully.") %
|
||||||
|
inactive_xmlobj.name)
|
||||||
|
|
||||||
|
elif vm_is_running and not performed_update:
|
||||||
|
print_stdout(
|
||||||
|
_("Changes will take effect after the domain is fully powered off."))
|
||||||
|
elif defined_xml_is_unchanged(conn, domain, original_xml):
|
||||||
|
log.warning(_("XML did not change after domain define. You may "
|
||||||
|
"have changed a value that libvirt is setting by default."))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def runcli(): # pragma: no cover
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except SystemExit as sys_e:
|
||||||
|
sys.exit(sys_e.code)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.debug("", exc_info=True)
|
||||||
|
print_stderr(_("Aborted at user request"))
|
||||||
|
except Exception as main_e:
|
||||||
|
fail(main_e)
|
Loading…
Reference in New Issue
Block a user