- Replace form_urlencoded with RFC 3986 compliant URI encoding - Implement aws_uri_encode() matching AWS SigV4 spec exactly - Unreserved chars (A-Z,a-z,0-9,-,_,.,~) not encoded - All other chars percent-encoded with uppercase hex - Preserve slashes in paths, encode in query params - Normalize empty paths to '/' per AWS spec - Fix test expectations (body hash, HMAC values) - Add comprehensive SigV4 signature determinism test This fixes the canonicalization mismatch that caused signature validation failures in T047. Auth can now be enabled for production. Refs: T058.S1
901 lines
30 KiB
Rust
901 lines
30 KiB
Rust
//! Integration tests for VM-to-VM Cross-Communication via PrismNET (T029.S3)
|
|
//!
|
|
//! These tests verify that:
|
|
//! 1. VMs on the same PrismNET subnet can communicate (logical L2 connectivity)
|
|
//! 2. Tenant isolation is enforced (different VPCs cannot communicate)
|
|
//! 3. Full lifecycle works correctly (create → attach → verify → delete)
|
|
//!
|
|
//! This test uses real service implementations with in-memory/mock backends:
|
|
//! - PrismNET: NetworkMetadataStore (in-memory) + OvnClient (mock)
|
|
//! - PlasmaVMC: VmServiceImpl with in-memory storage
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::time::sleep;
|
|
use tonic::transport::{Channel, Server};
|
|
use tonic::Request;
|
|
|
|
// PrismNET imports
|
|
use prismnet_api::proto::{
|
|
port_service_client::PortServiceClient, subnet_service_client::SubnetServiceClient,
|
|
vpc_service_client::VpcServiceClient, CreatePortRequest, CreateSubnetRequest, CreateVpcRequest,
|
|
GetPortRequest,
|
|
};
|
|
use prismnet_server::{
|
|
metadata::NetworkMetadataStore,
|
|
ovn::OvnClient,
|
|
services::{
|
|
port::PortServiceImpl, security_group::SecurityGroupServiceImpl,
|
|
subnet::SubnetServiceImpl, vpc::VpcServiceImpl,
|
|
},
|
|
};
|
|
|
|
// PlasmaVMC imports
|
|
use plasmavmc_api::proto::{
|
|
vm_service_client::VmServiceClient, CreateVmRequest, DeleteVmRequest,
|
|
HypervisorType as ProtoHypervisorType, NetworkSpec as ProtoNetworkSpec, VmSpec,
|
|
};
|
|
use plasmavmc_hypervisor::HypervisorRegistry;
|
|
use plasmavmc_kvm::KvmBackend;
|
|
use plasmavmc_server::VmServiceImpl;
|
|
|
|
// ============================================================================
|
|
// Service Startup Helpers
|
|
// ============================================================================
|
|
|
|
/// Start PrismNET server with in-memory metadata store and mock OVN client
|
|
async fn start_prismnet_server(addr: &str) -> tokio::task::JoinHandle<()> {
|
|
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(prismnet_api::proto::vpc_service_server::VpcServiceServer::new(vpc_svc))
|
|
.add_service(prismnet_api::proto::subnet_service_server::SubnetServiceServer::new(subnet_svc))
|
|
.add_service(prismnet_api::proto::port_service_server::PortServiceServer::new(port_svc))
|
|
.add_service(prismnet_api::proto::security_group_service_server::SecurityGroupServiceServer::new(sg_svc))
|
|
.serve(addr_parsed)
|
|
.await
|
|
.unwrap();
|
|
})
|
|
}
|
|
|
|
/// 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();
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Case 1: Two VMs in Same Subnet Connectivity
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires mock hypervisor mode
|
|
async fn test_vm_same_subnet_connectivity() {
|
|
// === Step 1: Start all services ===
|
|
|
|
// Start PrismNET server
|
|
let prismnet_addr = "127.0.0.1:50091";
|
|
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:50092";
|
|
let prismnet_endpoint = format!("http://{}", prismnet_addr);
|
|
let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await;
|
|
sleep(Duration::from_millis(300)).await;
|
|
|
|
// === Step 2: 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";
|
|
|
|
// === Step 3: Create PrismNET VPC and Subnet ===
|
|
|
|
// Create VPC (10.0.0.0/16)
|
|
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-to-VM connectivity".to_string(),
|
|
cidr_block: "10.0.0.0/16".to_string(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let vpc_id = vpc_resp.vpc.unwrap().id;
|
|
|
|
// Create Subnet (10.0.1.0/24)
|
|
let subnet_resp = subnet_client
|
|
.create_subnet(Request::new(CreateSubnetRequest {
|
|
vpc_id: vpc_id.clone(),
|
|
name: "test-subnet".to_string(),
|
|
description: "Test subnet for VM-to-VM connectivity".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;
|
|
|
|
// === Step 4: Create Port-1 for VM-1 (10.0.1.10) ===
|
|
|
|
let port1_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: "vm1-port".to_string(),
|
|
description: "Port for VM-1".to_string(),
|
|
ip_address: "10.0.1.10".to_string(),
|
|
security_group_ids: vec![],
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let port1 = port1_resp.port.unwrap();
|
|
let port1_id = port1.id.clone();
|
|
|
|
// Verify port is initially unattached
|
|
assert!(port1.device_id.is_empty(), "Port-1 should not have device_id initially");
|
|
assert_eq!(port1.ip_address, "10.0.1.10", "Port-1 should have correct IP");
|
|
|
|
// === Step 5: Create Port-2 for VM-2 (10.0.1.20) ===
|
|
|
|
let port2_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: "vm2-port".to_string(),
|
|
description: "Port for VM-2".to_string(),
|
|
ip_address: "10.0.1.20".to_string(),
|
|
security_group_ids: vec![],
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let port2 = port2_resp.port.unwrap();
|
|
let port2_id = port2.id.clone();
|
|
|
|
assert!(port2.device_id.is_empty(), "Port-2 should not have device_id initially");
|
|
assert_eq!(port2.ip_address, "10.0.1.20", "Port-2 should have correct IP");
|
|
|
|
// === Step 6: Create VM-1 with Port-1 ===
|
|
|
|
let vm1_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: port1_id.clone(),
|
|
mac_address: String::new(),
|
|
ip_address: String::new(),
|
|
model: 1, // VirtioNet
|
|
security_groups: vec![],
|
|
}],
|
|
boot: None,
|
|
security: None,
|
|
};
|
|
|
|
let create_vm1_resp = vm_client
|
|
.create_vm(Request::new(CreateVmRequest {
|
|
name: "test-vm-1".to_string(),
|
|
org_id: org_id.to_string(),
|
|
project_id: project_id.to_string(),
|
|
spec: Some(vm1_spec),
|
|
hypervisor: ProtoHypervisorType::Kvm as i32,
|
|
metadata: Default::default(),
|
|
labels: Default::default(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
|
|
let vm1_id = create_vm1_resp.id.clone();
|
|
assert_eq!(create_vm1_resp.name, "test-vm-1", "VM-1 should have correct name");
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// === Step 7: Create VM-2 with Port-2 ===
|
|
|
|
let vm2_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: port2_id.clone(),
|
|
mac_address: String::new(),
|
|
ip_address: String::new(),
|
|
model: 1, // VirtioNet
|
|
security_groups: vec![],
|
|
}],
|
|
boot: None,
|
|
security: None,
|
|
};
|
|
|
|
let create_vm2_resp = vm_client
|
|
.create_vm(Request::new(CreateVmRequest {
|
|
name: "test-vm-2".to_string(),
|
|
org_id: org_id.to_string(),
|
|
project_id: project_id.to_string(),
|
|
spec: Some(vm2_spec),
|
|
hypervisor: ProtoHypervisorType::Kvm as i32,
|
|
metadata: Default::default(),
|
|
labels: Default::default(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
|
|
let vm2_id = create_vm2_resp.id.clone();
|
|
assert_eq!(create_vm2_resp.name, "test-vm-2", "VM-2 should have correct name");
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// === Step 8: Verify ports are attached to VMs ===
|
|
|
|
let port1_after_vm = 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: port1_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
port1_after_vm.device_id, vm1_id,
|
|
"Port-1 should be attached to VM-1"
|
|
);
|
|
assert_eq!(
|
|
port1_after_vm.device_type, 2, // DeviceType::Vm = 2 (DEVICE_TYPE_VM from proto)
|
|
"Port-1 device_type should be Vm"
|
|
);
|
|
|
|
let port2_after_vm = 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: port2_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
port2_after_vm.device_id, vm2_id,
|
|
"Port-2 should be attached to VM-2"
|
|
);
|
|
assert_eq!(
|
|
port2_after_vm.device_type, 2, // DeviceType::Vm = 2
|
|
"Port-2 device_type should be Vm"
|
|
);
|
|
|
|
// === Step 9: Verify connectivity (mock mode - logical L2 connectivity) ===
|
|
|
|
// Both ports are in the same VPC and same subnet
|
|
// In a real deployment, this would allow L2 connectivity via OVN overlay
|
|
|
|
// Verify both ports are in the same subnet (logical L2 connectivity)
|
|
assert_eq!(
|
|
port1_after_vm.subnet_id, port2_after_vm.subnet_id,
|
|
"VM-1 and VM-2 ports should be in the same subnet for L2 connectivity"
|
|
);
|
|
|
|
// Verify both IPs are in the same /24 subnet
|
|
assert!(
|
|
port1_after_vm.ip_address.starts_with("10.0.1.") && port2_after_vm.ip_address.starts_with("10.0.1."),
|
|
"VM-1 IP ({}) and VM-2 IP ({}) should be in same subnet for connectivity",
|
|
port1_after_vm.ip_address,
|
|
port2_after_vm.ip_address
|
|
);
|
|
|
|
// Mock connectivity check: Verify both ports are attached to devices
|
|
// In real OVN, this configuration would allow ping between VMs
|
|
println!(
|
|
"VM-1 at {} and VM-2 at {} are logically connected via PrismNET overlay (VPC: {}, Subnet: {})",
|
|
port1_after_vm.ip_address, port2_after_vm.ip_address, vpc_id, subnet_id
|
|
);
|
|
|
|
// === Step 10: Cleanup ===
|
|
|
|
// Delete VM-1
|
|
vm_client
|
|
.delete_vm(Request::new(DeleteVmRequest {
|
|
org_id: org_id.to_string(),
|
|
project_id: project_id.to_string(),
|
|
vm_id: vm1_id.clone(),
|
|
force: true,
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Delete VM-2
|
|
vm_client
|
|
.delete_vm(Request::new(DeleteVmRequest {
|
|
org_id: org_id.to_string(),
|
|
project_id: project_id.to_string(),
|
|
vm_id: vm2_id.clone(),
|
|
force: true,
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// Verify ports are detached after deletion
|
|
let port1_after_delete = 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: port1_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
assert!(
|
|
port1_after_delete.device_id.is_empty(),
|
|
"Port-1 should be detached after VM-1 deletion"
|
|
);
|
|
|
|
let port2_after_delete = 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: port2_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
assert!(
|
|
port2_after_delete.device_id.is_empty(),
|
|
"Port-2 should be detached after VM-2 deletion"
|
|
);
|
|
|
|
// Cleanup server handles
|
|
prismnet_handle.abort();
|
|
plasmavmc_handle.abort();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Case 2: Tenant Isolation - Different VPCs
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires mock hypervisor mode
|
|
async fn test_tenant_isolation_different_vpc() {
|
|
// === Step 1: Start all services ===
|
|
|
|
let prismnet_addr = "127.0.0.1:50095";
|
|
let prismnet_handle = start_prismnet_server(prismnet_addr).await;
|
|
sleep(Duration::from_millis(300)).await;
|
|
|
|
let plasmavmc_addr = "127.0.0.1:50096";
|
|
let prismnet_endpoint = format!("http://{}", prismnet_addr);
|
|
let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await;
|
|
sleep(Duration::from_millis(300)).await;
|
|
|
|
// === Step 2: Create 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);
|
|
|
|
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";
|
|
|
|
// 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_id = vpc_a_resp.vpc.unwrap().id;
|
|
|
|
// 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_id = subnet_a_resp.subnet.unwrap().id;
|
|
|
|
// Create Port-A for VM (10.0.1.20)
|
|
let port_a_vm_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: "vm-a-port".to_string(),
|
|
description: "Port for Tenant A VM".to_string(),
|
|
ip_address: "10.0.1.20".to_string(),
|
|
security_group_ids: vec![],
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let port_a_vm_id = port_a_vm_resp.port.unwrap().id;
|
|
|
|
// Create VM-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_vm_id.clone(),
|
|
mac_address: String::new(),
|
|
ip_address: String::new(),
|
|
model: 1,
|
|
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;
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// === TENANT B: org-b, project-b ===
|
|
let org_b = "org-b";
|
|
let project_b = "project-b";
|
|
|
|
// 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_id = vpc_b_resp.vpc.unwrap().id;
|
|
|
|
// 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_id = subnet_b_resp.subnet.unwrap().id;
|
|
|
|
// Create Port-B for VM (10.1.1.20)
|
|
let port_b_vm_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: "vm-b-port".to_string(),
|
|
description: "Port for Tenant B VM".to_string(),
|
|
ip_address: "10.1.1.20".to_string(),
|
|
security_group_ids: vec![],
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let port_b_vm_id = port_b_vm_resp.port.unwrap().id;
|
|
|
|
// Create VM-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_vm_id.clone(),
|
|
mac_address: String::new(),
|
|
ip_address: String::new(),
|
|
model: 1,
|
|
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;
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// === VERIFICATION: Tenant Isolation ===
|
|
|
|
// Verify VPCs are different 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"
|
|
);
|
|
|
|
// Verify port isolation - different org_id/project_id
|
|
let port_a_vm_final = 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_vm_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
let port_b_vm_final = 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_vm_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
// Verify VM-A is attached to Subnet-A
|
|
assert_eq!(port_a_vm_final.device_id, vm_a_id);
|
|
assert_eq!(port_a_vm_final.ip_address, "10.0.1.20");
|
|
assert_eq!(port_a_vm_final.device_type, 2, "Port-A device_type should be Vm");
|
|
|
|
// Verify VM-B is attached to Subnet-B
|
|
assert_eq!(port_b_vm_final.device_id, vm_b_id);
|
|
assert_eq!(port_b_vm_final.ip_address, "10.1.1.20");
|
|
assert_eq!(port_b_vm_final.device_type, 2, "Port-B device_type should be Vm");
|
|
|
|
// Verify isolation: different subnets mean no L2 connectivity
|
|
// In OVN, different VPCs use different logical switches, enforced via subnet isolation
|
|
assert_ne!(
|
|
port_a_vm_final.subnet_id, port_b_vm_final.subnet_id,
|
|
"Tenant A and B must use different subnets for isolation"
|
|
);
|
|
|
|
// Additional verification: Different subnets belong to different VPCs
|
|
assert_ne!(
|
|
subnet_a_id, subnet_b_id,
|
|
"Tenant A and B must have different subnet IDs"
|
|
);
|
|
|
|
println!(
|
|
"Tenant isolation verified: VPC-A ({}) and VPC-B ({}) are isolated via different subnets",
|
|
vpc_a_id, vpc_b_id
|
|
);
|
|
|
|
// === Cleanup ===
|
|
prismnet_handle.abort();
|
|
plasmavmc_handle.abort();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Case 3: VM E2E Lifecycle
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires mock hypervisor mode
|
|
async fn test_vm_e2e_lifecycle() {
|
|
// === Step 1: Start all services ===
|
|
|
|
let prismnet_addr = "127.0.0.1:50099";
|
|
let prismnet_handle = start_prismnet_server(prismnet_addr).await;
|
|
sleep(Duration::from_millis(300)).await;
|
|
|
|
let plasmavmc_addr = "127.0.0.1:50100";
|
|
let prismnet_endpoint = format!("http://{}", prismnet_addr);
|
|
let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, prismnet_endpoint).await;
|
|
sleep(Duration::from_millis(300)).await;
|
|
|
|
// === Step 2: Create 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);
|
|
|
|
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 = "lifecycle-org";
|
|
let project_id = "lifecycle-project";
|
|
|
|
// === Step 3: Create VPC and Subnet ===
|
|
|
|
let vpc_resp = vpc_client
|
|
.create_vpc(Request::new(CreateVpcRequest {
|
|
org_id: org_id.to_string(),
|
|
project_id: project_id.to_string(),
|
|
name: "lifecycle-vpc".to_string(),
|
|
description: "VPC for VM lifecycle test".to_string(),
|
|
cidr_block: "10.2.0.0/16".to_string(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let vpc_id = vpc_resp.vpc.unwrap().id;
|
|
|
|
let subnet_resp = subnet_client
|
|
.create_subnet(Request::new(CreateSubnetRequest {
|
|
vpc_id: vpc_id.clone(),
|
|
name: "lifecycle-subnet".to_string(),
|
|
description: "Subnet for VM lifecycle test".to_string(),
|
|
cidr_block: "10.2.1.0/24".to_string(),
|
|
gateway_ip: "10.2.1.1".to_string(),
|
|
dhcp_enabled: true,
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let subnet_id = subnet_resp.subnet.unwrap().id;
|
|
|
|
// === Step 4: Create VM port ===
|
|
|
|
let vm_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: "lifecycle-vm-port".to_string(),
|
|
description: "Port for lifecycle test VM".to_string(),
|
|
ip_address: "10.2.1.20".to_string(),
|
|
security_group_ids: vec![],
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner();
|
|
let vm_port = vm_port_resp.port.unwrap();
|
|
let vm_port_id = vm_port.id.clone();
|
|
|
|
assert!(vm_port.device_id.is_empty(), "VM port should be unattached initially");
|
|
assert_eq!(vm_port.ip_address, "10.2.1.20", "VM port should have correct IP");
|
|
|
|
// === Step 5: Create VM and attach to 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: vm_port_id.clone(),
|
|
mac_address: String::new(),
|
|
ip_address: String::new(),
|
|
model: 1,
|
|
security_groups: vec![],
|
|
}],
|
|
boot: None,
|
|
security: None,
|
|
};
|
|
|
|
let create_vm_resp = vm_client
|
|
.create_vm(Request::new(CreateVmRequest {
|
|
name: "lifecycle-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, "lifecycle-vm");
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// === Step 6: Verify VM port state transition: unattached → attached ===
|
|
|
|
let vm_port_attached = 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: vm_port_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
vm_port_attached.device_id, vm_id,
|
|
"VM port should be attached to VM"
|
|
);
|
|
assert_eq!(vm_port_attached.device_type, 2, "VM port device_type should be Vm (DEVICE_TYPE_VM = 2)");
|
|
assert_eq!(vm_port_attached.subnet_id, subnet_id, "VM port should be in the correct subnet");
|
|
assert_eq!(vm_port_attached.ip_address, "10.2.1.20", "VM port should maintain its IP address");
|
|
|
|
println!(
|
|
"VM lifecycle: VM (IP: {}) attached to VPC {} and Subnet {}",
|
|
vm_port_attached.ip_address, vpc_id, subnet_id
|
|
);
|
|
|
|
// === Step 7: Delete VM ===
|
|
|
|
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();
|
|
|
|
sleep(Duration::from_millis(200)).await;
|
|
|
|
// === Step 8: Verify VM port state transition: attached → unattached ===
|
|
|
|
let vm_port_after_delete = 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: vm_port_id.clone(),
|
|
}))
|
|
.await
|
|
.unwrap()
|
|
.into_inner()
|
|
.port
|
|
.unwrap();
|
|
|
|
assert!(
|
|
vm_port_after_delete.device_id.is_empty(),
|
|
"VM port should be detached after VM deletion"
|
|
);
|
|
assert_eq!(
|
|
vm_port_after_delete.device_type, 0,
|
|
"VM port device_type should be None after deletion"
|
|
);
|
|
|
|
println!("VM lifecycle test completed: All resources cleaned up successfully");
|
|
|
|
// === Cleanup ===
|
|
prismnet_handle.abort();
|
|
plasmavmc_handle.abort();
|
|
}
|