"""Additional node-level handlers with durable ops persistence.""" from __future__ import annotations import copy import json import secrets import time from typing import Any from fastapi import Request from app.api.errors import ApiError from app.api.registry import HandlerRegistry from app.handlers.common import database, require_node, subdirs, values from app.handlers.nodes import default_node_ops, load_node_ops, save_node_ops from app.tasks.repository import TaskRepository from app.tasks.upid import Upid DEFAULT_HARDWARE: dict[str, Any] = { "pci": [ { "id": "0000:00:1f.2", "vendor_name": "Intel Corporation", "device_name": "SATA Controller", "iommugroup": 0, }, { "id": "0000:01:00.0", "vendor_name": "NVIDIA Corporation", "device_name": "GP102 [GeForce GTX 1080 Ti]", "iommugroup": 1, "mdev": 1, }, ], "usb": [ {"busnum": 1, "devnum": 1, "level": 0, "port": "1", "prodid": "0002", "vendid": "1d6b"}, {"busnum": 2, "devnum": 2, "level": 1, "port": "2", "prodid": "5591", "vendid": "0781"}, ], "mdev": { "0000:01:00.0": [ {"type": "nvidia-11", "available": 4, "description": "GRID profile"}, ] }, } DEFAULT_SCAN: dict[str, list[dict[str, Any]]] = { "cifs": [{"server": "files.local", "share": "backups"}], "iscsi": [{"portal": "10.0.0.50:3260", "target": "iqn.2024-01.local:storage"}], "lvm": [{"vg": "pve", "size": 500_000_000_000, "free": 100_000_000_000}], "lvmthin": [{"lv": "data", "vg": "pve", "lv_size": 400_000_000_000}], "nfs": [{"server": "nfs.local", "path": "/export/pve", "options": "vers=4"}], "pbs": [{"server": "pbs.local", "datastore": "store1"}], "zfs": [{"pool": "rpool", "name": "rpool/data", "size": 800_000_000_000}], } DEFAULT_SUBSCRIPTION: dict[str, Any] = { "status": "notfound", "message": "There is no subscription key", "serverid": "SIMULATOR", "sockets": 1, "productname": "Proxmox VE", "url": "https://www.proxmox.com/en/proxmox-virtual-environment/pricing", } DEFAULT_CONFIG: dict[str, Any] = { "description": "Simulator node", "startall-onboot-delay": 0, "wakeonlan": "", } DEFAULT_DNS: dict[str, Any] = { "search": "local", "dns1": "1.1.1.1", "dns2": "8.8.8.8", "dns3": "", } DEFAULT_TIME: dict[str, Any] = { "timezone": "UTC", "time": 0, "localtime": 0, } def _certificates(ops: dict[str, Any]) -> dict[str, Any]: certs = ops.setdefault( "certificates", { "custom": None, "acme": {"account": "default", "domains": [], "certificate": None}, "info": [], }, ) if not isinstance(certs, dict): certs = {"custom": None, "acme": {}, "info": []} ops["certificates"] = certs certs.setdefault("acme", {"account": "default", "domains": [], "certificate": None}) certs.setdefault("info", []) return certs def _hardware(ops: dict[str, Any]) -> dict[str, Any]: hardware = ops.get("hardware") if not isinstance(hardware, dict) or not hardware: hardware = copy.deepcopy(DEFAULT_HARDWARE) ops["hardware"] = hardware hardware.setdefault("pci", copy.deepcopy(DEFAULT_HARDWARE["pci"])) hardware.setdefault("usb", copy.deepcopy(DEFAULT_HARDWARE["usb"])) hardware.setdefault("mdev", copy.deepcopy(DEFAULT_HARDWARE["mdev"])) return hardware def _scan_cache(ops: dict[str, Any]) -> dict[str, Any]: scan = ops.get("scan") if not isinstance(scan, dict) or not scan: scan = copy.deepcopy(DEFAULT_SCAN) ops["scan"] = scan for key, value in DEFAULT_SCAN.items(): scan.setdefault(key, copy.deepcopy(value)) return scan def _subscription(ops: dict[str, Any]) -> dict[str, Any]: subscription = ops.get("subscription") if not isinstance(subscription, dict) or not subscription: subscription = copy.deepcopy(DEFAULT_SUBSCRIPTION) ops["subscription"] = subscription return subscription def _disk_items(ops: dict[str, Any], kind: str) -> list[dict[str, Any]]: disks = ops.setdefault("disks", default_node_ops()["disks"]) if not isinstance(disks, dict): disks = default_node_ops()["disks"] ops["disks"] = disks items = disks.setdefault(kind, []) if not isinstance(items, list): items = [] disks[kind] = items return items def _public_cert(entry: dict[str, Any] | None) -> dict[str, Any] | None: if entry is None: return None return {key: value for key, value in entry.items() if key not in {"key", "private-key"}} async def _node_task(request: Request, *, node: str, task_type: str, worker: str) -> str: from app.db.primitives import ConflictError pool = database(request).pool upid = str(Upid.allocate(node, worker, "0", str(request.state.principal))) try: task = await TaskRepository(pool).create( upid=upid, task_type=task_type, payload={"node": node}, resource_key=f"node:{node}:{task_type}", ) except ConflictError as error: raise ApiError(409, str(error)) from error return task.upid async def _set_guest_status(request: Request, node: str, status: str) -> None: await database(request).pool.execute( """UPDATE resources AS r SET state = jsonb_set(COALESCE(r.state, '{}'::jsonb), '{status}', to_jsonb($2::text), true), updated_at=now() WHERE r.node_id=(SELECT id FROM nodes WHERE name=$1) AND r.kind IN ('qemu', 'lxc')""", node, status, ) async def _migrate_guests(request: Request, node: str, target: str) -> None: target_row = await database(request).pool.fetchrow("SELECT id FROM nodes WHERE name=$1", target) if target_row is None: raise ApiError(404, "target node does not exist") await database(request).pool.execute( """UPDATE resources SET node_id=$2, updated_at=now() WHERE node_id=(SELECT id FROM nodes WHERE name=$1) AND kind IN ('qemu', 'lxc')""", node, target_row["id"], ) def register_nodes_extra_handlers(registry: HandlerRegistry) -> None: async def disks_create(request: Request, inputs: dict[str, Any], kind: str) -> dict[str, Any]: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) name = str( payload.get("name") or payload.get("device") or payload.get("vgname") or payload.get("pool") or f"{kind}-{secrets.token_hex(2)}" ) ops = await load_node_ops(request, node) items = _disk_items(ops, kind) if any(str(item.get("name")) == name for item in items): raise ApiError(400, f"{kind} '{name}' already exists") entry = { key: value for key, value in payload.items() if key not in {"node", "delete", "digest"} } entry["name"] = name items.append(entry) ops.setdefault("disks", default_node_ops()["disks"])[kind] = items await save_node_ops(request, node, ops) return entry async def disks_delete(request: Request, inputs: dict[str, Any], kind: str) -> None: payload = values(inputs) node = str(payload["node"]) name = str(payload["name"]) await require_node(request, node) ops = await load_node_ops(request, node) items = _disk_items(ops, kind) remaining = [item for item in items if str(item.get("name")) != name] if len(remaining) == len(items): raise ApiError(404, f"{kind} does not exist") ops.setdefault("disks", {})[kind] = remaining await save_node_ops(request, node, ops) async def disks_zfs_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: payload = values(inputs) node = str(payload["node"]) name = str(payload["name"]) await require_node(request, node) ops = await load_node_ops(request, node) for item in _disk_items(ops, "zfs"): if str(item.get("name")) == name: return dict(item) raise ApiError(404, "zfs pool does not exist") async def certificates_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]: await require_node(request, str(values(inputs)["node"])) return subdirs("acme", "custom", "info") async def certificates_acme_index( request: Request, inputs: dict[str, Any] ) -> list[dict[str, str]]: await require_node(request, str(values(inputs)["node"])) return subdirs("certificate") async def certificates_acme_mutate(request: Request, inputs: dict[str, Any]) -> str | None: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) certs = _certificates(ops) acme = dict(certs.get("acme") or {}) method = request.method.upper() if method == "DELETE": acme["certificate"] = None acme["domains"] = [] else: domains = payload.get("domains") or payload.get("domain") or acme.get("domains") or [] if isinstance(domains, str): domains = [part.strip() for part in domains.split(",") if part.strip()] acme["domains"] = list(domains) acme["account"] = str(payload.get("account") or acme.get("account") or "default") acme["certificate"] = { "pem": str(payload.get("certificates") or payload.get("certificate") or "SIM-ACME"), "issued": int(time.time()), } certs["acme"] = acme ops["certificates"] = certs await save_node_ops(request, node, ops) if method == "DELETE": return None return await _node_task(request, node=node, task_type="acme", worker="acme") async def certificates_custom(request: Request, inputs: dict[str, Any]) -> None: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) certs = _certificates(ops) if request.method.upper() == "DELETE": certs["custom"] = None else: certificates = str(payload.get("certificates") or payload.get("cert") or "") if not certificates: raise ApiError(400, "parameter verification failed - 'certificates' missing") key = str(payload.get("key") or payload.get("private-key") or "") certs["custom"] = { "certificates": certificates, "key": key, "restart": int(payload.get("restart") or 0), "filename": str(payload.get("filename") or "pveproxy-ssl.pem"), } info = list(certs.get("info") or []) info = [item for item in info if item.get("filename") != certs["custom"]["filename"]] info.append( { "filename": certs["custom"]["filename"], "fingerprint": secrets.token_hex(20), "issuer": "CN=Simulator", "subject": "CN=pve.local", "notbefore": int(time.time()) - 86_400, "notafter": int(time.time()) + 365 * 86_400, "san": ["DNS:pve.local"], "public-key-type": "rsa", "public-key-bits": 2048, } ) certs["info"] = info ops["certificates"] = certs await save_node_ops(request, node, ops) async def certificates_info(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) certs = _certificates(ops) info = list(certs.get("info") or []) custom = _public_cert( certs.get("custom") if isinstance(certs.get("custom"), dict) else None ) if custom and not any(item.get("filename") == custom.get("filename") for item in info): info.append( { "filename": custom.get("filename", "pveproxy-ssl.pem"), "fingerprint": secrets.token_hex(20), "issuer": "CN=Custom", "subject": "CN=pve.local", } ) certs["info"] = info ops["certificates"] = certs await save_node_ops(request, node, ops) return [dict(item) for item in info] async def scan_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]: await require_node(request, str(values(inputs)["node"])) return subdirs("cifs", "iscsi", "lvm", "lvmthin", "nfs", "pbs", "zfs") async def scan_kind( request: Request, inputs: dict[str, Any], kind: str ) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) scan = _scan_cache(ops) ops["scan"] = scan await save_node_ops(request, node, ops) items = scan.get(kind, []) return [dict(item) for item in items] if isinstance(items, list) else [] async def capabilities_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]: await require_node(request, str(values(inputs)["node"])) return subdirs("qemu") async def capabilities_qemu(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]: await require_node(request, str(values(inputs)["node"])) return subdirs("cpu", "cpu-flags", "machines", "migration") async def capabilities_cpu(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: await require_node(request, str(values(inputs)["node"])) return [ {"name": "host", "vendor": "QEMU", "custom": 0}, {"name": "x86-64-v2-AES", "vendor": "QEMU", "custom": 0}, {"name": "kvm64", "vendor": "QEMU", "custom": 0}, ] async def capabilities_cpu_flags( request: Request, inputs: dict[str, Any] ) -> list[dict[str, Any]]: await require_node(request, str(values(inputs)["node"])) return [ {"name": "aes", "introduces": "Westmere"}, {"name": "avx", "introduces": "SandyBridge"}, {"name": "avx2", "introduces": "Haswell"}, ] async def capabilities_machines( request: Request, inputs: dict[str, Any] ) -> list[dict[str, Any]]: await require_node(request, str(values(inputs)["node"])) return [ {"id": "pc-i440fx-9.0", "type": "i440fx", "version": "9.0"}, {"id": "pc-q35-9.0", "type": "q35", "version": "9.0"}, ] async def capabilities_migration(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: await require_node(request, str(values(inputs)["node"])) return {"network": "", "type": "secure", "enabled": 1} async def hardware_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]: await require_node(request, str(values(inputs)["node"])) return subdirs("pci", "usb") async def hardware_pci(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) hardware = _hardware(ops) ops["hardware"] = hardware await save_node_ops(request, node, ops) return [dict(item) for item in hardware.get("pci", [])] async def hardware_pci_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: payload = values(inputs) node = str(payload["node"]) pci_id = str(payload.get("pci-id-or-mapping") or payload.get("pciid") or "") await require_node(request, node) ops = await load_node_ops(request, node) for item in _hardware(ops).get("pci", []): if str(item.get("id")) == pci_id: return dict(item) raise ApiError(404, "pci device does not exist") async def hardware_pci_mdev(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: payload = values(inputs) node = str(payload["node"]) pci_id = str(payload.get("pci-id-or-mapping") or payload.get("pciid") or "") await require_node(request, node) ops = await load_node_ops(request, node) hardware = _hardware(ops) mdev = hardware.get("mdev", {}) items = mdev.get(pci_id, []) if isinstance(mdev, dict) else [] return [dict(item) for item in items] if isinstance(items, list) else [] async def hardware_usb(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) hardware = _hardware(ops) ops["hardware"] = hardware await save_node_ops(request, node, ops) return [dict(item) for item in hardware.get("usb", [])] async def subscription_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) public = dict(_subscription(ops)) public.pop("key", None) return public async def subscription_mutate( request: Request, inputs: dict[str, Any] ) -> dict[str, Any] | None: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) current = _subscription(ops) method = request.method.upper() if method == "DELETE": ops["subscription"] = copy.deepcopy(DEFAULT_SUBSCRIPTION) await save_node_ops(request, node, ops) return None if method == "POST": current["checktime"] = int(time.time()) current["status"] = current.get("status") or "Active" ops["subscription"] = current await save_node_ops(request, node, ops) return dict(current) key = str(payload.get("key") or current.get("key") or "") updated = { **current, **{k: v for k, v in payload.items() if k not in {"node", "delete", "digest"}}, "key": key, "status": "Active" if key else current.get("status", "notfound"), "message": "OK" if key else current.get("message", "There is no subscription key"), } ops["subscription"] = updated await save_node_ops(request, node, ops) public = dict(updated) public.pop("key", None) return public async def aplinfo_get(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) items = ops.get("aplinfo") if not isinstance(items, list): items = [ { "package": "alpine-3-standard", "section": "system", "type": "lxc", "version": "3.20", } ] ops["aplinfo"] = items await save_node_ops(request, node, ops) return [dict(item) for item in items] async def aplinfo_download(request: Request, inputs: dict[str, Any]) -> str: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) downloads = list(ops.get("aplinfo_downloads") or []) downloads.append( { "template": str(payload.get("template") or payload.get("storage") or "unknown"), "at": int(time.time()), } ) ops["aplinfo_downloads"] = downloads await save_node_ops(request, node, ops) return await _node_task(request, node=node, task_type="download", worker="download") async def apt_repositories_mutate(request: Request, inputs: dict[str, Any]) -> None: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) apt = ops.setdefault("apt", copy.deepcopy(default_node_ops()["apt"])) repositories = list(apt.get("repositories") or []) method = request.method.upper() if method == "POST": entry = { key: value for key, value in payload.items() if key not in {"node", "delete", "digest"} } entry.setdefault("path", f"/etc/apt/sources.list.d/sim-{secrets.token_hex(2)}.list") entry.setdefault("enabled", 1) repositories.append(entry) else: path = payload.get("path") handle = payload.get("handle") index = payload.get("index") updated: list[dict[str, Any]] = [] for idx, item in enumerate(repositories): match = False if path is not None and item.get("path") == path: match = True if handle is not None and item.get("handle") == handle: match = True if index is not None and idx == int(index): match = True if match or (path is None and handle is None and index is None and idx == 0): merged = { **item, **{ key: value for key, value in payload.items() if key not in {"node", "delete", "digest", "path", "handle", "index"} }, } updated.append(merged) else: updated.append(item) repositories = updated apt["repositories"] = repositories ops["apt"] = apt await save_node_ops(request, node, ops) async def node_config_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) config = ops.get("config") if not isinstance(config, dict): config = copy.deepcopy(DEFAULT_CONFIG) ops["config"] = config await save_node_ops(request, node, ops) return dict(config) async def node_config_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) config = dict(ops.get("config") or DEFAULT_CONFIG) config.update( { key: value for key, value in payload.items() if key not in {"node", "digest", "delete"} } ) ops["config"] = config await save_node_ops(request, node, ops) return config async def dns_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) dns = ops.get("dns") if not isinstance(dns, dict): dns = copy.deepcopy(DEFAULT_DNS) ops["dns"] = dns await save_node_ops(request, node, ops) return dict(dns) async def dns_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) dns = dict(ops.get("dns") or DEFAULT_DNS) dns.update( { key: value for key, value in payload.items() if key not in {"node", "digest", "delete"} } ) ops["dns"] = dns await save_node_ops(request, node, ops) return dns async def time_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) current = dict(ops.get("time") or DEFAULT_TIME) now = int(time.time()) current["time"] = now current["localtime"] = now current.setdefault("timezone", "UTC") ops["time"] = current await save_node_ops(request, node, ops) return current async def time_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) current = dict(ops.get("time") or DEFAULT_TIME) if "timezone" in payload: current["timezone"] = str(payload["timezone"]) now = int(time.time()) current["time"] = now current["localtime"] = now ops["time"] = current await save_node_ops(request, node, ops) return current async def execute(request: Request, inputs: dict[str, Any]) -> list[str]: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) commands = payload.get("commands") or payload.get("command") or [] if isinstance(commands, str): try: parsed = json.loads(commands) commands = parsed if isinstance(parsed, list) else [commands] except json.JSONDecodeError: commands = [commands] ops = await load_node_ops(request, node) log = list(ops.get("execute_log") or []) output: list[str] = [] for command in commands: entry = {"command": str(command), "at": int(time.time())} log.append(entry) output.append(f"OK: {command}") ops["execute_log"] = log[-100:] await save_node_ops(request, node, ops) return output async def hosts_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) hosts = ops.get("hosts") if not isinstance(hosts, dict): hosts = { "data": f"127.0.0.1 localhost\n10.0.0.10 {node}\n", "digest": secrets.token_hex(8), } ops["hosts"] = hosts await save_node_ops(request, node, ops) return {"data": str(hosts.get("data", "")), "digest": str(hosts.get("digest", ""))} async def hosts_post(request: Request, inputs: dict[str, Any]) -> None: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) ops = await load_node_ops(request, node) ops["hosts"] = { "data": str(payload.get("data") or ""), "digest": secrets.token_hex(8), } await save_node_ops(request, node, ops) async def journal(request: Request, inputs: dict[str, Any]) -> list[str]: node = str(values(inputs)["node"]) await require_node(request, node) start = int(values(inputs).get("startcursor") or values(inputs).get("start") or 0) limit = int(values(inputs).get("limit") or 50) lines = [ f"{index}: {node} systemd[1]: Started simulated service {index}." for index in range(start, start + limit) ] return lines async def syslog(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) limit = int(values(inputs).get("limit") or 50) return [ {"n": index, "t": f"{node} kernel: simulated syslog line {index}"} for index in range(limit) ] async def netstat(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) return [ {"in": 1_000_000, "out": 900_000, "vnet": "vmbr0", "hwaddr": "bc:24:11:00:00:01"}, {"in": 500_000, "out": 450_000, "vnet": "vmbr1", "hwaddr": "bc:24:11:00:00:02"}, ] async def report(request: Request, inputs: dict[str, Any]) -> str: node = str(values(inputs)["node"]) await require_node(request, node) return f"==== Proxmox node report for {node} ====\nuptime: simulated\n" async def rrd(_request: Request, inputs: dict[str, Any]) -> dict[str, Any]: await require_node(_request, str(values(inputs)["node"])) return {"filename": "/var/lib/rrdcached/db/pve-node.rrd"} async def rrddata(_request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: await require_node(_request, str(values(inputs)["node"])) now = int(time.time()) return [ {"time": now - 120, "cpu": 0.05, "memused": 1_000_000_000}, {"time": now - 60, "cpu": 0.07, "memused": 1_100_000_000}, {"time": now, "cpu": 0.04, "memused": 1_050_000_000}, ] async def startall(request: Request, inputs: dict[str, Any]) -> str: node = str(values(inputs)["node"]) await require_node(request, node) await _set_guest_status(request, node, "running") return await _node_task(request, node=node, task_type="startall", worker="startall") async def stopall(request: Request, inputs: dict[str, Any]) -> str: node = str(values(inputs)["node"]) await require_node(request, node) await _set_guest_status(request, node, "stopped") return await _node_task(request, node=node, task_type="stopall", worker="stopall") async def suspendall(request: Request, inputs: dict[str, Any]) -> str: node = str(values(inputs)["node"]) await require_node(request, node) await _set_guest_status(request, node, "paused") return await _node_task(request, node=node, task_type="suspendall", worker="suspendall") async def migrateall(request: Request, inputs: dict[str, Any]) -> str: payload = values(inputs) node = str(payload["node"]) target = str(payload.get("target") or "") await require_node(request, node) if not target: raise ApiError(400, "parameter verification failed - 'target' missing") await _migrate_guests(request, node, target) return await _node_task(request, node=node, task_type="migrateall", worker="migrateall") async def status_post(request: Request, inputs: dict[str, Any]) -> str | None: payload = values(inputs) node = str(payload["node"]) await require_node(request, node) command = str(payload.get("command") or "reboot") ops = await load_node_ops(request, node) ops["last_status_command"] = {"command": command, "at": int(time.time())} await save_node_ops(request, node, ops) return await _node_task(request, node=node, task_type=command, worker=command) async def wakeonlan(request: Request, inputs: dict[str, Any]) -> str: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) ops["wakeonlan"] = {"at": int(time.time())} await save_node_ops(request, node, ops) return "OK" async def _shell_proxy(request: Request, inputs: dict[str, Any], kind: str) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) payload = { "port": 5900 if kind == "vnc" else 22 if kind == "term" else 3128, "ticket": secrets.token_urlsafe(24), "user": str(getattr(request.state, "principal", "root@pam")), "upid": f"UPID:{node}:{secrets.token_hex(4)}:{kind}shell:0:root@pam:", } ops = await load_node_ops(request, node) shells = ops.setdefault("shells", {}) shells[kind] = {key: value for key, value in payload.items() if key != "ticket"} ops["shells"] = shells await save_node_ops(request, node, ops) return payload async def spiceshell(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: return await _shell_proxy(request, inputs, "spice") async def termproxy(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: return await _shell_proxy(request, inputs, "term") async def vncshell(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: return await _shell_proxy(request, inputs, "vnc") async def network_reload(request: Request, inputs: dict[str, Any]) -> None: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) if not isinstance(ops.get("network"), list): ops["network"] = copy.deepcopy(default_node_ops()["network"]) ops["network_applied"] = False await save_node_ops(request, node, ops) async def query_oci_repo_tags(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]: node = str(values(inputs)["node"]) await require_node(request, node) repo = str(values(inputs).get("repo") or "library/alpine") ops = await load_node_ops(request, node) cache = ops.setdefault("oci_tags", {}) if repo not in cache: cache[repo] = [{"tag": "latest"}, {"tag": "3.20"}] ops["oci_tags"] = cache await save_node_ops(request, node, ops) return [dict(item) for item in cache[repo]] async def query_url_metadata(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) url = str(values(inputs).get("url") or "") ops = await load_node_ops(request, node) cache = ops.setdefault("url_metadata", {}) if url not in cache: cache[url] = { "filename": url.rsplit("/", 1)[-1] or "download.bin", "mimetype": "application/octet-stream", "size": 1024, } ops["url_metadata"] = cache await save_node_ops(request, node, ops) return dict(cache[url]) async def vncwebsocket(request: Request, inputs: dict[str, Any]) -> dict[str, Any]: node = str(values(inputs)["node"]) await require_node(request, node) ops = await load_node_ops(request, node) shell = (ops.get("shells") or {}).get("vnc") or {"port": 5900} return { "port": shell.get("port", 5900), "ticket": secrets.token_urlsafe(24), } # Disks mutations (GET collections already registered in nodes.py) registry.register( "/nodes/{node}/disks/directory", "POST", lambda request, inputs: disks_create(request, inputs, "directory"), ) registry.register( "/nodes/{node}/disks/directory/{name}", "DELETE", lambda request, inputs: disks_delete(request, inputs, "directory"), ) registry.register( "/nodes/{node}/disks/lvm", "POST", lambda request, inputs: disks_create(request, inputs, "lvm"), ) registry.register( "/nodes/{node}/disks/lvm/{name}", "DELETE", lambda request, inputs: disks_delete(request, inputs, "lvm"), ) registry.register( "/nodes/{node}/disks/lvmthin", "POST", lambda request, inputs: disks_create(request, inputs, "lvmthin"), ) registry.register( "/nodes/{node}/disks/lvmthin/{name}", "DELETE", lambda request, inputs: disks_delete(request, inputs, "lvmthin"), ) registry.register( "/nodes/{node}/disks/zfs", "POST", lambda request, inputs: disks_create(request, inputs, "zfs"), ) registry.register("/nodes/{node}/disks/zfs/{name}", "GET", disks_zfs_get) registry.register( "/nodes/{node}/disks/zfs/{name}", "DELETE", lambda request, inputs: disks_delete(request, inputs, "zfs"), ) registry.register("/nodes/{node}/certificates", "GET", certificates_index) registry.register("/nodes/{node}/certificates/acme", "GET", certificates_acme_index) registry.register( "/nodes/{node}/certificates/acme/certificate", "POST", certificates_acme_mutate ) registry.register( "/nodes/{node}/certificates/acme/certificate", "PUT", certificates_acme_mutate ) registry.register( "/nodes/{node}/certificates/acme/certificate", "DELETE", certificates_acme_mutate ) registry.register("/nodes/{node}/certificates/custom", "POST", certificates_custom) registry.register("/nodes/{node}/certificates/custom", "DELETE", certificates_custom) registry.register("/nodes/{node}/certificates/info", "GET", certificates_info) registry.register("/nodes/{node}/scan", "GET", scan_index) registry.register("/nodes/{node}/scan/cifs", "GET", lambda r, i: scan_kind(r, i, "cifs")) registry.register("/nodes/{node}/scan/iscsi", "GET", lambda r, i: scan_kind(r, i, "iscsi")) registry.register("/nodes/{node}/scan/lvm", "GET", lambda r, i: scan_kind(r, i, "lvm")) registry.register("/nodes/{node}/scan/lvmthin", "GET", lambda r, i: scan_kind(r, i, "lvmthin")) registry.register("/nodes/{node}/scan/nfs", "GET", lambda r, i: scan_kind(r, i, "nfs")) registry.register("/nodes/{node}/scan/pbs", "GET", lambda r, i: scan_kind(r, i, "pbs")) registry.register("/nodes/{node}/scan/zfs", "GET", lambda r, i: scan_kind(r, i, "zfs")) registry.register("/nodes/{node}/capabilities", "GET", capabilities_index) registry.register("/nodes/{node}/capabilities/qemu", "GET", capabilities_qemu) registry.register("/nodes/{node}/capabilities/qemu/cpu", "GET", capabilities_cpu) registry.register("/nodes/{node}/capabilities/qemu/cpu-flags", "GET", capabilities_cpu_flags) registry.register("/nodes/{node}/capabilities/qemu/machines", "GET", capabilities_machines) registry.register("/nodes/{node}/capabilities/qemu/migration", "GET", capabilities_migration) registry.register("/nodes/{node}/hardware", "GET", hardware_index) registry.register("/nodes/{node}/hardware/pci", "GET", hardware_pci) registry.register("/nodes/{node}/hardware/pci/{pci-id-or-mapping}", "GET", hardware_pci_get) registry.register( "/nodes/{node}/hardware/pci/{pci-id-or-mapping}/mdev", "GET", hardware_pci_mdev ) registry.register("/nodes/{node}/hardware/usb", "GET", hardware_usb) registry.register("/nodes/{node}/subscription", "GET", subscription_get) registry.register("/nodes/{node}/subscription", "PUT", subscription_mutate) registry.register("/nodes/{node}/subscription", "POST", subscription_mutate) registry.register("/nodes/{node}/subscription", "DELETE", subscription_mutate) registry.register("/nodes/{node}/aplinfo", "GET", aplinfo_get) registry.register("/nodes/{node}/aplinfo", "POST", aplinfo_download) registry.register("/nodes/{node}/apt/repositories", "POST", apt_repositories_mutate) registry.register("/nodes/{node}/apt/repositories", "PUT", apt_repositories_mutate) registry.register("/nodes/{node}/config", "GET", node_config_get) registry.register("/nodes/{node}/config", "PUT", node_config_put) registry.register("/nodes/{node}/dns", "GET", dns_get) registry.register("/nodes/{node}/dns", "PUT", dns_put) registry.register("/nodes/{node}/time", "GET", time_get) registry.register("/nodes/{node}/time", "PUT", time_put) registry.register("/nodes/{node}/execute", "POST", execute) registry.register("/nodes/{node}/hosts", "GET", hosts_get) registry.register("/nodes/{node}/hosts", "POST", hosts_post) registry.register("/nodes/{node}/journal", "GET", journal) registry.register("/nodes/{node}/syslog", "GET", syslog) registry.register("/nodes/{node}/netstat", "GET", netstat) registry.register("/nodes/{node}/report", "GET", report) registry.register("/nodes/{node}/rrd", "GET", rrd) registry.register("/nodes/{node}/rrddata", "GET", rrddata) registry.register("/nodes/{node}/migrateall", "POST", migrateall) registry.register("/nodes/{node}/startall", "POST", startall) registry.register("/nodes/{node}/stopall", "POST", stopall) registry.register("/nodes/{node}/suspendall", "POST", suspendall) registry.register("/nodes/{node}/status", "POST", status_post) registry.register("/nodes/{node}/wakeonlan", "POST", wakeonlan) registry.register("/nodes/{node}/spiceshell", "POST", spiceshell) registry.register("/nodes/{node}/termproxy", "POST", termproxy) registry.register("/nodes/{node}/vncshell", "POST", vncshell) registry.register("/nodes/{node}/network", "DELETE", network_reload) registry.register("/nodes/{node}/query-oci-repo-tags", "GET", query_oci_repo_tags) registry.register("/nodes/{node}/query-url-metadata", "GET", query_url_metadata) registry.register("/nodes/{node}/vncwebsocket", "GET", vncwebsocket)