virt-manager/virtManager/migrate.py

441 lines
14 KiB
Python
Raw Normal View History

# Copyright (C) 2009, 2013 Red Hat, Inc.
2009-11-15 23:17:03 +03:00
# Copyright (C) 2009 Cole Robinson <crobinso@redhat.com>
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
2009-11-15 23:17:03 +03:00
import traceback
2009-11-15 23:17:03 +03:00
Convert to use GTK3 and GObject Introspection bindings Switch over to use GObject introspection bindings for all python modules related to GObject/GTK3/etc. It is not possible to mix and match old pyggtk/pygobject manual bindings with new introspection based bindings so it must be all changed in one go. Imports like import gtk Change to from gi.repository import Gtk The vmmGObject class is changed to always inherit from GObject.GObject There is no compelling reason to avoid a GObject dep for the virt-manager TUI & it horribly messed up the code. Signal declarations are changed from vmmChooseCD.signal_new(vmmChooseCD, "cdrom-chosen", [object, str]) To __gsignals__ = { "cdrom-chosen": (GObject.SignalFlags.RUN_FIRST, None, [object, str]) } which is required by new GObject bindings Most of the rest of the change is simply dealing with renamed constants / classes. Alot of legacy compat code was removed - ie helpers which check to see if certain GTK2 methods are available are no longer required since we're mandating GTK3 only. The event loop is replaced with LibvirtGLib's event loop. Still todo - Rip out all DBus stuff & make vmmEngine class inherit GtkApplication which provides unique support & DBus method handling - Switch to use LibvirtGConfig & LibvirtGObject for libvirt interaction - Possibly switch to Python 3 too ? - Figure out why GNOME keyring is missing Introspection support My suggestion is that the standalone GIT repo for virt-install only live on as a support branch for legacy platforms. A stable-0.9 branch of virt-manager can be kept for legacy PyGtk2 based virt-manager releases. The virt-manager master branch should exclusively use GObject inspection and ideally Python3 and contain both the virt-manager and virt-install codebases in one since they are intimately related to each other & using separate GIT repos has needlessly complicated life for everyone. crobinso: Some locking fixes Misc cleanups and dropping now-useless code Fix dbus usage Fix graph cell renderer regression Fix a couple tooltip issues
2012-05-14 17:24:56 +04:00
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import Pango
from virtinst import log
from virtinst import xmlutil
2009-11-15 23:17:03 +03:00
from .lib import uiutil
from .asyncjob import vmmAsyncJob
from .baseclass import vmmGObjectUI
from .connmanager import vmmConnectionManager
from .object.domain import vmmDomain
2009-11-15 23:17:03 +03:00
NUM_COLS = 3
(COL_LABEL,
COL_URI,
COL_CAN_MIGRATE) = range(NUM_COLS)
2010-12-09 01:26:19 +03:00
class vmmMigrateDialog(vmmGObjectUI):
2018-03-15 00:50:22 +03:00
@classmethod
def show_instance(cls, parentobj, vm):
try:
if not cls._instance:
cls._instance = vmmMigrateDialog()
2018-03-15 00:50:22 +03:00
cls._instance.show(parentobj.topwin, vm)
except Exception as e:
parentobj.err.show_err(
_("Error launching migrate dialog: %s") % str(e))
def __init__(self):
vmmGObjectUI.__init__(self, "migrate.ui", "vmm-migrate")
self.vm = None
self.builder.connect_signals({
"on_vmm_migrate_delete_event": self._delete_event,
"on_migrate_cancel_clicked": self._cancel_clicked,
"on_migrate_finish_clicked": self._finish_clicked,
"on_migrate_dest_changed": self._destconn_changed,
"on_migrate_set_address_toggled": self._set_address_toggled,
"on_migrate_set_port_toggled": self._set_port_toggled,
"on_migrate_mode_changed": self._mode_changed,
2009-11-15 23:17:03 +03:00
})
self.bind_escape_key_close()
self._cleanup_on_app_close()
2009-11-15 23:17:03 +03:00
self._init_state()
def _cleanup(self):
self.vm = None
@property
def _connobjs(self):
return vmmConnectionManager.get_instance().conns
@property
def conn(self):
return self.vm and self.vm.conn or None
##############
# Public API #
##############
def show(self, parent, vm):
log.debug("Showing migrate wizard")
self._set_vm(vm)
self._reset_state()
2011-04-14 16:47:42 +04:00
self.topwin.set_transient_for(parent)
2009-11-15 23:17:03 +03:00
self.topwin.present()
def close(self, ignore1=None, ignore2=None):
log.debug("Closing migrate wizard")
2009-11-15 23:17:03 +03:00
self.topwin.hide()
self._set_vm(None)
2009-11-15 23:17:03 +03:00
return 1
def _vm_removed(self, _conn, connkey):
if self.vm.get_connkey() == connkey:
self.close()
def _set_vm(self, newvm):
oldvm = self.vm
if oldvm:
oldvm.conn.disconnect_by_obj(self)
if newvm:
newvm.conn.connect("vm-removed", self._vm_removed)
self.vm = newvm
################
# Init helpers #
################
def _init_state(self):
blue = Gdk.color_parse("#0072A8")
self.widget("header").modify_bg(Gtk.StateType.NORMAL, blue)
# Connection combo
cols = [None] * NUM_COLS
cols[COL_LABEL] = str
cols[COL_URI] = str
cols[COL_CAN_MIGRATE] = bool
model = Gtk.ListStore(*cols)
combo = self.widget("migrate-dest")
combo.set_model(model)
text = uiutil.init_combo_text_column(combo, COL_LABEL)
text.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
text.set_property("width-chars", 30)
combo.add_attribute(text, 'sensitive', COL_CAN_MIGRATE)
model.set_sort_column_id(COL_LABEL, Gtk.SortType.ASCENDING)
def _sorter(model, iter1, iter2, ignore):
2018-01-28 00:21:59 +03:00
def _cmp(a, b):
return ((a > b) - (a < b))
row1 = model[iter1]
row2 = model[iter2]
if row1[COL_URI] is None:
return -1
if row2[COL_URI] is None:
return 1
2018-01-28 00:21:59 +03:00
return _cmp(row1[COL_LABEL], row2[COL_LABEL])
model.set_sort_func(COL_LABEL, _sorter)
# Mode combo
combo = self.widget("migrate-mode")
# label, is_tunnel
model = Gtk.ListStore(str, bool)
model.append([_("Direct"), False])
model.append([_("Tunnelled"), True])
combo.set_model(model)
uiutil.init_combo_text_column(combo, 0)
self.widget("migrate-dest").emit("changed")
self.widget("migrate-mode").set_tooltip_text(
self.widget("migrate-mode-label").get_tooltip_text())
self.widget("migrate-unsafe").set_tooltip_text(
self.widget("migrate-unsafe-label").get_tooltip_text())
self.widget("migrate-temporary").set_tooltip_text(
self.widget("migrate-temporary-label").get_tooltip_text())
2009-11-15 23:17:03 +03:00
def _reset_state(self):
2009-11-15 23:17:03 +03:00
title_str = ("<span size='large' color='white'>%s '%s'</span>" %
(_("Migrate"), xmlutil.xml_escape(self.vm.get_name())))
self.widget("header-label").set_markup(title_str)
2009-11-15 23:17:03 +03:00
self.widget("migrate-advanced-expander").set_expanded(False)
self.widget("migrate-cancel").grab_focus()
2009-12-15 00:48:35 +03:00
self.widget("config-box").set_visible(True)
2009-11-15 23:17:03 +03:00
hostname = self.conn.libvirt_gethostname()
srctext = "%s (%s)" % (hostname, self.conn.get_pretty_desc())
self.widget("migrate-label-name").set_text(self.vm.get_name_or_title())
self.widget("migrate-label-src").set_text(srctext)
self.widget("migrate-label-src").set_tooltip_text(self.conn.get_uri())
2009-11-15 23:17:03 +03:00
self.widget("migrate-set-address").set_active(True)
self.widget("migrate-set-address").emit("toggled")
self.widget("migrate-set-port").set_active(True)
2009-11-15 23:17:03 +03:00
self.widget("migrate-mode").set_active(0)
self.widget("migrate-unsafe").set_active(False)
self.widget("migrate-temporary").set_active(False)
2009-11-15 23:17:03 +03:00
if self.conn.is_xen():
# Default xen port is 8002
self.widget("migrate-port").set_value(8002)
2009-11-15 23:17:03 +03:00
else:
# QEMU migrate port range is 49152+64
self.widget("migrate-port").set_value(49152)
2009-11-15 23:17:03 +03:00
self._populate_destconn()
2009-11-15 23:17:03 +03:00
#############
# Listeners #
#############
def _delete_event(self, ignore1, ignore2):
self.close()
return 1
def _cancel_clicked(self, src):
ignore = src
self.close()
def _finish_clicked(self, src):
ignore = src
self._finish()
2009-11-15 23:17:03 +03:00
def _destconn_changed(self, src):
row = uiutil.get_list_selected_row(src)
if not row:
return
can_migrate = row and row[COL_CAN_MIGRATE] or False
uri = row[COL_URI]
tooltip = ""
if not can_migrate:
tooltip = _("A valid destination connection must be selected.")
self.widget("config-box").set_visible(can_migrate)
self.widget("migrate-finish").set_sensitive(can_migrate)
Convert to use GTK3 and GObject Introspection bindings Switch over to use GObject introspection bindings for all python modules related to GObject/GTK3/etc. It is not possible to mix and match old pyggtk/pygobject manual bindings with new introspection based bindings so it must be all changed in one go. Imports like import gtk Change to from gi.repository import Gtk The vmmGObject class is changed to always inherit from GObject.GObject There is no compelling reason to avoid a GObject dep for the virt-manager TUI & it horribly messed up the code. Signal declarations are changed from vmmChooseCD.signal_new(vmmChooseCD, "cdrom-chosen", [object, str]) To __gsignals__ = { "cdrom-chosen": (GObject.SignalFlags.RUN_FIRST, None, [object, str]) } which is required by new GObject bindings Most of the rest of the change is simply dealing with renamed constants / classes. Alot of legacy compat code was removed - ie helpers which check to see if certain GTK2 methods are available are no longer required since we're mandating GTK3 only. The event loop is replaced with LibvirtGLib's event loop. Still todo - Rip out all DBus stuff & make vmmEngine class inherit GtkApplication which provides unique support & DBus method handling - Switch to use LibvirtGConfig & LibvirtGObject for libvirt interaction - Possibly switch to Python 3 too ? - Figure out why GNOME keyring is missing Introspection support My suggestion is that the standalone GIT repo for virt-install only live on as a support branch for legacy platforms. A stable-0.9 branch of virt-manager can be kept for legacy PyGtk2 based virt-manager releases. The virt-manager master branch should exclusively use GObject inspection and ideally Python3 and contain both the virt-manager and virt-install codebases in one since they are intimately related to each other & using separate GIT repos has needlessly complicated life for everyone. crobinso: Some locking fixes Misc cleanups and dropping now-useless code Fix dbus usage Fix graph cell renderer regression Fix a couple tooltip issues
2012-05-14 17:24:56 +04:00
self.widget("migrate-finish").set_tooltip_text(tooltip)
address = ""
address_warning = ""
tunnel_warning = ""
tunnel_uri = ""
if can_migrate and uri in self._connobjs:
destconn = self._connobjs[uri]
tunnel_uri = destconn.get_uri()
if not destconn.is_remote():
tunnel_warning = _("A remotely accessible libvirt URI "
"is required for tunneled migration, but the "
"selected connection is a local URI. Libvirt will "
"reject this unless you add a transport.")
tunnel_warning = ("<span size='small'>%s</span>" %
tunnel_warning)
address = destconn.libvirt_gethostname()
if self._is_localhost(address):
address_warning = _("The destination's hostname is "
"'localhost', which will be rejected by libvirt. "
"You must configure the destination to have a valid "
"publicly accessible hostname.")
address_warning = ("<span size='small'>%s</span>" %
address_warning)
self.widget("migrate-address").set_text(address)
uiutil.set_grid_row_visible(
self.widget("migrate-address-warning-box"), bool(address_warning))
self.widget("migrate-address-warning-label").set_markup(address_warning)
self.widget("migrate-tunnel-uri").set_text(tunnel_uri)
uiutil.set_grid_row_visible(
self.widget("migrate-tunnel-warning-box"), bool(tunnel_warning))
self.widget("migrate-tunnel-warning-label").set_markup(tunnel_warning)
def _set_address_toggled(self, src):
2009-11-15 23:17:03 +03:00
enable = src.get_active()
self.widget("migrate-address").set_visible(enable)
self.widget("migrate-address-label").set_visible(not enable)
port_enable = self.widget("migrate-set-port").get_active()
self.widget("migrate-set-port").set_active(enable and port_enable)
self.widget("migrate-set-port").emit("toggled")
2009-11-15 23:17:03 +03:00
def _set_port_toggled(self, src):
2009-11-15 23:17:03 +03:00
enable = src.get_active()
self.widget("migrate-port").set_visible(enable)
self.widget("migrate-port-label").set_visible(not enable)
2009-11-15 23:17:03 +03:00
def _is_tunnel_selected(self):
return uiutil.get_list_selection(self.widget("migrate-mode"), column=1)
2009-11-15 23:17:03 +03:00
def _mode_changed(self, src):
ignore = src
is_tunnel = self._is_tunnel_selected()
self.widget("migrate-direct-box").set_visible(not is_tunnel)
self.widget("migrate-tunnel-box").set_visible(is_tunnel)
2009-11-15 23:17:03 +03:00
###########################
# destconn combo handling #
###########################
def _is_localhost(self, addr):
return not addr or addr.startswith("localhost")
def _build_dest_row(self, destconn):
driver = self.conn.get_driver()
origuri = self.conn.get_uri()
can_migrate = False
desc = destconn.get_pretty_desc()
reason = ""
desturi = destconn.get_uri()
if destconn.get_driver() != driver:
reason = _("Hypervisors do not match")
2014-09-12 02:16:21 +04:00
elif destconn.is_disconnected():
reason = _("Disconnected")
elif destconn.get_uri() == origuri:
reason = _("Same connection")
2014-09-12 02:16:21 +04:00
elif destconn.is_active():
can_migrate = True
if reason:
desc = "%s (%s)" % (desc, reason)
return [desc, desturi, can_migrate]
def _populate_destconn(self):
combo = self.widget("migrate-dest")
model = combo.get_model()
model.clear()
rows = []
for conn in list(self._connobjs.values()):
rows.append(self._build_dest_row(conn))
if not any([row[COL_CAN_MIGRATE] for row in rows]):
rows.insert(0,
[_("No usable connections available."), None, False])
for row in rows:
model.append(row)
combo.set_active(0)
for idx, row in enumerate(model):
if row[COL_CAN_MIGRATE]:
combo.set_active(idx)
break
2009-11-15 23:17:03 +03:00
####################
# migrate handling #
####################
2009-11-15 23:17:03 +03:00
def _build_regular_migrate_uri(self):
address = None
if self.widget("migrate-address").get_visible():
address = self.widget("migrate-address").get_text()
2009-11-15 23:17:03 +03:00
port = None
if self.widget("migrate-port").get_visible():
port = int(self.widget("migrate-port").get_value())
if not address:
return
if self.conn.is_xen():
uri = "%s" % address
else:
uri = "tcp:%s" % address
if port:
uri += ":%s" % port
return uri
2009-11-15 23:17:03 +03:00
def _finish_cb(self, error, details, destconn):
self.reset_finish_cursor()
if error:
error = _("Unable to migrate guest: %s") % error
self.err.show_err(error, details=details)
else:
destconn.schedule_priority_tick(pollvm=True)
self.conn.schedule_priority_tick(pollvm=True)
self.close()
def _finish(self):
2009-11-15 23:17:03 +03:00
try:
row = uiutil.get_list_selected_row(self.widget("migrate-dest"))
destlabel = row[COL_LABEL]
destconn = self._connobjs.get(row[COL_URI])
tunnel = self._is_tunnel_selected()
unsafe = self.widget("migrate-unsafe").get_active()
temporary = self.widget("migrate-temporary").get_active()
if tunnel:
uri = self.widget("migrate-tunnel-uri").get_text()
else:
uri = self._build_regular_migrate_uri()
except Exception as e:
2009-11-15 23:17:03 +03:00
details = "".join(traceback.format_exc())
self.err.show_err((_("Uncaught error validating input: %s") %
str(e)),
details=details)
2009-11-15 23:17:03 +03:00
return
self.set_finish_cursor()
2009-11-15 23:17:03 +03:00
cancel_cb = None
if self.vm.getjobinfo_supported:
cancel_cb = (self._cancel_migration, self.vm)
if uri:
destlabel += " " + uri
progWin = vmmAsyncJob(
self._async_migrate,
[self.vm, destconn, uri, tunnel, unsafe, temporary],
self._finish_cb, [destconn],
_("Migrating VM '%s'") % self.vm.get_name(),
(_("Migrating VM '%s' to %s. This may take a while.") %
(self.vm.get_name(), destlabel)),
self.topwin, cancel_cb=cancel_cb)
progWin.run()
2009-11-15 23:17:03 +03:00
def _cancel_migration(self, asyncjob, vm):
log.debug("Cancelling migrate job")
if not vm:
return
try:
vm.abort_job()
except Exception as e:
log.exception("Error cancelling migrate job")
asyncjob.show_warning(_("Error cancelling migrate job: %s") % e)
return
asyncjob.job_canceled = True
return
2010-12-10 17:57:42 +03:00
def _async_migrate(self, asyncjob,
origvm, origdconn, migrate_uri, tunnel, unsafe, temporary):
meter = asyncjob.get_meter()
srcconn = origvm.conn
dstconn = origdconn
vminst = srcconn.get_backend().lookupByName(origvm.get_name())
vm = vmmDomain(srcconn, vminst, vminst.UUID())
log.debug("Migrating vm=%s from %s to %s", vm.get_name(),
srcconn.get_uri(), dstconn.get_uri())
vm.migrate(dstconn, migrate_uri, tunnel, unsafe, temporary,
meter=meter)