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 22, 2024
1 parent 1ef8fca commit 02f7899
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 16 deletions.
8 changes: 7 additions & 1 deletion pkg/static/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,13 @@ function debug(...args) {
} else if (xhr.status == 403) {
login_failure(_(decodeURIComponent(xhr.statusText)) || _("Permission denied"));
} else if (xhr.status == 500 && xhr.statusText.indexOf("no-cockpit") > -1) {
login_failure(format(_("A compatible version of Cockpit is not installed on $0."), login_machine || "localhost"));
// always show what's going on
let message = format(_("A compatible version of Cockpit is not installed on $0."), login_machine || "localhost");
// in beiboot mode we get some more info
const error = JSON.parse(xhr.responseText);
if (error.supported)
message += " " + format(_("This is only supported for $0 on the target machine."), error.supported);
login_failure(message);
} else if (xhr.statusText) {
fatal(decodeURIComponent(xhr.statusText));
} else {
Expand Down
38 changes: 34 additions & 4 deletions src/cockpit/beiboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from cockpit._vendor import ferny
from cockpit._vendor.bei import bootloader
from cockpit.beipack import BridgeBeibootHelper
from cockpit.bridge import setup_logging
from cockpit.bridge import parse_os_release, setup_logging
from cockpit.channel import ChannelRoutingRule
from cockpit.channels import PackagesChannel
from cockpit.jsonutil import JsonObject, get_str
Expand Down 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 Down Expand Up @@ -137,7 +146,7 @@ def shutdown(self) -> None:


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 +234,23 @@ 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'):
unsupported = f'{remote_os.get("ID", "?")} {remote_os.get("VERSION_ID", "")}'
supported = f'{local_os.get("ID", "?")} {local_os.get("VERSION_ID", "")}'
raise CockpitProblem('no-cockpit', unsupported=unsupported, supported=supported)


def python_interpreter(comment: str) -> tuple[Sequence[str], Sequence[str]]:
return ('python3', '-ic', f'# {comment}'), ()
Expand Down Expand Up @@ -262,7 +288,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 +370,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 +509,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: if not installed, 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
8 changes: 0 additions & 8 deletions test/verify/check-loopback
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +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_text("#login-error-message", "A compatible version of Cockpit is not installed on localhost.")

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
17 changes: 17 additions & 0 deletions test/verify/check-shell-multi-machine
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,23 @@ 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-error-message', "A compatible version of Cockpit is not installed on 10.111.113.2")
source_os = m.execute('. /etc/os-release; echo "$ID $VERSION_ID"').strip()
b.wait_in_text('#login-error-message', f"This is only supported for {source_os} on the target machine")

fix_bridge(m2)

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

# Connect to bad machine
Expand Down

0 comments on commit 02f7899

Please sign in to comment.