"""Deterministic idempotent simulation seed profiles.""" from __future__ import annotations import json import uuid from dataclasses import dataclass import asyncpg # type: ignore[import-untyped] from asyncpg import Connection from app.security.auth import hash_secret NAMESPACE = uuid.UUID("c9040a72-b391-4a7e-9864-3ae46291a531") @dataclass(frozen=True, slots=True) class SeedNode: id: uuid.UUID name: str status: str @dataclass(frozen=True, slots=True) class SeedResource: id: uuid.UUID node_id: uuid.UUID kind: str external_id: str state: dict[str, object] @dataclass(frozen=True, slots=True) class SeedTask: id: uuid.UUID upid: str task_type: str payload: dict[str, object] @dataclass(frozen=True, slots=True) class SeedProfile: name: str nodes: tuple[SeedNode, ...] resources: tuple[SeedResource, ...] tasks: tuple[SeedTask, ...] = () def logical_state(self) -> dict[str, object]: nodes = [{"name": node.name, "status": node.status} for node in self.nodes] names = {node.id: node.name for node in self.nodes} resources = [ { "kind": resource.kind, "external_id": resource.external_id, "node": names[resource.node_id], "state": resource.state, } for resource in self.resources ] tasks = [ {"upid": task.upid, "task_type": task.task_type, "status": "success"} for task in self.tasks ] return {"profile": self.name, "nodes": nodes, "resources": resources, "tasks": tasks} def stable_id(name: str) -> uuid.UUID: return uuid.uuid5(NAMESPACE, name) def _string_list(state: dict[str, object], key: str) -> tuple[str, ...]: value = state.get(key, []) if not isinstance(value, list) or not all(isinstance(item, str) for item in value): raise ValueError(f"seed state {key} must be a string list") return tuple(value) def _node(name: str, status: str = "online") -> SeedNode: return SeedNode(stable_id(f"node:{name}"), name, status) def _resource( node: SeedNode, kind: str, external_id: str, state: dict[str, object] ) -> SeedResource: return SeedResource(stable_id(f"{kind}:{external_id}"), node.id, kind, external_id, state) def _completed_task(index: int, task_type: str, resource_id: str) -> SeedTask: return SeedTask( stable_id(f"task:{index}:{task_type}:{resource_id}"), f"UPID:pve1:0000000{index}:0000000{index}:6500000{index}:" f"{task_type}:{resource_id}:root@pam:", task_type, {"resource_id": resource_id, "seeded": True}, ) def small_profile() -> SeedProfile: node = _node("pve1") resources = ( _resource(node, "qemu", "100", {"name": "demo", "status": "stopped"}), _resource(node, "qemu", "101", {"name": "worker", "status": "stopped"}), _resource(node, "lxc", "200", {"name": "service", "status": "stopped"}), _resource(node, "storage", "local", {"content": ["iso", "backup"], "status": "available"}), _resource( node, "storage", "local-lvm", {"content": ["images", "rootdir"], "status": "available"} ), ) tasks = (_completed_task(1, "qmstart", "100"), _completed_task(2, "qmstop", "100")) return SeedProfile("small", (node,), resources, tasks) def medium_profile() -> SeedProfile: nodes = tuple(_node(f"pve{index}") for index in range(1, 4)) resources: list[SeedResource] = [] for vmid in range(100, 150): node = nodes[(vmid - 100) % len(nodes)] resources.append( _resource(node, "qemu", str(vmid), {"name": f"vm-{vmid}", "status": "stopped"}) ) for vmid in range(200, 220): node = nodes[(vmid - 200) % len(nodes)] resources.append( _resource(node, "lxc", str(vmid), {"name": f"ct-{vmid}", "status": "stopped"}) ) for node in nodes: resources.append( _resource( node, "storage", f"local-{node.name}", {"content": ["images"], "shared": False, "status": "available"}, ) ) resources.append( _resource( nodes[0], "storage", "shared", {"content": ["images", "backup"], "shared": True, "status": "available"}, ) ) resources.append(_resource(nodes[0], "pool", "development", {"members": ["100", "101", "200"]})) tasks = tuple(_completed_task(index, "qmstart", str(99 + index)) for index in range(1, 11)) return SeedProfile("medium", nodes, tuple(resources), tasks) def large_profile(*, node_count: int = 10, resource_count: int = 10_000) -> SeedProfile: if node_count < 1 or resource_count < 1: raise ValueError("large profile counts must be positive") nodes = tuple(_node(f"pve{index}") for index in range(1, node_count + 1)) resources = tuple( _resource( nodes[index % node_count], "qemu" if index % 4 else "lxc", str(100 + index), {"name": f"guest-{100 + index}", "status": "stopped"}, ) for index in range(resource_count) ) return SeedProfile("large", nodes, resources) def ha_demo_profile() -> SeedProfile: profile = medium_profile() resources = ( *profile.resources, _resource(profile.nodes[0], "ha", "vm:100", {"state": "started", "group": "primary"}), ) return SeedProfile("ha-demo", profile.nodes, resources, profile.tasks) def broken_storage_profile() -> SeedProfile: profile = small_profile() resources = tuple( _resource( next(node for node in profile.nodes if node.id == resource.node_id), resource.kind, resource.external_id, {**resource.state, "status": "offline", "error": "simulated I/O failure"} if resource.kind == "storage" and resource.external_id == "local-lvm" else resource.state, ) for resource in profile.resources ) return SeedProfile("broken-storage", profile.nodes, resources, profile.tasks) def build_profile( name: str, *, large_nodes: int = 10, large_resources: int = 10_000 ) -> SeedProfile: if name == "small": return small_profile() if name == "medium": return medium_profile() if name == "large": return large_profile(node_count=large_nodes, resource_count=large_resources) if name == "ha-demo": return ha_demo_profile() if name == "broken-storage": return broken_storage_profile() raise ValueError(f"unknown seed profile: {name}") async def apply_seed(connection: Connection, profile: SeedProfile) -> None: async with connection.transaction(): await connection.execute( """DELETE FROM task_logs; DELETE FROM task_events; DELETE FROM resource_locks; DELETE FROM tasks; DELETE FROM pool_members; DELETE FROM pools; DELETE FROM resources; DELETE FROM nodes""" ) await connection.executemany( "INSERT INTO nodes(id, name, status) VALUES($1, $2, $3)", [(node.id, node.name, node.status) for node in profile.nodes], ) await connection.executemany( """INSERT INTO resources(id, node_id, kind, external_id, state) VALUES($1, $2, $3, $4, $5::jsonb)""", [ ( resource.id, resource.node_id, resource.kind, resource.external_id, json.dumps(resource.state, sort_keys=True), ) for resource in profile.resources ], ) qemu = [resource for resource in profile.resources if resource.kind == "qemu"] if qemu: await connection.executemany( """INSERT INTO virtual_machines(resource_id, cluster_id, vmid, config) VALUES($1, 'dc760c47-d8d7-57e6-9404-f0c6f2395d8f', $2, $3::jsonb)""", [ ( resource.id, int(resource.external_id), json.dumps(resource.state, sort_keys=True), ) for resource in qemu ], ) containers = [resource for resource in profile.resources if resource.kind == "lxc"] if containers: await connection.executemany( """INSERT INTO containers(resource_id, cluster_id, vmid, config) VALUES($1, 'dc760c47-d8d7-57e6-9404-f0c6f2395d8f', $2, $3::jsonb)""", [ ( resource.id, int(resource.external_id), json.dumps(resource.state, sort_keys=True), ) for resource in containers ], ) storages = [resource for resource in profile.resources if resource.kind == "storage"] if storages: await connection.executemany( """INSERT INTO storages( resource_id, cluster_id, storage_id, storage_type, shared, config ) VALUES($1, 'dc760c47-d8d7-57e6-9404-f0c6f2395d8f', $2, $3, $4, $5::jsonb)""", [ ( resource.id, resource.external_id, "dir" if resource.external_id.startswith("local") else "nfs", bool(resource.state.get("shared", False)), json.dumps(resource.state, sort_keys=True), ) for resource in storages ], ) contents = [ ( stable_id(f"content:{resource.external_id}:{content}"), resource.id, f"{resource.external_id}:{content}/seeded", str(content), ) for resource in storages for content in _string_list(resource.state, "content") ] if contents: await connection.executemany( """INSERT INTO storage_contents( id, storage_resource_id, volume_id, content_type ) VALUES($1, $2, $3, $4)""", contents, ) pools = [resource for resource in profile.resources if resource.kind == "pool"] if pools: await connection.executemany( """INSERT INTO pools(id, cluster_id, pool_id, metadata) VALUES($1, 'dc760c47-d8d7-57e6-9404-f0c6f2395d8f', $2, $3::jsonb)""", [ (resource.id, resource.external_id, json.dumps(resource.state, sort_keys=True)) for resource in pools ], ) members = [ (pool.id, member.id) for pool in pools for external_id in _string_list(pool.state, "members") for member in profile.resources if member.external_id == external_id and member.kind in {"qemu", "lxc"} ] if members: await connection.executemany( "INSERT INTO pool_members(pool_id, resource_id) VALUES($1, $2)", members ) if profile.tasks: await connection.executemany( """INSERT INTO tasks(id, upid, status, payload, task_type, progress, result) VALUES($1, $2, 'success', $3::jsonb, $4, 100, '{\"seeded\":true}'::jsonb)""", [ (task.id, task.upid, json.dumps(task.payload, sort_keys=True), task.task_type) for task in profile.tasks ], ) await connection.execute( """INSERT INTO principals(id, name, password_hash, realm_name) VALUES($1, 'root@pam', $2, 'pam') ON CONFLICT (name) DO UPDATE SET password_hash=EXCLUDED.password_hash, realm_name=EXCLUDED.realm_name""", stable_id("principal:root@pam"), hash_secret("secret", salt=b"pve-simulator-v1"), ) await connection.execute( """INSERT INTO api_tokens(principal_id, token_id, secret_hash, privileges) VALUES($1, 'automation', $2, $3) ON CONFLICT (principal_id, token_id) DO UPDATE SET secret_hash=EXCLUDED.secret_hash, privileges=EXCLUDED.privileges""", stable_id("principal:root@pam"), hash_secret("automation-secret", salt=b"pve-token-seed-v1"), ["VM.Audit", "VM.PowerMgmt", "Sys.Audit"], ) auditor_id = stable_id("principal:auditor@pve") await connection.execute( """INSERT INTO principals(id, name, password_hash, realm_name) VALUES($1, 'auditor@pve', $2, 'pve') ON CONFLICT (name) DO UPDATE SET password_hash=EXCLUDED.password_hash, realm_name=EXCLUDED.realm_name""", auditor_id, hash_secret("auditor-secret", salt=b"pve-auditor-v1"), ) await connection.execute( """INSERT INTO roles(name, privileges) VALUES('PVEAuditor', $1) ON CONFLICT (name) DO UPDATE SET privileges=EXCLUDED.privileges""", ["Sys.Audit", "VM.Audit"], ) await connection.execute( "DELETE FROM acl_entries WHERE principal_id=$1 AND role_name='PVEAuditor'", auditor_id, ) auditor_group_id = stable_id("group:auditors") await connection.execute( """INSERT INTO identity_groups(id, group_id, comment) VALUES($1, 'auditors', 'Read-only operators') ON CONFLICT (group_id) DO UPDATE SET comment=EXCLUDED.comment""", auditor_group_id, ) await connection.execute( """INSERT INTO identity_group_members(group_id, principal_id) VALUES($1, $2) ON CONFLICT DO NOTHING""", auditor_group_id, auditor_id, ) await connection.execute( """INSERT INTO group_acl_entries(group_id, role_name, path, propagate) VALUES($1, 'PVEAuditor', '/', true) ON CONFLICT (group_id, role_name, path) DO UPDATE SET propagate=EXCLUDED.propagate""", auditor_group_id, ) await connection.execute( """INSERT INTO api_tokens(principal_id, token_id, secret_hash, privileges) VALUES($1, 'readonly', $2, $3) ON CONFLICT (principal_id, token_id) DO UPDATE SET secret_hash=EXCLUDED.secret_hash, privileges=EXCLUDED.privileges""", auditor_id, hash_secret("readonly-secret", salt=b"pve-readonly-v1"), ["Sys.Audit", "VM.Audit"], ) for username, role_name, privileges, acl_path, token_id, token_secret in ( ( "operator@pve", "PVEVMOperator", ["VM.Audit", "VM.PowerMgmt"], "/vms", "operator", "operator-secret", ), ( "storage@pve", "PVEStorageUser", ["Datastore.Audit", "Datastore.AllocateSpace"], "/storage", "storage", "storage-secret", ), ): principal_id = stable_id(f"principal:{username}") await connection.execute( """INSERT INTO principals(id, name, password_hash, realm_name) VALUES($1, $2, $3, 'pve') ON CONFLICT (name) DO UPDATE SET password_hash=EXCLUDED.password_hash, realm_name=EXCLUDED.realm_name""", principal_id, username, hash_secret(f"{username}-password", salt=f"seed:{username}".encode()), ) await connection.execute( """INSERT INTO roles(name, privileges) VALUES($1, $2) ON CONFLICT (name) DO UPDATE SET privileges=EXCLUDED.privileges""", role_name, privileges, ) await connection.execute( """INSERT INTO acl_entries(principal_id, role_name, path, propagate) VALUES($1, $2, $3, true) ON CONFLICT (principal_id, role_name, path) DO UPDATE SET propagate=EXCLUDED.propagate""", principal_id, role_name, acl_path, ) await connection.execute( """INSERT INTO api_tokens(principal_id, token_id, secret_hash, privileges) VALUES($1, $2, $3, $4) ON CONFLICT (principal_id, token_id) DO UPDATE SET secret_hash=EXCLUDED.secret_hash, privileges=EXCLUDED.privileges, privilege_separation=true""", principal_id, token_id, hash_secret(token_secret, salt=f"token:{username}".encode()), privileges, ) async def seed_url( database_url: str, profile_name: str = "small", *, large_nodes: int = 10, large_resources: int = 10_000, ) -> dict[str, object]: connection = await asyncpg.connect(database_url) try: profile = build_profile( profile_name, large_nodes=large_nodes, large_resources=large_resources ) await apply_seed(connection, profile) return profile.logical_state() finally: await connection.close()