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

# This file is part of Cockpit.
#
# Copyright (C) 2016 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 *


@skipImage("kexec-tools not installed", "fedora-atomic", "debian-stable",
           "debian-testing", "ubuntu-1804", "ubuntu-stable")
class TestKdump(MachineCase):

    def rreplace(self, s, old, new, count):
        li = s.rsplit(old, count)
        return new.join(li)

    def enableKdump(self):
        # we need to make sure that the kernel command line options include our crashkernel parameter
        if self.machine.atomic_image:
            # on atomic we have a crashkernel option already, but for auto
            contents = self.machine.execute(command="cat /boot/grub2/grub.cfg", quiet=True)
            self.assertIn("crashkernel=auto", contents)
            self.machine.write("/boot/grub2/grub.cfg", contents.replace("crashkernel=auto", "crashkernel=256M"))
        elif self.machine.image in ["rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1"]:
            # these images use BootLoaderSpec and grubenv
            self.machine.execute(
                "sed -i '/^kernelopts=/ { s/crashkernel=[^ ]*//; s/$/ crashkernel=256M/; }' /boot/grub2/grubenv")
        else:
            lines = self.machine.execute(command="cat /etc/default/grub", quiet=True).split("\n")
            lines = map(lambda line: self.rreplace(line, '"', ' crashkernel=256M"', 1)
                        if line.startswith("GRUB_CMDLINE_LINUX") else line, lines)
            self.machine.write("/etc/default/grub", "\n".join(lines))
            self.machine.execute("grub2-mkconfig -o /boot/grub2/grub.cfg")
        self.machine.execute("mkdir -p /var/crash")

    def enableLocalSsh(self):
        self.machine.execute("[ -f /root/.ssh/id_rsa ] || ssh-keygen -t rsa -N '' -f /root/.ssh/id_rsa")
        self.machine.execute("cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys")
        self.machine.execute("ssh-keyscan -H localhost >> /root/.ssh/known_hosts")

    def rebootMachine(self):
        # Now reboot things
        self.machine.spawn("sync && sync && sync && sleep 0.1 && reboot", "reboot")
        self.machine.wait_reboot()
        self.machine.start_cockpit()
        self.browser.switch_to_top()
        self.browser.relogin("/kdump")

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

        b.wait_timeout(120)
        self.allow_restart_journal_messages()

        if "atomic" not in m.image:
            m.execute("systemctl enable kdump && systemctl start kdump || true")

        self.login_and_go("/kdump")

        b.wait_visible("#app")

        def assertActive(active):
            # changed in PR #11769
            if m.image in ["rhel-8-0-distropkg"]:
                b.wait_in_text("div.btn-onoff-ct label.active", active and "On" or "Off")
            else:
                b.wait_present(".onoff-ct input" + (active and ":checked" or ":not(:checked)"))

        if m.image in ["rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1"]:
            # some OSes have kdump enabled by default (crashkernel=auto)
            b.wait_in_text("#app", "Service is running")
            assertActive(True)
        else:
            # right now we have no memory reserved
            b.mouse("#app span.popover-ct-kdump", "mouseover")
            b.wait_in_text("div.tooltip", "No memory reserved.")
            b.mouse("#app span.popover-ct-kdump", "mouseout")
            # service should indicate an error and the button should be off
            b.wait_in_text("#app", "Service has an error")
            assertActive(False)

        # there shouldn't be any crash reports in the target directory
        self.assertEqual(m.execute("""find "/var/crash" -maxdepth 1 -mindepth 1 -type d -exec echo {} \;"""), "")

        self.enableKdump()
        self.rebootMachine()
        b.wait_visible("#app")
        self.enableLocalSsh()

        if m.image not in ["rhel-8-0-distropkg"]:
            # minimal nfs validation
            settingsLink = "a:contains('locally in /var/crash')"
            b.click(settingsLink)
            b.set_val("#kdump-settings-location", "nfs")
            mountInput = "#kdump-settings-nfs-mount"
            b.set_input_text(mountInput, ":/var/crash")
            b.click("button.btn-primary:contains('Apply')")
            b.wait_present("div.dialog-error:contains('Unable to apply settings')")
            b.set_input_text(mountInput, "localhost:")
            b.click("button.btn-primary:contains('Apply')")
            b.wait_present("div.dialog-error:contains('Unable to apply settings')")
            b.click("button.btn-default:contains('Cancel')")

            # test compression
            b.click(settingsLink)
            b.click("#kdump-settings-compression")
            pathInput = "#kdump-settings-local-directory"
            b.click("button.btn-primary:contains('Apply')")
            b.wait_not_present(pathInput)
            m.execute("cat /etc/kdump.conf | grep -qE 'makedumpfile.*-c.*'")

            # generate a valid kdump config with ssh target
            b.click(settingsLink)
            b.set_val("#kdump-settings-location", "ssh")
            sshInput = "#kdump-settings-ssh-server"
            b.set_input_text(sshInput, "root@localhost")
            sshKeyInput = "#kdump-settings-ssh-key"
            b.set_input_text(sshKeyInput, "/root/.ssh/id_rsa")
            b.set_input_text(pathInput, "/var/crash")
            b.click("button.btn-primary:contains('Apply')")
            b.wait_not_present(pathInput)

        # we should have the amount of memory reserved that we indicated
        b.wait_in_text("#app", "256 MiB")
        # service should start up properly and the button should be on
        b.wait_in_text("#app", "Service is running")
        assertActive(True)
        b.wait_in_text("#app", "Service is running")

        # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1536327
        if m.image in ["rhel-8-0-distropkg"]:
            return

        # try to change the path to a directory that doesn't exist
        customPath = "/var/crash2"
        settingsLink = "a:contains('Remote over SSH')"
        b.click(settingsLink)
        b.set_val("#kdump-settings-location", "local")
        pathInput = "#kdump-settings-local-directory"
        b.set_input_text(pathInput, customPath)
        b.click("button.btn-primary:contains('Apply')")
        # we should get an error
        b.wait_present("div.dialog-error:contains('Unable to apply settings')")
        # also allow the journal message about failed touch
        self.allow_journal_messages(".*mktemp: failed to create file via template.*")
        # create the directory and try again
        m.execute("mkdir -p {0}".format(customPath))
        b.click("button.btn-primary:contains('Apply')")
        b.wait_not_present(pathInput)
        b.wait_present("a:contains('locally in {0}')".format(customPath))

        # service has to restart after changing the config, wait for it to be running
        # otherwise the button to test will be disabled
        b.wait_in_text("#app", "Service is running")
        assertActive(True)

        # crash the kernel and make sure it wrote a report into the right directory
        b.click("button.btn-default")
        # we should get a warning dialog, confirm
        crashButton = "button.btn-danger:contains('Crash system')"
        b.click(crashButton)

        # wait until we've actuall triggered a crash
        b.wait_present("div.spinner")

        # wait for disconnect and then try connecting again
        b.switch_to_top()
        b.wait_in_text("div.curtains-ct h1", "Disconnected")
        m.disconnect()
        m.wait_boot(timeout_sec=300)
        #b.click("#machine-reconnect")
        #b.expect_load()
        #b.wait_visible("#login")
        self.assertNotEqual(
            m.execute("""find "{0}" -maxdepth 1 -mindepth 1 -type d -exec echo {{}} \;""".format(customPath)), "")


if __name__ == '__main__':
    test_main()
