Files
MrBlake 26aea40e6a
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
feat: install plugins from approved source commits
2026-08-24 21:37:17 +02:00

218 lines
7.2 KiB
Python

from __future__ import annotations
import hashlib
import json
import sys
import zipfile
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from netbox_store_agent.catalog import HttpStatusError, PluginMetadata, ReleasePlan
from netbox_store_agent.config import (
AgentSettings,
CommandSettings,
Config,
PathSettings,
PolicySettings,
StoreSettings,
)
from netbox_store_agent.errors import ExecutionError
from netbox_store_agent.runner import CommandResult
def make_config(root: Path, *, dry_run: bool = True, require_peers: bool = False) -> Config:
root = root.resolve()
managed_root = root / "netbox"
managed_root.mkdir(parents=True, exist_ok=True)
state = root / "state"
return Config(
agent=AgentSettings(
socket_path=root / "agent.sock",
journal_path=state / "journal.sqlite3",
lock_path=state / "lifecycle.lock",
backup_dir=state / "backups",
dry_run=dry_run,
require_root=False,
require_peer_credentials=require_peers,
allowed_peer_uids=(0,),
allowed_peer_gids=(0,),
socket_mode=0o660,
socket_uid=0,
socket_gid=0,
max_request_bytes=65536,
connection_timeout_seconds=1,
worker_threads=1,
),
store=StoreSettings(
base_url="http://store.test",
plugin_endpoint_template="/api/v1/plugins/{plugin_slug}",
release_endpoint_template="/api/v1/plugins/{plugin_slug}/releases/{version}",
timeout_seconds=1,
max_catalog_bytes=1024 * 1024,
max_artifact_bytes=1024 * 1024,
allow_private_addresses=True,
allow_http_for_testing=True,
allowed_hosts=("store.test", "artifacts.test"),
bearer_token_file=None,
ca_file=None,
),
paths=PathSettings(
allowed_root=managed_root,
include_path=managed_root / "store_plugins.py",
requirements_path=managed_root / "store_requirements.txt",
temp_dir=state / "tmp",
),
commands=CommandSettings(
python_path=root / "bin" / "python",
manage_path=managed_root / "manage.py",
systemctl_path=root / "bin" / "systemctl",
services=("netbox", "netbox-rq"),
command_timeout_seconds=10,
),
policy=PolicySettings(
netbox_version="4.6.8",
min_supported_netbox="4.6.5",
max_supported_netbox="4.6.8",
self_plugin_slugs=("netbox-store", "netbox-plugin-store", "netbox_plugin_store"),
allow_prereleases=False,
require_release_for_enable=True,
),
)
def release_json(version: str = "1.2.3", **overrides: Any) -> dict[str, Any]:
result: dict[str, Any] = {
"version": version,
"download_url": f"http://artifacts.test/demo_plugin-{version}-py3-none-any.whl",
"sha256": "a" * 64,
"artifact_size": 100,
"artifact_kind": "wheel",
"artifact_filename": f"demo_plugin-{version}-py3-none-any.whl",
"commit_sha": "",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8",
"published_at": None,
"approved": True,
"status": "approved",
"immutable": True,
"approved_payload_sha256": "c" * 64,
}
result.update(overrides)
return result
def plugin_json(releases: list[dict[str, Any]] | None = None, **overrides: Any) -> dict[str, Any]:
result: dict[str, Any] = {
"api_version": "v1",
"slug": "demo-plugin",
"name": "Demo",
"summary": "Summary",
"description": "Description",
"repository_url": "https://git.test/demo",
"latest_version": "1.2.3",
"package_name": "demo-plugin",
"import_name": "demo_plugin",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8",
"approved": True,
"status": "approved",
"releases": releases or [],
}
result.update(overrides)
return result
class FakeTransport:
def __init__(self, responses: dict[str, Any], artifact: bytes | None = None):
self.responses = responses
self.artifact = artifact or b""
self.json_urls: list[str] = []
self.download_urls: list[str] = []
def get_json(self, url: str, max_bytes: int) -> Any:
self.json_urls.append(url)
if url not in self.responses:
raise HttpStatusError(404, "missing")
value = self.responses[url]
if isinstance(value, Exception):
raise value
assert len(json.dumps(value)) <= max_bytes
return value
def download(
self,
url: str,
destination: Path,
*,
max_bytes: int,
expected_size: int,
expected_sha256: str,
) -> None:
self.download_urls.append(url)
if len(self.artifact) != expected_size:
raise AssertionError("fixture size mismatch")
if hashlib.sha256(self.artifact).hexdigest() != expected_sha256:
raise AssertionError("fixture digest mismatch")
destination.write_bytes(self.artifact)
def wheel_bytes(path: Path, version: str = "1.2.3") -> bytes:
wheel = path / f"demo_plugin-{version}-py3-none-any.whl"
with zipfile.ZipFile(wheel, "w") as archive:
archive.writestr("demo_plugin/__init__.py", "")
archive.writestr(
f"demo_plugin-{version}.dist-info/WHEEL",
"Wheel-Version: 1.0\nGenerator: tests\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
)
return wheel.read_bytes()
def plan(version: str = "1.2.3") -> ReleasePlan:
plugin = PluginMetadata(
"demo-plugin", "demo-plugin", "demo_plugin", "4.6.5", "4.6.8", ()
)
return ReleasePlan(
plugin,
version,
f"http://artifacts.test/demo_plugin-{version}-py3-none-any.whl",
f"demo_plugin-{version}-py3-none-any.whl",
"b" * 64,
1,
"c" * 64,
)
class FakeStore:
def __init__(self, release: ReleasePlan | None = None):
self.release = release or plan()
self.calls: list[tuple[str, ...]] = []
def get_plugin(self, slug: str) -> PluginMetadata:
self.calls.append(("plugin", slug))
return self.release.plugin
def get_release(self, slug: str, version: str) -> ReleasePlan:
self.calls.append(("release", slug, version))
return self.release
def download_release(self, release: ReleasePlan, directory: Path) -> Path:
self.calls.append(("download", release.version))
target = directory / release.filename
target.write_bytes(b"x")
return target
class FakeRunner:
def __init__(self, fail_step: str | None = None):
self.commands: list[tuple[str, ...]] = []
self.fail_step = fail_step
def run(self, argv: list[str]) -> CommandResult:
command = tuple(argv)
self.commands.append(command)
if self.fail_step and self.fail_step in command:
raise ExecutionError("injected command failure")
return CommandResult(command, "", "")