#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)## Copyright (C) 2022 Red Hat, Inc.# SPDX-License-Identifier: LGPL-2.1-or-laterimport testlibHOST = "host.containers.internal"@testlib.onlyImage("no cockpit/ws container on this image", "fedora-coreos")@testlib.nondestructiveclass TestWsBastionContainer(testlib.MachineCase):def setUp(self):super().setUp()# stop ws container from previous runsself.machine.stop_cockpit()# undo cockpit/ws install stepsself.restore_file("/etc/systemd/system/cockpit.service")self.addCleanup(self.machine.execute, "podman rm -f --all")def approve_key(self, b, hostname):b.wait_visible("#hostkey-group")b.wait_in_text("#hostkey-message-1", f"You are connecting to {hostname} for the first time.")b.click("#login-button")def testPasswordLogin(self):m = self.machineb = self.browserm.execute("podman run -d --name cockpit-bastion -p 9090:9090 localhost/cockpit/ws")m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done", timeout=10)b.open("/", tls=True)b.wait_visible("#login")# should be pre-configured to RequireHostb.wait_not_visible("#option-group")b.wait_visible("#server-field")# LoginTitle from default-bastion.confb.wait_text("#server-name", "Cockpit Bastion")# No branding by defaultb.wait_text("#brand", "")b.set_val("#login-user-input", "admin")b.set_val("#login-password-input", "foobar")# Requires a hostb.click("#login-button")b.wait_text("#login-error-title", "Please specify the host to connect to")b.wait_not_visible("#login-error-message")# so connect to our own container hostb.set_val("#server-field", HOST)b.set_val("#login-password-input", "foobar")# key is unknownb.click("#login-button")self.approve_key(b, HOST)b.wait_visible('#content')b.wait_text('#current-username', 'admin')# Our test VM only pre-installs -system and -networkmanagerb.wait_in_text("#host-apps", "Overview")b.wait_in_text("#host-apps", "Networking")self.assertNotIn("Kernel dump", b.text("#host-apps"))# runs the ssh target's bridge (no beiboot)m.execute("pgrep -f /usr/bin/[c]ockpit-bridge")b.logout()# remembers the last host via URL, server field should be pre-filledself.assertEqual(b.eval_js("window.location.pathname"), f"/={HOST}/system")# this is only for Cockpit Clientb.wait_not_visible("#recent-hosts")# second time SSH key is knownb.try_login()b.wait_visible('#content')b.logout()# disable target bridge, should use beibootm.execute("mount -o bind /dev/null /usr/bin/cockpit-bridge")self.addCleanup(m.execute, "umount /usr/bin/cockpit-bridge")b.try_login()b.wait_visible('#content')m.execute("pgrep -f '[p]ython3 -ic # cockpit-bridge'")# beiboot mode has extra included pagesb.wait_in_text("#host-apps", "Overview")b.wait_in_text("#host-apps", "Networking")b.wait_in_text("#host-apps", "Kernel dump")b.wait_in_text("#host-apps", "SELinux")# but Applications is hidden due to failing manifest conditionsself.assertNotIn("Applications", b.text("#host-apps"))b.logout()def testKnownHosts(self):m = self.machineb = self.browserm.execute(f"ssh-keyscan localhost | sed 's/^localhost/{HOST}/' > /root/known_hosts")self.addCleanup(m.execute, "rm /root/known_hosts")def check_login():m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done", timeout=10)b.open("/", tls=True)b.set_val("#login-user-input", "admin")b.set_val("#login-password-input", "foobar")b.set_val("#server-field", HOST)b.click("#login-button")b.wait_visible('#content')b.logout()m.execute("podman rm -f cockpit-bastion")# default locationm.execute("podman run -d --name cockpit-bastion -p 9090:9090 ""-v /root/known_hosts:/etc/ssh/ssh_known_hosts:ro,Z ""localhost/cockpit/ws")check_login()# custom locationm.execute("podman run -d --name cockpit-bastion -p 9090:9090 ""-v /root/known_hosts:/known_hosts:ro,Z ""-e COCKPIT_SSH_KNOWN_HOSTS_FILE=/known_hosts ""localhost/cockpit/ws")check_login()def testCustomConf(self):m = self.machineb = self.browser# custom cockpit.conf and pretend we are Fedora CoreOS/RHELself.write_file("/root/cockpit.conf", """[WebService]LoginTitle = My Walden""")m.execute("cp /etc/os-release /root; ""podman run -d --name cockpit-bastion -p 9090:9090 ""-v /root/cockpit.conf:/etc/cockpit/cockpit.conf:ro,Z ""-v /root/os-release:/etc/os-release:ro,Z ""localhost/cockpit/ws")m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done", timeout=10)b.open("/", tls=True)b.wait_visible("#login")b.wait_text("#server-name", "My Walden")# custom conf does not have RequireHostb.wait_visible("#option-group")# Shows os-release brandingb.wait_in_text("#brand", "Fedora" if m.image == "fedora-coreos" else "Red Hat Enterprise Linux")# pre-fill target hostb.open(f"/={HOST}/", tls=True)b.wait_visible("#login")b.set_val("#login-user-input", "admin")b.set_val("#login-password-input", "foobar")b.click("#login-button")self.approve_key(b, HOST)b.wait_visible('#content')b.wait_text('#current-username', 'admin')def testKeyLogin(self):m = self.machineb = self.browserKEY_PASSWORD = "sshfoobar"# old RSA/PEM formatm.execute(f"ssh-keygen -q -f /root/id_bastion -t rsa -m PEM -N {KEY_PASSWORD}")m.execute(f"ssh-keyscan localhost | sed 's/^localhost/{HOST}/' > /root/known_hosts")self.addCleanup(m.execute, "rm /root/known_hosts /root/id_bastion /root/id_bastion.pub")def do_test_key_login(ssh_key_env: str):m.execute("podman run -d --name cockpit-bastion -p 9090:9090 ""-v /root/known_hosts:/etc/ssh/ssh_known_hosts:ro,Z ""-v /root/id_bastion:/id_bastion:ro,Z "f"-e {ssh_key_env}=/id_bastion ""-e COCKPIT_DEBUG=all ""localhost/cockpit/ws")m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done", timeout=10)b.open("/", tls=True)b.set_val("#login-user-input", "admin")b.set_val("#server-field", HOST)# the account password does not workb.set_val("#login-password-input", "foobar")b.click("#login-button")b.wait_text("#login-error-title", "Authentication failed")b.wait_text("#login-error-message", "Wrong user name or password")# SSH key password, but key is not authorizedb.set_val("#login-password-input", KEY_PASSWORD)b.click("#login-button")b.wait_text("#login-error-title", "Authentication failed")b.wait_text("#login-error-message", "Wrong user name or password")# authorize keyself.restore_file("/home/admin/.ssh/authorized_keys")# Do not use authorized_keys.d as that does not work on rhel4edge# Do not append but overwrite so we are sure the right key is usedm.execute("cat /root/id_bastion.pub > /home/admin/.ssh/authorized_keys")# fails with wrong key passwordb.set_val("#login-password-input", "notthispassword")b.click("#login-button")b.wait_text("#login-error-title", "Authentication failed")b.wait_text("#login-error-message", "Wrong user name or password")# works with correct key passwordb.set_val("#login-password-input", KEY_PASSWORD)b.click("#login-button")b.wait_visible('#content')# runs the ssh target's bridge (no beiboot)m.execute("pgrep -f /usr/bin/[c]ockpit-bridge")b.logout()# now test with current OpenSSH formatm.execute(f"yes | ssh-keygen -q -f /root/id_bastion -t rsa -N {KEY_PASSWORD}")m.execute("cat /root/id_bastion.pub > /home/admin/.ssh/authorized_keys")b.set_val("#login-user-input", "admin")b.set_val("#server-field", HOST)b.set_val("#login-password-input", KEY_PASSWORD)b.click("#login-button")b.wait_visible('#content')b.logout()# disable target bridge, should use beibootm.execute("mount -o bind /dev/null /usr/bin/cockpit-bridge")try:b.try_login(password=KEY_PASSWORD)b.wait_visible('#content')m.execute("pgrep -f '[p]ython3 -ic # cockpit-bridge'")finally:m.execute("umount /usr/bin/cockpit-bridge")b.logout()m.execute("podman rm -f -t0 cockpit-bastion")m.execute("rm /home/admin/.ssh/authorized_keys")for ssh_key_env in ["COCKPIT_SSH_KEY_PATH", f"COCKPIT_SSH_KEY_PATH_{HOST.upper()}"]:do_test_key_login(ssh_key_env=ssh_key_env)def testExternalAgent(self):m = self.machineb = self.browserKEY_PASSWORD = "sshfoobar"# run the container as user -- as root does not make sense, as we want to# share the user's SSH key with itm.execute(f"podman save localhost/cockpit/ws -o {self.vm_tmpdir}/cockpit-ws.tar")# HACK: user podman does not add default route if the host doesn't have anym.execute("ip route add default via 172.27.0.1")self.addCleanup(m.execute, "ip route del default")self.restore_dir("/home/admin")# podman needs logind session (https://bugzilla.redhat.com/show_bug.cgi?id=2375715)m.execute("loginctl enable-linger admin")self.addCleanup(m.execute, "loginctl disable-linger admin")m.execute("runuser -u admin -- sh -ex", input=f"""# we don't start a real login session here, fake itcd $HOMEexport XDG_RUNTIME_DIR=/run/user/$(id -u)# create new key, set it up for logging into hostssh-keygen -q -f ~/.ssh/id_bastion -N {KEY_PASSWORD}cat ~/.ssh/id_bastion.pub > /home/admin/.ssh/authorized_keysssh-keyscan localhost | sed 's/^localhost/{HOST}/' > ~/.ssh/known_hosts# start agent, load keyeval $(ssh-agent -a $XDG_RUNTIME_DIR/ssh-agent)printf '#!/bin/sh\necho {KEY_PASSWORD}\n' > /tmp/pwdchmod u+x /tmp/pwdSSH_ASKPASS_REQUIRE=force SSH_ASKPASS=/tmp/pwd ssh-add ~/.ssh/id_bastionrm /tmp/pwdssh-add -l# run containerpodman load -i {self.vm_tmpdir}/cockpit-ws.tarpodman run -d --rm --name cockpit-bastion -p 9090:9090 \-v ~/.ssh/known_hosts:/etc/ssh/ssh_known_hosts:ro \-v $XDG_RUNTIME_DIR/ssh-agent:/ssh-agent \-e SSH_AUTH_SOCK=/ssh-agent \--security-opt=label=disable \-e COCKPIT_DEBUG=all localhost/cockpit/wsuntil curl --fail --head -k https://localhost:9090/; do sleep 1; done""")# login works without a password, using the external agentb.open("/", tls=True)b.set_val("#server-field", HOST)b.try_login(password="")b.wait_visible('#content')@testlib.onlyImage("no cockpit/ws container on this image", "fedora-coreos")@testlib.nondestructiveclass TestWsPrivileged(testlib.MachineCase):def testService(self):# the install script should have created a cockpit.service, but not started itm = self.machineself.assertEqual(m.execute("! systemctl is-enabled cockpit.service").strip(), "disabled")self.assertEqual(m.execute("! systemctl is-active cockpit.service").strip(), "inactive")# stop ws container from previous test runsself.machine.stop_cockpit()self.assertEqual(m.execute("podman ps --noheading"), "")self.addCleanup(m.execute, "systemctl disable --now cockpit.service")m.execute("systemctl start cockpit.service")self.assertEqual(m.execute("systemctl is-active cockpit.service").strip(), "active")firstStartTime = m.execute("podman inspect cockpit-ws| jq '.[0].Created'")m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done", timeout=10)m.execute("systemctl restart cockpit.service")self.assertEqual(m.execute("systemctl is-active cockpit.service").strip(), "active")secondStartTime = m.execute("podman inspect cockpit-ws| jq '.[0].Created'")m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done", timeout=10)self.assertNotEqual(firstStartTime, secondStartTime)m.execute("systemctl stop cockpit.service")self.assertEqual(m.execute("! systemctl is-enabled cockpit.service").strip(), "disabled")self.assertEqual(m.execute("! systemctl is-active cockpit.service").strip(), "inactive")self.assertEqual(m.execute("podman ps --noheading"), "")# current container has `set -x` in its startup script, which ends up in the journalself.allow_journal_messages("^[/+'].*")if __name__ == '__main__':testlib.test_main()