//! Integration test for PlasmaVMC + PrismNET network port attachment use plasmavmc_api::proto::{ vm_service_client::VmServiceClient, CreateVmRequest, DeleteVmRequest, HypervisorType as ProtoHypervisorType, NetworkSpec as ProtoNetworkSpec, VmSpec, }; use plasmavmc_server::VmServiceImpl; use plasmavmc_hypervisor::HypervisorRegistry; use plasmavmc_kvm::KvmBackend; use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; use tonic::transport::{Channel, Server}; use tonic::Request; use prismnet_api::proto::{ vpc_service_client::VpcServiceClient, subnet_service_client::SubnetServiceClient, port_service_client::PortServiceClient, CreateVpcRequest, CreateSubnetRequest, CreatePortRequest, GetPortRequest, }; /// Helper to start PrismNET server async fn start_prismnet_server(addr: &str) -> tokio::task::JoinHandle<()> { use prismnet_server::{ metadata::NetworkMetadataStore, ovn::OvnClient, services::{vpc::VpcServiceImpl, subnet::SubnetServiceImpl, port::PortServiceImpl, security_group::SecurityGroupServiceImpl}, }; use prismnet_api::proto::{ vpc_service_server::VpcServiceServer, subnet_service_server::SubnetServiceServer, port_service_server::PortServiceServer, security_group_service_server::SecurityGroupServiceServer, }; let metadata_store = Arc::new(NetworkMetadataStore::new_in_memory()); let ovn_client = Arc::new(OvnClient::new_mock()); let vpc_svc = VpcServiceImpl::new(metadata_store.clone(), ovn_client.clone()); let subnet_svc = SubnetServiceImpl::new(metadata_store.clone()); let port_svc = PortServiceImpl::new(metadata_store.clone(), ovn_client.clone()); let sg_svc = SecurityGroupServiceImpl::new(metadata_store, ovn_client); let addr_parsed = addr.parse().unwrap(); tokio::spawn(async move { Server::builder() .add_service(VpcServiceServer::new(vpc_svc)) .add_service(SubnetServiceServer::new(subnet_svc)) .add_service(PortServiceServer::new(port_svc)) .add_service(SecurityGroupServiceServer::new(sg_svc)) .serve(addr_parsed) .await .unwrap(); }) } /// Helper to start PlasmaVMC server with PrismNET integration async fn start_plasmavmc_server(addr: &str, prismnet_endpoint: String) -> tokio::task::JoinHandle<()> { std::env::set_var("NOVANET_ENDPOINT", prismnet_endpoint); std::env::set_var("PLASMAVMC_STORAGE_BACKEND", "file"); let registry = Arc::new(HypervisorRegistry::new()); registry.register(Arc::new(KvmBackend::with_defaults())); let svc = VmServiceImpl::new(registry).await.unwrap(); let addr_parsed = addr.parse().unwrap(); tokio::spawn(async move { Server::builder() .add_service(plasmavmc_api::proto::vm_service_server::VmServiceServer::new(svc)) .serve(addr_parsed) .await .unwrap(); }) } #[tokio::test] #[ignore] // Requires mock hypervisor mode async fn prismnet_port_attachment_lifecycle() { // Start PrismNET server let prismnet_addr = "127.0.0.1:50081"; let prismnet_handle = start_prismnet_server(prismnet_addr).await; sleep(Duration::from_millis(300)).await; // Start PlasmaVMC server with PrismNET integration let plasmavmc_addr = "127.0.0.1:50082"; let prismnet_endpoint = format!("http://{}", prismnet_addr); let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await; sleep(Duration::from_millis(300)).await; // Create PrismNET clients let prismnet_channel = Channel::from_shared(format!("http://{}", prismnet_addr)) .unwrap() .connect() .await .unwrap(); let mut vpc_client = VpcServiceClient::new(prismnet_channel.clone()); let mut subnet_client = SubnetServiceClient::new(prismnet_channel.clone()); let mut port_client = PortServiceClient::new(prismnet_channel); // Create PlasmaVMC client let plasmavmc_channel = Channel::from_shared(format!("http://{}", plasmavmc_addr)) .unwrap() .connect() .await .unwrap(); let mut vm_client = VmServiceClient::new(plasmavmc_channel); let org_id = "test-org"; let project_id = "test-project"; // 1. Create VPC via PrismNET let vpc_resp = vpc_client .create_vpc(Request::new(CreateVpcRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), name: "test-vpc".to_string(), description: "Integration test VPC".to_string(), cidr_block: "10.0.0.0/16".to_string(), })) .await .unwrap() .into_inner(); let vpc_id = vpc_resp.vpc.unwrap().id; // 2. Create Subnet via PrismNET let subnet_resp = subnet_client .create_subnet(Request::new(CreateSubnetRequest { vpc_id: vpc_id.clone(), name: "test-subnet".to_string(), description: "Integration test subnet".to_string(), cidr_block: "10.0.1.0/24".to_string(), gateway_ip: "10.0.1.1".to_string(), dhcp_enabled: true, })) .await .unwrap() .into_inner(); let subnet_id = subnet_resp.subnet.unwrap().id; // 3. Create Port via PrismNET let port_resp = port_client .create_port(Request::new(CreatePortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), name: "test-port".to_string(), description: "Integration test port".to_string(), ip_address: "10.0.1.10".to_string(), security_group_ids: vec![], })) .await .unwrap() .into_inner(); let port = port_resp.port.unwrap(); let port_id = port.id.clone(); // Verify port is initially unattached assert!(port.device_id.is_empty(), "Port should not have device_id initially"); // 4. Create VM with port attachment via PlasmaVMC let vm_spec = VmSpec { cpu: None, memory: None, disks: vec![], network: vec![ProtoNetworkSpec { id: "eth0".to_string(), network_id: vpc_id.clone(), subnet_id: subnet_id.clone(), port_id: port_id.clone(), mac_address: String::new(), ip_address: String::new(), model: 1, // VirtioNet security_groups: vec![], }], boot: None, security: None, }; let create_vm_resp = vm_client .create_vm(Request::new(CreateVmRequest { name: "test-vm".to_string(), org_id: org_id.to_string(), project_id: project_id.to_string(), spec: Some(vm_spec), hypervisor: ProtoHypervisorType::Kvm as i32, metadata: Default::default(), labels: Default::default(), })) .await .unwrap() .into_inner(); let vm_id = create_vm_resp.id.clone(); assert_eq!(create_vm_resp.name, "test-vm"); // Give PrismNET time to process attachment sleep(Duration::from_millis(200)).await; // 5. Verify port status updated (device_id set to VM ID) let port_after_attach = port_client .get_port(Request::new(GetPortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), id: port_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); assert_eq!( port_after_attach.device_id, vm_id, "Port device_id should match VM ID after attachment" ); assert_eq!( port_after_attach.device_type, 1, // DeviceType::Vm "Port device_type should be Vm" ); // 6. Delete VM and verify port detached vm_client .delete_vm(Request::new(DeleteVmRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), vm_id: vm_id.clone(), force: true, })) .await .unwrap(); // Give PrismNET time to process detachment sleep(Duration::from_millis(200)).await; // Verify port is detached let port_after_detach = port_client .get_port(Request::new(GetPortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), id: port_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); assert!( port_after_detach.device_id.is_empty(), "Port device_id should be empty after VM deletion" ); assert_eq!( port_after_detach.device_type, 0, // DeviceType::None "Port device_type should be None after VM deletion" ); // Cleanup prismnet_handle.abort(); plasmavmc_handle.abort(); } #[tokio::test] #[ignore] // Requires mock hypervisor mode async fn test_network_tenant_isolation() { // Start PrismNET server let prismnet_addr = "127.0.0.1:50083"; let prismnet_handle = start_prismnet_server(prismnet_addr).await; sleep(Duration::from_millis(300)).await; // Start PlasmaVMC server with PrismNET integration let plasmavmc_addr = "127.0.0.1:50084"; let prismnet_endpoint = format!("http://{}", prismnet_addr); let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await; sleep(Duration::from_millis(300)).await; // Create PrismNET clients let prismnet_channel = Channel::from_shared(format!("http://{}", prismnet_addr)) .unwrap() .connect() .await .unwrap(); let mut vpc_client = VpcServiceClient::new(prismnet_channel.clone()); let mut subnet_client = SubnetServiceClient::new(prismnet_channel.clone()); let mut port_client = PortServiceClient::new(prismnet_channel); // Create PlasmaVMC client let plasmavmc_channel = Channel::from_shared(format!("http://{}", plasmavmc_addr)) .unwrap() .connect() .await .unwrap(); let mut vm_client = VmServiceClient::new(plasmavmc_channel); // === TENANT A: org-a, project-a === let org_a = "org-a"; let project_a = "project-a"; // 1. Create VPC-A (10.0.0.0/16) let vpc_a_resp = vpc_client .create_vpc(Request::new(CreateVpcRequest { org_id: org_a.to_string(), project_id: project_a.to_string(), name: "vpc-a".to_string(), description: "Tenant A VPC".to_string(), cidr_block: "10.0.0.0/16".to_string(), })) .await .unwrap() .into_inner(); let vpc_a = vpc_a_resp.vpc.unwrap(); let vpc_a_id = vpc_a.id.clone(); // 2. Create Subnet-A (10.0.1.0/24) let subnet_a_resp = subnet_client .create_subnet(Request::new(CreateSubnetRequest { vpc_id: vpc_a_id.clone(), name: "subnet-a".to_string(), description: "Tenant A Subnet".to_string(), cidr_block: "10.0.1.0/24".to_string(), gateway_ip: "10.0.1.1".to_string(), dhcp_enabled: true, })) .await .unwrap() .into_inner(); let subnet_a = subnet_a_resp.subnet.unwrap(); let subnet_a_id = subnet_a.id.clone(); // 3. Create Port-A (10.0.1.10) let port_a_resp = port_client .create_port(Request::new(CreatePortRequest { org_id: org_a.to_string(), project_id: project_a.to_string(), subnet_id: subnet_a_id.clone(), name: "port-a".to_string(), description: "Tenant A Port".to_string(), ip_address: "10.0.1.10".to_string(), security_group_ids: vec![], })) .await .unwrap() .into_inner(); let port_a = port_a_resp.port.unwrap(); let port_a_id = port_a.id.clone(); // 4. Create VM-A attached to Port-A let vm_a_spec = VmSpec { cpu: None, memory: None, disks: vec![], network: vec![ProtoNetworkSpec { id: "eth0".to_string(), network_id: vpc_a_id.clone(), subnet_id: subnet_a_id.clone(), port_id: port_a_id.clone(), mac_address: String::new(), ip_address: String::new(), model: 1, // VirtioNet security_groups: vec![], }], boot: None, security: None, }; let vm_a_resp = vm_client .create_vm(Request::new(CreateVmRequest { name: "vm-a".to_string(), org_id: org_a.to_string(), project_id: project_a.to_string(), spec: Some(vm_a_spec), hypervisor: ProtoHypervisorType::Kvm as i32, metadata: Default::default(), labels: Default::default(), })) .await .unwrap() .into_inner(); let vm_a_id = vm_a_resp.id.clone(); sleep(Duration::from_millis(200)).await; // === TENANT B: org-b, project-b === let org_b = "org-b"; let project_b = "project-b"; // 1. Create VPC-B (10.1.0.0/16) - DIFFERENT CIDR, DIFFERENT ORG let vpc_b_resp = vpc_client .create_vpc(Request::new(CreateVpcRequest { org_id: org_b.to_string(), project_id: project_b.to_string(), name: "vpc-b".to_string(), description: "Tenant B VPC".to_string(), cidr_block: "10.1.0.0/16".to_string(), })) .await .unwrap() .into_inner(); let vpc_b = vpc_b_resp.vpc.unwrap(); let vpc_b_id = vpc_b.id.clone(); // 2. Create Subnet-B (10.1.1.0/24) let subnet_b_resp = subnet_client .create_subnet(Request::new(CreateSubnetRequest { vpc_id: vpc_b_id.clone(), name: "subnet-b".to_string(), description: "Tenant B Subnet".to_string(), cidr_block: "10.1.1.0/24".to_string(), gateway_ip: "10.1.1.1".to_string(), dhcp_enabled: true, })) .await .unwrap() .into_inner(); let subnet_b = subnet_b_resp.subnet.unwrap(); let subnet_b_id = subnet_b.id.clone(); // 3. Create Port-B (10.1.1.10) let port_b_resp = port_client .create_port(Request::new(CreatePortRequest { org_id: org_b.to_string(), project_id: project_b.to_string(), subnet_id: subnet_b_id.clone(), name: "port-b".to_string(), description: "Tenant B Port".to_string(), ip_address: "10.1.1.10".to_string(), security_group_ids: vec![], })) .await .unwrap() .into_inner(); let port_b = port_b_resp.port.unwrap(); let port_b_id = port_b.id.clone(); // 4. Create VM-B attached to Port-B let vm_b_spec = VmSpec { cpu: None, memory: None, disks: vec![], network: vec![ProtoNetworkSpec { id: "eth0".to_string(), network_id: vpc_b_id.clone(), subnet_id: subnet_b_id.clone(), port_id: port_b_id.clone(), mac_address: String::new(), ip_address: String::new(), model: 1, // VirtioNet security_groups: vec![], }], boot: None, security: None, }; let vm_b_resp = vm_client .create_vm(Request::new(CreateVmRequest { name: "vm-b".to_string(), org_id: org_b.to_string(), project_id: project_b.to_string(), spec: Some(vm_b_spec), hypervisor: ProtoHypervisorType::Kvm as i32, metadata: Default::default(), labels: Default::default(), })) .await .unwrap() .into_inner(); let vm_b_id = vm_b_resp.id.clone(); sleep(Duration::from_millis(200)).await; // === VERIFICATION: Tenant Isolation === // Verify VPC-A and VPC-B are separate logical switches assert_ne!( vpc_a_id, vpc_b_id, "Tenant A and Tenant B must have different VPC IDs" ); // Verify subnet isolation assert_ne!( subnet_a_id, subnet_b_id, "Tenant A and Tenant B must have different Subnet IDs" ); assert_eq!(subnet_a.cidr_block, "10.0.1.0/24", "Tenant A subnet CIDR mismatch"); assert_eq!(subnet_b.cidr_block, "10.1.1.0/24", "Tenant B subnet CIDR mismatch"); // Verify port isolation assert_ne!( port_a_id, port_b_id, "Tenant A and Tenant B must have different Port IDs" ); assert_eq!(port_a.ip_address, "10.0.1.10", "Tenant A port IP mismatch"); assert_eq!(port_b.ip_address, "10.1.1.10", "Tenant B port IP mismatch"); // Verify VM-A is attached to VPC-A only assert_eq!( vm_a_resp.spec.as_ref().unwrap().network[0].network_id, vpc_a_id, "VM-A must be attached to VPC-A" ); assert_eq!( vm_a_resp.spec.as_ref().unwrap().network[0].port_id, port_a_id, "VM-A must be attached to Port-A" ); // Verify VM-B is attached to VPC-B only assert_eq!( vm_b_resp.spec.as_ref().unwrap().network[0].network_id, vpc_b_id, "VM-B must be attached to VPC-B" ); assert_eq!( vm_b_resp.spec.as_ref().unwrap().network[0].port_id, port_b_id, "VM-B must be attached to Port-B" ); // Verify ports are attached to correct VMs let port_a_after = port_client .get_port(Request::new(GetPortRequest { org_id: org_a.to_string(), project_id: project_a.to_string(), subnet_id: subnet_a_id.clone(), id: port_a_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); let port_b_after = port_client .get_port(Request::new(GetPortRequest { org_id: org_b.to_string(), project_id: project_b.to_string(), subnet_id: subnet_b_id.clone(), id: port_b_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); assert_eq!( port_a_after.device_id, vm_a_id, "Port-A must be attached to VM-A" ); assert_eq!( port_b_after.device_id, vm_b_id, "Port-B must be attached to VM-B" ); // Verify no cross-tenant references assert_ne!( vm_a_id, vm_b_id, "Tenant A and Tenant B must have different VM IDs" ); // Cleanup prismnet_handle.abort(); plasmavmc_handle.abort(); } #[tokio::test] #[ignore] // Requires mock hypervisor mode async fn test_create_vm_with_network() { // Start PrismNET server let prismnet_addr = "127.0.0.1:50085"; let prismnet_handle = start_prismnet_server(prismnet_addr).await; sleep(Duration::from_millis(300)).await; // Start PlasmaVMC server with PrismNET integration let plasmavmc_addr = "127.0.0.1:50086"; let prismnet_endpoint = format!("http://{}", prismnet_addr); let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await; sleep(Duration::from_millis(300)).await; // Create PrismNET clients let prismnet_channel = Channel::from_shared(format!("http://{}", prismnet_addr)) .unwrap() .connect() .await .unwrap(); let mut vpc_client = VpcServiceClient::new(prismnet_channel.clone()); let mut subnet_client = SubnetServiceClient::new(prismnet_channel.clone()); let mut port_client = PortServiceClient::new(prismnet_channel); // Create PlasmaVMC client let plasmavmc_channel = Channel::from_shared(format!("http://{}", plasmavmc_addr)) .unwrap() .connect() .await .unwrap(); let mut vm_client = VmServiceClient::new(plasmavmc_channel); let org_id = "test-org"; let project_id = "test-project"; // 1. Create VPC via PrismNET let vpc_resp = vpc_client .create_vpc(Request::new(CreateVpcRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), name: "test-vpc".to_string(), description: "Test VPC for VM creation".to_string(), cidr_block: "10.0.0.0/16".to_string(), })) .await .unwrap() .into_inner(); let vpc_id = vpc_resp.vpc.unwrap().id; // 2. Create Subnet via PrismNET with DHCP enabled let subnet_resp = subnet_client .create_subnet(Request::new(CreateSubnetRequest { vpc_id: vpc_id.clone(), name: "test-subnet".to_string(), description: "Test subnet with DHCP enabled".to_string(), cidr_block: "10.0.1.0/24".to_string(), gateway_ip: "10.0.1.1".to_string(), dhcp_enabled: true, })) .await .unwrap() .into_inner(); let subnet_id = subnet_resp.subnet.unwrap().id; // 3. Create Port via PrismNET let port_resp = port_client .create_port(Request::new(CreatePortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), name: "vm-port".to_string(), description: "Port for VM network interface".to_string(), ip_address: "10.0.1.10".to_string(), security_group_ids: vec![], })) .await .unwrap() .into_inner(); let port_id = port_resp.port.unwrap().id; // 4. Create VM with NetworkSpec specifying subnet_id let vm_spec = VmSpec { cpu: None, memory: None, disks: vec![], network: vec![ProtoNetworkSpec { id: "eth0".to_string(), network_id: vpc_id.clone(), subnet_id: subnet_id.clone(), port_id: port_id.clone(), mac_address: String::new(), ip_address: String::new(), model: 1, // VirtioNet security_groups: vec![], }], boot: None, security: None, }; let create_vm_resp = vm_client .create_vm(Request::new(CreateVmRequest { name: "test-vm-network".to_string(), org_id: org_id.to_string(), project_id: project_id.to_string(), spec: Some(vm_spec), hypervisor: ProtoHypervisorType::Kvm as i32, metadata: Default::default(), labels: Default::default(), })) .await .unwrap() .into_inner(); let vm_id = create_vm_resp.id.clone(); // Verify VM was created successfully assert_eq!(create_vm_resp.name, "test-vm-network"); assert!(!vm_id.is_empty(), "VM ID should be assigned"); // Verify VM has network spec with correct subnet let vm_network_spec = &create_vm_resp.spec.unwrap().network[0]; assert_eq!(vm_network_spec.subnet_id, subnet_id, "VM should be attached to correct subnet"); assert_eq!(vm_network_spec.network_id, vpc_id, "VM should be in correct VPC"); assert_eq!(vm_network_spec.port_id, port_id, "VM should use correct port"); // Give time for port attachment sleep(Duration::from_millis(200)).await; // Verify port is attached to VM let attached_port = port_client .get_port(Request::new(GetPortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), id: port_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); assert_eq!( attached_port.device_id, vm_id, "Port should be attached to VM" ); assert_eq!( attached_port.device_type, 1, // DeviceType::Vm "Port device_type should be Vm" ); // Cleanup prismnet_handle.abort(); plasmavmc_handle.abort(); } #[tokio::test] #[ignore] // Requires mock hypervisor mode async fn test_vm_gets_ip_from_dhcp() { // Start PrismNET server let prismnet_addr = "127.0.0.1:50087"; let prismnet_handle = start_prismnet_server(prismnet_addr).await; sleep(Duration::from_millis(300)).await; // Start PlasmaVMC server with PrismNET integration let plasmavmc_addr = "127.0.0.1:50088"; let prismnet_endpoint = format!("http://{}", prismnet_addr); let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await; sleep(Duration::from_millis(300)).await; // Create PrismNET clients let prismnet_channel = Channel::from_shared(format!("http://{}", prismnet_addr)) .unwrap() .connect() .await .unwrap(); let mut vpc_client = VpcServiceClient::new(prismnet_channel.clone()); let mut subnet_client = SubnetServiceClient::new(prismnet_channel.clone()); let mut port_client = PortServiceClient::new(prismnet_channel); // Create PlasmaVMC client let plasmavmc_channel = Channel::from_shared(format!("http://{}", plasmavmc_addr)) .unwrap() .connect() .await .unwrap(); let mut vm_client = VmServiceClient::new(plasmavmc_channel); let org_id = "test-org"; let project_id = "test-project"; // 1. Create VPC let vpc_resp = vpc_client .create_vpc(Request::new(CreateVpcRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), name: "dhcp-test-vpc".to_string(), description: "VPC for DHCP testing".to_string(), cidr_block: "10.2.0.0/16".to_string(), })) .await .unwrap() .into_inner(); let vpc_id = vpc_resp.vpc.unwrap().id; // 2. Create Subnet with DHCP explicitly enabled let subnet_resp = subnet_client .create_subnet(Request::new(CreateSubnetRequest { vpc_id: vpc_id.clone(), name: "dhcp-subnet".to_string(), description: "Subnet with DHCP enabled for IP allocation".to_string(), cidr_block: "10.2.1.0/24".to_string(), gateway_ip: "10.2.1.1".to_string(), dhcp_enabled: true, // DHCP enabled })) .await .unwrap() .into_inner(); let subnet = subnet_resp.subnet.unwrap(); let subnet_id = subnet.id.clone(); // Verify DHCP is enabled on subnet assert!(subnet.dhcp_enabled, "Subnet should have DHCP enabled"); assert_eq!(subnet.gateway_ip, "10.2.1.1", "Gateway IP should be set"); // 3. Create Port - IP will be allocated from DHCP pool let port_resp = port_client .create_port(Request::new(CreatePortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), name: "dhcp-port".to_string(), description: "Port with DHCP-allocated IP".to_string(), ip_address: "10.2.1.20".to_string(), // Static allocation for this test security_group_ids: vec![], })) .await .unwrap() .into_inner(); let port = port_resp.port.unwrap(); let port_id = port.id.clone(); // Verify port has IP address allocated assert!(!port.ip_address.is_empty(), "Port should have IP allocated"); assert_eq!(port.ip_address, "10.2.1.20", "Port should have correct IP from DHCP pool"); // Verify port has MAC address assert!(!port.mac_address.is_empty(), "Port should have MAC address allocated"); // 4. Create VM attached to DHCP-enabled port let vm_spec = VmSpec { cpu: None, memory: None, disks: vec![], network: vec![ProtoNetworkSpec { id: "eth0".to_string(), network_id: vpc_id.clone(), subnet_id: subnet_id.clone(), port_id: port_id.clone(), mac_address: String::new(), // Will be filled from port ip_address: String::new(), // Will be filled from port DHCP allocation model: 1, // VirtioNet security_groups: vec![], }], boot: None, security: None, }; let create_vm_resp = vm_client .create_vm(Request::new(CreateVmRequest { name: "dhcp-test-vm".to_string(), org_id: org_id.to_string(), project_id: project_id.to_string(), spec: Some(vm_spec), hypervisor: ProtoHypervisorType::Kvm as i32, metadata: Default::default(), labels: Default::default(), })) .await .unwrap() .into_inner(); let vm_id = create_vm_resp.id.clone(); // Give time for DHCP configuration sleep(Duration::from_millis(300)).await; // NOTE: In the current implementation, IP is populated from port during VM creation // In a real DHCP scenario, the VM would request IP via DHCP protocol // Here we verify the integration: Port has IP → VM inherits IP from Port // Verify the port has the expected IP let final_port = port_client .get_port(Request::new(GetPortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), id: port_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); assert_eq!( final_port.ip_address, "10.2.1.20", "Port should maintain DHCP-allocated IP" ); assert_eq!( final_port.device_id, vm_id, "Port should be attached to VM" ); // Verify IP is within subnet CIDR assert!( final_port.ip_address.starts_with("10.2.1."), "IP should be in subnet range 10.2.1.0/24" ); // Cleanup prismnet_handle.abort(); plasmavmc_handle.abort(); } #[tokio::test] #[ignore] // Requires mock hypervisor mode async fn test_vm_network_connectivity() { // Start PrismNET server let prismnet_addr = "127.0.0.1:50089"; let prismnet_handle = start_prismnet_server(prismnet_addr).await; sleep(Duration::from_millis(300)).await; // Start PlasmaVMC server with PrismNET integration let plasmavmc_addr = "127.0.0.1:50090"; let prismnet_endpoint = format!("http://{}", prismnet_addr); let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await; sleep(Duration::from_millis(300)).await; // Create PrismNET clients let prismnet_channel = Channel::from_shared(format!("http://{}", prismnet_addr)) .unwrap() .connect() .await .unwrap(); let mut vpc_client = VpcServiceClient::new(prismnet_channel.clone()); let mut subnet_client = SubnetServiceClient::new(prismnet_channel.clone()); let mut port_client = PortServiceClient::new(prismnet_channel); // Create PlasmaVMC client let plasmavmc_channel = Channel::from_shared(format!("http://{}", plasmavmc_addr)) .unwrap() .connect() .await .unwrap(); let mut vm_client = VmServiceClient::new(plasmavmc_channel); let org_id = "test-org"; let project_id = "test-project"; // 1. Create VPC let vpc_resp = vpc_client .create_vpc(Request::new(CreateVpcRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), name: "connectivity-vpc".to_string(), description: "VPC for connectivity testing".to_string(), cidr_block: "10.3.0.0/16".to_string(), })) .await .unwrap() .into_inner(); let vpc_id = vpc_resp.vpc.unwrap().id; // 2. Create Subnet with gateway configured let gateway_ip = "10.3.1.1"; let subnet_resp = subnet_client .create_subnet(Request::new(CreateSubnetRequest { vpc_id: vpc_id.clone(), name: "connectivity-subnet".to_string(), description: "Subnet with gateway for connectivity testing".to_string(), cidr_block: "10.3.1.0/24".to_string(), gateway_ip: gateway_ip.to_string(), dhcp_enabled: true, })) .await .unwrap() .into_inner(); let subnet = subnet_resp.subnet.unwrap(); let subnet_id = subnet.id.clone(); // 3. Create Port for VM let vm_ip = "10.3.1.10"; let port_resp = port_client .create_port(Request::new(CreatePortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), name: "connectivity-port".to_string(), description: "Port for connectivity test VM".to_string(), ip_address: vm_ip.to_string(), security_group_ids: vec![], })) .await .unwrap() .into_inner(); let port = port_resp.port.unwrap(); let port_id = port.id.clone(); // 4. Create VM let vm_spec = VmSpec { cpu: None, memory: None, disks: vec![], network: vec![ProtoNetworkSpec { id: "eth0".to_string(), network_id: vpc_id.clone(), subnet_id: subnet_id.clone(), port_id: port_id.clone(), mac_address: String::new(), ip_address: String::new(), model: 1, // VirtioNet security_groups: vec![], }], boot: None, security: None, }; let create_vm_resp = vm_client .create_vm(Request::new(CreateVmRequest { name: "connectivity-test-vm".to_string(), org_id: org_id.to_string(), project_id: project_id.to_string(), spec: Some(vm_spec), hypervisor: ProtoHypervisorType::Kvm as i32, metadata: Default::default(), labels: Default::default(), })) .await .unwrap() .into_inner(); let vm_id = create_vm_resp.id.clone(); sleep(Duration::from_millis(300)).await; // === CONNECTIVITY VERIFICATION (Mock Mode) === // In mock mode, we verify the network configuration is correct for connectivity: // 1. VM has IP in subnet range // 2. Subnet has gateway configured // 3. Port is attached to VM // 4. Port is in the same logical switch (VPC) as the gateway let final_port = port_client .get_port(Request::new(GetPortRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), subnet_id: subnet_id.clone(), id: port_id.clone(), })) .await .unwrap() .into_inner() .port .unwrap(); // Verify VM is attached to port assert_eq!( final_port.device_id, vm_id, "VM should be attached to network port" ); // Verify port has IP in same subnet as gateway assert_eq!( final_port.ip_address, vm_ip, "VM port should have IP in subnet" ); // Verify gateway is configured (VM would use this for routing) assert_eq!( subnet.gateway_ip, gateway_ip, "Subnet should have gateway configured" ); // Verify VM IP and gateway are in same /24 subnet assert!( final_port.ip_address.starts_with("10.3.1.") && gateway_ip.starts_with("10.3.1."), "VM IP and gateway should be in same subnet for connectivity" ); // Mock connectivity check: Verify port is in correct VPC logical switch // In real OVN, this would allow L2/L3 connectivity to the gateway let vm_network = &create_vm_resp.spec.unwrap().network[0]; assert_eq!( vm_network.network_id, vpc_id, "VM should be in VPC logical switch for gateway connectivity" ); // NOTE: Actual ping test would require: // - Real VM running (not mock hypervisor) // - TAP interface configured on host // - OVN forwarding rules active // - Gateway router port created // This mock test verifies the configuration prerequisites for connectivity // Cleanup prismnet_handle.abort(); plasmavmc_handle.abort(); }