#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2021 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 functools
import subprocess
import time
import xml.etree.ElementTree as ET
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 no_retry_when_changed, nondestructive, test_main, wait, Error  # noqa
from machinesxmls import NETWORK_XML_PXE, PXE_SERVER_CFG  # noqa


@nondestructive
class TestMachinesCreate(VirtualMachinesCase):

    # This test contains basic form validation of the Create VM dialog
    # None of the sub-tests will actually call virt-install
    def testCreateBasicValidation(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

        # Add an extra network interface that should appear in the PXE source dropdown
        iface = "eth42"
        self.add_veth(iface)

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

        # test just the DIALOG CREATION and cancel
        print("    *\n    * validation errors and ui info/warn messages expected:\n    * ")
        runner.cancelDialogTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                            location=config.NOVELL_MOCKUP_ISO_PATH,
                                                            memory_size=128, memory_size_unit='MiB',
                                                            storage_size=12500, storage_size_unit='GiB',
                                                            start_vm=True,
                                                            pixel_test_tag="iso"))

        runner.cancelDialogTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                            location=config.VALID_URL,
                                                            memory_size=256, memory_size_unit='MiB',
                                                            os_name=config.FEDORA_28,
                                                            start_vm=False,
                                                            pixel_test_tag="url"))

        # OS input check
        runner.checkOsInputTest(TestMachinesCreate.VmDialog(self))

        # OS input check when import
        runner.checkOsInputTest(TestMachinesCreate.VmDialog(self, sourceType="disk_image"))

        # Test OS autodetection from URL tree media
        runner.cancelDialogTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                            location=config.TREE_URL,
                                                            memory_size=256, memory_size_unit='MiB',
                                                            storage_pool="NoStorage",
                                                            os_name=config.FEDORA_28,
                                                            start_vm=False,
                                                            pixel_test_tag="auto"))

        # check if older os are filtered
        runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.REDHAT_RHEL_4_7_FILTERED_OS,
                                                               pixel_test_tag="filter"))

        runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.MANDRIVA_2011_FILTERED_OS))

        runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.MAGEIA_3_FILTERED_OS))

        # check that newer oses are present and searchable with substring match
        runner.checkFilteredOsTest(TestMachinesCreate.VmDialog(self, os_name=config.WINDOWS_SERVER_10, os_search_name=config.WINDOWS_SERVER_10_SHORT))

        # try to CREATE WITH DIALOG ERROR
        # name
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, "", storage_size=1), {"vm-name": "Name must not be empty"})

        # location
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                                         location="invalid/url",
                                                                         os_name=config.FEDORA_28), {"source-url": "Source should start with"})

        # memory
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, memory_size=0, os_name=None), {"memory": "Memory must not be 0"})

        # storage
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, storage_size=0), {"storage": "Storage size must not be 0"})

        # Try setting the memory to value bigger than it's available on the OS
        # The dialog should auto-adjust it to match the OS'es total memory
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                                         location=config.NOVELL_MOCKUP_ISO_PATH,
                                                                         memory_size=100000, memory_size_unit='MiB',
                                                                         storage_pool="NoStorage",
                                                                         start_vm=False),
                                             {"memory": "Up to "})

        # start vm
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, storage_size=1,
                                                                         os_name=config.FEDORA_28, start_vm=True),
                                             {"source-file": "Installation source must not be empty"})

        # disallow empty OS
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, sourceType='url', location=config.VALID_URL,
                                                                         storage_size=100, storage_size_unit='MiB',
                                                                         start_vm=False, os_name=None),
                                             {"os-select": "You need to select the most closely matching operating system"})

        # When switching from PXE mode to anything else make sure that the source input is empty
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, storage_size=1,
                                                                         sourceType='pxe',
                                                                         location="type=direct,source={0}".format(iface),
                                                                         sourceTypeSecondChoice='url',
                                                                         start_vm=False),
                                             {"source-url": "Installation source must not be empty"})

    def testCreateCloudBaseImage(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

        b = self.browser
        m = self.machine

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

        # try to CREATE few machines
        # --cloud-init user-data option exists since virt-install >= 3.0.0
        if m.image in ['centos-8-stream', 'rhel-8-4', 'rhel-8-5', 'ubuntu-2004', 'debian-stable']:
            b.click("#create-new-vm")
            b.wait_visible("#create-vm-dialog")
            self.browser.wait_not_present('select option[value=cloud]')
        else:
            runner.createCloudBaseImageTest(TestMachinesCreate.VmDialog(self, sourceType='cloud',
                                                                        storage_size=10, storage_size_unit='MiB',
                                                                        location=config.VALID_DISK_IMAGE_PATH,
                                                                        os_name=config.FEDORA_28,
                                                                        os_short_id=config.FEDORA_28_SHORTID,
                                                                        user_password="catsaremybestfr13nds",
                                                                        user_login="foo",
                                                                        root_password="dogsaremybestfr13nds",
                                                                        start_vm=True))

            # Test using a cloud image without setting the cloud init options
            # https://bugzilla.redhat.com/show_bug.cgi?id=1978206
            runner.createCloudBaseImageTest(TestMachinesCreate.VmDialog(self, sourceType='cloud',
                                                                        storage_size=10, storage_size_unit='MiB',
                                                                        location=config.VALID_DISK_IMAGE_PATH,
                                                                        os_name=config.FEDORA_28,
                                                                        os_short_id=config.FEDORA_28_SHORTID,
                                                                        start_vm=True))

    def testCreateDownloadAnOS(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

        b = self.browser
        m = self.machine

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

        # try to CREATE few machines
        if m.image in ['debian-stable']:
            b.click("#create-new-vm")
            b.wait_visible("#create-vm-dialog")
            b.wait_not_present('select option[value=os]')
        else:
            runner.createDownloadAnOSTest(TestMachinesCreate.VmDialog(self, sourceType='os',
                                                                      expected_memory_size=128,
                                                                      expected_storage_size=128,
                                                                      os_name=config.FEDORA_28,
                                                                      os_short_id=config.FEDORA_28_SHORTID,
                                                                      start_vm=True))

            # user-login option exists since virt-install >= 3.0.0
            if m.image not in ['centos-8-stream', 'rhel-8-4', 'rhel-8-5', 'ubuntu-2004']:
                runner.createDownloadAnOSTest(TestMachinesCreate.VmDialog(self, sourceType='os',
                                                                          is_unattended=True, profile="desktop",
                                                                          user_password="catsaremybestfr13nds",
                                                                          user_login="foo",
                                                                          root_password="dogsaremybestfr13nds",
                                                                          storage_size=246, storage_size_unit='MiB',
                                                                          os_name=config.FEDORA_28,
                                                                          os_short_id=config.FEDORA_28_SHORTID))

                # Don't create root account
                runner.createDownloadAnOSTest(TestMachinesCreate.VmDialog(self, sourceType='os',
                                                                          is_unattended=True, profile="desktop",
                                                                          user_login="foo",
                                                                          user_password="catsaremybestfr13nds",
                                                                          storage_size=256, storage_size_unit='MiB',
                                                                          os_name=config.FEDORA_28,
                                                                          os_short_id=config.FEDORA_28_SHORTID))
            else:
                b.click("#create-new-vm")
                b.wait_visible("#create-vm-dialog")
                b.click("#os-select-group > div button")
                b.click("#os-select li:contains('{0}') button".format(config.FEDORA_28))
                b.click("#unattended-installation")
                b.wait_not_present("#create-vm-dialog-user-password-pw1")
                b.wait_not_present("#user-login")
                b.click(".pf-c-modal-box__footer button:contains(Cancel)")
                b.wait_not_present("#create-vm-dialog")

            # Don't create user account
            runner.createDownloadAnOSTest(TestMachinesCreate.VmDialog(self, sourceType='os',
                                                                      is_unattended=True, profile="jeos",
                                                                      root_password="catsaremybestfr13nds",
                                                                      storage_size=256, storage_size_unit='MiB',
                                                                      os_name=config.FEDORA_28,
                                                                      os_short_id=config.FEDORA_28_SHORTID))

            # name already used from a VM that is currently being created
            # https://bugzilla.redhat.com/show_bug.cgi?id=1780451
            # os option exists only in virt-install >= 2.2.1 which is the reason we have the condition for the OSes list below
            if self.machine.image in ['debian-stable']:
                self.browser.wait_not_present('select option[value=os]')
            else:
                runner.createDownloadAnOSTest(TestMachinesCreate.VmDialog(self, name='existing-name', sourceType='os',
                                                                          expected_memory_size=128,
                                                                          expected_storage_size=128,
                                                                          os_name=config.FEDORA_28,
                                                                          os_short_id=config.FEDORA_28_SHORTID,
                                                                          start_vm=True, delete=False))

                runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, "existing-name", storage_size=1,
                                                                                 check_script_finished=False, env_is_empty=False), {"vm-name": "already exists"})

    def testCreatePXE(self):
        runner = TestMachinesCreate.CreateVmRunner(self)

        self.login_and_go("/machines")

        b = self.browser
        b.wait_in_text("body", "Virtual machines")

        # test PXE Source
        # check that the pxe booting is not available on session connection
        runner.checkPXENotAvailableSessionTest(TestMachinesCreate.VmDialog(self, name='pxe-guest',
                                                                           sourceType='pxe',
                                                                           storage_pool="NoStorage",
                                                                           connection="session"))

        self.machine.execute("virsh net-destroy default && virsh net-undefine default")

        # Set up the PXE server configuration files
        cmds = [
            "mkdir -p /var/lib/libvirt/pxe-config",
            "echo \"{0}\" > /var/lib/libvirt/pxe-config/pxe.cfg".format(PXE_SERVER_CFG),
            "chmod 666 /var/lib/libvirt/pxe-config/pxe.cfg"
        ]
        self.machine.execute(" && ".join(cmds))

        # Define and start a NAT network with tftp server configuration
        cmds = [
            "echo \"{0}\" > /tmp/pxe-nat.xml".format(NETWORK_XML_PXE),
            "virsh net-define /tmp/pxe-nat.xml",
            "virsh net-start pxe-nat"
        ]
        self.machine.execute(" && ".join(cmds))

        # Add an extra network interface that should appear in the PXE source dropdown
        iface = "eth42"
        self.add_veth(iface)

        # We don't handle events for networks yet, so reload the page to refresh the state
        b.reload()
        b.enter_page('/machines')
        b.wait_in_text("body", "Virtual machines")

        # First create the PXE VM but do not start it. We 'll need to tweak a bit the XML
        # to have serial console at bios and also redirect serial console to a file
        runner.createTest(TestMachinesCreate.VmDialog(self, name='pxe-guest', sourceType='pxe',
                                                      location="network=pxe-nat",
                                                      memory_size=256, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      start_vm=True, delete=False))

        # We don't want to use start_vm == False because if we get a separate install phase
        # virt-install will overwrite our changes.

        if self.machine.image in ["debian-stable", "ubuntu-2004", "centos-8-stream", "rhel-8-4", "rhel-8-5"]:
            # https://bugzilla.redhat.com/show_bug.cgi?id=1818089
            # After shutting it off virt-install will restart the domain and exit because of the above bug
            self.machine.execute("virsh destroy pxe-guest")

        # Wait for virt-install to define the VM and then stop it
        wait(lambda: "pxe-guest" in self.machine.execute("virsh list --persistent"), delay=3)
        self.machine.execute("virsh destroy pxe-guest")
        self.goToMainPage()

        # Remove all serial ports and consoles first and tehn add a console of type file
        # virt-xml tool does not allow to remove both serial and console devices at once
        # https://bugzilla.redhat.com/show_bug.cgi?id=1685541
        # So use python xml parsing to change the domain XML.
        domainXML = self.machine.execute("virsh dumpxml pxe-guest")
        root = ET.fromstring(domainXML)

        # Find the parent element of each "console" element, using XPATH
        for p in root.findall('.//console/..'):
            # Find each console element
            for element in p.findall('console'):
                # Remove the console element from its parent element
                p.remove(element)

        # Find the parent element of each "serial" element, using XPATH
        for p in root.findall('.//serial/..'):
            # Find each serial element
            for element in p.findall('serial'):
                # Remove the serial element from its parent element
                p.remove(element)

        # Set useserial attribute for bios os element
        bios = ET.SubElement(root.find('os'), 'bios')
        bios.set('useserial', 'yes')

        # Add a serial console of type file
        console = ET.fromstring(self.machine.execute("virt-xml --build --console file,path=/tmp/serial.txt,target_type=serial"))
        devices = root.find('devices')
        devices.append(console)

        # Redefine the domain with the new XML
        xmlstr = ET.tostring(root, encoding='unicode', method='xml')

        self.machine.execute("echo \'{0}\' > /tmp/domain.xml && virsh define --file /tmp/domain.xml".format(xmlstr))

        self.machine.execute("virsh start pxe-guest")

        # The file is full of ANSI control characters in between every letter, filter them out
        wait(lambda: self.machine.execute(r"sed 's,\x1B\[[0-9;]*[a-zA-Z],,g' /tmp/serial.txt | grep 'Rebooting in 60'"), delay=3)

        self.machine.execute("virsh destroy pxe-guest && virsh undefine pxe-guest")
        runner.checkEnvIsEmpty()

        # Check that host network devices are appearing in the options for PXE boot sources
        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='pxe',
                                                      name="pxe-guest",
                                                      location="type=direct,source={0}".format(iface),
                                                      storage_size=128, storage_size_unit='MiB',
                                                      start_vm=True,
                                                      delete=False))
        self.machine.execute("virsh destroy pxe-guest")

        # Verify that the newly created disk is first in the boot order and the network used for the PXE boot is not on the bootable devices list
        b.wait_in_text("#vm-pxe-guest-boot-order", "disk")
        b.wait_not_in_text("#vm-pxe-guest-boot-order", "network")

    @no_retry_when_changed
    def testCreateThenInstall(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

        dev = self.add_ram_disk(1)
        partition = dev.split('/')[2] + "1"
        cmds = [
            "virsh pool-define-as poolDisk disk - - {0} - {1}".format(dev, os.path.join(self.vm_tmpdir, "poolDiskImages")),
            "virsh pool-build poolDisk --overwrite",
            "virsh pool-start poolDisk",
            "virsh vol-create-as poolDisk {0} 1024".format(partition)
        ]
        self.machine.execute(" && ".join(cmds))

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

        # Test create VM with disk of type "block"
        # Check choosing existing volume as destination storage
        runner.createThenInstallTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                                 location=config.NOVELL_MOCKUP_ISO_PATH,
                                                                 memory_size=256, memory_size_unit='MiB',
                                                                 storage_pool="poolDisk",
                                                                 storage_volume=partition))

        if "debian" not in self.machine.image and "ubuntu" not in self.machine.image:
            target_iqn = "iqn.2019-09.cockpit.lan"
            self.prepareStorageDeviceOnISCSI(target_iqn)
            cmd = [
                "virsh pool-define-as iscsi-pool --type iscsi --target /dev/disk/by-id --source-host 127.0.0.1 --source-dev {0}",
                "ls -la /dev/disk/by-id",
                "virsh pool-start iscsi-pool"
            ]
            print(self.machine.execute("&& ".join(cmd).format(target_iqn)))
            wait(lambda: "unit:0:0:0" in self.machine.execute("virsh pool-refresh iscsi-pool && virsh vol-list iscsi-pool"), delay=3)

            self.addCleanup(self.machine.execute, "virsh pool-destroy iscsi-pool; virsh pool-delete iscsi-pool; virsh pool-undefine iscsi-pool")

            self.browser.reload()
            self.browser.enter_page('/machines')
            self.browser.wait_in_text("body", "Virtual machines")

            # Check choosing existing volume as destination storage
            runner.createThenInstallTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                                     location=config.NOVELL_MOCKUP_ISO_PATH,
                                                                     memory_size=256, memory_size_unit='MiB',
                                                                     storage_pool="iscsi-pool",
                                                                     storage_volume="unit:0:0:0"))

        # Click the install button through the VM details
        runner.createThenInstallTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                                 location=config.NOVELL_MOCKUP_ISO_PATH,
                                                                 memory_size=256, memory_size_unit='MiB',
                                                                 storage_pool="NoStorage"), True)

        # Try performing an installation which will fail and ensure that the 'Install' button is still available
        runner.createThenInstallTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                                 location=config.NOVELL_MOCKUP_ISO_PATH,
                                                                 memory_size=256, memory_size_unit='MiB',
                                                                 storage_pool="NoStorage"), True, True)

    def testCreateFileSource(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

        # Undefine the default storage pool for the first test
        self.machine.execute("virsh pool-destroy default; virsh pool-undefine default")

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

        # Check that when there is no storage pool defined a VM can still be created
        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                      location=config.NOVELL_MOCKUP_ISO_PATH,
                                                      storage_pool="NoStorage",
                                                      start_vm=True))

        self.browser.switch_to_top()
        self.browser.wait_not_visible("#navbar-oops")

        # define again the default storage pool for system connection
        # we need so that the UI will know the remaining available space when we use that pool's path
        cmds = [
            "virsh pool-define-as default --type dir --target /var/lib/libvirt/images",
            "virsh pool-start default"
        ]
        self.machine.execute(" && ".join(cmds))

        self.browser.reload()
        self.browser.enter_page('/machines')
        self.browser.wait_in_text("body", "Virtual machines")

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                      location=config.NOVELL_MOCKUP_ISO_PATH,
                                                      memory_size=256, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      start_vm=False,
                                                      connection='session'))
        cmds = [
            "mkdir -p '/var/lib/libvirt/pools/tmp pool'; chmod a+rwx '/var/lib/libvirt/pools/tmp pool'",
            "virsh pool-define-as 'tmp pool' --type dir --target '/var/lib/libvirt/pools/tmp pool'",
            "virsh pool-start 'tmp pool'",
            "qemu-img create -f qcow2 '/var/lib/libvirt/pools/tmp pool/vmTmpDestination.qcow2' 128M",
            "virsh pool-refresh 'tmp pool'"
        ]
        self.machine.execute(" && ".join(cmds))

        self.browser.reload()
        self.browser.enter_page('/machines')
        self.browser.wait_in_text("body", "Virtual machines")

        # Check choosing existing volume as destination storage
        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                      location=config.NOVELL_MOCKUP_ISO_PATH,
                                                      memory_size=256, memory_size_unit='MiB',
                                                      storage_pool="tmp pool",
                                                      storage_volume="vmTmpDestination.qcow2",
                                                      start_vm=True,))

        # Check "NoStorage" option (only define VM)
        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                      location=config.NOVELL_MOCKUP_ISO_PATH,
                                                      memory_size=256, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      start_vm=True,))

        self.machine.execute("touch '{0}'".format(config.PATH_WITH_SPACE))
        # Check file with empty space
        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='file',
                                                      location=config.PATH_WITH_SPACE,
                                                      memory_size=256, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      start_vm=True,))

    def testCreateImportDisk(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

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

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='disk_image',
                                                      location=config.VALID_DISK_IMAGE_PATH,
                                                      memory_size=256, memory_size_unit='MiB',
                                                      start_vm=False))

        # Recreate the image the previous test just deleted to reuse it
        self.machine.execute("qemu-img create {0} 500M".format(TestMachinesCreate.TestCreateConfig.VALID_DISK_IMAGE_PATH))

        # Unload KVM module, otherwise we get errors getting the nested VMs
        # to start properly.
        # This is applicable for all tests that we want to really successfully run a nested VM.
        # in order to allow the rest of the tests to run faster with QEMU KVM
        # Stop pmcd service if available which is invoking pmdakvm and is keeping KVM module used
        self.machine.execute("(systemctl stop pmcd || true) && modprobe -r kvm_intel && modprobe -r kvm_amd && modprobe -r kvm")

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='disk_image',
                                                      location=config.VALID_DISK_IMAGE_PATH,
                                                      memory_size=256, memory_size_unit='MiB',
                                                      start_vm=True))

    @no_retry_when_changed
    def testCreateUrlSource(self):
        runner = TestMachinesCreate.CreateVmRunner(self)
        config = TestMachinesCreate.TestCreateConfig

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

        runner.checkEnvIsEmpty()

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                      location=config.VALID_URL,
                                                      storage_size=1))

        # name already exists
        runner.createTest(TestMachinesCreate.VmDialog(self, name='existing-name', sourceType='url',
                                                      location=config.VALID_URL, storage_size=1,
                                                      delete=False))

        self.goToMainPage()
        runner.checkDialogFormValidationTest(TestMachinesCreate.VmDialog(self, "existing-name", storage_size=1,
                                                                         env_is_empty=False), {"vm-name": "already exists"})

        self.machine.execute("virsh undefine existing-name")

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                      location=config.VALID_URL,
                                                      memory_size=128, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      os_name=config.FEDORA_28))

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                      location=config.VALID_URL,
                                                      memory_size=128, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      os_name=config.FEDORA_28,
                                                      start_vm=False))

        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                      location=config.VALID_URL,
                                                      memory_size=128, memory_size_unit='MiB',
                                                      storage_size=128, storage_size_unit='MiB',
                                                      start_vm=False))

        # Test detection of ISO file in URL
        runner.createTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                      location=config.ISO_URL,
                                                      memory_size=128, memory_size_unit='MiB',
                                                      storage_pool="NoStorage",
                                                      start_vm=True))

        # This functionality works on debian only because of extra dep.
        # Check error is returned if dependency is missing
        if self.machine.image.startswith("debian"):
            # remove package
            self.machine.execute("dpkg -P qemu-block-extra")
            runner.checkDialogErrorTest(TestMachinesCreate.VmDialog(self, sourceType='url',
                                                                    location=config.ISO_URL,
                                                                    memory_size=256, memory_size_unit='MiB',
                                                                    storage_pool="NoStorage",
                                                                    start_vm=True), ["qemu", "protocol"])

    def testDisabledCreate(self):
        self.login_and_go("/machines")
        self.browser.wait_in_text("body", "Virtual machines")
        self.browser.wait_visible("#create-new-vm:not(:disabled)")
        self.browser.wait_attr("#create-new-vm", "testdata", None)

        virt_install_bin = self.machine.execute("which virt-install").strip()
        self.machine.execute('mount -o bind /dev/null {0}'.format(virt_install_bin))
        self.addCleanup(self.machine.execute, "umount {0}".format(virt_install_bin))

        self.browser.reload()
        self.browser.enter_page('/machines')
        self.browser.wait_visible("#create-new-vm:disabled")
        # There are many reasons why the button would be disabled, so check if it's correct one
        self.browser.wait_attr("#create-new-vm", "testdata", "disabledVirtInstall")

    class TestCreateConfig:
        VALID_URL = 'http://mirror.i3d.net/pub/centos/7/os/x86_64/'
        VALID_DISK_IMAGE_PATH = '/var/lib/libvirt/images/example.img'
        NOVELL_MOCKUP_ISO_PATH = '/var/lib/libvirt/novell.iso'
        PATH_WITH_SPACE = '/var/lib/libvirt/novell with spaces.iso'
        NOT_EXISTENT_PATH = '/tmp/not-existent.iso'
        ISO_URL = 'https://archive.fedoraproject.org/pub/archive/fedora/linux/releases/28/Server/x86_64/os/images/boot.iso'
        TREE_URL = 'https://archive.fedoraproject.org/pub/archive/fedora/linux/releases/28/Server/x86_64/os'

        # LINUX can be filtered if 3 years old
        REDHAT_RHEL_4_7_FILTERED_OS = 'Red Hat Enterprise Linux 4.9'

        FEDORA_28 = 'Fedora 28'
        FEDORA_28_SHORTID = 'fedora28'

        MANDRIVA_2011_FILTERED_OS = 'Mandriva Linux 2011'

        MAGEIA_3_FILTERED_OS = 'Mageia 3'

        WINDOWS_SERVER_10 = 'Microsoft Windows 10'
        WINDOWS_SERVER_10_SHORT = 'win'

    class VmDialog:
        vmId = 0

        def __init__(self, test_obj, name=None,
                     sourceType='file', sourceTypeSecondChoice=None, location='',
                     memory_size=256, memory_size_unit='MiB',
                     expected_memory_size=None,
                     storage_size=None, storage_size_unit='GiB',
                     expected_storage_size=None,
                     os_name="Fedora 28",
                     os_search_name=None,
                     os_short_id="fedora28",
                     is_unattended=None,
                     profile=None,
                     root_password=None,
                     user_password=None,
                     user_login=None,
                     storage_pool='NewVolume', storage_volume='',
                     start_vm=False,
                     delete=True,
                     env_is_empty=True,
                     check_script_finished=True,
                     connection=None,
                     pixel_test_tag=None):

            TestMachinesCreate.VmDialog.vmId += 1  # This variable is static - don't use self here

            if name is None:
                self.name = 'subVmTestCreate' + str(TestMachinesCreate.VmDialog.vmId)
            else:
                self.name = name

            self.browser = test_obj.browser
            self.machine = test_obj.machine
            self.assertTrue = test_obj.assertTrue
            self.assertIn = test_obj.assertIn
            self.goToVmPage = test_obj.goToVmPage
            self.goToMainPage = test_obj.goToMainPage

            self.sourceType = sourceType
            self.sourceTypeSecondChoice = sourceTypeSecondChoice
            self.location = location
            self.memory_size = memory_size
            self.memory_size_unit = memory_size_unit
            self.expected_memory_size = expected_memory_size
            self.storage_size = storage_size
            self.storage_size_unit = storage_size_unit
            self.expected_storage_size = expected_storage_size
            self.os_name = os_name
            self.os_search_name = os_search_name
            self.os_short_id = os_short_id
            self.is_unattended = is_unattended
            self.profile = profile
            self.root_password = root_password
            self.user_password = user_password
            self.user_login = user_login
            self.start_vm = start_vm
            self.storage_pool = storage_pool
            self.storage_volume = storage_volume
            self.delete = delete
            self.env_is_empty = env_is_empty
            self.check_script_finished = check_script_finished
            self.connection = connection

            self.pixel_test_tag = pixel_test_tag

        def open(self):
            b = self.browser

            if self.sourceType == 'disk_image':
                b.click("#import-vm-disk")
            else:
                b.click("#create-new-vm")

            b.wait_visible("#create-vm-dialog")
            if self.sourceType == 'disk_image':
                b.wait_in_text(".pf-c-modal-box .pf-c-modal-box__header .pf-c-modal-box__title", "Import a virtual machine")
            else:
                b.wait_in_text(".pf-c-modal-box .pf-c-modal-box__header .pf-c-modal-box__title", "Create new virtual machine")

            if self.os_name is not None:
                # check if there is os present in osinfo-query because it can be filtered out in the UI
                query_result = '{0}'.format(self.os_name)
                # throws exception if grep fails
                self.machine.execute(
                    r"osinfo-query os --fields=name | tail -n +3 | sed -e 's/\s*|\s*/|/g; s/^\s*//g; s/\s*$//g' | grep '{0}'".format(
                        query_result))

            return self

        def checkOsInput(self):
            b = self.browser

            # select "Installation Type" for more complete OS list
            if self.sourceType != "disk_image":
                b.select_from_dropdown("#source-type", "file")
            # input an OS and check there is no Ooops
            b.set_input_text("#os-select-group input", self.os_name)
            b.click("#os-select li button:contains('{}')".format(self.os_name))
            b.wait_attr_contains("#os-select-group input", "value", self.os_name)
            b.wait_not_present("#navbar-oops")

            # re-input an OS which is "Fedora 28"
            # need to click the extend button to show the OS list
            b.click("#os-select-group button[aria-label=\"Options menu\"]")
            b.set_input_text("#os-select-group input", "Fedora")
            b.click("#os-select li button:contains('Fedora 28')")
            b.wait_attr_contains("#os-select-group input", "value", "Fedora 28")
            b.wait_not_present("#navbar-oops")

            # click the 'X' button to clear the OS input and check there is no Ooops
            b.click("#os-select-group button[aria-label=\"Clear all\"]")
            b.wait_attr("#os-select-group input", "value", "")
            b.wait_not_present("#navbar-oops")

            return self

        def checkOsFiltered(self, present=False):
            b = self.browser

            b.focus("#os-select-group input")
            # os_search_name is meant to be used to test substring much
            b.key_press(self.os_search_name or self.os_name)

            if not present:
                try:
                    with b.wait_timeout(5):
                        b.wait_in_text("#os-select li button", "No results found")
                    return self
                except AssertionError:
                    # os found which is not ok
                    raise AssertionError("{0} was not filtered".format(self.os_name))
            else:
                b.wait_visible("#os-select li button:contains({0})".format(self.os_search_name))

        def checkPXENotAvailableSession(self):
            self.browser.set_checked("#connectionName-{0}".format(self.connection), True)
            # Our custom select does not respond on the click function
            self.browser.wait_not_present("#source-type option[value='{0}']".format(self.sourceType))
            return self

        def createAndVerifyVirtInstallArgsCloudInit(self):
            self.browser.click(".pf-c-modal-box__footer button:contains(Create)")
            self.browser.wait_not_present("#create-vm-dialog")

            self.goToVmPage(self.name)
            self.browser.wait_text("#vm-{}-disks-vda-bus".format(self.name), "virtio")
            # A cloud-init NoCloud ISO file is generated, and attached to the VM as a CDROM device.
            self.browser.wait_text("#vm-{}-disks-sda-device".format(self.name), "cdrom")
            self.goToMainPage()

            # usual tricks: prevent grep from seeing itself in the output of ps
            virt_install_cmd = "ps aux | grep '[v]irt-install --connect'"

            wait(lambda: self.machine.execute(virt_install_cmd), delay=3)
            virt_install_cmd_out = self.machine.execute(virt_install_cmd)
            if self.user_login or self.user_password:
                self.assertIn("--cloud-init user-data=", virt_install_cmd_out)

            self.machine.execute(f"virsh destroy {self.name}")
            self.assertIn(f"backing file: {self.location}", self.machine.execute(f"qemu-img info /var/lib/libvirt/images/{self.name}.qcow2"))

        def createAndVerifyVirtInstallArgsUnattended(self):
            self.browser.click(".pf-c-modal-box__footer button:contains(Create)")
            self.browser.wait_not_present("#create-vm-dialog")
            if self.storage_pool != "NoStorage":
                self.goToVmPage(self.name)
                self.browser.wait_text("#vm-{}-disks-vda-bus".format(self.name), "virtio")
                self.goToMainPage()

            # usual tricks: prevent grep from seeing itself in the output of ps
            virt_install_cmd = "ps aux | grep '[v]irt-install --connect'"
            wait(lambda: self.machine.execute(virt_install_cmd), delay=3)
            virt_install_cmd_out = self.machine.execute(virt_install_cmd)
            self.assertIn("--install os={}".format(self.os_short_id), virt_install_cmd_out)
            if self.is_unattended:
                self.assertIn("profile={0}".format(self.profile), virt_install_cmd_out)
                if self.root_password:
                    root_password_file = virt_install_cmd_out.split("admin-password-file=", 1)[1].split(",")[0]
                    self.assertIn(self.machine.execute("cat {0}".format(root_password_file)).rstrip(), self.root_password)
                if self.user_password:
                    user_password_file = virt_install_cmd_out.split("user-password-file=", 1)[1].split(",")[0]
                    self.assertIn(self.machine.execute("cat {0}".format(user_password_file)).rstrip(), self.user_password)
                if self.user_login:
                    user_login = virt_install_cmd_out.split("user-login=", 1)[1].split(",")[0].rstrip()
                    self.assertIn(user_login, self.user_login)

        def fill(self):
            b = self.browser
            b.set_input_text("#vm-name", self.name)

            if self.sourceType != 'disk_image':
                b.select_from_dropdown("#source-type", self.sourceType)
            else:
                b.wait_not_present("#source-type")
            if self.sourceType == 'file' or self.sourceType == 'cloud':
                b.set_file_autocomplete_val("#source-file-group", self.location)
            elif self.sourceType == 'disk_image':
                b.set_file_autocomplete_val("#source-disk-group", self.location)
            elif self.sourceType == 'pxe':
                b.select_from_dropdown("#network-select", self.location)
            elif self.sourceType == 'url':
                b.set_input_text("#source-url", self.location)

            if self.sourceTypeSecondChoice:
                b.select_from_dropdown("#source-type", self.sourceTypeSecondChoice)

            b.wait_attr("#os-select-group", "data-loading", "false")
            # For the mocked fedora URL installation wait to see that the OS section is automatically filled
            if self.sourceType == "url" and self.location == TestMachinesCreate.TestCreateConfig.TREE_URL:
                # libosinfo >= "1.4" is needed for OS autodetection
                # Otherwise we get this error:  Failed to load .treeinfo file: Operation not supported (15)
                if self.machine.image not in ["debian-stable"]:
                    b.wait_attr("#os-select-group input", "value", TestMachinesCreate.TestCreateConfig.FEDORA_28)
                else:
                    b.wait_attr("#os-select-group input", "value", "")
                    b.click("#os-select-group div button")
                    b.click("#os-select li:contains('{0}') button".format(self.os_name))
            elif self.os_name:
                b.click("#os-select-group > div button")
                b.click("#os-select li:contains('{0}') button".format(self.os_name))

            if self.sourceType != 'disk_image':
                if not self.expected_storage_size:
                    b.wait_visible("#storage-pool-select")
                    b.select_from_dropdown("#storage-pool-select", self.storage_pool)

                    if self.storage_pool == 'NewVolume' or self.storage_pool == 'NoStorage':
                        b.wait_not_present("#storage-volume-select")
                    else:
                        b.wait_visible("#storage-volume-select")
                        b.select_from_dropdown("#storage-volume-select", self.storage_volume)

                    if self.storage_pool != 'NewVolume':
                        b.wait_not_present("#storage-size")
                    else:
                        b.select_from_dropdown("#storage-size-unit-select", self.storage_size_unit)
                        if self.storage_size is not None:
                            b.set_input_text("#storage-size", str(self.storage_size), value_check=False)
                else:
                    b.wait_val("#storage-size", self.expected_storage_size)

            # First select the unit so that UI will auto-adjust the memory input
            # value according to the available total memory on the host
            if not self.expected_memory_size:
                b.select_from_dropdown("#memory-size-unit-select", self.memory_size_unit)
                b.set_input_text("#memory-size", str(self.memory_size), value_check=True)
            else:
                b.wait_val("#memory-size", self.expected_memory_size)

            if self.sourceType == "cloud":
                b.wait_not_present("#start-vm")
            else:
                b.wait_visible("#start-vm")
                if not self.start_vm:
                    b.click("#start-vm")  # TODO: fix this, do not assume initial state of the checkbox

            if (self.connection):
                b.set_checked("#connectionName-{0}".format(self.connection), True)

            if self.is_unattended or self.sourceType == "cloud":
                if self.is_unattended:
                    b.click("#unattended-installation")
                else:
                    if self.user_password or self.user_login or self.root_password:
                        b.click("#cloud-init-checkbox")

                if self.profile:
                    b.select_from_dropdown("#profile-select", self.profile)
                if self.user_password:
                    b.set_input_text("#create-vm-dialog-user-password-pw1", self.user_password)
                if self.user_login:
                    b.set_input_text("#user-login", self.user_login)
                if self.root_password:
                    b.set_input_text("#create-vm-dialog-root-password-pw1", self.root_password)

            return self

        def cancel(self, force=False):
            b = self.browser
            if b.is_present("#create-vm-dialog"):
                b.click(".pf-c-modal-box__footer button:contains(Cancel)")
                b.wait_not_present("#create-vm-dialog")
            elif force:
                raise Exception("There is no dialog to cancel")
            return self

        def createAndExpectInlineValidationErrors(self, errors):
            b = self.browser

            if self.sourceType == 'disk_image':
                b.click(".pf-c-modal-box__footer button:contains(Import)")
            else:
                b.click(".pf-c-modal-box__footer button:contains(Create)")

            for error, error_msg in errors.items():
                error_location = ".pf-c-modal-box__body #{0}-group .pf-c-form__helper-text.pf-m-error".format(error)
                b.wait_visible(error_location)
                if (error_msg):
                    b.wait_in_text(error_location, error_msg)

            if self.sourceType == 'disk_image':
                b.wait_visible(".pf-c-modal-box__footer button:contains(Import):disabled")
            else:
                b.wait_visible(".pf-c-modal-box__footer button:contains(Create):disabled")

            return self

        def createAndExpectError(self, errors):
            b = self.browser

            def waitForError(errors, error_location):
                for retry in range(0, 60):
                    error_message = b.text(error_location)
                    if any(error in error_message for error in errors):
                        break
                    time.sleep(5)
                else:
                    raise Error("Retry limit exceeded: None of [%s] is part of the error message '%s'" % (
                        ', '.join(errors), b.text(error_location)))

            b.click(".pf-c-modal-box__footer button:contains(Create)")

            error_location = ".pf-c-modal-box__footer div.pf-c-alert"

            b.wait_visible(".pf-c-modal-box__footer button.pf-m-in-progress")
            b.wait_not_present(".pf-c-modal-box__footer button.pf-m-in-progress")
            try:
                with b.wait_timeout(10):
                    b.wait_visible(error_location)
                    b.wait_in_text("button.alert-link.more-button", "show more")
                    b.click("button.alert-link.more-button")
                    waitForError(errors, error_location)

                # dialog can complete if the error was not returned immediately
            except Error:
                if b.is_present("#create-vm-dialog"):
                    raise
                else:
                    # then error should be shown in the notification area
                    error_location = ".pf-c-alert-group li .pf-c-alert"
                    with b.wait_timeout(20):
                        b.wait_visible(error_location)
                        b.wait_in_text("button.alert-link.more-button", "show more")
                        b.click("button.alert-link.more-button")
                        waitForError(errors, error_location)

            # Close the notification
            b.click(".pf-c-alert-group li .pf-c-alert button.pf-m-plain")
            b.wait_not_present(".pf-c-alert-group li .pf-c-alert")

            return self

        def assert_pixels(self, subtag=None):
            if self.pixel_test_tag:
                tag = self.pixel_test_tag + ("-" + subtag if subtag else "")
                ignore = ["#memory-size-helper"]
                if self.storage_pool != "NoStorage":
                    ignore += ["#storage-size-helper"]
                self.browser.assert_pixels("#create-vm-dialog", tag, ignore=ignore)
            return self

    class CreateVmRunner:

        def __init__(self, test_obj):
            self.browser = test_obj.browser
            self.machine = test_obj.machine
            self.assertTrue = test_obj.assertTrue
            self.test_obj = test_obj

            self.machine.execute("touch {0}".format(TestMachinesCreate.TestCreateConfig.NOVELL_MOCKUP_ISO_PATH))
            self.machine.execute("qemu-img create {0} 500M".format(TestMachinesCreate.TestCreateConfig.VALID_DISK_IMAGE_PATH))
            test_obj.addCleanup(test_obj.machine.execute, "rm -f {0} {1}".format(
                TestMachinesCreate.TestCreateConfig.NOVELL_MOCKUP_ISO_PATH,
                TestMachinesCreate.TestCreateConfig.VALID_DISK_IMAGE_PATH))

            self._fakeOsDBInfo()
            self._fakeFedoraTree()

            # console for try INSTALL
            self.test_obj.allow_journal_messages('.*connection.*')
            self.test_obj.allow_journal_messages('.*Connection.*')
            self.test_obj.allow_journal_messages('.*session closed.*')

            self.test_obj.allow_browser_errors("Failed when connecting: Connection closed")
            self.test_obj.allow_browser_errors("Tried changing state of a disconnected RFB object")

            # Deleting a running guest will disconnect the serial console
            self.test_obj.allow_browser_errors("Disconnection timed out.")
            self.test_obj.allow_journal_messages(".* couldn't shutdown fd: Transport endpoint is not connected")

            # See https://bugzilla.redhat.com/show_bug.cgi?id=1406979, this is a WONTFIX:
            # It suggests configure auditd to dontaudit these messages since selinux can't
            # offer whitelisting this directory for qemu process
            self.test_obj.allow_journal_messages('audit: type=1400 audit(.*): avc:  denied .*for .* comm="qemu-.* dev="proc" .*')

            # define default storage pool for system connection
            # we need so that the UI will know the remaining available space when we use that pool's path
            cmds = [
                "virsh pool-define-as default --type dir --target /var/lib/libvirt/images",
                "virsh pool-start default"
            ]
            self.machine.execute(" && ".join(cmds))

        def _setupMockFileServer(self):
            self.machine.upload(["files/min-openssl-config.cnf", "files/mock-range-server.py"], self.test_obj.vm_tmpdir)
            cmds = [
                # Generate certificate for https server
                "cd {0}".format(self.test_obj.vm_tmpdir),
                "openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -subj '/CN=archive.fedoraproject.org' -nodes -config {0}/min-openssl-config.cnf".format(
                    self.test_obj.vm_tmpdir),
                "cat cert.pem key.pem > server.pem"
            ]

            if self.machine.image.startswith("ubuntu") or self.machine.image.startswith("debian"):
                cmds += [
                    "cp {0}/cert.pem /usr/local/share/ca-certificates/cert.crt".format(self.test_obj.vm_tmpdir),
                    "update-ca-certificates"
                ]
            else:
                cmds += [
                    "cp {0}/cert.pem /etc/pki/ca-trust/source/anchors/cert.pem".format(self.test_obj.vm_tmpdir),
                    "update-ca-trust"
                ]
            self.machine.execute(" && ".join(cmds))

            # Run https server with range option support. QEMU uses range option
            # see: https://lists.gnu.org/archive/html/qemu-devel/2013-06/msg02661.html
            # or
            # https://github.com/qemu/qemu/blob/master/block/curl.c
            #
            # and on certain distribution supports only https (not http)
            # see: block-drv-ro-whitelist option in qemu-kvm.spec for certain distribution
            server = self.machine.spawn("cd /var/lib/libvirt && exec python3 {0}/mock-range-server.py {0}/server.pem".format(self.test_obj.vm_tmpdir), "httpsserver")
            self.test_obj.addCleanup(self.machine.execute, "kill {0}".format(server))

        def _fakeOsDBInfo(self):
            # Fake the osinfo-db data in order that it will allow spawn the installation - of course we don't expect it to succeed -
            # we just need to check that the VM was spawned
            fedora_28_xml = self.machine.execute("cat /usr/share/osinfo/os/fedoraproject.org/fedora-28.xml")
            root = ET.fromstring(fedora_28_xml)
            root.find('os').find('resources').find('minimum').find('ram').text = '134217728'
            root.find('os').find('resources').find('minimum').find('storage').text = '134217728'
            if self.machine.image not in ["debian-stable"]:
                root.find('os').find('eol-date').text = '9999-12-31'
            else:
                eol_date = ET.SubElement(root.find('os'), 'eol-date')
                eol_date.text = '9999-12-31'
            new_fedora_28_xml = ET.tostring(root)
            self.machine.execute("echo \'{0}\' > {1}/fedora-28.xml".format(str(new_fedora_28_xml, 'utf-8'), self.test_obj.vm_tmpdir))
            self.machine.execute("mount -o bind  {0}/fedora-28.xml /usr/share/osinfo/os/fedoraproject.org/fedora-28.xml".format(self.test_obj.vm_tmpdir))
            self.test_obj.addCleanup(self.machine.execute, "umount /usr/share/osinfo/os/fedoraproject.org/fedora-28.xml || true")

        def _fakeFedoraTree(self):
            self._setupMockFileServer()
            distro_tree_path = "/var/lib/libvirt/pub/archive/fedora/linux/releases/28/Server/x86_64/os/"
            self.machine.execute("mkdir -p {0}".format(distro_tree_path))
            self.machine.upload(["files/fakefedoratree/.treeinfo", "files/fakefedoratree/images"], distro_tree_path)
            # borrow the kernel executable for the test server
            self.machine.execute("cp /boot/vmlinuz-{0} {1}images/pxeboot/vmlinuz".format(self.machine.execute("uname -r").rstrip(), distro_tree_path))
            self.machine.execute("chown -R 777 /var/lib/libvirt/pub")
            self.test_obj.restore_file("/etc/hosts")
            self.machine.execute('echo "127.0.0.1 archive.fedoraproject.org" >> /etc/hosts')

        def _assertVmStates(self, name, before, after):
            b = self.browser
            selector = "#vm-{0}-state".format(name)

            b.wait_in_text(selector, before)

            # Make sure that the initial state goes away and then try to check what the new state is
            # because we might end up checking the text for the new state the momment it disappears
            wait(lambda: b.text(selector) != before, tries=30, delay=3)
            b.wait_in_text(selector, after)

        def _create(self, dialog):
            b = self.browser
            if dialog.sourceType == 'disk_image':
                b.click(".pf-c-modal-box__footer button:contains(Import)")
            else:
                b.click(".pf-c-modal-box__footer button:contains(Create)")
            init_state = "Creating VM installation" if dialog.start_vm else "Creating VM"
            second_state = "Running" if dialog.start_vm else "Shut off"

            self._assertVmStates(dialog.name, init_state, second_state)
            b.wait_not_present("#create-vm-dialog")

        def _tryCreate(self, dialog):
            name = dialog.name

            dialog.open() \
                .fill()
            self._create(dialog)

            # successfully created
            self.test_obj.waitVmRow(name, dialog.connection or "system")

            self._assertCorrectConfiguration(dialog)

            return self

        def _tryCreateThenInstall(self, dialog, installFromVmDetails, tryWithFailInstall):
            b = self.browser
            m = self.machine
            dialog.start_vm = False
            name = dialog.name

            dialog.open() \
                .fill()
            self._create(dialog)

            self.test_obj.waitVmRow(name)

            if installFromVmDetails:
                self.test_obj.goToVmPage(name)
                b.click("#vm-{0}-install".format(name))
            else:
                b.click("#vm-{0}-install".format(name))

            if tryWithFailInstall:
                # Deleting the default network will make the installation to fail immediately
                m.execute("virsh net-destroy default")
                b.wait_in_text("#vm-{}-state".format(name), "Shut off")
                b.wait_visible("#vm-{0}-install".format(name))
                b.wait_in_text(".pf-c-alert", "failed to get installed")
            else:
                b.wait_in_text("#vm-{}-state".format(name), "Running")
                self._assertCorrectConfiguration(dialog)

                # Wait for virt-install to define the VM and then stop it - otherwise we get 'domain is ready being removed' error
                wait(lambda: dialog.name in self.machine.execute("virsh list --persistent"), delay=3)

                # unfinished install script runs indefinitelly, so we need to force it off
                self.test_obj.performAction(name, "forceOff", False)
                # https://bugzilla.redhat.com/show_bug.cgi?id=1818089
                # After shutting it off virt-install will restart the domain and exit because of the above bug

            return self

        def _deleteVm(self, dialog):
            b = self.browser

            self.test_obj.performAction(dialog.name, "delete")
            b.wait_visible("#vm-{0}-delete-modal-dialog".format(dialog.name))
            b.click("#vm-{0}-delete-modal-dialog button:contains(Delete)".format(dialog.name))
            b.wait_not_present("#vm-{0}-delete-modal-dialog".format(dialog.name))
            self.test_obj.waitVmRow(dialog.name, "system", False)

            return self

        def _commandNotRunning(self, command):
            try:
                self.machine.execute("pgrep -c {0}".format(command))
                return False
            except subprocess.CalledProcessError as e:
                return hasattr(e, 'returncode') and e.returncode == 1

        def _assertCorrectConfiguration(self, dialog):
            b = self.browser
            name = dialog.name

            if not (b.is_present("#vm-details") and b.is_visible("#vm-details")):
                self.test_obj.goToVmPage(name, dialog.connection or "system")

            vm_state = b.text("#vm-{}-state".format(dialog.name))

            # check bus type
            if dialog.storage_pool != "NoStorage":
                self.browser.wait_in_text("#vm-{}-disks-vda-bus".format(dialog.name), "virtio")
            # check memory
            # adjust to how cockpit_format_bytes() formats sizes > 1024 MiB -- this depends on how much RAM
            # the host has (less or more than 1 GiB), and thus needs to be dynamic
            if dialog.memory_size >= 1024 and dialog.memory_size_unit == "MiB":
                memory_text = "/ " + ("%.1f" % (dialog.memory_size / 1024)).rstrip('.0') + " GiB"
            else:
                memory_text = "/ " + ("%.1f" % dialog.memory_size).rstrip('.0') + " %s" % dialog.memory_size_unit
            b.wait_in_text(".memory-usage-chart .pf-c-progress__status > .pf-c-progress__measure", memory_text)

            # check disks
            # Test disk got imported/created
            if dialog.sourceType == 'disk_image':
                if b.is_present("#vm-{0}-disks-vda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-vda-source-file".format(name), dialog.location)
                elif b.is_present("#vm-{0}-disks-hda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-hda-source-file".format(name), dialog.location)
                else:
                    raise AssertionError("Unknown disk device")
            # New volume was created or existing volume was already chosen as destination
            elif (dialog.storage_size is not None and dialog.storage_size > 0) or dialog.storage_pool not in ["NoStorage", "NewVolume"]:
                if b.is_present("#vm-{0}-disks-vda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-vda-device".format(name), "disk")
                elif b.is_present("#vm-{0}-disks-hda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-hda-device".format(name), "disk")
                elif b.is_present("#vm-{0}-disks-sda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-sda-device".format(name), "disk")
                else:
                    raise AssertionError("Unknown disk device")
            elif (vm_state == "Running" and (((dialog.storage_pool == 'NoStorage' or dialog.storage_size == 0) and dialog.sourceType == 'file') or dialog.sourceType == 'url')):
                if b.is_present("#vm-{0}-disks-sda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-sda-device".format(name), "cdrom")
                elif b.is_present("#vm-{0}-disks-hda-device".format(name)):
                    b.wait_in_text("#vm-{0}-disks-hda-device".format(name), "cdrom")
                else:
                    raise AssertionError("Unknown disk device")
            else:
                b.wait_in_text("#vm-{0}-disks .pf-c-empty-state".format(name), "No disks defined")
                b.click("#vm-{0}-disks-adddisk".format(name))
                b.click("#vm-{0}-disks-adddisk-dialog-cancel".format(name))
            return self

        def _assertScriptFinished(self):
            with self.browser.wait_timeout(20):
                self.browser.wait(functools.partial(self._commandNotRunning, "virt-install"))

            return self

        def _assertDomainDefined(self, name, connection):
            listCmd = ""
            if connection == "session":
                listCmd = "runuser -l admin -c 'virsh -c qemu:///session dumpxml --inactive {0}'".format(name)
            else:
                # When creating VMs from the UI default connection is the system
                # In this case don't use runuser -l admin because we get errors 'authentication unavailable'
                listCmd = "virsh -c qemu:///system dumpxml --inactive {0}".format(name)

            wait(lambda: "6" in self.machine.execute(listCmd + " | grep cockpit_machines | wc -l"))
            # The running XML has always substituted port
            # This checks that the defined domain is using the proper inactive XML
            wait(lambda: self.machine.execute(listCmd + " | grep \"type='vnc' port='-1'\""))

            return self

        def checkEnvIsEmpty(self):
            b = self.browser
            b.wait_in_text("#virtual-machines-listing .pf-c-empty-state", "No VM is running")
            # wait for the vm and disks to be deleted
            self.machine.execute("until test -z $(virsh list --all --name); do sleep 1; done")
            self.machine.execute("until test -z $(ls /home/admin/.local/share/libvirt/images/ 2>/dev/null); do sleep 1; done")

            alerts = self.browser.call_js_func("ph_count", ".pf-c-alert-group li")
            for i in range(alerts):
                b.click(".pf-c-alert-group li:first-of-type .pf-c-alert button.pf-m-plain")

            b.wait_not_present(".pf-c-alert-group li .pf-c-alert")

            return self

        def checkOsInputTest(self, dialog):
            dialog.open().checkOsInput().cancel(True)

            self._assertScriptFinished().checkEnvIsEmpty()

        # Many of testCreate* tests use these helpers - let's keep them here to avoid repetition
        def checkDialogFormValidationTest(self, dialog, errors):
            dialog.open() \
                .fill() \
                .createAndExpectInlineValidationErrors(errors) \
                .cancel(True)
            if dialog.check_script_finished:
                self._assertScriptFinished()
            if dialog.env_is_empty:
                self.checkEnvIsEmpty()

        def createTest(self, dialog):
            self._tryCreate(dialog) \

            # When not booting the actual OS from either existing image
            # configure virt-install to wait for the installation to complete.
            # Thus we should only check that virt-install exited when using existing disk images.
            if dialog.sourceType == 'disk_image':
                self._assertScriptFinished() \
                    ._assertDomainDefined(dialog.name, dialog.connection)

            if dialog.delete:
                self._deleteVm(dialog) \
                    .checkEnvIsEmpty()

        def cancelDialogTest(self, dialog):
            dialog.open() \
                .fill() \
                .assert_pixels() \
                .cancel(True)
            self._assertScriptFinished() \
                .checkEnvIsEmpty()

        def checkFilteredOsTest(self, dialog):
            dialog.open() \
                .checkOsFiltered() \
                .assert_pixels() \
                .cancel(True)
            self._assertScriptFinished() \
                .checkEnvIsEmpty()

        def createCloudBaseImageTest(self, dialog):
            dialog.open() \
                .fill() \
                .createAndVerifyVirtInstallArgsCloudInit()
            if dialog.delete:
                self._deleteVm(dialog) \
                    .checkEnvIsEmpty()

        def createDownloadAnOSTest(self, dialog):
            dialog.open() \
                .fill() \
                .createAndVerifyVirtInstallArgsUnattended()
            if dialog.delete:
                self._deleteVm(dialog) \
                    .checkEnvIsEmpty()

        def checkPXENotAvailableSessionTest(self, dialog):
            dialog.open() \
                .checkPXENotAvailableSession() \
                .cancel(True)
            self._assertScriptFinished() \
                .checkEnvIsEmpty()

        def createThenInstallTest(self, dialog, installFromVmDetails=False, tryWithFailInstall=False):
            self._tryCreateThenInstall(dialog, installFromVmDetails, tryWithFailInstall) \
                ._assertScriptFinished() \
                ._assertDomainDefined(dialog.name, dialog.connection) \
                ._deleteVm(dialog) \
                .checkEnvIsEmpty()

        def checkDialogErrorTest(self, dialog, errors):
            dialog.open() \
                .fill() \
                .createAndExpectError(errors) \
                .cancel(False)
            self._assertScriptFinished() \
                .checkEnvIsEmpty()

    # Check various configuration changes made before VM installation persist after installation
    def testConfigureBeforeInstall(self):
        TestMachinesCreate.CreateVmRunner(self)

        b = self.browser
        m = self.machine

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

        dialog = TestMachinesCreate.VmDialog(self, sourceType='file',
                                             name="VmNotInstalled",
                                             location=TestMachinesCreate.TestCreateConfig.NOVELL_MOCKUP_ISO_PATH,
                                             memory_size=256, memory_size_unit='MiB',
                                             storage_size=256, storage_size_unit='MiB',
                                             start_vm=False)
        dialog.open() \
            .fill() \

        b.click(".pf-c-modal-box__footer button:contains(Create)")
        b.wait_not_present("#create-vm-dialog")

        self.waitVmRow("VmNotInstalled")
        self.goToVmPage("VmNotInstalled")

        # Change autostart
        autostart = not b.get_checked("#vm-VmNotInstalled-autostart-checkbox")
        b.set_checked("#vm-VmNotInstalled-autostart-checkbox", autostart)

        # Change memory settings
        b.click("#vm-VmNotInstalled-memory-count button")
        b.wait_visible("#vm-memory-modal")
        b.set_input_text("#vm-VmNotInstalled-memory-modal-max-memory", "400")
        b.blur("#vm-VmNotInstalled-memory-modal-max-memory")
        b.set_input_text("#vm-VmNotInstalled-memory-modal-memory", "300")
        b.blur("#vm-VmNotInstalled-memory-modal-memory")
        b.click("#vm-VmNotInstalled-memory-modal-save")
        b.wait_not_present(".pf-c-modal-box__body")

        # Change vCPUs setting
        b.click("#vm-VmNotInstalled-vcpus-count button")
        b.wait_visible(".pf-c-modal-box__body")
        b.set_input_text("#machines-vcpu-max-field", "8")
        b.blur("#machines-vcpu-max-field")
        b.set_input_text("#machines-vcpu-count-field", "2")
        b.blur("#machines-vcpu-count-field")
        b.select_from_dropdown("#socketsSelect", "2")
        b.select_from_dropdown("#coresSelect", "2")
        b.select_from_dropdown("#threadsSelect", "2")
        b.click("#machines-vcpu-modal-dialog-apply")
        b.wait_not_present(".pf-c-modal-box__body")

        # Change Boot Order setting
        bootOrder = b.text("#vm-VmNotInstalled-boot-order")
        b.click("#vm-VmNotInstalled-boot-order button")
        b.wait_visible(".pf-c-modal-box__body")
        b.set_checked("#vm-VmNotInstalled-order-modal-device-row-1-checkbox", True)
        b.click("#vm-VmNotInstalled-order-modal-device-row-0 #vm-VmNotInstalled-order-modal-down")
        b.click("#vm-VmNotInstalled-order-modal-save")
        b.wait_not_present(".pf-c-modal-box__body")

        # Attach some interface
        m.execute("virsh attach-interface --persistent VmNotInstalled bridge virbr0")

        # Change the os boot firmware configuration
        supports_firmware_config = m.image not in ['debian-stable']
        if supports_firmware_config:
            b.wait_in_text("#vm-VmNotInstalled-firmware", "BIOS")
            b.click("#vm-VmNotInstalled-firmware")
            b.wait_visible(".pf-c-modal-box__body")
            b.select_from_dropdown(".pf-c-modal-box__body select", "efi")
            b.click("#firmware-dialog-apply")
            b.wait_not_present(".pf-c-modal-box__body")
            b.wait_in_text("#vm-VmNotInstalled-firmware", "UEFI")

            # Temporarily delete the OVMF binary and check the firmware options again
            if "fedora" in m.image or "rhel" in m.image or "centos-8" in m.image:
                ovmf_path = "/usr/share/edk2"
            elif "debian" in m.image or "ubuntu" in m.image:
                ovmf_path = "/usr/share/OVMF"
            else:
                raise AssertionError("Unhandled distro for OVMF path")

            m.execute("mount -t tmpfs tmpfs " + ovmf_path)
            self.addCleanup(m.execute, "umount {0} || true".format(ovmf_path))

            # Reload for the new configuration to be read
            b.reload()
            b.enter_page('/machines')

            # HACK: Capabilities are not updated dynamically
            # https://bugzilla.redhat.com/show_bug.cgi?id=1807198
            def hack_broken_caps():
                if m.image in [
                    "fedora-33", "fedora-34", "fedora-35", "fedora-testing",
                    "centos-8-stream", "rhel-8-4", "rhel-8-5", "rhel-9-0",
                    "ubuntu-2004", "ubuntu-stable", "debian-stable", "debian-testing"
                ]:
                    m.execute("systemctl restart libvirtd")
                    # We don't get events for shut off VMs so reload the page
                    b.reload()
                    b.enter_page('/machines')

            hack_broken_caps()

            b.mouse("#vm-VmNotInstalled-firmware-tooltip", "mouseenter")
            b.wait_in_text(".pf-c-tooltip", "Libvirt did not detect any UEFI/OVMF firmware image installed on the host")
            b.mouse("#vm-VmNotInstalled-firmware-tooltip", "mouseleave")
            b.wait_not_present("#missing-uefi-images")
            m.execute("umount " + ovmf_path)

            hack_broken_caps()
        else:
            b.wait_not_present("#vm-VmNotInstalled-firmware")

        # Install the VM
        b.click("#vm-VmNotInstalled-install")

        # Wait for virt-install to define the VM and then stop it - otherwise we get 'domain is ready being removed' error
        wait(lambda: "VmNotInstalled" in m.execute("virsh list --persistent"), delay=3)
        logfile = "/var/log/libvirt/qemu/VmNotInstalled.log"
        m.execute("> {0}".format(logfile))  # clear logfile
        self.performAction("VmNotInstalled", "forceOff", False)
        if m.image in ["debian-stable", "ubuntu-2004", "centos-8-stream", "rhel-8-4", "rhel-8-5"]:
            # https://bugzilla.redhat.com/show_bug.cgi?id=1818089
            # After shutting it off virt-install will restart the domain and exit because of the above bug
            # We need to wait till it's restarted and shut if off again in order to edit the offline VM configuration
            wait(lambda: "char device" in self.machine.execute("cat {0}".format(logfile)), delay=3)
            self.performAction("VmNotInstalled", "forceOff", False)
        b.wait_in_text("#vm-VmNotInstalled-state", "Shut off")
        wait(lambda: "307200" in m.execute("virsh dominfo VmNotInstalled | grep 'Used memory'"), delay=1)  # Wait until memory parameters get adjusted after shutting the VM

        # Check configuration changes survived installation
        # Check memory settings have persisted
        b.click("#vm-VmNotInstalled-memory-count button")
        b.wait_visible("#vm-VmNotInstalled-memory-modal-memory")
        b.wait_val("#vm-VmNotInstalled-memory-modal-max-memory", "400")
        b.wait_val("#vm-VmNotInstalled-memory-modal-memory", "300")
        b.click("#vm-VmNotInstalled-memory-modal-cancel")
        b.wait_not_present(".pf-c-modal-box__body")

        # Check vCPU settings have persisted
        b.click("#vm-VmNotInstalled-vcpus-count button")
        b.wait_visible(".pf-c-modal-box__body")
        b.wait_val("#machines-vcpu-max-field", "8")
        b.wait_val("#machines-vcpu-count-field", "2")
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#coresSelect", "2")
        b.wait_val("#threadsSelect", "2")
        b.click("#machines-vcpu-modal-dialog-cancel")
        b.wait_not_present(".pf-c-modal-box__body")

        # Check changed boot order have persisted
        b.wait_text_not("#vm-VmNotInstalled-boot-order", bootOrder)

        # Check firmware changes persisted
        if supports_firmware_config:
            b.wait_in_text("#vm-VmNotInstalled-firmware", "UEFI")
        else:
            b.wait_not_present("#vm-VmNotInstalled-firmware")

        # Check autostart have persisted
        self.assertEqual(b.get_checked("#vm-VmNotInstalled-autostart-checkbox"), autostart)

        # Check new vNIC have persisted
        b.wait_visible("#vm-VmNotInstalled-network-1-type")

        self.allow_browser_errors("Failed when connecting: Connection closed")
        self.allow_browser_errors("Tried changing state of a disconnected RFB object")
        self.allow_journal_messages(".* couldn't shutdown fd: Transport endpoint is not connected")


if __name__ == '__main__':
    test_main()
