Rename every remaining voxip name to daynivo, with a one-time migration
CI / test-bookworm (push) Successful in 29s
CI / test-latest (push) Successful in 31s

Services (daynivo-kiosk-*, daynivo-network-guard, daynivo-boot-import),
programs in /usr/lib/daynivo-kiosk, folders /etc, /var/lib, /var/log and
/var/cache daynivo-kiosk*, the system user daynivo, DAYNIVO_* settings,
the plymouth theme, network profiles daynivo-eth0/wlan0, the image stage
and its first user daynivoadmin.

install.sh moves a kiosk installed up to 1.2.x over once: old services
stopped and removed, folders moved (configuration, password, browser
profile, logs) with symlinks under the old names, the voxip user renamed
with the same uid, network profiles renamed unless a change awaits
confirmation, and old leftovers removed. Tested by upgrading a configured
1.2.0 installation in a container, and running the install a second time.

Old names keep working where people type or place them: voxip-config.json,
voxip-authorized_keys, voxip-network-reset, voxip-kiosk-ctl, and
VOXIP_DEFAULT_URL / VOXIP_DEVICE_NAME for image builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rothmanandClaude Opus 5.5 committed 2026-11-05 23:58:03 +01:00
1 parent 6733aacb6e
commit eb0242c182
68 files changed
+527 -424

No files matched your search

