#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import os
import sys

# import Cockpit's machinery for test VMs and its browser test API
TEST_DIR = os.path.dirname(__file__)
sys.path.append(os.path.join(TEST_DIR, "common"))
sys.path.append(os.path.join(os.path.dirname(TEST_DIR), "bots/machine"))

from machineslib import VirtualMachinesCase  # noqa
from testlib import nondestructive, wait, test_main  # noqa

# If this test fails to run, the host machine needs:
# echo "options kvm-intel nested=1" > /etc/modprobe.d/kvm-intel.conf
# rmmod kvm-intel && modprobe kvm-intel || true

# virt-install changed the default to "host-passthrough"
# https://github.com/virt-manager/virt-manager/commit/2c477f330244e04614e174f50fbf37260c535705
distrosWithDefaultHostModel = ["rhel-8-7", "centos-8-stream", "debian-stable"]


@nondestructive
class TestMachinesSettings(VirtualMachinesCase):

    def testVCPU(self):
        b = self.browser
        m = self.machine

        self.createVm("subVmTest1")

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        b.click("#vm-subVmTest1-vcpus-count button")  # open VCPU modal detail window

        b.wait_visible("#machines-vcpu-modal-dialog")

        # Test basic vCPU properties
        b.wait_val("#machines-vcpu-count-field", "1")
        b.wait_val("#machines-vcpu-max-field", "1")

        # Set new values
        b.set_input_text("#machines-vcpu-max-field", "4")
        b.set_input_text("#machines-vcpu-count-field", "3")

        # Set new socket value
        b.wait_val("#socketsSelect", "4")
        b.set_val("#socketsSelect", "2")
        b.wait_val("#coresSelect", "1")
        b.wait_val("#threadsSelect", "2")

        # Save
        b.click("#machines-vcpu-modal-dialog-apply")
        b.wait_not_present("#machines-vcpu-modal-dialog")

        # Make sure warning next to vcpus appears
        b.wait_visible("#vcpus-tooltip")

        # Shut off VM for applying changes after save
        self.performAction("subVmTest1", "forceOff")

        # Make sure warning is gone after shut off
        b.wait_not_present("#vcpus-tooltip")

        # Check changes
        b.wait_in_text("#vm-subVmTest1-vcpus-count", "3")

        # Check after boot
        # Run VM
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Check VCPU count
        b.wait_in_text("#vm-subVmTest1-vcpus-count", "3")

        # Open dialog window
        b.click("#vm-subVmTest1-vcpus-count button")
        b.wait_visible(".pf-c-modal-box__body")

        # Check basic values
        b.wait_val("#machines-vcpu-count-field", "3")
        b.wait_val("#machines-vcpu-max-field", "4")

        # Check sockets, cores and threads
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#coresSelect", "1")
        b.wait_val("#threadsSelect", "2")

        b.click("#machines-vcpu-modal-dialog-cancel")
        b.wait_not_present("#machines-vcpu-modal-dialog")

        # Shut off VM
        self.performAction("subVmTest1", "forceOff")

        # Open dialog
        b.click("#vm-subVmTest1-vcpus-count button")

        b.wait_visible(".pf-c-modal-box__body")

        b.set_input_text("#machines-vcpu-count-field", "2")

        # Set new socket value
        b.set_val("#coresSelect", "2")
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#threadsSelect", "1")

        # Save
        b.click("#machines-vcpu-modal-dialog-apply")
        b.wait_not_present("#machines-vcpu-modal-dialog")

        wait(lambda: m.execute(
            "virsh dumpxml subVmTest1 | tee /tmp/subVmTest1.xml | xmllint --xpath '/domain/cpu/topology[@sockets=\"2\"][@threads=\"1\"][@cores=\"2\"]' -"))

        # Run VM - this ensures that the internal state is updated before we move on.
        # We need this here because we can't wait for UI updates after we open the modal dialog.
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Wait for the VCPUs link to get new values before opening the dialog
        b.wait_visible("#vm-subVmTest1-vcpus-count")
        b.wait_in_text("#vm-subVmTest1-vcpus-count", "2")

        # Open dialog
        b.click("#vm-subVmTest1-vcpus-count button")

        b.wait_visible(".pf-c-modal-box__body")

        # Set new socket value
        b.wait_val("#coresSelect", "2")
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#threadsSelect", "1")

        b.wait_in_text("#vm-subVmTest1-vcpus-count", "2")

        # Check value of sockets, threads and cores from VM dumpxml
        m.execute(
            "virsh dumpxml subVmTest1 | xmllint --xpath '/domain/cpu/topology[@sockets=\"2\"][@threads=\"1\"][@cores=\"2\"]' -")

        # non-persistent VM doesn't have configurable vcpu
        m.execute("virsh undefine subVmTest1")
        b.wait_visible("#vm-subVmTest1-vcpus-count button:disabled")

    def testAutostart(self):
        b = self.browser
        m = self.machine

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")

        def checkAutostart(vm_name, running):
            self.createVm(vm_name, running=running)
            self.waitVmRow(vm_name)
            b.wait_in_text(f"#vm-{vm_name}-system-state", "Running" if running else "Shut off")
            self.goToVmPage(vm_name)

            # set checkbox state and check state of checkbox
            b.set_checked(f"#vm-{vm_name}-autostart-switch", True)  # don't know the initial state of checkbox, so set it to checked
            b.wait_visible(f"#vm-{vm_name}-autostart-switch:checked")
            # check virsh state
            autostartState = m.execute("virsh dominfo %s | grep 'Autostart:' | awk '{print $2}'" % vm_name).strip()
            self.assertEqual(autostartState, "enable")

            # change checkbox state and check state of checkbox
            b.click(f"#vm-{vm_name}-autostart-switch")
            b.wait_not_present(f"#vm-{vm_name}-autostart-switch:checked")
            # check virsh state
            autostartState = m.execute("virsh dominfo %s | grep 'Autostart:' | awk '{print $2}'" % vm_name).strip()
            self.assertEqual(autostartState, "disable")

            # change checkbox state and check state of checkbox
            b.click(f"#vm-{vm_name}-autostart-switch")
            b.wait_visible(f"#vm-{vm_name}-autostart-switch:checked")
            # check virsh state
            autostartState = m.execute("virsh dominfo %s | grep 'Autostart:' | awk '{print $2}'" % vm_name).strip()
            self.assertEqual(autostartState, "enable")

            # non-persistent VM doesn't have autostart
            if running:
                m.execute(f"virsh undefine {vm_name}")
                b.wait_not_present(f"#vm-{vm_name}-autostart-switch")

            self.goToMainPage()

        checkAutostart("subVmTest1", True)
        checkAutostart("subVmTest2", False)

    def testCPUModel(self):
        b = self.browser
        m = self.machine

        self.createVm("subVmTest1")

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")

        self.goToVmPage("subVmTest1")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        cpu_model = "host-model"
        if m.image in distrosWithDefaultHostModel:
            cpu_model = "host-passthrough"

        # Copy host CPU configuration
        b.click("#vm-subVmTest1-cpu-model button")
        b.wait_visible("#machines-cpu-type-modal")
        b.select_from_dropdown("#cpu-model-select-group select", cpu_model)
        b.click("#cpu-config-dialog-apply")
        b.wait_not_present("#machines-cpu-type-modal")

        # Warning about about difference in persistent and non-persitent XML should appear
        b.wait_visible("#cpu-tooltip")
        self.performAction("subVmTest1", "forceOff")
        # Warning should disappear and changes should be visible in the UI
        b.wait_not_present("#cpu-tooltip")
        b.wait_in_text("#vm-subVmTest1-cpu-model .pf-c-description-list__text", "host")

        # Choose manually a CPU model
        b.click("#vm-subVmTest1-cpu-model button")
        b.wait_visible("#machines-cpu-type-modal")
        b.select_from_dropdown("#cpu-model-select-group select", "coreduo")
        b.click("#cpu-config-dialog-apply")
        b.wait_not_present("#machines-cpu-type-modal")
        b.wait_in_text("#vm-subVmTest1-cpu-model .pf-c-description-list__text", "custom (coreduo)")

        # Verify libvirt XML
        dom_xml = "virsh dumpxml subVmTest1"
        xmllint_element = f"{dom_xml} | xmllint --xpath 'string(//domain/{{prop}})' - 2>&1 || true"
        self.assertEqual("coreduo", m.execute(xmllint_element.format(prop='cpu/model')).strip())

        # Host-model gets expanded  to custom mode when the VM is running
        b.click("#vm-subVmTest1-cpu-model button")
        b.wait_visible("#machines-cpu-type-modal")
        b.select_from_dropdown("#cpu-model-select-group select", "host-model")
        b.click("#cpu-config-dialog-apply")
        b.wait_not_present("#machines-cpu-type-modal")
        b.wait_in_text("#vm-subVmTest1-cpu-model .pf-c-description-list__text", "host")
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-cpu-model .pf-c-description-list__text", "custom")
        # In the test ENV libvirt does not properly set the CPU model so we see a tooltip https://bugzilla.redhat.com/show_bug.cgi?id=1913337
        # b.wait_not_present("#cpu-tooltip")

    def testBootOrder(self):
        b = self.browser
        m = self.machine

        args = self.createVm("subVmTest1")
        m.execute("touch /var/lib/libvirt/images/phonycdrom; virsh attach-disk subVmTest1 /var/lib/libvirt/images/phonycdrom sdb --type cdrom --config")

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        # Wait for the edit button
        bootOrder = b.text("#vm-subVmTest1-boot-order")

        # Ensure that it's disabled for running VMs
        b.wait_visible("#vm-subVmTest1-boot-order button[aria-disabled=true]")
        self.performAction("subVmTest1", "forceOff")

        # No host device is attached to VM by default, so attach a PCI device to have at least 1 host device in boot order
        # Take a device from the end of the list, since first device might be a root pci device
        pci_output = m.execute("virsh nodedev-list --cap pci").strip().splitlines()[-1]
        m.execute("virsh nodedev-list --cap pci").strip()
        m.execute("virsh nodedev-list --cap pci").strip().splitlines()
        m.execute("virsh nodedev-list --cap pci").strip().splitlines()[-1]
        # Turn 'pci_0123_45_67_8' into '0123:45:67.8'
        split = pci_output[len("pci_"):].split('_')
        pci_slot = f"{split[0]}:{split[1]}:{split[2]}.{split[3]}"
        m.execute(f"virt-xml subVmTest1 --add-device --hostdev {pci_slot}")

        # Older libvirt versions don't fire events for host device attachement so we have to reload the page
        b.reload()
        b.enter_page('/machines')

        # Open dialog
        b.click("#vm-subVmTest1-boot-order button")
        b.wait_visible("#vm-subVmTest1-order-modal-window")
        # Check boot options details:
        # Checking the VM system disk path is the same with virsh command results
        # Checking the VM MAC address is the same with virsh command results
        b.wait_in_text("#vm-subVmTest1-order-modal-device-row-0 .boot-order-additional-info",
                       m.execute("virsh domblklist subVmTest1 | awk 'NR==3{print $2}'").strip())
        b.wait_in_text("#vm-subVmTest1-order-modal-device-row-1 .boot-order-additional-info",
                       m.execute("virsh domiflist subVmTest1 | awk 'NR==3{print $5}'").strip())
        b.wait_visible(f".boot-order-additional-info .pf-c-description-list__description:contains('{pci_slot}')")
        # Check a cdrom attributes are shown correctly
        cdrom_row = b.text(".boot-order-list-view li:nth-child(3) .boot-order-additional-info")
        self.assertTrue("cdrom" and "/var/lib/libvirt/images/phonycdrom" in cdrom_row)
        # Move first device down and check whether succeeded
        row = b.text("#vm-subVmTest1-order-modal-device-row-1 .boot-order-additional-info")
        b.click("#vm-subVmTest1-order-modal-device-row-0 #vm-subVmTest1-order-modal-down")
        b.wait_in_text("#vm-subVmTest1-order-modal-device-row-0 .boot-order-additional-info", row)
        # Save
        b.click("#vm-subVmTest1-order-modal-save")
        b.wait_not_present("#vm-subVmTest1-order-modal-window")

        # Check boot order has changed and no warning is shown
        b.wait_not_in_text("#vm-subVmTest1-boot-order", bootOrder)

        bootOrder = b.text("#vm-subVmTest1-boot-order")

        # Open dialog
        b.click("#vm-subVmTest1-boot-order button")
        b.wait_visible("#vm-subVmTest1-order-modal-window")
        # Unselect second device
        b.set_checked("#vm-subVmTest1-order-modal-device-1-checkbox", False)

        # Save
        b.click("#vm-subVmTest1-order-modal-save")
        b.wait_not_present("#vm-subVmTest1-order-modal-window")

        # Check boot order has changed and no warning is shown
        b.wait_not_in_text("#vm-subVmTest1-boot-order", bootOrder)

        # After unchecking all the boot options, the VM should fall back to using the first hard disk as a boot image
        # Check that UI is able to detect this fallback
        def uncheckTheLastBootOption(firstOptionType="disk"):
            b.click("#vm-subVmTest1-boot-order button")
            b.wait_visible("#vm-subVmTest1-order-modal-window")

            # check boot option order
            # use the first and the third option since network will be the third option if unchecking it
            row0Type = "disk" if firstOptionType == "disk" else "network"
            row2Type = "network" if firstOptionType == "disk" else "disk"
            b.wait_text("#vm-subVmTest1-order-modal-device-row-0 .boot-order-description",
                        row0Type)
            b.wait_text("#vm-subVmTest1-order-modal-device-row-2 .boot-order-description",
                        row2Type)

            b.set_checked("#vm-subVmTest1-order-modal-device-row-0 input", False)
            b.click("#vm-subVmTest1-order-modal-save")
            b.wait_not_present("#vm-subVmTest1-order-modal-window")

            b.wait_text("#vm-subVmTest1-boot-order", "diskedit")

            # re-open and check the boot option is the disk
            b.click("#vm-subVmTest1-boot-order button")
            b.wait_visible("#vm-subVmTest1-order-modal-window")
            b.wait_text("#vm-subVmTest1-order-modal-device-row-0 .pf-c-description-list__description .pf-c-description-list__text", args["image"])

            b.click("#vm-subVmTest1-order-modal-cancel")
            b.wait_not_present("#vm-subVmTest1-order-modal-window")

        # Uncheck the last boot option which is network
        uncheckTheLastBootOption(firstOptionType="network")
        # Uncheck the last boot option which is disk
        uncheckTheLastBootOption()

    def testDomainMemorySettings(self):
        b = self.browser
        m = self.machine

        args = self.createVm("subVmTest1", memory=256)

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        # Wait for the edit link
        b.click("#vm-subVmTest1-memory-count button")

        # Change memory
        b.wait_visible("#vm-subVmTest1-memory-modal-memory")

        b.wait_attr("#vm-subVmTest1-memory-modal-memory-slider  div[role=slider]", "aria-valuemin", "128")
        b.wait_attr("#vm-subVmTest1-memory-modal-max-memory-slider  div[role=slider]", "aria-valuemin", "128")

        current_memory = int(b.attr("#vm-subVmTest1-memory-modal-memory", "value"))
        self.assertEqual(current_memory, 256)
        b.wait_attr("#vm-subVmTest1-memory-modal-max-memory", "disabled", "")

        # Check memory hotunplugging
        # The balloon driver needs to be loaded to descrease memory
        wait(lambda: "login as 'cirros' user" in self.machine.execute(f"cat {args['logfile']}"), delay=3)

        b.set_input_text("#vm-subVmTest1-memory-modal-memory", str(current_memory - 10))
        b.blur("#vm-subVmTest1-memory-modal-memory")
        # Save the memory settings
        b.click("#vm-subVmTest1-memory-modal-save")
        b.wait_not_present("#vm-memory-modal")

        b.wait_in_text("#vm-subVmTest1-memory-count", f"{'{:.1f}'.format(current_memory - 10)} MiB")

        # Shut off domain and check changes are  still there
        self.performAction("subVmTest1", "forceOff")
        b.wait_in_text("#vm-subVmTest1-memory-count", f"{'{:.1f}'.format(current_memory - 10)} MiB")

        # Click for the edit link
        b.click("#vm-subVmTest1-memory-count button")

        # Verify that limiting max memory in offline VMs below memory will decrease memory as well
        b.set_input_text("#vm-subVmTest1-memory-modal-max-memory", str(current_memory - 20))
        b.blur("#vm-subVmTest1-memory-modal-max-memory")
        self.assertEqual(int(b.attr("#vm-subVmTest1-memory-modal-memory", "value")), current_memory - 20)

        # Verify that unit conversions work
        b.select_from_dropdown("#vm-subVmTest1-memory-modal-memory-unit-select", "GiB")
        b.wait_attr("#vm-subVmTest1-memory-modal-memory", "value", "0")

        # Run VM
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        # Non-persistent VM doesn't have configurable memory
        m.execute("virsh undefine subVmTest1")
        b.wait_visible("#vm-subVmTest1-memory-count button:disabled")

    def testMultipleSettings(self):
        b = self.browser
        m = self.machine

        args = self.createVm("subVmTest1")

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        wait(lambda: "login as 'cirros' user" in m.execute(f"cat {args['logfile']}"), delay=3)

        # Change CPU model setting
        b.click("#vm-subVmTest1-cpu-model button")
        b.wait_visible("#machines-cpu-type-modal")
        cpu_model = "host-model"
        if m.image in distrosWithDefaultHostModel:
            cpu_model = "host-passthrough"
        b.select_from_dropdown("#cpu-model-select-group select", cpu_model)
        b.click("#cpu-config-dialog-apply")
        b.wait_not_present("#machines-cpu-type-modal")
        b.wait_visible("#cpu-tooltip")

        # Change vCPUs setting
        b.click("#vm-subVmTest1-vcpus-count button")  # Open dialog
        b.wait_visible("#machines-vcpu-modal-dialog")
        b.set_input_text("#machines-vcpu-max-field", "3")  # Change values
        b.set_input_text("#machines-vcpu-count-field", "3")
        b.click("#machines-vcpu-modal-dialog-apply")  # Save
        b.wait_not_present("#machines-vcpu-modal-dialog")

        # Shut off domain
        self.performAction("subVmTest1", "forceOff")

        dom_xml = "virsh -c qemu:///system dumpxml subVmTest1"

        xmllint_elem = f"{dom_xml} | xmllint --xpath 'string(//domain/cpu/@mode)' - 2>&1 || true"
        wait(lambda: cpu_model in m.execute(xmllint_elem).strip())

        xmllint_elem = f"{dom_xml} | xmllint --xpath 'string(//domain/vcpu)' - 2>&1 || true"
        wait(lambda: "3" in m.execute(xmllint_elem).strip())

        # Check both changes have been applied
        b.wait_in_text("#vm-subVmTest1-cpu-model", "host passthrough" if m.image in distrosWithDefaultHostModel else "host")
        b.wait_in_text("#vm-subVmTest1-vcpus-count", "3")

    def testWatchdog(self):
        b = self.browser
        m = self.machine
        action_strings = {
            "reset": "Reset",
            "shutdown": "Gracefully shutdown",
            "poweroff": "Power off",
            "pause": "Pause",
            "none": "Do nothing",
            "dump": "Dump",
            "inject-nmi": "Inject a non-maskable interrupt",
        }

        def setWatchdogAction(action, machine_has_no_watchdog=False, pixel_test_tag=None, reboot_machine=False):
            # If no watchdog action is set, we are attaching a new watchdog device. If watchdog already is set, we are editing an exiting watchdog device
            if machine_has_no_watchdog:
                b.wait_in_text("#vm-subVmTest1-watchdog-button", "add")
            else:
                b.wait_in_text("#vm-subVmTest1-watchdog-button", "edit")

            b.click("#vm-subVmTest1-watchdog-button")
            b.wait_visible("#vm-subVmTest1-watchdog-modal")

            if pixel_test_tag:
                b.assert_pixels("#vm-subVmTest1-watchdog-modal", pixel_test_tag)

            b.click(f"#{action}")

            if machine_has_no_watchdog:
                b.wait_in_text("#watchdog-dialog-apply", "Add")
            else:
                b.wait_in_text("#watchdog-dialog-apply", "Save")

            b.click("#watchdog-dialog-apply")
            b.wait_not_present("#vm-subVmTest1-watchdog-modal")

            b.wait_in_text("#vm-subVmTest1-watchdog-state", action_strings[action])

            virsh_output = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
            self.assertEqual(virsh_output, f'action="{action}"')

        def setWatchdogActionLive(action, previous_action=None, reboot_machine=False):
            b.click("#vm-subVmTest1-watchdog-button")
            b.wait_visible("#vm-subVmTest1-watchdog-modal")

            b.click(f"#{action}")
            if previous_action:
                # When editing an exiting watchdog, message warning user that changes will take effect after reboot should be present
                b.wait_visible("#vm-subVmTest1-watchdog-modal #vm-subVmTest1-idle-message")
            else:
                # When attaching adding a new watchdog, no message should be present
                b.wait_not_present("#vm-subVmTest1-watchdog-modal #vm-subVmTest1-idle-message")

            b.click("#watchdog-dialog-apply")
            b.wait_not_present("#vm-subVmTest1-watchdog-modal")

            if previous_action:
                # When editing an exiting watchdog, tooltip should be present on overview card warning user that changes will take effect after reboot
                b.wait_visible("#watchdog-tooltip")
            else:
                # When attaching adding a new watchdog, no tooltip should be present
                b.wait_not_present("#watchdog-tooltip")

            # Libvirt doesn't support editing wathdog device on RUNNING vm, so a live VM config will persist the previously configured watchdog action until reboot
            # On the other hand, libvirt does support attaching a watchdog to a RUNNING VM
            # So in summary:
            # - if no previous watchdog action is configured on a VM (meaning VM has no watchdog device), we are attaching a new wathdog device
            #   and live VM config have new action set
            # - if watchdog action is configured on a VM (meaning VM has a watchdog device), we are editing an editing wathdog device
            #   and live VM config will persist the old setting until VM is rebooted
            expected_action = previous_action if previous_action else action
            b.wait_in_text("#vm-subVmTest1-watchdog-state", action_strings[expected_action])
            virsh_output_offline = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
            self.assertEqual(virsh_output_offline, f'action="{expected_action}"')

            # Offline VM config will always have newly configured watchdog action, no matter if we are attaching a new watchdog device or editing an existing one
            virsh_output_offline = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
            self.assertEqual(virsh_output_offline, f'action="{action}"')

            if reboot_machine:
                # Check that after rebooting machine, live VM config will have new watchdog condiguration
                self.performAction("subVmTest1", "forceOff")
                b.click("#vm-subVmTest1-system-run")
                b.wait_in_text("#vm-subVmTest1-system-state", "Running")

                b.wait_in_text("#vm-subVmTest1-watchdog-state", action_strings[action])
                virsh_output_current = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
                self.assertEqual(virsh_output_current, f'action="{action}"')

        def removeWatchdogDevice(live=True):
            b.click("#vm-subVmTest1-watchdog-button")
            b.wait_visible("#vm-subVmTest1-watchdog-modal")

            b.click("#watchdog-dialog-detach")
            b.wait_not_present("#vm-subVmTest1-watchdog-modal")

            b.wait_in_text("#vm-subVmTest1-watchdog-state", "none")
            # check no watchdog is present in VM's xml (also xmllint is expected to return non-zero code)
            virsh_output = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog' - 2>&1 || true").strip()
            self.assertEqual(virsh_output, 'XPath set is empty')

            if live:
                # check no watchdog is present in VM's xml (also xmllint is expected to return non-zero code)
                virsh_output = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/watchdog' - 2>&1 || true").strip()
                self.assertEqual(virsh_output, 'XPath set is empty')

        args = self.createVm("subVmTest1", running=False)

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.goToVmPage("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-watchdog-state", "none")

        # Test configuring watchdog for shutoff VM
        setWatchdogAction(action="reset", machine_has_no_watchdog=True)
        setWatchdogAction(action="poweroff", pixel_test_tag="watchdog")
        setWatchdogAction(action="inject-nmi")
        removeWatchdogDevice()

        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Test configuring watchdog for running VM
        setWatchdogActionLive(action="reset")
        setWatchdogActionLive(action="pause", previous_action="reset")
        # Make sure that the VM booted normally before attempting to hotunplug
        wait(lambda: "login as 'cirros' user" in m.execute(f"cat {args['logfile']}"), delay=3)
        removeWatchdogDevice(live=True)
        # Check rebooting machine will not unexpectedly affect watchdog configuration
        setWatchdogActionLive(action="poweroff", reboot_machine=True)


if __name__ == '__main__':
    test_main()
