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