Files
MrBlake eb6be0e5fa
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
Fix host agent socket activation
2026-08-24 23:13:10 +02:00

536 lines
20 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
readonly REPOSITORY_BASE="https://git.mrblake.cc/MrBlake/Netbox-Store"
readonly CONFIG_BEGIN="# BEGIN NETBOX PLUGIN STORE (managed by install.sh)"
readonly CONFIG_END="# END NETBOX PLUGIN STORE (managed by install.sh)"
info() {
printf '\n\033[1;36m==> %s\033[0m\n' "$*"
}
warn() {
printf '\n\033[1;33mWARNUNG: %s\033[0m\n' "$*" >&2
}
die() {
printf '\n\033[1;31mFEHLER: %s\033[0m\n' "$*" >&2
exit 1
}
prompt_default() {
local prompt="$1"
local default="$2"
local answer
read -r -p "${prompt} [${default}]: " answer
printf '%s' "${answer:-$default}"
}
confirm() {
local prompt="$1"
local answer
read -r -p "${prompt} [j/N]: " answer
case "${answer,,}" in
j|ja|y|yes) return 0 ;;
*) return 1 ;;
esac
}
require_root() {
[[ "${EUID}" -eq 0 ]] || die "Bitte mit sudo ausführen: sudo bash install.sh"
[[ "$(uname -s)" == "Linux" ]] || die "Der Host-Agent wird ausschließlich unter Linux unterstützt."
[[ -t 0 ]] || die "Der Installer benötigt für die Sicherheitsabfragen ein interaktives Terminal."
}
validate_path() {
[[ "$1" =~ ^/[A-Za-z0-9._/-]+$ ]] || die "Ungültiger absoluter Pfad: $1"
}
validate_account() {
[[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_-]*\$?$ ]] || die "Ungültiges Dienstkonto: $1"
id "$1" >/dev/null 2>&1 || die "Das Dienstkonto '$1' existiert nicht."
}
ensure_os_tools() {
local missing=()
local command_name
for command_name in curl python3 systemctl unshare setpriv runuser; do
command -v "$command_name" >/dev/null 2>&1 || missing+=("$command_name")
done
if (( ${#missing[@]} == 0 )); then
return
fi
command -v apt-get >/dev/null 2>&1 \
|| die "Fehlende Werkzeuge (${missing[*]}) und kein apt-get verfügbar."
warn "Fehlende Systemwerkzeuge werden jetzt über apt installiert: ${missing[*]}"
apt-get update
apt-get install -y python3-venv util-linux curl
}
download() {
local url="$1"
local destination="$2"
curl --fail --silent --show-error --location \
--proto '=https' --tlsv1.2 \
"$url" --output "$destination"
}
backup_file() {
local path="$1"
if [[ -e "$path" ]]; then
cp -a -- "$path" "$BACKUP_DIR/$(basename "$path")"
fi
}
detect_netbox_version() {
local detected=""
detected=$("$NETBOX_PYTHON" "$MANAGE_PATH" shell -c \
'from django.conf import settings; print(settings.VERSION)' 2>/dev/null | tail -n 1 || true)
if [[ "$detected" =~ ^4\.6\.[5-8]$ ]]; then
printf '%s' "$detected"
else
printf '%s' "4.6.8"
fi
}
resolve_repository_commit() {
"$NETBOX_PYTHON" - <<'PY'
import json
import re
from urllib.request import Request, urlopen
request = Request(
"https://git.mrblake.cc/api/v1/repos/MrBlake/Netbox-Store/branches/main",
headers={"Accept": "application/json", "User-Agent": "netbox-store-installer/1"},
)
with urlopen(request, timeout=15) as response:
payload = json.load(response)
commit = str(payload.get("commit", {}).get("id", "")).lower()
if not re.fullmatch(r"[a-f0-9]{40}", commit):
raise SystemExit("Der main-Commit des Installationsrepositorys konnte nicht sicher ermittelt werden.")
print(commit)
PY
}
validate_store_url() {
STORE_URL_VALUE="$1" "$NETBOX_PYTHON" - <<'PY'
import os
from urllib.parse import urlsplit
value = os.environ["STORE_URL_VALUE"].rstrip("/")
parsed = urlsplit(value)
if (
parsed.scheme != "https"
or not parsed.hostname
or parsed.username
or parsed.password
or parsed.query
or parsed.fragment
):
raise SystemExit("Die Store-URL muss eine credential-freie HTTPS-URL ohne Query oder Fragment sein.")
print(value)
PY
}
update_requirements() {
REQUIREMENTS_PATH_VALUE="$LOCAL_REQUIREMENTS" \
STORE_REQUIREMENT_VALUE="$STORE_PLUGIN_REQUIREMENT" \
STORE_INCLUDE_VALUE="-r $STORE_REQUIREMENTS" \
"$NETBOX_PYTHON" - <<'PY'
import os
import re
import stat
import tempfile
from pathlib import Path
path = Path(os.environ["REQUIREMENTS_PATH_VALUE"])
requirement = os.environ["STORE_REQUIREMENT_VALUE"]
include = os.environ["STORE_INCLUDE_VALUE"]
text = path.read_text(encoding="utf-8") if path.exists() else ""
lines = [
line for line in text.splitlines()
if not re.match(r"^\s*netbox-plugin-store\s*@", line)
and line.strip() != include
]
lines.extend((requirement, include))
content = "\n".join(lines).rstrip() + "\n"
path.parent.mkdir(parents=True, exist_ok=True)
metadata = path.stat() if path.exists() else None
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
if metadata is not None:
os.chmod(temporary_name, stat.S_IMODE(metadata.st_mode))
if hasattr(os, "chown"):
os.chown(temporary_name, metadata.st_uid, metadata.st_gid)
else:
os.chmod(temporary_name, 0o644)
os.replace(temporary_name, path)
finally:
if os.path.exists(temporary_name):
os.unlink(temporary_name)
PY
}
update_agent_config() {
AGENT_CONFIG_VALUE="$AGENT_CONFIG" \
AGENT_DRY_RUN_VALUE="$AGENT_DRY_RUN" \
NETBOX_UID_VALUE="$NETBOX_UID" \
NETBOX_GID_VALUE="$NETBOX_GID" \
STORE_URL_VALUE="$STORE_URL" \
NETBOX_ROOT_VALUE="$NETBOX_ROOT" \
NETBOX_VERSION_VALUE="$NETBOX_VERSION" \
PYTHON_PATH_VALUE="$NETBOX_PYTHON" \
MANAGE_PATH_VALUE="$MANAGE_PATH" \
SYSTEMCTL_PATH_VALUE="$(command -v systemctl)" \
UNSHARE_PATH_VALUE="$(command -v unshare)" \
SETPRIV_PATH_VALUE="$(command -v setpriv)" \
"$NETBOX_PYTHON" - <<'PY'
import json
import os
import re
from pathlib import Path
from urllib.parse import urlsplit
path = Path(os.environ["AGENT_CONFIG_VALUE"])
store_url = os.environ["STORE_URL_VALUE"].rstrip("/")
store_host = urlsplit(store_url).hostname
allowed_hosts = list(dict.fromkeys((store_host, "git.mrblake.cc", "github.com", "codeload.github.com")))
root = Path(os.environ["NETBOX_ROOT_VALUE"])
updates = {
("agent", "dry_run"): os.environ["AGENT_DRY_RUN_VALUE"],
("agent", "allowed_peer_uids"): f'[{os.environ["NETBOX_UID_VALUE"]}]',
("agent", "allowed_peer_gids"): f'[{os.environ["NETBOX_GID_VALUE"]}]',
("agent", "socket_gid"): os.environ["NETBOX_GID_VALUE"],
("store", "base_url"): json.dumps(store_url),
("store", "allowed_hosts"): json.dumps(allowed_hosts),
("paths", "allowed_root"): json.dumps(str(root)),
("paths", "include_path"): json.dumps(str(root / "netbox/netbox/store_plugins.py")),
("paths", "requirements_path"): json.dumps(str(root / "local_requirements_store.txt")),
("commands", "python_path"): json.dumps(os.environ["PYTHON_PATH_VALUE"]),
("commands", "manage_path"): json.dumps(os.environ["MANAGE_PATH_VALUE"]),
("commands", "systemctl_path"): json.dumps(os.environ["SYSTEMCTL_PATH_VALUE"]),
("commands", "unshare_path"): json.dumps(os.environ["UNSHARE_PATH_VALUE"]),
("commands", "setpriv_path"): json.dumps(os.environ["SETPRIV_PATH_VALUE"]),
("policy", "netbox_version"): json.dumps(os.environ["NETBOX_VERSION_VALUE"]),
}
lines = path.read_text(encoding="utf-8").splitlines()
section = ""
seen = set()
result = []
for line in lines:
match = re.match(r"^\s*\[([A-Za-z0-9_-]+)]\s*$", line)
if match:
section = match.group(1)
key_match = re.match(r"^(\s*)([A-Za-z0-9_]+)\s*=", line)
key = (section, key_match.group(2)) if key_match else None
if key in updates:
result.append(f"{key_match.group(1)}{key[1]} = {updates[key]}")
seen.add(key)
else:
result.append(line)
missing = sorted(set(updates) - seen)
if missing:
raise SystemExit("Agent-Vorlage enthält erwartete Schlüssel nicht: " + ", ".join(f"{s}.{k}" for s, k in missing))
path.write_text("\n".join(result) + "\n", encoding="utf-8")
PY
chmod 0600 "$AGENT_CONFIG"
}
update_netbox_configuration() {
CONFIG_PATH_VALUE="$CONFIGURATION_PATH" \
STORE_URL_VALUE="$STORE_URL" \
EXECUTION_MODE_VALUE="$EXECUTION_MODE" \
DEFAULT_DRY_RUN_VALUE="$DEFAULT_DRY_RUN" \
CONFIG_BEGIN_VALUE="$CONFIG_BEGIN" \
CONFIG_END_VALUE="$CONFIG_END" \
"$NETBOX_PYTHON" - <<'PY'
import ast
import os
import re
import stat
import tempfile
from pathlib import Path
path = Path(os.environ["CONFIG_PATH_VALUE"])
start_marker = os.environ["CONFIG_BEGIN_VALUE"]
end_marker = os.environ["CONFIG_END_VALUE"]
store_url = os.environ["STORE_URL_VALUE"].rstrip("/")
execution_mode = os.environ["EXECUTION_MODE_VALUE"]
default_dry_run = os.environ["DEFAULT_DRY_RUN_VALUE"] == "True"
block = "\n".join([
start_marker,
"from netbox.store_plugins import STORE_PLUGINS",
"",
"for _store_plugin in STORE_PLUGINS:",
" if _store_plugin not in PLUGINS:",
" PLUGINS.append(_store_plugin)",
"",
"PLUGINS_CONFIG = dict(globals().get(\"PLUGINS_CONFIG\", {}))",
"PLUGINS_CONFIG[\"netbox_plugin_store\"] = {",
f" \"store_url\": {store_url!r},",
f" \"allowed_store_urls\": [{store_url!r}],",
" \"allowed_artifact_urls\": [",
f" {store_url!r},",
" \"https://git.mrblake.cc\",",
" \"https://github.com\",",
" \"https://codeload.github.com\",",
" ],",
f" \"execution_mode\": {execution_mode!r},",
" \"agent_socket_path\": \"/run/netbox-store-agent/agent.sock\",",
" \"agent_timeout\": 30,",
f" \"default_dry_run\": {default_dry_run!r},",
"}",
end_marker,
])
original = path.read_text(encoding="utf-8")
start_count = original.count(start_marker)
end_count = original.count(end_marker)
if (start_count, end_count) == (0, 0):
base = original
elif (start_count, end_count) == (1, 1):
before, remainder = original.split(start_marker, 1)
_, after = remainder.split(end_marker, 1)
base = before.rstrip() + "\n" + after.lstrip("\r\n")
else:
raise SystemExit("Der verwaltete configuration.py-Block ist unvollständig oder doppelt vorhanden.")
base = re.sub(
r"(?m)^from store_plugins import STORE_PLUGINS\s*$",
"from netbox.store_plugins import STORE_PLUGINS",
base,
)
tree = ast.parse(base)
assignments = []
for node in tree.body:
if isinstance(node, ast.Assign) and any(
isinstance(target, ast.Name) and target.id == "PLUGINS" for target in node.targets
):
assignments.append(node)
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id == "PLUGINS":
assignments.append(node)
if len(assignments) != 1:
raise SystemExit("configuration.py muss genau eine statische PLUGINS-Zuweisung enthalten.")
assignment = assignments[0]
try:
plugins = ast.literal_eval(assignment.value)
except (ValueError, TypeError, SyntaxError) as exc:
raise SystemExit("PLUGINS muss eine statische Liste oder ein Tupel sein.") from exc
if not isinstance(plugins, (list, tuple)) or any(not isinstance(item, str) for item in plugins):
raise SystemExit("PLUGINS muss ausschließlich Plugin-Importnamen enthalten.")
plugins = list(plugins)
if "netbox_plugin_store" not in plugins:
plugins.append("netbox_plugin_store")
newline = "\r\n" if "\r\n" in base else "\n"
lines = base.splitlines(keepends=True)
if assignment.lineno == assignment.end_lineno and ";" in lines[assignment.lineno - 1]:
raise SystemExit("Die PLUGINS-Zuweisung teilt sich eine Zeile und kann nicht sicher geändert werden.")
replacement = [f"PLUGINS = [{newline}"]
replacement.extend(f" {plugin!r},{newline}" for plugin in plugins)
replacement.append(f"]{newline}")
base = "".join(lines[: assignment.lineno - 1] + replacement + lines[assignment.end_lineno :])
candidate = base.rstrip() + "\n\n" + block + "\n"
compile(candidate, str(path), "exec")
metadata = path.stat()
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
handle.write(candidate)
handle.flush()
os.fsync(handle.fileno())
os.chmod(temporary_name, stat.S_IMODE(metadata.st_mode))
if hasattr(os, "chown"):
os.chown(temporary_name, metadata.st_uid, metadata.st_gid)
os.replace(temporary_name, path)
finally:
if os.path.exists(temporary_name):
os.unlink(temporary_name)
PY
}
require_root
printf '\nNetBox Plugin Store interaktiver Installer\n'
printf '=============================================\n'
printf ' 1) Sicherer Testmodus (keine Systemänderungen durch Lifecycle-Aktionen)\n'
printf ' 2) Produktionsmodus (echte Installationen über den Host-Agent)\n'
printf ' 3) Abbrechen\n\n'
read -r -p "Auswahl [1]: " MODE_CHOICE
MODE_CHOICE="${MODE_CHOICE:-1}"
case "$MODE_CHOICE" in
1)
EXECUTION_MODE="dry_run"
AGENT_DRY_RUN="true"
DEFAULT_DRY_RUN="True"
MODE_LABEL="Sicherer Testmodus"
;;
2)
EXECUTION_MODE="agent"
AGENT_DRY_RUN="false"
DEFAULT_DRY_RUN="False"
MODE_LABEL="Produktionsmodus mit echten Lifecycle-Änderungen"
;;
3)
exit 0
;;
*)
die "Ungültige Menüauswahl."
;;
esac
NETBOX_ROOT=$(prompt_default "NetBox-Installationspfad" "/opt/netbox")
validate_path "$NETBOX_ROOT"
[[ -d "$NETBOX_ROOT" ]] || die "NetBox-Pfad wurde nicht gefunden: $NETBOX_ROOT"
NETBOX_PYTHON="$NETBOX_ROOT/venv/bin/python"
MANAGE_PATH="$NETBOX_ROOT/netbox/manage.py"
CONFIGURATION_PATH="$NETBOX_ROOT/netbox/netbox/configuration.py"
LOCAL_REQUIREMENTS="$NETBOX_ROOT/local_requirements.txt"
STORE_PLUGINS_FILE="$NETBOX_ROOT/netbox/netbox/store_plugins.py"
STORE_REQUIREMENTS="$NETBOX_ROOT/local_requirements_store.txt"
[[ -x "$NETBOX_PYTHON" ]] || die "NetBox-Python wurde nicht gefunden: $NETBOX_PYTHON"
[[ -f "$MANAGE_PATH" ]] || die "manage.py wurde nicht gefunden: $MANAGE_PATH"
[[ -f "$CONFIGURATION_PATH" ]] || die "configuration.py wurde nicht gefunden: $CONFIGURATION_PATH"
DETECTED_USER=$(systemctl show netbox.service --property=User --value 2>/dev/null || true)
DETECTED_USER="${DETECTED_USER:-netbox}"
NETBOX_USER=$(prompt_default "Linux-Dienstkonto von NetBox" "$DETECTED_USER")
validate_account "$NETBOX_USER"
NETBOX_GROUP=$(id -gn "$NETBOX_USER")
NETBOX_UID=$(id -u "$NETBOX_USER")
NETBOX_GID=$(id -g "$NETBOX_USER")
NETBOX_VERSION=$(prompt_default "Installierte NetBox-Version" "$(detect_netbox_version)")
[[ "$NETBOX_VERSION" =~ ^4\.6\.[5-8]$ ]] \
|| die "Unterstützt werden ausschließlich NetBox 4.6.5 bis 4.6.8."
STORE_URL=$(prompt_default "Store-URL" "https://netbox.mrblake.cc")
STORE_URL=$(validate_store_url "$STORE_URL")
SOURCE_COMMIT=$(resolve_repository_commit)
SOURCE_ARCHIVE="${REPOSITORY_BASE}/archive/${SOURCE_COMMIT}.tar.gz"
RAW_BASE="${REPOSITORY_BASE}/raw/commit/${SOURCE_COMMIT}"
STORE_PLUGIN_REQUIREMENT="netbox-plugin-store @ ${SOURCE_ARCHIVE}#subdirectory=netbox_plugin"
AGENT_REQUIREMENT="mrblake-netbox-store-agent @ ${SOURCE_ARCHIVE}#subdirectory=host_agent"
readonly SOURCE_COMMIT SOURCE_ARCHIVE RAW_BASE STORE_PLUGIN_REQUIREMENT AGENT_REQUIREMENT
printf '\nZusammenfassung\n'
printf ' Modus: %s\n' "$MODE_LABEL"
printf ' NetBox: %s (%s)\n' "$NETBOX_ROOT" "$NETBOX_VERSION"
printf ' Dienstkonto: %s (UID %s, GID %s, Gruppe %s)\n' \
"$NETBOX_USER" "$NETBOX_UID" "$NETBOX_GID" "$NETBOX_GROUP"
printf ' Store: %s\n' "$STORE_URL"
printf ' Installer-Commit: %s\n' "$SOURCE_COMMIT"
printf ' Konfiguration: %s\n' "$CONFIGURATION_PATH"
confirm "Installation mit diesen Werten starten?" || exit 0
if [[ "$MODE_CHOICE" == "2" ]]; then
warn "Der Produktionsmodus darf Python-Pakete installieren, NetBox-Dateien ändern und Dienste neu starten."
read -r -p "Zum Fortfahren exakt ECHT INSTALLIEREN eingeben: " PRODUCTION_CONFIRMATION
[[ "$PRODUCTION_CONFIRMATION" == "ECHT INSTALLIEREN" ]] || die "Produktionsfreigabe wurde nicht erteilt."
fi
ensure_os_tools
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)-${BASHPID}"
BACKUP_DIR="/var/backups/netbox-plugin-store/$TIMESTAMP"
mkdir -p "$BACKUP_DIR"
chmod 0700 "$BACKUP_DIR"
backup_file "$CONFIGURATION_PATH"
backup_file "$LOCAL_REQUIREMENTS"
backup_file "/etc/netbox-store-agent/agent.toml"
backup_file "/etc/systemd/system/netbox-store-agent.service"
backup_file "/etc/systemd/system/netbox-store-agent.socket"
info "Backups: $BACKUP_DIR"
TMP_DIR=$(mktemp -d /tmp/netbox-store-install.XXXXXXXX)
cleanup() {
if [[ -n "${TMP_DIR:-}" && "$TMP_DIR" == /tmp/netbox-store-install.* ]]; then
rm -rf -- "$TMP_DIR"
fi
}
trap cleanup EXIT
trap 'warn "Installation in Zeile $LINENO abgebrochen. Backups liegen unter $BACKUP_DIR."' ERR
info "NetBox-Plugin installieren"
"$NETBOX_PYTHON" -m pip install --no-cache-dir --no-deps --upgrade --force-reinstall \
"$STORE_PLUGIN_REQUIREMENT"
update_requirements
info "Host-Agent installieren"
if [[ ! -x /opt/netbox-store-agent/venv/bin/python ]]; then
python3 -m venv /opt/netbox-store-agent/venv
fi
/opt/netbox-store-agent/venv/bin/python -m pip install --upgrade pip
/opt/netbox-store-agent/venv/bin/python -m pip install --no-cache-dir --upgrade --force-reinstall \
"$AGENT_REQUIREMENT"
install -d -o root -g root -m 0750 /etc/netbox-store-agent
AGENT_CONFIG="/etc/netbox-store-agent/agent.toml"
if [[ ! -f "$AGENT_CONFIG" ]]; then
download "$RAW_BASE/host_agent/examples/agent.toml" "$TMP_DIR/agent.toml"
install -o root -g root -m 0600 "$TMP_DIR/agent.toml" "$AGENT_CONFIG"
fi
update_agent_config
download "$RAW_BASE/host_agent/systemd/netbox-store-agent.service" "$TMP_DIR/netbox-store-agent.service"
download "$RAW_BASE/host_agent/systemd/netbox-store-agent.socket" "$TMP_DIR/netbox-store-agent.socket"
sed -i \
-e 's#/usr/local/bin/netbox-store-agent#/opt/netbox-store-agent/venv/bin/netbox-store-agent#' \
-e "s#ReadWritePaths=/opt/netbox #ReadWritePaths=$NETBOX_ROOT #" \
"$TMP_DIR/netbox-store-agent.service"
sed -i "s/^SocketGroup=.*/SocketGroup=$NETBOX_GROUP/" "$TMP_DIR/netbox-store-agent.socket"
install -o root -g root -m 0644 "$TMP_DIR/netbox-store-agent.service" \
/etc/systemd/system/netbox-store-agent.service
install -o root -g root -m 0644 "$TMP_DIR/netbox-store-agent.socket" \
/etc/systemd/system/netbox-store-agent.socket
info "Verwaltete NetBox-Dateien vorbereiten"
if [[ ! -f "$STORE_PLUGINS_FILE" ]]; then
printf 'STORE_PLUGINS = []\n' > "$TMP_DIR/store_plugins.py"
install -o root -g root -m 0644 "$TMP_DIR/store_plugins.py" "$STORE_PLUGINS_FILE"
fi
if [[ ! -f "$STORE_REQUIREMENTS" ]]; then
printf '# Generated by netbox-store-agent. Do not edit.\n' > "$TMP_DIR/local_requirements_store.txt"
install -o root -g root -m 0644 "$TMP_DIR/local_requirements_store.txt" "$STORE_REQUIREMENTS"
fi
chmod 0644 "$STORE_PLUGINS_FILE" "$STORE_REQUIREMENTS"
update_netbox_configuration
info "Konfiguration validieren"
/opt/netbox-store-agent/venv/bin/netbox-store-agent \
--config "$AGENT_CONFIG" validate-config
"$NETBOX_PYTHON" -m py_compile "$CONFIGURATION_PATH" "$STORE_PLUGINS_FILE"
info "Datenbank und statische Dateien aktualisieren"
"$NETBOX_PYTHON" "$MANAGE_PATH" migrate --no-input
"$NETBOX_PYTHON" "$MANAGE_PATH" collectstatic --no-input
info "Dienste aktivieren und neu starten"
systemctl daemon-reload
systemctl enable netbox-store-agent.socket
systemctl stop netbox-store-agent.service
systemctl restart netbox-store-agent.socket
systemctl restart netbox netbox-rq
if [[ ! -S /run/netbox-store-agent/agent.sock ]]; then
systemctl status netbox-store-agent.socket --no-pager -l || true
die "Der Agent-Socket /run/netbox-store-agent/agent.sock wurde nicht erstellt."
fi
info "Socket-Zugriff als NetBox-Dienstkonto prüfen"
runuser -u "$NETBOX_USER" -- \
/opt/netbox-store-agent/venv/bin/netbox-store-agent capabilities
printf '\n\033[1;32mInstallation abgeschlossen.\033[0m\n'
printf 'Backup: %s\n' "$BACKUP_DIR"
if [[ "$MODE_CHOICE" == "1" ]]; then
printf 'Lifecycle-Aktionen bleiben im Dry-Run-Modus und verändern das System nicht.\n'
else
printf 'Echte Lifecycle-Aktionen sind freigeschaltet. Prüfe im Dialog, dass Dry-Run nicht markiert ist.\n'
printf 'Neue Plugins werden zuerst installiert und bleiben deaktiviert; aktiviere sie anschließend separat.\n'
fi