diff --git a/README.md b/README.md index f10867b..21c7da2 100644 --- a/README.md +++ b/README.md @@ -227,14 +227,34 @@ Der normale Ablauf ist: Die Detailseite wird aus der gespeicherten README erzeugt. Relative Links und Bilder werden auf den synchronisierten Commit aufgelöst; HTML wird vor der Ausgabe bereinigt. Verändert sich ein Artifact oder ein installrelevantes Metadatum, muss das Release erneut geprüft werden. -## NetBox-Plugin installieren +## NetBox-Host installieren + +Der empfohlene Weg ist der interaktive Root-Installer. Er installiert das NetBox-Plugin und den separaten Host-Agent, erkennt Dienstkonto sowie NetBox-Version, richtet Requirements und systemd ein und verwaltet einen klar markierten Block in `configuration.py`. + +```text +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 +``` + +Das Script bietet zwei Modi: + +- **Sicherer Testmodus:** Lifecycle-Aufträge bleiben Dry-Runs und verändern keine Plugins. +- **Produktionsmodus:** Echte Änderungen laufen ausschließlich über den Host-Agent. Zusätzlich zur Zusammenfassung muss der Betreiber exakt `ECHT INSTALLIEREN` bestätigen. + +Alle während eines Laufs geladenen Komponenten werden auf denselben angezeigten Commit gepinnt. Vorhandene Dateien werden unter `/var/backups/netbox-plugin-store//` gesichert. Das Script kann für Updates erneut ausgeführt werden: Es ersetzt nur seinen markierten Konfigurationsblock und seine eigenen Requirement-Einträge. + +Nach der Einrichtung prüft der Installer den Agent-Socket als NetBox-Dienstkonto. Eine echte Plugin-Installation muss im Audit `Dry-Run: Nein` zeigen. Neu installierte Plugins bleiben zunächst deaktiviert und werden anschließend über eine separate Aktion aktiviert. + +### Manuelle Plugin-Installation Das Plugin unter `netbox_plugin/` ist auf NetBox 4.6.5–4.6.8 begrenzt. Zuerst wird ein Wheel gebaut und in die NetBox-Virtualenv installiert: ```text cd netbox_plugin python -m build -/opt/netbox/venv/bin/pip install dist/netbox_plugin_store-0.1.2-py3-none-any.whl +/opt/netbox/venv/bin/pip install dist/netbox_plugin_store-0.1.3-py3-none-any.whl ``` Das Paket muss außerdem in `/opt/netbox/local_requirements.txt` festgehalten werden. In `configuration.py` wird es zunächst sicher im Dry-run-Modus eingerichtet: @@ -259,7 +279,7 @@ PLUGINS_CONFIG = { Danach wird der übliche NetBox-Upgrade-Ablauf mit Migration, `collectstatic` und Neustart von Web- und RQ-Dienst ausgeführt. Details und alle Einstellungen stehen in [`netbox_plugin/README.md`](netbox_plugin/README.md). -## Produktion: Host-Agent +### Manuelle Produktionseinrichtung: Host-Agent Für echte Installationen ist `execution_mode = "agent"` die vorgesehene Trennung: Der NetBox-Webprozess bleibt unprivilegiert und sendet einen kleinen Auftrag über den Unix-Socket `/run/netbox-store-agent/agent.sock`. Der Agent ruft Plugin und Release erneut aus dem Store ab, vergleicht die Freigabemarkierung und prüft Wheel, Dateigröße und SHA-256 vor jeder Änderung. diff --git a/install.sh b/install.sh index 0d646e4..8e2f5ed 100755 --- a/install.sh +++ b/install.sh @@ -250,6 +250,7 @@ update_netbox_configuration() { CONFIG_BEGIN_VALUE="$CONFIG_BEGIN" \ CONFIG_END_VALUE="$CONFIG_END" \ "$NETBOX_PYTHON" - <<'PY' +import ast import os import re import stat @@ -266,9 +267,6 @@ 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)", @@ -291,21 +289,51 @@ block = "\n".join([ 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" + base = original 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" + 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) diff --git a/netbox_plugin/README.md b/netbox_plugin/README.md index 970cab6..1010161 100644 --- a/netbox_plugin/README.md +++ b/netbox_plugin/README.md @@ -22,7 +22,7 @@ The safe default is intentionally non-mutating. Installing the UI alone never gr Install the wheel into NetBox's virtual environment and persist it in `/opt/netbox/local_requirements.txt`: ```text -netbox-plugin-store==0.1.2 +netbox-plugin-store==0.1.3 ``` Add the plugin to `configuration.py`: diff --git a/netbox_plugin/netbox_plugin_store/lifecycle.py b/netbox_plugin/netbox_plugin_store/lifecycle.py index 69cf7cc..4407bd9 100644 --- a/netbox_plugin/netbox_plugin_store/lifecycle.py +++ b/netbox_plugin/netbox_plugin_store/lifecycle.py @@ -156,18 +156,23 @@ class LifecycleService: plugin = self.client.get_plugin(request.slug) ensure_not_self(plugin.package_name, plugin.import_name) installed = self.version_provider(plugin.package_name) - config_editor = PluginConfigurationEditor( - self.settings.configuration_path, - self.settings.backup_dir, - self.settings.backups_to_keep, - ) - requirements_editor = RequirementsEditor( - self.settings.requirements_path, - self.settings.backup_dir, - self.settings.backups_to_keep, - ) - enabled = plugin.import_name in config_editor.enabled_plugins() runtime_active = self.runtime_active_provider(plugin.import_name) + config_editor: PluginConfigurationEditor | None = None + requirements_editor: RequirementsEditor | None = None + if self.settings.execution_mode == "agent": + enabled = runtime_active + else: + config_editor = PluginConfigurationEditor( + self.settings.configuration_path, + self.settings.backup_dir, + self.settings.backups_to_keep, + ) + requirements_editor = RequirementsEditor( + self.settings.requirements_path, + self.settings.backup_dir, + self.settings.backups_to_keep, + ) + enabled = plugin.import_name in config_editor.enabled_plugins() release: Release | None = None if request.action in {"install", "update"}: release = plugin.select_release(self.settings.netbox_version, request.version) @@ -200,10 +205,20 @@ class LifecycleService: ) try: if self.settings.execution_mode == "agent": - result = self._execute_agent(request, plugin, release, installed, enabled, plan, requested_by) + result = self._execute_agent( + request, plugin, release, installed, enabled, plan, requested_by + ) else: + assert config_editor is not None and requirements_editor is not None result = self._execute_direct( - request, plugin, release, installed, enabled, plan, config_editor, requirements_editor + request, + plugin, + release, + installed, + enabled, + plan, + config_editor, + requirements_editor, ) except Exception as exc: self.repository.update_status( @@ -268,9 +283,19 @@ class LifecycleService: elif action == "update": plan.extend(["Update the persistent requirement pin atomically.", "Upgrade from the verified local artifact."]) elif action == "enable": - plan.append(f"Add {plugin.import_name} to the static PLUGINS list atomically.") + target = ( + "agent-managed plugin list" + if self.settings.execution_mode == "agent" + else "static PLUGINS list" + ) + plan.append(f"Add {plugin.import_name} to the {target} atomically.") elif action == "disable": - plan.append(f"Remove {plugin.import_name} from the static PLUGINS list atomically.") + target = ( + "agent-managed plugin list" + if self.settings.execution_mode == "agent" + else "static PLUGINS list" + ) + plan.append(f"Remove {plugin.import_name} from the {target} atomically.") elif action == "uninstall": plan.extend(["Remove the persistent requirement pin atomically.", "Uninstall the distribution with pip."]) if action == "enable" or (action == "update" and currently_enabled): diff --git a/netbox_plugin/netbox_plugin_store/version.py b/netbox_plugin/netbox_plugin_store/version.py index b3f4756..ae73625 100644 --- a/netbox_plugin/netbox_plugin_store/version.py +++ b/netbox_plugin/netbox_plugin_store/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/netbox_plugin/pyproject.toml b/netbox_plugin/pyproject.toml index 7421652..a82da54 100644 --- a/netbox_plugin/pyproject.toml +++ b/netbox_plugin/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netbox-plugin-store" -version = "0.1.2" +version = "0.1.3" description = "A secure NetBox 4.6 plugin lifecycle client for the MrBlake Plugin Store" readme = "README.md" requires-python = ">=3.12" diff --git a/netbox_plugin/tests/test_lifecycle.py b/netbox_plugin/tests/test_lifecycle.py index 4dc3eaf..a0ba63f 100644 --- a/netbox_plugin/tests/test_lifecycle.py +++ b/netbox_plugin/tests/test_lifecycle.py @@ -93,22 +93,25 @@ class FakeRunner: def runtime(root: Path, *, execution_mode="direct", allow=True): config = root / "configuration.py" config.write_text("PLUGINS = ['netbox_plugin_store']\n", encoding="utf-8") + values = { + "store_url": "https://store.example", + "allowed_store_urls": ["https://store.example"], + "allowed_artifact_urls": ["https://store.example"], + "configuration_path": str(config), + "requirements_path": str(root / "local_requirements.txt"), + "manage_path": str(root / "manage.py"), + "lock_path": str(root / "operation.lock"), + "backup_dir": str(root / "backups"), + "execution_mode": execution_mode, + "allow_lifecycle_mutations": allow, + "run_migrations": False, + "collect_static": False, + "allow_package_index": False, + } + if execution_mode == "agent": + values["agent_socket_path"] = str(root / "agent.sock") return RuntimeSettings.from_mapping( - { - "store_url": "https://store.example", - "allowed_store_urls": ["https://store.example"], - "allowed_artifact_urls": ["https://store.example"], - "configuration_path": str(config), - "requirements_path": str(root / "local_requirements.txt"), - "manage_path": str(root / "manage.py"), - "lock_path": str(root / "operation.lock"), - "backup_dir": str(root / "backups"), - "execution_mode": execution_mode, - "allow_lifecycle_mutations": allow, - "run_migrations": False, - "collect_static": False, - "allow_package_index": False, - }, + values, configuration_dir=root, netbox_root=root, base_dir=root, @@ -117,6 +120,27 @@ def runtime(root: Path, *, execution_mode="direct", allow=True): class LifecycleTests(unittest.TestCase): + def test_agent_mode_does_not_parse_agent_managed_plugins_from_configuration(self): + with tempfile.TemporaryDirectory() as temp_name: + root = Path(temp_name) + settings = runtime(root, execution_mode="agent") + settings.configuration_path.write_text( + "PLUGINS = ['netbox_plugin_store']\nPLUGINS = list(PLUGINS)\n", + encoding="utf-8", + ) + service = LifecycleService( + settings, + FakeClient(catalog_plugin()), + FakeRepository(), + runner=FakeRunner(), + version_provider=lambda _: "", + runtime_active_provider=lambda _: False, + ) + + result = service.execute(LifecycleRequest("example-plugin", "install", "1.0.0", True)) + + self.assertEqual(result.state, "dry-run") + def test_dry_run_has_no_download_or_subprocess(self): with tempfile.TemporaryDirectory() as temp_name: root = Path(temp_name) diff --git a/store/templates/installation.php b/store/templates/installation.php index b6d6163..932fa73 100644 --- a/store/templates/installation.php +++ b/store/templates/installation.php @@ -2,126 +2,104 @@

