|
# retoor <retoor@molodetz.nl>
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from devplacepy.database import db, get_table
|
|
from devplacepy.utils import generate_uid, make_combined_slug
|
|
|
|
ST_CREATED = "created"
|
|
ST_STARTING = "starting"
|
|
ST_RUNNING = "running"
|
|
ST_STOPPED = "stopped"
|
|
ST_PAUSED = "paused"
|
|
ST_CRASHED = "crashed"
|
|
ST_RESTARTING = "restarting"
|
|
ST_REMOVING = "removing"
|
|
ST_REMOVED = "removed"
|
|
|
|
DESIRED_RUNNING = "running"
|
|
DESIRED_STOPPED = "stopped"
|
|
DESIRED_PAUSED = "paused"
|
|
|
|
RESTART_POLICIES = ("never", "always", "on-failure", "unless-stopped")
|
|
METRICS_RING = 720
|
|
|
|
|
|
def now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _exists(table: str) -> bool:
|
|
return table in db.tables
|
|
|
|
|
|
# ---------------- instances ----------------
|
|
|
|
|
|
def create_instance(row: dict) -> dict:
|
|
uid = generate_uid()
|
|
base = {
|
|
"uid": uid,
|
|
"slug": make_combined_slug(row.get("name", "instance"), uid),
|
|
"container_id": "",
|
|
"status": ST_CREATED,
|
|
"exit_code": 0,
|
|
"restart_count": 0,
|
|
"ingress_slug": "",
|
|
"ingress_port": 0,
|
|
"started_at": "",
|
|
"stopped_at": "",
|
|
"created_at": now(),
|
|
"updated_at": now(),
|
|
}
|
|
base.update(row)
|
|
base["uid"] = uid
|
|
get_table("instances").insert(base)
|
|
return get_instance(uid)
|
|
|
|
|
|
def get_instance(uid: str):
|
|
table = get_table("instances")
|
|
if not _exists("instances"):
|
|
return None
|
|
return table.find_one(uid=uid) or table.find_one(slug=uid)
|
|
|
|
|
|
def list_instances(project_uid: str = None) -> list:
|
|
if not _exists("instances"):
|
|
return []
|
|
filters = {}
|
|
if project_uid:
|
|
filters["project_uid"] = project_uid
|
|
return sorted(
|
|
get_table("instances").find(**filters),
|
|
key=lambda r: r.get("created_at", ""),
|
|
reverse=True,
|
|
)
|
|
|
|
|
|
def all_instances() -> list:
|
|
if not _exists("instances"):
|
|
return []
|
|
return list(get_table("instances").find())
|
|
|
|
|
|
def find_instance_by_ingress(slug: str):
|
|
if not slug or not _exists("instances"):
|
|
return None
|
|
matches = list(get_table("instances").find(ingress_slug=slug))
|
|
for instance in matches:
|
|
if instance.get("status") == ST_RUNNING:
|
|
return instance
|
|
return matches[0] if matches else None
|
|
|
|
|
|
def update_instance(uid: str, changes: dict) -> None:
|
|
get_table("instances").update({"uid": uid, "updated_at": now(), **changes}, ["uid"])
|
|
|
|
|
|
def delete_instance(uid: str) -> None:
|
|
get_table("instances").delete(uid=uid)
|
|
|
|
|
|
# ---------------- events / metrics / schedules ----------------
|
|
|
|
|
|
def record_event(
|
|
instance: dict, event: str, actor_kind: str, actor_id: str, detail: dict = None
|
|
) -> None:
|
|
get_table("instance_events").insert(
|
|
{
|
|
"uid": generate_uid(),
|
|
"instance_uid": instance["uid"],
|
|
"project_uid": instance.get("project_uid", ""),
|
|
"event": event,
|
|
"actor_kind": actor_kind,
|
|
"actor_id": actor_id or "",
|
|
"detail": json.dumps(detail or {}),
|
|
"created_at": now(),
|
|
}
|
|
)
|
|
|
|
|
|
def list_events(instance_uid: str, limit: int = 100) -> list:
|
|
if not _exists("instance_events"):
|
|
return []
|
|
rows = sorted(
|
|
get_table("instance_events").find(instance_uid=instance_uid),
|
|
key=lambda r: r.get("created_at", ""),
|
|
reverse=True,
|
|
)
|
|
return rows[:limit]
|
|
|
|
|
|
def insert_metric(instance_uid: str, sample) -> None:
|
|
table = get_table("instance_metrics")
|
|
table.insert(
|
|
{
|
|
"uid": generate_uid(),
|
|
"instance_uid": instance_uid,
|
|
"ts": now(),
|
|
"cpu_pct": sample.cpu_pct,
|
|
"mem_bytes": sample.mem_bytes,
|
|
"mem_pct": sample.mem_pct,
|
|
"net_rx": sample.net_rx,
|
|
"net_tx": sample.net_tx,
|
|
"blk_read": sample.blk_read,
|
|
"blk_write": sample.blk_write,
|
|
}
|
|
)
|
|
rows = sorted(table.find(instance_uid=instance_uid), key=lambda r: r.get("ts", ""))
|
|
if len(rows) > METRICS_RING:
|
|
for old in rows[: len(rows) - METRICS_RING]:
|
|
table.delete(uid=old["uid"])
|
|
|
|
|
|
def recent_metrics(instance_uid: str, limit: int = 120) -> list:
|
|
if not _exists("instance_metrics"):
|
|
return []
|
|
rows = sorted(
|
|
get_table("instance_metrics").find(instance_uid=instance_uid),
|
|
key=lambda r: r.get("ts", ""),
|
|
)
|
|
return rows[-limit:]
|
|
|
|
|
|
def create_schedule(
|
|
instance: dict, action: str, schedule_columns: dict, next_run_at: str
|
|
) -> dict:
|
|
uid = generate_uid()
|
|
get_table("instance_schedules").insert(
|
|
{
|
|
"uid": uid,
|
|
"instance_uid": instance["uid"],
|
|
"project_uid": instance.get("project_uid", ""),
|
|
"action": action,
|
|
"schedule_json": json.dumps(schedule_columns),
|
|
"enabled": 1,
|
|
"next_run_at": next_run_at,
|
|
"last_run_at": "",
|
|
"run_count": 0,
|
|
"created_at": now(),
|
|
"updated_at": now(),
|
|
}
|
|
)
|
|
return get_table("instance_schedules").find_one(uid=uid)
|
|
|
|
|
|
def list_schedules(instance_uid: str) -> list:
|
|
if not _exists("instance_schedules"):
|
|
return []
|
|
return list(get_table("instance_schedules").find(instance_uid=instance_uid))
|
|
|
|
|
|
def due_schedules(now_iso: str) -> list:
|
|
if not _exists("instance_schedules"):
|
|
return []
|
|
return [
|
|
r
|
|
for r in get_table("instance_schedules").find(enabled=1)
|
|
if r.get("next_run_at") and r["next_run_at"] <= now_iso
|
|
]
|
|
|
|
|
|
def update_schedule(uid: str, changes: dict) -> None:
|
|
get_table("instance_schedules").update(
|
|
{"uid": uid, "updated_at": now(), **changes}, ["uid"]
|
|
)
|
|
|
|
|
|
def delete_schedule(uid: str) -> None:
|
|
get_table("instance_schedules").delete(uid=uid)
|