From f30eb5582cfd676c0d5ed8b9ecd89d14d20c9834 Mon Sep 17 00:00:00 2001 From: Guvenc Gulce Date: Mon, 29 Sep 2025 18:21:19 +0200 Subject: [PATCH 1/4] Implement AttachNic in vm-service GRPC API Signed-off-by: Guvenc Gulce --- cli/src/vm_commands.rs | 88 +++++++++++++++++-- feos/services/vm-service/src/api.rs | 21 +++-- feos/services/vm-service/src/dispatcher.rs | 13 +-- .../vm-service/src/dispatcher_handlers.rs | 50 ++++++++++- feos/services/vm-service/src/lib.rs | 17 ++-- .../services/vm-service/src/vmm/ch_adapter.rs | 72 ++++++++++++++- feos/services/vm-service/src/vmm/mod.rs | 10 ++- feos/services/vm-service/src/worker.rs | 19 +++- proto/v1/vm.proto | 11 +++ 9 files changed, 262 insertions(+), 39 deletions(-) diff --git a/cli/src/vm_commands.rs b/cli/src/vm_commands.rs index 3a20379..f74e719 100644 --- a/cli/src/vm_commands.rs +++ b/cli/src/vm_commands.rs @@ -7,11 +7,11 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use crossterm::tty::IsTty; use feos_proto::vm_service::{ net_config, stream_vm_console_request as console_input, vm_service_client::VmServiceClient, - AttachConsoleMessage, AttachDiskRequest, ConsoleData, CpuConfig, CreateVmRequest, - DeleteVmRequest, DiskConfig, GetVmRequest, ListVmsRequest, MemoryConfig, NetConfig, - PauseVmRequest, PingVmRequest, RemoveDiskRequest, ResumeVmRequest, ShutdownVmRequest, - StartVmRequest, StreamVmConsoleRequest, StreamVmEventsRequest, VfioPciConfig, VmConfig, - VmState, VmStateChangedEvent, + AttachConsoleMessage, AttachDiskRequest, AttachNicRequest, ConsoleData, CpuConfig, + CreateVmRequest, DeleteVmRequest, DiskConfig, GetVmRequest, ListVmsRequest, MemoryConfig, + NetConfig, PauseVmRequest, PingVmRequest, RemoveDiskRequest, ResumeVmRequest, + ShutdownVmRequest, StartVmRequest, StreamVmConsoleRequest, TapConfig, VfioPciConfig, VmConfig, + VmState, VmStateChangedEvent, StreamVmEventsRequest }; use prost::Message; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -158,6 +158,27 @@ pub enum VmCommand { )] device_id: String, }, + /// Attach a network interface to a VM + AttachNic { + #[arg(long, required = true, help = "VM identifier")] + vm_id: String, + #[arg( + long, + help = "Name of the TAP device to attach", + conflicts_with = "pci_device" + )] + tap_name: Option, + #[arg( + long, + help = "PCI device BDF to passthrough for networking (e.g., 0000:03:00.0)", + conflicts_with = "tap_name" + )] + pci_device: Option, + #[arg(long, help = "MAC address for the new interface")] + mac_address: Option, + #[arg(long, help = "Custom device identifier for the new interface")] + device_id: Option, + }, } pub async fn handle_vm_command(args: VmArgs) -> Result<()> { @@ -218,6 +239,23 @@ pub async fn handle_vm_command(args: VmArgs) -> Result<()> { VmCommand::RemoveDisk { vm_id, device_id } => { remove_disk(&mut client, vm_id, device_id).await? } + VmCommand::AttachNic { + vm_id, + tap_name, + pci_device, + mac_address, + device_id, + } => { + attach_nic( + &mut client, + vm_id, + tap_name, + pci_device, + mac_address, + device_id, + ) + .await? + } } Ok(()) @@ -232,10 +270,10 @@ async fn create_and_start_vm( pci_devices: Vec, hugepages: bool, ) -> Result<()> { - println!("🚀 Starting create and start operation for VM with image: {image_ref}"); + println!("� Starting create and start operation for VM with image: {image_ref}"); // Step 1: Create the VM - println!("📋 Step 1: Creating VM..."); + println!("� Step 1: Creating VM..."); let net_configs = pci_devices .iter() @@ -700,3 +738,39 @@ async fn remove_disk( println!("Disk remove request sent for device {device_id} on VM {vm_id}"); Ok(()) } + +async fn attach_nic( + client: &mut VmServiceClient, + vm_id: String, + tap_name: Option, + pci_device: Option, + mac_address: Option, + device_id: Option, +) -> Result<()> { + let backend = if let Some(tap) = tap_name { + Some(net_config::Backend::Tap(TapConfig { tap_name: tap })) + } else if let Some(bdf) = pci_device { + Some(net_config::Backend::VfioPci(VfioPciConfig { bdf })) + } else { + anyhow::bail!("Either --tap-name or --pci-device must be specified."); + }; + + let nic = NetConfig { + device_id: device_id.unwrap_or_default(), + mac_address: mac_address.unwrap_or_default(), + backend, + }; + + let request = AttachNicRequest { + vm_id: vm_id.clone(), + nic: Some(nic), + }; + + let response = client.attach_nic(request).await?.into_inner(); + println!( + "NIC attach request sent for VM: {}. Assigned device_id: {}", + vm_id, response.device_id + ); + + Ok(()) +} diff --git a/feos/services/vm-service/src/api.rs b/feos/services/vm-service/src/api.rs index 73a41cd..14e3976 100644 --- a/feos/services/vm-service/src/api.rs +++ b/feos/services/vm-service/src/api.rs @@ -3,11 +3,11 @@ use crate::Command; use feos_proto::vm_service::{ - vm_service_server::VmService, AttachDiskRequest, AttachDiskResponse, CreateVmRequest, - CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, - ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, - RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, - ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, + vm_service_server::VmService, AttachDiskRequest, AttachDiskResponse, AttachNicRequest, + AttachNicResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, + GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest, + PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, + ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, }; use log::info; @@ -204,4 +204,15 @@ impl VmService for VmApiHandler { }) .await } + + async fn attach_nic( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received AttachNic request."); + dispatch_and_wait(&self.dispatcher_tx, |resp_tx| { + Command::AttachNic(request.into_inner(), resp_tx) + }) + .await + } } diff --git a/feos/services/vm-service/src/dispatcher.rs b/feos/services/vm-service/src/dispatcher.rs index 5cb5049..bca5d17 100644 --- a/feos/services/vm-service/src/dispatcher.rs +++ b/feos/services/vm-service/src/dispatcher.rs @@ -3,11 +3,11 @@ use crate::{ dispatcher_handlers::{ - handle_attach_disk_command, handle_create_vm_command, handle_delete_vm_command, - handle_get_vm_command, handle_list_vms_command, handle_pause_vm_command, - handle_remove_disk_command, handle_resume_vm_command, handle_shutdown_vm_command, - handle_start_vm_command, handle_stream_vm_console_command, handle_stream_vm_events_command, - perform_startup_sanity_check, + handle_attach_disk_command, handle_attach_nic_command, handle_create_vm_command, + handle_delete_vm_command, handle_get_vm_command, handle_list_vms_command, + handle_pause_vm_command, handle_remove_disk_command, handle_resume_vm_command, + handle_shutdown_vm_command, handle_start_vm_command, handle_stream_vm_console_command, + handle_stream_vm_events_command, perform_startup_sanity_check, }, error::VmServiceError, persistence::repository::VmRepository, @@ -109,6 +109,9 @@ impl VmServiceDispatcher { Command::RemoveDisk(req, responder) => { handle_remove_disk_command(&self.repository, req, responder, hypervisor).await; } + Command::AttachNic(req, responder) => { + handle_attach_nic_command(&self.repository, req, responder, hypervisor).await; + } } }, Some(event) = self.event_bus_rx_for_dispatcher.recv() => { diff --git a/feos/services/vm-service/src/dispatcher_handlers.rs b/feos/services/vm-service/src/dispatcher_handlers.rs index 9fed868..062d6dd 100644 --- a/feos/services/vm-service/src/dispatcher_handlers.rs +++ b/feos/services/vm-service/src/dispatcher_handlers.rs @@ -11,10 +11,10 @@ use feos_proto::{ image_service::{image_service_client::ImageServiceClient, PullImageRequest}, vm_service::{ stream_vm_console_request as console_input, AttachConsoleMessage, AttachDiskRequest, - AttachDiskResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, - GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, PauseVmResponse, - RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, - ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, + AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, CreateVmResponse, + DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, + PauseVmRequest, PauseVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, + ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, VmState, VmStateChangedEvent, }, @@ -680,6 +680,48 @@ pub(crate) async fn handle_remove_disk_command( tokio::spawn(worker::handle_remove_disk(req, responder, hypervisor)); } +pub(crate) async fn handle_attach_nic_command( + repository: &VmRepository, + req: AttachNicRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let (_vm_id, mut record) = match parse_vm_id_and_get_record(&req.vm_id, repository).await { + Ok(result) => result, + Err(e) => { + let _ = responder.send(Err(e)); + return; + } + }; + + let current_state = record.status.state; + if matches!(current_state, VmState::Creating | VmState::Crashed) { + let _ = responder.send(Err(VmServiceError::InvalidState(format!( + "Cannot attach NIC to VM in {current_state:?} state." + )))); + return; + } + + let new_nic_config = match req.nic.clone() { + Some(nic) => nic, + None => { + let _ = responder.send(Err(VmServiceError::InvalidArgument( + "NetConfig is required in AttachNicRequest".to_string(), + ))); + return; + } + }; + + record.config.net.push(new_nic_config); + + if let Err(e) = repository.save_vm(&record).await { + let _ = responder.send(Err(e.into())); + return; + } + + tokio::spawn(worker::handle_attach_nic(req, responder, hypervisor)); +} + pub(crate) async fn check_and_cleanup_vms( repository: &VmRepository, hypervisor: Arc, diff --git a/feos/services/vm-service/src/lib.rs b/feos/services/vm-service/src/lib.rs index 6d0160d..9f94a35 100644 --- a/feos/services/vm-service/src/lib.rs +++ b/feos/services/vm-service/src/lib.rs @@ -3,12 +3,12 @@ use crate::error::VmServiceError; use feos_proto::vm_service::{ - AttachDiskRequest, AttachDiskResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, - DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, - PauseVmResponse, PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, - ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, - StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, - VmEvent, VmInfo, + AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, + CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, + ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, + RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, + StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, }; use tokio::sync::{mpsc, oneshot}; use tonic::{Status, Streaming}; @@ -83,6 +83,10 @@ pub enum Command { RemoveDiskRequest, oneshot::Sender>, ), + AttachNic( + AttachNicRequest, + oneshot::Sender>, + ), } impl std::fmt::Debug for Command { @@ -103,6 +107,7 @@ impl std::fmt::Debug for Command { Command::ResumeVm(req, _) => f.debug_tuple("ResumeVm").field(req).finish(), Command::AttachDisk(req, _) => f.debug_tuple("AttachDisk").field(req).finish(), Command::RemoveDisk(req, _) => f.debug_tuple("RemoveDisk").field(req).finish(), + Command::AttachNic(req, _) => f.debug_tuple("AttachNic").field(req).finish(), } } } diff --git a/feos/services/vm-service/src/vmm/ch_adapter.rs b/feos/services/vm-service/src/vmm/ch_adapter.rs index b7b304b..a171e64 100644 --- a/feos/services/vm-service/src/vmm/ch_adapter.rs +++ b/feos/services/vm-service/src/vmm/ch_adapter.rs @@ -11,10 +11,11 @@ use cloud_hypervisor_client::{ }, }; use feos_proto::vm_service::{ - net_config, AttachDiskRequest, AttachDiskResponse, CreateVmRequest, DeleteVmRequest, - DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, - RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, - ShutdownVmResponse, StartVmRequest, StartVmResponse, VmConfig, VmInfo, VmState, + net_config, AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, + CreateVmRequest, DeleteVmRequest, DeleteVmResponse, GetVmRequest, PauseVmRequest, + PauseVmResponse, PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, + ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, + StartVmResponse, VmConfig, VmInfo, VmState, }; use hyper_util::client::legacy::Client; use hyperlocal::{UnixClientExt, UnixConnector, Uri as HyperlocalUri}; @@ -458,4 +459,67 @@ impl Hypervisor for CloudHypervisorAdapter { "RemoveDisk not implemented for CloudHypervisorAdapter".to_string(), )) } + + async fn attach_nic(&self, req: AttachNicRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + let nic = req + .nic + .ok_or_else(|| VmmError::InvalidConfig("NetConfig is required".to_string()))?; + + let pci_info = match nic.backend { + Some(net_config::Backend::Tap(tap)) => { + let id = if !nic.device_id.is_empty() { + Some(nic.device_id) + } else { + Some(tap.tap_name.clone()) + }; + + let mac = if nic.mac_address.is_empty() { + None + } else { + Some(nic.mac_address) + }; + + let ch_net_config = models::NetConfig { + tap: Some(tap.tap_name), + mac, + id, + ..Default::default() + }; + api_client + .vm_add_net_put(ch_net_config) + .await + .map_err(|e| VmmError::ApiOperationFailed(format!("vm.add-net failed: {e}")))? + } + Some(net_config::Backend::VfioPci(vfio_pci)) => { + let device_path = format!("/sys/bus/pci/devices/{}", vfio_pci.bdf); + let id = if !nic.device_id.is_empty() { + Some(nic.device_id) + } else { + Some(device_path.clone()) + }; + + let ch_device_config = models::DeviceConfig { + path: device_path, + id, + ..Default::default() + }; + api_client + .vm_add_device_put(ch_device_config) + .await + .map_err(|e| { + VmmError::ApiOperationFailed(format!("vm.add-device failed: {e}")) + })? + } + None => { + return Err(VmmError::InvalidConfig( + "NetConfig backend (tap or vfio_pci) is required".to_string(), + )); + } + }; + + Ok(AttachNicResponse { + device_id: pci_info.id, + }) + } } diff --git a/feos/services/vm-service/src/vmm/mod.rs b/feos/services/vm-service/src/vmm/mod.rs index d15ca2b..b08deb3 100644 --- a/feos/services/vm-service/src/vmm/mod.rs +++ b/feos/services/vm-service/src/vmm/mod.rs @@ -3,10 +3,11 @@ use crate::VmEventWrapper; use feos_proto::vm_service::{ - AttachDiskRequest, AttachDiskResponse, CreateVmRequest, DeleteVmRequest, DeleteVmResponse, - GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, - RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, - ShutdownVmResponse, StartVmRequest, StartVmResponse, VmEvent, VmInfo, VmStateChangedEvent, + AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, + DeleteVmRequest, DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, + PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, + ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, + VmEvent, VmInfo, VmStateChangedEvent, }; use prost::Message; use prost_types::Any; @@ -90,6 +91,7 @@ pub trait Hypervisor: Send + Sync { async fn resume_vm(&self, req: ResumeVmRequest) -> Result; async fn attach_disk(&self, req: AttachDiskRequest) -> Result; async fn remove_disk(&self, req: RemoveDiskRequest) -> Result; + async fn attach_nic(&self, req: AttachNicRequest) -> Result; } pub async fn broadcast_state_change_event( diff --git a/feos/services/vm-service/src/worker.rs b/feos/services/vm-service/src/worker.rs index 6d1f4e6..bd713e8 100644 --- a/feos/services/vm-service/src/worker.rs +++ b/feos/services/vm-service/src/worker.rs @@ -9,10 +9,10 @@ use feos_proto::{ image_service::{ImageState as OciImageState, WatchImageStatusRequest}, vm_service::{ stream_vm_console_request as console_input, AttachDiskRequest, AttachDiskResponse, - ConsoleData, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, - GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, - RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, - ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, + AttachNicRequest, AttachNicResponse, ConsoleData, CreateVmRequest, CreateVmResponse, + DeleteVmRequest, DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, + PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, + ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, VmState, VmStateChangedEvent, }, @@ -431,6 +431,17 @@ pub async fn handle_remove_disk( } } +pub async fn handle_attach_nic( + req: AttachNicRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let result = hypervisor.attach_nic(req).await; + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for AttachNic."); + } +} + async fn bridge_console_streams( socket_path: PathBuf, mut grpc_input: Streaming, diff --git a/proto/v1/vm.proto b/proto/v1/vm.proto index bcf40e0..9418c9f 100644 --- a/proto/v1/vm.proto +++ b/proto/v1/vm.proto @@ -41,6 +41,8 @@ service VMService { rpc AttachDisk(AttachDiskRequest) returns (AttachDiskResponse); // Hot-unplugs a disk from a running VM. rpc RemoveDisk(RemoveDiskRequest) returns (RemoveDiskResponse); + // Hot-plugs a new network interface to a running VM. + rpc AttachNic(AttachNicRequest) returns (AttachNicResponse); } // Request stream from client to server for StreamVmConsole @@ -231,6 +233,15 @@ message RemoveDiskRequest { string device_id = 2; } +message AttachNicRequest { + string vm_id = 1; + NetConfig nic = 2; +} + +message AttachNicResponse { + string device_id = 1; +} + message StartVmResponse {} message DeleteVmResponse {} From 6581483b0e893c1767355405a691936bd058dac2 Mon Sep 17 00:00:00 2001 From: Guvenc Gulce Date: Mon, 29 Sep 2025 20:19:45 +0200 Subject: [PATCH 2/4] Implement RemoveNic vm-service GRPC command Signed-off-by: Guvenc Gulce --- cli/src/vm_commands.rs | 30 +++++++++-- feos/services/vm-service/src/api.rs | 18 +++++-- feos/services/vm-service/src/dispatcher.rs | 10 ++-- .../vm-service/src/dispatcher_handlers.rs | 53 +++++++++++++++++-- feos/services/vm-service/src/lib.rs | 11 ++-- .../services/vm-service/src/vmm/ch_adapter.rs | 17 ++++-- feos/services/vm-service/src/vmm/mod.rs | 7 +-- feos/services/vm-service/src/worker.rs | 20 +++++-- proto/v1/vm.proto | 9 ++++ 9 files changed, 149 insertions(+), 26 deletions(-) diff --git a/cli/src/vm_commands.rs b/cli/src/vm_commands.rs index f74e719..dc8453d 100644 --- a/cli/src/vm_commands.rs +++ b/cli/src/vm_commands.rs @@ -9,9 +9,9 @@ use feos_proto::vm_service::{ net_config, stream_vm_console_request as console_input, vm_service_client::VmServiceClient, AttachConsoleMessage, AttachDiskRequest, AttachNicRequest, ConsoleData, CpuConfig, CreateVmRequest, DeleteVmRequest, DiskConfig, GetVmRequest, ListVmsRequest, MemoryConfig, - NetConfig, PauseVmRequest, PingVmRequest, RemoveDiskRequest, ResumeVmRequest, - ShutdownVmRequest, StartVmRequest, StreamVmConsoleRequest, TapConfig, VfioPciConfig, VmConfig, - VmState, VmStateChangedEvent, StreamVmEventsRequest + NetConfig, PauseVmRequest, PingVmRequest, RemoveDiskRequest, RemoveNicRequest, ResumeVmRequest, + ShutdownVmRequest, StartVmRequest, StreamVmConsoleRequest, StreamVmEventsRequest, TapConfig, + VfioPciConfig, VmConfig, VmState, VmStateChangedEvent, }; use prost::Message; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -179,6 +179,13 @@ pub enum VmCommand { #[arg(long, help = "Custom device identifier for the new interface")] device_id: Option, }, + /// Remove a network interface from a VM + RemoveNic { + #[arg(long, required = true, help = "VM identifier")] + vm_id: String, + #[arg(long, required = true, help = "Device identifier of the NIC to remove")] + device_id: String, + }, } pub async fn handle_vm_command(args: VmArgs) -> Result<()> { @@ -256,6 +263,9 @@ pub async fn handle_vm_command(args: VmArgs) -> Result<()> { ) .await? } + VmCommand::RemoveNic { vm_id, device_id } => { + remove_nic(&mut client, vm_id, device_id).await? + } } Ok(()) @@ -774,3 +784,17 @@ async fn attach_nic( Ok(()) } + +async fn remove_nic( + client: &mut VmServiceClient, + vm_id: String, + device_id: String, +) -> Result<()> { + let request = RemoveNicRequest { + vm_id: vm_id.clone(), + device_id: device_id.clone(), + }; + client.remove_nic(request).await?; + println!("NIC remove request sent for device {device_id} on VM {vm_id}"); + Ok(()) +} diff --git a/feos/services/vm-service/src/api.rs b/feos/services/vm-service/src/api.rs index 14e3976..984d1e0 100644 --- a/feos/services/vm-service/src/api.rs +++ b/feos/services/vm-service/src/api.rs @@ -6,9 +6,10 @@ use feos_proto::vm_service::{ vm_service_server::VmService, AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest, - PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, - ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, - StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, + PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, RemoveNicResponse, + ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, + StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, + VmEvent, VmInfo, }; use log::info; use std::pin::Pin; @@ -215,4 +216,15 @@ impl VmService for VmApiHandler { }) .await } + + async fn remove_nic( + &self, + request: Request, + ) -> Result, Status> { + info!("VmApi: Received RemoveNic request."); + dispatch_and_wait(&self.dispatcher_tx, |resp_tx| { + Command::RemoveNic(request.into_inner(), resp_tx) + }) + .await + } } diff --git a/feos/services/vm-service/src/dispatcher.rs b/feos/services/vm-service/src/dispatcher.rs index bca5d17..b4fab35 100644 --- a/feos/services/vm-service/src/dispatcher.rs +++ b/feos/services/vm-service/src/dispatcher.rs @@ -5,9 +5,10 @@ use crate::{ dispatcher_handlers::{ handle_attach_disk_command, handle_attach_nic_command, handle_create_vm_command, handle_delete_vm_command, handle_get_vm_command, handle_list_vms_command, - handle_pause_vm_command, handle_remove_disk_command, handle_resume_vm_command, - handle_shutdown_vm_command, handle_start_vm_command, handle_stream_vm_console_command, - handle_stream_vm_events_command, perform_startup_sanity_check, + handle_pause_vm_command, handle_remove_disk_command, handle_remove_nic_command, + handle_resume_vm_command, handle_shutdown_vm_command, handle_start_vm_command, + handle_stream_vm_console_command, handle_stream_vm_events_command, + perform_startup_sanity_check, }, error::VmServiceError, persistence::repository::VmRepository, @@ -112,6 +113,9 @@ impl VmServiceDispatcher { Command::AttachNic(req, responder) => { handle_attach_nic_command(&self.repository, req, responder, hypervisor).await; } + Command::RemoveNic(req, responder) => { + handle_remove_nic_command(&self.repository, req, responder, hypervisor).await; + } } }, Some(event) = self.event_bus_rx_for_dispatcher.recv() => { diff --git a/feos/services/vm-service/src/dispatcher_handlers.rs b/feos/services/vm-service/src/dispatcher_handlers.rs index 062d6dd..c628436 100644 --- a/feos/services/vm-service/src/dispatcher_handlers.rs +++ b/feos/services/vm-service/src/dispatcher_handlers.rs @@ -13,10 +13,11 @@ use feos_proto::{ stream_vm_console_request as console_input, AttachConsoleMessage, AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, - PauseVmRequest, PauseVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, - ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, - StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, - VmState, VmStateChangedEvent, + PauseVmRequest, PauseVmResponse, RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, + RemoveNicResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, + StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, VmState, + VmStateChangedEvent, }, }; use hyper_util::rt::TokioIo; @@ -722,6 +723,50 @@ pub(crate) async fn handle_attach_nic_command( tokio::spawn(worker::handle_attach_nic(req, responder, hypervisor)); } +pub(crate) async fn handle_remove_nic_command( + repository: &VmRepository, + req: RemoveNicRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let (_vm_id, mut record) = match parse_vm_id_and_get_record(&req.vm_id, repository).await { + Ok(result) => result, + Err(e) => { + let _ = responder.send(Err(e)); + return; + } + }; + + let current_state = record.status.state; + if matches!(current_state, VmState::Creating | VmState::Crashed) { + let _ = responder.send(Err(VmServiceError::InvalidState(format!( + "Cannot remove NIC from VM in {current_state:?} state." + )))); + return; + } + + let initial_len = record.config.net.len(); + record + .config + .net + .retain(|nic| nic.device_id != req.device_id); + + if record.config.net.len() == initial_len { + let _ = responder.send(Err(VmServiceError::InvalidArgument(format!( + "NIC with device_id '{}' not found in VM configuration.", + req.device_id + )))); + return; + } + + if let Err(e) = repository.save_vm(&record).await { + let _ = responder.send(Err(e.into())); + return; + } + + tokio::spawn(worker::handle_remove_nic(req, responder, hypervisor)); +} + pub(crate) async fn check_and_cleanup_vms( repository: &VmRepository, hypervisor: Arc, diff --git a/feos/services/vm-service/src/lib.rs b/feos/services/vm-service/src/lib.rs index 9f94a35..315e222 100644 --- a/feos/services/vm-service/src/lib.rs +++ b/feos/services/vm-service/src/lib.rs @@ -6,9 +6,9 @@ use feos_proto::vm_service::{ AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, - RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, - ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, - StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, + RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, RemoveNicResponse, ResumeVmRequest, + ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, + StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, }; use tokio::sync::{mpsc, oneshot}; use tonic::{Status, Streaming}; @@ -87,6 +87,10 @@ pub enum Command { AttachNicRequest, oneshot::Sender>, ), + RemoveNic( + RemoveNicRequest, + oneshot::Sender>, + ), } impl std::fmt::Debug for Command { @@ -108,6 +112,7 @@ impl std::fmt::Debug for Command { Command::AttachDisk(req, _) => f.debug_tuple("AttachDisk").field(req).finish(), Command::RemoveDisk(req, _) => f.debug_tuple("RemoveDisk").field(req).finish(), Command::AttachNic(req, _) => f.debug_tuple("AttachNic").field(req).finish(), + Command::RemoveNic(req, _) => f.debug_tuple("RemoveNic").field(req).finish(), } } } diff --git a/feos/services/vm-service/src/vmm/ch_adapter.rs b/feos/services/vm-service/src/vmm/ch_adapter.rs index a171e64..58805a4 100644 --- a/feos/services/vm-service/src/vmm/ch_adapter.rs +++ b/feos/services/vm-service/src/vmm/ch_adapter.rs @@ -14,8 +14,8 @@ use feos_proto::vm_service::{ net_config, AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, DeleteVmRequest, DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, - ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, - StartVmResponse, VmConfig, VmInfo, VmState, + RemoveNicRequest, RemoveNicResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, VmConfig, VmInfo, VmState, }; use hyper_util::client::legacy::Client; use hyperlocal::{UnixClientExt, UnixConnector, Uri as HyperlocalUri}; @@ -59,7 +59,6 @@ impl CloudHypervisorAdapter { Ok(DefaultApiClient::new(Arc::new(configuration))) } - async fn perform_vm_creation( &self, vm_id: &str, @@ -522,4 +521,16 @@ impl Hypervisor for CloudHypervisorAdapter { device_id: pci_info.id, }) } + + async fn remove_nic(&self, req: RemoveNicRequest) -> Result { + let api_client = self.get_ch_api_client(&req.vm_id)?; + let device_to_remove = models::VmRemoveDevice { + id: Some(req.device_id), + }; + api_client + .vm_remove_device_put(device_to_remove) + .await + .map_err(|e| VmmError::ApiOperationFailed(format!("vm.remove-device failed: {e}")))?; + Ok(RemoveNicResponse {}) + } } diff --git a/feos/services/vm-service/src/vmm/mod.rs b/feos/services/vm-service/src/vmm/mod.rs index b08deb3..5316c65 100644 --- a/feos/services/vm-service/src/vmm/mod.rs +++ b/feos/services/vm-service/src/vmm/mod.rs @@ -5,9 +5,9 @@ use crate::VmEventWrapper; use feos_proto::vm_service::{ AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, DeleteVmRequest, DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, - PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, - ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, - VmEvent, VmInfo, VmStateChangedEvent, + PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, + RemoveNicResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, + StartVmRequest, StartVmResponse, VmEvent, VmInfo, VmStateChangedEvent, }; use prost::Message; use prost_types::Any; @@ -92,6 +92,7 @@ pub trait Hypervisor: Send + Sync { async fn attach_disk(&self, req: AttachDiskRequest) -> Result; async fn remove_disk(&self, req: RemoveDiskRequest) -> Result; async fn attach_nic(&self, req: AttachNicRequest) -> Result; + async fn remove_nic(&self, req: RemoveNicRequest) -> Result; } pub async fn broadcast_state_change_event( diff --git a/feos/services/vm-service/src/worker.rs b/feos/services/vm-service/src/worker.rs index bd713e8..0b99d3f 100644 --- a/feos/services/vm-service/src/worker.rs +++ b/feos/services/vm-service/src/worker.rs @@ -11,10 +11,11 @@ use feos_proto::{ stream_vm_console_request as console_input, AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, ConsoleData, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, PauseVmRequest, PauseVmResponse, - PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, ResumeVmRequest, - ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, - StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, - VmState, VmStateChangedEvent, + PingVmRequest, PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, + RemoveNicResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, + ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, + StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, VmState, + VmStateChangedEvent, }, }; use log::{error, info, warn}; @@ -442,6 +443,17 @@ pub async fn handle_attach_nic( } } +pub async fn handle_remove_nic( + req: RemoveNicRequest, + responder: oneshot::Sender>, + hypervisor: Arc, +) { + let result = hypervisor.remove_nic(req).await; + if responder.send(result.map_err(Into::into)).is_err() { + error!("VmWorker: Failed to send response for RemoveNic."); + } +} + async fn bridge_console_streams( socket_path: PathBuf, mut grpc_input: Streaming, diff --git a/proto/v1/vm.proto b/proto/v1/vm.proto index 9418c9f..fc65680 100644 --- a/proto/v1/vm.proto +++ b/proto/v1/vm.proto @@ -43,6 +43,8 @@ service VMService { rpc RemoveDisk(RemoveDiskRequest) returns (RemoveDiskResponse); // Hot-plugs a new network interface to a running VM. rpc AttachNic(AttachNicRequest) returns (AttachNicResponse); + // Hot-unplugs a network interface from a running VM. + rpc RemoveNic(RemoveNicRequest) returns (RemoveNicResponse); } // Request stream from client to server for StreamVmConsole @@ -242,6 +244,13 @@ message AttachNicResponse { string device_id = 1; } +message RemoveNicRequest { + string vm_id = 1; + string device_id = 2; +} + +message RemoveNicResponse {} + message StartVmResponse {} message DeleteVmResponse {} From 9418454f25fedba6dceff0fae4bc1b9a0798435f Mon Sep 17 00:00:00 2001 From: Guvenc Gulce Date: Tue, 30 Sep 2025 10:42:45 +0200 Subject: [PATCH 3/4] Refactor AttachNic and RemoveNic calls Signed-off-by: Guvenc Gulce --- .../vm-service/src/dispatcher_handlers.rs | 46 ++++-- .../services/vm-service/src/vmm/ch_adapter.rs | 143 ++++++++++-------- 2 files changed, 114 insertions(+), 75 deletions(-) diff --git a/feos/services/vm-service/src/dispatcher_handlers.rs b/feos/services/vm-service/src/dispatcher_handlers.rs index c628436..f080265 100644 --- a/feos/services/vm-service/src/dispatcher_handlers.rs +++ b/feos/services/vm-service/src/dispatcher_handlers.rs @@ -10,14 +10,14 @@ use crate::{ use feos_proto::{ image_service::{image_service_client::ImageServiceClient, PullImageRequest}, vm_service::{ - stream_vm_console_request as console_input, AttachConsoleMessage, AttachDiskRequest, - AttachDiskResponse, AttachNicRequest, AttachNicResponse, CreateVmRequest, CreateVmResponse, - DeleteVmRequest, DeleteVmResponse, GetVmRequest, ListVmsRequest, ListVmsResponse, - PauseVmRequest, PauseVmResponse, RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, - RemoveNicResponse, ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, - ShutdownVmResponse, StartVmRequest, StartVmResponse, StreamVmConsoleRequest, - StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, VmState, - VmStateChangedEvent, + net_config, stream_vm_console_request as console_input, AttachConsoleMessage, + AttachDiskRequest, AttachDiskResponse, AttachNicRequest, AttachNicResponse, + CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse, GetVmRequest, + ListVmsRequest, ListVmsResponse, PauseVmRequest, PauseVmResponse, RemoveDiskRequest, + RemoveDiskResponse, RemoveNicRequest, RemoveNicResponse, ResumeVmRequest, ResumeVmResponse, + ShutdownVmRequest, ShutdownVmResponse, StartVmRequest, StartVmResponse, + StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo, + VmState, VmStateChangedEvent, }, }; use hyper_util::rt::TokioIo; @@ -36,6 +36,21 @@ use tonic::{ use tower::service_fn; use uuid::Uuid; +fn ensure_net_config_device_id(net_config: &mut feos_proto::vm_service::NetConfig) { + if net_config.device_id.is_empty() { + if let Some(backend) = &net_config.backend { + match backend { + net_config::Backend::Tap(tap) => { + net_config.device_id = tap.tap_name.clone(); + } + net_config::Backend::VfioPci(pci) => { + net_config.device_id = format!("/sys/bus/pci/devices/{}", pci.bdf); + } + } + } + } +} + pub(crate) async fn get_image_service_client( ) -> Result, TonicTransportError> { let socket_path = PathBuf::from(IMAGE_SERVICE_SOCKET); @@ -113,6 +128,15 @@ async fn prepare_vm_creation( let image_uuid = Uuid::parse_str(&image_uuid_str) .map_err(|e| VmServiceError::ImageService(format!("Failed to parse image UUID: {e}")))?; + let mut vm_config = req.config.clone().ok_or(VmServiceError::InvalidArgument( + "VmConfig is required in CreateVmRequest".to_string(), + ))?; + + vm_config + .net + .iter_mut() + .for_each(ensure_net_config_device_id); + let record = VmRecord { vm_id, image_uuid, @@ -121,7 +145,7 @@ async fn prepare_vm_creation( last_msg: "VM creation initiated".to_string(), process_id: None, }, - config: req.config.clone().unwrap(), + config: vm_config, }; repository.save_vm(&record).await?; @@ -703,7 +727,7 @@ pub(crate) async fn handle_attach_nic_command( return; } - let new_nic_config = match req.nic.clone() { + let mut new_nic_config = match req.nic.clone() { Some(nic) => nic, None => { let _ = responder.send(Err(VmServiceError::InvalidArgument( @@ -713,6 +737,8 @@ pub(crate) async fn handle_attach_nic_command( } }; + ensure_net_config_device_id(&mut new_nic_config); + record.config.net.push(new_nic_config); if let Err(e) = repository.save_vm(&record).await { diff --git a/feos/services/vm-service/src/vmm/ch_adapter.rs b/feos/services/vm-service/src/vmm/ch_adapter.rs index 58805a4..b24d572 100644 --- a/feos/services/vm-service/src/vmm/ch_adapter.rs +++ b/feos/services/vm-service/src/vmm/ch_adapter.rs @@ -30,6 +30,67 @@ use tokio::sync::{broadcast, mpsc}; use tokio::time::{self, timeout, Duration}; use uuid::Uuid; +#[derive(Debug)] +pub enum ChNetworkDevice { + Net(Box), + Device(models::DeviceConfig), +} + +impl ChNetworkDevice { + pub fn id(&self) -> Option { + match self { + ChNetworkDevice::Net(config) => config.id.clone(), + ChNetworkDevice::Device(config) => config.id.clone(), + } + } +} + +fn convert_net_config_to_ch( + nic: &feos_proto::vm_service::NetConfig, +) -> Result { + match &nic.backend { + Some(net_config::Backend::Tap(tap)) => { + let id = if !nic.device_id.is_empty() { + Some(nic.device_id.clone()) + } else { + Some(tap.tap_name.clone()) + }; + + let mac = if nic.mac_address.is_empty() { + None + } else { + Some(nic.mac_address.clone()) + }; + + let ch_net_config = models::NetConfig { + tap: Some(tap.tap_name.clone()), + mac, + id, + ..Default::default() + }; + Ok(ChNetworkDevice::Net(Box::new(ch_net_config))) + } + Some(net_config::Backend::VfioPci(vfio_pci)) => { + let device_path = format!("/sys/bus/pci/devices/{}", vfio_pci.bdf); + let id = if !nic.device_id.is_empty() { + Some(nic.device_id.clone()) + } else { + Some(device_path.clone()) + }; + + let ch_device_config = models::DeviceConfig { + path: device_path, + id, + ..Default::default() + }; + Ok(ChNetworkDevice::Device(ch_device_config)) + } + None => Err(VmmError::InvalidConfig( + "NetConfig backend (tap or vfio_pci) is required".to_string(), + )), + } +} + pub struct CloudHypervisorAdapter { ch_binary_path: PathBuf, } @@ -132,28 +193,12 @@ impl CloudHypervisorAdapter { let mut ch_device_configs: Vec = Vec::new(); for nc in config.net { - if let Some(backend) = nc.backend { - match backend { - net_config::Backend::VfioPci(vfio_pci) => { - let device_path = format!("/sys/bus/pci/devices/{}", vfio_pci.bdf); - ch_device_configs.push(models::DeviceConfig { - path: device_path, - ..Default::default() - }); - } - net_config::Backend::Tap(tap) => { - let mac = if nc.mac_address.is_empty() { - None - } else { - Some(nc.mac_address) - }; - - ch_net_configs.push(models::NetConfig { - tap: Some(tap.tap_name), - mac, - ..Default::default() - }); - } + match convert_net_config_to_ch(&nc)? { + ChNetworkDevice::Net(net_config) => { + ch_net_configs.push(*net_config); + } + ChNetworkDevice::Device(device_config) => { + ch_device_configs.push(device_config); } } } @@ -465,60 +510,28 @@ impl Hypervisor for CloudHypervisorAdapter { .nic .ok_or_else(|| VmmError::InvalidConfig("NetConfig is required".to_string()))?; - let pci_info = match nic.backend { - Some(net_config::Backend::Tap(tap)) => { - let id = if !nic.device_id.is_empty() { - Some(nic.device_id) - } else { - Some(tap.tap_name.clone()) - }; - - let mac = if nic.mac_address.is_empty() { - None - } else { - Some(nic.mac_address) - }; - - let ch_net_config = models::NetConfig { - tap: Some(tap.tap_name), - mac, - id, - ..Default::default() - }; + let ch_device = convert_net_config_to_ch(&nic)?; + let device_id = ch_device.id(); + + match ch_device { + ChNetworkDevice::Net(ch_net_config) => { api_client - .vm_add_net_put(ch_net_config) + .vm_add_net_put(*ch_net_config) .await - .map_err(|e| VmmError::ApiOperationFailed(format!("vm.add-net failed: {e}")))? + .map_err(|e| VmmError::ApiOperationFailed(format!("vm.add-net failed: {e}")))?; } - Some(net_config::Backend::VfioPci(vfio_pci)) => { - let device_path = format!("/sys/bus/pci/devices/{}", vfio_pci.bdf); - let id = if !nic.device_id.is_empty() { - Some(nic.device_id) - } else { - Some(device_path.clone()) - }; - - let ch_device_config = models::DeviceConfig { - path: device_path, - id, - ..Default::default() - }; + ChNetworkDevice::Device(ch_device_config) => { api_client .vm_add_device_put(ch_device_config) .await .map_err(|e| { VmmError::ApiOperationFailed(format!("vm.add-device failed: {e}")) - })? + })?; } - None => { - return Err(VmmError::InvalidConfig( - "NetConfig backend (tap or vfio_pci) is required".to_string(), - )); - } - }; + } Ok(AttachNicResponse { - device_id: pci_info.id, + device_id: device_id.unwrap_or_default(), }) } From a8fe10f4b3cebae1e15dc053b5d634b4c2a66f01 Mon Sep 17 00:00:00 2001 From: Guvenc Gulce Date: Tue, 30 Sep 2025 13:44:37 +0200 Subject: [PATCH 4/4] Add test cases for AttachNic and RemoveNic Signed-off-by: Guvenc Gulce --- feos/tests/integration_tests.rs | 71 ++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/feos/tests/integration_tests.rs b/feos/tests/integration_tests.rs index 8e05636..10d557e 100644 --- a/feos/tests/integration_tests.rs +++ b/feos/tests/integration_tests.rs @@ -12,11 +12,11 @@ use feos_proto::{ ListImagesRequest, PullImageRequest, WatchImageStatusRequest, }, vm_service::{ - stream_vm_console_request as console_input, vm_service_client::VmServiceClient, - AttachConsoleMessage, CpuConfig, CreateVmRequest, DeleteVmRequest, GetVmRequest, - MemoryConfig, PauseVmRequest, PingVmRequest, ResumeVmRequest, ShutdownVmRequest, - StartVmRequest, StreamVmConsoleRequest, StreamVmEventsRequest, VmConfig, VmEvent, VmState, - VmStateChangedEvent, + net_config, stream_vm_console_request as console_input, vm_service_client::VmServiceClient, + AttachConsoleMessage, AttachNicRequest, CpuConfig, CreateVmRequest, DeleteVmRequest, + GetVmRequest, MemoryConfig, NetConfig, PauseVmRequest, PingVmRequest, RemoveNicRequest, + ResumeVmRequest, ShutdownVmRequest, StartVmRequest, StreamVmConsoleRequest, + StreamVmEventsRequest, TapConfig, VmConfig, VmEvent, VmState, VmStateChangedEvent, }, }; use hyper_util::rt::TokioIo; @@ -405,6 +405,67 @@ async fn test_create_and_start_vm() -> Result<()> { info!("VMM Ping successful, PID: {}", ping_res.pid); guard.pid = Some(Pid::from_raw(ping_res.pid as i32)); + info!("Attaching NIC 'test-nic' to vm_id: {}", &vm_id); + let attach_nic_req = AttachNicRequest { + vm_id: vm_id.clone(), + nic: Some(NetConfig { + device_id: "test".to_string(), + backend: Some(net_config::Backend::Tap(TapConfig { + tap_name: "test".to_string(), + })), + ..Default::default() + }), + }; + let attach_res = vm_client.attach_nic(attach_nic_req).await?.into_inner(); + assert_eq!(attach_res.device_id, "test"); + info!( + "AttachNic call successful, device_id: {}", + attach_res.device_id + ); + + tokio::time::sleep(Duration::from_millis(100)).await; + + info!("Verifying NIC was attached with GetVm"); + let get_req_after_attach = GetVmRequest { + vm_id: vm_id.clone(), + }; + let info_res_after_attach = vm_client.get_vm(get_req_after_attach).await?.into_inner(); + let nic_found = info_res_after_attach + .config + .expect("VM should have a config") + .net + .iter() + .any(|nic| nic.device_id == "test"); + assert!(nic_found, "Attached NIC 'test' was not found in VM config"); + info!("Successfully verified NIC attachment."); + + info!("Removing NIC 'test' from vm_id: {}", &vm_id); + let remove_nic_req = RemoveNicRequest { + vm_id: vm_id.clone(), + device_id: "test".to_string(), + }; + vm_client.remove_nic(remove_nic_req).await?; + info!("RemoveNic call successful"); + + tokio::time::sleep(Duration::from_millis(100)).await; + + info!("Verifying NIC was removed with GetVm"); + let get_req_after_remove = GetVmRequest { + vm_id: vm_id.clone(), + }; + let info_res_after_remove = vm_client.get_vm(get_req_after_remove).await?.into_inner(); + let nic_still_present = info_res_after_remove + .config + .expect("VM should have a config") + .net + .iter() + .any(|nic| nic.device_id == "test"); + assert!( + !nic_still_present, + "Removed NIC 'test' was still found in VM config" + ); + info!("Successfully verified NIC removal."); + info!("Deleting VM: {}", &vm_id); let delete_req = DeleteVmRequest { vm_id: vm_id.clone(),