NetBox 4.6.5–4.6.8

-

NetBox Store Plugin installieren

-

Das Plugin verbindet deine NetBox mit https://netbox.mrblake.cc. Docker ist nicht erforderlich.

+

NetBox Plugin Store installieren

+

Der interaktive Installer richtet Plugin, Host-Agent und NetBox-Konfiguration gemeinsam ein. Docker und ein lokales Git sind nicht erforderlich.

+
- -

Interaktive Komplettinstallation

-

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.

+

Voraussetzungen

+ + + +

1. Installer herunterladen und prüfen

+

Lade das Script zuerst als Datei herunter. Verwende keine direkte curl | sudo bash-Pipe, damit du den ausgeführten Inhalt vorher kontrollieren kannst.

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
-

Wähle für die erste Funktionsprüfung den sicheren Testmodus. Der Produktionsmodus verlangt zusätzlich die Eingabe ECHT INSTALLIEREN. Der folgende Abschnitt dokumentiert dieselben Schritte für eine manuelle Installation.

-

1. Plugin aus git.mrblake.cc installieren

-

Trage das Plugin dauerhaft in /opt/netbox/local_requirements.txt ein. Die empfohlene HTTPS-Archivvariante benötigt kein lokal installiertes Git. Für reproduzierbare Installationen solltest du main durch einen geprüften Commit-SHA ersetzen.

