Merge "libvirt: introduces module to handle domain xml migration"

This commit is contained in:
Jenkins 2016-05-12 09:47:19 +00:00 committed by Gerrit Code Review
commit e701653bbc
4 changed files with 359 additions and 136 deletions

View File

@ -96,6 +96,7 @@ from nova.virt.libvirt import firewall
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import host
from nova.virt.libvirt import imagebackend
from nova.virt.libvirt import migration as libvirt_migrate
from nova.virt.libvirt.storage import dmcrypt
from nova.virt.libvirt.storage import lvm
from nova.virt.libvirt.storage import rbd_utils
@ -7042,7 +7043,8 @@ class LibvirtConnTestCase(test.NoDBTestCase):
mget_info,\
mock.patch.object(drvr._host, 'get_domain') as mget_domain,\
mock.patch.object(fakelibvirt.virDomain, 'migrateToURI2'),\
mock.patch.object(drvr, '_update_xml') as mupdate:
mock.patch.object(
libvirt_migrate, 'get_updated_guest_xml') as mupdate:
mget_info.side_effect = exception.InstanceNotFound(
instance_id='foo')
@ -7051,8 +7053,10 @@ class LibvirtConnTestCase(test.NoDBTestCase):
self.assertFalse(drvr._live_migration_operation(
self.context, instance_ref, 'dest', False,
migrate_data, test_mock, []))
mupdate.assert_called_once_with(target_xml, migrate_data.bdms,
{}, '')
# guest object is created under this method we do not have
# other choice than using mock.ANY
mupdate.assert_called_once_with(
mock.ANY, migrate_data, mock.ANY)
def test_live_migration_with_valid_target_connect_addr(self):
self.compute = importutils.import_object(CONF.compute_manager)
@ -7090,7 +7094,8 @@ class LibvirtConnTestCase(test.NoDBTestCase):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
test_mock = mock.MagicMock()
with mock.patch.object(drvr, '_update_xml') as mupdate:
with mock.patch.object(libvirt_migrate,
'get_updated_guest_xml') as mupdate:
test_mock.XMLDesc.return_value = target_xml
drvr._live_migration_operation(self.context, instance_ref,
@ -7137,13 +7142,20 @@ class LibvirtConnTestCase(test.NoDBTestCase):
conf.source_type = "block"
conf.source_path = bdmi.connection_info['data'].get('device_path')
with mock.patch.object(drvr, '_get_volume_config',
return_value=conf):
guest = libvirt_guest.Guest(mock.MagicMock())
with test.nested(
mock.patch.object(drvr, '_get_volume_config',
return_value=conf),
mock.patch.object(guest, 'get_xml_desc',
return_value=initial_xml)):
config = libvirt_migrate.get_updated_guest_xml(guest,
objects.LibvirtLiveMigrateData(bdms=[bdmi]),
drvr._get_volume_config)
parser = etree.XMLParser(remove_blank_text=True)
xml_doc = etree.fromstring(initial_xml, parser)
config = drvr._update_volume_xml(xml_doc, [bdmi])
xml_doc = etree.fromstring(target_xml, parser)
self.assertEqual(etree.tostring(xml_doc), etree.tostring(config))
config = etree.fromstring(config, parser)
target_xml = etree.fromstring(target_xml, parser)
self.assertEqual(etree.tostring(target_xml),
etree.tostring(config))
def test_live_migration_uri(self):
hypervisor_uri_map = (
@ -7232,11 +7244,16 @@ class LibvirtConnTestCase(test.NoDBTestCase):
conf.source_type = "block"
conf.source_path = bdmi.connection_info['data'].get('device_path')
with mock.patch.object(drvr, '_get_volume_config',
return_value=conf):
xml_doc = etree.fromstring(initial_xml)
config = drvr._update_volume_xml(xml_doc, [bdmi])
self.assertEqual(target_xml, etree.tostring(config))
guest = libvirt_guest.Guest(mock.MagicMock())
with test.nested(
mock.patch.object(drvr, '_get_volume_config',
return_value=conf),
mock.patch.object(guest, 'get_xml_desc',
return_value=initial_xml)):
config = libvirt_migrate.get_updated_guest_xml(guest,
objects.LibvirtLiveMigrateData(bdms=[bdmi]),
drvr._get_volume_config)
self.assertEqual(target_xml, config)
def test_update_volume_xml_no_connection_info(self):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
@ -7257,11 +7274,17 @@ class LibvirtConnTestCase(test.NoDBTestCase):
format='qcow')
bdmi.connection_info = {}
conf = vconfig.LibvirtConfigGuestDisk()
with mock.patch.object(drvr, '_get_volume_config',
return_value=conf):
xml_doc = etree.fromstring(initial_xml)
config = drvr._update_volume_xml(xml_doc, [bdmi])
self.assertEqual(target_xml, etree.tostring(config))
guest = libvirt_guest.Guest(mock.MagicMock())
with test.nested(
mock.patch.object(drvr, '_get_volume_config',
return_value=conf),
mock.patch.object(guest, 'get_xml_desc',
return_value=initial_xml)):
config = libvirt_migrate.get_updated_guest_xml(
guest,
objects.LibvirtLiveMigrateData(bdms=[bdmi]),
drvr._get_volume_config)
self.assertEqual(target_xml, config)
@mock.patch.object(fakelibvirt.virDomain, "migrateToURI2")
@mock.patch.object(fakelibvirt.virDomain, "XMLDesc")
@ -7378,9 +7401,10 @@ class LibvirtConnTestCase(test.NoDBTestCase):
@mock.patch.object(host.Host, 'has_min_version', return_value=True)
@mock.patch.object(fakelibvirt.virDomain, "migrateToURI3")
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._update_xml',
@mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml',
return_value='')
@mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc', return_value='')
@mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc',
return_value='<xml></xml>')
def test_live_migration_uses_migrateToURI3(
self, mock_old_xml, mock_new_xml, mock_migrateToURI3,
mock_min_version):

