Initial snapshot support

This adds initial UI for managing snapshots: list, run/revert, delete,
add, and redefining (for changing <description>) supported, but currently
only for internal snapshots. The UI is mostly in its final form except for
some bells and whistles.

The real remaining question is what do we want to advertise and support.
Internal (qcow2) snapshots are by far the simplest to manage, very
mature, and already have the semantics we want.

However most recent libvirt and qemu work has been to facilitate
external snapshots, which are more extensible and can be performed
live, and with qemu-ga coordination for extra safety. However
they make things much harder for virt-manager at the moment.

Until we have a plan, this work should be considered experimental
and not be relied upon.
This commit is contained in:
Cole Robinson 2013-08-02 10:18:47 -04:00
parent 9d11c7eae3
commit e8531b1f40
8 changed files with 6272 additions and 5316 deletions

File diff suppressed because it is too large Load Diff

474
ui/vmm-snapshots.ui Normal file
View File

@ -0,0 +1,474 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.6 -->
<object class="GtkDialog" id="snapshot-new">
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="title" translatable="yes">Create snapshot</property>
<property name="type_hint">dialog</property>
<signal name="delete-event" handler="on_snapshot_new_delete_event" swapped="no"/>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox1">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area1">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="snapshot-new-cancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_snapshot_new_cancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="snapshot-new-ok">
<property name="label" translatable="yes">Finish</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="on_snapshot_new_ok_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Name:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="snapshot-new-name">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">●</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">snapshot-new-cancel</action-widget>
<action-widget response="0">snapshot-new-ok</action-widget>
</action-widgets>
</object>
<object class="GtkWindow" id="snapshot-top-window">
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="snapshot-top-box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkPaned" id="spaned1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="position">200</property>
<property name="position_set">True</property>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow7">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="snapshot-list">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection"/>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkNotebook" id="snapshot-notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkBox" id="sbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkAlignment" id="salignment12">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">3</property>
<property name="left_padding">5</property>
<child>
<object class="GtkGrid" id="sgrid1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkLabel" id="slabel94">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
<property name="label" translatable="yes">Description:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow8">
<property name="height_request">80</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="snapshot-description">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label92">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Status:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">3</property>
<child>
<object class="GtkImage" id="snapshot-status-icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-cancel</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="snapshot-status-text">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label">Shut down</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label93">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Timestamp:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="snapshot-timestamp">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label">label</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="snapshot-title">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label">&lt;b&gt;snapshot 'foo' (current)&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">2</property>
<property name="height">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel" id="label89">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">details</property>
</object>
<packing>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="snapshot-error-label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">error label</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="label90">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">empty</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkHBox" id="hbox10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="snapshot-add">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_markup" translatable="yes">Create new snapshot</property>
<property name="tooltip_text" translatable="yes">Create new snapshot</property>
<signal name="clicked" handler="on_snapshot_add_clicked" swapped="no"/>
<child>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-add</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="snapshot-start">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_markup" translatable="yes">Revert guest to selected snapshot</property>
<property name="tooltip_text" translatable="yes">Revert guest to selected snapshot</property>
<signal name="clicked" handler="on_snapshot_start_clicked" swapped="no"/>
<child>
<object class="GtkImage" id="someicon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-play</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="snapshot-delete">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_markup" translatable="yes">Delete selected snapshot</property>
<property name="tooltip_text" translatable="yes">Delete selected snapshot</property>
<signal name="clicked" handler="on_snapshot_delete_clicked" swapped="no"/>
<child>
<object class="GtkImage" id="image11">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-delete</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="buttonbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="snapshot-apply">
<property name="label">gtk-apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_markup" translatable="yes">Save updated snapshot metadata</property>
<property name="tooltip_text" translatable="yes">Save updated snapshot metadata</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_snapshot_apply_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -189,8 +189,11 @@ class vmmGObjectUI(vmmGObject):
self.builder.set_translation_domain("virt-manager")
self.builder.add_from_string(file(uifile).read())
self.topwin = self.widget(windowname)
self.topwin.hide()
if not topwin:
self.topwin = self.widget(windowname)
self.topwin.hide()
else:
self.topwin = topwin
else:
self.builder = builder
self.topwin = topwin

