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.set_translation_domain("virt-manager")
self.builder.add_from_string(file(uifile).read()) self.builder.add_from_string(file(uifile).read())
self.topwin = self.widget(windowname) if not topwin:
self.topwin.hide() self.topwin = self.widget(windowname)
self.topwin.hide()
else:
self.topwin = topwin
else: else:
self.builder = builder self.builder = builder
self.topwin = topwin self.topwin = topwin

View File

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

View File

@ -139,6 +139,34 @@ class vmmInspectionData(object):
self.applications = None 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 vmmDomain(vmmLibvirtObject):
""" """
Class wrapping virDomain libvirt objects. Is also extended to be Class wrapping virDomain libvirt objects. Is also extended to be
@ -172,6 +200,7 @@ class vmmDomain(vmmLibvirtObject):
self._is_management_domain = None self._is_management_domain = None
self._id = None self._id = None
self._name = None self._name = None
self._snapshot_list = None
self._inactive_xml_flags = 0 self._inactive_xml_flags = 0
self._active_xml_flags = 0 self._active_xml_flags = 0
@ -182,6 +211,7 @@ class vmmDomain(vmmLibvirtObject):
self._getjobinfo_supported = None self._getjobinfo_supported = None
self.managedsave_supported = False self.managedsave_supported = False
self.remote_console_supported = False self.remote_console_supported = False
self.snapshots_supported = False
self._guest = None self._guest = None
self._guest_to_define = None self._guest_to_define = None
@ -201,6 +231,11 @@ class vmmDomain(vmmLibvirtObject):
self._libvirt_init() self._libvirt_init()
def _cleanup(self):
for snap in self._snapshot_list or []:
snap.cleanup()
self._snapshot_list = None
def _get_getvcpus_supported(self): def _get_getvcpus_supported(self):
if self._getvcpus_supported is None: if self._getvcpus_supported is None:
self._getvcpus_supported = True self._getvcpus_supported = True
@ -232,6 +267,9 @@ class vmmDomain(vmmLibvirtObject):
self.remote_console_supported = self.conn.check_domain_support( self.remote_console_supported = self.conn.check_domain_support(
self._backend, self._backend,
self.conn.SUPPORT_DOMAIN_CONSOLE_STREAM) 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 # Determine available XML flags (older libvirt versions will error
# out if passed SECURE_XML, INACTIVE_XML, etc) # out if passed SECURE_XML, INACTIVE_XML, etc)
@ -282,6 +320,7 @@ class vmmDomain(vmmLibvirtObject):
prettyname = "%s %s" % (vendor, product) prettyname = "%s %s" % (vendor, product)
ret.append(error % prettyname) ret.append(error % prettyname)
########################### ###########################
# Misc API getter methods # # Misc API getter methods #
########################### ###########################
@ -339,6 +378,7 @@ class vmmDomain(vmmLibvirtObject):
return "-" return "-"
return str(i) return str(i)
############################# #############################
# Internal XML handling API # # Internal XML handling API #
############################# #############################
@ -448,7 +488,6 @@ class vmmDomain(vmmLibvirtObject):
self.emit("config-changed") self.emit("config-changed")
# Device Add/Remove # Device Add/Remove
def add_device(self, devobj): def add_device(self, devobj):
""" """
Redefine guest with appended device XML 'devxml' Redefine guest with appended device XML 'devxml'
@ -948,6 +987,30 @@ class vmmDomain(vmmLibvirtObject):
def open_console(self, devname, stream, flags=0): def open_console(self, devname, stream, flags=0):
return self._backend.openConsole(devname, stream, flags) 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 # # XML Parsing routines #
######################## ########################
@ -1539,24 +1602,12 @@ class vmmDomain(vmmLibvirtObject):
return self.status() in [libvirt.VIR_DOMAIN_PAUSED] return self.status() in [libvirt.VIR_DOMAIN_PAUSED]
def run_status_icon_name(self): 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() status = self.status()
if status not in status_icons: if status not in uihelpers.vm_status_icons:
logging.debug("Unknown status %d, using NOSTATE") logging.debug("Unknown status %d, using NOSTATE")
status = libvirt.VIR_DOMAIN_NOSTATE status = libvirt.VIR_DOMAIN_NOSTATE
return status_icons[status] return uihelpers.vm_status_icons[status]
def force_update_status(self): def force_update_status(self):
""" """

View File

@ -62,14 +62,6 @@ COL_DISK = 3
COL_NETWORK = 4 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): def _style_get_prop(widget, propname):
value = GObject.Value() value = GObject.Value()
value.init(GObject.TYPE_INT) value.init(GObject.TYPE_INT)
@ -903,7 +895,7 @@ class vmmManager(vmmGObjectUI):
if config_changed: if config_changed:
desc = vm.get_description() desc = vm.get_description()
if not can_set_row_none: if not uihelpers.can_set_row_none:
desc = desc or "" desc = desc or ""
row[ROW_HINT] = util.xml_escape(desc) row[ROW_HINT] = util.xml_escape(desc)
except libvirt.libvirtError, e: except libvirt.libvirtError, e:
@ -922,7 +914,7 @@ class vmmManager(vmmGObjectUI):
row = self.rows[self.vm_row_key(vm)] row = self.rows[self.vm_row_key(vm)]
new_icon = self.get_inspection_icon_pixbuf(vm, 16, 16) 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 "" new_icon = new_icon or ""
row[ROW_INSPECTION_OS_ICON] = new_icon row[ROW_INSPECTION_OS_ICON] = new_icon
model.row_changed(row.path, row.iter) 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_MEDIA_KEY = 4
OPTICAL_IS_VALID = 5 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 # # Helpers for shared storage UI between create/addhardware #