nova/nova/virt/libvirt/migration.py

200 lines
7.5 KiB
Python

# Copyright (c) 2016 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Utility methods to manage guests migration
"""
from lxml import etree
from oslo_log import log as logging
from nova.i18n import _LI
from nova.i18n import _LW
LOG = logging.getLogger(__name__)
# TODO(berrange): hack to avoid a "import libvirt" in this file.
# Remove this and similar hacks in guest.py, driver.py, host.py
# etc in Ocata.
libvirt = None
def graphics_listen_addrs(migrate_data):
"""Returns listen addresses of vnc/spice from a LibvirtLiveMigrateData"""
listen_addrs = None
if (migrate_data.obj_attr_is_set('graphics_listen_addr_vnc')
or migrate_data.obj_attr_is_set('graphics_listen_addr_spice')):
listen_addrs = {'vnc': None, 'spice': None}
if migrate_data.obj_attr_is_set('graphics_listen_addr_vnc'):
listen_addrs['vnc'] = str(migrate_data.graphics_listen_addr_vnc)
if migrate_data.obj_attr_is_set('graphics_listen_addr_spice'):
listen_addrs['spice'] = str(
migrate_data.graphics_listen_addr_spice)
return listen_addrs
def serial_listen_addr(migrate_data):
"""Returns listen address serial from a LibvirtLiveMigrateData"""
listen_addr = None
if migrate_data.obj_attr_is_set('serial_listen_addr'):
listen_addr = str(migrate_data.serial_listen_addr)
return listen_addr
def get_updated_guest_xml(guest, migrate_data, get_volume_config):
xml_doc = etree.fromstring(guest.get_xml_desc(dump_migratable=True))
xml_doc = _update_graphics_xml(xml_doc, migrate_data)
xml_doc = _update_serial_xml(xml_doc, migrate_data)
xml_doc = _update_volume_xml(xml_doc, migrate_data, get_volume_config)
return etree.tostring(xml_doc)
def _update_graphics_xml(xml_doc, migrate_data):
listen_addrs = graphics_listen_addrs(migrate_data)
# change over listen addresses
for dev in xml_doc.findall('./devices/graphics'):
gr_type = dev.get('type')
listen_tag = dev.find('listen')
if gr_type in ('vnc', 'spice'):
if listen_tag is not None:
listen_tag.set('address', listen_addrs[gr_type])
if dev.get('listen') is not None:
dev.set('listen', listen_addrs[gr_type])
return xml_doc
def _update_serial_xml(xml_doc, migrate_data):
listen_addr = serial_listen_addr(migrate_data)
for dev in xml_doc.findall("./devices/serial[@type='tcp']/source"):
if dev.get('host') is not None:
dev.set('host', listen_addr)
for dev in xml_doc.findall("./devices/console[@type='tcp']/source"):
if dev.get('host') is not None:
dev.set('host', listen_addr)
return xml_doc
def _update_volume_xml(xml_doc, migrate_data, get_volume_config):
"""Update XML using device information of destination host."""
migrate_bdm_info = migrate_data.bdms
# Update volume xml
parser = etree.XMLParser(remove_blank_text=True)
disk_nodes = xml_doc.findall('./devices/disk')
bdm_info_by_serial = {x.serial: x for x in migrate_bdm_info}
for pos, disk_dev in enumerate(disk_nodes):
serial_source = disk_dev.findtext('serial')
bdm_info = bdm_info_by_serial.get(serial_source)
if (serial_source is None or
not bdm_info or not bdm_info.connection_info or
serial_source not in bdm_info_by_serial):
continue
conf = get_volume_config(
bdm_info.connection_info, bdm_info.as_disk_info())
xml_doc2 = etree.XML(conf.to_xml(), parser)
serial_dest = xml_doc2.findtext('serial')
# Compare source serial and destination serial number.
# If these serial numbers match, continue the process.
if (serial_dest and (serial_source == serial_dest)):
LOG.debug("Find same serial number: pos=%(pos)s, "
"serial=%(num)s",
{'pos': pos, 'num': serial_source})
for cnt, item_src in enumerate(disk_dev):
# If source and destination have same item, update
# the item using destination value.
for item_dst in xml_doc2.findall(item_src.tag):
disk_dev.remove(item_src)
item_dst.tail = None
disk_dev.insert(cnt, item_dst)
# If destination has additional items, thses items should be
# added here.
for item_dst in list(xml_doc2):
item_dst.tail = None
disk_dev.insert(cnt, item_dst)
return xml_doc
def find_job_type(guest, instance):
"""Determine the (likely) current migration job type
:param guest: a nova.virt.libvirt.guest.Guest
:param instance: a nova.objects.Instance
Annoyingly when job type == NONE and migration is
no longer running, we don't know whether we stopped
because of failure or completion. We can distinguish
these cases by seeing if the VM still exists & is
running on the current host
:returns: a libvirt job type constant
"""
try:
if guest.is_active():
LOG.debug("VM running on src, migration failed",
instance=instance)
return libvirt.VIR_DOMAIN_JOB_FAILED
else:
LOG.debug("VM is shutoff, migration finished",
instance=instance)
return libvirt.VIR_DOMAIN_JOB_COMPLETED
except libvirt.libvirtError as ex:
LOG.debug("Error checking domain status %(ex)s",
{"ex": ex}, instance=instance)
if ex.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
LOG.debug("VM is missing, migration finished",
instance=instance)
return libvirt.VIR_DOMAIN_JOB_COMPLETED
else:
LOG.info(_LI("Error %(ex)s, migration failed"),
{"ex": ex}, instance=instance)
return libvirt.VIR_DOMAIN_JOB_FAILED
def should_abort(instance, now,
progress_time, progress_timeout,
elapsed, completion_timeout):
"""Determine if the migration should be aborted
:param instance: a nova.objects.Instance
:param now: current time in secs since epoch
:param progress_time: when progress was last made in secs since epoch
:param progress_timeout: time in secs to allow for progress
:param elapsed: total elapsed time of migration in secs
:param completion_timeout: time in secs to allow for completion
Check the progress and completion timeouts to determine if either
of them have been hit, and should thus cause migration to be aborted
:returns: True if migration should be aborted, False otherwise
"""
if (progress_timeout != 0 and
(now - progress_time) > progress_timeout):
LOG.warning(_LW("Live migration stuck for %d sec"),
(now - progress_time), instance=instance)
return True
if (completion_timeout != 0 and
elapsed > completion_timeout):
LOG.warning(
_LW("Live migration not completed after %d sec"),
completion_timeout, instance=instance)
return True
return False