Skip to content

Commit

Permalink
beiboot: Enable beiboot feature for distro packages for same OS
Browse files Browse the repository at this point in the history
We recently eliminated all on-demand installations of cockpit-* packages
in beiboot mode, which prevents accidentally moving from a beiboot to a
distro package scenario.

This means we can now carefully open up the beiboot feature for distro
packages. However, we need to be careful here: While we can reasonably
assume an ever-green flatpak and cockpit/ws container, distro packages
may be arbitrarily old, and hence incompatible with APIs from newer
OSes. So start small and only allow connecting to the same target OS as
the host. This should already cover a lot of use cases in homogenous
environments.

Note that this only applies to direct logins (bastion host). The code
path for the (deprecated) host switcher is completely different and
doesn't support beiboot at all.

Drop the `no-cockpit` part of TestLoopback.testBasic. Removing
/usr/bin/cockpit-bridge now just enables beiboot mode, the OS is always
compatible (as it's localhost), and we already check this in
TestMultiMachine.

Part of https://issues.redhat.com/browse/COCKPIT-1178
  • Loading branch information
martinpitt committed Oct 15, 2024
1 parent 6af58ad commit 6786f23
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 15 deletions.
41 changes: 38 additions & 3 deletions src/cockpit/beiboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ def __init__(self, file_status: Dict[str, bool]):
def report_exists(files):
command('cockpit.report-exists', {name: os.path.exists(name) for name in files})
""",
"check_os_release": r"""
import os
def check_os_release(_argv):
try:
with open('/etc/os-release') as f:
command('cockpit.check-os-release', f.read())
except OSError:
command('cockpit.check-os-release', "")
""",
"force_exec": r"""
import os
def force_exec(argv):
Expand All @@ -136,8 +145,14 @@ def shutdown(self) -> None:
self.peer.close()


def parse_os_release(text):
return dict(line.strip().split('=', 1)
for line in text.splitlines()
if not line.startswith('#') and '=' in line)


class AuthorizeResponder(ferny.AskpassHandler):
commands = ('ferny.askpass', 'cockpit.report-exists', 'cockpit.fail-no-cockpit')
commands = ('ferny.askpass', 'cockpit.report-exists', 'cockpit.fail-no-cockpit', 'cockpit.check-os-release')
router: Router

def __init__(self, router: Router, basic_password: Optional[str]):
Expand Down Expand Up @@ -225,6 +240,22 @@ async def do_custom_command(self, command: str, args: tuple, fds: list[int], std
if command == 'cockpit.fail-no-cockpit':
raise CockpitProblem('no-cockpit', message=args[0])

if command == 'cockpit.check-os-release':
remote_os = parse_os_release(args[0])
logger.debug("cockpit.check-os-release: remote: %r", remote_os)
try:
with open("/etc/os-release") as f:
local_os = parse_os_release(f.read())
except OSError as e:
logger.warning("failed to read local /etc/os-release, skipping OS compatibility check: %s", e)
return

logger.debug("cockpit.check-os-release: local: %r", local_os)
# for now, just support the same OS
if remote_os.get('ID') != local_os.get('ID') or remote_os.get('VERSION_ID') != local_os.get('VERSION_ID'):
msg = f'Unsupported OS {remote_os.get("ID", "?")} {remote_os.get("VERSION_ID", "?")}'
raise CockpitProblem('no-cockpit', message=msg)


def python_interpreter(comment: str) -> tuple[Sequence[str], Sequence[str]]:
return ('python3', '-ic', f'# {comment}'), ()
Expand Down Expand Up @@ -262,7 +293,7 @@ def flatpak_spawn(cmd: Sequence[str], env: Sequence[str]) -> tuple[Sequence[str]


class SshPeer(Peer):
mode: 'Literal["always"] | Literal["never"] | Literal["auto"]'
mode: 'Literal["always"] | Literal["never"] | Literal["supported"] | Literal["auto"]'

def __init__(self, router: Router, destination: str, args: argparse.Namespace):
self.destination = destination
Expand Down Expand Up @@ -344,6 +375,9 @@ async def boot(self, cmd: Sequence[str], env: Sequence[str]) -> None:
exec_cockpit_bridge_steps = [('try_exec', (['cockpit-bridge'],))]
elif self.remote_bridge == 'always':
exec_cockpit_bridge_steps = [('force_exec', (['cockpit-bridge'],))]
elif self.remote_bridge == 'supported':
# native bridge first; check OS compatibility for beiboot fallback
exec_cockpit_bridge_steps = [('try_exec', (['cockpit-bridge'],)), ('check_os_release', ([],))]
else:
assert self.remote_bridge == 'never'
exec_cockpit_bridge_steps = []
Expand Down Expand Up @@ -480,9 +514,10 @@ def main() -> None:
polyfills.install()

parser = argparse.ArgumentParser(description='cockpit-bridge is run automatically inside of a Cockpit session.')
parser.add_argument('--remote-bridge', choices=['auto', 'never', 'always'], default='auto',
parser.add_argument('--remote-bridge', choices=['auto', 'never', 'supported', 'always'], default='auto',
help="How to run cockpit-bridge from the remote host: auto: if installed (default), "
"never: always copy the local one; "
"supported: copy local one for compatible OSes, fail otherwise; "
"always: fail if not installed")
parser.add_argument('--debug', action='store_true')
parser.add_argument('destination', help="Name of the remote host to connect to, or 'localhost'")
Expand Down
6 changes: 3 additions & 3 deletions src/ws/cockpitauth.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
#define ACTION_NONE "none"
#define LOCAL_SESSION "local-session"

/* for the time being, we only support running an installed cockpit-bridge on the remote,
* and leave beibooting to the flatpak */
const gchar *cockpit_ws_ssh_program = "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=always";
/* we only support beibooting machines with a known/vetted OS, as it's impossible to guarantee
* forward compatibility for all pages */
const gchar *cockpit_ws_ssh_program = "/usr/bin/env python3 -m cockpit.beiboot --remote-bridge=supported";

/* Some tunables that can be set from tests */
const gchar *cockpit_ws_session_program = LIBEXECDIR "/cockpit-session";
Expand Down
9 changes: 0 additions & 9 deletions test/verify/check-loopback
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,6 @@ class TestLoopback(testlib.MachineCase):
b.logout()
b.wait_visible("#login")

self.restore_file("/usr/bin/cockpit-bridge")
m.execute("rm /usr/bin/cockpit-bridge")

b.set_val('#login-user-input', "admin")
b.set_val('#login-password-input', "foobar")
b.click('#login-button')
b.wait_visible('#login-fatal')
self.assertIn("no-cockpit", b.text('#login-fatal'))

m.disconnect()
self.restore_dir("/etc/ssh", restart_unit=self.sshd_service)
m.execute("sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)")
Expand Down
15 changes: 15 additions & 0 deletions test/verify/check-shell-multi-machine
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,21 @@ class TestMultiMachine(testlib.MachineCase):
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: future OS version → incompatible, not supported
# rolling OSes don't have a VERSION_ID
if m.image not in ["arch", "debian-testing"]:
m2.execute("sed -i '/^VERSION_ID/ s/$/1/' /etc/os-release")
b.try_login(password="alt-password")
b.wait_in_text('#login-fatal-message', "Unsupported OS")

fix_bridge(m2)

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

# Connect to bad machine
Expand Down

0 comments on commit 6786f23

Please sign in to comment.