mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
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:
committed by
Rob Bradford
parent
ad3790a0ea
commit
d419338a47
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user