mirror of
https://github.com/virt-manager/virt-manager.git
synced 2025-02-25 18:55:27 -06:00
Init a shared log instance in virtinst/logger.py, and use that throughout the code base, so we aren't calling directly into 'logging'. This helps protect our logging output from being cluttered with other library output, as happens with some 'requests' usage
302 lines
8.8 KiB
Python
302 lines
8.8 KiB
Python
# Copyright (C) 2014, 2015 Red Hat, Inc.
|
|
#
|
|
# This work is licensed under the GNU GPLv2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
import functools
|
|
import os
|
|
import queue
|
|
import socket
|
|
import signal
|
|
import threading
|
|
import ipaddress
|
|
|
|
from virtinst import log
|
|
|
|
from .baseclass import vmmGObject
|
|
|
|
|
|
class ConnectionInfo(object):
|
|
"""
|
|
Holds all the bits needed to make a connection to a graphical console
|
|
"""
|
|
def __init__(self, conn, gdev):
|
|
self.gtype = gdev.type
|
|
self.gport = gdev.port and str(gdev.port) or None
|
|
self.gsocket = (gdev.listens and gdev.listens[0].socket) or gdev.socket
|
|
self.gaddr = gdev.listen or "127.0.0.1"
|
|
self.gtlsport = gdev.tlsPort or None
|
|
self.glistentype = gdev.get_first_listen_type()
|
|
|
|
self.transport = conn.get_uri_transport()
|
|
self.connuser = conn.get_uri_username()
|
|
|
|
self._connhost = conn.get_uri_hostname() or "localhost"
|
|
self._connport = conn.get_uri_port()
|
|
if self._connhost == "localhost":
|
|
self._connhost = "127.0.0.1"
|
|
|
|
def _is_listen_localhost(self, host=None):
|
|
try:
|
|
return ipaddress.ip_network(str(host or self.gaddr)).is_loopback
|
|
except Exception:
|
|
return False
|
|
|
|
def _is_listen_any(self):
|
|
try:
|
|
return ipaddress.ip_network(str(self.gaddr)).is_unspecified
|
|
except Exception:
|
|
return False
|
|
|
|
def _is_listen_none(self):
|
|
if self.glistentype == "none":
|
|
return True
|
|
return not (self.gsocket or self.gport or self.gtlsport)
|
|
|
|
def need_tunnel(self):
|
|
if not self._is_listen_localhost():
|
|
return False
|
|
return self.transport == "ssh"
|
|
|
|
def bad_config(self):
|
|
if self.transport and self._is_listen_none():
|
|
return _("Guest is on a remote host, but is only configured "
|
|
"to allow local file descriptor connections.")
|
|
|
|
if self.need_tunnel() and (self.gtlsport and not self.gport):
|
|
return _("Guest is configured for TLS only which does not "
|
|
"work over SSH.")
|
|
|
|
if (not self.need_tunnel() and
|
|
self.transport and
|
|
self._is_listen_localhost(self.get_conn_host()[0])):
|
|
return _("Guest is on a remote host with transport '%s' "
|
|
"but is only configured to listen locally. "
|
|
"To connect remotely you will need to change the guest's "
|
|
"listen address." % self.transport)
|
|
|
|
def get_conn_host(self):
|
|
"""
|
|
Return host/port/tlsport needed for direct spice/vnc connection
|
|
"""
|
|
host = self.gaddr
|
|
port = self.gport
|
|
tlsport = self.gtlsport
|
|
if self._is_listen_any():
|
|
host = self._connhost
|
|
|
|
return host, port, tlsport
|
|
|
|
def get_tunnel_host(self):
|
|
"""
|
|
Physical host name/port needed for ssh tunnel connection
|
|
"""
|
|
return self._connhost, self._connport
|
|
|
|
def logstring(self):
|
|
return ("proto=%s trans=%s connhost=%s connuser=%s "
|
|
"connport=%s gaddr=%s gport=%s gtlsport=%s gsocket=%s" %
|
|
(self.gtype, self.transport, self._connhost, self.connuser,
|
|
self._connport, self.gaddr, self.gport, self.gtlsport,
|
|
self.gsocket))
|
|
|
|
|
|
class _TunnelScheduler(object):
|
|
"""
|
|
If the user is using Spice + SSH URI + no SSH keys, we need to
|
|
serialize connection opening otherwise ssh-askpass gets all angry.
|
|
This handles the locking and scheduling.
|
|
|
|
It's only instantiated once for the whole app, because we serialize
|
|
independent of connection, vm, etc.
|
|
"""
|
|
def __init__(self):
|
|
self._thread = None
|
|
self._queue = queue.Queue()
|
|
self._lock = threading.Lock()
|
|
|
|
def _handle_queue(self):
|
|
while True:
|
|
lock_cb, cb, args, = self._queue.get()
|
|
lock_cb()
|
|
vmmGObject.idle_add(cb, *args)
|
|
|
|
def schedule(self, lock_cb, cb, *args):
|
|
if not self._thread:
|
|
self._thread = threading.Thread(name="Tunnel thread",
|
|
target=self._handle_queue,
|
|
args=())
|
|
self._thread.daemon = True
|
|
if not self._thread.is_alive():
|
|
self._thread.start()
|
|
self._queue.put((lock_cb, cb, args))
|
|
|
|
def lock(self):
|
|
self._lock.acquire()
|
|
def unlock(self):
|
|
self._lock.release()
|
|
|
|
|
|
_tunnel_scheduler = _TunnelScheduler()
|
|
|
|
|
|
class _Tunnel(object):
|
|
def __init__(self):
|
|
self._pid = None
|
|
self._closed = False
|
|
self._errfd = None
|
|
|
|
def close(self):
|
|
if self._closed:
|
|
return
|
|
self._closed = True
|
|
|
|
log.debug("Close tunnel PID=%s ERRFD=%s",
|
|
self._pid, self._errfd and self._errfd.fileno() or None)
|
|
|
|
# Since this is a socket object, the file descriptor is closed
|
|
# when it's garbage collected.
|
|
self._errfd = None
|
|
|
|
if self._pid:
|
|
os.kill(self._pid, signal.SIGKILL)
|
|
os.waitpid(self._pid, 0)
|
|
self._pid = None
|
|
|
|
def get_err_output(self):
|
|
errout = ""
|
|
while True:
|
|
try:
|
|
new = self._errfd.recv(1024)
|
|
except Exception:
|
|
break
|
|
|
|
if not new:
|
|
break
|
|
|
|
errout += new.decode()
|
|
|
|
return errout
|
|
|
|
def open(self, argv, sshfd):
|
|
if self._closed:
|
|
return
|
|
|
|
errfds = socket.socketpair()
|
|
pid = os.fork()
|
|
if pid == 0:
|
|
errfds[0].close()
|
|
|
|
os.dup2(sshfd.fileno(), 0)
|
|
os.dup2(sshfd.fileno(), 1)
|
|
os.dup2(errfds[1].fileno(), 2)
|
|
os.execlp(*argv)
|
|
os._exit(1) # pylint: disable=protected-access
|
|
|
|
sshfd.close()
|
|
errfds[1].close()
|
|
|
|
self._errfd = errfds[0]
|
|
self._errfd.setblocking(0)
|
|
log.debug("Opened tunnel PID=%d ERRFD=%d",
|
|
pid, self._errfd.fileno())
|
|
|
|
self._pid = pid
|
|
|
|
|
|
def _make_ssh_command(ginfo):
|
|
if not ginfo.need_tunnel():
|
|
return None
|
|
|
|
host, port = ginfo.get_tunnel_host()
|
|
|
|
# Build SSH cmd
|
|
argv = ["ssh", "ssh"]
|
|
if port:
|
|
argv += ["-p", str(port)]
|
|
|
|
if ginfo.connuser:
|
|
argv += ['-l', ginfo.connuser]
|
|
|
|
argv += [host]
|
|
|
|
# Build 'nc' command run on the remote host
|
|
#
|
|
# This ugly thing is a shell script to detect availability of
|
|
# the -q option for 'nc': debian and suse based distros need this
|
|
# flag to ensure the remote nc will exit on EOF, so it will go away
|
|
# when we close the VNC tunnel. If it doesn't go away, subsequent
|
|
# VNC connection attempts will hang.
|
|
#
|
|
# Fedora's 'nc' doesn't have this option, and apparently defaults
|
|
# to the desired behavior.
|
|
#
|
|
if ginfo.gsocket:
|
|
nc_params = "-U %s" % ginfo.gsocket
|
|
else:
|
|
nc_params = "%s %s" % (ginfo.gaddr, ginfo.gport)
|
|
|
|
nc_cmd = (
|
|
"""nc -q 2>&1 | grep "requires an argument" >/dev/null;"""
|
|
"""if [ $? -eq 0 ] ; then"""
|
|
""" CMD="nc -q 0 %(nc_params)s";"""
|
|
"""else"""
|
|
""" CMD="nc %(nc_params)s";"""
|
|
"""fi;"""
|
|
"""eval "$CMD";""" %
|
|
{'nc_params': nc_params})
|
|
|
|
argv.append("sh -c")
|
|
argv.append("'%s'" % nc_cmd)
|
|
|
|
argv_str = functools.reduce(lambda x, y: x + " " + y, argv[1:])
|
|
log.debug("Pre-generated ssh command for ginfo: %s", argv_str)
|
|
return argv
|
|
|
|
|
|
class SSHTunnels(object):
|
|
def __init__(self, ginfo):
|
|
self._tunnels = []
|
|
self._sshcommand = _make_ssh_command(ginfo)
|
|
self._locked = False
|
|
|
|
def open_new(self):
|
|
t = _Tunnel()
|
|
self._tunnels.append(t)
|
|
|
|
# socket FDs are closed when the object is garbage collected. This
|
|
# can close an FD behind spice/vnc's back which causes crashes.
|
|
#
|
|
# Dup a bare FD for the viewer side of things, but keep the high
|
|
# level socket object for the SSH side, since it simplifies things
|
|
# in that area.
|
|
viewerfd, sshfd = socket.socketpair()
|
|
_tunnel_scheduler.schedule(self._lock, t.open, self._sshcommand, sshfd)
|
|
|
|
retfd = os.dup(viewerfd.fileno())
|
|
log.debug("Generated tunnel fd=%s for viewer", retfd)
|
|
return retfd
|
|
|
|
def close_all(self):
|
|
for l in self._tunnels:
|
|
l.close()
|
|
self._tunnels = []
|
|
self.unlock()
|
|
|
|
def get_err_output(self):
|
|
errstrings = []
|
|
for l in self._tunnels:
|
|
e = l.get_err_output().strip()
|
|
if e and e not in errstrings:
|
|
errstrings.append(e)
|
|
return "\n".join(errstrings)
|
|
|
|
def _lock(self):
|
|
_tunnel_scheduler.lock()
|
|
self._locked = True
|
|
|
|
def unlock(self, *args, **kwargs):
|
|
if self._locked:
|
|
_tunnel_scheduler.unlock(*args, **kwargs)
|
|
self._locked = False
|