diff --git a/cloud-hypervisor/src/bin/ch-remote.rs b/cloud-hypervisor/src/bin/ch-remote.rs index 22d81e26a..3c1b32606 100644 --- a/cloud-hypervisor/src/bin/ch-remote.rs +++ b/cloud-hypervisor/src/bin/ch-remote.rs @@ -71,6 +71,8 @@ enum Error { ReadingFile(#[source] std::io::Error), #[error("Invalid disk size")] InvalidDiskSize(#[source] ByteSizedParseError), + #[error("Error parsing receive migration configuration")] + ReceiveMigrationConfig(#[from] vmm::api::VmReceiveMigrationConfigError), #[error("Error parsing send migration configuration")] SendMigrationConfig(#[from] vmm::api::VmSendMigrationConfigError), } @@ -534,7 +536,7 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .unwrap() .get_one::("receive_migration_config") .unwrap(), - ); + )?; simple_api_command( socket, "PUT", @@ -753,7 +755,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) .unwrap() .get_one::("receive_migration_config") .unwrap(), - ); + )?; proxy.api_vm_receive_migration(&receive_migration_data) } Some("create") => { @@ -941,12 +943,10 @@ fn coredump_config(destination_url: &str) -> String { serde_json::to_string(&coredump_config).unwrap() } -fn receive_migration_data(url: &str) -> String { - let receive_migration_data = vmm::api::VmReceiveMigrationData { - receiver_url: url.to_owned(), - }; - - serde_json::to_string(&receive_migration_data).unwrap() +fn receive_migration_data(config: &str) -> Result { + let receive_migration_data = + vmm::api::VmReceiveMigrationData::parse(config).map_err(Error::ReceiveMigrationConfig)?; + Ok(serde_json::to_string(&receive_migration_data).unwrap()) } fn send_migration_data(config: &str) -> Result { @@ -1069,7 +1069,7 @@ fn get_cli_commands_sorted() -> Box<[Command]> { .arg( Arg::new("receive_migration_config") .index(1) - .help(""), + .help(vmm::api::VmReceiveMigrationData::SYNTAX), ), Command::new("remove-device") .about("Remove VFIO and PCI device") diff --git a/cloud-hypervisor/tests/common/utils.rs b/cloud-hypervisor/tests/common/utils.rs index 4139df727..43fd02c3b 100644 --- a/cloud-hypervisor/tests/common/utils.rs +++ b/cloud-hypervisor/tests/common/utils.rs @@ -1136,7 +1136,7 @@ pub(crate) fn start_live_migration( .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", - &format! {"unix:{migration_socket}"}, + &format!("receiver_url=unix:{migration_socket}"), ]) .stderr(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/cloud-hypervisor/tests/integration.rs b/cloud-hypervisor/tests/integration.rs index 97f1afbd4..85d168479 100644 --- a/cloud-hypervisor/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -6740,7 +6740,7 @@ mod common_parallel { .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", - &format!("tcp:0.0.0.0:{migration_port}"), + &format!("receiver_url=tcp:0.0.0.0:{migration_port}"), ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) @@ -7077,7 +7077,7 @@ mod common_parallel { .args([ &format!("--api-socket={dest_api_socket}"), "receive-migration", - &format!("tcp:0.0.0.0:{migration_port}"), + &format!("receiver_url=tcp:0.0.0.0:{migration_port}"), ]) .stdin(Stdio::null()) .stderr(Stdio::piped()) diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index 93bc40a5c..2c933820e 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -270,9 +270,80 @@ pub struct VmCoredumpData { } #[derive(Clone, Deserialize, Serialize, Default, Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct VmReceiveMigrationData { /// URL for the reception of migration state pub receiver_url: String, + /// Directory containing the TLS server certificate (server-cert.pem), the TLS server key (server-key.pem), and the client TLS root CA certificate (ca-cert.pem). + #[serde(default)] + pub tls_dir: Option, +} + +#[derive(Debug, Error)] +pub enum VmReceiveMigrationConfigError { + #[error("Error parsing receive migration parameters")] + ParseError(#[source] OptionParserError), + + #[error("Error validating receive migration parameters")] + ValidationError(String), +} + +impl VmReceiveMigrationData { + pub const SYNTAX: &'static str = "VM receive migration parameters \ + \"\" or \"receiver_url=[,tls_dir=]\""; + + pub fn parse(migration: &str) -> Result { + let mut parser = OptionParser::new(); + parser.add("receiver_url").add("tls_dir"); + parser + .parse(migration) + .map_err(VmReceiveMigrationConfigError::ParseError)?; + + let receiver_url = parser.get("receiver_url").ok_or_else(|| { + VmReceiveMigrationConfigError::ParseError(OptionParserError::InvalidSyntax( + "receiver_url is required".to_string(), + )) + })?; + let tls_dir = parser + .convert::("tls_dir") + .map_err(VmReceiveMigrationConfigError::ParseError)? + .map(|path| PathBuf::from(&path)); + + let data = Self { + receiver_url, + tls_dir, + }; + + data.validate()?; + + Ok(data) + } + + pub fn validate(&self) -> Result<(), VmReceiveMigrationConfigError> { + if let Some(addr) = self.receiver_url.strip_prefix("tcp:") { + tcp_address_to_server_name(addr).map_err(|e| { + VmReceiveMigrationConfigError::ValidationError(format!( + "receiver_url must use tcp:: or unix:: {e}." + )) + })?; + } else if self + .receiver_url + .strip_prefix("unix:") + .is_some_and(|path| !path.is_empty()) + { + if self.tls_dir.is_some() { + return Err(VmReceiveMigrationConfigError::ValidationError( + "UNIX sockets and TLS encryption cannot be used at the same time.".to_string(), + )); + } + } else { + return Err(VmReceiveMigrationConfigError::ValidationError( + "receiver_url must use tcp:: or unix:.".to_string(), + )); + } + + Ok(()) + } } #[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq)] @@ -1780,6 +1851,47 @@ impl ApiAction for VmNmi { mod unit_tests { use super::*; + #[test] + fn test_vm_receive_migration_data_parse() { + let data = VmReceiveMigrationData::parse("receiver_url=tcp:192.168.1.1:8080").unwrap(); + assert_eq!( + data, + VmReceiveMigrationData { + receiver_url: "tcp:192.168.1.1:8080".to_string(), + tls_dir: None, + } + ); + + let data = VmReceiveMigrationData::parse("receiver_url=tcp:[2001:db8::1]:8080").unwrap(); + assert_eq!(data.receiver_url, "tcp:[2001:db8::1]:8080"); + + let data = + VmReceiveMigrationData::parse("receiver_url=tcp:destination.example:8080").unwrap(); + assert_eq!(data.receiver_url, "tcp:destination.example:8080"); + + let data = VmReceiveMigrationData::parse("receiver_url=unix:/tmp/ch=migrate.sock").unwrap(); + assert_eq!(data.receiver_url, "unix:/tmp/ch=migrate.sock"); + + let tls_dir = std::env::temp_dir(); + let data = VmReceiveMigrationData::parse(&format!( + "receiver_url=tcp:192.168.1.1:8080,tls_dir={}", + tls_dir.display() + )) + .unwrap(); + assert_eq!( + data, + VmReceiveMigrationData { + receiver_url: "tcp:192.168.1.1:8080".to_string(), + tls_dir: Some(tls_dir), + } + ); + + VmReceiveMigrationData::parse("receiver_url=file:///tmp/migration").unwrap_err(); + VmReceiveMigrationData::parse("receiver_url=tcp:192.168.1.1").unwrap_err(); + VmReceiveMigrationData::parse("receiver_url=tcp:[2001:db8::1]").unwrap_err(); + VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,tls_dir=/tmp").unwrap_err(); + } + #[test] fn test_vm_send_migration_data_parse() { // Fully specified diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index 4ed651ced..e27117528 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -1506,6 +1506,12 @@ components: properties: receiver_url: type: string + tls_dir: + type: string + description: > + Directory containing the TLS server certificate (server-cert.pem), the TLS + server key (server-key.pem), and the client TLS root CA certificate (ca-cert.pem). + TLS is only supported with tcp:: receiver URLs. TimeoutStrategy: type: string