Improve artifact availability UX
This commit is contained in:
@@ -232,7 +232,7 @@ Das Plugin unter `netbox_plugin/` ist auf NetBox 4.6.5–4.6.8 begrenzt. Zuerst
|
|||||||
```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.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:
|
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:
|
||||||
|
|||||||
@@ -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.1
|
netbox-plugin-store==0.1.2
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the plugin to `configuration.py`:
|
Add the plugin to `configuration.py`:
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class CatalogPlugin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def select_release(self, netbox_version: str, requested_version: str = "") -> Release:
|
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:
|
if requested_version:
|
||||||
compatible = [release for release in compatible if release.version == requested_version]
|
compatible = [release for release in compatible if release.version == requested_version]
|
||||||
elif self.latest_version:
|
elif self.latest_version:
|
||||||
@@ -177,6 +177,26 @@ class CatalogPlugin:
|
|||||||
raise StoreClientError("No compatible release is available for this NetBox version.")
|
raise StoreClientError("No compatible release is available for this NetBox version.")
|
||||||
return max(compatible, key=lambda release: Version(release.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:
|
class StoreClient:
|
||||||
API_LIMIT = 5 * 1024 * 1024
|
API_LIMIT = 5 * 1024 * 1024
|
||||||
|
|||||||
@@ -34,21 +34,20 @@ class LifecycleConfirmForm(forms.Form):
|
|||||||
self.runtime = runtime
|
self.runtime = runtime
|
||||||
self.fields["confirmation"].widget.attrs.update({"autocomplete": "off", "placeholder": plugin.slug})
|
self.fields["confirmation"].widget.attrs.update({"autocomplete": "off", "placeholder": plugin.slug})
|
||||||
self.fields["dry_run"].initial = runtime.default_dry_run
|
self.fields["dry_run"].initial = runtime.default_dry_run
|
||||||
|
self.has_installable_release = True
|
||||||
if action in {"install", "update"}:
|
if action in {"install", "update"}:
|
||||||
choices = [
|
choices = [
|
||||||
(release.version, release.version)
|
(release.version, release.version)
|
||||||
for release in plugin.releases
|
for release in plugin.installable_releases(
|
||||||
if (
|
runtime.netbox_version,
|
||||||
release.supports(runtime.netbox_version, plugin)
|
require_approval_marker=runtime.execution_mode == "agent",
|
||||||
and release.approved
|
|
||||||
and release.immutable
|
|
||||||
and bool(release.sha256)
|
|
||||||
and (runtime.execution_mode != "agent" or bool(release.approved_payload_sha256))
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
choices.sort(reverse=True)
|
choices.sort(reverse=True)
|
||||||
self.fields["version"].choices = choices
|
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
|
self.fields["version"].initial = plugin.latest_version
|
||||||
else:
|
else:
|
||||||
self.fields.pop("version")
|
self.fields.pop("version")
|
||||||
@@ -64,6 +63,11 @@ class LifecycleConfirmForm(forms.Form):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned = super().clean()
|
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):
|
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.")
|
self.add_error("dry_run", "Reale Aktionen sind in execution_mode=dry_run deaktiviert.")
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<p class="text-secondary mb-0">Freigegebene Plugins für diese NetBox-Instanz.</p>
|
<p class="text-secondary mb-0">Freigegebene Plugins für diese NetBox-Instanz.</p>
|
||||||
</div>
|
</div>
|
||||||
{% if runtime %}
|
{% if runtime %}
|
||||||
<span class="badge {% if runtime.execution_mode == 'dry_run' %}bg-yellow text-dark{% else %}bg-green{% endif %}">
|
<span class="badge {% if runtime.execution_mode == 'dry_run' %}text-bg-warning{% else %}text-bg-success{% endif %}">
|
||||||
Modus: {{ runtime.execution_mode }}
|
Modus: {{ runtime.execution_mode }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -29,14 +29,14 @@
|
|||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h3 class="card-title">{{ card.plugin.name }}</h3>
|
<h3 class="card-title">{{ card.plugin.name }}</h3>
|
||||||
{% if card.plugin.approved %}
|
{% if card.plugin.approved %}
|
||||||
<span class="badge bg-green">Freigegeben</span>
|
<span class="badge text-bg-success">Freigegeben</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-red">Nicht freigegeben</span>
|
<span class="badge text-bg-danger">Nicht freigegeben</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary">{{ card.plugin.summary|default:"Keine Zusammenfassung vorhanden." }}</p>
|
<p class="text-secondary">{{ card.plugin.summary|default:"Keine Zusammenfassung vorhanden." }}</p>
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-5">Verfügbar</dt><dd class="col-7">{{ card.plugin.latest_version|default:"–" }}</dd>
|
<dt class="col-5">Verfügbar</dt><dd class="col-7">{{ card.plugin.latest_version|default:"Kein Artefakt freigegeben" }}</dd>
|
||||||
<dt class="col-5">Installiert</dt><dd class="col-7">{{ card.installed_version|default:"–" }}</dd>
|
<dt class="col-5">Installiert</dt><dd class="col-7">{{ card.installed_version|default:"–" }}</dd>
|
||||||
<dt class="col-5">Status</dt>
|
<dt class="col-5">Status</dt>
|
||||||
<dd class="col-7">
|
<dd class="col-7">
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
{% if runtime.execution_mode == 'dry_run' %}
|
{% if runtime.execution_mode == 'dry_run' %}
|
||||||
<div class="alert alert-info">Diese Instanz erlaubt ausschließlich Dry-Runs.</div>
|
<div class="alert alert-info">Diese Instanz erlaubt ausschließlich Dry-Runs.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not form.has_installable_release %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
Keine freigegebene, kompatible Version verfügbar. Synchronisiere das Repository im Store-Adminbereich und gib danach das erzeugte Source-Artefakt unter „Release-Artefakte“ frei.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<form method="post" action="{% url 'plugins:netbox_plugin_store:lifecycle-action' slug=plugin.slug action=action %}">
|
<form method="post" action="{% url 'plugins:netbox_plugin_store:lifecycle-action' slug=plugin.slug action=action %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
@@ -30,7 +35,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<a class="btn btn-outline-secondary" href="{% url 'plugins:netbox_plugin_store:plugin-detail' slug=plugin.slug %}">Abbrechen</a>
|
<a class="btn btn-outline-secondary" href="{% url 'plugins:netbox_plugin_store:plugin-detail' slug=plugin.slug %}">Abbrechen</a>
|
||||||
<button class="btn btn-danger" type="submit">Bestätigen</button>
|
<button class="btn btn-danger" type="submit" {% if not form.has_installable_release %}disabled{% endif %}>Bestätigen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,12 @@
|
|||||||
{% if local_status and local_status.restart_required %}
|
{% if local_status and local_status.restart_required %}
|
||||||
<div class="alert alert-warning">Ein Neustart von NetBox und den Workern ist erforderlich.</div>
|
<div class="alert alert-warning">Ein Neustart von NetBox und den Workern ist erforderlich.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if plugin.approved and not has_installable_release %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
@@ -55,9 +61,9 @@
|
|||||||
<div class="list-group-item d-flex justify-content-between">
|
<div class="list-group-item d-flex justify-content-between">
|
||||||
<span>{{ item.release.version }}</span>
|
<span>{{ item.release.version }}</span>
|
||||||
{% if item.compatible and item.release.approved and item.release.immutable and item.release.sha256 %}
|
{% if item.compatible and item.release.approved and item.release.immutable and item.release.sha256 %}
|
||||||
<span class="badge bg-green">installierbar</span>
|
<span class="badge text-bg-success">installierbar</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">gesperrt</span>
|
<span class="badge text-bg-secondary">gesperrt</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.1"
|
__version__ = "0.1.2"
|
||||||
|
|||||||
@@ -83,12 +83,16 @@ class PluginDetailView(StorePermissionMixin, View):
|
|||||||
except StoreClientError as exc:
|
except StoreClientError as exc:
|
||||||
raise Http404(redact_text(exc)) from exc
|
raise Http404(redact_text(exc)) from exc
|
||||||
state = _plugin_state(plugin, set(django_settings.PLUGINS))
|
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] = []
|
actions: list[str] = []
|
||||||
if not state["installed"]:
|
if not state["installed"]:
|
||||||
if plugin.approved:
|
if plugin.approved and installable_releases:
|
||||||
actions.append("install")
|
actions.append("install")
|
||||||
else:
|
else:
|
||||||
if plugin.approved:
|
if plugin.approved and installable_releases:
|
||||||
actions.append("update")
|
actions.append("update")
|
||||||
if state["enabled"]:
|
if state["enabled"]:
|
||||||
actions.append("disable")
|
actions.append("disable")
|
||||||
@@ -107,7 +111,9 @@ class PluginDetailView(StorePermissionMixin, View):
|
|||||||
"state": state,
|
"state": state,
|
||||||
"actions": actions,
|
"actions": actions,
|
||||||
"releases": releases,
|
"releases": releases,
|
||||||
|
"has_installable_release": bool(installable_releases),
|
||||||
"local_status": local_status,
|
"local_status": local_status,
|
||||||
|
"runtime": runtime,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-plugin-store"
|
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"
|
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"
|
||||||
|
|||||||
@@ -58,6 +58,27 @@ class URLPolicyTests(unittest.TestCase):
|
|||||||
with self.assertRaises(StoreClientError):
|
with self.assertRaises(StoreClientError):
|
||||||
plugin.select_release("4.6.9")
|
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):
|
class _Response(io.BytesIO):
|
||||||
def __init__(self, body):
|
def __init__(self, body):
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ include dirname(__DIR__) . '/partials/head.php';
|
|||||||
<table class="admin-table release-admin-table">
|
<table class="admin-table release-admin-table">
|
||||||
<thead><tr><th>Plugin / Version</th><th>Artefakt</th><th>Integrität</th><th>Status</th><th>Aktionen</th></tr></thead>
|
<thead><tr><th>Plugin / Version</th><th>Artefakt</th><th>Integrität</th><th>Status</th><th>Aktionen</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if (($releases ?? []) === []): ?><tr><td colspan="5" class="empty-row">Keine installierbaren Artefakte gefunden. Prüfe Version und Build-Metadaten des Repositorys.</td></tr><?php endif; ?>
|
<?php if (($releases ?? []) === []): ?><tr><td colspan="5" class="empty-row">Keine installierbaren Artefakte gefunden. Bei Repositorys ohne Release erzeugt eine erneute Synchronisierung automatisch ein commitgebundenes Source-Artefakt, sofern das Python-Paket eine Version definiert.</td></tr><?php endif; ?>
|
||||||
<?php foreach (($releases ?? []) as $release): ?>
|
<?php foreach (($releases ?? []) as $release): ?>
|
||||||
<?php $releasePlugin = $release['plugin'] ?? []; $releaseErrors = Approval::releaseErrors($releasePlugin, $release); ?>
|
<?php $releasePlugin = $release['plugin'] ?? []; $releaseErrors = Approval::releaseErrors($releasePlugin, $release); ?>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user