#!/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-later import re import subprocess import time import testlib def 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/work mount -t overlay overlay -o lowerdir=/usr/bin,upperdir=/tmp/overlay,workdir=/tmp/work /usr/bin rm /usr/bin/cockpit-bridge """) def fix_bridge(m): # umount lives in /usr, so needs a little dance for the EBUSY m.execute(""" umount -l /usr/bin while mountpoint -q /usr/bin; do sleep 0.5; done rm -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 firewalld machine.execute(f"firewall-cmd --permanent --zone=public --add-port={port}/tcp || true") machine.execute("firewall-cmd --reload || true") if machine.ostree_image: # no semanage machine.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 activated machine.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 = None while (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) return except Exception as e: error = e time.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 builds self.setup_provisioned_hosts(disable_preload=True) self.setup_ssh_auth() self.enable_multihost(self.machine) def testBasic(self): b = self.browser m = self.machine m2 = self.machine2 m3 = self.machine3 m3_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 reconnects b.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 machine kill_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 password b.wait_text("#output", "machine3") # the SSH connection originates from machine1, not 2 m.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.browser m = self.machine m3 = self.machine3 change_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 ws self.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.browser m2 = self.machine2 m = self.machine hostname_selector = "#system_information_hostname_text" # Direct to machine2, new login m2.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 machines b.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 1 m2.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 key b.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 /etc b.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, supported break_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 path m.execute('''sed -i '/^ID=/ s/=.*/=testux/; /^PLATFORM_ID/d; /^VERSION_ID=/ s/=.*$/="0815"/' ''' "/etc/os-release") # rolling OSes don't have a VERSION_ID if 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 supported if m2.image in ["arch", "debian-testing"]: # rolling OSes don't have a VERSION_ID m2.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 same m2.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 machine b.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.browser m = self.machine self.enable_multihost(m) self.setup_ssh_auth() # login into cockpit on machine1 self.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 machine2 self.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 false m.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.browser m = self.machine hostname_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 machine b.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 subnav b.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.browser m = self.machine m.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.browser m1 = self.machine m2 = 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 uncompressed machine.execute(f""" FILENAME=/usr/share/cockpit/systemd/terminal.html* cp $FILENAME /tmp test -f /tmp/terminal.html || gzip -d /tmp/terminal.html.gz sed -ie 's||magic-{name}-token|' /tmp/terminal.html gzip < /tmp/terminal.html > /tmp/terminal.html.gz mount -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.browser m2 = self.machine2 m2_path = "/@10.111.113.2/playground/test" # Add a machine self.login_and_go(None) b.add_machine("10.111.113.2") # 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) 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 fail 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_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 load b.go("/system") b.enter_page("/system") b.wait_in_text("#system_information_hostname_text", "machine1") b.switch_to_top() # re-enable admin m2.execute('passwd -u admin') # path should reconnect at this point b.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 disconnection b.wait_visible("img[src='hammer.gif']") b.switch_to_top() # Host shows it is up b.click("#hosts-sel button") b.wait_visible("a[href='/@10.111.113.2']") b.wait_not_present("#page-sidebar .nav-status") # Bad host also bounces b.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.browser m2 = self.machines["machine2"] frame = "cockpit1:10.111.113.2/playground/test" m2_path = "/@10.111.113.2/playground/test" # Add a machine self.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 m1 b.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 away m2.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.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(self.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") 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 correct m1.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 cockpit break_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 machine fix_hostkey(m1) b.add_machine("10.111.113.2", expect_warning=False) b.logout() b.wait_visible("#login") # Bad cockpit break_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 key fix_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 validation b.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 command cmd = 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 name self.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 variety b.focus("#login-custom-password") b.key("Enter") b.wait_not_present('#hosts_setup_server_dialog') # Reconnect b.wait_not_present(".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(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') # Reconnect b.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 m2 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 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 here b.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.browser m1 = self.machine m2 = 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 agent if 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 password m1.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 key self.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.browser m1 = self.machine m2 = 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()