#!/usr/bin/env python3
#
# virtio-balloon tests
#
# This work is licensed under the terms of the GNU GPL, version 2 or
# later.  See the COPYING file in the top-level directory.

import time

from qemu_test import QemuSystemTest, Asset
from qemu_test import wait_for_console_pattern
from qemu_test import exec_command_and_wait_for_pattern

UNSET_STATS_VALUE = 18446744073709551615


class VirtioBalloonx86(QemuSystemTest):

    ASSET_KERNEL = Asset(
        ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
         '/31/Server/x86_64/os/images/pxeboot/vmlinuz'),
        'd4738d03dbbe083ca610d0821d0a8f1488bebbdccef54ce33e3adb35fda00129')

    ASSET_INITRD = Asset(
        ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
         '/31/Server/x86_64/os/images/pxeboot/initrd.img'),
        '277cd6c7adf77c7e63d73bbb2cded8ef9e2d3a2f100000e92ff1f8396513cd8b')

    ASSET_DISKIMAGE = Asset(
        ('https://archives.fedoraproject.org/pub/archive/fedora/linux/releases'
         '/31/Cloud/x86_64/images/Fedora-Cloud-Base-31-1.9.x86_64.qcow2'),
        'e3c1b309d9203604922d6e255c2c5d098a309c2d46215d8fc026954f3c5c27a0')

    DEFAULT_KERNEL_PARAMS = ('root=/dev/vda1 console=ttyS0 net.ifnames=0 '
                             'rd.rescue quiet')

    def wait_for_console_pattern(self, success_message, vm=None):
        wait_for_console_pattern(
            self,
            success_message,
            failure_message="Kernel panic - not syncing",
            vm=vm,
        )

    def mount_root(self):
        self.wait_for_console_pattern('Entering emergency mode.')
        prompt = '# '
        self.wait_for_console_pattern(prompt)

        # Synchronize on virtio-block driver creating the root device
        exec_command_and_wait_for_pattern(self,
                        "while ! (dmesg -c | grep vda:) ; do sleep 1 ; done",
                        "vda1")

        exec_command_and_wait_for_pattern(self, 'mount /dev/vda1 /sysroot',
                                          prompt)
        exec_command_and_wait_for_pattern(self, 'chroot /sysroot',
                                          prompt)
        exec_command_and_wait_for_pattern(self, "modprobe virtio-balloon",
                                          prompt)

    def assert_initial_stats(self):
        ret = self.vm.qmp('qom-get',
                          {'path': '/machine/peripheral/balloon',
                           'property': 'guest-stats'})['return']
        when = ret.get('last-update')
        assert when == 0
        stats = ret.get('stats')
        for _name, val in stats.items():
            assert val == UNSET_STATS_VALUE

    def assert_running_stats(self, then):
        # We told the QEMU to refresh stats every 100ms, but
        # there can be a delay between virtio-ballon driver
        # being modprobed and seeing the first stats refresh
        # Retry a few times for robustness under heavy load
        retries = 10
        when = 0
        while when == 0 and retries:
            ret = self.vm.qmp('qom-get',
                              {'path': '/machine/peripheral/balloon',
                               'property': 'guest-stats'})['return']
            when = ret.get('last-update')
            if when == 0:
                retries = retries - 1
                time.sleep(0.5)

        now = time.time()

        assert now > when > then
        stats = ret.get('stats')
        # Stat we expect this particular Kernel to have set
        expect_data = [
            "stat-available-memory",
            "stat-disk-caches",
            "stat-free-memory",
            "stat-htlb-pgalloc",
            "stat-htlb-pgfail",
            "stat-major-faults",
            "stat-minor-faults",
            "stat-swap-in",
            "stat-swap-out",
            "stat-total-memory",
        ]
        for name, val in stats.items():
            if name in expect_data:
                assert val != UNSET_STATS_VALUE
            else:
                assert val == UNSET_STATS_VALUE

    def test_virtio_balloon_stats(self):
        self.set_machine('q35')
        self.require_accelerator("kvm")
        kernel_path = self.ASSET_KERNEL.fetch()
        initrd_path = self.ASSET_INITRD.fetch()
        diskimage_path = self.ASSET_DISKIMAGE.fetch()

        self.vm.set_console()
        self.vm.add_args("-S")
        self.vm.add_args("-cpu", "max")
        self.vm.add_args("-m", "2G")
        # Slow down BIOS phase with boot menu, so that after a system
        # reset, we can reliably catch the clean stats again in BIOS
        # phase before the guest OS launches
        self.vm.add_args("-boot", "menu=on")
        self.vm.add_args("-accel", "kvm")
        self.vm.add_args("-device", "virtio-balloon,id=balloon")
        self.vm.add_args('-drive',
                         f'file={diskimage_path},if=none,id=drv0,snapshot=on')
        self.vm.add_args('-device', 'virtio-blk-pci,bus=pcie.0,' +
                         'drive=drv0,id=virtio-disk0,bootindex=1')

        self.vm.add_args(
            "-kernel",
            kernel_path,
            "-initrd",
            initrd_path,
            "-append",
            self.DEFAULT_KERNEL_PARAMS
        )
        self.vm.launch()

        # Poll stats at 100ms
        self.vm.qmp('qom-set',
                    {'path': '/machine/peripheral/balloon',
                     'property': 'guest-stats-polling-interval',
                     'value': 100 })

        # We've not run any guest code yet, neither BIOS or guest,
        # so stats should be all default values
        self.assert_initial_stats()

        self.vm.qmp('cont')

        then = time.time()
        self.mount_root()
        self.assert_running_stats(then)

        # Race window between these two commands, where we
        # rely on '-boot menu=on' to (hopefully) ensure we're
        # still executing the BIOS when QEMU processes the
        # 'stop', and thus have not loaded the virtio-balloon
        # driver in the guest
        self.vm.qmp('system_reset')
        self.vm.qmp('stop')

        # If the above assumption held, we're in BIOS now and
        # stats should be all back at their default values
        self.assert_initial_stats()
        self.vm.qmp('cont')

        then = time.time()
        self.mount_root()
        self.assert_running_stats(then)


if __name__ == '__main__':
    QemuSystemTest.main()
