mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Receiving a migration happens inside the VMM thread, which blocks the API until a migration was received. On the other hand, sending a migration is actually just a dispatch operation. We adjust the wording to improve clarity of the error messages. On-behalf-of: SAP philipp.schuster@sap.com Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2202 lines
66 KiB
Rust
2202 lines
66 KiB
Rust
// Copyright © 2019 Intel Corporation
|
|
// Copyright 2024 Alyssa Ross <hi@alyssa.is>
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
//! The internal VMM API for Cloud Hypervisor.
|
|
//!
|
|
//! This API is a synchronous, [mpsc](https://doc.rust-lang.org/std/sync/mpsc/)
|
|
//! based IPC for sending commands to the VMM thread, from other
|
|
//! Cloud Hypervisor threads. The IPC follows a command-response protocol, i.e.
|
|
//! each command will receive a response back.
|
|
//!
|
|
//! The main Cloud Hypervisor thread creates an API event file descriptor
|
|
//! to notify the VMM thread about pending API commands, together with an
|
|
//! API mpsc channel. The former is the IPC control plane, the latter is the
|
|
//! IPC data plane.
|
|
//! In order to use the IPC, a Cloud Hypervisor thread needs to have a clone
|
|
//! of both the API event file descriptor and the channel Sender. Then it must
|
|
//! go through the following steps:
|
|
//!
|
|
//! 1. The thread creates an mpsc channel for receiving the command response.
|
|
//! 2. The thread sends an ApiRequest to the Sender endpoint. The ApiRequest
|
|
//! encapsulates the response channel Sender, for the VMM API server to be
|
|
//! able to send the response back.
|
|
//! 3. The thread writes to the API event file descriptor to notify the VMM
|
|
//! API server about a pending command.
|
|
//! 4. The thread reads the response back from the VMM API server, from the
|
|
//! response channel Receiver.
|
|
//! 5. The thread handles the response and forwards potential errors.
|
|
|
|
#[cfg(feature = "dbus_api")]
|
|
pub mod dbus;
|
|
pub mod http;
|
|
|
|
use std::io;
|
|
use std::num::{NonZeroU32, NonZeroU64};
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
use std::sync::mpsc::{RecvError, SendError, Sender, channel};
|
|
use std::time::Duration;
|
|
|
|
use log::info;
|
|
use micro_http::Body;
|
|
use option_parser::{OptionParser, OptionParserError, Toggle};
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
use vm_migration::MigratableError;
|
|
use vm_migration::tls::{TlsEndpoint, validate_tls_dir};
|
|
use vmm_sys_util::eventfd::EventFd;
|
|
|
|
#[cfg(feature = "dbus_api")]
|
|
pub use self::dbus::start_dbus_thread;
|
|
pub use self::http::{start_http_fd_thread, start_http_path_thread};
|
|
use crate::Error as VmmError;
|
|
use crate::config::RestoreConfig;
|
|
use crate::device_tree::DeviceTree;
|
|
use crate::migration::transport::{
|
|
MAX_MIGRATION_CONNECTIONS, TcpAddressParseError, tcp_address_to_server_name,
|
|
};
|
|
use crate::vm::{Error as VmError, VmState};
|
|
use crate::vm_config::{
|
|
DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig,
|
|
UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig,
|
|
};
|
|
|
|
/// API errors are sent back from the VMM API server through the ApiResponse.
|
|
#[derive(Error, Debug)]
|
|
pub enum ApiError {
|
|
/// Cannot write to EventFd.
|
|
#[error("Cannot write to EventFd")]
|
|
EventFdWrite(#[source] io::Error),
|
|
|
|
/// API request send error
|
|
#[error("API request send error")]
|
|
RequestSend(#[source] SendError<ApiRequest>),
|
|
|
|
/// Wrong response payload type
|
|
#[error("Wrong response payload type")]
|
|
ResponsePayloadType,
|
|
|
|
/// API response receive error
|
|
#[error("API response receive error")]
|
|
ResponseRecv(#[source] RecvError),
|
|
|
|
/// The VM could not boot.
|
|
#[error("The VM could not boot")]
|
|
VmBoot(#[source] VmError),
|
|
|
|
/// The VM could not be created.
|
|
#[error("The VM could not be created")]
|
|
VmCreate(#[source] VmError),
|
|
|
|
/// The VM could not be deleted.
|
|
#[error("The VM could not be deleted")]
|
|
VmDelete(#[source] VmError),
|
|
|
|
/// The VM info is not available.
|
|
#[error("The VM info is not available")]
|
|
VmInfo(#[source] VmError),
|
|
|
|
/// The VM could not be paused.
|
|
#[error("The VM could not be paused")]
|
|
VmPause(#[source] VmError),
|
|
|
|
/// The VM could not resume.
|
|
#[error("The VM could not resume")]
|
|
VmResume(#[source] VmError),
|
|
|
|
/// The VM is not booted.
|
|
#[error("The VM is not booted")]
|
|
VmNotBooted,
|
|
|
|
/// The VM is not created.
|
|
#[error("The VM is not created")]
|
|
VmNotCreated,
|
|
|
|
/// The VM could not shutdown.
|
|
#[error("The VM could not shutdown")]
|
|
VmShutdown(#[source] VmError),
|
|
|
|
/// The VM could not reboot.
|
|
#[error("The VM could not reboot")]
|
|
VmReboot(#[source] VmError),
|
|
|
|
/// The VM could not be snapshotted.
|
|
#[error("The VM could not be snapshotted")]
|
|
VmSnapshot(#[source] VmError),
|
|
|
|
/// The VM could not be restored.
|
|
#[error("The VM could not be restored")]
|
|
VmRestore(#[source] VmError),
|
|
|
|
/// The VM could not be coredumped.
|
|
#[error("The VM could not be coredumped")]
|
|
VmCoredump(#[source] VmError),
|
|
|
|
/// The VMM could not shutdown.
|
|
#[error("The VMM could not shutdown")]
|
|
VmmShutdown(#[source] VmError),
|
|
|
|
/// The VM could not be resized
|
|
#[error("The VM could not be resized")]
|
|
VmResize(#[source] VmError),
|
|
|
|
/// The disk could not be resized.
|
|
#[error("The disk could not be resized")]
|
|
VmResizeDisk(#[source] VmError),
|
|
|
|
/// The memory zone could not be resized.
|
|
#[error("The memory zone could not be resized")]
|
|
VmResizeZone(#[source] VmError),
|
|
|
|
/// The device could not be added to the VM.
|
|
#[error("The device could not be added to the VM")]
|
|
VmAddDevice(#[source] VmError),
|
|
|
|
/// The user device could not be added to the VM.
|
|
#[error("The user device could not be added to the VM")]
|
|
VmAddUserDevice(#[source] VmError),
|
|
|
|
/// The device could not be removed from the VM.
|
|
#[error("The device could not be removed from the VM")]
|
|
VmRemoveDevice(#[source] VmError),
|
|
|
|
/// Cannot create seccomp filter
|
|
#[error("Cannot create seccomp filter")]
|
|
CreateSeccompFilter(#[source] seccompiler::Error),
|
|
|
|
/// Cannot apply seccomp filter
|
|
#[error("Cannot apply seccomp filter")]
|
|
ApplySeccompFilter(#[source] seccompiler::Error),
|
|
|
|
/// The disk could not be added to the VM.
|
|
#[error("The disk could not be added to the VM")]
|
|
VmAddDisk(#[source] VmError),
|
|
|
|
/// The fs could not be added to the VM.
|
|
#[error("The fs could not be added to the VM")]
|
|
VmAddFs(#[source] VmError),
|
|
|
|
/// The generic vhost-user device could not be added to the VM.
|
|
#[error("The generic vhost-user device could not be added to the VM")]
|
|
VmAddGenericVhostUser(#[source] VmError),
|
|
|
|
/// The pmem device could not be added to the VM.
|
|
#[error("The pmem device could not be added to the VM")]
|
|
VmAddPmem(#[source] VmError),
|
|
|
|
/// The network device could not be added to the VM.
|
|
#[error("The network device could not be added to the VM")]
|
|
VmAddNet(#[source] VmError),
|
|
|
|
/// The vDPA device could not be added to the VM.
|
|
#[error("The vDPA device could not be added to the VM")]
|
|
VmAddVdpa(#[source] VmError),
|
|
|
|
/// The vsock device could not be added to the VM.
|
|
#[error("The vsock device could not be added to the VM")]
|
|
VmAddVsock(#[source] VmError),
|
|
|
|
/// Error while receiving a migration, either because the receiver could not
|
|
/// be started or because the migration failed mid-operation.
|
|
#[error("Error receiving migration")]
|
|
VmReceiveMigration(#[source] MigratableError),
|
|
|
|
/// Error while dispatching the migration worker (thread).
|
|
#[error("Error dispatching the migration worker")]
|
|
VmSendMigration(#[source] MigratableError),
|
|
|
|
/// Error triggering power button
|
|
#[error("Error triggering power button")]
|
|
VmPowerButton(#[source] VmError),
|
|
|
|
/// Error triggering NMI
|
|
#[error("Error triggering NMI")]
|
|
VmNmi(#[source] VmError),
|
|
}
|
|
pub type ApiResult<T> = Result<T, ApiError>;
|
|
|
|
#[serde_with::skip_serializing_none]
|
|
#[derive(Clone, Deserialize, Serialize)]
|
|
pub struct VmInfoResponse {
|
|
pub config: Box<VmConfig>,
|
|
pub state: VmState,
|
|
pub memory_actual_size: u64,
|
|
pub device_tree: Option<DeviceTree>,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize)]
|
|
pub struct VmmPingResponse {
|
|
pub build_version: String,
|
|
pub version: String,
|
|
pub pid: i64,
|
|
pub features: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
|
|
pub struct VmResizeData {
|
|
pub desired_vcpus: Option<u32>,
|
|
pub desired_ram: Option<u64>,
|
|
pub desired_balloon: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
|
|
pub struct VmResizeDiskData {
|
|
pub id: String,
|
|
pub desired_size: u64,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
|
|
pub struct VmResizeZoneData {
|
|
pub id: String,
|
|
pub desired_ram: u64,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
|
|
pub struct VmRemoveDeviceData {
|
|
pub id: String,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
|
|
pub struct VmSnapshotConfig {
|
|
/// The snapshot destination URL
|
|
pub destination_url: String,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Default, Debug)]
|
|
pub struct VmCoredumpData {
|
|
/// The coredump destination file
|
|
pub destination_url: String,
|
|
}
|
|
|
|
/// Memory transfer mode for a migration.
|
|
#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq)]
|
|
pub enum MigrationMode {
|
|
/// Transfer all guest memory before the destination resumes.
|
|
#[default]
|
|
Precopy,
|
|
/// Resume the destination first and fault guest pages in on demand.
|
|
/// This is an experimental mode. It uses a single connection even
|
|
/// when parallel connections are configured. Pages are served on
|
|
/// demand, but a background faulting mechanism also pulls in the
|
|
/// remaining pages to speed up completion.
|
|
Postcopy,
|
|
}
|
|
|
|
impl FromStr for MigrationMode {
|
|
type Err = String;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.to_lowercase().as_str() {
|
|
"precopy" => Ok(MigrationMode::Precopy),
|
|
"postcopy" => Ok(MigrationMode::Postcopy),
|
|
_ => Err(format!("Invalid migration mode: {s}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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 server's TLS root CA
|
|
/// certificate (`ca-cert.pem`).
|
|
///
|
|
/// If this is `Some`, the migration is instructed to use mTLS.
|
|
#[serde(default)]
|
|
pub tls_dir: Option<PathBuf>,
|
|
/// Memory transfer mode.
|
|
#[serde(default)]
|
|
pub memory_mode: MigrationMode,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum VmReceiveMigrationConfigError {
|
|
#[error("Error parsing receive migration parameters")]
|
|
ParseError(#[source] OptionParserError),
|
|
|
|
#[error("Error validating receive migration parameters: {0}")]
|
|
ValidationError(String),
|
|
}
|
|
|
|
impl VmReceiveMigrationData {
|
|
pub const SYNTAX: &'static str = "VM receive migration parameters \
|
|
\"<receiver_url>\" or \"receiver_url=<url>[,tls_dir=<path>][,memory_mode=precopy|postcopy]\"";
|
|
|
|
pub fn parse(migration: &str) -> Result<Self, VmReceiveMigrationConfigError> {
|
|
let mut parser = OptionParser::new();
|
|
parser.add("receiver_url").add("tls_dir").add("memory_mode");
|
|
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::<String>("tls_dir")
|
|
.map_err(VmReceiveMigrationConfigError::ParseError)?
|
|
.map(|path| PathBuf::from(&path));
|
|
let memory_mode = parser
|
|
.convert::<MigrationMode>("memory_mode")
|
|
.map_err(VmReceiveMigrationConfigError::ParseError)?
|
|
.unwrap_or_default();
|
|
|
|
let data = Self {
|
|
receiver_url,
|
|
tls_dir,
|
|
memory_mode,
|
|
};
|
|
|
|
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:<host>:<port> or unix:<path>: {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:<host>:<port> or unix:<path>.".to_string(),
|
|
));
|
|
}
|
|
|
|
if let Some(tls_dir) = &self.tls_dir {
|
|
validate_tls_dir(tls_dir, TlsEndpoint::Server).map_err(|e| {
|
|
VmReceiveMigrationConfigError::ValidationError(format!(
|
|
"invalid TLS configuration for receive-migration: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[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<Self, Self::Err> {
|
|
match s.to_lowercase().as_str() {
|
|
"cancel" => Ok(TimeoutStrategy::Cancel),
|
|
"ignore" => Ok(TimeoutStrategy::Ignore),
|
|
_ => Err(format!("Invalid timeout strategy: {s}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum VmSendMigrationConfigError {
|
|
#[error("Error parsing send migration parameters")]
|
|
ParseError(#[source] OptionParserError),
|
|
|
|
#[error(
|
|
"Error validating send migration parameters: destination_url must use tcp:<host>:<port> or unix:<path>."
|
|
)]
|
|
InvalidDestinationUrl(#[source] TcpAddressParseError),
|
|
|
|
#[error("Error validating send migration parameters: {0}")]
|
|
ValidationError(String),
|
|
}
|
|
|
|
/// Configuration for an outgoing migration.
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
#[cfg_attr(test, derive(PartialEq))]
|
|
pub struct VmSendMigrationData {
|
|
/// Migration destination, e.g. `tcp:<host>:<port>` or `unix:/path/to/socket`.
|
|
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,
|
|
|
|
/// The number of parallel TCP connections for migration.
|
|
///
|
|
/// Must be between 1 and `MAX_MIGRATION_CONNECTIONS` inclusive.
|
|
#[serde(default = "VmSendMigrationData::default_connections")]
|
|
pub connections: NonZeroU32,
|
|
/// Directory containing the TLS client certificate (`client-cert.pem`),
|
|
/// the TLS client key (`client-key.pem`), and the client's TLS root CA
|
|
/// certificate (`ca-cert.pem`).
|
|
///
|
|
/// If this is `Some`, the migration is instructed to use mTLS.
|
|
#[serde(default)]
|
|
pub tls_dir: Option<PathBuf>,
|
|
/// Memory transfer mode.
|
|
#[serde(default)]
|
|
pub memory_mode: MigrationMode,
|
|
}
|
|
|
|
impl VmSendMigrationData {
|
|
pub const SYNTAX: &'static str = "VM send migration parameters \
|
|
\"destination_url=<url>[,local=on|off,\
|
|
downtime_ms=<milliseconds>,timeout_s=<seconds>,\
|
|
timeout_strategy=cancel|ignore,connections=<amount>,\
|
|
tls_dir=<path>,memory_mode=precopy|postcopy]\"";
|
|
|
|
// 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()
|
|
}
|
|
|
|
// Use a single connection as default for backward compatibility.
|
|
fn default_connections() -> NonZeroU32 {
|
|
NonZeroU32::new(1).unwrap()
|
|
}
|
|
|
|
pub fn parse(migration: &str) -> Result<Self, VmSendMigrationConfigError> {
|
|
let mut parser = OptionParser::new();
|
|
parser
|
|
.add("destination_url")
|
|
.add("local")
|
|
.add("downtime_ms")
|
|
.add("timeout_s")
|
|
.add("timeout_strategy")
|
|
.add("connections")
|
|
.add("tls_dir")
|
|
.add("memory_mode");
|
|
parser
|
|
.parse(migration)
|
|
.map_err(VmSendMigrationConfigError::ParseError)?;
|
|
|
|
let destination_url = parser.get("destination_url").ok_or_else(|| {
|
|
VmSendMigrationConfigError::ParseError(OptionParserError::InvalidSyntax(
|
|
"destination_url is required".to_string(),
|
|
))
|
|
})?;
|
|
let local = parser
|
|
.convert::<Toggle>("local")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
.unwrap_or(Toggle(false))
|
|
.0;
|
|
let downtime_ms = match parser
|
|
.convert::<u64>("downtime_ms")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
{
|
|
Some(v) => NonZeroU64::new(v).ok_or_else(|| {
|
|
VmSendMigrationConfigError::ParseError(OptionParserError::InvalidValue(
|
|
"downtime_ms must be non-zero".to_string(),
|
|
))
|
|
})?,
|
|
None => Self::default_downtime_ms(),
|
|
};
|
|
let timeout_s = match parser
|
|
.convert::<u64>("timeout_s")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
{
|
|
Some(v) => NonZeroU64::new(v).ok_or_else(|| {
|
|
VmSendMigrationConfigError::ParseError(OptionParserError::InvalidValue(
|
|
"timeout_s must be non-zero".to_string(),
|
|
))
|
|
})?,
|
|
None => Self::default_timeout_s(),
|
|
};
|
|
let timeout_strategy = parser
|
|
.convert("timeout_strategy")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
.unwrap_or_default();
|
|
let connections = match parser
|
|
.convert::<u32>("connections")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
{
|
|
Some(v) => NonZeroU32::new(v).ok_or_else(|| {
|
|
VmSendMigrationConfigError::ParseError(OptionParserError::InvalidValue(
|
|
"connections must be non-zero".to_string(),
|
|
))
|
|
})?,
|
|
None => Self::default_connections(),
|
|
};
|
|
let tls_dir = parser
|
|
.convert::<String>("tls_dir")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
.map(|path| PathBuf::from(&path));
|
|
let memory_mode = parser
|
|
.convert::<MigrationMode>("memory_mode")
|
|
.map_err(VmSendMigrationConfigError::ParseError)?
|
|
.unwrap_or_default();
|
|
|
|
let data = Self {
|
|
destination_url,
|
|
local,
|
|
downtime_ms,
|
|
timeout_s,
|
|
timeout_strategy,
|
|
connections,
|
|
tls_dir,
|
|
memory_mode,
|
|
};
|
|
|
|
data.validate()?;
|
|
|
|
Ok(data)
|
|
}
|
|
|
|
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 fn validate(&self) -> Result<(), VmSendMigrationConfigError> {
|
|
if let Some(addr) = self.destination_url.strip_prefix("tcp:") {
|
|
tcp_address_to_server_name(addr)
|
|
.map_err(VmSendMigrationConfigError::InvalidDestinationUrl)?;
|
|
} else if self
|
|
.destination_url
|
|
.strip_prefix("unix:")
|
|
.is_some_and(|path| !path.is_empty())
|
|
{
|
|
if self.connections.get() > 1 {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"UNIX sockets and connections option cannot be used at the same time."
|
|
.to_string(),
|
|
));
|
|
}
|
|
if self.tls_dir.is_some() {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"UNIX sockets and TLS encryption cannot be used at the same time.".to_string(),
|
|
));
|
|
}
|
|
} else {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"destination_url must use tcp:<host>:<port> or unix:<path>.".to_string(),
|
|
));
|
|
}
|
|
|
|
if self.connections.get() > MAX_MIGRATION_CONNECTIONS {
|
|
return Err(VmSendMigrationConfigError::ValidationError(format!(
|
|
"connections must not exceed {MAX_MIGRATION_CONNECTIONS}."
|
|
)));
|
|
}
|
|
|
|
if self.local {
|
|
if !self.destination_url.starts_with("unix:") {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"local option is only supported with UNIX sockets.".to_string(),
|
|
));
|
|
}
|
|
|
|
if self.connections.get() > 1 {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"local option and connections option cannot be used at the same time."
|
|
.to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
if let Some(tls_dir) = &self.tls_dir {
|
|
validate_tls_dir(tls_dir, TlsEndpoint::Client).map_err(|e| {
|
|
VmSendMigrationConfigError::ValidationError(format!(
|
|
"invalid TLS configuration for send-migration: {e}"
|
|
))
|
|
})?;
|
|
}
|
|
|
|
if matches!(self.memory_mode, MigrationMode::Postcopy) {
|
|
if self.local {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"memory_mode=postcopy and local options are mutually exclusive.".to_string(),
|
|
));
|
|
}
|
|
|
|
if self.connections.get() > 1 {
|
|
return Err(VmSendMigrationConfigError::ValidationError(
|
|
"memory_mode=postcopy currently requires a single connection (connections=1)."
|
|
.to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub enum ApiResponsePayload {
|
|
/// No data is sent on the channel.
|
|
Empty,
|
|
|
|
/// Virtual machine information
|
|
VmInfo(VmInfoResponse),
|
|
|
|
/// Vmm ping response
|
|
VmmPing(VmmPingResponse),
|
|
|
|
/// Vm action response
|
|
VmAction(Option<Vec<u8>>),
|
|
}
|
|
|
|
/// This is the response sent by the VMM API server through the mpsc channel.
|
|
pub type ApiResponse = Result<ApiResponsePayload, ApiError>;
|
|
|
|
pub trait RequestHandler {
|
|
fn vm_create(&mut self, config: Box<VmConfig>) -> Result<(), VmError>;
|
|
|
|
fn vm_boot(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_pause(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_resume(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_snapshot(&mut self, destination_url: &str) -> Result<(), VmError>;
|
|
|
|
fn vm_restore(&mut self, restore_cfg: RestoreConfig) -> Result<(), VmError>;
|
|
|
|
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
|
fn vm_coredump(&mut self, destination_url: &str) -> Result<(), VmError>;
|
|
|
|
fn vm_shutdown(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_reboot(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_info(&self) -> Result<VmInfoResponse, VmError>;
|
|
|
|
fn vmm_ping(&self) -> VmmPingResponse;
|
|
|
|
fn vm_delete(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vmm_shutdown(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_resize(
|
|
&mut self,
|
|
desired_vcpus: Option<u32>,
|
|
desired_ram: Option<u64>,
|
|
desired_balloon: Option<u64>,
|
|
) -> Result<(), VmError>;
|
|
|
|
fn vm_resize_zone(&mut self, id: String, desired_ram: u64) -> Result<(), VmError>;
|
|
|
|
fn vm_resize_disk(&mut self, id: String, desired_size: u64) -> Result<(), VmError>;
|
|
|
|
fn vm_add_device(&mut self, device_cfg: DeviceConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_user_device(
|
|
&mut self,
|
|
device_cfg: UserDeviceConfig,
|
|
) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_remove_device(&mut self, id: String) -> Result<(), VmError>;
|
|
|
|
fn vm_add_disk(&mut self, disk_cfg: DiskConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_fs(&mut self, fs_cfg: FsConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_generic_vhost_user(
|
|
&mut self,
|
|
fs_cfg: GenericVhostUserConfig,
|
|
) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_pmem(&mut self, pmem_cfg: PmemConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_net(&mut self, net_cfg: NetConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_vdpa(&mut self, vdpa_cfg: VdpaConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_add_vsock(&mut self, vsock_cfg: VsockConfig) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_counters(&mut self) -> Result<Option<Vec<u8>>, VmError>;
|
|
|
|
fn vm_power_button(&mut self) -> Result<(), VmError>;
|
|
|
|
fn vm_receive_migration(
|
|
&mut self,
|
|
receive_data_migration: VmReceiveMigrationData,
|
|
) -> Result<(), MigratableError>;
|
|
|
|
fn vm_send_migration(
|
|
&mut self,
|
|
send_data_migration: VmSendMigrationData,
|
|
) -> Result<(), MigratableError>;
|
|
|
|
fn vm_nmi(&mut self) -> Result<(), VmError>;
|
|
}
|
|
|
|
/// It would be nice if we could pass around an object like this:
|
|
///
|
|
/// ```
|
|
/// # use vmm::api::ApiAction;
|
|
/// struct ApiRequest<Action: ApiAction + 'static> {
|
|
/// action: &'static Action,
|
|
/// body: Action::RequestBody,
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Unfortunately, it's not possible to use such a type in a trait object,
|
|
/// so as a workaround, we instead encapsulate that data in a closure, and have
|
|
/// the event loop call that closure to process a request.
|
|
pub type ApiRequest =
|
|
Box<dyn FnOnce(&mut dyn RequestHandler) -> Result<bool, VmmError> + Send + 'static>;
|
|
|
|
#[expect(clippy::needless_pass_by_value)]
|
|
fn get_response<Action: ApiAction>(
|
|
action: &Action,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Action::RequestBody,
|
|
) -> ApiResult<ApiResponsePayload> {
|
|
let (response_sender, response_receiver) = channel();
|
|
|
|
let request = action.request(data, response_sender);
|
|
|
|
// Send the VM request.
|
|
api_sender.send(request).map_err(ApiError::RequestSend)?;
|
|
api_evt.write(1).map_err(ApiError::EventFdWrite)?;
|
|
|
|
response_receiver.recv().map_err(ApiError::ResponseRecv)?
|
|
}
|
|
|
|
fn get_response_body<Action: ApiAction<ResponseBody = Option<Body>>>(
|
|
action: &Action,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Action::RequestBody,
|
|
) -> ApiResult<Option<Body>> {
|
|
let body = match get_response(action, api_evt, api_sender, data)? {
|
|
ApiResponsePayload::VmAction(response) => response.map(Body::new),
|
|
ApiResponsePayload::Empty => None,
|
|
_ => return Err(ApiError::ResponsePayloadType),
|
|
};
|
|
|
|
Ok(body)
|
|
}
|
|
|
|
pub trait ApiAction: Send + Sync {
|
|
type RequestBody: Send + Sync + Sized;
|
|
type ResponseBody: Send + Sized;
|
|
|
|
fn request(&self, body: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest;
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody>;
|
|
}
|
|
|
|
pub struct VmAddDevice;
|
|
|
|
impl ApiAction for VmAddDevice {
|
|
type RequestBody = DeviceConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddDevice {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_device(config)
|
|
.map_err(ApiError::VmAddDevice)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct AddDisk;
|
|
|
|
impl ApiAction for AddDisk {
|
|
type RequestBody = DiskConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: AddDisk {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_disk(config)
|
|
.map_err(ApiError::VmAddDisk)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddFs;
|
|
|
|
impl ApiAction for VmAddFs {
|
|
type RequestBody = FsConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddFs {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_fs(config)
|
|
.map_err(ApiError::VmAddFs)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddGenericVhostUser;
|
|
|
|
impl ApiAction for VmAddGenericVhostUser {
|
|
type RequestBody = GenericVhostUserConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddGenericVhostUser {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_generic_vhost_user(config)
|
|
.map_err(ApiError::VmAddGenericVhostUser)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddPmem;
|
|
|
|
impl ApiAction for VmAddPmem {
|
|
type RequestBody = PmemConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddPmem {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_pmem(config)
|
|
.map_err(ApiError::VmAddPmem)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddNet;
|
|
|
|
impl ApiAction for VmAddNet {
|
|
type RequestBody = NetConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddNet {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_net(config)
|
|
.map_err(ApiError::VmAddNet)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddVdpa;
|
|
|
|
impl ApiAction for VmAddVdpa {
|
|
type RequestBody = VdpaConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddVdpa {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_vdpa(config)
|
|
.map_err(ApiError::VmAddVdpa)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddVsock;
|
|
|
|
impl ApiAction for VmAddVsock {
|
|
type RequestBody = VsockConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddVsock {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_vsock(config)
|
|
.map_err(ApiError::VmAddVsock)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmAddUserDevice;
|
|
|
|
impl ApiAction for VmAddUserDevice {
|
|
type RequestBody = UserDeviceConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmAddUserDevice {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_add_user_device(config)
|
|
.map_err(ApiError::VmAddUserDevice)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmBoot;
|
|
|
|
impl ApiAction for VmBoot {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmBoot");
|
|
|
|
let response = vmm
|
|
.vm_boot()
|
|
.map_err(ApiError::VmBoot)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
|
pub struct VmCoredump;
|
|
|
|
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
|
impl ApiAction for VmCoredump {
|
|
type RequestBody = VmCoredumpData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
coredump_data: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmCoredump {coredump_data:?}");
|
|
|
|
let response = vmm
|
|
.vm_coredump(&coredump_data.destination_url)
|
|
.map_err(ApiError::VmCoredump)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmCounters;
|
|
|
|
impl ApiAction for VmCounters {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmCounters");
|
|
|
|
let response = vmm
|
|
.vm_counters()
|
|
.map_err(ApiError::VmInfo)
|
|
.map(ApiResponsePayload::VmAction);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmCreate;
|
|
|
|
impl ApiAction for VmCreate {
|
|
type RequestBody = Box<VmConfig>;
|
|
type ResponseBody = ();
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmCreate {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_create(config)
|
|
.map_err(ApiError::VmCreate)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<()> {
|
|
get_response(self, api_evt, api_sender, data)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct VmDelete;
|
|
|
|
impl ApiAction for VmDelete {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmDelete");
|
|
|
|
let response = vmm
|
|
.vm_delete()
|
|
.map_err(ApiError::VmDelete)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmInfo;
|
|
|
|
impl ApiAction for VmInfo {
|
|
type RequestBody = ();
|
|
type ResponseBody = VmInfoResponse;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmInfo");
|
|
|
|
let response = vmm
|
|
.vm_info()
|
|
.map_err(ApiError::VmInfo)
|
|
.map(ApiResponsePayload::VmInfo);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: (),
|
|
) -> ApiResult<VmInfoResponse> {
|
|
let vm_info = get_response(self, api_evt, api_sender, data)?;
|
|
|
|
match vm_info {
|
|
ApiResponsePayload::VmInfo(info) => Ok(info),
|
|
_ => Err(ApiError::ResponsePayloadType),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct VmPause;
|
|
|
|
impl ApiAction for VmPause {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmPause");
|
|
|
|
let response = vmm
|
|
.vm_pause()
|
|
.map_err(ApiError::VmPause)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmPowerButton;
|
|
|
|
impl ApiAction for VmPowerButton {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmPowerButton");
|
|
|
|
let response = vmm
|
|
.vm_power_button()
|
|
.map_err(ApiError::VmPowerButton)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmReboot;
|
|
|
|
impl ApiAction for VmReboot {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmReboot");
|
|
|
|
let response = vmm
|
|
.vm_reboot()
|
|
.map_err(ApiError::VmReboot)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmReceiveMigration;
|
|
|
|
impl ApiAction for VmReceiveMigration {
|
|
type RequestBody = VmReceiveMigrationData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, data: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmReceiveMigration {data:?}");
|
|
|
|
let response = vmm
|
|
.vm_receive_migration(data)
|
|
.map_err(ApiError::VmReceiveMigration)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmRemoveDevice;
|
|
|
|
impl ApiAction for VmRemoveDevice {
|
|
type RequestBody = VmRemoveDeviceData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
remove_device_data: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmRemoveDevice {remove_device_data:?}");
|
|
|
|
let response = vmm
|
|
.vm_remove_device(remove_device_data.id)
|
|
.map_err(ApiError::VmRemoveDevice)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmResize;
|
|
|
|
impl ApiAction for VmResize {
|
|
type RequestBody = VmResizeData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
resize_data: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmResize {resize_data:?}");
|
|
|
|
let response = vmm
|
|
.vm_resize(
|
|
resize_data.desired_vcpus,
|
|
resize_data.desired_ram,
|
|
resize_data.desired_balloon,
|
|
)
|
|
.map_err(ApiError::VmResize)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmResizeDisk;
|
|
|
|
impl ApiAction for VmResizeDisk {
|
|
type RequestBody = VmResizeDiskData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
resize_disk_data: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
let response = vmm
|
|
.vm_resize_disk(resize_disk_data.id, resize_disk_data.desired_size)
|
|
.map_err(ApiError::VmResizeDisk)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmResizeZone;
|
|
|
|
impl ApiAction for VmResizeZone {
|
|
type RequestBody = VmResizeZoneData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
resize_zone_data: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmResizeZone {resize_zone_data:?}");
|
|
|
|
let response = vmm
|
|
.vm_resize_zone(resize_zone_data.id, resize_zone_data.desired_ram)
|
|
.map_err(ApiError::VmResizeZone)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmRestore;
|
|
|
|
impl ApiAction for VmRestore {
|
|
type RequestBody = RestoreConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmRestore {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_restore(config)
|
|
.map_err(ApiError::VmRestore)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmResume;
|
|
|
|
impl ApiAction for VmResume {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmResume");
|
|
|
|
let response = vmm
|
|
.vm_resume()
|
|
.map_err(ApiError::VmResume)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmSendMigration;
|
|
|
|
impl ApiAction for VmSendMigration {
|
|
type RequestBody = VmSendMigrationData;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, data: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmSendMigration {data:?}");
|
|
|
|
let response = vmm
|
|
.vm_send_migration(data)
|
|
.map_err(ApiError::VmSendMigration)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmShutdown;
|
|
|
|
impl ApiAction for VmShutdown {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmShutdown {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_shutdown()
|
|
.map_err(ApiError::VmShutdown)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmSnapshot;
|
|
|
|
impl ApiAction for VmSnapshot {
|
|
type RequestBody = VmSnapshotConfig;
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(
|
|
&self,
|
|
config: Self::RequestBody,
|
|
response_sender: Sender<ApiResponse>,
|
|
) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmSnapshot {config:?}");
|
|
|
|
let response = vmm
|
|
.vm_snapshot(&config.destination_url)
|
|
.map_err(ApiError::VmSnapshot)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
pub struct VmmPing;
|
|
|
|
impl ApiAction for VmmPing {
|
|
type RequestBody = ();
|
|
type ResponseBody = VmmPingResponse;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmmPing");
|
|
|
|
let response = ApiResponsePayload::VmmPing(vmm.vmm_ping());
|
|
|
|
response_sender
|
|
.send(Ok(response))
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: (),
|
|
) -> ApiResult<VmmPingResponse> {
|
|
let vmm_pong = get_response(self, api_evt, api_sender, data)?;
|
|
|
|
match vmm_pong {
|
|
ApiResponsePayload::VmmPing(pong) => Ok(pong),
|
|
_ => Err(ApiError::ResponsePayloadType),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct VmmShutdown;
|
|
|
|
impl ApiAction for VmmShutdown {
|
|
type RequestBody = ();
|
|
type ResponseBody = ();
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmmShutdown");
|
|
|
|
let response = vmm
|
|
.vmm_shutdown()
|
|
.map_err(ApiError::VmmShutdown)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(true)
|
|
})
|
|
}
|
|
|
|
fn send(&self, api_evt: EventFd, api_sender: Sender<ApiRequest>, data: ()) -> ApiResult<()> {
|
|
get_response(self, api_evt, api_sender, data)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct VmNmi;
|
|
|
|
impl ApiAction for VmNmi {
|
|
type RequestBody = ();
|
|
type ResponseBody = Option<Body>;
|
|
|
|
fn request(&self, _: Self::RequestBody, response_sender: Sender<ApiResponse>) -> ApiRequest {
|
|
Box::new(move |vmm| {
|
|
info!("API request event: VmNmi");
|
|
|
|
let response = vmm
|
|
.vm_nmi()
|
|
.map_err(ApiError::VmNmi)
|
|
.map(|_| ApiResponsePayload::Empty);
|
|
|
|
response_sender
|
|
.send(response)
|
|
.map_err(VmmError::ApiResponseSend)?;
|
|
|
|
Ok(false)
|
|
})
|
|
}
|
|
|
|
fn send(
|
|
&self,
|
|
api_evt: EventFd,
|
|
api_sender: Sender<ApiRequest>,
|
|
data: Self::RequestBody,
|
|
) -> ApiResult<Self::ResponseBody> {
|
|
get_response_body(self, api_evt, api_sender, data)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod unit_tests {
|
|
use std::path::PathBuf;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use std::{env, fs, process};
|
|
|
|
use super::*;
|
|
|
|
struct TestDir {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl TestDir {
|
|
fn new(name: &str) -> Self {
|
|
let unique = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
let path = env::temp_dir().join(format!(
|
|
"cloud-hypervisor-api-{name}-{}-{unique}",
|
|
process::id()
|
|
));
|
|
fs::create_dir(&path).unwrap();
|
|
Self { path }
|
|
}
|
|
|
|
fn add_file(&self, file_name: &str) {
|
|
fs::write(self.path.join(file_name), b"test").unwrap();
|
|
}
|
|
|
|
fn add_receive_tls_files(&self) {
|
|
self.add_file("ca-cert.pem");
|
|
self.add_file("server-cert.pem");
|
|
self.add_file("server-key.pem");
|
|
}
|
|
|
|
fn add_send_tls_files(&self) {
|
|
self.add_file("ca-cert.pem");
|
|
self.add_file("client-cert.pem");
|
|
self.add_file("client-key.pem");
|
|
}
|
|
}
|
|
|
|
impl Drop for TestDir {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.path);
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
memory_mode: MigrationMode::Precopy,
|
|
}
|
|
);
|
|
|
|
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 = TestDir::new("receive-tls");
|
|
tls_dir.add_receive_tls_files();
|
|
let tls_dir_path = tls_dir.path.clone();
|
|
let data = VmReceiveMigrationData::parse(&format!(
|
|
"receiver_url=tcp:192.168.1.1:8080,tls_dir={}",
|
|
tls_dir_path.display()
|
|
))
|
|
.unwrap();
|
|
assert_eq!(
|
|
data,
|
|
VmReceiveMigrationData {
|
|
receiver_url: "tcp:192.168.1.1:8080".to_string(),
|
|
tls_dir: Some(tls_dir_path),
|
|
memory_mode: MigrationMode::Precopy,
|
|
}
|
|
);
|
|
|
|
let tls_dir = TestDir::new("receive-empty-tls");
|
|
VmReceiveMigrationData::parse(&format!(
|
|
"receiver_url=tcp:192.168.1.1:8080,tls_dir={}",
|
|
tls_dir.path.display()
|
|
))
|
|
.unwrap_err();
|
|
|
|
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();
|
|
|
|
// memory_mode defaults to precopy when not specified.
|
|
let data = VmReceiveMigrationData::parse("receiver_url=tcp:127.0.0.1:1234").unwrap();
|
|
assert_eq!(
|
|
data,
|
|
VmReceiveMigrationData {
|
|
receiver_url: "tcp:127.0.0.1:1234".to_string(),
|
|
tls_dir: None,
|
|
memory_mode: MigrationMode::Precopy,
|
|
}
|
|
);
|
|
|
|
// Explicit receiver_url with memory_mode=postcopy.
|
|
let data =
|
|
VmReceiveMigrationData::parse("receiver_url=unix:/tmp/sock,memory_mode=postcopy")
|
|
.unwrap();
|
|
assert_eq!(
|
|
data,
|
|
VmReceiveMigrationData {
|
|
receiver_url: "unix:/tmp/sock".to_string(),
|
|
tls_dir: None,
|
|
memory_mode: MigrationMode::Postcopy,
|
|
}
|
|
);
|
|
|
|
// Missing receiver_url in keyed form must fail.
|
|
VmReceiveMigrationData::parse("memory_mode=postcopy").unwrap_err();
|
|
}
|
|
|
|
#[test]
|
|
fn test_vm_send_migration_data_parse() {
|
|
// Fully specified
|
|
let data = VmSendMigrationData::parse(
|
|
"destination_url=unix:/tmp/migrate.sock,local=on,downtime_ms=200,timeout_s=3600,timeout_strategy=cancel"
|
|
).expect("valid migration string should parse");
|
|
assert_eq!(data.destination_url, "unix:/tmp/migrate.sock");
|
|
assert!(data.local);
|
|
assert_eq!(data.downtime_ms.get(), 200);
|
|
assert_eq!(data.timeout_s.get(), 3600);
|
|
assert_eq!(data.timeout_strategy, TimeoutStrategy::Cancel);
|
|
assert_eq!(data.connections.get(), 1);
|
|
|
|
// 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());
|
|
assert_eq!(data.connections, VmSendMigrationData::default_connections());
|
|
|
|
let data = VmSendMigrationData::parse("destination_url=tcp:[2001:db8::1]:8080")
|
|
.expect("IPv6 migration string should parse");
|
|
assert_eq!(data.destination_url, "tcp:[2001:db8::1]:8080");
|
|
|
|
let data = VmSendMigrationData::parse("destination_url=tcp:destination.example:8080")
|
|
.expect("hostname migration string should parse");
|
|
assert_eq!(data.destination_url, "tcp:destination.example:8080");
|
|
|
|
// 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");
|
|
|
|
// Zero connections is rejected
|
|
let _data =
|
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1:8080,connections=0")
|
|
.expect_err("zero connections should be rejected");
|
|
|
|
// Excessive numbers of parallel connections are rejected
|
|
let _data =
|
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1:8080,connections=129")
|
|
.expect_err("too many connections 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("invalid timeout strategy should be rejected");
|
|
|
|
// Invalid destination URL scheme is rejected
|
|
VmSendMigrationData::parse("destination_url=file:///tmp/migration").unwrap_err();
|
|
assert!(matches!(
|
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1").unwrap_err(),
|
|
VmSendMigrationConfigError::InvalidDestinationUrl(TcpAddressParseError::MissingPort)
|
|
));
|
|
assert!(matches!(
|
|
VmSendMigrationData::parse("destination_url=tcp:[2001:db8::1]").unwrap_err(),
|
|
VmSendMigrationConfigError::InvalidDestinationUrl(
|
|
TcpAddressParseError::MissingPortSeparatorAfterBracketedHost
|
|
)
|
|
));
|
|
|
|
// Local migration requires a UNIX socket destination
|
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1:8080,local=yes").unwrap_err();
|
|
|
|
// Local migration cannot use multiple connections
|
|
VmSendMigrationData::parse("destination_url=unix:/tmp/sock,local=yes,connections=2")
|
|
.unwrap_err();
|
|
|
|
// 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(),
|
|
connections: VmSendMigrationData::default_connections(),
|
|
tls_dir: None,
|
|
memory_mode: MigrationMode::Precopy,
|
|
}
|
|
);
|
|
|
|
// Happy path, fully specified
|
|
let tls_dir = TestDir::new("send-tls");
|
|
tls_dir.add_send_tls_files();
|
|
let tls_dir_path = tls_dir.path.clone();
|
|
let data =
|
|
VmSendMigrationData::parse(&format!("destination_url=tcp:192.168.1.1:8080,downtime_ms=150,timeout_s=900,timeout_strategy=ignore,connections=4,tls_dir={}", tls_dir_path.display()))
|
|
.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,
|
|
connections: NonZeroU32::new(4).unwrap(),
|
|
tls_dir: Some(tls_dir_path),
|
|
memory_mode: MigrationMode::Precopy,
|
|
}
|
|
);
|
|
|
|
// Postcopy happy path (TCP only, single connection).
|
|
let data =
|
|
VmSendMigrationData::parse("destination_url=tcp:192.168.1.1:8080,memory_mode=postcopy")
|
|
.unwrap();
|
|
assert_eq!(data.memory_mode, MigrationMode::Postcopy);
|
|
|
|
// memory_mode=postcopy + local must be rejected.
|
|
VmSendMigrationData::parse("destination_url=unix:/tmp/sock,local=on,memory_mode=postcopy")
|
|
.unwrap_err();
|
|
|
|
// memory_mode=postcopy + multi-connection must be rejected.
|
|
VmSendMigrationData::parse(
|
|
"destination_url=tcp:192.168.1.1:8080,memory_mode=postcopy,connections=4",
|
|
)
|
|
.unwrap_err();
|
|
}
|
|
}
|