#!/usr/bin/python3

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

import re
import subprocess
import time

import parent
from testlib import *
from machine_core.constants import TEST_OS_DEFAULT


def break_hostkey(m, address, filename=None):
    if not filename:
        filename = "/etc/ssh/ssh_known_hosts"

    m.execute('touch {}'.format(filename))

    line = "{0} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJqfgO2FPiix1n2sCJCXbaffwog1Vvi3zRdmcAxG//5T".format(address)
    m.execute("echo '{0}'> {1}".format(line, filename))


def fix_hostkey(m, key=None, filename=None):
    if not filename:
        filename = "/etc/ssh/ssh_known_hosts"

    if not key:
        key = ''

    m.execute('touch {}'.format(filename))
    m.execute("echo '{0}' > {1}".format(key, filename))


def break_bridge(m):
    m.execute("ln -snf /bin/false /usr/local/bin/cockpit-bridge")


def fix_bridge(m):
    m.execute("rm /usr/local/bin/cockpit-bridge")


def check_failed_state(b, expected_title):
    b.wait_in_text('#troubleshoot-dialog h4', expected_title)
    b.click("#troubleshoot-dialog .modal-footer button:contains(Close)")
    b.wait_popdown('troubleshoot-dialog')


def start_machine_troubleshoot(b, new=False, known_host=False):
    b.wait_visible("#machine-troubleshoot")
    b.click('#machine-troubleshoot')
    b.wait_popup('troubleshoot-dialog')
    if (new):
        b.click('#troubleshoot-dialog .modal-footer button:contains(Add)')

        if not known_host:
            b.wait_in_text('#troubleshoot-dialog', "Fingerprint")
            b.click('#troubleshoot-dialog .modal-footer button:contains(Connect)')


def fail_login(b):
    b.click('#troubleshoot-dialog .modal-footer button:contains(Log In)')
    b.wait_present('#troubleshoot-dialog .modal-footer button:contains(Log In):not([disabled])')
    b.wait_in_text("#troubleshoot-dialog .dialog-error", "Login failed")


def add_machine(b, address, known_host=False):
    b.switch_to_top()
    b.go("/@%s" % address)
    start_machine_troubleshoot(b, new=True, known_host=known_host)
    b.wait_popdown('troubleshoot-dialog')
    b.enter_page("/system", host=address)


def kill_user_admin(machine):
    machine.execute("loginctl terminate-user admin")


def change_ssh_port(machine, address, port=None, timeout_sec=120):
    try:
        port = int(port)
    except (ValueError, TypeError):
        port = 22

    # Keep in mind that not all operating systems have firewalld
    machine.execute("firewall-cmd --permanent --zone=public --add-port={0}/tcp || true".format(port))
    machine.execute("firewall-cmd --reload || true")
    if machine.image in ["fedora-coreos"]:  # no semanage
        machine.execute("setenforce 0")
    else:
        machine.execute("! selinuxenabled || semanage port -a -t ssh_port_t -p tcp {0}".format(port))
    machine.execute("sed -i 's/.*Port .*/#\\0/' /etc/ssh/sshd_config")
    machine.execute(
        "printf 'ListenAddress 127.27.0.15:22\nListenAddress {0}:{1}\n' >> /etc/ssh/sshd_config".format(address, port))

    # We stop the sshd.socket unit and just go with a regular
    # daemon.  This is more portable and reloading/restarting the
    # socket doesn't seem to work well.
    #
    machine.execute("( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")

    start_time = time.time()
    error = None
    while (time.time() - start_time) < timeout_sec:
        try:
            machine.execute(
                "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o CheckHostIP=no -o PasswordAuthentication=no -p {0} {1} 2>&1 | grep -q 'Permission denied'".format(port, address), quiet=True)
            return
        except Exception as e:
            error = e
        time.sleep(0.5)
    raise error


