Rolle kiosk: Vollbild-Browser mit Whitelist am Anmeldebildschirm, optional Autostart (roadmap/kiosk.md)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a778bf67a6
commit
9070313731
7 files changed
+880
No files matched your search
@@ -0,0 +1,2 @@
|
||||
tuxflotte_state: absent
|
||||
tuxflotte_optionen: {}
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/sh
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
#
|
||||
# Gibt die Anmeldung am Konto "kiosk" frei oder nimmt sie weg (Muster:
|
||||
# personalisierung-anmeldung bis ansible-content f975938). Frei nur, wenn
|
||||
# beides gilt:
|
||||
# - die Rolle hat eine gültige Konfiguration abgelegt (KIOSK), und
|
||||
# - das laufende LightDM nutzt schon den Kiosk-Wrapper. LightDM liest
|
||||
# seine Konfiguration nur beim Start; vorher bekäme das passwortlose
|
||||
# Konto den vollen Desktop.
|
||||
# Frei: passwortlose PAM-Freigabe, Shell bash, im Greeter sichtbar, mit
|
||||
# AUTOSTART zusätzlich automatische Anmeldung (wirkt ab dem nächsten Start
|
||||
# von LightDM, zusammen mit dem Wrapper).
|
||||
# Sonst: keine Freigabe, Shell nologin, in AccountsService
|
||||
# SystemAccount=true (sonst bleibt das Konto im Greeter stehen), keine
|
||||
# automatische Anmeldung.
|
||||
#
|
||||
# Aufruf: "start" aus ExecStartPre von lightdm.service (LightDM startet
|
||||
# gleich mit der aktuellen Konfiguration), ohne Argument von der Rolle.
|
||||
# Ausgabe: "frei" oder "gesperrt", bei einer Änderung mit " geaendert".
|
||||
set -eu
|
||||
|
||||
KONF=/etc/lightdm/lightdm.conf.d/86-tuxflotte-kiosk.conf
|
||||
AUTOLOGIN=/etc/lightdm/lightdm.conf.d/87-tuxflotte-kiosk-autostart.conf
|
||||
KIOSK=/etc/tuxflotte/kiosk.json
|
||||
AUTOSTART=/etc/tuxflotte/kiosk-autostart
|
||||
PAM=/etc/pam.d/lightdm
|
||||
ACCOUNTS=/var/lib/AccountsService/users/kiosk
|
||||
ZEILE='auth sufficient pam_succeed_if.so user = kiosk'
|
||||
MUSTER='^auth[[:space:]]+sufficient[[:space:]]+pam_succeed_if\.so[[:space:]]+user[[:space:]]+=[[:space:]]+kiosk[[:space:]]*$'
|
||||
|
||||
wrapper_aktiv() {
|
||||
[ -f "$KONF" ] || return 1
|
||||
[ "${1:-}" = start ] && return 0
|
||||
pid=$(systemctl show -p MainPID --value lightdm.service 2>/dev/null || true)
|
||||
[ "${pid:-0}" -gt 0 ] 2>/dev/null || return 1
|
||||
laufzeit=$(ps -o etimes= -p "$pid" | tr -d ' ')
|
||||
[ -n "$laufzeit" ] || return 1
|
||||
[ $(( $(date +%s) - laufzeit )) -gt "$(stat -c %Y "$KONF")" ]
|
||||
}
|
||||
|
||||
soll=gesperrt
|
||||
if [ -s "$KIOSK" ] && getent passwd kiosk >/dev/null && wrapper_aktiv "${1:-}"; then
|
||||
soll=frei
|
||||
fi
|
||||
|
||||
geaendert=""
|
||||
if [ "$soll" = frei ]; then
|
||||
if [ -f "$PAM" ] && ! grep -Eq "$MUSTER" "$PAM"; then
|
||||
sed -i "1i $ZEILE" "$PAM"
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
shell=/bin/bash
|
||||
else
|
||||
if [ -f "$PAM" ] && grep -Eq "$MUSTER" "$PAM"; then
|
||||
sed -i -E "/$MUSTER/d" "$PAM"
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
shell=/usr/sbin/nologin
|
||||
fi
|
||||
|
||||
# Automatische Anmeldung nur zusammen mit dem Wrapper: Beide Dateien liest
|
||||
# LightDM beim selben Start.
|
||||
if [ "$soll" = frei ] && [ -f "$AUTOSTART" ]; then
|
||||
if [ ! -f "$AUTOLOGIN" ]; then
|
||||
printf '%s\n' '# Verwaltet von Tuxflotte (Rolle kiosk)' '[Seat:*]' 'autologin-user=kiosk' 'autologin-user-timeout=0' > "$AUTOLOGIN"
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
elif [ -f "$AUTOLOGIN" ]; then
|
||||
rm -f "$AUTOLOGIN"
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
|
||||
if getent passwd kiosk >/dev/null; then
|
||||
if [ "$(getent passwd kiosk | cut -d: -f7)" != "$shell" ]; then
|
||||
usermod -s "$shell" kiosk
|
||||
geaendert=" geaendert"
|
||||
fi
|
||||
# AccountsService: SystemAccount=true blendet aus, ohne den Schlüssel
|
||||
# zeigt der Greeter das Konto. Andere Einträge der Datei bleiben stehen.
|
||||
if [ "$soll" = frei ]; then
|
||||
if [ -f "$ACCOUNTS" ] && grep -q '^SystemAccount=' "$ACCOUNTS"; then
|
||||
sed -i '/^SystemAccount=/d' "$ACCOUNTS"
|
||||
accounts_geaendert=1
|
||||
fi
|
||||
elif ! { [ -f "$ACCOUNTS" ] && grep -qx 'SystemAccount=true' "$ACCOUNTS"; }; then
|
||||
mkdir -p "$(dirname "$ACCOUNTS")"
|
||||
if [ -f "$ACCOUNTS" ] && grep -q '^\[User\]' "$ACCOUNTS"; then
|
||||
sed -i '/^SystemAccount=/d' "$ACCOUNTS"
|
||||
sed -i '/^\[User\]/a SystemAccount=true' "$ACCOUNTS"
|
||||
else
|
||||
printf '[User]\nSystemAccount=true\n' >> "$ACCOUNTS"
|
||||
fi
|
||||
accounts_geaendert=1
|
||||
fi
|
||||
if [ -n "${accounts_geaendert:-}" ]; then
|
||||
geaendert=" geaendert"
|
||||
# Beim Start von LightDM läuft accounts-daemon meist noch nicht und
|
||||
# liest die Datei selbst; try-restart tut dann nichts.
|
||||
systemctl try-restart accounts-daemon.service 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$soll$geaendert"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
#
|
||||
# Sitzung des Kontos "kiosk": startet den Kiosk-Browser immer wieder.
|
||||
# Rückgabe 0 = Abmelden (zurück zum Anmeldebildschirm, bei Autostart
|
||||
# meldet LightDM gleich wieder an), 3 = gewollter Neustart mit der
|
||||
# Startseite, sonst (Absturz, neue Konfiguration) nach kurzer Pause.
|
||||
while :; do
|
||||
/usr/bin/python3 /usr/local/lib/tuxflotte/tuxflotte-kiosk.py --konfig /etc/tuxflotte/kiosk.json
|
||||
case $? in
|
||||
0) exit 0 ;;
|
||||
3) ;;
|
||||
*) sleep 3 ;;
|
||||
esac
|
||||
done
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/python3
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
#
|
||||
# Kiosk-Browser (platform-docs roadmap/kiosk.md, 26.10.2026): zeigt die
|
||||
# Startseite im Vollbild, ohne Fenstermanager, Adresszeile, neue Fenster,
|
||||
# Dateidialoge, Downloads und Kontextmenü. Daten bleiben nur im Speicher.
|
||||
# Weiterentwicklung des Personalisierungs-Kiosks (Rolle personalisierung bis
|
||||
# f975938).
|
||||
#
|
||||
# Whitelist: geprüft werden nur Navigationen (Seite und Rahmen), nicht
|
||||
# Bilder, Skripte oder Schriften. Navigiert der Benutzer aus der Whitelist
|
||||
# hinaus, endet das Programm mit NEUSTART; kiosk-sitzung startet es frisch
|
||||
# mit der Startseite. Navigationen ohne Benutzeraktion (fremde Rahmen,
|
||||
# Skripte) werden nur still blockiert.
|
||||
#
|
||||
# Leerlauf: nach der eingestellten Zeit ohne Eingabe ebenfalls NEUSTART,
|
||||
# aber nur, wenn seit dem Start jemand etwas eingegeben hat.
|
||||
#
|
||||
# Beenden (Abmelden, Rückgabe 0): Knopf in der Leiste, Strg+Umschalt+Q oder
|
||||
# ein Link auf "tuxflotte:abmelden".
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
from gi.repository import Gdk, GLib, Gtk, WebKit2 # noqa: E402
|
||||
|
||||
ABMELDEN = "tuxflotte:abmelden"
|
||||
NEUSTART = 3
|
||||
|
||||
|
||||
def whitelist_eintrag(text):
|
||||
""""https://www.Example.org/katalog/" -> ("www.example.org", "/katalog")."""
|
||||
text = text.strip()
|
||||
if not text or text.startswith("#"):
|
||||
return None
|
||||
if "://" not in text:
|
||||
text = "http://" + text
|
||||
teile = urlsplit(text)
|
||||
if not teile.hostname:
|
||||
return None
|
||||
pfad = teile.path.rstrip("/")
|
||||
return (teile.hostname.lower().removeprefix("*."), pfad)
|
||||
|
||||
|
||||
def ja(wert):
|
||||
return wert is True or str(wert).strip().lower() in ("true", "1", "on", "yes")
|
||||
|
||||
|
||||
def host_passt(host, domain):
|
||||
return host == domain or host.endswith("." + domain)
|
||||
|
||||
|
||||
class Kiosk:
|
||||
def __init__(self, konfig):
|
||||
self.start_url = konfig["startseite"]
|
||||
self.frei = ja(konfig.get("frei_surfen"))
|
||||
self.leerlauf = max(0, int(konfig.get("leerlauf_minuten", 10))) * 60
|
||||
self.rueckgabe = 0
|
||||
|
||||
start_host = (urlsplit(self.start_url).hostname or "").lower()
|
||||
self.whitelist = [(start_host, "")]
|
||||
for zeile in konfig.get("whitelist", []):
|
||||
eintrag = whitelist_eintrag(zeile)
|
||||
if eintrag:
|
||||
self.whitelist.append(eintrag)
|
||||
|
||||
self.letzte_eingabe = time.monotonic()
|
||||
self.benutzt = False
|
||||
|
||||
if ja(konfig.get("bildschirm_an")):
|
||||
subprocess.run(["xset", "s", "off", "-dpms"], check=False)
|
||||
|
||||
kontext = WebKit2.WebContext.new_ephemeral()
|
||||
kontext.set_preferred_languages(["de-DE", "de"])
|
||||
kontext.connect("download-started", lambda _k, download: download.cancel())
|
||||
|
||||
self.view = WebKit2.WebView.new_with_context(kontext)
|
||||
settings = self.view.get_settings()
|
||||
settings.set_enable_developer_extras(False)
|
||||
settings.set_javascript_can_open_windows_automatically(False)
|
||||
settings.set_allow_file_access_from_file_urls(False)
|
||||
settings.set_allow_universal_access_from_file_urls(False)
|
||||
settings.set_enable_write_console_messages_to_stdout(False)
|
||||
|
||||
self.view.connect("decide-policy", self.on_policy)
|
||||
self.view.connect("create", self.on_create)
|
||||
self.view.connect("context-menu", lambda *_: True)
|
||||
self.view.connect("run-file-chooser", lambda *_: True)
|
||||
self.view.connect("print", lambda *_: True)
|
||||
self.view.connect("permission-request", self.on_permission)
|
||||
self.view.connect("authenticate", self.on_authenticate)
|
||||
self.view.connect("load-changed", self.on_load_changed)
|
||||
self.view.connect("load-failed", self.on_load_failed)
|
||||
self.view.connect("load-failed-with-tls-errors", self.on_tls_error)
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
if not ja(konfig.get("ohne_leiste")):
|
||||
box.pack_start(self.leiste(), False, False, 0)
|
||||
box.pack_start(self.view, True, True, 0)
|
||||
|
||||
self.fenster = Gtk.Window()
|
||||
self.fenster.set_decorated(False)
|
||||
self.fenster.add(box)
|
||||
self.fenster.connect("destroy", lambda *_: Gtk.main_quit())
|
||||
self.fenster.connect("key-press-event", self.on_taste)
|
||||
for signal in ("key-press-event", "button-press-event", "motion-notify-event", "scroll-event", "touch-event"):
|
||||
self.fenster.connect(signal, self.on_eingabe)
|
||||
self.view.connect(signal, self.on_eingabe)
|
||||
|
||||
# Ohne Fenstermanager wirkt fullscreen() nicht zuverlässig: Größe
|
||||
# und Lage selbst auf den Hauptbildschirm setzen.
|
||||
anzeige = Gdk.Display.get_default()
|
||||
monitor = anzeige.get_primary_monitor() or anzeige.get_monitor(0)
|
||||
geo = monitor.get_geometry()
|
||||
self.fenster.move(geo.x, geo.y)
|
||||
self.fenster.set_default_size(geo.width, geo.height)
|
||||
self.fenster.fullscreen()
|
||||
|
||||
if self.leerlauf:
|
||||
GLib.timeout_add_seconds(15, self.pruefe_leerlauf)
|
||||
self.view.load_uri(self.start_url)
|
||||
self.fenster.show_all()
|
||||
self.fenster.present()
|
||||
self.view.grab_focus()
|
||||
|
||||
def leiste(self):
|
||||
leiste = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
leiste.set_border_width(4)
|
||||
for text, aktion in (
|
||||
("← Zurück", lambda *_: self.view.go_back()),
|
||||
("⌂ Startseite", lambda *_: self.view.load_uri(self.start_url)),
|
||||
):
|
||||
knopf = Gtk.Button(label=text)
|
||||
knopf.set_can_focus(False)
|
||||
knopf.connect("clicked", aktion)
|
||||
leiste.pack_start(knopf, False, False, 0)
|
||||
abmelden = Gtk.Button(label="Abmelden")
|
||||
abmelden.set_can_focus(False)
|
||||
abmelden.connect("clicked", lambda *_: self.beenden(0))
|
||||
leiste.pack_end(abmelden, False, False, 0)
|
||||
return leiste
|
||||
|
||||
def beenden(self, rueckgabe):
|
||||
self.rueckgabe = rueckgabe
|
||||
Gtk.main_quit()
|
||||
return False
|
||||
|
||||
def erlaubt(self, uri):
|
||||
teile = urlsplit(uri)
|
||||
if teile.scheme not in ("http", "https") or not teile.hostname:
|
||||
return False
|
||||
if self.frei:
|
||||
return True
|
||||
host = teile.hostname.lower()
|
||||
pfad = teile.path or "/"
|
||||
for domain, anfang in self.whitelist:
|
||||
if host_passt(host, domain) and (not anfang or pfad == anfang or pfad.startswith(anfang + "/")):
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_eingabe(self, *_):
|
||||
self.letzte_eingabe = time.monotonic()
|
||||
self.benutzt = True
|
||||
return False
|
||||
|
||||
def on_taste(self, _fenster, ereignis):
|
||||
strg = ereignis.state & Gdk.ModifierType.CONTROL_MASK
|
||||
umschalt = ereignis.state & Gdk.ModifierType.SHIFT_MASK
|
||||
taste = Gdk.keyval_to_lower(ereignis.keyval)
|
||||
if strg and umschalt and taste == Gdk.KEY_q:
|
||||
return self.beenden(0) or True
|
||||
if strg and taste in (Gdk.KEY_plus, Gdk.KEY_KP_Add, Gdk.KEY_equal):
|
||||
self.view.set_zoom_level(min(self.view.get_zoom_level() + 0.1, 3.0))
|
||||
return True
|
||||
if strg and taste in (Gdk.KEY_minus, Gdk.KEY_KP_Subtract):
|
||||
self.view.set_zoom_level(max(self.view.get_zoom_level() - 0.1, 0.5))
|
||||
return True
|
||||
if strg and taste in (Gdk.KEY_0, Gdk.KEY_KP_0):
|
||||
self.view.set_zoom_level(1.0)
|
||||
return True
|
||||
return False
|
||||
|
||||
def pruefe_leerlauf(self):
|
||||
if self.benutzt and time.monotonic() - self.letzte_eingabe > self.leerlauf:
|
||||
self.beenden(NEUSTART)
|
||||
return False
|
||||
return True
|
||||
|
||||
def verlassen(self, aktion):
|
||||
"""Ziel außerhalb der Whitelist: Neustart, wenn der Benutzer es
|
||||
ausgelöst hat, sonst nur blockieren."""
|
||||
if aktion.is_user_gesture() or aktion.get_navigation_type() in (
|
||||
WebKit2.NavigationType.LINK_CLICKED,
|
||||
WebKit2.NavigationType.FORM_SUBMITTED,
|
||||
WebKit2.NavigationType.FORM_RESUBMITTED,
|
||||
):
|
||||
GLib.idle_add(self.beenden, NEUSTART)
|
||||
|
||||
def on_create(self, _view, aktion):
|
||||
# target="_blank" und window.open: im selben Fenster öffnen.
|
||||
uri = aktion.get_request().get_uri()
|
||||
if self.erlaubt(uri):
|
||||
self.view.load_uri(uri)
|
||||
else:
|
||||
self.verlassen(aktion)
|
||||
return None
|
||||
|
||||
def on_policy(self, _view, entscheidung, art):
|
||||
if art in (WebKit2.PolicyDecisionType.NAVIGATION_ACTION, WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION):
|
||||
aktion = entscheidung.get_navigation_action()
|
||||
uri = aktion.get_request().get_uri()
|
||||
if uri == ABMELDEN:
|
||||
entscheidung.ignore()
|
||||
GLib.idle_add(self.beenden, 0)
|
||||
return True
|
||||
if uri.startswith("about:"):
|
||||
return False
|
||||
if not self.erlaubt(uri):
|
||||
entscheidung.ignore()
|
||||
self.verlassen(aktion)
|
||||
return True
|
||||
return False
|
||||
if art == WebKit2.PolicyDecisionType.RESPONSE:
|
||||
if not entscheidung.is_mime_type_supported():
|
||||
entscheidung.ignore()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_load_changed(self, view, ereignis):
|
||||
# Sicherheitsnetz: Was trotzdem als Seite geladen wird, muss erlaubt
|
||||
# sein.
|
||||
if ereignis == WebKit2.LoadEvent.COMMITTED:
|
||||
uri = view.get_uri() or ""
|
||||
if uri and not uri.startswith("about:") and not self.erlaubt(uri) and uri != self.start_url:
|
||||
GLib.idle_add(self.beenden, NEUSTART)
|
||||
|
||||
def on_permission(self, _view, anfrage):
|
||||
anfrage.deny()
|
||||
return True
|
||||
|
||||
def on_authenticate(self, _view, anfrage):
|
||||
anfrage.cancel()
|
||||
return True
|
||||
|
||||
def fehlerseite(self, text):
|
||||
start = html.escape(self.start_url, quote=True)
|
||||
self.view.load_alternate_html(
|
||||
f"""<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
<style>
|
||||
body {{ font-family: sans-serif; background: #f4f4f4; color: #222;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
height: 100vh; margin: 0; }}
|
||||
main {{ max-width: 34em; text-align: center; }}
|
||||
a {{ display: inline-block; margin: 1em .5em; padding: .7em 1.4em;
|
||||
background: #2a6db0; color: #fff; text-decoration: none; border-radius: 4px; }}
|
||||
</style></head><body><main>
|
||||
<h1>Keine Verbindung</h1>
|
||||
<p>{html.escape(text)}</p>
|
||||
<a href="{start}">Erneut versuchen</a>
|
||||
</main></body></html>""",
|
||||
self.start_url,
|
||||
None,
|
||||
)
|
||||
# Ohne Bedienung (Anzeigetafel) nach einer Minute selbst neu laden.
|
||||
GLib.timeout_add_seconds(60, self.erneut_laden)
|
||||
|
||||
def erneut_laden(self):
|
||||
self.view.load_uri(self.start_url)
|
||||
return False
|
||||
|
||||
def on_load_failed(self, _view, _ereignis, _uri, fehler):
|
||||
# Von uns selbst abgelehnte oder abgebrochene Ladevorgänge sind keine
|
||||
# Störung.
|
||||
if fehler.domain == "WebKitPolicyError":
|
||||
return True
|
||||
if fehler.domain == "WebKitNetworkError" and fehler.code == WebKit2.NetworkError.CANCELLED:
|
||||
return True
|
||||
self.fehlerseite("Die Seite ist gerade nicht erreichbar. Bitte prüfe die Netzwerkverbindung.")
|
||||
return True
|
||||
|
||||
def on_tls_error(self, *_):
|
||||
self.fehlerseite("Die Verbindung ist nicht vertrauenswürdig. Bitte wende dich an die Verwaltung deiner Einrichtung.")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Tuxflotte-Kiosk")
|
||||
parser.add_argument("--konfig", default="/etc/tuxflotte/kiosk.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.konfig, encoding="utf-8") as datei:
|
||||
konfig = json.load(datei)
|
||||
|
||||
kiosk = Kiosk(konfig)
|
||||
Gtk.main()
|
||||
sys.exit(kiosk.rueckgabe)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,426 @@
|
||||
# Merkmal "Kiosk" (platform-docs roadmap/kiosk.md, 26.10.2026): passwortloses
|
||||
# Konto "kiosk" am Anmeldebildschirm, das statt eines Desktops nur den
|
||||
# Kiosk-Browser mit der Startseite startet; optional automatische Anmeldung
|
||||
# (Autostart, z.B. Bibliothek, Schwarzes Brett). Gastsitzung und andere
|
||||
# Konten bleiben daneben nutzbar.
|
||||
#
|
||||
# Optionen (kundenplattform optionen_felder.py, an OE oder Gruppe gesetzt):
|
||||
# kiosk_startseite (Pflicht, http/https), kiosk_whitelist (eine Domain je
|
||||
# Zeile, optional mit Pfad), kiosk_frei_surfen, kiosk_anzeigename,
|
||||
# kiosk_leerlauf_minuten (leer = 10, 0 = nie), kiosk_ohne_leiste,
|
||||
# kiosk_bildschirm_an, kiosk_autostart. Ohne gültige Startseite wird nichts
|
||||
# eingerichtet bzw. ein bestehender Kiosk abgebaut.
|
||||
#
|
||||
# Nur LightDM (Mint, Debian/Xfce). GDM (Ubuntu) kennt keinen
|
||||
# session-wrapper; dort wird übersprungen.
|
||||
|
||||
- name: Ensure marker directory exists
|
||||
ansible.builtin.file:
|
||||
path: /run/tuxflotte/agent/applied
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Check for LightDM PAM configuration
|
||||
ansible.builtin.stat:
|
||||
path: /etc/pam.d/lightdm
|
||||
register: tuxflotte_kiosk_lightdm
|
||||
|
||||
- name: Evaluate kiosk options
|
||||
ansible.builtin.set_fact:
|
||||
tuxflotte_kiosk_startseite: "{{ tuxflotte_optionen.get('kiosk_startseite') | default('', true) | trim }}"
|
||||
tuxflotte_kiosk_whitelist_roh: "{{ tuxflotte_optionen.get('kiosk_whitelist') | default('', true) }}"
|
||||
tuxflotte_kiosk_anzeigename: >-
|
||||
{{ (tuxflotte_optionen.get('kiosk_anzeigename') | default('', true) | regex_replace('[:\n\r,]', ' ') | trim)[:40]
|
||||
| default('Kiosk', true) }}
|
||||
tuxflotte_kiosk_leerlauf: "{{ tuxflotte_optionen.get('kiosk_leerlauf_minuten') | default('10', true) | string | trim }}"
|
||||
|
||||
- name: Decide whether the kiosk is configured
|
||||
ansible.builtin.set_fact:
|
||||
tuxflotte_kiosk_konfiguriert: >-
|
||||
{{ tuxflotte_state == "present"
|
||||
and (tuxflotte_kiosk_startseite is match('^https?://[^\s/]+[^\s]*$')) }}
|
||||
|
||||
# Den Wrapper der Distribution ermitteln, in LightDMs Lesereihenfolge (die
|
||||
# letzte Angabe gilt), ohne Dateien von Tuxflotte (auch nicht die der
|
||||
# früheren Personalisierung).
|
||||
- name: Determine the distribution's LightDM session wrapper
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
wrapper=$(
|
||||
for f in /usr/share/lightdm/lightdm.conf.d/*.conf \
|
||||
/usr/local/share/lightdm/lightdm.conf.d/*.conf \
|
||||
/etc/xdg/lightdm/lightdm.conf.d/*.conf \
|
||||
/etc/lightdm/lightdm.conf.d/*.conf \
|
||||
/etc/lightdm/lightdm.conf; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in *tuxflotte*) continue ;; esac
|
||||
sed -n 's/^[[:space:]]*session-wrapper[[:space:]]*=[[:space:]]*//p' "$f"
|
||||
done | tail -n 1
|
||||
)
|
||||
wrapper=${wrapper:-lightdm-session}
|
||||
case "$wrapper" in
|
||||
/*) ;;
|
||||
*) wrapper=$(PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin command -v "$wrapper" || true) ;;
|
||||
esac
|
||||
case "$wrapper" in /usr/local/lib/tuxflotte/*) wrapper="" ;; esac
|
||||
if [ -n "$wrapper" ] && [ -x "$wrapper" ]; then
|
||||
echo "$wrapper"
|
||||
fi
|
||||
register: tuxflotte_kiosk_wrapper
|
||||
changed_when: false
|
||||
when: tuxflotte_kiosk_konfiguriert | bool and tuxflotte_kiosk_lightdm.stat.exists
|
||||
|
||||
- name: Decide whether the kiosk can be set up here
|
||||
ansible.builtin.set_fact:
|
||||
tuxflotte_kiosk_aktiv: >-
|
||||
{{ (tuxflotte_kiosk_konfiguriert | bool) and tuxflotte_kiosk_lightdm.stat.exists
|
||||
and (tuxflotte_kiosk_wrapper.stdout | default('')) != '' }}
|
||||
tuxflotte_kiosk_wrapper_original: "{{ tuxflotte_kiosk_wrapper.stdout | default('') }}"
|
||||
|
||||
- name: Report skipped setup
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Kiosk: {{ 'keine gültige Startseite (http/https) in den Optionen'
|
||||
if not (tuxflotte_kiosk_konfiguriert | bool)
|
||||
else 'kein LightDM oder kein Sitzungs-Wrapper gefunden' }}, nicht eingerichtet
|
||||
when: tuxflotte_state == "present" and not (tuxflotte_kiosk_aktiv | bool)
|
||||
|
||||
# Läuft außer dem Kiosk eine Benutzersitzung, wird LightDM nicht neu
|
||||
# gestartet (das beendete sie); die Änderung wirkt dann beim nächsten Start.
|
||||
- name: Check for running user sessions other than the kiosk
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
for sitzung in $(loginctl list-sessions --no-legend | awk '{print $1}'); do
|
||||
werte=$(loginctl show-session "$sitzung" --property=Class --property=State --property=Name)
|
||||
echo "$werte" | grep -qx Class=user || continue
|
||||
echo "$werte" | grep -qx State=closing && continue
|
||||
echo "$werte" | grep -qx Name=kiosk && continue
|
||||
echo "belegt"
|
||||
exit 0
|
||||
done
|
||||
echo "frei"
|
||||
register: tuxflotte_kiosk_sitzungen
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
# --- einrichten ----------------------------------------------------------------
|
||||
|
||||
- name: Install kiosk dependencies
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- python3-gi
|
||||
- gir1.2-gtk-3.0
|
||||
- gir1.2-webkit2-4.1
|
||||
- x11-xserver-utils
|
||||
state: present
|
||||
register: tuxflotte_kiosk_pakete
|
||||
until: tuxflotte_kiosk_pakete is succeeded
|
||||
retries: 10
|
||||
delay: 30
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# Kein Passwort (gesperrt), Anmeldung nur über die PAM-Freigabe von
|
||||
# kiosk-anmeldung. Die Shell setzt das Skript; der Kommentar ist der
|
||||
# Anzeigename im Greeter.
|
||||
- name: Ensure kiosk account exists
|
||||
ansible.builtin.user:
|
||||
name: kiosk
|
||||
comment: "{{ tuxflotte_kiosk_anzeigename }}"
|
||||
create_home: true
|
||||
password_lock: true
|
||||
register: tuxflotte_kiosk_konto
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Ensure Tuxflotte directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop:
|
||||
- /usr/local/lib/tuxflotte
|
||||
- /etc/tuxflotte
|
||||
- /var/lib/tuxflotte
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# Kennzeichnet das Konto als unseres (für das Abwählen).
|
||||
- name: Record that the kiosk account belongs to this role
|
||||
ansible.builtin.copy:
|
||||
dest: /var/lib/tuxflotte/kiosk-konto
|
||||
content: "kiosk\n"
|
||||
mode: "0644"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# Eine Vormerkung aus einem früheren Abbau gälte sonst für den neuen
|
||||
# Wrapper (siehe Abbau unten).
|
||||
- name: Clear an old wrapper removal note
|
||||
ansible.builtin.file:
|
||||
path: /var/lib/tuxflotte/kiosk-wrapper-ungenutzt-seit
|
||||
state: absent
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Install kiosk program and scripts
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item }}"
|
||||
dest: "/usr/local/lib/tuxflotte/{{ item }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- tuxflotte-kiosk.py
|
||||
- kiosk-sitzung
|
||||
- kiosk-anmeldung
|
||||
register: tuxflotte_kiosk_programm
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Install LightDM session wrapper
|
||||
ansible.builtin.template:
|
||||
src: kiosk-wrapper.j2
|
||||
dest: /usr/local/lib/tuxflotte/kiosk-wrapper
|
||||
mode: "0755"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# Lesbar für das Kiosk-Konto, keine Zugangsdaten.
|
||||
- name: Write kiosk configuration
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/tuxflotte/kiosk.json
|
||||
content: "{{ tuxflotte_kiosk_konfig | to_nice_json }}\n"
|
||||
mode: "0644"
|
||||
vars:
|
||||
tuxflotte_kiosk_konfig:
|
||||
startseite: "{{ tuxflotte_kiosk_startseite }}"
|
||||
whitelist: >-
|
||||
{{ (tuxflotte_kiosk_whitelist_roh if tuxflotte_kiosk_whitelist_roh is string
|
||||
else tuxflotte_kiosk_whitelist_roh | join('\n')).splitlines()
|
||||
| map('trim') | select | list }}
|
||||
frei_surfen: "{{ tuxflotte_optionen.get('kiosk_frei_surfen', false) | bool }}"
|
||||
leerlauf_minuten: "{{ tuxflotte_kiosk_leerlauf | int(10) if tuxflotte_kiosk_leerlauf is match('^[0-9]+$') else 10 }}"
|
||||
ohne_leiste: "{{ tuxflotte_optionen.get('kiosk_ohne_leiste', false) | bool }}"
|
||||
bildschirm_an: "{{ tuxflotte_optionen.get('kiosk_bildschirm_an', false) | bool }}"
|
||||
register: tuxflotte_kiosk_json
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Record the autostart wish
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/tuxflotte/kiosk-autostart
|
||||
content: "# Verwaltet von Tuxflotte (Rolle kiosk)\n"
|
||||
mode: "0644"
|
||||
register: tuxflotte_kiosk_autostart_an
|
||||
when: (tuxflotte_kiosk_aktiv | bool) and (tuxflotte_optionen.get('kiosk_autostart', false) | bool)
|
||||
|
||||
- name: Remove the autostart wish
|
||||
ansible.builtin.file:
|
||||
path: /etc/tuxflotte/kiosk-autostart
|
||||
state: absent
|
||||
register: tuxflotte_kiosk_autostart_aus
|
||||
when: (tuxflotte_kiosk_aktiv | bool) and not (tuxflotte_optionen.get('kiosk_autostart', false) | bool)
|
||||
|
||||
- name: Ensure LightDM drop-in directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/lightdm/lightdm.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Use the kiosk session wrapper in LightDM
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/lightdm/lightdm.conf.d/86-tuxflotte-kiosk.conf
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
[Seat:*]
|
||||
session-wrapper=/usr/local/lib/tuxflotte/kiosk-wrapper
|
||||
mode: "0644"
|
||||
register: tuxflotte_kiosk_lightdm_konf
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Ensure lightdm.service drop-in directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/lightdm.service.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# Beim Start von LightDM gilt die neue Konfiguration sofort; "-": ein
|
||||
# fehlendes Skript hält LightDM nicht auf.
|
||||
- name: Apply the login switch when LightDM starts
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/lightdm.service.d/86-tuxflotte-kiosk.conf
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
[Service]
|
||||
ExecStartPre=-/usr/local/lib/tuxflotte/kiosk-anmeldung start
|
||||
mode: "0644"
|
||||
register: tuxflotte_kiosk_dropin
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Reload systemd for the lightdm.service drop-in
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_dropin is changed
|
||||
|
||||
- name: Ensure logind drop-in directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/logind.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# Kein Prozess des Kiosk-Kontos überlebt das Abmelden; logind führt die
|
||||
# KillOnlyUsers-Listen aller Dateien zusammen.
|
||||
- name: Kill leftover kiosk processes on logout (logind)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/logind.conf.d/50-tuxflotte-kiosk.conf
|
||||
content: |
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
[Login]
|
||||
KillUserProcesses=yes
|
||||
KillOnlyUsers=kiosk
|
||||
mode: "0644"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Apply the login switch now
|
||||
ansible.builtin.command: /usr/local/lib/tuxflotte/kiosk-anmeldung
|
||||
register: tuxflotte_kiosk_anmeldung
|
||||
changed_when: "'geaendert' in tuxflotte_kiosk_anmeldung.stdout"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
- name: Refresh the greeter's account data after a name change
|
||||
ansible.builtin.systemd:
|
||||
name: accounts-daemon.service
|
||||
state: restarted
|
||||
failed_when: false
|
||||
when: (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto is changed
|
||||
|
||||
# Neue Konfiguration: der laufende Kiosk-Browser endet, kiosk-sitzung
|
||||
# startet ihn mit ihr neu.
|
||||
- name: Restart a running kiosk browser with the new configuration
|
||||
ansible.builtin.command: pkill -u kiosk -f /usr/local/lib/tuxflotte/tuxflotte-kiosk.py
|
||||
register: tuxflotte_kiosk_pkill
|
||||
changed_when: tuxflotte_kiosk_pkill.rc == 0
|
||||
failed_when: false
|
||||
when: (tuxflotte_kiosk_aktiv | bool) and (tuxflotte_kiosk_json is changed or tuxflotte_kiosk_programm is changed)
|
||||
|
||||
# Wrapper und automatische Anmeldung liest LightDM nur beim Start. Ohne
|
||||
# fremde Benutzersitzung gleich neu starten (eine laufende Kiosk-Sitzung
|
||||
# endet dabei), sonst wirkt es beim nächsten Start des Geräts.
|
||||
- name: Restart the login screen to apply wrapper or autostart
|
||||
ansible.builtin.command: systemctl try-restart lightdm.service
|
||||
when: >-
|
||||
(tuxflotte_kiosk_aktiv | bool)
|
||||
and (tuxflotte_kiosk_sitzungen.stdout | default('')) == 'frei'
|
||||
and (tuxflotte_kiosk_lightdm_konf is changed or tuxflotte_kiosk_dropin is changed
|
||||
or tuxflotte_kiosk_autostart_an is changed or tuxflotte_kiosk_autostart_aus is changed)
|
||||
|
||||
- name: Record that the kiosk role ran
|
||||
ansible.builtin.copy:
|
||||
dest: /run/tuxflotte/agent/applied/kiosk.marker
|
||||
content: "{{ ansible_date_time.iso8601 }}\n"
|
||||
mode: "0644"
|
||||
when: tuxflotte_kiosk_aktiv | bool
|
||||
|
||||
# --- abbauen (abgewählt oder ohne gültige Startseite) --------------------------
|
||||
|
||||
- name: Remove passwordless login for kiosk
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/lightdm
|
||||
regexp: '^auth\s+sufficient\s+pam_succeed_if\.so\s+user\s+=\s+kiosk\s*$'
|
||||
state: absent
|
||||
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_lightdm.stat.exists
|
||||
|
||||
- name: Check whether the kiosk account is ours
|
||||
ansible.builtin.stat:
|
||||
path: /var/lib/tuxflotte/kiosk-konto
|
||||
register: tuxflotte_kiosk_konto_unser
|
||||
when: not (tuxflotte_kiosk_aktiv | bool)
|
||||
|
||||
- name: End a running kiosk session
|
||||
ansible.builtin.command: loginctl terminate-user kiosk
|
||||
register: tuxflotte_kiosk_terminate
|
||||
changed_when: tuxflotte_kiosk_terminate.rc == 0
|
||||
failed_when: false
|
||||
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto_unser.stat.exists
|
||||
|
||||
# kiosk-wrapper bleibt bewusst liegen: Ein laufendes LightDM ruft ihn bis
|
||||
# zu seinem nächsten Start weiter auf; ohne Konto "kiosk" reicht er nur
|
||||
# durch. Entfernt wird er weiter unten.
|
||||
- name: Remove kiosk files
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- /etc/lightdm/lightdm.conf.d/86-tuxflotte-kiosk.conf
|
||||
- /etc/lightdm/lightdm.conf.d/87-tuxflotte-kiosk-autostart.conf
|
||||
- /etc/systemd/system/lightdm.service.d/86-tuxflotte-kiosk.conf
|
||||
- /etc/systemd/logind.conf.d/50-tuxflotte-kiosk.conf
|
||||
- /etc/tuxflotte/kiosk.json
|
||||
- /etc/tuxflotte/kiosk-autostart
|
||||
- /usr/local/lib/tuxflotte/tuxflotte-kiosk.py
|
||||
- /usr/local/lib/tuxflotte/kiosk-sitzung
|
||||
- /usr/local/lib/tuxflotte/kiosk-anmeldung
|
||||
register: tuxflotte_kiosk_entfernt
|
||||
when: not (tuxflotte_kiosk_aktiv | bool)
|
||||
|
||||
- name: Reload systemd after removing the drop-in
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_entfernt is changed
|
||||
|
||||
- name: Remove kiosk account and home
|
||||
ansible.builtin.user:
|
||||
name: kiosk
|
||||
state: absent
|
||||
remove: true
|
||||
force: true
|
||||
register: tuxflotte_kiosk_konto_entfernt
|
||||
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto_unser.stat.exists
|
||||
|
||||
- name: Remove AccountsService entry and ownership note of the kiosk account
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- /var/lib/AccountsService/users/kiosk
|
||||
- /var/lib/tuxflotte/kiosk-konto
|
||||
when: not (tuxflotte_kiosk_aktiv | bool) and tuxflotte_kiosk_konto_unser.stat.exists
|
||||
|
||||
# slick-greeter übernimmt das verschwundene Konto nicht live.
|
||||
- name: Restart the login screen after removal
|
||||
ansible.builtin.command: systemctl try-restart lightdm.service
|
||||
when: >-
|
||||
not (tuxflotte_kiosk_aktiv | bool)
|
||||
and (tuxflotte_kiosk_sitzungen.stdout | default('')) == 'frei'
|
||||
and (tuxflotte_kiosk_konto_entfernt is changed or tuxflotte_kiosk_entfernt is changed)
|
||||
|
||||
# Den Wrapper erst entfernen, wenn das laufende LightDM nach dem Abbau seiner
|
||||
# Konfiguration gestartet wurde (Zeitpunkt in der Merkerdatei, beim ersten
|
||||
# Lauf nach dem Abbau gesetzt). Läuft kein LightDM, liest ein späterer Start
|
||||
# die Konfiguration ohne den Wrapper.
|
||||
- name: Remove the session wrapper once LightDM no longer uses it
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
WRAPPER=/usr/local/lib/tuxflotte/kiosk-wrapper
|
||||
SEIT=/var/lib/tuxflotte/kiosk-wrapper-ungenutzt-seit
|
||||
if [ ! -e "$WRAPPER" ]; then
|
||||
rm -f "$SEIT"
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -s "$SEIT" ]; then
|
||||
mkdir -p "$(dirname "$SEIT")"
|
||||
date +%s > "$SEIT"
|
||||
echo "vorgemerkt"
|
||||
exit 0
|
||||
fi
|
||||
pid=$(systemctl show -p MainPID --value lightdm.service 2>/dev/null || echo 0)
|
||||
if [ "${pid:-0}" -gt 0 ] 2>/dev/null; then
|
||||
laufzeit=$(ps -o etimes= -p "$pid" | tr -d ' ')
|
||||
[ -n "$laufzeit" ] || exit 0
|
||||
[ $(( $(date +%s) - laufzeit )) -gt "$(head -n 1 "$SEIT")" ] || exit 0
|
||||
fi
|
||||
rm -f "$WRAPPER" "$SEIT"
|
||||
echo "entfernt"
|
||||
register: tuxflotte_kiosk_wrapper_weg
|
||||
changed_when: "'entfernt' in tuxflotte_kiosk_wrapper_weg.stdout"
|
||||
when: not (tuxflotte_kiosk_aktiv | bool)
|
||||
|
||||
- name: Remove kiosk marker
|
||||
ansible.builtin.file:
|
||||
path: /run/tuxflotte/agent/applied/kiosk.marker
|
||||
state: absent
|
||||
when: not (tuxflotte_kiosk_aktiv | bool)
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Verwaltet von Tuxflotte (Rolle kiosk)
|
||||
#
|
||||
# LightDM-session-wrapper: das Konto "kiosk" bekommt statt der gewählten
|
||||
# Sitzung den Kiosk, alle anderen laufen unverändert über den Wrapper der
|
||||
# Distribution. Der Kiosk wird wie ein Sitzungskommando übergeben, damit
|
||||
# die übliche Sitzungsumgebung (Profil, D-Bus, Xsession.d) auch für ihn
|
||||
# gilt (wie früher bei der Rolle personalisierung).
|
||||
if [ "$(id -un)" = "kiosk" ]; then
|
||||
set -- /usr/local/lib/tuxflotte/kiosk-sitzung
|
||||
fi
|
||||
exec {{ tuxflotte_kiosk_wrapper_original }} "$@"
|
||||
@@ -102,6 +102,17 @@
|
||||
tuxflotte_optionen: "{{ tuxflotte_auftrag_optionen.get('personalisierung', {}) }}"
|
||||
tags: personalisierung
|
||||
|
||||
# Merkmal "Kiosk" (roadmap/kiosk.md, 26.10.2026): Konto mit Vollbild-
|
||||
# Browser am Anmeldebildschirm, optional Autostart.
|
||||
- include_role:
|
||||
name: kiosk
|
||||
apply:
|
||||
tags: kiosk
|
||||
vars:
|
||||
tuxflotte_state: "{{ tuxflotte_auftrag_states.get('kiosk', 'absent') }}"
|
||||
tuxflotte_optionen: "{{ tuxflotte_auftrag_optionen.get('kiosk', {}) }}"
|
||||
tags: kiosk
|
||||
|
||||
- include_role:
|
||||
name: brave
|
||||
apply:
|
||||
|
||||
Reference in new issue
Block a user