Files
Unraid-AppOps-Gateway-Public/backend/app/api/routes_playbooks.py
T

228 lines
11 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlmodel import Session, select
from ..db.models import Connector, Policy
from ..db.session import get_session
router = APIRouter(prefix='/api')
def _connector_names(connectors: list[Connector], ids: set[str]) -> list[str]:
return [connector.name for connector in connectors if connector.id in ids]
def _policy_risk(session: Session, action: str, target: str, fallback: str = 'medium') -> str:
policy = session.exec(select(Policy).where(Policy.action == action, Policy.target == target)).first()
if policy and policy.risk in {'low', 'medium', 'high', 'critical'}:
return policy.risk
return fallback
def _connector_labels(connectors: list[Connector], ids: set[str]) -> list[str]:
labels = _connector_names(connectors, ids)
known = {connector.id for connector in connectors}
for connector_id in sorted(ids - known):
labels.append(connector_id.replace('_', ' ').title())
return labels
def _success_rate(connectors: dict[str, Connector], ids: set[str], fallback: int = 80) -> int:
if not ids:
return fallback
present = [connectors.get(connector_id) for connector_id in ids]
connected = sum(1 for connector in present if connector and connector.status == 'connected')
return round((connected / len(ids)) * 100)
@router.get('/playbooks')
def list_playbooks(session: Session = Depends(get_session)):
connectors = session.exec(select(Connector).order_by(Connector.id)).all()
degraded = [
connector
for connector in connectors
if connector.status in {'missing_config', 'disconnected', 'error', 'auth_failed', 'unreachable'}
]
connector_by_id = {connector.id: connector for connector in connectors}
playbooks: list[dict] = []
playbooks.append({
'id': 'storage-pressure-review',
'name': 'Storage Pressure Review',
'description': 'Brengt array-, cache-, disk-, share- en container-mountdruk samen zonder storage-mutaties te starten.',
'risk': 'medium',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': _connector_labels(connectors, {'unraid', 'portainer'}),
'steps': [
'Open Storage & Volumes and review array/cache pressure',
'Inspect disks and critical shares above threshold',
'Compare Portainer mount hints with Unraid share usage',
'Open Safety or Remediation Planner for non-executing follow-up notes',
'Keep mover, prune and storage mutations outside this playbook',
],
'successRate': _success_rate(connector_by_id, {'unraid', 'portainer'}),
})
playbooks.append({
'id': 'exposure-auth-coverage-review',
'name': 'Exposure & Auth Coverage Review',
'description': 'Controleert NPM public hosts, SSL-force, certificaten en Authentik coverage als één securityroute.',
'risk': 'high',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': _connector_labels(connectors, {'nginx_proxy_manager', 'authentik'}),
'steps': [
'Open Exposure Center and sort high-risk public hosts first',
'Check SSL-force, certificate status and access-list evidence',
'Open matching Authentik coverage details for unprotected hosts',
'Create or review a remediation draft before touching external systems',
'Re-run Safety refresh after operator-side NPM/Auth changes',
],
'successRate': _success_rate(connector_by_id, {'nginx_proxy_manager', 'authentik'}),
})
playbooks.append({
'id': 'media-stalled-download-review',
'name': 'Media Pipeline Stalled Download Review',
'description': 'Volgt Sonarr/Radarr wanted items, Prowlarr health en Deluge stalled downloads zonder data te verwijderen.',
'risk': 'medium',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': _connector_labels(connectors, {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}),
'steps': [
'Open Media Pipeline summary and review stalled/wanted counters',
'Check Prowlarr indexers and Sonarr/Radarr queue status',
'Inspect Deluge stalled downloads and pause/resume policy guidance',
'Review Plex/Tautulli activity before starting scans',
'Use approval-gated actions only for non-destructive connector operations',
],
'successRate': _success_rate(connector_by_id, {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}),
})
playbooks.append({
'id': 'unhealthy-container-triage',
'name': 'Unhealthy Container Triage',
'description': 'Richt de operator op ongezonde of ongemapte containers met logs, Portainer deep-link en policy-aware restartpad.',
'risk': 'medium',
'allowedSources': ['manual'],
'requiredConnectors': _connector_labels(connectors, {'portainer'}),
'steps': [
'Open Containers with attention filter',
'Inspect health, import status, app mapping and redacted logs',
'Open Portainer deep-link only when live context is available',
'Request approval before restarting protected apps',
'Verify the container and app detail after any approved action',
],
'successRate': _success_rate(connector_by_id, {'portainer'}),
})
playbooks.append({
'id': 'app-inventory-normalization',
'name': 'App Inventory Normalization',
'description': 'Werkt Unknown, needs-review en no-connector apps systematisch weg zodat Apps minder ruis tonen.',
'risk': 'low',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': _connector_labels(connectors, {'portainer'}),
'steps': [
'Open Apps with attention filter',
'Review Unknown and needs-review app cards first',
'Map discovered containers to canonical apps where the match is clear',
'Bind connector candidates only after connector config has tested connected',
'Re-run inventory discovery and compare drift after review',
],
'successRate': _success_rate(connector_by_id, {'portainer'}),
})
playbooks.append({
'id': 'backup-readiness-check',
'name': 'Backup Readiness Check',
'description': 'Controleert SQLite backup posture, restore-notes en release hygiene zonder restore uit te voeren.',
'risk': 'low',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': ['AppOps DB'],
'steps': [
'Open Settings backups section',
'Review latest backup metadata and integrity status',
'Confirm restore remains manual stop-container flow',
'Open Audit for recent backup events',
'Update release notes after verified production changes',
],
'successRate': 100,
})
playbooks.append({
'id': 'notification-delivery-review',
'name': 'Notification Delivery Review',
'description': 'Controleert dashboard- en Home Assistant notificatiebereik zonder automatisch testberichten te verzenden.',
'risk': 'low',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': _connector_labels(connectors, {'home_assistant'}),
'steps': [
'Open Notifications and review active rules, channels and unread events',
'Validate Home Assistant readiness only when the operator enabled the channel',
'Run explicit evaluation for safety/media events when stale notifications appear',
'Keep external delivery opt-in and audited',
],
'successRate': _success_rate(connector_by_id, {'home_assistant'}, fallback=70),
})
if degraded:
degraded_names = [connector.name for connector in degraded]
playbooks.append({
'id': 'connector-readiness-review',
'name': 'Connector Readiness Review',
'description': 'Controleert welke connectors configuratie, binding of health-herstel nodig hebben zonder wijzigingen uit te voeren.',
'risk': 'medium',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': degraded_names,
'steps': [
f"Review {len(degraded)} degraded connector(s): {', '.join(degraded_names[:8])}",
'Open connector setup for missing_config entries',
'Run read-only connector tests',
'Review policy and audit status before any follow-up action',
],
'successRate': max(0, 100 - (len(degraded) * 10)),
})
if {'portainer', 'widefrog'} <= set(connector_by_id):
widefrog_ready = connector_by_id['widefrog'].status == 'connected'
portainer_ready = connector_by_id['portainer'].status == 'connected'
playbooks.append({
'id': 'fix-widefrog',
'name': 'Fix Widefrog',
'description': 'Bereidt een veilige Widefrog herstelprocedure voor met read-only checks en policy-gated vervolgstappen.',
'risk': _policy_risk(session, 'widefrog.retry_failed', 'widefrog', 'medium'),
'allowedSources': ['manual', 'voice'],
'requiredConnectors': _connector_names(connectors, {'portainer', 'widefrog'}),
'steps': [
'Read Portainer container status',
'Read Widefrog health, queue and failed jobs',
'Inspect recent redacted logs when available',
'Draft follow-up action only when policy allows it',
'Require approval for retry or restart when policy demands it',
'Write audit result after an approved execution path',
],
'successRate': 90 if widefrog_ready and portainer_ready else 50,
})
if 'unraid' in connector_by_id:
unraid_ready = connector_by_id['unraid'].status == 'connected'
playbooks.append({
'id': 'unraid-health-scan',
'name': 'Unraid Health Scan',
'description': 'Leest array, disk, cache en docker-status uit zonder muterende acties te starten.',
'risk': 'low',
'allowedSources': ['manual', 'automation'],
'requiredConnectors': _connector_names(connectors, {'unraid'}),
'steps': [
'Read array status',
'Read parity status',
'Check disk warning count',
'Check cache usage',
'Check docker service status',
'Write read-only audit summary',
],
'successRate': 100 if unraid_ready else 60,
})
return playbooks