Change listen address on libvirt live-migration
Previously, in order for libvirt live migration to work, the graphics listen addresses (VNC and/or Spice) had to be set on both machines to be 0.0.0.0 or ::. This was because libvirt would try to rebind to the source address on the destination machine, but would fail due to the source machine already having that address (this would not happen for 0.0.0.0 or ::, however). Now, a modified copy of the instance's XML is passed to libvirt's migrate function, such that the listen addresses are that of the destination machine. The addresses are stashed in the pre-migration data by the destination pre-migration method. Note that this functionality will only work on versions of libvirt with the VIR_DOMAIN_XML_MIGRATABLE flag. If the flag is not detected (anything vanilla libvirt less than 1.0.0), the live migration code will fall back to the old method. Fixes: bug #1279563 Change-Id: I6c9500283027193179c11d18ad4242e165a9ad6e
This commit is contained in:
parent
422b9dc4ff
commit
ea7da5152c
|
@ -4056,9 +4056,72 @@ class LibvirtConnTestCase(test.TestCase):
|
|||
conn.check_can_live_migrate_source,
|
||||
self.context, instance_ref, dest_check_data)
|
||||
|
||||
def test_live_migration_raises_exception(self):
|
||||
# Confirms recover method is called when exceptions are raised.
|
||||
# Preparing data
|
||||
@mock.patch.object(libvirt, 'VIR_DOMAIN_XML_MIGRATABLE', 8675, create=True)
|
||||
def test_live_migration_changes_listen_addresses(self):
|
||||
self.compute = importutils.import_object(CONF.compute_manager)
|
||||
instance_dict = {'host': 'fake',
|
||||
'power_state': power_state.RUNNING,
|
||||
'vm_state': vm_states.ACTIVE}
|
||||
instance_ref = db.instance_create(self.context, self.test_instance)
|
||||
instance_ref = db.instance_update(self.context, instance_ref['uuid'],
|
||||
instance_dict)
|
||||
|
||||
xml_tmpl = ("<domain type='kvm'>"
|
||||
"<devices>"
|
||||
"<graphics type='vnc' listen='{vnc}'>"
|
||||
"<listen address='{vnc}'/>"
|
||||
"</graphics>"
|
||||
"<graphics type='spice' listen='{spice}'>"
|
||||
"<listen address='{spice}'/>"
|
||||
"</graphics>"
|
||||
"</devices>"
|
||||
"</domain>")
|
||||
|
||||
initial_xml = xml_tmpl.format(vnc='1.2.3.4',
|
||||
spice='5.6.7.8')
|
||||
|
||||
target_xml = xml_tmpl.format(vnc='10.0.0.1',
|
||||
spice='10.0.0.2')
|
||||
target_xml = etree.tostring(etree.fromstring(target_xml))
|
||||
|
||||
# Preparing mocks
|
||||
vdmock = self.mox.CreateMock(libvirt.virDomain)
|
||||
self.mox.StubOutWithMock(vdmock, "migrateToURI2")
|
||||
_bandwidth = CONF.libvirt.live_migration_bandwidth
|
||||
vdmock.XMLDesc(libvirt.VIR_DOMAIN_XML_MIGRATABLE).AndReturn(
|
||||
initial_xml)
|
||||
vdmock.migrateToURI2(CONF.libvirt.live_migration_uri % 'dest',
|
||||
None,
|
||||
target_xml,
|
||||
mox.IgnoreArg(),
|
||||
None,
|
||||
_bandwidth).AndRaise(libvirt.libvirtError("ERR"))
|
||||
|
||||
def fake_lookup(instance_name):
|
||||
if instance_name == instance_ref['name']:
|
||||
return vdmock
|
||||
|
||||
self.create_fake_libvirt_mock(lookupByName=fake_lookup)
|
||||
self.mox.StubOutWithMock(self.compute, "_rollback_live_migration")
|
||||
self.compute._rollback_live_migration(self.context, instance_ref,
|
||||
'dest', False)
|
||||
|
||||
#start test
|
||||
migrate_data = {'pre_live_migration_result':
|
||||
{'graphics_listen_addrs':
|
||||
{'vnc': '10.0.0.1', 'spice': '10.0.0.2'}}}
|
||||
self.mox.ReplayAll()
|
||||
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
self.assertRaises(libvirt.libvirtError,
|
||||
conn._live_migration,
|
||||
self.context, instance_ref, 'dest', False,
|
||||
self.compute._rollback_live_migration,
|
||||
migrate_data=migrate_data)
|
||||
|
||||
db.instance_destroy(self.context, instance_ref['uuid'])
|
||||
|
||||
@mock.patch.object(libvirt, 'VIR_DOMAIN_XML_MIGRATABLE', None, create=True)
|
||||
def test_live_migration_uses_migrateToURI_without_migratable_flag(self):
|
||||
self.compute = importutils.import_object(CONF.compute_manager)
|
||||
instance_dict = {'host': 'fake',
|
||||
'power_state': power_state.RUNNING,
|
||||
|
@ -4074,7 +4137,7 @@ class LibvirtConnTestCase(test.TestCase):
|
|||
vdmock.migrateToURI(CONF.libvirt.live_migration_uri % 'dest',
|
||||
mox.IgnoreArg(),
|
||||
None,
|
||||
_bandwidth).AndRaise(libvirt.libvirtError('ERR'))
|
||||
_bandwidth).AndRaise(libvirt.libvirtError("ERR"))
|
||||
|
||||
def fake_lookup(instance_name):
|
||||
if instance_name == instance_ref['name']:
|
||||
|
@ -4086,12 +4149,148 @@ class LibvirtConnTestCase(test.TestCase):
|
|||
'dest', False)
|
||||
|
||||
#start test
|
||||
migrate_data = {'pre_live_migration_result':
|
||||
{'graphics_listen_addrs':
|
||||
{'vnc': '0.0.0.0', 'spice': '0.0.0.0'}}}
|
||||
self.mox.ReplayAll()
|
||||
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
self.assertRaises(libvirt.libvirtError,
|
||||
conn._live_migration,
|
||||
self.context, instance_ref, 'dest', False,
|
||||
self.compute._rollback_live_migration)
|
||||
self.compute._rollback_live_migration,
|
||||
migrate_data=migrate_data)
|
||||
|
||||
db.instance_destroy(self.context, instance_ref['uuid'])
|
||||
|
||||
def test_live_migration_uses_migrateToURI_without_dest_listen_addrs(self):
|
||||
self.compute = importutils.import_object(CONF.compute_manager)
|
||||
instance_dict = {'host': 'fake',
|
||||
'power_state': power_state.RUNNING,
|
||||
'vm_state': vm_states.ACTIVE}
|
||||
instance_ref = db.instance_create(self.context, self.test_instance)
|
||||
instance_ref = db.instance_update(self.context, instance_ref['uuid'],
|
||||
instance_dict)
|
||||
|
||||
# Preparing mocks
|
||||
vdmock = self.mox.CreateMock(libvirt.virDomain)
|
||||
self.mox.StubOutWithMock(vdmock, "migrateToURI")
|
||||
_bandwidth = CONF.libvirt.live_migration_bandwidth
|
||||
vdmock.migrateToURI(CONF.libvirt.live_migration_uri % 'dest',
|
||||
mox.IgnoreArg(),
|
||||
None,
|
||||
_bandwidth).AndRaise(libvirt.libvirtError("ERR"))
|
||||
|
||||
def fake_lookup(instance_name):
|
||||
if instance_name == instance_ref['name']:
|
||||
return vdmock
|
||||
|
||||
self.create_fake_libvirt_mock(lookupByName=fake_lookup)
|
||||
self.mox.StubOutWithMock(self.compute, "_rollback_live_migration")
|
||||
self.compute._rollback_live_migration(self.context, instance_ref,
|
||||
'dest', False)
|
||||
|
||||
#start test
|
||||
migrate_data = {}
|
||||
self.mox.ReplayAll()
|
||||
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
self.assertRaises(libvirt.libvirtError,
|
||||
conn._live_migration,
|
||||
self.context, instance_ref, 'dest', False,
|
||||
self.compute._rollback_live_migration,
|
||||
migrate_data=migrate_data)
|
||||
|
||||
db.instance_destroy(self.context, instance_ref['uuid'])
|
||||
|
||||
@mock.patch.object(libvirt, 'VIR_DOMAIN_XML_MIGRATABLE', None, create=True)
|
||||
def test_live_migration_fails_without_migratable_flag_or_0_addr(self):
|
||||
self.flags(vnc_enabled=True, vncserver_listen='1.2.3.4')
|
||||
self.compute = importutils.import_object(CONF.compute_manager)
|
||||
instance_dict = {'host': 'fake',
|
||||
'power_state': power_state.RUNNING,
|
||||
'vm_state': vm_states.ACTIVE}
|
||||
instance_ref = db.instance_create(self.context, self.test_instance)
|
||||
instance_ref = db.instance_update(self.context, instance_ref['uuid'],
|
||||
instance_dict)
|
||||
|
||||
# Preparing mocks
|
||||
vdmock = self.mox.CreateMock(libvirt.virDomain)
|
||||
self.mox.StubOutWithMock(vdmock, "migrateToURI")
|
||||
|
||||
def fake_lookup(instance_name):
|
||||
if instance_name == instance_ref['name']:
|
||||
return vdmock
|
||||
|
||||
self.create_fake_libvirt_mock(lookupByName=fake_lookup)
|
||||
self.mox.StubOutWithMock(self.compute, "_rollback_live_migration")
|
||||
self.compute._rollback_live_migration(self.context, instance_ref,
|
||||
'dest', False)
|
||||
|
||||
#start test
|
||||
migrate_data = {'pre_live_migration_result':
|
||||
{'graphics_listen_addrs':
|
||||
{'vnc': '1.2.3.4', 'spice': '1.2.3.4'}}}
|
||||
self.mox.ReplayAll()
|
||||
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
self.assertRaises(exception.MigrationError,
|
||||
conn._live_migration,
|
||||
self.context, instance_ref, 'dest', False,
|
||||
self.compute._rollback_live_migration,
|
||||
migrate_data=migrate_data)
|
||||
|
||||
db.instance_destroy(self.context, instance_ref['uuid'])
|
||||
|
||||
def test_live_migration_raises_exception(self):
|
||||
# Confirms recover method is called when exceptions are raised.
|
||||
# Preparing data
|
||||
self.compute = importutils.import_object(CONF.compute_manager)
|
||||
instance_dict = {'host': 'fake',
|
||||
'power_state': power_state.RUNNING,
|
||||
'vm_state': vm_states.ACTIVE}
|
||||
instance_ref = db.instance_create(self.context, self.test_instance)
|
||||
instance_ref = db.instance_update(self.context, instance_ref['uuid'],
|
||||
instance_dict)
|
||||
|
||||
# Preparing mocks
|
||||
vdmock = self.mox.CreateMock(libvirt.virDomain)
|
||||
self.mox.StubOutWithMock(vdmock, "migrateToURI2")
|
||||
_bandwidth = CONF.libvirt.live_migration_bandwidth
|
||||
if getattr(libvirt, 'VIR_DOMAIN_XML_MIGRATABLE', None) is None:
|
||||
vdmock.migrateToURI(CONF.libvirt.live_migration_uri % 'dest',
|
||||
mox.IgnoreArg(),
|
||||
None,
|
||||
_bandwidth).AndRaise(
|
||||
libvirt.libvirtError('ERR'))
|
||||
else:
|
||||
vdmock.XMLDesc(libvirt.VIR_DOMAIN_XML_MIGRATABLE).AndReturn(
|
||||
FakeVirtDomain().XMLDesc(0))
|
||||
vdmock.migrateToURI2(CONF.libvirt.live_migration_uri % 'dest',
|
||||
None,
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
None,
|
||||
_bandwidth).AndRaise(
|
||||
libvirt.libvirtError('ERR'))
|
||||
|
||||
def fake_lookup(instance_name):
|
||||
if instance_name == instance_ref['name']:
|
||||
return vdmock
|
||||
|
||||
self.create_fake_libvirt_mock(lookupByName=fake_lookup)
|
||||
self.mox.StubOutWithMock(self.compute, "_rollback_live_migration")
|
||||
self.compute._rollback_live_migration(self.context, instance_ref,
|
||||
'dest', False)
|
||||
|
||||
#start test
|
||||
migrate_data = {'pre_live_migration_result':
|
||||
{'graphics_listen_addrs':
|
||||
{'vnc': '127.0.0.1', 'spice': '127.0.0.1'}}}
|
||||
self.mox.ReplayAll()
|
||||
conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
self.assertRaises(libvirt.libvirtError,
|
||||
conn._live_migration,
|
||||
self.context, instance_ref, 'dest', False,
|
||||
self.compute._rollback_live_migration,
|
||||
migrate_data=migrate_data)
|
||||
|
||||
instance_ref = db.instance_get(self.context, instance_ref['id'])
|
||||
self.assertTrue(instance_ref['vm_state'] == vm_states.ACTIVE)
|
||||
|
@ -4224,7 +4423,10 @@ class LibvirtConnTestCase(test.TestCase):
|
|||
|
||||
self.mox.ReplayAll()
|
||||
result = conn.pre_live_migration(c, inst_ref, vol, nw_info, None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
target_res = {'graphics_listen_addrs': {'spice': '127.0.0.1',
|
||||
'vnc': '127.0.0.1'}}
|
||||
self.assertEqual(result, target_res)
|
||||
|
||||
def test_pre_live_migration_block_with_config_drive_mocked(self):
|
||||
# Creating testdata
|
||||
|
@ -4286,7 +4488,9 @@ class LibvirtConnTestCase(test.TestCase):
|
|||
}
|
||||
ret = conn.pre_live_migration(c, inst_ref, vol, nw_info, None,
|
||||
migrate_data)
|
||||
self.assertIsNone(ret)
|
||||
target_ret = {'graphics_listen_addrs': {'spice': '127.0.0.1',
|
||||
'vnc': '127.0.0.1'}}
|
||||
self.assertEqual(ret, target_ret)
|
||||
self.assertTrue(os.path.exists('%s/%s/' % (tmpdir,
|
||||
inst_ref['name'])))
|
||||
db.instance_destroy(self.context, inst_ref['uuid'])
|
||||
|
|
|
@ -4479,6 +4479,64 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||
post_method, recover_method, block_migration,
|
||||
migrate_data)
|
||||
|
||||
def _correct_listen_addr(self, old_xml_str, listen_addrs):
|
||||
# NB(sross): can't just use LibvirtConfigGuest#parse_str
|
||||
# here b/c it doesn't capture the entire XML
|
||||
# description
|
||||
xml_doc = etree.fromstring(old_xml_str)
|
||||
|
||||
# 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 etree.tostring(xml_doc)
|
||||
|
||||
def _check_graphics_addresses_can_live_migrate(self, listen_addrs):
|
||||
LOCAL_ADDRS = ('0.0.0.0', '127.0.0.1', '::', '::1')
|
||||
|
||||
local_vnc = CONF.vncserver_listen in LOCAL_ADDRS
|
||||
local_spice = CONF.spice.server_listen in LOCAL_ADDRS
|
||||
|
||||
if ((CONF.vnc_enabled and not local_vnc) or
|
||||
(CONF.spice.enabled and not local_spice)):
|
||||
|
||||
raise exception.MigrationError(
|
||||
_('Your libvirt version does not support the'
|
||||
' VIR_DOMAIN_XML_MIGRATABLE flag or your'
|
||||
' destination node does not support'
|
||||
' retrieving listen addresses. In order'
|
||||
' for live migration to work properly, you'
|
||||
' must configure the graphics (VNC and/or'
|
||||
' SPICE) listen addresses to be either'
|
||||
' the catch-all address (0.0.0.0 or ::) or'
|
||||
' the local address (127.0.0.1 or ::1).'))
|
||||
|
||||
if listen_addrs is not None:
|
||||
dest_local_vnc = listen_addrs['vnc'] in LOCAL_ADDRS
|
||||
dest_local_spice = listen_addrs['spice'] in LOCAL_ADDRS
|
||||
|
||||
if ((CONF.vnc_enabled and not dest_local_vnc) or
|
||||
(CONF.spice.enabled and not dest_local_spice)):
|
||||
|
||||
LOG.warn(_('Your libvirt version does not support the'
|
||||
' VIR_DOMAIN_XML_MIGRATABLE flag, and the '
|
||||
' graphics (VNC and/or SPICE) listen'
|
||||
' addresses on the destination node do not'
|
||||
' match the addresses on the source node.'
|
||||
' Since the source node has listen'
|
||||
' addresses set to either the catch-all'
|
||||
' address (0.0.0.0 or ::) or the local'
|
||||
' address (127.0.0.1 or ::1), the live'
|
||||
' migration will succeed, but the VM will'
|
||||
' continue to listen on the current'
|
||||
' addresses.'))
|
||||
|
||||
def _live_migration(self, context, instance, dest, post_method,
|
||||
recover_method, block_migration=False,
|
||||
migrate_data=None):
|
||||
|
@ -4509,10 +4567,31 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||
logical_sum = reduce(lambda x, y: x | y, flagvals)
|
||||
|
||||
dom = self._lookup_by_name(instance["name"])
|
||||
dom.migrateToURI(CONF.libvirt.live_migration_uri % dest,
|
||||
logical_sum,
|
||||
None,
|
||||
CONF.libvirt.live_migration_bandwidth)
|
||||
|
||||
pre_live_migrate_data = (migrate_data or {}).get(
|
||||
'pre_live_migration_result', {})
|
||||
listen_addrs = pre_live_migrate_data.get('graphics_listen_addrs')
|
||||
|
||||
migratable_flag = getattr(libvirt, 'VIR_DOMAIN_XML_MIGRATABLE',
|
||||
None)
|
||||
|
||||
if migratable_flag is None or listen_addrs is None:
|
||||
self._check_graphics_addresses_can_live_migrate(listen_addrs)
|
||||
dom.migrateToURI(CONF.libvirt.live_migration_uri % dest,
|
||||
logical_sum,
|
||||
None,
|
||||
CONF.libvirt.live_migration_bandwidth)
|
||||
else:
|
||||
old_xml_str = dom.XMLDesc(migratable_flag)
|
||||
new_xml_str = self._correct_listen_addr(old_xml_str,
|
||||
listen_addrs)
|
||||
|
||||
dom.migrateToURI2(CONF.libvirt.live_migration_uri % dest,
|
||||
None,
|
||||
new_xml_str,
|
||||
logical_sum,
|
||||
None,
|
||||
CONF.libvirt.live_migration_bandwidth)
|
||||
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
|
@ -4636,6 +4715,12 @@ class LibvirtDriver(driver.ComputeDriver):
|
|||
instance=instance)
|
||||
greenthread.sleep(1)
|
||||
|
||||
res_data = {'graphics_listen_addrs': {}}
|
||||
res_data['graphics_listen_addrs']['vnc'] = CONF.vncserver_listen
|
||||
res_data['graphics_listen_addrs']['spice'] = CONF.spice.server_listen
|
||||
|
||||
return res_data
|
||||
|
||||
def _create_images_and_backing(self, context, instance, instance_dir,
|
||||
disk_info_json):
|
||||
""":param context: security context
|
||||
|
|
Loading…
Reference in New Issue