Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
confconsole (2.2.3+keel13) 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 <Saved>, 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 <mendez.foto@gmail.com> Fri, 02 Oct 2026 18:00:00 +0000

confconsole (2.2.3+keel12) trixie; urgency=low

* The Instance menu offers a screen only where what it configures is in
Expand Down
28 changes: 27 additions & 1 deletion keelfirstboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
(_TURNKEY_INIT) is an explicit interactive run and asks again.
"""

import glob
import os
import shutil
import sys
Expand Down Expand Up @@ -346,13 +347,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
Expand Down
65 changes: 65 additions & 0 deletions tests/test_first_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import runpy
import stat
import subprocess
import sys
from pathlib import Path

import pytest
Expand Down Expand Up @@ -399,6 +400,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")])
Expand All @@ -407,10 +410,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()
Expand All @@ -432,6 +454,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
Expand Down
Loading