-
sudo sed -i '/^[[:space:]]*netbox-plugin-store[[:space:]]*@/d' /opt/netbox/local_requirements.txt
-echo 'netbox-plugin-store @ https://git.mrblake.cc/MrBlake/Netbox-Store/archive/main.tar.gz#subdirectory=netbox_plugin' | sudo tee -a /opt/netbox/local_requirements.txt
-sudo /opt/netbox/upgrade.sh
-

Der erste Befehl entfernt ausschließlich ältere Bezugsquellen dieses Plugins. Dadurch bleibt auch nach einem Wechsel zwischen Git- und Archivinstallation genau ein Eintrag vorhanden.

-

Alternative mit Git

-

Wenn du stattdessen eine git+https-Requirement verwendest, muss das Programm git vor dem NetBox-Upgrade installiert sein:

-
sudo apt update
-sudo apt install -y git
+  

Der Installer ermittelt zu Beginn einen festen 40-stelligen Commit. Plugin, Agent, Konfigurationsvorlage und systemd-Units werden anschließend ausschließlich aus genau diesem Stand geladen.

-# Requirement: -netbox-plugin-store @ git+https://git.mrblake.cc/MrBlake/Netbox-Store.git@main#subdirectory=netbox_plugin
-

2. API-Token-Pepper konfigurieren