View File

@ -0,0 +1,181 @@
# 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.
from lxml import etree
import mock
import six
from nova import objects
from nova import test
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import migration
class UtilityMigrationTestCase(test.NoDBTestCase):
def test_graphics_listen_addrs(self):
data = objects.LibvirtLiveMigrateData(
graphics_listen_addr_vnc='127.0.0.1',
graphics_listen_addr_spice='127.0.0.2')
addrs = migration.graphics_listen_addrs(data)
self.assertEqual({
'vnc': '127.0.0.1',
'spice': '127.0.0.2'}, addrs)
def test_graphics_listen_addrs_empty(self):
data = objects.LibvirtLiveMigrateData()
addrs = migration.graphics_listen_addrs(data)
self.assertIsNone(None, addrs)
def test_graphics_listen_addrs_spice(self):
data = objects.LibvirtLiveMigrateData(
graphics_listen_addr_spice='127.0.0.2')
addrs = migration.graphics_listen_addrs(data)
self.assertEqual({
'vnc': None,
'spice': '127.0.0.2'}, addrs)
def test_graphics_listen_addrs_vnc(self):
data = objects.LibvirtLiveMigrateData(
graphics_listen_addr_vnc='127.0.0.1')
addrs = migration.graphics_listen_addrs(data)
self.assertEqual({
'vnc': '127.0.0.1',
'spice': None}, addrs)
def test_serial_listen_addr(self):
data = objects.LibvirtLiveMigrateData(
serial_listen_addr='127.0.0.1')
addr = migration.serial_listen_addr(data)
self.assertEqual('127.0.0.1', addr)
def test_serial_listen_addr_emtpy(self):
data = objects.LibvirtLiveMigrateData()
addr = migration.serial_listen_addr(data)
self.assertIsNone(addr)
@mock.patch('lxml.etree.tostring')
@mock.patch.object(migration, '_update_graphics_xml')
@mock.patch.object(migration, '_update_serial_xml')
@mock.patch.object(migration, '_update_volume_xml')
def test_get_updated_guest_xml(
self, mock_volume, mock_serial, mock_graphics,
mock_tostring):
data = objects.LibvirtLiveMigrateData()
mock_guest = mock.Mock(spec=libvirt_guest.Guest)
get_volume_config = mock.MagicMock()
mock_guest.get_xml_desc.return_value = '<domain></domain>'
migration.get_updated_guest_xml(mock_guest, data, get_volume_config)
mock_graphics.assert_called_once_with(mock.ANY, data)
mock_serial.assert_called_once_with(mock.ANY, data)
mock_volume.assert_called_once_with(mock.ANY, data, get_volume_config)
self.assertEqual(1, mock_tostring.called)
def test_update_serial_xml_serial(self):
data = objects.LibvirtLiveMigrateData(
serial_listen_addr='127.0.0.100')
xml = """<domain>
<devices>
<serial type="tcp">
<source host="127.0.0.1"/>
</serial>
</devices>
</domain>"""
doc = etree.fromstring(xml)
res = etree.tostring(migration._update_serial_xml(doc, data))
self.assertIn('127.0.0.100', six.text_type(res))
def test_update_serial_xml_console(self):
data = objects.LibvirtLiveMigrateData(
serial_listen_addr='127.0.0.100')
xml = """<domain>
<devices>
<console type="tcp">
<source host="127.0.0.1"/>
</console>
</devices>
</domain>"""
doc = etree.fromstring(xml)
res = etree.tostring(migration._update_serial_xml(doc, data))
self.assertIn('127.0.0.100', six.text_type(res))
def test_update_graphics(self):
data = objects.LibvirtLiveMigrateData(
graphics_listen_addr_vnc='127.0.0.100',
graphics_listen_addr_spice='127.0.0.200')
xml = """<domain>
<devices>
<graphics type="vnc">
<listen type="address" address="127.0.0.1"/>
</graphics>
<graphics type="spice">
<listen type="address" address="127.0.0.2"/>
</graphics>
</devices>
</domain>"""
doc = etree.fromstring(xml)
res = etree.tostring(migration._update_graphics_xml(doc, data))
self.assertIn('127.0.0.100', six.text_type(res))
self.assertIn('127.0.0.200', six.text_type(res))
def test_update_volume_xml(self):
connection_info = {
'driver_volume_type': 'iscsi',
'serial': '58a84f6d-3f0c-4e19-a0af-eb657b790657',
'data': {
'access_mode': 'rw',
'target_discovered': False,
'target_iqn': 'ip-1.2.3.4:3260-iqn.cde.67890.opst-lun-Z',
'volume_id': '58a84f6d-3f0c-4e19-a0af-eb657b790657',
'device_path':
'/dev/disk/by-path/ip-1.2.3.4:3260-iqn.cde.67890.opst-lun-Z'}}
bdm = objects.LibvirtLiveMigrateBDMInfo(
serial='58a84f6d-3f0c-4e19-a0af-eb657b790657',
bus='virtio', type='disk', dev='vdb',
connection_info=connection_info)
data = objects.LibvirtLiveMigrateData(
target_connect_addr='127.0.0.1',
bdms=[bdm],
block_migration=False)
xml = """<domain>
<devices>
<disk type='block' device='disk'>
<driver name='qemu' type='raw' cache='none'/>
<source dev='/dev/disk/by-path/ip-1.2.3.4:3260-iqn.abc.12345.opst-lun-X'/>
<target bus='virtio' dev='vdb'/>
<serial>58a84f6d-3f0c-4e19-a0af-eb657b790657</serial>
<address type='pci' domain='0x0' bus='0x0' slot='0x04' function='0x0'/>
</disk>
</devices>
</domain>"""
conf = vconfig.LibvirtConfigGuestDisk()
conf.source_device = bdm.type
conf.driver_name = "qemu"
conf.driver_format = "raw"
conf.driver_cache = "none"
conf.target_dev = bdm.dev
conf.target_bus = bdm.bus
conf.serial = bdm.connection_info.get('serial')
conf.source_type = "block"
conf.source_path = bdm.connection_info['data'].get('device_path')
get_volume_config = mock.MagicMock(return_value=conf)
doc = etree.fromstring(xml)
res = etree.tostring(migration._update_volume_xml(
doc, data, get_volume_config))
self.assertIn('ip-1.2.3.4:3260-iqn.cde.67890.opst-lun-Z',
six.text_type(res))

