Butler 2.3.1: zuverlässige Runtime-Probes #2
4 changed files with 29 additions and 5 deletions
|
|
@ -4,7 +4,7 @@ Unified API proxy and infrastructure management for Homelab Pfannkuchen.
|
||||||
|
|
||||||
- **Base URL:** `http://10.4.1.116:8888`
|
- **Base URL:** `http://10.4.1.116:8888`
|
||||||
- **Authentication:** `Authorization: Bearer <BUTLER_TOKEN>`
|
- **Authentication:** `Authorization: Bearer <BUTLER_TOKEN>`
|
||||||
- **Version:** 2.3.0
|
- **Version:** 2.3.1
|
||||||
- **Interactive API documentation:** `/docs`
|
- **Interactive API documentation:** `/docs`
|
||||||
|
|
||||||
## Service proxy
|
## Service proxy
|
||||||
|
|
@ -97,6 +97,11 @@ Integration Compose definition: `tests/compose.integration.yaml` (binds only to
|
||||||
|
|
||||||
## Changelog
|
## 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
|
### 2.3.0 — 22.07.2026
|
||||||
|
|
||||||
- Added `/overview`, a compact schema-versioned verdict for lightweight language models.
|
- Added `/overview`, a compact schema-versioned verdict for lightweight language models.
|
||||||
|
|
|
||||||
5
app.py
5
app.py
|
|
@ -11,7 +11,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
log = logging.getLogger("butler")
|
log = logging.getLogger("butler")
|
||||||
VERSION = "2.3.0"
|
VERSION = "2.3.1"
|
||||||
|
|
||||||
API_DIR = os.environ.get("API_KEY_DIR", "/data/api")
|
API_DIR = os.environ.get("API_KEY_DIR", "/data/api")
|
||||||
VAULT_CACHE_DIR = os.environ.get("VAULT_CACHE_DIR", "/data/vault-cache")
|
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):
|
def _ssh(host, cmd, timeout=600):
|
||||||
try:
|
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)
|
capture_output=True, text=True, timeout=timeout)
|
||||||
return r.returncode, r.stdout, r.stderr
|
return r.returncode, r.stdout, r.stderr
|
||||||
except _sp.TimeoutExpired:
|
except _sp.TimeoutExpired:
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ services:
|
||||||
health_path: "/api/healthz"
|
health_path: "/api/healthz"
|
||||||
|
|
||||||
semaphore:
|
semaphore:
|
||||||
url: "http://10.4.1.116:8090"
|
url: "http://10.4.1.116:3010"
|
||||||
auth: bearer
|
auth: bearer
|
||||||
key_file: semaphore
|
key_file: semaphore
|
||||||
vault_key: semaphore_token
|
vault_key: semaphore_token
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ def test_health_exposes_current_version():
|
||||||
with TestClient(app.app) as client:
|
with TestClient(app.app) as client:
|
||||||
response = client.get("/health")
|
response = client.get("/health")
|
||||||
assert response.status_code == 200
|
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():
|
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"
|
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 test_ssh_timeout_is_normalized_instead_of_crashing_collection(monkeypatch):
|
||||||
def timeout(*_args, **_kwargs):
|
def timeout(*_args, **_kwargs):
|
||||||
raise app._sp.TimeoutExpired(cmd=["ssh"], timeout=1)
|
raise app._sp.TimeoutExpired(cmd=["ssh"], timeout=1)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue