mirror of
https://github.com/virt-manager/virt-manager.git
synced 2025-02-25 18:55:27 -06:00
The reason we fork by default, is to force ssh to invoke ssh-askpass when a password is required, rather than prompt on a terminal no one is looking at. There's a more thorough explanation here: https://github.com/virt-manager/virt-manager/issues/731 With SSH_ASKPASS_REQUIRE=force, we now have a way to force ssh to use askpass in the above scenario, when ssh and libvirt are new enough. The default forking behavior has caused maintenance pain in the past, and is currently causing issues on macos: https://github.com/virt-manager/virt-manager/issues/620 Let's flip the default to `--no-fork`. The VIRT_MANAGER_DEFAULT_FORK env variable is there as an escape hatch incase I really miscalculated. I don't expect many people are depending on use of askpass either way, or if they are, they are launching virt-manager from their desktop and not a terminal, which already gives us the correct behavior AFAICT> My suspicion is barely anyone will notice, which is why I'm ok with changing this now, despite the libvirt support being brand new. If this doesn't raise any issues, then we can eventually drop the forking behavior all together. Signed-off-by: Cole Robinson <crobinso@redhat.com>
309 lines
10 KiB
Python
309 lines
10 KiB
Python
# 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("Gdk", "3.0")
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version('LibvirtGLib', '1.0')
|
|
from gi.repository import LibvirtGLib
|
|
|
|
from virtinst import BuildConfig
|
|
from virtinst import cli
|
|
from virtinst import log
|
|
|
|
from .lib.testmock import CLITestOptionsClass
|
|
|
|
# pygobject commit that I believe universally changed bool arguments
|
|
# to handle passed None. Without this, None usage keeps slipping into
|
|
# the code, so add the requirement.
|
|
# https://github.com/GNOME/pygobject/commit/6c69fc7b582ec1fd3faef4de3fade9a0cb7f8c05
|
|
_PYGOBJECT_VERSION = "3.31.3"
|
|
try:
|
|
gi.check_version(_PYGOBJECT_VERSION)
|
|
except (ValueError, AttributeError): # pragma: no cover
|
|
print("pygobject3 %s or later is required." % _PYGOBJECT_VERSION)
|
|
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")
|
|
errmsg = (_("Error starting Virtual Machine Manager: %(error)s") %
|
|
{"error": msg})
|
|
err.show_err(errmsg,
|
|
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[:]
|
|
from gi.repository import Gtk
|
|
leftovers = sys.argv[1:]
|
|
|
|
if Gtk.check_version(3, 22, 0): # pragma: no cover
|
|
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
|
|
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:
|
|
# pylint: disable=protected-access
|
|
os._exit(0) # pragma: no cover
|
|
|
|
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: # pragma: no cover
|
|
pass
|
|
|
|
os.open(os.devnull, os.O_RDWR)
|
|
os.dup2(0, 1)
|
|
os.dup2(0, 2)
|
|
|
|
|
|
def do_we_fork(options):
|
|
if options.debug or options.no_fork:
|
|
return False
|
|
if options.fork:
|
|
return True
|
|
|
|
key = "VIRT_MANAGER_DEFAULT_FORK"
|
|
val = os.environ.get(key, None)
|
|
if val == "yes":
|
|
log.debug("%s=%s, defaulting to --fork", key, val)
|
|
return True
|
|
if val == "no":
|
|
log.debug("%s=%s, defaulting to --no-fork", key, val)
|
|
return False
|
|
if val:
|
|
log.warning("Unknown %s=%s, expected 'yes' or 'no'", key, val)
|
|
|
|
# Default is `--no-fork`
|
|
return False
|
|
|
|
|
|
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)
|
|
|
|
# comma separated string of options to tweak app behavior,
|
|
# for manual and automated testing config
|
|
parser.add_argument("--test-options", action='append',
|
|
default=[], 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("--fork", action="store_true",
|
|
help="Force fork into background on startup (this is the default)")
|
|
|
|
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")
|
|
parser.add_argument("--show-systray", action="store_true",
|
|
help="Launch virt-manager only in system tray")
|
|
|
|
return parser.parse_known_args()
|
|
|
|
|
|
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)
|
|
|
|
# With F27 gnome+wayland we need to set these before GTK import
|
|
os.environ["GSETTINGS_SCHEMA_DIR"] = BuildConfig.gsettings_dir
|
|
|
|
# Force SSH to use askpass if a password is required,
|
|
# rather than possibly prompting on a terminal the user isn't looking at.
|
|
os.environ.setdefault("SSH_ASKPASS_REQUIRE", "force")
|
|
log.debug("Using SSH_ASKPASS_REQUIRE=%s",
|
|
os.environ["SSH_ASKPASS_REQUIRE"])
|
|
|
|
# Now we've got basic environment up & running we can fork
|
|
do_fork = do_we_fork(options)
|
|
if do_fork:
|
|
drop_tty()
|
|
|
|
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_fork:
|
|
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())
|
|
|
|
# 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
|
|
elif options.show_systray:
|
|
show_window = vmmEngine.CLI_SHOW_SYSTEM_TRAY
|
|
|
|
if (show_window and show_window != vmmEngine.CLI_SHOW_SYSTEM_TRAY and
|
|
options.uri is None): # pragma: no cover
|
|
raise RuntimeError("can't use --show-* options without --connect "
|
|
"(except --show-systray)")
|
|
|
|
skip_autostart = False
|
|
if show_window and show_window != vmmEngine.CLI_SHOW_SYSTEM_TRAY:
|
|
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: # pragma: no cover
|
|
log.debug("Received KeyboardInterrupt. Exiting application.")
|
|
except Exception as run_e:
|
|
if "Gtk" not in globals():
|
|
raise # pragma: no cover
|
|
_show_startup_error(str(run_e), "".join(traceback.format_exc()))
|