summaryrefslogtreecommitdiff
path: root/virtManager
diff options
context:
space:
mode:
authorCole Robinson <crobinso@redhat.com>2013-08-02 10:18:47 -0400
committerCole Robinson <crobinso@redhat.com>2013-08-21 18:41:42 -0400
commite8531b1f405e075d76dc50f13dee41acee7147e0 (patch)
treebb3f9659603c0a45a1faca1a301d1eae3a39a842 /virtManager
parent9d11c7eae3ff5a7782ae6eed54501075c3dc7235 (diff)
downloadvirt-manager-e8531b1f405e075d76dc50f13dee41acee7147e0.tar.gz
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.
Diffstat (limited to 'virtManager')
-rw-r--r--virtManager/baseclass.py7
-rw-r--r--virtManager/details.py111
-rw-r--r--virtManager/domain.py81
-rw-r--r--virtManager/manager.py12
-rw-r--r--virtManager/snapshots.py365
-rw-r--r--virtManager/uihelpers.py19
6 files changed, 523 insertions, 72 deletions
diff --git a/virtManager/baseclass.py b/virtManager/baseclass.py
index 615c85fc..c56c43b8 100644
--- a/virtManager/baseclass.py
+++ b/virtManager/baseclass.py
@@ -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
diff --git a/virtManager/details.py b/virtManager/details.py
index a10eef8a..d52d2252 100644
--- a/virtManager/details.py
+++ b/virtManager/details.py
@@ -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")
diff --git a/virtManager/domain.py b/virtManager/domain.py
index a2ae5c51..f4624bb3 100644
--- a/virtManager/domain.py
+++ b/virtManager/domain.py
@@ -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):
"""
diff --git a/virtManager/manager.py b/virtManager/manager.py
index 9116a323..72318ff5 100644
--- a/virtManager/manager.py
+++ b/virtManager/manager.py
@@ -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)
diff --git a/virtManager/snapshots.py b/virtManager/snapshots.py
new file mode 100644
index 00000000..ec2c5028
--- /dev/null
+++ b/virtManager/snapshots.py
@@ -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))
diff --git a/virtManager/uihelpers.py b/virtManager/uihelpers.py
index 9380d230..b9cd2bbd 100644
--- a/virtManager/uihelpers.py
+++ b/virtManager/uihelpers.py
@@ -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 #