Compare commits
7
Commits
33ef898fcc
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e60f9bf9 | ||
|
|
eb0242c182 | ||
|
|
6733aacb6e | ||
|
|
d0f3206ff4 | ||
|
|
b0840d240e | ||
|
|
348644a48a | ||
|
|
f8303754f7 |
No files matched your search
@@ -1,8 +1,11 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
|
# Only pushes to this repository, no pull_request: the runner allows privileged
|
||||||
|
# containers, and a pull request from a fork could otherwise run its own
|
||||||
|
# workflow with root access on the runner machine.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
branches: ["**"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-bookworm:
|
test-bookworm:
|
||||||
@@ -10,16 +13,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: debian:bookworm
|
container: debian:bookworm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install Debian packages
|
- name: Install Debian packages
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
python3 python3-flask python3-argon2 python3-websocket python3-pytest shellcheck
|
ca-certificates git python3 python3-flask python3-argon2 python3-websocket python3-pytest shellcheck
|
||||||
|
- name: Checkout
|
||||||
|
# Plain git: JavaScript actions need Node, which debian:bookworm doesn't have.
|
||||||
|
run: |
|
||||||
|
git clone "${{ github.server_url }}/${{ github.repository }}.git" .
|
||||||
|
git checkout "${{ github.sha }}"
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: python3 -m pytest -p no:cacheprovider
|
run: python3 -m pytest -p no:cacheprovider
|
||||||
- name: Shellcheck
|
- name: Shellcheck
|
||||||
run: shellcheck -S warning install.sh update.sh uninstall.sh image/build.sh image/stage-voxip/prerun.sh image/stage-voxip/00-voxip/00-run.sh bin/voxip-kiosk-x
|
run: shellcheck -S warning install.sh install-online.sh update.sh uninstall.sh tools/release-upload.sh image/build.sh image/stage-daynivo/prerun.sh image/stage-daynivo/00-daynivo/00-run.sh bin/daynivo-kiosk-x
|
||||||
|
|
||||||
test-latest:
|
test-latest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Gitea Actions: a tag vX.Y.Z (matching VERSION) becomes a release with
|
||||||
|
# daynivo-kiosk-os-X.Y.Z.tar.gz (+ .sha256) installed by install-online.sh and the updater
|
||||||
|
# install-online.sh the one-line installer
|
||||||
|
# <date>-daynivo-kiosk-os-X.Y.Z.img.xz (+ .sha256), built afterwards (30–90+ minutes)
|
||||||
|
#
|
||||||
|
# Runner requirements: act_runner with the "ubuntu-latest" label, and for the
|
||||||
|
# image job `container.privileged: true` in its config (pi-gen needs loop
|
||||||
|
# devices and binfmt_misc). A secret RELEASE_TOKEN (repository write access)
|
||||||
|
# is used when set, otherwise the job's own token.
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: debian:bookworm
|
||||||
|
steps:
|
||||||
|
- name: Tools
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates curl git python3 python3-flask python3-argon2 python3-websocket python3-pytest shellcheck
|
||||||
|
- name: Checkout
|
||||||
|
# Plain git: JavaScript actions need Node, which debian:bookworm doesn't have.
|
||||||
|
run: git clone --depth 1 --branch "${{ github.ref_name }}" "${{ github.server_url }}/${{ github.repository }}.git" src
|
||||||
|
- name: Tag matches VERSION
|
||||||
|
working-directory: src
|
||||||
|
run: test "v$(tr -d '[:space:]' < VERSION)" = "${{ github.ref_name }}"
|
||||||
|
- name: Tests
|
||||||
|
working-directory: src
|
||||||
|
run: |
|
||||||
|
python3 -m pytest -q -p no:cacheprovider
|
||||||
|
shellcheck -S warning install.sh install-online.sh update.sh uninstall.sh tools/release-upload.sh image/build.sh
|
||||||
|
- name: Package
|
||||||
|
working-directory: src
|
||||||
|
run: |
|
||||||
|
V="$(tr -d '[:space:]' < VERSION)"
|
||||||
|
mkdir -p ../dist
|
||||||
|
git archive --prefix="daynivo-kiosk-os-$V/" -o "../dist/daynivo-kiosk-os-$V.tar.gz" HEAD
|
||||||
|
(cd ../dist && sha256sum "daynivo-kiosk-os-$V.tar.gz" > "daynivo-kiosk-os-$V.tar.gz.sha256")
|
||||||
|
cp install-online.sh ../dist/
|
||||||
|
- name: Publish
|
||||||
|
run: src/tools/release-upload.sh "${{ github.ref_name }}" dist/*
|
||||||
|
|
||||||
|
image:
|
||||||
|
needs: package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: debian:bookworm
|
||||||
|
options: --privileged
|
||||||
|
timeout-minutes: 360
|
||||||
|
steps:
|
||||||
|
- name: Tools (pi-gen)
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates curl git python3 openssl coreutils quilt parted qemu-user-static binfmt-support \
|
||||||
|
debootstrap zerofree zip dosfstools libarchive-tools libcap2-bin grep rsync xz-utils file bc gpg \
|
||||||
|
pigz xxd arch-test bmap-tools kmod fdisk udev
|
||||||
|
- name: Enable ARM emulation
|
||||||
|
run: |
|
||||||
|
mountpoint -q /proc/sys/fs/binfmt_misc || mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc
|
||||||
|
update-binfmts --enable qemu-arm
|
||||||
|
- name: Checkout
|
||||||
|
run: git clone --depth 1 --branch "${{ github.ref_name }}" "${{ github.server_url }}/${{ github.repository }}.git" src
|
||||||
|
- name: Build image
|
||||||
|
working-directory: src
|
||||||
|
run: ./image/build.sh --native
|
||||||
|
- name: Publish
|
||||||
|
run: src/tools/release-upload.sh "${{ github.ref_name }}" src/image/deploy/*.img.xz src/image/deploy/*.img.xz.sha256
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
PY ?= .venv/bin/python
|
PY ?= .venv/bin/python
|
||||||
DEV := $(CURDIR)/.dev
|
DEV := $(CURDIR)/.dev
|
||||||
DEV_ENV := PYTHONPATH=src VOXIP_CONFIG_DIR=$(DEV)/etc VOXIP_STATE_DIR=$(DEV)/state VOXIP_LOG_DIR=$(DEV)/log \
|
DEV_ENV := PYTHONPATH=src DAYNIVO_CONFIG_DIR=$(DEV)/etc DAYNIVO_STATE_DIR=$(DEV)/state DAYNIVO_LOG_DIR=$(DEV)/log \
|
||||||
VOXIP_SHARE_DIR=$(DEV)/share VOXIP_AGENT_RUN_DIR=$(DEV)/run VOXIP_BROWSER_RUN_DIR=$(DEV)/run \
|
DAYNIVO_SHARE_DIR=$(DEV)/share DAYNIVO_AGENT_RUN_DIR=$(DEV)/run DAYNIVO_BROWSER_RUN_DIR=$(DEV)/run \
|
||||||
VOXIP_NETWORK_STATE_DIR=$(DEV)/net VOXIP_HELPER=/nonexistent/voxip-kiosk-helper
|
DAYNIVO_NETWORK_STATE_DIR=$(DEV)/net DAYNIVO_HELPER=/nonexistent/daynivo-kiosk-helper
|
||||||
|
|
||||||
.PHONY: venv test lint test-bookworm dev image clean
|
.PHONY: venv test lint test-bookworm dev image clean
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ test:
|
|||||||
$(PY) -m pytest
|
$(PY) -m pytest
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
shellcheck -S warning install.sh update.sh uninstall.sh image/build.sh image/stage-voxip/prerun.sh \
|
shellcheck -S warning install.sh update.sh uninstall.sh image/build.sh image/stage-daynivo/prerun.sh \
|
||||||
image/stage-voxip/00-voxip/00-run.sh bin/voxip-kiosk-x
|
image/stage-daynivo/00-daynivo/00-run.sh bin/daynivo-kiosk-x
|
||||||
|
|
||||||
# The test suite against the exact Python/Flask/argon2 versions that Raspberry Pi OS Bookworm ships.
|
# The test suite against the exact Python/Flask/argon2 versions that Raspberry Pi OS Bookworm ships.
|
||||||
test-bookworm:
|
test-bookworm:
|
||||||
@@ -22,7 +22,7 @@ test-bookworm:
|
|||||||
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||||
python3 python3-flask python3-argon2 python3-websocket python3-pytest shellcheck >/dev/null && \
|
python3 python3-flask python3-argon2 python3-websocket python3-pytest shellcheck >/dev/null && \
|
||||||
cp -r /src /work && cd /work && rm -rf .venv && python3 -m pytest -p no:cacheprovider && \
|
cp -r /src /work && cd /work && rm -rf .venv && python3 -m pytest -p no:cacheprovider && \
|
||||||
shellcheck -S warning install.sh update.sh uninstall.sh image/build.sh bin/voxip-kiosk-x'
|
shellcheck -S warning install.sh install-online.sh update.sh uninstall.sh tools/release-upload.sh image/build.sh bin/daynivo-kiosk-x'
|
||||||
|
|
||||||
# Web interface on http://127.0.0.1:8080 with throwaway state in .dev/ (system actions fail gracefully).
|
# Web interface on http://127.0.0.1:8080 with throwaway state in .dev/ (system actions fail gracefully).
|
||||||
dev:
|
dev:
|
||||||
@@ -30,7 +30,7 @@ dev:
|
|||||||
test -f $(DEV)/etc/config.json || cp config/config.default.json $(DEV)/etc/config.json
|
test -f $(DEV)/etc/config.json || cp config/config.default.json $(DEV)/etc/config.json
|
||||||
cp VERSION $(DEV)/share/VERSION
|
cp VERSION $(DEV)/share/VERSION
|
||||||
@echo "Setupcode: zie $(DEV)/etc/setup-code (wordt aangemaakt bij de start)"
|
@echo "Setupcode: zie $(DEV)/etc/setup-code (wordt aangemaakt bij de start)"
|
||||||
$(DEV_ENV) $(PY) -m flask --app 'voxip_kiosk.web:create_app()' run --host 127.0.0.1 --port 8080
|
$(DEV_ENV) $(PY) -m flask --app 'daynivo_kiosk.web:create_app()' run --host 127.0.0.1 --port 8080
|
||||||
|
|
||||||
image:
|
image:
|
||||||
./image/build.sh
|
./image/build.sh
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ webinterface **Daynivo Kiosk Manager** op `http://kiosk.local:8080`.
|
|||||||
een setupcode die op het scherm staat.
|
een setupcode die op het scherm staat.
|
||||||
- Netwerkwijzigingen met automatische rollback.
|
- Netwerkwijzigingen met automatische rollback.
|
||||||
- Geen klantspecifieke code: alles per klant staat in
|
- Geen klantspecifieke code: alles per klant staat in
|
||||||
`/etc/voxip-kiosk/config.json` en kan bij uitrol via de bootpartitie
|
`/etc/daynivo-kiosk/config.json` en kan bij uitrol via de bootpartitie
|
||||||
worden meegegeven.
|
worden meegegeven.
|
||||||
|
|
||||||
De architectuur en de veiligheidskeuzes staan in
|
De architectuur en de veiligheidskeuzes staan in
|
||||||
@@ -23,14 +23,15 @@ hardware staat in [`docs/HARDWARE-TEST.md`](docs/HARDWARE-TEST.md).
|
|||||||
|
|
||||||
### Optie A — kant-en-klare image
|
### Optie A — kant-en-klare image
|
||||||
|
|
||||||
1. Bouw de image (zie hieronder) of gebruik een uitgebrachte
|
1. Download de nieuwste `*-daynivo-kiosk-os-*.img.xz` bij de
|
||||||
`*-daynivo-kiosk-os-*.img.xz`.
|
[releases](https://git.rothman.nl/rothman/daynivo-kiosk-os/releases),
|
||||||
|
of bouw hem zelf (zie hieronder).
|
||||||
2. Schrijf hem met **Raspberry Pi Imager** (*Eigen image gebruiken*) of
|
2. Schrijf hem met **Raspberry Pi Imager** (*Eigen image gebruiken*) of
|
||||||
**balenaEtcher** naar een microSD-kaart van 16 GB of groter.
|
**balenaEtcher** naar een microSD-kaart van 16 GB of groter.
|
||||||
3. Optioneel, in Raspberry Pi Imager → *OS-aanpassingen*: wifi, gebruiker
|
3. Optioneel, in Raspberry Pi Imager → *OS-aanpassingen*: wifi, gebruiker
|
||||||
met wachtwoord en/of SSH-sleutel, tijdzone. Zonder aanpassingen heeft de
|
met wachtwoord en/of SSH-sleutel, tijdzone. Zonder aanpassingen heeft de
|
||||||
image **geen** bruikbaar wachtwoord en staat SSH uit.
|
image **geen** bruikbaar wachtwoord en staat SSH uit.
|
||||||
4. Optioneel: zet een `voxip-config.json` op de bootpartitie (zie
|
4. Optioneel: zet een `daynivo-config.json` op de bootpartitie (zie
|
||||||
[Uitrol bij meerdere klanten](#uitrol-bij-meerdere-klanten)).
|
[Uitrol bij meerdere klanten](#uitrol-bij-meerdere-klanten)).
|
||||||
5. Start de Pi. Bij de eerste boot wordt de rootpartitie automatisch
|
5. Start de Pi. Bij de eerste boot wordt de rootpartitie automatisch
|
||||||
vergroot tot de hele SD-kaart. Daarna verschijnt een setupscherm met de
|
vergroot tot de hele SD-kaart. Daarna verschijnt een setupscherm met de
|
||||||
@@ -44,14 +45,23 @@ Vereist: Raspberry Pi OS Lite Bookworm of Trixie met NetworkManager (de
|
|||||||
standaard sinds Bookworm).
|
standaard sinds Bookworm).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install -y git
|
curl -fsSL https://daynivo.nl/kiosk/install.sh | sudo bash -s -- \
|
||||||
git clone <url-van-deze-repository> daynivo-kiosk-os
|
--url "https://schermen-ijsselflat.voxip.nl" --name "IJsselflat" --hostname ijsselflat
|
||||||
|
```
|
||||||
|
|
||||||
|
`install-online.sh` (dat is wat daynivo.nl serveert, en het hangt ook bij
|
||||||
|
elke release) haalt de nieuwste release op, controleert de SHA-256, pakt hem
|
||||||
|
uit in `/opt/daynivo-kiosk-os/<versie>` en draait daar `install.sh`. Git is
|
||||||
|
niet nodig. Vanuit een clone kan het ook:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.rothman.nl/rothman/daynivo-kiosk-os.git
|
||||||
cd daynivo-kiosk-os
|
cd daynivo-kiosk-os
|
||||||
sudo ./install.sh --url "https://schermen-ijsselflat.voxip.nl" --name "IJsselflat" --hostname ijsselflat
|
sudo ./install.sh --url "https://schermen-ijsselflat.voxip.nl" --name "IJsselflat" --hostname ijsselflat
|
||||||
```
|
```
|
||||||
|
|
||||||
Alle opties zijn optioneel. Het script installeert de benodigde pakketten,
|
Alle opties zijn optioneel. Het script installeert de benodigde pakketten,
|
||||||
maakt de gebruikers `kiosk` en `voxip` aan, installeert de services en
|
maakt de gebruikers `kiosk` en `daynivo` aan, installeert de services en
|
||||||
start de kiosk. Aan het eind toont het de setupcode.
|
start de kiosk. Aan het eind toont het de setupcode.
|
||||||
|
|
||||||
Draaide hier eerder de Daynivo-kiosk (`raspberry-pi/install-kiosk.sh` uit
|
Draaide hier eerder de Daynivo-kiosk (`raspberry-pi/install-kiosk.sh` uit
|
||||||
@@ -60,17 +70,59 @@ de Daynivo-repository)? Dan neemt `install.sh` de display-URL (inclusief
|
|||||||
|
|
||||||
### Updaten
|
### Updaten
|
||||||
|
|
||||||
|
In de webinterface onder **Systeem → Software-updates**: *Nu controleren*,
|
||||||
|
*Bijwerken naar …*, en het vinkje **Automatisch bijwerken**. Elke nacht
|
||||||
|
tussen 03:00 en 05:00 (`daynivo-kiosk-update.timer`) kijkt de kiosk of er een
|
||||||
|
nieuwe release is; met het vinkje aan installeert hij die meteen. Dat werkt
|
||||||
|
ook op een apparaat dat met de image of vanuit een clone is geïnstalleerd.
|
||||||
|
Vanaf de commandoregel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo /usr/lib/daynivo-kiosk/daynivo-kiosk-updater --check # alleen kijken
|
||||||
|
sudo /usr/lib/daynivo-kiosk/daynivo-kiosk-updater --install # nieuwste release installeren
|
||||||
|
```
|
||||||
|
|
||||||
|
De updater installeert alleen een nieuwere versie met een kloppende
|
||||||
|
checksum, pakt hem uit in `/opt/daynivo-kiosk-os/<versie>` (de laatste twee
|
||||||
|
blijven bewaard) en draait daar `update.sh --no-pull`. Vanuit een clone kan
|
||||||
|
het ook met git:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd daynivo-kiosk-os
|
cd daynivo-kiosk-os
|
||||||
sudo ./update.sh # git pull --ff-only + back-up van /etc/voxip-kiosk + install.sh --upgrade
|
sudo ./update.sh # git pull --ff-only + back-up van /etc/daynivo-kiosk + install.sh --upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
Een update overschrijft nooit bestaande configuratie of het
|
Een update overschrijft nooit bestaande configuratie of het
|
||||||
beheerderswachtwoord. Back-ups staan in `/var/backups/voxip-kiosk-*.tar.gz`
|
beheerderswachtwoord. Back-ups staan in `/var/backups/daynivo-kiosk-*.tar.gz`
|
||||||
(de laatste vijf). Op een apparaat met de image staat de broncode in
|
(de laatste vijf). Op een apparaat met de image staat de broncode in
|
||||||
`/usr/src/voxip-kiosk`. Clone de repository voor updates naar een eigen map,
|
`/usr/src/daynivo-kiosk`. Clone de repository voor updates naar een eigen map,
|
||||||
of pak een release uit en draai daar `sudo ./update.sh --no-pull`.
|
of pak een release uit en draai daar `sudo ./update.sh --no-pull`.
|
||||||
|
|
||||||
|
### Van de VoxIP-namen naar Daynivo
|
||||||
|
|
||||||
|
Kiosks tot en met versie 1.2.x gebruiken nog de oude namen: services
|
||||||
|
`voxip-kiosk-*`, mappen `/etc/voxip-kiosk`, `/var/lib/voxip-kiosk*` en
|
||||||
|
`/usr/lib/voxip-kiosk`, en de gebruiker `voxip`. De eerste update naar een
|
||||||
|
nieuwere versie zet zo'n kiosk eenmalig over, gewoon via de webinterface of
|
||||||
|
's nachts automatisch:
|
||||||
|
|
||||||
|
- services, programma's en instellingen krijgen de `daynivo-`-namen; de oude
|
||||||
|
services worden uitgeschakeld en verwijderd;
|
||||||
|
- configuratie, beheerderswachtwoord, browserprofiel (offline-cache), logs en
|
||||||
|
updatestatus verhuizen naar de `daynivo-`-mappen; de oude mapnamen blijven
|
||||||
|
als verwijzing bestaan;
|
||||||
|
- de gebruiker `voxip` wordt `daynivo`, met hetzelfde uid, zodat alle
|
||||||
|
bestanden hun eigenaar houden;
|
||||||
|
- de netwerkprofielen `voxip-eth0`/`voxip-wlan0` worden `daynivo-eth0`/
|
||||||
|
`daynivo-wlan0` (niet zolang er een netwerkwijziging op bevestiging wacht;
|
||||||
|
dan bij de volgende update);
|
||||||
|
- wie in de Kiosk Manager was ingelogd, moet één keer opnieuw inloggen.
|
||||||
|
|
||||||
|
Op de bootpartitie blijven `voxip-config.json`, `voxip-authorized_keys` en
|
||||||
|
`voxip-network-reset` werken, net als de opdracht `voxip-kiosk-ctl`. Bij het
|
||||||
|
bouwen van een image worden `VOXIP_DEFAULT_URL` en `VOXIP_DEVICE_NAME` nog
|
||||||
|
gelezen als de `DAYNIVO_`-varianten ontbreken.
|
||||||
|
|
||||||
### Verwijderen
|
### Verwijderen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -82,7 +134,7 @@ sudo ./uninstall.sh --purge # ook configuratie, status, logs, netwerkprofielen
|
|||||||
|
|
||||||
Het image wordt gebouwd met [pi-gen](https://github.com/RPi-Distro/pi-gen),
|
Het image wordt gebouwd met [pi-gen](https://github.com/RPi-Distro/pi-gen),
|
||||||
de officiële bouwtool van Raspberry Pi OS. De extra stage
|
de officiële bouwtool van Raspberry Pi OS. De extra stage
|
||||||
`image/stage-voxip` voert hetzelfde `install.sh` uit als een handmatige
|
`image/stage-daynivo` voert hetzelfde `install.sh` uit als een handmatige
|
||||||
installatie.
|
installatie.
|
||||||
|
|
||||||
Vereist: een Linux-buildmachine (x86_64 of arm64) met Docker en git. Op
|
Vereist: een Linux-buildmachine (x86_64 of arm64) met Docker en git. Op
|
||||||
@@ -104,19 +156,47 @@ Instelbaar via omgevingsvariabelen:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PI_GEN_REF` | `2024-11-19-raspios-bookworm-armhf` | pi-gen-tag (32-bit) |
|
| `PI_GEN_REF` | `2024-11-19-raspios-bookworm-armhf` | pi-gen-tag (32-bit) |
|
||||||
| `RELEASE` | `bookworm` | `bookworm` of `trixie` (moet bij `PI_GEN_REF` passen) |
|
| `RELEASE` | `bookworm` | `bookworm` of `trixie` (moet bij `PI_GEN_REF` passen) |
|
||||||
| `VOXIP_DEFAULT_URL` | `https://schermen-ijsselflat.voxip.nl` | standaard kiosk-URL |
|
| `DAYNIVO_DEFAULT_URL` | `https://schermen-ijsselflat.voxip.nl` | standaard kiosk-URL |
|
||||||
| `VOXIP_DEVICE_NAME` | `Daynivo Kiosk` | standaard apparaatnaam |
|
| `DAYNIVO_DEVICE_NAME` | `Daynivo Kiosk` | standaard apparaatnaam |
|
||||||
| `TARGET_HOSTNAME` | `kiosk` | hostname (`http://<hostname>.local:8080`) |
|
| `TARGET_HOSTNAME` | `kiosk` | hostname (`http://<hostname>.local:8080`) |
|
||||||
| `ALLOW_DIRTY` | `0` | `1` = bouw de werkmap in plaats van de laatste commit |
|
| `ALLOW_DIRTY` | `0` | `1` = bouw de werkmap in plaats van de laatste commit |
|
||||||
|
|
||||||
**Reproduceerbaarheid.** pi-gen staat vast op een tag en de broncode
|
**Reproduceerbaarheid.** pi-gen staat vast op een tag en de broncode
|
||||||
gaat via `git archive` van één commit mee. Beide staan in `BUILD-INFO`
|
gaat via `git archive` van één commit mee. Beide staan in `BUILD-INFO`
|
||||||
(ook in de image: `/usr/share/voxip-kiosk/BUILD-INFO`). De Debian- en
|
(ook in de image: `/usr/share/daynivo-kiosk/BUILD-INFO`). De Debian- en
|
||||||
Raspberry Pi-pakketten komen uit de archieven zoals die op het moment van
|
Raspberry Pi-pakketten komen uit de archieven zoals die op het moment van
|
||||||
bouwen zijn. Er bestaat geen snapshot-archief voor Raspberry Pi OS, dus
|
bouwen zijn. Er bestaat geen snapshot-archief voor Raspberry Pi OS, dus
|
||||||
twee builds op verschillende dagen kunnen nieuwere pakketversies bevatten.
|
twee builds op verschillende dagen kunnen nieuwere pakketversies bevatten.
|
||||||
Bewaar voor een uitrol daarom de gebouwde `.img.xz` zelf.
|
Bewaar voor een uitrol daarom de gebouwde `.img.xz` zelf.
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Een release maak je door `VERSION` te verhogen, te committen en een tag met
|
||||||
|
hetzelfde nummer te pushen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 1.2.0 > VERSION && git commit -am "Release 1.2.0"
|
||||||
|
git tag v1.2.0 && git push && git push origin v1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Gitea Actions (`.github/workflows/release.yml`) draait dan de tests en maakt
|
||||||
|
een release met `daynivo-kiosk-os-<versie>.tar.gz` (+ `.sha256`) en
|
||||||
|
`install-online.sh`. Daarna bouwt dezelfde workflow het SD-image en hangt het
|
||||||
|
`.img.xz` (+ `.sha256`) aan dezelfde release; dat duurt 30 tot 90 minuten of
|
||||||
|
langer. Zodra de tar-bestanden er staan, zien de kiosks de nieuwe versie.
|
||||||
|
|
||||||
|
Vereist op de Gitea-server:
|
||||||
|
|
||||||
|
- een **act_runner** met het label `ubuntu-latest`;
|
||||||
|
- voor het image: `container.privileged: true` in de runnerconfiguratie
|
||||||
|
(pi-gen heeft loop-devices en binfmt_misc nodig) en genoeg schijfruimte
|
||||||
|
(± 15 GB tijdens het bouwen);
|
||||||
|
- de maximale bijlagegrootte (`[attachment] MAX_SIZE` in `app.ini`, in MB)
|
||||||
|
groter dan het image (± 1 GB);
|
||||||
|
- optioneel een repository-secret `RELEASE_TOKEN` (token met schrijfrecht op
|
||||||
|
de repository), als het standaardtoken van de workflow geen releases mag
|
||||||
|
maken.
|
||||||
|
|
||||||
## Beheer
|
## Beheer
|
||||||
|
|
||||||
### Webinterface — `http://<hostname>.local:8080`
|
### Webinterface — `http://<hostname>.local:8080`
|
||||||
@@ -136,18 +216,18 @@ browser blijft draaien. *Chromium herstarten* herstart alleen de browser.
|
|||||||
### Commandoregel (via SSH)
|
### Commandoregel (via SSH)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo voxip-kiosk-ctl status # overzicht
|
sudo daynivo-kiosk-ctl status # overzicht
|
||||||
sudo voxip-kiosk-ctl setup-code # setupcode voor de eerste configuratie
|
sudo daynivo-kiosk-ctl setup-code # setupcode voor de eerste configuratie
|
||||||
sudo voxip-kiosk-ctl reset-password # wachtwoord vergeten: nieuwe setupcode
|
sudo daynivo-kiosk-ctl reset-password # wachtwoord vergeten: nieuwe setupcode
|
||||||
sudo voxip-kiosk-ctl set-url https://… # URL wijzigen
|
sudo daynivo-kiosk-ctl set-url https://… # URL wijzigen
|
||||||
sudo voxip-kiosk-ctl reload # pagina herladen
|
sudo daynivo-kiosk-ctl reload # pagina herladen
|
||||||
sudo voxip-kiosk-ctl restart-browser # Chromium herstarten
|
sudo daynivo-kiosk-ctl restart-browser # Chromium herstarten
|
||||||
sudo voxip-kiosk-ctl health # healthcheck-rapport
|
sudo daynivo-kiosk-ctl health # healthcheck-rapport
|
||||||
sudo voxip-kiosk-ctl maintenance on|off # healthcheck tijdelijk niets laten herstellen
|
sudo daynivo-kiosk-ctl maintenance on|off # healthcheck tijdelijk niets laten herstellen
|
||||||
journalctl -u voxip-kiosk-browser -u voxip-kiosk-agent -f
|
journalctl -u daynivo-kiosk-browser -u daynivo-kiosk-agent -f
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuratie — `/etc/voxip-kiosk/config.json`
|
### Configuratie — `/etc/daynivo-kiosk/config.json`
|
||||||
|
|
||||||
| Sleutel | Standaard | Betekenis |
|
| Sleutel | Standaard | Betekenis |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -164,8 +244,9 @@ journalctl -u voxip-kiosk-browser -u voxip-kiosk-agent -f
|
|||||||
| `admin_port` | `8080` | poort van de beheerinterface |
|
| `admin_port` | `8080` | poort van de beheerinterface |
|
||||||
| `admin_https` | `false` | HTTPS met zelfondertekend certificaat |
|
| `admin_https` | `false` | HTTPS met zelfondertekend certificaat |
|
||||||
| `admin_allow_public` | `false` | ook toegang vanaf publieke IP-adressen (afgeraden) |
|
| `admin_allow_public` | `false` | ook toegang vanaf publieke IP-adressen (afgeraden) |
|
||||||
|
| `auto_update` | `false` | elke nacht een nieuwe release installeren |
|
||||||
|
|
||||||
Wijzigingen via de webinterface of `voxip-kiosk-ctl` worden atomair
|
Wijzigingen via de webinterface of `daynivo-kiosk-ctl` worden atomair
|
||||||
opgeslagen, met een `.bak` van de vorige versie. Een URL-wijziging gaat in
|
opgeslagen, met een `.bak` van de vorige versie. Een URL-wijziging gaat in
|
||||||
zonder dat Chromium herstart. Scherminstellingen gelden na een herstart van
|
zonder dat Chromium herstart. Scherminstellingen gelden na een herstart van
|
||||||
Chromium (een vinkje in de webinterface doet dat direct).
|
Chromium (een vinkje in de webinterface doet dat direct).
|
||||||
@@ -175,15 +256,15 @@ Chromium (een vinkje in de webinterface doet dat direct).
|
|||||||
Eén image voor alle klanten. Per apparaat zet je na het flashen op de
|
Eén image voor alle klanten. Per apparaat zet je na het flashen op de
|
||||||
bootpartitie (`bootfs`, zichtbaar op Windows en macOS):
|
bootpartitie (`bootfs`, zichtbaar op Windows en macOS):
|
||||||
|
|
||||||
- **`voxip-config.json`**: instellingen voor deze kiosk, plus optioneel
|
- **`daynivo-config.json`**: instellingen voor deze kiosk, plus optioneel
|
||||||
`hostname` en `wifi_country`. Een voorbeeld staat als
|
`hostname` en `wifi_country`. Een voorbeeld staat als
|
||||||
`voxip-config.example.json` op de bootpartitie.
|
`daynivo-config.example.json` op de bootpartitie.
|
||||||
- **`voxip-authorized_keys`**: openbare SSH-sleutels. SSH gaat dan aan, alleen
|
- **`daynivo-authorized_keys`**: openbare SSH-sleutels. SSH gaat dan aan, alleen
|
||||||
met sleutel-login.
|
met sleutel-login.
|
||||||
- Wifi en een eigen gebruiker stel je in via Raspberry Pi Imager.
|
- Wifi en een eigen gebruiker stel je in via Raspberry Pi Imager.
|
||||||
|
|
||||||
De bestanden worden bij het opstarten verwerkt en hernoemd naar
|
De bestanden worden bij het opstarten verwerkt en hernoemd naar
|
||||||
`*.applied`, of naar `*.rejected` met de reden in `voxip-config.error.txt`.
|
`*.applied`, of naar `*.rejected` met de reden in `daynivo-config.error.txt`.
|
||||||
|
|
||||||
### Daynivo-schermen
|
### Daynivo-schermen
|
||||||
|
|
||||||
@@ -214,8 +295,8 @@ make dev # http://127.0.0.1:8080 met tijdelijke mappen in .dev/
|
|||||||
|
|
||||||
| Symptoom | Oplossing |
|
| Symptoom | Oplossing |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Zwart scherm na opstarten | `sudo voxip-kiosk-ctl status`. Kijk in de journal van `voxip-kiosk-x`/`voxip-kiosk-browser`. Staat de monitor aan vóór de Pi? |
|
| Zwart scherm na opstarten | `sudo daynivo-kiosk-ctl status`. Kijk in de journal van `daynivo-kiosk-x`/`daynivo-kiosk-browser`. Staat de monitor aan vóór de Pi? |
|
||||||
| Beheerinterface onbereikbaar na netwerkwijziging | Wacht 3 minuten (automatische rollback) of zet `voxip-network-reset` op de bootpartitie en herstart. |
|
| Beheerinterface onbereikbaar na netwerkwijziging | Wacht 3 minuten (automatische rollback) of zet `daynivo-network-reset` op de bootpartitie en herstart. |
|
||||||
| Wachtwoord vergeten | `sudo voxip-kiosk-ctl reset-password` via SSH, of verwijder `/etc/voxip-kiosk/admin.json` met de SD-kaart in een Linux-pc. |
|
| Wachtwoord vergeten | `sudo daynivo-kiosk-ctl reset-password` via SSH, of verwijder `/etc/daynivo-kiosk/admin.json` met de SD-kaart in een Linux-pc. |
|
||||||
| `kiosk.local` werkt niet | Gebruik het IP-adres (staat op het setupscherm en in je router). Windows heeft soms Bonjour/mDNS nodig. Geef iedere kiosk een unieke hostname. |
|
| `kiosk.local` werkt niet | Gebruik het IP-adres (staat op het setupscherm en in je router). Windows heeft soms Bonjour/mDNS nodig. Geef iedere kiosk een unieke hostname. |
|
||||||
| Onderspanning in het dashboard | Gebruik de officiële 5,1 V/2,5 A-voeding. Onderspanning veroorzaakt crashes en SD-kaartcorruptie. |
|
| Onderspanning in het dashboard | Gebruik de officiële 5,1 V/2,5 A-voeding. Onderspanning veroorzaakt crashes en SD-kaartcorruptie. |
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.agent import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.browser import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.ctl import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.healthcheck import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.helper import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.manager import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/python3 -I
|
||||||
|
# Installed to /usr/lib/daynivo-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
|
|
||||||
|
from daynivo_kiosk.updater import main # noqa: E402
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# Starts the kiosk X server on vt7 for user "kiosk" (via Xorg.wrap).
|
# Starts the kiosk X server on vt7 for user "kiosk" (via Xorg.wrap).
|
||||||
# A fresh MIT cookie per start; Chromium reads it via XAUTHORITY.
|
# A fresh MIT cookie per start; Chromium reads it via XAUTHORITY.
|
||||||
set -eu
|
set -eu
|
||||||
AUTH="${RUNTIME_DIRECTORY:-/run/voxip-kiosk-x}/Xauthority"
|
AUTH="${RUNTIME_DIRECTORY:-/run/daynivo-kiosk-x}/Xauthority"
|
||||||
umask 077
|
umask 077
|
||||||
: > "$AUTH"
|
: > "$AUTH"
|
||||||
xauth -q -f "$AUTH" add :0 . "$(mcookie)"
|
xauth -q -f "$AUTH" add :0 . "$(mcookie)"
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/python3 -I
|
|
||||||
# Installed to /usr/lib/voxip-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
|
||||||
|
|
||||||
from voxip_kiosk.agent import main # noqa: E402
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/python3 -I
|
|
||||||
# Installed to /usr/lib/voxip-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
|
||||||
|
|
||||||
from voxip_kiosk.browser import main # noqa: E402
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/python3 -I
|
|
||||||
# Installed to /usr/lib/voxip-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
|
||||||
|
|
||||||
from voxip_kiosk.ctl import main # noqa: E402
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/python3 -I
|
|
||||||
# Installed to /usr/lib/voxip-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
|
||||||
|
|
||||||
from voxip_kiosk.healthcheck import main # noqa: E402
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/python3 -I
|
|
||||||
# Installed to /usr/lib/voxip-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
|
||||||
|
|
||||||
from voxip_kiosk.helper import main # noqa: E402
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/python3 -I
|
|
||||||
# Installed to /usr/lib/voxip-kiosk/ by install.sh. -I: ignore PYTHON* env and user site-packages.
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
|
||||||
|
|
||||||
from voxip_kiosk.manager import main # noqa: E402
|
|
||||||
|
|
||||||
sys.exit(main())
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -11,5 +11,6 @@
|
|||||||
"browser_profile": "persistent",
|
"browser_profile": "persistent",
|
||||||
"admin_port": 8080,
|
"admin_port": 8080,
|
||||||
"admin_https": false,
|
"admin_https": false,
|
||||||
"admin_allow_public": false
|
"admin_allow_public": false,
|
||||||
|
"auto_update": false
|
||||||
}
|
}
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
/var/log/voxip-kiosk/*.log {
|
/var/log/daynivo-kiosk/*.log {
|
||||||
weekly
|
weekly
|
||||||
rotate 8
|
rotate 8
|
||||||
maxsize 2M
|
maxsize 2M
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
delaycompress
|
delaycompress
|
||||||
missingok
|
missingok
|
||||||
notifempty
|
notifempty
|
||||||
create 0640 voxip voxip
|
create 0640 daynivo daynivo
|
||||||
su voxip voxip
|
su daynivo daynivo
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Plymouth Theme]
|
||||||
|
Name=daynivo-kiosk
|
||||||
|
Description=Daynivo Kiosk boot splash (logo on white)
|
||||||
|
ModuleName=script
|
||||||
|
|
||||||
|
[script]
|
||||||
|
ImageDir=/usr/share/plymouth/themes/daynivo-kiosk
|
||||||
|
ScriptFile=/usr/share/plymouth/themes/daynivo-kiosk/daynivo-kiosk.script
|
||||||
File renamed without changes.
File renamed without changes.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
@@ -1,8 +0,0 @@
|
|||||||
[Plymouth Theme]
|
|
||||||
Name=voxip-kiosk
|
|
||||||
Description=Daynivo Kiosk boot splash (logo on white)
|
|
||||||
ModuleName=script
|
|
||||||
|
|
||||||
[script]
|
|
||||||
ImageDir=/usr/share/plymouth/themes/voxip-kiosk
|
|
||||||
ScriptFile=/usr/share/plymouth/themes/voxip-kiosk/voxip-kiosk.script
|
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
# Daynivo Kiosk OS: the web interface (user voxip) may run exactly one program
|
# Daynivo Kiosk OS: the web interface (user daynivo) may run exactly one program
|
||||||
# as root: the restricted helper, which validates all of its input itself.
|
# as root: the restricted helper, which validates all of its input itself.
|
||||||
Defaults:voxip !requiretty
|
Defaults:daynivo !requiretty
|
||||||
voxip ALL=(root) NOPASSWD: /usr/lib/voxip-kiosk/voxip-kiosk-helper
|
daynivo ALL=(root) NOPASSWD: /usr/lib/daynivo-kiosk/daynivo-kiosk-helper
|
||||||
+37
-33
@@ -11,33 +11,33 @@ samenwerken en waarom bepaalde keuzes zijn gemaakt.
|
|||||||
- Eén HTTPS-pagina fullscreen in Chromium, zonder desktopomgeving.
|
- Eén HTTPS-pagina fullscreen in Chromium, zonder desktopomgeving.
|
||||||
- Lokale webbeheerinterface (*Daynivo Kiosk Manager*) op poort 8080.
|
- Lokale webbeheerinterface (*Daynivo Kiosk Manager*) op poort 8080.
|
||||||
- Geen klantspecifieke code: alles wat per klant verschilt staat in
|
- Geen klantspecifieke code: alles wat per klant verschilt staat in
|
||||||
`/etc/voxip-kiosk/config.json` en kan bij uitrol via de bootpartitie
|
`/etc/daynivo-kiosk/config.json` en kan bij uitrol via de bootpartitie
|
||||||
worden meegegeven (`voxip-config.json`).
|
worden meegegeven (`daynivo-config.json`).
|
||||||
|
|
||||||
## Overzicht
|
## Overzicht
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────── Raspberry Pi ────────────────────────┐
|
┌──────────────────────── Raspberry Pi ────────────────────────┐
|
||||||
│ │
|
│ │
|
||||||
HDMI ◄────────┤ voxip-kiosk-x.service Xorg :0 op vt7 (user kiosk) │
|
HDMI ◄────────┤ daynivo-kiosk-x.service Xorg :0 op vt7 (user kiosk) │
|
||||||
│ ▲ │
|
│ ▲ │
|
||||||
│ │ DISPLAY=:0 │
|
│ │ DISPLAY=:0 │
|
||||||
│ voxip-kiosk-browser.service Chromium --kiosk (user kiosk) │
|
│ daynivo-kiosk-browser.service Chromium --kiosk (user kiosk) │
|
||||||
│ ▲ CDP 127.0.0.1:9222 │
|
│ ▲ CDP 127.0.0.1:9222 │
|
||||||
│ │ ┌─────────────────────────────┐ │
|
│ │ ┌─────────────────────────────┐ │
|
||||||
│ ├──┤ voxip-kiosk-agent.service │ navigatie, offline-│
|
│ ├──┤ daynivo-kiosk-agent.service │ navigatie, offline-│
|
||||||
│ │ │ (user kiosk) │ pagina, verversen, │
|
│ │ │ (user kiosk) │ pagina, verversen, │
|
||||||
│ │ └─────────────────────────────┘ hang-detectie │
|
│ │ └─────────────────────────────┘ hang-detectie │
|
||||||
│ │ │
|
│ │ │
|
||||||
LAN :8080 ───►│ voxip-kiosk-manager.service Flask + gunicorn (user voxip) │
|
LAN :8080 ───►│ daynivo-kiosk-manager.service Flask + gunicorn (user daynivo) │
|
||||||
│ │ sudo -n (één commando) │
|
│ │ sudo -n (één commando) │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ voxip-kiosk-helper root, vaste werkwoorden, │
|
│ daynivo-kiosk-helper root, vaste werkwoorden, │
|
||||||
│ strikte invoervalidatie │
|
│ strikte invoervalidatie │
|
||||||
│ │
|
│ │
|
||||||
│ voxip-kiosk-healthcheck.timer elke minuut, root │
|
│ daynivo-kiosk-healthcheck.timer elke minuut, root │
|
||||||
│ voxip-network-guard.service netwerk-rollback bij boot │
|
│ daynivo-network-guard.service netwerk-rollback bij boot │
|
||||||
│ voxip-boot-import.service config/SSH-sleutels vanaf │
|
│ daynivo-boot-import.service config/SSH-sleutels vanaf │
|
||||||
│ de bootpartitie │
|
│ de bootpartitie │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
@@ -46,15 +46,16 @@ samenwerken en waarom bepaalde keuzes zijn gemaakt.
|
|||||||
|
|
||||||
| Onderdeel | Gebruiker | Taak |
|
| Onderdeel | Gebruiker | Taak |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `voxip-kiosk-x` | `kiosk` (via Xorg.wrap) | Minimale X-server op vt7, zonder cursor, zonder TCP. |
|
| `daynivo-kiosk-x` | `kiosk` (via Xorg.wrap) | Minimale X-server op vt7, zonder cursor, zonder TCP. |
|
||||||
| `voxip-kiosk-browser` | `kiosk` | Past rotatie/resolutie/DPMS toe (xrandr/xset), ruimt een vastgelopen profiel op en start Chromium. |
|
| `daynivo-kiosk-browser` | `kiosk` | Past rotatie/resolutie/DPMS toe (xrandr/xset), ruimt een vastgelopen profiel op en start Chromium. |
|
||||||
| `voxip-kiosk-agent` | `kiosk` | Bestuurt Chromium via het DevTools Protocol op localhost: offline-pagina, URL-wissel zonder herstart, periodiek verversen, hang-detectie. |
|
| `daynivo-kiosk-agent` | `kiosk` | Bestuurt Chromium via het DevTools Protocol op localhost: offline-pagina, URL-wissel zonder herstart, periodiek verversen, hang-detectie. |
|
||||||
| `voxip-kiosk-manager` | `voxip` | Flask-webinterface (gunicorn, 1 worker, 4 threads). |
|
| `daynivo-kiosk-manager` | `daynivo` | Flask-webinterface (gunicorn, 1 worker, 4 threads). |
|
||||||
| `voxip-kiosk-helper` | `root` (via sudo) | Enige geprivilegieerde ingang: reboot, uitschakelen, Chromium herstarten, hostname, wifi-land, wifi-scan, netwerkconfiguratie + rollback. |
|
| `daynivo-kiosk-helper` | `root` (via sudo) | Enige geprivilegieerde ingang: reboot, uitschakelen, Chromium herstarten, hostname, wifi-land, wifi-scan, netwerkconfiguratie + rollback, updates controleren en starten. |
|
||||||
| `voxip-kiosk-healthcheck` | `root` | Bewaakt alle services en herstelt ze, met logging. |
|
| `daynivo-kiosk-updater` | `root` (timer, of eigen transient unit via de helper) | Haalt de nieuwste release op (HTTPS, SHA-256), pakt hem uit in `/opt/daynivo-kiosk-os/<versie>` en draait `update.sh --no-pull`. Nooit een oudere of gelijke versie. |
|
||||||
|
| `daynivo-kiosk-healthcheck` | `root` | Bewaakt alle services en herstelt ze, met logging. |
|
||||||
|
|
||||||
De webinterface draait **nooit** als root. De sudoers-regel staat
|
De webinterface draait **nooit** als root. De sudoers-regel staat
|
||||||
uitsluitend `/usr/lib/voxip-kiosk/voxip-kiosk-helper` toe. De helper:
|
uitsluitend `/usr/lib/daynivo-kiosk/daynivo-kiosk-helper` toe. De helper:
|
||||||
|
|
||||||
- draait met `python3 -I` (geen `PYTHONPATH`, geen user site-packages);
|
- draait met `python3 -I` (geen `PYTHONPATH`, geen user site-packages);
|
||||||
- kent alleen een vaste lijst werkwoorden; interne werkwoorden (zoals
|
- kent alleen een vaste lijst werkwoorden; interne werkwoorden (zoals
|
||||||
@@ -68,8 +69,8 @@ uitsluitend `/usr/lib/voxip-kiosk/voxip-kiosk-helper` toe. De helper:
|
|||||||
|
|
||||||
Xorg en Chromium zijn aparte services. Daardoor is *Chromium herstarten*
|
Xorg en Chromium zijn aparte services. Daardoor is *Chromium herstarten*
|
||||||
snel (X blijft draaien) en geeft het exitcode van Chromium direct aan
|
snel (X blijft draaien) en geeft het exitcode van Chromium direct aan
|
||||||
systemd door. `voxip-kiosk-browser` heeft `Requires=` en `PartOf=` op
|
systemd door. `daynivo-kiosk-browser` heeft `Requires=` en `PartOf=` op
|
||||||
`voxip-kiosk-x`: valt X weg, dan start de browser opnieuw mee.
|
`daynivo-kiosk-x`: valt X weg, dan start de browser opnieuw mee.
|
||||||
|
|
||||||
### Chromium besturen via CDP
|
### Chromium besturen via CDP
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ webinterface gebruiken dit om:
|
|||||||
### Offline-gedrag
|
### Offline-gedrag
|
||||||
|
|
||||||
Chromium start altijd op een lokale startpagina
|
Chromium start altijd op een lokale startpagina
|
||||||
(`/usr/share/voxip-kiosk/www/start.html`). De agent controleert met een
|
(`/usr/share/daynivo-kiosk/www/start.html`). De agent controleert met een
|
||||||
HTTPS-verzoek (mét certificaatvalidatie) of de doel-URL bereikbaar is en
|
HTTPS-verzoek (mét certificaatvalidatie) of de doel-URL bereikbaar is en
|
||||||
navigeert pas dan. De offline-pagina verschijnt alleen als de pagina niet
|
navigeert pas dan. De offline-pagina verschijnt alleen als de pagina niet
|
||||||
**geladen** kan worden (startpagina, Chromium-foutpagina). Valt het
|
**geladen** kan worden (startpagina, Chromium-foutpagina). Valt het
|
||||||
@@ -103,35 +104,38 @@ Zolang er geen beheerderswachtwoord is:
|
|||||||
adressen van de beheerinterface;
|
adressen van de beheerinterface;
|
||||||
3. kan een wachtwoord alleen ingesteld worden met die code.
|
3. kan een wachtwoord alleen ingesteld worden met die code.
|
||||||
|
|
||||||
Wie fysiek bij het scherm staat (of via SSH `voxip-kiosk-ctl setup-code`
|
Wie fysiek bij het scherm staat (of via SSH `daynivo-kiosk-ctl setup-code`
|
||||||
draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
|
draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
|
||||||
|
|
||||||
### Netwerkwijzigingen met rollback
|
### Netwerkwijzigingen met rollback
|
||||||
|
|
||||||
1. De helper maakt een snapshot van `/etc/NetworkManager/system-connections`
|
1. De helper maakt een snapshot van `/etc/NetworkManager/system-connections`
|
||||||
en van de actieve verbindingen.
|
en van de actieve verbindingen.
|
||||||
2. Hij schrijft het nieuwe profiel (`voxip-eth0` / `voxip-wlan0`, prioriteit
|
2. Hij schrijft het nieuwe profiel (`daynivo-eth0` / `daynivo-wlan0`, prioriteit
|
||||||
100) en plant een rollback-timer (`systemd-run`, 180 s).
|
100) en plant een rollback-timer (`systemd-run`, 180 s).
|
||||||
3. Activatie gebeurt asynchroon, zodat het HTTP-antwoord nog aankomt.
|
3. Activatie gebeurt asynchroon, zodat het HTTP-antwoord nog aankomt.
|
||||||
4. De beheerder bevestigt binnen 180 s (eventueel via het nieuwe adres),
|
4. De beheerder bevestigt binnen 180 s (eventueel via het nieuwe adres),
|
||||||
anders zet de timer de snapshot terug.
|
anders zet de timer de snapshot terug.
|
||||||
5. Valt de stroom uit tijdens die periode, dan zet
|
5. Valt de stroom uit tijdens die periode, dan zet
|
||||||
`voxip-network-guard.service` de snapshot terug bij het opstarten, nog
|
`daynivo-network-guard.service` de snapshot terug bij het opstarten, nog
|
||||||
vóór NetworkManager start.
|
vóór NetworkManager start.
|
||||||
6. Noodherstel: een leeg bestand `voxip-network-reset` op de bootpartitie
|
6. Noodherstel: een leeg bestand `daynivo-network-reset` op de bootpartitie
|
||||||
verwijdert bij de volgende boot alle Daynivo-netwerkprofielen (terug naar
|
verwijdert bij de volgende boot alle Daynivo-netwerkprofielen (terug naar
|
||||||
DHCP).
|
DHCP).
|
||||||
|
|
||||||
### Configuratie
|
### Configuratie
|
||||||
|
|
||||||
- `/etc/voxip-kiosk/config.json`: eigenaar `voxip`, groep `kiosk`, 0640.
|
- `/etc/daynivo-kiosk/config.json`: eigenaar `daynivo`, groep `kiosk`, 0640.
|
||||||
Wordt atomair opgeslagen (tijdelijk bestand, `fsync`, `rename`, `fsync`
|
Wordt atomair opgeslagen (tijdelijk bestand, `fsync`, `rename`, `fsync`
|
||||||
van de map) met een `.bak` van de vorige versie. Ongeldige waarden vallen
|
van de map) met een `.bak` van de vorige versie. Ongeldige waarden vallen
|
||||||
bij het laden terug op de standaard, zodat de kiosk altijd start.
|
bij het laden terug op de standaard, zodat de kiosk altijd start.
|
||||||
Onbekende sleutels blijven bewaard (vooruit-/achteruitcompatibel).
|
Onbekende sleutels blijven bewaard (vooruit-/achteruitcompatibel).
|
||||||
- `/etc/voxip-kiosk/admin.json`: Argon2id-hash, 0600, alleen `voxip`.
|
- `/etc/daynivo-kiosk/admin.json`: Argon2id-hash, 0600, alleen `daynivo`.
|
||||||
- Updates (`install.sh --upgrade` / `update.sh`) overschrijven nooit
|
- Updates (`install.sh --upgrade` / `update.sh` / de updater) overschrijven
|
||||||
bestaande configuratie.
|
nooit bestaande configuratie. De updater draait in een eigen systemd-unit,
|
||||||
|
zodat het herstarten van de beheerinterface tijdens `install.sh` de update
|
||||||
|
niet afbreekt. De checksum komt van dezelfde server als de download: hij
|
||||||
|
vangt een kapotte download af, geen gehackte releaseserver.
|
||||||
|
|
||||||
### Betrouwbaarheid (24/7)
|
### Betrouwbaarheid (24/7)
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@ draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
|
|||||||
(toegestaan: privé, link-local, loopback, CGNAT/VPN 100.64.0.0/10).
|
(toegestaan: privé, link-local, loopback, CGNAT/VPN 100.64.0.0/10).
|
||||||
- Optioneel HTTPS (zelfondertekend certificaat, automatisch aangemaakt).
|
- Optioneel HTTPS (zelfondertekend certificaat, automatisch aangemaakt).
|
||||||
- SSH staat in de image standaard uit en wordt alleen ingeschakeld met
|
- SSH staat in de image standaard uit en wordt alleen ingeschakeld met
|
||||||
credentials uit Raspberry Pi Imager of `voxip-authorized_keys` op de
|
credentials uit Raspberry Pi Imager of `daynivo-authorized_keys` op de
|
||||||
bootpartitie (dan alleen sleutel-login).
|
bootpartitie (dan alleen sleutel-login).
|
||||||
- Strikte security-headers en CSP (`default-src 'self'`).
|
- Strikte security-headers en CSP (`default-src 'self'`).
|
||||||
|
|
||||||
@@ -175,8 +179,8 @@ daynivo-kiosk-os/
|
|||||||
├── install.sh installatie op bestaande Pi OS + gebruikt door image-build
|
├── install.sh installatie op bestaande Pi OS + gebruikt door image-build
|
||||||
├── update.sh update met backup van configuratie
|
├── update.sh update met backup van configuratie
|
||||||
├── uninstall.sh verwijderen (--purge voor alles)
|
├── uninstall.sh verwijderen (--purge voor alles)
|
||||||
├── bin/ dunne launchers → /usr/lib/voxip-kiosk/
|
├── bin/ dunne launchers → /usr/lib/daynivo-kiosk/
|
||||||
├── src/voxip_kiosk/ Python-package (alle logica)
|
├── src/daynivo_kiosk/ Python-package (alle logica)
|
||||||
│ ├── config.py laden/valideren/atomair opslaan
|
│ ├── config.py laden/valideren/atomair opslaan
|
||||||
│ ├── validation.py gedeelde invoervalidatie
|
│ ├── validation.py gedeelde invoervalidatie
|
||||||
│ ├── cdp.py Chrome DevTools Protocol-client
|
│ ├── cdp.py Chrome DevTools Protocol-client
|
||||||
@@ -188,12 +192,12 @@ daynivo-kiosk-os/
|
|||||||
│ ├── netinfo.py netwerkstatus (zonder rechten)
|
│ ├── netinfo.py netwerkstatus (zonder rechten)
|
||||||
│ ├── sysinfo.py CPU, temperatuur, geheugen, opslag, IP
|
│ ├── sysinfo.py CPU, temperatuur, geheugen, opslag, IP
|
||||||
│ ├── services.py systemd- en journal-status
|
│ ├── services.py systemd- en journal-status
|
||||||
│ ├── ctl.py CLI voor SSH-beheer (voxip-kiosk-ctl)
|
│ ├── ctl.py CLI voor SSH-beheer (daynivo-kiosk-ctl)
|
||||||
│ └── web/ Flask-app, templates, statische bestanden
|
│ └── web/ Flask-app, templates, statische bestanden
|
||||||
├── www/ lokale kioskpagina's (start, offline, setup)
|
├── www/ lokale kioskpagina's (start, offline, setup)
|
||||||
├── systemd/ unit-bestanden
|
├── systemd/ unit-bestanden
|
||||||
├── config/ standaardconfig, sudoers, journald, logrotate, …
|
├── config/ standaardconfig, sudoers, journald, logrotate, …
|
||||||
├── image/ pi-gen image-builder (stage-voxip)
|
├── image/ pi-gen image-builder (stage-daynivo)
|
||||||
├── tests/ pytest-suite
|
├── tests/ pytest-suite
|
||||||
└── docs/ architectuur, hardwaretest
|
└── docs/ architectuur, hardwaretest
|
||||||
```
|
```
|
||||||
+20
-12
@@ -20,7 +20,7 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
|
|||||||
- [ ] Binnen 90 s na stroom aan verschijnt het setupscherm met de setupcode,
|
- [ ] Binnen 90 s na stroom aan verschijnt het setupscherm met de setupcode,
|
||||||
`http://kiosk.local:8080` en het IP-adres. Er is geen muiscursor en
|
`http://kiosk.local:8080` en het IP-adres. Er is geen muiscursor en
|
||||||
er zijn geen dialoogvensters.
|
er zijn geen dialoogvensters.
|
||||||
- [ ] SSH is uit (`ssh voxipadmin@kiosk.local` wordt geweigerd), en er is
|
- [ ] SSH is uit (`ssh daynivoadmin@kiosk.local` wordt geweigerd), en er is
|
||||||
geen bruikbaar standaardwachtwoord.
|
geen bruikbaar standaardwachtwoord.
|
||||||
|
|
||||||
## 2. Initiële configuratie
|
## 2. Initiële configuratie
|
||||||
@@ -47,23 +47,23 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
|
|||||||
(zie Logboek → Kiosk-agent).
|
(zie Logboek → Kiosk-agent).
|
||||||
- [ ] Screenshot op de Kiosk-pagina toont wat er op het scherm staat.
|
- [ ] Screenshot op de Kiosk-pagina toont wat er op het scherm staat.
|
||||||
- [ ] Daynivo-URL met `?token=`: het token is overal gemaskeerd
|
- [ ] Daynivo-URL met `?token=`: het token is overal gemaskeerd
|
||||||
(dashboard, `voxip-kiosk-ctl status`, `journalctl -u voxip-kiosk-agent`).
|
(dashboard, `daynivo-kiosk-ctl status`, `journalctl -u daynivo-kiosk-agent`).
|
||||||
|
|
||||||
## 4. Herstel
|
## 4. Herstel
|
||||||
|
|
||||||
- [ ] `sudo pkill -9 chromium`: binnen 15 s is de pagina terug. Het logboek
|
- [ ] `sudo pkill -9 chromium`: binnen 15 s is de pagina terug. Het logboek
|
||||||
toont de herstart.
|
toont de herstart.
|
||||||
- [ ] `sudo pkill -9 Xorg`: X en Chromium komen binnen 20 s terug.
|
- [ ] `sudo pkill -9 Xorg`: X en Chromium komen binnen 20 s terug.
|
||||||
- [ ] Crashloop: `sudo voxip-kiosk-ctl set-url https://localhost:1/` en
|
- [ ] Crashloop: `sudo daynivo-kiosk-ctl set-url https://localhost:1/` en
|
||||||
daarna 7× `sudo pkill -9 chromium` binnen 2 minuten. De service komt
|
daarna 7× `sudo pkill -9 chromium` binnen 2 minuten. De service komt
|
||||||
in de fouttoestand (start-limit). De healthcheck logt een fout en start
|
in de fouttoestand (start-limit). De healthcheck logt een fout en start
|
||||||
hem na ongeveer 5 minuten opnieuw. Er is geen eindeloze stille lus.
|
hem na ongeveer 5 minuten opnieuw. Er is geen eindeloze stille lus.
|
||||||
Zet de URL daarna terug.
|
Zet de URL daarna terug.
|
||||||
- [ ] Vastgelopen pagina: open via SSH-tunnel DevTools of zet een testpagina met
|
- [ ] Vastgelopen pagina: open via SSH-tunnel DevTools of zet een testpagina met
|
||||||
`while(true){}` in. Na ±15 s herlaadt de agent, na ±30 s herstart Chromium.
|
`while(true){}` in. Na ±15 s herlaadt de agent, na ±30 s herstart Chromium.
|
||||||
- [ ] `sudo systemctl stop voxip-kiosk-agent`: de healthcheck start hem binnen 2 minuten.
|
- [ ] `sudo systemctl stop daynivo-kiosk-agent`: de healthcheck start hem binnen 2 minuten.
|
||||||
- [ ] De webinterface blijft werken terwijl Chromium gestopt is
|
- [ ] De webinterface blijft werken terwijl Chromium gestopt is
|
||||||
(`sudo systemctl stop voxip-kiosk-browser`).
|
(`sudo systemctl stop daynivo-kiosk-browser`).
|
||||||
|
|
||||||
## 5. Netwerk
|
## 5. Netwerk
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
|
|||||||
- [ ] **Rollback na stroomuitval:** herhaal, trek binnen 3 minuten de stroom
|
- [ ] **Rollback na stroomuitval:** herhaal, trek binnen 3 minuten de stroom
|
||||||
eruit. Na het opstarten zijn de oude instellingen actief.
|
eruit. Na het opstarten zijn de oude instellingen actief.
|
||||||
- [ ] **Noodreset:** stel iets onbereikbaars in en bevestig het. Zet
|
- [ ] **Noodreset:** stel iets onbereikbaars in en bevestig het. Zet
|
||||||
`voxip-network-reset` op de bootpartitie en start: weer DHCP.
|
`daynivo-network-reset` op de bootpartitie en start: weer DHCP.
|
||||||
|
|
||||||
## 6. Systeem
|
## 6. Systeem
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
|
|||||||
zonder ingrijpen terug in de kiosk, zonder "herstellen"-melding.
|
zonder ingrijpen terug in de kiosk, zonder "herstellen"-melding.
|
||||||
- [ ] 72 uur continu draaien met verversinterval 5 min. Daarna: geheugen
|
- [ ] 72 uur continu draaien met verversinterval 5 min. Daarna: geheugen
|
||||||
< 80% (dashboard), geen onderspanning, `journalctl -p err -b` zonder
|
< 80% (dashboard), geen onderspanning, `journalctl -p err -b` zonder
|
||||||
terugkerende fouten, `voxip-kiosk-ctl status` alles *Actief*.
|
terugkerende fouten, `daynivo-kiosk-ctl status` alles *Actief*.
|
||||||
- [ ] SD-kaartschrijfacties: `cat /proc/diskstats | grep mmcblk0` op t=0 en
|
- [ ] SD-kaartschrijfacties: `cat /proc/diskstats | grep mmcblk0` op t=0 en
|
||||||
na 1 uur stationair. Het verschil in geschreven sectoren (kolom 10) is
|
na 1 uur stationair. Het verschil in geschreven sectoren (kolom 10) is
|
||||||
minder dan 50 MB per uur.
|
minder dan 50 MB per uur.
|
||||||
@@ -112,14 +112,22 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
|
|||||||
|
|
||||||
## 8. Uitrol en updates
|
## 8. Uitrol en updates
|
||||||
|
|
||||||
- [ ] Nieuwe kaart met `voxip-config.json` (url, device_name, hostname,
|
- [ ] Nieuwe kaart met `daynivo-config.json` (url, device_name, hostname,
|
||||||
rotation) en `voxip-authorized_keys` op de bootpartitie: bij de eerste
|
rotation) en `daynivo-authorized_keys` op de bootpartitie: bij de eerste
|
||||||
boot toegepast, hernoemd naar `.applied`, SSH alleen met sleutel.
|
boot toegepast, hernoemd naar `.applied`, SSH alleen met sleutel.
|
||||||
- [ ] Ongeldige `voxip-config.json` (http-URL): `.rejected` en
|
- [ ] Ongeldige `daynivo-config.json` (http-URL): `.rejected` en
|
||||||
`voxip-config.error.txt` met de reden. De kiosk start met de standaardconfiguratie.
|
`daynivo-config.error.txt` met de reden. De kiosk start met de standaardconfiguratie.
|
||||||
- [ ] `sudo ./update.sh` vanuit een nieuwere checkout: versie in het dashboard
|
- [ ] `sudo ./update.sh` vanuit een nieuwere checkout: versie in het dashboard
|
||||||
verhoogd, configuratie en wachtwoord ongewijzigd, back-up in `/var/backups`.
|
verhoogd, configuratie en wachtwoord ongewijzigd, back-up in `/var/backups`.
|
||||||
|
- [ ] Systeem → Software-updates → *Nu controleren* met een nieuwere release
|
||||||
|
op Gitea: "beschikbaar". *Bijwerken naar …*: binnen enkele minuten is de
|
||||||
|
nieuwe versie actief, de beheerinterface komt terug, configuratie en
|
||||||
|
wachtwoord ongewijzigd.
|
||||||
|
- [ ] *Automatisch bijwerken* aan, `sudo systemctl start daynivo-kiosk-update.service`:
|
||||||
|
de nieuwere release wordt geïnstalleerd. Met het vinkje uit: alleen gecontroleerd.
|
||||||
|
- [ ] `curl -fsSL https://daynivo.nl/kiosk/install.sh | sudo bash` op een verse
|
||||||
|
Raspberry Pi OS Lite: installeert de nieuwste release, setupcode op het scherm.
|
||||||
- [ ] `sudo ./uninstall.sh --purge` op een testkaart: geen Daynivo-services of
|
- [ ] `sudo ./uninstall.sh --purge` op een testkaart: geen Daynivo-services of
|
||||||
-bestanden meer (`systemctl list-unit-files | grep voxip` is leeg).
|
-bestanden meer (`systemctl list-unit-files | grep daynivo` is leeg).
|
||||||
- [ ] Installatie op een bestaande Raspberry Pi OS Lite (Bookworm) met de
|
- [ ] Installatie op een bestaande Raspberry Pi OS Lite (Bookworm) met de
|
||||||
Daynivo-kiosk erop: URL overgenomen, `display-kiosk.service` uitgeschakeld.
|
Daynivo-kiosk erop: URL overgenomen, `display-kiosk.service` uitgeschakeld.
|
||||||
@@ -4,11 +4,11 @@ Daynivo Kiosk OS — bestanden op deze bootpartitie
|
|||||||
Je kunt een kiosk vooraf instellen door vóór de eerste start bestanden op
|
Je kunt een kiosk vooraf instellen door vóór de eerste start bestanden op
|
||||||
deze partitie te zetten (bijvoorbeeld direct na het flashen). Ze worden bij
|
deze partitie te zetten (bijvoorbeeld direct na het flashen). Ze worden bij
|
||||||
het opstarten verwerkt en daarna hernoemd naar *.applied (of *.rejected bij
|
het opstarten verwerkt en daarna hernoemd naar *.applied (of *.rejected bij
|
||||||
een fout; de reden staat dan in voxip-config.error.txt).
|
een fout; de reden staat dan in daynivo-config.error.txt).
|
||||||
|
|
||||||
voxip-config.json
|
daynivo-config.json
|
||||||
Instellingen voor deze kiosk. Zie voxip-config.example.json. Toegestaan:
|
Instellingen voor deze kiosk. Zie daynivo-config.example.json. Toegestaan:
|
||||||
alle sleutels uit /etc/voxip-kiosk/config.json plus "hostname" en
|
alle sleutels uit /etc/daynivo-kiosk/config.json plus "hostname" en
|
||||||
"wifi_country". Voorbeeld:
|
"wifi_country". Voorbeeld:
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -18,12 +18,12 @@ voxip-config.json
|
|||||||
"rotation": 90
|
"rotation": 90
|
||||||
}
|
}
|
||||||
|
|
||||||
voxip-authorized_keys
|
daynivo-authorized_keys
|
||||||
Openbare SSH-sleutels (één per regel). SSH wordt dan ingeschakeld, alleen
|
Openbare SSH-sleutels (één per regel). SSH wordt dan ingeschakeld, alleen
|
||||||
met sleutel-login, voor de beheerder (voxipadmin of de gebruiker die je
|
met sleutel-login, voor de beheerder (daynivoadmin of de gebruiker die je
|
||||||
in Raspberry Pi Imager hebt aangemaakt).
|
in Raspberry Pi Imager hebt aangemaakt).
|
||||||
|
|
||||||
voxip-network-reset
|
daynivo-network-reset
|
||||||
Leeg bestand. Verwijdert bij de volgende start alle via de
|
Leeg bestand. Verwijdert bij de volgende start alle via de
|
||||||
beheerinterface ingestelde netwerkprofielen (terug naar DHCP). Gebruik
|
beheerinterface ingestelde netwerkprofielen (terug naar DHCP). Gebruik
|
||||||
dit als de kiosk na een netwerkwijziging onbereikbaar is.
|
dit als de kiosk na een netwerkwijziging onbereikbaar is.
|
||||||
|
|||||||
+14
-13
@@ -8,8 +8,8 @@
|
|||||||
# Environment (all optional):
|
# Environment (all optional):
|
||||||
# PI_GEN_REF pi-gen tag/branch (32-bit armhf). Default: pinned Bookworm release.
|
# PI_GEN_REF pi-gen tag/branch (32-bit armhf). Default: pinned Bookworm release.
|
||||||
# RELEASE bookworm | trixie (must match PI_GEN_REF). Default: bookworm.
|
# RELEASE bookworm | trixie (must match PI_GEN_REF). Default: bookworm.
|
||||||
# VOXIP_DEFAULT_URL kiosk URL baked into the default config.
|
# DAYNIVO_DEFAULT_URL kiosk URL baked into the default config.
|
||||||
# VOXIP_DEVICE_NAME default device name.
|
# DAYNIVO_DEVICE_NAME default device name.
|
||||||
# TARGET_HOSTNAME default hostname (the admin UI is at http://<hostname>.local:8080).
|
# TARGET_HOSTNAME default hostname (the admin UI is at http://<hostname>.local:8080).
|
||||||
# ALLOW_DIRTY=1 build from the working tree instead of the committed HEAD.
|
# ALLOW_DIRTY=1 build from the working tree instead of the committed HEAD.
|
||||||
#
|
#
|
||||||
@@ -23,8 +23,9 @@ OUT="$ROOT/image/deploy"
|
|||||||
PI_GEN_REPO="${PI_GEN_REPO:-https://github.com/RPi-Distro/pi-gen.git}"
|
PI_GEN_REPO="${PI_GEN_REPO:-https://github.com/RPi-Distro/pi-gen.git}"
|
||||||
PI_GEN_REF="${PI_GEN_REF:-2024-11-19-raspios-bookworm-armhf}"
|
PI_GEN_REF="${PI_GEN_REF:-2024-11-19-raspios-bookworm-armhf}"
|
||||||
RELEASE="${RELEASE:-bookworm}"
|
RELEASE="${RELEASE:-bookworm}"
|
||||||
VOXIP_DEFAULT_URL="${VOXIP_DEFAULT_URL:-https://schermen-ijsselflat.voxip.nl}"
|
# VOXIP_* are the names from before the rename and still work.
|
||||||
VOXIP_DEVICE_NAME="${VOXIP_DEVICE_NAME:-Daynivo Kiosk}"
|
DAYNIVO_DEFAULT_URL="${DAYNIVO_DEFAULT_URL:-${VOXIP_DEFAULT_URL:-https://schermen-ijsselflat.voxip.nl}}"
|
||||||
|
DAYNIVO_DEVICE_NAME="${DAYNIVO_DEVICE_NAME:-${VOXIP_DEVICE_NAME:-Daynivo Kiosk}}"
|
||||||
TARGET_HOSTNAME="${TARGET_HOSTNAME:-kiosk}"
|
TARGET_HOSTNAME="${TARGET_HOSTNAME:-kiosk}"
|
||||||
NATIVE=0
|
NATIVE=0
|
||||||
[[ ${1:-} == "--native" ]] && NATIVE=1
|
[[ ${1:-} == "--native" ]] && NATIVE=1
|
||||||
@@ -45,13 +46,13 @@ git clone --quiet --depth 1 --branch "$PI_GEN_REF" "$PI_GEN_REPO" "$WORK/pi-gen"
|
|||||||
PI_GEN_COMMIT="$(git -C "$WORK/pi-gen" rev-parse HEAD)"
|
PI_GEN_COMMIT="$(git -C "$WORK/pi-gen" rev-parse HEAD)"
|
||||||
PIGEN="$WORK/pi-gen"
|
PIGEN="$WORK/pi-gen"
|
||||||
|
|
||||||
# Do not export the plain Lite image from stage2; ours is exported after stage-voxip.
|
# Do not export the plain Lite image from stage2; ours is exported after stage-daynivo.
|
||||||
touch "$PIGEN/stage2/SKIP_IMAGES"
|
touch "$PIGEN/stage2/SKIP_IMAGES"
|
||||||
rm -f "$PIGEN/stage2/EXPORT_NOOBS"
|
rm -f "$PIGEN/stage2/EXPORT_NOOBS"
|
||||||
|
|
||||||
echo "==> Daynivo-stage voorbereiden"
|
echo "==> Daynivo-stage voorbereiden"
|
||||||
cp -a "$ROOT/image/stage-voxip" "$PIGEN/stage-voxip"
|
cp -a "$ROOT/image/stage-daynivo" "$PIGEN/stage-daynivo"
|
||||||
PAYLOAD="$PIGEN/stage-voxip/00-voxip/files/voxip-kiosk"
|
PAYLOAD="$PIGEN/stage-daynivo/00-daynivo/files/daynivo-kiosk"
|
||||||
mkdir -p "$PAYLOAD"
|
mkdir -p "$PAYLOAD"
|
||||||
if [[ ${ALLOW_DIRTY:-0} == 1 ]] || ! git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
if [[ ${ALLOW_DIRTY:-0} == 1 ]] || ! git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
echo " (werkmap wordt gebruikt, niet de laatste commit)"
|
echo " (werkmap wordt gebruikt, niet de laatste commit)"
|
||||||
@@ -67,12 +68,12 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
BUILD_DATE="$(date -u +%Y-%m-%d)"
|
BUILD_DATE="$(date -u +%Y-%m-%d)"
|
||||||
cat > "$PIGEN/stage-voxip/00-voxip/files/build.env" <<EOF
|
cat > "$PIGEN/stage-daynivo/00-daynivo/files/build.env" <<EOF
|
||||||
VOXIP_DEFAULT_URL=$(printf %q "$VOXIP_DEFAULT_URL")
|
DAYNIVO_DEFAULT_URL=$(printf %q "$DAYNIVO_DEFAULT_URL")
|
||||||
VOXIP_DEVICE_NAME=$(printf %q "$VOXIP_DEVICE_NAME")
|
DAYNIVO_DEVICE_NAME=$(printf %q "$DAYNIVO_DEVICE_NAME")
|
||||||
EOF
|
EOF
|
||||||
cat > "$PAYLOAD/BUILD-INFO" <<EOF
|
cat > "$PAYLOAD/BUILD-INFO" <<EOF
|
||||||
voxip_kiosk_os=$VERSION
|
daynivo_kiosk_os=$VERSION
|
||||||
source_revision=$SOURCE_REV
|
source_revision=$SOURCE_REV
|
||||||
pi_gen_ref=$PI_GEN_REF
|
pi_gen_ref=$PI_GEN_REF
|
||||||
pi_gen_commit=$PI_GEN_COMMIT
|
pi_gen_commit=$PI_GEN_COMMIT
|
||||||
@@ -97,11 +98,11 @@ KEYBOARD_LAYOUT="English (US)"
|
|||||||
TIMEZONE_DEFAULT=Europe/Amsterdam
|
TIMEZONE_DEFAULT=Europe/Amsterdam
|
||||||
WPA_COUNTRY=NL
|
WPA_COUNTRY=NL
|
||||||
TARGET_HOSTNAME=$TARGET_HOSTNAME
|
TARGET_HOSTNAME=$TARGET_HOSTNAME
|
||||||
FIRST_USER_NAME=voxipadmin
|
FIRST_USER_NAME=daynivoadmin
|
||||||
FIRST_USER_PASS=$FIRST_USER_PASS
|
FIRST_USER_PASS=$FIRST_USER_PASS
|
||||||
DISABLE_FIRST_BOOT_USER_RENAME=1
|
DISABLE_FIRST_BOOT_USER_RENAME=1
|
||||||
ENABLE_SSH=0
|
ENABLE_SSH=0
|
||||||
STAGE_LIST="stage0 stage1 stage2 stage-voxip"
|
STAGE_LIST="stage0 stage1 stage2 stage-daynivo"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "==> Image bouwen (dit duurt 30–90 minuten)"
|
echo "==> Image bouwen (dit duurt 30–90 minuten)"
|
||||||
|
|||||||
File renamed without changes.
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
# Install Daynivo Kiosk OS into the image with the same install.sh that is
|
||||||
|
# used on existing Raspberry Pi OS installations.
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source files/build.env
|
||||||
|
|
||||||
|
rm -rf "${ROOTFS_DIR}/usr/src/daynivo-kiosk"
|
||||||
|
install -d "${ROOTFS_DIR}/usr/src/daynivo-kiosk"
|
||||||
|
cp -a files/daynivo-kiosk/. "${ROOTFS_DIR}/usr/src/daynivo-kiosk/"
|
||||||
|
|
||||||
|
on_chroot << EOF_CHROOT
|
||||||
|
/usr/src/daynivo-kiosk/install.sh --image --url $(printf %q "${DAYNIVO_DEFAULT_URL}") --name $(printf %q "${DAYNIVO_DEVICE_NAME}")
|
||||||
|
install -m 0644 /usr/src/daynivo-kiosk/BUILD-INFO /usr/share/daynivo-kiosk/BUILD-INFO
|
||||||
|
# No usable default password: unlocked only by Raspberry Pi Imager credentials.
|
||||||
|
passwd -l ${FIRST_USER_NAME}
|
||||||
|
systemctl disable ssh.service 2>/dev/null || true
|
||||||
|
apt-get clean
|
||||||
|
EOF_CHROOT
|
||||||
|
|
||||||
|
# Instructions next to config.txt for whoever opens the boot partition.
|
||||||
|
install -m 0644 files/daynivo-kiosk/image/boot-README.txt "${ROOTFS_DIR}/boot/firmware/DAYNIVO-LEESMIJ.txt"
|
||||||
|
install -m 0644 files/daynivo-kiosk/image/daynivo-config.example.json "${ROOTFS_DIR}/boot/firmware/daynivo-config.example.json"
|
||||||
File renamed without changes.
File renamed without changes.
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
# Install Daynivo Kiosk OS into the image with the same install.sh that is
|
|
||||||
# used on existing Raspberry Pi OS installations.
|
|
||||||
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source files/build.env
|
|
||||||
|
|
||||||
rm -rf "${ROOTFS_DIR}/usr/src/voxip-kiosk"
|
|
||||||
install -d "${ROOTFS_DIR}/usr/src/voxip-kiosk"
|
|
||||||
cp -a files/voxip-kiosk/. "${ROOTFS_DIR}/usr/src/voxip-kiosk/"
|
|
||||||
|
|
||||||
on_chroot << EOF_CHROOT
|
|
||||||
/usr/src/voxip-kiosk/install.sh --image --url $(printf %q "${VOXIP_DEFAULT_URL}") --name $(printf %q "${VOXIP_DEVICE_NAME}")
|
|
||||||
install -m 0644 /usr/src/voxip-kiosk/BUILD-INFO /usr/share/voxip-kiosk/BUILD-INFO
|
|
||||||
# No usable default password: unlocked only by Raspberry Pi Imager credentials.
|
|
||||||
passwd -l ${FIRST_USER_NAME}
|
|
||||||
systemctl disable ssh.service 2>/dev/null || true
|
|
||||||
apt-get clean
|
|
||||||
EOF_CHROOT
|
|
||||||
|
|
||||||
# Instructions next to config.txt for whoever opens the boot partition.
|
|
||||||
install -m 0644 files/voxip-kiosk/image/boot-README.txt "${ROOTFS_DIR}/boot/firmware/VOXIP-LEESMIJ.txt"
|
|
||||||
install -m 0644 files/voxip-kiosk/image/voxip-config.example.json "${ROOTFS_DIR}/boot/firmware/voxip-config.example.json"
|
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Install Daynivo Kiosk OS from the latest release, without git:
|
||||||
|
#
|
||||||
|
# curl -fsSL https://daynivo.nl/kiosk/install.sh | sudo bash
|
||||||
|
# curl -fsSL https://daynivo.nl/kiosk/install.sh | sudo bash -s -- --url "https://…" --name "Hal"
|
||||||
|
#
|
||||||
|
# Downloads the newest release from the Gitea repository, checks its SHA-256,
|
||||||
|
# unpacks it to /opt/daynivo-kiosk-os/<version> and runs its install.sh with
|
||||||
|
# the given options (see install.sh --help). Later updates come from the web
|
||||||
|
# interface (Systeem → Software-updates) or the same updater.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
API="${DAYNIVO_RELEASE_API:-https://git.rothman.nl/api/v1/repos/rothman/daynivo-kiosk-os/releases/latest}"
|
||||||
|
DEST=/opt/daynivo-kiosk-os
|
||||||
|
|
||||||
|
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||||
|
die() { printf '\033[1;31mFout:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Everything runs from main(), called on the last line: a download that breaks
|
||||||
|
# off halfway through this script runs nothing.
|
||||||
|
main() {
|
||||||
|
[[ $EUID -eq 0 ]] || die "Voer dit uit met sudo (… | sudo bash)."
|
||||||
|
for tool in curl python3 sha256sum tar; do
|
||||||
|
command -v "$tool" >/dev/null || die "$tool ontbreekt."
|
||||||
|
done
|
||||||
|
|
||||||
|
say "Nieuwste release opzoeken"
|
||||||
|
local json version tarball checksum
|
||||||
|
json="$(curl -fsSL --proto '=https' "$API")" || die "Kan de releaseserver niet bereiken ($API)."
|
||||||
|
read -r version tarball checksum < <(python3 -I -c '
|
||||||
|
import json, sys
|
||||||
|
data = json.loads(sys.stdin.read())
|
||||||
|
version = data["tag_name"].removeprefix("v")
|
||||||
|
assets = {a["name"]: a["browser_download_url"] for a in data.get("assets") or []}
|
||||||
|
name = f"daynivo-kiosk-os-{version}.tar.gz"
|
||||||
|
print(version, assets[name], assets[name + ".sha256"])
|
||||||
|
' <<<"$json") || die "De nieuwste release heeft geen installatiebestand."
|
||||||
|
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "Onbekend versienummer: $version"
|
||||||
|
|
||||||
|
say "Daynivo Kiosk OS $version downloaden"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
curl -fsSL --proto '=https' -o "$tmp/release.tar.gz" "$tarball"
|
||||||
|
curl -fsSL --proto '=https' -o "$tmp/release.sha256" "$checksum"
|
||||||
|
local expected actual
|
||||||
|
expected="$(awk '{print $1; exit}' "$tmp/release.sha256")"
|
||||||
|
actual="$(sha256sum "$tmp/release.tar.gz" | awk '{print $1}')"
|
||||||
|
[[ -n $expected && $expected == "$actual" ]] || die "Checksum klopt niet; er wordt niets geïnstalleerd."
|
||||||
|
|
||||||
|
tar -C "$tmp" --no-same-owner -xzf "$tmp/release.tar.gz"
|
||||||
|
[[ "$(tr -d '[:space:]' < "$tmp/daynivo-kiosk-os-$version/VERSION")" == "$version" ]] || die "Release bevat een andere versie."
|
||||||
|
install -d -m 0755 "$DEST"
|
||||||
|
rm -rf "${DEST:?}/$version"
|
||||||
|
mv "$tmp/daynivo-kiosk-os-$version" "$DEST/$version"
|
||||||
|
|
||||||
|
say "Installeren"
|
||||||
|
"$DEST/$version/install.sh" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
+129
-57
@@ -8,9 +8,9 @@
|
|||||||
#
|
#
|
||||||
# sudo ./install.sh [--url https://…] [--name "Hal begane grond"] [--hostname hal-bg]
|
# sudo ./install.sh [--url https://…] [--name "Hal begane grond"] [--hostname hal-bg]
|
||||||
# sudo ./install.sh --upgrade # update code, keep configuration
|
# sudo ./install.sh --upgrade # update code, keep configuration
|
||||||
# ./install.sh --image … # used by image/stage-voxip (no service starts)
|
# ./install.sh --image … # used by image/stage-daynivo (no service starts)
|
||||||
#
|
#
|
||||||
# Existing configuration in /etc/voxip-kiosk is never overwritten.
|
# Existing configuration in /etc/daynivo-kiosk is never overwritten.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -21,14 +21,15 @@ NAME=""
|
|||||||
NEW_HOSTNAME=""
|
NEW_HOSTNAME=""
|
||||||
FORCE=0
|
FORCE=0
|
||||||
|
|
||||||
LIB=/usr/lib/voxip-kiosk
|
LIB=/usr/lib/daynivo-kiosk
|
||||||
SHARE=/usr/share/voxip-kiosk
|
SHARE=/usr/share/daynivo-kiosk
|
||||||
DOC=/usr/share/doc/voxip-kiosk
|
DOC=/usr/share/doc/daynivo-kiosk
|
||||||
ETC=/etc/voxip-kiosk
|
ETC=/etc/daynivo-kiosk
|
||||||
UNITS=(voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service
|
UNITS=(daynivo-kiosk-x.service daynivo-kiosk-browser.service daynivo-kiosk-agent.service daynivo-kiosk-manager.service
|
||||||
voxip-kiosk-healthcheck.service voxip-kiosk-healthcheck.timer voxip-network-guard.service voxip-boot-import.service)
|
daynivo-kiosk-healthcheck.service daynivo-kiosk-healthcheck.timer daynivo-network-guard.service daynivo-boot-import.service
|
||||||
ENABLE=(voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service
|
daynivo-kiosk-update.service daynivo-kiosk-update.timer)
|
||||||
voxip-kiosk-healthcheck.timer voxip-network-guard.service voxip-boot-import.service)
|
ENABLE=(daynivo-kiosk-x.service daynivo-kiosk-browser.service daynivo-kiosk-agent.service daynivo-kiosk-manager.service
|
||||||
|
daynivo-kiosk-healthcheck.timer daynivo-network-guard.service daynivo-boot-import.service daynivo-kiosk-update.timer)
|
||||||
|
|
||||||
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||||
warn() { printf '\033[1;33mLet op:\033[0m %s\n' "$*" >&2; }
|
warn() { printf '\033[1;33mLet op:\033[0m %s\n' "$*" >&2; }
|
||||||
@@ -53,7 +54,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
[[ $EUID -eq 0 ]] || die "Voer dit script uit met sudo."
|
[[ $EUID -eq 0 ]] || die "Voer dit script uit met sudo."
|
||||||
[[ -f "$SRC/src/voxip_kiosk/config.py" ]] || die "Start install.sh vanuit de Daynivo Kiosk OS-repository."
|
[[ -f "$SRC/src/daynivo_kiosk/config.py" ]] || die "Start install.sh vanuit de Daynivo Kiosk OS-repository."
|
||||||
|
|
||||||
in_chroot() {
|
in_chroot() {
|
||||||
if command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt --chroot --quiet; then
|
if command -v systemd-detect-virt >/dev/null 2>&1 && systemd-detect-virt --chroot --quiet; then
|
||||||
@@ -105,6 +106,75 @@ pkg_available() { apt-cache policy "$1" 2>/dev/null | grep -q 'Candidate: [0-9]'
|
|||||||
apt-get install -y --no-install-recommends "${PKGS[@]}"
|
apt-get install -y --no-install-recommends "${PKGS[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- migration from the VoxIP names ---------------------------------------------------
|
||||||
|
# Kiosks installed before the rename (up to 1.2.x) use voxip-* services and folders
|
||||||
|
# and a "voxip" user. Move them over once, keeping the configuration, the password,
|
||||||
|
# the browser profile (offline cache) and the network settings. The old folder
|
||||||
|
# names stay as symlinks for whatever still runs from the old version, such as
|
||||||
|
# the updater that started this upgrade.
|
||||||
|
if [[ -d /etc/voxip-kiosk && ! -L /etc/voxip-kiosk && ! -e /etc/daynivo-kiosk ]]; then
|
||||||
|
say "Overstappen van de VoxIP-namen naar Daynivo"
|
||||||
|
if [[ $LIVE -eq 1 ]]; then
|
||||||
|
# Not voxip-kiosk-update.service: a nightly auto-update runs this very script in it.
|
||||||
|
systemctl stop voxip-kiosk-healthcheck.timer voxip-kiosk-update.timer voxip-kiosk-agent.service \
|
||||||
|
voxip-kiosk-browser.service voxip-kiosk-x.service voxip-kiosk-manager.service 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
for unit in voxip-kiosk-x.service voxip-kiosk-browser.service voxip-kiosk-agent.service voxip-kiosk-manager.service \
|
||||||
|
voxip-kiosk-healthcheck.service voxip-kiosk-healthcheck.timer voxip-kiosk-update.service \
|
||||||
|
voxip-kiosk-update.timer voxip-network-guard.service voxip-boot-import.service; do
|
||||||
|
systemctl disable "$unit" >/dev/null 2>&1 || true
|
||||||
|
rm -f "/etc/systemd/system/$unit"
|
||||||
|
done
|
||||||
|
if [[ $LIVE -eq 1 ]]; then systemctl daemon-reload; fi
|
||||||
|
|
||||||
|
for old in /etc/voxip-kiosk /var/lib/voxip-kiosk /var/lib/voxip-kiosk-network /var/lib/voxip-kiosk-browser \
|
||||||
|
/var/log/voxip-kiosk /var/cache/voxip-kiosk; do
|
||||||
|
new="${old/voxip/daynivo}"
|
||||||
|
if [[ -d $old && ! -L $old && ! -e $new ]]; then
|
||||||
|
mv "$old" "$new"
|
||||||
|
ln -s "$new" "$old"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Same uid and gid, so every file keeps the right owner.
|
||||||
|
if id voxip >/dev/null 2>&1 && ! id daynivo >/dev/null 2>&1; then
|
||||||
|
usermod -l daynivo -d /var/lib/daynivo-kiosk voxip
|
||||||
|
if getent group voxip >/dev/null; then groupmod -n daynivo voxip; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Network profiles written by the web interface; not while a change awaits confirmation.
|
||||||
|
if [[ ! -f /var/lib/daynivo-kiosk-network/pending.json ]]; then
|
||||||
|
renamed=0
|
||||||
|
for iface in eth0 wlan0; do
|
||||||
|
old=/etc/NetworkManager/system-connections/voxip-$iface.nmconnection
|
||||||
|
new=/etc/NetworkManager/system-connections/daynivo-$iface.nmconnection
|
||||||
|
if [[ -f $old && ! -e $new ]]; then
|
||||||
|
sed -i "s/^id=voxip-$iface\$/id=daynivo-$iface/" "$old"
|
||||||
|
mv "$old" "$new"
|
||||||
|
chmod 0600 "$new"
|
||||||
|
renamed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ $renamed -eq 1 && $LIVE -eq 1 ]]; then nmcli connection reload 2>/dev/null || true; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /etc/sudoers.d/voxip-kiosk /etc/systemd/journald.conf.d/voxip-kiosk.conf \
|
||||||
|
/etc/systemd/system.conf.d/voxip-watchdog.conf /etc/logrotate.d/voxip-kiosk \
|
||||||
|
/etc/avahi/services/voxip-kiosk.service /etc/chromium/policies/managed/voxip-kiosk.json \
|
||||||
|
/etc/chromium-browser/policies/managed/voxip-kiosk.json /usr/local/sbin/voxip-kiosk-ctl
|
||||||
|
if [[ -f /etc/ssh/sshd_config.d/voxip-kiosk.conf ]]; then
|
||||||
|
mv /etc/ssh/sshd_config.d/voxip-kiosk.conf /etc/ssh/sshd_config.d/daynivo-kiosk.conf
|
||||||
|
fi
|
||||||
|
if [[ -f /etc/X11/Xwrapper.config.voxip-orig ]]; then
|
||||||
|
mv /etc/X11/Xwrapper.config.voxip-orig /etc/X11/Xwrapper.config.daynivo-orig
|
||||||
|
fi
|
||||||
|
if [[ -f /etc/systemd/system/display-kiosk.service.voxip-disabled ]]; then
|
||||||
|
mv /etc/systemd/system/display-kiosk.service.voxip-disabled /etc/systemd/system/display-kiosk.service.daynivo-disabled
|
||||||
|
fi
|
||||||
|
# The old programs: an updater that started this install has its code loaded already.
|
||||||
|
rm -rf /usr/lib/voxip-kiosk /usr/share/voxip-kiosk /usr/share/doc/voxip-kiosk /usr/share/plymouth/themes/voxip-kiosk
|
||||||
|
fi
|
||||||
|
|
||||||
# --- users ----------------------------------------------------------------------
|
# --- users ----------------------------------------------------------------------
|
||||||
say "Gebruikers aanmaken"
|
say "Gebruikers aanmaken"
|
||||||
getent group kiosk >/dev/null || groupadd --system kiosk
|
getent group kiosk >/dev/null || groupadd --system kiosk
|
||||||
@@ -113,11 +183,11 @@ id kiosk >/dev/null 2>&1 || useradd --system --gid kiosk --home-dir /home/kiosk
|
|||||||
for g in video render input audio; do
|
for g in video render input audio; do
|
||||||
getent group "$g" >/dev/null && usermod -aG "$g" kiosk
|
getent group "$g" >/dev/null && usermod -aG "$g" kiosk
|
||||||
done
|
done
|
||||||
getent group voxip >/dev/null || groupadd --system voxip
|
getent group daynivo >/dev/null || groupadd --system daynivo
|
||||||
id voxip >/dev/null 2>&1 || useradd --system --gid voxip --home-dir /var/lib/voxip-kiosk --no-create-home \
|
id daynivo >/dev/null 2>&1 || useradd --system --gid daynivo --home-dir /var/lib/daynivo-kiosk --no-create-home \
|
||||||
--shell /usr/sbin/nologin --comment "Daynivo Kiosk Manager" voxip
|
--shell /usr/sbin/nologin --comment "Daynivo Kiosk Manager" daynivo
|
||||||
for g in systemd-journal video; do
|
for g in systemd-journal video; do
|
||||||
getent group "$g" >/dev/null && usermod -aG "$g" voxip
|
getent group "$g" >/dev/null && usermod -aG "$g" daynivo
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- application files --------------------------------------------------------------
|
# --- application files --------------------------------------------------------------
|
||||||
@@ -125,7 +195,7 @@ say "Programmabestanden installeren"
|
|||||||
install -d -m 0755 "$LIB" "$SHARE" "$DOC"
|
install -d -m 0755 "$LIB" "$SHARE" "$DOC"
|
||||||
rm -rf "$LIB/python.new"
|
rm -rf "$LIB/python.new"
|
||||||
install -d -m 0755 "$LIB/python.new"
|
install -d -m 0755 "$LIB/python.new"
|
||||||
tar -C "$SRC/src" --exclude='__pycache__' --exclude='*.pyc' -cf - voxip_kiosk | tar -C "$LIB/python.new" -xf -
|
tar -C "$SRC/src" --exclude='__pycache__' --exclude='*.pyc' -cf - daynivo_kiosk | tar -C "$LIB/python.new" -xf -
|
||||||
chown -R root:root "$LIB/python.new"
|
chown -R root:root "$LIB/python.new"
|
||||||
chmod -R u=rwX,go=rX "$LIB/python.new"
|
chmod -R u=rwX,go=rX "$LIB/python.new"
|
||||||
python3 -m compileall -q "$LIB/python.new" >/dev/null
|
python3 -m compileall -q "$LIB/python.new" >/dev/null
|
||||||
@@ -134,10 +204,12 @@ rm -rf "$LIB/python.old"
|
|||||||
mv "$LIB/python.new" "$LIB/python"
|
mv "$LIB/python.new" "$LIB/python"
|
||||||
rm -rf "$LIB/python.old"
|
rm -rf "$LIB/python.old"
|
||||||
|
|
||||||
for bin in voxip-kiosk-agent voxip-kiosk-browser voxip-kiosk-healthcheck voxip-kiosk-helper voxip-kiosk-manager voxip-kiosk-x; do
|
for bin in daynivo-kiosk-agent daynivo-kiosk-browser daynivo-kiosk-healthcheck daynivo-kiosk-helper daynivo-kiosk-manager daynivo-kiosk-updater daynivo-kiosk-x; do
|
||||||
install -m 0755 -o root -g root "$SRC/bin/$bin" "$LIB/$bin"
|
install -m 0755 -o root -g root "$SRC/bin/$bin" "$LIB/$bin"
|
||||||
done
|
done
|
||||||
install -m 0755 -o root -g root "$SRC/bin/voxip-kiosk-ctl" /usr/local/sbin/voxip-kiosk-ctl
|
install -m 0755 -o root -g root "$SRC/bin/daynivo-kiosk-ctl" /usr/local/sbin/daynivo-kiosk-ctl
|
||||||
|
# The name from before the rename to Daynivo keeps working.
|
||||||
|
ln -sfn daynivo-kiosk-ctl /usr/local/sbin/voxip-kiosk-ctl
|
||||||
|
|
||||||
rm -rf "$SHARE/www"
|
rm -rf "$SHARE/www"
|
||||||
install -d -m 0755 "$SHARE/www"
|
install -d -m 0755 "$SHARE/www"
|
||||||
@@ -148,10 +220,10 @@ install -m 0644 "$SRC"/docs/*.md "$DOC/"
|
|||||||
|
|
||||||
# --- configuration -------------------------------------------------------------------
|
# --- configuration -------------------------------------------------------------------
|
||||||
say "Configuratie"
|
say "Configuratie"
|
||||||
install -d -o voxip -g kiosk -m 2750 "$ETC"
|
install -d -o daynivo -g kiosk -m 2750 "$ETC"
|
||||||
install -d -o voxip -g voxip -m 0750 /var/lib/voxip-kiosk
|
install -d -o daynivo -g daynivo -m 0750 /var/lib/daynivo-kiosk
|
||||||
install -d -o root -g root -m 0755 /var/lib/voxip-kiosk-network
|
install -d -o root -g root -m 0755 /var/lib/daynivo-kiosk-network
|
||||||
install -d -o voxip -g voxip -m 0750 /var/log/voxip-kiosk
|
install -d -o daynivo -g daynivo -m 0750 /var/log/daynivo-kiosk
|
||||||
|
|
||||||
# Migrate from Daynivo's raspberry-pi/install-kiosk.sh: take over its URL, then disable it.
|
# Migrate from Daynivo's raspberry-pi/install-kiosk.sh: take over its URL, then disable it.
|
||||||
LEGACY_KIOSK_UNIT=/etc/systemd/system/display-kiosk.service
|
LEGACY_KIOSK_UNIT=/etc/systemd/system/display-kiosk.service
|
||||||
@@ -162,14 +234,14 @@ if [[ -f $LEGACY_KIOSK_UNIT ]]; then
|
|||||||
say "URL overgenomen van de Daynivo-kioskinstallatie"
|
say "URL overgenomen van de Daynivo-kioskinstallatie"
|
||||||
fi
|
fi
|
||||||
if [[ $LIVE -eq 1 ]]; then systemctl disable --now display-kiosk.service 2>/dev/null || true; fi
|
if [[ $LIVE -eq 1 ]]; then systemctl disable --now display-kiosk.service 2>/dev/null || true; fi
|
||||||
mv "$LEGACY_KIOSK_UNIT" "$LEGACY_KIOSK_UNIT.voxip-disabled"
|
mv "$LEGACY_KIOSK_UNIT" "$LEGACY_KIOSK_UNIT.daynivo-disabled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PYRUN=(python3 -I -c)
|
PYRUN=(python3 -I -c)
|
||||||
"${PYRUN[@]}" '
|
"${PYRUN[@]}" '
|
||||||
import json, os, sys
|
import json, os, sys
|
||||||
sys.path.insert(0, "/usr/lib/voxip-kiosk/python")
|
sys.path.insert(0, "/usr/lib/daynivo-kiosk/python")
|
||||||
from voxip_kiosk import config as c
|
from daynivo_kiosk import config as c
|
||||||
path, default, url, name = sys.argv[1:5]
|
path, default, url, name = sys.argv[1:5]
|
||||||
updates = {}
|
updates = {}
|
||||||
if url: updates["url"] = url
|
if url: updates["url"] = url
|
||||||
@@ -191,36 +263,36 @@ else:
|
|||||||
c.atomic_write_json(c.Path(path), data)
|
c.atomic_write_json(c.Path(path), data)
|
||||||
print("Nieuwe configuratie aangemaakt")
|
print("Nieuwe configuratie aangemaakt")
|
||||||
' "$ETC/config.json" "$SRC/config/config.default.json" "$URL" "$NAME" || die "Ongeldige --url of --name."
|
' "$ETC/config.json" "$SRC/config/config.default.json" "$URL" "$NAME" || die "Ongeldige --url of --name."
|
||||||
chown voxip:kiosk "$ETC/config.json" "$ETC/config.json.bak" 2>/dev/null || true
|
chown daynivo:kiosk "$ETC/config.json" "$ETC/config.json.bak" 2>/dev/null || true
|
||||||
chmod 0640 "$ETC/config.json"
|
chmod 0640 "$ETC/config.json"
|
||||||
touch "$ETC/.config.lock" && chown voxip:kiosk "$ETC/.config.lock" && chmod 0640 "$ETC/.config.lock"
|
touch "$ETC/.config.lock" && chown daynivo:kiosk "$ETC/.config.lock" && chmod 0640 "$ETC/.config.lock"
|
||||||
[[ -f $ETC/admin.json ]] && chown voxip:voxip "$ETC/admin.json" && chmod 0600 "$ETC/admin.json"
|
[[ -f $ETC/admin.json ]] && chown daynivo:daynivo "$ETC/admin.json" && chmod 0600 "$ETC/admin.json"
|
||||||
|
|
||||||
# --- system integration ----------------------------------------------------------------
|
# --- system integration ----------------------------------------------------------------
|
||||||
say "Systeemintegratie"
|
say "Systeemintegratie"
|
||||||
TMP_SUDOERS="$(mktemp)"
|
TMP_SUDOERS="$(mktemp)"
|
||||||
install -m 0440 "$SRC/config/sudoers" "$TMP_SUDOERS"
|
install -m 0440 "$SRC/config/sudoers" "$TMP_SUDOERS"
|
||||||
visudo -cqf "$TMP_SUDOERS" || die "sudoers-bestand ongeldig"
|
visudo -cqf "$TMP_SUDOERS" || die "sudoers-bestand ongeldig"
|
||||||
mv "$TMP_SUDOERS" /etc/sudoers.d/voxip-kiosk
|
mv "$TMP_SUDOERS" /etc/sudoers.d/daynivo-kiosk
|
||||||
chown root:root /etc/sudoers.d/voxip-kiosk
|
chown root:root /etc/sudoers.d/daynivo-kiosk
|
||||||
|
|
||||||
install -D -m 0644 "$SRC/config/journald.conf" /etc/systemd/journald.conf.d/voxip-kiosk.conf
|
install -D -m 0644 "$SRC/config/journald.conf" /etc/systemd/journald.conf.d/daynivo-kiosk.conf
|
||||||
install -D -m 0644 "$SRC/config/watchdog.conf" /etc/systemd/system.conf.d/voxip-watchdog.conf
|
install -D -m 0644 "$SRC/config/watchdog.conf" /etc/systemd/system.conf.d/daynivo-watchdog.conf
|
||||||
install -D -m 0644 "$SRC/config/logrotate" /etc/logrotate.d/voxip-kiosk
|
install -D -m 0644 "$SRC/config/logrotate" /etc/logrotate.d/daynivo-kiosk
|
||||||
|
|
||||||
if [[ -f /etc/X11/Xwrapper.config && ! -f /etc/X11/Xwrapper.config.voxip-orig ]]; then
|
if [[ -f /etc/X11/Xwrapper.config && ! -f /etc/X11/Xwrapper.config.daynivo-orig ]]; then
|
||||||
cp -a /etc/X11/Xwrapper.config /etc/X11/Xwrapper.config.voxip-orig
|
cp -a /etc/X11/Xwrapper.config /etc/X11/Xwrapper.config.daynivo-orig
|
||||||
fi
|
fi
|
||||||
install -D -m 0644 "$SRC/config/Xwrapper.config" /etc/X11/Xwrapper.config
|
install -D -m 0644 "$SRC/config/Xwrapper.config" /etc/X11/Xwrapper.config
|
||||||
|
|
||||||
for dir in /etc/chromium/policies/managed /etc/chromium-browser/policies/managed; do
|
for dir in /etc/chromium/policies/managed /etc/chromium-browser/policies/managed; do
|
||||||
install -D -m 0644 "$SRC/config/chromium/voxip-kiosk.json" "$dir/voxip-kiosk.json"
|
install -D -m 0644 "$SRC/config/chromium/daynivo-kiosk.json" "$dir/daynivo-kiosk.json"
|
||||||
done
|
done
|
||||||
|
|
||||||
ADMIN_PORT="$(python3 -I -c 'import sys; sys.path.insert(0, "/usr/lib/voxip-kiosk/python"); from voxip_kiosk.config import load_config; print(load_config(__import__("pathlib").Path(sys.argv[1]))["admin_port"])' "$ETC/config.json")"
|
ADMIN_PORT="$(python3 -I -c 'import sys; sys.path.insert(0, "/usr/lib/daynivo-kiosk/python"); from daynivo_kiosk.config import load_config; print(load_config(__import__("pathlib").Path(sys.argv[1]))["admin_port"])' "$ETC/config.json")"
|
||||||
install -d -m 0755 /etc/avahi/services
|
install -d -m 0755 /etc/avahi/services
|
||||||
sed "s#<port>8080</port>#<port>${ADMIN_PORT}</port>#" "$SRC/config/avahi-voxip.service" > /etc/avahi/services/voxip-kiosk.service
|
sed "s#<port>8080</port>#<port>${ADMIN_PORT}</port>#" "$SRC/config/avahi-daynivo.service" > /etc/avahi/services/daynivo-kiosk.service
|
||||||
chmod 0644 /etc/avahi/services/voxip-kiosk.service
|
chmod 0644 /etc/avahi/services/daynivo-kiosk.service
|
||||||
|
|
||||||
# Swap: zram in RAM instead of a swap file on the SD card (Trixie's rpi-swap already does this).
|
# Swap: zram in RAM instead of a swap file on the SD card (Trixie's rpi-swap already does this).
|
||||||
if dpkg -s systemd-zram-generator >/dev/null 2>&1 && ! dpkg -s rpi-swap >/dev/null 2>&1; then
|
if dpkg -s systemd-zram-generator >/dev/null 2>&1 && ! dpkg -s rpi-swap >/dev/null 2>&1; then
|
||||||
@@ -260,18 +332,18 @@ done
|
|||||||
|
|
||||||
# Boot splash: logo on white (config/plymouth). Rebuilding the initramfs takes a while
|
# Boot splash: logo on white (config/plymouth). Rebuilding the initramfs takes a while
|
||||||
# on a Pi 3, so only when the theme changed.
|
# on a Pi 3, so only when the theme changed.
|
||||||
SPLASH_DIR=/usr/share/plymouth/themes/voxip-kiosk
|
SPLASH_DIR=/usr/share/plymouth/themes/daynivo-kiosk
|
||||||
install -d -m 0755 "$SPLASH_DIR"
|
install -d -m 0755 "$SPLASH_DIR"
|
||||||
install -m 0644 "$SRC"/config/plymouth/voxip-kiosk/* "$SPLASH_DIR/"
|
install -m 0644 "$SRC"/config/plymouth/daynivo-kiosk/* "$SPLASH_DIR/"
|
||||||
if [[ ! -f /var/lib/voxip-kiosk/previous-plymouth-theme ]]; then
|
if [[ ! -f /var/lib/daynivo-kiosk/previous-plymouth-theme ]]; then
|
||||||
plymouth-set-default-theme > /var/lib/voxip-kiosk/previous-plymouth-theme 2>/dev/null || true
|
plymouth-set-default-theme > /var/lib/daynivo-kiosk/previous-plymouth-theme 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
SPLASH_SUM="$(cat "$SPLASH_DIR"/* | sha256sum | cut -d' ' -f1)"
|
SPLASH_SUM="$(cat "$SPLASH_DIR"/* | sha256sum | cut -d' ' -f1)"
|
||||||
if [[ "$(plymouth-set-default-theme 2>/dev/null)" != voxip-kiosk || "$(cat /var/lib/voxip-kiosk/splash.sha256 2>/dev/null)" != "$SPLASH_SUM" ]]; then
|
if [[ "$(plymouth-set-default-theme 2>/dev/null)" != daynivo-kiosk || "$(cat /var/lib/daynivo-kiosk/splash.sha256 2>/dev/null)" != "$SPLASH_SUM" ]]; then
|
||||||
say "Opstartscherm instellen (initramfs bijwerken, dit duurt even)"
|
say "Opstartscherm instellen (initramfs bijwerken, dit duurt even)"
|
||||||
plymouth-set-default-theme voxip-kiosk
|
plymouth-set-default-theme daynivo-kiosk
|
||||||
if update-initramfs -u -k all >/dev/null 2>&1; then
|
if update-initramfs -u -k all >/dev/null 2>&1; then
|
||||||
echo "$SPLASH_SUM" > /var/lib/voxip-kiosk/splash.sha256
|
echo "$SPLASH_SUM" > /var/lib/daynivo-kiosk/splash.sha256
|
||||||
else
|
else
|
||||||
warn "initramfs bijwerken mislukt; het opstartscherm verschijnt mogelijk pas later tijdens het opstarten."
|
warn "initramfs bijwerken mislukt; het opstartscherm verschijnt mogelijk pas later tijdens het opstarten."
|
||||||
fi
|
fi
|
||||||
@@ -283,8 +355,8 @@ for dm in lightdm.service gdm3.service sddm.service; do
|
|||||||
if systemctl is-enabled --quiet "$dm" 2>/dev/null; then
|
if systemctl is-enabled --quiet "$dm" 2>/dev/null; then
|
||||||
warn "$dm wordt uitgeschakeld (de kiosk gebruikt een eigen minimale X-sessie)."
|
warn "$dm wordt uitgeschakeld (de kiosk gebruikt een eigen minimale X-sessie)."
|
||||||
systemctl disable "$dm" 2>/dev/null || true
|
systemctl disable "$dm" 2>/dev/null || true
|
||||||
grep -qx "$dm" /var/lib/voxip-kiosk/disabled-display-managers 2>/dev/null \
|
grep -qx "$dm" /var/lib/daynivo-kiosk/disabled-display-managers 2>/dev/null \
|
||||||
|| echo "$dm" >> /var/lib/voxip-kiosk/disabled-display-managers
|
|| echo "$dm" >> /var/lib/daynivo-kiosk/disabled-display-managers
|
||||||
fi
|
fi
|
||||||
if [[ $LIVE -eq 1 ]] && systemctl is-active --quiet "$dm" 2>/dev/null; then
|
if [[ $LIVE -eq 1 ]] && systemctl is-active --quiet "$dm" 2>/dev/null; then
|
||||||
warn "$dm wordt gestopt: de desktop op het scherm sluit nu af."
|
warn "$dm wordt gestopt: de desktop op het scherm sluit nu af."
|
||||||
@@ -299,15 +371,15 @@ for unit in "${UNITS[@]}"; do
|
|||||||
done
|
done
|
||||||
if [[ $LIVE -eq 1 ]]; then systemctl daemon-reload; fi
|
if [[ $LIVE -eq 1 ]]; then systemctl daemon-reload; fi
|
||||||
systemctl enable "${ENABLE[@]}" NetworkManager.service avahi-daemon.service >/dev/null
|
systemctl enable "${ENABLE[@]}" NetworkManager.service avahi-daemon.service >/dev/null
|
||||||
if [[ ! -f /var/lib/voxip-kiosk/previous-default-target ]]; then
|
if [[ ! -f /var/lib/daynivo-kiosk/previous-default-target ]]; then
|
||||||
systemctl get-default > /var/lib/voxip-kiosk/previous-default-target 2>/dev/null || true
|
systemctl get-default > /var/lib/daynivo-kiosk/previous-default-target 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
systemctl set-default multi-user.target >/dev/null
|
systemctl set-default multi-user.target >/dev/null
|
||||||
|
|
||||||
if [[ -n $NEW_HOSTNAME && $LIVE -eq 0 ]]; then
|
if [[ -n $NEW_HOSTNAME && $LIVE -eq 0 ]]; then
|
||||||
warn "--hostname wordt genegeerd in image-modus (gebruik TARGET_HOSTNAME van pi-gen)."
|
warn "--hostname wordt genegeerd in image-modus (gebruik TARGET_HOSTNAME van pi-gen)."
|
||||||
elif [[ -n $NEW_HOSTNAME ]]; then
|
elif [[ -n $NEW_HOSTNAME ]]; then
|
||||||
"$LIB/voxip-kiosk-helper" set-hostname "$NEW_HOSTNAME" >/dev/null || die "Ongeldige hostname: $NEW_HOSTNAME"
|
"$LIB/daynivo-kiosk-helper" set-hostname "$NEW_HOSTNAME" >/dev/null || die "Ongeldige hostname: $NEW_HOSTNAME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $LIVE -eq 0 ]]; then
|
if [[ $LIVE -eq 0 ]]; then
|
||||||
@@ -318,11 +390,11 @@ fi
|
|||||||
say "Services starten"
|
say "Services starten"
|
||||||
systemctl restart systemd-journald.service || true
|
systemctl restart systemd-journald.service || true
|
||||||
systemctl restart avahi-daemon.service || true
|
systemctl restart avahi-daemon.service || true
|
||||||
systemctl restart voxip-kiosk-manager.service voxip-kiosk-agent.service
|
systemctl restart daynivo-kiosk-manager.service daynivo-kiosk-agent.service
|
||||||
systemctl start voxip-kiosk-healthcheck.timer
|
systemctl start daynivo-kiosk-healthcheck.timer daynivo-kiosk-update.timer
|
||||||
systemctl reset-failed voxip-kiosk-x.service voxip-kiosk-browser.service 2>/dev/null || true
|
systemctl reset-failed daynivo-kiosk-x.service daynivo-kiosk-browser.service 2>/dev/null || true
|
||||||
systemctl restart voxip-kiosk-x.service
|
systemctl restart daynivo-kiosk-x.service
|
||||||
systemctl restart voxip-kiosk-browser.service
|
systemctl restart daynivo-kiosk-browser.service
|
||||||
|
|
||||||
HOST="$(hostname)"
|
HOST="$(hostname)"
|
||||||
SCHEME=http
|
SCHEME=http
|
||||||
@@ -339,4 +411,4 @@ if [[ ! -f $ETC/admin.json ]]; then
|
|||||||
echo " Setupcode: $(cat "$ETC/setup-code") (staat ook op het scherm)"
|
echo " Setupcode: $(cat "$ETC/setup-code") (staat ook op het scherm)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo " Status: sudo voxip-kiosk-ctl status"
|
echo " Status: sudo daynivo-kiosk-ctl status"
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "voxip-kiosk"
|
name = "daynivo-kiosk"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Daynivo Kiosk OS — kiosk runtime and Daynivo Kiosk Manager"
|
description = "Daynivo Kiosk OS — kiosk runtime and Daynivo Kiosk Manager"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
File renamed without changes.
@@ -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
|
Runs as user ``kiosk`` and steers Chromium through the DevTools Protocol on
|
||||||
127.0.0.1:
|
127.0.0.1:
|
||||||
@@ -33,7 +33,7 @@ from .config import admin_url, load_config
|
|||||||
from .paths import Paths, load_paths
|
from .paths import Paths, load_paths
|
||||||
from .validation import mask_url
|
from .validation import mask_url
|
||||||
|
|
||||||
log = logging.getLogger("voxip-kiosk-agent")
|
log = logging.getLogger("daynivo-kiosk-agent")
|
||||||
|
|
||||||
TICK_SECONDS = 5
|
TICK_SECONDS = 5
|
||||||
PROBE_MAX_AGE_LOADING = 10
|
PROBE_MAX_AGE_LOADING = 10
|
||||||
@@ -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
|
server is up. Exit status tells systemd whether to restart: 1 when
|
||||||
``browser_restart`` is enabled (``Restart=on-failure`` restarts it and the
|
``browser_restart`` is enabled (``Restart=on-failure`` restarts it and the
|
||||||
start limit catches crash loops), 0 when it is disabled.
|
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 .paths import Paths, load_paths
|
||||||
from .validation import mask_url, parse_resolution
|
from .validation import mask_url, parse_resolution
|
||||||
|
|
||||||
log = logging.getLogger("voxip-kiosk-browser")
|
log = logging.getLogger("daynivo-kiosk-browser")
|
||||||
|
|
||||||
CDP_PORT = 9222
|
CDP_PORT = 9222
|
||||||
CHROMIUM_CANDIDATES = ("/usr/bin/chromium-browser", "/usr/bin/chromium")
|
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 = data.setdefault("profile", {})
|
||||||
profile["exit_type"] = "Normal"
|
profile["exit_type"] = "Normal"
|
||||||
profile["exited_cleanly"] = True
|
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")
|
tmp.write_text(json.dumps(data), encoding="utf-8")
|
||||||
os.replace(tmp, prefs)
|
os.replace(tmp, prefs)
|
||||||
|
|
||||||
|
|
||||||
def profile_dirs(config: dict[str, Any]) -> tuple[Path, Path]:
|
def profile_dirs(config: dict[str, Any]) -> tuple[Path, Path]:
|
||||||
runtime = Path(os.environ.get("RUNTIME_DIRECTORY", "/run/voxip-kiosk-browser"))
|
runtime = Path(os.environ.get("RUNTIME_DIRECTORY", "/run/daynivo-kiosk-browser"))
|
||||||
state = Path(os.environ.get("STATE_DIRECTORY", "/var/lib/voxip-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"
|
profile = state / "profile" if config.get("browser_profile") == "persistent" else runtime / "profile"
|
||||||
if config.get("browser_profile") != "persistent" and (state / "profile").exists():
|
if config.get("browser_profile") != "persistent" and (state / "profile").exists():
|
||||||
shutil.rmtree(state / "profile", ignore_errors=True)
|
shutil.rmtree(state / "profile", ignore_errors=True)
|
||||||
File renamed without changes.
@@ -36,6 +36,7 @@ DEFAULTS: dict[str, Any] = {
|
|||||||
"admin_port": 8080,
|
"admin_port": 8080,
|
||||||
"admin_https": False,
|
"admin_https": False,
|
||||||
"admin_allow_public": False,
|
"admin_allow_public": False,
|
||||||
|
"auto_update": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ VALIDATORS: dict[str, Callable[[Any], Any]] = {
|
|||||||
"admin_port": _int_range(1024, 65535, "Poort moet tussen 1024 en 65535 liggen."),
|
"admin_port": _int_range(1024, 65535, "Poort moet tussen 1024 en 65535 liggen."),
|
||||||
"admin_https": _bool,
|
"admin_https": _bool,
|
||||||
"admin_allow_public": _bool,
|
"admin_allow_public": _bool,
|
||||||
|
"auto_update": _bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +236,7 @@ def save_config(path: Path, updates: dict[str, Any]) -> dict[str, Any]:
|
|||||||
lock_path = path.with_name(".config.lock")
|
lock_path = path.with_name(".config.lock")
|
||||||
with _thread_lock:
|
with _thread_lock:
|
||||||
# Read-only is enough for flock and works for every writer (the web
|
# 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)
|
lock_fd = os.open(str(lock_path), os.O_RDONLY | os.O_CREAT, 0o640)
|
||||||
try:
|
try:
|
||||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""voxip-kiosk-ctl — command-line management over SSH (run with sudo)."""
|
"""daynivo-kiosk-ctl — command-line management over SSH (run with sudo)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ def cmd_status(args) -> int:
|
|||||||
print(f"Apparaat: {config['device_name']}")
|
print(f"Apparaat: {config['device_name']}")
|
||||||
print(f"URL: {config['url'] if args.show_secrets else mask_url(config['url'])}")
|
print(f"URL: {config['url'] if args.show_secrets else mask_url(config['url'])}")
|
||||||
print(f"Beheer: {'https' if config['admin_https'] else 'http'}://{os.uname().nodename}.local:{config['admin_port']}")
|
print(f"Beheer: {'https' if config['admin_https'] else 'http'}://{os.uname().nodename}.local:{config['admin_port']}")
|
||||||
print(f"Wachtwoord: {'ingesteld' if paths.admin_file.exists() else 'NOG NIET INGESTELD (setupcode: sudo voxip-kiosk-ctl setup-code)'}")
|
print(f"Wachtwoord: {'ingesteld' if paths.admin_file.exists() else 'NOG NIET INGESTELD (setupcode: sudo daynivo-kiosk-ctl setup-code)'}")
|
||||||
for key, info in services.summary().items():
|
for key, info in services.summary().items():
|
||||||
print(f"{info['unit']:<34} {info['label']} (herstarts: {info['restarts']})")
|
print(f"{info['unit']:<34} {info['label']} (herstarts: {info['restarts']})")
|
||||||
if agent:
|
if agent:
|
||||||
@@ -48,7 +48,7 @@ def cmd_setup_code(_args) -> int:
|
|||||||
return 1
|
return 1
|
||||||
code = setupcode.read(paths.setup_code_file)
|
code = setupcode.read(paths.setup_code_file)
|
||||||
if not code:
|
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
|
return 1
|
||||||
print(code)
|
print(code)
|
||||||
return 0
|
return 0
|
||||||
@@ -65,10 +65,10 @@ def cmd_reset_password(args) -> int:
|
|||||||
code = setupcode.generate()
|
code = setupcode.generate()
|
||||||
setupcode.write(paths.setup_code_file, code)
|
setupcode.write(paths.setup_code_file, code)
|
||||||
try:
|
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:
|
except KeyError:
|
||||||
pass
|
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(f"Wachtwoord verwijderd. Nieuwe setupcode: {code}")
|
||||||
print("Open de beheerinterface en stel een nieuw wachtwoord in; de code staat ook op het kioskscherm.")
|
print("Open de beheerinterface en stel een nieuw wachtwoord in; de code staat ook op het kioskscherm.")
|
||||||
return 0
|
return 0
|
||||||
@@ -138,7 +138,7 @@ def cmd_version(_args) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(prog="voxip-kiosk-ctl", description="Beheer van Daynivo Kiosk OS vanaf de commandoregel.")
|
parser = argparse.ArgumentParser(prog="daynivo-kiosk-ctl", description="Beheer van Daynivo Kiosk OS vanaf de commandoregel.")
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
p = sub.add_parser("status", help="toon de status")
|
p = sub.add_parser("status", help="toon de status")
|
||||||
p.add_argument("--show-secrets", action="store_true", help="toon de volledige URL inclusief token")
|
p.add_argument("--show-secrets", action="store_true", help="toon de volledige URL inclusief token")
|
||||||
@@ -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
|
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
|
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 .config import load_config
|
||||||
from .paths import Paths, load_paths
|
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)
|
ALWAYS_ON = (services.AGENT_UNIT, services.MANAGER_UNIT)
|
||||||
DISPLAY_UNITS = (services.X_UNIT, services.BROWSER_UNIT)
|
DISPLAY_UNITS = (services.X_UNIT, services.BROWSER_UNIT)
|
||||||
@@ -147,7 +147,7 @@ class HealthCheck:
|
|||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
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("--check-only", action="store_true", help="alleen rapporteren, niets herstellen")
|
||||||
parser.add_argument("--json", action="store_true", help="rapport als JSON")
|
parser.add_argument("--json", action="store_true", help="rapport als JSON")
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
@@ -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
|
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
|
every argument itself, never invokes a shell and prints a single JSON
|
||||||
object on stdout.
|
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
|
from __future__ import annotations
|
||||||
@@ -23,17 +23,21 @@ from pathlib import Path
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from . import config as cfg
|
from . import config as cfg
|
||||||
from . import netconfig
|
from . import netconfig, updater
|
||||||
from . import validation as v
|
from . import validation as v
|
||||||
from .paths import Paths, default_paths
|
from .paths import Paths, default_paths
|
||||||
|
|
||||||
HELPER_PATH = "/usr/lib/voxip-kiosk/voxip-kiosk-helper"
|
HELPER_PATH = "/usr/lib/daynivo-kiosk/daynivo-kiosk-helper"
|
||||||
ROLLBACK_UNIT = "voxip-network-rollback"
|
ROLLBACK_UNIT = "daynivo-network-rollback"
|
||||||
ACTIVATE_UNIT = "voxip-network-activate"
|
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"))
|
BOOT_DIRS = (Path("/boot/firmware"), Path("/boot"))
|
||||||
RESET_TRIGGER = "voxip-network-reset"
|
# Files on the boot partition. The daynivo-* names are from before the rename to
|
||||||
IMPORT_CONFIG = "voxip-config.json"
|
# Daynivo and keep working, so existing SD cards and instructions stay valid.
|
||||||
IMPORT_KEYS = "voxip-authorized_keys"
|
RESET_TRIGGERS = ("daynivo-network-reset", "voxip-network-reset")
|
||||||
|
IMPORT_CONFIGS = ("voxip-config.json", "daynivo-config.json") # applied in this order: the Daynivo name wins
|
||||||
|
IMPORT_KEYS = ("voxip-authorized_keys", "daynivo-authorized_keys")
|
||||||
SSH_KEY_TYPES = ("ssh-ed25519", "ssh-rsa", "ecdsa-sha2-", "sk-ssh-ed25519@openssh.com", "sk-ecdsa-sha2-")
|
SSH_KEY_TYPES = ("ssh-ed25519", "ssh-rsa", "ecdsa-sha2-", "sk-ssh-ed25519@openssh.com", "sk-ecdsa-sha2-")
|
||||||
|
|
||||||
# Verbs the web interface may call through sudo. Everything else is only
|
# Verbs the web interface may call through sudo. Everything else is only
|
||||||
@@ -41,6 +45,7 @@ SSH_KEY_TYPES = ("ssh-ed25519", "ssh-rsa", "ecdsa-sha2-", "sk-ssh-ed25519@openss
|
|||||||
PUBLIC_VERBS = {
|
PUBLIC_VERBS = {
|
||||||
"reboot", "poweroff", "restart-browser", "restart-manager", "set-hostname", "set-wifi-country",
|
"reboot", "poweroff", "restart-browser", "restart-manager", "set-hostname", "set-wifi-country",
|
||||||
"wifi-scan", "network-apply", "network-confirm", "network-rollback", "network-pending",
|
"wifi-scan", "network-apply", "network-confirm", "network-rollback", "network-pending",
|
||||||
|
"update-check", "update-install",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,27 +83,43 @@ class Helper:
|
|||||||
# -- power & browser -------------------------------------------------
|
# -- power & browser -------------------------------------------------
|
||||||
|
|
||||||
def reboot(self) -> dict[str, Any]:
|
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")
|
log(syslog.LOG_NOTICE, "Herstart aangevraagd via beheerinterface")
|
||||||
return {"message": "De Raspberry Pi wordt over enkele seconden herstart."}
|
return {"message": "De Raspberry Pi wordt over enkele seconden herstart."}
|
||||||
|
|
||||||
def poweroff(self) -> dict[str, Any]:
|
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")
|
log(syslog.LOG_NOTICE, "Uitschakelen aangevraagd via beheerinterface")
|
||||||
return {"message": "De Raspberry Pi wordt over enkele seconden uitgeschakeld."}
|
return {"message": "De Raspberry Pi wordt over enkele seconden uitgeschakeld."}
|
||||||
|
|
||||||
def restart_browser(self) -> dict[str, Any]:
|
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", "reset-failed", "daynivo-kiosk-browser.service", "daynivo-kiosk-x.service"], check=False)
|
||||||
self.run(["systemctl", "restart", "--no-block", "voxip-kiosk-browser.service"])
|
self.run(["systemctl", "restart", "--no-block", "daynivo-kiosk-browser.service"])
|
||||||
log(syslog.LOG_NOTICE, "Chromium wordt herstart")
|
log(syslog.LOG_NOTICE, "Chromium wordt herstart")
|
||||||
return {"message": "Chromium wordt herstart."}
|
return {"message": "Chromium wordt herstart."}
|
||||||
|
|
||||||
def restart_manager(self) -> dict[str, Any]:
|
def restart_manager(self) -> dict[str, Any]:
|
||||||
# Delayed so the web interface can still answer the request that asked for it.
|
# 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")
|
log(syslog.LOG_NOTICE, "Beheerinterface wordt herstart")
|
||||||
return {"message": "De beheerinterface wordt herstart."}
|
return {"message": "De beheerinterface wordt herstart."}
|
||||||
|
|
||||||
|
# -- software updates -----------------------------------------------
|
||||||
|
|
||||||
|
def update_check(self) -> dict[str, Any]:
|
||||||
|
state, _ = updater.Updater(self.paths).check()
|
||||||
|
return {"update": state}
|
||||||
|
|
||||||
|
def update_install(self) -> dict[str, Any]:
|
||||||
|
# A transient unit of its own: install.sh restarts the web interface, which
|
||||||
|
# must not take the running update down with it.
|
||||||
|
proc = self.run(["systemd-run", f"--unit={UPDATE_UNIT}", "--collect", "--quiet",
|
||||||
|
UPDATER_PATH, "--install"], check=False)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise HelperError("Er loopt al een update, of die kon niet worden gestart.")
|
||||||
|
log(syslog.LOG_NOTICE, "Update gestart via beheerinterface")
|
||||||
|
return {"message": "De update is gestart. De beheerinterface is zo meteen even niet bereikbaar."}
|
||||||
|
|
||||||
# -- hostname -------------------------------------------------------
|
# -- hostname -------------------------------------------------------
|
||||||
|
|
||||||
def set_hostname(self, name: str) -> dict[str, Any]:
|
def set_hostname(self, name: str) -> dict[str, Any]:
|
||||||
@@ -287,15 +308,16 @@ class Helper:
|
|||||||
self.network_restore(boot=True)
|
self.network_restore(boot=True)
|
||||||
result["rolled_back"] = True
|
result["rolled_back"] = True
|
||||||
boot = self._boot_dir()
|
boot = self._boot_dir()
|
||||||
trigger = boot / RESET_TRIGGER if boot else None
|
triggers = [boot / name for name in RESET_TRIGGERS if (boot / name).exists()] if boot else []
|
||||||
if trigger and trigger.exists():
|
if triggers:
|
||||||
stash = self.paths.network_state_dir / f"disabled-{int(time.time())}"
|
stash = self.paths.network_state_dir / f"disabled-{int(time.time())}"
|
||||||
stash.mkdir(parents=True, mode=0o700)
|
stash.mkdir(parents=True, mode=0o700)
|
||||||
for iface in netconfig.INTERFACES:
|
for iface in netconfig.INTERFACES:
|
||||||
src = self._keyfile_path(iface)
|
src = self._keyfile_path(iface)
|
||||||
if src.exists():
|
if src.exists():
|
||||||
shutil.move(str(src), str(stash / src.name))
|
shutil.move(str(src), str(stash / src.name))
|
||||||
trigger.unlink()
|
for trigger in triggers:
|
||||||
|
trigger.unlink()
|
||||||
log(syslog.LOG_WARNING, f"Netwerk-reset uitgevoerd; Daynivo-profielen verplaatst naar {stash}")
|
log(syslog.LOG_WARNING, f"Netwerk-reset uitgevoerd; Daynivo-profielen verplaatst naar {stash}")
|
||||||
result["reset"] = True
|
result["reset"] = True
|
||||||
return result
|
return result
|
||||||
@@ -304,7 +326,7 @@ class Helper:
|
|||||||
|
|
||||||
def _chown_config(self, path: Path) -> None:
|
def _chown_config(self, path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
uid = pwd.getpwnam("voxip").pw_uid
|
uid = pwd.getpwnam("daynivo").pw_uid
|
||||||
gid = grp.getgrnam("kiosk").gr_gid
|
gid = grp.getgrnam("kiosk").gr_gid
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
return
|
||||||
@@ -316,12 +338,14 @@ class Helper:
|
|||||||
if boot is None:
|
if boot is None:
|
||||||
return {"imported": []}
|
return {"imported": []}
|
||||||
imported = []
|
imported = []
|
||||||
conf = boot / IMPORT_CONFIG
|
for name in IMPORT_CONFIGS:
|
||||||
if conf.exists():
|
conf = boot / name
|
||||||
|
if not conf.exists():
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
data = json.loads(conf.read_text(encoding="utf-8-sig"))
|
data = json.loads(conf.read_text(encoding="utf-8-sig"))
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise HelperError("voxip-config.json moet een JSON-object zijn")
|
raise HelperError(f"{name} moet een JSON-object zijn")
|
||||||
data = dict(data)
|
data = dict(data)
|
||||||
host = data.pop("hostname", None)
|
host = data.pop("hostname", None)
|
||||||
country = data.pop("wifi_country", None)
|
country = data.pop("wifi_country", None)
|
||||||
@@ -332,22 +356,24 @@ class Helper:
|
|||||||
self.set_hostname(str(host))
|
self.set_hostname(str(host))
|
||||||
if country:
|
if country:
|
||||||
self.set_wifi_country(str(country))
|
self.set_wifi_country(str(country))
|
||||||
conf.rename(conf.with_name(IMPORT_CONFIG + ".applied"))
|
conf.rename(conf.with_name(name + ".applied"))
|
||||||
imported.append(IMPORT_CONFIG)
|
imported.append(name)
|
||||||
log(syslog.LOG_NOTICE, "Configuratie geïmporteerd vanaf de bootpartitie")
|
log(syslog.LOG_NOTICE, f"Configuratie geïmporteerd vanaf de bootpartitie ({name})")
|
||||||
except (HelperError, cfg.ConfigError, v.ValidationError, ValueError, OSError) as exc:
|
except (HelperError, cfg.ConfigError, v.ValidationError, ValueError, OSError) as exc:
|
||||||
(boot / "voxip-config.error.txt").write_text(f"{exc}\n", encoding="utf-8")
|
(boot / name.replace(".json", ".error.txt")).write_text(f"{exc}\n", encoding="utf-8")
|
||||||
conf.rename(conf.with_name(IMPORT_CONFIG + ".rejected"))
|
conf.rename(conf.with_name(name + ".rejected"))
|
||||||
log(syslog.LOG_ERR, f"voxip-config.json geweigerd: {exc}")
|
log(syslog.LOG_ERR, f"{name} geweigerd: {exc}")
|
||||||
keys = boot / IMPORT_KEYS
|
for name in IMPORT_KEYS:
|
||||||
if keys.exists():
|
keys = boot / name
|
||||||
|
if not keys.exists():
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
self._import_ssh_keys(keys.read_text(encoding="utf-8-sig"))
|
self._import_ssh_keys(keys.read_text(encoding="utf-8-sig"))
|
||||||
keys.rename(keys.with_name(IMPORT_KEYS + ".applied"))
|
keys.rename(keys.with_name(name + ".applied"))
|
||||||
imported.append(IMPORT_KEYS)
|
imported.append(name)
|
||||||
except (HelperError, OSError) as exc:
|
except (HelperError, OSError) as exc:
|
||||||
keys.rename(keys.with_name(IMPORT_KEYS + ".rejected"))
|
keys.rename(keys.with_name(name + ".rejected"))
|
||||||
log(syslog.LOG_ERR, f"SSH-sleutels geweigerd: {exc}")
|
log(syslog.LOG_ERR, f"SSH-sleutels geweigerd ({name}): {exc}")
|
||||||
return {"imported": imported}
|
return {"imported": imported}
|
||||||
|
|
||||||
def _import_ssh_keys(self, text: str) -> None:
|
def _import_ssh_keys(self, text: str) -> None:
|
||||||
@@ -366,7 +392,7 @@ class Helper:
|
|||||||
os.chown(ssh_dir, user.pw_uid, user.pw_gid)
|
os.chown(ssh_dir, user.pw_uid, user.pw_gid)
|
||||||
sshd_dir = self.etc / "ssh" / "sshd_config.d"
|
sshd_dir = self.etc / "ssh" / "sshd_config.d"
|
||||||
sshd_dir.mkdir(parents=True, exist_ok=True)
|
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"
|
"# Beheerd door Daynivo Kiosk OS: alleen inloggen met SSH-sleutels.\n"
|
||||||
"PasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no\n",
|
"PasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -403,6 +429,8 @@ class Helper:
|
|||||||
"network-pending": lambda: (no_args(), self.network_pending())[1],
|
"network-pending": lambda: (no_args(), self.network_pending())[1],
|
||||||
"network-boot-guard": lambda: (no_args(), self.network_boot_guard())[1],
|
"network-boot-guard": lambda: (no_args(), self.network_boot_guard())[1],
|
||||||
"boot-import": lambda: (no_args(), self.boot_import())[1],
|
"boot-import": lambda: (no_args(), self.boot_import())[1],
|
||||||
|
"update-check": lambda: (no_args(), self.update_check())[1],
|
||||||
|
"update-install": lambda: (no_args(), self.update_install())[1],
|
||||||
}
|
}
|
||||||
if verb not in table:
|
if verb not in table:
|
||||||
raise HelperError(f"Onbekende opdracht: {verb}")
|
raise HelperError(f"Onbekende opdracht: {verb}")
|
||||||
@@ -421,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:
|
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
|
argv = sys.argv[1:] if argv is None else argv
|
||||||
environ = os.environ if environ is None else environ
|
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:
|
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
|
return 2
|
||||||
verb, args = argv[0], argv[1:]
|
verb, args = argv[0], argv[1:]
|
||||||
caller = environ.get("SUDO_USER")
|
caller = environ.get("SUDO_USER")
|
||||||
@@ -432,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"}))
|
print(json.dumps({"ok": False, "error": "Opdracht niet toegestaan"}))
|
||||||
return 1
|
return 1
|
||||||
if helper is None and os.geteuid() != 0:
|
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
|
return 1
|
||||||
helper = helper or Helper()
|
helper = helper or Helper()
|
||||||
stdin = sys.stdin.read(16385) if verb == "network-apply" else ""
|
stdin = sys.stdin.read(16385) if verb == "network-apply" else ""
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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
|
Reads port and HTTPS settings from the kiosk configuration, creates a
|
||||||
self-signed certificate when HTTPS is enabled and none exists yet, then
|
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 .config import load_config
|
||||||
from .paths import Paths, load_paths
|
from .paths import Paths, load_paths
|
||||||
|
|
||||||
log = logging.getLogger("voxip-kiosk-manager")
|
log = logging.getLogger("daynivo-kiosk-manager")
|
||||||
PYTHON_LIB = "/usr/lib/voxip-kiosk/python"
|
PYTHON_LIB = "/usr/lib/daynivo-kiosk/python"
|
||||||
|
|
||||||
|
|
||||||
def ensure_certificate(paths: Paths) -> tuple[Path, Path]:
|
def ensure_certificate(paths: Paths) -> tuple[Path, Path]:
|
||||||
@@ -55,7 +55,7 @@ def gunicorn_argv(paths: Paths, config: dict) -> list[str]:
|
|||||||
if config["admin_https"]:
|
if config["admin_https"]:
|
||||||
cert, key = ensure_certificate(paths)
|
cert, key = ensure_certificate(paths)
|
||||||
argv += ["--certfile", str(cert), "--keyfile", str(key)]
|
argv += ["--certfile", str(cert), "--keyfile", str(key)]
|
||||||
argv.append("voxip_kiosk.web:create_app()")
|
argv.append("daynivo_kiosk.web:create_app()")
|
||||||
return argv
|
return argv
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ from typing import Any
|
|||||||
from . import validation as v
|
from . import validation as v
|
||||||
|
|
||||||
INTERFACES = {"eth0": "ethernet", "wlan0": "wifi"}
|
INTERFACES = {"eth0": "ethernet", "wlan0": "wifi"}
|
||||||
PROFILE_PREFIX = "voxip-"
|
PROFILE_PREFIX = "daynivo-"
|
||||||
ROLLBACK_SECONDS = 180
|
ROLLBACK_SECONDS = 180
|
||||||
|
|
||||||
|
|
||||||
File renamed without changes.
@@ -1,7 +1,7 @@
|
|||||||
"""Filesystem locations used by every Daynivo Kiosk component.
|
"""Filesystem locations used by every Daynivo Kiosk component.
|
||||||
|
|
||||||
The defaults match the installed layout. Unprivileged components may
|
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
|
suite and for local development). The root helper always uses
|
||||||
:func:`default_paths` and never reads the environment.
|
:func:`default_paths` and never reads the environment.
|
||||||
"""
|
"""
|
||||||
@@ -15,15 +15,15 @@ from pathlib import Path
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Paths:
|
class Paths:
|
||||||
config_dir: Path = Path("/etc/voxip-kiosk")
|
config_dir: Path = Path("/etc/daynivo-kiosk")
|
||||||
state_dir: Path = Path("/var/lib/voxip-kiosk")
|
state_dir: Path = Path("/var/lib/daynivo-kiosk")
|
||||||
network_state_dir: Path = Path("/var/lib/voxip-kiosk-network")
|
network_state_dir: Path = Path("/var/lib/daynivo-kiosk-network")
|
||||||
agent_run_dir: Path = Path("/run/voxip-kiosk-agent")
|
agent_run_dir: Path = Path("/run/daynivo-kiosk-agent")
|
||||||
browser_run_dir: Path = Path("/run/voxip-kiosk-browser")
|
browser_run_dir: Path = Path("/run/daynivo-kiosk-browser")
|
||||||
health_run_dir: Path = Path("/run/voxip-kiosk-health")
|
health_run_dir: Path = Path("/run/daynivo-kiosk-health")
|
||||||
share_dir: Path = Path("/usr/share/voxip-kiosk")
|
share_dir: Path = Path("/usr/share/daynivo-kiosk")
|
||||||
log_dir: Path = Path("/var/log/voxip-kiosk")
|
log_dir: Path = Path("/var/log/daynivo-kiosk")
|
||||||
helper: Path = Path("/usr/lib/voxip-kiosk/voxip-kiosk-helper")
|
helper: Path = Path("/usr/lib/daynivo-kiosk/daynivo-kiosk-helper")
|
||||||
nm_connections_dir: Path = Path("/etc/NetworkManager/system-connections")
|
nm_connections_dir: Path = Path("/etc/NetworkManager/system-connections")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -76,6 +76,10 @@ class Paths:
|
|||||||
def version_file(self) -> Path:
|
def version_file(self) -> Path:
|
||||||
return self.share_dir / "VERSION"
|
return self.share_dir / "VERSION"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_state_file(self) -> Path:
|
||||||
|
return self.state_dir / "update.json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audit_log(self) -> Path:
|
def audit_log(self) -> Path:
|
||||||
return self.log_dir / "audit.log"
|
return self.log_dir / "audit.log"
|
||||||
@@ -86,15 +90,15 @@ def default_paths() -> Paths:
|
|||||||
|
|
||||||
|
|
||||||
_ENV = {
|
_ENV = {
|
||||||
"config_dir": "VOXIP_CONFIG_DIR",
|
"config_dir": "DAYNIVO_CONFIG_DIR",
|
||||||
"state_dir": "VOXIP_STATE_DIR",
|
"state_dir": "DAYNIVO_STATE_DIR",
|
||||||
"network_state_dir": "VOXIP_NETWORK_STATE_DIR",
|
"network_state_dir": "DAYNIVO_NETWORK_STATE_DIR",
|
||||||
"agent_run_dir": "VOXIP_AGENT_RUN_DIR",
|
"agent_run_dir": "DAYNIVO_AGENT_RUN_DIR",
|
||||||
"browser_run_dir": "VOXIP_BROWSER_RUN_DIR",
|
"browser_run_dir": "DAYNIVO_BROWSER_RUN_DIR",
|
||||||
"health_run_dir": "VOXIP_HEALTH_RUN_DIR",
|
"health_run_dir": "DAYNIVO_HEALTH_RUN_DIR",
|
||||||
"share_dir": "VOXIP_SHARE_DIR",
|
"share_dir": "DAYNIVO_SHARE_DIR",
|
||||||
"log_dir": "VOXIP_LOG_DIR",
|
"log_dir": "DAYNIVO_LOG_DIR",
|
||||||
"helper": "VOXIP_HELPER",
|
"helper": "DAYNIVO_HELPER",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -5,11 +5,11 @@ from __future__ import annotations
|
|||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
BROWSER_UNIT = "voxip-kiosk-browser.service"
|
BROWSER_UNIT = "daynivo-kiosk-browser.service"
|
||||||
X_UNIT = "voxip-kiosk-x.service"
|
X_UNIT = "daynivo-kiosk-x.service"
|
||||||
AGENT_UNIT = "voxip-kiosk-agent.service"
|
AGENT_UNIT = "daynivo-kiosk-agent.service"
|
||||||
MANAGER_UNIT = "voxip-kiosk-manager.service"
|
MANAGER_UNIT = "daynivo-kiosk-manager.service"
|
||||||
HEALTH_UNIT = "voxip-kiosk-healthcheck.service"
|
HEALTH_UNIT = "daynivo-kiosk-healthcheck.service"
|
||||||
|
|
||||||
# Units whose logs the web interface may show (label → journalctl -u args).
|
# Units whose logs the web interface may show (label → journalctl -u args).
|
||||||
LOG_SOURCES: dict[str, tuple[str, list[str]]] = {
|
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]),
|
"agent": ("Kiosk-agent", [AGENT_UNIT]),
|
||||||
"health": ("Healthcheck", [HEALTH_UNIT]),
|
"health": ("Healthcheck", [HEALTH_UNIT]),
|
||||||
"manager": ("Beheerinterface", [MANAGER_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", []),
|
"system": ("Volledig systeem", []),
|
||||||
}
|
}
|
||||||
LOG_LINES = (50, 100, 200, 500)
|
LOG_LINES = (50, 100, 200, 500)
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,244 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
* checks the latest release and records the result in ``update.json`` in the
|
||||||
|
state directory (the web interface shows it),
|
||||||
|
* downloads and verifies a newer release, unpacks it under
|
||||||
|
``/opt/daynivo-kiosk-os/<version>`` and runs that release's
|
||||||
|
``update.sh --no-pull`` (config backup + ``install.sh --upgrade``).
|
||||||
|
|
||||||
|
It never installs an older or equal version. Usage::
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from . import logutil
|
||||||
|
from .config import atomic_write_json, load_config
|
||||||
|
from .paths import Paths, default_paths, read_version
|
||||||
|
|
||||||
|
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/daynivo-kiosk")
|
||||||
|
KEEP_RELEASES = 2
|
||||||
|
MAX_DOWNLOAD = 64 * 1024 * 1024
|
||||||
|
USER_AGENT = "Daynivo-Kiosk-OS/updater"
|
||||||
|
_VERSION = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Release:
|
||||||
|
version: str
|
||||||
|
tarball_url: str
|
||||||
|
checksum_url: str
|
||||||
|
page_url: str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(text: str) -> tuple[int, int, int] | None:
|
||||||
|
match = _VERSION.match(text.strip())
|
||||||
|
return (int(match[1]), int(match[2]), int(match[3])) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def is_newer(candidate: str, current: str) -> bool:
|
||||||
|
new, old = parse_version(candidate), parse_version(current.removesuffix("-dev"))
|
||||||
|
return new is not None and (old is None or new > old)
|
||||||
|
|
||||||
|
|
||||||
|
def http_get(url: str, limit: int = MAX_DOWNLOAD, timeout: float = 30) -> bytes:
|
||||||
|
if not url.startswith("https://"):
|
||||||
|
raise UpdateError(f"Alleen HTTPS-downloads: {url}")
|
||||||
|
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/json, */*"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout, context=ssl.create_default_context()) as response:
|
||||||
|
data = response.read(limit + 1)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
if exc.code == 404 and url == RELEASE_API:
|
||||||
|
raise UpdateError("Er is nog geen release gepubliceerd") from exc
|
||||||
|
raise UpdateError(f"Downloaden mislukt ({url}): HTTP {exc.code}") from exc
|
||||||
|
except OSError as exc:
|
||||||
|
raise UpdateError(f"Downloaden mislukt ({url}): {exc}") from exc
|
||||||
|
if len(data) > limit:
|
||||||
|
raise UpdateError(f"Download te groot: {url}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def parse_release(data: dict[str, Any]) -> Release:
|
||||||
|
version = str(data.get("tag_name", "")).removeprefix("v")
|
||||||
|
if parse_version(version) is None:
|
||||||
|
raise UpdateError(f"Onbekend versienummer in release: {data.get('tag_name')!r}")
|
||||||
|
assets = {a.get("name"): a.get("browser_download_url") for a in data.get("assets") or [] if isinstance(a, dict)}
|
||||||
|
tarball = assets.get(f"{ASSET_PREFIX}{version}.tar.gz")
|
||||||
|
checksum = assets.get(f"{ASSET_PREFIX}{version}.tar.gz.sha256")
|
||||||
|
if not tarball or not checksum:
|
||||||
|
raise UpdateError(f"Release {version} mist het installatiebestand of de checksum")
|
||||||
|
return Release(version, tarball, checksum, str(data.get("html_url") or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_latest(get: Callable[[str], bytes] = http_get, api: str = RELEASE_API) -> Release:
|
||||||
|
try:
|
||||||
|
data = json.loads(get(api))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise UpdateError("Ongeldig antwoord van de releaseserver") from exc
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise UpdateError("Ongeldig antwoord van de releaseserver")
|
||||||
|
return parse_release(data)
|
||||||
|
|
||||||
|
|
||||||
|
def read_state(paths: Paths) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
data = json.loads(paths.update_state_file.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return {}
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def safe_extract(archive: Path, target: Path) -> None:
|
||||||
|
"""Unpack only regular files and directories that stay inside ``target``."""
|
||||||
|
with tarfile.open(archive, "r:gz") as tar:
|
||||||
|
members = tar.getmembers()
|
||||||
|
for member in members:
|
||||||
|
path = PurePosixPath(member.name)
|
||||||
|
if path.is_absolute() or ".." in path.parts or not (member.isfile() or member.isdir()):
|
||||||
|
raise UpdateError(f"Onveilig bestand in release: {member.name}")
|
||||||
|
tar.extractall(target, members=members) # noqa: S202 - members checked above
|
||||||
|
|
||||||
|
|
||||||
|
class Updater:
|
||||||
|
def __init__(self, paths: Paths | None = None, get: Callable[[str], bytes] = http_get,
|
||||||
|
run: Callable[..., Any] = subprocess.run, release_dir: Path = RELEASE_DIR,
|
||||||
|
cache_dir: Path = CACHE_DIR, now: Callable[[], float] = time.time):
|
||||||
|
self.paths = paths or default_paths()
|
||||||
|
self.get = get
|
||||||
|
self.run = run
|
||||||
|
self.release_dir = release_dir
|
||||||
|
self.cache_dir = cache_dir
|
||||||
|
self.now = now
|
||||||
|
|
||||||
|
def _write_state(self, **values: Any) -> dict[str, Any]:
|
||||||
|
state = {**read_state(self.paths), **values}
|
||||||
|
try:
|
||||||
|
atomic_write_json(self.paths.update_state_file, state, mode=0o644, owner=(0, 0))
|
||||||
|
except PermissionError:
|
||||||
|
atomic_write_json(self.paths.update_state_file, state, mode=0o644)
|
||||||
|
except OSError as exc:
|
||||||
|
log.warning("Kon updatestatus niet opslaan: %s", exc)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def check(self) -> tuple[dict[str, Any], Release | None]:
|
||||||
|
current = read_version(self.paths)
|
||||||
|
try:
|
||||||
|
release = fetch_latest(self.get)
|
||||||
|
except UpdateError as exc:
|
||||||
|
log.warning("Controleren op updates mislukt: %s", exc)
|
||||||
|
return self._write_state(checked_at=int(self.now()), current=current, check_error=str(exc)), None
|
||||||
|
available = is_newer(release.version, current)
|
||||||
|
state = self._write_state(checked_at=int(self.now()), current=current, latest=release.version,
|
||||||
|
available=available, release_url=release.page_url, check_error=None)
|
||||||
|
if available:
|
||||||
|
log.info("Nieuwe versie beschikbaar: %s (geïnstalleerd: %s)", release.version, current)
|
||||||
|
return state, release if available else None
|
||||||
|
|
||||||
|
def install(self, release: Release) -> dict[str, Any]:
|
||||||
|
current = read_version(self.paths)
|
||||||
|
if not is_newer(release.version, current):
|
||||||
|
raise UpdateError(f"Versie {release.version} is niet nieuwer dan {current}")
|
||||||
|
self._write_state(status="updating", target=release.version, started_at=int(self.now()), install_error=None)
|
||||||
|
log.info("Bijwerken naar %s", release.version)
|
||||||
|
try:
|
||||||
|
folder = self._download(release)
|
||||||
|
proc = self.run([str(folder / "update.sh"), "--no-pull"], capture_output=True, text=True, timeout=3600)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
tail = (proc.stderr or proc.stdout or "").strip().splitlines()[-5:]
|
||||||
|
raise UpdateError("Installatie mislukt: " + " / ".join(tail))
|
||||||
|
except (UpdateError, OSError, subprocess.SubprocessError) as exc:
|
||||||
|
log.error("Bijwerken naar %s mislukt: %s", release.version, exc)
|
||||||
|
return self._write_state(status="failed", finished_at=int(self.now()), install_error=str(exc))
|
||||||
|
self._prune(keep=release.version)
|
||||||
|
log.info("Bijgewerkt naar %s", release.version)
|
||||||
|
return self._write_state(status="done", finished_at=int(self.now()), current=release.version,
|
||||||
|
available=False, install_error=None)
|
||||||
|
|
||||||
|
def _download(self, release: Release) -> Path:
|
||||||
|
tarball = self.get(release.tarball_url)
|
||||||
|
expected = self.get(release.checksum_url, limit=4096).decode("ascii", "replace").split()
|
||||||
|
actual = hashlib.sha256(tarball).hexdigest()
|
||||||
|
if not expected or expected[0].lower() != actual:
|
||||||
|
raise UpdateError("Checksum klopt niet; de download wordt niet geïnstalleerd")
|
||||||
|
|
||||||
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.release_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.TemporaryDirectory(dir=self.cache_dir) as tmp:
|
||||||
|
archive = Path(tmp) / "release.tar.gz"
|
||||||
|
archive.write_bytes(tarball)
|
||||||
|
unpacked = Path(tmp) / "unpacked"
|
||||||
|
safe_extract(archive, unpacked)
|
||||||
|
source = unpacked / f"{ASSET_PREFIX}{release.version}"
|
||||||
|
found = (source / "VERSION").read_text(encoding="utf-8").strip() if (source / "VERSION").exists() else None
|
||||||
|
if found != release.version:
|
||||||
|
raise UpdateError(f"Release bevat versie {found!r} in plaats van {release.version}")
|
||||||
|
target = self.release_dir / release.version
|
||||||
|
shutil.rmtree(target, ignore_errors=True)
|
||||||
|
shutil.move(str(source), target)
|
||||||
|
return target
|
||||||
|
|
||||||
|
def _prune(self, keep: str) -> None:
|
||||||
|
versions = sorted((p for p in self.release_dir.iterdir() if p.is_dir() and parse_version(p.name)),
|
||||||
|
key=lambda p: parse_version(p.name), reverse=True)
|
||||||
|
for old in versions[KEEP_RELEASES:]:
|
||||||
|
if old.name != keep:
|
||||||
|
shutil.rmtree(old, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None, updater: Updater | None = None) -> int:
|
||||||
|
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")
|
||||||
|
mode.add_argument("--install", action="store_true", help="controleren en een nieuwere versie installeren")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
logutil.setup()
|
||||||
|
updater = updater or Updater()
|
||||||
|
|
||||||
|
state, release = updater.check()
|
||||||
|
if args.check or release is None:
|
||||||
|
print(json.dumps(state, ensure_ascii=False))
|
||||||
|
return 1 if state.get("check_error") else 0
|
||||||
|
if args.auto:
|
||||||
|
if not load_config(updater.paths.config_file)["auto_update"]:
|
||||||
|
log.info("Automatisch bijwerken staat uit; %s wordt niet geïnstalleerd", release.version)
|
||||||
|
return 0
|
||||||
|
if (updater.paths.health_run_dir / "maintenance").exists():
|
||||||
|
log.info("Onderhoudsmodus actief; bijwerken overgeslagen")
|
||||||
|
return 0
|
||||||
|
state = updater.install(release)
|
||||||
|
print(json.dumps(state, ensure_ascii=False))
|
||||||
|
return 0 if state.get("status") == "done" else 1
|
||||||
File renamed without changes.
@@ -48,7 +48,7 @@ def create_app(paths: Paths | None = None, *, helper: Any = None, cdp: Any = Non
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = _secret_key(paths)
|
app.secret_key = _secret_key(paths)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_NAME="voxip_session",
|
SESSION_COOKIE_NAME="daynivo_session",
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE="Strict",
|
SESSION_COOKIE_SAMESITE="Strict",
|
||||||
SESSION_COOKIE_SECURE=config["admin_https"],
|
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 = CredentialStore(paths.admin_file, paths.setup_code_file)
|
||||||
credentials.ensure_setup_code()
|
credentials.ensure_setup_code()
|
||||||
app.extensions["voxip"] = Services(
|
app.extensions["daynivo"] = Services(
|
||||||
paths=paths,
|
paths=paths,
|
||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
throttle=LoginThrottle(),
|
throttle=LoginThrottle(),
|
||||||
@@ -21,7 +21,7 @@ HASHER = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=2, hash_len=
|
|||||||
|
|
||||||
MIN_LENGTH = 10
|
MIN_LENGTH = 10
|
||||||
MAX_LENGTH = 128
|
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:
|
def password_problem(password: str, confirm: str | None = None) -> str | None:
|
||||||
File renamed without changes.
@@ -28,6 +28,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fo
|
|||||||
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
|
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
|
||||||
h2 { font-size: 1.1rem; margin: 0 0 .9rem; }
|
h2 { font-size: 1.1rem; margin: 0 0 .9rem; }
|
||||||
.mt { margin-top: 1.6rem; }
|
.mt { margin-top: 1.6rem; }
|
||||||
|
.mt-sm { margin-top: 1rem; }
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
.small { font-size: .875rem; }
|
.small { font-size: .875rem; }
|
||||||
.break { overflow-wrap: anywhere; }
|
.break { overflow-wrap: anywhere; }
|
||||||
File renamed without changes.
File renamed without changes.
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
File renamed without changes.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -12,6 +12,6 @@
|
|||||||
</label>
|
</label>
|
||||||
<button type="submit" class="btn btn-primary btn-block">Inloggen</button>
|
<button type="submit" class="btn btn-primary btn-block">Inloggen</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="muted small">Wachtwoord vergeten? Log via SSH in en voer <code>sudo voxip-kiosk-ctl reset-password</code> uit.</p>
|
<p class="muted small">Wachtwoord vergeten? Log via SSH in en voer <code>sudo daynivo-kiosk-ctl reset-password</code> uit.</p>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -94,7 +94,7 @@
|
|||||||
<h2>Herstel bij een onbereikbare kiosk</h2>
|
<h2>Herstel bij een onbereikbare kiosk</h2>
|
||||||
<ul class="plain">
|
<ul class="plain">
|
||||||
<li>Iedere wijziging wordt na {{ rollback_seconds // 60 }} minuten automatisch teruggedraaid als je hem niet bevestigt, ook als de stroom tussendoor uitvalt.</li>
|
<li>Iedere wijziging wordt na {{ rollback_seconds // 60 }} minuten automatisch teruggedraaid als je hem niet bevestigt, ook als de stroom tussendoor uitvalt.</li>
|
||||||
<li>Noodherstel: zet een leeg bestand <code>voxip-network-reset</code> op de bootpartitie van de SD-kaart en start de Pi. Alle Daynivo-netwerkprofielen worden dan verwijderd (terug naar DHCP).</li>
|
<li>Noodherstel: zet een leeg bestand <code>daynivo-network-reset</code> op de bootpartitie van de SD-kaart en start de Pi. Alle Daynivo-netwerkprofielen worden dan verwijderd (terug naar DHCP).</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File renamed without changes.
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<section class="card card-narrow">
|
<section class="card card-narrow">
|
||||||
<h1>Welkom bij Daynivo Kiosk Manager</h1>
|
<h1>Welkom bij Daynivo Kiosk Manager</h1>
|
||||||
<p>Stel eerst een uniek beheerderswachtwoord in. Je hebt daarvoor de <strong>setupcode</strong> nodig die op het
|
<p>Stel eerst een uniek beheerderswachtwoord in. Je hebt daarvoor de <strong>setupcode</strong> nodig die op het
|
||||||
scherm van de kiosk staat (of via SSH: <code>sudo voxip-kiosk-ctl setup-code</code>).</p>
|
scherm van de kiosk staat (of via SSH: <code>sudo daynivo-kiosk-ctl setup-code</code>).</p>
|
||||||
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
|
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
|
||||||
<form method="post" class="form">
|
<form method="post" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
+34
@@ -62,6 +62,40 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="card" id="updates">
|
||||||
|
<h2>Software-updates</h2>
|
||||||
|
<dl class="kv">
|
||||||
|
<dt>Geïnstalleerd</dt><dd>Daynivo Kiosk OS {{ s.version }}</dd>
|
||||||
|
<dt>Nieuwste versie</dt>
|
||||||
|
<dd>
|
||||||
|
{% if update.check_error %}<span class="muted">Onbekend — controleren mislukt: {{ update.check_error }}</span>
|
||||||
|
{% elif update.latest %}{{ update.latest }}{% if update.available %} <strong>— beschikbaar</strong>{% else %} <span class="muted">(je bent bij)</span>{% endif %}
|
||||||
|
{% else %}<span class="muted">Nog niet gecontroleerd</span>{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% if update.checked %}<dt>Gecontroleerd</dt><dd>{{ update.checked }}</dd>{% endif %}
|
||||||
|
{% if update.running %}<dt>Status</dt><dd>Bezig met bijwerken naar {{ update.target }}…</dd>
|
||||||
|
{% elif update.status == 'failed' %}<dt>Laatste update</dt><dd>Mislukt{% if update.finished %} op {{ update.finished }}{% endif %}: {{ update.install_error }}</dd>
|
||||||
|
{% elif update.status == 'done' and update.finished %}<dt>Laatste update</dt><dd>Gelukt op {{ update.finished }}</dd>{% endif %}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('ui.system_updates') }}" class="form mt-sm">
|
||||||
|
{{ csrf() }}
|
||||||
|
<label class="check"><input type="checkbox" name="auto_update" value="1" {% if conf.auto_update %}checked{% endif %}> Automatisch bijwerken <span class="muted">(elke nacht tussen 03:00 en 05:00, als er een nieuwe versie is)</span></label>
|
||||||
|
<button type="submit" class="btn btn-secondary">Opslaan</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="actions-row mt-sm">
|
||||||
|
<form method="post" action="{{ url_for('ui.system_update_check') }}" class="inline-form">{{ csrf() }}<button type="submit" class="btn btn-secondary">Nu controleren</button></form>
|
||||||
|
{% if update.available and not update.running %}
|
||||||
|
<form method="post" action="{{ url_for('ui.system_update_install') }}" class="inline-form"
|
||||||
|
data-confirm="Daynivo Kiosk OS wordt bijgewerkt naar {{ update.latest }}. Het scherm en de beheerinterface zijn een paar minuten niet beschikbaar." data-confirm-title="Nu bijwerken">
|
||||||
|
{{ csrf() }}<button type="submit" class="btn btn-primary">Bijwerken naar {{ update.latest }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="muted small">De configuratie blijft behouden en wordt voor elke update eerst bewaard in <code>/var/backups</code>.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Systeeminformatie</h2>
|
<h2>Systeeminformatie</h2>
|
||||||
<dl class="kv">
|
<dl class="kv">
|
||||||
@@ -13,6 +13,7 @@ from flask import (Blueprint, Response, abort, current_app, flash, g, jsonify, r
|
|||||||
|
|
||||||
from .. import netconfig, netinfo, services, sysinfo
|
from .. import netconfig, netinfo, services, sysinfo
|
||||||
from ..agent import read_state
|
from ..agent import read_state
|
||||||
|
from ..updater import read_state as read_update_state
|
||||||
from ..cdp import CDPError
|
from ..cdp import CDPError
|
||||||
from ..config import ConfigError, load_config, save_config
|
from ..config import ConfigError, load_config, save_config
|
||||||
from ..helper_client import HelperError
|
from ..helper_client import HelperError
|
||||||
@@ -21,14 +22,14 @@ from . import security
|
|||||||
from .auth import password_problem
|
from .auth import password_problem
|
||||||
|
|
||||||
bp = Blueprint("ui", __name__)
|
bp = Blueprint("ui", __name__)
|
||||||
audit_log = logging.getLogger("voxip-kiosk-audit")
|
audit_log = logging.getLogger("daynivo-kiosk-audit")
|
||||||
|
|
||||||
IDLE_TIMEOUT = 3600
|
IDLE_TIMEOUT = 3600
|
||||||
PUBLIC_ENDPOINTS = {"ui.login", "ui.setup", "static"}
|
PUBLIC_ENDPOINTS = {"ui.login", "ui.setup", "static"}
|
||||||
|
|
||||||
|
|
||||||
def svc():
|
def svc():
|
||||||
return current_app.extensions["voxip"]
|
return current_app.extensions["daynivo"]
|
||||||
|
|
||||||
|
|
||||||
def cfg() -> dict[str, Any]:
|
def cfg() -> dict[str, Any]:
|
||||||
@@ -404,7 +405,25 @@ def system():
|
|||||||
outputs = info.get("outputs") or []
|
outputs = info.get("outputs") or []
|
||||||
modes = outputs[0]["modes"] if outputs else []
|
modes = outputs[0]["modes"] if outputs else []
|
||||||
return render_template("system.html", active="system", conf=cfg(), modes=modes,
|
return render_template("system.html", active="system", conf=cfg(), modes=modes,
|
||||||
output=outputs[0] if outputs else None, s=collect_status())
|
output=outputs[0] if outputs else None, s=collect_status(), update=update_view())
|
||||||
|
|
||||||
|
|
||||||
|
def _when(timestamp: Any) -> str | None:
|
||||||
|
if not isinstance(timestamp, (int, float)):
|
||||||
|
return None
|
||||||
|
return time.strftime("%d-%m-%Y %H:%M", time.localtime(timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
def update_view() -> dict[str, Any]:
|
||||||
|
"""The updater's last result (update.json, written by root) in display form."""
|
||||||
|
state = read_update_state(svc().paths)
|
||||||
|
return {
|
||||||
|
**state,
|
||||||
|
"checked": _when(state.get("checked_at")),
|
||||||
|
"finished": _when(state.get("finished_at")),
|
||||||
|
# A run that started more than two hours ago and never finished is reported as failed.
|
||||||
|
"running": state.get("status") == "updating" and time.time() - state.get("started_at", 0) < 7200,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/system/display")
|
@bp.post("/system/display")
|
||||||
@@ -465,6 +484,34 @@ def system_admin():
|
|||||||
return redirect(url_for("ui.system"))
|
return redirect(url_for("ui.system"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/system/updates")
|
||||||
|
def system_updates():
|
||||||
|
auto = _form_bool("auto_update")
|
||||||
|
if _save({"auto_update": auto}) is not None:
|
||||||
|
audit("update_settings", auto_update=auto)
|
||||||
|
flash("Automatisch bijwerken staat " + ("aan." if auto else "uit."), "success")
|
||||||
|
return redirect(url_for("ui.system") + "#updates")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/system/update/check")
|
||||||
|
def system_update_check():
|
||||||
|
back = url_for("ui.system") + "#updates"
|
||||||
|
try:
|
||||||
|
state = svc().helper.call("update-check", timeout=60).get("update", {})
|
||||||
|
except HelperError as exc:
|
||||||
|
return _action_response(False, str(exc), back)
|
||||||
|
if state.get("check_error"):
|
||||||
|
return _action_response(False, f"Controleren mislukt: {state['check_error']}", back)
|
||||||
|
if state.get("available"):
|
||||||
|
return _action_response(True, f"Versie {state.get('latest')} is beschikbaar.", back)
|
||||||
|
return _action_response(True, f"Je hebt de nieuwste versie ({state.get('current')}).", back)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/system/update/install")
|
||||||
|
def system_update_install():
|
||||||
|
return _helper_action("update_install", "update-install", back=url_for("ui.system") + "#updates")
|
||||||
|
|
||||||
|
|
||||||
# -- logs ------------------------------------------------------------------------
|
# -- logs ------------------------------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/logs")
|
@bp.get("/logs")
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daynivo configuratie en SSH-sleutels importeren vanaf de bootpartitie
|
||||||
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
|
After=local-fs.target
|
||||||
|
Before=daynivo-kiosk-manager.service daynivo-kiosk-browser.service ssh.service
|
||||||
|
ConditionPathExistsGlob=|/boot/firmware/daynivo-config.json
|
||||||
|
ConditionPathExistsGlob=|/boot/firmware/daynivo-authorized_keys
|
||||||
|
ConditionPathExistsGlob=|/boot/daynivo-config.json
|
||||||
|
ConditionPathExistsGlob=|/boot/daynivo-authorized_keys
|
||||||
|
ConditionPathExistsGlob=|/boot/firmware/voxip-config.json
|
||||||
|
ConditionPathExistsGlob=|/boot/firmware/voxip-authorized_keys
|
||||||
|
ConditionPathExistsGlob=|/boot/voxip-config.json
|
||||||
|
ConditionPathExistsGlob=|/boot/voxip-authorized_keys
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-helper boot-import
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Daynivo Kiosk agent (navigatie, offlinepagina, verversen)
|
Description=Daynivo Kiosk agent (navigatie, offlinepagina, verversen)
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
After=voxip-kiosk-browser.service
|
After=daynivo-kiosk-browser.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=kiosk
|
User=kiosk
|
||||||
Group=kiosk
|
Group=kiosk
|
||||||
RuntimeDirectory=voxip-kiosk-agent
|
RuntimeDirectory=daynivo-kiosk-agent
|
||||||
RuntimeDirectoryMode=0755
|
RuntimeDirectoryMode=0755
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-agent
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-agent
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
SyslogIdentifier=voxip-kiosk-agent
|
SyslogIdentifier=daynivo-kiosk-agent
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daynivo Kiosk Chromium
|
||||||
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
|
Requires=daynivo-kiosk-x.service
|
||||||
|
After=daynivo-kiosk-x.service daynivo-boot-import.service
|
||||||
|
# Restarting or stopping X restarts/stops the browser as well.
|
||||||
|
PartOf=daynivo-kiosk-x.service
|
||||||
|
# A crash loop stops after 6 starts in 10 minutes; the healthcheck logs it
|
||||||
|
# and retries after a cool-down (see daynivo-kiosk-healthcheck).
|
||||||
|
StartLimitIntervalSec=600
|
||||||
|
StartLimitBurst=6
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=kiosk
|
||||||
|
Group=kiosk
|
||||||
|
Environment=DISPLAY=:0
|
||||||
|
Environment=XAUTHORITY=/run/daynivo-kiosk-x/Xauthority
|
||||||
|
Environment=HOME=/home/kiosk
|
||||||
|
RuntimeDirectory=daynivo-kiosk-browser
|
||||||
|
RuntimeDirectoryMode=0755
|
||||||
|
StateDirectory=daynivo-kiosk-browser
|
||||||
|
StateDirectoryMode=0700
|
||||||
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-browser
|
||||||
|
# daynivo-kiosk-browser exits 1 after a crash when browser_restart is on, 0 when it is off.
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStopSec=15
|
||||||
|
KillMode=control-group
|
||||||
|
OOMScoreAdjust=-100
|
||||||
|
LogRateLimitIntervalSec=30s
|
||||||
|
LogRateLimitBurst=300
|
||||||
|
SyslogIdentifier=daynivo-kiosk-browser
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daynivo Kiosk healthcheck
|
||||||
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RuntimeDirectory=daynivo-kiosk-health
|
||||||
|
RuntimeDirectoryPreserve=yes
|
||||||
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-healthcheck
|
||||||
|
SyslogIdentifier=daynivo-kiosk-healthcheck
|
||||||
|
TimeoutStartSec=60
|
||||||
File renamed without changes.
@@ -1,27 +1,27 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Daynivo Kiosk Manager (webbeheer)
|
Description=Daynivo Kiosk Manager (webbeheer)
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
After=network.target voxip-boot-import.service
|
After=network.target daynivo-boot-import.service
|
||||||
StartLimitIntervalSec=300
|
StartLimitIntervalSec=300
|
||||||
StartLimitBurst=10
|
StartLimitBurst=10
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=voxip
|
User=daynivo
|
||||||
Group=voxip
|
Group=daynivo
|
||||||
# systemd-journal: read logs; video: vcgencmd (voltage/throttling)
|
# systemd-journal: read logs; video: vcgencmd (voltage/throttling)
|
||||||
SupplementaryGroups=systemd-journal video
|
SupplementaryGroups=systemd-journal video
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-manager
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-manager
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
UMask=0027
|
UMask=0027
|
||||||
LogsDirectory=voxip-kiosk
|
LogsDirectory=daynivo-kiosk
|
||||||
LogsDirectoryMode=0750
|
LogsDirectoryMode=0750
|
||||||
SyslogIdentifier=voxip-kiosk-manager
|
SyslogIdentifier=daynivo-kiosk-manager
|
||||||
# No NoNewPrivileges/ProtectSystem/ProtectHome: the helper started through
|
# No NoNewPrivileges/ProtectSystem/ProtectHome: the helper started through
|
||||||
# "sudo -n voxip-kiosk-helper" inherits this unit's mount namespace and must
|
# "sudo -n daynivo-kiosk-helper" inherits this unit's mount namespace and must
|
||||||
# be able to write /etc (hostname, NetworkManager profiles). The web process
|
# be able to write /etc (hostname, NetworkManager profiles). The web process
|
||||||
# itself runs unprivileged as user voxip and can only write its own dirs.
|
# itself runs unprivileged as user daynivo and can only write its own dirs.
|
||||||
# Also no ProtectKernelTunables/ProtectKernelModules/LockPersonality,
|
# Also no ProtectKernelTunables/ProtectKernelModules/LockPersonality,
|
||||||
# RestrictSUIDSGID, SystemCall*, ...: for a non-root unit those imply
|
# RestrictSUIDSGID, SystemCall*, ...: for a non-root unit those imply
|
||||||
# NoNewPrivileges, and sudo then refuses to run (tests/test_units.py).
|
# NoNewPrivileges, and sudo then refuses to run (tests/test_units.py).
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daynivo Kiosk OS: controleren op updates (en installeren als dat aan staat)
|
||||||
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-updater --auto
|
||||||
|
TimeoutStartSec=1h
|
||||||
|
SyslogIdentifier=daynivo-kiosk-updater
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daynivo Kiosk OS: dagelijks controleren op updates
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Night-time, spread over two hours so a fleet doesn't hit the release server at once.
|
||||||
|
OnCalendar=*-*-* 03:00
|
||||||
|
RandomizedDelaySec=2h
|
||||||
|
Persistent=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Daynivo Kiosk X-server (vt7)
|
Description=Daynivo Kiosk X-server (vt7)
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
After=systemd-user-sessions.service systemd-logind.service
|
After=systemd-user-sessions.service systemd-logind.service
|
||||||
Conflicts=getty@tty7.service
|
Conflicts=getty@tty7.service
|
||||||
StartLimitIntervalSec=600
|
StartLimitIntervalSec=600
|
||||||
@@ -20,10 +20,10 @@ StandardOutput=journal
|
|||||||
StandardError=journal
|
StandardError=journal
|
||||||
UtmpIdentifier=tty7
|
UtmpIdentifier=tty7
|
||||||
UtmpMode=user
|
UtmpMode=user
|
||||||
RuntimeDirectory=voxip-kiosk-x
|
RuntimeDirectory=daynivo-kiosk-x
|
||||||
RuntimeDirectoryMode=0700
|
RuntimeDirectoryMode=0700
|
||||||
ExecStartPre=+/usr/bin/chvt 7
|
ExecStartPre=+/usr/bin/chvt 7
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-x
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-x
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Daynivo netwerkherstel bij opstarten (rollback en noodreset)
|
Description=Daynivo netwerkherstel bij opstarten (rollback en noodreset)
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
Documentation=file:///usr/share/doc/daynivo-kiosk/README.md
|
||||||
DefaultDependencies=no
|
DefaultDependencies=no
|
||||||
After=local-fs.target
|
After=local-fs.target
|
||||||
Before=NetworkManager.service network-pre.target shutdown.target
|
Before=NetworkManager.service network-pre.target shutdown.target
|
||||||
@@ -10,7 +10,7 @@ Conflicts=shutdown.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-helper network-boot-guard
|
ExecStart=/usr/lib/daynivo-kiosk/daynivo-kiosk-helper network-boot-guard
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Daynivo configuratie en SSH-sleutels importeren vanaf de bootpartitie
|
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
|
||||||
After=local-fs.target
|
|
||||||
Before=voxip-kiosk-manager.service voxip-kiosk-browser.service ssh.service
|
|
||||||
ConditionPathExistsGlob=|/boot/firmware/voxip-config.json
|
|
||||||
ConditionPathExistsGlob=|/boot/firmware/voxip-authorized_keys
|
|
||||||
ConditionPathExistsGlob=|/boot/voxip-config.json
|
|
||||||
ConditionPathExistsGlob=|/boot/voxip-authorized_keys
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
RemainAfterExit=yes
|
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-helper boot-import
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Daynivo Kiosk Chromium
|
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
|
||||||
Requires=voxip-kiosk-x.service
|
|
||||||
After=voxip-kiosk-x.service voxip-boot-import.service
|
|
||||||
# Restarting or stopping X restarts/stops the browser as well.
|
|
||||||
PartOf=voxip-kiosk-x.service
|
|
||||||
# A crash loop stops after 6 starts in 10 minutes; the healthcheck logs it
|
|
||||||
# and retries after a cool-down (see voxip-kiosk-healthcheck).
|
|
||||||
StartLimitIntervalSec=600
|
|
||||||
StartLimitBurst=6
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=kiosk
|
|
||||||
Group=kiosk
|
|
||||||
Environment=DISPLAY=:0
|
|
||||||
Environment=XAUTHORITY=/run/voxip-kiosk-x/Xauthority
|
|
||||||
Environment=HOME=/home/kiosk
|
|
||||||
RuntimeDirectory=voxip-kiosk-browser
|
|
||||||
RuntimeDirectoryMode=0755
|
|
||||||
StateDirectory=voxip-kiosk-browser
|
|
||||||
StateDirectoryMode=0700
|
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-browser
|
|
||||||
# voxip-kiosk-browser exits 1 after a crash when browser_restart is on, 0 when it is off.
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
TimeoutStopSec=15
|
|
||||||
KillMode=control-group
|
|
||||||
OOMScoreAdjust=-100
|
|
||||||
LogRateLimitIntervalSec=30s
|
|
||||||
LogRateLimitBurst=300
|
|
||||||
SyslogIdentifier=voxip-kiosk-browser
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Daynivo Kiosk healthcheck
|
|
||||||
Documentation=file:///usr/share/doc/voxip-kiosk/README.md
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
RuntimeDirectory=voxip-kiosk-health
|
|
||||||
RuntimeDirectoryPreserve=yes
|
|
||||||
ExecStart=/usr/lib/voxip-kiosk/voxip-kiosk-healthcheck
|
|
||||||
SyslogIdentifier=voxip-kiosk-healthcheck
|
|
||||||
TimeoutStartSec=60
|
|
||||||
+7
-7
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from voxip_kiosk.paths import Paths
|
from daynivo_kiosk.paths import Paths
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class FakeHelper:
|
|||||||
self.responses = {}
|
self.responses = {}
|
||||||
|
|
||||||
def call(self, verb, *args, payload=None, timeout=60):
|
def call(self, verb, *args, payload=None, timeout=60):
|
||||||
from voxip_kiosk.helper_client import HelperError
|
from daynivo_kiosk.helper_client import HelperError
|
||||||
|
|
||||||
self.calls.append((verb, args, payload))
|
self.calls.append((verb, args, payload))
|
||||||
if self.fail:
|
if self.fail:
|
||||||
@@ -53,7 +53,7 @@ class FakeCDP:
|
|||||||
self.fail = None
|
self.fail = None
|
||||||
|
|
||||||
def _do(self, name, *args):
|
def _do(self, name, *args):
|
||||||
from voxip_kiosk.cdp import CDPError
|
from daynivo_kiosk.cdp import CDPError
|
||||||
|
|
||||||
self.calls.append((name, args))
|
self.calls.append((name, args))
|
||||||
if self.fail:
|
if self.fail:
|
||||||
@@ -80,7 +80,7 @@ def fake_cdp():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def quiet_system(monkeypatch):
|
def quiet_system(monkeypatch):
|
||||||
"""Keep the web tests independent of the machine they run on."""
|
"""Keep the web tests independent of the machine they run on."""
|
||||||
from voxip_kiosk import netinfo, services, sysinfo
|
from daynivo_kiosk import netinfo, services, sysinfo
|
||||||
|
|
||||||
monkeypatch.setattr(services, "summary", lambda: {
|
monkeypatch.setattr(services, "summary", lambda: {
|
||||||
"browser": {"unit": services.BROWSER_UNIT, "status": "ok", "label": "Actief", "restarts": "2", "since": ""},
|
"browser": {"unit": services.BROWSER_UNIT, "status": "ok", "label": "Actief", "restarts": "2", "since": ""},
|
||||||
@@ -96,7 +96,7 @@ def quiet_system(monkeypatch):
|
|||||||
"available": True, "wifi_country": "NL",
|
"available": True, "wifi_country": "NL",
|
||||||
"devices": {
|
"devices": {
|
||||||
iface: {"interface": iface, "kind": kind, "present": True, "state": "connected", "state_label": "Verbonden",
|
iface: {"interface": iface, "kind": kind, "present": True, "state": "connected", "state_label": "Verbonden",
|
||||||
"connected": True, "connection": f"voxip-{iface}", "addresses": ["192.168.10.20/24"], "gateway": "192.168.10.1",
|
"connected": True, "connection": f"daynivo-{iface}", "addresses": ["192.168.10.20/24"], "gateway": "192.168.10.1",
|
||||||
"dns": ["192.168.10.1"], "hwaddr": "B8:27:EB:00:00:01",
|
"dns": ["192.168.10.1"], "hwaddr": "B8:27:EB:00:00:01",
|
||||||
"profile": {"exists": True, "method": "dhcp", "address": "", "gateway": "", "dns": "", "ssid": "Kantoor", "hidden": False}}
|
"profile": {"exists": True, "method": "dhcp", "address": "", "gateway": "", "dns": "", "ssid": "Kantoor", "hidden": False}}
|
||||||
for iface, kind in (("eth0", "ethernet"), ("wlan0", "wifi"))
|
for iface, kind in (("eth0", "ethernet"), ("wlan0", "wifi"))
|
||||||
@@ -106,7 +106,7 @@ def quiet_system(monkeypatch):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app(paths, fake_helper, fake_cdp, quiet_system):
|
def app(paths, fake_helper, fake_cdp, quiet_system):
|
||||||
from voxip_kiosk.web import create_app
|
from daynivo_kiosk.web import create_app
|
||||||
|
|
||||||
application = create_app(paths, helper=fake_helper, cdp=fake_cdp)
|
application = create_app(paths, helper=fake_helper, cdp=fake_cdp)
|
||||||
application.config["TESTING"] = True
|
application.config["TESTING"] = True
|
||||||
@@ -130,7 +130,7 @@ PASSWORD = "Correct-Paard-Batterij-42"
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def logged_in(app, client, paths):
|
def logged_in(app, client, paths):
|
||||||
"""A client with a configured admin password and an active session."""
|
"""A client with a configured admin password and an active session."""
|
||||||
app.extensions["voxip"].credentials.set_password(PASSWORD)
|
app.extensions["daynivo"].credentials.set_password(PASSWORD)
|
||||||
page = client.get("/login")
|
page = client.get("/login")
|
||||||
token = csrf_from(page)
|
token = csrf_from(page)
|
||||||
resp = client.post("/login", data={"password": PASSWORD, "csrf_token": token})
|
resp = client.post("/login", data={"password": PASSWORD, "csrf_token": token})
|
||||||
|
|||||||
+3
-3
@@ -2,9 +2,9 @@ import json
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from voxip_kiosk.agent import Agent, HUNG_RELOAD_AFTER, HUNG_RESTART_AFTER
|
from daynivo_kiosk.agent import Agent, HUNG_RELOAD_AFTER, HUNG_RESTART_AFTER
|
||||||
from voxip_kiosk.cdp import CDPError
|
from daynivo_kiosk.cdp import CDPError
|
||||||
from voxip_kiosk.config import save_config
|
from daynivo_kiosk.config import save_config
|
||||||
|
|
||||||
|
|
||||||
class Clock:
|
class Clock:
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs, urlsplit
|
from urllib.parse import parse_qs, urlsplit
|
||||||
|
|
||||||
from voxip_kiosk import browser as b
|
from daynivo_kiosk import browser as b
|
||||||
from voxip_kiosk.config import DEFAULTS
|
from daynivo_kiosk.config import DEFAULTS
|
||||||
|
|
||||||
XRANDR = """Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 7680 x 7680
|
XRANDR = """Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 7680 x 7680
|
||||||
HDMI-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 531mm x 299mm
|
HDMI-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 531mm x 299mm
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import stat
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from voxip_kiosk import config as c
|
from daynivo_kiosk import config as c
|
||||||
|
|
||||||
|
|
||||||
def test_default_file_matches_defaults(paths):
|
def test_default_file_matches_defaults(paths):
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from voxip_kiosk import services
|
from daynivo_kiosk import services
|
||||||
from voxip_kiosk.cdp import CDPError
|
from daynivo_kiosk.cdp import CDPError
|
||||||
from voxip_kiosk.config import save_config
|
from daynivo_kiosk.config import save_config
|
||||||
from voxip_kiosk.healthcheck import CDP_FAILURES_BEFORE_RESTART, DISPLAY_RETRY_AFTER, HealthCheck
|
from daynivo_kiosk.healthcheck import CDP_FAILURES_BEFORE_RESTART, DISPLAY_RETRY_AFTER, HealthCheck
|
||||||
|
|
||||||
|
|
||||||
class World:
|
class World:
|
||||||
|
|||||||
+62
-26
@@ -3,8 +3,8 @@ import subprocess
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from voxip_kiosk import helper as h
|
from daynivo_kiosk import helper as h
|
||||||
from voxip_kiosk.config import load_config
|
from daynivo_kiosk.config import load_config
|
||||||
|
|
||||||
|
|
||||||
class Recorder:
|
class Recorder:
|
||||||
@@ -35,7 +35,7 @@ def helper(paths, tmp_path, boot):
|
|||||||
etc.mkdir()
|
etc.mkdir()
|
||||||
(etc / "hosts").write_text("127.0.0.1\tlocalhost\n127.0.1.1\tkiosk\n")
|
(etc / "hosts").write_text("127.0.0.1\tlocalhost\n127.0.1.1\tkiosk\n")
|
||||||
run = Recorder({"nmcli -t -f": "aaaa-1111\n"})
|
run = Recorder({"nmcli -t -f": "aaaa-1111\n"})
|
||||||
return h.Helper(paths=paths, run=run, etc=etc, boot_dirs=(boot,), helper_path="/usr/lib/voxip-kiosk/voxip-kiosk-helper")
|
return h.Helper(paths=paths, run=run, etc=etc, boot_dirs=(boot,), helper_path="/usr/lib/daynivo-kiosk/daynivo-kiosk-helper")
|
||||||
|
|
||||||
|
|
||||||
def run_main(helper, argv, env=None, capsys=None):
|
def run_main(helper, argv, env=None, capsys=None):
|
||||||
@@ -45,17 +45,17 @@ def run_main(helper, argv, env=None, capsys=None):
|
|||||||
|
|
||||||
|
|
||||||
def test_internal_verbs_refused_via_sudo(helper, capsys):
|
def test_internal_verbs_refused_via_sudo(helper, capsys):
|
||||||
code, out = run_main(helper, ["network-restore"], {"SUDO_USER": "voxip"}, capsys)
|
code, out = run_main(helper, ["network-restore"], {"SUDO_USER": "daynivo"}, capsys)
|
||||||
assert code == 1 and out["ok"] is False
|
assert code == 1 and out["ok"] is False
|
||||||
code, out = run_main(helper, ["boot-import"], {"SUDO_USER": "voxip"}, capsys)
|
code, out = run_main(helper, ["boot-import"], {"SUDO_USER": "daynivo"}, capsys)
|
||||||
assert code == 1
|
assert code == 1
|
||||||
assert helper.run.commands == []
|
assert helper.run.commands == []
|
||||||
|
|
||||||
|
|
||||||
def test_public_verb_via_sudo(helper, capsys):
|
def test_public_verb_via_sudo(helper, capsys):
|
||||||
code, out = run_main(helper, ["restart-browser"], {"SUDO_USER": "voxip"}, capsys)
|
code, out = run_main(helper, ["restart-browser"], {"SUDO_USER": "daynivo"}, capsys)
|
||||||
assert code == 0 and out["ok"] is True
|
assert code == 0 and out["ok"] is True
|
||||||
assert helper.run.ran("systemctl", "restart", "--no-block", "voxip-kiosk-browser.service")
|
assert helper.run.ran("systemctl", "restart", "--no-block", "daynivo-kiosk-browser.service")
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_verb_and_bad_arity(helper, capsys):
|
def test_unknown_verb_and_bad_arity(helper, capsys):
|
||||||
@@ -99,7 +99,7 @@ ETH_STATIC = {"interface": "eth0", "method": "static", "address": "192.168.5.10/
|
|||||||
def test_network_apply_snapshot_pending_and_timers(helper, paths):
|
def test_network_apply_snapshot_pending_and_timers(helper, paths):
|
||||||
(paths.nm_connections_dir / "Wired connection 1.nmconnection").write_text("[connection]\nid=Wired connection 1\n")
|
(paths.nm_connections_dir / "Wired connection 1.nmconnection").write_text("[connection]\nid=Wired connection 1\n")
|
||||||
result = helper.network_apply(ETH_STATIC)
|
result = helper.network_apply(ETH_STATIC)
|
||||||
keyfile = paths.nm_connections_dir / "voxip-eth0.nmconnection"
|
keyfile = paths.nm_connections_dir / "daynivo-eth0.nmconnection"
|
||||||
assert keyfile.exists() and oct(keyfile.stat().st_mode & 0o777) == "0o600"
|
assert keyfile.exists() and oct(keyfile.stat().st_mode & 0o777) == "0o600"
|
||||||
assert "address1=192.168.5.10/24,192.168.5.1" in keyfile.read_text()
|
assert "address1=192.168.5.10/24,192.168.5.1" in keyfile.read_text()
|
||||||
assert (paths.network_backup_dir / "Wired connection 1.nmconnection").exists()
|
assert (paths.network_backup_dir / "Wired connection 1.nmconnection").exists()
|
||||||
@@ -108,8 +108,8 @@ def test_network_apply_snapshot_pending_and_timers(helper, paths):
|
|||||||
assert pending["deadline"] - pending["created"] == 180
|
assert pending["deadline"] - pending["created"] == 180
|
||||||
assert result["pending"]["interface"] == "eth0"
|
assert result["pending"]["interface"] == "eth0"
|
||||||
runs = [c for c in helper.run.commands if c[0] == "systemd-run"]
|
runs = [c for c in helper.run.commands if c[0] == "systemd-run"]
|
||||||
assert runs[0][1:3] == ["--unit=voxip-network-rollback", "--on-active=180"] and runs[0][-1] == "network-restore"
|
assert runs[0][1:3] == ["--unit=daynivo-network-rollback", "--on-active=180"] and runs[0][-1] == "network-restore"
|
||||||
assert runs[1][1:3] == ["--unit=voxip-network-activate", "--on-active=2"] and runs[1][-2:] == ["network-activate", "eth0"]
|
assert runs[1][1:3] == ["--unit=daynivo-network-activate", "--on-active=2"] and runs[1][-2:] == ["network-activate", "eth0"]
|
||||||
|
|
||||||
|
|
||||||
def test_second_apply_refused_while_pending(helper):
|
def test_second_apply_refused_while_pending(helper):
|
||||||
@@ -123,8 +123,8 @@ def test_confirm_clears_pending(helper, paths):
|
|||||||
helper.network_confirm()
|
helper.network_confirm()
|
||||||
assert not paths.network_pending_file.exists()
|
assert not paths.network_pending_file.exists()
|
||||||
assert not paths.network_backup_dir.exists()
|
assert not paths.network_backup_dir.exists()
|
||||||
assert (paths.nm_connections_dir / "voxip-eth0.nmconnection").exists()
|
assert (paths.nm_connections_dir / "daynivo-eth0.nmconnection").exists()
|
||||||
assert helper.run.ran("systemctl", "stop", "voxip-network-rollback.timer")
|
assert helper.run.ran("systemctl", "stop", "daynivo-network-rollback.timer")
|
||||||
|
|
||||||
|
|
||||||
def test_restore_puts_old_profiles_back(helper, paths):
|
def test_restore_puts_old_profiles_back(helper, paths):
|
||||||
@@ -134,7 +134,7 @@ def test_restore_puts_old_profiles_back(helper, paths):
|
|||||||
old.unlink()
|
old.unlink()
|
||||||
helper.network_restore()
|
helper.network_restore()
|
||||||
assert old.exists()
|
assert old.exists()
|
||||||
assert not (paths.nm_connections_dir / "voxip-eth0.nmconnection").exists()
|
assert not (paths.nm_connections_dir / "daynivo-eth0.nmconnection").exists()
|
||||||
assert not paths.network_pending_file.exists()
|
assert not paths.network_pending_file.exists()
|
||||||
assert helper.run.ran("nmcli", "--wait", "30", "connection", "up", "uuid", "aaaa-1111")
|
assert helper.run.ran("nmcli", "--wait", "30", "connection", "up", "uuid", "aaaa-1111")
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ def test_wifi_keeps_existing_password_for_same_ssid(helper, paths):
|
|||||||
helper.network_confirm()
|
helper.network_confirm()
|
||||||
helper.network_apply({"interface": "wlan0", "method": "static", "address": "10.0.0.9/24", "gateway": "10.0.0.1", "dns": "10.0.0.1",
|
helper.network_apply({"interface": "wlan0", "method": "static", "address": "10.0.0.9/24", "gateway": "10.0.0.1", "dns": "10.0.0.1",
|
||||||
"wifi": {"ssid": "Kantoor", "psk": ""}})
|
"wifi": {"ssid": "Kantoor", "psk": ""}})
|
||||||
assert "psk=geheim-wachtwoord" in (paths.nm_connections_dir / "voxip-wlan0.nmconnection").read_text()
|
assert "psk=geheim-wachtwoord" in (paths.nm_connections_dir / "daynivo-wlan0.nmconnection").read_text()
|
||||||
helper.network_confirm()
|
helper.network_confirm()
|
||||||
with pytest.raises(h.HelperError, match="wachtwoord"):
|
with pytest.raises(h.HelperError, match="wachtwoord"):
|
||||||
helper.network_apply({"interface": "wlan0", "method": "dhcp", "wifi": {"ssid": "Ander", "psk": ""}})
|
helper.network_apply({"interface": "wlan0", "method": "dhcp", "wifi": {"ssid": "Ander", "psk": ""}})
|
||||||
@@ -165,37 +165,73 @@ def test_boot_guard_rolls_back_and_honours_reset(helper, paths, boot):
|
|||||||
helper.run.commands.clear()
|
helper.run.commands.clear()
|
||||||
result = helper.network_boot_guard()
|
result = helper.network_boot_guard()
|
||||||
assert result == {"rolled_back": True}
|
assert result == {"rolled_back": True}
|
||||||
assert not (paths.nm_connections_dir / "voxip-eth0.nmconnection").exists()
|
assert not (paths.nm_connections_dir / "daynivo-eth0.nmconnection").exists()
|
||||||
assert not any(c[0] == "nmcli" for c in helper.run.commands), "NetworkManager is not running yet at boot"
|
assert not any(c[0] == "nmcli" for c in helper.run.commands), "NetworkManager is not running yet at boot"
|
||||||
|
|
||||||
(paths.nm_connections_dir / "voxip-wlan0.nmconnection").write_text("x")
|
(paths.nm_connections_dir / "daynivo-wlan0.nmconnection").write_text("x")
|
||||||
(boot / "voxip-network-reset").write_text("")
|
(boot / "daynivo-network-reset").write_text("")
|
||||||
assert helper.network_boot_guard() == {"reset": True}
|
assert helper.network_boot_guard() == {"reset": True}
|
||||||
assert not (paths.nm_connections_dir / "voxip-wlan0.nmconnection").exists()
|
assert not (paths.nm_connections_dir / "daynivo-wlan0.nmconnection").exists()
|
||||||
assert not (boot / "voxip-network-reset").exists()
|
assert not (boot / "daynivo-network-reset").exists()
|
||||||
assert old.exists()
|
assert old.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_boot_guard_still_honours_the_old_reset_name(helper, paths, boot):
|
||||||
|
(paths.nm_connections_dir / "daynivo-eth0.nmconnection").write_text("x")
|
||||||
|
(boot / "voxip-network-reset").write_text("")
|
||||||
|
assert helper.network_boot_guard() == {"reset": True}
|
||||||
|
assert not (paths.nm_connections_dir / "daynivo-eth0.nmconnection").exists()
|
||||||
|
assert not (boot / "voxip-network-reset").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_boot_import_applies_config(helper, paths, boot):
|
def test_boot_import_applies_config(helper, paths, boot):
|
||||||
(boot / "voxip-config.json").write_text(json.dumps({"device_name": "Hal", "url": "https://klant.example.nl/display/hal?token=x",
|
(boot / "daynivo-config.json").write_text(json.dumps({"device_name": "Hal", "url": "https://klant.example.nl/display/hal?token=x",
|
||||||
"hostname": "hal", "rotation": 90}))
|
"hostname": "hal", "rotation": 90}))
|
||||||
assert helper.boot_import() == {"imported": ["voxip-config.json"]}
|
assert helper.boot_import() == {"imported": ["daynivo-config.json"]}
|
||||||
conf = load_config(paths.config_file)
|
conf = load_config(paths.config_file)
|
||||||
assert conf["device_name"] == "Hal" and conf["rotation"] == 90 and conf["url"].startswith("https://klant")
|
assert conf["device_name"] == "Hal" and conf["rotation"] == 90 and conf["url"].startswith("https://klant")
|
||||||
assert (helper.etc / "hostname").read_text() == "hal\n"
|
assert (helper.etc / "hostname").read_text() == "hal\n"
|
||||||
|
assert (boot / "daynivo-config.json.applied").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_boot_import_still_accepts_the_old_names(helper, paths, boot):
|
||||||
|
(boot / "voxip-config.json").write_text(json.dumps({"device_name": "Oud", "rotation": 180}))
|
||||||
|
(boot / "daynivo-config.json").write_text(json.dumps({"device_name": "Nieuw"}))
|
||||||
|
assert helper.boot_import() == {"imported": ["voxip-config.json", "daynivo-config.json"]}
|
||||||
|
conf = load_config(paths.config_file)
|
||||||
|
assert conf["device_name"] == "Nieuw" and conf["rotation"] == 180 # the Daynivo file is applied last
|
||||||
assert (boot / "voxip-config.json.applied").exists()
|
assert (boot / "voxip-config.json.applied").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_boot_import_rejects_invalid_config(helper, paths, boot):
|
def test_boot_import_rejects_invalid_config(helper, paths, boot):
|
||||||
before = paths.config_file.read_text()
|
before = paths.config_file.read_text()
|
||||||
(boot / "voxip-config.json").write_text(json.dumps({"url": "http://onveilig.example"}))
|
(boot / "daynivo-config.json").write_text(json.dumps({"url": "http://onveilig.example"}))
|
||||||
helper.boot_import()
|
helper.boot_import()
|
||||||
assert paths.config_file.read_text() == before
|
assert paths.config_file.read_text() == before
|
||||||
assert (boot / "voxip-config.json.rejected").exists()
|
assert (boot / "daynivo-config.json.rejected").exists()
|
||||||
assert "HTTPS" in (boot / "voxip-config.error.txt").read_text()
|
assert "HTTPS" in (boot / "daynivo-config.error.txt").read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_boot_import_rejects_non_key_lines(helper, boot):
|
def test_boot_import_rejects_non_key_lines(helper, boot):
|
||||||
(boot / "voxip-authorized_keys").write_text("dit is geen sleutel\n")
|
(boot / "daynivo-authorized_keys").write_text("dit is geen sleutel\n")
|
||||||
|
(boot / "voxip-authorized_keys").write_text("ook geen sleutel\n")
|
||||||
helper.boot_import()
|
helper.boot_import()
|
||||||
|
assert (boot / "daynivo-authorized_keys.rejected").exists()
|
||||||
assert (boot / "voxip-authorized_keys.rejected").exists()
|
assert (boot / "voxip-authorized_keys.rejected").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_verbs_are_public_and_install_runs_in_its_own_unit(helper):
|
||||||
|
assert {"update-check", "update-install"} <= h.PUBLIC_VERBS
|
||||||
|
result = helper.dispatch("update-install", [])
|
||||||
|
assert "gestart" in result["message"]
|
||||||
|
command = helper.run.ran("systemd-run")[-1]
|
||||||
|
assert f"--unit={h.UPDATE_UNIT}" in command and command[-2:] == [h.UPDATER_PATH, "--install"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_install_reports_a_running_update(helper):
|
||||||
|
def busy(args, timeout=30, check=True, input=None):
|
||||||
|
return subprocess.CompletedProcess(args, 1, stdout="", stderr="Unit daynivo-kiosk-update-now.service already exists.")
|
||||||
|
|
||||||
|
helper.run = busy
|
||||||
|
with pytest.raises(h.HelperError, match="al een update"):
|
||||||
|
helper.dispatch("update-install", [])
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from voxip_kiosk import netconfig as n
|
from daynivo_kiosk import netconfig as n
|
||||||
from voxip_kiosk.validation import ValidationError
|
from daynivo_kiosk.validation import ValidationError
|
||||||
|
|
||||||
|
|
||||||
def test_validate_static_ethernet():
|
def test_validate_static_ethernet():
|
||||||
@@ -27,7 +27,7 @@ def test_keyfile_static_ethernet():
|
|||||||
req = n.validate_request({"interface": "eth0", "method": "static", "address": "10.1.2.3/16", "gateway": "10.1.0.1", "dns": ["10.1.0.1"]})
|
req = n.validate_request({"interface": "eth0", "method": "static", "address": "10.1.2.3/16", "gateway": "10.1.0.1", "dns": ["10.1.0.1"]})
|
||||||
text = n.build_keyfile(req, uuid="11111111-2222-3333-4444-555555555555")
|
text = n.build_keyfile(req, uuid="11111111-2222-3333-4444-555555555555")
|
||||||
kf = n.read_keyfile(text)
|
kf = n.read_keyfile(text)
|
||||||
assert kf.get("connection", "id") == "voxip-eth0"
|
assert kf.get("connection", "id") == "daynivo-eth0"
|
||||||
assert kf.get("connection", "type") == "ethernet"
|
assert kf.get("connection", "type") == "ethernet"
|
||||||
assert kf.get("connection", "autoconnect-priority") == "100"
|
assert kf.get("connection", "autoconnect-priority") == "100"
|
||||||
assert kf.get("ipv4", "method") == "manual"
|
assert kf.get("ipv4", "method") == "manual"
|
||||||
|
|||||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user