View File

@ -35,6 +35,7 @@ from virtManager.baseclass import vmmGObjectUI
from virtManager.addhardware import vmmAddHardware
from virtManager.choosecd import vmmChooseCD
from virtManager.console import vmmConsolePages
from virtManager.snapshots import vmmSnapshotPage
from virtManager.serialcon import vmmSerialConsole
from virtManager.graphwidgets import Sparkline
@ -42,8 +43,7 @@ import virtinst
from virtinst import util
# Parameters that can be edited in the details window
EDIT_TOTAL = 39
# Parameters that can be editted in the details window
(EDIT_NAME,
EDIT_ACPI,
EDIT_APIC,
@ -95,36 +95,36 @@ EDIT_WATCHDOG_ACTION,
EDIT_CONTROLLER_MODEL,
EDIT_TPM_TYPE,
) = range(EDIT_TOTAL)
) = range(1, 40)
# Columns in hw list model
HW_LIST_COL_LABEL = 0
HW_LIST_COL_ICON_NAME = 1
HW_LIST_COL_ICON_SIZE = 2
HW_LIST_COL_TYPE = 3
HW_LIST_COL_DEVICE = 4
(HW_LIST_COL_LABEL,
HW_LIST_COL_ICON_NAME,
HW_LIST_COL_ICON_SIZE,
HW_LIST_COL_TYPE,
HW_LIST_COL_DEVICE) = range(5)
# Types for the hw list model: numbers specify what order they will be listed
HW_LIST_TYPE_GENERAL = 0
HW_LIST_TYPE_STATS = 1
HW_LIST_TYPE_CPU = 2
HW_LIST_TYPE_MEMORY = 3
HW_LIST_TYPE_BOOT = 4
HW_LIST_TYPE_DISK = 5
HW_LIST_TYPE_NIC = 6
HW_LIST_TYPE_INPUT = 7
HW_LIST_TYPE_GRAPHICS = 8
HW_LIST_TYPE_SOUND = 9
HW_LIST_TYPE_CHAR = 10
HW_LIST_TYPE_HOSTDEV = 11
HW_LIST_TYPE_VIDEO = 12
HW_LIST_TYPE_WATCHDOG = 13
HW_LIST_TYPE_CONTROLLER = 14
HW_LIST_TYPE_FILESYSTEM = 15
HW_LIST_TYPE_SMARTCARD = 16
HW_LIST_TYPE_REDIRDEV = 17
HW_LIST_TYPE_TPM = 18
(HW_LIST_TYPE_GENERAL,
HW_LIST_TYPE_STATS,
HW_LIST_TYPE_CPU,
HW_LIST_TYPE_MEMORY,
HW_LIST_TYPE_BOOT,
HW_LIST_TYPE_DISK,
HW_LIST_TYPE_NIC,
HW_LIST_TYPE_INPUT,
HW_LIST_TYPE_GRAPHICS,
HW_LIST_TYPE_SOUND,
HW_LIST_TYPE_CHAR,
HW_LIST_TYPE_HOSTDEV,
HW_LIST_TYPE_VIDEO,
HW_LIST_TYPE_WATCHDOG,
HW_LIST_TYPE_CONTROLLER,
HW_LIST_TYPE_FILESYSTEM,
HW_LIST_TYPE_SMARTCARD,
HW_LIST_TYPE_REDIRDEV,
HW_LIST_TYPE_TPM) = range(19)
remove_pages = [HW_LIST_TYPE_NIC, HW_LIST_TYPE_INPUT,
HW_LIST_TYPE_GRAPHICS, HW_LIST_TYPE_SOUND, HW_LIST_TYPE_CHAR,
@ -134,15 +134,16 @@ remove_pages = [HW_LIST_TYPE_NIC, HW_LIST_TYPE_INPUT,
HW_LIST_TYPE_REDIRDEV, HW_LIST_TYPE_TPM]
# Boot device columns
BOOT_DEV_TYPE = 0
BOOT_LABEL = 1
BOOT_ICON = 2
BOOT_ACTIVE = 3
(BOOT_DEV_TYPE,
BOOT_LABEL,
BOOT_ICON,
BOOT_ACTIVE) = range(4)
# Main tab pages
PAGE_CONSOLE = 0
PAGE_DETAILS = 1
PAGE_DYNAMIC_OFFSET = 2
(PAGE_CONSOLE,
PAGE_DETAILS,
PAGE_SNAPSHOTS,
PAGE_DYNAMIC_OFFSET) = range(4)
def prettyify_disk_bus(bus):
@ -374,6 +375,8 @@ class vmmDetails(vmmGObjectUI):
self._cpu_copy_host = False
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)
# Set default window size
w, h = self.vm.get_details_window_size()
@ -400,6 +403,7 @@ class vmmDetails(vmmGObjectUI):
"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,
@ -425,6 +429,7 @@ class vmmDetails(vmmGObjectUI):
"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,
@ -576,6 +581,8 @@ class vmmDetails(vmmGObjectUI):
self.console.cleanup()
self.console = None
self.snapshots.cleanup()
self.snapshots = None
self.vm = None
self.conn = None
@ -1369,10 +1376,10 @@ class vmmDetails(vmmGObjectUI):
if not src.get_active():
return
is_details = False
if (src == self.widget("control-vm-details") or
src == self.widget("details-menu-view-details")):
is_details = True
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() == PAGE_DETAILS:
@ -1383,29 +1390,40 @@ class vmmDetails(vmmGObjectUI):
if is_details:
pages.set_current_page(PAGE_DETAILS)
elif is_snapshot:
self.snapshots.show_page()
pages.set_current_page(PAGE_SNAPSHOTS)
else:
pages.set_current_page(self.last_console_page)
def sync_details_console_view(self, is_details):
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 == PAGE_DETAILS
is_snapshot = newpage == 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)
console.set_active(not is_details)
console_menu.set_active(not 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, ignore1=None, ignore2=None, newpage=None):
self.page_refresh(newpage)
self.sync_details_console_view(newpage == PAGE_DETAILS)
self.sync_details_console_view(newpage)
self.console.set_allow_fullscreen()
if newpage == PAGE_CONSOLE or newpage >= PAGE_DYNAMIC_OFFSET:
@ -1467,8 +1485,7 @@ class vmmDetails(vmmGObjectUI):
if not run:
self.activate_default_console_page()
self.widget("overview-status-text").set_text(
self.vm.run_status())
self.widget("overview-status-text").set_text(self.vm.run_status())
self.widget("overview-status-icon").set_from_icon_name(
self.vm.run_status_icon_name(), Gtk.IconSize.MENU)
@ -1507,6 +1524,9 @@ class vmmDetails(vmmGObjectUI):
self._show_serial_tab(name, serialidx)
break
# activate_* are called from engine.py via CLI options
def activate_default_page(self):
pages = self.widget("details-pages")
pages.set_current_page(PAGE_CONSOLE)
@ -2166,7 +2186,8 @@ class vmmDetails(vmmGObjectUI):
if self.widget("security-type-box").get_sensitive():
semodel = self.get_text("security-model")
add_define(self.vm.define_seclabel, semodel, setype, selabel, relabel)
add_define(self.vm.define_seclabel,
semodel, setype, selabel, relabel)
if self.edited(EDIT_DESC):
desc_widget = self.widget("overview-description")

View File

@ -139,6 +139,34 @@ class vmmInspectionData(object):
self.applications = None
class vmmDomainSnapshot(vmmLibvirtObject):
"""
Class wrapping a virDomainSnapshot object
"""
def __init__(self, conn, backend):
vmmLibvirtObject.__init__(self, conn, backend, backend.getName())
self._xmlbackend = None
self.refresh_xml()
def get_name(self):
return self.xml.name
def _XMLDesc(self, flags):
rawxml = self._backend.getXMLDesc(flags=flags)
self._xmlbackend = virtinst.DomainSnapshot(self.conn.get_backend(),
rawxml)
return self._xmlbackend.get_xml_config()
def _get_xml_backend(self):
return self._xmlbackend
xml = property(_get_xml_backend)
def is_current(self):
return self._backend.isCurrent()
def delete(self):
self._backend.delete()
class vmmDomain(vmmLibvirtObject):
"""
Class wrapping virDomain libvirt objects. Is also extended to be
@ -172,6 +200,7 @@ class vmmDomain(vmmLibvirtObject):
self._is_management_domain = None
self._id = None
self._name = None
self._snapshot_list = None
self._inactive_xml_flags = 0
self._active_xml_flags = 0
@ -182,6 +211,7 @@ class vmmDomain(vmmLibvirtObject):
self._getjobinfo_supported = None
self.managedsave_supported = False
self.remote_console_supported = False
self.snapshots_supported = False
self._guest = None
self._guest_to_define = None
@ -201,6 +231,11 @@ class vmmDomain(vmmLibvirtObject):
self._libvirt_init()
def _cleanup(self):
for snap in self._snapshot_list or []:
snap.cleanup()
self._snapshot_list = None
def _get_getvcpus_supported(self):
if self._getvcpus_supported is None:
self._getvcpus_supported = True
@ -232,6 +267,9 @@ class vmmDomain(vmmLibvirtObject):
self.remote_console_supported = self.conn.check_domain_support(
self._backend,
self.conn.SUPPORT_DOMAIN_CONSOLE_STREAM)
self.snapshots_supported = self.conn.check_domain_support(
self._backend,
self.conn.SUPPORT_DOMAIN_LIST_SNAPSHOTS)
# Determine available XML flags (older libvirt versions will error
# out if passed SECURE_XML, INACTIVE_XML, etc)
@ -282,6 +320,7 @@ class vmmDomain(vmmLibvirtObject):
prettyname = "%s %s" % (vendor, product)
ret.append(error % prettyname)
###########################
# Misc API getter methods #
###########################
@ -339,6 +378,7 @@ class vmmDomain(vmmLibvirtObject):
return "-"
return str(i)
#############################
# Internal XML handling API #
#############################
@ -448,7 +488,6 @@ class vmmDomain(vmmLibvirtObject):
self.emit("config-changed")
# Device Add/Remove
def add_device(self, devobj):
"""
Redefine guest with appended device XML 'devxml'
@ -948,6 +987,30 @@ class vmmDomain(vmmLibvirtObject):
def open_console(self, devname, stream, flags=0):
return self._backend.openConsole(devname, stream, flags)
def refresh_snapshots(self):
self._snapshot_list = None
def list_snapshots(self):
if self._snapshot_list is None:
newlist = []
for rawsnap in self._backend.listAllSnapshots():
newlist.append(vmmDomainSnapshot(self.conn, rawsnap))
self._snapshot_list = newlist
return self._snapshot_list[:]
def revert_to_snapshot(self, snap):
self._backend.revertToSnapshot(snap.get_backend())
self.idle_add(self.force_update_status)
def create_snapshot(self, xml, redefine=False):
flags = 0
if redefine:
flags = (flags | libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_REDEFINE)
logging.debug("Creating snapshot flags=%s xml=\n%s", flags, xml)
self._backend.snapshotCreateXML(xml, flags)
########################
# XML Parsing routines #
########################
@ -1539,24 +1602,12 @@ class vmmDomain(vmmLibvirtObject):
return self.status() in [libvirt.VIR_DOMAIN_PAUSED]
def run_status_icon_name(self):
status_icons = {
libvirt.VIR_DOMAIN_BLOCKED: "state_running",
libvirt.VIR_DOMAIN_CRASHED: "state_shutoff",
libvirt.VIR_DOMAIN_PAUSED: "state_paused",
libvirt.VIR_DOMAIN_RUNNING: "state_running",
libvirt.VIR_DOMAIN_SHUTDOWN: "state_shutoff",
libvirt.VIR_DOMAIN_SHUTOFF: "state_shutoff",
libvirt.VIR_DOMAIN_NOSTATE: "state_running",
# VIR_DOMAIN_PMSUSPENDED
7: "state_paused",
}
status = self.status()
if status not in status_icons:
if status not in uihelpers.vm_status_icons:
logging.debug("Unknown status %d, using NOSTATE")
status = libvirt.VIR_DOMAIN_NOSTATE
return status_icons[status]
return uihelpers.vm_status_icons[status]
def force_update_status(self):
"""

View File

@ -62,14 +62,6 @@ COL_DISK = 3
COL_NETWORK = 4
try:
import gi
gi.check_version("3.7.4")
can_set_row_none = True
except (ValueError, AttributeError):
can_set_row_none = False
def _style_get_prop(widget, propname):
value = GObject.Value()
value.init(GObject.TYPE_INT)
@ -903,7 +895,7 @@ class vmmManager(vmmGObjectUI):
if config_changed:
desc = vm.get_description()
if not can_set_row_none:
if not uihelpers.can_set_row_none:
desc = desc or ""
row[ROW_HINT] = util.xml_escape(desc)
except libvirt.libvirtError, e:
@ -922,7 +914,7 @@ class vmmManager(vmmGObjectUI):
row = self.rows[self.vm_row_key(vm)]
new_icon = self.get_inspection_icon_pixbuf(vm, 16, 16)
if not can_set_row_none:
if not uihelpers.can_set_row_none:
new_icon = new_icon or ""
row[ROW_INSPECTION_OS_ICON] = new_icon
model.row_changed(row.path, row.iter)

365
virtManager/snapshots.py Normal file
View File

@ -0,0 +1,365 @@
#
# Copyright (C) 2013 Red Hat, Inc.
# Copyright (C) 2013 Cole Robinson <crobinso@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 datetime
import logging
# pylint: disable=E0611
from gi.repository import Gdk
from gi.repository import Gtk
# pylint: enable=E0611
import libvirt
import virtinst
from virtinst import util
from virtManager import uihelpers
from virtManager.baseclass import vmmGObjectUI
from virtManager.asyncjob import vmmAsyncJob
def _snapshot_state_icon_name(state):
statemap = {
"nostate": libvirt.VIR_DOMAIN_NOSTATE,
"running": libvirt.VIR_DOMAIN_RUNNING,
"blocked": libvirt.VIR_DOMAIN_BLOCKED,
"paused": libvirt.VIR_DOMAIN_PAUSED,
"shutdown": libvirt.VIR_DOMAIN_SHUTDOWN,
"shutoff": libvirt.VIR_DOMAIN_SHUTOFF,
"crashed": libvirt.VIR_DOMAIN_CRASHED,
"pmsuspended": 7,
}
if state == "disk-snapshot" or state not in statemap:
state = "shutoff"
return uihelpers.vm_status_icons[statemap[state]]
class vmmSnapshotPage(vmmGObjectUI):
def __init__(self, vm, builder, topwin):
vmmGObjectUI.__init__(self, "vmm-snapshots.ui",
None, builder=builder, topwin=topwin)
self.vm = vm
self._initial_populate = False
self._init_ui()
self._snapshot_new = self.widget("snapshot-new")
self._snapshot_new.set_transient_for(self.topwin)
self.builder.connect_signals({
"on_snapshot_add_clicked": self._on_add_clicked,
"on_snapshot_delete_clicked": self._on_delete_clicked,
"on_snapshot_start_clicked": self._on_start_clicked,
"on_snapshot_apply_clicked": self._on_apply_clicked,
# 'Create' dialog
"on_snapshot_new_delete_event": self._snapshot_new_close,
"on_snapshot_new_ok_clicked": self._on_new_ok_clicked,
"on_snapshot_new_cancel_clicked" : self._snapshot_new_close,
})
self.top_box = self.widget("snapshot-top-box")
self.widget("snapshot-top-window").remove(self.top_box)
self.widget("snapshot-list").get_selection().connect("changed",
self._snapshot_selected)
self._set_snapshot_state(None)
##############
# Init stuff #
##############
def _cleanup(self):
self.vm = None
self._snapshot_new.destroy()
self._snapshot_new = None
def _init_ui(self):
self.widget("snapshot-notebook").set_show_tabs(False)
buf = Gtk.TextBuffer()
buf.connect("changed", self._description_changed)
self.widget("snapshot-description").set_buffer(buf)
# XXX: This should be a TreeStore, heirarchy is important
# for external snapshots.
# [handle, name, tooltip, is_current]
model = Gtk.ListStore(object, str, str, bool)
model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
col = Gtk.TreeViewColumn("")
col.set_min_width(150)
col.set_expand(True)
col.set_spacing(6)
img = Gtk.CellRendererPixbuf()
img.set_property("icon-name", Gtk.STOCK_YES)
img.set_property("stock-size", Gtk.IconSize.MENU)
img.set_property("xalign", 0)
txt = Gtk.CellRendererText()
col.pack_start(txt, False)
col.pack_start(img, True)
col.add_attribute(txt, 'text', 1)
col.add_attribute(img, 'visible', 3)
slist = self.widget("snapshot-list")
slist.set_model(model)
slist.set_tooltip_column(2)
slist.append_column(col)
self.widget("snapshot-new-ok").set_image(
Gtk.Image.new_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.BUTTON))
###################
# Functional bits #
###################
def _get_current_snapshot(self):
widget = self.widget("snapshot-list")
selection = widget.get_selection()
model, treepath = selection.get_selected()
if treepath is None:
return None
return model[treepath][0]
def _refresh_snapshots(self):
self.vm.refresh_snapshots()
self._populate_snapshot_list()
def show_page(self):
if not self._initial_populate:
self._populate_snapshot_list()
def _set_error_page(self, msg):
self._set_snapshot_state(None)
self.widget("snapshot-notebook").set_current_page(1)
self.widget("snapshot-error-label").set_text(msg)
def _populate_snapshot_list(self):
model = self.widget("snapshot-list").get_model()
model.clear()
if not self.vm.snapshots_supported:
self._set_error_page(_("Libvirt connection does not support "
"snapshots."))
return
try:
snapshots = self.vm.list_snapshots()
except Exception, e:
logging.exception(e)
self._set_error_page(_("Error refreshing snapshot list: %s") %
str(e))
return
do_select = None
for snap in snapshots:
desc = snap.xml.description
if not uihelpers.can_set_row_none:
desc = desc or ""
# XXX: For disk snapshots, this isn't sufficient for determining
# 'current' status
current = bool(snap.is_current())
treeiter = model.append([snap, snap.get_name(),
desc, current])
if current:
do_select = treeiter
self._set_snapshot_state(None)
if len(model):
if do_select is None:
do_select = model.get_iter_from_string("0")
self.widget("snapshot-list").get_selection().select_iter(do_select)
self._initial_populate = True
def _set_snapshot_state(self, snap=None):
self.widget("snapshot-notebook").set_current_page(0)
name = snap and snap.get_name() or ""
desc = snap and snap.xml.description or ""
state = snap and snap.xml.state or "shutoff"
timestamp = ""
if snap:
timestamp = str(datetime.datetime.fromtimestamp(
snap.xml.creationTime))
current = ""
if snap and snap.is_current():
current = " (current)"
title = ""
if name:
title = "<b>Snapshot '%s'%s:</b>" % (util.xml_escape(name),
current)
self.widget("snapshot-title").set_markup(title)
self.widget("snapshot-timestamp").set_text(timestamp)
self.widget("snapshot-description").get_buffer().set_text(desc)
self.widget("snapshot-status-text").set_text(state)
self.widget("snapshot-status-icon").set_from_icon_name(
_snapshot_state_icon_name(state),
Gtk.IconSize.MENU)
self.widget("snapshot-add").set_sensitive(True)
self.widget("snapshot-delete").set_sensitive(bool(snap))
self.widget("snapshot-start").set_sensitive(bool(snap))
self.widget("snapshot-apply").set_sensitive(False)
#############
# Listeners #
#############
def _snapshot_new_close(self, *args, **kwargs):
ignore = args
ignore = kwargs
self._snapshot_new.hide()
return 1
def _description_changed(self, ignore):
self.widget("snapshot-apply").set_sensitive(True)
def _on_apply_clicked(self, ignore):
snap = self._get_current_snapshot()
if not snap:
return
desc_widget = self.widget("snapshot-description")
desc = desc_widget.get_buffer().get_property("text") or ""
snap.xml.description = desc
newxml = snap.xml.get_xml_config()
self.vm.create_snapshot(newxml, redefine=True)
snap.refresh_xml()
self._set_snapshot_state(snap)
# XXX refresh in place
def _on_new_ok_clicked(self, ignore):
name = self.widget("snapshot-new-name").get_text()
newsnap = virtinst.DomainSnapshot(self.vm.conn.get_backend())
newsnap.name = name
# XXX: all manner of flags here: live, quiesce, atomic, etc.
# most aren't relevant for internal?
self.topwin.set_sensitive(False)
self.topwin.get_window().set_cursor(
Gdk.Cursor.new(Gdk.CursorType.WATCH))
self._snapshot_new_close()
progWin = vmmAsyncJob(
lambda ignore, xml: self.vm.create_snapshot(xml),
[newsnap.get_xml_config()],
_("Creating snapshot"),
_("Creating virtual machine snapshot"),
self.topwin)
error, details = progWin.run()
self.topwin.set_sensitive(True)
self.topwin.get_window().set_cursor(
Gdk.Cursor.new(Gdk.CursorType.TOP_LEFT_ARROW))
if error is not None:
error = _("Error creating snapshot: %s") % error
self.err.show_err(error, details=details)
return
self._refresh_snapshots()
def _on_add_clicked(self, ignore):
snap = self._get_current_snapshot()
if not snap:
return
if self._snapshot_new.is_visible():
return
# XXX: generate name
# XXX: default focus, tab order, default action, esc key, alt
self.widget("snapshot-new-name").set_text("foo")
self._snapshot_new.show()
def _on_start_clicked(self, ignore):
snap = self._get_current_snapshot()
if not snap:
return
# XXX: Not true with external disk snapshots, disk changes are
# encoded in the latest snapshot
# XXX: Don't run current?
# XXX: Warn about state change?
result = self.err.yes_no(_("Are you sure you want to revert to "
"snapshot '%s'? All disk changes since "
"the last snapshot will be discarded.") %
snap.get_name())
if not result:
return
logging.debug("Revertin to snapshot '%s'", snap.get_name())
vmmAsyncJob.simple_async_noshow(self.vm.revert_to_snapshot,
[snap], self,
_("Error reverting to snapshot '%s'") %
snap.get_name())
self._refresh_snapshots()
def _on_delete_clicked(self, ignore):
snap = self._get_current_snapshot()
if not snap:
return
result = self.err.yes_no(_("Are you sure you want to permanently "
"delete the snapshot '%s'?") %
snap.get_name())
if not result:
return
# XXX: how does the work for 'current' snapshot?
# XXX: all sorts of flags here like 'delete children', do we care?
logging.debug("Deleting snapshot '%s'", snap.get_name())
vmmAsyncJob.simple_async_noshow(snap.delete, [], self,
_("Error deleting snapshot '%s'") % snap.get_name())
self._refresh_snapshots()
def _snapshot_selected(self, selection):
model, treepath = selection.get_selected()
if treepath is None:
self._set_error_page(_("No snapshot selected."))
return
snap = model[treepath][0]
try:
self._set_snapshot_state(snap)
except Exception, e:
logging.exception(e)
self._set_error_page(_("Error selecting snapshot: %s") % str(e))

View File

@ -39,6 +39,25 @@ OPTICAL_DEV_KEY = 3
OPTICAL_MEDIA_KEY = 4
OPTICAL_IS_VALID = 5
try:
import gi
gi.check_version("3.7.4")
can_set_row_none = True
except (ValueError, AttributeError):
can_set_row_none = False
vm_status_icons = {
libvirt.VIR_DOMAIN_BLOCKED: "state_running",
libvirt.VIR_DOMAIN_CRASHED: "state_shutoff",
libvirt.VIR_DOMAIN_PAUSED: "state_paused",
libvirt.VIR_DOMAIN_RUNNING: "state_running",
libvirt.VIR_DOMAIN_SHUTDOWN: "state_shutoff",
libvirt.VIR_DOMAIN_SHUTOFF: "state_shutoff",
libvirt.VIR_DOMAIN_NOSTATE: "state_running",
# VIR_DOMAIN_PMSUSPENDED
7: "state_paused",
}
############################################################
# Helpers for shared storage UI between create/addhardware #