View File

@ -101,6 +101,7 @@ from nova.virt.libvirt import host
from nova.virt.libvirt import imagebackend
from nova.virt.libvirt import imagecache
from nova.virt.libvirt import instancejobtracker
from nova.virt.libvirt import migration as libvirt_migrate
from nova.virt.libvirt.storage import dmcrypt
from nova.virt.libvirt.storage import lvm
from nova.virt.libvirt.storage import rbd_utils
@ -5304,22 +5305,8 @@ class LibvirtDriver(driver.ComputeDriver):
md_obj.from_legacy_dict(dest_check_data)
dest_check_data = md_obj
listen_addrs = None
# We are building listen_addrs of vnc/spice from
# LibvirtLiveMigrateData; in some certains (e.g. an old-code
# destination host) those fields may have not been set and we
# want to avoid any unfortunates exceptions raised.
# TODO(sahid): The method
# _check_graphics_addresses_can_live_migrate_should to take an
# object LibvirtLiveMigrate itself.
if (dest_check_data.obj_attr_is_set('graphics_listen_addr_vnc')
or dest_check_data.obj_attr_is_set('graphics_listen_addr_spice')):
listen_addrs = {'vnc': None, 'spice': None}
if dest_check_data.obj_attr_is_set('graphics_listen_addr_vnc'):
listen_addrs['vnc'] = dest_check_data.graphics_listen_addr_vnc
if dest_check_data.obj_attr_is_set('graphics_listen_addr_spice'):
listen_addrs['spice'] = dest_check_data.graphics_listen_addr_spice
listen_addrs = libvirt_migrate.graphics_listen_addrs(
dest_check_data)
migratable_flag = self._host.is_migratable_xml_flag()
if not migratable_flag or not listen_addrs:
# In this context want to ensure we do not have to migrate
@ -5644,90 +5631,6 @@ class LibvirtDriver(driver.ComputeDriver):
e, instance=instance)
raise
def _update_xml(self, xml_str, migrate_bdm_info, listen_addrs,
serial_listen_addr):
xml_doc = etree.fromstring(xml_str)
if migrate_bdm_info:
xml_doc = self._update_volume_xml(xml_doc, migrate_bdm_info)
if listen_addrs:
xml_doc = self._update_graphics_xml(xml_doc, listen_addrs)
else:
self._check_graphics_addresses_can_live_migrate(listen_addrs)
if serial_listen_addr:
xml_doc = self._update_serial_xml(xml_doc, serial_listen_addr)
else:
self._verify_serial_console_is_disabled()
return etree.tostring(xml_doc)
def _update_graphics_xml(self, xml_doc, listen_addrs):
# 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_volume_xml(self, xml_doc, migrate_bdm_info):
"""Update XML using device information of destination host."""
# 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 = self._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 _update_serial_xml(self, xml_doc, listen_addr):
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 _check_graphics_addresses_can_live_migrate(self, listen_addrs):
LOCAL_ADDRS = ('0.0.0.0', '127.0.0.1', '::', '::1')
@ -5808,15 +5711,8 @@ class LibvirtDriver(driver.ComputeDriver):
else:
migration_flags = self._live_migration_flags
listen_addrs = {}
if 'graphics_listen_addr_vnc' in migrate_data:
listen_addrs['vnc'] = str(
migrate_data.graphics_listen_addr_vnc)
if 'graphics_listen_addr_spice' in migrate_data:
listen_addrs['spice'] = str(
migrate_data.graphics_listen_addr_spice)
serial_listen_addr = (migrate_data.serial_listen_addr if
'serial_listen_addr' in migrate_data else None)
listen_addrs = libvirt_migrate.graphics_listen_addrs(
migrate_data)
if ('target_connect_addr' in migrate_data and
migrate_data.target_connect_addr is not None):
dest = migrate_data.target_connect_addr
@ -5828,11 +5724,11 @@ class LibvirtDriver(driver.ComputeDriver):
None,
CONF.libvirt.live_migration_bandwidth)
else:
old_xml_str = guest.get_xml_desc(dump_migratable=True)
new_xml_str = self._update_xml(old_xml_str,
migrate_data.bdms,
listen_addrs,
serial_listen_addr)
new_xml_str = libvirt_migrate.get_updated_guest_xml(
# TODO(sahid): It's not a really well idea to pass
# the method _get_volume_config and we should to find
# a way to avoid this in future.
guest, migrate_data, self._get_volume_config)
if self._host.has_min_version(
MIN_LIBVIRT_BLOCK_LM_WITH_VOLUMES_VERSION):
params = {

View File

@ -0,0 +1,122 @@
# 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
LOG = logging.getLogger(__name__)
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