virt-manager/virtManager/vmwindow.py

574 lines
20 KiB
Python
Raw Normal View History

# Copyright (C) 2006-2008, 2013, 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 logging
from gi.repository import Gdk
from gi.repository import Gtk
import libvirt
from virtinst import util
from . import vmmenu
from .baseclass import vmmGObjectUI
from .details import vmmDetails
from .engine import vmmEngine
from .snapshots import vmmSnapshotPage
# Main tab pages
(DETAILS_PAGE_DETAILS,
DETAILS_PAGE_CONSOLE,
DETAILS_PAGE_SNAPSHOTS) = range(3)
class vmmVMWindow(vmmGObjectUI):
__gsignals__ = {
"customize-finished": (vmmGObjectUI.RUN_FIRST, None, [object]),
"closed": (vmmGObjectUI.RUN_FIRST, None, []),
}
@classmethod
def get_instance(cls, parentobj, vm):
try:
# Maintain one dialog per VM
key = "%s+%s" % (vm.conn.get_uri(), vm.get_uuid())
if cls._instances is None:
cls._instances = {}
if key not in cls._instances:
cls._instances[key] = vmmVMWindow(vm)
return cls._instances[key]
except Exception as e:
if not parentobj:
raise
parentobj.err.show_err(
_("Error launching details: %s") % str(e))
def __init__(self, vm, parent=None):
vmmGObjectUI.__init__(self, "vmwindow.ui", "vmm-vmwindow")
self.vm = vm
self.is_customize_dialog = False
if parent:
# Details window is being abused as a 'configure before install'
# dialog, set things as appropriate
self.is_customize_dialog = True
self.topwin.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.topwin.set_transient_for(parent)
self.widget("toolbar-box").show()
self.widget("customize-toolbar").show()
self.widget("details-toolbar").hide()
self.widget("details-menubar").hide()
pages = self.widget("details-pages")
pages.set_current_page(DETAILS_PAGE_DETAILS)
else:
self.conn.connect("vm-removed", self._vm_removed)
self._mediacombo = None
self.ignoreDetails = False
from .console import vmmConsolePages
self.console = vmmConsolePages(self.vm, self.builder, self.topwin)
self.snapshots = vmmSnapshotPage(self.vm, self.builder, self.topwin)
self.widget("snapshot-placeholder").add(self.snapshots.top_box)
self._details = vmmDetails(self.vm, self.builder, self.topwin,
self.is_customize_dialog)
self.widget("details-placeholder").add(self._details.top_box)
# Set default window size
w, h = self.vm.get_details_window_size()
if w <= 0:
w = 800
if h <= 0:
h = 600
self.topwin.set_default_size(w, h)
self._window_size = None
self._shutdownmenu = None
self._vmmenu = None
self.init_menus()
self.builder.connect_signals({
"on_close_details_clicked": self.close,
"on_details_menu_close_activate": self.close,
"on_vmm_details_delete_event": self._window_delete_event,
"on_vmm_details_configure_event": self.window_resized,
"on_details_menu_quit_activate": self.exit_app,
"on_control_vm_details_toggled": self.details_console_changed,
"on_control_vm_console_toggled": self.details_console_changed,
"on_control_snapshots_toggled": self.details_console_changed,
"on_control_run_clicked": self.control_vm_run,
"on_control_shutdown_clicked": self.control_vm_shutdown,
"on_control_pause_toggled": self.control_vm_pause,
"on_control_fullscreen_toggled": self.control_fullscreen,
"on_details_customize_finish_clicked": self.customize_finish,
"on_details_cancel_customize_clicked": self._customize_cancel_clicked,
"on_details_menu_virtual_manager_activate": self.control_vm_menu,
"on_details_menu_screenshot_activate": self.control_vm_screenshot,
"on_details_menu_usb_redirection": self.control_vm_usb_redirection,
"on_details_menu_view_toolbar_activate": self.toggle_toolbar,
"on_details_menu_view_manager_activate": self.view_manager,
"on_details_menu_view_details_toggled": self.details_console_changed,
"on_details_menu_view_console_toggled": self.details_console_changed,
"on_details_menu_view_snapshots_toggled": self.details_console_changed,
"on_details_pages_switch_page": self.switch_page,
# Listeners stored in vmmConsolePages
"on_details_menu_view_fullscreen_activate": (
self.console.details_toggle_fullscreen),
"on_details_menu_view_size_to_vm_activate": (
self.console.details_size_to_vm),
"on_details_menu_view_scale_always_toggled": (
self.console.details_scaling_ui_changed_cb),
"on_details_menu_view_scale_fullscreen_toggled": (
self.console.details_scaling_ui_changed_cb),
"on_details_menu_view_scale_never_toggled": (
self.console.details_scaling_ui_changed_cb),
"on_details_menu_view_resizeguest_toggled": (
self.console.details_resizeguest_ui_changed_cb),
"on_console_pages_switch_page": (
self.console.details_page_changed),
"on_console_auth_password_activate": (
self.console.details_auth_login),
"on_console_auth_login_clicked": (
self.console.details_auth_login),
})
# Deliberately keep all this after signal connection
self.vm.connect("state-changed", self.refresh_vm_state)
self.vm.connect("resources-sampled", self.refresh_resources)
self.vm.connect("inspection-changed",
lambda *x: self._details.refresh_os_page())
self.refresh_vm_state()
self.activate_default_page()
@property
def conn(self):
return self.vm.conn
def _cleanup(self):
self.console.cleanup()
self.console = None
self.snapshots.cleanup()
self.snapshots = None
self._shutdownmenu.destroy()
self._shutdownmenu = None
self._vmmenu.destroy()
self._vmmenu = None
if self._window_size:
self.vm.set_details_window_size(*self._window_size)
self.conn.disconnect_by_obj(self)
self.vm = None
def show(self):
logging.debug("Showing VM details: %s", self.vm)
vis = self.is_visible()
self.topwin.present()
if vis:
return
vmmEngine.get_instance().increment_window_counter()
self.refresh_vm_state()
def customize_finish(self, src):
ignore = src
if self._details.vmwindow_has_unapplied_changes():
return
self.emit("customize-finished", self.vm)
def _vm_removed(self, _conn, connkey):
if self.vm.get_connkey() == connkey:
self.cleanup()
def _customize_cancel(self):
logging.debug("Asking to cancel customization")
result = self.err.yes_no(
_("This will abort the installation. Are you sure?"))
if not result:
logging.debug("Customize cancel aborted")
return
logging.debug("Canceling customization")
return self._close()
def _customize_cancel_clicked(self, src):
ignore = src
return self._customize_cancel()
def _window_delete_event(self, ignore1=None, ignore2=None):
return self.close()
def close(self, ignore1=None, ignore2=None):
if self.is_visible():
logging.debug("Closing VM details: %s", self.vm)
return self._close()
def _close(self):
fs = self.widget("details-menu-view-fullscreen")
if fs.get_active():
fs.set_active(False)
if not self.is_visible():
return
self.topwin.hide()
if self.console.details_viewer_is_visible():
try:
self.console.details_close_viewer()
except Exception:
logging.error("Failure when disconnecting from desktop server")
self.emit("closed")
vmmEngine.get_instance().decrement_window_counter()
return 1
##########################
# Initialization helpers #
##########################
def init_menus(self):
# Virtual Machine menu
self._shutdownmenu = vmmenu.VMShutdownMenu(self, lambda: self.vm)
self.widget("control-shutdown").set_menu(self._shutdownmenu)
self.widget("control-shutdown").set_icon_name("system-shutdown")
topmenu = self.widget("details-vm-menu")
submenu = topmenu.get_submenu()
self._vmmenu = vmmenu.VMActionMenu(
self, lambda: self.vm, show_open=False)
for child in submenu.get_children():
submenu.remove(child)
self._vmmenu.add(child)
topmenu.set_submenu(self._vmmenu)
topmenu.show_all()
self.widget("details-pages").set_show_tabs(False)
self.widget("details-menu-view-toolbar").set_active(
self.config.get_details_show_toolbar())
##########################
# Window state listeners #
##########################
def window_resized(self, ignore, ignore2):
if not self.is_visible():
return
self._window_size = self.topwin.get_size()
def control_fullscreen(self, src):
menu = self.widget("details-menu-view-fullscreen")
if src.get_active() != menu.get_active():
menu.set_active(src.get_active())
def toggle_toolbar(self, src):
if self.is_customize_dialog:
return
active = src.get_active()
self.config.set_details_show_toolbar(active)
if (active and not
self.widget("details-menu-view-fullscreen").get_active()):
self.widget("toolbar-box").show()
else:
self.widget("toolbar-box").hide()
def details_console_changed(self, src):
if self.ignoreDetails:
return
if not src.get_active():
return
is_details = (src == self.widget("control-vm-details") or
src == self.widget("details-menu-view-details"))
is_snapshot = (src == self.widget("control-snapshots") or
src == self.widget("details-menu-view-snapshots"))
pages = self.widget("details-pages")
if pages.get_current_page() == DETAILS_PAGE_DETAILS:
if self._details.vmwindow_has_unapplied_changes():
self.sync_details_console_view(True)
return
self._details.disable_apply()
if is_details:
pages.set_current_page(DETAILS_PAGE_DETAILS)
elif is_snapshot:
self.snapshots.show_page()
pages.set_current_page(DETAILS_PAGE_SNAPSHOTS)
else:
pages.set_current_page(DETAILS_PAGE_CONSOLE)
def sync_details_console_view(self, newpage):
details = self.widget("control-vm-details")
details_menu = self.widget("details-menu-view-details")
console = self.widget("control-vm-console")
console_menu = self.widget("details-menu-view-console")
snapshot = self.widget("control-snapshots")
snapshot_menu = self.widget("details-menu-view-snapshots")
is_details = newpage == DETAILS_PAGE_DETAILS
is_snapshot = newpage == DETAILS_PAGE_SNAPSHOTS
is_console = not is_details and not is_snapshot
try:
self.ignoreDetails = True
details.set_active(is_details)
details_menu.set_active(is_details)
snapshot.set_active(is_snapshot)
snapshot_menu.set_active(is_snapshot)
console.set_active(is_console)
console_menu.set_active(is_console)
finally:
self.ignoreDetails = False
def switch_page(self, notebook=None, ignore2=None, newpage=None):
for i in range(notebook.get_n_pages()):
w = notebook.get_nth_page(i)
w.set_visible(i == newpage)
self.page_refresh(newpage)
self.sync_details_console_view(newpage)
self.console.details_refresh_can_fullscreen()
def change_run_text(self, can_restore):
if can_restore:
text = _("_Restore")
else:
text = _("_Run")
strip_text = text.replace("_", "")
self.widget("details-vm-menu").get_submenu().change_run_text(text)
self.widget("control-run").set_label(strip_text)
def refresh_vm_state(self, ignore=None):
vm = self.vm
self.widget("details-menu-view-toolbar").set_active(
self.config.get_details_show_toolbar())
self.toggle_toolbar(self.widget("details-menu-view-toolbar"))
run = vm.is_runable()
stop = vm.is_stoppable()
paused = vm.is_paused()
if vm.managedsave_supported:
self.change_run_text(vm.has_managed_save())
self.widget("control-run").set_sensitive(run)
self.widget("control-shutdown").set_sensitive(stop)
self.widget("control-shutdown").get_menu().update_widget_states(vm)
self.widget("control-pause").set_sensitive(stop)
if paused:
pauseTooltip = _("Resume the virtual machine")
else:
pauseTooltip = _("Pause the virtual machine")
self.widget("control-pause").set_tooltip_text(pauseTooltip)
self.widget("details-vm-menu").get_submenu().update_widget_states(vm)
self.set_pause_state(paused)
errmsg = self.vm.snapshots_supported()
cansnap = not bool(errmsg)
self.widget("control-snapshots").set_sensitive(cansnap)
self.widget("details-menu-view-snapshots").set_sensitive(cansnap)
tooltip = _("Manage VM snapshots")
if not cansnap:
tooltip += "\n" + errmsg
self.widget("control-snapshots").set_tooltip_text(tooltip)
details = self.widget("details-pages")
self.page_refresh(details.get_current_page())
self._details.vmwindow_refresh_vm_state()
self.console.details_update_widget_states()
if not run:
self.activate_default_console_page()
#############################
# External action listeners #
#############################
def view_manager(self, _src):
from .manager import vmmManager
vmmManager.get_instance(self).show()
def exit_app(self, _src):
vmmEngine.get_instance().exit_app()
def activate_default_console_page(self):
pages = self.widget("details-pages")
# console.activate_default_console_page() will as a side effect
# switch to DETAILS_PAGE_CONSOLE. However this code path is triggered
# when the user runs a VM while they are focused on the details page,
# and we don't want to switch pages out from under them.
origpage = pages.get_current_page()
self.console.details_activate_default_console_page()
pages.set_current_page(origpage)
# activate_* are called from engine.py via CLI options
def activate_default_page(self):
if self.is_customize_dialog:
return
pages = self.widget("details-pages")
pages.set_current_page(DETAILS_PAGE_CONSOLE)
self.activate_default_console_page()
def activate_console_page(self):
pages = self.widget("details-pages")
pages.set_current_page(DETAILS_PAGE_CONSOLE)
def activate_performance_page(self):
self.widget("details-pages").set_current_page(DETAILS_PAGE_DETAILS)
self._details.vmwindow_activate_performance_page()
def activate_config_page(self):
self.widget("details-pages").set_current_page(DETAILS_PAGE_DETAILS)
def set_pause_state(self, state):
src = self.widget("control-pause")
try:
src.handler_block_by_func(self.control_vm_pause)
src.set_active(state)
finally:
src.handler_unblock_by_func(self.control_vm_pause)
def control_vm_pause(self, src):
do_pause = src.get_active()
# Set button state back to original value: just let the status
# update function fix things for us
self.set_pause_state(not do_pause)
if do_pause:
vmmenu.VMActionUI.suspend(self, self.vm)
else:
vmmenu.VMActionUI.resume(self, self.vm)
def control_vm_menu(self, src_ignore):
can_usb = bool(self.console.details_viewer_has_usb_redirection() and
self.vm.has_spicevmc_type_redirdev())
self.widget("details-menu-usb-redirection").set_sensitive(can_usb)
def control_vm_run(self, src_ignore):
if self._details.vmwindow_has_unapplied_changes():
return
vmmenu.VMActionUI.run(self, self.vm)
def control_vm_shutdown(self, src_ignore):
vmmenu.VMActionUI.shutdown(self, self.vm)
def control_vm_screenshot(self, src):
ignore = src
try:
return self._take_screenshot()
except Exception as e:
self.err.show_err(_("Error taking screenshot: %s") % str(e))
def control_vm_usb_redirection(self, src):
ignore = src
spice_usbdev_dialog = self.err
spice_usbdev_widget = self.console.details_viewer_get_usb_widget()
if not spice_usbdev_widget:
self.err.show_err(_("Error initializing spice USB device widget"))
return
spice_usbdev_widget.show()
spice_usbdev_dialog.show_info(_("Select USB devices for redirection"),
widget=spice_usbdev_widget,
buttons=Gtk.ButtonsType.CLOSE)
def _take_screenshot(self):
image = self.console.details_viewer_get_pixbuf()
metadata = {
'tEXt::Hypervisor URI': self.vm.conn.get_uri(),
'tEXt::Domain Name': self.vm.get_name(),
'tEXt::Domain UUID': self.vm.get_uuid(),
'tEXt::Generator App': self.config.get_appname(),
'tEXt::Generator Version': self.config.get_appversion(),
}
ret = image.save_to_bufferv(
'png', list(metadata.keys()), list(metadata.values())
)
# On Fedora 19, ret is (bool, str)
# Someday the bindings might be fixed to just return the str, try
# and future proof it a bit
if isinstance(ret, tuple) and len(ret) >= 2:
ret = ret[1]
# F24 rawhide, ret[1] is a named tuple with a 'buffer' element...
if hasattr(ret, "buffer"):
ret = ret.buffer
import datetime
now = str(datetime.datetime.now()).split(".")[0].replace(" ", "_")
default = "Screenshot_%s_%s.png" % (self.vm.get_name(), now)
path = self.err.browse_local(
self.vm.conn, _("Save Virtual Machine Screenshot"),
_type=("png", _("PNG files")),
dialog_type=Gtk.FileChooserAction.SAVE,
browse_reason=self.config.CONFIG_DIR_SCREENSHOT,
default_name=default)
if not path:
logging.debug("No screenshot path given, skipping save.")
return
filename = path
if not filename.endswith(".png"):
filename += ".png"
open(filename, "wb").write(ret)
########################
# Details page refresh #
########################
def refresh_resources(self, ignore):
details = self.widget("details-pages")
page = details.get_current_page()
# If the dialog is visible, we want to make sure the XML is always
# up to date
try:
if self.is_visible():
self.vm.ensure_latest_xml()
except libvirt.libvirtError as e:
if util.exception_is_libvirt_error(e, "VIR_ERR_NO_DOMAIN"):
self.close()
return
raise
if page == DETAILS_PAGE_DETAILS:
self._details.vmwindow_resources_refreshed()
def page_refresh(self, page):
if page == DETAILS_PAGE_DETAILS:
self._details.vmwindow_page_refresh()