Add interactive NetBox host installer
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run

This commit is contained in:
2026-08-24 23:00:28 +02:00
parent a4cecec7b9
commit 541f107e1e
11 changed files with 551 additions and 8 deletions
+3 -1
View File
@@ -8,6 +8,8 @@ Dieses Repository besteht aus drei getrennten Komponenten:
| `netbox_plugin/` | NetBox-Oberfläche für Katalog, Installation und Plugin-Lebenszyklus. Unterstützt NetBox 4.6.5 bis 4.6.8. | | `netbox_plugin/` | NetBox-Oberfläche für Katalog, Installation und Plugin-Lebenszyklus. Unterstützt NetBox 4.6.5 bis 4.6.8. |
| `host_agent/` | Kleine privilegierte Linux-Komponente, die freigegebene Artefakte prüft, Source-Archive lokal als Wheel baut und die Änderungen am NetBox-Host ausführt. | | `host_agent/` | Kleine privilegierte Linux-Komponente, die freigegebene Artefakte prüft, Source-Archive lokal als Wheel baut und die Änderungen am NetBox-Host ausführt. |
Für NetBox-Hosts steht zusätzlich [`install.sh`](install.sh) bereit. Der interaktive Installer kann Plugin, Host-Agent, systemd-Socket, Requirements und den verwalteten Block in `configuration.py` in einem Durchlauf einrichten. Er startet standardmäßig im Dry-Run-Modus, legt vor Änderungen Backups an und verlangt für echte Lifecycle-Änderungen eine zusätzliche ausdrückliche Bestätigung.
Der Store verwendet standardmäßig eine lokale JSON-Datei. MariaDB ist optional. Die konfigurierte Standardquelle gilt als vom Betreiber freigegeben; weitere Quellen werden im Admin-Bereich zunächst ausstehend angelegt. Automatisch erkannte Plugins und Releases erscheinen erst nach einer ausdrücklichen Freigabe im öffentlichen Katalog. Der Store verwendet standardmäßig eine lokale JSON-Datei. MariaDB ist optional. Die konfigurierte Standardquelle gilt als vom Betreiber freigegeben; weitere Quellen werden im Admin-Bereich zunächst ausstehend angelegt. Automatisch erkannte Plugins und Releases erscheinen erst nach einer ausdrücklichen Freigabe im öffentlichen Katalog.
## Native Installation mit Apache und JSON ## Native Installation mit Apache und JSON
@@ -269,7 +271,7 @@ Kurzablauf auf dem Linux-NetBox-Host:
4. Die vom Agent verwaltete Plugin-Liste einmalig in `configuration.py` einbinden: 4. Die vom Agent verwaltete Plugin-Liste einmalig in `configuration.py` einbinden:
```python ```python
from store_plugins import STORE_PLUGINS from netbox.store_plugins import STORE_PLUGINS
PLUGINS += STORE_PLUGINS PLUGINS += STORE_PLUGINS
``` ```
+1 -1
View File
@@ -32,7 +32,7 @@ The agent owns only these two configured files:
In the operator-owned NetBox `configuration.py`, add once, after the normal `PLUGINS` declaration: In the operator-owned NetBox `configuration.py`, add once, after the normal `PLUGINS` declaration:
```python ```python
from store_plugins import STORE_PLUGINS from netbox.store_plugins import STORE_PLUGINS
PLUGINS += STORE_PLUGINS PLUGINS += STORE_PLUGINS
``` ```
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "mrblake-netbox-store-agent" name = "mrblake-netbox-store-agent"
version = "0.1.1" version = "0.1.2"
description = "Fail-closed host agent for curated NetBox plugin lifecycle operations" description = "Fail-closed host agent for curated NetBox plugin lifecycle operations"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@@ -260,7 +260,11 @@ class OperationProcessor:
temp_root = self.config.paths.temp_dir temp_root = self.config.paths.temp_dir
if temp_root.is_symlink(): if temp_root.is_symlink():
raise PolicyError("temporary directory may not be a symlink") raise PolicyError("temporary directory may not be a symlink")
temp_root.mkdir(parents=True, exist_ok=True, mode=0o700) temp_root.mkdir(parents=True, exist_ok=True, mode=0o711)
# The dropped-privilege source builder needs traversal to its own
# 0700 operation directory. It cannot list this root, the journal,
# or the separately protected backup directory.
os.chmod(temp_root, 0o711)
with tempfile.TemporaryDirectory(prefix="operation-", dir=temp_root) as temporary: with tempfile.TemporaryDirectory(prefix="operation-", dir=temp_root) as temporary:
self._event(operation_id, "artifact", "Downloading and verifying approved artifact") self._event(operation_id, "artifact", "Downloading and verifying approved artifact")
artifact = self.store.download_release(plan, Path(temporary)) artifact = self.store.download_release(plan, Path(temporary))
@@ -10,9 +10,9 @@ User=root
Group=root Group=root
ExecStart=/usr/local/bin/netbox-store-agent --config /etc/netbox-store-agent/agent.toml daemon ExecStart=/usr/local/bin/netbox-store-agent --config /etc/netbox-store-agent/agent.toml daemon
StateDirectory=netbox-store-agent StateDirectory=netbox-store-agent
StateDirectoryMode=0700 StateDirectoryMode=0711
RuntimeDirectory=netbox-store-agent RuntimeDirectory=netbox-store-agent
RuntimeDirectoryMode=0750 RuntimeDirectoryMode=0755
UMask=0077 UMask=0077
NoNewPrivileges=true NoNewPrivileges=true
PrivateTmp=true PrivateTmp=true
+1 -1
View File
@@ -7,7 +7,7 @@ SocketMode=0660
SocketUser=root SocketUser=root
# Replace with the group of the NetBox service. # Replace with the group of the NetBox service.
SocketGroup=netbox SocketGroup=netbox
DirectoryMode=0750 DirectoryMode=0755
RemoveOnStop=true RemoveOnStop=true
[Install] [Install]
+4
View File
@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os
import stat
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -56,6 +58,8 @@ class ExecutorTests(unittest.TestCase):
self.assertEqual(runner.commands, []) self.assertEqual(runner.commands, [])
self.assertIsNone(journal.get_managed_plugin("demo-plugin")) self.assertIsNone(journal.get_managed_plugin("demo-plugin"))
self.assertFalse(config.paths.include_path.exists()) self.assertFalse(config.paths.include_path.exists())
if os.name == "posix":
self.assertEqual(stat.S_IMODE(config.paths.temp_dir.stat().st_mode), 0o711)
def test_install_is_disabled_and_uses_fixed_pip_argv(self) -> None: def test_install_is_disabled_and_uses_fixed_pip_argv(self) -> None:
config = make_config(self.root, dry_run=False) config = make_config(self.root, dry_run=False)
+22
View File
@@ -0,0 +1,22 @@
import unittest
from pathlib import Path
class SystemdUnitTests(unittest.TestCase):
def test_socket_directory_is_traversable_but_socket_remains_restricted(self):
socket_unit = Path("systemd/netbox-store-agent.socket").read_text(encoding="utf-8")
self.assertIn("DirectoryMode=0755", socket_unit)
self.assertIn("SocketMode=0660", socket_unit)
self.assertIn("SocketGroup=netbox", socket_unit)
def test_state_traversal_does_not_relax_service_umask(self):
service_unit = Path("systemd/netbox-store-agent.service").read_text(encoding="utf-8")
self.assertIn("StateDirectoryMode=0711", service_unit)
self.assertIn("RuntimeDirectoryMode=0755", service_unit)
self.assertIn("UMask=0077", service_unit)
if __name__ == "__main__":
unittest.main()
Executable
+502
View File
@@ -0,0 +1,502 @@
#!/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 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",
"",
"PLUGINS = list(globals().get(\"PLUGINS\", []))",
"if \"netbox_plugin_store\" not in PLUGINS:",
" PLUGINS.append(\"netbox_plugin_store\")",
"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")
original = re.sub(
r"(?m)^from store_plugins import STORE_PLUGINS\s*$",
"from netbox.store_plugins import STORE_PLUGINS",
original,
)
start_count = original.count(start_marker)
end_count = original.count(end_marker)
if (start_count, end_count) == (0, 0):
candidate = original.rstrip() + "\n\n" + block + "\n"
elif (start_count, end_count) == (1, 1):
before, remainder = original.split(start_marker, 1)
_, after = remainder.split(end_marker, 1)
candidate = before.rstrip() + "\n\n" + block + after.rstrip() + "\n"
else:
raise SystemExit("Der verwaltete configuration.py-Block ist unvollständig oder doppelt vorhanden.")
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 restart netbox-store-agent.socket
systemctl restart netbox-store-agent.service
systemctl restart netbox netbox-rq
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
+8 -1
View File
@@ -8,6 +8,13 @@
</section> </section>
<section class="shell prose-shell install-guide"> <section class="shell prose-shell install-guide">
<aside class="notice"><strong>Vorher sichern:</strong> Erstelle ein Backup von NetBox und der Datenbank. Führe die Befehle auf dem NetBox-Host aus.</aside> <aside class="notice"><strong>Vorher sichern:</strong> Erstelle ein Backup von NetBox und der Datenbank. Führe die Befehle auf dem NetBox-Host aus.</aside>
<h2>Interaktive Komplettinstallation</h2>
<p>Der Installer richtet NetBox-Plugin, Host-Agent, systemd-Socket, Requirements und den markierten Konfigurationsblock gemeinsam ein. Er fragt Pfade, Dienstkonto, NetBox-Version und Betriebsmodus ab, zeigt vorab eine Zusammenfassung und erstellt Sicherungskopien. Lade ihn zuerst herunter und prüfe seinen Inhalt; führe keinen ungeprüften Remote-Code direkt über eine Pipe als Root aus.</p>
<pre><code>curl -fsSLo /tmp/netbox-store-install.sh \
https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/install.sh
less /tmp/netbox-store-install.sh
sudo bash /tmp/netbox-store-install.sh</code></pre>
<p>Wähle für die erste Funktionsprüfung den sicheren Testmodus. Der Produktionsmodus verlangt zusätzlich die Eingabe <code>ECHT INSTALLIEREN</code>. Der folgende Abschnitt dokumentiert dieselben Schritte für eine manuelle Installation.</p>
<h2>1. Plugin aus git.mrblake.cc installieren</h2> <h2>1. Plugin aus git.mrblake.cc installieren</h2>
<p>Trage das Plugin dauerhaft in <code>/opt/netbox/local_requirements.txt</code> ein. Die empfohlene HTTPS-Archivvariante benötigt kein lokal installiertes Git. Für reproduzierbare Installationen solltest du <code>main</code> durch einen geprüften Commit-SHA ersetzen.</p> <p>Trage das Plugin dauerhaft in <code>/opt/netbox/local_requirements.txt</code> ein. Die empfohlene HTTPS-Archivvariante benötigt kein lokal installiertes Git. Für reproduzierbare Installationen solltest du <code>main</code> durch einen geprüften Commit-SHA ersetzen.</p>
<pre><code>sudo sed -i '/^[[:space:]]*netbox-plugin-store[[:space:]]*@/d' /opt/netbox/local_requirements.txt <pre><code>sudo sed -i '/^[[:space:]]*netbox-plugin-store[[:space:]]*@/d' /opt/netbox/local_requirements.txt
@@ -94,7 +101,7 @@ sudo chmod 0644 /opt/netbox/netbox/netbox/store_plugins.py /opt/netbox/local_req
grep -qxF -- '-r /opt/netbox/local_requirements_store.txt' /opt/netbox/local_requirements.txt \ grep -qxF -- '-r /opt/netbox/local_requirements_store.txt' /opt/netbox/local_requirements.txt \
|| echo '-r /opt/netbox/local_requirements_store.txt' | sudo tee -a /opt/netbox/local_requirements.txt</code></pre> || echo '-r /opt/netbox/local_requirements_store.txt' | sudo tee -a /opt/netbox/local_requirements.txt</code></pre>
<p>Füge in <code>configuration.py</code> direkt nach der vorhandenen <code>PLUGINS</code>-Liste einmalig Folgendes ein:</p> <p>Füge in <code>configuration.py</code> direkt nach der vorhandenen <code>PLUGINS</code>-Liste einmalig Folgendes ein:</p>
<pre><code>from store_plugins import STORE_PLUGINS <pre><code>from netbox.store_plugins import STORE_PLUGINS
PLUGINS += STORE_PLUGINS</code></pre> PLUGINS += STORE_PLUGINS</code></pre>
<p>Prüfe zunächst die Agent-Konfiguration und den Socket im sicheren Dry-Run-Modus:</p> <p>Prüfe zunächst die Agent-Konfiguration und den Socket im sicheren Dry-Run-Modus:</p>
+2
View File
@@ -492,6 +492,8 @@ test('public and admin templates render safely with complete artifact evidence',
assertTrue(str_contains($detail, 'Wheel')); assertTrue(str_contains($detail, 'Wheel'));
$installation = $view->render('installation', $common + ['title' => 'Installation']); $installation = $view->render('installation', $common + ['title' => 'Installation']);
assertTrue(str_contains($installation, 'https://git.mrblake.cc/MrBlake/Netbox-Store/archive/main.tar.gz')); assertTrue(str_contains($installation, 'https://git.mrblake.cc/MrBlake/Netbox-Store/archive/main.tar.gz'));
assertTrue(str_contains($installation, 'raw/branch/main/install.sh'));
assertTrue(str_contains($installation, 'ECHT INSTALLIEREN'));
assertTrue(str_contains($installation, "sed -i '/^[[:space:]]*netbox-plugin-store")); assertTrue(str_contains($installation, "sed -i '/^[[:space:]]*netbox-plugin-store"));
assertTrue(str_contains($installation, 'sudo apt install -y git')); assertTrue(str_contains($installation, 'sudo apt install -y git'));
assertTrue(str_contains($installation, 'https://netbox.mrblake.cc')); assertTrue(str_contains($installation, 'https://netbox.mrblake.cc'));