From 898bb13304b68f33a130281d5acf425a1b23d171 Mon Sep 17 00:00:00 2001 From: Markus Zoeller Date: Tue, 22 Nov 2016 17:08:14 +0100 Subject: [PATCH] libvirt: Acquire TCP ports for console during live migration During a live migration process we reserve serial ports on destination host and update domain XML during migration to use the new reserved ports from destination. If the migration succeeds we release ports on the source host. If the migration fails we release reserved ports on the destination host. Co-Authored-By: Sahid Ferdjaoui Change-Id: Ie2524191d22cca2287eb7dbaa22b74d43e43c896 Closes-Bug: #1455252 --- nova/tests/unit/virt/libvirt/test_driver.py | 42 ++++++++++++------ nova/virt/libvirt/driver.py | 48 +++++++++++++++++++-- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index a9c69d3ca347..856340ae109e 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -3615,15 +3615,21 @@ class LibvirtConnTestCase(test.NoDBTestCase): self.assertEqual(cfg.devices[5].type, "spice") self.assertEqual(cfg.devices[6].type, "qxl") + @mock.patch.object(host.Host, 'get_guest') + @mock.patch.object(libvirt_driver.LibvirtDriver, + '_get_serial_ports_from_guest') @mock.patch('nova.console.serial.acquire_port') @mock.patch('nova.virt.hardware.get_number_of_serial_ports', return_value=1) @mock.patch.object(libvirt_driver.libvirt_utils, 'get_arch',) def test_create_serial_console_devices_based_on_arch(self, mock_get_arch, - mock_get_port_number, - mock_acquire_port): + mock_get_port_number, + mock_acquire_port, + mock_ports, + mock_guest): self.flags(enabled=True, group='serial_console') drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + instance = objects.Instance(**self.test_instance) expected = {arch.X86_64: vconfig.LibvirtConfigGuestSerial, arch.S390: vconfig.LibvirtConfigGuestConsole, @@ -3635,19 +3641,23 @@ class LibvirtConnTestCase(test.NoDBTestCase): mock_get_arch.return_value = guest_arch guest = vconfig.LibvirtConfigGuest() - drvr._create_consoles(virt_type="kvm", guest=guest, log_path="", - flavor={}, image_meta={}, caps=caps) + drvr._create_consoles(virt_type="kvm", guest=guest, + instance=instance, flavor={}, + image_meta={}, caps=caps) self.assertEqual(2, len(guest.devices)) console_device = guest.devices[0] self.assertIsInstance(console_device, device_type) self.assertEqual("tcp", console_device.type) + @mock.patch.object(host.Host, 'get_guest') + @mock.patch.object(libvirt_driver.LibvirtDriver, + '_get_serial_ports_from_guest') @mock.patch('nova.virt.hardware.get_number_of_serial_ports', return_value=4) @mock.patch.object(libvirt_driver.libvirt_utils, 'get_arch', side_effect=[arch.X86_64, arch.S390, arch.S390X]) def test_create_serial_console_devices_with_limit_exceeded_based_on_arch( - self, mock_get_arch, mock_get_port_number): + self, mock_get_arch, mock_get_port_number, mock_ports, mock_guest): self.flags(enabled=True, group='serial_console') self.flags(virt_type="qemu", group='libvirt') flavor = 'fake_flavor' @@ -3655,20 +3665,20 @@ class LibvirtConnTestCase(test.NoDBTestCase): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) caps = drvr._host.get_capabilities() guest = vconfig.LibvirtConfigGuest() - log_path = "" + instance = objects.Instance(**self.test_instance) self.assertRaises(exception.SerialPortNumberLimitExceeded, drvr._create_consoles, - "kvm", guest, log_path, flavor, image_meta, caps) + "kvm", guest, instance, flavor, image_meta, caps) mock_get_arch.assert_called_with(image_meta) mock_get_port_number.assert_called_with(flavor, image_meta) - drvr._create_consoles("kvm", guest, log_path, flavor, image_meta, caps) + drvr._create_consoles("kvm", guest, instance, flavor, image_meta, caps) mock_get_arch.assert_called_with(image_meta) mock_get_port_number.assert_called_with(flavor, image_meta) - drvr._create_consoles("kvm", guest, log_path, flavor, image_meta, caps) + drvr._create_consoles("kvm", guest, instance, flavor, image_meta, caps) mock_get_arch.assert_called_with(image_meta) mock_get_port_number.assert_called_with(flavor, image_meta) @@ -7903,24 +7913,27 @@ class LibvirtConnTestCase(test.NoDBTestCase): drvr._get_volume_config) self.assertEqual(target_xml, config) + @mock.patch.object(libvirt_driver.LibvirtDriver, + '_get_serial_ports_from_guest') @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): + mock_migrate, mock_get): 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') + initial_xml = xml_tmpl.format(addr='9.0.0.1', port='10100') - target_xml = xml_tmpl.format(addr='9.0.0.12') + target_xml = xml_tmpl.format(addr='9.0.0.12', port='10200') target_xml = etree.tostring(etree.fromstring(target_xml)) # Preparing mocks @@ -7935,7 +7948,8 @@ class LibvirtConnTestCase(test.NoDBTestCase): serial_listen_addr='9.0.0.12', target_connect_addr=None, bdms=[], - block_migration=False) + block_migration=False, + serial_listen_ports=[10200]) dom = fakelibvirt.virDomain guest = libvirt_guest.Guest(dom) drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index a51f23f9f5ce..d6b38030dd99 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -4334,13 +4334,27 @@ class LibvirtDriver(driver.ComputeDriver): else: guest.os_boot_dev = blockinfo.get_boot_order(disk_info) - def _create_consoles(self, virt_type, guest, log_path, flavor, image_meta, + def _create_consoles(self, virt_type, guest, instance, flavor, image_meta, caps): + log_path = self._get_console_log_path(instance) if virt_type in ("qemu", "kvm"): # Create the serial console char devices guest_arch = libvirt_utils.get_arch(image_meta) if CONF.serial_console.enabled: + try: + # TODO(sahid): the guest param of this method should + # be renamed as guest_cfg then guest_obj to guest. + guest_obj = self._host.get_guest(instance) + if list(self._get_serial_ports_from_guest(guest_obj)): + # Serial port are already configured for instance that + # means we are in a context of migration. + return + except exception.InstanceNotFound: + LOG.debug( + "Instance does not exist yet on libvirt, we can " + "safely pass on looking for already defined serial " + "ports in its domain XML", instance=instance) num_ports = hardware.get_number_of_serial_ports( flavor, image_meta) @@ -4540,8 +4554,7 @@ class LibvirtDriver(driver.ComputeDriver): flavor, virt_type, self._host) guest.add_device(config) - log_path = self._get_console_log_path(instance) - self._create_consoles(virt_type, guest, log_path, flavor, + self._create_consoles(virt_type, guest, instance, flavor, image_meta, caps) pointer = self._get_guest_pointer_model(guest.os_type, image_meta) @@ -5927,12 +5940,25 @@ class LibvirtDriver(driver.ComputeDriver): libvirt.VIR_MIGRATE_TUNNELLED != 0): params.pop('migrate_disks') + # TODO(sahid): This should be in + # post_live_migration_at_source but no way to retrieve + # ports acquired on the host for the guest at this + # step. Since the domain is going to be removed from + # libvird on source host after migration, we backup the + # serial ports to release them if all went well. + serial_ports = [] + if CONF.serial_console.enabled: + serial_ports = list(self._get_serial_ports_from_guest(guest)) + guest.migrate(self._live_migration_uri(dest), migrate_uri=migrate_uri, flags=migration_flags, params=params, domain_xml=new_xml_str, bandwidth=CONF.libvirt.live_migration_bandwidth) + + for hostname, port in serial_ports: + serial_console.release_port(host=hostname, port=port) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error(_LE("Live Migration failure: %s"), e, @@ -6441,6 +6467,13 @@ class LibvirtDriver(driver.ComputeDriver): is_shared_instance_path = True if migrate_data: is_shared_instance_path = migrate_data.is_shared_instance_path + if (migrate_data.obj_attr_is_set("serial_listen_ports") + and migrate_data.serial_listen_ports): + # Releases serial ports reserved. + for port in migrate_data.serial_listen_ports: + serial_console.release_port( + host=migrate_data.serial_listen_addr, port=port) + if not is_shared_instance_path: instance_dir = libvirt_utils.get_instance_path_at_destination( instance, migrate_data) @@ -6568,6 +6601,15 @@ class LibvirtDriver(driver.ComputeDriver): CONF.libvirt.live_migration_inbound_addr migrate_data.supported_perf_events = self._supported_perf_events + migrate_data.serial_listen_ports = [] + if CONF.serial_console.enabled: + num_ports = hardware.get_number_of_serial_ports( + instance.flavor, instance.image_meta) + for port in six.moves.range(num_ports): + migrate_data.serial_listen_ports.append( + serial_console.acquire_port( + migrate_data.serial_listen_addr)) + for vol in block_device_mapping: connection_info = vol['connection_info'] if connection_info.get('serial'):