Fix agent plugin configuration handling
This commit is contained in:
@@ -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/<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.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.
|
||||
|
||||
|
||||
+38
-10
@@ -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)
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.2"
|
||||
__version__ = "0.1.3"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,126 +2,104 @@
|
||||
<section class="install-hero">
|
||||
<div class="shell prose-shell">
|
||||
<p class="eyebrow">NetBox 4.6.5–4.6.8</p>
|
||||
<h1>NetBox Store Plugin installieren</h1>
|
||||
<p>Das Plugin verbindet deine NetBox mit <code>https://netbox.mrblake.cc</code>. Docker ist nicht erforderlich.</p>
|
||||
<h1>NetBox Plugin Store installieren</h1>
|
||||
<p>Der interaktive Installer richtet Plugin, Host-Agent und NetBox-Konfiguration gemeinsam ein. Docker und ein lokales Git sind nicht erforderlich.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>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>
|
||||
<h2>Voraussetzungen</h2>
|
||||
<ul>
|
||||
<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 \
|
||||
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>
|
||||
<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
|
||||
<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>
|
||||
|
||||
# Requirement:
|
||||
netbox-plugin-store @ git+https://git.mrblake.cc/MrBlake/Netbox-Store.git@main#subdirectory=netbox_plugin</code></pre>
|
||||
<h2>2. API-Token-Pepper konfigurieren</h2>
|
||||
<p>NetBox 4.6 warnt beim Start, wenn kein Pepper für v2-API-Tokens vorhanden ist. Erzeuge einmalig einen zufälligen Wert:</p>
|
||||
<h2>2. Betriebsmodus auswählen</h2>
|
||||
<ul>
|
||||
<li><strong>Sicherer Testmodus:</strong> Katalog und Lifecycle-Pläne können geprüft werden; Lifecycle-Aktionen verändern das System nicht.</li>
|
||||
<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>
|
||||
<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 = {
|
||||
1: "HIER_DEN_GENERIERTEN_WERT_EINTRAGEN",
|
||||
}</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 = {
|
||||
"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>
|
||||
<h3>Gunicorn meldet „Permission denied: /nonexistent“</h3>
|
||||
<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
|
||||
|
||||
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>
|
||||
<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>
|
||||
<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'
|
||||
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>
|
||||
|
||||
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</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>
|
||||
<h2>Manuelle Installation</h2>
|
||||
<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>
|
||||
</section>
|
||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||
|
||||
+7
-8
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user