Align sized cluster seeds and GET dumps with PVE wire shapes; restyle DATA panel.
- Scale small/large/big seeds (3×50 / 10×1000 / 20×2000) with proportional backups, snapshots, HA, replication, Ceph capacity, and OSD totals (10 / 100 / 500) plus matching node disks and crush/pg metadata - Enrich handler responses for apt, certificates, qemu/lxc status, storage, SDN, metrics export, and related cluster/node dumps - Flatten nested body_example fields into PARAMS and sync the request body via dotted paths (oVirt-style) - Restyle DATA controls as size cards with full-width Reset to minimal / Refresh stats; unload reloads the minimal cluster
This commit is contained in:
+155
-24
@@ -19,7 +19,7 @@ from app.handlers.ceph import register_ceph_handlers
|
||||
from app.handlers.cluster import register_cluster_handlers
|
||||
from app.handlers.cluster_config import register_cluster_config_handlers
|
||||
from app.handlers.cluster_extra import register_cluster_extra_handlers
|
||||
from app.handlers.common import require_node, subdirs
|
||||
from app.handlers.common import cluster_metadata, require_node, subdirs
|
||||
from app.handlers.firewall import register_firewall_handlers
|
||||
from app.handlers.ha import register_ha_handlers
|
||||
from app.handlers.legacy_aliases import register_legacy_aliases
|
||||
@@ -60,10 +60,14 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
raise ApiError(401, "authentication failure")
|
||||
key = settings.ticket_signing_key.get_secret_value().encode()
|
||||
ticket = issue_ticket(username, key)
|
||||
cluster_name = await _database(request).pool.fetchval(
|
||||
"SELECT name FROM clusters ORDER BY created_at LIMIT 1"
|
||||
)
|
||||
return {
|
||||
"username": username,
|
||||
"ticket": ticket,
|
||||
"CSRFPreventionToken": csrf_token(ticket, key),
|
||||
"clustername": str(cluster_name or "pve-simulator"),
|
||||
"cap": {"vms": {"VM.Audit": 1, "VM.PowerMgmt": 1}},
|
||||
}
|
||||
|
||||
@@ -81,7 +85,9 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
status_dict = dict(status_payload) if isinstance(status_payload, dict) else {}
|
||||
fingerprint = status_dict.get("ssl_fingerprint") or status_dict.get("fingerprint")
|
||||
if fingerprint in (None, "", 0) or isinstance(fingerprint, dict | list):
|
||||
fingerprint = ":".join(["00"] * 32)
|
||||
from app.simulation.seed import _seed_fingerprint
|
||||
|
||||
fingerprint = _seed_fingerprint(name)
|
||||
|
||||
def _as_float(value: object, default: float) -> float:
|
||||
if isinstance(value, bool) or value is None or isinstance(value, dict | list):
|
||||
@@ -126,10 +132,62 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
ops = await load_node_ops(request, node)
|
||||
status = ops.get("status")
|
||||
payload = dict(status) if isinstance(status, dict) else {}
|
||||
mem_used = int(payload.get("mem") or 0)
|
||||
mem_total = int(payload.get("maxmem") or 8 * 1024**3)
|
||||
memory = payload.get("memory")
|
||||
if isinstance(memory, dict):
|
||||
mem_used = int(memory.get("used") or mem_used)
|
||||
mem_total = int(memory.get("total") or mem_total)
|
||||
rootfs = payload.get("rootfs")
|
||||
if not isinstance(rootfs, dict):
|
||||
rootfs = {
|
||||
"used": int(payload.get("rootfs_used") or mem_used // 8),
|
||||
"total": int(payload.get("rootfs_total") or 100 * 1024**3),
|
||||
"avail": int(payload.get("rootfs_avail") or 80 * 1024**3),
|
||||
"free": int(payload.get("rootfs_free") or 80 * 1024**3),
|
||||
}
|
||||
cpuinfo = payload.get("cpuinfo")
|
||||
if not isinstance(cpuinfo, dict):
|
||||
cpuinfo = {
|
||||
"cpus": int(payload.get("maxcpu") or 4),
|
||||
"cores": int(payload.get("maxcpu") or 4),
|
||||
"sockets": 1,
|
||||
"model": str(payload.get("cpu_model") or "QEMU Virtual CPU"),
|
||||
"flags": "",
|
||||
}
|
||||
boot_info = payload.get("boot-info")
|
||||
if not isinstance(boot_info, dict):
|
||||
boot_info = {"mode": "efi", "secureboot": 0}
|
||||
current_kernel = payload.get("current-kernel")
|
||||
if not isinstance(current_kernel, dict):
|
||||
kversion = str(payload.get("kversion") or "6.8.12-1-pve")
|
||||
current_kernel = {
|
||||
"sysname": "Linux",
|
||||
"release": kversion,
|
||||
"version": f"#1 SMP PREEMPT_DYNAMIC {kversion}",
|
||||
"machine": "x86_64",
|
||||
}
|
||||
return {
|
||||
"status": str(row["status"]),
|
||||
"node": str(row["name"]),
|
||||
**payload,
|
||||
"uptime": int(payload.get("uptime") or 0),
|
||||
"wait": float(payload.get("wait") or 0.0),
|
||||
"idle": float(payload.get("idle") or 0.95),
|
||||
"cpu": float(payload.get("cpu") or 0.0),
|
||||
"loadavg": list(payload.get("loadavg") or ["0.00", "0.00", "0.00"]),
|
||||
"memory": {
|
||||
"used": mem_used,
|
||||
"total": mem_total,
|
||||
"free": max(mem_total - mem_used, 0),
|
||||
"available": max(mem_total - mem_used, 0),
|
||||
},
|
||||
"rootfs": rootfs,
|
||||
"swap": payload.get("swap")
|
||||
if isinstance(payload.get("swap"), dict)
|
||||
else {"used": 0, "total": 0, "free": 0},
|
||||
"cpuinfo": cpuinfo,
|
||||
"kversion": str(payload.get("kversion") or current_kernel.get("release") or ""),
|
||||
"pveversion": str(payload.get("pveversion") or "pve-manager/9.2.3"),
|
||||
"current-kernel": current_kernel,
|
||||
"boot-info": boot_info,
|
||||
}
|
||||
|
||||
async def resources(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
@@ -197,6 +255,15 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
ops = await load_node_ops(request, name)
|
||||
status_payload = ops.get("status")
|
||||
status_dict = dict(status_payload) if isinstance(status_payload, dict) else {}
|
||||
mem_raw = status_dict.get("mem", status_dict.get("memory"))
|
||||
if isinstance(mem_raw, dict):
|
||||
mem_val = int(_num(mem_raw.get("used"), 0))
|
||||
maxmem_val = int(
|
||||
_num(status_dict.get("maxmem", mem_raw.get("total")), 8 * 1024**3)
|
||||
)
|
||||
else:
|
||||
mem_val = int(_num(mem_raw, 0))
|
||||
maxmem_val = int(_num(status_dict.get("maxmem"), 8 * 1024**3))
|
||||
result.append(
|
||||
{
|
||||
"type": "node",
|
||||
@@ -205,10 +272,8 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
"status": str(row["status"]),
|
||||
"cpu": float(_num(status_dict.get("cpu"), 0.0)),
|
||||
"maxcpu": int(_num(status_dict.get("maxcpu"), 4)),
|
||||
"mem": int(
|
||||
_num(status_dict.get("mem"), _num(status_dict.get("memory"), 0))
|
||||
),
|
||||
"maxmem": int(_num(status_dict.get("maxmem"), 8 * 1024**3)),
|
||||
"mem": mem_val,
|
||||
"maxmem": maxmem_val,
|
||||
"uptime": int(_num(status_dict.get("uptime"), 0)),
|
||||
"level": str(status_dict.get("level") or ""),
|
||||
}
|
||||
@@ -220,12 +285,40 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
elif type_filter == "storage":
|
||||
kind_filter = ("storage",)
|
||||
elif type_filter in (None,):
|
||||
kind_filter = ("qemu", "lxc", "storage")
|
||||
kind_filter = ("qemu", "lxc", "storage", "pool")
|
||||
elif type_filter in {"qemu", "lxc", "storage", "pool", "sdn"}:
|
||||
kind_filter = (str(type_filter),)
|
||||
else:
|
||||
kind_filter = ()
|
||||
|
||||
ha_by_guest: dict[str, str] = {}
|
||||
if kind_filter and ("qemu" in kind_filter or "lxc" in kind_filter):
|
||||
ha_rows = await _database(request).pool.fetch(
|
||||
"SELECT external_id, state FROM resources WHERE kind='ha'"
|
||||
)
|
||||
for ha_row in ha_rows:
|
||||
sid = str(ha_row["external_id"])
|
||||
ha_state = _as_dict(ha_row["state"])
|
||||
ha_by_guest[sid] = str(ha_state.get("state") or "started")
|
||||
|
||||
pool_by_vmid: dict[str, str] = {}
|
||||
if kind_filter and ("qemu" in kind_filter or "lxc" in kind_filter or "pool" in kind_filter):
|
||||
membership = await _database(request).pool.fetch(
|
||||
"""SELECT p.pool_id AS pool, r.external_id AS vmid
|
||||
FROM pool_members pm
|
||||
JOIN pools p ON p.id=pm.pool_id
|
||||
JOIN resources r ON r.id=pm.resource_id"""
|
||||
)
|
||||
for row in membership:
|
||||
pool_by_vmid[str(row["vmid"])] = str(row["pool"])
|
||||
|
||||
node_names = [
|
||||
str(item["node"])
|
||||
for item in await _database(request).pool.fetch(
|
||||
"SELECT name AS node FROM nodes ORDER BY name"
|
||||
)
|
||||
]
|
||||
|
||||
if kind_filter:
|
||||
rows = await _database(request).pool.fetch(
|
||||
"""SELECT r.kind AS type, r.external_id, r.state, n.name AS node
|
||||
@@ -243,6 +336,7 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
status = str(state.get("status") or "stopped")
|
||||
running = status in {"running", "paused"}
|
||||
vmid = int(external_id)
|
||||
sid = f"{'vm' if kind == 'qemu' else 'ct'}:{external_id}"
|
||||
item: dict[str, Any] = {
|
||||
"type": kind,
|
||||
"id": f"{kind}/{external_id}",
|
||||
@@ -250,7 +344,7 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
"vmid": vmid,
|
||||
"name": str(state.get("name") or f"{kind}-{external_id}"),
|
||||
"status": status,
|
||||
"template": 1 if state.get("template") in {True, "1"} else 0,
|
||||
"template": bool(state.get("template") in {True, "1", 1}),
|
||||
"cpu": _cpu_util(state, running=running),
|
||||
"maxcpu": _maxcpu(state),
|
||||
"mem": int(_num(state.get("mem"), 0)) if running else 0,
|
||||
@@ -259,27 +353,64 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
|
||||
"maxdisk": int(_num(state.get("maxdisk"), 0)),
|
||||
"uptime": int(_num(state.get("uptime"), 0)) if running else 0,
|
||||
}
|
||||
if sid in ha_by_guest:
|
||||
item["hastate"] = ha_by_guest[sid]
|
||||
if external_id in pool_by_vmid:
|
||||
item["pool"] = pool_by_vmid[external_id]
|
||||
result.append(item)
|
||||
elif kind == "storage":
|
||||
content = state.get("content")
|
||||
if isinstance(content, list):
|
||||
content_text = ",".join(str(item) for item in content)
|
||||
content_text = ",".join(str(part) for part in content)
|
||||
else:
|
||||
content_text = str(content or "")
|
||||
shared = bool(_num(state.get("shared"), 0))
|
||||
# Schema allows one storages row per storage_id; expand to every
|
||||
# node like a real PVE dump (local + shared storages).
|
||||
targets = node_names if (shared or len(node_names) > 1) else [node]
|
||||
if not shared and external_id not in {"local", "local-lvm", "ceph"}:
|
||||
targets = [node]
|
||||
for target in targets:
|
||||
result.append(
|
||||
{
|
||||
"type": "storage",
|
||||
"id": f"storage/{target}/{external_id}",
|
||||
"node": target,
|
||||
"storage": external_id,
|
||||
"status": str(state.get("status") or "available"),
|
||||
"content": content_text,
|
||||
"disk": int(_num(state.get("disk"), 0)),
|
||||
"maxdisk": int(_num(state.get("maxdisk"), 1 * 1024**3)),
|
||||
"shared": shared,
|
||||
"plugintype": str(
|
||||
state.get("plugintype") or state.get("type") or "dir"
|
||||
),
|
||||
}
|
||||
)
|
||||
elif kind == "pool":
|
||||
result.append(
|
||||
{
|
||||
"type": "storage",
|
||||
"id": f"storage/{node}/{external_id}",
|
||||
"node": node,
|
||||
"storage": external_id,
|
||||
"status": str(state.get("status") or "available"),
|
||||
"content": content_text,
|
||||
"disk": int(_num(state.get("disk"), 0)),
|
||||
"maxdisk": int(_num(state.get("maxdisk"), 1 * 1024**3)),
|
||||
"shared": int(_num(state.get("shared"), 0)),
|
||||
"plugintype": str(
|
||||
state.get("plugintype") or state.get("type") or "dir"
|
||||
),
|
||||
"type": "pool",
|
||||
"id": f"pool/{external_id}",
|
||||
"pool": external_id,
|
||||
"comment": str(state.get("comment") or ""),
|
||||
}
|
||||
)
|
||||
|
||||
if type_filter in (None, "sdn"):
|
||||
metadata = await cluster_metadata(request)
|
||||
sdn = metadata.get("sdn")
|
||||
zones = sdn.get("zones") if isinstance(sdn, dict) else None
|
||||
if isinstance(zones, dict):
|
||||
for zone_name, zone in sorted(zones.items()):
|
||||
if not isinstance(zone, dict):
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"type": "sdn",
|
||||
"id": f"sdn/{zone_name}",
|
||||
"sdn": zone_name,
|
||||
"status": str(zone.get("status") or "ok"),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user