From 1f659251c7509cab045024044a6b8d642ad85aef Mon Sep 17 00:00:00 2001 From: Markus Zoeller Date: Tue, 6 Dec 2016 11:40:25 +0100 Subject: [PATCH] libvirt: virtlogd: use virtlogd for char devices This change makes actual usage of the "logd" sub-element for char devices. The two REST APIs ``os-getConsoleOutput`` and ``os-getSerialConsole`` can now be satisfied at the same time. This is valid for any combination of: * char device element: "console", "serial" * char device type: "tcp", "pty" There is also no need to create multiple different device types anymore. If we have a tcp device, we don't need the pty device anymore. The logging will be done in the tcp device. Implements blueprint libvirt-virtlogd Closes-Bug: 832507 Change-Id: Ia412f55bd988f6e11cd78c4c5a50a86389e648b0 --- nova/tests/unit/virt/libvirt/test_driver.py | 39 ++++++++++ nova/virt/libvirt/driver.py | 85 ++++++++++++++++++--- 2 files changed, 114 insertions(+), 10 deletions(-) diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 3ae35e3971e0..49abe87b4477 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -3829,6 +3829,45 @@ class LibvirtConnTestCase(test.NoDBTestCase): self.assertEqual("tcp", cfg.devices[3].type) self.assertEqual("tcp", cfg.devices[4].type) + @mock.patch.object(host.Host, 'has_min_version', return_value=True) + @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_guest_config_char_device_logd(self, mock_get_arch, + mock_get_number_serial_ports, + mock_acquire_port, + mock_host_has_min_version): + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) + + def _test_consoles(arch_to_mock, serial_enabled, + expected_device_type, expected_device_cls): + guest_cfg = vconfig.LibvirtConfigGuest() + mock_get_arch.return_value = arch_to_mock + self.flags(enabled=serial_enabled, group='serial_console') + instance = objects.Instance(**self.test_instance) + + drvr._create_consoles("qemu", guest_cfg, instance=instance, + flavor=None, image_meta=None) + + self.assertEqual(1, len(guest_cfg.devices)) + device = guest_cfg.devices[0] + self.assertEqual(expected_device_type, device.type) + self.assertIsInstance(device, expected_device_cls) + self.assertIsInstance(device.log, + vconfig.LibvirtConfigGuestCharDeviceLog) + self.assertEqual("off", device.log.append) + self.assertTrue(device.log.file.endswith("console.log")) + + _test_consoles(fields.Architecture.X86_64, True, + "tcp", vconfig.LibvirtConfigGuestSerial) + _test_consoles(fields.Architecture.X86_64, False, + "pty", vconfig.LibvirtConfigGuestSerial) + _test_consoles(fields.Architecture.S390, True, + "tcp", vconfig.LibvirtConfigGuestConsole) + _test_consoles(fields.Architecture.S390X, False, + "pty", vconfig.LibvirtConfigGuestConsole) + @mock.patch('nova.console.serial.acquire_port') def test_get_guest_config_serial_console_through_port_rng_exhausted( self, acquire_port): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 59328c6f6137..90a38c60d2a2 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -269,6 +269,10 @@ MIN_QEMU_S390_VERSION = (2, 3, 0) # libvirt 1.3 fix f391889f4e942e22b9ef8ecca492de05106ce41e MIN_LIBVIRT_PF_WITH_NO_VFS_CAP_VERSION = (1, 3, 0) +# Use the "logd" backend for handling stdout/stderr from QEMU processes. +MIN_LIBVIRT_VIRTLOGD = (1, 3, 3) +MIN_QEMU_VIRTLOGD = (2, 7, 0) + # ppc64/ppc64le architectures with KVM # NOTE(rfolco): Same levels for Libvirt/Qemu on Big Endian and Little # Endian giving the nuance around guest vs host architectures @@ -595,6 +599,10 @@ class LibvirtDriver(driver.ComputeDriver): return True return False + def _is_virtlogd_available(self): + return self._host.has_min_version(MIN_LIBVIRT_VIRTLOGD, + MIN_QEMU_VIRTLOGD) + def _handle_live_migration_post_copy(self, migration_flags): if CONF.libvirt.live_migration_permit_post_copy: if self._is_post_copy_available(): @@ -4364,6 +4372,21 @@ class LibvirtDriver(driver.ComputeDriver): def _create_consoles(self, virt_type, guest_cfg, instance, flavor, image_meta): + # NOTE(markus_z): Beware! Below are so many conditionals that it is + # easy to lose track. Use this chart to figure out your case: + # + # case | is serial | has | is qemu | resulting + # | enabled? | virtlogd? | or kvm? | devices + # -------------------------------------------------- + # 1 | no | no | no | pty* + # 2 | no | no | yes | file + pty + # 3 | no | yes | no | see case 1 + # 4 | no | yes | yes | pty with logd + # 5 | yes | no | no | see case 1 + # 6 | yes | no | yes | tcp + pty + # 7 | yes | yes | no | see case 1 + # 8 | yes | yes | yes | tcp with logd + # * exception: virt_type "parallels" doesn't create a device if virt_type == 'parallels': pass elif virt_type not in ("qemu", "kvm"): @@ -4384,38 +4407,73 @@ class LibvirtDriver(driver.ComputeDriver): def _create_consoles_qemu_kvm(self, guest_cfg, instance, flavor, image_meta): char_dev_cls = vconfig.LibvirtConfigGuestSerial + log_path = self._get_console_log_path(instance) if CONF.serial_console.enabled: if not self._serial_ports_already_defined(instance): num_ports = hardware.get_number_of_serial_ports(flavor, image_meta) self._check_number_of_serial_console(num_ports) self._create_serial_consoles(guest_cfg, num_ports, - char_dev_cls) + char_dev_cls, log_path) else: self._create_file_device(guest_cfg, instance, char_dev_cls) - self._create_pty_device(guest_cfg, char_dev_cls) + self._create_pty_device(guest_cfg, char_dev_cls, log_path=log_path) def _create_consoles_s390x(self, guest_cfg, instance, flavor, image_meta): char_dev_cls = vconfig.LibvirtConfigGuestConsole + log_path = self._get_console_log_path(instance) if CONF.serial_console.enabled: if not self._serial_ports_already_defined(instance): num_ports = hardware.get_number_of_serial_ports(flavor, image_meta) self._create_serial_consoles(guest_cfg, num_ports, - char_dev_cls) + char_dev_cls, log_path) else: self._create_file_device(guest_cfg, instance, char_dev_cls, "sclplm") - self._create_pty_device(guest_cfg, char_dev_cls, "sclp") + self._create_pty_device(guest_cfg, char_dev_cls, "sclp", log_path) - def _create_pty_device(self, guest_cfg, char_dev_cls, target_type=None): - consolepty = char_dev_cls() - consolepty.target_type = target_type - consolepty.type = "pty" - guest_cfg.add_device(consolepty) + def _create_pty_device(self, guest_cfg, char_dev_cls, target_type=None, + log_path=None): + def _create_base_dev(): + consolepty = char_dev_cls() + consolepty.target_type = target_type + consolepty.type = "pty" + return consolepty + + def _create_logd_dev(): + consolepty = _create_base_dev() + log = vconfig.LibvirtConfigGuestCharDeviceLog() + log.file = log_path + consolepty.log = log + return consolepty + + if CONF.serial_console.enabled: + if self._is_virtlogd_available(): + return + else: + # NOTE(markus_z): You may wonder why this is necessary and + # so do I. I'm certain that this is *not* needed in any + # real use case. It is, however, useful if you want to + # pypass the Nova API and use "virsh console " on + # an hypervisor, as this CLI command doesn't work with TCP + # devices (like the serial console is). + # https://bugzilla.redhat.com/show_bug.cgi?id=781467 + # Pypassing the Nova API however is a thing we don't want. + # Future changes should remove this and fix the unit tests + # which ask for the existence. + guest_cfg.add_device(_create_base_dev()) + else: + if self._is_virtlogd_available(): + guest_cfg.add_device(_create_logd_dev()) + else: + guest_cfg.add_device(_create_base_dev()) def _create_file_device(self, guest_cfg, instance, char_dev_cls, target_type=None): + if self._is_virtlogd_available(): + return + consolelog = char_dev_cls() consolelog.target_type = target_type consolelog.type = "file" @@ -4436,7 +4494,8 @@ class LibvirtDriver(driver.ComputeDriver): "ports in its domain XML", instance=instance) return False - def _create_serial_consoles(self, guest_cfg, num_ports, char_dev_cls): + def _create_serial_consoles(self, guest_cfg, num_ports, char_dev_cls, + log_path): for port in six.moves.range(num_ports): console = char_dev_cls() console.port = port @@ -4444,6 +4503,12 @@ class LibvirtDriver(driver.ComputeDriver): console.listen_host = CONF.serial_console.proxyclient_address listen_port = serial_console.acquire_port(console.listen_host) console.listen_port = listen_port + # NOTE: only the first serial console gets the boot messages, + # that's why we attach the logd subdevice only to that. + if port == 0 and self._is_virtlogd_available(): + log = vconfig.LibvirtConfigGuestCharDeviceLog() + log.file = log_path + console.log = log guest_cfg.add_device(console) def _cpu_config_to_vcpu_model(self, cpu_config, vcpu_model):