Skip to content

Commit 5c26751

Browse files
authored
Merge pull request #397 from Dstack-TEE/rpc-reloadvms
vmm: Add ReloadVms API to sync VM directory
2 parents b5adc45 + 6560cb0 commit 5c26751

8 files changed

Lines changed: 812 additions & 6 deletions

File tree

vmm/rpc/proto/vmm_rpc.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ message ListGpusResponse {
231231
bool allow_attach_all = 2;
232232
}
233233

234+
message ReloadVmsResponse {
235+
uint32 loaded = 1; // Number of VMs that were loaded
236+
uint32 updated = 2; // Number of VMs that were updated
237+
uint32 removed = 3; // Number of VMs that were removed
238+
}
239+
234240
message GpuInfo {
235241
string slot = 1;
236242
string product_id = 2;
@@ -276,4 +282,7 @@ service Vmm {
276282

277283
// List GPUs
278284
rpc ListGpus(google.protobuf.Empty) returns (ListGpusResponse);
285+
286+
// Reload VMs directory and sync with memory state
287+
rpc ReloadVms(google.protobuf.Empty) returns (ReloadVmsResponse);
279288
}

vmm/src/app.rs

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ use dstack_kms_rpc::kms_client::KmsClient;
1010
use dstack_types::shared_filenames::{
1111
APP_COMPOSE, ENCRYPTED_ENV, INSTANCE_INFO, SYS_CONFIG, USER_CONFIG,
1212
};
13-
use dstack_vmm_rpc::{self as pb, GpuInfo, StatusRequest, StatusResponse, VmConfiguration};
13+
use dstack_vmm_rpc::{
14+
self as pb, GpuInfo, ReloadVmsResponse, StatusRequest, StatusResponse, VmConfiguration,
15+
};
1416
use fs_err as fs;
1517
use guest_api::client::DefaultClient as GuestClient;
1618
use id_pool::IdPool;
1719
use ra_rpc::client::RaClient;
1820
use serde::{Deserialize, Serialize};
1921
use serde_json::json;
20-
use std::collections::{BTreeSet, HashMap, VecDeque};
22+
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
2123
use std::net::IpAddr;
2224
use std::path::{Path, PathBuf};
2325
use std::sync::{Arc, Mutex, MutexGuard};
2426
use std::time::SystemTime;
2527
use supervisor_client::SupervisorClient;
26-
use tracing::{error, info};
28+
use tracing::{error, info, warn};
2729

2830
pub use image::{Image, ImageInfo};
2931
pub use qemu::{VmConfig, VmWorkDir};
@@ -326,6 +328,199 @@ impl App {
326328
Ok(())
327329
}
328330

331+
/// Reload VMs directory and sync with memory state while preserving statistics
332+
pub async fn reload_vms_sync(&self) -> Result<ReloadVmsResponse> {
333+
let vm_path = self.vm_dir();
334+
let mut loaded = 0u32;
335+
let mut updated = 0u32;
336+
let mut removed = 0u32;
337+
338+
// Get running VMs to preserve CIDs and process info
339+
let running_vms = self.supervisor.list().await.context("Failed to list VMs")?;
340+
let running_vms_map: HashMap<String, _> = running_vms
341+
.into_iter()
342+
.map(|p| (p.config.id.clone(), p))
343+
.collect();
344+
let occupied_cids = running_vms_map
345+
.iter()
346+
.filter(|(_, p)| {
347+
serde_json::from_str::<ProcessAnnotation>(&p.config.note)
348+
.unwrap_or_default()
349+
.is_cvm()
350+
})
351+
.map(|(id, p)| (id.clone(), p.config.cid.unwrap()))
352+
.collect::<HashMap<_, _>>();
353+
354+
// Update CID pool with running VMs
355+
{
356+
let mut state = self.lock();
357+
// First clear the pool and re-occupy running VM CIDs
358+
state.cid_pool.clear();
359+
for cid in occupied_cids.values() {
360+
state.cid_pool.occupy(*cid)?;
361+
}
362+
}
363+
364+
// Get VM IDs from filesystem
365+
let mut fs_vm_ids = HashSet::new();
366+
if vm_path.exists() {
367+
for entry in fs::read_dir(&vm_path).context("Failed to read VM directory")? {
368+
let entry = entry.context("Failed to read directory entry")?;
369+
let vm_dir_path = entry.path();
370+
if vm_dir_path.is_dir() {
371+
// Try to get VM ID from directory name or manifest
372+
if let Some(vm_id) = vm_dir_path.file_name().and_then(|n| n.to_str()) {
373+
fs_vm_ids.insert(vm_id.to_string());
374+
}
375+
}
376+
}
377+
}
378+
379+
// Get VM IDs currently in memory and their CIDs
380+
let (memory_vm_ids, existing_cids): (HashSet<String>, HashSet<u32>) = {
381+
let state = self.lock();
382+
(
383+
state.vms.keys().cloned().collect(),
384+
state.vms.values().map(|vm| vm.config.cid).collect(),
385+
)
386+
};
387+
388+
// Remove VMs that no longer exist in filesystem
389+
let to_remove: Vec<String> = memory_vm_ids.difference(&fs_vm_ids).cloned().collect();
390+
if !to_remove.is_empty() {
391+
for vm_id in &to_remove {
392+
// Stop the VM process first if it's running
393+
if running_vms_map.contains_key(vm_id) {
394+
if let Err(err) = self.supervisor.stop(vm_id).await {
395+
warn!("Failed to stop VM process {vm_id}: {err:?}");
396+
}
397+
}
398+
399+
// Remove from memory and free CID
400+
let mut state = self.lock();
401+
if let Some(vm) = state.vms.remove(vm_id) {
402+
state.cid_pool.free(vm.config.cid);
403+
removed += 1;
404+
info!("Removed VM {vm_id} from memory (directory no longer exists)");
405+
}
406+
}
407+
}
408+
409+
// Load or update VMs from filesystem
410+
if vm_path.exists() {
411+
for entry in fs::read_dir(vm_path).context("Failed to read VM directory")? {
412+
let entry = entry.context("Failed to read directory entry")?;
413+
let vm_path = entry.path();
414+
if vm_path.is_dir() {
415+
match self.load_or_update_vm(&vm_path, &occupied_cids, true).await {
416+
Ok(is_new) => {
417+
if is_new {
418+
loaded += 1;
419+
} else {
420+
updated += 1;
421+
}
422+
}
423+
Err(err) => {
424+
error!("Failed to load or update VM: {err:?}");
425+
}
426+
}
427+
}
428+
}
429+
}
430+
431+
// Clean up any orphaned CIDs that aren't being used
432+
{
433+
let mut state = self.lock();
434+
let used_cids: HashSet<u32> = state.vms.values().map(|vm| vm.config.cid).collect();
435+
let orphaned_cids: Vec<u32> = existing_cids.difference(&used_cids).cloned().collect();
436+
for cid in orphaned_cids {
437+
state.cid_pool.free(cid);
438+
info!("Released orphaned CID {cid}");
439+
}
440+
}
441+
442+
Ok(ReloadVmsResponse {
443+
loaded,
444+
updated,
445+
removed,
446+
})
447+
}
448+
449+
/// Load or update a VM, preserving existing statistics
450+
async fn load_or_update_vm(
451+
&self,
452+
work_dir: impl AsRef<Path>,
453+
cids_assigned: &HashMap<String, u32>,
454+
auto_start: bool,
455+
) -> Result<bool> {
456+
let vm_work_dir = VmWorkDir::new(work_dir.as_ref());
457+
let manifest = vm_work_dir.manifest().context("Failed to read manifest")?;
458+
if manifest.image.len() > 64
459+
|| manifest.image.contains("..")
460+
|| !manifest
461+
.image
462+
.chars()
463+
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
464+
{
465+
bail!("Invalid image name");
466+
}
467+
let image_path = self.config.image_path.join(&manifest.image);
468+
let image = Image::load(&image_path).context("Failed to load image")?;
469+
let vm_id = manifest.id.clone();
470+
let app_compose = vm_work_dir
471+
.app_compose()
472+
.context("Failed to read compose file")?;
473+
474+
let mut is_new = false;
475+
{
476+
let mut states = self.lock();
477+
478+
// For existing VMs, keep their current CID
479+
// For new VMs, try to use assigned CID or allocate a new one
480+
let cid = if let Some(existing_vm) = states.get(&vm_id) {
481+
// Keep existing CID
482+
existing_vm.config.cid
483+
} else if let Some(assigned_cid) = cids_assigned.get(&vm_id) {
484+
// Use assigned CID from running processes
485+
*assigned_cid
486+
} else {
487+
// Allocate new CID only for truly new VMs
488+
states.cid_pool.allocate().context("CID pool exhausted")?
489+
};
490+
491+
let vm_config = VmConfig {
492+
manifest,
493+
image,
494+
cid,
495+
workdir: vm_work_dir.path().to_path_buf(),
496+
gateway_enabled: app_compose.gateway_enabled(),
497+
};
498+
499+
match states.get_mut(&vm_id) {
500+
Some(vm) => {
501+
// Update existing VM but preserve statistics and CID
502+
let old_state = vm.state.clone();
503+
vm.config = vm_config.into();
504+
vm.state = old_state; // Preserve the existing state with statistics
505+
}
506+
None => {
507+
// This is a new VM, need to occupy its CID if it wasn't allocated
508+
if !cids_assigned.contains_key(&vm_id) {
509+
states.cid_pool.occupy(cid)?;
510+
}
511+
states.add(VmState::new(vm_config));
512+
is_new = true;
513+
}
514+
}
515+
};
516+
517+
if auto_start && vm_work_dir.started().unwrap_or_default() {
518+
self.start_vm(&vm_id).await?;
519+
}
520+
521+
Ok(is_new)
522+
}
523+
329524
pub async fn list_vms(&self, request: StatusRequest) -> Result<StatusResponse> {
330525
let vms = self
331526
.supervisor

vmm/src/app/id_pool.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,8 @@ impl<T: Number> IdPool<T> {
6262
pub fn free(&mut self, id: T) {
6363
self.allocated.remove(&id);
6464
}
65+
66+
pub fn clear(&mut self) {
67+
self.allocated.clear();
68+
}
6569
}

0 commit comments

Comments
 (0)