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