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