Prepare 0.1.0 for lab release: durable handlers, HTTP Compose, CI, and pulumi-tests.
- Harden DB-backed handlers and seed profiles; align client wire shapes for cluster resources, QEMU config, and node SSL fields - Serve plain HTTP on Compose :8006; keep TLS optional (--profile tls) and terminate HTTPS at Kubernetes Ingress - Add pulumi-tests (full contract surface majors 6–9 + BPG lifecycle) and make pulumi-tests - Ship bilingual docs, CHANGELOG, SECURITY, CONTRIBUTING, and GitHub Actions (make ci + Compose/Helm validation)
This commit is contained in:
@@ -21,7 +21,9 @@ class FakePool:
|
||||
"name": "root@pam",
|
||||
"password_hash": hash_secret("secret", salt=b"pve-simulator-v1"),
|
||||
}
|
||||
if "FROM nodes" in sql and args[0] == "pve1":
|
||||
if "FROM nodes" in sql and "metadata" in sql and args and args[0] == "pve1":
|
||||
return {"metadata": '{"ops":{"status":{"uptime":100}}}'}
|
||||
if "FROM nodes" in sql and args and args[0] == "pve1":
|
||||
return {"name": "pve1", "status": "online"}
|
||||
if "FROM resources r" in sql and args == ("pve1", "100"):
|
||||
if "SELECT r.id" in sql:
|
||||
@@ -196,7 +198,12 @@ async def test_core_login_and_read_endpoints(
|
||||
assert version.json()["data"]["release"] == "test"
|
||||
assert nodes.json()["data"][0]["node"] == "pve1"
|
||||
assert status.json()["data"]["status"] == "online"
|
||||
assert resources.json()["data"][0]["type"] == "qemu"
|
||||
resource_types = {item["type"] for item in resources.json()["data"]}
|
||||
assert "qemu" in resource_types
|
||||
qemu_resources = [item for item in resources.json()["data"] if item["type"] == "qemu"]
|
||||
assert qemu_resources
|
||||
assert isinstance(qemu_resources[0]["cpu"], int | float)
|
||||
assert qemu_resources[0]["vmid"] == 100
|
||||
assert qemu.json()["data"][0]["vmid"] == 100
|
||||
assert config.json()["data"]["name"] == "demo"
|
||||
assert start.json()["data"].startswith("UPID:pve1:")
|
||||
|
||||
@@ -5,11 +5,15 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import Request
|
||||
from fastapi import FastAPI, Request
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.api.registry import HandlerRegistry, RouteCollisionError
|
||||
from app.api.registry import (
|
||||
HandlerRegistry,
|
||||
RouteCollisionError,
|
||||
register_unbound_handler_routes,
|
||||
)
|
||||
from app.config import Settings
|
||||
from app.contracts.model import Method, PathContract, Schema, Snapshot
|
||||
from app.main import create_app
|
||||
@@ -97,3 +101,21 @@ def test_duplicate_semantic_handlers_are_rejected() -> None:
|
||||
handlers.register("/version", "GET", handler)
|
||||
with pytest.raises(RouteCollisionError, match="duplicate"):
|
||||
handlers.register("/version", "GET", handler)
|
||||
|
||||
|
||||
def test_unbound_handler_routes_are_mounted() -> None:
|
||||
app = FastAPI()
|
||||
handlers = HandlerRegistry()
|
||||
|
||||
async def firewall_log(_request: Request, _inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return [{"n": 0, "msg": "from-db"}]
|
||||
|
||||
handlers.register("/cluster/firewall/log", "GET", firewall_log)
|
||||
register_unbound_handler_routes(app, handlers, "error")
|
||||
|
||||
routes = {
|
||||
(getattr(route, "path", None), tuple(sorted(getattr(route, "methods", set()) or set())))
|
||||
for route in app.routes
|
||||
}
|
||||
assert ("/api2/json/cluster/firewall/log", ("GET",)) in routes
|
||||
assert ("/api2/extjs/cluster/firewall/log", ("GET",)) in routes
|
||||
|
||||
@@ -66,7 +66,11 @@ class HandlerPool:
|
||||
async def fetchrow(self, sql: str, *args: object) -> dict[str, object] | None:
|
||||
del args
|
||||
if "FROM nodes WHERE name" in sql:
|
||||
return {"name": "pve01", "status": "online"} if self.node_exists else None
|
||||
if not self.node_exists:
|
||||
return None
|
||||
if "metadata" in sql:
|
||||
return {"metadata": "{}"}
|
||||
return {"name": "pve01", "status": "online"}
|
||||
if "FROM clusters" in sql:
|
||||
return {"metadata": '{"options":{"keyboard":"de-ch"}}'}
|
||||
if "FROM storages" in sql:
|
||||
@@ -156,7 +160,7 @@ async def test_storage_and_ceph_handlers() -> None:
|
||||
)
|
||||
assert osds[0]["status"] == "up"
|
||||
ceph_status = await _call(registry.get("/cluster/ceph/status", "GET"), {})
|
||||
assert ceph_status["osdmap"]["num_osds"] == 300
|
||||
assert ceph_status["osdmap"]["num_osds"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Persistent QEMU CRUD semantic handler tests."""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, cast
|
||||
@@ -66,9 +67,20 @@ class QemuPool:
|
||||
return {"state": '{"status":"stopped"}', "config": '{"name":"vm"}'}
|
||||
if "SELECT r.id, r.state" in sql:
|
||||
status = "running" if self.running else "stopped"
|
||||
agent = {
|
||||
"enabled": True,
|
||||
"results": {
|
||||
"info": {"version": "9.2.0"},
|
||||
"get-osinfo": {"machine": "x86_64", "name": "l26"},
|
||||
"get-host-name": {"host-name": "vm"},
|
||||
"network-get-interfaces": [{"name": "eth0"}],
|
||||
"get-time": {"seconds": 1_700_000_000},
|
||||
"ping": {},
|
||||
},
|
||||
}
|
||||
return {
|
||||
"id": self.resource_id,
|
||||
"state": f'{{"status":"{status}"}}',
|
||||
"state": json.dumps({"status": status, "name": "vm", "agent": agent}),
|
||||
"config": ('{"agent":1,"name":"vm","scsi0":"local-lvm:vm-150-disk-0,size=8G"}'),
|
||||
}
|
||||
if "SELECT r.id, r.state, v.config" in sql:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""Deterministic seed profile tests."""
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from app.simulation.seed import (
|
||||
build_profile,
|
||||
clear_simulation_state,
|
||||
cluster_domain_metadata,
|
||||
default_node_ops_for_seed,
|
||||
enrich_guest_state,
|
||||
enrich_storage_state,
|
||||
large_profile,
|
||||
small_profile,
|
||||
stable_id,
|
||||
@@ -101,6 +107,70 @@ def test_stable_ids_are_namespaced_and_repeatable() -> None:
|
||||
assert stable_id("qemu:100") != stable_id("qemu:101")
|
||||
|
||||
|
||||
def test_cluster_domain_metadata_seeds_list_domains() -> None:
|
||||
meta = cast(dict[str, Any], cluster_domain_metadata(small_profile()))
|
||||
assert meta["firewall"]["scopes"]["cluster"]["rules"]
|
||||
assert meta["firewall"]["scopes"]["cluster"]["aliases"]
|
||||
assert meta["firewall"]["scopes"]["cluster"]["ipset"]
|
||||
assert meta["firewall"]["scopes"]["cluster"]["groups"]
|
||||
assert meta["firewall"]["scopes"]["node:pve01"]["rules"]
|
||||
assert meta["firewall"]["scopes"]["qemu:pve01:100"]["rules"]
|
||||
assert meta["firewall"]["scopes"]["lxc:pve01:200"]["rules"]
|
||||
assert meta["firewall_macros"]
|
||||
assert meta["sdn"]["zones"]
|
||||
assert meta["sdn"]["vnets"]
|
||||
assert meta["sdn"]["controllers"]
|
||||
assert meta["sdn"]["ipams"]
|
||||
assert meta["sdn"]["dns"]
|
||||
assert meta["notifications"]["endpoints"]["smtp"]
|
||||
assert meta["notifications"]["endpoints"]["gotify"]
|
||||
assert meta["notifications"]["matchers"]
|
||||
assert meta["notifications"]["matcher_fields"]
|
||||
assert meta["acme"]["accounts"]
|
||||
assert meta["acme"]["plugins"]
|
||||
assert meta["acme"]["directories"]
|
||||
assert meta["acme"]["challenge_schema"]
|
||||
assert meta["mapping"]["pci"]
|
||||
assert meta["mapping"]["usb"]
|
||||
assert meta["mapping"]["dir"]
|
||||
assert meta["replication"]
|
||||
assert meta["metrics"]["servers"]
|
||||
assert meta["ha_groups"]
|
||||
assert meta["ceph"]["pools"]
|
||||
assert meta["ceph"]["flags"]
|
||||
assert meta["ceph"]["version"]
|
||||
assert meta["qemu_cpu_flags"]
|
||||
assert meta["metrics"]["export_data"]
|
||||
assert meta["jobs"]["schedule_analyze_results"]
|
||||
assert meta["replication"][0]["log"]
|
||||
ops = cast(dict[str, Any], default_node_ops_for_seed("pve01"))
|
||||
assert ops["capabilities"]["cpu"]
|
||||
assert ops["capabilities"]["machines"]
|
||||
assert ops["hosts"]["data"]
|
||||
assert ops["journal"]
|
||||
assert ops["syslog"]
|
||||
assert ops["netstat"]
|
||||
assert ops["rrddata"]
|
||||
assert ops["status"]["uptime"]
|
||||
assert ops["ip"]
|
||||
assert meta["cluster_config"]["totem"]
|
||||
assert meta["quorate"] == 1
|
||||
guest = cast(
|
||||
dict[str, Any],
|
||||
enrich_guest_state({"name": "demo", "status": "stopped"}, kind="qemu", vmid="100"),
|
||||
)
|
||||
assert guest["agent"]["results"]["info"]
|
||||
assert guest["rrddata"]
|
||||
assert guest["migrate_preconditions"]
|
||||
storage = cast(
|
||||
dict[str, Any],
|
||||
enrich_storage_state({"total_bytes": 1000, "used_bytes": 100}, storage_id="local"),
|
||||
)
|
||||
assert storage["rrddata"]
|
||||
assert storage["file_restore"]
|
||||
assert storage["import_metadata"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_simulation_state_wipes_api_created_identity() -> None:
|
||||
executed: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user