From d419338a474988d6d741f3e2c16f7be22cb0f9c1 Mon Sep 17 00:00:00 2001 From: Pascal Scholz Date: Mon, 20 Jul 2026 15:16:08 +0200 Subject: [PATCH] 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 On-behalf-of: SAP pascal.scholz@sap. --- option_parser/src/lib.rs | 22 ++++++- vmm/src/api/mod.rs | 51 ++++++++++++++++- vmm/src/config.rs | 121 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 185 insertions(+), 9 deletions(-) diff --git a/option_parser/src/lib.rs b/option_parser/src/lib.rs index 26bf1c439..680e82943 100644 --- a/option_parser/src/lib.rs +++ b/option_parser/src/lib.rs @@ -426,7 +426,7 @@ impl> Parseable for IntegerList { /// Types that can appear as the second element of a [`Tuple`] pair. /// -/// Implemented for `u64`, `Vec`, `Vec`, and `Vec`. +/// Implemented for `u32`, `u64`, `Vec`, `Vec`, and `Vec`. pub trait TupleValue { /// Parses the value portion of a `key@value` tuple element. fn parse_value(input: &str) -> Result @@ -440,6 +440,12 @@ impl TupleValue for u64 { } } +impl TupleValue for u32 { + fn parse_value(input: &str) -> Result { + input.parse::().map_err(TupleError::InvalidInteger) + } +} + impl TupleValue for Vec { fn parse_value(input: &str) -> Result { Ok(IntegerList::::from_str(input) @@ -1026,6 +1032,20 @@ mod unit_tests { ); } + #[test] + fn test_tuple_parse_u32_value() { + use std::num::IntErrorKind; + let t = Tuple::::from_str("foo@42").unwrap(); + assert_eq!(t, Tuple("foo".to_owned(), 42)); + let t = Tuple::::from_str("foo@0").unwrap(); + assert_eq!(t, Tuple("foo".to_owned(), 0)); + let e = Tuple::::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(); diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index f273da8b0..73f020d7b 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -338,7 +338,8 @@ pub enum VmReceiveMigrationConfigError { impl VmReceiveMigrationData { pub const SYNTAX: &'static str = "VM receive migration parameters \ \"\" or \"receiver_url=[,tls_dir=][,memory_mode=precopy|postcopy]\ - [,vfio_fds=][,iommufd_fd=]\""; + [,vfio_fds=][,iommufd_fd=]\ + [,zone_updates=[]]\""; pub fn parse(migration: &str) -> Result { 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::("iommufd_fd") .map_err(VmReceiveMigrationConfigError::ParseError)?; + let zone_updates: Vec = parser + .convert::>("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::>(); + 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] diff --git a/vmm/src/config.rs b/vmm/src/config.rs index aec8b77d4..f72bf7646 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -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 = result::Result; @@ -2861,7 +2867,8 @@ impl RestoreConfig { pub const SYNTAX: &'static str = "Restore from a VM snapshot. \ \nRestore parameters \"source_url=,prefault=on|off,memory_restore_mode=copy|ondemand,\ net_fds=,\ - vfio_fds=,iommufd_fd=,resume=true|false\" \ + vfio_fds=,iommufd_fd=,resume=true|false,\ + zone_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 { 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 = parser + .convert::>("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::>(); + 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::( + 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]