6033967e6a
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
205 lines
7.1 KiB
Python
205 lines
7.1 KiB
Python
"""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
|