-

NetBox 4.6 warnt beim Start, wenn kein Pepper für v2-API-Tokens vorhanden ist. Erzeuge einmalig einen zufälligen Wert:

+

2. Betriebsmodus auswählen

+ +

Danach bestätigt der Benutzer NetBox-Pfad, Dienstkonto, erkannte NetBox-Version und Store-URL. Vor der ersten Änderung zeigt das Script alle Werte noch einmal zusammengefasst an.

+ +

3. Was automatisch eingerichtet wird

+ +

Vorhandene Konfigurationen werden nicht vollständig überschrieben. Vor jeder Änderung entstehen Sicherungskopien unter /var/backups/netbox-plugin-store/.

+ +

4. Installation prüfen

+
sudo systemctl status netbox-store-agent.socket --no-pager -l
+sudo systemctl status netbox-store-agent.service --no-pager -l
+sudo systemctl status netbox netbox-rq --no-pager -l
+
+sudo -u netbox \
+  /opt/netbox-store-agent/venv/bin/netbox-store-agent capabilities
+
+sudo /opt/netbox/venv/bin/python -c \
+  'from netbox_plugin_store import __version__; print(__version__)'
+ +

5. Erstes Store-Plugin installieren

+
    +
  1. Öffne in NetBox den Plugin Store und wähle ein Plugin mit verfügbarer Version.
  2. +
  3. Starte Installieren. Im Produktionsmodus darf „Nur prüfen (Dry-Run)“ nicht markiert sein.
  4. +
  5. Kontrolliere im Audit, dass Dry-Run: Nein und ein Agent-Vorgang angezeigt werden.
  6. +
  7. Eine erfolgreiche Installation lässt das Plugin absichtlich deaktiviert. Führe anschließend separat Aktivieren aus.
  8. +
