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