photoncloud-monorepo/plasmavmc/crates/plasmavmc-server/tests/novanet_integration.rs
centra a7ec7e2158 Add T026 practical test + k8shost to flake + workspace files
- Created T026-practical-test task.yaml for MVP smoke testing
- Added k8shost-server to flake.nix (packages, apps, overlays)
- Staged all workspace directories for nix flake build
- Updated flake.nix shellHook to include k8shost

Resolves: T026.S1 blocker (R8 - nix submodule visibility)
2025-12-09 06:07:50 +09:00

570 lines
18 KiB
Rust

//! Integration test for PlasmaVMC + NovaNET 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 novanet_api::proto::{
vpc_service_client::VpcServiceClient, subnet_service_client::SubnetServiceClient,
port_service_client::PortServiceClient, CreateVpcRequest, CreateSubnetRequest,
CreatePortRequest, GetPortRequest,
};
/// Helper to start NovaNET server
async fn start_novanet_server(addr: &str) -> tokio::task::JoinHandle<()> {
use novanet_server::{
metadata::NetworkMetadataStore,
services::{vpc::VpcServiceImpl, subnet::SubnetServiceImpl, port::PortServiceImpl, security_group::SecurityGroupServiceImpl},
};
use novanet_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 vpc_svc = VpcServiceImpl::new(metadata_store.clone());
let subnet_svc = SubnetServiceImpl::new(metadata_store.clone());
let port_svc = PortServiceImpl::new(metadata_store.clone());
let sg_svc = SecurityGroupServiceImpl::new(metadata_store);
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 NovaNET integration
async fn start_plasmavmc_server(addr: &str, novanet_endpoint: String) -> tokio::task::JoinHandle<()> {
std::env::set_var("NOVANET_ENDPOINT", novanet_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 novanet_port_attachment_lifecycle() {
// Start NovaNET server
let novanet_addr = "127.0.0.1:50081";
let novanet_handle = start_novanet_server(novanet_addr).await;
sleep(Duration::from_millis(300)).await;
// Start PlasmaVMC server with NovaNET integration
let plasmavmc_addr = "127.0.0.1:50082";
let novanet_endpoint = format!("http://{}", novanet_addr);
let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, novanet_endpoint).await;
sleep(Duration::from_millis(300)).await;
// Create NovaNET clients
let novanet_channel = Channel::from_shared(format!("http://{}", novanet_addr))
.unwrap()
.connect()
.await
.unwrap();
let mut vpc_client = VpcServiceClient::new(novanet_channel.clone());
let mut subnet_client = SubnetServiceClient::new(novanet_channel.clone());
let mut port_client = PortServiceClient::new(novanet_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 NovaNET
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: "10.0.0.0/16".to_string(),
}))
.await
.unwrap()
.into_inner();
let vpc_id = vpc_resp.vpc.unwrap().id;
// 2. Create Subnet via NovaNET
let subnet_resp = subnet_client
.create_subnet(Request::new(CreateSubnetRequest {
org_id: org_id.to_string(),
project_id: project_id.to_string(),
vpc_id: vpc_id.clone(),
name: "test-subnet".to_string(),
description: "Integration test subnet".to_string(),
cidr: "10.0.1.0/24".to_string(),
gateway: "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 NovaNET
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(),
mac_address: String::new(), // Auto-generated
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 NovaNET 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 NovaNET 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
novanet_handle.abort();
plasmavmc_handle.abort();
}
#[tokio::test]
#[ignore] // Requires mock hypervisor mode
async fn test_network_tenant_isolation() {
// Start NovaNET server
let novanet_addr = "127.0.0.1:50083";
let novanet_handle = start_novanet_server(novanet_addr).await;
sleep(Duration::from_millis(300)).await;
// Start PlasmaVMC server with NovaNET integration
let plasmavmc_addr = "127.0.0.1:50084";
let novanet_endpoint = format!("http://{}", novanet_addr);
let plasmavmc_handle = start_plasmavmc_server(plasmavmc_addr, novanet_endpoint).await;
sleep(Duration::from_millis(300)).await;
// Create NovaNET clients
let novanet_channel = Channel::from_shared(format!("http://{}", novanet_addr))
.unwrap()
.connect()
.await
.unwrap();
let mut vpc_client = VpcServiceClient::new(novanet_channel.clone());
let mut subnet_client = SubnetServiceClient::new(novanet_channel.clone());
let mut port_client = PortServiceClient::new(novanet_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: "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 {
org_id: org_a.to_string(),
project_id: project_a.to_string(),
vpc_id: vpc_a_id.clone(),
name: "subnet-a".to_string(),
description: "Tenant A Subnet".to_string(),
cidr: "10.0.1.0/24".to_string(),
gateway: "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(),
mac_address: String::new(),
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: "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 {
org_id: org_b.to_string(),
project_id: project_b.to_string(),
vpc_id: vpc_b_id.clone(),
name: "subnet-b".to_string(),
description: "Tenant B Subnet".to_string(),
cidr: "10.1.1.0/24".to_string(),
gateway: "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(),
mac_address: String::new(),
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, "10.0.1.0/24", "Tenant A subnet CIDR mismatch");
assert_eq!(subnet_b.cidr, "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
novanet_handle.abort();
plasmavmc_handle.abort();
}