class TestMultiMachineAdd(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20"},
        "machine2": {"address": "10.111.113.2/20"},
        "machine3": {"address": "10.111.113.3/20"},
    }

    def setUp(self):
        super().setUp()
        self.machine2 = self.machines['machine2']
        self.machine2.execute("hostnamectl set-hostname machine2")
        self.machine3 = self.machines['machine3']
        self.machine3.execute("hostnamectl set-hostname machine3")

        # Disable preloading on all machines ("machine1" is done in testlib.py)
        # Preloading on machines with debug build can overload the browser and cause slowness and browser crashes
        # In these tests we actually switch between machines in quick succession which can make things even worse
        if self.machine.image == TEST_OS_DEFAULT:
            self.machines["machine2"].write("/usr/share/cockpit/packagekit/override.json", '{ "preload": [ ] }')
            self.machines["machine2"].write("/usr/share/cockpit/systemd/override.json", '{ "preload": [ ] }')
            self.machines["machine3"].write("/usr/share/cockpit/packagekit/override.json", '{ "preload": [ ] }')
            self.machines["machine3"].write("/usr/share/cockpit/systemd/override.json", '{ "preload": [ ] }')

    def testBasic(self):
        b = self.browser
        m2 = self.machine2
        m3 = self.machine3
        m3_host = "10.111.113.3:2222"
        change_ssh_port(m3, "10.111.113.3", 2222)

        hostname_selector = "#system_information_hostname_text"

        self.login_and_go(None)
        add_machine(b, "10.111.113.2")
        add_machine(b, m3_host)

        # TODO: The concrete error message when killing the bridge and
        # session is not predictable.  So we just wait for any error
        # to appear without checking the precise text.  We print the
        # error message, to help with issue #606.
        #
        # https://github.com/cockpit-project/cockpit/issues/606

        b.go("/dashboard")
        b.enter_page("/dashboard")
        kill_user_admin(m2)
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.2'] img", 'src', '../shell/images/server-error.png')

        kill_user_admin(m3)
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.3'] img", 'src', '../shell/images/server-error.png')

        # Navigating reconnects
        b.click("#dashboard-hosts a[data-address='10.111.113.2']")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname != "/dashboard"')
        b.enter_page("/system", host="10.111.113.2")
        b.wait_text(hostname_selector, "machine2")
        b.switch_to_top()
        b.go("/dashboard")
        b.enter_page("/dashboard")
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.2'] img", 'src', '../shell/images/server-small.png')

        b.click("#dashboard-hosts a[data-address='10.111.113.3']")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname != "/dashboard"')
        b.enter_page("/system", host=m3_host)
        b.wait_text(hostname_selector, "machine3")
        b.switch_to_top()
        b.go("/dashboard")
        b.enter_page("/dashboard")
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.3'] img", 'src', '../shell/images/server-small.png')

        self.allow_restart_journal_messages()
        self.allow_hostkey_messages()
        # Might happen when killing the bridge.
        self.allow_journal_messages("localhost: dropping message while waiting for child to exit",
                                    "Received message for unknown channel: .*",
                                    '.*: Socket error: disconnected',
                                    ".*: error reading from ssh",
                                    ".*: bridge failed: .*",
                                    ".*: bridge program failed: Child process exited with code .*")

    def testGlobalSSHConfig(self):
        b = self.browser
        m = self.machine
        m3 = self.machine3

        change_ssh_port(m3, "10.111.113.3", 2222)
        m.execute("echo -e 'Host m2\n\tHostName 10.111.113.2\n' >> /etc/ssh/ssh_config")
        m.execute("echo -e 'Host m3\n\tHostName 10.111.113.3\n\tPort 2222\n' >> /etc/ssh/ssh_config")

        self.login_and_go(None)
        add_machine(b, "m2")
        add_machine(b, "m3")

        b.go("/dashboard")
        b.enter_page("/dashboard")
        b.wait_attr("#dashboard-hosts a[data-address='m2'] img", 'src', '../shell/images/server-small.png')
        b.wait_attr("#dashboard-hosts a[data-address='m3'] img", 'src', '../shell/images/server-small.png')


