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