Skip to content

Commit 8630143

Browse files
authored
Merge pull request #593 from Dstack-TEE/worktree-vmm-discovery
feat(vmm): improve VMM discovery - XDG_RUNTIME_DIR, cross-user, subcommands
2 parents d46ba28 + 71e7a47 commit 8630143

4 files changed

Lines changed: 100 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vmm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ uuid = { workspace = true, features = ["v4"] }
2323
sha2.workspace = true
2424
hex.workspace = true
2525
fs-err.workspace = true
26+
nix = { workspace = true, features = ["user"] }
2627
dirs.workspace = true
2728
which.workspace = true
2829
clap = { workspace = true, features = ["derive", "string"] }

vmm/src/discovery.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ use std::path::{Path, PathBuf};
1313
use tracing::{info, warn};
1414
use uuid::Uuid;
1515

16-
/// Well-known directory where VMM instances register themselves.
17-
const DISCOVERY_DIR: &str = "/run/dstack-vmm";
16+
/// Returns the discovery directory path under $XDG_RUNTIME_DIR/dstack-vmm.
17+
/// Falls back to /run/user/<uid>/dstack-vmm if XDG_RUNTIME_DIR is not set.
18+
fn discovery_dir() -> PathBuf {
19+
if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
20+
return PathBuf::from(xdg).join("dstack-vmm");
21+
}
22+
let uid = nix::unistd::getuid();
23+
PathBuf::from(format!("/run/user/{uid}/dstack-vmm"))
24+
}
1825

