Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
"""Per-device-type connection and read-only diagnostic playbooks.
|
|
|
|
Each entry maps a DeviceType to:
|
|
- mcp_server: default MCP server that drives the device
|
|
- connect: (tool_name, arg_builder) to establish a session (optional)
|
|
- diagnostics: list of (tool_name, arg_builder) read-only calls to gather state
|
|
|
|
arg_builder is a callable(device_ctx) -> dict, where device_ctx contains
|
|
address, port, username, secret.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
|
|
from app.agent.asterisk_profiles import ASTERISK_PLAYBOOKS
|
|
from app.agent.device_connect import pfsense_connect_args, ssh_connect_args
|
|
from app.models.enums import DeviceType
|
|
|
|
ArgBuilder = Callable[[dict], dict]
|
|
|
|
|
|
def _ip(ctx: dict) -> dict:
|
|
return {"ip": ctx["address"]}
|
|
|
|
|
|
def _pfsense_connect(ctx: dict) -> dict:
|
|
return pfsense_connect_args(ctx)
|
|
|
|
|
|
def _ssh(ctx: dict) -> dict:
|
|
return ssh_connect_args(ctx)
|
|
|
|
|
|
PLAYBOOKS: dict[DeviceType, dict] = {
|
|
DeviceType.pfsense: {
|
|
"mcp_server": "pfsense-mcp",
|
|
"connect": ("pfsense_connect", _pfsense_connect),
|
|
"diagnostics": [
|
|
("pfsense_get_system_status", lambda c: {}),
|
|
("pfsense_list_gateways", lambda c: {}),
|
|
("pfsense_get_gateway_status", lambda c: {}),
|
|
("pfsense_list_interfaces", lambda c: {}),
|
|
],
|
|
},
|
|
DeviceType.proxmox: {
|
|
# Connect + diagnostics handled by proxmox_profiles.run_proxmox_diagnostics (API → SSH).
|
|
"mcp_server": "proxmox-mcp",
|
|
"connect": None,
|
|
"diagnostics": [],
|
|
},
|
|
DeviceType.asterisk: {
|
|
# Legacy key — prefer asterisk_geneseasx catalog slot (see ASTERISK_PLAYBOOKS).
|
|
"mcp_server": "ssh-generic-mcp",
|
|
"connect": ("ssh_connect", _ssh),
|
|
"diagnostics": [],
|
|
},
|
|
# GeneseasX / satbox variants are resolved by catalog_key in playbook_for().
|
|
DeviceType.debian: {
|
|
"mcp_server": "ssh-generic-mcp",
|
|
"connect": (
|
|
"ssh_connect",
|
|
lambda c: ssh_connect_args(c),
|
|
),
|
|
"diagnostics": [
|
|
("ssh_run", lambda c: {"command": "uptime && uname -a"}),
|
|
("ssh_run", lambda c: {"command": "df -h"}),
|
|
("ssh_run", lambda c: {"command": "free -m"}),
|
|
("ssh_run", lambda c: {"command": "systemctl --failed --no-pager"}),
|
|
("ssh_run", lambda c: {"command": "journalctl -p err -n 100 --no-pager"}),
|
|
],
|
|
},
|
|
DeviceType.geneseasx: {
|
|
"mcp_server": "ssh-generic-mcp",
|
|
"connect": (
|
|
"ssh_connect",
|
|
lambda c: ssh_connect_args(c),
|
|
),
|
|
"diagnostics": [
|
|
("ssh_run", lambda c: {"command": "uptime && uname -a"}),
|
|
("ssh_run", lambda c: {"command": "docker ps --format '{{.Names}}: {{.Status}}'"}),
|
|
("ssh_run", lambda c: {"command": "systemctl --failed --no-pager"}),
|
|
],
|
|
},
|
|
DeviceType.fortigate: {
|
|
"mcp_server": "fortigate-mcp",
|
|
"connect": (
|
|
"fortigate_connect",
|
|
lambda c: {
|
|
"host": c["address"],
|
|
"api_key": c.get("secret"),
|
|
"username": c.get("username"),
|
|
},
|
|
),
|
|
"diagnostics": [
|
|
("fortigate_get_system_status", lambda c: {}),
|
|
("fortigate_list_interfaces", lambda c: {}),
|
|
("fortigate_list_policies", lambda c: {}),
|
|
("fortigate_get_logs", lambda c: {"lines": 100}),
|
|
],
|
|
},
|
|
DeviceType.fortiswitch: {
|
|
"mcp_server": "fortiswitch-mcp",
|
|
"connect": (
|
|
"fortiswitch_connect",
|
|
lambda c: {
|
|
"host": c["address"],
|
|
"username": c.get("username"),
|
|
"password": c.get("secret"),
|
|
},
|
|
),
|
|
"diagnostics": [
|
|
("fortiswitch_get_system_status", lambda c: {}),
|
|
("fortiswitch_list_ports", lambda c: {}),
|
|
("fortiswitch_get_port_stats", lambda c: {}),
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def playbook_for(
|
|
device_type: DeviceType,
|
|
mcp_override: str | None,
|
|
catalog_key: str | None = None,
|
|
) -> dict | None:
|
|
if catalog_key and catalog_key in ASTERISK_PLAYBOOKS:
|
|
pb = ASTERISK_PLAYBOOKS[catalog_key]
|
|
else:
|
|
pb = PLAYBOOKS.get(device_type)
|
|
if pb is None:
|
|
return None
|
|
# Catalog playbooks define the MCP server; ignore stale device-row overrides.
|
|
if mcp_override and catalog_key not in ASTERISK_PLAYBOOKS and device_type != DeviceType.proxmox:
|
|
pb = {**pb, "mcp_server": mcp_override}
|
|
return pb
|