feat(ollama): add Ollama local LLM (.27) card & remote SSH control to service-controller
This commit is contained in:
@@ -84,21 +84,29 @@ def _container_state(target: str) -> str:
|
|||||||
return out or ("unknown" if r.returncode != 0 else "unknown")
|
return out or ("unknown" if r.returncode != 0 else "unknown")
|
||||||
|
|
||||||
|
|
||||||
def _systemd_user_state(target: str) -> str:
|
def _systemd_user_state(target: str, remote: str | None = None) -> str:
|
||||||
r = subprocess.run(["systemctl", "--user", "is-active", target],
|
cmd = ["systemctl", "--user", "is-active", target]
|
||||||
capture_output=True, text=True)
|
if remote:
|
||||||
return (r.stdout.strip() or "inactive").lower()
|
cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", remote] + cmd
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
out = (r.stdout.strip() or "inactive").lower()
|
||||||
|
if out == "active":
|
||||||
|
return "running"
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _do(kind: str, action: str, target: str) -> str:
|
def _do(kind: str, action: str, target: str, remote: str | None = None) -> str:
|
||||||
"""Run start/stop. Returns the state after a short settle."""
|
"""Run start/stop. Returns the state after a short settle."""
|
||||||
if kind == "container":
|
if kind == "container":
|
||||||
subprocess.run(["docker", action, target], capture_output=True, text=True)
|
subprocess.run(["docker", action, target], capture_output=True, text=True)
|
||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
return _container_state(target)
|
return _container_state(target)
|
||||||
subprocess.run(["systemctl", "--user", action, target], capture_output=True, text=True)
|
cmd = ["systemctl", "--user", action, target]
|
||||||
|
if remote:
|
||||||
|
cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", remote] + cmd
|
||||||
|
subprocess.run(cmd, capture_output=True, text=True)
|
||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
return _systemd_user_state(target)
|
return _systemd_user_state(target, remote=remote)
|
||||||
|
|
||||||
|
|
||||||
def _targets(s: dict[str, Any]) -> list[str]:
|
def _targets(s: dict[str, Any]) -> list[str]:
|
||||||
@@ -114,12 +122,13 @@ def _probe(s: dict[str, Any]) -> tuple[str, list[str]]:
|
|||||||
"""Collect the state of every target. Returns (aggregate, per-target list).
|
"""Collect the state of every target. Returns (aggregate, per-target list).
|
||||||
Aggregate: running if ANY is running; stopped if ALL stopped; else partial."""
|
Aggregate: running if ANY is running; stopped if ALL stopped; else partial."""
|
||||||
kind = s["kind"]
|
kind = s["kind"]
|
||||||
|
remote = s.get("remote")
|
||||||
states = []
|
states = []
|
||||||
for tgt in _targets(s):
|
for tgt in _targets(s):
|
||||||
if kind == "container":
|
if kind == "container":
|
||||||
states.append(_container_state(tgt))
|
states.append(_container_state(tgt))
|
||||||
else:
|
else:
|
||||||
states.append(_systemd_user_state(tgt))
|
states.append(_systemd_user_state(tgt, remote=remote))
|
||||||
n_ok = sum(1 for st in states if st in ("running", "active", "started"))
|
n_ok = sum(1 for st in states if st in ("running", "active", "started"))
|
||||||
n_none = sum(1 for st in states if st in ("unknown",)) or 0
|
n_none = sum(1 for st in states if st in ("unknown",)) or 0
|
||||||
if n_ok == len(states):
|
if n_ok == len(states):
|
||||||
@@ -184,9 +193,10 @@ async def service_action(sid: str, action: str,
|
|||||||
if s is None:
|
if s is None:
|
||||||
raise HTTPException(status_code=404, detail="unknown service")
|
raise HTTPException(status_code=404, detail="unknown service")
|
||||||
actor = (who or "unknown-user").strip()[:40]
|
actor = (who or "unknown-user").strip()[:40]
|
||||||
|
remote = s.get("remote")
|
||||||
results = {}
|
results = {}
|
||||||
for tgt in _targets(s):
|
for tgt in _targets(s):
|
||||||
results[tgt] = _do(s["kind"], action, tgt)
|
results[tgt] = _do(s["kind"], action, tgt, remote=remote)
|
||||||
agg, _ = _probe(s)
|
agg, _ = _probe(s)
|
||||||
audit(f"{actor} {action} {sid} -> {agg} ({len(_targets(s))} targets)")
|
audit(f"{actor} {action} {sid} -> {agg} ({len(_targets(s))} targets)")
|
||||||
return {"ok": True, "id": sid, "state": agg, "targets": results}
|
return {"ok": True, "id": sid, "state": agg, "targets": results}
|
||||||
|
|||||||
@@ -94,3 +94,14 @@ label = "World Monitor"
|
|||||||
ram_mb = 120
|
ram_mb = 120
|
||||||
default_state = "running"
|
default_state = "running"
|
||||||
who = ["sam"]
|
who = ["sam"]
|
||||||
|
|
||||||
|
[[services]]
|
||||||
|
id = "ollama"
|
||||||
|
kind = "systemd-user"
|
||||||
|
target = "ollama.service"
|
||||||
|
remote = "sam@192.168.20.27"
|
||||||
|
group = "ai"
|
||||||
|
label = "Local LLM Server (Ollama .27)"
|
||||||
|
ram_mb = 9000
|
||||||
|
default_state = "stopped"
|
||||||
|
who = ["sam", "jo", "harry", "finn"]
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ def _cat() -> list[Tool]:
|
|||||||
"Chat admin console (admins only)", "https://pb.chat.lab.audasmedia.com.au", login=True, admin=True),
|
"Chat admin console (admins only)", "https://pb.chat.lab.audasmedia.com.au", login=True, admin=True),
|
||||||
Tool("ai-router", "AI Router", "ai",
|
Tool("ai-router", "AI Router", "ai",
|
||||||
"OmniRoute LLM gateway dashboard", "https://omniroute.lab.audasmedia.com.au"),
|
"OmniRoute LLM gateway dashboard", "https://omniroute.lab.audasmedia.com.au"),
|
||||||
|
Tool("ollama", "Ollama Local LLM", "ai",
|
||||||
|
"Local AI model server (Qwen 2.5 Coder on .27)", "http://192.168.20.27:11434",
|
||||||
|
service_id="ollama", ram_mb=9000, togglable=True, lan=True),
|
||||||
Tool("n8n", "n8n", "ai",
|
Tool("n8n", "n8n", "ai",
|
||||||
"Workflow automation", "https://n8n.lab.audasmedia.com.au", login=True),
|
"Workflow automation", "https://n8n.lab.audasmedia.com.au", login=True),
|
||||||
Tool("prefect", "Prefect", "ai",
|
Tool("prefect", "Prefect", "ai",
|
||||||
|
|||||||
Reference in New Issue
Block a user