+ + +

Updates und erneute Konfiguration

+

Der Installer ist wiederholbar. Lade bei einem Update die aktuelle Datei erneut herunter und führe sie noch einmal aus. Veraltete Store-Requirements und der markierte Konfigurationsblock werden aktualisiert; manuelle Einstellungen außerhalb dieses Blocks bleiben erhalten.

+ +

Häufige Hinweise

+

Audit erfolgreich, aber nichts installiert

+

Steht im Audit Dry-Run: Ja oder state: dry-run, wurde nur der Ablauf geprüft. Für eine reale Installation muss der Produktionsmodus eingerichtet und der Dry-Run-Haken deaktiviert sein.

+ +

Keine Version auswählbar

+

Plugin und Artefakt werden getrennt freigegeben. Synchronisiere das Repository im Store-Adminbereich und gib danach das Wheel- oder Source-Artefakt unter „Release-Artefakte“ frei.

+ +

„configuration.py must contain exactly one static PLUGINS assignment“

+

Eine ältere Installer-Version konnte im verwalteten Block eine zweite PLUGINS-Zuweisung anlegen. Lade den aktuellen Installer erneut herunter und führe ihn mit denselben gewünschten Einstellungen aus. Er entfernt den alten Block, ergänzt netbox_plugin_store in der einzigen statischen Liste und erzeugt den korrigierten Block neu.

+
curl -fsSLo /tmp/netbox-store-install.sh \
+  https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/install.sh
+sudo bash /tmp/netbox-store-install.sh
+
+sudo /opt/netbox/venv/bin/python -m py_compile \
+  /opt/netbox/netbox/netbox/configuration.py
+ +

API_TOKEN_PEPPERS-Warnung

+

Diese NetBox-Einstellung gehört nicht zum Plugin und wird deshalb nicht automatisch verändert:

sudo /opt/netbox/venv/bin/python /opt/netbox/netbox/generate_secret_key.py
-

Übernimm die Ausgabe anschließend in /opt/netbox/netbox/netbox/configuration.py. Die Ganzzahl 1 ist die Pepper-ID:

+

Trage den erzeugten geheimen Wert mit einem Doppelpunkt nach der numerischen ID in configuration.py ein und bewahre ihn dauerhaft auf:

API_TOKEN_PEPPERS = {
     1: "HIER_DEN_GENERIERTEN_WERT_EINTRAGEN",
 }
-

Achte auf den Doppelpunkt nach der numerischen ID (1:, nicht 1;). Prüfe die Python-Syntax, bevor du das Upgrade fortsetzt; bei Erfolg erzeugt dieser Befehl keine Ausgabe:

-
sudo /opt/netbox/venv/bin/python -m py_compile /opt/netbox/netbox/netbox/configuration.py
- -

3. Plugin in NetBox aktivieren

-

Ergänze die NetBox-Konfiguration:

-
PLUGINS = [
-    "netbox_plugin_store",
-]
 
-PLUGINS_CONFIG = {
-    "netbox_plugin_store": {
-        "store_url": "https://netbox.mrblake.cc",
-        "allowed_store_urls": ["https://netbox.mrblake.cc"],
-        "allowed_artifact_urls": [
-            "https://git.mrblake.cc",
-            "https://github.com",
-            "https://codeload.github.com",
-        ],
-        "execution_mode": "dry_run",
-    }
-}
-

Führe danach Migrationen und statische Dateien aus und starte NetBox neu:

