From 9d21e2da95a5364c1ca7e3dec3de7b4985ed89ab Mon Sep 17 00:00:00 2001 From: centra
Date: Sat, 28 Mar 2026 03:14:11 +0900
Subject: [PATCH] Add PrismNet-backed PlasmaVMC matrix coverage
---
docs/component-matrix.md | 11 +-
docs/testing.md | 2 +-
nix/test-cluster/README.md | 3 +-
nix/test-cluster/run-cluster.sh | 205 +++++++++++++++++-
.../plasmavmc-server/src/prismnet_client.rs | 63 +++++-
.../crates/plasmavmc-server/src/vm_service.rs | 20 +-
6 files changed, 281 insertions(+), 23 deletions(-)
diff --git a/docs/component-matrix.md b/docs/component-matrix.md
index 7c2338a..955585d 100644
--- a/docs/component-matrix.md
+++ b/docs/component-matrix.md
@@ -1,7 +1,7 @@
# Component Matrix
PhotonCloud is intended to validate meaningful service combinations, not only a single all-on deployment.
-This page separates the compositions that are already exercised by the VM-cluster harness from the next combinations that still need dedicated automation.
+This page summarizes the compositions that are exercised by the VM-cluster harness today.
## Validated Control Plane
@@ -18,9 +18,11 @@ These combinations justify the existence of the network services as composable p
## Validated VM Hosting Layer
+- `plasmavmc + prismnet`
- `plasmavmc + lightningstor`
- `plasmavmc + coronafs`
- `plasmavmc + coronafs + lightningstor`
+- `plasmavmc + prismnet + coronafs + lightningstor`
This split keeps mutable VM volumes on CoronaFS and immutable VM images on LightningStor object storage.
@@ -40,11 +42,6 @@ This split keeps mutable VM volumes on CoronaFS and immutable VM images on Light
- `creditservice + iam`
- `deployer + iam + chainfire`
-## Next Compositions To Automate
-
-- `plasmavmc + prismnet`
-- `plasmavmc + prismnet + coronafs + lightningstor`
-
## Validation Direction
The VM cluster harness now exposes:
@@ -54,4 +51,4 @@ nix run ./nix/test-cluster#cluster -- matrix
nix run ./nix/test-cluster#cluster -- fresh-matrix
```
-`fresh-matrix` is the publishable path because it rebuilds the host-side VM images before validating the composed service scenarios.
+`fresh-matrix` is the publishable path because it rebuilds the host-side VM images before validating the composed service scenarios, including PrismNet-backed PlasmaVMC guests.
diff --git a/docs/testing.md b/docs/testing.md
index 21cdf42..38d76e6 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -25,7 +25,7 @@ nix run ./nix/test-cluster#cluster -- fresh-bench-storage
Use these three commands as the release-facing local proof set:
- `fresh-smoke`: whole-cluster readiness, core behavior, and fault injection
-- `fresh-matrix`: composed service scenarios such as `prismnet + flashdns + fiberlb` and VM hosting bundles
+- `fresh-matrix`: composed service scenarios such as `prismnet + flashdns + fiberlb` and PrismNet-backed VM hosting bundles with `plasmavmc + coronafs + lightningstor`
- `fresh-bench-storage`: CoronaFS local-vs-shared-volume throughput, cross-worker volume visibility, and LightningStor large/small-object throughput capture
## Operational Commands
diff --git a/nix/test-cluster/README.md b/nix/test-cluster/README.md
index c210b3f..ff30e33 100644
--- a/nix/test-cluster/README.md
+++ b/nix/test-cluster/README.md
@@ -9,6 +9,7 @@ All VM images are built on the host in a single Nix invocation and then booted a
- 3-node control-plane formation for `chainfire`, `flaredb`, and `iam`
- control-plane service health for `prismnet`, `flashdns`, `fiberlb`, `plasmavmc`, `lightningstor`, and `k8shost`
- worker-node `plasmavmc` and `lightningstor` startup
+- PrismNet port binding for PlasmaVMC guests, including lifecycle cleanup on VM deletion
- nested KVM inside worker VMs by booting an inner guest with `qemu-system-x86_64 -accel kvm`
- gateway-node `apigateway`, `nightlight`, and minimal `creditservice` startup
- host-forwarded access to the API gateway and NightLight HTTP surfaces
@@ -59,7 +60,7 @@ Preferred entrypoint for publishable verification: `nix run ./nix/test-cluster#c
`make cluster-smoke` is a convenience wrapper for the same clean host-build VM validation flow.
-`nix run ./nix/test-cluster#cluster -- matrix` reuses the current running cluster to exercise composed service scenarios such as `prismnet + flashdns + fiberlb`, VM hosting with `plasmavmc + coronafs + lightningstor`, the Kubernetes-style hosting bundle, and API-gateway-mediated `nightlight` / `creditservice` flows.
+`nix run ./nix/test-cluster#cluster -- matrix` reuses the current running cluster to exercise composed service scenarios such as `prismnet + flashdns + fiberlb`, PrismNet-backed VM hosting with `plasmavmc + prismnet + coronafs + lightningstor`, the Kubernetes-style hosting bundle, and API-gateway-mediated `nightlight` / `creditservice` flows.
Preferred entrypoint for publishable matrix verification: `nix run ./nix/test-cluster#cluster -- fresh-matrix`
diff --git a/nix/test-cluster/run-cluster.sh b/nix/test-cluster/run-cluster.sh
index ee390ff..dd598f0 100755
--- a/nix/test-cluster/run-cluster.sh
+++ b/nix/test-cluster/run-cluster.sh
@@ -450,6 +450,112 @@ create_prismnet_vpc_with_retry() {
done
}
+prismnet_get_port_json() {
+ local token="$1"
+ local org_id="$2"
+ local project_id="$3"
+ local subnet_id="$4"
+ local port_id="$5"
+
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg subnet "${subnet_id}" --arg id "${port_id}" '{orgId:$org, projectId:$project, subnetId:$subnet, id:$id}')" \
+ 127.0.0.1:15081 prismnet.PortService/GetPort
+}
+
+wait_for_prismnet_port_binding() {
+ local token="$1"
+ local org_id="$2"
+ local project_id="$3"
+ local subnet_id="$4"
+ local port_id="$5"
+ local vm_id="$6"
+ local timeout="${7:-${HTTP_WAIT_TIMEOUT}}"
+ local deadline=$((SECONDS + timeout))
+ local port_json=""
+
+ while true; do
+ if port_json="$(prismnet_get_port_json "${token}" "${org_id}" "${project_id}" "${subnet_id}" "${port_id}" 2>/dev/null || true)"; then
+ if [[ -n "${port_json}" ]] && printf '%s' "${port_json}" | jq -e --arg vm "${vm_id}" '
+ .port.deviceId == $vm and .port.deviceType == "DEVICE_TYPE_VM"
+ ' >/dev/null 2>&1; then
+ printf '%s\n' "${port_json}"
+ return 0
+ fi
+ fi
+ if (( SECONDS >= deadline )); then
+ die "timed out waiting for PrismNet port ${port_id} to bind to VM ${vm_id}"
+ fi
+ sleep 2
+ done
+}
+
+wait_for_prismnet_port_detachment() {
+ local token="$1"
+ local org_id="$2"
+ local project_id="$3"
+ local subnet_id="$4"
+ local port_id="$5"
+ local timeout="${6:-${HTTP_WAIT_TIMEOUT}}"
+ local deadline=$((SECONDS + timeout))
+ local port_json=""
+
+ while true; do
+ if port_json="$(prismnet_get_port_json "${token}" "${org_id}" "${project_id}" "${subnet_id}" "${port_id}" 2>/dev/null || true)"; then
+ if [[ -n "${port_json}" ]] && printf '%s' "${port_json}" | jq -e '
+ (.port.deviceId // "") == "" and
+ ((.port.deviceType // "") == "DEVICE_TYPE_NONE" or (.port.deviceType // "") == "DEVICE_TYPE_UNSPECIFIED")
+ ' >/dev/null 2>&1; then
+ printf '%s\n' "${port_json}"
+ return 0
+ fi
+ fi
+ if (( SECONDS >= deadline )); then
+ die "timed out waiting for PrismNet port ${port_id} to detach"
+ fi
+ sleep 2
+ done
+}
+
+wait_for_vm_network_spec() {
+ local token="$1"
+ local get_vm_json="$2"
+ local port_id="$3"
+ local subnet_id="$4"
+ local mac_address="$5"
+ local ip_address="$6"
+ local vm_port="${7:-15082}"
+ local timeout="${8:-${HTTP_WAIT_TIMEOUT}}"
+ local deadline=$((SECONDS + timeout))
+ local vm_json=""
+
+ while true; do
+ if vm_json="$(try_get_vm_json "${token}" "${get_vm_json}" "${vm_port}" 2>/dev/null || true)"; then
+ if [[ -n "${vm_json}" ]] && printf '%s' "${vm_json}" | jq -e \
+ --arg port "${port_id}" \
+ --arg subnet "${subnet_id}" \
+ --arg mac "${mac_address}" \
+ --arg ip "${ip_address}" '
+ (.spec.network // []) | any(
+ .portId == $port and
+ .subnetId == $subnet and
+ .macAddress == $mac and
+ .ipAddress == $ip
+ )
+ ' >/dev/null 2>&1; then
+ printf '%s\n' "${vm_json}"
+ return 0
+ fi
+ fi
+ if (( SECONDS >= deadline )); then
+ die "timed out waiting for VM network spec to reflect PrismNet port ${port_id}"
+ fi
+ sleep 2
+ done
+}
+
build_link() {
printf '%s/build-%s' "$(vm_dir)" "$1"
}
@@ -3582,11 +3688,12 @@ validate_lightningstor_distributed_storage() {
validate_vm_storage_flow() {
log "Validating PlasmaVMC image import, shared-volume execution, and cross-node migration"
- local iam_tunnel="" ls_tunnel="" vm_tunnel="" coronafs_tunnel=""
+ local iam_tunnel="" prism_tunnel="" ls_tunnel="" vm_tunnel="" coronafs_tunnel=""
local node04_coronafs_tunnel="" node05_coronafs_tunnel=""
local current_worker_coronafs_port="" peer_worker_coronafs_port=""
local vm_port=15082
iam_tunnel="$(start_ssh_tunnel node01 15080 50080)"
+ prism_tunnel="$(start_ssh_tunnel node01 15081 50081)"
ls_tunnel="$(start_ssh_tunnel node01 15086 50086)"
vm_tunnel="$(start_ssh_tunnel node01 "${vm_port}" 50082)"
coronafs_tunnel="$(start_ssh_tunnel node01 15088 "${CORONAFS_API_PORT}")"
@@ -3594,7 +3701,32 @@ validate_vm_storage_flow() {
node05_coronafs_tunnel="$(start_ssh_tunnel node05 35088 "${CORONAFS_API_PORT}")"
local image_source_path=""
local node01_proto_root="/var/lib/plasmavmc/test-protos"
+ local vpc_id="" subnet_id="" port_id="" port_ip="" port_mac=""
cleanup_vm_storage_flow() {
+ if [[ -n "${token:-}" && -n "${port_id:-}" && -n "${subnet_id:-}" ]]; then
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg subnet "${subnet_id}" --arg id "${port_id}" '{orgId:$org, projectId:$project, subnetId:$subnet, id:$id}')" \
+ 127.0.0.1:15081 prismnet.PortService/DeletePort >/dev/null 2>&1 || true
+ fi
+ if [[ -n "${token:-}" && -n "${subnet_id:-}" && -n "${vpc_id:-}" ]]; then
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg vpc "${vpc_id}" --arg id "${subnet_id}" '{orgId:$org, projectId:$project, vpcId:$vpc, id:$id}')" \
+ 127.0.0.1:15081 prismnet.SubnetService/DeleteSubnet >/dev/null 2>&1 || true
+ fi
+ if [[ -n "${token:-}" && -n "${vpc_id:-}" ]]; then
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id:-}" --arg project "${project_id:-}" --arg id "${vpc_id}" '{orgId:$org, projectId:$project, id:$id}')" \
+ 127.0.0.1:15081 prismnet.VpcService/DeleteVpc >/dev/null 2>&1 || true
+ fi
if [[ -n "${image_source_path}" && "${image_source_path}" != /nix/store/* ]]; then
ssh_node node01 "rm -f ${image_source_path}" >/dev/null 2>&1 || true
fi
@@ -3603,6 +3735,7 @@ validate_vm_storage_flow() {
stop_ssh_tunnel node01 "${coronafs_tunnel}"
stop_ssh_tunnel node01 "${vm_tunnel}"
stop_ssh_tunnel node01 "${ls_tunnel}"
+ stop_ssh_tunnel node01 "${prism_tunnel}"
stop_ssh_tunnel node01 "${iam_tunnel}"
}
trap cleanup_vm_storage_flow RETURN
@@ -3615,6 +3748,38 @@ validate_vm_storage_flow() {
local token
token="$(issue_project_admin_token 15080 "${org_id}" "${project_id}" "${principal_id}")"
+ log "Matrix case: PlasmaVMC + PrismNet"
+ vpc_id="$(create_prismnet_vpc_with_retry \
+ "${token}" \
+ "${org_id}" \
+ "${project_id}" \
+ "vm-network-vpc" \
+ "vm storage matrix networking" \
+ "10.62.0.0/16" | jq -r '.vpc.id')"
+ [[ -n "${vpc_id}" && "${vpc_id}" != "null" ]] || die "failed to create PrismNet VPC for PlasmaVMC matrix"
+
+ subnet_id="$(grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg vpc "${vpc_id}" '{vpcId:$vpc, name:"vm-network-subnet", description:"vm storage matrix subnet", cidrBlock:"10.62.10.0/24", gatewayIp:"10.62.10.1", dhcpEnabled:true}')" \
+ 127.0.0.1:15081 prismnet.SubnetService/CreateSubnet | jq -r '.subnet.id')"
+ [[ -n "${subnet_id}" && "${subnet_id}" != "null" ]] || die "failed to create PrismNet subnet for PlasmaVMC matrix"
+
+ local prismnet_port_response
+ prismnet_port_response="$(grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg subnet "${subnet_id}" '{orgId:$org, projectId:$project, subnetId:$subnet, name:"vm-network-port", description:"vm storage matrix port", ipAddress:""}')" \
+ 127.0.0.1:15081 prismnet.PortService/CreatePort)"
+ port_id="$(printf '%s' "${prismnet_port_response}" | jq -r '.port.id')"
+ port_ip="$(printf '%s' "${prismnet_port_response}" | jq -r '.port.ipAddress')"
+ port_mac="$(printf '%s' "${prismnet_port_response}" | jq -r '.port.macAddress')"
+ [[ -n "${port_id}" && "${port_id}" != "null" ]] || die "failed to create PrismNet port for PlasmaVMC matrix"
+ [[ -n "${port_ip}" && "${port_ip}" != "null" ]] || die "PrismNet port ${port_id} did not return an IP address"
+ [[ -n "${port_mac}" && "${port_mac}" != "null" ]] || die "PrismNet port ${port_id} did not return a MAC address"
+
ensure_lightningstor_bucket 15086 "${token}" "plasmavmc-images" "${org_id}" "${project_id}"
wait_for_lightningstor_write_quorum 15086 "${token}" "plasmavmc-images" "PlasmaVMC image import"
@@ -3764,6 +3929,8 @@ EOS
--arg org "${org_id}" \
--arg project "${project_id}" \
--arg image_id "${image_id}" \
+ --arg subnet_id "${subnet_id}" \
+ --arg port_id "${port_id}" \
'{
name:$name,
orgId:$org,
@@ -3788,6 +3955,14 @@ EOS
bus:"DISK_BUS_VIRTIO",
cache:"DISK_CACHE_WRITEBACK"
}
+ ],
+ network:[
+ {
+ id:"tenant0",
+ subnetId:$subnet_id,
+ portId:$port_id,
+ model:"NIC_MODEL_VIRTIO_NET"
+ }
]
}
}'
@@ -3845,6 +4020,8 @@ EOS
current_worker_coronafs_port=35088
peer_worker_coronafs_port=25088
fi
+ wait_for_vm_network_spec "${token}" "${get_vm_json}" "${port_id}" "${subnet_id}" "${port_mac}" "${port_ip}" "${vm_port}" >/dev/null
+ wait_for_prismnet_port_binding "${token}" "${org_id}" "${project_id}" "${subnet_id}" "${port_id}" "${vm_id}" >/dev/null
grpcurl -plaintext \
-H "authorization: Bearer ${token}" \
@@ -3872,7 +4049,7 @@ EOS
sleep 2
done
- log "Matrix case: PlasmaVMC + CoronaFS"
+ log "Matrix case: PlasmaVMC + PrismNet + CoronaFS + LightningStor"
local volume_id="${vm_id}-root"
local data_volume_id="${vm_id}-data"
local volume_path="${CORONAFS_VOLUME_ROOT}/${volume_id}.raw"
@@ -4108,6 +4285,7 @@ EOS
(( $(printf '%s' "${data_volume_state_json}" | jq -r '.lastFlushedAttachmentGeneration // 0') < next_data_attachment_generation )) || die "data volume ${data_volume_id} unexpectedly reported destination flush before post-migration stop"
root_attachment_generation="${next_root_attachment_generation}"
data_attachment_generation="${next_data_attachment_generation}"
+ wait_for_prismnet_port_binding "${token}" "${org_id}" "${project_id}" "${subnet_id}" "${port_id}" "${vm_id}" >/dev/null
grpcurl -plaintext \
-H "authorization: Bearer ${token}" \
@@ -4235,6 +4413,7 @@ EOS
fi
sleep 2
done
+ wait_for_prismnet_port_detachment "${token}" "${org_id}" "${project_id}" "${subnet_id}" "${port_id}" >/dev/null
ssh_node "${node_id}" "bash -lc '[[ ! -d $(printf '%q' "$(vm_runtime_dir_path "${vm_id}")") ]]'"
ssh_node node01 "bash -lc '[[ ! -f ${volume_path} ]]'"
@@ -4283,6 +4462,28 @@ EOS
die "shared-fs VM data volume unexpectedly persisted to LightningStor object storage"
fi
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg subnet "${subnet_id}" --arg id "${port_id}" '{orgId:$org, projectId:$project, subnetId:$subnet, id:$id}')" \
+ 127.0.0.1:15081 prismnet.PortService/DeletePort >/dev/null
+ port_id=""
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg vpc "${vpc_id}" --arg id "${subnet_id}" '{orgId:$org, projectId:$project, vpcId:$vpc, id:$id}')" \
+ 127.0.0.1:15081 prismnet.SubnetService/DeleteSubnet >/dev/null
+ subnet_id=""
+ grpcurl -plaintext \
+ -H "authorization: Bearer ${token}" \
+ -import-path "${PRISMNET_PROTO_DIR}" \
+ -proto "${PRISMNET_PROTO}" \
+ -d "$(jq -cn --arg org "${org_id}" --arg project "${project_id}" --arg id "${vpc_id}" '{orgId:$org, projectId:$project, id:$id}')" \
+ 127.0.0.1:15081 prismnet.VpcService/DeleteVpc >/dev/null
+ vpc_id=""
+
grpcurl -plaintext \
-H "authorization: Bearer ${token}" \
-import-path "${PLASMAVMC_PROTO_DIR}" \
diff --git a/plasmavmc/crates/plasmavmc-server/src/prismnet_client.rs b/plasmavmc/crates/plasmavmc-server/src/prismnet_client.rs
index 1e393b6..e8d62dd 100644
--- a/plasmavmc/crates/plasmavmc-server/src/prismnet_client.rs
+++ b/plasmavmc/crates/plasmavmc-server/src/prismnet_client.rs
@@ -4,21 +4,41 @@ use prismnet_api::proto::{
port_service_client::PortServiceClient, GetPortRequest, AttachDeviceRequest,
DetachDeviceRequest,
};
+use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
/// PrismNET client wrapper
pub struct PrismNETClient {
+ auth_token: String,
port_client: PortServiceClient