xref: /qemu/tests/functional/test_s390x_topology.py (revision 5f9976486970b0fec50ff4c07da7af620cd7d0a0)
1#!/usr/bin/env python3
2#
3# Functional test that boots a Linux kernel and checks the console
4#
5# Copyright IBM Corp. 2023
6#
7# Author:
8#  Pierre Morel <pmorel@linux.ibm.com>
9#
10# This work is licensed under the terms of the GNU GPL, version 2 or
11# later.  See the COPYING file in the top-level directory.
12
13import os
14
15from qemu_test import QemuSystemTest, Asset
16from qemu_test import exec_command
17from qemu_test import exec_command_and_wait_for_pattern
18from qemu_test import wait_for_console_pattern
19from qemu_test.utils import lzma_uncompress
20
21
22class S390CPUTopology(QemuSystemTest):
23    """
24    S390x CPU topology consists of 4 topology layers, from bottom to top,
25    the cores, sockets, books and drawers and 2 modifiers attributes,
26    the entitlement and the dedication.
27    See: docs/system/s390x/cpu-topology.rst.
28
29    S390x CPU topology is setup in different ways:
30    - implicitly from the '-smp' argument by completing each topology
31      level one after the other beginning with drawer 0, book 0 and
32      socket 0.
33    - explicitly from the '-device' argument on the QEMU command line
34    - explicitly by hotplug of a new CPU using QMP or HMP
35    - it is modified by using QMP 'set-cpu-topology'
36
37    The S390x modifier attribute entitlement depends on the machine
38    polarization, which can be horizontal or vertical.
39    The polarization is changed on a request from the guest.
40    """
41    timeout = 90
42    event_timeout = 10
43
44    KERNEL_COMMON_COMMAND_LINE = ('printk.time=0 '
45                                  'root=/dev/ram '
46                                  'selinux=0 '
47                                  'rdinit=/bin/sh')
48    ASSET_F35_KERNEL = Asset(
49        ('https://archives.fedoraproject.org/pub/archive'
50         '/fedora-secondary/releases/35/Server/s390x/os'
51         '/images/kernel.img'),
52        '1f2dddfd11bb1393dd2eb2e784036fbf6fc11057a6d7d27f9eb12d3edc67ef73')
53
54    ASSET_F35_INITRD = Asset(
55        ('https://archives.fedoraproject.org/pub/archive'
56         '/fedora-secondary/releases/35/Server/s390x/os'
57         '/images/initrd.img'),
58        '1100145fbca00240c8c372ae4b89b48c99844bc189b3dfbc3f481dc60055ca46')
59
60    def wait_until_booted(self):
61        wait_for_console_pattern(self, 'no job control',
62                                 failure_message='Kernel panic - not syncing',
63                                 vm=None)
64
65    def check_topology(self, c, s, b, d, e, t):
66        res = self.vm.qmp('query-cpus-fast')
67        cpus =  res['return']
68        for cpu in cpus:
69            core = cpu['props']['core-id']
70            socket = cpu['props']['socket-id']
71            book = cpu['props']['book-id']
72            drawer = cpu['props']['drawer-id']
73            entitlement = cpu.get('entitlement')
74            dedicated = cpu.get('dedicated')
75            if core == c:
76                self.assertEqual(drawer, d)
77                self.assertEqual(book, b)
78                self.assertEqual(socket, s)
79                self.assertEqual(entitlement, e)
80                self.assertEqual(dedicated, t)
81
82    def kernel_init(self):
83        """
84        We need a VM that supports CPU topology,
85        currently this only the case when using KVM, not TCG.
86        We need a kernel supporting the CPU topology.
87        We need a minimal root filesystem with a shell.
88        """
89        self.require_accelerator("kvm")
90        kernel_path = self.ASSET_F35_KERNEL.fetch()
91        initrd_path_xz = self.ASSET_F35_INITRD.fetch()
92        initrd_path = os.path.join(self.workdir, 'initrd-raw.img')
93        lzma_uncompress(initrd_path_xz, initrd_path)
94
95        self.vm.set_console()
96        kernel_command_line = self.KERNEL_COMMON_COMMAND_LINE
97        self.vm.add_args('-nographic',
98                         '-enable-kvm',
99                         '-cpu', 'max,ctop=on',
100                         '-m', '512',
101                         '-kernel', kernel_path,
102                         '-initrd', initrd_path,
103                         '-append', kernel_command_line)
104
105    def system_init(self):
106        self.log.info("System init")
107        exec_command_and_wait_for_pattern(self,
108                """ mount proc -t proc /proc;
109                    mount sys -t sysfs /sys;
110                    cat /sys/devices/system/cpu/dispatching """,
111                    '0')
112
113    def test_single(self):
114        """
115        This test checks the simplest topology with a single CPU.
116        """
117        self.set_machine('s390-ccw-virtio')
118        self.kernel_init()
119        self.vm.launch()
120        self.wait_until_booted()
121        self.check_topology(0, 0, 0, 0, 'medium', False)
122
123    def test_default(self):
124        """
125        This test checks the implicit topology.
126        """
127        self.set_machine('s390-ccw-virtio')
128        self.kernel_init()
129        self.vm.add_args('-smp',
130                         '13,drawers=2,books=2,sockets=3,cores=2,maxcpus=24')
131        self.vm.launch()
132        self.wait_until_booted()
133        self.check_topology(0, 0, 0, 0, 'medium', False)
134        self.check_topology(1, 0, 0, 0, 'medium', False)
135        self.check_topology(2, 1, 0, 0, 'medium', False)
136        self.check_topology(3, 1, 0, 0, 'medium', False)
137        self.check_topology(4, 2, 0, 0, 'medium', False)
138        self.check_topology(5, 2, 0, 0, 'medium', False)
139        self.check_topology(6, 0, 1, 0, 'medium', False)
140        self.check_topology(7, 0, 1, 0, 'medium', False)
141        self.check_topology(8, 1, 1, 0, 'medium', False)
142        self.check_topology(9, 1, 1, 0, 'medium', False)
143        self.check_topology(10, 2, 1, 0, 'medium', False)
144        self.check_topology(11, 2, 1, 0, 'medium', False)
145        self.check_topology(12, 0, 0, 1, 'medium', False)
146
147    def test_move(self):
148        """
149        This test checks the topology modification by moving a CPU
150        to another socket: CPU 0 is moved from socket 0 to socket 2.
151        """
152        self.set_machine('s390-ccw-virtio')
153        self.kernel_init()
154        self.vm.add_args('-smp',
155                         '1,drawers=2,books=2,sockets=3,cores=2,maxcpus=24')
156        self.vm.launch()
157        self.wait_until_booted()
158
159        self.check_topology(0, 0, 0, 0, 'medium', False)
160        res = self.vm.qmp('set-cpu-topology',
161                          {'core-id': 0, 'socket-id': 2, 'entitlement': 'low'})
162        self.assertEqual(res['return'], {})
163        self.check_topology(0, 2, 0, 0, 'low', False)
164
165    def test_dash_device(self):
166        """
167        This test verifies that a CPU defined with the '-device'
168        command line option finds its right place inside the topology.
169        """
170        self.set_machine('s390-ccw-virtio')
171        self.kernel_init()
172        self.vm.add_args('-smp',
173                         '1,drawers=2,books=2,sockets=3,cores=2,maxcpus=24')
174        self.vm.add_args('-device', 'max-s390x-cpu,core-id=10')
175        self.vm.add_args('-device',
176                         'max-s390x-cpu,'
177                         'core-id=1,socket-id=0,book-id=1,drawer-id=1,entitlement=low')
178        self.vm.add_args('-device',
179                         'max-s390x-cpu,'
180                         'core-id=2,socket-id=0,book-id=1,drawer-id=1,entitlement=medium')
181        self.vm.add_args('-device',
182                         'max-s390x-cpu,'
183                         'core-id=3,socket-id=1,book-id=1,drawer-id=1,entitlement=high')
184        self.vm.add_args('-device',
185                         'max-s390x-cpu,'
186                         'core-id=4,socket-id=1,book-id=1,drawer-id=1')
187        self.vm.add_args('-device',
188                         'max-s390x-cpu,'
189                         'core-id=5,socket-id=2,book-id=1,drawer-id=1,dedicated=true')
190
191        self.vm.launch()
192        self.wait_until_booted()
193
194        self.check_topology(10, 2, 1, 0, 'medium', False)
195        self.check_topology(1, 0, 1, 1, 'low', False)
196        self.check_topology(2, 0, 1, 1, 'medium', False)
197        self.check_topology(3, 1, 1, 1, 'high', False)
198        self.check_topology(4, 1, 1, 1, 'medium', False)
199        self.check_topology(5, 2, 1, 1, 'high', True)
200
201
202    def guest_set_dispatching(self, dispatching):
203        exec_command(self,
204                f'echo {dispatching} > /sys/devices/system/cpu/dispatching')
205        self.vm.event_wait('CPU_POLARIZATION_CHANGE', self.event_timeout)
206        exec_command_and_wait_for_pattern(self,
207                'cat /sys/devices/system/cpu/dispatching', dispatching)
208
209
210    def test_polarization(self):
211        """
212        This test verifies that QEMU modifies the entitlement change after
213        several guest polarization change requests.
214        """
215        self.set_machine('s390-ccw-virtio')
216        self.kernel_init()
217        self.vm.launch()
218        self.wait_until_booted()
219
220        self.system_init()
221        res = self.vm.qmp('query-s390x-cpu-polarization')
222        self.assertEqual(res['return']['polarization'], 'horizontal')
223        self.check_topology(0, 0, 0, 0, 'medium', False)
224
225        self.guest_set_dispatching('1');
226        res = self.vm.qmp('query-s390x-cpu-polarization')
227        self.assertEqual(res['return']['polarization'], 'vertical')
228        self.check_topology(0, 0, 0, 0, 'medium', False)
229
230        self.guest_set_dispatching('0');
231        res = self.vm.qmp('query-s390x-cpu-polarization')
232        self.assertEqual(res['return']['polarization'], 'horizontal')
233        self.check_topology(0, 0, 0, 0, 'medium', False)
234
235
236    def check_polarization(self, polarization):
237        #We need to wait for the change to have been propagated to the kernel
238        exec_command_and_wait_for_pattern(self,
239            "\n".join([
240                "timeout 1 sh -c 'while true",
241                'do',
242                '    syspath="/sys/devices/system/cpu/cpu0/polarization"',
243                '    polarization="$(cat "$syspath")" || exit',
244               f'    if [ "$polarization" = "{polarization}" ]; then',
245                '        exit 0',
246                '    fi',
247                '    sleep 0.01',
248                #searched for strings mustn't show up in command, '' to obfuscate
249                "done' && echo succ''ess || echo fail''ure",
250            ]),
251            "success", "failure")
252
253
254    def test_entitlement(self):
255        """
256        This test verifies that QEMU modifies the entitlement
257        after a guest request and that the guest sees the change.
258        """
259        self.set_machine('s390-ccw-virtio')
260        self.kernel_init()
261        self.vm.launch()
262        self.wait_until_booted()
263
264        self.system_init()
265
266        self.check_polarization('horizontal')
267        self.check_topology(0, 0, 0, 0, 'medium', False)
268
269        self.guest_set_dispatching('1')
270        self.check_polarization('vertical:medium')
271        self.check_topology(0, 0, 0, 0, 'medium', False)
272
273        res = self.vm.qmp('set-cpu-topology',
274                          {'core-id': 0, 'entitlement': 'low'})
275        self.assertEqual(res['return'], {})
276        self.check_polarization('vertical:low')
277        self.check_topology(0, 0, 0, 0, 'low', False)
278
279        res = self.vm.qmp('set-cpu-topology',
280                          {'core-id': 0, 'entitlement': 'medium'})
281        self.assertEqual(res['return'], {})
282        self.check_polarization('vertical:medium')
283        self.check_topology(0, 0, 0, 0, 'medium', False)
284
285        res = self.vm.qmp('set-cpu-topology',
286                          {'core-id': 0, 'entitlement': 'high'})
287        self.assertEqual(res['return'], {})
288        self.check_polarization('vertical:high')
289        self.check_topology(0, 0, 0, 0, 'high', False)
290
291        self.guest_set_dispatching('0');
292        self.check_polarization("horizontal")
293        self.check_topology(0, 0, 0, 0, 'high', False)
294
295
296    def test_dedicated(self):
297        """
298        This test verifies that QEMU adjusts the entitlement correctly when a
299        CPU is made dedicated.
300        QEMU retains the entitlement value when horizontal polarization is in effect.
301        For the guest, the field shows the effective value of the entitlement.
302        """
303        self.set_machine('s390-ccw-virtio')
304        self.kernel_init()
305        self.vm.launch()
306        self.wait_until_booted()
307
308        self.system_init()
309
310        self.check_polarization("horizontal")
311
312        res = self.vm.qmp('set-cpu-topology',
313                          {'core-id': 0, 'dedicated': True})
314        self.assertEqual(res['return'], {})
315        self.check_topology(0, 0, 0, 0, 'high', True)
316        self.check_polarization("horizontal")
317
318        self.guest_set_dispatching('1');
319        self.check_topology(0, 0, 0, 0, 'high', True)
320        self.check_polarization("vertical:high")
321
322        self.guest_set_dispatching('0');
323        self.check_topology(0, 0, 0, 0, 'high', True)
324        self.check_polarization("horizontal")
325
326
327    def test_socket_full(self):
328        """
329        This test verifies that QEMU does not accept to overload a socket.
330        The socket-id 0 on book-id 0 already contains CPUs 0 and 1 and can
331        not accept any new CPU while socket-id 0 on book-id 1 is free.
332        """
333        self.set_machine('s390-ccw-virtio')
334        self.kernel_init()
335        self.vm.add_args('-smp',
336                         '3,drawers=2,books=2,sockets=3,cores=2,maxcpus=24')
337        self.vm.launch()
338        self.wait_until_booted()
339
340        self.system_init()
341
342        res = self.vm.qmp('set-cpu-topology',
343                          {'core-id': 2, 'socket-id': 0, 'book-id': 0})
344        self.assertEqual(res['error']['class'], 'GenericError')
345
346        res = self.vm.qmp('set-cpu-topology',
347                          {'core-id': 2, 'socket-id': 0, 'book-id': 1})
348        self.assertEqual(res['return'], {})
349
350    def test_dedicated_error(self):
351        """
352        This test verifies that QEMU refuses to lower the entitlement
353        of a dedicated CPU
354        """
355        self.set_machine('s390-ccw-virtio')
356        self.kernel_init()
357        self.vm.launch()
358        self.wait_until_booted()
359
360        self.system_init()
361
362        res = self.vm.qmp('set-cpu-topology',
363                          {'core-id': 0, 'dedicated': True})
364        self.assertEqual(res['return'], {})
365
366        self.check_topology(0, 0, 0, 0, 'high', True)
367
368        self.guest_set_dispatching('1');
369
370        self.check_topology(0, 0, 0, 0, 'high', True)
371
372        res = self.vm.qmp('set-cpu-topology',
373                          {'core-id': 0, 'entitlement': 'low', 'dedicated': True})
374        self.assertEqual(res['error']['class'], 'GenericError')
375
376        res = self.vm.qmp('set-cpu-topology',
377                          {'core-id': 0, 'entitlement': 'low'})
378        self.assertEqual(res['error']['class'], 'GenericError')
379
380        res = self.vm.qmp('set-cpu-topology',
381                          {'core-id': 0, 'entitlement': 'medium', 'dedicated': True})
382        self.assertEqual(res['error']['class'], 'GenericError')
383
384        res = self.vm.qmp('set-cpu-topology',
385                          {'core-id': 0, 'entitlement': 'medium'})
386        self.assertEqual(res['error']['class'], 'GenericError')
387
388        res = self.vm.qmp('set-cpu-topology',
389                          {'core-id': 0, 'entitlement': 'low', 'dedicated': False})
390        self.assertEqual(res['return'], {})
391
392        res = self.vm.qmp('set-cpu-topology',
393                          {'core-id': 0, 'entitlement': 'medium', 'dedicated': False})
394        self.assertEqual(res['return'], {})
395
396    def test_move_error(self):
397        """
398        This test verifies that QEMU refuses to move a CPU to an
399        nonexistent location
400        """
401        self.set_machine('s390-ccw-virtio')
402        self.kernel_init()
403        self.vm.launch()
404        self.wait_until_booted()
405
406        self.system_init()
407
408        res = self.vm.qmp('set-cpu-topology', {'core-id': 0, 'drawer-id': 1})
409        self.assertEqual(res['error']['class'], 'GenericError')
410
411        res = self.vm.qmp('set-cpu-topology', {'core-id': 0, 'book-id': 1})
412        self.assertEqual(res['error']['class'], 'GenericError')
413
414        res = self.vm.qmp('set-cpu-topology', {'core-id': 0, 'socket-id': 1})
415        self.assertEqual(res['error']['class'], 'GenericError')
416
417        self.check_topology(0, 0, 0, 0, 'medium', False)
418
419if __name__ == '__main__':
420    QemuSystemTest.main()
421