From dfde4b43f56451b1bf58fb5cbe5cf4d4c6f77424 Mon Sep 17 00:00:00 2001 From: navigator Date: Fri, 2 Oct 2026 13:49:30 +0000 Subject: [PATCH] fix: the first boot role screen skips keel inspect without a database server keel inspect reads the whole machine to find the engine, 2 s on a Web container and 8 s with a quarter of a CPU, while the password screen stayed up with nothing new on it (the maintainer's Proxmox console after ). With no server binary of keel's own table (keel.inspect.dbengines.ENGINES) there is nothing to find, so the role stays standalone at once; when the table cannot be read, keel is asked. --- debian/changelog | 13 ++++++++ keelfirstboot.py | 28 ++++++++++++++++- tests/test_first_boot.py | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 02a2b82..96a5eb6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,16 @@ +confconsole (2.2.3+keel12) trixie; urgency=low + + * The first boot role screen does not run keel inspect on a machine + with no database server. keel inspect reads the whole machine to find + the engine: 2 s on a Web container, 8 s with a quarter of a CPU, with + the password screen still up and nothing new on it (the maintainer's + Proxmox console after , 2026-10-02). Without any server binary + of keel's own table (keel.inspect.dbengines.ENGINES) there is nothing + to find, so the role stays standalone at once, and the journal says + why as before. When the table cannot be read, keel is asked. + + -- Marcos Méndez Fri, 02 Oct 2026 16:00:00 +0000 + confconsole (2.2.3+keel11) trixie; urgency=low * Database mode on a machine with no database server says only that: diff --git a/keelfirstboot.py b/keelfirstboot.py index 1027278..16b3e1a 100644 --- a/keelfirstboot.py +++ b/keelfirstboot.py @@ -38,6 +38,7 @@ (_TURNKEY_INIT) is an explicit interactive run and asks again. """ +import glob import os import shutil import sys @@ -333,13 +334,38 @@ def choose_role(console, path: str, document: dict) -> None: def find_engine(server: dict) -> str: - """The declared engine, else the one keel inspect finds, quietly""" + """The declared engine, else the one keel inspect finds, quietly + + keel inspect reads the whole machine, which took 2 s on a Web + container and 8 s on a quarter of a CPU, a gap with nothing on the + screen after the password; with no server binary there is nothing for + it to find, so it is not asked. + """ declared = str(server.get("engine") or "") if declared: return declared + if not database_server_installed(): + return "" return dbscreen.observed_engine()[0] +def database_server_installed(root: str = "/") -> bool: + """Whether a server binary keel inspect looks for is under ROOT + + The binaries are keel's own table (keel.inspect.dbengines.ENGINES), so + the two never disagree; when it cannot be read, keel is asked. + """ + try: + from keel.inspect.dbengines import ENGINES + except ImportError: + return True + return any( + glob.glob(os.path.join(root, pattern)) + for engine in ENGINES + for pattern in engine.server_binaries + ) + + def standalone(console, path: str, document: dict, server: dict, engine: str) -> None: """Record the default, or go back to it through its own screen diff --git a/tests/test_first_boot.py b/tests/test_first_boot.py index c7f60f9..dd9a950 100644 --- a/tests/test_first_boot.py +++ b/tests/test_first_boot.py @@ -12,6 +12,7 @@ import runpy import stat import subprocess +import sys from pathlib import Path import pytest @@ -374,6 +375,8 @@ def test_the_engine_comes_from_the_machine_when_undeclared( self, keel, spec, flows, monkeypatch ): spec({"version": 1}) + monkeypatch.setattr(keelfirstboot, "database_server_installed", + lambda: True) monkeypatch.setattr(dbscreen, "observed_engine", lambda: ("mariadb", "", "")) console = FakeConsole(menus=[("ok", "Primary")]) @@ -382,10 +385,29 @@ def test_the_engine_comes_from_the_machine_when_undeclared( assert flows == [("primary", {}, "mariadb")] + def test_no_server_binary_means_no_role_and_no_keel_inspect( + self, keel, spec, flows, monkeypatch, capsys + ): + # 2026-10-02: keel inspect took 2 s on a Web container (8 s with a + # quarter of a CPU) to say what the missing server binary says + spec({"version": 1}) + monkeypatch.setattr(keelfirstboot, "database_server_installed", + lambda: False) + monkeypatch.setattr(dbscreen, "observed_engine", pytest.fail) + console = FakeConsole() + + keelfirstboot.choose_role(console, str(spec.path), read(spec.path)) + + assert console.calls == [] + assert flows == [] + assert "no database server" in capsys.readouterr().err + def test_a_machine_without_a_database_server_has_no_role_to_ask( self, keel, spec, flows, monkeypatch, capsys ): spec({"version": 1}) + monkeypatch.setattr(keelfirstboot, "database_server_installed", + lambda: True) monkeypatch.setattr(dbscreen, "observed_engine", lambda: ("", "nothing", "")) console = FakeConsole() @@ -407,6 +429,49 @@ def test_a_menu_that_did_not_answer_changes_nothing( assert flows == [] +class TestTheServerBinaries: + """keel's own table says which binaries prove a server is installed""" + + @pytest.fixture + def engines(self, monkeypatch): + """A keel.inspect.dbengines whose ENGINES name these patterns""" + def install(*patterns): + engine = type("Engine", (), {"server_binaries": patterns}) + module = type(sys)("keel.inspect.dbengines") + module.ENGINES = (engine,) + for name in ("keel", "keel.inspect"): + monkeypatch.setitem(sys.modules, name, type(sys)(name)) + monkeypatch.setitem(sys.modules, "keel.inspect.dbengines", + module) + return install + + def test_a_server_binary_present_is_installed(self, engines, tmp_path): + engines("usr/sbin/mariadbd", "usr/lib/postgresql/*/bin/postgres") + binary = tmp_path / "usr/lib/postgresql/17/bin/postgres" + binary.parent.mkdir(parents=True) + binary.touch() + + assert keelfirstboot.database_server_installed(str(tmp_path)) + + def test_no_server_binary_is_not_installed(self, engines, tmp_path): + engines("usr/sbin/mariadbd", "usr/sbin/mysqld") + + assert not keelfirstboot.database_server_installed(str(tmp_path)) + + def test_without_keel_s_table_keel_is_asked(self, monkeypatch, tmp_path): + monkeypatch.setitem(sys.modules, "keel.inspect.dbengines", None) + + assert keelfirstboot.database_server_installed(str(tmp_path)) + + def test_a_table_of_another_shape_means_keel_is_asked( + self, engines, monkeypatch, tmp_path + ): + engines() + monkeypatch.delattr(sys.modules["keel.inspect.dbengines"], "ENGINES") + + assert keelfirstboot.database_server_installed(str(tmp_path)) + + class TestStandalone: def test_a_fresh_node_records_the_role_without_applying( self, keel, spec