Skip to content

Commit e4c0ed2

Browse files
authored
Merge pull request #584 from Dstack-TEE/worktree-vmm-discovery
feat(vmm): local VMM instance discovery
2 parents d147c75 + 428738f commit e4c0ed2

3 files changed

Lines changed: 359 additions & 2 deletions

File tree

vmm/src/discovery.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
//! VMM instance discovery via registration files.
6+
//!
7+
//! On startup each `dstack-vmm` process writes a JSON file to a well-known
8+
//! directory so that CLI tools can discover all running instances on the host.
9+
10+
use anyhow::{Context, Result};
11+
use serde::{Deserialize, Serialize};
12+
use std::path::{Path, PathBuf};
13+
use tracing::{info, warn};
14+
use uuid::Uuid;
15+
16+
/// Well-known directory where VMM instances register themselves.
17+
const DISCOVERY_DIR: &str = "/run/dstack-vmm";
18+
19+
#[derive(Debug, Clone, Serialize, Deserialize)]
20+
pub struct VmmInstanceInfo {
21+
/// Unique identifier for this VMM instance (random UUID).
22+
pub id: String,
23+
/// Process ID.
24+
pub pid: u32,
25+
/// Address the external API listens on (e.g. "unix:./vmm.sock" or "0.0.0.0:9080").
26+
pub address: String,
27+
/// Working directory of the VMM process.
28+
pub working_dir: String,
29+
/// Path to the configuration file (if provided via -c).
30+
pub config_file: Option<String>,
31+
/// Path where VM images are stored.
32+
pub image_path: String,
33+
/// Path where VM runtime data is stored.
34+
pub run_path: String,
35+
/// Node name from configuration.
36+
pub node_name: String,
37+
/// VMM version string.
38+
pub version: String,
39+
/// Unix timestamp (seconds) when the instance started.
40+
pub started_at: u64,
41+
}
42+
43+
/// Handle that manages the lifecycle of a discovery registration file.
44+
/// The file is removed when this handle is dropped.
45+
pub struct DiscoveryRegistration {
46+
path: PathBuf,
47+
}
48+
49+
impl DiscoveryRegistration {
50+
/// Register a new VMM instance. Creates the discovery directory if needed
51+
/// and writes the instance info JSON file.
52+
pub fn register(
53+
listen_address: &str,
54+
config_file: Option<&str>,
55+
image_path: &Path,
56+
run_path: &Path,
57+
node_name: &str,
58+
version: &str,
59+
) -> Result<Self> {
60+
let dir = Path::new(DISCOVERY_DIR);
61+
fs_err::create_dir_all(dir).context("failed to create discovery directory")?;
62+
63+
let id = Uuid::new_v4().to_string();
64+
let path = dir.join(format!("{id}.json"));
65+
66+
let cwd = std::env::current_dir().context("failed to get current directory")?;
67+
68+
let info = VmmInstanceInfo {
69+
id: id.clone(),
70+
pid: std::process::id(),
71+
address: listen_address.to_string(),
72+
working_dir: cwd.to_string_lossy().to_string(),
73+
config_file: config_file.map(|s| s.to_string()),
74+
image_path: image_path.to_string_lossy().to_string(),
75+
run_path: run_path.to_string_lossy().to_string(),
76+
node_name: node_name.to_string(),
77+
version: version.to_string(),
78+
started_at: std::time::SystemTime::now()
79+
.duration_since(std::time::UNIX_EPOCH)
80+
.unwrap_or_default()
81+
.as_secs(),
82+
};
83+
84+
let json =
85+
serde_json::to_string_pretty(&info).context("failed to serialize instance info")?;
86+
fs_err::write(&path, &json).context("failed to write discovery file")?;
87+
88+
info!("registered VMM instance {id} at {}", path.display());
89+
Ok(Self { path })
90+
}
91+
}
92+
93+
impl Drop for DiscoveryRegistration {
94+
fn drop(&mut self) {
95+
match std::fs::remove_file(&self.path) {
96+
Ok(()) => info!("unregistered VMM instance at {}", self.path.display()),
97+
Err(e) => warn!(
98+
"failed to remove discovery file {}: {e}",
99+
self.path.display()
100+
),
101+
}
102+
}
103+
}
104+
105+
/// Clean up stale discovery files from dead processes.
106+
pub fn cleanup_stale_registrations() {
107+
let dir = Path::new(DISCOVERY_DIR);
108+
let entries = match std::fs::read_dir(dir) {
109+
Ok(e) => e,
110+
Err(_) => return,
111+
};
112+
113+
for entry in entries.flatten() {
114+
let path = entry.path();
115+
if path.extension().and_then(|e| e.to_str()) != Some("json") {
116+
continue;
117+
}
118+
let content = match std::fs::read_to_string(&path) {
119+
Ok(c) => c,
120+
Err(_) => continue,
121+
};
122+
let info: VmmInstanceInfo = match serde_json::from_str(&content) {
123+
Ok(i) => i,
124+
Err(_) => continue,
125+
};
126+
// Check if the process is still alive
127+
let alive = Path::new(&format!("/proc/{}", info.pid)).exists();
128+
if !alive {
129+
info!(
130+
"removing stale discovery file for pid {} at {}",
131+
info.pid,
132+
path.display()
133+
);
134+
let _ = std::fs::remove_file(&path);
135+
}
136+
}
137+
}

vmm/src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use tracing::{error, info};
2323

2424
mod app;
2525
mod config;
26+
mod discovery;
2627
mod guest_api_service;
2728
mod host_api_service;
2829
mod main_routes;
@@ -179,6 +180,31 @@ async fn main() -> Result<()> {
179180
}
180181
}
181182

183+
// Register this VMM instance for local discovery
184+
discovery::cleanup_stale_registrations();
185+
let listen_address = {
186+
// Use Rocket's Endpoint type to parse the address exactly as Rocket would,
187+
// then override the port with the figment's port value (matching Rocket's behavior).
188+
let endpoint: rocket::listener::Endpoint =
189+
figment.extract_inner("address").unwrap_or_default();
190+
match endpoint.tcp() {
191+
Some(addr) => {
192+
let port: u16 = figment.extract_inner("port").unwrap_or(addr.port());
193+
format!("{}:{port}", addr.ip())
194+
}
195+
None => endpoint.to_string(),
196+
}
197+
};
198+
let _discovery_reg = discovery::DiscoveryRegistration::register(
199+
&listen_address,
200+
args.config.as_deref(),
201+
&config.image_path,
202+
&config.run_path,
203+
&config.node_name,
204+
&app_version(),
205+
)
206+
.context("failed to register VMM instance for discovery")?;
207+
182208
let api_auth = ApiToken::new(config.auth.tokens.clone(), config.auth.enabled);
183209
let supervisor = {
184210
let cfg = &config.supervisor;

0 commit comments

Comments
 (0)