- 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
1074 lines
35 KiB
Rust
1074 lines
35 KiB
Rust
//! 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();
|
|
}
|