Fix agent plugin configuration handling
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run

This commit is contained in:
2026-08-24 23:10:02 +02:00
parent 541f107e1e
commit acadcdfeee
9 changed files with 231 additions and 157 deletions
+23 -3
View File
@@ -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. 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/<Zeitstempel>/` 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.54.6.8 begrenzt. Zuerst wird ein Wheel gebaut und in die NetBox-Virtualenv installiert: Das Plugin unter `netbox_plugin/` ist auf NetBox 4.6.54.6.8 begrenzt. Zuerst wird ein Wheel gebaut und in die NetBox-Virtualenv installiert:
```text ```text
cd netbox_plugin cd netbox_plugin
python -m build 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: 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). 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. 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.
+38 -10
View File
@@ -250,6 +250,7 @@ update_netbox_configuration() {
CONFIG_BEGIN_VALUE="$CONFIG_BEGIN" \ CONFIG_BEGIN_VALUE="$CONFIG_BEGIN" \
CONFIG_END_VALUE="$CONFIG_END" \ CONFIG_END_VALUE="$CONFIG_END" \
"$NETBOX_PYTHON" - <<'PY' "$NETBOX_PYTHON" - <<'PY'
import ast
import os import os
import re import re
import stat import stat
@@ -266,9 +267,6 @@ block = "\n".join([
start_marker, start_marker,
"from netbox.store_plugins import STORE_PLUGINS", "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:", "for _store_plugin in STORE_PLUGINS:",
" if _store_plugin not in PLUGINS:", " if _store_plugin not in PLUGINS:",
" PLUGINS.append(_store_plugin)", " PLUGINS.append(_store_plugin)",
@@ -291,21 +289,51 @@ block = "\n".join([
end_marker, end_marker,
]) ])
original = path.read_text(encoding="utf-8") 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) start_count = original.count(start_marker)
end_count = original.count(end_marker) end_count = original.count(end_marker)
if (start_count, end_count) == (0, 0): if (start_count, end_count) == (0, 0):
candidate = original.rstrip() + "\n\n" + block + "\n" base = original
elif (start_count, end_count) == (1, 1): elif (start_count, end_count) == (1, 1):
before, remainder = original.split(start_marker, 1) before, remainder = original.split(start_marker, 1)
_, after = remainder.split(end_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: else:
raise SystemExit("Der verwaltete configuration.py-Block ist unvollständig oder doppelt vorhanden.") 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") compile(candidate, str(path), "exec")
metadata = path.stat() metadata = path.stat()
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
+1 -1
View File
@@ -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`: Install the wheel into NetBox's virtual environment and persist it in `/opt/netbox/local_requirements.txt`:
```text ```text
netbox-plugin-store==0.1.2 netbox-plugin-store==0.1.3
``` ```
Add the plugin to `configuration.py`: Add the plugin to `configuration.py`:
+40 -15
View File
@@ -156,18 +156,23 @@ class LifecycleService:
plugin = self.client.get_plugin(request.slug) plugin = self.client.get_plugin(request.slug)
ensure_not_self(plugin.package_name, plugin.import_name) ensure_not_self(plugin.package_name, plugin.import_name)
installed = self.version_provider(plugin.package_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) 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 release: Release | None = None
if request.action in {"install", "update"}: if request.action in {"install", "update"}:
release = plugin.select_release(self.settings.netbox_version, request.version) release = plugin.select_release(self.settings.netbox_version, request.version)
@@ -200,10 +205,20 @@ class LifecycleService:
) )
try: try:
if self.settings.execution_mode == "agent": 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: else:
assert config_editor is not None and requirements_editor is not None
result = self._execute_direct( 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: except Exception as exc:
self.repository.update_status( self.repository.update_status(
@@ -268,9 +283,19 @@ class LifecycleService:
elif action == "update": elif action == "update":
plan.extend(["Update the persistent requirement pin atomically.", "Upgrade from the verified local artifact."]) plan.extend(["Update the persistent requirement pin atomically.", "Upgrade from the verified local artifact."])
elif action == "enable": 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": 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": elif action == "uninstall":
plan.extend(["Remove the persistent requirement pin atomically.", "Uninstall the distribution with pip."]) plan.extend(["Remove the persistent requirement pin atomically.", "Uninstall the distribution with pip."])
if action == "enable" or (action == "update" and currently_enabled): if action == "enable" or (action == "update" and currently_enabled):
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.2" __version__ = "0.1.3"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netbox-plugin-store" 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" description = "A secure NetBox 4.6 plugin lifecycle client for the MrBlake Plugin Store"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+39 -15
View File
@@ -93,22 +93,25 @@ class FakeRunner:
def runtime(root: Path, *, execution_mode="direct", allow=True): def runtime(root: Path, *, execution_mode="direct", allow=True):
config = root / "configuration.py" config = root / "configuration.py"
config.write_text("PLUGINS = ['netbox_plugin_store']\n", encoding="utf-8") 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( return RuntimeSettings.from_mapping(
{ 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,
},
configuration_dir=root, configuration_dir=root,
netbox_root=root, netbox_root=root,
base_dir=root, base_dir=root,
@@ -117,6 +120,27 @@ def runtime(root: Path, *, execution_mode="direct", allow=True):
class LifecycleTests(unittest.TestCase): 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): def test_dry_run_has_no_download_or_subprocess(self):
with tempfile.TemporaryDirectory() as temp_name: with tempfile.TemporaryDirectory() as temp_name:
root = Path(temp_name) root = Path(temp_name)
+81 -103
View File
@@ -2,126 +2,104 @@
<section class="install-hero"> <section class="install-hero">
<div class="shell prose-shell"> <div class="shell prose-shell">
<p class="eyebrow">NetBox 4.6.54.6.8</p> <p class="eyebrow">NetBox 4.6.54.6.8</p>
<h1>NetBox Store Plugin installieren</h1> <h1>NetBox Plugin Store installieren</h1>
<p>Das Plugin verbindet deine NetBox mit <code>https://netbox.mrblake.cc</code>. Docker ist nicht erforderlich.</p> <p>Der interaktive Installer richtet Plugin, Host-Agent und NetBox-Konfiguration gemeinsam ein. Docker und ein lokales Git sind nicht erforderlich.</p>
</div> </div>
</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> <h2>Voraussetzungen</h2>
<h2>Interaktive Komplettinstallation</h2> <ul>
<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> <li>NetBox 4.6.5 bis 4.6.8 auf einem Linux-Host, standardmäßig unter <code>/opt/netbox</code></li>
<li><code>sudo</code>- beziehungsweise Root-Zugriff und eine funktionierende Internetverbindung</li>
<li>ein aktuelles Backup von NetBox-Konfiguration und Datenbank</li>
</ul>
<aside class="notice warning"><strong>Produktionshinweis:</strong> Der Agent darf geprüfte Python-Pakete installieren, verwaltete NetBox-Dateien ändern und die Dienste <code>netbox</code> sowie <code>netbox-rq</code> neu starten. Prüfe das Script vor der Ausführung.</aside>
<h2>1. Installer herunterladen und prüfen</h2>
<p>Lade das Script zuerst als Datei herunter. Verwende keine direkte <code>curl | sudo bash</code>-Pipe, damit du den ausgeführten Inhalt vorher kontrollieren kannst.</p>
<pre><code>curl -fsSLo /tmp/netbox-store-install.sh \ <pre><code>curl -fsSLo /tmp/netbox-store-install.sh \
https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/install.sh https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/install.sh
less /tmp/netbox-store-install.sh less /tmp/netbox-store-install.sh
sudo bash /tmp/netbox-store-install.sh</code></pre> 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> <p>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.</p>
<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>
<pre><code>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</code></pre>
<p>Der erste Befehl entfernt ausschließlich ältere Bezugsquellen dieses Plugins. Dadurch bleibt auch nach einem Wechsel zwischen Git- und Archivinstallation genau ein Eintrag vorhanden.</p>
<h3>Alternative mit Git</h3>
<p>Wenn du stattdessen eine <code>git+https</code>-Requirement verwendest, muss das Programm <code>git</code> vor dem NetBox-Upgrade installiert sein:</p>
<pre><code>sudo apt update
sudo apt install -y git
# Requirement: <h2>2. Betriebsmodus auswählen</h2>
netbox-plugin-store @ git+https://git.mrblake.cc/MrBlake/Netbox-Store.git@main#subdirectory=netbox_plugin</code></pre> <ul>
<h2>2. API-Token-Pepper konfigurieren</h2> <li><strong>Sicherer Testmodus:</strong> Katalog und Lifecycle-Pläne können geprüft werden; Lifecycle-Aktionen verändern das System nicht.</li>
<p>NetBox 4.6 warnt beim Start, wenn kein Pepper für v2-API-Tokens vorhanden ist. Erzeuge einmalig einen zufälligen Wert:</p> <li><strong>Produktionsmodus:</strong> Installationen, Updates, Aktivierungen, Deaktivierungen und Deinstallationen laufen über den Host-Agent. Dieser Modus verlangt zusätzlich die exakte Eingabe <code>ECHT INSTALLIEREN</code>.</li>
</ul>
<p>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.</p>
<h2>3. Was automatisch eingerichtet wird</h2>
<ul>
<li><code>netbox-plugin-store</code> in der NetBox-Virtualenv und in <code>local_requirements.txt</code></li>
<li>der Host-Agent unter <code>/opt/netbox-store-agent/venv</code></li>
<li><code>/etc/netbox-store-agent/agent.toml</code> mit Dienstkonto und NetBox-Version</li>
<li>systemd-Service und Unix-Socket</li>
<li><code>netbox.store_plugins</code> und die verwaltete Requirements-Datei</li>
<li>ein eindeutig markierter, bei Updates ersetzbarer Block in <code>configuration.py</code></li>
<li>Migrationen, statische Dateien und Neustart der beteiligten Dienste</li>
</ul>
<p>Vorhandene Konfigurationen werden nicht vollständig überschrieben. Vor jeder Änderung entstehen Sicherungskopien unter <code>/var/backups/netbox-plugin-store/</code>.</p>
<h2>4. Installation prüfen</h2>
<pre><code>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__)'</code></pre>
<h2>5. Erstes Store-Plugin installieren</h2>
<ol>
<li>Öffne in NetBox den Plugin Store und wähle ein Plugin mit verfügbarer Version.</li>
<li>Starte <strong>Installieren</strong>. Im Produktionsmodus darf „Nur prüfen (Dry-Run)“ nicht markiert sein.</li>
<li>Kontrolliere im Audit, dass <code>Dry-Run: Nein</code> und ein Agent-Vorgang angezeigt werden.</li>
<li>Eine erfolgreiche Installation lässt das Plugin absichtlich deaktiviert. Führe anschließend separat <strong>Aktivieren</strong> aus.</li>
</ol>
<aside class="notice"><strong>Keine Repository-Releases erforderlich:</strong> Definiert das Python-Paket eine Version, erzeugt der Store beim Synchronisieren ein commitgebundenes Source-Artefakt. Dieses muss im Store-Adminbereich unter „Release-Artefakte“ separat freigegeben werden.</aside>
<h2>Updates und erneute Konfiguration</h2>
<p>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.</p>
<h2>Häufige Hinweise</h2>
<h3>Audit erfolgreich, aber nichts installiert</h3>
<p>Steht im Audit <code>Dry-Run: Ja</code> oder <code>state: dry-run</code>, wurde nur der Ablauf geprüft. Für eine reale Installation muss der Produktionsmodus eingerichtet und der Dry-Run-Haken deaktiviert sein.</p>
<h3>Keine Version auswählbar</h3>
<p>Plugin und Artefakt werden getrennt freigegeben. Synchronisiere das Repository im Store-Adminbereich und gib danach das Wheel- oder Source-Artefakt unter „Release-Artefakte“ frei.</p>
<h3>„configuration.py must contain exactly one static PLUGINS assignment“</h3>
<p>Eine ältere Installer-Version konnte im verwalteten Block eine zweite <code>PLUGINS</code>-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 <code>netbox_plugin_store</code> in der einzigen statischen Liste und erzeugt den korrigierten Block neu.</p>
<pre><code>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</code></pre>
<h3>API_TOKEN_PEPPERS-Warnung</h3>
<p>Diese NetBox-Einstellung gehört nicht zum Plugin und wird deshalb nicht automatisch verändert:</p>
<pre><code>sudo /opt/netbox/venv/bin/python /opt/netbox/netbox/generate_secret_key.py</code></pre> <pre><code>sudo /opt/netbox/venv/bin/python /opt/netbox/netbox/generate_secret_key.py</code></pre>
<p>Übernimm die Ausgabe anschließend in <code>/opt/netbox/netbox/netbox/configuration.py</code>. Die Ganzzahl <code>1</code> ist die Pepper-ID:</p> <p>Trage den erzeugten geheimen Wert mit einem Doppelpunkt nach der numerischen ID in <code>configuration.py</code> ein und bewahre ihn dauerhaft auf:</p>
<pre><code>API_TOKEN_PEPPERS = { <pre><code>API_TOKEN_PEPPERS = {
1: "HIER_DEN_GENERIERTEN_WERT_EINTRAGEN", 1: "HIER_DEN_GENERIERTEN_WERT_EINTRAGEN",
}</code></pre> }</code></pre>
<p>Achte auf den Doppelpunkt nach der numerischen ID (<code>1:</code>, nicht <code>1;</code>). Prüfe die Python-Syntax, bevor du das Upgrade fortsetzt; bei Erfolg erzeugt dieser Befehl keine Ausgabe:</p>
<pre><code>sudo /opt/netbox/venv/bin/python -m py_compile /opt/netbox/netbox/netbox/configuration.py</code></pre>
<aside class="notice warning"><strong>Geheimnis dauerhaft sichern:</strong> Der Pepper muss vertraulich und über Upgrades hinweg unverändert erhalten bleiben. Entferne oder ändere einen bereits verwendeten Pepper nicht, da davon bestehende v2-API-Tokens abhängen. Für eine spätere Rotation wird eine weitere numerische ID ergänzt.</aside>
<h2>3. Plugin in NetBox aktivieren</h2>
<p>Ergänze die NetBox-Konfiguration:</p>
<pre><code>PLUGINS = [
"netbox_plugin_store",
]
PLUGINS_CONFIG = { <h3>Gunicorn meldet „Permission denied: /nonexistent“</h3>
"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",
}
}</code></pre>
<p>Führe danach Migrationen und statische Dateien aus und starte NetBox neu:</p>
<pre><code>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</code></pre>
<h3>Gunicorn 25/26: Control-Socket deaktivieren</h3>
<p>Wenn <code>journalctl -u netbox</code> den Fehler <code>Control server error: Permission denied: '/nonexistent'</code> zeigt oder Gunicorn trotz laufender Worker nicht antwortet, deaktiviere den optionalen Control-Socket:</p>
<pre><code>grep -qE '^[[:space:]]*control_socket_disable[[:space:]]*=[[:space:]]*True' /opt/netbox/gunicorn.py \ <pre><code>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 || echo 'control_socket_disable = True' | sudo tee -a /opt/netbox/gunicorn.py
sudo systemctl restart netbox 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/</code></pre> curl -sS -o /dev/null -w 'HTTP %{http_code} in %{time_total}s\n' \
<p>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.</p> --max-time 10 http://127.0.0.1:8001/login/</code></pre>
<h2>4. Sicheren Host-Agent einrichten</h2>
<p><code>dry_run</code> 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:</p>
<pre><code>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'
sudo install -d -o root -g root -m 0750 /etc/netbox-store-agent <h2>Manuelle Installation</h2>
curl -fsSL https://git.mrblake.cc/MrBlake/Netbox-Store/raw/branch/main/host_agent/examples/agent.toml \ <p>Für abweichende Pfade oder eigene systemd-Policies stehen die technischen Einzelheiten in der <a href="https://git.mrblake.cc/MrBlake/Netbox-Store/src/branch/main/netbox_plugin/README.md" rel="noreferrer noopener">Plugin-Dokumentation</a> und der <a href="https://git.mrblake.cc/MrBlake/Netbox-Store/src/branch/main/host_agent/README.md" rel="noreferrer noopener">Host-Agent-Dokumentation</a>.</p>
| 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</code></pre>
<p>Ermittle anschließend UID und GID des NetBox-Dienstkontos:</p>
<pre><code>id netbox
id -u netbox
id -g netbox</code></pre>
<p>Öffne <code>/etc/netbox-store-agent/agent.toml</code> und trage die ausgegebenen Zahlen bei <code>allowed_peer_uids</code>, <code>allowed_peer_gids</code> und <code>socket_gid</code> ein. Setze außerdem unter <code>[policy]</code> die tatsächlich installierte NetBox-Version. Pfade und erlaubte Hosts des Beispiels sind bereits auf die Standardinstallation unter <code>/opt/netbox</code> und diesen Store ausgerichtet.</p>
<p>Erzeuge die beiden verwalteten Startdateien und binde sie einmalig ein:</p>
<pre><code>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</code></pre>
<p>Füge in <code>configuration.py</code> direkt nach der vorhandenen <code>PLUGINS</code>-Liste einmalig Folgendes ein:</p>
<pre><code>from netbox.store_plugins import STORE_PLUGINS
PLUGINS += STORE_PLUGINS</code></pre>
<p>Prüfe zunächst die Agent-Konfiguration und den Socket im sicheren Dry-Run-Modus:</p>
<pre><code>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</code></pre>
<p>Wenn dieser Test erfolgreich ist, ändere in <code>agent.toml</code> den Eintrag unter <code>[agent]</code> bewusst auf <code>dry_run = false</code>. Stelle anschließend die Plugin-Konfiguration um:</p>
<pre><code>"execution_mode": "agent",
"agent_socket_path": "/run/netbox-store-agent/agent.sock",
"agent_timeout": 30,
"default_dry_run": False,</code></pre>
<p>Aktiviere die reale Ausführung erst nach Prüfung aller Werte:</p>
<pre><code>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</code></pre>
<aside class="notice"><strong>Zwei bewusste Freigaben:</strong> Für eine reale Aktion müssen sowohl <code>execution_mode = "agent"</code> im NetBox-Plugin als auch <code>dry_run = false</code> im Host-Agent gesetzt sein. Kontrolliere im Bestätigungsdialog außerdem, dass „Nur prüfen (Dry-Run)“ nicht markiert ist. Eine Installation lässt das neue Plugin zunächst deaktiviert; führe danach separat „Aktivieren“ aus.</aside>
<p>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.</p>
<aside class="notice warning"><strong>Wichtig:</strong> Source-Builds führen den Build-Code des freigegebenen Repository-Commits aus. Gib nur vertrauenswürdige Kandidaten frei.</aside>
</section> </section>
<?php include __DIR__ . '/partials/footer.php'; ?> <?php include __DIR__ . '/partials/footer.php'; ?>
+7 -8
View File
@@ -491,22 +491,21 @@ 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, 'raw/branch/main/install.sh')); assertTrue(str_contains($installation, 'raw/branch/main/install.sh'));
assertTrue(str_contains($installation, 'ECHT INSTALLIEREN')); assertTrue(str_contains($installation, 'ECHT INSTALLIEREN'));
assertTrue(str_contains($installation, "sed -i '/^[[:space:]]*netbox-plugin-store")); assertTrue(str_contains($installation, '40-stelligen Commit'));
assertTrue(str_contains($installation, 'sudo apt install -y git')); 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, 'https://netbox.mrblake.cc'));
assertTrue(str_contains($installation, 'API_TOKEN_PEPPERS')); assertTrue(str_contains($installation, 'API_TOKEN_PEPPERS'));
assertTrue(str_contains($installation, '/opt/netbox/netbox/generate_secret_key.py')); 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, 'control_socket_disable = True'));
assertTrue(str_contains($installation, 'http://127.0.0.1:8001/login/')); 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, 'netbox-store-agent.socket'));
assertTrue(str_contains($installation, 'dry_run = false')); assertTrue(str_contains($installation, 'netbox-store-agent capabilities'));
assertTrue(str_contains($installation, '"execution_mode": "agent"'));
assertTrue(str_contains($installation, '"default_dry_run": False'));
$admin = $view->render('admin/dashboard', [ $admin = $view->render('admin/dashboard', [
'title' => 'Admin', 'currentPath' => '/admin', 'adminEnabled' => true, 'adminUser' => 'admin', 'title' => 'Admin', 'currentPath' => '/admin', 'adminEnabled' => true, 'adminUser' => 'admin',
'csrf' => 'safe-token', 'ok' => '', 'error' => '', 'sources' => $state['sources'], 'csrf' => 'safe-token', 'ok' => '', 'error' => '', 'sources' => $state['sources'],