photoncloud-monorepo/docs/por/T015-overlay-networking/plasmavmc-integration.md
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

18 KiB
Raw Blame History

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は以下のフィールドを持っています:

pub struct NetworkSpec {
    pub id: String,
    pub network_id: String,        // Currently: "default" or user-specified
    pub mac_address: Option<String>,
    pub ip_address: Option<String>,
    pub model: NicModel,
    pub security_groups: Vec<String>,
}

3.2 Extended NetworkSpec

network_idフィールドを拡張して、subnet_idを明示的に指定できるようにします

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<String>,
    
    /// 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<String>,
    
    /// IP address (DHCP if None, static if Some)
    pub ip_address: Option<String>,
    
    /// 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<String>,
}

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

impl VmService {
    async fn create_vm(&self, request: CreateVmRequest) -> Result<VirtualMachine> {
        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<String> {
        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:

pub trait NetworkServiceClient: Send + Sync {
    /// Create a port for VM network interface
    async fn create_port(&self, req: CreatePortRequest) -> Result<Port>;
    
    /// Get port details
    async fn get_port(&self, org_id: &str, project_id: &str, port_id: &str) -> Result<Option<Port>>;
    
    /// Get TAP interface name for port
    async fn get_port_tap_name(&self, port_id: &str) -> Result<String>;
    
    /// 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 <logical_switch> <port_name>
   ├── Set port options (MAC, IP if static)
   │   └── ovn-nbctl lsp-set-addresses <port> <mac> <ip>
   ├── Apply security groups (OVN ACLs)
   │   └── ovn-nbctl acl-add <switch> <direction> <priority> <match> <action>
   ├── 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 <bridge> <tap> -- set Interface <tap> type=internal
   └── Attach TAP to VM NIC

5.2 Default Subnet Creation

プロジェクトのデフォルトサブネットが存在しない場合、自動作成:

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)

// 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

// 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

// 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

fn resolve_security_groups(
    org_id: &str,
    project_id: &str,
    security_groups: &[String],
) -> Vec<String> {
    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

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

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

// 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

// 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

[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