feat(system): add compute contour telemetry APIs
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import stat
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
from k1link.web.compute_contour_api import (
|
||||
CATALOG_SCHEMA,
|
||||
ComputeContourCreate,
|
||||
ComputeContourPut,
|
||||
ComputeContourStore,
|
||||
build_compute_contour_router,
|
||||
)
|
||||
|
||||
|
||||
def _endpoint(router: APIRouter, path: str, method: str) -> Callable[..., Any]:
|
||||
for route in router.routes:
|
||||
if (
|
||||
isinstance(route, APIRoute)
|
||||
and route.path == path
|
||||
and route.methods is not None
|
||||
and method in route.methods
|
||||
):
|
||||
return route.endpoint
|
||||
raise AssertionError(f"{method} {path} route is missing")
|
||||
|
||||
|
||||
def test_contour_store_migrates_worker_006_as_first_configuration(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = ComputeContourStore(tmp_path / "system")
|
||||
contours = store.list_contours()
|
||||
|
||||
assert len(contours) == 1
|
||||
assert contours[0].contour_id == "worker-006"
|
||||
assert contours[0].telemetry_mode == "agent-mqtt"
|
||||
assert not store.path.exists()
|
||||
|
||||
|
||||
def test_contour_store_creates_and_updates_private_catalog(tmp_path: Path) -> None:
|
||||
store = ComputeContourStore(tmp_path / "system")
|
||||
created = store.create(
|
||||
ComputeContourCreate(
|
||||
display_name="Field Worker",
|
||||
expected_node_id="FIELD-01",
|
||||
platform="linux",
|
||||
address="192.0.2.25",
|
||||
mqtt_host="192.0.2.5",
|
||||
)
|
||||
)
|
||||
updated = store.update(
|
||||
created.contour_id,
|
||||
ComputeContourPut(
|
||||
revision=created.revision,
|
||||
display_name="Field Worker 01",
|
||||
expected_node_id="FIELD-01",
|
||||
platform="linux",
|
||||
telemetry_mode="agent-mqtt",
|
||||
address="192.0.2.25",
|
||||
ssh_port=22,
|
||||
mqtt_host="192.0.2.5",
|
||||
mqtt_port=1883,
|
||||
),
|
||||
)
|
||||
|
||||
assert updated.display_name == "Field Worker 01"
|
||||
assert updated.revision == 1
|
||||
assert stat.S_IMODE(store.path.stat().st_mode) == 0o600
|
||||
assert len(store.list_contours()) == 2
|
||||
with pytest.raises(RuntimeError, match="revision changed"):
|
||||
store.update(
|
||||
created.contour_id,
|
||||
ComputeContourPut(
|
||||
revision=0,
|
||||
display_name="stale",
|
||||
expected_node_id="FIELD-01",
|
||||
platform="linux",
|
||||
telemetry_mode="agent-mqtt",
|
||||
address="",
|
||||
ssh_port=22,
|
||||
mqtt_host="127.0.0.1",
|
||||
mqtt_port=1883,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_contour_router_exposes_catalog_and_safe_install_contract(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
router = build_compute_contour_router(
|
||||
root_provider=lambda: tmp_path / "system"
|
||||
)
|
||||
list_contours = _endpoint(router, "/api/v1/system/contours", "GET")
|
||||
install = _endpoint(
|
||||
router,
|
||||
"/api/v1/system/contours/{contour_id}/agent-install",
|
||||
"GET",
|
||||
)
|
||||
|
||||
catalog = list_contours()
|
||||
assert catalog["schema_version"] == CATALOG_SCHEMA
|
||||
assert catalog["contours"][0]["contour_id"] == "worker-006"
|
||||
document = install("worker-006")
|
||||
assert document["agent"]["distribution"] == "Telegraf"
|
||||
assert "MQTT password" in document["command"]
|
||||
assert "password" not in document["agent"]["environment"]
|
||||
assert document["ready"] is False
|
||||
|
||||
|
||||
def test_contour_router_returns_404_for_unknown_contour(tmp_path: Path) -> None:
|
||||
router = build_compute_contour_router(
|
||||
root_provider=lambda: tmp_path / "system"
|
||||
)
|
||||
install = _endpoint(
|
||||
router,
|
||||
"/api/v1/system/contours/{contour_id}/agent-install",
|
||||
"GET",
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as error:
|
||||
install("missing")
|
||||
assert error.value.status_code == 404
|
||||
@@ -16,6 +16,7 @@ from k1link.web.system_telemetry_api import (
|
||||
WorkerConnectionProfilePut,
|
||||
WorkerProfileStore,
|
||||
WorkerTelemetryService,
|
||||
_agent_raw_document,
|
||||
_ssh_arguments,
|
||||
build_system_telemetry_router,
|
||||
)
|
||||
@@ -42,6 +43,7 @@ def _probe(
|
||||
return {
|
||||
"reachable": True,
|
||||
"identity_matches": node_id == EXPECTED_NODE_ID,
|
||||
"source": "agent-mqtt",
|
||||
"node_id": node_id,
|
||||
"latency_ms": 12.5,
|
||||
"observed_at_utc": "2026-07-27T12:00:00Z",
|
||||
@@ -208,6 +210,7 @@ def test_worker_telemetry_separates_mission_core_and_external_load(
|
||||
runtimes = {runtime["name"]: runtime for runtime in document["runtimes"]}
|
||||
|
||||
assert document["connection"]["identity_matches"] is True
|
||||
assert document["connection"]["source"] == "agent-mqtt"
|
||||
assert document["node"]["memory"]["used_percent"] == 60
|
||||
assert document["node"]["gpu"]["memory_used_percent"] == 50
|
||||
assert document["node"]["triton"]["requests_succeeded"] == 12
|
||||
@@ -237,6 +240,106 @@ def test_worker_telemetry_separates_mission_core_and_external_load(
|
||||
)["share_percent"] is None
|
||||
|
||||
|
||||
def test_worker_telemetry_prefers_ndc_container_names_during_migration(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
probe = _probe()
|
||||
raw = probe["raw"]
|
||||
triton_stats = raw["docker_stats"].pop("mission-core-triton")
|
||||
triton_stats["Name"] = "ndc-mission-core-triton"
|
||||
raw["docker_stats"]["ndc-mission-core-triton"] = triton_stats
|
||||
raw["container_states"]["ndc-mission-core-triton"] = raw[
|
||||
"container_states"
|
||||
].pop("mission-core-triton")
|
||||
|
||||
service = WorkerTelemetryService(
|
||||
WorkerProfileStore(tmp_path / "system"),
|
||||
lambda _: probe,
|
||||
cache_seconds=0,
|
||||
)
|
||||
document = service.snapshot(10)
|
||||
mission_core_runtimes = [
|
||||
runtime for runtime in document["runtimes"] if not runtime["external"]
|
||||
]
|
||||
|
||||
assert len(mission_core_runtimes) == 2
|
||||
triton = next(
|
||||
runtime
|
||||
for runtime in mission_core_runtimes
|
||||
if runtime["role"] == "Inference Runtime"
|
||||
)
|
||||
assert triton["name"] == "ndc-mission-core-triton"
|
||||
assert triton["canonical_name"] == "ndc-mission-core-triton"
|
||||
|
||||
|
||||
def test_agent_metrics_are_mapped_to_the_existing_product_contract() -> None:
|
||||
document = _agent_raw_document(
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"node_id": EXPECTED_NODE_ID,
|
||||
"observed_at_utc": "2026-07-27T12:00:00Z",
|
||||
"kind": "host",
|
||||
"measurement": "cpu",
|
||||
"payload": {
|
||||
"fields": {"usage_active": 12.5},
|
||||
"tags": {"cpu": "cpu-total"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"node_id": EXPECTED_NODE_ID,
|
||||
"observed_at_utc": "2026-07-27T12:00:00Z",
|
||||
"kind": "host",
|
||||
"measurement": "mem",
|
||||
"payload": {
|
||||
"fields": {
|
||||
"total": 1_000,
|
||||
"available": 400,
|
||||
},
|
||||
"tags": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
"node_id": EXPECTED_NODE_ID,
|
||||
"observed_at_utc": "2026-07-27T12:00:00Z",
|
||||
"kind": "host",
|
||||
"measurement": "net",
|
||||
"payload": {
|
||||
"fields": {
|
||||
"bytes_recv": 2_000,
|
||||
"bytes_sent": 1_000,
|
||||
},
|
||||
"tags": {"interface": "Ethernet"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"node_id": EXPECTED_NODE_ID,
|
||||
"observed_at_utc": "2026-07-27T12:00:00Z",
|
||||
"kind": "host",
|
||||
"measurement": "docker_container_cpu",
|
||||
"payload": {
|
||||
"fields": {"usage_percent": 2.5},
|
||||
"tags": {
|
||||
"container_name": "ndc-mission-core-triton",
|
||||
"container_image": "triton@sha256:accepted",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert document["node_id"] == EXPECTED_NODE_ID
|
||||
assert document["cpu"]["load_percent"] == 12.5
|
||||
assert document["memory"] == {"total_bytes": 1_000.0, "free_bytes": 400.0}
|
||||
assert document["network"][0]["name"] == "Ethernet"
|
||||
assert document["docker_stats"]["ndc-mission-core-triton"]["CPUPerc"] == "2.500%"
|
||||
assert (
|
||||
document["container_states"]["ndc-mission-core-triton"]["image"]
|
||||
== "triton@sha256:accepted"
|
||||
)
|
||||
|
||||
|
||||
def test_profile_apply_fails_closed_on_wrong_node_and_keeps_old_profile(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user