vmm: Add parsing logic for zone_updates

This commit adds support for parsing `zone_updates` from the CLI
for the live migration and restore paths.

Signed-off-by: Pascal Scholz <pascal.scholz@cyberus-technology.de>
On-behalf-of: SAP pascal.scholz@sap.
This commit is contained in:
Pascal Scholz
2026-07-20 15:16:08 +02:00
committed by Rob Bradford
parent ad3790a0ea
commit d419338a47
3 changed files with 185 additions and 9 deletions

View File

@@ -426,7 +426,7 @@ impl<T: TryFrom<u64>> Parseable for IntegerList<T> {
/// Types that can appear as the second element of a [`Tuple`] pair.
///
/// Implemented for `u64`, `Vec<u8>`, `Vec<u64>`, and `Vec<usize>`.
/// Implemented for `u32`, `u64`, `Vec<u8>`, `Vec<u64>`, and `Vec<usize>`.
pub trait TupleValue {
/// Parses the value portion of a `key@value` tuple element.
fn parse_value(input: &str) -> Result<Self, TupleError>
@@ -440,6 +440,12 @@ impl TupleValue for u64 {
}
}
impl TupleValue for u32 {
fn parse_value(input: &str) -> Result<Self, TupleError> {
input.parse::<u32>().map_err(TupleError::InvalidInteger)
}
}
impl TupleValue for Vec<u8> {
fn parse_value(input: &str) -> Result<Self, TupleError> {
Ok(IntegerList::<u8>::from_str(input)
@@ -1026,6 +1032,20 @@ mod unit_tests {
);
}
#[test]
fn test_tuple_parse_u32_value() {
use std::num::IntErrorKind;
let t = Tuple::<String, u32>::from_str("foo@42").unwrap();
assert_eq!(t, Tuple("foo".to_owned(), 42));
let t = Tuple::<String, u32>::from_str("foo@0").unwrap();
assert_eq!(t, Tuple("foo".to_owned(), 0));
let e = Tuple::<String, u32>::from_str("foo@-1").unwrap_err();
assert!(
matches!(e, TupleError::InvalidInteger(ref e) if *e.kind() == IntErrorKind::InvalidDigit),
"Expected \"InvalidInteger(ParseIntError(kind: InvalidDigit))\"; got \"{e:?}\"",
);
}
#[test]
fn test_split_commas_unbalanced_bracket() {
split_commas("[a,b").unwrap_err();

View File

@@ -338,7 +338,8 @@ pub enum VmReceiveMigrationConfigError {
impl VmReceiveMigrationData {
pub const SYNTAX: &'static str = "VM receive migration parameters \
\"<receiver_url>\" or \"receiver_url=<url>[,tls_dir=<path>][,memory_mode=precopy|postcopy]\
[,vfio_fds=<list_of_vfio_ids_with_their_associated_fd>][,iommufd_fd=<fd>]\"";
[,vfio_fds=<list_of_vfio_ids_with_their_associated_fd>][,iommufd_fd=<fd>]\
[,zone_updates=[<id@host_numa_node>]]\"";
pub fn parse(migration: &str) -> Result<Self, VmReceiveMigrationConfigError> {
let mut parser = OptionParser::new();
@@ -347,7 +348,8 @@ impl VmReceiveMigrationData {
.add("tls_dir")
.add("memory_mode")
.add("vfio_fds")
.add("iommufd_fd");
.add("iommufd_fd")
.add("zone_updates");
parser
.parse(migration)
.map_err(VmReceiveMigrationConfigError::ParseError)?;
@@ -380,13 +382,25 @@ impl VmReceiveMigrationData {
.convert::<i32>("iommufd_fd")
.map_err(VmReceiveMigrationConfigError::ParseError)?;
let zone_updates: Vec<VmMemoryZoneUpdateData> = parser
.convert::<TupleList<String, u32>>("zone_updates")
.map_err(VmReceiveMigrationConfigError::ParseError)?
.map_or(Vec::new(), |v| {
v.0.iter()
.map(|Tuple(id, host_numa_node)| VmMemoryZoneUpdateData {
id: id.clone(),
host_numa_node: *host_numa_node,
})
.collect()
});
let data = Self {
receiver_url,
tls_dir,
memory_mode,
vfio_fds,
iommufd_fd,
zone_updates: vec![],
zone_updates,
};
data.validate()?;
@@ -425,6 +439,22 @@ impl VmReceiveMigrationData {
})?;
}
let unique_zones = self
.zone_updates
.iter()
.map(|update| update.id.as_str())
.collect::<HashSet<_>>();
if self.zone_updates.len() != unique_zones.len() {
return Err(VmReceiveMigrationConfigError::ValidationError(
"more than one update was defined for at least one memory zone".to_string(),
));
}
if unique_zones.contains("") {
return Err(VmReceiveMigrationConfigError::ValidationError(
"Empty Id".to_string(),
));
}
Ok(())
}
@@ -2193,6 +2223,21 @@ mod unit_tests {
assert_eq!(fds[1].id, "vfio1");
assert_eq!(fds[1].fd, Some(7));
assert_eq!(data.iommufd_fd, Some(9));
// zone update tests
VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,zone_updates=[]").unwrap_err();
VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,zone_updates=[zone1 3]")
.unwrap_err();
VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,zone_updates=[zone1@invalid]")
.unwrap_err();
VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,zone_updates=[zone1@1,zone1@2]")
.unwrap_err();
// Mind the space before the second zone. If the whitespace isn't trimmed, we end up with
// two different ID
VmReceiveMigrationData::parse(
"receiver_url=unix:/tmp/sock,zone_updates=[zone1@1, zone1@2]",
)
.unwrap_err();
}
#[test]

View File

@@ -439,6 +439,12 @@ pub enum ValidationError {
/// Invalid to set both 'mergeable' and 'shared' for memory
#[error("Invalid to set both 'mergeable' and 'shared' for memory")]
InvalidSharedMemoryWithMergeable,
/// More than one update was specified for one or more MemoryZone
#[error("Multiple updates for the same zone defined")]
MultipleMemoryZoneUpdates,
/// More than one update was specified that contains an empty memory zone ID
#[error("At least one memory zone update with an empty ID was defined")]
MemoryZoneUpdatesEmptyId,
}
type ValidationResult<T> = result::Result<T, ValidationError>;
@@ -2861,7 +2867,8 @@ impl RestoreConfig {
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
net_fds=<list_of_net_ids_with_their_associated_fds>,\
vfio_fds=<list_of_vfio_ids_with_their_associated_fd>,iommufd_fd=<fd>,resume=true|false\" \
vfio_fds=<list_of_vfio_ids_with_their_associated_fd>,iommufd_fd=<fd>,resume=true|false,\
zone_updates=<list_of_updates>\"
\n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \
\n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \
\n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \
@@ -2872,7 +2879,8 @@ impl RestoreConfig {
sysfs path or host. Requires `iommufd_fd`.\
\n`iommufd_fd` is a new iommufd file descriptor for the restored VM. \
The one saved in the snapshot does not survive serialization.\
\n `resume` controls whether the VM will be directly resumed after restore ";
\n `resume` controls whether the VM will be directly resumed after restore \
\n `zone_updates` can be used to update NUMA memory zones. Expects a list of elements in the form `id@host_numa_node`";
pub fn parse(restore: &str) -> Result<Self> {
let mut parser = OptionParser::new();
@@ -2883,7 +2891,8 @@ impl RestoreConfig {
.add("net_fds")
.add("vfio_fds")
.add("iommufd_fd")
.add("resume");
.add("resume")
.add("zone_updates");
parser.parse(restore).map_err(Error::ParseRestore)?;
let source_url = parser
@@ -2931,6 +2940,18 @@ impl RestoreConfig {
.unwrap_or(Toggle(false))
.0;
let zone_updates: Vec<VmMemoryZoneUpdateData> = parser
.convert::<TupleList<String, u32>>("zone_updates")
.map_err(Error::ParseRestore)?
.map_or(Vec::new(), |v| {
v.0.iter()
.map(|Tuple(id, host_numa_node)| VmMemoryZoneUpdateData {
id: id.clone(),
host_numa_node: *host_numa_node,
})
.collect()
});
Ok(RestoreConfig {
source_url,
prefault,
@@ -2939,7 +2960,7 @@ impl RestoreConfig {
vfio_fds,
iommufd_fd,
resume,
zone_updates: vec![],
zone_updates,
})
}
@@ -2986,6 +3007,18 @@ impl RestoreConfig {
}
}
let unique_zones = self
.zone_updates
.iter()
.map(|update| update.id.as_str())
.collect::<HashSet<_>>();
if self.zone_updates.len() != unique_zones.len() {
return Err(ValidationError::MultipleMemoryZoneUpdates);
}
if unique_zones.contains("") {
return Err(ValidationError::MemoryZoneUpdatesEmptyId);
}
if !restored_net_with_fds.is_empty() {
warn!("Ignoring unused 'net_fds' for VM restore.");
}
@@ -5251,7 +5284,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
}
);
assert_eq!(
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on")?,
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[zone1@1]")?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
@@ -5260,6 +5293,33 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
vfio_fds: None,
iommufd_fd: None,
resume: true,
zone_updates: vec![VmMemoryZoneUpdateData {
host_numa_node: 1,
id: "zone1".to_string(),
}],
}
);
assert_eq!(
RestoreConfig::parse(
"source_url=/path/to/snapshot,vfio_fds=[vfio0@5,vfio1@6],iommufd_fd=7"
)?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: Some(vec![
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
},
RestoredVfioConfig {
id: "vfio1".to_string(),
fd: Some(6),
},
]),
iommufd_fd: Some(7),
resume: false,
zone_updates: vec![],
}
);
@@ -5290,6 +5350,18 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
// Parsing should fail as source_url is a required field
RestoreConfig::parse("prefault=off").unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=bogus").unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[@1]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[@]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[id1@]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[id1 1]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=[[id1@1]]")
.unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,resume=on,zone_updates=id1@1")
.unwrap_err();
Ok(())
}
@@ -5309,6 +5381,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
.memory_restore_mode,
MemoryRestoreMode::OnDemand
);
assert_eq!(
serde_json::from_str::<RestoreConfig>(
r#"{"source_url":"/path/to/snapshot","zone_updates":[{"id": "zone1", "host_numa_node": 1}]}"#
)
.unwrap()
.zone_updates,
vec![VmMemoryZoneUpdateData {
host_numa_node: 1,
id: "zone1".to_string(),
}],
);
}
#[test]
@@ -5493,6 +5576,34 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
invalid_restore_mode.validate(&snapshot_vm_config),
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
);
// It is invalid to submit more than one update for a single zone.
let mut invalid_config_zone_updates = valid_config.clone();
invalid_config_zone_updates.zone_updates = vec![
VmMemoryZoneUpdateData {
id: "id1".to_string(),
host_numa_node: 0,
},
VmMemoryZoneUpdateData {
id: "id1".to_string(),
host_numa_node: 20,
},
];
assert_eq!(
invalid_config_zone_updates.validate(&snapshot_vm_config),
Err(ValidationError::MultipleMemoryZoneUpdates)
);
// It is invalid to submit an update without referring to a memory zone by specifying an ID.
let mut invalid_config_zone_updates = valid_config.clone();
invalid_config_zone_updates.zone_updates = vec![VmMemoryZoneUpdateData {
id: String::new(),
host_numa_node: 0,
}];
assert_eq!(
invalid_config_zone_updates.validate(&snapshot_vm_config),
Err(ValidationError::MemoryZoneUpdatesEmptyId)
);
}
#[test]