class TestMultiMachine(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20"},
        "machine2": {"address": "10.111.113.2/20"},
        "machine3": {"address": "10.111.113.3/20"},
    }

    def setUp(self):
        super().setUp()
        self.machine.execute("hostnamectl set-hostname machine1")
        self.machine2 = self.machines['machine2']
        self.machine2.execute("hostnamectl set-hostname machine2")
        self.machines['machine3'].execute("hostnamectl set-hostname machine3")
        self.allow_journal_messages("sudo: unable to resolve host machine1: .*")

        # Disable preloading on all machines ("machine1" is done in testlib.py)
        # Preloading on machines with debug build can overload the browser and cause slowness and browser crashes
        # In these tests we actually switch between machines in quick succession which can make things even worse
        if self.machine.image == TEST_OS_DEFAULT:
            self.machines["machine2"].write("/usr/share/cockpit/packagekit/override.json", '{ "preload": [ ] }')
            self.machines["machine2"].write("/usr/share/cockpit/systemd/override.json", '{ "preload": [ ] }')
            self.machines["machine3"].write("/usr/share/cockpit/packagekit/override.json", '{ "preload": [ ] }')
            self.machines["machine3"].write("/usr/share/cockpit/systemd/override.json", '{ "preload": [ ] }')

    def checkDirectLogin(self, root='/'):
        b = self.browser
        m2 = self.machine2
        m = self.machine

        hostname_selector = "#system_information_hostname_text"

        b.switch_to_top()
        b.go("{}/system".format(root))
        b.enter_page("/system")
        b.wait_text(hostname_selector, "machine1")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "{0}system"'.format(root))
        b.click("a[href='/dashboard']")
        b.enter_page("/dashboard")
        b.wait_present("a[data-address='10.111.113.2']")

        # Direct to machine2, new login
        m2.execute("echo admin:alt-password | chpasswd")
        b.switch_to_top()
        b.open("{0}=10.111.113.2".format(root))
        b.wait_visible("#login")
        b.wait_visible("#server-name")
        b.wait_not_visible("#badge")
        b.wait_not_visible("#brand")
        b.wait_in_text("#server-name", "10.111.113.2")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "alt-password")
        b.click('#login-button')
        b.expect_load()
        b.enter_page("/system")
        b.wait_in_text(hostname_selector, "machine2")
        b.switch_to_top()

        b.wait_js_cond('window.location.pathname == "{0}=10.111.113.2/system"'.format(root))
        m2_dashboard_name = "machine2"

        b.click("a[href='/dashboard']")
        b.enter_page("/dashboard")
        b.wait_in_text("a[data-address='localhost']", m2_dashboard_name)
        b.wait_not_present("a[data-address='10.111.113.2']")
        b.logout()

        # Bad host key
        m.execute("echo '10.111.113.2 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgPMmTosSQ4NxMtq+aL2NKLC+W4I9/jbD1e74cnOKTW' > /etc/ssh/ssh_known_hosts")
        b.open("{0}=10.111.113.2".format(root))
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "alt-password")
        b.click('#login-button')

        b.wait_not_visible("#conversation-group")
        b.wait_visible("#password-group")
        b.wait_visible("#user-group")
        b.wait_visible("#option-group")
        b.wait_not_visible("#server-group")
        b.wait_in_text("#login-error-message", "Hostkey does not match")

        # Clear host key.
        m.execute("echo '' > /etc/ssh/ssh_known_hosts")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "alt-password")
        b.click('#login-button')

        b.wait_not_visible("#conversation-group")
        b.wait_visible("#password-group")
        b.wait_visible("#user-group")
        b.wait_visible("#option-group")
        b.wait_not_visible("#server-group")
        b.wait_in_text("#login-error-message", "Host is unknown")

        m.execute("echo '\n[SSH-Login]\nallowUnknown = true' >> /etc/cockpit/cockpit.conf")
        m.restart_cockpit()
        b.open("{0}=10.111.113.2".format(root))
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "alt-password")
        b.click('#login-button')

        b.wait_visible("#conversation-group")
        b.wait_not_visible("#password-group")
        b.wait_not_visible("#user-group")
        b.wait_not_visible("#option-group")
        b.wait_not_visible("#server-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.set_val('#conversation-input', 'foobar')
        b.click('#login-button')

        b.wait_not_visible("#conversation-group")
        b.wait_visible("#password-group")
        b.wait_visible("#user-group")
        b.wait_visible("#option-group")
        b.wait_not_visible("#server-group")
        b.wait_in_text("#login-error-message", "Hostkey is unknown")

        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "alt-password")
        b.click('#login-button')

        b.wait_visible("#conversation-group")
        b.wait_not_visible("#password-group")
        b.wait_not_visible("#user-group")
        b.wait_not_visible("#option-group")
        b.wait_not_visible("#server-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")

        match = re.match(r'(MD5|SHA256) Fingerprint \(ssh-(.*)\):', b.text("#conversation-prompt"))
        assert match
        (hash_fn, algo) = match.groups()
        try:
            line = m2.execute("ssh-keygen -l -E %s -f /etc/ssh/ssh_host_%s_key.pub" %
                              (hash_fn, algo.lower()), quiet=True)
            # SHA256 base64 fingerprints include an "SHA256:" prefix, but MD5 hex FPs don't
            fp = line.split(" ")[1].replace('MD5:', '')
            self.assertEqual(b.val('#conversation-input'), fp)
        except subprocess.CalledProcessError:
            # ssh-keygen doesn't support -E, just make sure we have a fp
            self.assertTrue(b.val('#conversation-input'))

        b.click('#login-button')
        b.expect_load()
        b.enter_page("/system")
        b.wait_in_text(hostname_selector, "machine2")
        b.logout()

        # Check hostkey isn't saved
        self.assertFalse(m.execute("cat /etc/ssh/ssh_known_hosts").strip())

        # Direct to machine2, via form
        b.open("{0}other".format(root))
        b.wait_visible("#login")
        b.wait_visible("#user-group")
        b.wait_visible("#password-group")
        b.wait_not_visible("#server-group")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "bad-password")
        b.click('#login-button')
        b.wait_visible("#login-error-message")

        login_options = '#show-other-login-options'

        # Connect to bad machine
        b.set_val("#login-password-input", "bad-password")
        b.click(login_options)
        b.wait_visible("#server-group")
        b.set_val("#server-field", "bad")
        b.click(login_options)
        b.wait_not_visible("#server-group")
        b.click('#login-button')
        b.wait_visible("#server-group")
        b.wait_in_text("#login-error-message", "Unable to connect")

        # remote login via "Connect to:" login page field; should connect to unknown hosts
        m.execute("sed -i '/allowUnknown/d' /etc/cockpit/cockpit.conf")
        m.restart_cockpit()
        b.open(root)
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")

        # Connect to machine2
        b.click(login_options)
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.set_val("#login-password-input", "alt-password")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#conversation-group")
        b.wait_not_visible("#password-group")
        b.wait_not_visible("#user-group")
        b.wait_not_visible("#option-group")
        b.wait_not_visible("#server-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.click('#login-button')
        b.expect_load()
        b.enter_page("/system")
        b.wait_in_text(hostname_selector, "machine2")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "{0}=10.111.113.2/system"'.format(root))

        b.click("a[href='/dashboard']")
        b.enter_page("/dashboard")
        b.wait_in_text("a[data-address='localhost']", m2_dashboard_name)
        b.wait_not_present("a[data-address='10.111.113.2']")

        # Might happen when we switch away.
        self.allow_hostkey_messages()
        self.allow_journal_messages(".*: Connection reset by peer",
                                    "connection unexpectedly closed by peer")

    def testDirectLogin(self):
        b = self.browser

        self.login_and_go("/dashboard")
        add_machine(b, "10.111.113.2")
        self.checkDirectLogin('/')

    @skipImage("test VM image does not have FIPS support", "debian-stable",
               "debian-testing", "ubuntu-2004", "ubuntu-stable", "fedora-coreos")
    def testFIPS(self):
        b = self.browser

        # enable FIPS: RHEL has and needs a convenience script for that
        # https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening#switching-the-system-to-fips-mode_using-the-system-wide-cryptographic-policies
        self.machine.execute(r'''set -e;
            if type fips-mode-setup >/dev/null 2>&1; then
                fips-mode-setup --enable
            else
                grubby --update-kernel=$(grubby --default-kernel) --args=fips=1
                uuid=$(findmnt -no uuid /boot)
                [ -n "$uuid" ] && grubby --update-kernel=$(grubby --default-kernel) --args=boot=UUID=${uuid}
            fi''')
        self.machine.spawn('sync && sync && sync && sleep 0.1 && reboot', 'reboot')
        self.machine.wait_reboot()
        # ensure it's really enabled
        self.assertEqual(self.machine.execute('cat /proc/sys/crypto/fips_enabled').strip(), "1")

        self.login_and_go("/dashboard")
        add_machine(b, "10.111.113.2")
        self.checkDirectLogin('/')

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

        hostname_selector = "#system_information_hostname_text"

        m.execute('mkdir -p /etc/cockpit/ && echo "[WebService]\nUrlRoot = cockpit-new" > /etc/cockpit/cockpit.conf')
        m.start_cockpit()

        # Make sure normal urls don't work.
        output = m.execute('curl -s -o /dev/null -w "%{http_code}" http://localhost:9090/cockpit/socket')
        self.assertIn('404', output)

        output = m.execute('curl -s -o /dev/null -w "%{http_code}" http://localhost:9090/cockpit/socket')
        self.assertIn('404', output)

        b.open("/cockpit-new/system")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked("#authorized-input", True)
        b.click('#login-button')
        b.expect_load()
        b.enter_page("/system")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "/cockpit-new/system"')

        b.click("a[href='/dashboard']")
        b.enter_page("/dashboard")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "/cockpit-new/dashboard"')
        b.enter_page("/dashboard")
        b.click("a[data-address='localhost']")
        b.enter_page("/system")
        b.switch_to_top()

        # Test 2nd machine
        b.click("a[href='/dashboard']")
        b.enter_page("/dashboard")
        add_machine(b, "10.111.113.2")
        b.enter_page("/system", host="10.111.113.2")
        b.wait_text(hostname_selector, "machine2")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "/cockpit-new/@10.111.113.2/system"')

        # Test subnav
        b.click('a[href="/@10.111.113.2/users"]')
        b.enter_page("/users", host="10.111.113.2")
        b.wait_present("#accounts-list")
        b.click("#accounts-list .cockpit-account:first-child")
        b.wait_text("#account-user-name", "admin")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "/cockpit-new/@10.111.113.2/users"')
        b.wait_js_cond('window.location.hash == "#/admin"')

        self.checkDirectLogin('/cockpit-new/')
        self.allow_hostkey_messages()

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

        m.execute('mkdir -p /etc/cockpit/ && echo "[WebService]\nUrlRoot = cockpit-new" > /etc/cockpit/cockpit.conf')
        m.start_cockpit()

        b.open("/cockpit-new/system?access_token=XXXX")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked("#authorized-input", True)
        b.click('#login-button')
        b.expect_load()
        b.enter_page("/system")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname == "/cockpit-new/system"')

    def testExternalPage(self):
        b = self.browser
        m1 = self.machine
        m2 = self.machine2

        # Modify the terminals to be different on the two machines.
        m1.execute("zcat /usr/share/cockpit/systemd/terminal.html.gz | sed -e 's|</body>|magic-m1-token</body>|' | gzip"
                   " > /tmp/terminal.html.gz && mount -o bind /tmp/terminal.html.gz /usr/share/cockpit/systemd/terminal.html.gz")
        m2.execute("zcat /usr/share/cockpit/systemd/terminal.html.gz | sed -e 's|</body>|magic-m2-token</body>|' | gzip"
                   " > /tmp/terminal.html.gz && mount -o bind /tmp/terminal.html.gz /usr/share/cockpit/systemd/terminal.html.gz")

        self.login_and_go("/dashboard")
        add_machine(b, "10.111.113.2")

        b.leave_page()
        b.go("/@10.111.113.2/system/terminal")
        b.enter_page("/system/terminal", host="10.111.113.2")
        b.wait_in_text("body", "magic-m2-token")

        b.leave_page()
        b.go("/@localhost/system/terminal")
        b.enter_page("/system/terminal")
        b.wait_in_text("body", "magic-m1-token")

        self.allow_hostkey_messages()

    def testFrameNavigation(self):
        b = self.browser
        m2 = self.machine2

        m2_path = "/@10.111.113.2/playground/test"
        dashboard_path = "/dashboard"

        # Add a machine
        self.login_and_go(None)
        add_machine(b, "10.111.113.2")

        b.go(dashboard_path)
        b.enter_page("/dashboard")
        # Check the server image
        b.wait_present("#dashboard-hosts a[data-address='10.111.113.2']")
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.2'] img", 'src', '../shell/images/server-small.png')
        b.switch_to_top()

        # Go to the path, remove the image
        b.go(m2_path)
        b.enter_page("/playground/test", "10.111.113.2")
        b.click("img[src='hammer.gif']")
        b.switch_to_top()

        # kill admin, lock account
        m2.execute('passwd -l admin')
        kill_user_admin(m2)

        b.wait_text(".curtains-ct h1", "Couldn't connect to the machine")
        self.assertEqual(b.text("#machine-reconnect"), "Reconnect")

        b.go(dashboard_path)
        b.enter_page("/dashboard")
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.2'] img", 'src', '../shell/images/server-error.png')
        b.switch_to_top()

        # navigating there again will fail
        b.go(m2_path)
        b.wait_text(".curtains-ct h1", "Couldn't connect to the machine")
        self.assertEqual(b.text("#machine-reconnect"), "Reconnect")
        self.assertTrue(b.is_present("#machine-reconnect:not(.disabled)"))

        # wait for dashboard to load
        b.go(dashboard_path)
        b.enter_page("/dashboard")
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.2'] img", 'src', '../shell/images/server-error.png')
        b.switch_to_top()

        # renable admin
        m2.execute('passwd -u admin')

        # path should reconnect at this point
        b.reload()
        b.go(m2_path)
        b.enter_page("/playground/test", "10.111.113.2", reconnect=True)
        # image is back because it page was reloaded after disconnection
        b.wait_present("img[src='hammer.gif']")
        b.switch_to_top()

        # Dashboard shows it as up
        b.go(dashboard_path)
        b.enter_page("/dashboard")
        b.wait_attr("#dashboard-hosts a[data-address='10.111.113.2'] img", 'src', '../shell/images/server-small.png')
        b.switch_to_top()

        # Bad host also bounces
        b.go("/@10.0.0.0/playground/test")
        b.wait_text(".curtains-ct h1", "Couldn't connect to the machine")
        self.assertEqual(b.text(".curtains-ct p"), "Cannot connect to an unknown machine")

        self.allow_restart_journal_messages()
        self.allow_hostkey_messages()
        # Might happen when killing the bridge.
        self.allow_journal_messages("localhost: dropping message while waiting for child to exit",
                                    "Received message for unknown channel: .*",
                                    '.*: Socket error: disconnected',
                                    ".*: error reading from ssh",
                                    ".*: bridge failed: .*",
                                    ".*: bridge program failed: Child process exited with code .*",
                                    "/playground/test.html: failed to retrieve resource: authentication-failed")

    def testFrameReload(self):
        b = self.browser

        frame = "cockpit1:10.111.113.2/playground/test"
        m2_path = "/@10.111.113.2/playground/test"

        # Add a machine
        self.login_and_go(None)
        add_machine(b, "10.111.113.2")

        b.switch_to_top()
        b.go(m2_path)
        b.enter_page("/playground/test", "10.111.113.2")

        b.wait_text('#file-content', "0")
        b.click("#modify-file")
        b.wait_text('#file-content', "1")

        # load the same page on m1
        b.switch_to_top()
        b.go("/@localhost/playground/test")
        b.enter_page("/playground/test")
        b.wait_text('#file-content', "0")

        # go back to m2 and reload frame.
        b.switch_to_top()
        b.go(m2_path)
        b.enter_page("/playground/test", "10.111.113.2")
        b.wait_text('#file-content', "1")
        b.switch_to_top()

        b.eval_js('ph_set_attr("iframe[name=\'%s\']", "data-ready", null)' % frame)
        b.eval_js('ph_set_attr("iframe[name=\'%s\']", "src", "../playground/test.html?i=1#/")' % frame)
        b.wait_present("iframe.container-frame[name='%s'][data-ready]" % frame)

        b.enter_page("/playground/test", "10.111.113.2")

        b.wait_text('#file-content', "1")

        self.allow_hostkey_messages()

    def testTroubleshooting(self):
        b = self.browser
        m1 = self.machine
        m2 = self.machine2

        # Logging in as root is no longer allowed by default by sshd
        m2.execute("sed -ri 's/#?PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config")
        m2.execute("systemctl restart sshd")

        machine_path = "/@10.111.113.2"

        self.login_and_go(None)

        # Troubleshoot while adding
        b.go(machine_path)

        # Bad hostkey
        break_hostkey(m1, "10.111.113.2")
        start_machine_troubleshoot(b, True, True)
        b.wait_in_text('#troubleshoot-dialog {}'.format(self.default_btn_class), "Close")
        b.wait_in_text('#troubleshoot-dialog h4', "Incorrect Host Key")
        b.wait_in_text("#troubleshoot-dialog", "10.111.113.2")
        b.click("#troubleshoot-dialog {}".format(self.default_btn_class))
        b.wait_popdown('troubleshoot-dialog')
        fix_hostkey(m1)

        # Host key path is correct
        m1.execute("mkdir -p /home/admin/.ssh/")
        break_hostkey(m1, "10.111.113.2", filename="/home/admin/.ssh/known_hosts")

        start_machine_troubleshoot(b, True, True)
        b.wait_in_text('#troubleshoot-dialog {}'.format(self.default_btn_class), "Close")
        b.wait_in_text('#troubleshoot-dialog h4', "Incorrect Host Key")
        b.wait_in_text("#troubleshoot-dialog", "10.111.113.2")
        # only fixed in 141 (PR #6720)
        b.click("#troubleshoot-dialog button")
        b.wait_popdown('troubleshoot-dialog')
        fix_hostkey(m1, filename="/home/admin/.ssh/known_hosts")

        # Bad cockpit
        break_bridge(m2)
        start_machine_troubleshoot(b, True)
        check_failed_state(b, "Cockpit is not installed")
        fix_bridge(m2)

        # Troubleshoot existing
        # Properly add machine
        fix_hostkey(m1)
        add_machine(b, "10.111.113.2")
        b.logout()
        b.wait_visible("#login")

        # Bad cockpit
        break_bridge(m2)
        self.login_and_go(None)
        b.go(machine_path)
        with b.wait_timeout(240):
            start_machine_troubleshoot(b)

        check_failed_state(b, "Cockpit is not installed")
        b.wait_visible("#machine-reconnect")
        fix_bridge(m2)

        # Clear host key
        fix_hostkey(m1)
        b.click("#machine-reconnect")
        start_machine_troubleshoot(b)
        b.wait_in_text('#troubleshoot-dialog h4', "Unknown Host Key")
        b.wait_in_text('#troubleshoot-dialog {}'.format(self.primary_btn_class), "Connect")
        b.click('#troubleshoot-dialog {}'.format(self.primary_btn_class))
        b.wait_popdown('troubleshoot-dialog')

        # Reconnect
        b.wait_not_visible(".curtains-ct")
        b.enter_page('/system', "10.111.113.2")

        b.logout()
        b.wait_visible("#login")

        # Break auth
        m2.execute("echo admin:alt-password | chpasswd")
        self.login_and_go(None)
        b.go(machine_path)

        with b.wait_timeout(120):
            b.wait_visible("#machine-reconnect")
        start_machine_troubleshoot(b)

        b.wait_in_text('#troubleshoot-dialog h4', "Log in to")
        b.wait_present("#troubleshoot-dialog {}:not([disabled])".format(self.primary_btn_class))
        b.wait_visible("#login-diff-password")
        b.wait_not_visible("#login-available")
        self.assertEqual(b.text("#login-type button span"), "Type a password")
        b.click("#login-type button")
        b.click("#login-type li[value=stored] a")
        b.wait_in_text("#login-type button span", "Using available credentials")
        b.wait_not_visible("#login-diff-password")
        b.wait_in_text("#login-available", "Login Password")
        b.wait_not_in_text("#login-available", "Kerberos Ticket")
        b.wait_not_present("#login-available .empty")

        fail_login(b)

        b.click("#login-type button")
        b.click("#login-type li[value=password] a")
        b.wait_in_text("#login-type button span", "Type a password")
        b.wait_visible("#login-diff-password")
        b.wait_not_visible("#login-available")
        self.assertEqual(b.val("#login-custom-password"), "")
        self.assertEqual(b.val("#login-custom-user"), "")
        self.assertEqual(b.attr("#login-custom-user", "placeholder"), "admin")
        b.set_val("#login-custom-user", "admin")
        b.set_val("#login-custom-password", "bad")
        fail_login(b)
        b.set_val("#login-custom-password", "alt-password")
        b.click('#troubleshoot-dialog {}'.format(self.primary_btn_class))
        b.wait_popdown('troubleshoot-dialog')

        # Reconnect
        b.wait_not_visible(".curtains-ct")
        b.enter_page('/system', "admin@10.111.113.2")
        b.logout()
        b.wait_visible("#login")

        if m1.image in ["fedora-coreos"]:  # no semanage
            m1.execute("setenforce 0")
        else:
            m1.execute("! selinuxenabled || semanage port -a -t ssh_port_t -p tcp 2222")

        # Keep in mind that not all operating systems have firewalld
        m2.execute("firewall-cmd --permanent --zone=public --add-port=2222/tcp || true")
        m2.execute("firewall-cmd --reload || true")
        if m2.image in ["fedora-coreos"]:  # no semanage
            m2.execute("setenforce 0")
        else:
            m2.execute("! selinuxenabled || semanage port -a -t ssh_port_t -p tcp 2222")
        m2.execute("sed -i 's/.*Port .*/#\\0/' /etc/ssh/sshd_config")
        m2.execute("printf 'ListenAddress 127.27.0.15:22\nListenAddress 10.111.113.2:2222\n' >> /etc/ssh/sshd_config")

        # We stop the sshd.socket unit and just go with a regular
        # daemon.  This is more portable and reloading/restarting the
        # socket doesn't seem to work well.
        m2.execute("( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
        m2.disconnect()
        m2 = None # No more access to m2

        self.login_and_go(None)
        b.go(machine_path)
        with b.wait_timeout(120):
            b.wait_visible("#machine-reconnect")
        start_machine_troubleshoot(b)
        b.wait_in_text('#troubleshoot-dialog h4', "Could not contact")
        b.set_val("#edit-machine-port", "2222")
        b.click('#troubleshoot-dialog {}'.format(self.primary_btn_class))
        # Using libssh's knownhosts api port is taken into account when verifying a host
        b.wait_in_text('#troubleshoot-dialog h4', "Unknown Host Key")
        b.wait_in_text('#troubleshoot-dialog {}'.format(self.primary_btn_class), "Connect")
        b.click('#troubleshoot-dialog {}'.format(self.primary_btn_class))
        b.wait_in_text('#troubleshoot-dialog h4', "Log in to")
        b.wait_visible("#login-diff-password")
        b.click("#login-type button")
        b.click("#login-type ul li[value='stored'] a")
        b.wait_not_visible("#login-diff-password")
        b.set_val("#login-custom-user", "root")
        b.click('#troubleshoot-dialog {}'.format(self.primary_btn_class))
        b.wait_popdown('troubleshoot-dialog')

        b.wait_not_visible(".curtains-ct")
        b.enter_page('/system', "root@10.111.113.2:2222")
        b.logout()

        # Login with no saved password
        b.go(machine_path)
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked("#authorized-input", False)
        # We also need to switch off superuser since SSH connections
        # might use the password saved for the superuser startup.
        b.eval_js('window.localStorage.setItem("superuser:admin", "none");')
        b.click('#login-button')
        b.expect_load()

        with b.wait_timeout(120):
            b.wait_visible("#machine-reconnect")
        b.click('#machine-troubleshoot')
        b.wait_popup('troubleshoot-dialog')
        b.wait_in_text('#troubleshoot-dialog h4', "Log in to")
        b.wait_present("#login-diff-password")
        b.wait_not_visible("#login-available")
        self.assertEqual(b.text("#login-type button span"), "Type a password")
        b.click("#login-type button")
        b.click("#login-type li[value=stored] a")
        b.wait_in_text("#login-type button span", "Using available credentials")
        b.wait_not_visible("#login-diff-password")

        b.wait_not_in_text("#login-available", "Login Password")
        b.wait_not_in_text("#login-available", "Kerberos Ticket")
        b.wait_visible("#login-available .empty")

        self.allow_hostkey_messages()
        self.allow_authorize_journal_messages()
        self.allow_journal_messages('.* couldn\'t connect: .*',
                                    '.*: Connection reset by peer',
                                    '.* failed to retrieve resource: invalid-hostkey',
                                    '.* host key for server has changed to: .*',
                                    '.* spawning remote bridge failed .*',
                                    '.*: bridge failed: .*',
                                    '.*: received truncated .*',
                                    '.*: Socket error: disconnected',
                                    '.*: host key for this server changed key type: .*',
                                    '.*: server offered unsupported authentication methods: .*')


if __name__ == '__main__':
    test_main()
