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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ permissions:
jobs:
tests:
# COVERAGE.md: the conf script at 100 percent (42/42), the first boot
# hook at 100 (31/31) and its library at 100 (56/56), 70 bats tests. The
# hook at 100 (31/31) and its library at 100 (56/56), 73 bats tests. The
# threshold is the measured number and is only ever raised (decision
# 0006). This job produces the check
# "tests / coverage", the required status on main; the job id is part of
Expand Down
4 changes: 2 additions & 2 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ repository does not carry a boot test of its own.
| overlay/usr/lib/inithooks/firstboot.d/35redispass | tests/hook.bats (18 tests) | 100 percent (31/31) under kcov | every path, including the four failures that matter |
| conf | tests/conf.bats (20 tests) | 100 percent (42/42) under kcov | the include appended once and last, the fragments, the running server on both families, every refusal, the CRLF an INFO reply really carries, the server log a failure prints, and the packaged log file the check must not leave behind |
| overlay/etc/redis/redis.conf.d/\* | tests/conf.bats (2 tests) | not executable | asserted as content: two literal bind addresses and no name, the default account restricted to `+info`, the administrative account published `off`, and no `requirepass` anywhere |
| overlay/usr/lib/inithooks/bin/redispass.py | none | 0 | dialog wrapper, only reached with a terminal attached |
| overlay/usr/lib/inithooks/bin/redispass.py | tests/dialog.bats (3 tests) | not measured (kcov measures the shell) | dialog wrapper, run as the hook runs it inside a pseudo terminal: the answer reaches the hook and the box is drawn on the terminal |

Total over the three measured shell files: **100 percent (129/129)**,
70 bats tests over four files (conf, hook, library, unit shape).
73 bats tests over five files (conf, hook, library, unit shape, dialog).
`tests/coverage.sh` fails below `COVERAGE_THRESHOLD`, which the workflow sets
to 100, the measured number. It is only ever raised (decision 0006).

Expand Down
13 changes: 13 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
unit-redis-1.0.3 (1) keel; urgency=low

* bin/redispass.py draws its password box on the terminal.
firstboot.d/35redispass captures the script's standard output for
DB_PASS=, and dialog draws on standard output, so an interactive first
boot would freeze at the Redis password with the box drawn into the
hook's command substitution, as keel-wordpress 19.0-3 did at the
MariaDB password on Proxmox (2026-09-30). The answers now go to a copy
of that pipe and standard output is the terminal. tests/dialog.bats
runs the script as the hook does, inside a pseudo terminal.

-- Keel Linux maintainers <admin@keellinux.org> Wed, 30 Sep 2026 04:30:00 +0000

unit-redis-1.0.2 (1) keel; urgency=low

* Declare the administrative account once. 20-keel-acl.conf published it
Expand Down
40 changes: 39 additions & 1 deletion overlay/usr/lib/inithooks/bin/redispass.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
Syntax: redispass.py NAME [NAME ...] NAME is DB_PASS
"""

import os
import sys

from libinithooks.dialog_wrapper import Dialog

TTY = "/dev/tty"

TITLE = "Keel - First boot configuration"


Expand All @@ -27,13 +30,48 @@ def ask(name: str, dialog: Dialog) -> str:
raise SystemExit(f"redispass.py: unknown value name {name!r}")


def terminal_path(tty: str = TTY) -> str:
"""The terminal to draw on: the one the hook checked on standard input,
else the controlling terminal"""
try:
return os.ttyname(sys.stdin.fileno())
except OSError:
return tty


def answers_out(tty: str = TTY):
"""The hook's pipe for the answers, with standard output on the terminal

