#!/usr/bin/python2
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2017 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 parent
from testlib import *
import json

# The oVirt image has following content
# DC: "Default"
# Cluster: "Default"
# Host: "host" running in the image with plaintext communication to engine
# VM: "VM" Blank-based VM with network boot
#

OVIRT_ADDR = "10.111.113.5" # from test machine (self.machine)
ENGINE_FQDN = OVIRT_ADDR
ENGINE_PORT = 443
VIRSH_CONNECTION_URI = "qemu+tcp://10.111.113.5/system"
OVIRT_USER = "admin@internal"
OVIRT_PWD = "engine"
CA_PEM_FILE = '/etc/pki/vdsm/certs/cacert.pem'
VDSM_CONF_FILE = '/etc/vdsm/vdsm.conf'

WAIT_SCRIPT = """
set -ex
for x in $(seq 1 60); do
    if [ "$(%(api)s https://%(addr)s/ovirt-engine/api | xmllint --xpath "string(/api/product_info/name)" -)" = "oVirt Engine" ]; then
        break
    else
        sleep 10
    fi
done
"""

TEST_VDSM_CONF_CONTENT = "Dummy content of the vdsm.conf file"

@skipImage("No oVirt release", "continuous-atomic", "debian-stable", "debian-testing", "fedora-i386", "fedora-atomic", "rhel-atomic", "fedora-testing", "ubuntu-1604", "ubuntu-stable")
class TestOVirtMachines(MachineCase):
    provision = {
        "0": { "address": "10.111.113.1/20" },
        "ovirt": { "image": "ovirt", "memory_mb": 4096, "forward": { "443": 30000 }} # oVirt API listens on 10.111.113.5:443
    }

    access_token = "unknown"

    def setUp(self):
        MachineCase.setUp(self)
        self.ovirt = self.machines['ovirt']
        m = self.machine

        m.execute("hostname ovirt.cockpit.lan") # used as fallback when resolving based on browser's URL is not matching, see ovirt/helpers.es6:getHost()

        self.engine_url = "https://{0}/ovirt-engine/".format(self.ovirt.forward['443']) # from phantomJS
        print(("Engine URL: {0}".format(self.engine_url)))

        # Wait for the web service to be accessible
        args = { "api": "curl -s -k -u admin@internal:engine -H Content-type:application/xml", "addr": OVIRT_ADDR }
        m.execute(script=WAIT_SCRIPT % args)

        # Run the prepared VM
        vmid = m.execute("%(api)s https://%(addr)s/ovirt-engine/api/vms/ | xmllint --xpath \"string(/vms/vm[name=\'VM\']/@id)\" -" % args)
        self.assertNotEqual(vmid, "")
        m.execute("%(api)s -X POST -d \"<action/>\"  https://%(addr)s/ovirt-engine/api/vms/" % args + vmid + "/start")

        self.enforce_cockpit_ovirt()
        self.hack_for_missing_vdsm()
        self.pass_install_dialog()

    def wait_for_dc(self):
        # wait for DC to get up
        m = self.machine
        curl = "curl -s -k -u {0}:{1} -H Content-type:application/xml".format(OVIRT_USER, OVIRT_PWD)
        cmd = "{0} https://{1}/ovirt-engine/api/datacenters?search=name%3DDefault | xmllint --xpath \"string(/data_centers/data_center/@id)\" -".format(curl, OVIRT_ADDR)
        dc_id = m.execute(cmd)

        cmd = "{0} https://{1}/ovirt-engine/api/datacenters/{2} | xmllint --xpath \"string(/data_center/status)\" -".format(curl, OVIRT_ADDR, dc_id)
        status = m.execute(cmd)
        if status != "up":
            print(("Data Center status: {0}, waiting for automatic reactivation".format(status)))
            wait(lambda: "up" in m.execute(cmd), delay=10)

    def enforce_cockpit_ovirt(self):
        # enforce use of cockpit-ovirt instead of cockpit-machines
        m = self.machine
        m.execute("sed -i 's/\"priority\".*$/\"priority\": 0,/' {0}".format("/usr/share/cockpit/machines/manifest.json"))
        m.execute("sed -i 's/\"priority\".*$/\"priority\": 100,/' {0}".format("/usr/share/cockpit/ovirt/manifest.json"))

    def pass_install_dialog(self):
        # Mimic installation dialog rendered when the application is used for the first time.
        # In case of passing this initialization step via UI, a series of redirects (oVirt verification and authentication) would follow.
        # The installation dialog calls 'ovirt/install.sh' script, so this simplification is acceptable for the test.
        m = self.machine

        m.execute("bash /usr/share/cockpit/ovirt/install.sh {0} {1} {2}".format(ENGINE_FQDN, ENGINE_PORT, VIRSH_CONNECTION_URI))
        # m.execute("sed -i 's/false/true/' /etc/cockpit/machines-ovirt.config") # turn on debug messages

        self.access_token = self.get_ovirt_token()

    def get_ovirt_token(self):
        m = self.machine
        response = m.execute("curl --insecure -H 'Accept: application/json' --data 'grant_type=password&scope=ovirt-app-api&username={0}&password={1}' https://{2}:{3}/ovirt-engine/sso/oauth/token".format(OVIRT_USER, OVIRT_PWD, ENGINE_FQDN, ENGINE_PORT))
        print(response)
        data = json.loads(response)
        token = data['access_token']
        print(("oVirt token retrieved: {0}".format(token)))
        return token

    def hack_for_missing_vdsm(self):
        # Cockpit runs on self.machine, VDSM runs on self.ovirt
        # and 'virsh' calls are redirected via VIRSH_CONNECTION_URI.
        # For this reason, pkg/ovirt incorrectly modifies vdsm.conf on self.machine and not on self.ovirt.
        # This is fine for production (does not happen there), but the test is adjusted accordingly
        self.machine.execute("mkdir -p /etc/vdsm")
        self.machine.execute("echo 'original content before test modification' >> {0} && chmod a+w {0} && chmod a+w /etc/vdsm".format(VDSM_CONF_FILE))

        # engine's cacert.pem would be normally present on a valid oVirt host
        self.machine.execute("mkdir -p /etc/pki/vdsm/certs")
        self.machine.execute("curl --insecure 'https://{0}/ovirt-engine/services/pki-resource?resource=ca-certificate&format=X509-PEM-CA' > {1}".format(OVIRT_ADDR, CA_PEM_FILE))

    def testBasic(self):
        b = self.browser

        # By-passing the ovirt-authentication cycle of redirects by retrieving oVirt SSO token independently
        # and navigating directly to the last step of auth procedure by passing the '#token=' URL param
        self.login_and_go("/machines#token={0}".format(self.access_token))
        b.wait_present(".main-app-div")
        b.wait_in_text("body", "Virtual Machines")

        b.wait_in_text("tbody tr th", "VM")
        b.click("tbody tr th") # click on the row header
        b.wait_present("#vm-VM-state")
        b.wait_in_text("#vm-VM-state", "running")
        b.wait_present("#vm-VM-vcpus")
        b.wait_in_text("#vm-VM-vcpus", "1")

        b.wait_not_present("#vm-VM-delete") # oVirt-managed VM can not be deleted from Cockpit

        b.wait_present("#vm-VM-ovirt-description") # oVirt specific extension
        b.wait_present("#vm-VM-ovirt-suspendbutton") # oVirt specific extension

        # switch to and check the 'oVirt' subtab (data retrieved from oVirt API)
        b.wait_present("#vm-VM-ovirt")
        b.click("#vm-VM-ovirt")
        b.wait_present("#vm-VM-ovirt-template") # wait till templates are retrieved
        b.wait_present("#vm-VM-ovirt-description")
        b.wait_in_text("#vm-VM-ovirt-template", "Blank")
        b.wait_in_text("#vm-VM-ovirt-ostype", "other")
        b.wait_in_text("#vm-VM-ovirt-ha", "disabled")
        b.wait_in_text("#vm-VM-ovirt-stateless", "no")
        b.wait_in_text("#vm-VM-ovirt-optimizedfor", "desktop")

        b.wait_present("#vm-VM-ovirt-migratetobutton") # do not click, so far test environment has single host only
        b.wait_present("#vm-VM-ovirt-migratetoselect")
        b.click("#vm-VM-ovirt-migratetoselect") # expand

        # force shutdown and wait till VM list is empty
        b.click("#vm-VM-off-caret")
        b.wait_visible("#vm-VM-forceOff")
        b.click("#vm-VM-forceOff")
        b.wait_in_text("body", "No VM is running or defined on this host")

        # switch to cluster view and check expected cluster content
        b.click("#ovirt-topnav-clustervms")
        b.wait_in_text("body", "Virtual Machines of Default cluster")
        b.wait_in_text("tr.listing-ct-item > th", "VM") # name
        b.wait_in_text("tr.listing-ct-item > td:nth-child(4)", "Blank") # template
        b.wait_in_text("tr.listing-ct-item > td:nth-child(5)", "1 GiB") # memory
        b.wait_in_text("tr.listing-ct-item > td:nth-child(7)", "other") # OS
        b.wait_in_text("tr.listing-ct-item > td:nth-child(8)", "no") # HA
        with b.wait_timeout(180):
            b.wait_in_text("tr.listing-ct-item > td:nth-child(12)", "shut off") # state

        b.click("button.dropdown-toggle") # caret for 'Run|Run Here'
        b.wait_visible("a:contains('Run Here')")
        b.click("a:contains('Run Here')") # start the VM on this host
        with b.wait_timeout(180):
            b.wait_in_text("tr.listing-ct-item > td:nth-child(10)", "host") # Host of name 'host'

        # switch back to VM list and check
        b.click("#ovirt-topnav-hostvms")
        b.wait_present("tbody tr th")
        b.wait_in_text("tbody tr th", "VM")
        b.click("tbody tr th") # click on the row header
        b.wait_present("#vm-VM-state")
        b.wait_in_text("#vm-VM-state", "running")
        b.wait_present("#vm-VM-vcpus")
        b.wait_in_text("#vm-VM-vcpus", "1")

        # switch to Templates
        b.click("#ovirt-topnav-clustertemplates")
        b.wait_in_text("body", "Templates of Default cluster")
        b.wait_in_text("tr.listing-ct-item > th", "Blank") # name
        b.wait_in_text("tr.listing-ct-item > td:nth-child(4)", "Blank") # Base Template
        b.wait_in_text("tr.listing-ct-item > td:nth-child(5)", "Blank template") # Description
        b.wait_in_text("tr.listing-ct-item > td:nth-child(6)", "1 GiB") # memory
        b.wait_in_text("tr.listing-ct-item > td:nth-child(7)", "1") # vCPUs
        b.wait_in_text("tr.listing-ct-item > td:nth-child(8)", "other") # OS
        b.wait_in_text("tr.listing-ct-item > td:nth-child(9)", "no") # HA
        b.wait_in_text("tr.listing-ct-item > td:nth-child(10)", "no") # stateless

        # create a VM from the single Base template
        b.wait_present("button:contains('Create VM')")
        b.click("button:contains('Create VM')")
        b.wait_present("button:contains('Cancel')") # check Cancel
        b.click("button:contains('Cancel')")
        b.wait_present("button:contains('Create VM')") # try again
        b.click("button:contains('Create VM')")
        b.wait_present("input.form-control")
        b.set_val("input.form-control", "myVMFromBlankTemplate")

        # Workaround to speed-up whole test execution - should be in setUp() but effectively needed up here.
        self.wait_for_dc() # make sure the ovirt's DC is up before new VM is created
        b.click("button:contains('Create')") # create a VM from Blank template

        # check error message by attempt to create a VM with existing name
        b.wait_present("button:contains('Create VM')")
        b.click("button:contains('Create VM')")
        b.wait_present("input.form-control")
        b.set_val("input.form-control", "VM") # the 'VM' machine exists already
        # assuming the DC is still up - otherwise its environment issue
        b.click("button:contains('Create')") # expected to fail here
        b.wait_present("#clustervm-Blank-actionerror")
        b.wait_in_text("#clustervm-Blank-actionerror", "CREATE VM action failed")

        # check the VM created in previous step
        b.click("#ovirt-topnav-clustervms")
        b.wait_in_text("body", "Virtual Machines of Default cluster")
        with b.wait_timeout(180):
            b.wait_present("table > tbody:nth-child(4) > tr.listing-ct-item > th") # at least two VMs are listed
        b.wait_in_text("table > tbody:nth-child(4) > tr.listing-ct-item > th", "myVMFromBlankTemplate") # check 'name' of the second one

        # check error message by attempt to run an unexecutable VM
        b.wait_present("#cluster-myVMFromBlankTemplate-run") # run button
        b.click("#cluster-myVMFromBlankTemplate-run") # expected to fail
        b.wait_present("#clustervm-myVMFromBlankTemplate-actionerror")
        b.wait_in_text("#clustervm-myVMFromBlankTemplate-actionerror", "START action failed")

        # switch to VDSM view
        b.click("#ovirt-topnav-vdsm")
        b.wait_in_text("body", "Edit the vdsm.conf")
        b.wait_present("#vdsmview-data-loaded")
        b.set_val("textarea", TEST_VDSM_CONF_CONTENT)
        b.click("button:contains('Save')")
        b.wait_present("button:contains('OK')")
        b.click("button:contains('OK')")  # Limitation of this test env: In fact self.machine is updated and not the VDSM, see hack_for_missing_vdsm().
        b.wait(lambda: TEST_VDSM_CONF_CONTENT in self.machine.execute("cat {0}".format(VDSM_CONF_FILE)))

        # sit(self.machines)

    def testHostToMaintenance(self):
        b = self.browser

        # By-passing the ovirt-authentication cycle of redirects by retrieving oVirt SSO token independently
        # and navigating directly to the last step of auth procedure by passing the '#token=' URL param
        self.login_and_go("/machines#token={0}".format(self.access_token))
        b.wait_present(".main-app-div")
        b.wait_in_text("body", "Virtual Machines")

        b.wait_in_text("tbody tr th", "VM") # row with a vitual machine
        b.wait_present("#ovirt-host-to-maintenance")
        b.click("#ovirt-host-to-maintenance")

        b.wait_present("#cockpit_modal_dialog")
        b.wait_present("#cockpit_modal_dialog div:contains(Switch Host to Maintenance)")
        b.click("#cockpit_modal_dialog button:contains(OK)")

        # Since the test environment has just a single host, the action can not succeed on oVirt side - there is no
        # remaining host to migrate the VM to or reassign SPM. In addition, the test environment is based on faqemu (fake qemu).
        # Building of full-featured test environment seems to be not worth of the effort recently since we are not
        # testing the oVirt itself but pkg/ovirt and it's connection to oVirt API.
        #
        # For this reason the test checks just for rendered error which is produced by oVirt API to verify proper action
        # calling and processing on pkg/ovirt side.
        b.wait_in_text(".modal-footer", "The following Hosts have running VMs and cannot be switched to maintenance mode: host.Please ensure that the following Clusters have at least one Host in UP state: Default.")
        b.click("#cockpit_modal_dialog button:contains(Cancel)")
        b.wait_not_present("#cockpit_modal_dialog")

        # sit(self.machines)

if __name__ == '__main__':
    test_main()
