Freigaben einbinden, apt-Installation und RPC-Neustart korrigiert
Drei Fehler, alle drei gemessen statt vermutet.
1. Freigaben wurden nie eingebunden ("mount error(1): Operation not
permitted"). Ursache war NICHT der Mount-Namensraum, sondern
SystemCallFilter=@system-service: darin fehlen mount/umount2, und
SystemCallErrorNumber=EPERM macht daraus genau diese Meldung. Nachgewiesen
in einem nachgebauten Sandkasten: mit "@mount" gelingt die Einbindung,
ohne nicht. Die Unit von TESM erlaubt jetzt "@system-service @mount"; alles
andere an der Haertung bleibt. Der zuvor eingebaute Umweg ueber nsenter ist
zurueckgenommen -- RestrictNamespaces=yes verbietet setns, er konnte nie
funktionieren.
Eingebunden wird damit im Namensraum des Webprozesses. Deshalb ist die
Aufraeumaufgabe fuer abgelaufene Einbindungen aus dem Ueberwachungsdienst in
den Webprozess gewandert: ein anderer Dienst sieht diese Einbindungen nicht
und haette Datenbankzeilen als geloest markiert, waehrend die Freigabe
eingebunden blieb.
2. apt-Installationen aus der Oberflaeche scheiterten mit "dpkg returned an
error code (1)". Der Sandkasten wird an dpkg und dessen Maintainer-Skripte
weitervererbt; mit ProtectKernelLogs=yes liefert "dmesg" ein EPERM und ein
Postinst-Skript bricht daran ab. apt laeuft jetzt ueber systemd-run, also
von PID 1 gestartet und damit ohne unsere Haertung.
3. Der RPC-Neustart loeste SSH aus. run_reboot entschied den Weg anhand von
device["category"] -- und dieser Schluessel bedeutet je Herkunft etwas
anderes: die Wartungsabfrage legt die Kategorie der *Zugangsdaten* darunter,
inventory.get_device die des *Geraets*. Ueber die Statusuebersicht kam ein
Windows-Geraet mit leerer Geraetekategorie an, wurde fuer Linux gehalten und
ueber SSH auf Port 22 angesprochen, bis das Zeitlimit griff. Der Weg wird
jetzt uebergeben -- entschieden wird er ohnehin schon vorher in
restart.resolve. Ausserdem zeigte die Neustart-Historie alles ausser "ssh"
als "PoE-Port"; RPC hat jetzt einen eigenen Eintrag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -287,6 +287,39 @@ def _startup_tasks(app: Flask) -> None:
|
|||||||
|
|
||||||
extension.license.start_heartbeat(hostname=socket.gethostname())
|
extension.license.start_heartbeat(hostname=socket.gethostname())
|
||||||
|
|
||||||
|
_start_fileshare_sweep(app)
|
||||||
|
|
||||||
|
|
||||||
|
#: Wie oft nach abgelaufenen Einbindungen gesehen wird.
|
||||||
|
FILESHARE_SWEEP_SECONDS = 300
|
||||||
|
|
||||||
|
|
||||||
|
def _start_fileshare_sweep(app: Flask) -> None:
|
||||||
|
"""Loest abgelaufene Freigabe-Einbindungen -- im Webprozess.
|
||||||
|
|
||||||
|
Muss hier laufen und nicht im Ueberwachungsdienst: eingebunden wird im
|
||||||
|
Mount-Namensraum dieses Dienstes, und ein anderer Dienst sieht diese
|
||||||
|
Einbindungen nicht einmal. Ein ``umount`` von dort waere ein Aufruf ins
|
||||||
|
Leere -- die Datenbankzeile waere als geloest markiert, die Freigabe aber
|
||||||
|
weiter eingebunden.
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .services import fileshare
|
||||||
|
|
||||||
|
def schleife() -> None:
|
||||||
|
while True:
|
||||||
|
time.sleep(FILESHARE_SWEEP_SECONDS)
|
||||||
|
try:
|
||||||
|
with app.app_context():
|
||||||
|
geloest = fileshare.sweep_expired(core(app).database)
|
||||||
|
if geloest:
|
||||||
|
logger.info("%s abgelaufene Freigabe-Einbindungen geloest", geloest)
|
||||||
|
except Exception: # noqa: BLE001 - der Webprozess darf nie sterben
|
||||||
|
logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen")
|
||||||
|
|
||||||
|
threading.Thread(target=schleife, name="fileshare-sweep", daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
# Fuer ``gunicorn tesm:app``
|
# Fuer ``gunicorn tesm:app``
|
||||||
def _lazy_app() -> Flask: # pragma: no cover - nur vom WSGI-Server benutzt
|
def _lazy_app() -> Flask: # pragma: no cover - nur vom WSGI-Server benutzt
|
||||||
|
|||||||
@@ -307,6 +307,10 @@ def _restart_as_job(extension: Any, conn: Any, device: Any, method: Any): # typ
|
|||||||
password=password,
|
password=password,
|
||||||
wait_seconds=int(extension.settings.get(conn, "maintenance_reboot_wait") or 300),
|
wait_seconds=int(extension.settings.get(conn, "maintenance_reboot_wait") or 300),
|
||||||
actor=current_user.username,
|
actor=current_user.username,
|
||||||
|
# Der Weg ist oben schon entschieden (restart.resolve). Ihn hier
|
||||||
|
# mitzugeben ist der Unterschied zwischen "Windows-Geraet ueber RPC" und
|
||||||
|
# "Windows-Geraet ueber SSH auf Port 22, bis das Zeitlimit greift".
|
||||||
|
method=method.key,
|
||||||
)
|
)
|
||||||
flash(
|
flash(
|
||||||
f"{method.label} fuer {device['name']} gestartet. Der Verlauf steht unter Wartung.",
|
f"{method.label} fuer {device['name']} gestartet. Der Verlauf steht unter Wartung.",
|
||||||
|
|||||||
@@ -255,6 +255,15 @@ def _start(kind: str) -> None:
|
|||||||
password=password,
|
password=password,
|
||||||
wait_seconds=int(extension.settings.get(conn, "maintenance_reboot_wait") or 300),
|
wait_seconds=int(extension.settings.get(conn, "maintenance_reboot_wait") or 300),
|
||||||
actor=current_user.username,
|
actor=current_user.username,
|
||||||
|
# Hier steht in ``category`` die Kategorie der *Zugangsdaten*
|
||||||
|
# (siehe _query_devices) -- daraus folgt der Weg. Uebergeben
|
||||||
|
# statt in run_reboot erraten: dort bedeutet derselbe
|
||||||
|
# Schluessel je Herkunft etwas anderes.
|
||||||
|
method=(
|
||||||
|
service.METHOD_RPC
|
||||||
|
if str(device["category"]) == inventory.NON_SSH_CATEGORY
|
||||||
|
else service.METHOD_SSH
|
||||||
|
),
|
||||||
)
|
)
|
||||||
started += 1
|
started += 1
|
||||||
|
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ def run_reboot(
|
|||||||
password: str,
|
password: str,
|
||||||
wait_seconds: int,
|
wait_seconds: int,
|
||||||
actor: str = "system",
|
actor: str = "system",
|
||||||
|
method: str = METHOD_SSH,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Loest einen Neustart aus und wartet auf die Rueckkehr.
|
"""Loest einen Neustart aus und wartet auf die Rueckkehr.
|
||||||
|
|
||||||
@@ -518,7 +519,15 @@ def run_reboot(
|
|||||||
return int((time.monotonic() - started) * 1000)
|
return int((time.monotonic() - started) * 1000)
|
||||||
|
|
||||||
host = str(device["ip"])
|
host = str(device["ip"])
|
||||||
is_windows = str(device.get("category") or "") == WINDOWS_CATEGORY
|
# Der Weg wird **uebergeben**, nicht aus dem Geraet abgeleitet. Vorher stand
|
||||||
|
# hier ``device["category"]`` -- und dieser Schluessel bedeutet je Herkunft
|
||||||
|
# etwas anderes: die Wartungsabfrage legt die Kategorie der *Zugangsdaten*
|
||||||
|
# darunter, ``inventory.get_device`` die des *Geraets*. Ueber die
|
||||||
|
# Statusuebersicht kam damit ein Windows-Geraet mit leerer Geraetekategorie
|
||||||
|
# an, wurde fuer Linux gehalten und ueber SSH auf Port 22 angesprochen --
|
||||||
|
# bis zum Zeitlimit. Die Entscheidung faellt jetzt einmal beim Aufrufer
|
||||||
|
# (``restart.resolve``) und wird hier nur noch befolgt.
|
||||||
|
is_windows = method == METHOD_RPC
|
||||||
# Woran erkennt man, dass das Geraet zurueck ist? Am selben Dienst, ueber
|
# Woran erkennt man, dass das Geraet zurueck ist? Am selben Dienst, ueber
|
||||||
# den der Neustart lief: SSH bei Linux, SMB/RPC bei Windows.
|
# den der Neustart lief: SSH bei Linux, SMB/RPC bei Windows.
|
||||||
probe_port = WINDOWS_PROBE_PORT if is_windows else int(device.get("ssh_port") or 22)
|
probe_port = WINDOWS_PROBE_PORT if is_windows else int(device.get("ssh_port") or 22)
|
||||||
@@ -566,6 +575,7 @@ def run_reboot(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail=str(exc),
|
detail=str(exc),
|
||||||
|
method=method,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -607,6 +617,7 @@ def run_reboot(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail=problem,
|
detail=problem,
|
||||||
|
method=method,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -646,6 +657,7 @@ def run_reboot(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail="Neustart ueber SSH, Geraet ist zurueckgekehrt.",
|
detail="Neustart ueber SSH, Geraet ist zurueckgekehrt.",
|
||||||
|
method=method,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
time.sleep(REBOOT_POLL_SECONDS)
|
time.sleep(REBOOT_POLL_SECONDS)
|
||||||
@@ -669,7 +681,8 @@ def run_reboot(
|
|||||||
f"Neustart ausgeloest, aber {host}:{ssh_port} hat innerhalb von "
|
f"Neustart ausgeloest, aber {host}:{ssh_port} hat innerhalb von "
|
||||||
f"{wait_seconds}s nicht wieder geantwortet."
|
f"{wait_seconds}s nicht wieder geantwortet."
|
||||||
),
|
),
|
||||||
)
|
method=method,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _log_restart(
|
def _log_restart(
|
||||||
|
|||||||
@@ -36,8 +36,13 @@ Der Vorgaenger protokollierte Neustarts ausdruecklich nicht.', 'info') }}
|
|||||||
<td class="fs-sm nowrap" data-sort-value="{{ row.ts }}">{{ row.ts | dt }}</td>
|
<td class="fs-sm nowrap" data-sort-value="{{ row.ts }}">{{ row.ts | dt }}</td>
|
||||||
<td class="fw-600 fs-sm">{{ row.device_name or '–' | safe }}</td>
|
<td class="fw-600 fs-sm">{{ row.device_name or '–' | safe }}</td>
|
||||||
<td class="fs-sm">
|
<td class="fs-sm">
|
||||||
|
{# Kein if/else mit zwei Zweigen: es gibt drei Wege, und alles
|
||||||
|
ausser 'ssh' als "PoE-Port" anzuzeigen hat RPC-Neustarts
|
||||||
|
jahrelang als PoE ausgegeben. #}
|
||||||
{% if row.method == 'ssh' %}{{ pill('SSH', 'info') }}
|
{% if row.method == 'ssh' %}{{ pill('SSH', 'info') }}
|
||||||
{% else %}{{ pill('PoE-Port', '') }}{% endif %}
|
{% elif row.method == 'rpc' %}{{ pill('RPC', 'accent') }}
|
||||||
|
{% elif row.method == 'poe' %}{{ pill('PoE-Port', '') }}
|
||||||
|
{% else %}{{ pill(row.method or 'unbekannt', 'warning') }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-sm">{{ row.trigger }}</td>
|
<td class="fs-sm">{{ row.trigger }}</td>
|
||||||
<td class="fs-sm">{{ row.actor }}</td>
|
<td class="fs-sm">{{ row.actor }}</td>
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ from types import FrameType
|
|||||||
from tesm_core.extension import core
|
from tesm_core.extension import core
|
||||||
|
|
||||||
from . import create_app
|
from . import create_app
|
||||||
from .services import fileshare, inventory, monitor
|
from .services import inventory, monitor
|
||||||
|
|
||||||
logger = logging.getLogger("tesm.monitor")
|
logger = logging.getLogger("tesm.monitor")
|
||||||
|
|
||||||
MIN_INTERVAL = 30
|
MIN_INTERVAL = 30
|
||||||
FILESHARE_SWEEP_EVERY = 30 * 60
|
|
||||||
|
|
||||||
|
|
||||||
class MonitorDaemon:
|
class MonitorDaemon:
|
||||||
@@ -35,7 +34,6 @@ class MonitorDaemon:
|
|||||||
self.app = create_app()
|
self.app = create_app()
|
||||||
self.extension = core(self.app)
|
self.extension = core(self.app)
|
||||||
self.running = True
|
self.running = True
|
||||||
self._last_fileshare_sweep = 0.0
|
|
||||||
|
|
||||||
def stop(self, signum: int, _frame: FrameType | None) -> None:
|
def stop(self, signum: int, _frame: FrameType | None) -> None:
|
||||||
logger.info("Signal %s empfangen -- beende nach dem laufenden Durchlauf.", signum)
|
logger.info("Signal %s empfangen -- beende nach dem laufenden Durchlauf.", signum)
|
||||||
@@ -100,15 +98,12 @@ class MonitorDaemon:
|
|||||||
logger.exception("Durchlauf fehlgeschlagen")
|
logger.exception("Durchlauf fehlgeschlagen")
|
||||||
live.error("monitor", "Durchlauf fehlgeschlagen -- siehe app.log")
|
live.error("monitor", "Durchlauf fehlgeschlagen -- siehe app.log")
|
||||||
|
|
||||||
if time.monotonic() - self._last_fileshare_sweep > FILESHARE_SWEEP_EVERY:
|
# Das Aufraeumen abgelaufener Freigabe-Einbindungen laeuft **nicht**
|
||||||
self._last_fileshare_sweep = time.monotonic()
|
# hier, sondern im Webprozess: eingebunden wird in dessen
|
||||||
try:
|
# Mount-Namensraum, und dieser Dienst sieht diese Einbindungen nicht
|
||||||
with self.app.app_context():
|
# einmal. Ein umount von hier waere ein Aufruf ins Leere -- die
|
||||||
released = fileshare.sweep_expired(self.extension.database)
|
# Datenbankzeile als geloest markiert, die Freigabe aber weiter
|
||||||
if released:
|
# eingebunden. Siehe tesm/__init__.py::_start_fileshare_sweep.
|
||||||
live.info("fileshare", f"{released} abgelaufene Einbindungen geloest")
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen")
|
|
||||||
|
|
||||||
elapsed = time.monotonic() - started
|
elapsed = time.monotonic() - started
|
||||||
remaining = max(1.0, interval - elapsed)
|
remaining = max(1.0, interval - elapsed)
|
||||||
|
|||||||
@@ -59,7 +59,20 @@ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
|||||||
LockPersonality=yes
|
LockPersonality=yes
|
||||||
MemoryDenyWriteExecute=no
|
MemoryDenyWriteExecute=no
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
SystemCallFilter=@system-service
|
# "@mount" zusaetzlich zu @system-service: die Dateifreigaben werden ueber
|
||||||
|
# mount.cifs eingebunden, und @system-service enthaelt mount/umount2 nicht.
|
||||||
|
# Ohne diese Ergaenzung scheitert jede Einbindung mit "mount error(1):
|
||||||
|
# Operation not permitted" -- gemessen, und zwar unabhaengig von Rechten,
|
||||||
|
# Zugangsdaten und Mount-Optionen: aus einer normalen Root-Shell gelingt
|
||||||
|
# derselbe Aufruf sofort. Der Umweg ueber nsenter in den Namensraum des Wirts
|
||||||
|
# ist keine Alternative, weil "RestrictNamespaces=yes" setns verbietet.
|
||||||
|
#
|
||||||
|
# Eingebunden wird damit im Mount-Namensraum *dieses* Dienstes. Das ist genau
|
||||||
|
# richtig: nur er liest die Dateien, andere Prozesse haben dort nichts zu
|
||||||
|
# suchen, und mit dem Dienst verschwinden die Einbindungen wieder. Die
|
||||||
|
# Aufraeumaufgabe fuer abgelaufene Einbindungen laeuft deshalb im Webprozess --
|
||||||
|
# der Ueberwachungsdienst koennte sie nicht einmal sehen.
|
||||||
|
SystemCallFilter=@system-service @mount
|
||||||
SystemCallErrorNumber=EPERM
|
SystemCallErrorNumber=EPERM
|
||||||
# Bewusst **kein** einschraenkendes CapabilityBoundingSet: sudo braucht
|
# Bewusst **kein** einschraenkendes CapabilityBoundingSet: sudo braucht
|
||||||
# CAP_SETUID/CAP_SETGID, und der Helfer dahinter braucht Root. Was hier fehlt,
|
# CAP_SETUID/CAP_SETGID, und der Helfer dahinter braucht Root. Was hier fehlt,
|
||||||
|
|||||||
+37
-22
@@ -258,19 +258,17 @@ case "$verb" in
|
|||||||
# Kommandozeile (Prozessliste) und nie ueber die Umgebung
|
# Kommandozeile (Prozessliste) und nie ueber die Umgebung
|
||||||
# (/proc/<pid>/environ).
|
# (/proc/<pid>/environ).
|
||||||
read_stdin_limited > "$creds"
|
read_stdin_limited > "$creds"
|
||||||
# Der aufrufende Dienst laeuft in einem eigenen Mount-Namensraum
|
# Eingebunden wird im Mount-Namensraum des aufrufenden Dienstes -- absichtlich:
|
||||||
# (ProtectSystem, PrivateTmp, ReadWritePaths). Ein Einbinden *darin*
|
# nur dieser Dienst liest die Dateien, und mit ihm verschwinden die
|
||||||
# scheitert mit "mount error(1): Operation not permitted" -- gemessen: mit
|
# Einbindungen wieder. Damit der Syscall durchkommt, braucht die Unit
|
||||||
# denselben Optionen und denselben Zugangsdaten gelingt es aus einer
|
# "@mount" im SystemCallFilter. Ohne das scheitert es mit
|
||||||
# normalen Root-Shell sofort. Deshalb wird im Namensraum des Wirts
|
# "mount error(1): Operation not permitted"; gemessen war der Syscall-Filter
|
||||||
# eingebunden; von dort wird der Mount in den Namensraum des Dienstes
|
# die Ursache, nicht der Namensraum. Ein Umweg ueber nsenter in den
|
||||||
# weitergegeben (er haengt als "slave" darunter), die Anwendung sieht ihn
|
# Namensraum des Wirts ist keine Option -- "RestrictNamespaces=yes"
|
||||||
# also. Die Zugangsdatei unter /run ist ueber die Grenze lesbar, /run ist
|
# verbietet setns, und das Recht dafuer wieder aufzugeben waere teurer als
|
||||||
# nicht privatisiert.
|
# der Gewinn.
|
||||||
im_wirt=(nsenter --mount=/proc/1/ns/mnt --)
|
mkdir -p "$target"
|
||||||
command -v nsenter >/dev/null 2>&1 || im_wirt=()
|
if mount -t cifs "$share" "$target" \
|
||||||
"${im_wirt[@]}" mkdir -p "$target"
|
|
||||||
if "${im_wirt[@]}" mount -t cifs "$share" "$target" \
|
|
||||||
-o "credentials=$creds,uid=$(id -u tesm 2>/dev/null || echo 0),gid=$(id -g tesm 2>/dev/null || echo 0),file_mode=0640,dir_mode=0750,vers=3.1.1,seal"
|
-o "credentials=$creds,uid=$(id -u tesm 2>/dev/null || echo 0),gid=$(id -g tesm 2>/dev/null || echo 0),file_mode=0640,dir_mode=0750,vers=3.1.1,seal"
|
||||||
then
|
then
|
||||||
shred -u "$creds" 2>/dev/null || rm -f "$creds"
|
shred -u "$creds" 2>/dev/null || rm -f "$creds"
|
||||||
@@ -282,13 +280,12 @@ case "$verb" in
|
|||||||
|
|
||||||
umount)
|
umount)
|
||||||
check_mount_target "${1:?Ziel fehlt}"
|
check_mount_target "${1:?Ziel fehlt}"
|
||||||
# Gegenstueck zu mount-cifs: der Mount liegt im Namensraum des Wirts, also
|
# Gegenstueck zu mount-cifs, und deshalb im selben Namensraum: loesen kann
|
||||||
# muss er auch dort geloest werden. Ein umount im Dienst-Namensraum wuerde
|
# nur, wer die Einbindung auch sieht. Ein Aufruf aus einem anderen Dienst
|
||||||
# ihn nur dort ausblenden und die Freigabe bliebe eingebunden.
|
# greift ins Leere -- genau deshalb laeuft die Aufraeumaufgabe fuer
|
||||||
im_wirt=(nsenter --mount=/proc/1/ns/mnt --)
|
# abgelaufene Einbindungen im Webprozess und nicht im Ueberwachungsdienst.
|
||||||
command -v nsenter >/dev/null 2>&1 || im_wirt=()
|
umount -l "$1" || true
|
||||||
"${im_wirt[@]}" umount -l "$1" || true
|
rmdir "$1" 2>/dev/null || true
|
||||||
"${im_wirt[@]}" rmdir "$1" 2>/dev/null || true
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
apt-install)
|
apt-install)
|
||||||
@@ -296,9 +293,27 @@ case "$verb" in
|
|||||||
for package in "$@"; do
|
for package in "$@"; do
|
||||||
check_in_list "$package" "${PACKAGES_ALLOWED[@]}" || fail "Paket nicht erlaubt: $package"
|
check_in_list "$package" "${PACKAGES_ALLOWED[@]}" || fail "Paket nicht erlaubt: $package"
|
||||||
done
|
done
|
||||||
|
# apt laeuft ausserhalb des Sandkastens des aufrufenden Dienstes. Der wird
|
||||||
|
# sonst an dpkg und dessen Maintainer-Skripte weitervererbt, und dort
|
||||||
|
# scheitert etwas voellig Beilaeufiges: mit "ProtectKernelLogs=yes" gibt
|
||||||
|
# "dmesg" ein EPERM zurueck, ein Postinst-Skript bricht daran ab, und dpkg
|
||||||
|
# endet mit Code 1 -- die Installation schlaegt fehl, obwohl mit dem Paket
|
||||||
|
# alles in Ordnung ist. Gemessen und mit systemd-run reproduzierbar
|
||||||
|
# behoben: PID 1 startet den Prozess, also ohne unsere Haertung.
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -qq
|
if command -v systemd-run >/dev/null 2>&1 && [[ -S /run/dbus/system_bus_socket ]]; then
|
||||||
apt-get install -y -qq "$@"
|
# Zwei getrennte Aufrufe, damit keine Shell dazwischen steht: die
|
||||||
|
# Paketnamen gehen als eigene Argumente an apt-get, nicht durch eine
|
||||||
|
# Kommandozeile, die sie erst wieder zerlegen muesste.
|
||||||
|
ausserhalb=(systemd-run --wait --pipe --collect --quiet
|
||||||
|
--setenv=DEBIAN_FRONTEND=noninteractive)
|
||||||
|
"${ausserhalb[@]}" apt-get update -qq
|
||||||
|
"${ausserhalb[@]}" apt-get install -y -qq "$@"
|
||||||
|
else
|
||||||
|
# Kein systemd (Container, Entwicklungsrechner): direkt, wie bisher.
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq "$@"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
certbot-issue)
|
certbot-issue)
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user