| 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # virtio-balloon tests |
| 4 | # |
| 5 | # This work is licensed under the terms of the GNU GPL, version 2 or |
| 6 | # later. See the COPYING file in the top-level directory. |
| 7 | |
| 8 | import time |
| 9 | |
| 10 | from qemu_test import QemuSystemTest, Asset |
| 11 | from qemu_test import wait_for_console_pattern |
| 12 | from qemu_test import exec_command_and_wait_for_pattern |
| 13 | |
| 14 | UNSET_STATS_VALUE = 18446744073709551615 |
| 15 | |
| 16 | |
| 17 | class VirtioBalloonx86(QemuSystemTest): |
| 18 | |
| 19 | ASSET_KERNEL = Asset( |
| 20 | ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases' |
| 21 | '/31/Server/x86_64/os/images/pxeboot/vmlinuz'), |
| 22 | 'd4738d03dbbe083ca610d0821d0a8f1488bebbdccef54ce33e3adb35fda00129') |
| 23 | |
| 24 | ASSET_INITRD = Asset( |
| 25 | ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases' |
| 26 | '/31/Server/x86_64/os/images/pxeboot/initrd.img'), |
| 27 | '277cd6c7adf77c7e63d73bbb2cded8ef9e2d3a2f100000e92ff1f8396513cd8b') |
| 28 | |
| 29 | ASSET_DISKIMAGE = Asset( |
| 30 | ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases' |
| 31 | '/31/Cloud/x86_64/images/Fedora-Cloud-Base-31-1.9.x86_64.qcow2'), |
| 32 | 'e3c1b309d9203604922d6e255c2c5d098a309c2d46215d8fc026954f3c5c27a0') |
| 33 | |
| 34 | DEFAULT_KERNEL_PARAMS = ('root=/dev/vda1 console=ttyS0 net.ifnames=0 ' |
| 35 | 'rd.rescue quiet') |
| 36 | |
| 37 | def wait_for_console_pattern(self, success_message, vm=None): |
| 38 | wait_for_console_pattern( |
| 39 | self, |
| 40 | success_message, |
| 41 | failure_message="Kernel panic - not syncing", |
| 42 | vm=vm, |
| 43 | ) |
| 44 | |
| 45 | def mount_root(self): |
| 46 | self.wait_for_console_pattern('Entering emergency mode.') |
| 47 | prompt = '# ' |
| 48 | self.wait_for_console_pattern(prompt) |
| 49 | |
| 50 | # Synchronize on virtio-block driver creating the root device |
| 51 | exec_command_and_wait_for_pattern(self, |
| 52 | "while ! (dmesg -c | grep vda:) ; do sleep 1 ; done", |
| 53 | "vda1") |
| 54 | |
| 55 | exec_command_and_wait_for_pattern(self, 'mount /dev/vda1 /sysroot', |
| 56 | prompt) |
| 57 | exec_command_and_wait_for_pattern(self, 'chroot /sysroot', |
| 58 | prompt) |
| 59 | exec_command_and_wait_for_pattern(self, "modprobe virtio-balloon", |
| 60 | prompt) |
| 61 | |
| 62 | def assert_initial_stats(self): |
| 63 | ret = self.vm.qmp('qom-get', |
| 64 | {'path': '/machine/peripheral/balloon', |
| 65 | 'property': 'guest-stats'})['return'] |
| 66 | when = ret.get('last-update') |
| 67 | assert when == 0 |
| 68 | stats = ret.get('stats') |
| 69 | for _name, val in stats.items(): |
| 70 | assert val == UNSET_STATS_VALUE |
| 71 | |
| 72 | def assert_running_stats(self, then): |
| 73 | # We told the QEMU to refresh stats every 100ms, but |
| 74 | # there can be a delay between virtio-ballon driver |
| 75 | # being modprobed and seeing the first stats refresh |
| 76 | # Retry a few times for robustness under heavy load |
| 77 | retries = 10 |
| 78 | when = 0 |
| 79 | while when == 0 and retries: |
| 80 | ret = self.vm.qmp('qom-get', |
| 81 | {'path': '/machine/peripheral/balloon', |
| 82 | 'property': 'guest-stats'})['return'] |
| 83 | when = ret.get('last-update') |
| 84 | if when == 0: |
| 85 | retries = retries - 1 |
| 86 | time.sleep(0.5) |
| 87 | |
| 88 | now = time.time() |
| 89 | |
| 90 | assert now > when > then |
| 91 | stats = ret.get('stats') |
| 92 | # Stat we expect this particular Kernel to have set |
| 93 | expect_data = [ |
| 94 | "stat-available-memory", |
| 95 | "stat-disk-caches", |
| 96 | "stat-free-memory", |
| 97 | "stat-htlb-pgalloc", |
| 98 | "stat-htlb-pgfail", |
| 99 | "stat-major-faults", |
| 100 | "stat-minor-faults", |
| 101 | "stat-swap-in", |
| 102 | "stat-swap-out", |
| 103 | "stat-total-memory", |
| 104 | ] |
| 105 | for name, val in stats.items(): |
| 106 | if name in expect_data: |
| 107 | assert val != UNSET_STATS_VALUE |
| 108 | else: |
| 109 | assert val == UNSET_STATS_VALUE |
| 110 | |
| 111 | def test_virtio_balloon_stats(self): |
| 112 | self.set_machine('q35') |
| 113 | self.require_accelerator("kvm") |
| 114 | kernel_path = self.ASSET_KERNEL.fetch() |
| 115 | initrd_path = self.ASSET_INITRD.fetch() |
| 116 | diskimage_path = self.ASSET_DISKIMAGE.fetch() |
| 117 | |
| 118 | self.vm.set_console() |
| 119 | self.vm.add_args("-S") |
| 120 | self.vm.add_args("-cpu", "max") |
| 121 | self.vm.add_args("-m", "2G") |
| 122 | # Slow down BIOS phase with boot menu, so that after a system |
| 123 | # reset, we can reliably catch the clean stats again in BIOS |
| 124 | # phase before the guest OS launches |
| 125 | self.vm.add_args("-boot", "menu=on") |
| 126 | self.vm.add_args("-accel", "kvm") |
| 127 | self.vm.add_args("-device", "virtio-balloon,id=balloon") |
| 128 | self.vm.add_args('-drive', |
| 129 | f'file={diskimage_path},if=none,id=drv0,snapshot=on') |
| 130 | self.vm.add_args('-device', 'virtio-blk-pci,bus=pcie.0,' + |
| 131 | 'drive=drv0,id=virtio-disk0,bootindex=1') |
| 132 | |
| 133 | self.vm.add_args( |
| 134 | "-kernel", |
| 135 | kernel_path, |
| 136 | "-initrd", |
| 137 | initrd_path, |
| 138 | "-append", |
| 139 | self.DEFAULT_KERNEL_PARAMS |
| 140 | ) |
| 141 | self.vm.launch() |
| 142 | |
| 143 | # Poll stats at 100ms |
| 144 | self.vm.qmp('qom-set', |
| 145 | {'path': '/machine/peripheral/balloon', |
| 146 | 'property': 'guest-stats-polling-interval', |
| 147 | 'value': 100 }) |
| 148 | |
| 149 | # We've not run any guest code yet, neither BIOS or guest, |
| 150 | # so stats should be all default values |
| 151 | self.assert_initial_stats() |
| 152 | |
| 153 | self.vm.qmp('cont') |
| 154 | |
| 155 | then = time.time() |
| 156 | self.mount_root() |
| 157 | self.assert_running_stats(then) |
| 158 | |
| 159 | # Race window between these two commands, where we |
| 160 | # rely on '-boot menu=on' to (hopefully) ensure we're |
| 161 | # still executing the BIOS when QEMU processes the |
| 162 | # 'stop', and thus have not loaded the virtio-balloon |
| 163 | # driver in the guest |
| 164 | self.vm.qmp('system_reset') |
| 165 | self.vm.qmp('stop') |
| 166 | |
| 167 | # If the above assumption held, we're in BIOS now and |
| 168 | # stats should be all back at their default values |
| 169 | self.assert_initial_stats() |
| 170 | self.vm.qmp('cont') |
| 171 | |
| 172 | then = time.time() |
| 173 | self.mount_root() |
| 174 | self.assert_running_stats(then) |
| 175 | |
| 176 | |
| 177 | if __name__ == '__main__': |
| 178 | QemuSystemTest.main() |