Skip to content

Commit

Permalink
login: Beibooting to all supported OSes
Browse files Browse the repository at this point in the history
Previously we only allowed beibooting to the same target OS as the host.
But we know which OSes Cockpit works on -- exactly the ones which we
have in CI. So check the remote OS against a list of known supported
ones.

Building that supported list isn't quite easy, though: It *roughly*
corresponds to our bots/lib/testmap.py, but not quite -- e.g.
"debian-stable" is "Debian 12" right now. So for the time being, keep
that list static. We'll have to update it for each new OS that we
support, but our integration tests will tell us.

In the future we might be doing something clever, like automatically
collecting `os-release` files from all our supported images in bots, and
use that to auto-build `osinfo.py`.

Enable the "incompatible future OS version" check for Debian/Arch as
well, by appending a `VERSION_ID`.

https://issues.redhat.com/browse/COCKPIT-1193
  • Loading branch information
martinpitt committed Jan 15, 2025
1 parent 2eabd3a commit d9b5815
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 25 deletions.
9 changes: 1 addition & 8 deletions pkg/static/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -1042,17 +1042,10 @@ function debug(...args) {
const status = decodeURIComponent(xhr.statusText).trim();
login_failure(_("Permission denied"), status === "Permission denied" ? "" : status);
} else if (xhr.status == 500 && xhr.statusText.indexOf("no-cockpit") > -1) {
let message = format(
const message = format(
_("Install the cockpit-system package (and optionally other cockpit packages) on $0 to enable web console access."),
login_machine || "localhost");

// with os-release (all but really weird targets) we get some more info
const error = JSON.parse(xhr.responseText);
if (error.supported)
message = format(
_("Transient packageless sessions require the same operating system and version, for compatibility reasons: $0."),
error.supported) + " " + message;

login_failure(_("Packageless session unavailable"), message);
} else if (xhr.statusText) {
fatal(decodeURIComponent(xhr.statusText));
Expand Down
25 changes: 18 additions & 7 deletions src/cockpit/beiboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from cockpit.channel import ChannelRoutingRule
from cockpit.channels import PackagesChannel
from cockpit.jsonutil import JsonObject, get_str
from cockpit.osinfo import supported_oses
from cockpit.packages import Packages, PackagesLoader, patch_libexecdir
from cockpit.peer import Peer
from cockpit.protocol import CockpitProblem, CockpitProtocolError
Expand Down Expand Up @@ -236,20 +237,30 @@ async def do_custom_command(self, command: str, args: tuple, fds: list[int], std

if command == 'cockpit.check-os-release':
remote_os = parse_os_release(args[0])
logger.debug("cockpit.check-os-release: remote OS: %r", remote_os)
logger.debug("cockpit.check-os-release: supported OSes: %r", supported_oses)

for osinfo in supported_oses:
# we want to allow e.g. VERSION_ID == None matching to check for key absence
if all(remote_os.get(k) == v for k, v in osinfo.items()):
logger.debug("cockpit.check-os-release: remote matches supported OS %r", osinfo)
return

# allow unknown OSes as long as local and remote are the same
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())
this_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("NAME", remote_os.get("ID", "?"))} {remote_os.get("VERSION_ID", "")}'
supported = f'{local_os.get("NAME", local_os.get("ID", "?"))} {local_os.get("VERSION_ID", "")}'
raise CockpitProblem('no-cockpit', unsupported=unsupported, supported=supported)
if remote_os.get('ID') == this_os.get('ID') and remote_os.get('VERSION_ID') == this_os.get('VERSION_ID'):
logger.debug("cockpit.check-os-release: remote OS matches local OS %r", this_os)
return

unsupported = f'{remote_os.get("NAME", remote_os.get("ID", "?"))} {remote_os.get("VERSION_ID", "")}'
raise CockpitProblem('no-cockpit', unsupported=unsupported)


def python_interpreter(comment: str) -> tuple[Sequence[str], Sequence[str]]:
Expand Down
25 changes: 25 additions & 0 deletions src/cockpit/osinfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# supported OSes for beibooting; entries are os-release keys
# keep this in sync with bots/lib/testmap.py
supported_oses: 'list[dict[str, str | None]]' = [
# rolling release
{"ID": "arch", "VERSION_ID": None},

# match/describe CentOS separately, it's the upstream of all RHEL clones
{"ID": "centos", "PLATFORM_ID": "platform:el9"},
{"ID": "centos", "PLATFORM_ID": "platform:el10"},
{"PLATFORM_ID": "platform:el8"},
{"PLATFORM_ID": "platform:el9"},
{"PLATFORM_ID": "platform:el10"},

{"ID": "debian", "VERSION_ID": "12"},
# rolling release
{"ID": "debian", "VERSION_ID": None},

{"ID": "fedora", "VERSION_ID": "40"},
{"ID": "fedora", "VERSION_ID": "41"},
{"ID": "fedora", "VERSION_ID": "42"},

{"ID": "ubuntu", "VERSION_ID": "22.04"},
{"ID": "ubuntu", "VERSION_ID": "24.04"},
{"ID": "ubuntu", "VERSION_ID": "24.10"},
]
36 changes: 26 additions & 10 deletions test/verify/check-shell-multi-machine
Original file line number Diff line number Diff line change
Expand Up @@ -363,17 +363,33 @@ class TestMultiMachine(testlib.MachineCase):
b.wait_visible('#content')
b.logout()

# beiboot mode: future OS version → incompatible, not supported
# 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 not in ["arch", "debian-testing"]:
m2.execute("sed -i '/^VERSION_ID/ s/$/1/' /etc/os-release")
b.try_login(password="alt-password")
source_os = m.execute('. /etc/os-release; echo "$NAME $VERSION_ID"').strip()
b.wait_text('#login-error-title', "Packageless session unavailable")
b.wait_in_text('#login-error-message', "Transient packageless sessions")
b.wait_in_text('#login-error-message', source_os)
b.wait_in_text('#login-error-message', "Install the cockpit-system package")
b.wait_in_text('#login-error-message', "on 10.111.113.2")
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)

Expand Down

0 comments on commit d9b5815

Please sign in to comment.