-
sudo /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py migrate
-sudo /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input
-sudo systemctl restart netbox netbox-rq
-

Gunicorn 25/26: Control-Socket deaktivieren

-

Wenn journalctl -u netbox den Fehler Control server error: Permission denied: '/nonexistent' zeigt oder Gunicorn trotz laufender Worker nicht antwortet, deaktiviere den optionalen Control-Socket:

+

Gunicorn meldet „Permission denied: /nonexistent“

grep -qE '^[[:space:]]*control_socket_disable[[:space:]]*=[[:space:]]*True' /opt/netbox/gunicorn.py \
   || echo 'control_socket_disable = True' | sudo tee -a /opt/netbox/gunicorn.py
 
 sudo systemctl restart netbox
-curl -sS -o /dev/null -w 'HTTP %{http_code} in %{time_total}s\n' --max-time 10 http://127.0.0.1:8001/login/
-

Die Control-Schnittstelle wird von NetBox nicht für den normalen WSGI-Betrieb benötigt. Der Test muss innerhalb von zehn Sekunden einen HTTP-Status ausgeben.

-

4. Sicheren Host-Agent einrichten

-

dry_run verändert das System nicht. Für Installieren, Aktualisieren, Aktivieren und Entfernen wird der mitgelieferte Linux Host-Agent benötigt. Installiere ihn in eine eigene virtuelle Umgebung:

-
sudo apt update
-sudo apt install -y python3-venv util-linux curl
-sudo python3 -m venv /opt/netbox-store-agent/venv
-sudo /opt/netbox-store-agent/venv/bin/pip install --upgrade pip
-sudo /opt/netbox-store-agent/venv/bin/pip install \
-  'mrblake-netbox-store-agent @ https://git.mrblake.cc/MrBlake/Netbox-Store/archive/main.tar.gz#subdirectory=host_agent'
+curl -sS -o /dev/null -w 'HTTP %{http_code} in %{time_total}s\n' \
+  --max-time 10 http://127.0.0.1:8001/login/
-sudo install -d -o root -g root -m 0750 /etc/netbox-store-agent -curl -fsSL https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/host_agent/examples/agent.toml \ - | sudo tee /etc/netbox-store-agent/agent.toml >/dev/null -sudo chmod 0600 /etc/netbox-store-agent/agent.toml - -curl -fsSL https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/host_agent/systemd/netbox-store-agent.service \ - | sudo tee /etc/systemd/system/netbox-store-agent.service >/dev/null -curl -fsSL https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/host_agent/systemd/netbox-store-agent.socket \ - | sudo tee /etc/systemd/system/netbox-store-agent.socket >/dev/null -sudo sed -i 's#/usr/local/bin/netbox-store-agent#/opt/netbox-store-agent/venv/bin/netbox-store-agent#' \ - /etc/systemd/system/netbox-store-agent.service -

Ermittle anschließend UID und GID des NetBox-Dienstkontos:

-
id netbox
-id -u netbox
-id -g netbox
-

Öffne /etc/netbox-store-agent/agent.toml und trage die ausgegebenen Zahlen bei allowed_peer_uids, allowed_peer_gids und socket_gid ein. Setze außerdem unter [policy] die tatsächlich installierte NetBox-Version. Pfade und erlaubte Hosts des Beispiels sind bereits auf die Standardinstallation unter /opt/netbox und diesen Store ausgerichtet.

-

Erzeuge die beiden verwalteten Startdateien und binde sie einmalig ein:

-
echo 'STORE_PLUGINS = []' | sudo tee /opt/netbox/netbox/netbox/store_plugins.py >/dev/null
-echo '# Generated by netbox-store-agent. Do not edit.' | sudo tee /opt/netbox/local_requirements_store.txt >/dev/null
-sudo chmod 0644 /opt/netbox/netbox/netbox/store_plugins.py /opt/netbox/local_requirements_store.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
-

Füge in configuration.py direkt nach der vorhandenen PLUGINS-Liste einmalig Folgendes ein:

