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,145 @@
|
||||
"""Shared handler helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.api.errors import ApiError
|
||||
from app.db.pool import AsyncpgDatabase
|
||||
|
||||
|
||||
def database(request: Request) -> AsyncpgDatabase:
|
||||
return cast(AsyncpgDatabase, request.app.state.database)
|
||||
|
||||
|
||||
def values(inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
return cast(dict[str, Any], inputs["values"])
|
||||
|
||||
|
||||
def require_value(payload: Mapping[str, Any], key: str) -> Any:
|
||||
if key not in payload or payload[key] in {None, ""}:
|
||||
raise ApiError(400, f"parameter '{key}' is required")
|
||||
return payload[key]
|
||||
|
||||
|
||||
def state(value: object) -> dict[str, Any]:
|
||||
if isinstance(value, str):
|
||||
return cast(dict[str, Any], json.loads(value))
|
||||
return dict(cast(Mapping[str, Any], value))
|
||||
|
||||
|
||||
def subdirs(*names: str) -> list[dict[str, str]]:
|
||||
return [{"subdir": name} for name in names]
|
||||
|
||||
|
||||
_SIZE_RE = re.compile(r"^(?P<value>\d+)(?P<unit>[KMGT]?)$", re.IGNORECASE)
|
||||
_UNITS = {"": 1, "K": 2**10, "M": 2**20, "G": 2**30, "T": 2**40}
|
||||
|
||||
|
||||
def parse_size_bytes(value: str) -> int:
|
||||
match = _SIZE_RE.fullmatch(value.strip())
|
||||
if match is None:
|
||||
raise ValueError(f"invalid disk size: {value}")
|
||||
return int(match.group("value")) * _UNITS[match.group("unit").upper()]
|
||||
|
||||
|
||||
def resize_size_bytes(value: str, current: int) -> int:
|
||||
if value.startswith("+"):
|
||||
return current + parse_size_bytes(value[1:])
|
||||
result = parse_size_bytes(value)
|
||||
if result < current:
|
||||
raise ValueError("shrinking disks is not supported")
|
||||
return result
|
||||
|
||||
|
||||
def replace_disk_size(value: str, size: int) -> str:
|
||||
parts = [part for part in value.split(",") if not part.startswith("size=")]
|
||||
parts.append(f"size={size // 2**30}G" if size % 2**30 == 0 else f"size={size}")
|
||||
return ",".join(parts)
|
||||
|
||||
|
||||
def disk_size_bytes(value: str) -> int:
|
||||
for part in value.split(","):
|
||||
if part.startswith("size="):
|
||||
return parse_size_bytes(part.removeprefix("size="))
|
||||
return 0
|
||||
|
||||
|
||||
async def require_node(request: Request, node: str) -> None:
|
||||
exists = await database(request).pool.fetchval(
|
||||
"SELECT EXISTS(SELECT 1 FROM nodes WHERE name=$1)",
|
||||
node,
|
||||
)
|
||||
if not exists:
|
||||
raise ApiError(404, "node does not exist")
|
||||
|
||||
|
||||
async def cluster_metadata(request: Request) -> dict[str, Any]:
|
||||
from app.simulation.seed import CLUSTER_ID
|
||||
|
||||
row = await database(request).pool.fetchrow(
|
||||
"SELECT metadata FROM clusters WHERE id=$1",
|
||||
CLUSTER_ID,
|
||||
)
|
||||
return state(row["metadata"]) if row is not None else {}
|
||||
|
||||
|
||||
async def save_cluster_metadata(request: Request, metadata: dict[str, Any]) -> None:
|
||||
from app.simulation.seed import CLUSTER_ID
|
||||
|
||||
await database(request).pool.execute(
|
||||
"UPDATE clusters SET metadata=$2::jsonb, updated_at=now() WHERE id=$1",
|
||||
CLUSTER_ID,
|
||||
json.dumps(metadata, sort_keys=True),
|
||||
)
|
||||
|
||||
|
||||
async def node_metadata(request: Request, node: str) -> dict[str, Any]:
|
||||
row = await database(request).pool.fetchrow(
|
||||
"SELECT metadata FROM nodes WHERE name=$1",
|
||||
node,
|
||||
)
|
||||
if row is None:
|
||||
raise ApiError(404, "node does not exist")
|
||||
return state(row["metadata"])
|
||||
|
||||
|
||||
async def save_node_metadata(request: Request, node: str, metadata: dict[str, Any]) -> None:
|
||||
status = await database(request).pool.execute(
|
||||
"UPDATE nodes SET metadata=$2::jsonb, updated_at=now() WHERE name=$1",
|
||||
node,
|
||||
json.dumps(metadata, sort_keys=True),
|
||||
)
|
||||
if status != "UPDATE 1":
|
||||
raise ApiError(404, "node does not exist")
|
||||
|
||||
|
||||
def storage_payload(row: Any) -> dict[str, Any]:
|
||||
config = state(row["config"])
|
||||
content = config.get("content", [])
|
||||
if isinstance(content, list):
|
||||
content_str = ",".join(str(item) for item in content)
|
||||
else:
|
||||
content_str = str(content)
|
||||
total = int(row["capacity_bytes"] or 0)
|
||||
used = int(row["used_bytes"] or 0)
|
||||
avail = max(total - used, 0)
|
||||
payload: dict[str, Any] = {
|
||||
"storage": str(row["storage_id"]),
|
||||
"type": str(row["storage_type"]),
|
||||
"shared": int(bool(row["shared"])),
|
||||
"content": content_str,
|
||||
"active": 1,
|
||||
"enabled": 1,
|
||||
"total": total,
|
||||
"used": used,
|
||||
"avail": avail,
|
||||
}
|
||||
if total:
|
||||
payload["used_fraction"] = used / total
|
||||
return payload
|
||||
Reference in New Issue
Block a user