332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""Glance Image API v2 (lab subset)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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=["Glance"])
|
|
|
|
|
|
def _image(row: Any) -> dict[str, Any]:
|
|
return {
|
|
"id": str(row["id"]),
|
|
"name": row["name"],
|
|
"status": row["status"],
|
|
"visibility": row["visibility"],
|
|
"size": row["size"],
|
|
"disk_format": row["disk_format"],
|
|
"container_format": row["container_format"],
|
|
"min_disk": 0,
|
|
"min_ram": 0,
|
|
"protected": False,
|
|
"checksum": None,
|
|
"owner": str(row["owner_project_id"]) if row["owner_project_id"] else None,
|
|
"created_at": row["created_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"updated_at": row["updated_at"].strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"tags": [],
|
|
"file": f"/v2/images/{row['id']}/file",
|
|
"schema": "/v2/schemas/image",
|
|
}
|
|
|
|
|
|
@router.get("/v2")
|
|
@router.get("/v2/")
|
|
async def glance_versions(conn: Annotated[Connection, Depends(get_conn)]) -> dict[str, object]:
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(
|
|
conn, service="glance", resource_type="discovery_version", name="default"
|
|
)
|
|
|
|
|
|
@router.get("/v2/images")
|
|
async def list_images(
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.paging import paginate_rows
|
|
|
|
name = request.query_params.get("name")
|
|
if name:
|
|
rows = list(
|
|
await conn.fetch(
|
|
"""SELECT * FROM os_images
|
|
WHERE (visibility = 'public' OR owner_project_id = $1)
|
|
AND name = $2
|
|
ORDER BY created_at, id""",
|
|
ctx.project_id,
|
|
name,
|
|
)
|
|
)
|
|
else:
|
|
rows = list(
|
|
await conn.fetch(
|
|
"""SELECT * FROM os_images
|
|
WHERE visibility = 'public'
|
|
OR owner_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] = {
|
|
"images": [_image(r) for r in page],
|
|
"first": "/v2/images",
|
|
"schema": "/v2/schemas/images",
|
|
}
|
|
if links:
|
|
body["images_links"] = links
|
|
return body
|
|
|
|
|
|
async def _show_image(
|
|
resource_id: str,
|
|
conn: Connection,
|
|
ctx: TokenContext,
|
|
) -> dict[str, Any]:
|
|
row = await conn.fetchrow(
|
|
"""SELECT * FROM os_images
|
|
WHERE (id::text = $1 OR name = $1)
|
|
AND (visibility = 'public' OR owner_project_id = $2)
|
|
ORDER BY CASE WHEN id::text = $1 THEN 0 ELSE 1 END
|
|
LIMIT 1""",
|
|
resource_id,
|
|
ctx.project_id,
|
|
)
|
|
if row is None:
|
|
raise OpenStackError("ImageNotFound", "Image not found", status_code=404)
|
|
return _image(row)
|
|
|
|
|
|
@router.get("/v2/images/{image_id}")
|
|
async def show_image(
|
|
image_id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_token)],
|
|
) -> dict[str, object]:
|
|
return await _show_image(image_id, conn, ctx)
|
|
|
|
|
|
@router.get("/v2/images/{id}")
|
|
async def show_image_by_id(
|
|
id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_token)],
|
|
) -> dict[str, object]:
|
|
return await _show_image(id, conn, ctx)
|
|
|
|
|
|
async def _update_image(
|
|
resource_id: str,
|
|
request: Request,
|
|
conn: Connection,
|
|
ctx: TokenContext,
|
|
) -> dict[str, Any]:
|
|
payload = await request.json()
|
|
body = payload.get("image") if isinstance(payload.get("image"), dict) else payload
|
|
row = await conn.fetchrow(
|
|
"""UPDATE os_images
|
|
SET name = COALESCE($1, name), updated_at = now()
|
|
WHERE id = $2::uuid AND owner_project_id = $3
|
|
RETURNING *""",
|
|
body.get("name") if isinstance(body, dict) else None,
|
|
resource_id,
|
|
ctx.project_id,
|
|
)
|
|
if row is None:
|
|
raise OpenStackError("ImageNotFound", "Image not found", status_code=404)
|
|
return _image(row)
|
|
|
|
|
|
@router.put("/v2/images/{image_id}")
|
|
@router.patch("/v2/images/{image_id}")
|
|
async def update_image(
|
|
image_id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, Any]:
|
|
return await _update_image(image_id, request, conn, ctx)
|
|
|
|
|
|
@router.put("/v2/images/{id}")
|
|
@router.patch("/v2/images/{id}")
|
|
async def update_image_by_id(
|
|
id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, Any]:
|
|
return await _update_image(id, request, conn, ctx)
|
|
|
|
|
|
@router.post("/v2/images", status_code=201)
|
|
async def create_image(
|
|
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
|
|
|
|
payload = await request.json()
|
|
defaults = (
|
|
await fetch_doc(conn, service="glance", resource_type="image_defaults", name="default")
|
|
or {}
|
|
)
|
|
image_id = uuid4()
|
|
row = await conn.fetchrow(
|
|
"""INSERT INTO os_images(id, name, status, visibility, size, disk_format,
|
|
container_format, owner_project_id)
|
|
VALUES($1, $2, 'queued', $3, 0, $4, $5, $6)
|
|
RETURNING *""",
|
|
image_id,
|
|
payload.get("name") or defaults.get("name") or "image",
|
|
payload.get("visibility") or defaults.get("visibility"),
|
|
payload.get("disk_format") or defaults.get("disk_format"),
|
|
payload.get("container_format") or defaults.get("container_format"),
|
|
ctx.project_id,
|
|
)
|
|
return _image(row)
|
|
|
|
|
|
@router.get("/v2/images/{image_id}/file")
|
|
async def download_image_file(
|
|
image_id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> Response:
|
|
row = await conn.fetchrow(
|
|
"""SELECT id, size FROM os_images
|
|
WHERE (id::text=$1 OR name=$1)
|
|
AND (owner_project_id=$2 OR visibility='public')
|
|
LIMIT 1""",
|
|
image_id,
|
|
ctx.project_id,
|
|
)
|
|
if row is None:
|
|
# Materialize pack/schema image rows into os_images on first download.
|
|
api = await conn.fetchrow(
|
|
"""SELECT id, name, data FROM os_api_objects
|
|
WHERE service='glance' AND resource_type='image'
|
|
AND (id::text=$1 OR name=$1)
|
|
LIMIT 1""",
|
|
image_id,
|
|
)
|
|
if api is None:
|
|
raise OpenStackError("ImageNotFound", f"image {image_id} not found", status_code=404)
|
|
data = api["data"]
|
|
if isinstance(data, str):
|
|
import json as _json
|
|
|
|
data = _json.loads(data or "{}")
|
|
await conn.execute(
|
|
"""INSERT INTO os_images(id, name, status, visibility, size, disk_format,
|
|
container_format, owner_project_id)
|
|
VALUES($1::uuid,$2,'active',$3,$4,$5,$6,$7)
|
|
ON CONFLICT (id) DO UPDATE SET updated_at=now()""",
|
|
api["id"],
|
|
api["name"] or (data or {}).get("name") or "image",
|
|
(data or {}).get("visibility") or "private",
|
|
int((data or {}).get("size") or 0),
|
|
(data or {}).get("disk_format") or "qcow2",
|
|
(data or {}).get("container_format") or "bare",
|
|
ctx.project_id,
|
|
)
|
|
size = int((data or {}).get("size") or 0)
|
|
else:
|
|
size = int(row["size"] or 0)
|
|
# Lab payload is capped; Content-Length must match the bytes we actually send
|
|
# (advertising the virtual image size breaks urllib/clients with IncompleteRead).
|
|
content = b"\0" * min(size, 64) if size else b"probe-image"
|
|
return Response(
|
|
content=content,
|
|
media_type="application/octet-stream",
|
|
headers={"Content-Length": str(len(content))},
|
|
)
|
|
|
|
|
|
@router.put("/v2/images/{image_id}/file", status_code=204)
|
|
async def upload_image_file(
|
|
image_id: str,
|
|
request: Request,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> Response:
|
|
body = await request.body()
|
|
result = await conn.execute(
|
|
"""UPDATE os_images
|
|
SET status = 'active', size = $1, updated_at = now()
|
|
WHERE (id::text = $2 OR name = $2) AND owner_project_id = $3""",
|
|
len(body),
|
|
image_id,
|
|
ctx.project_id,
|
|
)
|
|
if result.endswith("0"):
|
|
raise OpenStackError("ImageNotFound", "Image not found", status_code=404)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.delete("/v2/images/{image_id}", status_code=204)
|
|
async def delete_image(
|
|
image_id: str,
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> Response:
|
|
result = await conn.execute(
|
|
"DELETE FROM os_images WHERE id = $1::uuid AND owner_project_id = $2",
|
|
image_id,
|
|
ctx.project_id,
|
|
)
|
|
if result.endswith("0"):
|
|
raise OpenStackError("ImageNotFound", "Image not found", status_code=404)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.get("/v2/info/stores")
|
|
async def glance_stores(
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
_ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(conn, service="glance", resource_type="info_stores", name="default")
|
|
|
|
|
|
@router.get("/v2/info/import")
|
|
async def glance_import_info(
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
_ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(conn, service="glance", resource_type="info_import", name="default")
|
|
|
|
|
|
@router.get("/v2/schemas/image")
|
|
async def glance_schema_image(
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
_ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(conn, service="glance", resource_type="schema", name="image")
|
|
|
|
|
|
@router.get("/v2/schemas/images")
|
|
async def glance_schema_images(
|
|
conn: Annotated[Connection, Depends(get_conn)],
|
|
_ctx: Annotated[TokenContext, Depends(require_project_token)],
|
|
) -> dict[str, object]:
|
|
from app.openstack.db_docs import require_doc
|
|
|
|
return await require_doc(conn, service="glance", resource_type="schema", name="images")
|