1926
#[derive(Debug, Clone, Serialize, Deserialize)]
2027
pub struct VmmInstanceInfo {
@@ -57,8 +64,8 @@ impl DiscoveryRegistration {
5764
node_name: &str,
5865
version: &str,
5966
) -> Result<Self> {
60-
let dir = Path::new(DISCOVERY_DIR);
61-
fs_err::create_dir_all(dir).context("failed to create discovery directory")?;
67+
let dir = discovery_dir();
68+
fs_err::create_dir_all(&dir).context("failed to create discovery directory")?;
6269

6370
let id = Uuid::new_v4().to_string();
6471
let path = dir.join(format!("{id}.json"));
@@ -104,7 +111,7 @@ impl Drop for DiscoveryRegistration {
104111

105112
/// Clean up stale discovery files from dead processes.
106113
pub fn cleanup_stale_registrations() {
107-
let dir = Path::new(DISCOVERY_DIR);
114+
let dir = discovery_dir();
108115
let entries = match std::fs::read_dir(dir) {
109116
Ok(e) => e,
110117
Err(_) => return,

vmm/src/vmm-cli.py

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,29 @@
3434
DEFAULT_CONFIG_PATH = os.path.expanduser("~/.dstack-vmm/config.json")
3535
DEFAULT_KMS_WHITELIST_PATH = os.path.expanduser("~/.dstack-vmm/kms-whitelist.json")
3636

37-
# VMM discovery directory
38-
DISCOVERY_DIR = "/run/dstack-vmm"
37+
38+
# VMM discovery directories
39+
# Each user's instances are in $XDG_RUNTIME_DIR/dstack-vmm (typically /run/user/<uid>/dstack-vmm).
40+
# CLI scans all users' directories so operators can see every instance on the host.
41+
def _get_discovery_dirs() -> List[Tuple[str, Optional[str]]]:
42+
"""Return list of (discovery_dir, username) tuples."""
43+
import pwd
44+
45+
dirs = []
46+
run_user = "/run/user"
47+
if os.path.isdir(run_user):
48+
try:
49+
for uid_str in os.listdir(run_user):
50+
candidate = os.path.join(run_user, uid_str, "dstack-vmm")
51+
if os.path.isdir(candidate):
52+
try:
53+
username = pwd.getpwuid(int(uid_str)).pw_name
54+
except (KeyError, ValueError):
55+
username = f"uid:{uid_str}"
56+
dirs.append((candidate, username))
57+
except PermissionError:
58+
pass
59+
return dirs
3960

4061

4162
def load_config() -> Dict[str, Any]:
@@ -56,31 +77,29 @@ def load_config() -> Dict[str, Any]:
5677

5778

5879
def discover_vmm_instances() -> List[Dict[str, Any]]:
59-
"""Discover all running VMM instances from the discovery directory.
80+
"""Discover all running VMM instances across all users on the host.
6081
6182
Returns:
6283
List of VMM instance info dicts, sorted by started_at.
6384
6485
"""
6586
instances = []
66-
if not os.path.isdir(DISCOVERY_DIR):
67-
return instances
68-
69-
for fname in os.listdir(DISCOVERY_DIR):
70-
if not fname.endswith(".json"):
71-
continue
72-
fpath = os.path.join(DISCOVERY_DIR, fname)
73-
try:
74-
with open(fpath, "r") as f:
75-
info = json.load(f)
76-
# Check if process is still alive
77-
pid = info.get("pid")
78-
if pid and not os.path.exists(f"/proc/{pid}"):
79-
# Stale file, skip
87+
for discovery_dir, username in _get_discovery_dirs():
88+
for fname in os.listdir(discovery_dir):
89+
if not fname.endswith(".json"):
90+
continue
91+
fpath = os.path.join(discovery_dir, fname)
92+
try:
93+
with open(fpath, "r") as f:
94+
info = json.load(f)
95+
pid = info.get("pid")
96+
if pid and not os.path.exists(f"/proc/{pid}"):
97+
continue
98+
if username:
99+
info["user"] = username
100+
instances.append(info)
101+
except (json.JSONDecodeError, FileNotFoundError, PermissionError):
80102
continue
81-
instances.append(info)
82-
except (json.JSONDecodeError, FileNotFoundError, PermissionError):
83-
continue
84103

85104
instances.sort(key=lambda x: x.get("started_at", 0))
86105
return instances
@@ -163,7 +182,9 @@ def cmd_ls_vmm(args):
163182

164183
if not instances:
165184
print("No running VMM instances found.")
166-
print(f" (discovery directory: {DISCOVERY_DIR})")
185+
print(
186+
f" (scanned: {', '.join(d for d, _ in _get_discovery_dirs()) or '/run/user/*/dstack-vmm'})"
187+
)
167188
return
168189

169190
if getattr(args, "json", False):
@@ -172,18 +193,19 @@ def cmd_ls_vmm(args):
172193

173194
# Table output
174195

175-
fmt = " {active} {id:<12s} {pid:<8s} {node:<12s} {address:<24s} {workdir}"
196+
fmt = " {active} {id:<12s} {pid:<8s} {user:<10s} {node:<12s} {address:<24s} {workdir}"
176197
print(
177198
fmt.format(
178199
active="",
179200
id="ID",
180201
pid="PID",
202+
user="USER",
181203
node="NAME",
182204
address="ADDRESS",
183205
workdir="WORKING DIR",
184206
)
185207
)
186-
print(" " + "-" * 90)
208+
print(" " + "-" * 100)
187209

188210
for inst in instances:
189211
short_id = inst["id"][:8]
@@ -196,6 +218,7 @@ def cmd_ls_vmm(args):
196218
active=is_active,
197219
id=short_id,
198220
pid=str(inst.get("pid", "?")),
221+
user=inst.get("user", "?")[:10],
199222
node=node_name[:12],
200223
address=address[:24],
201224
workdir=inst.get("working_dir", "?"),
@@ -1517,21 +1540,47 @@ def main():
15171540

15181541
subparsers = parser.add_subparsers(dest="command", help="Commands")
15191542

1520-
# VMM discovery commands
1521-
ls_vmm_parser = subparsers.add_parser(
1522-
"ls-vmm", help="List all running VMM instances on this host"
1543+
# VMM discovery commands (vmm ls / vmm switch)
1544+
vmm_parser = subparsers.add_parser(
1545+
"vmm", help="VMM instance management (ls, switch)"
15231546
)
1524-
ls_vmm_parser.add_argument(
1547+
vmm_subparsers = vmm_parser.add_subparsers(
1548+
dest="vmm_command", help="VMM sub-commands"
1549+
)
1550+
1551+
vmm_ls_parser = vmm_subparsers.add_parser(
1552+
"ls", help="List all running VMM instances on this host"
1553+
)
1554+
vmm_ls_parser.add_argument(
15251555
"--json", action="store_true", help="Output in JSON format"
15261556
)
15271557

1528-
switch_vmm_parser = subparsers.add_parser(
1529-
"switch-vmm", help="Switch active VMM instance"
1558+
vmm_switch_parser = vmm_subparsers.add_parser(
1559+
"switch", help="Switch active VMM instance"
15301560
)
1531-
switch_vmm_parser.add_argument(
1561+
vmm_switch_parser.add_argument(
15321562
"vmm_id", help="VMM instance ID (prefix match supported)"
15331563
)
15341564

1565+
# Register nested subcommands for top-level help display
1566+
_nested_commands = {
1567+
"vmm ls": "List all running VMM instances on this host",
1568+
"vmm switch": "Switch active VMM instance",
1569+
}
1570+
1571+
# Patch parser's format_help to show nested subcommands
1572+
_orig_format_help = parser.format_help
1573+
1574+
def _patched_format_help():
1575+
text = _orig_format_help()
1576+
# Append nested subcommands after the subparser listing
1577+
extra = "\nnested commands:\n"
1578+
for cmd, desc in _nested_commands.items():
1579+
extra += f" {cmd:<24s}{desc}\n"
1580+
return text + extra
1581+
1582+
parser.format_help = _patched_format_help
1583+
15351584
# List command
15361585
lsvm_parser = subparsers.add_parser("lsvm", help="List VMs")
15371586
lsvm_parser.add_argument(
@@ -1879,11 +1928,13 @@ def main():
18791928
args = parser.parse_args()
18801929

18811930
# Handle discovery commands before creating CLI (they don't need a connection)
1882-
if args.command == "ls-vmm":
1883-
cmd_ls_vmm(args)
1884-
return
1885-
elif args.command == "switch-vmm":
1886-
cmd_switch_vmm(args)
1931+
if args.command == "vmm":
1932+
if args.vmm_command == "ls":
1933+
cmd_ls_vmm(args)
1934+
elif args.vmm_command == "switch":
1935+
cmd_switch_vmm(args)
1936+
else:
1937+
vmm_parser.print_help()
18871938
return
18881939

18891940
# Resolve the URL with auto-discovery

0 commit comments

Comments
 (0)