Add a stateful Proxmox API console and broad handler coverage beyond the

initial QEMU slice, backed by imported contracts for majors 6–9.
- Implement durable handlers for access/auth, cluster, LXC, storage, HA,
  firewall, Ceph, SDN, ACME, notifications, pools, mapping, and node ops
- Serve an interactive Web UI with catalog browsing, demo seed controls,
  and OpenAPI/help surfaces
- Bundle PVE 6.4-15, 7.4-16, and 8.4.5 contract revisions alongside 9.2.3
- Support in-memory runtime contract Apply (POST /ui/api/contract/apply)
  so /version and /api2 routes follow the selected major until restart
- Expand seed profiles (including demo-cluster), migrations 007–008, TLS
  gateway config, Compose/Makefile tooling, and compatibility evidence
- Tighten .gitignore for macOS, hidden directories (.*/), and local secrets
This commit is contained in:
Sergey Antropoff
2026-07-16 01:08:01 +03:00
parent 003ee5d634
commit 777926487b
189 changed files with 241501 additions and 944 deletions
+135 -3
View File
@@ -1,5 +1,6 @@
"""API-token lifecycle handler tests without external services."""
import json
from typing import Any, cast
import pytest
@@ -44,12 +45,79 @@ class TokenPool:
return "DELETE 1"
class RealmPool:
def __init__(self) -> None:
self.realms: dict[str, dict[str, Any]] = {
"pam": {
"kind": "pam",
"config": {"comment": "Linux PAM standard authentication"},
},
"pve": {
"kind": "pve",
"config": {"comment": "Proxmox VE authentication server"},
},
}
self.principals: dict[str, str] = {"root@pam": "pam"}
async def fetch(self, query: str, *arguments: object) -> list[dict[str, Any]]:
del arguments
if "FROM realms ORDER BY name" in query:
return [
{"name": name, "kind": data["kind"], "config": dict(data["config"])}
for name, data in sorted(self.realms.items())
]
raise AssertionError(query)
async def fetchrow(self, query: str, *arguments: object) -> dict[str, Any] | None:
if "FROM realms WHERE name" in query:
realm = str(arguments[0])
data = self.realms.get(realm)
if data is None:
return None
return {"name": realm, "kind": data["kind"], "config": dict(data["config"])}
raise AssertionError(query)
async def fetchval(self, query: str, *arguments: object) -> bool:
realm = str(arguments[0])
if "EXISTS(SELECT 1 FROM realms" in query:
return realm in self.realms
if "EXISTS(SELECT 1 FROM principals" in query:
return any(value == realm for value in self.principals.values())
raise AssertionError(query)
async def execute(self, query: str, *arguments: object) -> str:
if "INSERT INTO realms" in query:
self.realms[str(arguments[0])] = {
"kind": str(arguments[1]),
"config": json.loads(str(arguments[2])),
}
return "INSERT 0 1"
if "UPDATE realms SET config=$2" in query:
realm = str(arguments[0])
self.realms[realm]["config"] = json.loads(str(arguments[1]))
return "UPDATE 1"
if "SET config = config - 'default'" in query:
skip = str(arguments[0]) if arguments else None
for name, data in self.realms.items():
if skip is not None and name == skip:
continue
data["config"].pop("default", None)
return "UPDATE 0"
if "DELETE FROM realms" in query:
realm = str(arguments[0])
if realm not in self.realms:
return "DELETE 0"
del self.realms[realm]
return "DELETE 1"
raise AssertionError(query)
class FakeDatabase:
def __init__(self, pool: TokenPool) -> None:
def __init__(self, pool: TokenPool | RealmPool) -> None:
self.pool = pool
def request(pool: TokenPool, principal: str = "root@pam") -> Request:
def request(pool: TokenPool | RealmPool, principal: str = "root@pam") -> Request:
app = FastAPI()
app.state.database = cast(AsyncpgDatabase, FakeDatabase(pool))
result = Request(
@@ -70,7 +138,7 @@ def request(pool: TokenPool, principal: str = "root@pam") -> Request:
def values(**items: object) -> dict[str, Any]:
return {"values": items}
return {"values": items, "provided": frozenset(items)}
async def test_token_lifecycle_returns_secret_once_and_persists_metadata() -> None:
@@ -113,3 +181,67 @@ async def test_token_lifecycle_rejects_non_owner() -> None:
with pytest.raises(ApiError) as denied:
await handler(request(TokenPool(), "auditor@pve"), values(userid="other@pve"))
assert denied.value.status_code == 403
async def test_domain_lifecycle_persists_realm_config() -> None:
registry = HandlerRegistry()
register_access_handlers(registry)
pool = RealmPool()
http_request = request(pool)
create = registry.get("/access/domains", "POST")
listing = registry.get("/access/domains", "GET")
get = registry.get("/access/domains/{realm}", "GET")
update = registry.get("/access/domains/{realm}", "PUT")
delete = registry.get("/access/domains/{realm}", "DELETE")
assert create and listing and get and update and delete
await create(
http_request,
values(
realm="corp",
type="ldap",
comment="Corporate LDAP",
server1="ldap.example.com",
password="secret", # noqa: S106 - fixture secret for unit test
default=1,
),
)
listed = await listing(http_request, values())
assert any(item["realm"] == "corp" and item["type"] == "ldap" for item in listed)
created = await get(http_request, values(realm="corp"))
assert created["comment"] == "Corporate LDAP"
assert created["server1"] == "ldap.example.com"
assert created["default"] == 1
assert "password" not in created
await update(
http_request,
values(realm="corp", comment="Updated LDAP", delete="default"),
)
updated = await get(http_request, values(realm="corp"))
assert updated["comment"] == "Updated LDAP"
assert "default" not in updated
await delete(http_request, values(realm="corp"))
with pytest.raises(ApiError) as missing:
await get(http_request, values(realm="corp"))
assert missing.value.status_code == 404
async def test_domain_delete_rejects_builtin_and_in_use_realms() -> None:
registry = HandlerRegistry()
register_access_handlers(registry)
pool = RealmPool()
http_request = request(pool)
delete = registry.get("/access/domains/{realm}", "DELETE")
assert delete
with pytest.raises(ApiError) as builtin:
await delete(http_request, values(realm="pam"))
assert builtin.value.status_code == 400
pool.realms["corp"] = {"kind": "ldap", "config": {}}
pool.principals["alice@corp"] = "corp"
with pytest.raises(ApiError) as in_use:
await delete(http_request, values(realm="corp"))
assert in_use.value.status_code == 400