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
+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`:
```text
netbox-plugin-store==0.1.2
netbox-plugin-store==0.1.3
```
Add the plugin to `configuration.py`:
+40 -15
View File
@@ -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
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]
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"
+39 -15
View File
@@ -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)