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.
|
/// 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 {
|
pub trait TupleValue {
|
||||||
/// Parses the value portion of a `key@value` tuple element.
|
/// Parses the value portion of a `key@value` tuple element.
|
||||||
fn parse_value(input: &str) -> Result<Self, TupleError>
|
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> {
|
impl TupleValue for Vec<u8> {
|
||||||
fn parse_value(input: &str) -> Result<Self, TupleError> {
|
fn parse_value(input: &str) -> Result<Self, TupleError> {
|
||||||
Ok(IntegerList::<u8>::from_str(input)
|
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]
|
#[test]
|
||||||
fn test_split_commas_unbalanced_bracket() {
|
fn test_split_commas_unbalanced_bracket() {
|
||||||
split_commas("[a,b").unwrap_err();
|
split_commas("[a,b").unwrap_err();
|
||||||
|
|||||||
@@ -338,7 +338,8 @@ pub enum VmReceiveMigrationConfigError {
|
|||||||
impl VmReceiveMigrationData {
|
impl VmReceiveMigrationData {
|
||||||
pub const SYNTAX: &'static str = "VM receive migration parameters \
|
pub const SYNTAX: &'static str = "VM receive migration parameters \
|
||||||
\"<receiver_url>\" or \"receiver_url=<url>[,tls_dir=<path>][,memory_mode=precopy|postcopy]\
|
\"<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> {
|
pub fn parse(migration: &str) -> Result<Self, VmReceiveMigrationConfigError> {
|
||||||
let mut parser = OptionParser::new();
|
let mut parser = OptionParser::new();
|
||||||
@@ -347,7 +348,8 @@ impl VmReceiveMigrationData {
|
|||||||
.add("tls_dir")
|
.add("tls_dir")
|
||||||
.add("memory_mode")
|
.add("memory_mode")
|
||||||
.add("vfio_fds")
|
.add("vfio_fds")
|
||||||
.add("iommufd_fd");
|
.add("iommufd_fd")
|
||||||
|
.add("zone_updates");
|
||||||
parser
|
parser
|
||||||
.parse(migration)
|
.parse(migration)
|
||||||
.map_err(VmReceiveMigrationConfigError::ParseError)?;
|
.map_err(VmReceiveMigrationConfigError::ParseError)?;
|
||||||
@@ -380,13 +382,25 @@ impl VmReceiveMigrationData {
|
|||||||
.convert::<i32>("iommufd_fd")
|
.convert::<i32>("iommufd_fd")
|
||||||
.map_err(VmReceiveMigrationConfigError::ParseError)?;
|
.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 {
|
let data = Self {
|
||||||
receiver_url,
|
receiver_url,
|
||||||
tls_dir,
|
tls_dir,
|
||||||
memory_mode,
|
memory_mode,
|
||||||
vfio_fds,
|
vfio_fds,
|
||||||
iommufd_fd,
|
iommufd_fd,
|
||||||
zone_updates: vec![],
|
zone_updates,
|
||||||
};
|
};
|
||||||
|
|
||||||
data.validate()?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2193,6 +2223,21 @@ mod unit_tests {
|
|||||||
assert_eq!(fds[1].id, "vfio1");
|
assert_eq!(fds[1].id, "vfio1");
|
||||||
assert_eq!(fds[1].fd, Some(7));
|
assert_eq!(fds[1].fd, Some(7));
|
||||||
assert_eq!(data.iommufd_fd, Some(9));
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -439,6 +439,12 @@ pub enum ValidationError {
|
|||||||
/// Invalid to set both 'mergeable' and 'shared' for memory
|
/// Invalid to set both 'mergeable' and 'shared' for memory
|
||||||
#[error("Invalid to set both 'mergeable' and 'shared' for memory")]
|
#[error("Invalid to set both 'mergeable' and 'shared' for memory")]
|
||||||
InvalidSharedMemoryWithMergeable,
|
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>;
|
type ValidationResult<T> = result::Result<T, ValidationError>;
|
||||||
@@ -2861,7 +2867,8 @@ impl RestoreConfig {
|
|||||||
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
|
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
|
||||||
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
|
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
|
||||||
net_fds=<list_of_net_ids_with_their_associated_fds>,\
|
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`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`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 \
|
\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`.\
|
sysfs path or host. Requires `iommufd_fd`.\
|
||||||
\n`iommufd_fd` is a new iommufd file descriptor for the restored VM. \
|
\n`iommufd_fd` is a new iommufd file descriptor for the restored VM. \
|
||||||
The one saved in the snapshot does not survive serialization.\
|
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> {
|
pub fn parse(restore: &str) -> Result<Self> {
|
||||||
let mut parser = OptionParser::new();
|
let mut parser = OptionParser::new();
|
||||||
@@ -2883,7 +2891,8 @@ impl RestoreConfig {
|
|||||||
.add("net_fds")
|
.add("net_fds")
|
||||||
.add("vfio_fds")
|
.add("vfio_fds")
|
||||||
.add("iommufd_fd")
|
.add("iommufd_fd")
|
||||||
.add("resume");
|
.add("resume")
|
||||||
|
.add("zone_updates");
|
||||||
parser.parse(restore).map_err(Error::ParseRestore)?;
|
parser.parse(restore).map_err(Error::ParseRestore)?;
|
||||||
|
|
||||||
let source_url = parser
|
let source_url = parser
|
||||||
@@ -2931,6 +2940,18 @@ impl RestoreConfig {
|
|||||||
.unwrap_or(Toggle(false))
|
.unwrap_or(Toggle(false))
|
||||||
.0;
|
.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 {
|
Ok(RestoreConfig {
|
||||||
source_url,
|
source_url,
|
||||||
prefault,
|
prefault,
|
||||||
@@ -2939,7 +2960,7 @@ impl RestoreConfig {
|
|||||||
vfio_fds,
|
vfio_fds,
|
||||||
iommufd_fd,
|
iommufd_fd,
|
||||||
resume,
|
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() {
|
if !restored_net_with_fds.is_empty() {
|
||||||
warn!("Ignoring unused 'net_fds' for VM restore.");
|
warn!("Ignoring unused 'net_fds' for VM restore.");
|
||||||
}
|
}
|
||||||
@@ -5251,7 +5284,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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 {
|
RestoreConfig {
|
||||||
source_url: PathBuf::from("/path/to/snapshot"),
|
source_url: PathBuf::from("/path/to/snapshot"),
|
||||||
prefault: false,
|
prefault: false,
|
||||||
@@ -5260,6 +5293,33 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
|
|||||||
vfio_fds: None,
|
vfio_fds: None,
|
||||||
iommufd_fd: None,
|
iommufd_fd: None,
|
||||||
resume: true,
|
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![],
|
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
|
// Parsing should fail as source_url is a required field
|
||||||
RestoreConfig::parse("prefault=off").unwrap_err();
|
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,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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5309,6 +5381,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
|
|||||||
.memory_restore_mode,
|
.memory_restore_mode,
|
||||||
MemoryRestoreMode::OnDemand
|
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]
|
#[test]
|
||||||
@@ -5493,6 +5576,34 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
|
|||||||
invalid_restore_mode.validate(&snapshot_vm_config),
|
invalid_restore_mode.validate(&snapshot_vm_config),
|
||||||
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user