Fix CI/pulumi blockers and document test suites with latest PASS results.

Stabilize node status, cluster-wide Ceph OSD ids, and QEMU cpu utilization so
status/list no longer 500 on model strings; align pulumi defaults to pve1;
add bilingual testing docs with the 2026-07-18 ci-all + pulumi-tests pass.
This commit is contained in:
Sergey Antropoff
2026-07-18 09:38:28 +03:00
parent 6f42d4c318
commit baa1f58ad0
31 changed files with 230 additions and 58 deletions
+3 -3
View File
@@ -205,7 +205,7 @@ def _format_key_example(key: str, raw: Mapping[str, Any]) -> object:
if "default" in raw and raw["default"] is not None:
return raw["default"]
enum = raw.get("enum")
if isinstance(enum, (list, tuple)) and enum:
if isinstance(enum, list | tuple) and enum:
return enum[0]
nested_format = raw.get("format")
if isinstance(nested_format, Mapping):
@@ -226,10 +226,10 @@ def _format_key_example(key: str, raw: Mapping[str, Any]) -> object:
return 1
if typ == "integer":
minimum = raw.get("minimum")
return int(minimum) if isinstance(minimum, (int, float)) else 1
return int(minimum) if isinstance(minimum, int | float) else 1
if typ == "number":
minimum = raw.get("minimum")
return float(minimum) if isinstance(minimum, (int, float)) else 1.0
return float(minimum) if isinstance(minimum, int | float) else 1.0
pattern = raw.get("pattern")
if isinstance(pattern, str) and "second|minute|hour|day" in pattern:
return "1/second"
+1 -1
View File
@@ -131,7 +131,7 @@ def register_acme_handlers(registry: HandlerRegistry) -> None:
entry = {"plugin": plugin_id, **{k: v for k, v in item.items() if k != "data"}}
if "digest" not in entry:
material = {k: v for k, v in entry.items() if k != "digest"}
entry["digest"] = hashlib.sha1(
entry["digest"] = hashlib.sha1( # noqa: S324 - Proxmox config digests use SHA-1
json.dumps(material, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
result.append(entry)
+12 -6
View File
@@ -245,7 +245,7 @@ def register_ceph_handlers(registry: HandlerRegistry) -> None:
def _wire_ceph_pool(name: str, item: dict[str, Any], *, index: int) -> dict[str, Any]:
pool_id = item.get("pool_id", item.get("pool"))
try:
pool_num = int(pool_id)
pool_num = int(pool_id) if pool_id is not None else index
except (TypeError, ValueError):
pool_num = index
crush_raw = item.get("crush_rule", 0)
@@ -586,9 +586,8 @@ def register_ceph_handlers(registry: HandlerRegistry) -> None:
MAX(NULLIF(regexp_replace(external_id, '\\D', '', 'g'), '')::int),
-1
) + 1
FROM resources r JOIN nodes n ON n.id=r.node_id
WHERE n.name=$1 AND r.kind='ceph-osd'""",
node,
FROM resources
WHERE kind='ceph-osd'""",
)
osd_id = int(next_id or 0)
external_id = f"osd.{osd_id}"
@@ -607,8 +606,15 @@ def register_ceph_handlers(registry: HandlerRegistry) -> None:
"used_bytes": 0,
}
await database(request).pool.execute(
"""INSERT INTO resources(id, node_id, kind, external_id, state)
VALUES(gen_random_uuid(), $1, 'ceph-osd', $2, $3::jsonb)""",
"""INSERT INTO resources(id, node_id, cluster_id, kind, external_id, state)
VALUES(
gen_random_uuid(),
$1,
(SELECT cluster_id FROM nodes WHERE id=$1),
'ceph-osd',
$2,
$3::jsonb
)""",
node_id,
external_id,
json.dumps(osd_state, sort_keys=True),
+2 -2
View File
@@ -193,8 +193,8 @@ def register_cluster_extra_handlers(registry: HandlerRegistry) -> None:
now = int(time.time())
seeded = metrics.get("export_series")
if isinstance(seeded, list) and seeded:
series = [dict(item) for item in seeded if isinstance(item, dict)]
return {"data": series, "timestamp": now}
seeded_series = [dict(item) for item in seeded if isinstance(item, dict)]
return {"data": seeded_series, "timestamp": now}
# Build a PVE-shaped metric array from live inventory (scales with seed).
rows = await database(request).pool.fetch("SELECT name FROM nodes ORDER BY name")
series: list[dict[str, Any]] = []
+2 -1
View File
@@ -168,6 +168,7 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
"machine": "x86_64",
}
return {
"status": str(row["status"]),
"uptime": int(payload.get("uptime") or 0),
"wait": float(payload.get("wait") or 0.0),
"idle": float(payload.get("idle") or 0.95),
@@ -344,7 +345,7 @@ def build_core_handlers(settings: Settings) -> HandlerRegistry:
"vmid": vmid,
"name": str(state.get("name") or f"{kind}-{external_id}"),
"status": status,
"template": bool(state.get("template") in {True, "1", 1}),
"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,
+1 -1
View File
@@ -30,7 +30,7 @@ def _ha_groups(metadata: dict[str, Any]) -> dict[str, dict[str, Any]]:
def _ha_digest(payload: dict[str, Any]) -> str:
material = {key: value for key, value in payload.items() if key != "digest"}
return hashlib.sha1(
return hashlib.sha1( # noqa: S324 - Proxmox HA digests use SHA-1
json.dumps(material, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
+1 -1
View File
@@ -70,7 +70,7 @@ def _public_lxc_config(
continue
payload[key] = value
digest_src = {key: value for key, value in payload.items() if key != "digest"}
payload["digest"] = hashlib.sha1(
payload["digest"] = hashlib.sha1( # noqa: S324 - Proxmox config digests use SHA-1
json.dumps(digest_src, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
return payload
+4 -4
View File
@@ -554,10 +554,10 @@ def register_nodes_extra_handlers(registry: HandlerRegistry) -> None:
}
)
else:
path = payload.get("path")
target_path = payload.get("path")
index = payload.get("index")
for file_idx, file_entry in enumerate(files):
if path is not None and file_entry.get("path") != path:
if target_path is not None and file_entry.get("path") != target_path:
continue
repos = [
dict(item)
@@ -584,7 +584,7 @@ def register_nodes_extra_handlers(registry: HandlerRegistry) -> None:
}
},
}
elif path is not None or file_idx == 0:
elif target_path is not None or file_idx == 0:
if repos:
repos[0] = {
**repos[0],
@@ -604,7 +604,7 @@ def register_nodes_extra_handlers(registry: HandlerRegistry) -> None:
},
}
file_entry["repositories"] = repos
if path is not None:
if target_path is not None:
break
apt["repositories"] = {
"digest": secrets.token_hex(4),
+16 -2
View File
@@ -78,12 +78,26 @@ def _public_qemu_config(config: dict[str, Any], state: dict[str, Any]) -> dict[s
agent = config.get("agent", state.get("agent", "0"))
payload["agent"] = _agent_config_string(agent)
digest_src = {key: value for key, value in payload.items() if key != "digest"}
payload["digest"] = hashlib.sha1(
payload["digest"] = hashlib.sha1( # noqa: S324 - Proxmox config digests use SHA-1
json.dumps(digest_src, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
return payload
def _cpu_utilization(vm_state: Mapping[str, Any], *, running: bool, idle: float = 0.1) -> float:
"""Status ``cpu`` is utilization; config may store model strings like ``qemu64``."""
raw = vm_state.get("cpu")
if isinstance(raw, (int, float)):
return float(raw)
if isinstance(raw, str):
try:
return float(raw)
except ValueError:
pass
return idle if running else 0.0
def _qemu_index_item(vmid: int, vm_state: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
status = str(vm_state.get("status", "stopped"))
running = status in {"running", "paused"}
@@ -96,7 +110,7 @@ def _qemu_index_item(vmid: int, vm_state: dict[str, Any], config: dict[str, Any]
"status": status,
"qmpstatus": status if running else "stopped",
"cpus": int(config.get("cores", config.get("cpus", 1))),
"cpu": float(vm_state.get("cpu", 0.1 if running else 0.0)),
"cpu": _cpu_utilization(vm_state, running=running),
"maxmem": maxmem,
"mem": mem,
"memhost": int(vm_state.get("memhost", mem)),
+9 -9
View File
@@ -1682,7 +1682,7 @@ def _seed_tasks_for_guests(
return tuple(tasks)
# UI / ops cluster sizes (hosts × total guests). Infra scales with the size.
# UI / ops cluster sizes (hosts x total guests). Infra scales with the size.
_CLUSTER_SIZE_SPECS: dict[str, dict[str, int]] = {
"lab": {
"nodes": 1,
@@ -1953,13 +1953,13 @@ def _sized_cluster_profile(name: str, *, overrides: dict[str, int] | None = None
def small_profile() -> SeedProfile:
"""DATA UI: 3 hosts × 50 guests."""
"""DATA UI: 3 hosts x 50 guests."""
return _sized_cluster_profile("small")
def large_profile(*, node_count: int = 10, resource_count: int = 1_000) -> SeedProfile:
"""DATA UI: 10 hosts × 1000 guests (overrides allowed for CLI stress)."""
"""DATA UI: 10 hosts x 1000 guests (overrides allowed for CLI stress)."""
if node_count < 1 or resource_count < 1:
raise ValueError("large profile counts must be positive")
@@ -1971,22 +1971,22 @@ def large_profile(*, node_count: int = 10, resource_count: int = 1_000) -> SeedP
overrides={
"nodes": node_count,
"guests": resource_count,
"osds": max(10, int(round(100 * scale))),
"ha": max(3, int(round(12 * scale))),
"tasks": max(12, int(round(50 * scale))),
"pool_members": max(12, min(resource_count, int(round(50 * scale)))),
"osds": max(10, round(100 * scale)),
"ha": max(3, round(12 * scale)),
"tasks": max(12, round(50 * scale)),
"pool_members": max(12, min(resource_count, round(50 * scale))),
},
)
def big_profile() -> SeedProfile:
"""DATA UI: 20 hosts × 2000 guests."""
"""DATA UI: 20 hosts x 2000 guests."""
return _sized_cluster_profile("big")
def medium_profile() -> SeedProfile:
"""Compatibility alias for the small (3×50) cluster."""
"""Compatibility alias for the small (3x50) cluster."""
profile = small_profile()
return SeedProfile("medium", profile.nodes, profile.resources, profile.tasks)