Files
openstack-api-simulator/app/openstack/routes/heat.py
T
inecs 6033967e6a 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.
2026-07-18 04:26:48 +03:00

269 lines
8.8 KiB
Python

"""Heat Orchestration API v1."""
from __future__ import annotations
import json
from typing import Annotated, Any
from uuid import uuid4
from asyncpg import Connection
from fastapi import APIRouter, Depends, Request, Response
from app.openstack.auth import TokenContext
from app.openstack.deps import get_conn, require_project_token, require_token
from app.openstack.errors import OpenStackError
router = APIRouter(tags=["Heat"])
def _stack(row: Any) -> dict[str, Any]:
return {
"id": str(row["id"]),
"stack_name": row["stack_name"],
"stack_status": row["stack_status"],
"description": row["description"],
"creation_time": row["created_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
"updated_time": row["updated_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
"stack_owner": str(row["project_id"]),
"parent": None,
"stack_user_project_id": str(row["project_id"]),
"outputs": row["outputs"]
if not isinstance(row["outputs"], str)
else json.loads(row["outputs"]),
"parameters": row["parameters"]
if not isinstance(row["parameters"], str)
else json.loads(row["parameters"]),
"links": [
{
"rel": "self",
"href": f"/v1/{row['project_id']}/stacks/{row['stack_name']}/{row['id']}",
}
],
}
@router.get("/v1")
@router.get("/v1/")
async def heat_versions(conn: Annotated[Connection, Depends(get_conn)]) -> dict[str, object]:
from app.openstack.db_docs import require_doc
return await require_doc(
conn, service="heat", resource_type="discovery_version", name="default"
)
@router.get("/v1/{tenant_id}/stacks")
async def list_stacks(
tenant_id: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> dict[str, object]:
from app.openstack.paging import paginate_rows
_ = tenant_id
rows = list(
await conn.fetch(
"SELECT * FROM os_stacks WHERE project_id = $1 ORDER BY created_at, id",
ctx.project_id,
)
)
page, links = paginate_rows(rows, request, id_attr=lambda r: str(r["id"]))
body: dict[str, object] = {"stacks": [_stack(r) for r in page]}
if links:
body["stacks_links"] = links
return body
@router.get("/v1/{tenant_id}/stacks/detail")
async def list_stacks_detail(
tenant_id: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> dict[str, object]:
return await list_stacks(tenant_id, request, conn, ctx)
@router.post("/v1/{tenant_id}/stacks", status_code=201)
async def create_stack(
tenant_id: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> dict[str, object]:
from app.openstack.db_docs import fetch_doc
_ = tenant_id
payload = await request.json()
stack = payload.get("stack") or payload
defaults = (
await fetch_doc(conn, service="heat", resource_type="stack_defaults", name="default") or {}
)
name = stack.get("stack_name") or stack.get("name") or f"stack-{uuid4().hex[:8]}"
template = (
stack.get("template")
if isinstance(stack.get("template"), dict)
else defaults.get("template")
)
parameters = (
stack.get("parameters")
if isinstance(stack.get("parameters"), dict)
else defaults.get("parameters")
)
if not isinstance(template, dict):
template = {}
if not isinstance(parameters, dict):
parameters = {}
row = await conn.fetchrow(
"""INSERT INTO os_stacks(id, project_id, stack_name, stack_status, description, template, parameters, outputs)
VALUES($1,$2,$3,'CREATE_COMPLETE',$4,$5::jsonb,$6::jsonb,'[]'::jsonb) RETURNING *""",
uuid4(),
ctx.project_id,
name,
stack.get("description") or "",
json.dumps(template),
json.dumps(parameters),
)
return {"stack": _stack(row)}
@router.get("/v1/{tenant_id}/stacks/{id}")
async def show_stack_by_id(
tenant_id: str,
id: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> dict[str, object]:
_ = tenant_id
row = await conn.fetchrow(
"SELECT * FROM os_stacks WHERE project_id=$1 AND (id::text=$2 OR stack_name=$2) ORDER BY created_at DESC LIMIT 1",
ctx.project_id,
id,
)
if row is None:
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
return {"stack": _stack(row)}
@router.put("/v1/{tenant_id}/stacks/{id}")
@router.patch("/v1/{tenant_id}/stacks/{id}")
async def update_stack_by_id(
tenant_id: str,
id: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> dict[str, object]:
_ = tenant_id
payload = await request.json()
stack = payload.get("stack") or payload
row = await conn.fetchrow(
"SELECT * FROM os_stacks WHERE project_id=$1 AND (id::text=$2 OR stack_name=$2) ORDER BY created_at DESC LIMIT 1",
ctx.project_id,
id,
)
if row is None:
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
desc = stack.get("description") if "description" in stack else row["description"]
await conn.execute(
"UPDATE os_stacks SET description=$1, updated_at=now(), stack_status='UPDATE_COMPLETE' WHERE id=$2",
desc,
row["id"],
)
row = await conn.fetchrow("SELECT * FROM os_stacks WHERE id=$1", row["id"])
return {"stack": _stack(row)}
@router.delete("/v1/{tenant_id}/stacks/{id}", status_code=204)
async def delete_stack_by_id(
tenant_id: str,
id: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> Response:
_ = tenant_id
result = await conn.execute(
"DELETE FROM os_stacks WHERE project_id=$1 AND (id::text=$2 OR stack_name=$2)",
ctx.project_id,
id,
)
if result.endswith("0"):
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
return Response(status_code=204)
@router.get("/v1/{tenant_id}/stacks/{stack_name}/{stack_id}")
@router.get("/v1/{tenant_id}/stacks/{stack_name}")
async def show_stack(
tenant_id: str,
stack_name: str,
request: Request,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
stack_id: str | None = None,
) -> dict[str, object]:
_ = tenant_id
if stack_name == "detail" and stack_id is None:
from app.openstack.paging import paginate_rows
rows = list(
await conn.fetch(
"SELECT * FROM os_stacks WHERE project_id = $1 ORDER BY created_at, id",
ctx.project_id,
)
)
page, links = paginate_rows(rows, request, id_attr=lambda r: str(r["id"]))
body: dict[str, object] = {"stacks": [_stack(r) for r in page]}
if links:
body["stacks_links"] = links
return body
if stack_id:
row = await conn.fetchrow(
"SELECT * FROM os_stacks WHERE project_id=$1 AND id::text=$2",
ctx.project_id,
stack_id,
)
else:
row = await conn.fetchrow(
"SELECT * FROM os_stacks WHERE project_id=$1 AND stack_name=$2 ORDER BY created_at DESC LIMIT 1",
ctx.project_id,
stack_name,
)
if row is None:
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
return {"stack": _stack(row)}
@router.delete("/v1/{tenant_id}/stacks/{stack_name}/{stack_id}", status_code=204)
async def delete_stack(
tenant_id: str,
stack_name: str,
stack_id: str,
conn: Annotated[Connection, Depends(get_conn)],
ctx: Annotated[TokenContext, Depends(require_project_token)],
) -> Response:
_ = tenant_id, stack_name
result = await conn.execute(
"DELETE FROM os_stacks WHERE project_id=$1 AND id::text=$2",
ctx.project_id,
stack_id,
)
if result.endswith("0"):
raise OpenStackError("StackNotFound", "Stack not found", status_code=404)
return Response(status_code=204)
@router.get("/v1/{tenant_id}/resource_types")
async def resource_types(
tenant_id: str,
conn: Annotated[Connection, Depends(get_conn)],
_ctx: Annotated[TokenContext, Depends(require_token)],
) -> dict[str, object]:
_ = tenant_id
from app.openstack.db_docs import require_doc
return await require_doc(
conn, service="heat", resource_type="resource_type_list", name="default"
)