mirror of
https://github.com/virt-manager/virt-manager.git
synced 2025-01-08 21:18:04 +03:00
snapshots: Add screenshot support
Show a screenshot in the 'new snapshot' wizard. If we successfully create that snapshot, save the screenshot in ~/.cache/virt-manager/$connuri/$vmuuid/snap-screenshot-$snapname.$ext And show it in the snapshot details overview. We don't do any reaping on snapshot delete, vm delete, etc, but that could be added later.
This commit is contained in:
parent
81c6beca03
commit
868fbd9fc9
@ -603,6 +603,7 @@ test-many-devices, like an alternate RNG.
|
||||
<source file='/dev/default-pool/test-clone-simple.img'/>
|
||||
<target dev='hda' bus='ide'/>
|
||||
</disk>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
|
||||
<test:domainsnapshot>
|
||||
@ -637,6 +638,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -674,6 +676,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>1</active>
|
||||
@ -708,6 +711,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -745,6 +749,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -782,6 +787,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -816,6 +822,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -850,6 +857,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -886,6 +894,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
@ -921,6 +930,7 @@ ba</description>
|
||||
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
|
||||
</disk>
|
||||
<controller type='ide' index='0'/>
|
||||
<graphics type='vnc'/>
|
||||
</devices>
|
||||
</domain>
|
||||
<active>0</active>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.16.0 on Mon Sep 30 18:13:03 2013 -->
|
||||
<!-- Generated with glade 3.16.0 on Tue Oct 1 09:37:34 2013 -->
|
||||
<interface>
|
||||
<!-- interface-requires gtk+ 3.6 -->
|
||||
<object class="GtkImage" id="image3">
|
||||
@ -210,6 +210,34 @@
|
||||
<property name="height">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label6">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Screenshot:</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="GtkImage" id="snapshot-new-screenshot">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="stock">gtk-missing-image</property>
|
||||
</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>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@ -492,6 +520,57 @@
|
||||
<property name="height">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label7">
|
||||
<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">Screenshot:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">5</property>
|
||||
<property name="width">1</property>
|
||||
<property name="height">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="box5">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="snapshot-screenshot">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-missing-image</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-screenshot-label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">No screenshot available</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">5</property>
|
||||
<property name="width">1</property>
|
||||
<property name="height">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@ -501,9 +580,6 @@
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tab">
|
||||
|
@ -312,6 +312,13 @@ class vmmConnection(vmmGObject):
|
||||
return self.config.rhel6_defaults
|
||||
return True
|
||||
|
||||
def get_cache_dir(self):
|
||||
uri = self.get_uri().replace("/", "_")
|
||||
ret = os.path.join(util.get_cache_dir(), uri)
|
||||
if not os.path.exists(ret):
|
||||
os.mkdir(ret, 755)
|
||||
return ret
|
||||
|
||||
|
||||
####################################
|
||||
# Connection pretty print routines #
|
||||
|
@ -23,6 +23,7 @@ from gi.repository import GObject
|
||||
# pylint: enable=E0611
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
|
||||
@ -1660,6 +1661,12 @@ class vmmDomain(vmmLibvirtObject):
|
||||
return self.config.set_pervm(self.uuid, "/console-password",
|
||||
(username, keyid))
|
||||
|
||||
def get_cache_dir(self):
|
||||
ret = os.path.join(self.conn.get_cache_dir(), self.get_uuid())
|
||||
if not os.path.exists(ret):
|
||||
os.mkdir(ret, 0755)
|
||||
return ret
|
||||
|
||||
|
||||
###################
|
||||
# Polling helpers #
|
||||
|
@ -19,10 +19,14 @@
|
||||
#
|
||||
|
||||
import datetime
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import StringIO
|
||||
|
||||
# pylint: disable=E0611
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Gtk
|
||||
# pylint: enable=E0611
|
||||
|
||||
@ -34,6 +38,23 @@ from virtManager.baseclass import vmmGObjectUI
|
||||
from virtManager.asyncjob import vmmAsyncJob
|
||||
|
||||
|
||||
mimemap = {
|
||||
"image/x-portable-pixmap": "ppm",
|
||||
"image/png": "png",
|
||||
}
|
||||
|
||||
|
||||
def _mime_to_ext(val, reverse=False):
|
||||
for m, e in mimemap.items():
|
||||
if val == m and not reverse:
|
||||
return e
|
||||
if val == e and reverse:
|
||||
return m
|
||||
logging.debug("Don't know how to convert %s=%s to %s",
|
||||
reverse and "extension" or "mime", val,
|
||||
reverse and "mime" or "extension")
|
||||
|
||||
|
||||
class vmmSnapshotPage(vmmGObjectUI):
|
||||
def __init__(self, vm, builder, topwin):
|
||||
vmmGObjectUI.__init__(self, "snapshots.ui",
|
||||
@ -137,9 +158,9 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
pass
|
||||
return None
|
||||
|
||||
def _refresh_snapshots(self):
|
||||
def _refresh_snapshots(self, select_name=None):
|
||||
self.vm.refresh_snapshots()
|
||||
self._populate_snapshot_list()
|
||||
self._populate_snapshot_list(select_name)
|
||||
|
||||
def show_page(self):
|
||||
if not self._initial_populate:
|
||||
@ -150,7 +171,7 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
self.widget("snapshot-notebook").set_current_page(1)
|
||||
self.widget("snapshot-error-label").set_text(msg)
|
||||
|
||||
def _populate_snapshot_list(self):
|
||||
def _populate_snapshot_list(self, select_name=None):
|
||||
cursnap = self._get_selected_snapshot()
|
||||
model = self.widget("snapshot-list").get_model()
|
||||
model.clear()
|
||||
@ -189,10 +210,49 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
if has_internal and has_external:
|
||||
model.append([None, None, None, None, "2"])
|
||||
|
||||
uihelpers.set_row_selection(self.widget("snapshot-list"),
|
||||
cursnap and cursnap.get_name() or None)
|
||||
select_name = select_name or (cursnap and cursnap.get_name() or None)
|
||||
uihelpers.set_row_selection(self.widget("snapshot-list"), select_name)
|
||||
self._initial_populate = True
|
||||
|
||||
def _make_screenshot_pixbuf(self, mime, sdata):
|
||||
loader = GdkPixbuf.PixbufLoader.new_with_mime_type(mime)
|
||||
loader.write(sdata)
|
||||
pixbuf = loader.get_pixbuf()
|
||||
loader.close()
|
||||
|
||||
maxsize = 450
|
||||
def _scale(big, small, maxsize):
|
||||
if big <= maxsize:
|
||||
return big, small
|
||||
factor = float(maxsize) / float(big)
|
||||
return maxsize, int(factor * float(small))
|
||||
|
||||
width = pixbuf.get_width()
|
||||
height = pixbuf.get_height()
|
||||
if width > height:
|
||||
width, height = _scale(width, height, maxsize)
|
||||
else:
|
||||
height, width = _scale(height, width, maxsize)
|
||||
|
||||
return pixbuf.scale_simple(width, height,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
def _read_screenshot_file(self, name):
|
||||
if not name:
|
||||
return
|
||||
|
||||
cache_dir = self.vm.get_cache_dir()
|
||||
basename = os.path.join(cache_dir, "snap-screenshot-%s" % name)
|
||||
files = glob.glob(basename + ".*")
|
||||
if not files:
|
||||
return
|
||||
|
||||
filename = files[0]
|
||||
mime = _mime_to_ext(os.path.splitext(filename)[1][1:], reverse=True)
|
||||
if not mime:
|
||||
return
|
||||
return self._make_screenshot_pixbuf(mime, file(filename, "rb").read())
|
||||
|
||||
def _set_snapshot_state(self, snap=None):
|
||||
self.widget("snapshot-notebook").set_current_page(0)
|
||||
|
||||
@ -234,6 +294,12 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
mode = _("External disk only")
|
||||
self.widget("snapshot-mode").set_text(mode)
|
||||
|
||||
sn = self._read_screenshot_file(name)
|
||||
self.widget("snapshot-screenshot").set_visible(bool(sn))
|
||||
self.widget("snapshot-screenshot-label").set_visible(not bool(sn))
|
||||
if sn:
|
||||
self.widget("snapshot-screenshot").set_from_pixbuf(sn)
|
||||
|
||||
self.widget("snapshot-add").set_sensitive(True)
|
||||
self.widget("snapshot-delete").set_sensitive(bool(snap))
|
||||
self.widget("snapshot-start").set_sensitive(bool(snap))
|
||||
@ -244,6 +310,52 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
# 'New' handling #
|
||||
##################
|
||||
|
||||
def _take_screenshot(self):
|
||||
stream = None
|
||||
try:
|
||||
stream = self.vm.conn.get_backend().newStream(0)
|
||||
screen = 0
|
||||
flags = 0
|
||||
mime = self.vm.get_backend().screenshot(stream, screen, flags)
|
||||
|
||||
ret = StringIO.StringIO()
|
||||
def _write_cb(_stream, data, userdata):
|
||||
ignore = stream
|
||||
ignore = userdata
|
||||
ret.write(data)
|
||||
|
||||
stream.recvAll(_write_cb, None)
|
||||
return mime, ret.getvalue()
|
||||
finally:
|
||||
try:
|
||||
if stream:
|
||||
stream.finish()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _get_screenshot(self):
|
||||
if not self.vm.is_active():
|
||||
logging.debug("Skipping screenshot since VM is not active")
|
||||
return
|
||||
if not self.vm.get_graphics_devices():
|
||||
logging.debug("Skipping screenshot since VM has no graphics")
|
||||
return
|
||||
|
||||
try:
|
||||
mime, sdata = self._take_screenshot()
|
||||
except:
|
||||
logging.exception("Error taking screenshot")
|
||||
return
|
||||
|
||||
ext = _mime_to_ext(mime)
|
||||
if not ext:
|
||||
return
|
||||
|
||||
newpix = self._make_screenshot_pixbuf(mime, sdata)
|
||||
setattr(newpix, "vmm_mimetype", mime)
|
||||
setattr(newpix, "vmm_sndata", sdata)
|
||||
return newpix
|
||||
|
||||
def _reset_new_state(self):
|
||||
collidelist = [s.get_xmlobj().name for s in self.vm.list_snapshots()]
|
||||
default_name = DomainSnapshot.find_free_name(
|
||||
@ -257,10 +369,17 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
self.widget("snapshot-new-status-icon").set_from_icon_name(
|
||||
self.vm.run_status_icon_name(), Gtk.IconSize.MENU)
|
||||
|
||||
sn = self._get_screenshot()
|
||||
uihelpers.set_grid_row_visible(
|
||||
self.widget("snapshot-new-screenshot"), bool(sn))
|
||||
if sn:
|
||||
self.widget("snapshot-new-screenshot").set_from_pixbuf(sn)
|
||||
|
||||
|
||||
def _snapshot_new_name_changed(self, src):
|
||||
self.widget("snapshot-new-ok").set_sensitive(bool(src.get_text()))
|
||||
|
||||
def _new_finish_cb(self, error, details):
|
||||
def _new_finish_cb(self, error, details, newname):
|
||||
self.topwin.set_sensitive(True)
|
||||
self.topwin.get_window().set_cursor(
|
||||
Gdk.Cursor.new(Gdk.CursorType.TOP_LEFT_ARROW))
|
||||
@ -269,7 +388,7 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
error = _("Error creating snapshot: %s") % error
|
||||
self.err.show_err(error, details=details)
|
||||
return
|
||||
self._refresh_snapshots()
|
||||
self._refresh_snapshots(newname)
|
||||
|
||||
def _validate_new_snapshot(self):
|
||||
name = self.widget("snapshot-new-name").get_text()
|
||||
@ -281,23 +400,65 @@ class vmmSnapshotPage(vmmGObjectUI):
|
||||
newsnap.name = name
|
||||
newsnap.description = desc or None
|
||||
newsnap.validate()
|
||||
return newsnap.get_xml_config()
|
||||
newsnap.get_xml_config()
|
||||
return newsnap
|
||||
except Exception, e:
|
||||
return self.err.val_err(_("Error validating snapshot: %s" % e))
|
||||
|
||||
def _get_screenshot_data_for_save(self):
|
||||
snwidget = self.widget("snapshot-new-screenshot")
|
||||
if not snwidget.is_visible():
|
||||
return None, None
|
||||
|
||||
sn = snwidget.get_pixbuf()
|
||||
if not sn:
|
||||
return None, None
|
||||
|
||||
mime = getattr(sn, "vmm_mimetype", None)
|
||||
sndata = getattr(sn, "vmm_sndata", None)
|
||||
return mime, sndata
|
||||
|
||||
def _do_create_snapshot(self, asyncjob, xml, name, mime, sndata):
|
||||
ignore = asyncjob
|
||||
|
||||
self.vm.create_snapshot(xml)
|
||||
|
||||
try:
|
||||
cachedir = self.vm.get_cache_dir()
|
||||
basesn = os.path.join(cachedir, "snap-screenshot-%s" % name)
|
||||
|
||||
# Remove any pre-existing screenshots so we don't show stale data
|
||||
for ext in mimemap.values():
|
||||
p = basesn + "." + ext
|
||||
if os.path.exists(basesn + "." + ext):
|
||||
os.unlink(p)
|
||||
|
||||
if not mime or not sndata:
|
||||
return
|
||||
|
||||
filename = basesn + "." + _mime_to_ext(mime)
|
||||
logging.debug("Writing screenshot to %s", filename)
|
||||
file(filename, "wb").write(sndata)
|
||||
except:
|
||||
logging.exception("Error saving screenshot")
|
||||
|
||||
def _create_new_snapshot(self):
|
||||
xml = self._validate_new_snapshot()
|
||||
if xml is False:
|
||||
snap = self._validate_new_snapshot()
|
||||
if not snap:
|
||||
return
|
||||
|
||||
xml = snap.get_xml_config()
|
||||
name = snap.name
|
||||
mime, sndata = self._get_screenshot_data_for_save()
|
||||
|
||||
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, x: self.vm.create_snapshot(x), [xml],
|
||||
self._new_finish_cb, [],
|
||||
self._do_create_snapshot, [xml, name, mime, sndata],
|
||||
self._new_finish_cb, [name],
|
||||
_("Creating snapshot"),
|
||||
_("Creating virtual machine snapshot"),
|
||||
self.topwin)
|
||||
|
Loading…
Reference in New Issue
Block a user