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, and we don't get the official
human names (such as "Red Hat Enterprise Linux" or "CentOS Stream") from
the testmap either. 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 10, 2025
1 parent 639cf2b commit 7e04504
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 28 deletions.
4 changes: 4 additions & 0 deletions pkg/static/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ <h1 id="brand" class="hide-before"></h1>
<span id="login-error-title" class="pf-v5-screen-reader"></span>
</p>
<div class="pf-v5-c-alert__description">
<div id="login-supported-os" hidden="true">
<p translate="yes">Transient packageless sessions are only supported on these operating systems for compatibility reasons:</p>
<ul id="login-supported-os-list"></ul>
</div>
<p id="login-error-message">
<span class="noscript" translate="yes">Please enable JavaScript to use the Web Console.</span>
</p>
Expand Down
27 changes: 16 additions & 11 deletions pkg/static/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ function debug(...args) {
id("login-info-message").textContent = "";
}

function login_failure(title, msg, form) {
function login_failure(title, msg, form, supported_oses) {
clear_errors();
if (title) {
/* OAuth failures are always fatal */
Expand All @@ -553,6 +553,16 @@ function debug(...args) {
show_form(form || "login");
id("login-error-title").textContent = title;
id("login-error-message").textContent = msg;
hideToggle("#login-supported-os", !supported_oses);
if (supported_oses) {
const os_list = id("login-supported-os-list");
os_list.innerHTML = "";
supported_oses.forEach(name => {
const li = document.createElement("li");
li.textContent = name;
os_list.appendChild(li);
});
}
hideToggle("#error-group .pf-v5-c-alert__description", !msg);
show("#error-group");
}
Expand Down Expand Up @@ -1042,18 +1052,13 @@ 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(
_("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;
debug("send_login_request(): Not installed/supported cockpit:", error);

login_failure(_("Packageless session unavailable"), message);
const message = format(
_("Install the cockpit-system package (and optionally other cockpit packages) on $0 to enable web console access."),
login_machine || "localhost");
login_failure(_("Packageless session unavailable"), message, undefined, error.supported);
} else if (xhr.statusText) {
fatal(decodeURIComponent(xhr.statusText));
} else {
Expand Down
27 changes: 20 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,32 @@ 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 or k == "UI_NAME") 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,
supported=[i["UI_NAME"] for i in supported_oses])


def python_interpreter(comment: str) -> tuple[Sequence[str], Sequence[str]]:
Expand Down
23 changes: 23 additions & 0 deletions src/cockpit/osinfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# supported OSes for beibooting; entries are os-release keys
# UI_NAME is shown on the login page, it's not used programmatically for matching
# keep this in sync with bots/lib/testmap.py
supported_oses: 'list[dict[str, str | None]]' = [
{"ID": "arch", "UI_NAME": "Arch Linux"},

# match/describe CentOS separately, it's the upstream of all RHEL clones
{"ID": "centos", "PLATFORM_ID": "platform:el9", "UI_NAME": "CentOS Stream 9"},
{"ID": "centos", "PLATFORM_ID": "platform:el10", "UI_NAME": "CentOS Stream 10"},
{"PLATFORM_ID": "platform:el9", "UI_NAME": "Red Hat Enterprise Linux 9"},
{"PLATFORM_ID": "platform:el10", "UI_NAME": "Red Hat Enterprise Linux 10"},

{"ID": "debian", "VERSION_ID": "12", "UI_NAME": "Debian GNU/Linux 12"},
{"ID": "debian", "VERSION_ID": None, "UI_NAME": "Debian GNU/Linux testing/unstable"},

{"ID": "fedora", "VERSION_ID": "40", "UI_NAME": "Fedora Linux 40"},
{"ID": "fedora", "VERSION_ID": "41", "UI_NAME": "Fedora Linux 41"},
{"ID": "fedora", "VERSION_ID": "42", "UI_NAME": "Fedora Linux 42"},

{"ID": "ubuntu", "VERSION_ID": "22.04", "UI_NAME": "Ubuntu 22.04 LTS"},
{"ID": "ubuntu", "VERSION_ID": "24.04", "UI_NAME": "Ubuntu 24.04 LTS"},
{"ID": "ubuntu", "VERSION_ID": "24.10", "UI_NAME": "Ubuntu 24.10"},
]
37 changes: 27 additions & 10 deletions test/verify/check-shell-multi-machine
Original file line number Diff line number Diff line change
Expand Up @@ -363,17 +363,34 @@ 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/; /^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"/' /etc/os-release''')
b.try_login(password="alt-password")
b.wait_text('#login-error-title', "Packageless session unavailable")
b.wait_in_text('#login-supported-os', "Transient packageless sessions")
b.wait_in_text('#login-supported-os', "Fedora")
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 7e04504

Please sign in to comment.