utp_service/backend/app/plugins/loader.py
2026-05-05 11:32:25 +03:00

116 lines
4.3 KiB
Python

"""
PluginLoader — registers action plugins at startup and syncs them with the DB.
Startup flow:
1. For each builtin action, INSERT into `plugins` table (skip if already exists).
This preserves the `enabled` flag set by admins.
2. Load the list of enabled plugin names from DB.
3. Register only enabled actions in the registry.
Hot enable/disable (via Admin API):
- Disable: set DB.enabled=False, call action_registry.unregister(name)
- Enable: set DB.enabled=True, call action_registry.register(BUILTIN_ACTIONS[name])
"""
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from app.models.plugin import Plugin
from app.plugins.base import BaseAction, CompositeAction
from app.plugins.builtin.camera.get_status import CameraGetStatusAction
from app.plugins.builtin.camera.set_ntp import CameraSetNTPAction
from app.plugins.builtin.common.ping import PingAction
from app.plugins.builtin.mikrotik.get_config import MikrotikGetConfigAction
from app.plugins.builtin.mikrotik.get_ntp import MikrotikGetNTPAction
from app.plugins.builtin.mikrotik.set_ntp import MikrotikSetNTPAction
from app.plugins.builtin.ssh.debian_system_info import DebianSystemInfoAction
from app.plugins.builtin.ssh.disk_usage import DiskUsageAction
from app.plugins.builtin.ssh.get_file import GetFileAction
from app.plugins.builtin.ssh.get_timedatectl import GetTimedatectlAction
from app.plugins.builtin.ssh.ssh_command import SSHCommandAction
from app.plugins.registry import action_registry
from app.services.database import AsyncSessionLocal
# Shared sub-action instances reused inside composite actions
_timedatectl = GetTimedatectlAction()
_mikrotik_get_ntp = MikrotikGetNTPAction()
_camera_get_status = CameraGetStatusAction()
# Composite: Get NTP Info — routes by device category
_get_ntp_info = CompositeAction(
name="get_ntp_info",
display_name="Get NTP Info",
routing={
"main_server": _timedatectl,
"vm": _timedatectl,
"server": _timedatectl,
"raspberry": _timedatectl,
"embedded": _timedatectl,
"router": _mikrotik_get_ntp,
"mikrotik": _mikrotik_get_ntp,
"camera": _camera_get_status,
},
)
# Public dict — used by Admin router for hot enable/disable
BUILTIN_ACTIONS: dict[str, BaseAction] = {
a.name: a
for a in [
PingAction(),
SSHCommandAction(),
DebianSystemInfoAction(),
DiskUsageAction(),
_timedatectl,
GetFileAction(),
MikrotikGetConfigAction(),
_mikrotik_get_ntp,
MikrotikSetNTPAction(),
_camera_get_status,
CameraSetNTPAction(),
_get_ntp_info,
]
}
async def load_plugins() -> None:
now = datetime.now(timezone.utc)
# Insert new plugins with code defaults; preserve existing DB rows (including user edits)
async with AsyncSessionLocal() as db:
async with db.begin():
for action in BUILTIN_ACTIONS.values():
stmt = (
pg_insert(Plugin)
.values(
name=action.name,
display_name=action.display_name,
description=(action.__class__.__doc__ or "").strip() or None,
enabled=True,
registered_at=now,
compatible_categories=action.compatible_categories,
)
.on_conflict_do_nothing(index_elements=["name"])
)
await db.execute(stmt)
# Load all plugins from DB; apply stored categories to action instances
async with AsyncSessionLocal() as db:
result = await db.execute(
select(Plugin.name, Plugin.enabled, Plugin.compatible_categories)
)
rows = result.all()
enabled_count = 0
for name, enabled, categories in rows:
action = BUILTIN_ACTIONS.get(name)
if action is None:
continue
# Override in-memory categories with whatever is stored in DB
action.compatible_categories = categories or []
if enabled:
action_registry.register(action)
enabled_count += 1
print(f"[plugins] Registered {enabled_count}/{len(BUILTIN_ACTIONS)} plugin(s)")