photoncloud-monorepo/plasmavmc/crates/plasmavmc-server/src/prismnet_client.rs
centra 4ab47b1726
Implement declarative tenant networking and local VM dataplane
Add tenant-scoped PrismNET routing, security-group, port, and service-IP APIs plus a deployer reconciler and Nix module that apply declarative tenant network state.

Teach PlasmaVMC to realize PrismNET NICs as a concrete local worker dataplane with Linux bridges, dnsmasq-backed DHCP, tap devices, richer network metadata, stable managed-volume IDs, and file:// image imports.

Expand the VM cluster validation around the new path, including the guest webapp demo, restart and cross-node migration checks, IAM listener reservation hardening, and a flake workspace-source-root audit so Nix builds keep path dependencies complete.
2026-04-04 00:07:43 +09:00

216 lines
6.7 KiB
Rust

//! PrismNET client for port management
use prismnet_api::proto::{
port_service_client::PortServiceClient, subnet_service_client::SubnetServiceClient,
AttachDeviceRequest, CreatePortRequest, DeletePortRequest, DetachDeviceRequest, GetPortRequest,
GetSubnetRequest,
};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
/// PrismNET client wrapper
pub struct PrismNETClient {
auth_token: String,
port_client: PortServiceClient<Channel>,
subnet_client: SubnetServiceClient<Channel>,
}
impl PrismNETClient {
/// Create a new PrismNET client
pub async fn new(
endpoint: String,
auth_token: String,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let channel = Channel::from_shared(endpoint)?.connect().await?;
let port_client = PortServiceClient::new(channel.clone());
let subnet_client = SubnetServiceClient::new(channel);
Ok(Self {
auth_token,
port_client,
subnet_client,
})
}
fn request_with_auth<T>(
auth_token: &str,
payload: T,
) -> Result<tonic::Request<T>, Box<dyn std::error::Error + Send + Sync>> {
let mut request = tonic::Request::new(payload);
let token_value = MetadataValue::try_from(auth_token)?;
request
.metadata_mut()
.insert("x-photon-auth-token", token_value);
Ok(request)
}
/// Get port details
pub async fn get_port(
&mut self,
org_id: &str,
project_id: &str,
subnet_id: &str,
port_id: &str,
) -> Result<prismnet_api::proto::Port, Box<dyn std::error::Error + Send + Sync>> {
let request = Self::request_with_auth(
&self.auth_token,
GetPortRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
subnet_id: subnet_id.to_string(),
id: port_id.to_string(),
},
)?;
let response = self.port_client.get_port(request).await?;
Ok(response
.into_inner()
.port
.ok_or("Port not found in response")?)
}
/// Get subnet details, resolving by subnet ID when VPC ID is not known locally.
pub async fn get_subnet(
&mut self,
org_id: &str,
project_id: &str,
subnet_id: &str,
) -> Result<prismnet_api::proto::Subnet, Box<dyn std::error::Error + Send + Sync>> {
let request = Self::request_with_auth(
&self.auth_token,
GetSubnetRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
vpc_id: String::new(),
id: subnet_id.to_string(),
},
)?;
let response = self.subnet_client.get_subnet(request).await?;
Ok(response
.into_inner()
.subnet
.ok_or("Subnet not found in response")?)
}
/// Create a port for a VM-managed NIC
pub async fn create_port(
&mut self,
org_id: &str,
project_id: &str,
subnet_id: &str,
name: &str,
description: Option<&str>,
ip_address: Option<&str>,
security_group_ids: Vec<String>,
) -> Result<prismnet_api::proto::Port, Box<dyn std::error::Error + Send + Sync>> {
let request = Self::request_with_auth(
&self.auth_token,
CreatePortRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
subnet_id: subnet_id.to_string(),
name: name.to_string(),
description: description.unwrap_or_default().to_string(),
ip_address: ip_address.unwrap_or_default().to_string(),
security_group_ids,
},
)?;
let response = self.port_client.create_port(request).await?;
Ok(response
.into_inner()
.port
.ok_or("Port not found in response")?)
}
/// Attach a device to a port
pub async fn attach_device(
&mut self,
org_id: &str,
project_id: &str,
subnet_id: &str,
port_id: &str,
device_id: &str,
device_type: i32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let request = Self::request_with_auth(
&self.auth_token,
AttachDeviceRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
subnet_id: subnet_id.to_string(),
port_id: port_id.to_string(),
device_id: device_id.to_string(),
device_type,
},
)?;
self.port_client.attach_device(request).await?;
Ok(())
}
/// Detach a device from a port
pub async fn detach_device(
&mut self,
org_id: &str,
project_id: &str,
subnet_id: &str,
port_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let request = Self::request_with_auth(
&self.auth_token,
DetachDeviceRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
subnet_id: subnet_id.to_string(),
port_id: port_id.to_string(),
},
)?;
self.port_client.detach_device(request).await?;
Ok(())
}
/// Delete a port
pub async fn delete_port(
&mut self,
org_id: &str,
project_id: &str,
subnet_id: &str,
port_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let request = Self::request_with_auth(
&self.auth_token,
DeletePortRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
subnet_id: subnet_id.to_string(),
id: port_id.to_string(),
},
)?;
self.port_client.delete_port(request).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_with_auth_adds_internal_token_header() {
let request = PrismNETClient::request_with_auth(
"test-token",
GetPortRequest {
org_id: "org".to_string(),
project_id: "project".to_string(),
subnet_id: "subnet".to_string(),
id: "port".to_string(),
},
)
.expect("request metadata should be constructible");
assert_eq!(
request
.metadata()
.get("x-photon-auth-token")
.and_then(|value| value.to_str().ok()),
Some("test-token")
);
}
}