//! PrismNET CNI Plugin for k8shost //! //! This binary implements the CNI 1.0.0 specification to integrate k8shost pods //! with PrismNET's OVN-based virtual networking. //! //! CNI operations: //! - ADD: Create network interface and attach to OVN logical switch //! - DEL: Remove network interface and clean up OVN resources //! - CHECK: Verify network configuration is correct //! - VERSION: Report supported CNI versions use anyhow::{Context, Result}; use prismnet_api::{ port_service_client::PortServiceClient, CreatePortRequest, DeletePortRequest, ListPortsRequest, }; use serde::{Deserialize, Serialize}; use std::io::{self, Read}; #[derive(Debug, Serialize, Deserialize)] struct CniConfig { #[serde(rename = "cniVersion")] cni_version: String, name: String, #[serde(rename = "type")] plugin_type: String, #[serde(default)] prismnet: PrismNETConfig, } #[derive(Debug, Default, Serialize, Deserialize)] struct PrismNETConfig { server_addr: String, subnet_id: String, org_id: String, project_id: String, } #[derive(Debug, Serialize, Deserialize)] struct CniResult { #[serde(rename = "cniVersion")] cni_version: String, interfaces: Vec, ips: Vec, routes: Vec, dns: DnsConfig, } #[derive(Debug, Serialize, Deserialize)] struct Interface { name: String, mac: String, sandbox: String, } #[derive(Debug, Serialize, Deserialize)] struct IpConfig { interface: usize, address: String, gateway: String, } #[derive(Debug, Serialize, Deserialize)] struct Route { dst: String, gw: String, } #[derive(Debug, Default, Serialize, Deserialize)] struct DnsConfig { nameservers: Vec, } #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() .with_writer(std::io::stderr) .init(); let command = std::env::var("CNI_COMMAND").context("CNI_COMMAND not set")?; match command.as_str() { "ADD" => handle_add().await, "DEL" => handle_del().await, "CHECK" => handle_check(), "VERSION" => handle_version(), _ => Err(anyhow::anyhow!("Unknown CNI command: {}", command)), } } async fn handle_add() -> Result<()> { // Read CNI config from stdin let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer)?; let config: CniConfig = serde_json::from_str(&buffer)?; // Parse CNI environment variables let container_id = std::env::var("CNI_CONTAINERID").context("CNI_CONTAINERID not set")?; let netns = std::env::var("CNI_NETNS").context("CNI_NETNS not set")?; let ifname = std::env::var("CNI_IFNAME").context("CNI_IFNAME not set")?; tracing::info!( container_id = %container_id, netns = %netns, ifname = %ifname, "CNI ADD operation starting" ); // Connect to PrismNET server let prismnet_addr = if config.prismnet.server_addr.is_empty() { std::env::var("NOVANET_SERVER_ADDR").unwrap_or_else(|_| "http://127.0.0.1:50052".to_string()) } else { config.prismnet.server_addr.clone() }; let mut port_client = PortServiceClient::connect(prismnet_addr.clone()) .await .context("Failed to connect to PrismNET server")?; // Extract tenant context from config or environment let org_id = if !config.prismnet.org_id.is_empty() { config.prismnet.org_id.clone() } else { std::env::var("K8SHOST_ORG_ID").unwrap_or_else(|_| "default-org".to_string()) }; let project_id = if !config.prismnet.project_id.is_empty() { config.prismnet.project_id.clone() } else { std::env::var("K8SHOST_PROJECT_ID").unwrap_or_else(|_| "default-project".to_string()) }; let subnet_id = if !config.prismnet.subnet_id.is_empty() { config.prismnet.subnet_id.clone() } else { std::env::var("K8SHOST_SUBNET_ID").context("subnet_id not configured")? }; // Create port in PrismNET let port_name = format!("pod-{}", container_id); let create_req = CreatePortRequest { org_id: org_id.clone(), project_id: project_id.clone(), subnet_id: subnet_id.clone(), name: port_name.clone(), description: format!("k8shost pod {} network port", container_id), ip_address: String::new(), // Let PrismNET auto-allocate security_group_ids: vec![], }; let create_resp = port_client .create_port(create_req) .await .context("Failed to create PrismNET port")? .into_inner(); let port = create_resp.port.context("Port not returned in response")?; tracing::info!( port_id = %port.id, ip_address = %port.ip_address, mac_address = %port.mac_address, "PrismNET port created successfully" ); // TODO: In production, we would: // 1. Create veth pair // 2. Move one end to container network namespace // 3. Configure IP address and routes // 4. Configure OVN logical switch port with MAC/IP // // For MVP, we return the allocated IP/MAC information // Extract gateway from subnet (would come from GetSubnet call in production) let gateway = port.ip_address.split('.').take(3).collect::>().join(".") + ".1"; // Return CNI result let result = CniResult { cni_version: config.cni_version, interfaces: vec![Interface { name: ifname.clone(), mac: port.mac_address.clone(), sandbox: netns, }], ips: vec![IpConfig { interface: 0, address: format!("{}/24", port.ip_address), gateway: gateway.clone(), }], routes: vec![Route { dst: "0.0.0.0/0".to_string(), gw: gateway, }], dns: DnsConfig { nameservers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()], }, }; println!("{}", serde_json::to_string(&result)?); tracing::info!("CNI ADD operation completed successfully"); Ok(()) } async fn handle_del() -> Result<()> { // Read CNI config from stdin let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer)?; let config: CniConfig = serde_json::from_str(&buffer)?; // Parse CNI environment variables let container_id = std::env::var("CNI_CONTAINERID").context("CNI_CONTAINERID not set")?; tracing::info!( container_id = %container_id, "CNI DEL operation starting" ); // Connect to PrismNET server let prismnet_addr = if config.prismnet.server_addr.is_empty() { std::env::var("NOVANET_SERVER_ADDR").unwrap_or_else(|_| "http://127.0.0.1:50052".to_string()) } else { config.prismnet.server_addr.clone() }; let mut port_client = PortServiceClient::connect(prismnet_addr.clone()) .await .context("Failed to connect to PrismNET server")?; // Extract tenant context let org_id = if !config.prismnet.org_id.is_empty() { config.prismnet.org_id.clone() } else { std::env::var("K8SHOST_ORG_ID").unwrap_or_else(|_| "default-org".to_string()) }; let project_id = if !config.prismnet.project_id.is_empty() { config.prismnet.project_id.clone() } else { std::env::var("K8SHOST_PROJECT_ID").unwrap_or_else(|_| "default-project".to_string()) }; let subnet_id = if !config.prismnet.subnet_id.is_empty() { config.prismnet.subnet_id.clone() } else { std::env::var("K8SHOST_SUBNET_ID").unwrap_or_default() }; // Find port by container ID using device_id filter // List ports to find our port ID let list_req = ListPortsRequest { org_id: org_id.clone(), project_id: project_id.clone(), subnet_id: subnet_id.clone(), device_id: container_id.clone(), page_size: 10, page_token: String::new(), }; let list_resp = port_client.list_ports(list_req).await; if let Ok(resp) = list_resp { let ports = resp.into_inner().ports; if let Some(port) = ports.first() { // Delete the port let delete_req = DeletePortRequest { org_id, project_id, subnet_id, id: port.id.clone(), }; port_client .delete_port(delete_req) .await .context("Failed to delete PrismNET port")?; tracing::info!( port_id = %port.id, "PrismNET port deleted successfully" ); } } // TODO: In production, we would also: // 1. Remove network interfaces from container namespace // 2. Clean up veth pair // 3. Remove OVN logical switch port configuration tracing::info!("CNI DEL operation completed successfully"); Ok(()) } fn handle_check() -> Result<()> { // TODO: Implement CHECK logic // Verify that the network configuration is still valid // For now, return success tracing::info!("CNI CHECK operation - basic validation passed"); Ok(()) } fn handle_version() -> Result<()> { let version = serde_json::json!({ "cniVersion": "1.0.0", "supportedVersions": ["0.3.0", "0.3.1", "0.4.0", "1.0.0"] }); println!("{}", serde_json::to_string(&version)?); Ok(()) }