virt-manager/virtManager/serialcon.py

470 lines
14 KiB
Python

#
# Copyright (C) 2006, 2013 Red Hat, Inc.
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
#
# 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 os
import termios
import tty
import pty
import fcntl
import logging
import gi
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk
# We can use either 2.91 or 2.90. This is just to silence runtime warnings
# pylint: disable=wrong-import-position
try:
gi.require_version("Vte", "2.91")
logging.debug("Using VTE API 2.91")
except ValueError:
gi.require_version("Vte", "2.90")
logging.debug("Using VTE API 2.90")
from gi.repository import Vte
import libvirt
from .baseclass import vmmGObject
class ConsoleConnection(vmmGObject):
def __init__(self, vm):
vmmGObject.__init__(self)
self.vm = vm
self.conn = vm.conn
def _cleanup(self):
self.close()
self.vm = None
self.conn = None
def is_open(self):
raise NotImplementedError()
def open(self, dev, terminal):
raise NotImplementedError()
def close(self):
raise NotImplementedError()
def send_data(self, src, text, length, terminal):
"""
Callback when data has been entered into VTE terminal
"""
raise NotImplementedError()
class LocalConsoleConnection(ConsoleConnection):
def __init__(self, vm):
ConsoleConnection.__init__(self, vm)
self.fd = None
self.source = None
self.origtermios = None
def is_open(self):
return self.fd is not None
def open(self, dev, terminal):
if self.fd is not None:
self.close()
ipty = dev and dev.source_path or None
logging.debug("Opening serial tty path: %s", ipty)
if ipty is None:
return
self.fd = pty.slave_open(ipty)
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
self.source = GLib.io_add_watch(self.fd,
GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP,
self.display_data, terminal)
# Save term settings & set to raw mode
self.origtermios = termios.tcgetattr(self.fd)
tty.setraw(self.fd, termios.TCSANOW)
def close(self):
if self.fd is None:
return
# Restore term settings
try:
if self.origtermios:
termios.tcsetattr(self.fd, termios.TCSANOW, self.origtermios)
except Exception:
# domain may already have exited, destroying the pty, so ignore
pass
os.close(self.fd)
self.fd = None
GLib.source_remove(self.source)
self.source = None
self.origtermios = None
def send_data(self, src, text, length, terminal):
ignore = src
ignore = length
ignore = terminal
if self.fd is None:
return
os.write(self.fd, text)
def display_data(self, src, cond, terminal):
ignore = src
if cond != GLib.IO_IN:
self.close()
return False
data = os.read(self.fd, 1024)
terminal.feed(data)
return True
class LibvirtConsoleConnection(ConsoleConnection):
def __init__(self, vm):
ConsoleConnection.__init__(self, vm)
self.stream = None
self.streamToTerminal = b""
self.terminalToStream = ""
def _event_on_stream(self, stream, events, opaque):
ignore = stream
terminal = opaque
if (events & libvirt.VIR_EVENT_HANDLE_ERROR or
events & libvirt.VIR_EVENT_HANDLE_HANGUP):
logging.debug("Received stream ERROR/HANGUP, closing console")
self.close()
return
if events & libvirt.VIR_EVENT_HANDLE_READABLE:
try:
got = self.stream.recv(1024 * 100)
except Exception:
logging.exception("Error receiving stream data")
self.close()
return
if got == -2:
# This is basically EAGAIN
return
if len(got) == 0:
logging.debug("Received EOF from stream, closing")
self.close()
return
queued_text = bool(self.streamToTerminal)
self.streamToTerminal += got
if not queued_text:
self.idle_add(self.display_data, terminal)
if (events & libvirt.VIR_EVENT_HANDLE_WRITABLE and
self.terminalToStream):
try:
done = self.stream.send(self.terminalToStream.encode())
except Exception:
logging.exception("Error sending stream data")
self.close()
return
if done == -2:
# This is basically EAGAIN
return
self.terminalToStream = self.terminalToStream[done:]
if not self.terminalToStream:
self.stream.eventUpdateCallback(libvirt.VIR_STREAM_EVENT_READABLE |
libvirt.VIR_STREAM_EVENT_ERROR |
libvirt.VIR_STREAM_EVENT_HANGUP)
def is_open(self):
return self.stream is not None
def open(self, dev, terminal):
if self.stream:
self.close()
name = dev and dev.alias.name or None
logging.debug("Opening console stream for dev=%s alias=%s",
dev, name)
# libxl doesn't set aliases, their open_console just defaults to
# opening the first console device, so don't force prescence of
# an alias
stream = self.conn.get_backend().newStream(libvirt.VIR_STREAM_NONBLOCK)
self.vm.open_console(name, stream)
self.stream = stream
self.stream.eventAddCallback((libvirt.VIR_STREAM_EVENT_READABLE |
libvirt.VIR_STREAM_EVENT_ERROR |
libvirt.VIR_STREAM_EVENT_HANGUP),
self._event_on_stream,
terminal)
def close(self):
if self.stream:
try:
self.stream.eventRemoveCallback()
except Exception:
logging.exception("Error removing stream callback")
try:
self.stream.finish()
except Exception:
logging.exception("Error finishing stream")
self.stream = None
def send_data(self, src, text, length, terminal):
ignore = src
ignore = length
ignore = terminal
if self.stream is None:
return
self.terminalToStream += text
if self.terminalToStream:
self.stream.eventUpdateCallback(libvirt.VIR_STREAM_EVENT_READABLE |
libvirt.VIR_STREAM_EVENT_WRITABLE |
libvirt.VIR_STREAM_EVENT_ERROR |
libvirt.VIR_STREAM_EVENT_HANGUP)
def display_data(self, terminal):
if not self.streamToTerminal:
return
terminal.feed(self.streamToTerminal)
self.streamToTerminal = b""
class vmmSerialConsole(vmmGObject):
@staticmethod
def support_remote_console(vm):
"""
Check if we can connect to a remote console
"""
return bool(vm.remote_console_supported)
@staticmethod
def can_connect(vm, dev):
"""
Check if we think we can actually open passed console/serial dev
"""
usable_types = ["pty"]
ctype = dev.type
path = dev.source_path
is_remote = vm.conn.is_remote()
support_tunnel = vmmSerialConsole.support_remote_console(vm)
err = ""
if is_remote and not support_tunnel:
err = _("Remote serial console not supported for this "
"connection")
elif not vm.is_active():
err = _("Serial console not available for inactive guest")
elif ctype not in usable_types:
err = (_("Console for device type '%s' not yet supported") %
ctype)
elif (not is_remote and
not support_tunnel and
(path and not os.access(path, os.R_OK | os.W_OK))):
err = _("Can not access console path '%s'") % str(path)
return err
def __init__(self, vm, target_port, name):
vmmGObject.__init__(self)
self.vm = vm
self.target_port = target_port
self.name = name
self.lastpath = None
# Always use libvirt console streaming if available, so
# we exercise the same code path (it's what virsh console does)
if vmmSerialConsole.support_remote_console(self.vm):
self.console = LibvirtConsoleConnection(self.vm)
else:
self.console = LocalConsoleConnection(self.vm)
self.serial_popup = None
self.serial_copy = None
self.serial_paste = None
self.serial_close = None
self.init_popup()
self.terminal = None
self.init_terminal()
self.box = None
self.error_label = None
self.init_ui()
self.vm.connect("state-changed", self.vm_status_changed)
def init_terminal(self):
self.terminal = Vte.Terminal()
self.terminal.set_scrollback_lines(1000)
self.terminal.set_audible_bell(False)
self.terminal.get_accessible().set_name("Serial Terminal")
self.terminal.connect("button-press-event", self.show_serial_rcpopup)
self.terminal.connect("commit", self.console.send_data, self.terminal)
self.terminal.show()
def init_popup(self):
self.serial_popup = Gtk.Menu()
self.serial_copy = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_COPY,
None)
self.serial_copy.connect("activate", self.serial_copy_text)
self.serial_popup.add(self.serial_copy)
self.serial_paste = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_PASTE,
None)
self.serial_paste.connect("activate", self.serial_paste_text)
self.serial_popup.add(self.serial_paste)
def init_ui(self):
self.box = Gtk.Notebook()
self.box.set_show_tabs(False)
self.box.set_show_border(False)
align = Gtk.Alignment()
align.set_padding(2, 2, 2, 2)
evbox = Gtk.EventBox()
evbox.modify_bg(Gtk.StateType.NORMAL, Gdk.Color(0, 0, 0))
terminalbox = Gtk.HBox()
scrollbar = Gtk.VScrollbar()
self.error_label = Gtk.Label()
self.error_label.set_width_chars(40)
self.error_label.set_line_wrap(True)
if self.terminal:
scrollbar.set_adjustment(self.terminal.get_vadjustment())
align.add(self.terminal)
evbox.add(align)
terminalbox.pack_start(evbox, True, True, 0)
terminalbox.pack_start(scrollbar, False, False, 0)
self.box.append_page(terminalbox, Gtk.Label(""))
self.box.append_page(self.error_label, Gtk.Label(""))
self.box.show_all()
scrollbar.hide()
scrollbar.get_adjustment().connect(
"changed", self._scrollbar_adjustment_changed, scrollbar)
def _scrollbar_adjustment_changed(self, adjustment, scrollbar):
scrollbar.set_visible(
adjustment.get_upper() > adjustment.get_page_size())
def _cleanup(self):
self.console.cleanup()
self.console = None
self.vm = None
self.terminal = None
self.box = None
def close(self):
if self.console:
self.console.close()
def show_error(self, msg):
self.error_label.set_markup("<b>%s</b>" % msg)
self.box.set_current_page(1)
def open_console(self):
try:
if not self.console.is_open():
self.console.open(self.lookup_dev(), self.terminal)
self.box.set_current_page(0)
return True
except Exception as e:
logging.exception("Error opening serial console")
self.show_error(_("Error connecting to text console: %s") % e)
try:
self.console.close()
except Exception:
pass
return False
def vm_status_changed(self, vm):
if vm.status() in [libvirt.VIR_DOMAIN_RUNNING]:
self.open_console()
else:
self.console.close()
def lookup_dev(self):
devs = self.vm.get_serial_devs()
for dev in devs:
port = dev.vmmindex
path = dev.source_path
if port == self.target_port:
if path != self.lastpath:
logging.debug("Serial console '%s' path changed to %s",
self.target_port, path)
self.lastpath = path
return dev
logging.debug("No devices found for serial target port '%s'",
self.target_port)
self.lastpath = None
return None
#######################
# Popup menu handling #
#######################
def show_serial_rcpopup(self, src, event):
if event.button != 3:
return
self.serial_popup.show_all()
if src.get_has_selection():
self.serial_copy.set_sensitive(True)
else:
self.serial_copy.set_sensitive(False)
self.serial_popup.popup(None, None, None, None, 0, event.time)
def serial_copy_text(self, src_ignore):
self.terminal.copy_clipboard()
def serial_paste_text(self, src_ignore):
self.terminal.paste_clipboard()