-
from netbox.store_plugins import STORE_PLUGINS
-
-PLUGINS += STORE_PLUGINS
-

Prüfe zunächst die Agent-Konfiguration und den Socket im sicheren Dry-Run-Modus:

-
sudo /opt/netbox-store-agent/venv/bin/netbox-store-agent \
-  --config /etc/netbox-store-agent/agent.toml validate-config
-sudo systemctl daemon-reload
-sudo systemctl enable --now netbox-store-agent.socket
-sudo -u netbox /opt/netbox-store-agent/venv/bin/netbox-store-agent capabilities
-

Wenn dieser Test erfolgreich ist, ändere in agent.toml den Eintrag unter [agent] bewusst auf dry_run = false. Stelle anschließend die Plugin-Konfiguration um:

-
"execution_mode": "agent",
-"agent_socket_path": "/run/netbox-store-agent/agent.sock",
-"agent_timeout": 30,
-"default_dry_run": False,
-

Aktiviere die reale Ausführung erst nach Prüfung aller Werte:

-
sudo systemctl restart netbox-store-agent.service
-sudo /opt/netbox/venv/bin/python -m py_compile /opt/netbox/netbox/netbox/configuration.py
-sudo systemctl restart netbox netbox-rq
-sudo -u netbox /opt/netbox-store-agent/venv/bin/netbox-store-agent capabilities
- -

Source-Kandidaten werden nur nach Admin-Freigabe verarbeitet. Der Agent prüft Commitbindung, Größe und SHA-256, baut daraus lokal ein Wheel und installiert nicht direkt aus einem beweglichen Branch.

- +

Manuelle Installation

+

Für abweichende Pfade oder eigene systemd-Policies stehen die technischen Einzelheiten in der Plugin-Dokumentation und der Host-Agent-Dokumentation.

diff --git a/store/tests/run.php b/store/tests/run.php index 10905c9..01820f5 100644 --- a/store/tests/run.php +++ b/store/tests/run.php @@ -491,22 +491,21 @@ test('public and admin templates render safely with complete artifact evidence', ]); assertTrue(str_contains($detail, 'Wheel')); $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, '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, 'sudo apt install -y git')); + assertTrue(str_contains($installation, '40-stelligen Commit')); + assertTrue(str_contains($installation, '/var/backups/netbox-plugin-store/')); + assertTrue(str_contains($installation, 'Dry-Run: Nein')); + assertTrue(str_contains($installation, 'Release-Artefakte')); + assertTrue(str_contains($installation, 'configuration.py must contain exactly one static PLUGINS assignment')); + assertTrue(str_contains($installation, 'python -m py_compile')); assertTrue(str_contains($installation, 'https://netbox.mrblake.cc')); assertTrue(str_contains($installation, 'API_TOKEN_PEPPERS')); assertTrue(str_contains($installation, '/opt/netbox/netbox/generate_secret_key.py')); - assertTrue(str_contains($installation, 'python -m py_compile')); assertTrue(str_contains($installation, 'control_socket_disable = True')); assertTrue(str_contains($installation, 'http://127.0.0.1:8001/login/')); - assertTrue(str_contains($installation, 'mrblake-netbox-store-agent @ https://git.mrblake.cc')); assertTrue(str_contains($installation, 'netbox-store-agent.socket')); - assertTrue(str_contains($installation, 'dry_run = false')); - assertTrue(str_contains($installation, '"execution_mode": "agent"')); - assertTrue(str_contains($installation, '"default_dry_run": False')); + assertTrue(str_contains($installation, 'netbox-store-agent capabilities')); $admin = $view->render('admin/dashboard', [ 'title' => 'Admin', 'currentPath' => '/admin', 'adminEnabled' => true, 'adminUser' => 'admin', 'csrf' => 'safe-token', 'ok' => '', 'error' => '', 'sources' => $state['sources'],