From 984cc474efc6ecbeb1240f49479b6439bc9a9416 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Fri, 12 Jun 2015 14:21:07 +0200 Subject: [PATCH] libvirt: enable live migration with serial console When migrating an instance that uses serial console, the domain XML must be changed to reflect the serial console configuration of the target host. Also prevent live migration when serial console is enabled and the libvirt version is not able to process modified domain XML. Change-Id: I654e444584124334e34d4673db6464cc2f2b822e Partial-Bug: #1455252 --- nova/tests/unit/virt/libvirt/test_driver.py | 62 ++++++++++++++++++++- nova/virt/libvirt/driver.py | 43 +++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 4769a603f2..7e02a9b537 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -6102,7 +6102,7 @@ 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, volume, None) + mupdate.assert_called_once_with(target_xml, volume, None, None) def test_update_volume_xml(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) @@ -6237,6 +6237,63 @@ class LibvirtConnTestCase(test.NoDBTestCase): volume_xml['volume']) self.assertEqual(target_xml, etree.tostring(config)) + @mock.patch.object(fakelibvirt.virDomain, "migrateToURI2") + @mock.patch.object(fakelibvirt.virDomain, "XMLDesc") + def test_live_migration_update_serial_console_xml(self, mock_xml, + mock_migrate): + self.compute = importutils.import_object(CONF.compute_manager) + instance_ref = self.test_instance + + xml_tmpl = ("" + "" + "" + "" + "" + "" + "") + + initial_xml = xml_tmpl.format(addr='9.0.0.1') + + target_xml = xml_tmpl.format(addr='9.0.0.12') + target_xml = etree.tostring(etree.fromstring(target_xml)) + + # Preparing mocks + mock_xml.return_value = initial_xml + mock_migrate.side_effect = fakelibvirt.libvirtError("ERR") + + # start test + bandwidth = CONF.libvirt.live_migration_bandwidth + migrate_data = {'pre_live_migration_result': + {'graphics_listen_addrs': + {'vnc': '10.0.0.1', 'spice': '10.0.0.2'}, + 'serial_listen_addr': '9.0.0.12'}} + dom = fakelibvirt.virDomain + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + self.assertRaises(fakelibvirt.libvirtError, + drvr._live_migration_operation, + self.context, instance_ref, 'dest', + False, migrate_data, dom) + mock_xml.assert_called_once_with( + flags=fakelibvirt.VIR_DOMAIN_XML_MIGRATABLE) + mock_migrate.assert_called_once_with( + CONF.libvirt.live_migration_uri % 'dest', + None, target_xml, mock.ANY, None, bandwidth) + + @mock.patch.object(fakelibvirt, 'VIR_DOMAIN_XML_MIGRATABLE', None, + create=True) + def test_live_migration_fails_with_serial_console_without_migratable(self): + self.compute = importutils.import_object(CONF.compute_manager) + instance_ref = self.test_instance + + CONF.set_override("enabled", True, "serial_console") + dom = fakelibvirt.virDomain + + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + self.assertRaises(exception.MigrationError, + drvr._live_migration_operation, + self.context, instance_ref, 'dest', + False, None, dom) + @mock.patch.object(fakelibvirt, 'VIR_DOMAIN_XML_MIGRATABLE', None, create=True) def test_live_migration_uses_migrateToURI_without_migratable_flag(self): @@ -7048,6 +7105,7 @@ class LibvirtConnTestCase(test.NoDBTestCase): target_ret = { 'graphics_listen_addrs': {'spice': '127.0.0.1', 'vnc': '127.0.0.1'}, + 'serial_listen_addr': '127.0.0.1', 'volume': { '12345': {'connection_info': {u'data': {'device_path': u'/dev/disk/by-path/ip-1.2.3.4:3260-iqn.abc.12345.opst-lun-X'}, @@ -7110,6 +7168,7 @@ class LibvirtConnTestCase(test.NoDBTestCase): ) self.assertEqual({'graphics_listen_addrs': {'spice': '127.0.0.1', 'vnc': '127.0.0.1'}, + 'serial_listen_addr': '127.0.0.1', 'volume': {}}, res_data) def test_pre_live_migration_vol_backed_works_correctly_mocked(self): @@ -7162,6 +7221,7 @@ class LibvirtConnTestCase(test.NoDBTestCase): target_ret = { 'graphics_listen_addrs': {'spice': '127.0.0.1', 'vnc': '127.0.0.1'}, + 'serial_listen_addr': '127.0.0.1', 'volume': { '12345': {'connection_info': {u'data': {'device_path': u'/dev/disk/by-path/ip-1.2.3.4:3260-iqn.abc.12345.opst-lun-X'}, diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 7f7e6a46c7..718b18f5e9 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -5496,7 +5496,7 @@ class LibvirtDriver(driver.ComputeDriver): post_method, recover_method, block_migration, migrate_data) - def _update_xml(self, xml_str, volume, listen_addrs): + def _update_xml(self, xml_str, volume, listen_addrs, serial_listen_addr): xml_doc = etree.fromstring(xml_str) if volume: @@ -5505,6 +5505,10 @@ class LibvirtDriver(driver.ComputeDriver): 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) @@ -5565,6 +5569,17 @@ class LibvirtDriver(driver.ComputeDriver): 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') @@ -5605,6 +5620,18 @@ class LibvirtDriver(driver.ComputeDriver): ' continue to listen on the current' ' addresses.')) + def _verify_serial_console_is_disabled(self): + if CONF.serial_console.enabled: + + msg = _('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 either disable serial console or' + ' upgrade your libvirt version.') + raise exception.MigrationError(reason=msg) + def _live_migration_operation(self, context, instance, dest, block_migration, migrate_data, dom): """Invoke the live migration operation @@ -5636,13 +5663,18 @@ class LibvirtDriver(driver.ComputeDriver): 'pre_live_migration_result', {}) listen_addrs = pre_live_migrate_data.get('graphics_listen_addrs') volume = pre_live_migrate_data.get('volume') + serial_listen_addr = pre_live_migrate_data.get( + 'serial_listen_addr') migratable_flag = getattr(libvirt, 'VIR_DOMAIN_XML_MIGRATABLE', None) if (migratable_flag is None or (listen_addrs is None and not volume)): + # TODO(alexs-h): These checks could be moved to the + # check_can_live_migrate_destination/source phase self._check_graphics_addresses_can_live_migrate(listen_addrs) + self._verify_serial_console_is_disabled() dom.migrateToURI(CONF.libvirt.live_migration_uri % dest, logical_sum, None, @@ -5651,7 +5683,8 @@ class LibvirtDriver(driver.ComputeDriver): old_xml_str = guest.get_xml_desc(dump_migratable=True) new_xml_str = self._update_xml(old_xml_str, volume, - listen_addrs) + listen_addrs, + serial_listen_addr) try: dom.migrateToURI2(CONF.libvirt.live_migration_uri % dest, None, @@ -5679,6 +5712,7 @@ class LibvirtDriver(driver.ComputeDriver): instance=instance) self._check_graphics_addresses_can_live_migrate( listen_addrs) + self._verify_serial_console_is_disabled() dom.migrateToURI( CONF.libvirt.live_migration_uri % dest, logical_sum, @@ -6242,9 +6276,12 @@ class LibvirtDriver(driver.ComputeDriver): greenthread.sleep(1) # Store vncserver_listen and latest disk device info - res_data = {'graphics_listen_addrs': {}, 'volume': {}} + res_data = {'graphics_listen_addrs': {}, 'volume': {}, + 'serial_listen_addr': {}} res_data['graphics_listen_addrs']['vnc'] = CONF.vnc.vncserver_listen res_data['graphics_listen_addrs']['spice'] = CONF.spice.server_listen + res_data['serial_listen_addr'] = \ + CONF.serial_console.proxyclient_address for vol in block_device_mapping: connection_info = vol['connection_info'] if connection_info.get('serial'):