mirror of
https://github.com/virt-manager/virt-manager.git
synced 2025-02-01 05:47:21 +03:00
4c10a627ab
According to http://grammar.quickanddirtytips.com/a-while-awhile.aspx, you can tell whether to use 'awhile' or 'a while' by substituting 'quietly' or 'a year' and seeing if the resulting sentence still makes sense. During migration, we used an adverb, but want the noun form. * src/virtManager/migrate.py (vmmMigrateDialog.finish): Use correct grammar. (crobinso: Add Eric to AUTHORS)
557 lines
19 KiB
Python
557 lines
19 KiB
Python
#
|
|
# Copyright (C) 2009, 2013 Red Hat, Inc.
|
|
# Copyright (C) 2009 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 traceback
|
|
import logging
|
|
import threading
|
|
|
|
from gi.repository import Gdk
|
|
from gi.repository import GLib
|
|
from gi.repository import Gtk
|
|
|
|
import virtinst
|
|
import libvirt
|
|
|
|
from virtManager import util
|
|
from virtManager.baseclass import vmmGObjectUI
|
|
from virtManager.asyncjob import vmmAsyncJob
|
|
from virtManager.domain import vmmDomain
|
|
|
|
def uri_join(uri_tuple):
|
|
scheme, user, host, path, query, fragment = uri_tuple
|
|
|
|
user = (user and (user + "@") or "")
|
|
host = host or ""
|
|
path = path or "/"
|
|
query = (query and ("?" + query) or "")
|
|
fragment = (fragment and ("#" + fragment) or "")
|
|
|
|
return "%s://%s%s%s%s%s" % (scheme, user, host, path, fragment, query)
|
|
|
|
|
|
class vmmMigrateDialog(vmmGObjectUI):
|
|
def __init__(self, vm, engine):
|
|
vmmGObjectUI.__init__(self, "vmm-migrate.ui", "vmm-migrate")
|
|
self.vm = vm
|
|
self.conn = vm.conn
|
|
self.engine = engine
|
|
|
|
self.destconn_rows = []
|
|
|
|
self.builder.connect_signals({
|
|
"on_vmm_migrate_delete_event" : self.close,
|
|
|
|
"on_migrate_cancel_clicked" : self.close,
|
|
"on_migrate_finish_clicked" : self.finish,
|
|
|
|
"on_migrate_dest_changed" : self.destconn_changed,
|
|
"on_migrate_set_rate_toggled" : self.toggle_set_rate,
|
|
"on_migrate_set_interface_toggled" : self.toggle_set_interface,
|
|
"on_migrate_set_port_toggled" : self.toggle_set_port,
|
|
"on_migrate_set_maxdowntime_toggled" : self.toggle_set_maxdowntime,
|
|
})
|
|
self.bind_escape_key_close()
|
|
|
|
blue = Gdk.color_parse("#0072A8")
|
|
self.widget("migrate-header").modify_bg(Gtk.StateType.NORMAL,
|
|
blue)
|
|
image = Gtk.Image.new_from_icon_name("vm_clone_wizard",
|
|
Gtk.IconSize.DIALOG)
|
|
image.show()
|
|
self.widget("migrate-vm-icon-box").pack_end(image, False, False, False)
|
|
|
|
self.init_state()
|
|
|
|
def show(self, parent):
|
|
logging.debug("Showing migrate wizard")
|
|
self.reset_state()
|
|
self.topwin.resize(1, 1)
|
|
self.topwin.set_transient_for(parent)
|
|
self.topwin.present()
|
|
|
|
def close(self, ignore1=None, ignore2=None):
|
|
logging.debug("Closing migrate wizard")
|
|
self.topwin.hide()
|
|
# If we only do this at show time, operation takes too long and
|
|
# user actually sees the expander close.
|
|
self.widget("migrate-advanced-expander").set_expanded(False)
|
|
return 1
|
|
|
|
def _cleanup(self):
|
|
self.vm = None
|
|
self.conn = None
|
|
self.engine = None
|
|
self.destconn_rows = None
|
|
|
|
# Not sure why we need to do this manually, but it matters
|
|
self.widget("migrate-dest").get_model().clear()
|
|
|
|
def init_state(self):
|
|
# [hostname, conn, can_migrate, tooltip]
|
|
dest_model = Gtk.ListStore(str, object, bool, str)
|
|
dest_combo = self.widget("migrate-dest")
|
|
dest_combo.set_model(dest_model)
|
|
text = Gtk.CellRendererText()
|
|
dest_combo.pack_start(text, True)
|
|
dest_combo.add_attribute(text, 'text', 0)
|
|
dest_combo.add_attribute(text, 'sensitive', 2)
|
|
dest_model.set_sort_column_id(0, Gtk.SortType.ASCENDING)
|
|
# XXX no way to set tooltips here, kind of annoying
|
|
|
|
# Hook up signals to get connection listing
|
|
self.engine.connect("conn-added", self.dest_add_conn)
|
|
self.engine.connect("conn-removed", self.dest_remove_conn)
|
|
self.destconn_changed(dest_combo)
|
|
|
|
def reset_state(self):
|
|
title_str = ("<span size='large' color='white'>%s '%s'</span>" %
|
|
(_("Migrate"), util.xml_escape(self.vm.get_name())))
|
|
self.widget("migrate-main-label").set_markup(title_str)
|
|
|
|
self.widget("migrate-cancel").grab_focus()
|
|
|
|
name = self.vm.get_name()
|
|
srchost = self.conn.get_hostname()
|
|
|
|
self.widget("migrate-label-name").set_text(name)
|
|
self.widget("migrate-label-src").set_text(srchost)
|
|
|
|
self.widget("migrate-set-interface").set_active(False)
|
|
self.widget("migrate-set-rate").set_active(False)
|
|
self.widget("migrate-set-port").set_active(False)
|
|
self.widget("migrate-set-maxdowntime").set_active(False)
|
|
self.widget("migrate-max-downtime").set_value(30)
|
|
|
|
running = self.vm.is_active()
|
|
self.widget("migrate-offline").set_active(not running)
|
|
self.widget("migrate-offline").set_sensitive(running)
|
|
|
|
self.widget("migrate-rate").set_value(0)
|
|
self.widget("migrate-secure").set_active(False)
|
|
|
|
downtime_box = self.widget("migrate-maxdowntime-box")
|
|
support_downtime = self.vm.support_downtime()
|
|
downtime_tooltip = ""
|
|
if not support_downtime:
|
|
downtime_tooltip = _("Libvirt version does not support setting "
|
|
"downtime.")
|
|
downtime_box.set_sensitive(support_downtime)
|
|
downtime_box.set_tooltip_text(downtime_tooltip)
|
|
|
|
if self.conn.is_xen():
|
|
# Default xen port is 8002
|
|
self.widget("migrate-port").set_value(8002)
|
|
else:
|
|
# QEMU migrate port range is 49152+64
|
|
self.widget("migrate-port").set_value(49152)
|
|
|
|
secure_box = self.widget("migrate-secure-box")
|
|
support_secure = hasattr(libvirt, "VIR_MIGRATE_TUNNELLED")
|
|
secure_tooltip = ""
|
|
if not support_secure:
|
|
secure_tooltip = _("Libvirt version does not support tunnelled "
|
|
"migration.")
|
|
|
|
secure_box.set_sensitive(support_secure)
|
|
secure_box.set_tooltip_text(secure_tooltip)
|
|
|
|
self.rebuild_dest_rows()
|
|
|
|
def set_state(self, vm):
|
|
self.vm = vm
|
|
self.conn = vm.conn
|
|
self.reset_state()
|
|
|
|
def destconn_changed(self, src):
|
|
active = src.get_active()
|
|
tooltip = ""
|
|
if active == -1:
|
|
tooltip = _("A valid destination connection must be selected.")
|
|
|
|
self.widget("migrate-finish").set_sensitive(active != -1)
|
|
self.widget("migrate-finish").set_tooltip_text(tooltip)
|
|
|
|
def toggle_set_rate(self, src):
|
|
enable = src.get_active()
|
|
self.widget("migrate-rate").set_sensitive(enable)
|
|
|
|
def toggle_set_interface(self, src):
|
|
enable = src.get_active()
|
|
port_enable = self.widget("migrate-set-port").get_active()
|
|
self.widget("migrate-interface").set_sensitive(enable)
|
|
self.widget("migrate-set-port").set_sensitive(enable)
|
|
self.widget("migrate-port").set_sensitive(enable and port_enable)
|
|
|
|
def toggle_set_maxdowntime(self, src):
|
|
enable = src.get_active()
|
|
self.widget("migrate-max-downtime").set_sensitive(enable)
|
|
|
|
def toggle_set_port(self, src):
|
|
enable = src.get_active()
|
|
self.widget("migrate-port").set_sensitive(enable)
|
|
|
|
def get_config_destconn(self):
|
|
combo = self.widget("migrate-dest")
|
|
model = combo.get_model()
|
|
|
|
idx = combo.get_active()
|
|
if idx == -1:
|
|
return None
|
|
|
|
row = model[idx]
|
|
if not row[2]:
|
|
return None
|
|
|
|
return row[1]
|
|
|
|
def get_config_offline(self):
|
|
return self.widget("migrate-offline").get_active()
|
|
|
|
def get_config_max_downtime(self):
|
|
if not self.get_config_max_downtime_enabled():
|
|
return 0
|
|
return int(self.widget("migrate-max-downtime").get_value())
|
|
|
|
def get_config_secure(self):
|
|
return self.widget("migrate-secure").get_active()
|
|
|
|
def get_config_max_downtime_enabled(self):
|
|
return self.widget("migrate-max-downtime").get_sensitive()
|
|
|
|
def get_config_rate_enabled(self):
|
|
return self.widget("migrate-rate").get_sensitive()
|
|
def get_config_rate(self):
|
|
if not self.get_config_rate_enabled():
|
|
return 0
|
|
return int(self.widget("migrate-rate").get_value())
|
|
|
|
def get_config_interface_enabled(self):
|
|
return self.widget("migrate-interface").get_sensitive()
|
|
def get_config_interface(self):
|
|
if not self.get_config_interface_enabled():
|
|
return None
|
|
return self.widget("migrate-interface").get_text()
|
|
|
|
def get_config_port_enabled(self):
|
|
return self.widget("migrate-port").get_sensitive()
|
|
def get_config_port(self):
|
|
if not self.get_config_port_enabled():
|
|
return 0
|
|
return int(self.widget("migrate-port").get_value())
|
|
|
|
def build_localhost_uri(self, destconn, srcuri):
|
|
desthost = destconn.get_qualified_hostname()
|
|
if desthost == "localhost":
|
|
# We couldn't find a host name for the destination machine
|
|
# that is accessible from the source machine.
|
|
# /etc/hosts is likely borked and the only hostname it will
|
|
# give us is localhost. Remember, the dest machine can actually
|
|
# be our local machine so we may not already know its hostname
|
|
raise RuntimeError(_("Could not determine remotely accessible "
|
|
"hostname for destination connection."))
|
|
|
|
# Since the connection started as local, we have no clue about
|
|
# how to access it remotely. Assume users have a uniform access
|
|
# setup and use the same credentials as the remote source URI
|
|
return self.edit_uri(srcuri, desthost, None)
|
|
|
|
def edit_uri(self, uri, hostname, port):
|
|
split = list(virtinst.util.uri_split(uri))
|
|
|
|
hostname = hostname or split[2]
|
|
if port:
|
|
if hostname.count(":"):
|
|
hostname = hostname.split(":")[0]
|
|
hostname += ":%s" % port
|
|
|
|
split[2] = hostname
|
|
return uri_join(tuple(split))
|
|
|
|
def build_migrate_uri(self, destconn, srcuri):
|
|
conn = self.conn
|
|
|
|
interface = self.get_config_interface()
|
|
port = self.get_config_port()
|
|
secure = self.get_config_secure()
|
|
|
|
if not interface and not secure:
|
|
return None
|
|
|
|
if secure:
|
|
# P2P migration uri is a libvirt connection uri, e.g.
|
|
# qemu+ssh://root@foobar/system
|
|
|
|
# For secure migration, we need to make sure we aren't migrating
|
|
# to the local connection, because libvirt will pull try to use
|
|
# 'qemu:///system' as the migrate URI which will deadlock
|
|
if destconn.is_local():
|
|
uri = self.build_localhost_uri(destconn, srcuri)
|
|
else:
|
|
uri = destconn.get_uri()
|
|
|
|
uri = self.edit_uri(uri, interface, port)
|
|
|
|
else:
|
|
# Regular migration URI is HV specific
|
|
uri = ""
|
|
if conn.is_xen():
|
|
uri = "xenmigr://%s" % interface
|
|
|
|
else:
|
|
uri = "tcp:%s" % interface
|
|
|
|
if port:
|
|
uri += ":%s" % port
|
|
|
|
return uri or None
|
|
|
|
def rebuild_dest_rows(self):
|
|
newrows = []
|
|
|
|
for row in self.destconn_rows:
|
|
newrows.append(self.build_dest_row(row[1]))
|
|
|
|
self.destconn_rows = newrows
|
|
self.populate_dest_combo()
|
|
|
|
def populate_dest_combo(self):
|
|
combo = self.widget("migrate-dest")
|
|
model = combo.get_model()
|
|
idx = combo.get_active()
|
|
idxconn = None
|
|
if idx != -1:
|
|
idxconn = model[idx][1]
|
|
|
|
rows = [[_("No connections available."), None, False, None]]
|
|
if self.destconn_rows:
|
|
rows = self.destconn_rows
|
|
|
|
model.clear()
|
|
for r in rows:
|
|
# Don't list the current connection
|
|
if r[1] == self.conn:
|
|
continue
|
|
model.append(r)
|
|
|
|
# Find old index
|
|
idx = -1
|
|
for i in range(len(model)):
|
|
row = model[i]
|
|
conn = row[1]
|
|
|
|
if idxconn:
|
|
if conn == idxconn and row[2]:
|
|
idx = i
|
|
break
|
|
else:
|
|
if row[2]:
|
|
idx = i
|
|
break
|
|
|
|
combo.set_active(idx)
|
|
|
|
def dest_add_conn(self, engine_ignore, conn):
|
|
combo = self.widget("migrate-dest")
|
|
model = combo.get_model()
|
|
|
|
newrow = self.build_dest_row(conn)
|
|
|
|
# Make sure connection isn't already present
|
|
for row in model:
|
|
if row[1] and row[1].get_uri() == newrow[1].get_uri():
|
|
return
|
|
|
|
conn.connect("state-changed", self.destconn_state_changed)
|
|
self.destconn_rows.append(newrow)
|
|
self.populate_dest_combo()
|
|
|
|
def dest_remove_conn(self, engine_ignore, uri):
|
|
# Make sure connection isn't already present
|
|
for row in self.destconn_rows:
|
|
if row[1] and row[1].get_uri() == uri:
|
|
self.destconn_rows.remove(row)
|
|
|
|
self.populate_dest_combo()
|
|
|
|
def destconn_state_changed(self, conn):
|
|
for row in self.destconn_rows:
|
|
if row[1] == conn:
|
|
self.destconn_rows.remove(row)
|
|
self.destconn_rows.append(self.build_dest_row(conn))
|
|
|
|
self.populate_dest_combo()
|
|
|
|
def build_dest_row(self, destconn):
|
|
driver = self.conn.get_driver()
|
|
origuri = self.conn.get_uri()
|
|
|
|
can_migrate = False
|
|
desc = destconn.get_pretty_desc_inactive()
|
|
reason = ""
|
|
desturi = destconn.get_uri()
|
|
|
|
if destconn.get_driver() != driver:
|
|
reason = _("Connection hypervisors do not match.")
|
|
elif destconn.get_state() == destconn.STATE_DISCONNECTED:
|
|
reason = _("Connection is disconnected.")
|
|
elif destconn.get_uri() == origuri:
|
|
# Same connection
|
|
pass
|
|
elif destconn.get_state() == destconn.STATE_ACTIVE:
|
|
# Assumably we can migrate to this connection
|
|
can_migrate = True
|
|
reason = desturi
|
|
|
|
return [desc, destconn, can_migrate, reason]
|
|
|
|
|
|
def validate(self):
|
|
interface = self.get_config_interface()
|
|
rate = self.get_config_rate()
|
|
port = self.get_config_port()
|
|
max_downtime = self.get_config_max_downtime()
|
|
|
|
if self.get_config_max_downtime_enabled() and max_downtime == 0:
|
|
return self.err.val_err(_("max downtime must be greater than 0."))
|
|
|
|
if self.get_config_interface_enabled() and interface is None:
|
|
return self.err.val_err(_("An interface must be specified."))
|
|
|
|
if self.get_config_rate_enabled() and rate == 0:
|
|
return self.err.val_err(_("Transfer rate must be greater than 0."))
|
|
|
|
if self.get_config_port_enabled() and port == 0:
|
|
return self.err.val_err(_("Port must be greater than 0."))
|
|
|
|
return True
|
|
|
|
def finish(self, src_ignore):
|
|
try:
|
|
if not self.validate():
|
|
return
|
|
|
|
destconn = self.get_config_destconn()
|
|
srcuri = self.vm.conn.get_uri()
|
|
srchost = self.vm.conn.get_hostname()
|
|
dsthost = destconn.get_qualified_hostname()
|
|
max_downtime = self.get_config_max_downtime()
|
|
live = not self.get_config_offline()
|
|
secure = self.get_config_secure()
|
|
uri = self.build_migrate_uri(destconn, srcuri)
|
|
rate = self.get_config_rate()
|
|
if rate:
|
|
rate = int(rate)
|
|
except Exception, e:
|
|
details = "".join(traceback.format_exc())
|
|
self.err.show_err((_("Uncaught error validating input: %s") %
|
|
str(e)),
|
|
details=details)
|
|
return
|
|
|
|
self.topwin.set_sensitive(False)
|
|
self.topwin.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
|
|
|
|
if self.vm.getjobinfo_supported:
|
|
_cancel_back = self.cancel_migration
|
|
_cancel_args = [self.vm]
|
|
else:
|
|
_cancel_back = None
|
|
_cancel_args = [None]
|
|
|
|
progWin = vmmAsyncJob(self._async_migrate,
|
|
[self.vm, destconn, uri, rate, live, secure,
|
|
max_downtime],
|
|
_("Migrating VM '%s'" % self.vm.get_name()),
|
|
(_("Migrating VM '%s' from %s to %s. "
|
|
"This may take a while.") %
|
|
(self.vm.get_name(), srchost, dsthost)),
|
|
self.topwin,
|
|
cancel_back=_cancel_back,
|
|
cancel_args=_cancel_args)
|
|
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:
|
|
error = _("Unable to migrate guest: %s") % error
|
|
self.err.show_err(error,
|
|
details=details)
|
|
else:
|
|
self.conn.tick(noStatsUpdate=True)
|
|
destconn.tick(noStatsUpdate=True)
|
|
self.close()
|
|
|
|
def _async_set_max_downtime(self, vm, max_downtime, migrate_thread):
|
|
if not migrate_thread.isAlive():
|
|
return False
|
|
try:
|
|
vm.migrate_set_max_downtime(max_downtime, 0)
|
|
return False
|
|
except libvirt.libvirtError, e:
|
|
if (isinstance(e, libvirt.libvirtError) and
|
|
e.get_error_code() == libvirt.VIR_ERR_OPERATION_INVALID):
|
|
# migration has not been started, wait 100 milliseconds
|
|
return True
|
|
|
|
logging.warning("Error setting migrate downtime: %s", e)
|
|
return False
|
|
|
|
def cancel_migration(self, asyncjob, vm):
|
|
logging.debug("Cancelling migrate job")
|
|
if not vm:
|
|
return
|
|
|
|
try:
|
|
vm.abort_job()
|
|
except Exception, e:
|
|
logging.exception("Error cancelling migrate job")
|
|
asyncjob.show_warning(_("Error cancelling migrate job: %s") % e)
|
|
return
|
|
|
|
asyncjob.job_canceled = True
|
|
return
|
|
|
|
def _async_migrate(self, asyncjob,
|
|
origvm, origdconn, migrate_uri, rate, live,
|
|
secure, max_downtime):
|
|
meter = asyncjob.get_meter()
|
|
|
|
srcconn = util.dup_conn(origvm.conn)
|
|
dstconn = util.dup_conn(origdconn)
|
|
|
|
vminst = srcconn.vmm.lookupByName(origvm.get_name())
|
|
vm = vmmDomain(srcconn, vminst, vminst.UUID())
|
|
|
|
logging.debug("Migrating vm=%s from %s to %s", vm.get_name(),
|
|
srcconn.get_uri(), dstconn.get_uri())
|
|
|
|
timer = None
|
|
if max_downtime != 0:
|
|
# 0 means that the spin box migrate-max-downtime does not
|
|
# be enabled.
|
|
current_thread = threading.currentThread()
|
|
timer = self.timeout_add(100, self._async_set_max_downtime,
|
|
vm, max_downtime, current_thread)
|
|
|
|
vm.migrate(dstconn, migrate_uri, rate, live, secure, meter=meter)
|
|
if timer:
|
|
self.idle_add(GLib.source_remove, timer)
|