Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 105 additions & 7 deletions cli/src/vm_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, RemoveNicRequest, ResumeVmRequest,
ShutdownVmRequest, StartVmRequest, StreamVmConsoleRequest, StreamVmEventsRequest, TapConfig,
VfioPciConfig, VmConfig, VmState, VmStateChangedEvent,
};
use prost::Message;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
Expand Down Expand Up @@ -158,6 +158,34 @@ 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<String>,
#[arg(
long,
help = "PCI device BDF to passthrough for networking (e.g., 0000:03:00.0)",
conflicts_with = "tap_name"
)]
pci_device: Option<String>,
#[arg(long, help = "MAC address for the new interface")]
mac_address: Option<String>,
#[arg(long, help = "Custom device identifier for the new interface")]
device_id: Option<String>,
},
/// 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<()> {
Expand Down Expand Up @@ -218,6 +246,26 @@ 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?
}
VmCommand::RemoveNic { vm_id, device_id } => {
remove_nic(&mut client, vm_id, device_id).await?
}
}

Ok(())
Expand All @@ -232,10 +280,10 @@ async fn create_and_start_vm(
pci_devices: Vec<String>,
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()
Expand Down Expand Up @@ -700,3 +748,53 @@ 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<Channel>,
vm_id: String,
tap_name: Option<String>,
pci_device: Option<String>,
mac_address: Option<String>,
device_id: Option<String>,
) -> 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(())
}

async fn remove_nic(
client: &mut VmServiceClient<Channel>,
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(())
}
35 changes: 29 additions & 6 deletions feos/services/vm-service/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

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,
StreamVmConsoleResponse, StreamVmEventsRequest, VmEvent, VmInfo,
vm_service_server::VmService, AttachDiskRequest, AttachDiskResponse, AttachNicRequest,
AttachNicResponse, CreateVmRequest, CreateVmResponse, DeleteVmRequest, DeleteVmResponse,
GetVmRequest, ListVmsRequest, ListVmsResponse, PauseVmRequest, PauseVmResponse, PingVmRequest,
PingVmResponse, RemoveDiskRequest, RemoveDiskResponse, RemoveNicRequest, RemoveNicResponse,
ResumeVmRequest, ResumeVmResponse, ShutdownVmRequest, ShutdownVmResponse, StartVmRequest,
StartVmResponse, StreamVmConsoleRequest, StreamVmConsoleResponse, StreamVmEventsRequest,
VmEvent, VmInfo,
};
use log::info;
use std::pin::Pin;
Expand Down Expand Up @@ -204,4 +205,26 @@ impl VmService for VmApiHandler {
})
.await
}

async fn attach_nic(
&self,
request: Request<AttachNicRequest>,
) -> Result<Response<AttachNicResponse>, Status> {
info!("VmApi: Received AttachNic request.");
dispatch_and_wait(&self.dispatcher_tx, |resp_tx| {
Command::AttachNic(request.into_inner(), resp_tx)
})
.await
}

async fn remove_nic(
&self,
request: Request<RemoveNicRequest>,
) -> Result<Response<RemoveNicResponse>, Status> {
info!("VmApi: Received RemoveNic request.");
dispatch_and_wait(&self.dispatcher_tx, |resp_tx| {
Command::RemoveNic(request.into_inner(), resp_tx)
})
.await
}
}
15 changes: 11 additions & 4 deletions feos/services/vm-service/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +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,
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_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,
Expand Down Expand Up @@ -109,6 +110,12 @@ 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;
}
Command::RemoveNic(req, responder) => {
handle_remove_nic_command(&self.repository, req, responder, hypervisor).await;
}
}
},
Some(event) = self.event_bus_rx_for_dispatcher.recv() => {
Expand Down
123 changes: 118 additions & 5 deletions feos/services/vm-service/src/dispatcher_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ use crate::{
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,
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,
Expand All @@ -35,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<ImageServiceClient<Channel>, TonicTransportError> {
let socket_path = PathBuf::from(IMAGE_SERVICE_SOCKET);
Expand Down Expand Up @@ -112,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,
Expand All @@ -120,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?;
Expand Down Expand Up @@ -680,6 +705,94 @@ 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<Result<AttachNicResponse, VmServiceError>>,
hypervisor: Arc<dyn Hypervisor>,
) {
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 mut 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;
}
};

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 {
let _ = responder.send(Err(e.into()));
return;
}

tokio::spawn(worker::handle_attach_nic(req, responder, hypervisor));
}

pub(crate) async fn handle_remove_nic_command(
repository: &VmRepository,
req: RemoveNicRequest,
responder: oneshot::Sender<Result<RemoveNicResponse, VmServiceError>>,
hypervisor: Arc<dyn Hypervisor>,
) {
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<dyn Hypervisor>,
Expand Down
Loading
Loading