Butler 2.3.1: zuverlässige Runtime-Probes #2

Merged
sascha merged 4 commits from trulla/butler-2.3.1 into main 2026-07-22 11:09:42 +02:00
4 changed files with 29 additions and 5 deletions

View file

@ -4,7 +4,7 @@ Unified API proxy and infrastructure management for Homelab Pfannkuchen.
- **Base URL:** `http://10.4.1.116:8888`
- **Authentication:** `Authorization: Bearer <BUTLER_TOKEN>`
- **Version:** 2.3.0
- **Version:** 2.3.1
- **Interactive API documentation:** `/docs`
## Service proxy
@ -97,6 +97,11 @@ Integration Compose definition: `tests/compose.integration.yaml` (binds only to
## Changelog
### 2.3.1 — 22.07.2026
- Use a writable runtime `known_hosts` file for SSH probes with read-only SSH mounts.
- Correct the Semaphore backend port from the repurposed MCP-Dockhand port to port 3010.
### 2.3.0 — 22.07.2026
- Added `/overview`, a compact schema-versioned verdict for lightweight language models.

5
app.py
View file

@ -11,7 +11,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from contextlib import asynccontextmanager
log = logging.getLogger("butler")
VERSION = "2.3.0"
VERSION = "2.3.1"
API_DIR = os.environ.get("API_KEY_DIR", "/data/api")
VAULT_CACHE_DIR = os.environ.get("VAULT_CACHE_DIR", "/data/vault-cache")
@ -788,7 +788,8 @@ class VMCreate(BaseModel):
def _ssh(host, cmd, timeout=600):
try:
r = _sp.run(["ssh","-o","ConnectTimeout=10","-o","StrictHostKeyChecking=accept-new",host,cmd],
r = _sp.run(["ssh","-o","ConnectTimeout=10","-o","StrictHostKeyChecking=accept-new",
"-o","UserKnownHostsFile=/tmp/butler_known_hosts",host,cmd],
capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout, r.stderr
except _sp.TimeoutExpired:

View file

@ -106,7 +106,7 @@ services:
health_path: "/api/healthz"
semaphore:
url: "http://10.4.1.116:8090"
url: "http://10.4.1.116:3010"
auth: bearer
key_file: semaphore
vault_key: semaphore_token

View file

@ -42,7 +42,7 @@ def test_health_exposes_current_version():
with TestClient(app.app) as client:
response = client.get("/health")
assert response.status_code == 200
assert response.json()["version"] == app.VERSION == "2.3.0"
assert response.json()["version"] == app.VERSION == "2.3.1"
def test_invalid_log_target_is_rejected_before_ssh():
@ -145,6 +145,24 @@ def test_backup_item_reports_age_and_severity():
assert app._backup_item(1, "", "timeout")["state"] == "unknown"
def test_ssh_uses_writable_runtime_known_hosts(monkeypatch):
captured = {}
class Result:
returncode = 0
stdout = "ok"
stderr = ""
def fake_run(args, **_kwargs):
captured["args"] = args
return Result()
monkeypatch.setattr(app._sp, "run", fake_run)
assert app._ssh("sascha@example", "true", timeout=1)[0] == 0
joined = " ".join(captured["args"])
assert "UserKnownHostsFile=/tmp/butler_known_hosts" in joined
def test_ssh_timeout_is_normalized_instead_of_crashing_collection(monkeypatch):
def timeout(*_args, **_kwargs):
raise app._sp.TimeoutExpired(cmd=["ssh"], timeout=1)