From 6a9aa6827b5873ed48a5341e0a3b780e5bf0ce8d Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Sun, 21 Sep 2014 13:36:28 -0400 Subject: [PATCH] console: Break out tunnel handling to its own file --- virtManager/console.py | 294 +++---------------------------------- virtManager/sshtunnels.py | 296 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 278 deletions(-) create mode 100644 virtManager/sshtunnels.py diff --git a/virtManager/console.py b/virtManager/console.py index 957a72b9..874eed61 100644 --- a/virtManager/console.py +++ b/virtManager/console.py @@ -30,16 +30,13 @@ from gi.repository import SpiceClientGLib import libvirt import logging -import os -import Queue -import signal import socket -import threading from .autodrawer import AutoDrawer from .baseclass import vmmGObjectUI, vmmGObject -from .serialcon import vmmSerialConsole from .details import DETAILS_PAGE_CONSOLE +from .serialcon import vmmSerialConsole +from .sshtunnels import ConnectionInfo, SSHTunnels # Console pages (CONSOLE_PAGE_UNAVAILABLE, @@ -48,7 +45,7 @@ from .details import DETAILS_PAGE_CONSOLE CONSOLE_PAGE_OFFSET) = range(4) -def has_property(obj, setting): +def _has_property(obj, setting): try: obj.get_property(setting) except TypeError: @@ -56,272 +53,9 @@ def has_property(obj, setting): return True - -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.socket - self.gaddr = gdev.listen or "127.0.0.1" - self.gtlsport = gdev.tlsPort or None - - self.transport, self.connuser = conn.get_transport() - - (self._connhost, - self._connport) = conn.get_backend().get_uri_host_port() - if self._connhost == "localhost": - self._connhost = "127.0.0.1" - - def _is_listen_localhost(self, host=None): - return (host or self.gaddr) in ["127.0.0.1", "::1"] - - def _is_listen_any(self): - return self.gaddr in ["0.0.0.0", "::"] - - def need_tunnel(self): - if not self._is_listen_localhost(): - return False - return self.transport in ["ssh", "ext"] - - def is_bad_localhost(self): - """ - Return True if the guest is listening on localhost, but the libvirt - URI doesn't give us any way to tunnel the connection - """ - host = self.get_conn_host()[0] - if self.need_tunnel(): - return False - return self.transport and self._is_listen_localhost(host) - - def get_conn_host(self): - host = self._connhost - port = self._connport - tlsport = None - - if not self.need_tunnel(): - port = self.gport - tlsport = self.gtlsport - if not self._is_listen_any(): - host = self.gaddr - - return host, port, tlsport - - 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)) - def console_active(self): - if self.gsocket: - return True - if (self.gport in [None, -1] and self.gtlsport in [None, -1]): - return False - return True - - -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 = threading.Thread(name="Tunnel thread", - target=self._handle_queue, - args=()) - self._thread.daemon = True - self._queue = Queue.Queue() - self._lock = threading.Lock() - - def _handle_queue(self): - while True: - cb, args, = self._queue.get() - self.lock() - vmmGObject.idle_add(cb, *args) - - def schedule(self, cb, *args): - if not self._thread.is_alive(): - self._thread.start() - self._queue.put((cb, args)) - - def lock(self): - self._lock.acquire() - def unlock(self): - self._lock.release() - -_tunnel_sched = _TunnelScheduler() - - -class _Tunnel(object): - def __init__(self): - self.outfd = None - self.errfd = None - self.pid = None - self._outfds = None - self._errfds = None - self.closed = False - - def open(self, ginfo): - self._outfds = socket.socketpair() - self._errfds = socket.socketpair() - - return self._outfds[0].fileno(), self._launch_tunnel, ginfo - - def close(self): - if self.closed: - return - self.closed = True - - logging.debug("Close tunnel PID=%s OUTFD=%s ERRFD=%s", - self.pid, - self.outfd and self.outfd.fileno() or self._outfds, - self.errfd and self.errfd.fileno() or self._errfds) - - if self.outfd: - self.outfd.close() - elif self._outfds: - self._outfds[0].close() - self._outfds[1].close() - self.outfd = None - self._outfds = None - - if self.errfd: - self.errfd.close() - elif self._errfds: - self._errfds[0].close() - self._errfds[1].close() - self.errfd = None - self._errfds = 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: - break - - if not new: - break - - errout += new - - return errout - - def _launch_tunnel(self, ginfo): - if self.closed: - return -1 - - host, port, ignore = ginfo.get_conn_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 = reduce(lambda x, y: x + " " + y, argv[1:]) - logging.debug("Creating SSH tunnel: %s", argv_str) - - pid = os.fork() - if pid == 0: - self._outfds[0].close() - self._errfds[0].close() - - os.close(0) - os.close(1) - os.close(2) - os.dup(self._outfds[1].fileno()) - os.dup(self._outfds[1].fileno()) - os.dup(self._errfds[1].fileno()) - os.execlp(*argv) - os._exit(1) # pylint: disable=protected-access - else: - self._outfds[1].close() - self._errfds[1].close() - - logging.debug("Open tunnel PID=%d OUTFD=%d ERRFD=%d", - pid, self._outfds[0].fileno(), self._errfds[0].fileno()) - self._errfds[0].setblocking(0) - - self.outfd = self._outfds[0] - self.errfd = self._errfds[0] - self._outfds = None - self._errfds = None - self.pid = pid - - -class Tunnels(object): - def __init__(self, ginfo): - self.ginfo = ginfo - self._tunnels = [] - - def open_new(self): - t = _Tunnel() - fd, cb, args = t.open(self.ginfo) - self._tunnels.append(t) - _tunnel_sched.schedule(cb, args) - - return fd - - def close_all(self): - for l in self._tunnels: - l.close() - - def get_err_output(self): - errout = "" - for l in self._tunnels: - errout += l.get_err_output() - return errout - - lock = _tunnel_sched.lock - unlock = _tunnel_sched.unlock - +################################## +# VNC/Spice abstraction handling # +################################## class Viewer(vmmGObject): def __init__(self, console): @@ -732,13 +466,13 @@ class SpiceViewer(Viewer): def get_desktop_resolution(self): if (not self._display_channel or - not has_property(self._display_channel, "width")): + not _has_property(self._display_channel, "width")): return None return self._display_channel.get_properties("width", "height") def has_agent(self): if (not self.main_channel or - not has_property(self.main_channel, "agent-connected")): + not _has_property(self.main_channel, "agent-connected")): return False ret = self.main_channel.get_property("agent-connected") return ret @@ -791,12 +525,12 @@ class SpiceViewer(Viewer): self.spice_session.connect() def get_scaling(self): - if not has_property(self._display, "scaling"): + if not _has_property(self._display, "scaling"): return False return self._display.get_property("scaling") def set_scaling(self, scaling): - if not has_property(self._display, "scaling"): + if not _has_property(self._display, "scaling"): logging.debug("Spice version doesn't support scaling.") return self._display.set_property("scaling", scaling) @@ -851,6 +585,10 @@ class SpiceViewer(Viewer): return False +##################### +# UI logic handling # +##################### + class vmmConsolePages(vmmGObjectUI): def __init__(self, vm, builder, topwin): vmmGObjectUI.__init__(self, None, None, builder=builder, topwin=topwin) @@ -1078,7 +816,7 @@ class vmmConsolePages(vmmGObjectUI): self.gtk_settings_accel = settings.get_property('gtk-menu-bar-accel') settings.set_property('gtk-menu-bar-accel', None) - if has_property(settings, "gtk-enable-mnemonics"): + if _has_property(settings, "gtk-enable-mnemonics"): self.gtk_settings_mnemonic = settings.get_property( "gtk-enable-mnemonics") settings.set_property("gtk-enable-mnemonics", False) @@ -1520,7 +1258,7 @@ class vmmConsolePages(vmmGObjectUI): self.set_enable_accel() if ginfo.need_tunnel(): - self.tunnels = Tunnels(ginfo) + self.tunnels = SSHTunnels(ginfo) self.viewer.open_ginfo(ginfo) except Exception, e: logging.exception("Error connection to graphical console") diff --git a/virtManager/sshtunnels.py b/virtManager/sshtunnels.py new file mode 100644 index 00000000..b6bcf880 --- /dev/null +++ b/virtManager/sshtunnels.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +import logging +import os +import Queue +import socket +import signal +import threading + +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.socket + self.gaddr = gdev.listen or "127.0.0.1" + self.gtlsport = gdev.tlsPort or None + + self.transport, self.connuser = conn.get_transport() + + (self._connhost, + self._connport) = conn.get_backend().get_uri_host_port() + if self._connhost == "localhost": + self._connhost = "127.0.0.1" + + def _is_listen_localhost(self, host=None): + return (host or self.gaddr) in ["127.0.0.1", "::1"] + + def _is_listen_any(self): + return self.gaddr in ["0.0.0.0", "::"] + + def need_tunnel(self): + if not self._is_listen_localhost(): + return False + return self.transport in ["ssh", "ext"] + + def is_bad_localhost(self): + """ + Return True if the guest is listening on localhost, but the libvirt + URI doesn't give us any way to tunnel the connection + """ + host = self.get_conn_host()[0] + if self.need_tunnel(): + return False + return self.transport and self._is_listen_localhost(host) + + def get_conn_host(self): + host = self._connhost + port = self._connport + tlsport = None + + if not self.need_tunnel(): + port = self.gport + tlsport = self.gtlsport + if not self._is_listen_any(): + host = self.gaddr + + return host, port, tlsport + + 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)) + def console_active(self): + if self.gsocket: + return True + if (self.gport in [None, -1] and self.gtlsport in [None, -1]): + return False + return True + + +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 = threading.Thread(name="Tunnel thread", + target=self._handle_queue, + args=()) + self._thread.daemon = True + self._queue = Queue.Queue() + self._lock = threading.Lock() + + def _handle_queue(self): + while True: + cb, args, = self._queue.get() + self.lock() + vmmGObject.idle_add(cb, *args) + + def schedule(self, cb, *args): + if not self._thread.is_alive(): + self._thread.start() + self._queue.put((cb, args)) + + def lock(self): + self._lock.acquire() + def unlock(self): + self._lock.release() + + +class _Tunnel(object): + def __init__(self): + self.outfd = None + self.errfd = None + self.pid = None + self._outfds = None + self._errfds = None + self.closed = False + + def open(self, ginfo): + self._outfds = socket.socketpair() + self._errfds = socket.socketpair() + + return self._outfds[0].fileno(), self._launch_tunnel, ginfo + + def close(self): + if self.closed: + return + self.closed = True + + logging.debug("Close tunnel PID=%s OUTFD=%s ERRFD=%s", + self.pid, + self.outfd and self.outfd.fileno() or self._outfds, + self.errfd and self.errfd.fileno() or self._errfds) + + if self.outfd: + self.outfd.close() + elif self._outfds: + self._outfds[0].close() + self._outfds[1].close() + self.outfd = None + self._outfds = None + + if self.errfd: + self.errfd.close() + elif self._errfds: + self._errfds[0].close() + self._errfds[1].close() + self.errfd = None + self._errfds = 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: + break + + if not new: + break + + errout += new + + return errout + + def _launch_tunnel(self, ginfo): + if self.closed: + return -1 + + host, port, ignore = ginfo.get_conn_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 = reduce(lambda x, y: x + " " + y, argv[1:]) + logging.debug("Creating SSH tunnel: %s", argv_str) + + pid = os.fork() + if pid == 0: + self._outfds[0].close() + self._errfds[0].close() + + os.close(0) + os.close(1) + os.close(2) + os.dup(self._outfds[1].fileno()) + os.dup(self._outfds[1].fileno()) + os.dup(self._errfds[1].fileno()) + os.execlp(*argv) + os._exit(1) # pylint: disable=protected-access + else: + self._outfds[1].close() + self._errfds[1].close() + + logging.debug("Open tunnel PID=%d OUTFD=%d ERRFD=%d", + pid, self._outfds[0].fileno(), self._errfds[0].fileno()) + self._errfds[0].setblocking(0) + + self.outfd = self._outfds[0] + self.errfd = self._errfds[0] + self._outfds = None + self._errfds = None + self.pid = pid + + +class SSHTunnels(object): + _tunnel_sched = _TunnelScheduler() + + def __init__(self, ginfo): + self.ginfo = ginfo + self._tunnels = [] + + def open_new(self): + t = _Tunnel() + fd, cb, args = t.open(self.ginfo) + self._tunnels.append(t) + self._tunnel_sched.schedule(cb, args) + + return fd + + def close_all(self): + for l in self._tunnels: + l.close() + + def get_err_output(self): + errout = "" + for l in self._tunnels: + errout += l.get_err_output() + return errout + + def lock(self, *args, **kwargs): + return self._tunnel_sched.lock(*args, **kwargs) + def unlock(self, *args, **kwargs): + return self._tunnel_sched.unlock(*args, **kwargs)