Initial import: bare-metal stacks, Server UI, GPU fan, tutorials.

Infrastructure configs for GMKtec K11 (Docker, vLLM, LocalAI, ComfyUI,
control-plane, gpu-fan agent, Server UI with CLI/file explorer/GPU fan curve).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
tomasz-syn-grzegorza
2026-07-05 12:02:04 +00:00
commit 359afb3a59
153 changed files with 18169 additions and 0 deletions
+433
View File
@@ -0,0 +1,433 @@
"""Docker Compose helpers for whitelisted stacks."""
from __future__ import annotations
import json
import os
import re
import subprocess
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
RESERVED_PORTS = frozenset({80, 443, 8090, 18090})
PORT_MIN = 1024
PORT_MAX = 65535
class ComposeError(RuntimeError):
def __init__(self, message: str, returncode: int = 1, stderr: str = "") -> None:
super().__init__(message)
self.returncode = returncode
self.stderr = stderr
class PortConfigError(ValueError):
pass
@dataclass(frozen=True)
class StackConfig:
id: str
name: str
compose_dir: str
profile: str
container: str
ui_port: int | None
ui_scheme: str
gpu: bool
port_env: str | None = None
port_default: int | None = None
port_editable: bool = True
@property
def path(self) -> Path:
return Path(self.compose_dir)
@property
def env_path(self) -> Path:
return self.path / ".env"
def read_env_value(env_path: Path, key: str) -> str | None:
if not env_path.exists():
return None
prefix = f"{key}="
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if line.startswith(prefix):
val = line[len(prefix) :].strip()
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
val = val[1:-1]
elif val.startswith("'") and val.endswith("'") and len(val) >= 2:
val = val[1:-1]
return val
return None
def write_env_value(env_path: Path, key: str, value: str) -> None:
env_path.parent.mkdir(parents=True, exist_ok=True)
prefix = f"{key}="
new_line = f"{key}={value}"
lines: list[str] = []
found = False
if env_path.exists():
for raw in env_path.read_text(encoding="utf-8").splitlines():
stripped = raw.strip()
if stripped and not stripped.startswith("#") and stripped.startswith(prefix):
lines.append(new_line)
found = True
else:
lines.append(raw)
if not found:
if lines and lines[-1].strip():
lines.append("")
lines.append(new_line)
fd, tmp_path = tempfile.mkstemp(
dir=env_path.parent,
prefix=".env.",
suffix=".tmp",
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines))
if lines:
fh.write("\n")
os.replace(tmp_path, env_path)
except Exception:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
def resolve_stack_port(stack: StackConfig) -> int | None:
if stack.port_env:
raw = read_env_value(stack.env_path, stack.port_env)
if raw and raw.isdigit():
return int(raw)
if stack.ui_port is not None:
return stack.ui_port
return stack.port_default
def collect_used_ports(
stacks: list[StackConfig],
server_ui_port: int,
exclude_stack_id: str | None = None,
) -> set[int]:
used = set(RESERVED_PORTS) | {server_ui_port}
for s in stacks:
if s.id == exclude_stack_id:
continue
port = resolve_stack_port(s)
if port is not None:
used.add(port)
return used
def validate_port(
port: int,
stack: StackConfig,
stacks: list[StackConfig],
server_ui_port: int,
) -> None:
if not PORT_MIN <= port <= PORT_MAX:
raise PortConfigError(f"Port must be between {PORT_MIN} and {PORT_MAX}")
if not stack.port_editable or not stack.port_env:
raise PortConfigError(f"Port is not editable for stack: {stack.id}")
conflicts = collect_used_ports(stacks, server_ui_port, exclude_stack_id=stack.id)
if port in conflicts:
raise PortConfigError(f"Port {port} is already in use or reserved")
def get_published_host_port(stack: StackConfig) -> int | None:
proc = subprocess.run(
[
"docker",
"port",
stack.container,
],
capture_output=True,
text=True,
timeout=15,
check=False,
)
if proc.returncode != 0 or not proc.stdout.strip():
return None
for line in proc.stdout.strip().splitlines():
# 0.0.0.0:8070/tcp or [::]:8070/tcp
m = re.search(r":(\d+)/", line)
if m:
return int(m.group(1))
return None
def load_stacks(config_path: Path, stacks_base: Path) -> list[StackConfig]:
import yaml # PyYAML optional — parse minimal yaml ourselves if missing
text = config_path.read_text(encoding="utf-8")
try:
data = yaml.safe_load(text)
except Exception:
data = _parse_stacks_yaml_simple(text)
items = data.get("stacks", []) if isinstance(data, dict) else []
stacks: list[StackConfig] = []
for item in items:
compose_rel = item["compose_dir"]
full = stacks_base / compose_rel
stacks.append(
StackConfig(
id=item["id"],
name=item["name"],
compose_dir=str(full),
profile=item["profile"],
container=item.get("container", item["id"]),
ui_port=item.get("ui_port"),
ui_scheme=item.get("ui_scheme") or "http",
gpu=bool(item.get("gpu", False)),
port_env=item.get("port_env"),
port_default=item.get("port_default"),
port_editable=bool(item.get("port_editable", item.get("port_env") is not None)),
)
)
return stacks
def _parse_stacks_yaml_simple(text: str) -> dict[str, Any]:
"""Minimal parser fallback when PyYAML is not installed."""
import re
stacks: list[dict[str, Any]] = []
current: dict[str, Any] | None = None
for line in text.splitlines():
if re.match(r"^\s*-\s+id:\s*", line):
if current:
stacks.append(current)
current = {"id": line.split(":", 1)[1].strip()}
elif current is not None and ":" in line:
key, _, val = line.strip().partition(":")
key = key.strip()
val = val.strip()
if val == "null":
val = None
elif val == "true":
val = True
elif val == "false":
val = False
elif val.isdigit():
val = int(val)
current[key] = val
if current:
stacks.append(current)
return {"stacks": stacks}
def _run_compose(
stack: StackConfig,
*args: str,
timeout: int = 120,
) -> subprocess.CompletedProcess[str]:
cmd = [
"docker",
"compose",
"--project-directory",
stack.compose_dir,
"--profile",
stack.profile,
*args,
]
return subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
def get_container_state(stack: StackConfig) -> dict[str, Any]:
proc = _run_compose(
stack,
"ps",
"--format",
"json",
timeout=30,
)
running = False
status = "stopped"
health = None
if proc.returncode == 0 and proc.stdout.strip():
for line in proc.stdout.strip().splitlines():
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
name = row.get("Name", "")
if stack.container in name or name == stack.container:
state = row.get("State", "").lower()
health = row.get("Health")
if state == "running":
running = True
status = "running"
elif state:
status = state
break
else:
ps = subprocess.run(
[
"docker",
"ps",
"-a",
"--filter",
f"name=^{stack.container}$",
"--format",
"{{.State}}",
],
capture_output=True,
text=True,
timeout=15,
check=False,
)
if ps.stdout.strip():
status = ps.stdout.strip().lower()
running = status == "running"
ui_port = resolve_stack_port(stack)
published = get_published_host_port(stack) if running else None
port_pending_restart = bool(
running and ui_port is not None and published is not None and published != ui_port
)
return {
"id": stack.id,
"name": stack.name,
"profile": stack.profile,
"container": stack.container,
"ui_port": ui_port,
"ui_scheme": stack.ui_scheme,
"gpu": stack.gpu,
"running": running,
"status": status,
"health": health,
"compose_dir": stack.compose_dir,
"port_env": stack.port_env,
"port_editable": stack.port_editable and bool(stack.port_env),
"port_default": stack.port_default,
"published_port": published,
"port_pending_restart": port_pending_restart,
}
def get_logs(stack: StackConfig, tail: int = 100) -> str:
proc = _run_compose(
stack,
"logs",
"--tail",
str(tail),
"--no-color",
timeout=60,
)
if proc.returncode != 0:
raise ComposeError(
proc.stderr.strip() or "Failed to fetch logs",
proc.returncode,
proc.stderr,
)
return proc.stdout
def stack_start(stack: StackConfig) -> str:
proc = _run_compose(stack, "up", "-d", timeout=300)
if proc.returncode != 0:
raise ComposeError(
proc.stderr.strip() or proc.stdout.strip() or "start failed",
proc.returncode,
proc.stderr,
)
return (proc.stdout + proc.stderr).strip()
def stack_stop(stack: StackConfig) -> str:
proc = _run_compose(stack, "stop", timeout=120)
if proc.returncode != 0:
raise ComposeError(
proc.stderr.strip() or "stop failed",
proc.returncode,
proc.stderr,
)
return (proc.stdout + proc.stderr).strip()
def stack_restart(stack: StackConfig) -> str:
proc = _run_compose(stack, "restart", timeout=180)
if proc.returncode != 0:
raise ComposeError(
proc.stderr.strip() or "restart failed",
proc.returncode,
proc.stderr,
)
return (proc.stdout + proc.stderr).strip()
def stack_recreate(stack: StackConfig) -> str:
proc = _run_compose(stack, "up", "-d", "--force-recreate", timeout=300)
if proc.returncode != 0:
raise ComposeError(
proc.stderr.strip() or proc.stdout.strip() or "recreate failed",
proc.returncode,
proc.stderr,
)
return (proc.stdout + proc.stderr).strip()
def set_stack_port(
stack: StackConfig,
port: int,
stacks: list[StackConfig],
server_ui_port: int,
*,
recreate: bool = False,
) -> dict[str, Any]:
validate_port(port, stack, stacks, server_ui_port)
assert stack.port_env is not None
write_env_value(stack.env_path, stack.port_env, str(port))
state = get_container_state(stack)
recreated = False
requires_restart = state["running"]
if recreate and state["running"]:
stack_recreate(stack)
recreated = True
requires_restart = False
state = get_container_state(stack)
return {
"ok": True,
"stack_id": stack.id,
"port": port,
"port_env": stack.port_env,
"requires_restart": requires_restart and not recreated,
"recreated": recreated,
"running": state["running"],
}
def find_running_gpu_stacks(stacks: list[StackConfig], exclude_id: str | None = None) -> list[str]:
running: list[str] = []
for s in stacks:
if not s.gpu or s.id == exclude_id:
continue
state = get_container_state(s)
if state["running"]:
running.append(s.id)
return running