#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)## Copyright (C) 2013 Red Hat, Inc.# SPDX-License-Identifier: LGPL-2.1-or-laterimport reimport subprocessimport timeimport testlibdef break_hostkey(m, address):filename = "/home/admin/.ssh/known_hosts"m.execute(f'su admin -c "mkdir -p -m 700 `dirname {filename}`"')key = f"{address} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJqfgO2FPiix1n2sCJCXbaffwog1Vvi3zRdmcAxG//5T"m.write(filename, key, owner="admin:admin")def fix_hostkey(m, key=None):filename = "/home/admin/.ssh/known_hosts"m.execute(f'su admin -c "mkdir -p -m 700 `dirname {filename}`"')m.write(filename, key or '', owner="admin:admin")def break_bridge(m):# we really want to get a "not found" in the shell, not a "permission denied" (which we would get with a# non-executable present file or link)m.execute("""mkdir -p /tmp/overlay /tmp/workmount -t overlay overlay -o lowerdir=/usr/bin,upperdir=/tmp/overlay,workdir=/tmp/work /usr/binrm /usr/bin/cockpit-bridge""")def fix_bridge(m):# umount lives in /usr, so needs a little dance for the EBUSYm.execute("""umount -l /usr/binwhile mountpoint -q /usr/bin; do sleep 0.5; donerm -rf /tmp/overlay /tmp/work /tmp/umount""")def check_failed_state(b, expected_title):b.wait_in_text('#hosts_setup_server_dialog h1', expected_title)b.click("#hosts_setup_server_dialog button:contains('Close')")b.wait_not_present('#hosts_setup_server_dialog')def fail_login(b):b.click('#hosts_setup_server_dialog button:contains("Log in")')b.wait_visible('#hosts_setup_server_dialog button:contains("Log in"):not([disabled])')b.wait_in_text("#hosts_setup_server_dialog .pf-v6-c-alert", "Login failed")def kill_user_admin(machine):machine.execute("loginctl terminate-user admin")def change_ssh_port(machine, address, sshd_socket, sshd_service, port=None, timeout_sec=120):try:port = int(port)except (ValueError, TypeError):port = 22# Keep in mind that not all operating systems have firewalldmachine.execute(f"firewall-cmd --permanent --zone=public --add-port={port}/tcp || true")machine.execute("firewall-cmd --reload || true")if machine.ostree_image: # no semanagemachine.execute("setenforce 0")else:machine.execute(f"! selinuxenabled || semanage port -a -t ssh_port_t -p tcp {port}")if machine.image.startswith("ubuntu"): # always socket activatedmachine.write("/etc/systemd/system/ssh.socket.d/override.conf",f"[Socket]\nListenStream=\nListenStream=127.27.0.15:22\nListenStream={address}:{port}")machine.execute("systemctl daemon-reload")machine.execute("systemctl restart ssh.socket")else:machine.execute("sed -i 's/.*Port .*/#\\0/' /etc/ssh/sshd_config")machine.write("/etc/ssh/sshd_config",f"ListenAddress 127.27.0.15:22\nListenAddress {address}:{port}\n",append=True)# 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.#if sshd_socket:machine.execute(f"systemctl stop {sshd_socket}")machine.execute(f"systemctl restart {sshd_service}")start_time = time.time()error = Nonewhile (time.time() - start_time) < timeout_sec:try:machine.execute(f"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o CheckHostIP=no -o PasswordAuthentication=no -p {port} {address} 2>&1 | grep -q 'Permission denied'", quiet=True)returnexcept Exception as e:error = etime.sleep(0.5)raise error@testlib.skipBeiboot("host switching disabled in beiboot mode")class TestMultiMachineAdd(testlib.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.machine3 = self.machines['machine3']# Disable preloading on all machines# Preloading on machines debug build can overload the browser and cause slowness and browser crashes,# and failing to load software updates breaks pixel tests in release buildsself.setup_provisioned_hosts(disable_preload=True)self.setup_ssh_auth()self.enable_multihost(self.machine)def testBasic(self):b = self.browserm = self.machinem2 = self.machine2m3 = self.machine3m3_host = "10.111.113.3:2222"change_ssh_port(m3, "10.111.113.3", self.sshd_socket, self.sshd_service, port=2222)hostname_selector = "#system_information_hostname_text"self.login_and_go(None)b.add_machine("10.111.113.2", password=None)b.add_machine(m3_host, password=None, expect_warning=False)b.switch_to_top()b.click("#hosts-sel button")kill_user_admin(m2)with b.wait_timeout(30):b.wait_visible("#machine2-error")kill_user_admin(m3)with b.wait_timeout(30):b.wait_visible("#machine3-error")# Navigating reconnectsb.click("a[href='/@10.111.113.2']")b.wait_js_cond('window.location.pathname == "/@10.111.113.2/system"')b.enter_page("/system", host="10.111.113.2")b.wait_text(hostname_selector, "machine2")b.switch_to_top()b.click("#hosts-sel button")b.wait_visible("a[href='/@10.111.113.2']")b.wait_not_present("#machine2-error")b.click("a[href='/@10.111.113.3']")b.wait_js_cond('window.location.pathname == "/@10.111.113.3/system"')b.enter_page("/system", host=m3_host)b.wait_text(hostname_selector, "machine3")b.switch_to_top()b.click("#hosts-sel button")b.wait_visible("a[href='/@10.111.113.3']")b.wait_not_present("#machine3-error")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 .*")# in a remote Shell secondary session, ssh-connecting to other machines happens from primary machinekill_user_admin(m3)with b.wait_timeout(30):b.wait_visible("#machine3-error")b.click("a[href='/@10.111.113.2']")b.wait_js_cond('window.location.pathname == "/@10.111.113.2/system"')m.execute("while pgrep -af [s]sh.*10.111.113.3; do sleep 1; done", stdout=None)b.go("/@10.111.113.2/playground/remote")b.enter_page("/playground/remote", host="10.111.113.2")b.set_input_text("#host", m3_host)b.click("button")# this works without dialog, as the SSH key get auto-unlocked with the user passwordb.wait_text("#output", "machine3")# the SSH connection originates from machine1, not 2m.execute("pgrep -af [s]sh.*10.111.113.3", stdout=None)m2.execute("! pgrep -af [s]sh.*10.111.113.3", stdout=None)def testGlobalSSHConfig(self):b = self.browserm = self.machinem3 = self.machine3change_ssh_port(m3, "10.111.113.3", self.sshd_socket, self.sshd_service, port=2222)m.write("/etc/ssh/ssh_config", "Host m2\n\tHostName 10.111.113.2\n", append=True)m.write("/etc/ssh/ssh_config", "Host m3\n\tHostName 10.111.113.3\n\tPort 2222\n", append=True)self.login_and_go(None)b.add_machine("m2", password=None)b.add_machine("m3", password=None, expect_warning=False)b.switch_to_top()b.click("#hosts-sel button")b.wait_visible("a[href='/@m2']")b.wait_visible("a[href='/@m3']")b.wait_not_present("#page-sidebar .nav-status")self.allow_hostkey_messages()@testlib.skipBeiboot("host switching disabled in beiboot mode")class TestMultiMachine(testlib.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.machine3 = self.machines['machine3']self.allow_journal_messages("sudo: unable to resolve host machine1: .*")# HACK: idling too long on the login page in Negotiate in one machine, while tests# do things on the other times out in ws and kills cockpit-session;# this should be fixed properly in wsself.allow_journal_messages('.*session timed out during authentication')self.allow_journal_messages('.*authentication timed out')self.setup_provisioned_hosts(disable_preload=True)self.enable_multihost(self.machine)def checkDirectLogin(self, root='/', known_host=False):b = self.browserm2 = self.machine2m = self.machinehostname_selector = "#system_information_hostname_text"# Direct to machine2, new loginm2.execute("echo admin:alt-password | chpasswd")b.switch_to_top()b.open(f"{root}=10.111.113.2")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.wait_val("#server-field", "10.111.113.2")b.set_input_text("#login-user-input", "admin")b.set_input_text("#login-password-input", "alt-password")b.click('#login-button')if not known_host:b.wait_in_text("#hostkey-message-1", "10.111.113.2")match = re.match(r'\((?:ssh-)?([^-]*).*\)', b.text("#hostkey-type"))self.assertIsNotNone(match)algo = match.groups()[0]# This assumes that all fingerprints use SHA256.line = m2.execute(f"ssh-keygen -l -E SHA256 -f /etc/ssh/ssh_host_{algo.lower()}_key.pub", quiet=True)fp = line.split(" ")[1]self.assertEqual(b.text('#hostkey-fingerprint'), fp)b.click('#login-button')b.enter_page("/system")b.wait_in_text(hostname_selector, "machine2")b.switch_to_top()b.wait_js_cond(f'window.location.pathname == "{root}=10.111.113.2/system"')b.wait_in_text("#hosts-sel", "machine2")# This is a primary session, we can also ssh onwards to other machinesb.go("/playground/remote")b.enter_page("/playground/remote")b.set_input_text("#host", "10.111.113.3")b.click("button")b.wait_text("#ssh-unknown-host-dialog .pf-v6-c-modal-box__title-text", "Unknown host: 10.111.113.3")b.click("#ssh-unknown-host-dialog button.pf-m-primary")b.wait_not_present("#ssh-unknown-host-dialog")b.wait_text("#ssh-change-auth-dialog .pf-v6-c-modal-box__title-text", "Log in to 10.111.113.3")b.set_input_text("#login-custom-password", "foobar")b.click("#ssh-change-auth-dialog button.pf-m-primary")b.wait_not_present("#ssh-change-auth-dialog")b.wait_text("#output", "machine3")# the SSH connection originates from machine2, not 1m2.execute("pgrep -af [s]sh.*10.111.113.3", stdout=None)m.execute("! pgrep -af [s]sh.*10.111.113.3", stdout=None)b.logout()# Bad host keyb.eval_js("""window.localStorage.setItem("known_hosts", '{}')""")m.write("/etc/ssh/ssh_known_hosts", "10.111.113.2 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgPMmTosSQ4NxMtq+aL2NKLC+W4I9/jbD1e74cnOKTW")b.open(f"{root}=10.111.113.2")b.wait_visible("#login")b.set_input_text("#login-user-input", "admin")b.set_input_text("#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_visible("#server-group")b.wait_text("#login-error-title", "Refusing to connect")b.wait_text("#login-error-message", "Hostkey does not match")# Clear bad host key in /etc and set bad host key in# localStorage.m.write("/etc/ssh/ssh_known_hosts", "")b.eval_js("""window.localStorage.setItem("known_hosts", '{"10.111.113.2":"BAD"}')""")b.set_input_text("#login-user-input", "admin")b.set_input_text("#login-password-input", "alt-password")b.click('#login-button')b.wait_visible("#hostkey-group")b.wait_in_text("#hostkey-title", "10.111.113.2 key changed")b.click('#login-button')b.enter_page("/system")b.wait_in_text(hostname_selector, "machine2")b.logout()# Clear localStorage and set correct host key in /etcb.eval_js("""window.localStorage.setItem("known_hosts", '{}')""")m.execute("ssh-keyscan 10.111.113.2 > /etc/ssh/ssh_known_hosts")b.set_input_text("#login-user-input", "admin")b.set_input_text("#login-password-input", "alt-password")b.click('#login-button')b.enter_page("/system")b.wait_in_text(hostname_selector, "machine2")b.logout()# beiboot mode: same OS → compatible, supportedbreak_bridge(m2)b.try_login(password="alt-password")b.wait_visible('#content')b.logout()# beiboot mode: known remote OS; m2 runs TEST_OS, so we cover all supported ones in our testmap# damage local os-release to avoid the "same unknown OS" code pathm.execute('''sed -i '/^ID=/ s/=.*/=testux/; /^PLATFORM_ID/d; /^VERSION_ID=/ s/=.*$/="0815"/' '''"/etc/os-release")# rolling OSes don't have a VERSION_IDif m.image in ["arch", "debian-testing"]:m.execute('''echo 'VERSION_ID="0815"' >> /etc/os-release''')b.try_login(password="alt-password")b.wait_visible('#content')b.logout()# beiboot mode: different OS version → incompatible, not supportedif m2.image in ["arch", "debian-testing"]:# rolling OSes don't have a VERSION_IDm2.execute('''echo 'VERSION_ID="0815"' >> /etc/os-release''')else:m2.execute('''sed -i '/^VERSION_ID/ s/=.*$/="0815"/; /^PLATFORM_ID/d' /etc/os-release''')b.try_login(password="alt-password")b.wait_text('#login-error-title', "Packageless session unavailable")b.wait_in_text('#login-error-message', "Install the cockpit-system package")b.wait_in_text('#login-error-message', "on 10.111.113.2")# beiboot mode: local and remote OS are unknown but the samem2.execute('''sed -i '/^ID=/ s/=.*/=testux/' /etc/os-release''')b.try_login(password="alt-password")b.wait_visible('#content')b.logout()fix_bridge(m2)login_options = '#show-other-login-options'# Connect to bad machineb.open(f"{root}other")b.set_input_text("#login-user-input", "admin")b.set_input_text("#login-password-input", "bad-password")b.click(login_options)b.wait_visible("#server-group")b.set_input_text("#server-field", "bad")b.click(login_options)b.wait_not_visible("#server-group")b.click('#login-button')b.wait_visible("#server-group")b.wait_text("#login-error-title", "Refusing to connect")b.wait_text("#login-error-message", "Host is unknown")# Might happen when we switch away.self.allow_hostkey_messages()self.allow_journal_messages(".* Failed to resolve hostname bad .*")def testDirectLogin(self):self.machine.start_cockpit()self.checkDirectLogin('/')# Also check whether AllowMultiHost blocks access already in cockpit-ws.b = self.browserm = self.machineself.enable_multihost(m)self.setup_ssh_auth()# login into cockpit on machine1self.login_and_go("/system")cookie = b.cookie("cockpit")def http_code(url):return int(subprocess.check_output(["curl","--silent","-b", f"cockpit={cookie['value']}","-o", "/dev/null", "-w", "%{http_code" + "}",f"http://{b.address}:{b.port}{url}"]))# Now we can get resources from machine1 and machine2self.assertEqual(200, http_code("/cockpit/@localhost/manifests.json"))self.assertEqual(200, http_code("/cockpit/@10.111.113.2/manifests.json"))# But not when AllowMultiHost is falsem.write("/etc/cockpit/cockpit.conf",'[WebService]\nAllowMultiHost=no\n')m.restart_cockpit()b.relogin("/system")cookie = b.cookie("cockpit")self.assertEqual(200, http_code("/cockpit/@localhost/manifests.json"))self.assertEqual(403, http_code("/cockpit/@10.111.113.2/manifests.json"))def testUrlRoot(self):b = self.browserm = self.machinehostname_selector = "#system_information_hostname_text"m.write("/etc/cockpit/cockpit.conf", "[WebService]\nUrlRoot = cockpit-new\nAllowMultiHost=yes\n")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_input_text("#login-user-input", "admin")b.set_input_text("#login-password-input", "foobar")b.click('#login-button')b.enter_page("/system")b.switch_to_top()b.wait_js_cond('window.location.pathname == "/cockpit-new/system"')# Test 2nd machineb.add_machine("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 subnavb.click_system_menu("/cockpit-new/@10.111.113.2/users", enter=False)b.enter_page("/users", host="10.111.113.2")b.click('#accounts-list td[data-label="Username"] a[href="#/admin"]')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"')b.logout()self.checkDirectLogin('/cockpit-new/')self.allow_hostkey_messages()def testUrlRootWithQuery(self):b = self.browserm = self.machinem.write("/etc/cockpit/cockpit.conf", "[WebService]\nUrlRoot = cockpit-new")m.start_cockpit()b.open("/cockpit-new/system?access_token=XXXX")b.wait_visible("#login")b.set_input_text("#login-user-input", "admin")b.set_input_text("#login-password-input", "foobar")b.click('#login-button')b.enter_page("/system")b.switch_to_top()b.wait_js_cond('window.location.pathname == "/cockpit-new/system"')def testExternalPage(self):b = self.browserm1 = self.machinem2 = self.machine2# Modify the terminals to be different on the two machines.for machine, name in zip((m1, m2), ('m1', 'm2'), strict=True):# This page may be either compressed or uncompressedmachine.execute(f"""FILENAME=/usr/share/cockpit/systemd/terminal.html*cp $FILENAME /tmptest -f /tmp/terminal.html || gzip -d /tmp/terminal.html.gzsed -ie 's|</body>|magic-{name}-token</body>|' /tmp/terminal.htmlgzip < /tmp/terminal.html > /tmp/terminal.html.gzmount -o bind /tmp/"$(basename $FILENAME)" $FILENAME""")self.login_and_go("/system")b.add_machine("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.browserm2 = self.machine2m2_path = "/@10.111.113.2/playground/test"# Add a machineself.login_and_go(None)b.add_machine("10.111.113.2")# Go to the path, remove the imageb.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 accountm2.execute('passwd -l admin')kill_user_admin(m2)with b.wait_timeout(30):b.wait_text(".curtains-ct h1", "Not connected to host")b.wait_text("#machine-reconnect", "Reconnect")b.click("#hosts-sel button")b.wait_visible("a[href='/@10.111.113.2']")b.wait_visible("#machine2-error")b.go("/system")b.enter_page("/system")b.wait_in_text("#system_information_hostname_text", "machine1")b.switch_to_top()# navigating there again will failb.go(m2_path)with b.wait_timeout(30):b.wait_visible("#hosts_setup_server_dialog")b.start_machine_troubleshoot(expect_curtain=False, password="foobar",expect_closed_dialog=False, expect_warning=False)b.wait_in_text("#hosts_setup_server_dialog .pf-v6-c-alert", "Login failed")b.click("#hosts_setup_server_dialog button:contains('Cancel')")# wait for system to loadb.go("/system")b.enter_page("/system")b.wait_in_text("#system_information_hostname_text", "machine1")b.switch_to_top()# re-enable adminm2.execute('passwd -u admin')# path should reconnect at this pointb.reload()b.go(m2_path)with b.wait_timeout(30):b.wait_visible("#hosts_setup_server_dialog")b.start_machine_troubleshoot(expect_curtain=False, password="foobar", expect_warning=False)b.enter_page("/playground/test", "10.111.113.2", reconnect=True)# image is back because it page was reloaded after disconnectionb.wait_visible("img[src='hammer.gif']")b.switch_to_top()# Host shows it is upb.click("#hosts-sel button")b.wait_visible("a[href='/@10.111.113.2']")b.wait_not_present("#page-sidebar .nav-status")# Bad host also bouncesb.go("/@10.0.0.0/playground/test")with b.wait_timeout(30):b.wait_text(".curtains-ct h1", "Not connected to host")self.assertEqual(b.text(".curtains-ct .pf-v6-c-empty-state__body"), "Cannot connect to an unknown host")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.browserm2 = self.machines["machine2"]frame = "cockpit1:10.111.113.2/playground/test"m2_path = "/@10.111.113.2/playground/test"# Add a machineself.login_and_go(None)b.add_machine("10.111.113.2")b.switch_to_top()b.go(m2_path)b.enter_page("/playground/test", "10.111.113.2")# Check that channels are closed when a frame is reloaded.# Lock a file on m2, that keeps a channel open.m2.execute("touch /tmp/playground-test-lock")b.click(".lock-channel button")b.wait_in_text(".lock-channel span", 'locked')m2.execute("! flock --nonblock /tmp/playground-test-lock true")# load the same page on m1b.switch_to_top()b.go("/@localhost/playground/test")b.enter_page("/playground/test")b.wait_text('#file-content', "0")# Now reload the m2 frame by kicking its "src" attribute.b.go(m2_path)b.enter_page("/playground/test", "10.111.113.2")b.wait_in_text(".lock-channel span", 'locked')b.switch_to_top()b.eval_js('ph_set_attr("iframe[name=\'%s\']", "src", "../playground/test.html?i=1#/")' % frame)# The channel and the lock should have gone awaym2.execute("flock --timeout 10 /tmp/playground-test-lock true")b.enter_page("/playground/test", "10.111.113.2")b.wait_not_in_text(".lock-channel span", 'locked')self.allow_hostkey_messages()def testTroubleshooting(self):b = self.browserm1 = self.machinem2 = self.machine2# Logging in as root is no longer allowed by default by sshdm2.execute("sed -ri 's/#?PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config")m2.execute(self.restart_sshd)machine_path = "/@10.111.113.2"self.login_and_go(None)# Troubleshoot while addingb.go(machine_path)# Bad hostkeybreak_hostkey(m1, "10.111.113.2")b.start_machine_troubleshoot(new=True, known_host=True, expect_closed_dialog=False)b.wait_in_text('#hosts_setup_server_dialog', "10.111.113.2 key changed")b.click("#hosts_setup_server_dialog button:contains('Cancel')")b.wait_not_present('#hosts_setup_server_dialog')fix_hostkey(m1)# Host key path is correctm1.execute("mkdir -p /home/admin/.ssh/")break_hostkey(m1, "10.111.113.2")b.start_machine_troubleshoot(new=True, known_host=True, expect_closed_dialog=False, expect_warning=False)b.wait_in_text('#hosts_setup_server_dialog', "10.111.113.2 key changed")b.click("#hosts_setup_server_dialog button:contains('Cancel')")fix_hostkey(m1)# Bad cockpitbreak_bridge(m2)b.start_machine_troubleshoot(new=True, password="foobar", expect_closed_dialog=False, expect_warning=False)check_failed_state(b, "Cockpit is not installed")fix_bridge(m2)# Troubleshoot existing# Properly add machinefix_hostkey(m1)b.add_machine("10.111.113.2", expect_warning=False)b.logout()b.wait_visible("#login")# Bad cockpitbreak_bridge(m2)self.login_and_go(None)b.go(machine_path)with b.wait_timeout(20):b.start_machine_troubleshoot(expect_curtain=False, password="foobar", expect_closed_dialog=False)check_failed_state(b, "Cockpit is not installed")b.wait_visible("#machine-reconnect")fix_bridge(m2)# Clear host keyfix_hostkey(m1)b.click("#machine-reconnect")b.start_machine_troubleshoot(expect_curtain=False, expect_closed_dialog=False, expect_warning=False)b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")# show fingerprint validationb.wait_not_visible("#hosts_setup_server_dialog .hostkey-verify-help-cmds")b.click("#hosts_setup_server_dialog .pf-v6-c-expandable-section__toggle button")b.wait_visible("#hosts_setup_server_dialog .hostkey-verify-help-cmds")# run validation commandcmd = b.val("#hosts_setup_server_dialog .hostkey-verify-help-cmds input")self.assertIn("ssh-keyscan", cmd)fingerprint = m2.execute(cmd).strip()# there is some additional noise around it, like the OpenSSH version and host nameself.assertIn(b.val("#hosts_setup_server_dialog .hostkey-fingerprint input"), fingerprint)b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")b.set_input_text('#login-custom-password', "foobar")# Submit the dialog with Enter instead of clicking the Login button, for varietyb.focus("#login-custom-password")b.key("Enter")b.wait_not_present('#hosts_setup_server_dialog')# Reconnectb.wait_not_present(".curtains-ct")b.enter_page('/system', "10.111.113.2")b.logout()b.wait_visible("#login")# Break authm2.execute("echo admin:alt-password | chpasswd")self.login_and_go(None)b.go(machine_path)with b.wait_timeout(20):b.wait_visible("#hosts_connect_server_dialog")b.start_machine_troubleshoot(expect_curtain=False, expect_closed_dialog=False)b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")b.set_input_text("#login-custom-password", "bad")fail_login(b)b.set_input_text("#login-custom-password", "alt-password")b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')b.wait_not_present('#hosts_setup_server_dialog')# Reconnectb.wait_not_present(".curtains-ct")b.enter_page('/system', "10.111.113.2")b.logout()b.wait_visible("#login")change_ssh_port(m2, "10.111.113.2", self.sshd_socket, self.sshd_service, port=2222)m2.disconnect()del self.machines["machine2"] # No more access to m2self.login_and_go(None)b.go(machine_path)with b.wait_timeout(20):b.wait_visible("#hosts_connect_server_dialog")b.start_machine_troubleshoot(expect_curtain=False, expect_closed_dialog=False)b.wait_in_text('#hosts_setup_server_dialog', "Unable to contact")b.set_input_text("#add-machine-address", "10.111.113.2:2222")b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')# ssh(1) tracks known hosts by name/IP, not by port; so not expecting a host key prompt hereb.wait_in_text('#hosts_setup_server_dialog h1', "Log in to")b.set_input_text("#login-custom-password", "alt-password")b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')b.wait_not_present('#hosts_setup_server_dialog')b.wait_not_present(".curtains-ct")b.enter_page('/system', "10.111.113.2:2222")b.logout()self.allow_hostkey_messages()self.allow_journal_messages('.* couldn\'t connect: .*','.* failed to retrieve resource: invalid-hostkey','.* host key for server has changed to: .*','.* spawning remote bridge failed .*','.*: bridge failed: .*','.*: cockpit-bridge: command not found','.*: received truncated .*','.*: Socket error: disconnected','.*: host key for this server changed key type: .*','.*: server offered unsupported authentication methods: .*')@testlib.skipImage("TODO: Broken on Arch Linux", "arch")def testSshKeySetup(self):b = self.browserm1 = self.machinem2 = self.machine2# Let's not use "admin" on the remote machine. Creating a# dedicated user gives us a guaranteed clean slate and also# tests more code paths.m2.execute("useradd -m fred")m2.execute("echo fred:foobar | chpasswd")self.login_and_go(None)b.go("/@10.111.113.2")b.click('#machine-troubleshoot')b.wait_visible('#hosts_setup_server_dialog')b.wait_in_text('#hosts_setup_server_dialog', "new host")b.set_input_text('#add-machine-user', "fred")b.click('#hosts_setup_server_dialog button:contains("Add")')b.wait_visible('#hosts_connect_server_dialog')b.click("#hosts_connect_server_dialog button.pf-m-warning")b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")# There is no key yet. Create and authorize it.m1.execute("! test -f /home/admin/.ssh/id_rsa")m2.execute("! test -f /home/fred/.ssh/authorized_keys")b.wait_in_text("#hosts_setup_server_dialog", "Create a new SSH key and authorize it")b.set_input_text('#login-custom-password', "foobar")b.set_checked("#login-setup-keys", val=True)# Leave passphrase empty on Coreos, since it can't load keys into the agentif not m1.ostree_image:b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password', "foobar")b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password2', "foobar")b.click('#hosts_setup_server_dialog button:contains("Log in")')with b.wait_timeout(30):b.wait_not_present('#hosts_setup_server_dialog')b.enter_page("/system", host="fred@10.111.113.2")m1.execute("test -f /home/admin/.ssh/id_rsa; test -f /home/admin/.ssh/id_rsa.pub")self.assertEqual(m1.execute("cat /home/admin/.ssh/id_rsa.pub"),m2.execute("cat /home/fred/.ssh/authorized_keys"))# Relogin. This should now work seamlessly (except for the warning).b.relogin(None, wait_remote_session_machine=m1)b.wait_visible('#hosts_connect_server_dialog')b.click("#hosts_connect_server_dialog button.pf-m-warning")b.enter_page("/system", host="fred@10.111.113.2")# De-authorize key and relogin, then re-authorize.m2.execute("rm /home/fred/.ssh/authorized_keys")b.relogin(None, wait_remote_session_machine=m1)b.wait_visible('#hosts_connect_server_dialog')b.click("#hosts_connect_server_dialog button.pf-m-warning")b.wait_visible('#hosts_setup_server_dialog')b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")b.wait_in_text("#hosts_setup_server_dialog", "Authorize SSH key")b.set_checked("#login-setup-keys", val=True)b.set_input_text('#login-custom-password', "foobar")b.click('#hosts_setup_server_dialog button:contains("Log in")')b.wait_not_present('#hosts_setup_server_dialog')b.enter_page("/system", host="fred@10.111.113.2")self.assertEqual(m1.execute("cat /home/admin/.ssh/id_rsa.pub"),m2.execute("cat /home/fred/.ssh/authorized_keys"))# Put a 'better' passphrase on the key and relogin, then# change the passphrase back to the login passwordm1.execute("ssh-keygen -q -f /home/admin/.ssh/id_rsa -p -P foobar -N foobarfoo")b.relogin(None, wait_remote_session_machine=m1)b.wait_visible('#hosts_connect_server_dialog')b.click("#hosts_connect_server_dialog button.pf-m-warning")b.wait_visible('#hosts_setup_server_dialog')b.wait_in_text('#hosts_setup_server_dialog', "The SSH key for logging in")b.set_checked('#hosts_setup_server_dialog input[value=key]', val=True)b.set_input_text('#hosts_setup_server_dialog #locked-identity-password', "foobarfoo")b.set_checked("#login-setup-keys", val=True)b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password', "foobar")b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password2', "foobar")b.click('#hosts_setup_server_dialog button:contains("Log in")')b.wait_not_present('#hosts_setup_server_dialog')b.enter_page("/system", host="fred@10.111.113.2")# Relogin. This should now work seamlessly (except on OSTree images# which don't have pam-ssh-add in its PAM stack.)if not m1.ostree_image:b.relogin(None, wait_remote_session_machine=m1)b.wait_visible('#hosts_connect_server_dialog')b.click("#hosts_connect_server_dialog button.pf-m-warning")b.enter_page("/system", host="fred@10.111.113.2")# The authorized_keys files should still only have a single keyself.assertEqual(m1.execute("cat /home/admin/.ssh/id_rsa.pub"),m2.execute("cat /home/fred/.ssh/authorized_keys"))self.allow_hostkey_messages()def testSshKeySetupCustom(self):b = self.browserm1 = self.machinem2 = self.machine2# This tests how the ssh key setup reacts to a already# existing configuration involving a custom key with a# passphrase.m1.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")m1.execute("ssh-keygen -f /home/admin/.ssh/id_local -t rsa -N 'foobar'")m1.execute("chown admin:admin /home/admin/.ssh/id_local*")m1.write("/home/admin/.ssh/config", "Host 10.111.113.2\n IdentityFile /home/admin/.ssh/id_local\n")pubkey = self.machine.execute("cat /home/admin/.ssh/id_local.pub")m2.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")m2.write("/home/admin/.ssh/authorized_keys", pubkey)m2.execute("chown admin:admin /home/admin/.ssh/authorized_keys")self.login_and_go(None)b.go("/@10.111.113.2")b.click('#machine-troubleshoot')b.wait_visible('#hosts_setup_server_dialog')b.click('#hosts_setup_server_dialog button:contains("Add")')b.wait_visible('#hosts_connect_server_dialog')b.click('#hosts_connect_server_dialog button:contains("Connect")')b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")b.wait_in_text('#hosts_setup_server_dialog', "The SSH key")b.wait_not_present('.password-change-advice')b.wait_not_present('.login-setup-auto')self.allow_hostkey_messages()if __name__ == '__main__':testlib.test_main()