vmm: Add validation for Generic Initiator NUMA

Validate device_id in numa config is mutually
exclusive with cpus and memory_zones

Add NumaConfig::validate() and modify NumaConfig::parse()

Add ValidationError::InvalidNumaConfig for detailed error
messages

Include unit tests covering valid and invalid configs

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
This commit is contained in:
Saravanan D 2026-02-11 06:41:05 +00:00 committed by Rob Bradford
parent 6d4827b5ff
commit fa43548975

View file

@ -362,6 +362,9 @@ pub enum ValidationError {
MaskProvidedWithoutIp,
#[error("IP provided without a mask")]
IpProvidedWithoutMask,
/// Invalid NUMA Configuration
#[error("NUMA Configuration is invalid")]
InvalidNumaConfig(String),
}
type ValidationResult<T> = std::result::Result<T, ValidationError>;
@ -2233,6 +2236,11 @@ impl NumaConfig {
.convert::<IntegerList>("pci_segments")
.map_err(Error::ParseNuma)?
.map(|v| v.0.iter().map(|e| *e as u16).collect());
if device_id.is_some() && (cpus.is_some() || memory_zones.is_some()) {
return Err(Error::ParseNuma(OptionParserError::InvalidValue(
"device_id in numa config cannot be used with cpus or memory zones".to_string(),
)));
}
Ok(NumaConfig {
guest_numa_id,
cpus,
@ -2242,6 +2250,58 @@ impl NumaConfig {
pci_segments,
})
}
pub fn is_generic_initiator(&self) -> bool {
self.device_id.is_some()
}
/// Validates NumaConfig
pub fn validate(&self) -> result::Result<(), ValidationError> {
match (&self.device_id, &self.cpus, &self.memory_zones) {
(Some(device_id), None, None) => {
// Valid generic initiator case
if device_id.is_empty() {
return Err(ValidationError::InvalidNumaConfig(
"device_id in numa config cannot be empty".to_string(),
));
}
Ok(())
}
(None, Some(cpus), _) => {
// Standard NUMA with cpus
if cpus.is_empty() {
return Err(ValidationError::InvalidNumaConfig(
"cpus list in numa config cannot be empty".to_string(),
));
}
Ok(())
}
(None, _, Some(memory_zones)) => {
// Standard NUMA with memory_zones (cpus is None here)
if memory_zones.is_empty() {
return Err(ValidationError::InvalidNumaConfig(
"memory_zones in numa config cannot be empty".to_string(),
));
}
Ok(())
}
_ => {
// Default handles all error cases
if self.device_id.is_some() && (self.cpus.is_some() || self.memory_zones.is_some())
{
Err(ValidationError::InvalidNumaConfig(
"device_id in numa config is mutually exclusive with cpus and memory_zones"
.to_string(),
))
} else {
Err(ValidationError::InvalidNumaConfig(
"numa config must specify either device_id or cpus/memory_zones"
.to_string(),
))
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
@ -2778,6 +2838,7 @@ impl VmConfig {
let mut used_numa_node_memory_zones = HashMap::new();
let mut used_pci_segments = HashMap::new();
for numa_node in numa.iter() {
numa_node.validate()?;
if let Some(memory_zones) = numa_node.memory_zones.clone() {
for memory_zone in memory_zones.iter() {
if used_numa_node_memory_zones.contains_key(memory_zone) {
@ -3921,6 +3982,121 @@ mod unit_tests {
Ok(())
}
#[test]
fn test_numa_config_parsing() -> Result<()> {
// Error when device_id and cpu/memory are present
let invalid_input = "guest_numa_id=0,cpus=[0,1],distances=[0@25,1@20],\
device_id=vfio0,memory_zones=[mem1],pci_segments=[0]";
NumaConfig::parse(invalid_input).unwrap_err();
// Successful numa config parsing
let standard_input = "guest_numa_id=1,cpus=[2,3],distances=[0@20],\
memory_zones=[mem0],pci_segments=[0]";
let expected_standard = NumaConfig {
guest_numa_id: 1,
cpus: Some(vec![2, 3]),
distances: Some(vec![NumaDistance {
destination: 0,
distance: 20,
}]),
device_id: None,
memory_zones: Some(vec!["mem0".to_string()]),
pci_segments: Some(vec![0]),
};
assert_eq!(NumaConfig::parse(standard_input)?, expected_standard);
// Successful generic initiator config parse
let gi_input = "guest_numa_id=2,device_id=vfio1,distances=[0@30],pci_segments=[1]";
let expected_gi = NumaConfig {
guest_numa_id: 2,
cpus: None,
distances: Some(vec![NumaDistance {
destination: 0,
distance: 30,
}]),
device_id: Some("vfio1".to_string()),
memory_zones: None,
pci_segments: Some(vec![1]),
};
assert_eq!(NumaConfig::parse(gi_input)?, expected_gi);
Ok(())
}
#[test]
fn test_numa_config_generic_initiator_valid() {
// device_id specified, no cpus/memory_zones
let config = NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: Some(vec![NumaDistance {
destination: 1,
distance: 20,
}]),
memory_zones: None,
device_id: Some("vfio0".to_string()),
pci_segments: None,
};
config.validate().unwrap();
assert!(config.is_generic_initiator());
}
#[test]
fn test_numa_config_invalid_device_id() {
// empty device_id
let config = NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: None,
memory_zones: None,
device_id: Some(String::new()),
pci_segments: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_numa_config_invalid_both_device_cpus() {
// device_id and cpus specified
let config = NumaConfig {
guest_numa_id: 0,
cpus: Some(vec![0, 1]),
distances: None,
device_id: Some("vfio0".to_string()),
memory_zones: None,
pci_segments: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_numa_config_invalid_both_device_memory() {
// device_id and memory zones specified
let config = NumaConfig {
guest_numa_id: 0,
cpus: None,
distances: None,
device_id: Some("vfio0".to_string()),
memory_zones: Some(vec!["mem0".to_string()]),
pci_segments: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_numa_config_standard_valid() {
// No device_id
let config = NumaConfig {
guest_numa_id: 0,
cpus: Some(vec![0, 1]),
distances: Some(vec![NumaDistance {
destination: 1,
distance: 20,
}]),
device_id: None,
memory_zones: Some(vec!["mem0".to_string()]),
pci_segments: None,
};
config.validate().unwrap();
}
#[test]
fn test_restore_parsing() -> Result<()> {
assert_eq!(
@ -4636,11 +4812,13 @@ mod unit_tests {
invalid_config.numa = Some(vec![
NumaConfig {
guest_numa_id: 0,
cpus: Some(vec![0]),
pci_segments: Some(vec![1]),
..numa_fixture()
},
NumaConfig {
guest_numa_id: 1,
cpus: Some(vec![1]),
pci_segments: Some(vec![1]),
..numa_fixture()
},
@ -4676,10 +4854,12 @@ mod unit_tests {
invalid_config.numa = Some(vec![
NumaConfig {
guest_numa_id: 0,
cpus: Some(vec![0]),
..numa_fixture()
},
NumaConfig {
guest_numa_id: 1,
cpus: Some(vec![1]),
pci_segments: Some(vec![0]),
..numa_fixture()
},
@ -4693,11 +4873,13 @@ mod unit_tests {
invalid_config.numa = Some(vec![
NumaConfig {
guest_numa_id: 0,
cpus: Some(vec![0]),
pci_segments: Some(vec![0]),
..numa_fixture()
},
NumaConfig {
guest_numa_id: 1,
cpus: Some(vec![1]),
pci_segments: Some(vec![1]),
..numa_fixture()
},