Initial commit: VMware vSphere API simulator scaffold.
Add the FastAPI app, PostgreSQL migrations, Docker/Helm packaging, API contracts, docs, client examples, and the unit/integration/compatibility test suite for local client and tooling labs without a real vCenter.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""Tests for cluster, storage, pool and ceph handlers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import Request
|
||||
|
||||
from app.api.errors import ApiError
|
||||
from app.api.registry import HandlerRegistry
|
||||
from app.handlers.ceph import register_ceph_handlers
|
||||
from app.handlers.cluster import register_cluster_handlers
|
||||
from app.handlers.pools import register_pool_handlers
|
||||
from app.handlers.storage import register_storage_handlers
|
||||
|
||||
|
||||
class HandlerPool:
|
||||
def __init__(self) -> None:
|
||||
self.node_exists = True
|
||||
|
||||
async def fetch(self, sql: str, *args: object) -> list[dict[str, object]]:
|
||||
del args
|
||||
if "FROM nodes" in sql and "ORDER BY name" in sql:
|
||||
return [{"id": uuid.uuid4(), "name": "pve01", "status": "online"}]
|
||||
if "FROM storages" in sql and "DISTINCT storage_id" in sql:
|
||||
return [{"storage_id": "local-lvm-pve01"}]
|
||||
if "FROM storages s" in sql:
|
||||
return [
|
||||
{
|
||||
"storage_id": "local-lvm-pve01",
|
||||
"storage_type": "lvmthin",
|
||||
"shared": False,
|
||||
"capacity_bytes": 1_000_000,
|
||||
"used_bytes": 250_000,
|
||||
"config": '{"content":["images"]}',
|
||||
}
|
||||
]
|
||||
if "ceph-osd" in sql:
|
||||
return [
|
||||
{
|
||||
"external_id": "osd.0",
|
||||
"state": '{"osd_id":0,"status":"up","in":true,"weight":1.0}',
|
||||
}
|
||||
]
|
||||
if "FROM pools" in sql:
|
||||
return [
|
||||
{
|
||||
"id": uuid.uuid4(),
|
||||
"pool_id": "production",
|
||||
"comment": "prod",
|
||||
"metadata": '{"members":["100"]}',
|
||||
}
|
||||
]
|
||||
if "FROM pool_members" in sql:
|
||||
return [{"external_id": "100"}]
|
||||
if "FROM task_logs" in sql:
|
||||
return [{"message": "seeded task", "sequence": 1}]
|
||||
if "FROM tasks" in sql:
|
||||
return [{"upid": "UPID:pve01:1:1:1:qmstart:100:root@pam:"}]
|
||||
if "FROM storage_contents" in sql or "FROM backups" in sql:
|
||||
return []
|
||||
raise AssertionError(sql)
|
||||
|
||||
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 "FROM clusters" in sql:
|
||||
return {"metadata": '{"options":{"keyboard":"de-ch"}}'}
|
||||
if "FROM storages" in sql:
|
||||
return {
|
||||
"storage_id": "local-lvm-pve01",
|
||||
"storage_type": "lvmthin",
|
||||
"shared": False,
|
||||
"capacity_bytes": 1_000_000,
|
||||
"used_bytes": 250_000,
|
||||
"config": '{"content":["images"]}',
|
||||
"node_name": "pve01",
|
||||
"resource_id": uuid.uuid4(),
|
||||
}
|
||||
if "ceph-osd" in sql:
|
||||
return {
|
||||
"external_id": "osd.0",
|
||||
"state": '{"osd_id":0,"status":"up","in":true,"weight":1.0,"size_bytes":1000}',
|
||||
}
|
||||
if "storage_type='ceph'" in sql:
|
||||
return {"capacity_bytes": 5_000_000, "used_bytes": 3_000_000}
|
||||
raise AssertionError(sql)
|
||||
|
||||
async def fetchval(self, sql: str, *args: object) -> Any:
|
||||
del args
|
||||
if "EXISTS(SELECT 1 FROM nodes" in sql:
|
||||
return self.node_exists
|
||||
if "MAX(external_id::integer)" in sql:
|
||||
return 150
|
||||
if "count(*)::int FROM resources WHERE kind='ceph-osd'" in sql:
|
||||
return 300
|
||||
if "SELECT resource_id FROM storages" in sql:
|
||||
return uuid.uuid4()
|
||||
return False
|
||||
|
||||
async def execute(self, sql: str, *args: object) -> str:
|
||||
del sql, args
|
||||
return "UPDATE 1"
|
||||
|
||||
|
||||
def _request(pool: HandlerPool) -> Request:
|
||||
app = type("App", (), {})()
|
||||
app.state = type("State", (), {"database": type("DB", (), {"pool": pool})()})()
|
||||
scope = {
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"headers": [],
|
||||
"query_string": b"",
|
||||
"client": ("test", 1234),
|
||||
"server": ("test", 80),
|
||||
"scheme": "http",
|
||||
"root_path": "",
|
||||
"app": app,
|
||||
}
|
||||
request = Request(scope)
|
||||
request.state.principal = "root@pam"
|
||||
return request
|
||||
|
||||
|
||||
async def _call(handler: Any, values: dict[str, Any], pool: HandlerPool | None = None) -> Any:
|
||||
return await handler(_request(pool or HandlerPool()), {"values": values})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cluster_status_and_nextid() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_cluster_handlers(registry)
|
||||
status = await _call(registry.get("/cluster/status", "GET"), {})
|
||||
assert status[0]["name"] == "pve01"
|
||||
nextid = await _call(registry.get("/cluster/nextid", "GET"), {})
|
||||
assert nextid == 151
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_storage_and_ceph_handlers() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_storage_handlers(registry)
|
||||
register_ceph_handlers(registry)
|
||||
storage = await _call(
|
||||
registry.get("/nodes/{node}/storage", "GET"),
|
||||
{"node": "pve01"},
|
||||
)
|
||||
assert storage[0]["storage"] == "local-lvm-pve01"
|
||||
osds = await _call(
|
||||
registry.get("/nodes/{node}/ceph/osd", "GET"),
|
||||
{"node": "pve01"},
|
||||
)
|
||||
assert osds[0]["status"] == "up"
|
||||
ceph_status = await _call(registry.get("/cluster/ceph/status", "GET"), {})
|
||||
assert ceph_status["osdmap"]["num_osds"] == 300
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pools_list() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_pool_handlers(registry)
|
||||
pools = await _call(registry.get("/pools", "GET"), {})
|
||||
assert pools[0]["poolid"] == "production"
|
||||
assert pools[0]["members"] == ["100"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_node_returns_404() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_storage_handlers(registry)
|
||||
pool = HandlerPool()
|
||||
pool.node_exists = False
|
||||
handler = registry.get("/nodes/{node}/storage", "GET")
|
||||
assert handler is not None
|
||||
with pytest.raises(ApiError, match="node does not exist"):
|
||||
await handler(_request(pool), {"values": {"node": "missing"}})
|
||||
Reference in New Issue
Block a user