"""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