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
5 changes: 5 additions & 0 deletions dstack/dstack-mr/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ struct MachineConfig {
#[arg(long, default_value = "0")]
num_nvswitches: u32,

/// Number of virtio-net NICs
#[arg(long, default_value = "1")]
num_nics: u32,

/// Disable hotplug
#[arg(long, default_value = "false")]
hotplug_off: Bool,
Expand Down Expand Up @@ -133,6 +137,7 @@ fn main() -> Result<()> {
.hugepages(config.hugepages)
.num_gpus(config.num_gpus)
.num_nvswitches(config.num_nvswitches)
.num_nics(config.num_nics)
.hotplug_off(config.hotplug_off)
.root_verity(config.root_verity)
.maybe_qemu_version(config.qemu_version.clone())
Expand Down
16 changes: 12 additions & 4 deletions dstack/dstack-mr/src/acpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,18 @@ impl Machine<'_> {
&format!("file={dummy_disk},if=none,id=hd1,format=raw,readonly=on"),
"-device",
"virtio-blk-pci,drive=hd1",
"-netdev",
"user,id=net0",
"-device",
"virtio-net-pci,netdev=net0",
]);

// One virtio-net-pci per NIC. Emitted at the same position and, for the
// default single-NIC case, with the exact same args as the previous
// hardcoded layout so RTMR0 stays byte-for-byte unchanged.
for i in 0..self.num_nics {
cmd.arg("-netdev").arg(format!("user,id=net{i}"));
cmd.arg("-device")
.arg(format!("virtio-net-pci,netdev=net{i}"));
}

cmd.args([
"-object",
"tdx-guest,id=tdx",
"-device",
Expand Down
5 changes: 5 additions & 0 deletions dstack/dstack-mr/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub struct Machine<'a> {
pub hugepages: bool,
pub num_gpus: u32,
pub num_nvswitches: u32,
/// Number of virtio-net NICs. Each NIC contributes a PCI device to the
/// ACPI/DSDT layout measured into RTMR0. Defaults to 1 so callers that
/// predate this field keep the historical single-NIC layout.
#[builder(default = 1)]
pub num_nics: u32,
pub hotplug_off: bool,
pub root_verity: bool,
#[builder(default)]
Expand Down
3 changes: 3 additions & 0 deletions dstack/dstack-mr/src/tdx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ fn select_mrtd(measurement: &TdxOsImageMeasurement, vm_config: &VmConfig) -> Res
})
.hugepages(vm_config.hugepages)
.num_gpus(vm_config.num_gpus)
.num_nics(vm_config.num_nics)
.num_nvswitches(vm_config.num_nvswitches)
.host_share_mode(vm_config.host_share_mode.clone())
.ovmf_variant(measurement.tdvf.ovmf_variant)
Expand Down Expand Up @@ -472,6 +473,7 @@ pub fn tdx_measurements_for_image_dir_without_rtmr0(
})
.hugepages(vm_config.hugepages)
.num_gpus(vm_config.num_gpus)
.num_nics(vm_config.num_nics)
.num_nvswitches(vm_config.num_nvswitches)
.host_share_mode(vm_config.host_share_mode.clone())
.ovmf_variant(ovmf_variant)
Expand Down Expand Up @@ -567,6 +569,7 @@ pub fn tdx_measurements_for_image_dir_with_acpi_hashes(
})
.hugepages(vm_config.hugepages)
.num_gpus(vm_config.num_gpus)
.num_nics(vm_config.num_nics)
.num_nvswitches(vm_config.num_nvswitches)
.host_share_mode(vm_config.host_share_mode.clone())
.ovmf_variant(ovmf_variant)
Expand Down
59 changes: 59 additions & 0 deletions dstack/dstack-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,14 @@ impl SysConfig {
}
}

fn default_num_nics() -> u32 {
1
}

fn is_default_num_nics(n: &u32) -> bool {
*n == default_num_nics()
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct VmConfig {
#[serde(with = "hex_bytes", default)]
Expand All @@ -666,6 +674,16 @@ pub struct VmConfig {
pub num_gpus: u32,
#[serde(default)]
pub num_nvswitches: u32,
/// Number of virtio-net NICs attached to the guest. Each NIC adds a PCI
/// device to the ACPI/DSDT layout and therefore changes RTMR0, so it must
/// be measured. Defaults to 1 and is omitted from the serialized form when
/// equal to 1, keeping configs (and their cache keys / hashes) produced
/// before this field existed byte-for-byte stable.
#[serde(
default = "default_num_nics",
skip_serializing_if = "is_default_num_nics"
)]
pub num_nics: u32,
#[serde(default)]
pub hotplug_off: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -1529,3 +1547,44 @@ impl Platform {
}
}
}

