mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-28 03:51:08 -04:00
BE+FE+DOCS: run plugin from custom props/actions
This commit is contained in:
1 parent
cddb237cd3
commit
4d0175c660
5 files changed
+132
-6
No files matched your search
@@ -35,10 +35,11 @@ Visible properties (`CUSTPROP_show: true`) are displayed as interactive icons in
|
||||
- Manage devices with actions like delete.
|
||||
- Example: Quickly remove a device from the network.
|
||||
|
||||
4. **Plugins**:
|
||||
4. **Plugins (Experimental 🧪)**:
|
||||
|
||||
- Future placeholder for running custom plugin scripts.
|
||||
- **Note**: Not implemented yet.
|
||||
- Manually trigger an on-demand run of a plugin, regardless of its configured `RUN` schedule.
|
||||
- Example: Add a button to re-run a scan-type plugin like `NMAPDEV` on demand.
|
||||
- **Note**: The plugin runs with whatever settings/params it already has configured - it is not passed any device-specific or ad-hoc arguments. Some plugins require settings (credentials, target host, subnet, etc.) to be configured before they'll run properly; triggering an unconfigured plugin this way may fail or do nothing useful.
|
||||
|
||||
---
|
||||
|
||||
@@ -56,6 +57,10 @@ Visible properties (`CUSTPROP_show: true`) are displayed as interactive icons in
|
||||
|
||||
- Enable device removal functionality using `CUSTPROP_type: delete_dev`.
|
||||
|
||||
4. **Run a Plugin On Demand (Experimental 🧪)**:
|
||||
|
||||
- Use `CUSTPROP_type: run_plugin` with `CUSTPROP_args` set to the target plugin's unique prefix (e.g. `NMAPDEV`) to add a button that triggers that plugin immediately. Make sure the plugin's own settings (credentials, target host, subnet, etc.) are already configured, since none are passed in via this action.
|
||||
|
||||
---
|
||||
|
||||
## Defining Custom Properties
|
||||
@@ -97,14 +102,14 @@ Commonly useful fields: `devMac`, `devLastIP`, `devName`, `devVendor`, `devType`
|
||||
- **Link**: Redirects to a specified URL in the current browser tab. (**Arguments** Needs to contain the full URL.)
|
||||
- **Link (New Tab)**: Opens a specified URL in a new browser tab. (**Arguments** Needs to contain the full URL.)
|
||||
- **Delete Device**: Deletes the device using its MAC address.
|
||||
- **Run Plugin**: Placeholder for executing custom plugins (not implemented yet).
|
||||
- **Run Plugin (Experimental 🧪)**: Triggers an on-demand run of the plugin named in **Arguments** (its unique prefix, e.g. `NMAPDEV`), regardless of that plugin's configured `RUN` schedule.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Plugin Functionality**: The `run_plugin` action type is currently not implemented and will show an alert if used.
|
||||
- **Plugin Functionality (Experimental 🧪)**: `run_plugin` requires `CUSTPROP_args` to exactly match an enabled plugin's unique prefix (as listed in the `LOADED_PLUGINS` setting); an unrecognized or disabled prefix is rejected by the backend. It simply re-runs the plugin with its existing configuration - some plugins need required settings/params (e.g. credentials, target host, subnet) filled in first, or the run will fail or silently do nothing. Marked experimental until this is more clearly surfaced in the UI.
|
||||
- **Custom Icons (Experimental 🧪)**: Use Base64-encoded HTML to provide custom icons for each property. You can add your icons in Setttings via the `CUSTPROP_icon` settings
|
||||
- **Visibility Control**: Only properties with `CUSTPROP_show: true` will appear on the listing page.
|
||||
|
||||
|
||||
+1
-1
@@ -1308,7 +1308,7 @@ $(document).on('click', '.devicePropAction', function () {
|
||||
openInNewTab(args);
|
||||
break;
|
||||
case "run_plugin":
|
||||
alert('Not implemented');
|
||||
runPlugin(args, name);
|
||||
break;
|
||||
case "delete_dev":
|
||||
askDeleteDeviceByMac(mac);
|
||||
|
||||
@@ -43,4 +43,28 @@ function deleteDeviceByMac(mac) {
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Manually trigger an on-demand plugin run, regardless of its configured RUN
|
||||
// schedule. Used by the "run_plugin" device custom property action, where
|
||||
// `prefix` is the plugin's unique_prefix (e.g. NMAPDEV) supplied via CUSTPROP_args.
|
||||
function runPlugin(prefix, name) {
|
||||
const apiBase = getApiBase();
|
||||
const apiToken = getSetting("API_TOKEN");
|
||||
const url = `${apiBase}/plugin/${encodeURIComponent(prefix)}/run`;
|
||||
const label = name || prefix;
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||
success: function(response) {
|
||||
showMessage(response.success ? `Run triggered for ${label}` : (response.error || "Unknown error"));
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error running plugin:", status, error);
|
||||
showMessage("Error: " + (xhr.responseJSON?.error || error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2089,6 +2089,36 @@ def api_plugin_stats(payload=None):
|
||||
return jsonify({"success": True, "data": data})
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Plugin Run endpoint
|
||||
# --------------------------
|
||||
@app.route("/plugin/<prefix>/run", methods=["POST"])
|
||||
@validate_request(
|
||||
operation_id="run_plugin",
|
||||
summary="Run Plugin",
|
||||
description="Manually trigger an on-demand run of a plugin by its unique prefix, regardless of its "
|
||||
"configured RUN schedule (e.g. used by the 'Run Plugin' device custom property action).",
|
||||
path_params=[{
|
||||
"name": "prefix",
|
||||
"description": "Plugin unique prefix (e.g. NMAPDEV, ARPSCAN)",
|
||||
"schema": {"type": "string"}
|
||||
}],
|
||||
response_model=BaseResponse,
|
||||
tags=["plugins"],
|
||||
validation_error_code=400,
|
||||
auth_callable=is_authorized
|
||||
)
|
||||
def api_run_plugin(prefix, payload=None):
|
||||
loaded_plugins = get_setting_value('LOADED_PLUGINS')
|
||||
if prefix not in loaded_plugins:
|
||||
return jsonify({"success": False, "error": f"Invalid plugin. Must be one of: {', '.join(loaded_plugins)}"}), 400
|
||||
|
||||
queue = UserEventsQueueInstance()
|
||||
queue.add_event(f"run|{prefix}")
|
||||
|
||||
return jsonify({"success": True, "message": f"Run triggered for plugin: {prefix}"}), 200
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Background Server Start
|
||||
# --------------------------
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Tests for /plugin/<prefix>/run endpoint."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from helper import get_setting_value # noqa: E402
|
||||
from api_server.api_server_start import app # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_token():
|
||||
return get_setting_value("API_TOKEN")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
|
||||
def auth_headers(token):
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_run_plugin_unauthorized(client):
|
||||
"""Missing token should be forbidden."""
|
||||
resp = client.post("/plugin/ARPSCAN/run")
|
||||
assert resp.status_code == 403
|
||||
assert resp.get_json().get("success") is False
|
||||
|
||||
|
||||
@patch("api_server.api_server_start.UserEventsQueueInstance")
|
||||
def test_run_plugin_success(mock_queue_class, client, api_token):
|
||||
"""A known, loaded plugin prefix queues a run event."""
|
||||
mock_queue = MagicMock()
|
||||
mock_queue_class.return_value = mock_queue
|
||||
|
||||
loaded_plugins = get_setting_value("LOADED_PLUGINS")
|
||||
prefix = loaded_plugins[0]
|
||||
|
||||
resp = client.post(f"/plugin/{prefix}/run", headers=auth_headers(api_token))
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["success"] is True
|
||||
mock_queue.add_event.assert_called_once()
|
||||
call_args = mock_queue.add_event.call_args[0]
|
||||
assert f"run|{prefix}" in call_args[0]
|
||||
|
||||
|
||||
@patch("api_server.api_server_start.UserEventsQueueInstance")
|
||||
def test_run_plugin_invalid_prefix(mock_queue_class, client, api_token):
|
||||
"""An unknown plugin prefix is rejected before touching the queue."""
|
||||
mock_queue = MagicMock()
|
||||
mock_queue_class.return_value = mock_queue
|
||||
|
||||
resp = client.post("/plugin/NOT_A_REAL_PLUGIN/run", headers=auth_headers(api_token))
|
||||
|
||||
assert resp.status_code == 400
|
||||
data = resp.get_json()
|
||||
assert data["success"] is False
|
||||
mock_queue.add_event.assert_not_called()
|
||||
Reference in new issue
Block a user