diff --git a/README.md b/README.md index 468b3e9..d0e5654 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ Das Plugin unter `netbox_plugin/` ist auf NetBox 4.6.5–4.6.8 begrenzt. Zuerst ```text cd netbox_plugin python -m build -/opt/netbox/venv/bin/pip install dist/netbox_plugin_store-0.1.1-py3-none-any.whl +/opt/netbox/venv/bin/pip install dist/netbox_plugin_store-0.1.2-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: diff --git a/netbox_plugin/README.md b/netbox_plugin/README.md index a5d76e5..970cab6 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.1 +netbox-plugin-store==0.1.2 ``` Add the plugin to `configuration.py`: diff --git a/netbox_plugin/netbox_plugin_store/client.py b/netbox_plugin/netbox_plugin_store/client.py index f06d9ed..989f330 100644 --- a/netbox_plugin/netbox_plugin_store/client.py +++ b/netbox_plugin/netbox_plugin_store/client.py @@ -166,7 +166,7 @@ class CatalogPlugin: ) def select_release(self, netbox_version: str, requested_version: str = "") -> Release: - compatible = [release for release in self.releases if release.supports(netbox_version, self)] + compatible = list(self.installable_releases(netbox_version)) if requested_version: compatible = [release for release in compatible if release.version == requested_version] elif self.latest_version: @@ -177,6 +177,26 @@ class CatalogPlugin: raise StoreClientError("No compatible release is available for this NetBox version.") return max(compatible, key=lambda release: Version(release.version)) + def installable_releases( + self, + netbox_version: str, + *, + require_approval_marker: bool = False, + ) -> tuple[Release, ...]: + return tuple( + release + for release in self.releases + if ( + release.supports(netbox_version, self) + and release.approved + and release.immutable + and bool(release.download_url) + and bool(release.sha256) + and release.artifact_kind in {"wheel", "source_archive"} + and (not require_approval_marker or bool(release.approved_payload_sha256)) + ) + ) + class StoreClient: API_LIMIT = 5 * 1024 * 1024 diff --git a/netbox_plugin/netbox_plugin_store/forms.py b/netbox_plugin/netbox_plugin_store/forms.py index 88494d7..8893967 100644 --- a/netbox_plugin/netbox_plugin_store/forms.py +++ b/netbox_plugin/netbox_plugin_store/forms.py @@ -34,21 +34,20 @@ class LifecycleConfirmForm(forms.Form): self.runtime = runtime self.fields["confirmation"].widget.attrs.update({"autocomplete": "off", "placeholder": plugin.slug}) self.fields["dry_run"].initial = runtime.default_dry_run + self.has_installable_release = True if action in {"install", "update"}: choices = [ (release.version, release.version) - for release in plugin.releases - if ( - release.supports(runtime.netbox_version, plugin) - and release.approved - and release.immutable - and bool(release.sha256) - and (runtime.execution_mode != "agent" or bool(release.approved_payload_sha256)) + for release in plugin.installable_releases( + runtime.netbox_version, + require_approval_marker=runtime.execution_mode == "agent", ) ] choices.sort(reverse=True) self.fields["version"].choices = choices - self.fields["version"].required = True + self.has_installable_release = bool(choices) + self.fields["version"].required = self.has_installable_release + self.fields["version"].disabled = not self.has_installable_release self.fields["version"].initial = plugin.latest_version else: self.fields.pop("version") @@ -64,6 +63,11 @@ class LifecycleConfirmForm(forms.Form): def clean(self): cleaned = super().clean() + if self.action in {"install", "update"} and not self.has_installable_release: + raise forms.ValidationError( + "Keine freigegebene, kompatible Plugin-Version ist verfügbar. " + "Das Source-Artefakt muss zunächst im Store synchronisiert und freigegeben werden." + ) if self.runtime.execution_mode == "dry_run" and not cleaned.get("dry_run", False): self.add_error("dry_run", "Reale Aktionen sind in execution_mode=dry_run deaktiviert.") return cleaned diff --git a/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/catalog.html b/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/catalog.html index 14fe7da..cae1582 100644 --- a/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/catalog.html +++ b/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/catalog.html @@ -9,7 +9,7 @@

Freigegebene Plugins für diese NetBox-Instanz.

{% if runtime %} - + Modus: {{ runtime.execution_mode }} {% endif %} @@ -29,14 +29,14 @@

{{ card.plugin.name }}

{% if card.plugin.approved %} - Freigegeben + Freigegeben {% else %} - Nicht freigegeben + Nicht freigegeben {% endif %}

{{ card.plugin.summary|default:"Keine Zusammenfassung vorhanden." }}

-
Verfügbar
{{ card.plugin.latest_version|default:"–" }}
+
Verfügbar
{{ card.plugin.latest_version|default:"Kein Artefakt freigegeben" }}
Installiert
{{ card.installed_version|default:"–" }}
Status
diff --git a/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/confirm.html b/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/confirm.html index 8edcb2b..df04f5d 100644 --- a/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/confirm.html +++ b/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/confirm.html @@ -17,6 +17,11 @@ {% if runtime.execution_mode == 'dry_run' %}
Diese Instanz erlaubt ausschließlich Dry-Runs.
{% endif %} + {% if not form.has_installable_release %} +
+ Keine freigegebene, kompatible Version verfügbar. Synchronisiere das Repository im Store-Adminbereich und gib danach das erzeugte Source-Artefakt unter „Release-Artefakte“ frei. +
+ {% endif %}
{% csrf_token %} {{ form.non_field_errors }} @@ -30,7 +35,7 @@ {% endfor %}
Abbrechen - +
diff --git a/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/detail.html b/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/detail.html index 29bf593..42b65bc 100644 --- a/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/detail.html +++ b/netbox_plugin/netbox_plugin_store/templates/netbox_plugin_store/detail.html @@ -22,6 +22,12 @@ {% if local_status and local_status.restart_required %}
Ein Neustart von NetBox und den Workern ist erforderlich.
{% endif %} +{% if plugin.approved and not has_installable_release %} +
+ Für dieses Plugin ist noch kein freigegebenes, mit NetBox {{ runtime.netbox_version }} kompatibles Artefakt verfügbar. + Repositorys ohne eigenen Release werden beim Store-Sync als commitgebundenes Source-Archiv eingelesen; dieses muss anschließend im Store-Adminbereich unter „Release-Artefakte“ freigegeben werden. +
+{% endif %}
@@ -55,9 +61,9 @@
{{ item.release.version }} {% if item.compatible and item.release.approved and item.release.immutable and item.release.sha256 %} - installierbar + installierbar {% else %} - gesperrt + gesperrt {% endif %}
{% empty %} diff --git a/netbox_plugin/netbox_plugin_store/version.py b/netbox_plugin/netbox_plugin_store/version.py index 485f44a..b3f4756 100644 --- a/netbox_plugin/netbox_plugin_store/version.py +++ b/netbox_plugin/netbox_plugin_store/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/netbox_plugin/netbox_plugin_store/views.py b/netbox_plugin/netbox_plugin_store/views.py index b7a1202..9572db1 100644 --- a/netbox_plugin/netbox_plugin_store/views.py +++ b/netbox_plugin/netbox_plugin_store/views.py @@ -83,12 +83,16 @@ class PluginDetailView(StorePermissionMixin, View): except StoreClientError as exc: raise Http404(redact_text(exc)) from exc state = _plugin_state(plugin, set(django_settings.PLUGINS)) + installable_releases = plugin.installable_releases( + runtime.netbox_version, + require_approval_marker=runtime.execution_mode == "agent", + ) actions: list[str] = [] if not state["installed"]: - if plugin.approved: + if plugin.approved and installable_releases: actions.append("install") else: - if plugin.approved: + if plugin.approved and installable_releases: actions.append("update") if state["enabled"]: actions.append("disable") @@ -107,7 +111,9 @@ class PluginDetailView(StorePermissionMixin, View): "state": state, "actions": actions, "releases": releases, + "has_installable_release": bool(installable_releases), "local_status": local_status, + "runtime": runtime, }, ) diff --git a/netbox_plugin/pyproject.toml b/netbox_plugin/pyproject.toml index d412d00..7421652 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.1" +version = "0.1.2" 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_client.py b/netbox_plugin/tests/test_client.py index 2c67f89..a8d774e 100644 --- a/netbox_plugin/tests/test_client.py +++ b/netbox_plugin/tests/test_client.py @@ -58,6 +58,27 @@ class URLPolicyTests(unittest.TestCase): with self.assertRaises(StoreClientError): plugin.select_release("4.6.9") + def test_only_verified_supported_artifacts_are_installable(self): + payload, _ = plugin_payload() + payload["releases"].append( + { + "version": "1.3.0", + "download_url": "https://store.example/source.tar.gz", + "sha256": "b" * 64, + "approved": False, + "immutable": True, + "artifact_kind": "source_archive", + "min_netbox_version": "4.6.5", + "max_netbox_version": "4.6.8", + } + ) + plugin = CatalogPlugin.from_mapping(payload) + + self.assertEqual( + tuple(release.version for release in plugin.installable_releases("4.6.8")), + ("1.2.0",), + ) + class _Response(io.BytesIO): def __init__(self, body): diff --git a/store/templates/admin/dashboard.php b/store/templates/admin/dashboard.php index b116480..fa083b7 100644 --- a/store/templates/admin/dashboard.php +++ b/store/templates/admin/dashboard.php @@ -126,7 +126,7 @@ include dirname(__DIR__) . '/partials/head.php'; - +
Plugin / VersionArtefaktIntegritätStatusAktionen
Keine installierbaren Artefakte gefunden. Prüfe Version und Build-Metadaten des Repositorys.
Keine installierbaren Artefakte gefunden. Bei Repositorys ohne Release erzeugt eine erneute Synchronisierung automatisch ein commitgebundenes Source-Artefakt, sofern das Python-Paket eine Version definiert.