Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,296 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
|
||||
from ..connectors.portainer import PortainerConnector
|
||||
from ..core.config import settings
|
||||
from ..core.redaction import redact_message
|
||||
from ..db.models import JobRun
|
||||
from ..services.approval_service import create_approval
|
||||
from ..services.audit_logger import log_event
|
||||
from ..services.job_runner import complete_job, create_job, start_job
|
||||
from ..services.policy_engine import evaluate_policy
|
||||
|
||||
|
||||
def _widefrog_health_endpoint() -> tuple[str | None, str | None]:
|
||||
raw_url = str(settings.apis.get("WIDEFROG_URL") or "").strip()
|
||||
if not raw_url:
|
||||
return None, "missing_config"
|
||||
|
||||
candidate = (
|
||||
raw_url if raw_url.startswith(("http://", "https://")) else f"http://{raw_url}"
|
||||
)
|
||||
try:
|
||||
parsed = urlparse(candidate)
|
||||
except Exception:
|
||||
return None, "invalid_widefrog_url"
|
||||
|
||||
host = parsed.hostname
|
||||
if not host:
|
||||
return None, "invalid_widefrog_url"
|
||||
|
||||
port = parsed.port or 1207
|
||||
return f"{parsed.scheme}://{host}:{port}/", None
|
||||
|
||||
|
||||
def _widefrog_health_check() -> dict[str, Any]:
|
||||
health_url, reason = _widefrog_health_endpoint()
|
||||
if not health_url:
|
||||
return {"status": "error", "reason": reason or "missing_config"}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=4.0, verify=settings.verify_tls) as client:
|
||||
response = client.get(health_url)
|
||||
except (httpx.RequestError, ValueError) as exc:
|
||||
return {"status": "error", "reason": redact_message(str(exc))}
|
||||
|
||||
if response.status_code >= 500:
|
||||
return {
|
||||
"status": "error",
|
||||
"reason": f"widefrog health check failed: HTTP {response.status_code}",
|
||||
}
|
||||
if response.status_code >= 400:
|
||||
return {
|
||||
"status": "warning",
|
||||
"reason": f"widefrog health returned HTTP {response.status_code}",
|
||||
}
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
def _format_success_result(
|
||||
summary: str,
|
||||
*,
|
||||
steps: list[str] | None = None,
|
||||
before_state: dict[str, Any] | None = None,
|
||||
after_state: dict[str, Any] | None = None,
|
||||
post_checks: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"success": True,
|
||||
"decision": "allowed",
|
||||
"status": "success",
|
||||
"policy_decision": "allowed",
|
||||
"result": "success",
|
||||
"message": summary,
|
||||
"plan": steps or [],
|
||||
"steps": steps or [],
|
||||
"before_state": before_state or {},
|
||||
"after_state": after_state or {},
|
||||
"post_checks": post_checks or [],
|
||||
}
|
||||
|
||||
|
||||
def _format_blocked_result(audit_id: str, reason: str) -> dict[str, Any]:
|
||||
return {
|
||||
"success": False,
|
||||
"decision": "blocked",
|
||||
"status": "failed",
|
||||
"policy_decision": "blocked",
|
||||
"result": "blocked",
|
||||
"message": reason,
|
||||
"auditId": audit_id,
|
||||
}
|
||||
|
||||
|
||||
def _format_approval_result(
|
||||
audit_id: str, approval_id: str, reason: str
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"success": False,
|
||||
"decision": "approval_required",
|
||||
"status": "failed",
|
||||
"policy_decision": "approval_required",
|
||||
"result": "approval_required",
|
||||
"message": reason,
|
||||
"approvalId": approval_id,
|
||||
"auditId": audit_id,
|
||||
}
|
||||
|
||||
|
||||
def execute_container_restart(
|
||||
*,
|
||||
session,
|
||||
action: str,
|
||||
target: str,
|
||||
source: str = "manual",
|
||||
risk: str = "medium",
|
||||
steps: list[str] | None = None,
|
||||
approved: bool = False,
|
||||
policy_override: dict[str, Any] | None = None,
|
||||
):
|
||||
plan_steps = steps or [
|
||||
"Resolve container",
|
||||
"Restart container",
|
||||
"Post-check health",
|
||||
]
|
||||
if policy_override is not None:
|
||||
decision = {
|
||||
"decision": str(policy_override.get("decision") or "blocked"),
|
||||
"reason": str(policy_override.get("reason") or "Policy override."),
|
||||
"risk": str(policy_override.get("risk") or risk),
|
||||
}
|
||||
elif approved:
|
||||
decision = {
|
||||
"decision": "allowed",
|
||||
"reason": "Exact action context was approved by an AppOps operator.",
|
||||
"risk": risk,
|
||||
}
|
||||
else:
|
||||
decision = evaluate_policy(session, action=action, target=target, source=source)
|
||||
|
||||
if decision["decision"] == "blocked":
|
||||
audit = log_event(
|
||||
session,
|
||||
source="action",
|
||||
identity=source,
|
||||
action_id=action,
|
||||
target=target,
|
||||
risk=risk,
|
||||
policy_decision=decision["decision"],
|
||||
reason=decision["reason"],
|
||||
result="blocked",
|
||||
execution_plan={"plan": plan_steps, "policyReason": decision["reason"]},
|
||||
)
|
||||
return _format_blocked_result(audit.id, decision["reason"])
|
||||
|
||||
if decision["decision"] == "approval_required":
|
||||
approval = create_approval(
|
||||
session,
|
||||
action=action,
|
||||
target=target,
|
||||
source=source,
|
||||
risk=risk,
|
||||
reason=decision["reason"],
|
||||
requester=source,
|
||||
)
|
||||
audit = log_event(
|
||||
session,
|
||||
source="action",
|
||||
identity=source,
|
||||
action_id=action,
|
||||
target=target,
|
||||
risk=risk,
|
||||
policy_decision=decision["decision"],
|
||||
reason=decision["reason"],
|
||||
result="approval_requested",
|
||||
execution_plan={"plan": plan_steps, "approvalId": approval.id},
|
||||
redacted_metadata={"approval_id": approval.id},
|
||||
)
|
||||
return _format_approval_result(
|
||||
audit.id, approval.id, "Action requires approval before execution."
|
||||
)
|
||||
|
||||
# Allowed restart flow
|
||||
job: JobRun = create_job(
|
||||
session,
|
||||
action_id=action,
|
||||
target=target,
|
||||
source=source,
|
||||
risk=risk,
|
||||
policy_decision=decision["decision"],
|
||||
steps=plan_steps,
|
||||
)
|
||||
start_job(session, job)
|
||||
|
||||
try:
|
||||
connector = PortainerConnector()
|
||||
|
||||
precheck = connector.get_container_status(target)
|
||||
if precheck.get("status") != "ok":
|
||||
# keep execution context in logs while still allowing best-effort continuation
|
||||
precheck = {
|
||||
"status": "warn",
|
||||
"container": {"name": target},
|
||||
"reason": precheck.get("reason"),
|
||||
}
|
||||
|
||||
restart_result = connector.restart_container(target)
|
||||
if restart_result.get("status") != "ok":
|
||||
raise RuntimeError(
|
||||
str(restart_result.get("reason", "container_restart_failed"))
|
||||
)
|
||||
|
||||
post_check = {"container": restart_result.get("container", target)}
|
||||
if target == "widefrog":
|
||||
health = _widefrog_health_check()
|
||||
post_checks = [
|
||||
f"widefrog health: {health.get('status')}",
|
||||
f"widefrog reason: {health.get('reason', '')}".strip(),
|
||||
]
|
||||
post_check["widefrog_health"] = health
|
||||
else:
|
||||
post_checks = []
|
||||
health = {}
|
||||
|
||||
completed = complete_job(
|
||||
session,
|
||||
job,
|
||||
status="succeeded",
|
||||
result_summary="Container restart executed through Portainer connector.",
|
||||
)
|
||||
|
||||
audit = log_event(
|
||||
session,
|
||||
source="action",
|
||||
identity=source,
|
||||
action_id=action,
|
||||
target=target,
|
||||
risk=risk,
|
||||
policy_decision=decision["decision"],
|
||||
reason=decision["reason"],
|
||||
result="success",
|
||||
execution_plan={
|
||||
"plan": plan_steps,
|
||||
"jobId": completed.id,
|
||||
"precheck": precheck,
|
||||
},
|
||||
before_state=precheck,
|
||||
after_state=post_check,
|
||||
post_checks=health if target == "widefrog" else None,
|
||||
)
|
||||
|
||||
result = _format_success_result(
|
||||
"Container restart executed.",
|
||||
steps=plan_steps,
|
||||
before_state=precheck,
|
||||
after_state=post_check,
|
||||
post_checks=post_checks,
|
||||
)
|
||||
result["jobId"] = completed.id
|
||||
result["auditId"] = audit.id
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
failed = complete_job(
|
||||
session,
|
||||
job,
|
||||
status="failed",
|
||||
result_summary=f"Container restart failed: {redact_message(str(exc))}",
|
||||
)
|
||||
audit = log_event(
|
||||
session,
|
||||
source="action",
|
||||
identity=source,
|
||||
action_id=action,
|
||||
target=target,
|
||||
risk=risk,
|
||||
policy_decision=decision["decision"],
|
||||
reason="Container restart execution failed.",
|
||||
result="failed",
|
||||
execution_plan={"plan": plan_steps, "jobId": failed.id},
|
||||
after_state={"status": "failed", "reason": redact_message(str(exc))},
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"decision": "failed",
|
||||
"status": "failed",
|
||||
"policy_decision": decision["decision"],
|
||||
"result": "failed",
|
||||
"message": redact_message(str(exc)),
|
||||
"jobId": failed.id,
|
||||
"auditId": audit.id,
|
||||
}
|
||||
Reference in new issue
Block a user