Initial commit: stateful OpenStack API laboratory simulator.
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
"""Load and hot-swap OpenStack series contract packs from contracts/openstack/."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.openstack.opspec import OperationSpec, SeriesManifest, ServicePack
|
||||
|
||||
_CONTRACTS_ROOT = Path(__file__).resolve().parents[2] / "contracts" / "openstack"
|
||||
|
||||
_SERIES_MAJOR = {
|
||||
"yoga": 6,
|
||||
"antelope": 7,
|
||||
"caracal": 8,
|
||||
"dalmatian": 9,
|
||||
}
|
||||
_MAJOR_SERIES = {v: k for k, v in _SERIES_MAJOR.items()}
|
||||
|
||||
|
||||
def contracts_root() -> Path:
|
||||
return _CONTRACTS_ROOT
|
||||
|
||||
|
||||
def series_for_major(major: int) -> str:
|
||||
return _MAJOR_SERIES.get(major, "dalmatian")
|
||||
|
||||
|
||||
def major_for_series(series: str) -> int:
|
||||
return _SERIES_MAJOR.get(series.lower(), 9)
|
||||
|
||||
|
||||
def list_series() -> list[dict[str, Any]]:
|
||||
root = contracts_root()
|
||||
if not root.exists():
|
||||
return []
|
||||
result: list[dict[str, Any]] = []
|
||||
for path in sorted(root.iterdir()):
|
||||
man = path / "manifest.json"
|
||||
if not man.is_file():
|
||||
continue
|
||||
data = json.loads(man.read_text())
|
||||
result.append(
|
||||
{
|
||||
"series": data.get("series", path.name),
|
||||
"major": data.get("major", major_for_series(path.name)),
|
||||
"operation_count": data.get("operation_count", 0),
|
||||
"service_count": data.get("service_count", 0),
|
||||
"checksum": data.get("checksum", ""),
|
||||
"generated_at": data.get("generated_at", ""),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _op_from_dict(service: str, raw: dict[str, Any]) -> OperationSpec:
|
||||
return OperationSpec(
|
||||
operation_id=str(raw["operation_id"]),
|
||||
method=raw["method"], # type: ignore[arg-type]
|
||||
path=str(raw["path"]),
|
||||
service=service,
|
||||
resource_type=str(raw.get("resource_type") or "object"),
|
||||
collection_key=raw.get("collection_key"),
|
||||
item_key=raw.get("item_key"),
|
||||
kind=raw.get("kind") or "collection", # type: ignore[arg-type]
|
||||
status_code=int(raw.get("status_code") or 200),
|
||||
create_status=int(raw.get("create_status") or raw.get("status_code") or 201),
|
||||
microversion_min=raw.get("microversion_min"),
|
||||
microversion_max=raw.get("microversion_max"),
|
||||
requires_auth=bool(raw.get("requires_auth", True)),
|
||||
requires_project=bool(raw.get("requires_project", True)),
|
||||
action_name=raw.get("action_name"),
|
||||
response_fixture=raw.get("response_fixture"),
|
||||
notes=str(raw.get("notes") or ""),
|
||||
)
|
||||
|
||||
|
||||
def load_series_pack(series: str) -> dict[str, ServicePack]:
|
||||
series = series.lower()
|
||||
series_dir = contracts_root() / series
|
||||
man_path = series_dir / "manifest.json"
|
||||
if not man_path.is_file():
|
||||
raise FileNotFoundError(f"OpenStack contract pack not found: {series_dir}")
|
||||
packs: dict[str, ServicePack] = {}
|
||||
for svc_dir in sorted(series_dir.iterdir()):
|
||||
api = svc_dir / "api.json"
|
||||
if not api.is_file():
|
||||
continue
|
||||
data = json.loads(api.read_text())
|
||||
name = str(data["service"])
|
||||
ops = [_op_from_dict(name, raw) for raw in data.get("operations") or []]
|
||||
packs[name] = ServicePack(
|
||||
name=name,
|
||||
typ=str(data.get("type") or name),
|
||||
port=int(data["port"]),
|
||||
version_path=str(data.get("version_path") or "/"),
|
||||
default_microversion=data.get("default_microversion"),
|
||||
max_microversion=data.get("max_microversion"),
|
||||
operations=ops,
|
||||
)
|
||||
return packs
|
||||
|
||||
|
||||
def load_manifest(series: str) -> SeriesManifest:
|
||||
data = json.loads((contracts_root() / series.lower() / "manifest.json").read_text())
|
||||
services = list(data.get("services") or [])
|
||||
return SeriesManifest(
|
||||
series=str(data["series"]),
|
||||
major=int(data["major"]),
|
||||
services=services,
|
||||
checksum=str(data.get("checksum") or ""),
|
||||
generated_at=str(data.get("generated_at") or ""),
|
||||
operation_count=int(data.get("operation_count") or 0),
|
||||
service_count=int(data.get("service_count") or len(services)),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContractRuntime:
|
||||
"""Process-wide active OpenStack contract pack + per-service microversion overrides."""
|
||||
|
||||
series: str = "dalmatian"
|
||||
packs: dict[str, ServicePack] = field(default_factory=dict)
|
||||
microversion_overrides: dict[str, str] = field(default_factory=dict)
|
||||
_lock: threading.RLock = field(default_factory=threading.RLock)
|
||||
|
||||
def reload(self, series: str | None = None) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
target = (series or self.series).lower()
|
||||
self.packs = load_series_pack(target)
|
||||
self.series = target
|
||||
man = load_manifest(target)
|
||||
return {
|
||||
"series": man.series,
|
||||
"major": man.major,
|
||||
"operation_count": man.operation_count,
|
||||
"service_count": man.service_count,
|
||||
"checksum": man.checksum,
|
||||
"services": sorted(self.packs.keys()),
|
||||
}
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
man = None
|
||||
try:
|
||||
man = load_manifest(self.series)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return {
|
||||
"series": self.series,
|
||||
"major": major_for_series(self.series),
|
||||
"operation_count": sum(p.operation_count() for p in self.packs.values()),
|
||||
"service_count": len(self.packs),
|
||||
"checksum": man.checksum if man else "",
|
||||
"microversion_overrides": dict(self.microversion_overrides),
|
||||
"services": [
|
||||
{
|
||||
"name": p.name,
|
||||
"type": p.typ,
|
||||
"port": p.port,
|
||||
"operation_count": p.operation_count(),
|
||||
"default_microversion": p.default_microversion,
|
||||
"max_microversion": p.max_microversion,
|
||||
"active_microversion": self.microversion_overrides.get(
|
||||
p.name, p.default_microversion
|
||||
),
|
||||
}
|
||||
for p in sorted(self.packs.values(), key=lambda x: x.name)
|
||||
],
|
||||
}
|
||||
|
||||
def set_microversion(self, service: str, version: str | None) -> None:
|
||||
with self._lock:
|
||||
if version is None:
|
||||
self.microversion_overrides.pop(service, None)
|
||||
else:
|
||||
self.microversion_overrides[service] = version
|
||||
|
||||
def active_microversion(self, service: str) -> str | None:
|
||||
with self._lock:
|
||||
if service in self.microversion_overrides:
|
||||
return self.microversion_overrides[service]
|
||||
pack = self.packs.get(service)
|
||||
return pack.default_microversion if pack else None
|
||||
|
||||
|
||||
_RUNTIME = ContractRuntime()
|
||||
|
||||
|
||||
def get_runtime() -> ContractRuntime:
|
||||
return _RUNTIME
|
||||
|
||||
|
||||
def ensure_loaded(series: str = "dalmatian") -> ContractRuntime:
|
||||
rt = get_runtime()
|
||||
if not rt.packs:
|
||||
try:
|
||||
rt.reload(series)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return rt
|
||||
Reference in New Issue
Block a user