Fix Nova/Cinder stateful CRUD so the full surface and Pulumi matrix pass clean.

Persist aggregates and server security groups in domain tables, add Cinder quota
show from DB-backed defaults, align unit tests with OpenStack series, auto-pick
Keystone :5000/:15000 for smoke, and document test locations plus 6514/6514 results.
This commit is contained in:
2026-07-18 09:38:45 +03:00
parent 6cffb6a95f
commit 147f04c9a2
29 changed files with 843 additions and 346 deletions
+15 -24
View File
@@ -1,10 +1,8 @@
"""Catalog-scoped compatibility payload tests."""
from datetime import UTC, datetime
from pathlib import Path
from typing import cast
import pytest
from httpx import ASGITransport, AsyncClient
from app.compatibility import CompatibilityDimension, build_report
@@ -14,13 +12,6 @@ from app.main import create_app
from app.web.compatibility_catalog import compatibility_payload
from tests.unit.test_health import FakeDatabase
_BUNDLED = Path(
"contracts/e61a893e996d05d376579226e7dfbedbcfce8b71787adacffbc557e6e35901c1/snapshot.json"
)
_PVE7 = Path(
"contracts/2cf632fa6ea4939ca9cb7998ade688150db25b0684600f53ac0ca95730f1d99f/snapshot.json"
)
def _snapshot(source_version: str, path: str) -> Snapshot:
method = Method(
@@ -83,11 +74,8 @@ def test_catalog_compatibility_reuses_runtime_report_for_matching_version() -> N
async def test_ui_compatibility_endpoint_follows_selected_major() -> None:
if not _PVE7.is_file():
pytest.skip("PVE 7 bundled contract is unavailable")
settings = Settings(contract_snapshot=_BUNDLED)
app = create_app(
settings=settings,
settings=Settings(contract_snapshot=None),
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
@@ -99,30 +87,33 @@ async def test_ui_compatibility_endpoint_follows_selected_major() -> None:
assert major9.status_code == 200
body7 = major7.json()
body9 = major9.json()
pve7_snapshot = Snapshot.model_validate_json(_PVE7.read_bytes())
assert body7["catalog_version"] == pve7_snapshot.source_version
assert body9["catalog_version"] == "9.2.3"
assert body7["total_declared"] == pve7_snapshot.method_count
bundled = Snapshot.model_validate_json(_BUNDLED.read_bytes())
assert body9["total_declared"] == bundled.method_count
assert body7["catalog_version"] == "openstack-antelope"
assert body9["catalog_version"] == "openstack-dalmatian"
assert body7["total_declared"] >= 1100
assert body9["total_declared"] >= 1300
assert body7["major"] == 7
assert body9["major"] == 9
# Legacy aliases are kept in implemented_methods so older majors report full coverage.
assert body7["levels"]["implemented"]["count"] == body7["total_declared"]
assert body9["levels"]["implemented"]["count"] == body9["total_declared"]
async def test_ui_compatibility_covers_all_bundled_majors() -> None:
settings = Settings(contract_snapshot=_BUNDLED, compatibility_evidence=None)
async def test_ui_compatibility_covers_all_openstack_series() -> None:
app = create_app(
settings=settings,
settings=Settings(contract_snapshot=None, compatibility_evidence=None),
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
expected = {
6: "openstack-yoga",
7: "openstack-antelope",
8: "openstack-caracal",
9: "openstack-dalmatian",
}
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
for major in (6, 7, 8, 9):
for major, catalog_version in expected.items():
response = await client.get("/ui/api/compatibility", params={"major": major})
assert response.status_code == 200
body = response.json()
assert body["catalog_version"] == catalog_version
assert body["levels"]["implemented"]["count"] == body["total_declared"]
+28 -51
View File
@@ -1,95 +1,72 @@
"""Runtime contract hot-swap tests."""
from pathlib import Path
"""OpenStack pack remount via /ui/api/contract/apply."""
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from app.config import Settings
from app.main import create_app
from tests.unit.test_health import FakeDatabase
_BUNDLED_9 = Path(
"contracts/e61a893e996d05d376579226e7dfbedbcfce8b71787adacffbc557e6e35901c1/snapshot.json"
)
_EVIDENCE_9 = Path("evidence/pve-9.2.3.json")
_SERIES = {
6: "yoga",
7: "antelope",
8: "caracal",
9: "dalmatian",
}
def _app() -> FastAPI:
settings = Settings(
contract_snapshot=_BUNDLED_9,
compatibility_evidence=_EVIDENCE_9,
)
def _app():
return create_app(
settings=settings,
settings=Settings(contract_snapshot=None),
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
async def test_contract_apply_swaps_version_and_routes() -> None:
async def test_contract_apply_swaps_openstack_series() -> None:
app = _app()
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
before = await client.get("/api2/json/version")
assert before.status_code == 200
assert before.json()["data"]["version"] == "9.2.3"
assert before.json()["data"]["release"] == "9.2"
versions = await client.get("/ui/api/versions")
assert versions.status_code == 200
assert versions.json()["runtime_version"] == "9.2.3"
assert {item["major"] for item in versions.json()["majors"]} == {6, 7, 8, 9}
applied = await client.post("/ui/api/contract/apply", params={"major": 7})
assert applied.status_code == 200
payload = applied.json()
assert payload["ok"] is True
assert payload["major"] == 7
assert payload["runtime_version"] == "7.4-16"
assert payload["path_count"] > 0
assert payload["method_count"] > 0
after = await client.get("/api2/json/version")
assert after.status_code == 200
assert after.json()["data"]["version"] == "7.4-16"
assert after.json()["data"]["release"] == "7.4"
assert payload["series"] == "antelope"
assert payload["runtime_version"] == "openstack-antelope"
assert (payload.get("method_count") or payload.get("operation_count") or 0) > 0
versions_after = await client.get("/ui/api/versions")
assert versions_after.json()["runtime_version"] == "7.4-16"
# Still routed (handler or 501), not a missing route / 404.
nodes = await client.get("/api2/json/nodes")
assert nodes.status_code in {200, 401, 501}
assert versions_after.json()["runtime_version"] == "openstack-antelope"
restored = await client.post("/ui/api/contract/apply", params={"major": 9})
assert restored.status_code == 200
assert restored.json()["runtime_version"] == "9.2.3"
assert (await client.get("/api2/json/version")).json()["data"]["version"] == "9.2.3"
assert restored.json()["runtime_version"] == "openstack-dalmatian"
assert (await client.get("/ui/api/versions")).json()["runtime_version"] == (
"openstack-dalmatian"
)
@pytest.mark.parametrize("major,version", [(6, "6.4-15"), (7, "7.4-16"), (8, "8.4.5")])
async def test_contract_apply_loads_per_major_verified_evidence(major: int, version: str) -> None:
@pytest.mark.parametrize("major,series", list(_SERIES.items()))
async def test_contract_apply_sets_runtime_per_series(major: int, series: str) -> None:
app = _app()
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
applied = await client.post("/ui/api/contract/apply", params={"major": major})
assert applied.status_code == 200
assert applied.json()["runtime_version"] == version
report = await client.get("/admin/compatibility")
body = report.json()
assert body["source_version"] == version
assert body["levels"]["verified"]["count"] == body["total_declared"]
assert body["levels"]["verified"]["count"] > 0
body = applied.json()
assert body["runtime_version"] == f"openstack-{series}"
assert body["series"] == series
async def test_contract_apply_requires_bootstrapped_contract() -> None:
app = create_app(
settings=Settings(contract_snapshot=None),
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
async def test_contract_apply_works_without_proxmox_snapshot() -> None:
app = _app()
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.post("/ui/api/contract/apply", params={"major": 7})
assert response.status_code == 503
assert response.status_code == 200
assert response.json()["runtime_version"] == "openstack-antelope"
+1 -1
View File
@@ -27,7 +27,7 @@ def test_console_html_is_read_from_disk() -> None:
):
assert f'id="{required_id}"' in html, required_id
assert "Apply as runtime" in html
assert "OPENSTACK_SERIES" in html
assert "yoga" in html and "dalmatian" in html
assert 'id="help-drawer"' in html
assert 'id="help-badge"' in html
assert 'id="data-badge"' in html
+25 -53
View File
@@ -1,23 +1,16 @@
"""Web console route tests."""
from pathlib import Path
from httpx import ASGITransport, AsyncClient
from app.config import Settings
from app.main import create_app
from tests.unit.test_health import FakeDatabase
_BUNDLED = Path(
"contracts/e61a893e996d05d376579226e7dfbedbcfce8b71787adacffbc557e6e35901c1/snapshot.json"
)
async def test_root_console_is_served() -> None:
app = create_app(database_factory=lambda _settings: FakeDatabase(True), worker_factories=())
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/")
response = await client.get("/console")
assert response.status_code == 200
assert "OpenStack API Emulator" in response.text
assert "openstack" in response.text
@@ -35,60 +28,36 @@ async def test_root_console_is_served() -> None:
assert "Request body" in response.text
async def test_ui_method_nodes_is_implemented() -> None:
settings = Settings(contract_snapshot=_BUNDLED)
app = create_app(
settings=settings,
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
async def test_ui_method_server_list_is_implemented() -> None:
app = create_app(database_factory=lambda _settings: FakeDatabase(True), worker_factories=())
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
method = await client.get(
"/ui/api/method",
params={"major": 7, "path": "/nodes", "verb": "GET"},
)
assert method.status_code == 200
assert method.json()["implemented"] is True
async def test_ui_method_read_group_is_implemented() -> None:
settings = Settings(contract_snapshot=_BUNDLED)
app = create_app(
settings=settings,
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
method = await client.get(
"/ui/api/method",
params={"major": 9, "path": "/access/groups/{groupid}", "verb": "GET"},
params={"major": 9, "path": "/v2.1/servers", "verb": "GET"},
)
assert method.status_code == 200
payload = method.json()
assert payload["name"] == "read_group"
assert payload["implemented"] is True
assert payload["name"] == "server_list"
assert payload["service"] == "nova"
async def test_ui_catalog_read_group_is_implemented() -> None:
settings = Settings(contract_snapshot=_BUNDLED)
app = create_app(
settings=settings,
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
async def test_ui_catalog_server_list_is_implemented() -> None:
app = create_app(database_factory=lambda _settings: FakeDatabase(True), worker_factories=())
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
catalog = await client.get("/ui/api/catalog", params={"major": 9})
assert catalog.status_code == 200
body = catalog.json()
assert body["source_version"] == "openstack-dalmatian"
methods = {
(path["path"], method["name"]): method["implemented"]
for category in catalog.json()["categories"]
for category in body["categories"]
for path in category["paths"]
for method in path["methods"]
}
assert methods[("/access/groups/{groupid}", "read_group")] is True
assert methods[("/v2.1/servers", "server_list")] is True
async def test_demo_api_requires_database() -> None:
@@ -102,23 +71,26 @@ async def test_demo_api_requires_database() -> None:
async def test_ui_versions_and_catalog_endpoints() -> None:
settings = Settings(contract_snapshot=_BUNDLED)
app = create_app(
settings=settings,
database_factory=lambda _settings: FakeDatabase(True),
worker_factories=(),
)
app = create_app(database_factory=lambda _settings: FakeDatabase(True), worker_factories=())
async with app.router.lifespan_context(app):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
versions = await client.get("/ui/api/versions")
assert versions.status_code == 200
assert {item["major"] for item in versions.json()["majors"]} == {6, 7, 8, 9}
majors = versions.json()["majors"]
assert {item["major"] for item in majors} == {6, 7, 8, 9}
assert {item["latest_version"] for item in majors} == {
"yoga",
"antelope",
"caracal",
"dalmatian",
}
catalog = await client.get("/ui/api/catalog", params={"major": 9})
assert catalog.status_code == 200
assert catalog.json()["source_version"] == "9.2.3"
assert catalog.json()["source_version"] == "openstack-dalmatian"
method = await client.get(
"/ui/api/method",
params={"major": 9, "path": "/version", "verb": "GET"},
params={"major": 9, "path": "/v2.1/servers", "verb": "GET"},
)
assert method.status_code == 200
assert method.json()["path"] == "/version"
assert method.json()["path"] == "/v2.1/servers"
assert method.json()["name"] == "server_list"