virt-manager/virtManager/console.py
Cole Robinson 3b671a9ac3 console: Large amount of internal API tidying
It's very difficult to tell what bits of Viewer that vmmConsolePages
accesses, and what bits of vmmConsolePages that vmmDetails will access.

Clean up the Viewer side with consistent function names, lots of comments,
and privatizing everything that shouldn't be shared.

Yeah this is a big ugly commit...
2015-04-03 20:43:48 -04:00

1669 lines
56 KiB
Python

#
# Copyright (C) 2006-2008, 2013 Red Hat, Inc.
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
# Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@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 gi
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
gi.require_version('GtkVnc', '2.0')
from gi.repository import GtkVnc
gi.require_version('SpiceClientGtk', '3.0')
from gi.repository import SpiceClientGtk
from gi.repository import SpiceClientGLib
import libvirt
import logging
import socket
from .autodrawer import AutoDrawer
from .baseclass import vmmGObjectUI, vmmGObject
from .details import DETAILS_PAGE_CONSOLE
from .serialcon import vmmSerialConsole
from .sshtunnels import ConnectionInfo, SSHTunnels
# Console pages
(CONSOLE_PAGE_UNAVAILABLE,
CONSOLE_PAGE_AUTHENTICATE,
CONSOLE_PAGE_VIEWER,
CONSOLE_PAGE_OFFSET) = range(4)
##################################
# VNC/Spice abstraction handling #
##################################
class Viewer(vmmGObject):
"""
Base class for viewer abstraction
"""
__gsignals__ = {
"add-display-widget": (GObject.SignalFlags.RUN_FIRST, None, [object]),
"size-allocate": (GObject.SignalFlags.RUN_FIRST, None, [object]),
"focus-in-event": (GObject.SignalFlags.RUN_FIRST, None, [object]),
"focus-out-event": (GObject.SignalFlags.RUN_FIRST, None, [object]),
"pointer-grab": (GObject.SignalFlags.RUN_FIRST, None, []),
"pointer-ungrab": (GObject.SignalFlags.RUN_FIRST, None, []),
"connected": (GObject.SignalFlags.RUN_FIRST, None, []),
"disconnected": (GObject.SignalFlags.RUN_FIRST, None, []),
"auth-error": (GObject.SignalFlags.RUN_FIRST, None, [str]),
"need-auth": (GObject.SignalFlags.RUN_FIRST, None, [bool, bool]),
"agent-connected": (GObject.SignalFlags.RUN_FIRST, None, []),
"usb-redirect-error": (GObject.SignalFlags.RUN_FIRST, None, [str]),
}
def __init__(self):
vmmGObject.__init__(self)
self._display = None
self._tunnels = None
self.add_gsettings_handle(
self.config.on_keys_combination_changed(self._refresh_grab_keys))
self.add_gsettings_handle(
self.config.on_keyboard_grab_default_changed(
self._refresh_keyboard_grab_default))
self.connect("add-display-widget", self._common_init)
def _cleanup(self):
self.close()
if self._display:
self._display.destroy()
self._display = None
self._reset_tunnels()
self._tunnels = None
########################
# Internal helper APIs #
########################
def _make_signal_proxy(self, new_signal):
"""
Helper for redirecting a signal from self._display out
through the viewer
"""
def _proxy_signal(src, *args, **kwargs):
ignore = src
self.emit(new_signal, *args, **kwargs)
return _proxy_signal
def _common_init(self, ignore1, ignore2):
self._refresh_grab_keys()
self._refresh_keyboard_grab_default()
self._display.connect("size-allocate",
self._make_signal_proxy("size-allocate"))
self._display.connect("focus-in-event",
self._make_signal_proxy("focus-in-event"))
self._display.connect("focus-out-event",
self._make_signal_proxy("focus-out-event"))
#########################
# Generic internal APIs #
#########################
def _reset_tunnels(self):
if not self._tunnels:
return
errout = self._tunnels.get_err_output()
self._tunnels.close_all()
self._tunnels = None
return errout
def _grab_focus(self):
if self._display:
self._display.grab_focus()
def _has_focus(self):
return self._display and self._display.get_property("has-focus")
def _set_size_request(self, *args, **kwargs):
return self._display.set_size_request(*args, **kwargs)
def _size_allocate(self, *args, **kwargs):
return self._display.size_allocate(*args, **kwargs)
def _get_visible(self):
return self._display and self._display.get_visible()
def _get_pixbuf(self):
return self._display.get_pixbuf()
def _open_ginfo(self, ginfo):
if ginfo.need_tunnel():
self._tunnels = SSHTunnels(ginfo)
self._open_fd(self._tunnels.open_new())
else:
self._open_host(ginfo)
def _get_grab_keys(self):
return self._display.get_grab_keys().as_string()
#######################################################
# Internal API that will be overwritten by subclasses #
#######################################################
def close(self):
raise NotImplementedError()
def _is_open(self):
raise NotImplementedError()
def _set_username(self, cred):
raise NotImplementedError()
def _set_password(self, cred):
raise NotImplementedError()
def _send_keys(self, keys):
raise NotImplementedError()
def _refresh_grab_keys(self):
raise NotImplementedError()
def _refresh_keyboard_grab_default(self):
raise NotImplementedError()
def _open_host(self, ginfo):
raise NotImplementedError()
def _open_fd(self, fd):
raise NotImplementedError()
def _get_desktop_resolution(self):
raise NotImplementedError()
def _get_scaling(self):
raise NotImplementedError()
def _set_scaling(self, val):
raise NotImplementedError()
def _set_resizeguest(self, val):
raise NotImplementedError()
def _get_resizeguest(self):
raise NotImplementedError()
def _get_usb_widget(self):
raise NotImplementedError()
def _has_usb_redirection(self):
raise NotImplementedError()
def _has_agent(self):
raise NotImplementedError()
####################################
# APIs accessed by vmmConsolePages #
####################################
def console_is_open(self):
return self._is_open()
def console_grab_focus(self):
return self._grab_focus()
def console_has_focus(self):
return self._has_focus()
def console_set_size_request(self, *args, **kwargs):
return self._set_size_request(*args, **kwargs)
def console_size_allocate(self, *args, **kwargs):
return self._size_allocate(*args, **kwargs)
def console_get_visible(self):
return self._get_visible()
def console_get_pixbuf(self):
return self._get_pixbuf()
def console_open_ginfo(self, ginfo):
return self._open_ginfo(ginfo)
def console_set_password(self, val):
return self._set_password(val)
def console_set_username(self, val):
return self._set_username(val)
def console_send_keys(self, keys):
return self._send_keys(keys)
def console_reset_tunnels(self):
return self._reset_tunnels()
def console_get_grab_keys(self):
return self._get_grab_keys()
def console_get_desktop_resolution(self):
return self._get_desktop_resolution()
def console_get_scaling(self):
return self._get_scaling()
def console_set_scaling(self, val):
return self._set_scaling(val)
def console_get_resizeguest(self):
return self._get_resizeguest()
def console_set_resizeguest(self, val):
return self._set_resizeguest(val)
def console_get_usb_widget(self):
return self._get_usb_widget()
def console_has_usb_redirection(self):
return self._has_usb_redirection()
def console_has_agent(self):
return self._has_agent()
####################
# VNC viewer class #
####################
class VNCViewer(Viewer):
viewer_type = "vnc"
def __init__(self, *args, **kwargs):
Viewer.__init__(self, *args, **kwargs)
self._display = None
self._sockfd = None
self._desktop_resolution = None
self._tunnel_unlocked = False
###################
# Private helpers #
###################
def _init_widget(self):
self._display = GtkVnc.Display.new()
# Make sure viewer doesn't force resize itself
self._display.set_force_size(False)
self._display.set_pointer_grab(True)
self.emit("add-display-widget", self._display)
self._display.realize()
self._display.connect("vnc-pointer-grab",
self._make_signal_proxy("pointer-grab"))
self._display.connect("vnc-pointer-ungrab",
self._make_signal_proxy("pointer-ungrab"))
self._display.connect("vnc-auth-credential", self._auth_credential)
self._display.connect("vnc-initialized", self._connected_cb)
self._display.connect("vnc-disconnected", self._disconnected_cb)
self._display.connect("vnc-desktop-resize", self._desktop_resize)
self._display.show()
def _unlock_tunnel(self):
if self._tunnels and not self._tunnel_unlocked:
self._tunnels.unlock()
self._tunnel_unlocked = True
def _connected_cb(self, ignore):
self._unlock_tunnel()
self.emit("connected")
def _disconnected_cb(self, ignore):
self._unlock_tunnel()
self.emit("disconnected")
def _desktop_resize(self, src_ignore, w, h):
self._desktop_resolution = (w, h)
# Queue a resize
self.emit("size-allocate", None)
def _auth_credential(self, src_ignore, credList):
values = []
for idx in range(int(credList.n_values)):
values.append(credList.get_nth(idx))
for cred in values:
if cred in [GtkVnc.DisplayCredential.PASSWORD,
GtkVnc.DisplayCredential.USERNAME,
GtkVnc.DisplayCredential.CLIENTNAME]:
continue
errmsg = (_("Unable to provide requested credentials to the VNC "
"server.\n The credential type %s is not supported") %
str(cred))
self.emit("auth-error", errmsg)
return
withUsername = False
withPassword = False
for cred in values:
logging.debug("Got credential request %s", cred)
if cred == GtkVnc.DisplayCredential.PASSWORD:
withPassword = True
elif cred == GtkVnc.DisplayCredential.USERNAME:
withUsername = True
elif cred == GtkVnc.DisplayCredential.CLIENTNAME:
self._display.set_credential(cred, "libvirt-vnc")
if withUsername or withPassword:
self.emit("need-auth", withPassword, withUsername)
###############################
# Private API implementations #
###############################
def close(self):
self._display.close()
if not self._sockfd:
return
self._sockfd.close()
self._sockfd = None
def _is_open(self):
return self._display.is_open()
def _get_scaling(self):
return self._display.get_scaling()
def _set_scaling(self, scaling):
return self._display.set_scaling(scaling)
def _get_grab_keys(self):
return self._display.get_grab_keys().as_string()
def _refresh_grab_keys(self):
if not self._display:
return
try:
keys = self.config.get_keys_combination()
if not keys:
return
try:
keys = [int(k) for k in keys.split(',')]
except:
logging.debug("Error in grab_keys configuration in Gsettings",
exc_info=True)
return
seq = GtkVnc.GrabSequence.new(keys)
self._display.set_grab_keys(seq)
except Exception, e:
logging.debug("Error when getting the grab keys combination: %s",
str(e))
def _send_keys(self, keys):
return self._display.send_keys([Gdk.keyval_from_name(k) for k in keys])
def _refresh_keyboard_grab_default(self):
if not self._display:
return
self._display.set_keyboard_grab(self.config.get_keyboard_grab_default())
def _get_desktop_resolution(self):
return self._desktop_resolution
def _set_username(self, cred):
self._display.set_credential(GtkVnc.DisplayCredential.USERNAME, cred)
def _set_password(self, cred):
self._display.set_credential(GtkVnc.DisplayCredential.PASSWORD, cred)
def _set_resizeguest(self, val):
ignore = val
def _get_resizeguest(self):
return False
def _get_usb_widget(self):
return None
def _has_usb_redirection(self):
return False
def _has_agent(self):
return False
#######################
# Connection routines #
#######################
def _open_ginfo(self, *args, **kwargs):
self._init_widget()
return Viewer._open_ginfo(self, *args, **kwargs)
def _open_host(self, ginfo):
host, port, ignore = ginfo.get_conn_host()
if not ginfo.gsocket:
logging.debug("VNC connection to %s:%s", host, port)
self._display.open_host(host, port)
return
logging.debug("VNC connecting to socket=%s", ginfo.gsocket)
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(ginfo.gsocket)
self._sockfd = sock
except Exception, e:
raise RuntimeError(_("Error opening socket path '%s': %s") %
(ginfo.gsocket, e))
fd = self._sockfd.fileno()
if fd < 0:
raise RuntimeError((_("Error opening socket path '%s'") %
ginfo.gsocket) + " fd=%s" % fd)
self._open_fd(fd)
def _open_fd(self, fd):
self._display.open_fd(fd)
######################
# Spice viewer class #
######################
class SpiceViewer(Viewer):
viewer_type = "spice"
def __init__(self, *args, **kwargs):
Viewer.__init__(self, *args, **kwargs)
self._spice_session = None
self._display = None
self._audio = None
self._main_channel = None
self._main_channel_hids = []
self._display_channel = None
self._usbdev_manager = None
###################
# Private helpers #
###################
def _init_widget(self):
self.emit("add-display-widget", self._display)
self._display.realize()
self._display.connect("mouse-grab", self._mouse_grab_event)
self._display.show()
def _mouse_grab_event(self, ignore, grab):
if grab:
self.emit("pointer-grab")
else:
self.emit("pointer-ungrab")
def _close_main_channel(self):
for i in self._main_channel_hids:
self._main_channel.handler_disconnect(i)
self._main_channel_hids = []
self._main_channel = None
def _create_spice_session(self):
self._spice_session = SpiceClientGLib.Session()
SpiceClientGLib.set_session_option(self._spice_session)
gtk_session = SpiceClientGtk.GtkSession.get(self._spice_session)
gtk_session.set_property("auto-clipboard", True)
GObject.GObject.connect(self._spice_session, "channel-new",
self._channel_new_cb)
self._usbdev_manager = SpiceClientGLib.UsbDeviceManager.get(
self._spice_session)
self._usbdev_manager.connect("auto-connect-failed",
self._usbdev_redirect_error)
self._usbdev_manager.connect("device-error",
self._usbdev_redirect_error)
autoredir = self.config.get_auto_redirection()
if autoredir:
gtk_session.set_property("auto-usbredir", True)
#####################
# Channel listeners #
#####################
def _main_channel_event_cb(self, channel, event):
if event == SpiceClientGLib.ChannelEvent.CLOSED:
self.emit("disconnected")
elif event == SpiceClientGLib.ChannelEvent.ERROR_AUTH:
logging.debug("Spice channel received ERROR_AUTH, assuming "
"it needs credentials.")
self.emit("need-auth", True, False)
self._close_main_channel()
elif event in [SpiceClientGLib.ChannelEvent.ERROR_CONNECT,
SpiceClientGLib.ChannelEvent.ERROR_IO,
SpiceClientGLib.ChannelEvent.ERROR_LINK,
SpiceClientGLib.ChannelEvent.ERROR_TLS]:
logging.debug("Spice channel event error: %s", event)
self.emit("disconnected")
def _fd_channel_event_cb(self, channel, event):
# When we see any event from the channel, release the
# associated tunnel lock
channel.disconnect_by_func(self._fd_channel_event_cb)
self._tunnels.unlock()
def _channel_open_fd_request(self, channel, tls_ignore):
if not self._tunnels:
raise SystemError("Got fd request with no configured tunnel!")
logging.debug("Opening tunnel for channel: %s", channel)
channel.connect_after("channel-event", self._fd_channel_event_cb)
fd = self._tunnels.open_new()
channel.open_fd(fd)
def _channel_new_cb(self, session, channel):
GObject.GObject.connect(channel, "open-fd",
self._channel_open_fd_request)
if (type(channel) == SpiceClientGLib.MainChannel and
not self._main_channel):
if self._tunnels:
self._tunnels.unlock()
self._main_channel = channel
hid = self._main_channel.connect_after("channel-event",
self._main_channel_event_cb)
self._main_channel_hids.append(hid)
hid = self._main_channel.connect_after("notify::agent-connected",
self._agent_connected_cb)
self._main_channel_hids.append(hid)
elif (type(channel) == SpiceClientGLib.DisplayChannel and
not self._display):
channel_id = channel.get_property("channel-id")
if channel_id != 0:
logging.debug("Spice multi-head unsupported")
return
self._display_channel = channel
self._display = SpiceClientGtk.Display.new(self._spice_session,
channel_id)
self._init_widget()
self.emit("connected")
elif (type(channel) in [SpiceClientGLib.PlaybackChannel,
SpiceClientGLib.RecordChannel] and
not self._audio):
self._audio = SpiceClientGLib.Audio.get(self._spice_session, None)
def _agent_connected_cb(self, src, val):
self.emit("agent-connected")
################################
# Internal API implementations #
################################
def close(self):
if self._spice_session is not None:
self._spice_session.disconnect()
self._spice_session = None
self._audio = None
if self._display:
self._display.destroy()
self._display = None
self._display_channel = None
self._close_main_channel()
self._usbdev_manager = None
def _is_open(self):
return self._spice_session is not None
def _refresh_grab_keys(self):
if not self._display:
return
try:
keys = self.config.get_keys_combination()
if not keys:
return
try:
keys = [int(k) for k in keys.split(',')]
except:
logging.debug("Error in grab_keys configuration in Gsettings",
exc_info=True)
return
seq = SpiceClientGtk.GrabSequence.new(keys)
self._display.set_grab_keys(seq)
except Exception, e:
logging.debug("Error when getting the grab keys combination: %s",
str(e))
def _send_keys(self, keys):
return self._display.send_keys([Gdk.keyval_from_name(k) for k in keys],
SpiceClientGtk.DisplayKeyEvent.CLICK)
def _refresh_keyboard_grab_default(self):
if not self._display:
return
self._display.set_property("grab-keyboard",
self.config.get_keyboard_grab_default())
def _get_desktop_resolution(self):
if not self._display_channel:
return None
return self._display_channel.get_properties("width", "height")
def _has_agent(self):
if not self._main_channel:
return False
return self._main_channel.get_property("agent-connected")
def _open_host(self, ginfo):
host, port, tlsport = ginfo.get_conn_host()
self._create_spice_session()
self._spice_session.set_property("host", str(host))
if port:
self._spice_session.set_property("port", str(port))
if tlsport:
self._spice_session.set_property("tls-port", str(tlsport))
self._spice_session.connect()
def _open_fd(self, fd):
self._create_spice_session()
self._spice_session.open_fd(fd)
def _set_username(self, val):
ignore = val
def _set_password(self, cred):
self._spice_session.set_property("password", cred)
if self._tunnels:
fd = self._tunnels.open_new()
self._spice_session.open_fd(fd)
else:
self._spice_session.connect()
def _get_scaling(self):
return self._display.get_property("scaling")
def _set_scaling(self, scaling):
self._display.set_property("scaling", scaling)
def _set_resizeguest(self, val):
if self._display:
self._display.set_property("resize-guest", val)
def _get_resizeguest(self):
if self._display:
return self._display.get_property("resize-guest")
return False
def _usbdev_redirect_error(self, spice_usbdev_widget, spice_usb_device,
errstr):
ignore = spice_usbdev_widget
ignore = spice_usb_device
self.emit("usb-redirect-error", errstr)
def _get_usb_widget(self):
if not self._spice_session:
return
usbwidget = SpiceClientGtk.UsbDeviceWidget.new(self._spice_session,
None)
usbwidget.connect("connect-failed", self._usbdev_redirect_error)
return usbwidget
def _has_usb_redirection(self):
if not self._spice_session or not self._usbdev_manager:
return False
for c in self._spice_session.get_channels():
if c.__class__ is SpiceClientGLib.UsbredirChannel:
return True
return False
#####################
# UI logic handling #
#####################
class vmmConsolePages(vmmGObjectUI):
def __init__(self, vm, builder, topwin):
vmmGObjectUI.__init__(self, None, None, builder=builder, topwin=topwin)
self.vm = vm
self._pointer_is_grabbed = False
self._change_title()
self.vm.connect("config-changed", self._change_title)
self.force_resize = False
# State for disabling modifiers when keyboard is grabbed
self.accel_groups = Gtk.accel_groups_from_object(self.topwin)
self.gtk_settings_accel = None
self.gtk_settings_mnemonic = None
# Initialize display widget
self._viewer = None
self._viewerRetriesScheduled = 0
self._viewerRetryDelay = 125
self._viewer_is_connected = False
self._viewer_is_connecting = False
# Fullscreen toolbar
self.send_key_button = None
self.fs_toolbar = None
self.fs_drawer = None
self.keycombo_menu = self.build_keycombo_menu(self.send_key)
self.init_fs_toolbar()
# Make viewer widget background always be black
black = Gdk.Color(0, 0, 0)
self.widget("console-gfx-viewport").modify_bg(Gtk.StateType.NORMAL,
black)
self.serial_tabs = []
self.last_gfx_page = 0
self._init_menus()
# Signals are added by vmmDetails. Don't use connect_signals here
# or it changes will be overwritten
self.refresh_can_fullscreen()
self.refresh_scaling_from_settings()
self.add_gsettings_handle(
self.vm.on_console_scaling_changed(
self.refresh_scaling_from_settings))
self._refresh_resizeguest_from_settings()
self.add_gsettings_handle(
self.vm.on_console_resizeguest_changed(
self._refresh_resizeguest_from_settings))
scroll = self.widget("console-gfx-scroll")
scroll.connect("size-allocate", self.scroll_size_allocate)
self.add_gsettings_handle(
self.config.on_console_accels_changed(self._refresh_enable_accel))
self.page_changed()
def is_visible(self):
if self.topwin:
return self.topwin.get_visible()
else:
return False
def _cleanup(self):
self.vm = None
if self._viewer:
self._viewer.cleanup()
self._viewer = None
self.keycombo_menu.destroy()
self.keycombo_menu = None
self.fs_drawer.destroy()
self.fs_drawer = None
self.fs_toolbar.destroy()
self.fs_toolbar = None
for serial in self.serial_tabs:
serial.cleanup()
self.serial_tabs = []
##########################
# Initialization helpers #
##########################
@staticmethod
def build_keycombo_menu(cb):
# Shared with vmmDetails
menu = Gtk.Menu()
def make_item(name, combo):
item = Gtk.MenuItem.new_with_mnemonic(name)
item.connect("activate", cb, combo)
menu.add(item)
make_item("Ctrl+Alt+_Backspace", ["Control_L", "Alt_L", "BackSpace"])
make_item("Ctrl+Alt+_Delete", ["Control_L", "Alt_L", "Delete"])
menu.add(Gtk.SeparatorMenuItem())
for i in range(1, 13):
make_item("Ctrl+Alt+F_%d" % i, ["Control_L", "Alt_L", "F%d" % i])
menu.add(Gtk.SeparatorMenuItem())
make_item("_Printscreen", ["Print"])
menu.show_all()
return menu
def init_fs_toolbar(self):
scroll = self.widget("console-gfx-scroll")
pages = self.widget("console-pages")
pages.remove(scroll)
self.fs_toolbar = Gtk.Toolbar()
self.fs_toolbar.set_show_arrow(False)
self.fs_toolbar.set_no_show_all(True)
self.fs_toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
# Exit fullscreen button
button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_LEAVE_FULLSCREEN)
button.set_tooltip_text(_("Leave fullscreen"))
button.show()
self.fs_toolbar.add(button)
button.connect("clicked", self.leave_fullscreen)
def keycombo_menu_clicked(src):
ignore = src
def menu_location(menu, toolbar):
ignore = menu
ignore, x, y = toolbar.get_window().get_origin()
height = toolbar.get_window().get_height()
return x, y + height, True
self.keycombo_menu.popup(None, None, menu_location,
self.fs_toolbar, 0,
Gtk.get_current_event_time())
self.send_key_button = Gtk.ToolButton()
self.send_key_button.set_icon_name(
"preferences-desktop-keyboard-shortcuts")
self.send_key_button.set_tooltip_text(_("Send key combination"))
self.send_key_button.show_all()
self.send_key_button.connect("clicked", keycombo_menu_clicked)
self.fs_toolbar.add(self.send_key_button)
self.fs_drawer = AutoDrawer()
self.fs_drawer.set_active(False)
self.fs_drawer.set_over(self.fs_toolbar)
self.fs_drawer.set_under(scroll)
self.fs_drawer.set_offset(-1)
self.fs_drawer.set_fill(False)
self.fs_drawer.set_overlap_pixels(1)
self.fs_drawer.set_nooverlap_pixels(0)
self.fs_drawer.period = 20
self.fs_drawer.step = .1
self.fs_drawer.show_all()
pages.add(self.fs_drawer)
def _init_menus(self):
# Serial list menu
smenu = Gtk.Menu()
smenu.connect("show", self._populate_serial_menu)
self.widget("details-menu-view-serial-list").set_submenu(smenu)
def _change_title(self, ignore1=None):
title = self.vm.get_name() + " " + _("Virtual Machine")
if self._pointer_is_grabbed and self._viewer:
keystr = self._viewer.console_get_grab_keys()
keymsg = _("Press %s to release pointer.") % keystr
title = keymsg + " " + title
self.topwin.set_title(title)
def someone_has_focus(self):
if (self._viewer and
self._viewer.console_has_focus() and
self._viewer_is_connected):
return True
for serial in self.serial_tabs:
if (serial.terminal and
serial.terminal.get_property("has-focus")):
return True
def viewer_is_visible(self):
return bool(self._viewer and self._viewer.console_get_visible())
def viewer_has_usb_redirection(self):
return bool(self._viewer and
self._viewer.console_has_usb_redirection())
def viewer_get_usb_widget(self):
return self._viewer.console_get_usb_widget()
def viewer_get_pixbuf(self):
return self._viewer.console_get_pixbuf()
def _disable_modifiers(self):
if self.gtk_settings_accel is not None:
return
for g in self.accel_groups:
self.topwin.remove_accel_group(g)
settings = Gtk.Settings.get_default()
self.gtk_settings_accel = settings.get_property('gtk-menu-bar-accel')
settings.set_property('gtk-menu-bar-accel', None)
self.gtk_settings_mnemonic = settings.get_property(
"gtk-enable-mnemonics")
settings.set_property("gtk-enable-mnemonics", False)
def _enable_modifiers(self):
if self.gtk_settings_accel is None:
return
settings = Gtk.Settings.get_default()
settings.set_property('gtk-menu-bar-accel', self.gtk_settings_accel)
self.gtk_settings_accel = None
if self.gtk_settings_mnemonic is not None:
settings.set_property("gtk-enable-mnemonics",
self.gtk_settings_mnemonic)
for g in self.accel_groups:
self.topwin.add_accel_group(g)
def _refresh_enable_accel(self):
# Make sure modifiers are up to date
self._viewer_focus_changed()
def _refresh_resizeguest_from_settings(self):
tooltip = ""
if self._viewer:
if self._viewer.viewer_type != "spice":
tooltip = (
_("Graphics type '%s' does not support auto resize.") %
self._viewer.viewer_type)
elif not self._viewer.console_has_agent():
tooltip = _("Guest agent is not available.")
val = self.vm.get_console_resizeguest()
widget = self.widget("details-menu-view-resizeguest")
widget.set_tooltip_text(tooltip)
widget.set_sensitive(not bool(tooltip))
if not tooltip:
self.widget("details-menu-view-resizeguest").set_active(bool(val))
self.sync_resizeguest_with_display()
def resizeguest_ui_changed_cb(self, src):
# Called from details.py
if not src.get_sensitive():
return
val = int(self.widget("details-menu-view-resizeguest").get_active())
self.vm.set_console_resizeguest(val)
self.sync_resizeguest_with_display()
def sync_resizeguest_with_display(self):
if not self._viewer:
return
val = bool(self.vm.get_console_resizeguest())
self._viewer.console_set_resizeguest(val)
self.widget("console-gfx-scroll").queue_resize()
def refresh_scaling_from_settings(self):
scale_type = self.vm.get_console_scaling()
self.widget("details-menu-view-scale-always").set_active(
scale_type == self.config.CONSOLE_SCALE_ALWAYS)
self.widget("details-menu-view-scale-never").set_active(
scale_type == self.config.CONSOLE_SCALE_NEVER)
self.widget("details-menu-view-scale-fullscreen").set_active(
scale_type == self.config.CONSOLE_SCALE_FULLSCREEN)
self._sync_scaling_with_display()
def scaling_ui_changed_cb(self, src):
# Called from details.py
if not src.get_active():
return
scale_type = 0
if src == self.widget("details-menu-view-scale-always"):
scale_type = self.config.CONSOLE_SCALE_ALWAYS
elif src == self.widget("details-menu-view-scale-fullscreen"):
scale_type = self.config.CONSOLE_SCALE_FULLSCREEN
elif src == self.widget("details-menu-view-scale-never"):
scale_type = self.config.CONSOLE_SCALE_NEVER
self.vm.set_console_scaling(scale_type)
self._sync_scaling_with_display()
def _sync_scaling_with_display(self):
if not self._viewer:
return
curscale = self._viewer.console_get_scaling()
fs = self.widget("control-fullscreen").get_active()
scale_type = self.vm.get_console_scaling()
if (scale_type == self.config.CONSOLE_SCALE_NEVER
and curscale is True):
self._viewer.console_set_scaling(False)
elif (scale_type == self.config.CONSOLE_SCALE_ALWAYS
and curscale is False):
self._viewer.console_set_scaling(True)
elif (scale_type == self.config.CONSOLE_SCALE_FULLSCREEN
and curscale != fs):
self._viewer.console_set_scaling(fs)
# Refresh viewer size
self.widget("console-gfx-scroll").queue_resize()
def details_auth_login(self, ignore):
self.widget("console-pages").set_current_page(CONSOLE_PAGE_UNAVAILABLE)
self.set_credentials()
self.activate_viewer_page()
def toggle_fullscreen(self, src):
do_fullscreen = src.get_active()
self._change_fullscreen(do_fullscreen)
def leave_fullscreen(self, ignore=None):
self._change_fullscreen(False)
def _change_fullscreen(self, do_fullscreen):
self.widget("control-fullscreen").set_active(do_fullscreen)
if do_fullscreen:
self.topwin.fullscreen()
self.fs_toolbar.show()
self.fs_drawer.set_active(True)
self.widget("toolbar-box").hide()
self.widget("details-menubar").hide()
else:
self.fs_toolbar.hide()
self.fs_drawer.set_active(False)
self.topwin.unfullscreen()
if self.widget("details-menu-view-toolbar").get_active():
self.widget("toolbar-box").show()
self.widget("details-menubar").show()
self._sync_scaling_with_display()
def size_to_vm(self, src_ignore):
# Resize the console to best fit the VM resolution
if not self._viewer:
return
if not self._viewer.console_get_desktop_resolution():
return
self.topwin.unmaximize()
self.topwin.resize(1, 1)
self.force_resize = True
self.widget("console-gfx-scroll").queue_resize()
def send_key(self, src, keys):
ignore = src
if keys is not None:
self._viewer.console_send_keys(keys)
##########################
# State tracking methods #
##########################
def view_vm_status(self):
if not self.vm:
# window has been closed and no pages to update are available.
return
status = self.vm.status()
if status == libvirt.VIR_DOMAIN_SHUTOFF:
self.activate_unavailable_page(_("Guest not running"))
else:
if status == libvirt.VIR_DOMAIN_CRASHED:
self.activate_unavailable_page(_("Guest has crashed"))
def close_viewer(self):
if self._viewer is None:
return
viewer = self._viewer
display = getattr(viewer, "_display")
self._viewer = None
viewport = self.widget("console-gfx-viewport")
if display and display in viewport.get_children():
viewport.remove(display)
viewer.close()
self._viewer_is_connected = False
self.refresh_can_fullscreen()
self.leave_fullscreen()
for serial in self.serial_tabs:
serial.close()
def update_widget_states(self, vm, status_ignore):
runable = vm.is_runable()
pages = self.widget("console-pages")
page = pages.get_current_page()
if runable:
if page != CONSOLE_PAGE_UNAVAILABLE:
pages.set_current_page(CONSOLE_PAGE_UNAVAILABLE)
self.view_vm_status()
elif page in [CONSOLE_PAGE_UNAVAILABLE, CONSOLE_PAGE_VIEWER]:
if self._viewer and self._viewer.console_is_open():
self.activate_viewer_page()
else:
self._viewerRetriesScheduled = 0
self._viewerRetryDelay = 125
self.try_login()
return
###################
# Page Navigation #
###################
def activate_unavailable_page(self, msg):
"""
This function is passed to serialcon.py at least, so change
with care
"""
self.close_viewer()
self.widget("console-pages").set_current_page(CONSOLE_PAGE_UNAVAILABLE)
self.widget("details-menu-vm-screenshot").set_sensitive(False)
self.widget("details-menu-usb-redirection").set_sensitive(False)
self.widget("console-unavailable").set_label("<b>" + msg + "</b>")
def activate_auth_page(self, withPassword, withUsername):
(pw, username) = self.config.get_console_password(self.vm)
self.widget("details-menu-vm-screenshot").set_sensitive(False)
self.widget("details-menu-usb-redirection").set_sensitive(False)
self.widget("console-auth-password").set_visible(withPassword)
self.widget("label-auth-password").set_visible(withPassword)
self.widget("console-auth-username").set_visible(withUsername)
self.widget("label-auth-username").set_visible(withUsername)
if withUsername:
self.widget("console-auth-username").grab_focus()
else:
self.widget("console-auth-password").grab_focus()
self.widget("console-auth-username").set_text(username)
self.widget("console-auth-password").set_text(pw)
self.widget("console-auth-remember").set_sensitive(
bool(self.config.has_keyring()))
if self.config.has_keyring():
self.widget("console-auth-remember").set_active(bool(pw and
username))
self.widget("console-pages").set_current_page(
CONSOLE_PAGE_AUTHENTICATE)
def activate_viewer_page(self):
self.widget("console-pages").set_current_page(CONSOLE_PAGE_VIEWER)
self.widget("details-menu-vm-screenshot").set_sensitive(True)
if self._viewer:
self._viewer.console_grab_focus()
if (self._viewer.console_has_usb_redirection() and
self.vm.has_spicevmc_type_redirdev()):
self.widget("details-menu-usb-redirection").set_sensitive(True)
return
def page_changed(self, ignore1=None, ignore2=None, newpage=None):
pagenum = self.widget("console-pages").get_current_page()
if newpage is not None:
for i in range(self.widget("console-pages").get_n_pages()):
w = self.widget("console-pages").get_nth_page(i)
w.set_visible(i == newpage)
if pagenum < CONSOLE_PAGE_OFFSET:
self.last_gfx_page = pagenum
self.refresh_can_fullscreen()
def refresh_can_fullscreen(self):
cpage = self.widget("console-pages").get_current_page()
dpage = self.widget("details-pages").get_current_page()
allow_fullscreen = (dpage == DETAILS_PAGE_CONSOLE and
cpage == CONSOLE_PAGE_VIEWER and
self._viewer_is_connected)
self.widget("control-fullscreen").set_sensitive(allow_fullscreen)
self.widget("details-menu-view-fullscreen").set_sensitive(allow_fullscreen)
def schedule_retry(self):
if self._viewerRetriesScheduled >= 10:
logging.error("Too many connection failures, not retrying again")
return
self.timeout_add(self._viewerRetryDelay, self.try_login)
if self._viewerRetryDelay < 2000:
self._viewerRetryDelay = self._viewerRetryDelay * 2
def skip_connect_attempt(self):
return (self._viewer or
not self.is_visible())
def guest_not_avail(self):
return (self.vm.is_shutoff() or self.vm.is_crashed())
def try_login(self, src_ignore=None):
if self._viewer_is_connecting:
return
try:
self._viewer_is_connecting = True
self._try_login()
finally:
self._viewer_is_connecting = False
def _try_login(self):
if self.skip_connect_attempt():
# Don't try and login for these cases
return
if self.guest_not_avail():
# Guest isn't running, schedule another try
self.activate_unavailable_page(_("Guest not running"))
self.schedule_retry()
return
ginfo = None
try:
gdevs = self.vm.get_graphics_devices()
gdev = gdevs and gdevs[0] or None
if gdev:
ginfo = ConnectionInfo(self.vm.conn, gdev)
except Exception, e:
# We can fail here if VM is destroyed: xen is a bit racy
# and can't handle domain lookups that soon after
logging.exception("Getting graphics console failed: %s", str(e))
return
if ginfo is None:
logging.debug("No graphics configured for guest")
self.activate_unavailable_page(
_("Graphical console not configured for guest"))
return
if ginfo.gtype not in self.config.embeddable_graphics():
logging.debug("Don't know how to show graphics type '%s' "
"disabling console page", ginfo.gtype)
msg = (_("Cannot display graphical console type '%s'")
% ginfo.gtype)
self.activate_unavailable_page(msg)
return
if ginfo.is_bad_localhost():
self.activate_unavailable_page(
_("Guest is on a remote host with transport '%s'\n"
"but is only configured to listen on locally.\n"
"Connect using 'ssh' transport or change the\n"
"guest's listen address." % ginfo.transport))
return
if not ginfo.console_active():
self.activate_unavailable_page(
_("Graphical console is not yet active for guest"))
self.schedule_retry()
return
self.activate_unavailable_page(
_("Connecting to graphical console for guest"))
logging.debug("Starting connect process for %s", ginfo.logstring())
try:
if ginfo.gtype == "vnc":
viewer_class = VNCViewer
elif ginfo.gtype == "spice":
viewer_class = SpiceViewer
self._viewer = viewer_class()
self._connect_viewer_signals()
self._refresh_enable_accel()
self._viewer.console_open_ginfo(ginfo)
except Exception, e:
logging.exception("Error connection to graphical console")
self.activate_unavailable_page(
_("Error connecting to graphical console") + ":\n%s" % e)
def set_credentials(self, src_ignore=None):
passwd = self.widget("console-auth-password")
username = self.widget("console-auth-username")
if passwd.get_visible():
self._viewer.console_set_password(passwd.get_text())
if username.get_visible():
self._viewer.console_set_username(username.get_text())
if self.widget("console-auth-remember").get_active():
self.config.set_console_password(self.vm, passwd.get_text(),
username.get_text())
def scroll_size_allocate(self, src_ignore, req):
if (not self._viewer or
not self._viewer.console_get_desktop_resolution()):
return
scroll = self.widget("console-gfx-scroll")
is_scale = self._viewer.console_get_scaling()
is_resizeguest = self._viewer.console_get_resizeguest()
dx = 0
dy = 0
align_ratio = float(req.width) / float(req.height)
# pylint: disable=unpacking-non-sequence
desktop_w, desktop_h = self._viewer.console_get_desktop_resolution()
if desktop_h == 0:
return
desktop_ratio = float(desktop_w) / float(desktop_h)
if is_scale or self.force_resize:
# Make sure we never show scrollbars when scaling
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
else:
scroll.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
if not self.force_resize and is_resizeguest:
# With resize guest, we don't want to maintain aspect ratio,
# since the guest can resize to arbitrary resolutions.
self._viewer.console_set_size_request(req.width, req.height)
return
if not is_scale or self.force_resize:
# Scaling disabled is easy, just force the VNC widget size. Since
# we are inside a scrollwindow, it shouldn't cause issues.
self.force_resize = False
self._viewer.console_set_size_request(desktop_w, desktop_h)
return
# Make sure there is no hard size requirement so we can scale down
self._viewer.console_set_size_request(-1, -1)
# Make sure desktop aspect ratio is maintained
if align_ratio > desktop_ratio:
desktop_w = int(req.height * desktop_ratio)
desktop_h = req.height
dx = (req.width - desktop_w) / 2
else:
desktop_w = req.width
desktop_h = int(req.width / desktop_ratio)
dy = (req.height - desktop_h) / 2
viewer_alloc = Gdk.Rectangle()
viewer_alloc.x = dx
viewer_alloc.y = dy
viewer_alloc.width = desktop_w
viewer_alloc.height = desktop_h
self._viewer.console_size_allocate(viewer_alloc)
##########################
# Viewer signal handling #
##########################
def _viewer_add_display(self, ignore, display):
self.widget("console-gfx-viewport").add(display)
# Sync initial settings
self._sync_scaling_with_display()
self._refresh_resizeguest_from_settings()
def _pointer_grabbed(self, ignore):
self._pointer_is_grabbed = True
self._change_title()
def _pointer_ungrabbed(self, ignore):
self._pointer_is_grabbed = False
self._change_title()
def _viewer_allocate_cb(self, src, ignore):
self.widget("console-gfx-scroll").queue_resize()
def _viewer_focus_changed(self, ignore1=None, ignore2=None):
force_accel = self.config.get_console_accels()
if force_accel:
self._enable_modifiers()
elif self.someone_has_focus():
self._disable_modifiers()
else:
self._enable_modifiers()
def _viewer_auth_error(self, viewer, errmsg):
viewer.close()
self.activate_unavailable_page(errmsg)
def _viewer_need_auth(self, ignore, withPassword, withUsername):
self.activate_auth_page(withPassword, withUsername)
def _viewer_agent_connected(self, ignore):
self._refresh_resizeguest_from_settings()
def _viewer_usb_redirect_error(self, ignore, errstr):
self.err.show_err(_("USB redirection error"),
text2=str(errstr), modal=True)
def _viewer_disconnected(self, ignore):
errout = ""
if self._viewer:
errout = self._viewer.console_reset_tunnels()
self.widget("console-pages").set_current_page(CONSOLE_PAGE_UNAVAILABLE)
self.close_viewer()
logging.debug("Viewer disconnected")
# Make sure modifiers are set correctly
self._viewer_focus_changed()
if self.guest_not_avail():
# Exit was probably for legitimate reasons
self.view_vm_status()
return
error = _("Error: viewer connection to hypervisor host got refused "
"or disconnected!")
if errout:
logging.debug("Error output from closed console: %s", errout)
error += "\n\nError: %s" % errout
self.activate_unavailable_page(error)
self._refresh_resizeguest_from_settings()
def _viewer_connected(self, ignore):
self._viewer_is_connected = True
self.refresh_can_fullscreen()
logging.debug("Viewer connected")
self.activate_viewer_page()
# Had a successful connect, so reset counters now
self._viewerRetriesScheduled = 0
self._viewerRetryDelay = 125
# Make sure modifiers are set correctly
self._viewer_focus_changed()
def _connect_viewer_signals(self):
self._viewer.connect("add-display-widget", self._viewer_add_display)
self._viewer.connect("pointer-grab", self._pointer_grabbed)
self._viewer.connect("pointer-ungrab", self._pointer_ungrabbed)
self._viewer.connect("size-allocate", self._viewer_allocate_cb)
self._viewer.connect("focus-in-event", self._viewer_focus_changed)
self._viewer.connect("focus-out-event", self._viewer_focus_changed)
self._viewer.connect("connected", self._viewer_connected)
self._viewer.connect("disconnected", self._viewer_disconnected)
self._viewer.connect("auth-error", self._viewer_auth_error)
self._viewer.connect("need-auth", self._viewer_need_auth)
self._viewer.connect("agent-connected", self._viewer_agent_connected)
self._viewer.connect("usb-redirect-error",
self._viewer_usb_redirect_error)
###########################
# Serial console handling #
###########################
def activate_default_console_page(self):
if self.vm.get_graphics_devices() or not self.vm.get_serial_devs():
return
self._populate_serial_menu()
menu = self.widget("details-menu-view-serial-list").get_submenu()
for child in menu.get_children():
if isinstance(child, Gtk.SeparatorMenuItem):
break
if child.get_sensitive():
child.toggled()
break
def _selected_serial_dev(self):
current_page = self.widget("console-pages").get_current_page()
if not current_page >= CONSOLE_PAGE_OFFSET:
return
serial_idx = current_page - CONSOLE_PAGE_OFFSET
if len(self.serial_tabs) < serial_idx:
return
return self.serial_tabs[serial_idx]
def _make_serial_menu_label(self, dev):
if dev.virtual_device_type == "console":
return "Text Console %d" % (dev.vmmindex + 1)
return "Serial %d" % (dev.vmmindex + 1)
def _console_menu_toggled(self, src, dev):
ignore = src
self.widget("details-pages").set_current_page(DETAILS_PAGE_CONSOLE)
if dev.virtual_device_type == "graphics":
self.widget("console-pages").set_current_page(self.last_gfx_page)
return
target_port = dev.vmmindex
name = self._make_serial_menu_label(dev)
serial = None
for s in self.serial_tabs:
if s.name == name:
serial = s
break
if not serial:
serial = vmmSerialConsole(self.vm, target_port, name)
serial.terminal.connect("focus-in-event",
self._viewer_focus_changed)
serial.terminal.connect("focus-out-event",
self._viewer_focus_changed)
title = Gtk.Label(label=name)
self.widget("console-pages").append_page(serial.box, title)
self.serial_tabs.append(serial)
serial.open_console()
page_idx = self.serial_tabs.index(serial) + CONSOLE_PAGE_OFFSET
self.widget("console-pages").set_current_page(page_idx)
def _build_serial_menu_items(self, menu_item_cb):
showing_serial_dev = self._selected_serial_dev()
devs = self.vm.get_serial_devs()
if len(devs) == 0:
menu_item_cb(_("No text console available"),
radio=False, sensitive=False)
return
for dev in devs:
label = self._make_serial_menu_label(dev)
tooltip = vmmSerialConsole.can_connect(self.vm, dev)
sensitive = not bool(tooltip)
active = (sensitive and
showing_serial_dev and
showing_serial_dev.name == label)
menu_item_cb(label, sensitive=sensitive, active=active,
tooltip=tooltip, cb=self._console_menu_toggled, cbdata=dev)
def _build_graphical_menu_items(self, menu_item_cb):
showing_graphics = (
self.widget("console-pages").get_current_page() ==
CONSOLE_PAGE_VIEWER)
# Populate graphical devices
devs = self.vm.get_graphics_devices()
if len(devs) == 0:
menu_item_cb(_("No graphical console available"),
radio=False, sensitive=False)
return
# Only one graphical device supported for now
for idx, dev in enumerate(devs):
label = (_("Graphical Console") + " " +
dev.pretty_type_simple(dev.type))
sensitive = True
tooltip = None
if idx > 0:
label += " %s" % (idx + 1)
sensitive = False
tooltip = _("virt-manager does not support more "
"that one graphical console")
menu_item_cb(label, active=showing_graphics,
sensitive=sensitive, tooltip=tooltip,
cb=self._console_menu_toggled, cbdata=dev)
def _populate_serial_menu(self, ignore=None):
src = self.widget("details-menu-view-serial-list").get_submenu()
for child in src:
src.remove(child)
def menu_item_cb(label, sensitive=True, active=False,
radio=True, tooltip=None, cb=None, cbdata=None):
if radio:
item = Gtk.RadioMenuItem(menu_item_cb.radio_group)
if menu_item_cb.radio_group is None:
menu_item_cb.radio_group = item
item.set_label(label)
else:
item = Gtk.MenuItem.new_with_label(label)
item.set_sensitive(sensitive)
if active:
item.set_active(True)
if tooltip:
item.set_tooltip_text(tooltip)
if cb and sensitive:
item.connect("toggled", cb, cbdata)
src.add(item)
menu_item_cb.radio_group = None
self._build_serial_menu_items(menu_item_cb)
src.add(Gtk.SeparatorMenuItem())
self._build_graphical_menu_items(menu_item_cb)
src.show_all()