+2 -2
View File
@@ -1,4 +1,4 @@
"""voxip-kiosk-agent — keeps the kiosk page where it should be.
"""daynivo-kiosk-agent — keeps the kiosk page where it should be.
Runs as user ``kiosk`` and steers Chromium through the DevTools Protocol on
127.0.0.1:
@@ -33,7 +33,7 @@ from .config import admin_url, load_config
from .paths import Paths, load_paths
from .validation import mask_url
log = logging.getLogger("voxip-kiosk-agent")
log = logging.getLogger("daynivo-kiosk-agent")
TICK_SECONDS = 5
PROBE_MAX_AGE_LOADING = 10
+6 -6
View File
@@ -1,6 +1,6 @@
"""voxip-kiosk-browser — prepares the display and runs Chromium.
"""daynivo-kiosk-browser — prepares the display and runs Chromium.
Started by ``voxip-kiosk-browser.service`` as user ``kiosk`` once the X
Started by ``daynivo-kiosk-browser.service`` as user ``kiosk`` once the X
server is up. Exit status tells systemd whether to restart: 1 when
``browser_restart`` is enabled (``Restart=on-failure`` restarts it and the
start limit catches crash loops), 0 when it is disabled.
@@ -25,7 +25,7 @@ from .config import admin_url, load_config
from .paths import Paths, load_paths
from .validation import mask_url, parse_resolution
log = logging.getLogger("voxip-kiosk-browser")
log = logging.getLogger("daynivo-kiosk-browser")
CDP_PORT = 9222
CHROMIUM_CANDIDATES = ("/usr/bin/chromium-browser", "/usr/bin/chromium")
@@ -212,14 +212,14 @@ def clean_profile(profile_dir: Path) -> None:
profile = data.setdefault("profile", {})
profile["exit_type"] = "Normal"
profile["exited_cleanly"] = True
tmp = prefs.with_suffix(".voxip-tmp")
tmp = prefs.with_suffix(".daynivo-tmp")
tmp.write_text(json.dumps(data), encoding="utf-8")
os.replace(tmp, prefs)
def profile_dirs(config: dict[str, Any]) -> tuple[Path, Path]:
runtime = Path(os.environ.get("RUNTIME_DIRECTORY", "/run/voxip-kiosk-browser"))
state = Path(os.environ.get("STATE_DIRECTORY", "/var/lib/voxip-kiosk-browser"))
runtime = Path(os.environ.get("RUNTIME_DIRECTORY", "/run/daynivo-kiosk-browser"))
state = Path(os.environ.get("STATE_DIRECTORY", "/var/lib/daynivo-kiosk-browser"))
profile = state / "profile" if config.get("browser_profile") == "persistent" else runtime / "profile"
if config.get("browser_profile") != "persistent" and (state / "profile").exists():
shutil.rmtree(state / "profile", ignore_errors=True)
+1 -1
View File
@@ -236,7 +236,7 @@ def save_config(path: Path, updates: dict[str, Any]) -> dict[str, Any]:
lock_path = path.with_name(".config.lock")
with _thread_lock:
# Read-only is enough for flock and works for every writer (the web
# interface as voxip, the helper as root), whoever created the file.
# interface as daynivo, the helper as root), whoever created the file.
lock_fd = os.open(str(lock_path), os.O_RDONLY | os.O_CREAT, 0o640)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX)
+3 -3
View File
@@ -48,7 +48,7 @@ def cmd_setup_code(_args) -> int:
return 1
code = setupcode.read(paths.setup_code_file)
if not code:
print("Nog geen setupcode; start de beheerinterface: sudo systemctl restart voxip-kiosk-manager")
print("Nog geen setupcode; start de beheerinterface: sudo systemctl restart daynivo-kiosk-manager")
return 1
print(code)
return 0
@@ -65,10 +65,10 @@ def cmd_reset_password(args) -> int:
code = setupcode.generate()
setupcode.write(paths.setup_code_file, code)
try:
os.chown(paths.setup_code_file, pwd.getpwnam("voxip").pw_uid, grp.getgrnam("kiosk").gr_gid)
os.chown(paths.setup_code_file, pwd.getpwnam("daynivo").pw_uid, grp.getgrnam("kiosk").gr_gid)
except KeyError:
pass
subprocess.run(["systemctl", "restart", "voxip-kiosk-manager.service"], check=False)
subprocess.run(["systemctl", "restart", "daynivo-kiosk-manager.service"], check=False)
print(f"Wachtwoord verwijderd. Nieuwe setupcode: {code}")
print("Open de beheerinterface en stel een nieuw wachtwoord in; de code staat ook op het kioskscherm.")
return 0
+3 -3
View File
@@ -1,4 +1,4 @@
"""voxip-kiosk-healthcheck — runs every minute (timer) as root.
"""daynivo-kiosk-healthcheck — runs every minute (timer) as root.
Keeps every kiosk service alive without hiding problems: each recovery is
logged, and a crash loop that hit systemd's start limit is retried only
@@ -21,7 +21,7 @@ from .cdp import CDP, CDPError
from .config import load_config
from .paths import Paths, load_paths
log = logging.getLogger("voxip-kiosk-healthcheck")
log = logging.getLogger("daynivo-kiosk-healthcheck")
ALWAYS_ON = (services.AGENT_UNIT, services.MANAGER_UNIT)
DISPLAY_UNITS = (services.X_UNIT, services.BROWSER_UNIT)
@@ -147,7 +147,7 @@ class HealthCheck:
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="voxip-kiosk-healthcheck")
parser = argparse.ArgumentParser(prog="daynivo-kiosk-healthcheck")
parser.add_argument("--check-only", action="store_true", help="alleen rapporteren, niets herstellen")
parser.add_argument("--json", action="store_true", help="rapport als JSON")
args = parser.parse_args(argv)
+19 -19
View File
@@ -1,11 +1,11 @@
"""voxip-kiosk-helper — the only privileged entry point of Daynivo Kiosk OS.
"""daynivo-kiosk-helper — the only privileged entry point of Daynivo Kiosk OS.
Runs as root, either through ``sudo -n`` from the web interface (user
``voxip``) or from systemd units. It accepts a fixed set of verbs, validates
``daynivo``) or from systemd units. It accepts a fixed set of verbs, validates
every argument itself, never invokes a shell and prints a single JSON
object on stdout.
Usage: voxip-kiosk-helper <verb> [args] (network-apply reads JSON on stdin)
Usage: daynivo-kiosk-helper <verb> [args] (network-apply reads JSON on stdin)
"""
from __future__ import annotations
@@ -27,13 +27,13 @@ from . import netconfig, updater
from . import validation as v
from .paths import Paths, default_paths
HELPER_PATH = "/usr/lib/voxip-kiosk/voxip-kiosk-helper"
ROLLBACK_UNIT = "voxip-network-rollback"
UPDATE_UNIT = "voxip-kiosk-update-now"
UPDATER_PATH = "/usr/lib/voxip-kiosk/voxip-kiosk-updater"
ACTIVATE_UNIT = "voxip-network-activate"
HELPER_PATH = "/usr/lib/daynivo-kiosk/daynivo-kiosk-helper"
ROLLBACK_UNIT = "daynivo-network-rollback"
UPDATE_UNIT = "daynivo-kiosk-update-now"
UPDATER_PATH = "/usr/lib/daynivo-kiosk/daynivo-kiosk-updater"
ACTIVATE_UNIT = "daynivo-network-activate"
BOOT_DIRS = (Path("/boot/firmware"), Path("/boot"))
# Files on the boot partition. The voxip-* names are from before the rename to
# Files on the boot partition. The daynivo-* names are from before the rename to
# Daynivo and keep working, so existing SD cards and instructions stay valid.
RESET_TRIGGERS = ("daynivo-network-reset", "voxip-network-reset")
IMPORT_CONFIGS = ("voxip-config.json", "daynivo-config.json") # applied in this order: the Daynivo name wins
@@ -83,24 +83,24 @@ class Helper:
# -- power & browser -------------------------------------------------
def reboot(self) -> dict[str, Any]:
self.run(["systemd-run", "--on-active=3", "--unit=voxip-power-reboot", "/bin/systemctl", "reboot"])
self.run(["systemd-run", "--on-active=3", "--unit=daynivo-power-reboot", "/bin/systemctl", "reboot"])
log(syslog.LOG_NOTICE, "Herstart aangevraagd via beheerinterface")
return {"message": "De Raspberry Pi wordt over enkele seconden herstart."}
def poweroff(self) -> dict[str, Any]:
self.run(["systemd-run", "--on-active=3", "--unit=voxip-power-off", "/bin/systemctl", "poweroff"])
self.run(["systemd-run", "--on-active=3", "--unit=daynivo-power-off", "/bin/systemctl", "poweroff"])
log(syslog.LOG_NOTICE, "Uitschakelen aangevraagd via beheerinterface")
return {"message": "De Raspberry Pi wordt over enkele seconden uitgeschakeld."}
def restart_browser(self) -> dict[str, Any]:
self.run(["systemctl", "reset-failed", "voxip-kiosk-browser.service", "voxip-kiosk-x.service"], check=False)
self.run(["systemctl", "restart", "--no-block", "voxip-kiosk-browser.service"])
self.run(["systemctl", "reset-failed", "daynivo-kiosk-browser.service", "daynivo-kiosk-x.service"], check=False)
self.run(["systemctl", "restart", "--no-block", "daynivo-kiosk-browser.service"])
log(syslog.LOG_NOTICE, "Chromium wordt herstart")
return {"message": "Chromium wordt herstart."}
def restart_manager(self) -> dict[str, Any]:
# Delayed so the web interface can still answer the request that asked for it.
self.run(["systemd-run", "--on-active=2", "--unit=voxip-manager-restart", "/bin/systemctl", "restart", "voxip-kiosk-manager.service"])
self.run(["systemd-run", "--on-active=2", "--unit=daynivo-manager-restart", "/bin/systemctl", "restart", "daynivo-kiosk-manager.service"])
log(syslog.LOG_NOTICE, "Beheerinterface wordt herstart")
return {"message": "De beheerinterface wordt herstart."}
@@ -326,7 +326,7 @@ class Helper:
def _chown_config(self, path: Path) -> None:
try:
uid = pwd.getpwnam("voxip").pw_uid
uid = pwd.getpwnam("daynivo").pw_uid
gid = grp.getgrnam("kiosk").gr_gid
except KeyError:
return
@@ -392,7 +392,7 @@ class Helper:
os.chown(ssh_dir, user.pw_uid, user.pw_gid)
sshd_dir = self.etc / "ssh" / "sshd_config.d"
sshd_dir.mkdir(parents=True, exist_ok=True)
(sshd_dir / "voxip-kiosk.conf").write_text(
(sshd_dir / "daynivo-kiosk.conf").write_text(
"# Beheerd door Daynivo Kiosk OS: alleen inloggen met SSH-sleutels.\n"
"PasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no\n",
encoding="utf-8",
@@ -449,9 +449,9 @@ def _parse_json(text: str) -> Any:
def main(argv: list[str] | None = None, helper: Helper | None = None, environ: dict[str, str] | None = None) -> int:
argv = sys.argv[1:] if argv is None else argv
environ = os.environ if environ is None else environ
syslog.openlog("voxip-kiosk-helper", 0, syslog.LOG_DAEMON)
syslog.openlog("daynivo-kiosk-helper", 0, syslog.LOG_DAEMON)
if not argv:
print(json.dumps({"ok": False, "error": "Gebruik: voxip-kiosk-helper <opdracht> [argumenten]"}))
print(json.dumps({"ok": False, "error": "Gebruik: daynivo-kiosk-helper <opdracht> [argumenten]"}))
return 2
verb, args = argv[0], argv[1:]
caller = environ.get("SUDO_USER")
@@ -460,7 +460,7 @@ def main(argv: list[str] | None = None, helper: Helper | None = None, environ: d
print(json.dumps({"ok": False, "error": "Opdracht niet toegestaan"}))
return 1
if helper is None and os.geteuid() != 0:
print(json.dumps({"ok": False, "error": "voxip-kiosk-helper moet als root draaien"}))
print(json.dumps({"ok": False, "error": "daynivo-kiosk-helper moet als root draaien"}))
return 1
helper = helper or Helper()
stdin = sys.stdin.read(16385) if verb == "network-apply" else ""
+3 -3
View File
@@ -1,4 +1,4 @@
"""voxip-kiosk-manager — starts the web interface under gunicorn.
"""daynivo-kiosk-manager — starts the web interface under gunicorn.
Reads port and HTTPS settings from the kiosk configuration, creates a
self-signed certificate when HTTPS is enabled and none exists yet, then
@@ -19,8 +19,8 @@ from . import logutil
from .config import load_config
from .paths import Paths, load_paths
log = logging.getLogger("voxip-kiosk-manager")
PYTHON_LIB = "/usr/lib/voxip-kiosk/python"
log = logging.getLogger("daynivo-kiosk-manager")
PYTHON_LIB = "/usr/lib/daynivo-kiosk/python"
def ensure_certificate(paths: Paths) -> tuple[Path, Path]:
+1 -1
View File
@@ -15,7 +15,7 @@ from typing import Any
from . import validation as v
INTERFACES = {"eth0": "ethernet", "wlan0": "wifi"}
PROFILE_PREFIX = "voxip-"
PROFILE_PREFIX = "daynivo-"
ROLLBACK_SECONDS = 180
+19 -19
View File
@@ -1,7 +1,7 @@
"""Filesystem locations used by every Daynivo Kiosk component.
The defaults match the installed layout. Unprivileged components may
override them through ``VOXIP_*`` environment variables (used by the test
override them through ``DAYNIVO_*`` environment variables (used by the test
suite and for local development). The root helper always uses
:func:`default_paths` and never reads the environment.
"""
@@ -15,15 +15,15 @@ from pathlib import Path
@dataclass(frozen=True)
class Paths:
config_dir: Path = Path("/etc/voxip-kiosk")
state_dir: Path = Path("/var/lib/voxip-kiosk")
network_state_dir: Path = Path("/var/lib/voxip-kiosk-network")
agent_run_dir: Path = Path("/run/voxip-kiosk-agent")
browser_run_dir: Path = Path("/run/voxip-kiosk-browser")
health_run_dir: Path = Path("/run/voxip-kiosk-health")
share_dir: Path = Path("/usr/share/voxip-kiosk")
log_dir: Path = Path("/var/log/voxip-kiosk")
helper: Path = Path("/usr/lib/voxip-kiosk/voxip-kiosk-helper")
config_dir: Path = Path("/etc/daynivo-kiosk")
state_dir: Path = Path("/var/lib/daynivo-kiosk")
network_state_dir: Path = Path("/var/lib/daynivo-kiosk-network")
agent_run_dir: Path = Path("/run/daynivo-kiosk-agent")
browser_run_dir: Path = Path("/run/daynivo-kiosk-browser")
health_run_dir: Path = Path("/run/daynivo-kiosk-health")
share_dir: Path = Path("/usr/share/daynivo-kiosk")
log_dir: Path = Path("/var/log/daynivo-kiosk")
helper: Path = Path("/usr/lib/daynivo-kiosk/daynivo-kiosk-helper")
nm_connections_dir: Path = Path("/etc/NetworkManager/system-connections")
@property
@@ -90,15 +90,15 @@ def default_paths() -> Paths:
_ENV = {
"config_dir": "VOXIP_CONFIG_DIR",
"state_dir": "VOXIP_STATE_DIR",
"network_state_dir": "VOXIP_NETWORK_STATE_DIR",
"agent_run_dir": "VOXIP_AGENT_RUN_DIR",
"browser_run_dir": "VOXIP_BROWSER_RUN_DIR",
"health_run_dir": "VOXIP_HEALTH_RUN_DIR",
"share_dir": "VOXIP_SHARE_DIR",
"log_dir": "VOXIP_LOG_DIR",
"helper": "VOXIP_HELPER",
"config_dir": "DAYNIVO_CONFIG_DIR",
"state_dir": "DAYNIVO_STATE_DIR",
"network_state_dir": "DAYNIVO_NETWORK_STATE_DIR",
"agent_run_dir": "DAYNIVO_AGENT_RUN_DIR",
"browser_run_dir": "DAYNIVO_BROWSER_RUN_DIR",
"health_run_dir": "DAYNIVO_HEALTH_RUN_DIR",
"share_dir": "DAYNIVO_SHARE_DIR",
"log_dir": "DAYNIVO_LOG_DIR",
"helper": "DAYNIVO_HELPER",
}
+6 -6
View File
@@ -5,11 +5,11 @@ from __future__ import annotations
import subprocess
from typing import Any
BROWSER_UNIT = "voxip-kiosk-browser.service"
X_UNIT = "voxip-kiosk-x.service"
AGENT_UNIT = "voxip-kiosk-agent.service"
MANAGER_UNIT = "voxip-kiosk-manager.service"
HEALTH_UNIT = "voxip-kiosk-healthcheck.service"
BROWSER_UNIT = "daynivo-kiosk-browser.service"
X_UNIT = "daynivo-kiosk-x.service"
AGENT_UNIT = "daynivo-kiosk-agent.service"
MANAGER_UNIT = "daynivo-kiosk-manager.service"
HEALTH_UNIT = "daynivo-kiosk-healthcheck.service"
# Units whose logs the web interface may show (label → journalctl -u args).
LOG_SOURCES: dict[str, tuple[str, list[str]]] = {
@@ -18,7 +18,7 @@ LOG_SOURCES: dict[str, tuple[str, list[str]]] = {
"agent": ("Kiosk-agent", [AGENT_UNIT]),
"health": ("Healthcheck", [HEALTH_UNIT]),
"manager": ("Beheerinterface", [MANAGER_UNIT]),
"network": ("Netwerk", ["NetworkManager.service", "voxip-network-guard.service", "voxip-network-rollback.service", "voxip-network-activate.service"]),
"network": ("Netwerk", ["NetworkManager.service", "daynivo-network-guard.service", "daynivo-network-rollback.service", "daynivo-network-activate.service"]),
"system": ("Volledig systeem", []),
}
LOG_LINES = (50, 100, 200, 500)
+7 -7
View File
@@ -1,4 +1,4 @@
"""voxip-kiosk-updater — installs new Daynivo Kiosk OS releases (runs as root).
"""daynivo-kiosk-updater — installs new Daynivo Kiosk OS releases (runs as root).
A release is a tag ``vX.Y.Z`` on the Gitea repository with two assets:
``daynivo-kiosk-os-X.Y.Z.tar.gz`` and its ``.sha256``. The updater
@@ -11,9 +11,9 @@ A release is a tag ``vX.Y.Z`` on the Gitea repository with two assets:
It never installs an older or equal version. Usage::
voxip-kiosk-updater --check # only check
voxip-kiosk-updater --auto # check; install when auto_update is on (daily timer)
voxip-kiosk-updater --install # check and install when newer (button in the web interface)
daynivo-kiosk-updater --check # only check
daynivo-kiosk-updater --auto # check; install when auto_update is on (daily timer)
daynivo-kiosk-updater --install # check and install when newer (button in the web interface)
"""
from __future__ import annotations
@@ -39,12 +39,12 @@ from . import logutil
from .config import atomic_write_json, load_config
from .paths import Paths, default_paths, read_version
log = logging.getLogger("voxip-kiosk-updater")
log = logging.getLogger("daynivo-kiosk-updater")
RELEASE_API = "https://git.rothman.nl/api/v1/repos/rothman/daynivo-kiosk-os/releases/latest"
ASSET_PREFIX = "daynivo-kiosk-os-"
RELEASE_DIR = Path("/opt/daynivo-kiosk-os")
CACHE_DIR = Path("/var/cache/voxip-kiosk")
CACHE_DIR = Path("/var/cache/daynivo-kiosk")
KEEP_RELEASES = 2
MAX_DOWNLOAD = 64 * 1024 * 1024
USER_AGENT = "Daynivo-Kiosk-OS/updater"
@@ -219,7 +219,7 @@ class Updater:
def main(argv: list[str] | None = None, updater: Updater | None = None) -> int:
parser = argparse.ArgumentParser(prog="voxip-kiosk-updater", description="Daynivo Kiosk OS bijwerken.")
parser = argparse.ArgumentParser(prog="daynivo-kiosk-updater", description="Daynivo Kiosk OS bijwerken.")
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--check", action="store_true", help="alleen controleren")
mode.add_argument("--auto", action="store_true", help="controleren en bijwerken als automatisch bijwerken aan staat")
+2 -2
View File
@@ -48,7 +48,7 @@ def create_app(paths: Paths | None = None, *, helper: Any = None, cdp: Any = Non
app = Flask(__name__)
app.secret_key = _secret_key(paths)
app.config.update(
SESSION_COOKIE_NAME="voxip_session",
SESSION_COOKIE_NAME="daynivo_session",
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Strict",
SESSION_COOKIE_SECURE=config["admin_https"],
@@ -58,7 +58,7 @@ def create_app(paths: Paths | None = None, *, helper: Any = None, cdp: Any = Non
)
credentials = CredentialStore(paths.admin_file, paths.setup_code_file)
credentials.ensure_setup_code()
app.extensions["voxip"] = Services(
app.extensions["daynivo"] = Services(
paths=paths,
credentials=credentials,
throttle=LoginThrottle(),
+1 -1
View File
@@ -21,7 +21,7 @@ HASHER = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=2, hash_len=
MIN_LENGTH = 10
MAX_LENGTH = 128
WEAK = {"admin", "administrator", "password", "wachtwoord", "raspberry", "voxip", "kiosk", "welkom", "welcome", "qwerty", "123456"}
WEAK = {"admin", "administrator", "password", "wachtwoord", "raspberry", "daynivo", "kiosk", "welkom", "welcome", "qwerty", "123456"}
def password_problem(password: str, confirm: str | None = None) -> str | None:
+2 -2
View File
@@ -22,14 +22,14 @@ from . import security
from .auth import password_problem
bp = Blueprint("ui", __name__)
audit_log = logging.getLogger("voxip-kiosk-audit")
audit_log = logging.getLogger("daynivo-kiosk-audit")
IDLE_TIMEOUT = 3600
PUBLIC_ENDPOINTS = {"ui.login", "ui.setup", "static"}
def svc():
return current_app.extensions["voxip"]
return current_app.extensions["daynivo"]
def cfg() -> dict[str, Any]: