# PlasmaVMC Integration Design **Date:** 2025-12-08 **Task:** T015 S4 **Status:** Design Complete ## 1. Overview PlasmaVMC VmServiceとOverlay Network Serviceの統合設計。VM作成時にネットワークポートを自動的に作成・アタッチし、IPアドレス割り当てとセキュリティグループ適用を行う。 ## 2. Integration Architecture ### 2.1 Service Dependencies ``` VmService (plasmavmc-server) │ ├──→ NetworkService (overlay-network-server) │ ├──→ ChainFire (network state) │ └──→ OVN (logical network) │ └──→ HypervisorBackend (KVM/FireCracker) └──→ OVN Controller (via OVS) └──→ VM TAP Interface ``` ### 2.2 Integration Flow ``` 1. User → VmService.create_vm(NetworkSpec) 2. VmService → NetworkService.create_port() └── Creates OVN Logical Port └── Allocates IP (DHCP or static) └── Applies security groups 3. VmService → HypervisorBackend.create() └── Creates VM with TAP interface └── Attaches TAP to OVN port 4. OVN → Updates network state └── Port appears in Logical Switch └── DHCP server ready ``` ## 3. VmConfig Network Schema Extension ### 3.1 Current NetworkSpec 既存の`NetworkSpec`は以下のフィールドを持っています: ```rust pub struct NetworkSpec { pub id: String, pub network_id: String, // Currently: "default" or user-specified pub mac_address: Option, pub ip_address: Option, pub model: NicModel, pub security_groups: Vec, } ``` ### 3.2 Extended NetworkSpec `network_id`フィールドを拡張して、subnet_idを明示的に指定できるようにします: ```rust pub struct NetworkSpec { /// Interface identifier (unique within VM) pub id: String, /// Subnet identifier: "{org_id}/{project_id}/{subnet_name}" /// If not specified, uses default subnet for project pub subnet_id: Option, /// Legacy network_id field (deprecated, use subnet_id instead) /// If subnet_id is None and network_id is set, treated as subnet name #[deprecated(note = "Use subnet_id instead")] pub network_id: String, /// MAC address (auto-generated if None) pub mac_address: Option, /// IP address (DHCP if None, static if Some) pub ip_address: Option, /// NIC model (virtio-net, e1000, etc.) pub model: NicModel, /// Security group IDs: ["{org_id}/{project_id}/{sg_name}", ...] /// If empty, uses default security group pub security_groups: Vec, } ``` ### 3.3 Migration Strategy **Phase 1: Backward Compatibility** - `network_id`が設定されている場合、`subnet_id`に変換 - `network_id = "default"` → `subnet_id = "{org_id}/{project_id}/default"` - `network_id = "{subnet_name}"` → `subnet_id = "{org_id}/{project_id}/{subnet_name}"` **Phase 2: Deprecation** - `network_id`フィールドを非推奨としてマーク - 新規VM作成では`subnet_id`を使用 **Phase 3: Removal** - `network_id`フィールドを削除(将来のバージョン) ## 4. VM Creation Integration ### 4.1 VmService.create_vm() Flow ```rust impl VmService { async fn create_vm(&self, request: CreateVmRequest) -> Result { let req = request.into_inner(); // 1. Validate network specs for net_spec in &req.spec.network { self.validate_network_spec(&req.org_id, &req.project_id, net_spec)?; } // 2. Create VM record let mut vm = VirtualMachine::new( req.name, &req.org_id, &req.project_id, Self::proto_spec_to_types(req.spec), ); // 3. Create network ports let mut ports = Vec::new(); for net_spec in &vm.spec.network { let port = self.network_service .create_port(CreatePortRequest { org_id: vm.org_id.clone(), project_id: vm.project_id.clone(), subnet_id: self.resolve_subnet_id( &vm.org_id, &vm.project_id, &net_spec.subnet_id, )?, vm_id: vm.id.to_string(), mac_address: net_spec.mac_address.clone(), ip_address: net_spec.ip_address.clone(), security_group_ids: if net_spec.security_groups.is_empty() { vec!["default".to_string()] } else { net_spec.security_groups.clone() }, }) .await?; ports.push(port); } // 4. Create VM via hypervisor backend let handle = self.hypervisor_backend .create(&vm) .await?; // 5. Attach network ports to VM for (net_spec, port) in vm.spec.network.iter().zip(ports.iter()) { self.attach_port_to_vm(port, &handle, net_spec).await?; } // 6. Persist VM and ports self.store.save_vm(&vm).await?; for port in &ports { self.network_service.save_port(port).await?; } Ok(vm) } fn resolve_subnet_id( &self, org_id: &str, project_id: &str, subnet_id: Option<&String>, ) -> Result { match subnet_id { Some(id) if id.starts_with(&format!("{}/{}", org_id, project_id)) => { Ok(id.clone()) } Some(name) => { // Treat as subnet name Ok(format!("{}/{}/{}", org_id, project_id, name)) } None => { // Use default subnet Ok(format!("{}/{}/default", org_id, project_id)) } } } async fn attach_port_to_vm( &self, port: &Port, handle: &VmHandle, net_spec: &NetworkSpec, ) -> Result<()> { // 1. Get TAP interface name from OVN port let tap_name = self.network_service .get_port_tap_name(&port.id) .await?; // 2. Attach TAP to VM via hypervisor backend match vm.hypervisor { HypervisorType::Kvm => { // QEMU: Use -netdev tap with TAP interface self.kvm_backend.attach_nic(handle, &NetworkSpec { id: net_spec.id.clone(), network_id: port.subnet_id.clone(), mac_address: Some(port.mac_address.clone()), ip_address: port.ip_address.clone(), model: net_spec.model, security_groups: port.security_group_ids.clone(), }).await?; } HypervisorType::Firecracker => { // FireCracker: Use TAP interface in network config self.firecracker_backend.attach_nic(handle, &NetworkSpec { id: net_spec.id.clone(), network_id: port.subnet_id.clone(), mac_address: Some(port.mac_address.clone()), ip_address: port.ip_address.clone(), model: net_spec.model, security_groups: port.security_group_ids.clone(), }).await?; } _ => { return Err(Error::Unsupported("Hypervisor not supported".into())); } } Ok(()) } } ``` ### 4.2 NetworkService Integration Points **Required Methods:** ```rust pub trait NetworkServiceClient: Send + Sync { /// Create a port for VM network interface async fn create_port(&self, req: CreatePortRequest) -> Result; /// Get port details async fn get_port(&self, org_id: &str, project_id: &str, port_id: &str) -> Result>; /// Get TAP interface name for port async fn get_port_tap_name(&self, port_id: &str) -> Result; /// Delete port async fn delete_port(&self, org_id: &str, project_id: &str, port_id: &str) -> Result<()>; /// Ensure VPC and default subnet exist for project async fn ensure_project_network(&self, org_id: &str, project_id: &str) -> Result<()>; } ``` ## 5. Port Creation Details ### 5.1 Port Creation Flow ``` 1. VmService.create_vm() called with NetworkSpec └── subnet_id: "{org_id}/{project_id}/{subnet_name}" or None (default) 2. NetworkService.create_port() called ├── Resolve subnet_id (use default if None) ├── Ensure VPC and subnet exist (create if not) ├── Create OVN Logical Port │ └── ovn-nbctl lsp-add ├── Set port options (MAC, IP if static) │ └── ovn-nbctl lsp-set-addresses ├── Apply security groups (OVN ACLs) │ └── ovn-nbctl acl-add ├── Allocate IP address (if static) │ └── Update ChainFire IPAM state └── Return Port object 3. HypervisorBackend.create() called └── Creates VM with network interface 4. Attach port to VM ├── Get TAP interface name from OVN ├── Create TAP interface (if not exists) ├── Bind TAP to OVN port │ └── ovs-vsctl add-port -- set Interface type=internal └── Attach TAP to VM NIC ``` ### 5.2 Default Subnet Creation プロジェクトのデフォルトサブネットが存在しない場合、自動作成: ```rust async fn ensure_project_network( &self, org_id: &str, project_id: &str, ) -> Result<()> { // Check if VPC exists let vpc_id = format!("{}/{}", org_id, project_id); if self.get_vpc(org_id, project_id).await?.is_none() { // Create VPC with auto-allocated CIDR self.create_vpc(CreateVpcRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), name: "default".to_string(), cidr: None, // Auto-allocate }).await?; } // Check if default subnet exists let subnet_id = format!("{}/{}/default", org_id, project_id); if self.get_subnet(org_id, project_id, "default").await?.is_none() { // Get VPC CIDR let vpc = self.get_vpc(org_id, project_id).await?.unwrap(); let vpc_cidr: IpNet = vpc.cidr.parse()?; // Create default subnet: first /24 in VPC let subnet_cidr = format!("{}.0.0/24", vpc_cidr.network().octets()[1]); self.create_subnet(CreateSubnetRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), vpc_id: vpc_id.clone(), name: "default".to_string(), cidr: subnet_cidr, dhcp_enabled: true, dns_servers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()], }).await?; // Create default security group self.create_security_group(CreateSecurityGroupRequest { org_id: org_id.to_string(), project_id: project_id.to_string(), name: "default".to_string(), description: "Default security group".to_string(), ingress_rules: vec![ SecurityRule { protocol: Protocol::All, port_range: None, source_type: SourceType::SecurityGroup, source: format!("{}/{}/default", org_id, project_id), }, ], egress_rules: vec![ SecurityRule { protocol: Protocol::All, port_range: None, source_type: SourceType::Cidr, source: "0.0.0.0/0".to_string(), }, ], }).await?; } Ok(()) } ``` ## 6. IP Address Assignment ### 6.1 DHCP Assignment (Default) ```rust // Port creation with DHCP let port = network_service.create_port(CreatePortRequest { subnet_id: subnet_id.clone(), vm_id: vm_id.clone(), ip_address: None, // DHCP // ... }).await?; // IP will be assigned by OVN DHCP server // Port.ip_address will be None until DHCP lease is obtained // VmService should poll or wait for IP assignment ``` ### 6.2 Static Assignment ```rust // Port creation with static IP let port = network_service.create_port(CreatePortRequest { subnet_id: subnet_id.clone(), vm_id: vm_id.clone(), ip_address: Some("10.1.0.10".to_string()), // Static // ... }).await?; // IP is allocated immediately // Port.ip_address will be Some("10.1.0.10") ``` ### 6.3 IP Assignment Tracking ```rust // Update VM status with assigned IPs vm.status.ip_addresses = ports .iter() .filter_map(|p| p.ip_address.clone()) .collect(); // Persist updated VM status store.save_vm(&vm).await?; ``` ## 7. Security Group Binding ### 7.1 Security Group Resolution ```rust fn resolve_security_groups( org_id: &str, project_id: &str, security_groups: &[String], ) -> Vec { if security_groups.is_empty() { // Use default security group vec![format!("{}/{}/default", org_id, project_id)] } else { // Resolve security group IDs security_groups .iter() .map(|sg| { if sg.contains('/') { // Full ID: "{org_id}/{project_id}/{sg_name}" sg.clone() } else { // Name only: "{sg_name}" format!("{}/{}/{}", org_id, project_id, sg) } }) .collect() } } ``` ### 7.2 OVN ACL Application ```rust async fn apply_security_groups( &self, port: &Port, security_groups: &[String], ) -> Result<()> { for sg_id in security_groups { let sg = self.get_security_group_by_id(sg_id).await?; // Apply ingress rules for rule in &sg.ingress_rules { let acl_match = build_acl_match(rule, &sg.id)?; ovn_nbctl.acl_add( &port.subnet_id, "to-lport", 1000, &acl_match, "allow-related", ).await?; } // Apply egress rules for rule in &sg.egress_rules { let acl_match = build_acl_match(rule, &sg.id)?; ovn_nbctl.acl_add( &port.subnet_id, "from-lport", 1000, &acl_match, "allow-related", ).await?; } } Ok(()) } ``` ## 8. VM Deletion Integration ### 8.1 Port Cleanup ```rust impl VmService { async fn delete_vm(&self, request: DeleteVmRequest) -> Result<()> { let req = request.into_inner(); // 1. Get VM and ports let vm = self.get_vm(&req.org_id, &req.project_id, &req.vm_id).await?; let ports = self.network_service .list_ports(&req.org_id, &req.project_id, Some(&req.vm_id)) .await?; // 2. Stop VM if running if matches!(vm.state, VmState::Running | VmState::Starting) { self.stop_vm(request.clone()).await?; } // 3. Delete VM via hypervisor backend if let Some(handle) = self.handles.get(&TenantKey::new( &req.org_id, &req.project_id, &req.vm_id, )) { self.hypervisor_backend.delete(&handle).await?; } // 4. Delete network ports for port in &ports { self.network_service .delete_port(&req.org_id, &req.project_id, &port.id) .await?; } // 5. Delete VM from storage self.store.delete_vm(&req.org_id, &req.project_id, &req.vm_id).await?; Ok(()) } } ``` ## 9. Error Handling ### 9.1 Network Creation Failures ```rust // If network creation fails, VM creation should fail match network_service.create_port(req).await { Ok(port) => port, Err(NetworkError::SubnetNotFound) => { // Try to create default subnet network_service.ensure_project_network(org_id, project_id).await?; network_service.create_port(req).await? } Err(e) => return Err(VmError::NetworkError(e)), } ``` ### 9.2 Port Attachment Failures ```rust // If port attachment fails, clean up created port match self.attach_port_to_vm(&port, &handle, &net_spec).await { Ok(()) => {} Err(e) => { // Clean up port let _ = self.network_service .delete_port(&vm.org_id, &vm.project_id, &port.id) .await; return Err(e); } } ``` ## 10. Configuration ### 10.1 VmService Configuration ```toml [vm_service] network_service_endpoint = "http://127.0.0.1:8081" network_service_timeout_secs = 30 [network] auto_create_default_subnet = true default_security_group_name = "default" ``` ### 10.2 Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `PLASMAVMC_NETWORK_SERVICE_ENDPOINT` | `http://127.0.0.1:8081` | NetworkService gRPC endpoint | | `PLASMAVMC_AUTO_CREATE_NETWORK` | `true` | Auto-create VPC/subnet for project | ## 11. Testing Considerations ### 11.1 Unit Tests - Mock NetworkService client - Test subnet_id resolution - Test security group resolution - Test port creation flow ### 11.2 Integration Tests - Real NetworkService + OVN - VM creation with network attachment - IP assignment verification - Security group enforcement ### 11.3 Test Scenarios 1. **VM creation with default network** - No NetworkSpec → uses default subnet - Default security group applied 2. **VM creation with custom subnet** - NetworkSpec with subnet_id - Custom security groups 3. **VM creation with static IP** - NetworkSpec with ip_address - IP allocation verification 4. **VM deletion with port cleanup** - Ports deleted on VM deletion - IP addresses released ## 12. Future Enhancements 1. **Hot-plug NIC**: Attach/detach network interfaces to running VMs 2. **Network migration**: Move VM between subnets 3. **Multi-NIC support**: Multiple network interfaces per VM 4. **Network QoS**: Bandwidth limits and priority 5. **Network monitoring**: Traffic statistics per port