Prepare 0.1.0 for lab release: durable handlers, HTTP Compose, CI, and pulumi-tests.
- Harden DB-backed handlers and seed profiles; align client wire shapes for cluster resources, QEMU config, and node SSL fields - Serve plain HTTP on Compose :8006; keep TLS optional (--profile tls) and terminate HTTPS at Kubernetes Ingress - Add pulumi-tests (full contract surface majors 6–9 + BPG lifecycle) and make pulumi-tests - Ship bilingual docs, CHANGELOG, SECURITY, CONTRIBUTING, and GitHub Actions (make ci + Compose/Helm validation)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:XH46KobfYos=:v1:tQDcaIs9vVMnsfrW:b9Gi6Lg3VJtY1sIwqiebZuWZnUs2JQ==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:A8xu+B2cPSM=:v1:pyNe+LdXmI8S/syj:y55+cE0uCXlcqMIU9QH3lcy2VdM0/g==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:UzHEGVVb7Hg=:v1:wFaQ/0fPsClXx/FH:If0c+b7X1v2JlAsjgqw3VbiEixE4WQ==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:T7d8B72/cgw=:v1:JQTIJDuIttxYRG3P:dkgjqexvURaSUfheXRFBnnhk4gShAg==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:os3oQmc2a3w=:v1:ucPclEUo9WaE7yPN:HC3Vd0B6beGLxZVvOI9J9GO5BHdwCQ==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:cp9iAk4kDHE=:v1:F/B2BBJxO0k5ZUA5:DTlM5vdnm0ZCzhBdO0PeNwVdutV5yg==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:2+s2B58ijdg=:v1:1Kp9yO99F2P3ge1T:kGUur8A2sLlkUZ4oHzqi8FtnQaAGAg==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:iFaG0Vq94ro=:v1:tk3lV99BGpFbDimz:n1hcd1LrIDd5UOfPp3Zr3VBZjWd59Q==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:erx+K9e1Euc=:v1:AdiBWzU1hqyX6uoG:BybNBCL5DmFOBYT2bMZXpYcRPqgvow==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:72zO/F62ADI=:v1:1eWLQhjIh/pym+XQ:acoyK9LVJRwiQti6/ISvHEEY5b949Q==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:aJXNBbnMxrc=:v1:b3PW3ve+9nVeIQzL:8aERP/Q0sfJdxfzCpp036++baWcIPw==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:FncbMOoIrxc=:v1:SQhsQbyz402+sdJr:eLVKSgilS1OXn3h/akvFMUl9Hn/SQQ==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
encryptionsalt: v1:i+BOW7Km3mY=:v1:+fV2zYTynoK3+SSe:bv6NNjPsbqdf+xpIPB8FjuglWAfEjQ==
|
||||
config:
|
||||
hx-lifecycle:smoke: "0"
|
||||
@@ -0,0 +1,3 @@
|
||||
name: hx-lifecycle
|
||||
runtime: python
|
||||
description: Compact Proxmox simulator lifecycle scenarios
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Lifecycle suite driven primarily by pulumi-proxmoxve (BPG bridge).
|
||||
|
||||
Surface probing of every contract method stays in ``run_suite.py`` (HTTP).
|
||||
This program covers provider-backed inventory + VM lifecycle with non-empty
|
||||
output checks. Negative auth still uses httpx against the plain HTTP API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pulumi
|
||||
import pulumi_proxmoxve as proxmox
|
||||
from pvelib.api import Pve
|
||||
|
||||
cfg = pulumi.Config()
|
||||
smoke = (cfg.get("smoke") or os.environ.get("SMOKE_ONLY") or "0") == "1"
|
||||
|
||||
node = os.environ.get("PVE_NODE", "pve01")
|
||||
endpoint = (
|
||||
os.environ.get("PROXMOX_VE_ENDPOINT") or "https://tls-gateway:8443/"
|
||||
).rstrip("/") + "/"
|
||||
username = os.environ.get("PROXMOX_VE_USERNAME") or os.environ.get("API_USER", "root@pam")
|
||||
password = os.environ.get("PROXMOX_VE_PASSWORD") or os.environ.get("API_PASSWORD", "secret")
|
||||
insecure = (os.environ.get("PROXMOX_VE_INSECURE") or "true").lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
}
|
||||
|
||||
|
||||
def _vmid(tag: str) -> int:
|
||||
digest = hashlib.sha1(f"hx-pve-{tag}-{os.getpid()}".encode()).hexdigest()
|
||||
return 710000 + (int(digest[:6], 16) % 90000)
|
||||
|
||||
|
||||
def _require(value: Any, label: str) -> Any:
|
||||
if value is None:
|
||||
raise AssertionError(f"{label}: value is None")
|
||||
if isinstance(value, (str, bytes)) and not str(value).strip():
|
||||
raise AssertionError(f"{label}: empty string")
|
||||
if isinstance(value, (list, tuple, set, dict)) and len(value) == 0:
|
||||
raise AssertionError(f"{label}: empty {type(value).__name__}")
|
||||
return value
|
||||
|
||||
|
||||
def _datastore_ids(datastores: Any) -> list[str]:
|
||||
items = getattr(datastores, "datastores", None) or datastores or []
|
||||
ids: list[str] = []
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
value = item.get("id") or item.get("datastore_id") or item.get("storage")
|
||||
else:
|
||||
value = (
|
||||
getattr(item, "id", None)
|
||||
or getattr(item, "datastore_id", None)
|
||||
or getattr(item, "storage", None)
|
||||
)
|
||||
if value not in (None, ""):
|
||||
ids.append(str(value))
|
||||
return ids
|
||||
|
||||
|
||||
provider = proxmox.Provider(
|
||||
"proxmoxve",
|
||||
endpoint=endpoint,
|
||||
username=username,
|
||||
password=password,
|
||||
insecure=insecure,
|
||||
)
|
||||
prov_opts = pulumi.ResourceOptions(provider=provider)
|
||||
invoke_opts = pulumi.InvokeOptions(provider=provider)
|
||||
|
||||
# --- Provider data sources (inventory) ---
|
||||
version = proxmox.get_version_legacy(opts=invoke_opts)
|
||||
identity = version.version or version.release or version.repository_id
|
||||
_require(identity, "get_version_legacy identity")
|
||||
|
||||
nodes = proxmox.get_nodes_legacy(opts=invoke_opts)
|
||||
node_names = list(_require(nodes.names, "get_nodes_legacy.names"))
|
||||
if node not in node_names:
|
||||
raise AssertionError(f"expected node {node!r} in {node_names!r}")
|
||||
|
||||
stores = proxmox.get_datastores_legacy(node_name=node, opts=invoke_opts)
|
||||
store_ids = _require(_datastore_ids(stores), "get_datastores_legacy ids")
|
||||
|
||||
# --- Negative auth via HTTP ---
|
||||
_bad = Pve(authenticate=False)
|
||||
try:
|
||||
try:
|
||||
_bad.login(password="definitely-wrong")
|
||||
raise AssertionError("expected login failure for bad password")
|
||||
except AssertionError:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_bad.close()
|
||||
|
||||
# --- VM via pulumi-proxmoxve (mirrors examples/terraform cookbook shape) ---
|
||||
vm_id = _vmid("qemu")
|
||||
vm_name = f"hxpu{vm_id}"
|
||||
|
||||
vm = proxmox.VmLegacy(
|
||||
"hx-vm",
|
||||
name=vm_name,
|
||||
node_name=node,
|
||||
vm_id=vm_id,
|
||||
started=False,
|
||||
on_boot=False,
|
||||
agent={"enabled": False},
|
||||
cpu={"cores": 1 if smoke else 2},
|
||||
memory={"dedicated": 512 if smoke else 1024},
|
||||
opts=prov_opts,
|
||||
)
|
||||
|
||||
|
||||
def _check_vm(args: list[Any]) -> dict[str, Any]:
|
||||
name, vmid, node_name = args
|
||||
_require(name, "VmLegacy.name")
|
||||
_require(vmid, "VmLegacy.vm_id")
|
||||
_require(node_name, "VmLegacy.node_name")
|
||||
if str(name) != vm_name:
|
||||
raise AssertionError(f"name mismatch {name!r} != {vm_name!r}")
|
||||
if int(vmid) != vm_id:
|
||||
raise AssertionError(f"vmid mismatch {vmid!r} != {vm_id!r}")
|
||||
if str(node_name) != node:
|
||||
raise AssertionError(f"node mismatch {node_name!r} != {node!r}")
|
||||
return {"name": str(name), "vm_id": int(vmid), "node": str(node_name)}
|
||||
|
||||
|
||||
vm_checked = pulumi.Output.all(vm.name, vm.vm_id, vm.node_name).apply(_check_vm)
|
||||
|
||||
scenario_ids = [
|
||||
"provider_version",
|
||||
"provider_nodes",
|
||||
"provider_datastores",
|
||||
"auth_bad_password",
|
||||
"vm_lifecycle_provider",
|
||||
]
|
||||
|
||||
pulumi.export(
|
||||
"inventory",
|
||||
{
|
||||
"version": str(identity),
|
||||
"nodes": node_names,
|
||||
"datastores": store_ids,
|
||||
},
|
||||
)
|
||||
pulumi.export("vm", vm_checked)
|
||||
pulumi.export("scenario_ids", scenario_ids)
|
||||
Reference in New Issue
Block a user