|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # Functional test that boots a Linux kernel and checks the console | 
|  | # | 
|  | # Copyright IBM Corp. 2023 | 
|  | # | 
|  | # Author: | 
|  | #  Pierre Morel <pmorel@linux.ibm.com> | 
|  | # | 
|  | # This work is licensed under the terms of the GNU GPL, version 2 or | 
|  | # later.  See the COPYING file in the top-level directory. | 
|  |  | 
|  | import os | 
|  | import time | 
|  |  | 
|  | from qemu_test import QemuSystemTest, Asset | 
|  | from qemu_test import exec_command | 
|  | from qemu_test import exec_command_and_wait_for_pattern | 
|  | from qemu_test import wait_for_console_pattern | 
|  | from qemu_test.utils import lzma_uncompress | 
|  |  | 
|  |  | 
|  | class S390CPUTopology(QemuSystemTest): | 
|  | """ | 
|  | S390x CPU topology consists of 4 topology layers, from bottom to top, | 
|  | the cores, sockets, books and drawers and 2 modifiers attributes, | 
|  | the entitlement and the dedication. | 
|  | See: docs/system/s390x/cpu-topology.rst. | 
|  |  | 
|  | S390x CPU topology is setup in different ways: | 
|  | - implicitly from the '-smp' argument by completing each topology | 
|  | level one after the other beginning with drawer 0, book 0 and | 
|  | socket 0. | 
|  | - explicitly from the '-device' argument on the QEMU command line | 
|  | - explicitly by hotplug of a new CPU using QMP or HMP | 
|  | - it is modified by using QMP 'set-cpu-topology' | 
|  |  | 
|  | The S390x modifier attribute entitlement depends on the machine | 
|  | polarization, which can be horizontal or vertical. | 
|  | The polarization is changed on a request from the guest. | 
|  | """ | 
|  | timeout = 90 | 
|  | event_timeout = 10 | 
|  |  | 
|  | KERNEL_COMMON_COMMAND_LINE = ('printk.time=0 ' | 
|  | 'root=/dev/ram ' | 
|  | 'selinux=0 ' | 
|  | 'rdinit=/bin/sh') | 
|  | ASSET_F35_KERNEL = Asset( | 
|  | ('https://archives.fedoraproject.org/pub/archive' | 
|  | '/fedora-secondary/releases/35/Server/s390x/os' | 
|  | '/images/kernel.img'), | 
|  | '1f2dddfd11bb1393dd2eb2e784036fbf6fc11057a6d7d27f9eb12d3edc67ef73') | 
|  |  | 
|  | ASSET_F35_INITRD = Asset( | 
|  | ('https://archives.fedoraproject.org/pub/archive' | 
|  | '/fedora-secondary/releases/35/Server/s390x/os' | 
|  | '/images/initrd.img'), | 
|  | '1100145fbca00240c8c372ae4b89b48c99844bc189b3dfbc3f481dc60055ca46') | 
|  |  | 
|  | def wait_until_booted(self): | 
|  | wait_for_console_pattern(self, 'no job control', | 
|  | failure_message='Kernel panic - not syncing', | 
|  | vm=None) | 
|  |  | 
|  | def check_topology(self, c, s, b, d, e, t): | 
|  | res = self.vm.qmp('query-cpus-fast') | 
|  | cpus =  res['return'] | 
|  | for cpu in cpus: | 
|  | core = cpu['props']['core-id'] | 
|  | socket = cpu['props']['socket-id'] | 
|  | book = cpu['props']['book-id'] | 
|  | drawer = cpu['props']['drawer-id'] | 
|  | entitlement = cpu.get('entitlement') | 
|  | dedicated = cpu.get('dedicated') | 
|  | if core == c: | 
|  | self.assertEqual(drawer, d) | 
|  | self.assertEqual(book, b) | 
|  | self.assertEqual(socket, s) | 
|  | self.assertEqual(entitlement, e) | 
|  | self.assertEqual(dedicated, t) | 
|  |  | 
|  | def kernel_init(self): | 
|  | """ | 
|  | We need a VM that supports CPU topology, | 
|  | currently this only the case when using KVM, not TCG. | 
|  | We need a kernel supporting the CPU topology. | 
|  | We need a minimal root filesystem with a shell. | 
|  | """ | 
|  | self.require_accelerator("kvm") | 
|  | kernel_path = self.ASSET_F35_KERNEL.fetch() | 
|  | initrd_path_xz = self.ASSET_F35_INITRD.fetch() | 
|  | initrd_path = os.path.join(self.workdir, 'initrd-raw.img') | 
|  | lzma_uncompress(initrd_path_xz, initrd_path) | 
|  |  | 
|  | self.vm.set_console() | 
|  | kernel_command_line = self.KERNEL_COMMON_COMMAND_LINE | 
|  | self.vm.add_args('-nographic', | 
|  | '-enable-kvm', | 
|  | '-cpu', 'max,ctop=on', | 
|  | '-m', '512', | 
|  | '-kernel', kernel_path, | 
|  | '-initrd', initrd_path, | 
|  | '-append', kernel_command_line) | 
|  |  | 
|  | def system_init(self): | 
|  | self.log.info("System init") | 
|  | exec_command_and_wait_for_pattern(self, | 
|  | """ mount proc -t proc /proc; | 
|  | mount sys -t sysfs /sys; | 
|  | cat /sys/devices/system/cpu/dispatching """, | 
|  | '0') | 
|  |  | 
|  | def test_single(self): | 
|  | """ | 
|  | This test checks the simplest topology with a single CPU. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | def test_default(self): | 
|  | """ | 
|  | This test checks the implicit topology. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.add_args('-smp', | 
|  | '13,drawers=2,books=2,sockets=3,cores=2,maxcpus=24') | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  | self.check_topology(1, 0, 0, 0, 'medium', False) | 
|  | self.check_topology(2, 1, 0, 0, 'medium', False) | 
|  | self.check_topology(3, 1, 0, 0, 'medium', False) | 
|  | self.check_topology(4, 2, 0, 0, 'medium', False) | 
|  | self.check_topology(5, 2, 0, 0, 'medium', False) | 
|  | self.check_topology(6, 0, 1, 0, 'medium', False) | 
|  | self.check_topology(7, 0, 1, 0, 'medium', False) | 
|  | self.check_topology(8, 1, 1, 0, 'medium', False) | 
|  | self.check_topology(9, 1, 1, 0, 'medium', False) | 
|  | self.check_topology(10, 2, 1, 0, 'medium', False) | 
|  | self.check_topology(11, 2, 1, 0, 'medium', False) | 
|  | self.check_topology(12, 0, 0, 1, 'medium', False) | 
|  |  | 
|  | def test_move(self): | 
|  | """ | 
|  | This test checks the topology modification by moving a CPU | 
|  | to another socket: CPU 0 is moved from socket 0 to socket 2. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.add_args('-smp', | 
|  | '1,drawers=2,books=2,sockets=3,cores=2,maxcpus=24') | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'socket-id': 2, 'entitlement': 'low'}) | 
|  | self.assertEqual(res['return'], {}) | 
|  | self.check_topology(0, 2, 0, 0, 'low', False) | 
|  |  | 
|  | def test_dash_device(self): | 
|  | """ | 
|  | This test verifies that a CPU defined with the '-device' | 
|  | command line option finds its right place inside the topology. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.add_args('-smp', | 
|  | '1,drawers=2,books=2,sockets=3,cores=2,maxcpus=24') | 
|  | self.vm.add_args('-device', 'max-s390x-cpu,core-id=10') | 
|  | self.vm.add_args('-device', | 
|  | 'max-s390x-cpu,' | 
|  | 'core-id=1,socket-id=0,book-id=1,drawer-id=1,entitlement=low') | 
|  | self.vm.add_args('-device', | 
|  | 'max-s390x-cpu,' | 
|  | 'core-id=2,socket-id=0,book-id=1,drawer-id=1,entitlement=medium') | 
|  | self.vm.add_args('-device', | 
|  | 'max-s390x-cpu,' | 
|  | 'core-id=3,socket-id=1,book-id=1,drawer-id=1,entitlement=high') | 
|  | self.vm.add_args('-device', | 
|  | 'max-s390x-cpu,' | 
|  | 'core-id=4,socket-id=1,book-id=1,drawer-id=1') | 
|  | self.vm.add_args('-device', | 
|  | 'max-s390x-cpu,' | 
|  | 'core-id=5,socket-id=2,book-id=1,drawer-id=1,dedicated=true') | 
|  |  | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.check_topology(10, 2, 1, 0, 'medium', False) | 
|  | self.check_topology(1, 0, 1, 1, 'low', False) | 
|  | self.check_topology(2, 0, 1, 1, 'medium', False) | 
|  | self.check_topology(3, 1, 1, 1, 'high', False) | 
|  | self.check_topology(4, 1, 1, 1, 'medium', False) | 
|  | self.check_topology(5, 2, 1, 1, 'high', True) | 
|  |  | 
|  |  | 
|  | def guest_set_dispatching(self, dispatching): | 
|  | exec_command(self, | 
|  | f'echo {dispatching} > /sys/devices/system/cpu/dispatching') | 
|  | self.vm.event_wait('CPU_POLARIZATION_CHANGE', self.event_timeout) | 
|  | exec_command_and_wait_for_pattern(self, | 
|  | 'cat /sys/devices/system/cpu/dispatching', dispatching) | 
|  |  | 
|  |  | 
|  | def test_polarization(self): | 
|  | """ | 
|  | This test verifies that QEMU modifies the entitlement change after | 
|  | several guest polarization change requests. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.system_init() | 
|  | res = self.vm.qmp('query-s390x-cpu-polarization') | 
|  | self.assertEqual(res['return']['polarization'], 'horizontal') | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | self.guest_set_dispatching('1'); | 
|  | res = self.vm.qmp('query-s390x-cpu-polarization') | 
|  | self.assertEqual(res['return']['polarization'], 'vertical') | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | self.guest_set_dispatching('0'); | 
|  | res = self.vm.qmp('query-s390x-cpu-polarization') | 
|  | self.assertEqual(res['return']['polarization'], 'horizontal') | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  |  | 
|  | def check_polarization(self, polarization): | 
|  | #We need to wait for the change to have been propagated to the kernel | 
|  | exec_command_and_wait_for_pattern(self, | 
|  | "\n".join([ | 
|  | "timeout 1 sh -c 'while true", | 
|  | 'do', | 
|  | '    syspath="/sys/devices/system/cpu/cpu0/polarization"', | 
|  | '    polarization="$(cat "$syspath")" || exit', | 
|  | f'    if [ "$polarization" = "{polarization}" ]; then', | 
|  | '        exit 0', | 
|  | '    fi', | 
|  | '    sleep 0.01', | 
|  | #searched for strings mustn't show up in command, '' to obfuscate | 
|  | "done' && echo succ''ess || echo fail''ure", | 
|  | ]), | 
|  | "success", "failure") | 
|  |  | 
|  |  | 
|  | def test_entitlement(self): | 
|  | """ | 
|  | This test verifies that QEMU modifies the entitlement | 
|  | after a guest request and that the guest sees the change. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.system_init() | 
|  |  | 
|  | self.check_polarization('horizontal') | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | self.guest_set_dispatching('1') | 
|  | self.check_polarization('vertical:medium') | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'low'}) | 
|  | self.assertEqual(res['return'], {}) | 
|  | self.check_polarization('vertical:low') | 
|  | self.check_topology(0, 0, 0, 0, 'low', False) | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'medium'}) | 
|  | self.assertEqual(res['return'], {}) | 
|  | self.check_polarization('vertical:medium') | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'high'}) | 
|  | self.assertEqual(res['return'], {}) | 
|  | self.check_polarization('vertical:high') | 
|  | self.check_topology(0, 0, 0, 0, 'high', False) | 
|  |  | 
|  | self.guest_set_dispatching('0'); | 
|  | self.check_polarization("horizontal") | 
|  | self.check_topology(0, 0, 0, 0, 'high', False) | 
|  |  | 
|  |  | 
|  | def test_dedicated(self): | 
|  | """ | 
|  | This test verifies that QEMU adjusts the entitlement correctly when a | 
|  | CPU is made dedicated. | 
|  | QEMU retains the entitlement value when horizontal polarization is in effect. | 
|  | For the guest, the field shows the effective value of the entitlement. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.system_init() | 
|  |  | 
|  | self.check_polarization("horizontal") | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'dedicated': True}) | 
|  | self.assertEqual(res['return'], {}) | 
|  | self.check_topology(0, 0, 0, 0, 'high', True) | 
|  | self.check_polarization("horizontal") | 
|  |  | 
|  | self.guest_set_dispatching('1'); | 
|  | self.check_topology(0, 0, 0, 0, 'high', True) | 
|  | self.check_polarization("vertical:high") | 
|  |  | 
|  | self.guest_set_dispatching('0'); | 
|  | self.check_topology(0, 0, 0, 0, 'high', True) | 
|  | self.check_polarization("horizontal") | 
|  |  | 
|  |  | 
|  | def test_socket_full(self): | 
|  | """ | 
|  | This test verifies that QEMU does not accept to overload a socket. | 
|  | The socket-id 0 on book-id 0 already contains CPUs 0 and 1 and can | 
|  | not accept any new CPU while socket-id 0 on book-id 1 is free. | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.add_args('-smp', | 
|  | '3,drawers=2,books=2,sockets=3,cores=2,maxcpus=24') | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.system_init() | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 2, 'socket-id': 0, 'book-id': 0}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 2, 'socket-id': 0, 'book-id': 1}) | 
|  | self.assertEqual(res['return'], {}) | 
|  |  | 
|  | def test_dedicated_error(self): | 
|  | """ | 
|  | This test verifies that QEMU refuses to lower the entitlement | 
|  | of a dedicated CPU | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.system_init() | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'dedicated': True}) | 
|  | self.assertEqual(res['return'], {}) | 
|  |  | 
|  | self.check_topology(0, 0, 0, 0, 'high', True) | 
|  |  | 
|  | self.guest_set_dispatching('1'); | 
|  |  | 
|  | self.check_topology(0, 0, 0, 0, 'high', True) | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'low', 'dedicated': True}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'low'}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'medium', 'dedicated': True}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'medium'}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'low', 'dedicated': False}) | 
|  | self.assertEqual(res['return'], {}) | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', | 
|  | {'core-id': 0, 'entitlement': 'medium', 'dedicated': False}) | 
|  | self.assertEqual(res['return'], {}) | 
|  |  | 
|  | def test_move_error(self): | 
|  | """ | 
|  | This test verifies that QEMU refuses to move a CPU to an | 
|  | nonexistent location | 
|  | """ | 
|  | self.set_machine('s390-ccw-virtio') | 
|  | self.kernel_init() | 
|  | self.vm.launch() | 
|  | self.wait_until_booted() | 
|  |  | 
|  | self.system_init() | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', {'core-id': 0, 'drawer-id': 1}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', {'core-id': 0, 'book-id': 1}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | res = self.vm.qmp('set-cpu-topology', {'core-id': 0, 'socket-id': 1}) | 
|  | self.assertEqual(res['error']['class'], 'GenericError') | 
|  |  | 
|  | self.check_topology(0, 0, 0, 0, 'medium', False) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | QemuSystemTest.main() |