From: Leonardo Garcia <lagarcia@xxxxxxxxxx> Signed-off-by: Leonardo Garcia <lagarcia@xxxxxxxxxx> Signed-off-by: Eduardo Elias Ferreira <edusf@xxxxxxxxxxxxxxxxxx> --- ui/vmm-details.ui | 179 ++++++++++++++++++++++++++++++++++++++++++++++++ virtManager/details.py | 150 ++++++++++++++++++++++++++++++++++------- virtManager/domain.py | 34 +++++++++ virtManager/engine.py | 33 +++++++++ virtManager/error.py | 23 ++++-- 5 files changed, 388 insertions(+), 31 deletions(-) diff --git a/ui/vmm-details.ui b/ui/vmm-details.ui index a04be18..502e70f 100644 --- a/ui/vmm-details.ui +++ b/ui/vmm-details.ui @@ -110,6 +110,17 @@ <property name="can_focus">False</property> <property name="stock">gtk-missing-image</property> </object> + <object class="GtkListStore" id="snapshot-liststore"> + <columns> + <!-- column-name vm-snapshots --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkEntry" id="snapshot-name-entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â??</property> + </object> <object class="GtkWindow" id="vmm-details"> <property name="can_focus">False</property> <property name="title" translatable="yes">Virtual Machine</property> @@ -377,6 +388,17 @@ </object> </child> <child> + <object class="GtkRadioMenuItem" id="details-menu-view-snapshots"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Snapshots</property> + <property name="use_underline">True</property> + <property name="draw_as_radio">True</property> + <property name="group">details-menu-view-console</property> + <signal name="toggled" handler="on_details_menu_view_snapshots_toggled" swapped="no"/> + </object> + </child> + <child> <object class="GtkSeparatorMenuItem" id="menuitem2"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -529,6 +551,23 @@ </packing> </child> <child> + <object class="GtkRadioToolButton" id="control-vm-snapshots"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_markup" translatable="yes">Show virtual hardware details</property> + <property name="tooltip_text" translatable="yes">Show virtual hardware details</property> + <property name="label" translatable="yes">Snapshots</property> + <property name="stock_id">gtk-floppy</property> + <property name="group">control-vm-console</property> + <signal name="toggled" handler="on_control_vm_snapshots_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> <object class="GtkSeparatorToolItem" id="toolbutton3"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -7191,6 +7230,146 @@ I/O:</property> <property name="tab_fill">False</property> </packing> </child> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">3</property> + <child> + <object class="GtkButtonBox" id="buttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">spread</property> + <child> + <object class="GtkButton" id="take-snapshot"> + <property name="label" translatable="yes">Take</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_take_snapshot_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="restore-snapshot"> + <property name="label" translatable="yes">Restore</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_restore_snapshot_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="delete-snapshot"> + <property name="label" translatable="yes">Delete</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_delete_snapshot_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="error-vm-snapshot-box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">3</property> + <child> + <object class="GtkImage" id="error-vm-snapshot-icon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-dialog-error</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="error-vm-snapshot-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Snapshots are disabled for this VM.</property> + <property name="use_markup">True</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> + <child> + <object class="GtkTreeView" id="snapshot-list-treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">snapshot-liststore</property> + <property name="headers_clickable">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="snapshot-treeview-selection"/> + </child> + <child> + <object class="GtkTreeViewColumn" id="vm-snapshot-column"> + <property name="title" translatable="yes">VM Snapshots</property> + <child> + <object class="GtkCellRendererText" id="vm-snapshot-cell-renderer-text"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label88"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Snapshots</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> </object> <packing> <property name="expand">True</property> diff --git a/virtManager/details.py b/virtManager/details.py index afb7945..c2e745d 100644 --- a/virtManager/details.py +++ b/virtManager/details.py @@ -21,6 +21,8 @@ import logging import traceback +from datetime import datetime + # pylint: disable=E0611 from gi.repository import GObject from gi.repository import Gtk @@ -38,6 +40,7 @@ from virtManager.console import vmmConsolePages from virtManager.serialcon import vmmSerialConsole from virtManager.graphwidgets import Sparkline from virtManager import util as util +from virtManager.error import vmmErrorDialog import virtinst @@ -142,7 +145,8 @@ BOOT_ACTIVE = 3 # Main tab pages PAGE_CONSOLE = 0 PAGE_DETAILS = 1 -PAGE_DYNAMIC_OFFSET = 2 +PAGE_SNAPSHOTS = 2 +PAGE_DYNAMIC_OFFSET = 3 def prettyify_disk_bus(bus): @@ -335,6 +339,9 @@ class vmmDetails(vmmGObjectUI): "action-migrate-domain": (GObject.SignalFlags.RUN_FIRST, None, [str, str]), "action-delete-domain": (GObject.SignalFlags.RUN_FIRST, None, [str, str]), "action-clone-domain": (GObject.SignalFlags.RUN_FIRST, None, [str, str]), + "action-take-snapshot": (GObject.SignalFlags.RUN_FIRST, None, [str, str, str]), + "action-restore-snapshot": (GObject.SignalFlags.RUN_FIRST, None, [str, str, str]), + "action-delete-snapshot": (GObject.SignalFlags.RUN_FIRST, None, [str, str, str]), "details-closed": (GObject.SignalFlags.RUN_FIRST, None, []), "details-opened": (GObject.SignalFlags.RUN_FIRST, None, []), "customize-finished": (GObject.SignalFlags.RUN_FIRST, None, []), @@ -400,6 +407,7 @@ class vmmDetails(vmmGObjectUI): "on_control_vm_details_toggled": self.details_console_changed, "on_control_vm_console_toggled": self.details_console_changed, + "on_control_vm_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 +433,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, @@ -516,6 +525,10 @@ class vmmDetails(vmmGObjectUI): "on_hw_list_button_press_event": self.popup_addhw_menu, + "on_take_snapshot_clicked": self._take_snapshot, + "on_restore_snapshot_clicked": self._restore_snapshot, + "on_delete_snapshot_clicked": self._delete_snapshot, + # Listeners stored in vmmConsolePages "on_details_menu_view_fullscreen_activate": self.console.toggle_fullscreen, "on_details_menu_view_size_to_vm_activate": self.console.size_to_vm, @@ -533,6 +546,7 @@ class vmmDetails(vmmGObjectUI): # Deliberately keep all this after signal connection self.vm.connect("status-changed", self.refresh_vm_state) self.vm.connect("config-changed", self.refresh_vm_state) + self.vm.connect("snapshot-changed", self.refresh_vm_state) self.vm.connect("resources-sampled", self.refresh_resources) self.widget("hw-list").get_selection().connect("changed", self.hw_changed) @@ -1364,10 +1378,11 @@ 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_snapshots = (src == self.widget("control-vm-snapshots") or + src == self.widget("details-menu-view-snapshots")) pages = self.widget("details-pages") if pages.get_current_page() == PAGE_DETAILS: @@ -1378,29 +1393,38 @@ class vmmDetails(vmmGObjectUI): if is_details: pages.set_current_page(PAGE_DETAILS) + elif is_snapshots: + 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, page): details = self.widget("control-vm-details") details_menu = self.widget("details-menu-view-details") + snapshots = self.widget("control-vm-snapshots") + snapshots_menu = self.widget("details-menu-view-snapshots") console = self.widget("control-vm-console") console_menu = self.widget("details-menu-view-console") try: self.ignoreDetails = True + is_details = page == PAGE_DETAILS + is_snapshots = page == PAGE_SNAPSHOTS + is_console = not is_details and not is_snapshots details.set_active(is_details) details_menu.set_active(is_details) - console.set_active(not is_details) - console_menu.set_active(not is_details) + snapshots.set_active(is_snapshots) + snapshots_menu.set_active(is_snapshots) + 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: @@ -1649,8 +1673,7 @@ class vmmDetails(vmmGObjectUI): if type(ret) is tuple and len(ret) >= 2: ret = ret[1] - import datetime - now = str(datetime.datetime.now()).split(".")[0].replace(" ", "_") + now = str(datetime.now()).split(".")[0].replace(" ", "_") default = "Screenshot_%s_%s.png" % (self.vm.get_name(), now) path = util.browse_local( @@ -2625,6 +2648,66 @@ class vmmDetails(vmmGObjectUI): return True + ###################### + # Snapshot listeners # + ###################### + + def _take_snapshot(self, src_ignore): + name_dialog = self.err + name_dialog_title = _("Snapshot name") + name_dialog_text = _("Give a unique name for this snapshot") + name_entry = self.widget("snapshot-name-entry") + name = datetime.now().strftime('%Y-%m-%d_%H:%M:%S') + name_entry.set_text(name) + def process_snapshot_name(src, response): + if response == Gtk.ResponseType.CANCEL: + src.destroy() + return + + name = name_entry.get_text() + for snapshot in self.vm.get_snapshots(): + if name == snapshot: + err = vmmErrorDialog(src) + err_summary = _("Snapshot name conflict!") + err_details = _("You need to choose a snapshot name " + "different from the existing ones.") + err.show_err(err_summary, err_details) + return + src.destroy() + self.emit("action-take-snapshot", self.vm.conn.get_uri(), + self.vm.get_uuid(), name) + + name_entry.show() + name_dialog.ask_info(name_dialog_text, None, name_dialog_title, + name_entry, process_snapshot_name) + + def _restore_snapshot(self, src_ignore): + selection = self.widget("snapshot-treeview-selection") + model, path = selection.get_selected_rows() + if not path: + summary = _("Please, select a snapshot to be restored.") + self.err.show_err(summary, dialog_type=Gtk.MessageType.WARNING) + return + selected_snapshot = model[path[0][0]][0] + self.emit("action-restore-snapshot", self.vm.conn.get_uri(), + self.vm.get_uuid(), selected_snapshot) + + def _delete_snapshot(self, src_ignore): + selection = self.widget("snapshot-treeview-selection") + model, path = selection.get_selected_rows() + if not path: + summary = _("Please, select a snapshot to be deleted.") + self.err.show_err(summary, dialog_type=Gtk.MessageType.WARNING) + return + selected_snapshot = model[path[0][0]][0] + self.emit("action-delete-snapshot", self.vm.conn.get_uri(), + self.vm.get_uuid(), selected_snapshot) + + def _enable_snapshot_buttons(self, enable): + self.widget("take-snapshot").set_sensitive(enable) + self.widget("restore-snapshot").set_sensitive(enable) + self.widget("delete-snapshot").set_sensitive(enable) + ######################## # Details page refresh # ######################## @@ -2644,26 +2727,36 @@ class vmmDetails(vmmGObjectUI): self.refresh_stats_page() def page_refresh(self, page): - if page != PAGE_DETAILS: - return - # This function should only be called when the VM xml actually # changes (not everytime it is refreshed). This saves us from blindly # parsing the xml every tick - # Add / remove new devices - self.repopulate_hw_list() + if page == PAGE_DETAILS: + # Add / remove new devices + self.repopulate_hw_list() - pagetype = self.get_hw_selection(HW_LIST_COL_TYPE) - if pagetype is None: - return + pagetype = self.get_hw_selection(HW_LIST_COL_TYPE) + if pagetype is None: + return - if self.widget("config-apply").get_sensitive(): - # Apply button sensitive means user is making changes, don't - # erase them - return + if self.widget("config-apply").get_sensitive(): + # Apply button sensitive means user is making changes, don't + # erase them + return - self.hw_selected(page=pagetype) + self.hw_selected(page=pagetype) + elif page == PAGE_SNAPSHOTS: + error_box = self.widget("error-vm-snapshot-box") + is_snapshot_supported, reason = self.vm.supports_snapshot() + if is_snapshot_supported: + error_box.hide() + self._enable_snapshot_buttons(True) + self._populate_snapshot_list() + else: + self.widget("error-vm-snapshot-label").set_label(reason) + error_box.show() + self._clear_snapshot_list() + self._enable_snapshot_buttons(False) def refresh_overview_page(self): # Basic details @@ -3770,6 +3863,15 @@ class vmmDetails(vmmGObjectUI): # Set a default selection selection.select_path("0") + def _populate_snapshot_list(self): + snapshots = self.widget("snapshot-liststore") + snapshots.clear() + for snapshot in self.vm.get_snapshots(): + snapshots.append([snapshot]) + + def _clear_snapshot_list(self): + self.widget("snapshot-liststore").clear() + def show_pair(self, basename, show): combo = self.widget(basename) label = self.widget(basename + "-title") diff --git a/virtManager/domain.py b/virtManager/domain.py index de0df8a..81f8c97 100644 --- a/virtManager/domain.py +++ b/virtManager/domain.py @@ -29,6 +29,8 @@ import threading import libvirt import virtinst +from virtinst import VirtualDisk + from virtManager import util from virtManager.libvirtobject import vmmLibvirtObject @@ -148,6 +150,7 @@ class vmmDomain(vmmLibvirtObject): "status-changed": (GObject.SignalFlags.RUN_FIRST, None, [int, int]), "resources-sampled": (GObject.SignalFlags.RUN_FIRST, None, []), "inspection-changed": (GObject.SignalFlags.RUN_FIRST, None, []), + "snapshot-changed": (GObject.SignalFlags.RUN_FIRST, None, []), "pre-startup": (GObject.SignalFlags.RUN_FIRST, None, [object]), } @@ -338,6 +341,37 @@ class vmmDomain(vmmLibvirtObject): return "-" return str(i) + def supports_snapshot(self): + disks = self.get_disk_devices() + for disk in disks: + if disk.device == VirtualDisk.DEVICE_CDROM: + continue + if disk.driver_type != "qcow2": + return False, _("You cannot take a snapshot of a VM whose " + "disks are not all in QCOW2 format.") + + if self.is_active(): + return False, _("You cannot take a snapshot of a running VM.") + + return True, None + + def get_snapshots(self): + return self._backend.snapshotListNames() + + def create_snapshot(self, name): + snapshot_xml = "<domainsnapshot><name>%s</name></domainsnapshot>" % name + self._backend.snapshotCreateXML(snapshot_xml, 0) + self.emit("snapshot-changed") + + def restore_snapshot(self, name): + snapshot = self._backend.snapshotLookupByName(name, 0) + self._backend.revertToSnapshot(snapshot, 0) + + def delete_snapshot(self, name): + snapshot = self._backend.snapshotLookupByName(name, 0) + snapshot.delete(0) + self.emit("snapshot-changed") + ############################# # Internal XML handling API # ############################# diff --git a/virtManager/engine.py b/virtManager/engine.py index 456f597..2215bd2 100644 --- a/virtManager/engine.py +++ b/virtManager/engine.py @@ -687,6 +687,9 @@ class vmmEngine(vmmGObject): obj.connect("action-migrate-domain", self._do_show_migrate) obj.connect("action-delete-domain", self._do_delete_domain) obj.connect("action-clone-domain", self._do_show_clone) + obj.connect("action-take-snapshot", self._do_take_snapshot) + obj.connect("action-restore-snapshot", self._do_restore_snapshot) + obj.connect("action-delete-snapshot", self._do_delete_snapshot) obj.connect("details-opened", self.increment_window_counter) obj.connect("details-closed", self.decrement_window_counter) @@ -1081,3 +1084,33 @@ class vmmEngine(vmmGObject): if not self.delete_dialog: self.delete_dialog = vmmDeleteDialog() self.delete_dialog.show(vm, src.topwin) + + def _do_take_snapshot(self, src, uri, uuid, snapshot_name): + conn = self._lookup_conn(uri) + vm = conn.get_vm(uuid) + def errorcb(error, details): + src.err.show_err(_("Error creating snapshot") + ": " + error, + details=details) + title = _("Creating VM snapshot") + vmmAsyncJob.simple_async(vm.create_snapshot, [snapshot_name], title, + "", src, "", errorcb=errorcb) + + def _do_restore_snapshot(self, src, uri, uuid, snapshot_name): + conn = self._lookup_conn(uri) + vm = conn.get_vm(uuid) + def errorcb(error, details): + src.err.show_err(_("Error restoring snapshot") + ": " + error, + details=details) + title = _("Restoring VM snapshot") + vmmAsyncJob.simple_async(vm.restore_snapshot, [snapshot_name], title, + "", src, "", errorcb=errorcb) + + def _do_delete_snapshot(self, src, uri, uuid, snapshot_name): + conn = self._lookup_conn(uri) + vm = conn.get_vm(uuid) + def errorcb(error, details): + src.err.show_err(_("Error deleting snapshot") + ": " + error, + details=details) + title = _("Deleting VM snapshot") + vmmAsyncJob.simple_async(vm.delete_snapshot, [snapshot_name], title, + "", src, "", errorcb=errorcb) diff --git a/virtManager/error.py b/virtManager/error.py index 9cbcd1b..2af5f12 100644 --- a/virtManager/error.py +++ b/virtManager/error.py @@ -28,7 +28,7 @@ from virtManager.baseclass import vmmGObject def _launch_dialog(dialog, primary_text, secondary_text, title, - widget=None, sync=True): + widget=None, sync=True, response_cb=None): dialog.set_property("text", primary_text) dialog.format_secondary_text(secondary_text or None) dialog.set_title(title) @@ -42,9 +42,11 @@ def _launch_dialog(dialog, primary_text, secondary_text, title, res = bool(res in [Gtk.ResponseType.YES, Gtk.ResponseType.OK]) dialog.destroy() else: - def response_destroy(src, ignore): - src.destroy() - dialog.connect("response", response_destroy) + if not response_cb: + def destroy_cb(src, ignore): + src.destroy() + response_cb = destroy_cb + dialog.connect("response", response_cb) dialog.show() return res @@ -97,8 +99,8 @@ class vmmErrorDialog(vmmGObject): # Simple one shot message dialogs # ################################### - def _simple_dialog(self, dialog_type, buttons, text1, - text2, title, widget=None, async=False): + def _simple_dialog(self, dialog_type, buttons, text1, text2, title, + widget=None, async=False, response_cb=None): dialog = Gtk.MessageDialog(self.get_parent(), flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, @@ -111,7 +113,8 @@ class vmmErrorDialog(vmmGObject): return _launch_dialog(self._simple, text1, text2 or "", title or "", widget=widget, - sync=not async) + sync=not async, + response_cb=response_cb) def val_err(self, text1, text2=None, title=_("Input Error"), async=True): logtext = "Validation Error: %s" % text1 @@ -137,6 +140,12 @@ class vmmErrorDialog(vmmGObject): self._simple_dialog(dtype, buttons, text1, text2, title, widget, async) return False + def ask_info(self, text1, text2=None, title="", widget=None, response_cb=None): + dtype = Gtk.MessageType.INFO + buttons = Gtk.ButtonsType.OK_CANCEL + self._simple_dialog(dtype, buttons, text1, text2, title, widget, + async=True, response_cb=response_cb) + def yes_no(self, text1, text2=None, title=None): dtype = Gtk.MessageType.WARNING buttons = Gtk.ButtonsType.YES_NO -- 1.7.1
_______________________________________________ virt-tools-list mailing list virt-tools-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/virt-tools-list