#[cfg(test)]
mod vm_config_num_nics_tests {
use super::VmConfig;

fn legacy_json() -> serde_json::Value {
serde_json::json!({
"cpu_count": 4,
"memory_size": 4294967296u64,
"num_gpus": 0,
"num_nvswitches": 0,
})
}

#[test]
fn legacy_config_without_num_nics_defaults_to_one() {
let cfg: VmConfig = serde_json::from_value(legacy_json()).unwrap();
assert_eq!(cfg.num_nics, 1);
}

#[test]
fn single_nic_is_omitted_to_keep_cache_key_stable() {
// A config with the default single NIC must serialize identically to a
// legacy config, so the verifier's measurement cache key (a hash of the
// serialized VmConfig) is unchanged for existing deployments.
let cfg: VmConfig = serde_json::from_value(legacy_json()).unwrap();
let serialized = serde_json::to_value(&cfg).unwrap();
assert!(
serialized.get("num_nics").is_none(),
"num_nics must be omitted when equal to 1, got {serialized}"
);
}

#[test]
fn multi_nic_is_serialized() {
let mut cfg: VmConfig = serde_json::from_value(legacy_json()).unwrap();
cfg.num_nics = 2;
let serialized = serde_json::to_value(&cfg).unwrap();
assert_eq!(serialized.get("num_nics").and_then(|v| v.as_u64()), Some(2));
}
}
1 change: 1 addition & 0 deletions dstack/verifier/src/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ impl CvmVerifier {
})
.hugepages(vm_config.hugepages)
.num_gpus(vm_config.num_gpus)
.num_nics(vm_config.num_nics)
.num_nvswitches(vm_config.num_nvswitches)
.host_share_mode(vm_config.host_share_mode.clone())
.ovmf_variant(ovmf_variant)
Expand Down
32 changes: 32 additions & 0 deletions dstack/vmm/rpc/proto/vmm_rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ message VmInfo {
string image_version = 13;
// Events
repeated GuestEvent events = 14;
// Effective network interfaces resolved against node config.
repeated NetworkInterfaceStatus interfaces = 15;
}

// Runtime status of a resolved VM network interface.
message NetworkInterfaceStatus {
// Product-facing mode: "user", "bridge", or "custom".
string mode = 1;
// QEMU/network backend shape: "slirp", "tap_bridge", or "custom".
string backend = 2;
string mac = 3;
// Linux bridge name for tap_bridge backend.
optional string bridge_name = 4;
// QEMU netdev id, e.g. "net0".
optional string netdev_id = 5;
}

// Structured log or lifecycle event emitted by the guest or runtime.
Expand Down Expand Up @@ -101,12 +116,16 @@ message VmConfiguration {
bool no_tee = 17;
// Per-VM networking mode override (if unset, uses global cvm.networking).
optional NetworkingConfig networking = 18;
// Per-VM networking overrides. If set, wins over singular networking.
repeated NetworkingConfig networks = 19;
}

// Per-VM networking configuration.
message NetworkingConfig {
// Networking mode: "bridge", "user"
string mode = 1;
// Per-VM bridge interface name. Empty = node default bridge.
string bridge_name = 2;
}

// Requested GPU layout for a CVM.
Expand Down Expand Up @@ -163,6 +182,10 @@ message UpdateVmRequest {
optional string image = 17;
// Disable or re-enable TEE for an existing VM.
optional bool no_tee = 18;
// Optional update networking.
bool update_networking = 19;
// Networking list. Empty + update_networking=true resets to node default.
repeated NetworkingConfig networks = 20;
}

// Parameters for listing CVMs with pagination and keyword filtering.
Expand Down Expand Up @@ -262,11 +285,20 @@ message ResourcesSettings {
uint32 max_allocable_memory_in_mb = 3; // in MB.
}

message NetworkingCapabilities {
repeated string supported_modes = 1;
string default_mode = 2;
bool forward_service_enabled = 3;
// Default bridge configured in vmm.toml [cvm.networking].bridge.
string default_bridge = 4;
}

// Aggregated metadata exposed through GetMeta.
message GetMetaResponse {
KmsSettings kms = 1;
GatewaySettings gateway = 2;
ResourcesSettings resources = 3;
NetworkingCapabilities networking = 4;
}

// Build information for the running VMM binary.
Expand Down
Loading
Loading