""" 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.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(), _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)")