536 lines
20 KiB
Bash
Executable File
536 lines
20 KiB
Bash
Executable File
#!/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
|