diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index fd290a8a7..4b51c7eb3 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -34,7 +34,10 @@ pub mod dbus; pub mod http; use std::io; +use std::num::NonZeroU64; +use std::str::FromStr; use std::sync::mpsc::{RecvError, SendError, Sender, channel}; +use std::time::Duration; use log::info; use micro_http::Body; @@ -267,27 +270,83 @@ pub struct VmReceiveMigrationData { pub receiver_url: String, } +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq)] +/// The migration timeout strategy. +/// +/// This strategy describes the behavior of the migration when the target +/// downtime can't be reached in the given timeout. +pub enum TimeoutStrategy { + #[default] + /// Cancel the migration and keep the VM running on the source. + Cancel, + /// Ignore the timeout and migrate anyway. + Ignore, +} + +impl FromStr for TimeoutStrategy { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "cancel" => Ok(TimeoutStrategy::Cancel), + "ignore" => Ok(TimeoutStrategy::Ignore), + _ => Err(format!("Invalid timeout strategy: {s}")), + } + } +} + #[derive(Debug, Error)] #[error("Error parsing send migration parameters")] pub struct VmSendMigrationParseError(#[source] OptionParserError); /// Configuration for an outgoing migration. #[derive(Clone, Deserialize, Serialize, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct VmSendMigrationData { /// URL to migrate the VM to pub destination_url: String, /// Send memory across socket without copying #[serde(default)] pub local: bool, + /// The maximum downtime the migration aims for. + /// + /// Usually, on the order of a few hundred milliseconds. + #[serde(default = "VmSendMigrationData::default_downtime_ms")] + downtime_ms: NonZeroU64, + /// The timeout for the migration, i.e., the maximum duration. + #[serde(default = "VmSendMigrationData::default_timeout_s")] + timeout_s: NonZeroU64, + /// The timeout strategy for the migration. + #[serde(default)] + pub timeout_strategy: TimeoutStrategy, } impl VmSendMigrationData { pub const SYNTAX: &'static str = "VM send migration parameters \ - \"destination_url=[,local=on|off]\""; + \"destination_url=[,local=on|off,\ + downtime_ms=,timeout_s=,\ + timeout_strategy=cancel|ignore]\""; + + // Same as QEMU. + pub const DEFAULT_DOWNTIME: Duration = Duration::from_millis(300); + pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60 * 60 /* one hour */); + + fn default_downtime_ms() -> NonZeroU64 { + let ms_u64 = u64::try_from(Self::DEFAULT_DOWNTIME.as_millis()).unwrap(); + NonZeroU64::new(ms_u64).unwrap() + } + + fn default_timeout_s() -> NonZeroU64 { + NonZeroU64::new(Self::DEFAULT_TIMEOUT.as_secs()).unwrap() + } pub fn parse(migration: &str) -> Result { let mut parser = OptionParser::new(); - parser.add("destination_url").add("local"); + parser + .add("destination_url") + .add("local") + .add("downtime_ms") + .add("timeout_s") + .add("timeout_strategy"); parser.parse(migration).map_err(VmSendMigrationParseError)?; let destination_url = parser.get("destination_url").ok_or_else(|| { @@ -300,12 +359,49 @@ impl VmSendMigrationData { .map_err(VmSendMigrationParseError)? .unwrap_or(Toggle(false)) .0; + let downtime_ms = match parser + .convert::("downtime_ms") + .map_err(VmSendMigrationParseError)? + { + Some(v) => NonZeroU64::new(v).ok_or_else(|| { + VmSendMigrationParseError(OptionParserError::InvalidValue( + "downtime_ms must be non-zero".to_string(), + )) + })?, + None => Self::default_downtime_ms(), + }; + let timeout_s = match parser + .convert::("timeout_s") + .map_err(VmSendMigrationParseError)? + { + Some(v) => NonZeroU64::new(v).ok_or_else(|| { + VmSendMigrationParseError(OptionParserError::InvalidValue( + "timeout_s must be non-zero".to_string(), + )) + })?, + None => Self::default_timeout_s(), + }; + let timeout_strategy = parser + .convert("timeout_strategy") + .map_err(VmSendMigrationParseError)? + .unwrap_or_default(); Ok(Self { destination_url, local, + downtime_ms, + timeout_s, + timeout_strategy, }) } + + pub fn downtime(&self) -> Duration { + Duration::from_millis(self.downtime_ms.get()) + } + + pub fn timeout(&self) -> Duration { + Duration::from_secs(self.timeout_s.get()) + } } pub enum ApiResponsePayload { @@ -1582,15 +1678,76 @@ mod unit_tests { #[test] fn test_vm_send_migration_data_parse() { // Fully specified - let data = VmSendMigrationData::parse("destination_url=tcp://192.168.1.1:8080,local=on") - .expect("valid migration string should parse"); + let data = VmSendMigrationData::parse( + "destination_url=tcp://192.168.1.1:8080,local=on,downtime_ms=200,timeout_s=3600,timeout_strategy=cancel" + ).expect("valid migration string should parse"); assert_eq!(data.destination_url, "tcp://192.168.1.1:8080"); assert!(data.local); + assert_eq!(data.downtime_ms.get(), 200); + assert_eq!(data.timeout_s.get(), 3600); + assert_eq!(data.timeout_strategy, TimeoutStrategy::Cancel); + + // Defaults applied when optional fields are omitted + let data = VmSendMigrationData::parse("destination_url=tcp://192.168.1.1:8080") + .expect("minimal migration string should parse"); + assert_eq!(data.destination_url, "tcp://192.168.1.1:8080"); + assert!(!data.local); + assert_eq!(data.downtime_ms, VmSendMigrationData::default_downtime_ms()); + assert_eq!(data.timeout_s, VmSendMigrationData::default_timeout_s()); + assert_eq!(data.timeout_strategy, TimeoutStrategy::default()); + + // Missing destination_url is an error + VmSendMigrationData::parse("local=on,downtime_ms=200").unwrap_err(); + + // Zero downtime_ms is rejected + let _data = + VmSendMigrationData::parse("destination_url=tcp://192.168.1.1:8080,downtime_ms=0") + .expect_err("zero downtime_ms should be rejected"); + + // Zero timeout_s is rejected + let _data = VmSendMigrationData::parse("destination_url=unix:/tmp/sock,timeout_s=0") + .expect_err("zero timeout_s should be rejected"); // Unknown option is an error VmSendMigrationData::parse("destination_url=unix:/tmp/sock,unknown_field=foo").unwrap_err(); // Invalid toggle value is an error VmSendMigrationData::parse("destination_url=unix:/tmp/sock,local=yes").unwrap_err(); + + // Timeout strategy + let _data = VmSendMigrationData::parse( + "destination_url=tcp://192.168.1.1:8080,timeout_strategy=invalid", + ) + .expect_err("zero downtime_ms should be rejected"); + + // Happy path with some defaults + let data = + VmSendMigrationData::parse("destination_url=tcp://192.168.1.1:8080,downtime_ms=150") + .unwrap(); + assert_eq!( + data, + VmSendMigrationData { + destination_url: "tcp://192.168.1.1:8080".to_string(), + local: false, + downtime_ms: NonZeroU64::new(150).unwrap(), + timeout_s: VmSendMigrationData::default_timeout_s(), + timeout_strategy: Default::default(), + } + ); + + // Happy path, fully specified + let data = + VmSendMigrationData::parse("destination_url=tcp://192.168.1.1:8080,downtime_ms=150,timeout_s=900,timeout_strategy=ignore") + .unwrap(); + assert_eq!( + data, + VmSendMigrationData { + destination_url: "tcp://192.168.1.1:8080".to_string(), + local: false, + downtime_ms: NonZeroU64::new(150).unwrap(), + timeout_s: NonZeroU64::new(900).unwrap(), + timeout_strategy: TimeoutStrategy::Ignore, + } + ); } } diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index cb72d69d9..bb775390a 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -1308,6 +1308,8 @@ impl Vmm { fn do_memory_migration( vm: &mut Vm, socket: &mut SocketStream, + // Used in next commit + _send_data_migration: &VmSendMigrationData, ) -> result::Result<(), MigratableError> { const MAX_ITERATIONS: usize = 5; @@ -1424,7 +1426,7 @@ impl Vmm { // Now pause VM vm.pause()?; } else { - Self::do_memory_migration(vm, &mut socket)?; + Self::do_memory_migration(vm, &mut socket, send_data_migration)?; } // We release the locks early to enable locking them on the destination host. @@ -2438,8 +2440,12 @@ impl RequestHandler for Vmm { send_data_migration: VmSendMigrationData, ) -> result::Result<(), MigratableError> { info!( - "Sending migration: destination_url = {}, local = {}", - send_data_migration.destination_url, send_data_migration.local + "Sending migration: destination_url={},local={},downtime={}ms,timeout={}s,timeout_strategy={:?}", + send_data_migration.destination_url, + send_data_migration.local, + send_data_migration.downtime().as_millis(), + send_data_migration.timeout().as_secs(), + send_data_migration.timeout_strategy ); if !self