The hook reads this script's standard output, and dialog draws its
screen on standard output: left there, the password box is drawn into
the hook's pipe, and the console shows a frozen screen waiting for a
password nobody can see (2026-09-30, keel-wordpress on Proxmox). So
the pipe is kept on a new descriptor for the KEY=value lines, and
standard output, which dialog inherits, becomes the terminal.
"""
path = terminal_path(tty)
try:
terminal = os.open(path, os.O_WRONLY)
except OSError as e:
raise SystemExit(
f"redispass.py: no terminal to draw the dialog on ({path}:"
f" {e.strerror}); declare secrets.db_password in the instance"
" description instead")
answers = os.fdopen(os.dup(sys.stdout.fileno()), "w")
os.dup2(terminal, sys.stdout.fileno())
os.close(terminal)
return answers


def main(names: list[str]) -> int:
if not names:
print(__doc__, file=sys.stderr)
return 1
answers = answers_out()
dialog = Dialog(TITLE)
for name in names:
print(f"{name}={ask(name, dialog)}")
print(f"{name}={ask(name, dialog)}", file=answers)
answers.close()
return 0


Expand Down
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ organization's `test-shell.yml` and the check is `tests / coverage`.
| File | What it covers |
| --- | --- |
| `conf.bats` | the build time conf script, run for real against a scratch tree, with `redis-server` and `redis-cli` as PATH stubs. Also the two configuration fragments, asserted as content |
| `dialog.bats` | `bin/redispass.py` run as the hook runs it, output redirected, inside a pseudo terminal (`script -qec`), with a stand-in for libinithooks whose dialog refuses to draw anywhere but a terminal: the answer goes to the hook and the box to the screen |
| `hook.bats` | the first boot hook, run for real against a scratch tree, with `systemctl` and `redis-cli` as PATH stubs and a stub dialog on a scratch `INITHOOKS_PATH` whose `lib` is a symlink to the real library, so kcov measures the file the component ships |
| `redis.bats` | the library behind the hook, function by function |
| `unit.bats` | the shape fab and `bt-layer` require of a unit: the executable conf, the plan, the version against the changelog, the overlay's exact file list, and the conf script as POSIX shell under shellcheck |
Expand Down
60 changes: 60 additions & 0 deletions tests/dialog.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bats
# bin/redispass.py draws its dialog on the terminal, not in the hook's pipe.
#
# firstboot.d/35redispass reads the script's standard output for KEY=value,
# and dialog draws its screen on standard output. On 2026-09-30 a
# keel-wordpress first boot on Proxmox froze at a password box because the
# box was drawn into such a pipe, and this script had the same shape. These
# tests run the script as the hook does, output redirected, inside a real
# pseudo terminal (script(1)), with a stand-in for libinithooks whose dialog
# refuses to draw anywhere but a terminal.

setup() {
here="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)"
REDISPASS="$here/../overlay/usr/lib/inithooks/bin/redispass.py"
FAKE="$BATS_TEST_TMPDIR/lib"
mkdir -p "$FAKE/libinithooks"
: > "$FAKE/libinithooks/__init__.py"
cat > "$FAKE/libinithooks/dialog_wrapper.py" << 'EOF'
import os


class Dialog:
def __init__(self, title):
self.title = title

def get_password(self, title, text):
# what dialog needs to be seen: a terminal on standard output
if not os.isatty(1):
raise SystemExit("dialog would draw into a pipe")
return "typed-at-the-console"
EOF
OUT="$BATS_TEST_TMPDIR/answers"
}

run_as_the_hook() {
# stdin and /dev/tty are the pseudo terminal; stdout is a file, as the
# hook's command substitution makes it a pipe
script -qec "PYTHONPATH='$FAKE' python3 '$REDISPASS' $* > '$OUT'" /dev/null
}

@test "the answer reaches the hook while the dialog draws on the terminal" {
run run_as_the_hook DB_PASS
[ "$status" -eq 0 ]
[ "$(cat "$OUT")" = "DB_PASS=typed-at-the-console" ]
}

@test "without any terminal it says so instead of a traceback" {
# setsid: no controlling terminal, and standard input is not one either
PYTHONPATH="$FAKE" run setsid -w python3 "$REDISPASS" DB_PASS < /dev/null
[ "$status" -ne 0 ]
[[ "$output" == *"no terminal to draw the dialog on"* ]]
[[ "$output" == *"secrets.db_password"* ]]
[[ "$output" != *"Traceback"* ]]
}

@test "without a name it prints its usage and fails" {
PYTHONPATH="$FAKE" run python3 "$REDISPASS"
[ "$status" -eq 1 ]
[[ "$output" == *"Syntax: redispass.py NAME"* ]]
}
2 changes: 1 addition & 1 deletion version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.2
1.0.3
Loading