From f39fdf297c6bd6c65613c3e9310dacd6a57942f1 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Tue, 7 Nov 2023 14:52:55 +0000 Subject: [PATCH 01/11] snapshot: switch to using serde instead of Versionize We want to simplify the way we handle Firecracker snapshots. Versionize crate was created as a mechanism to help us keep track multiple versions for each struct we want to (de)serialize in a Firecracker snapshot. This allowed us to support forwards & backwards compatibility across multiple Firecracker versions. In reality, we don't really make use of this compatibility flexibility. Instead, we keep maintaining multiple versions for struct fields (even for versions no longer supported), which increases code and testing complexity. We will revert to simply using serde for serializing Firecracker state into a snapshot file. We will also decouple Firecracker from snapshot versions. The snapshot format will now have an independent version which starts from v1.0.0. This change touches many parts of the code base, so we will do it gradually. In this commit, we change the snapshot crate and the snapshotting API. We only make the minimum changes needed to the rest of the codebase to keep Firecracker working. We change `MicrovmState`, the structure that describes the Firecracker state we serialize in a snapshot to skip serialization of all its fields (essentially it is now describing an empty snapshot). In subsequent commits, we will start re-adding fields as we adapt the relevant structures from Versionize to serde. Integration tests will not be working for this commit. Signed-off-by: Babis Chalios --- Cargo.lock | 8 +- src/firecracker/src/main.rs | 32 +- src/snapshot-editor/Cargo.toml | 1 + src/snapshot-editor/src/info.rs | 24 +- src/snapshot-editor/src/utils.rs | 12 +- src/snapshot/Cargo.toml | 13 +- src/snapshot/benches/version_map.rs | 133 ------ src/snapshot/src/crc.rs | 159 +++++++ src/snapshot/src/lib.rs | 705 ++++++++++------------------ src/snapshot/tests/test.rs | 230 --------- src/vmm/src/lib.rs | 2 - src/vmm/src/persist.rs | 74 +-- src/vmm/src/rpc_interface.rs | 11 +- src/vmm/src/version_map.rs | 128 ----- src/vmm/tests/integration_tests.rs | 34 +- tools/bump-version.sh | 11 - 16 files changed, 479 insertions(+), 1098 deletions(-) delete mode 100644 src/snapshot/benches/version_map.rs create mode 100644 src/snapshot/src/crc.rs delete mode 100644 src/snapshot/tests/test.rs delete mode 100644 src/vmm/src/version_map.rs diff --git a/Cargo.lock b/Cargo.lock index bbb53c559a0..662c90c0f53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1177,13 +1177,14 @@ checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" name = "snapshot" version = "0.1.0" dependencies = [ - "criterion", + "bincode", + "crc64", "displaydoc", "libc", "log-instrument", + "semver", + "serde", "thiserror", - "versionize", - "versionize_derive", ] [[package]] @@ -1195,6 +1196,7 @@ dependencies = [ "displaydoc", "libc", "log-instrument", + "semver", "snapshot", "thiserror", "utils", diff --git a/src/firecracker/src/main.rs b/src/firecracker/src/main.rs index f21f79c182f..517a6c2abf1 100644 --- a/src/firecracker/src/main.rs +++ b/src/firecracker/src/main.rs @@ -24,9 +24,9 @@ use vmm::builder::StartMicrovmError; use vmm::logger::{ debug, error, info, LoggerConfig, ProcessTimeReporter, StoreMetric, LOGGER, METRICS, }; +use vmm::persist::SNAPSHOT_VERSION; use vmm::resources::VmResources; use vmm::signal_handler::register_signal_handlers; -use vmm::version_map::{FC_VERSION_TO_SNAP_VERSION, VERSION_MAP}; use vmm::vmm_config::instance_info::{InstanceInfo, VmState}; use vmm::vmm_config::metrics::{init_metrics, MetricsConfig, MetricsConfigError}; use vmm::{EventManager, FcExitCode, HTTP_MAX_PAYLOAD_SIZE}; @@ -228,10 +228,16 @@ fn main_exec() -> Result<(), MainError> { "Whether or not to load boot timer device for logging elapsed time since \ InstanceStart command.", )) - .arg(Argument::new("version").takes_value(false).help( - "Print the binary version number and a list of supported snapshot data format \ - versions.", - )) + .arg( + Argument::new("version") + .takes_value(false) + .help("Print the binary version number."), + ) + .arg( + Argument::new("snapshot-version") + .takes_value(false) + .help("Print the supported data format version."), + ) .arg( Argument::new("describe-snapshot") .takes_value(true) @@ -263,6 +269,11 @@ fn main_exec() -> Result<(), MainError> { return Ok(()); } + if arguments.flag_present("snapshot-version") { + println!("v{SNAPSHOT_VERSION}"); + return Ok(()); + } + if let Some(snapshot_path) = arguments.single_value("describe-snapshot") { print_snapshot_data_format(snapshot_path)?; return Ok(()); @@ -520,8 +531,6 @@ enum SnapshotVersionError { OpenSnapshot(io::Error), /// Invalid data format version of snapshot file: {0} SnapshotVersion(SnapshotError), - /// Cannot translate snapshot data version {0} to Firecracker microVM version - FirecrackerVersion(u16), } // Print data format of provided snapshot state file. @@ -529,15 +538,10 @@ fn print_snapshot_data_format(snapshot_path: &str) -> Result<(), SnapshotVersion let mut snapshot_reader = File::open(snapshot_path).map_err(SnapshotVersionError::OpenSnapshot)?; - let data_format_version = Snapshot::get_data_version(&mut snapshot_reader, &VERSION_MAP) + let data_format_version = Snapshot::get_format_version(&mut snapshot_reader) .map_err(SnapshotVersionError::SnapshotVersion)?; - let (key, _) = FC_VERSION_TO_SNAP_VERSION - .iter() - .find(|(_, &val)| val == data_format_version) - .ok_or_else(|| SnapshotVersionError::FirecrackerVersion(data_format_version))?; - - println!("v{}", key); + println!("v{}", data_format_version); Ok(()) } diff --git a/src/snapshot-editor/Cargo.toml b/src/snapshot-editor/Cargo.toml index 70ae45ffb71..f07302c6b93 100644 --- a/src/snapshot-editor/Cargo.toml +++ b/src/snapshot-editor/Cargo.toml @@ -14,6 +14,7 @@ clap = { version = "4.4.14", features = ["derive", "string"] } displaydoc = "0.2.4" libc = "0.2.151" log-instrument = { path = "../log-instrument", optional = true } +semver = "1.0.20" snapshot = { path = "../snapshot" } thiserror = "1.0.53" vmm = { path = "../vmm" } diff --git a/src/snapshot-editor/src/info.rs b/src/snapshot-editor/src/info.rs index 532f622e512..659c86e6b50 100644 --- a/src/snapshot-editor/src/info.rs +++ b/src/snapshot-editor/src/info.rs @@ -4,15 +4,13 @@ use std::path::PathBuf; use clap::Subcommand; +use semver::Version; use vmm::persist::MicrovmState; -use vmm::version_map::FC_VERSION_TO_SNAP_VERSION; use crate::utils::*; #[derive(Debug, thiserror::Error, displaydoc::Display)] pub enum InfoVmStateError { - /// Cannot translate snapshot data version {0} to Firecracker microVM version - InvalidVersion(u16), /// {0} Utils(#[from] UtilsError), } @@ -52,27 +50,19 @@ pub fn info_vmstate_command(command: InfoVmStateSubCommand) -> Result<(), InfoVm fn info( vmstate_path: &PathBuf, - f: impl Fn(&MicrovmState, u16) -> Result<(), InfoVmStateError>, + f: impl Fn(&MicrovmState, Version) -> Result<(), InfoVmStateError>, ) -> Result<(), InfoVmStateError> { let (vmstate, version) = open_vmstate(vmstate_path)?; f(&vmstate, version)?; Ok(()) } -fn info_version(_: &MicrovmState, version: u16) -> Result<(), InfoVmStateError> { - match FC_VERSION_TO_SNAP_VERSION - .iter() - .find(|(_, &v)| v == version) - { - Some((key, _)) => { - println!("v{key}"); - Ok(()) - } - None => Err(InfoVmStateError::InvalidVersion(version)), - } +fn info_version(_: &MicrovmState, version: Version) -> Result<(), InfoVmStateError> { + println!("v{version}"); + Ok(()) } -fn info_vcpu_states(state: &MicrovmState, _: u16) -> Result<(), InfoVmStateError> { +fn info_vcpu_states(state: &MicrovmState, _: Version) -> Result<(), InfoVmStateError> { for (i, state) in state.vcpu_states.iter().enumerate() { println!("vcpu {i}:"); println!("{state:#?}"); @@ -80,7 +70,7 @@ fn info_vcpu_states(state: &MicrovmState, _: u16) -> Result<(), InfoVmStateError Ok(()) } -fn info_vmstate(vmstate: &MicrovmState, _version: u16) -> Result<(), InfoVmStateError> { +fn info_vmstate(vmstate: &MicrovmState, _version: Version) -> Result<(), InfoVmStateError> { println!("{vmstate:#?}"); Ok(()) } diff --git a/src/snapshot-editor/src/utils.rs b/src/snapshot-editor/src/utils.rs index bf9be3ac5e2..625d7c58129 100644 --- a/src/snapshot-editor/src/utils.rs +++ b/src/snapshot-editor/src/utils.rs @@ -5,9 +5,9 @@ use std::fs::{File, OpenOptions}; use std::path::PathBuf; use fc_utils::u64_to_usize; +use semver::Version; use snapshot::Snapshot; use vmm::persist::MicrovmState; -use vmm::version_map::VERSION_MAP; // Some errors are only used in aarch64 code #[allow(unused)] @@ -26,12 +26,11 @@ pub enum UtilsError { } #[allow(unused)] -pub fn open_vmstate(snapshot_path: &PathBuf) -> Result<(MicrovmState, u16), UtilsError> { - let version_map = VERSION_MAP.clone(); +pub fn open_vmstate(snapshot_path: &PathBuf) -> Result<(MicrovmState, Version), UtilsError> { let mut snapshot_reader = File::open(snapshot_path).map_err(UtilsError::VmStateFileOpen)?; let metadata = std::fs::metadata(snapshot_path).map_err(UtilsError::VmStateFileMeta)?; let snapshot_len = u64_to_usize(metadata.len()); - Snapshot::load(&mut snapshot_reader, snapshot_len, version_map).map_err(UtilsError::VmStateLoad) + Snapshot::load(&mut snapshot_reader, snapshot_len).map_err(UtilsError::VmStateLoad) } // This method is used only in aarch64 code so far @@ -39,16 +38,15 @@ pub fn open_vmstate(snapshot_path: &PathBuf) -> Result<(MicrovmState, u16), Util pub fn save_vmstate( microvm_state: MicrovmState, output_path: &PathBuf, - version: u16, + version: Version, ) -> Result<(), UtilsError> { - let version_map = VERSION_MAP.clone(); let mut output_file = OpenOptions::new() .create(true) .write(true) .truncate(true) .open(output_path) .map_err(UtilsError::OutputFileOpen)?; - let mut snapshot = Snapshot::new(version_map, version); + let mut snapshot = Snapshot::new(version); snapshot .save(&mut output_file, µvm_state) .map_err(UtilsError::VmStateSave)?; diff --git a/src/snapshot/Cargo.toml b/src/snapshot/Cargo.toml index 0d0e578f482..cc37d7c23f6 100644 --- a/src/snapshot/Cargo.toml +++ b/src/snapshot/Cargo.toml @@ -11,18 +11,13 @@ bench = false [dependencies] libc = "0.2.117" -versionize = "0.2.0" -versionize_derive = "0.1.6" thiserror = "1.0.32" displaydoc = "0.2.4" log-instrument = { path = "../log-instrument", optional = true } - -[dev-dependencies] -criterion = { version = "0.5.0", default-features = false } - -[[bench]] -name = "version_map" -harness = false +semver = { version = "1.0.20", features = ["serde"] } +serde = { version = "1.0.192", features = ["derive"] } +bincode = "1.3.3" +crc64 = "2.0.0" [features] tracing = ["log-instrument"] diff --git a/src/snapshot/benches/version_map.rs b/src/snapshot/benches/version_map.rs deleted file mode 100644 index ea3c38b86d7..00000000000 --- a/src/snapshot/benches/version_map.rs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use snapshot::Snapshot; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; - -#[derive(Clone, Debug, Default, Versionize)] -struct Test { - a: Vec, - #[version(start = 1)] - b: u64, - #[version(start = 2)] - c: u64, - #[version(start = 3)] - d: u32, - #[version(start = 4)] - e: Vec, -} - -#[derive(Clone, Debug, Default, Versionize)] -struct Dummy { - a: String, - #[version(start = 2)] - b: [u64; 32], -} - -#[inline] -fn restore(mut snapshot_mem: &[u8], vm: VersionMap) { - Snapshot::unchecked_load::<&[u8], Test>(&mut snapshot_mem, vm).unwrap(); -} - -#[inline] -fn save(mut snapshot_mem: &mut W, vm: VersionMap) { - let state = Test { - a: vec![ - Dummy { - a: "a string".to_owned(), - b: [0x1234u64; 32] - }; - 200 - ], - b: 0, - c: 1, - d: 2, - e: vec![0x4321; 100], - }; - - let mut snapshot = Snapshot::new(vm.clone(), vm.latest_version()); - snapshot - .save_without_crc(&mut snapshot_mem, &state) - .unwrap(); -} - -pub fn criterion_benchmark(c: &mut Criterion) { - let mut snapshot_mem = vec![0u8; 1024 * 1024 * 128]; - let mut vm = VersionMap::new(); - - vm.new_version() - .set_type_version(Test::type_id(), 2) - .new_version() - .set_type_version(Test::type_id(), 3) - .new_version() - .set_type_version(Test::type_id(), 4) - .set_type_version(Dummy::type_id(), 2); - - let mut slice = &mut snapshot_mem.as_mut_slice(); - save(&mut slice, vm.clone()); - let snapshot_len = slice.as_ptr() as usize - snapshot_mem.as_slice().as_ptr() as usize; - println!("Snapshot length: {} bytes", snapshot_len); - - c.bench_function("Serialize in vspace=4", |b| { - b.iter(|| { - save( - black_box(&mut snapshot_mem.as_mut_slice()), - black_box(vm.clone()), - ) - }) - }); - - c.bench_function("Deserialize in vspace=4", |b| { - b.iter(|| restore(black_box(snapshot_mem.as_slice()), black_box(vm.clone()))) - }); - - // Extend vspace to 100. - for _ in 0..96 { - vm.new_version(); - } - - save(&mut snapshot_mem.as_mut_slice(), vm.clone()); - - c.bench_function("Serialize in vspace=100", |b| { - b.iter(|| { - save( - black_box(&mut snapshot_mem.as_mut_slice()), - black_box(vm.clone()), - ) - }) - }); - c.bench_function("Deserialize in vspace=100", |b| { - b.iter(|| restore(black_box(snapshot_mem.as_slice()), black_box(vm.clone()))) - }); - - // Extend vspace to 1001. - for _ in 0..900 { - vm.new_version(); - } - - // Save the snapshot at version 1001. - save(&mut snapshot_mem.as_mut_slice(), vm.clone()); - - c.bench_function("Serialize in vspace=1000", |b| { - b.iter(|| { - save( - black_box(&mut snapshot_mem.as_mut_slice()), - black_box(vm.clone()), - ) - }) - }); - c.bench_function("Deserialize in vspace=1000", |b| { - b.iter(|| restore(black_box(snapshot_mem.as_slice()), black_box(vm.clone()))) - }); -} - -criterion_group! { - name = benches; - config = Criterion::default().sample_size(200).noise_threshold(0.05); - targets = criterion_benchmark -} - -criterion_main! { - benches -} diff --git a/src/snapshot/src/crc.rs b/src/snapshot/src/crc.rs new file mode 100644 index 00000000000..e80c916777b --- /dev/null +++ b/src/snapshot/src/crc.rs @@ -0,0 +1,159 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Implements readers and writers that compute the CRC64 checksum of the bytes +//! read/written. + +use std::io::{Read, Write}; + +use crc64::crc64; + +/// Computes the CRC64 checksum of the read bytes. +/// +/// ``` +/// use std::io::Read; +/// +/// use snapshot::crc::CRC64Reader; +/// +/// let buf = vec![1, 2, 3, 4, 5]; +/// let mut read_buf = Vec::new(); +/// let mut slice = buf.as_slice(); +/// +/// // Create a reader from a slice. +/// let mut crc_reader = CRC64Reader::new(&mut slice); +/// +/// let count = crc_reader.read_to_end(&mut read_buf).unwrap(); +/// assert_eq!(crc_reader.checksum(), 0xFB04_60DE_0638_3654); +/// assert_eq!(read_buf, buf); +/// ``` +#[derive(Debug)] +pub struct CRC64Reader { + reader: T, + crc64: u64, +} + +impl CRC64Reader +where + T: Read, +{ + /// Create a new reader. + pub fn new(reader: T) -> Self { + CRC64Reader { crc64: 0, reader } + } + /// Returns the current checksum value. + pub fn checksum(&self) -> u64 { + self.crc64 + } +} + +impl Read for CRC64Reader +where + T: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_read = self.reader.read(buf)?; + self.crc64 = crc64(self.crc64, &buf[..bytes_read]); + Ok(bytes_read) + } +} + +/// Computes the CRC64 checksum of the written bytes. +/// +/// ``` +/// use std::io::Write; +/// +/// use snapshot::crc::CRC64Writer; +/// +/// let mut buf = vec![0; 16]; +/// let write_buf = vec![123; 16]; +/// let mut slice = buf.as_mut_slice(); +/// +/// // Create a new writer from slice. +/// let mut crc_writer = CRC64Writer::new(&mut slice); +/// +/// crc_writer.write_all(&write_buf.as_slice()).unwrap(); +/// assert_eq!(crc_writer.checksum(), 0x29D5_3572_1632_6566); +/// assert_eq!(write_buf, buf); +/// ``` +#[derive(Debug)] +pub struct CRC64Writer { + writer: T, + crc64: u64, +} + +impl CRC64Writer +where + T: Write, +{ + /// Create a new writer. + pub fn new(writer: T) -> Self { + CRC64Writer { crc64: 0, writer } + } + + /// Returns the current checksum value. + pub fn checksum(&self) -> u64 { + self.crc64 + } +} + +impl Write for CRC64Writer +where + T: Write, +{ + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let bytes_written = self.writer.write(buf)?; + self.crc64 = crc64(self.crc64, &buf[..bytes_written]); + Ok(bytes_written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} + +#[cfg(test)] +mod tests { + use super::{CRC64Reader, CRC64Writer, Read, Write}; + + #[test] + fn test_crc_new() { + let buf = vec![1; 5]; + let mut slice = buf.as_slice(); + let crc_reader = CRC64Reader::new(&mut slice); + assert_eq!(crc_reader.crc64, 0); + assert_eq!(crc_reader.reader, &[1; 5]); + assert_eq!(crc_reader.checksum(), 0); + + let mut buf = vec![0; 5]; + let mut slice = buf.as_mut_slice(); + let crc_writer = CRC64Writer::new(&mut slice); + assert_eq!(crc_writer.crc64, 0); + assert_eq!(crc_writer.writer, &[0; 5]); + assert_eq!(crc_writer.checksum(), 0); + } + + #[test] + fn test_crc_read() { + let buf = vec![1, 2, 3, 4, 5]; + let mut read_buf = vec![0; 16]; + + let mut slice = buf.as_slice(); + let mut crc_reader = CRC64Reader::new(&mut slice); + crc_reader.read_to_end(&mut read_buf).unwrap(); + assert_eq!(crc_reader.checksum(), 0xFB04_60DE_0638_3654); + assert_eq!(crc_reader.checksum(), crc_reader.crc64); + } + + #[test] + fn test_crc_write() { + let mut buf = vec![0; 16]; + let write_buf = vec![123; 16]; + + let mut slice = buf.as_mut_slice(); + let mut crc_writer = CRC64Writer::new(&mut slice); + crc_writer.write_all(write_buf.as_slice()).unwrap(); + crc_writer.flush().unwrap(); + assert_eq!(crc_writer.checksum(), 0x29D5_3572_1632_6566); + assert_eq!(crc_writer.checksum(), crc_writer.crc64); + } +} diff --git a/src/snapshot/src/lib.rs b/src/snapshot/src/lib.rs index 633e0c9f1fd..1072436f78a 100644 --- a/src/snapshot/src/lib.rs +++ b/src/snapshot/src/lib.rs @@ -1,47 +1,47 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 #![warn(missing_docs)] -//! Provides version tolerant serialization and deserialization facilities and -//! implements a persistent storage format for Firecracker state snapshots. +//! Provides serialization and deserialization facilities and implements a persistent storage +//! format for Firecracker state snapshots. //! //! The `Snapshot` API manages serialization and deserialization of collections of objects -//! that implement the `Versionize` trait. +//! that implement the `serde` `Serialize`, `Deserialize` trait. Currently, we use +//! [`bincode`](https://docs.rs/bincode/latest/bincode/) for performing the serialization. //! -//! |----------------------------| -//! | 64 bit magic_id | -//! |----------------------------| -//! | SnapshotHdr | -//! |----------------------------| -//! | State | -//! |----------------------------| -//! | optional CRC64 | -//! |----------------------------| +//! The snapshot format uses the following layout: //! -//! Each structure, union or enum is versioned separately and only needs to increment their version -//! if a field is added or removed. For each state snapshot we define 2 versions: -//! - **the format version** which refers to the SnapshotHdr, CRC, or the representation of -//! primitives types (currently we use versionize that uses serde bincode as a backend). The current -//! implementation does not have any logic dependent on it. -//! - **the data version** which refers to the state. +//! |-----------------------------| +//! | 64 bit magic_id | +//! |-----------------------------| +//! | version string | +//! |-----------------------------| +//! | State | +//! |-----------------------------| +//! | optional CRC64 | +//! |-----------------------------| +//! +//! +//! The snapshot format uses a version value in the form of `MAJOR.MINOR.PATCH`. The version is +//! provided by the library clients (it is not tied to this crate). +pub mod crc; mod persist; use std::fmt::Debug; use std::io::{Read, Write}; -use versionize::crc::{CRC64Reader, CRC64Writer}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; +use semver::Version; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use crate::crc::{CRC64Reader, CRC64Writer}; pub use crate::persist::Persist; -const BASE_MAGIC_ID_MASK: u64 = !0xFFFFu64; - #[cfg(target_arch = "x86_64")] -const BASE_MAGIC_ID: u64 = 0x0710_1984_8664_0000u64; +const SNAPSHOT_MAGIC_ID: u64 = 0x0710_1984_8664_0000u64; #[cfg(target_arch = "aarch64")] -const BASE_MAGIC_ID: u64 = 0x0710_1984_AAAA_0000u64; +const SNAPSHOT_MAGIC_ID: u64 = 0x0710_1984_AAAA_0000u64; /// Error definitions for the Snapshot API. #[derive(Debug, thiserror::Error, displaydoc::Display, PartialEq)] @@ -49,106 +49,108 @@ pub enum Error { /// CRC64 validation failed: {0} Crc64(u64), /// Invalid data version: {0} - InvalidDataVersion(u16), - /// Invalid format version: {0} - InvalidFormatVersion(u16), + InvalidFormatVersion(Version), /// Magic value does not match arch: {0} InvalidMagic(u64), /// Snapshot file is smaller than CRC length. InvalidSnapshotSize, /// An IO error occurred: {0} Io(i32), - /// A versioned serialization/deserialization error occurred: {0} - Versionize(versionize::VersionizeError), + /// An error occured with serialization/deserialization: {0} + Serde(String), } -#[derive(Default, Debug, Versionize)] +/// Firecracker snapshot header +#[derive(Debug, Serialize, Deserialize)] struct SnapshotHdr { - /// Snapshot data version (firecracker version). - data_version: u16, -} - -/// The `Snapshot` API manages serialization and deserialization of collections of objects -/// that implement the `Versionize` trait. -#[derive(Debug)] -pub struct Snapshot { - hdr: SnapshotHdr, - version_map: VersionMap, - // Required for serialization. - target_version: u16, + /// magic value + magic: u64, + /// Snapshot data version + version: Version, } -// Parse a magic_id and return the format version. -fn get_format_version(magic_id: u64) -> Result { - let magic_arch = magic_id & BASE_MAGIC_ID_MASK; - if magic_arch == BASE_MAGIC_ID { - return Ok((magic_id & !BASE_MAGIC_ID_MASK) as u16); +impl SnapshotHdr { + fn new(version: Version) -> Self { + Self { + magic: SNAPSHOT_MAGIC_ID, + version, + } } - Err(Error::InvalidMagic(magic_id)) } -fn build_magic_id(format_version: u16) -> u64 { - BASE_MAGIC_ID | u64::from(format_version) +/// Firecracker snapshot type +/// +/// A type used to store and load Firecracker snapshots of a particular version +#[derive(Debug)] +pub struct Snapshot { + // The snapshot version we can handle + version: Version, } impl Snapshot { /// Creates a new instance which can only be used to save a new snapshot. - pub fn new(version_map: VersionMap, target_version: u16) -> Snapshot { - Snapshot { - version_map, - hdr: SnapshotHdr::default(), - target_version, - } + pub fn new(version: Version) -> Snapshot { + Snapshot { version } } /// Fetches snapshot data version. - pub fn get_data_version(mut reader: &mut T, version_map: &VersionMap) -> Result + pub fn get_format_version(reader: &mut T) -> Result where T: Read + Debug, { - let format_version_map = Self::format_version_map(); - let magic_id = - ::deserialize(&mut reader, &format_version_map, 0 /* unused */) - .map_err(Error::Versionize)?; - - let format_version = get_format_version(magic_id)?; - if format_version > format_version_map.latest_version() || format_version == 0 { - return Err(Error::InvalidFormatVersion(format_version)); - } + let hdr: SnapshotHdr = Self::deserialize(reader)?; + Ok(hdr.version) + } - let hdr: SnapshotHdr = - SnapshotHdr::deserialize(&mut reader, &format_version_map, format_version) - .map_err(Error::Versionize)?; - if hdr.data_version > version_map.latest_version() || hdr.data_version == 0 { - return Err(Error::InvalidDataVersion(hdr.data_version)); - } + /// Helper function to deserialize an object from a reader + pub fn deserialize(reader: &mut T) -> Result + where + T: Read, + O: DeserializeOwned + Debug, + { + bincode::deserialize_from(reader).map_err(|err| Error::Serde(err.to_string())) + } - Ok(hdr.data_version) + /// Helper function to serialize an object to a writer + pub fn serialize(writer: &mut T, data: &O) -> Result<(), Error> + where + T: Write, + O: Serialize + Debug, + { + bincode::serialize_into(writer, data).map_err(|err| Error::Serde(err.to_string())) } - /// Attempts to load an existing snapshot without CRC validation. - pub fn unchecked_load( - mut reader: &mut T, - version_map: VersionMap, - ) -> Result<(O, u16), Error> { - let data_version = Self::get_data_version(&mut reader, &version_map)?; - let res = - O::deserialize(&mut reader, &version_map, data_version).map_err(Error::Versionize)?; - Ok((res, data_version)) + /// Attempts to load an existing snapshot without performing CRC or version validation. + /// + /// This will check that the snapshot magic value is correct. + fn unchecked_load(reader: &mut T) -> Result<(O, Version), Error> + where + T: Read + Debug, + O: DeserializeOwned + Debug, + { + let hdr: SnapshotHdr = Self::deserialize(reader)?; + if hdr.magic != SNAPSHOT_MAGIC_ID { + return Err(Error::InvalidMagic(hdr.magic)); + } + + let data: O = Self::deserialize(reader)?; + Ok((data, hdr.version)) } - /// Attempts to load an existing snapshot and validate CRC. - pub fn load( - reader: &mut T, - snapshot_len: usize, - version_map: VersionMap, - ) -> Result<(O, u16), Error> { + /// Load a snapshot from a reader and validate its CRC + pub fn load(reader: &mut T, snapshot_len: usize) -> Result<(O, Version), Error> + where + T: Read + Debug, + O: DeserializeOwned + Debug, + { let mut crc_reader = CRC64Reader::new(reader); - // Extract snapshot data without stored checksum, which is 8 bytes in size + // Fail-fast if the snapshot length is too small let raw_snapshot_len = snapshot_len .checked_sub(std::mem::size_of::()) .ok_or(Error::InvalidSnapshotSize)?; + + // Read everything apart from the CRC. let mut snapshot = vec![0u8; raw_snapshot_len]; crc_reader .read_exact(&mut snapshot) @@ -158,79 +160,57 @@ impl Snapshot { // 2 statements is important, we first get the checksum computed on the read bytes // then read the stored checksum. let computed_checksum = crc_reader.checksum(); - let format_vm = Self::format_version_map(); - let stored_checksum: u64 = - Versionize::deserialize(&mut crc_reader, &format_vm, 0).map_err(Error::Versionize)?; + let stored_checksum: u64 = Self::deserialize(&mut crc_reader)?; if computed_checksum != stored_checksum { return Err(Error::Crc64(computed_checksum)); } let mut snapshot_slice: &[u8] = snapshot.as_mut_slice(); - Snapshot::unchecked_load::<_, O>(&mut snapshot_slice, version_map) + Snapshot::unchecked_load::<_, O>(&mut snapshot_slice) + } + + /// Load a snapshot from a reader object and perform a snapshot version check + pub fn load_with_version_check( + &self, + reader: &mut T, + snapshot_len: usize, + ) -> Result + where + T: Read + Debug, + O: DeserializeOwned + Debug, + { + let (data, version) = Snapshot::load::<_, O>(reader, snapshot_len)?; + if version.major != self.version.major || version.minor > self.version.minor { + Err(Error::InvalidFormatVersion(version)) + } else { + Ok(data) + } } /// Saves a snapshot and include a CRC64 checksum. - pub fn save(&mut self, writer: &mut T, object: &O) -> Result<(), Error> + pub fn save(&self, writer: &mut T, object: &O) -> Result<(), Error> where T: Write + Debug, - O: Versionize + Debug, + O: Serialize + Debug, { let mut crc_writer = CRC64Writer::new(writer); self.save_without_crc(&mut crc_writer, object)?; + // Now write CRC value let checksum = crc_writer.checksum(); - checksum - .serialize(&mut crc_writer, &Self::format_version_map(), 0) - .map_err(Error::Versionize)?; - Ok(()) + Self::serialize(&mut crc_writer, &checksum) } - // TODO Remove `skip(crc_writer)` when https://github.com/firecracker-microvm/versionize/pull/59 - // is merged and included. /// Save a snapshot with no CRC64 checksum included. - pub fn save_without_crc(&mut self, mut writer: &mut T, object: &O) -> Result<(), Error> + pub fn save_without_crc(&self, mut writer: &mut T, object: &O) -> Result<(), Error> where T: Write, - O: Versionize + Debug, + O: Serialize + Debug, { - self.hdr = SnapshotHdr { - data_version: self.target_version, - }; - - let format_version_map = Self::format_version_map(); - let magic_id = build_magic_id(format_version_map.latest_version()); - - // Serialize magic id using the format version map. - magic_id - .serialize(&mut writer, &format_version_map, 0 /* unused */) - .map_err(Error::Versionize)?; - - // Serialize header using the format version map. - self.hdr - .serialize( - &mut writer, - &format_version_map, - format_version_map.latest_version(), - ) - .map_err(Error::Versionize)?; - - // Serialize the object using the state version map. - object - .serialize(&mut writer, &self.version_map, self.target_version) - .map_err(Error::Versionize)?; - writer - .flush() - .map_err(|ref err| Error::Io(err.raw_os_error().unwrap_or(libc::EINVAL))) - } - - // Returns the current snapshot format version. - // Not to be confused with data version which refers to the aplication - // defined structures. - // This version map allows us to change the underlying storage format - - // for example the way we encode vectors or moving to something else than bincode. - fn format_version_map() -> VersionMap { - // Firecracker snapshot format version 1. - VersionMap::new() + // Write magic value and snapshot version + Self::serialize(&mut writer, &SnapshotHdr::new(self.version.clone()))?; + // Write data + Self::serialize(&mut writer, object) } } @@ -238,352 +218,157 @@ impl Snapshot { mod tests { use super::*; - #[derive(Clone, Debug, Versionize)] - pub struct Test1 { - field_x: u64, - field0: u64, - field1: u32, - } - - #[derive(Clone, Debug, Versionize)] - pub struct Test { - field_x: u64, - field0: u64, - field1: u32, - #[version(start = 2, default_fn = "field2_default")] - field2: u64, - #[version( - start = 3, - default_fn = "field3_default", - ser_fn = "field3_serialize", - de_fn = "field3_deserialize" - )] - field3: String, - #[version( - start = 4, - default_fn = "field4_default", - ser_fn = "field4_serialize", - de_fn = "field4_deserialize" - )] - field4: Vec, - } - - impl Test { - fn field2_default(_: u16) -> u64 { - 20 - } - fn field3_default(_: u16) -> String { - "default".to_owned() - } - fn field4_default(_: u16) -> Vec { - vec![1, 2, 3, 4] - } - fn field4_serialize(&mut self, target_version: u16) -> VersionizeResult<()> { - // Fail if semantic serialization is called for the latest version. - assert_ne!(target_version, Test::version()); - self.field0 = self.field4.iter().sum(); - - if self.field0 == 6666 { - return Err(versionize::VersionizeError::Semantic( - "field4 element sum is 6666".to_owned(), - )); - } - Ok(()) - } - fn field4_deserialize(&mut self, source_version: u16) -> VersionizeResult<()> { - // Fail if semantic deserialization is called for the latest version. - assert_ne!(source_version, Test::version()); - self.field4 = vec![self.field0; 4]; - Ok(()) - } - - fn field3_serialize(&mut self, target_version: u16) -> VersionizeResult<()> { - // Fail if semantic serialization is called for the previous versions only. - assert!(target_version < 3); - self.field_x += 1; - Ok(()) - } - - fn field3_deserialize(&mut self, source_version: u16) -> VersionizeResult<()> { - // Fail if semantic deserialization is called for the latest version. - assert!(source_version < 3); - self.field_x += 1; - if self.field0 == 7777 { - return Err(versionize::VersionizeError::Semantic( - "field0 is 7777".to_owned(), - )); - } - Ok(()) - } - } - #[test] - fn test_get_format_version() { - // Check if `get_format_version()` returns indeed the format - // version (the least significant 2 bytes) if the id is valid - // (the other bytes == BASE_MAGIC_ID). - #[cfg(target_arch = "x86_64")] - let good_magic_id = 0x0710_1984_8664_0001u64; - #[cfg(target_arch = "aarch64")] - let good_magic_id = 0x0710_1984_AAAA_0001u64; - - assert_eq!(get_format_version(good_magic_id).unwrap(), 1u16); - - // Flip a bit to invalidate the arch id. - let invalid_magic_id = good_magic_id | (1u64 << 63); - assert_eq!( - get_format_version(invalid_magic_id).unwrap_err(), - Error::InvalidMagic(invalid_magic_id) - ); - } - - #[test] - fn test_struct_semantic_fn() { - let mut vm = VersionMap::new(); - vm.new_version() - .set_type_version(Test::type_id(), 2) - .new_version() - .set_type_version(Test::type_id(), 3) - .new_version() - .set_type_version(Test::type_id(), 4); - let state = Test { - field0: 0, - field1: 1, - field2: 2, - field3: "test".to_owned(), - field4: vec![4, 3, 2, 1], - field_x: 0, - }; - - let mut snapshot_mem = vec![0u8; 1024]; - - // Serialize as v1. - let mut snapshot = Snapshot::new(vm.clone(), 1); - snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) - .unwrap(); + fn test_parse_version_from_file() { + let snapshot = Snapshot::new(Version::new(1, 0, 42)); - let (mut restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - - // The semantic serializer fn for field4 will set field0 to field4.iter().sum() == 10. - assert_eq!(restored_state.field0, state.field4.iter().sum::()); - // The semantic deserializer for field4 will change field's value to vec![field0; 4]. - assert_eq!(restored_state.field4, vec![restored_state.field0; 4]); - // The semantic serializer and deserializer for field3 will both increment field_x value. - assert_eq!(restored_state.field_x, 2); - // field1 should have the original value. - assert_eq!(restored_state.field1, 1); - // field2 should have the default value as this field was added at version 2. - assert_eq!(restored_state.field2, 20); - - // Serialize as v3. - let mut snapshot = Snapshot::new(vm.clone(), 3); - snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) - .unwrap(); - - (restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - - // We expect only the semantic serializer and deserializer for field4 to be called at - // version 3. The semantic serializer will set field0 to field4.iter().sum() == 10. - assert_eq!(restored_state.field0, state.field4.iter().sum::()); - // The semantic deserializer will create a 4 elements vec with all values == field0. - assert_eq!(restored_state.field4, vec![restored_state.field0; 4]); - // The semantic fn for field3 must not be called at version 3. - assert_eq!(restored_state.field_x, 0); + // Enough memory for the header, 1 byte and the CRC + let mut snapshot_data = vec![0u8; 100]; - // Serialize as v4. - snapshot = Snapshot::new(vm.clone(), 4); snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) + .save(&mut snapshot_data.as_mut_slice(), &42u8) .unwrap(); - (restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - - // The 4 semantic fns must not be called at version 4. - assert_eq!(restored_state.field0, 0); - assert_eq!(restored_state.field4, vec![4, 3, 2, 1]); - - // Test error propagation from `versionize` crate. - // Load operation should fail if we don't use the whole `snapshot_mem` resulted from - // serialization. - snapshot_mem.truncate(10); - let restored_state_result: Result<(Test, _), Error> = - Snapshot::unchecked_load(&mut snapshot_mem.as_slice(), vm); - assert_eq!( - restored_state_result.unwrap_err(), - Error::Versionize(versionize::VersionizeError::Deserialize(String::from( - "Io(Error { kind: UnexpectedEof, message: \"failed to fill whole buffer\" })" - ))) + Snapshot::get_format_version(&mut snapshot_data.as_slice()).unwrap(), + Version::new(1, 0, 42) ); } #[test] - fn test_struct_default_fn() { - let mut vm = VersionMap::new(); - vm.new_version() - .set_type_version(Test::type_id(), 2) - .new_version() - .set_type_version(Test::type_id(), 3) - .new_version() - .set_type_version(Test::type_id(), 4); - let state = Test { - field0: 0, - field1: 1, - field2: 2, - field3: "test".to_owned(), - field4: vec![4, 3, 2, 1], - field_x: 0, - }; - - let state_1 = Test1 { - field_x: 0, - field0: 0, - field1: 1, - }; - - let mut snapshot_mem = vec![0u8; 1024]; - - // Serialize as v1. - let mut snapshot = Snapshot::new(vm.clone(), 1); - snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state_1) - .unwrap(); - - let (mut restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - assert_eq!(restored_state.field1, state_1.field1); - assert_eq!(restored_state.field2, 20); - assert_eq!(restored_state.field3, "default"); - - // Serialize as v2. - snapshot = Snapshot::new(vm.clone(), 2); - snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) - .unwrap(); + fn test_bad_snapshot_size() { + let snapshot_data = vec![0u8; 1]; + + let snapshot = Snapshot::new(Version::new(1, 6, 1)); + assert!(matches!( + snapshot.load_with_version_check::<_, u8>( + &mut snapshot_data.as_slice(), + snapshot_data.len() + ), + Err(Error::InvalidSnapshotSize) + )); + } - (restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - assert_eq!(restored_state.field1, state.field1); - assert_eq!(restored_state.field2, 2); - assert_eq!(restored_state.field3, "default"); + #[test] + fn test_bad_reader() { + #[derive(Debug)] + struct BadReader; - // Serialize as v3. - snapshot = Snapshot::new(vm.clone(), 3); - snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) - .unwrap(); + impl Read for BadReader { + fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { + Err(std::io::ErrorKind::InvalidInput.into()) + } + } - (restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - assert_eq!(restored_state.field1, state.field1); - assert_eq!(restored_state.field2, 2); - assert_eq!(restored_state.field3, "test"); + let mut reader = BadReader {}; - // Serialize as v4. - snapshot = Snapshot::new(vm.clone(), 4); - snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) - .unwrap(); - - (restored_state, _) = - Snapshot::unchecked_load::<_, Test>(&mut snapshot_mem.as_slice(), vm.clone()).unwrap(); - assert_eq!(restored_state.field1, state.field1); - assert_eq!(restored_state.field2, 2); - assert_eq!(restored_state.field3, "test"); + let snapshot = Snapshot::new(Version::new(42, 27, 18)); + assert!(matches!( + snapshot.load_with_version_check::<_, u8>(&mut reader, 1024), + Err(Error::Io(_)) + )); } #[test] - fn test_crc_ok() { - let vm = VersionMap::new(); - let state_1 = Test1 { - field_x: 0, - field0: 0, - field1: 1, - }; - - let mut snapshot_mem = vec![0u8; 1024]; - - // Serialize as v1. - let mut snapshot = Snapshot::new(vm.clone(), 1); - snapshot - .save(&mut snapshot_mem.as_mut_slice(), &state_1) - .unwrap(); - - let _ = Snapshot::load::<_, Test1>(&mut snapshot_mem.as_slice(), 38, vm).unwrap(); + fn test_bad_magic() { + let mut data = vec![0u8; 100]; + + let snapshot = Snapshot::new(Version::new(24, 16, 1)); + snapshot.save(&mut data.as_mut_slice(), &42u8).unwrap(); + + // Writing dummy values in the first bytes of the snapshot data (we are on little-endian + // machines) should trigger an `Error::InvalidMagic` error. + data[0] = 0x01; + data[1] = 0x02; + data[2] = 0x03; + data[3] = 0x04; + data[4] = 0x42; + data[5] = 0x43; + data[6] = 0x44; + data[7] = 0x45; + assert!(matches!( + Snapshot::unchecked_load::<_, u8>(&mut data.as_slice()), + Err(Error::InvalidMagic(0x4544_4342_0403_0201u64)) + )); } #[test] - fn test_invalid_snapshot_size() { - let vm = VersionMap::new(); - // Create a snapshot shorter than CRC length. - let snapshot_mem = vec![0u8; 4]; - let expected_err = Error::InvalidSnapshotSize; - let load_result: Result<(Test1, _), Error> = - Snapshot::load(&mut snapshot_mem.as_slice(), 4, vm); - assert_eq!(load_result.unwrap_err(), expected_err); - } + fn test_bad_crc() { + let mut data = vec![0u8; 100]; - #[test] - fn test_corrupted_snapshot() { - let vm = VersionMap::new(); - let state_1 = Test1 { - field_x: 0, - field0: 0, - field1: 1, - }; - - let mut snapshot_mem = vec![0u8; 1024]; - - // Serialize as v1. - let mut snapshot = Snapshot::new(vm.clone(), 1); + let snapshot = Snapshot::new(Version::new(12, 1, 3)); + snapshot.save(&mut data.as_mut_slice(), &42u8).unwrap(); + + // Tamper the bytes written, without touching the previously CRC. snapshot - .save(&mut snapshot_mem.as_mut_slice(), &state_1) + .save_without_crc(&mut data.as_mut_slice(), &43u8) .unwrap(); - snapshot_mem[20] = 123; - #[cfg(target_arch = "aarch64")] - let expected_err = Error::Crc64(0x1960_4E6A_A13F_6615); - #[cfg(target_arch = "x86_64")] - let expected_err = Error::Crc64(0x103F_8F52_8F51_20B1); - - let load_result: Result<(Test1, _), Error> = - Snapshot::load(&mut snapshot_mem.as_slice(), 38, vm); - assert_eq!(load_result.unwrap_err(), expected_err); + assert!(matches!( + snapshot.load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()), + Err(Error::Crc64(_)) + )); } - #[allow(non_upper_case_globals)] - #[allow(non_camel_case_types)] - #[allow(non_snake_case)] #[test] - fn test_kvm_bindings_struct() { - #[repr(C)] - #[derive(Debug, PartialEq, Eq, Versionize)] - pub struct kvm_pit_config { - pub flags: ::std::os::raw::c_uint, - pub pad: [::std::os::raw::c_uint; 15usize], - } - - let state = kvm_pit_config { - flags: 123_456, - pad: [0; 15usize], - }; - - let vm = VersionMap::new(); - let mut snapshot_mem = vec![0u8; 1024]; - // Serialize as v1. - let mut snapshot = Snapshot::new(vm.clone(), 1); + fn test_bad_version() { + let mut data = vec![0u8; 100]; + + // We write a snapshot with version "v1.3.12" + let snapshot = Snapshot::new(Version::new(1, 3, 12)); + snapshot.save(&mut data.as_mut_slice(), &42u8).unwrap(); + + // Different major versions should not work + let snapshot = Snapshot::new(Version::new(2, 3, 12)); + assert!(matches!( + snapshot.load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()), + Err(Error::InvalidFormatVersion(Version { + major: 1, + minor: 3, + patch: 12, + .. + })) + )); + let snapshot = Snapshot::new(Version::new(0, 3, 12)); + assert!(matches!( + snapshot.load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()), + Err(Error::InvalidFormatVersion(Version { + major: 1, + minor: 3, + patch: 12, + .. + })) + )); + + // We can't support minor versions bigger than ours + let snapshot = Snapshot::new(Version::new(1, 2, 12)); + assert!(matches!( + snapshot.load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()), + Err(Error::InvalidFormatVersion(Version { + major: 1, + minor: 3, + patch: 12, + .. + })) + )); + + // But we can support minor versions smaller or equeal to ours. We also support + // all patch versions within our supported major.minor version. + let snapshot = Snapshot::new(Version::new(1, 4, 12)); snapshot - .save_without_crc(&mut snapshot_mem.as_mut_slice(), &state) + .load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()) + .unwrap(); + let snapshot = Snapshot::new(Version::new(1, 3, 0)); + snapshot + .load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()) + .unwrap(); + let snapshot = Snapshot::new(Version::new(1, 3, 12)); + snapshot + .load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()) + .unwrap(); + let snapshot = Snapshot::new(Version::new(1, 3, 1024)); + snapshot + .load_with_version_check::<_, u8>(&mut data.as_slice(), data.len()) .unwrap(); - - let (restored_state, _) = - Snapshot::unchecked_load::<_, kvm_pit_config>(&mut snapshot_mem.as_slice(), vm) - .unwrap(); - assert_eq!(restored_state, state); } } diff --git a/src/snapshot/tests/test.rs b/src/snapshot/tests/test.rs deleted file mode 100644 index 37d552eed12..00000000000 --- a/src/snapshot/tests/test.rs +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use snapshot::{Error, Snapshot}; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; - -#[derive(Debug, PartialEq, Eq, Versionize)] -pub enum TestState { - Zero, - One(u32), - #[version(start = 2, default_fn = "default_state_two")] - Two(u64), -} - -impl TestState { - fn default_state_two(&self, target_version: u16) -> VersionizeResult { - match target_version { - 1 => Ok(TestState::One(2)), - i => Err(VersionizeError::Serialize(format!( - "Unknown target version: {}", - i - ))), - } - } -} - -#[derive(Debug, PartialEq, Eq, Versionize)] -pub struct A { - a: u32, - #[version(start = 1, end = 2)] - b: Option, - #[version(start = 2, default_fn = "default_c")] - c: String, -} - -impl A { - fn default_c(_source_version: u16) -> String { - "some_string".to_owned() - } -} - -#[test] -fn test_hardcoded_snapshot_deserialization() { - // We are testing representation compatibility between versions, at the `snapshot` crate - // level, by checking that only the version number and the newly added/removed fields changes - // between versions are reflected in the hardcoded snapshot. - - #[rustfmt::skip] - let v1_hardcoded_snapshot: &[u8] = &[ - // This blob consists of the following: magic_id (8 bytes), - 0x01, 0x00, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "x86_64")] - 0x64, - #[cfg(target_arch = "x86_64")] - 0x86, - 0x84, 0x19, 0x10, 0x07, - // target version (2 bytes) + - 0x01, 0x00, - // `a` field + - 0x10, 0x00, 0x00, 0x00, - // `b` field: Option variant type (1 byte) + inner enum variant type (4 bytes) - // + inner enum value (4 bytes). - 0x01, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - ]; - - #[rustfmt::skip] - let v2_hardcoded_snapshot: &[u8] = &[ - 0x01, - 0x00, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "x86_64")] - 0x64, - #[cfg(target_arch = "x86_64")] - 0x86, 0x84, 0x19, 0x10, 0x07, - // Version 2 + - 0x02, 0x00, - // `a` field + - 0x10, 0x00, 0x00, 0x00, - // `c` field: String len (8 bytes) + actual String; the Option field is not available at v2. - 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x61, 0x6E, 0x64, 0x6F, 0x6D, - ]; - - let mut vm = VersionMap::new(); - vm.new_version() - .set_type_version(A::type_id(), 2) - .set_type_version(TestState::type_id(), 2); - - let mut snapshot_blob = v1_hardcoded_snapshot; - - let (mut restored_struct, _) = - Snapshot::unchecked_load::<_, A>(&mut snapshot_blob, vm.clone()).unwrap(); - - let mut expected_struct = A { - a: 16u32, - b: Some(TestState::One(2)), - c: "some_string".to_owned(), - }; - - assert_eq!(restored_struct, expected_struct); - - snapshot_blob = v2_hardcoded_snapshot; - - (restored_struct, _) = - Snapshot::unchecked_load::<_, A>(&mut snapshot_blob, vm.clone()).unwrap(); - - expected_struct = A { - a: 16u32, - b: None, - c: "random".to_owned(), - }; - - assert_eq!(restored_struct, expected_struct); -} - -#[test] -fn test_invalid_format_version() { - #[rustfmt::skip] - let mut invalid_format_snap: &[u8] = &[ - // This blob consists of the following: magic_id (8 bytes), - 0xAA, 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "x86_64")] - 0x64, - #[cfg(target_arch = "x86_64")] - 0x86, - 0x84, 0x19, 0x10, 0x07, - // target version (2 bytes) + - 0x01, 0x00, - // `a` field + - 0x10, 0x00, 0x00, 0x00, - // `b` field: Option variant type (1 byte) + inner enum variant type (4 bytes) - // + inner enum value (4 bytes). - 0x01, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - ]; - - let mut result: Result<(A, _), Error> = - Snapshot::unchecked_load(&mut invalid_format_snap, VersionMap::new()); - let mut expected_err = Error::InvalidFormatVersion(0xAAAA); - assert_eq!(result.unwrap_err(), expected_err); - - #[rustfmt::skip] - let mut null_format_snap: &[u8] = &[ - // This blob consists of the following: magic_id (8 bytes), - 0x00, 0x00, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "x86_64")] - 0x64, - #[cfg(target_arch = "x86_64")] - 0x86, - 0x84, 0x19, 0x10, 0x07, - // target version (2 bytes) + - 0x01, 0x00, - // `a` field + - 0x10, 0x00, 0x00, 0x00, - // `b` field: Option variant type (1 byte) + inner enum variant type (4 bytes) - // + inner enum value (4 bytes). - 0x01, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - ]; - - result = Snapshot::unchecked_load(&mut null_format_snap, VersionMap::new()); - expected_err = Error::InvalidFormatVersion(0); - assert_eq!(result.unwrap_err(), expected_err); -} - -#[test] -fn test_invalid_data_version() { - #[rustfmt::skip] - let mut invalid_data_version_snap: &[u8] = &[ - // This blob consists of the following: magic_id (8 bytes), - 0x01, 0x00, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "x86_64")] - 0x64, - #[cfg(target_arch = "x86_64")] - 0x86, - 0x84, 0x19, 0x10, 0x07, - // target version (2 bytes) + - 0xAA, 0xAA, - // `a` field + - 0x10, 0x00, 0x00, 0x00, - // `b` field: Option variant type (1 byte) + inner enum variant type (4 bytes) - // + inner enum value (4 bytes). - 0x01, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - ]; - let mut result: Result<(A, _), Error> = - Snapshot::unchecked_load(&mut invalid_data_version_snap, VersionMap::new()); - let mut expected_err = Error::InvalidDataVersion(0xAAAA); - assert_eq!(result.unwrap_err(), expected_err); - - #[rustfmt::skip] - let mut null_data_version_snap: &[u8] = &[ - // This blob consists of the following: magic_id (8 bytes), - 0x01, 0x00, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "aarch64")] - 0xAA, - #[cfg(target_arch = "x86_64")] - 0x64, - #[cfg(target_arch = "x86_64")] - 0x86, - 0x84, 0x19, 0x10, 0x07, - // target version (2 bytes) + - 0x00, 0x00, - // `a` field + - 0x10, 0x00, 0x00, 0x00, - // `b` field: Option variant type (1 byte) + inner enum variant type (4 bytes) - // + inner enum value (4 bytes). - 0x01, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - ]; - result = Snapshot::unchecked_load(&mut null_data_version_snap, VersionMap::new()); - expected_err = Error::InvalidDataVersion(0); - assert_eq!(result.unwrap_err(), expected_err); -} diff --git a/src/vmm/src/lib.rs b/src/vmm/src/lib.rs index 7126a59b48d..06a1e50f422 100644 --- a/src/vmm/src/lib.rs +++ b/src/vmm/src/lib.rs @@ -99,8 +99,6 @@ pub mod seccomp_filters; pub mod signal_handler; /// Utility functions for integration and benchmark testing pub mod utilities; -/// microVM state versions. -pub mod version_map; /// Wrappers over structures used to configure the VMM. pub mod vmm_config; /// Module with virtual state structs. diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index 864041c7d96..092490dbbc4 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -12,7 +12,8 @@ use std::path::Path; use std::sync::{Arc, Mutex}; use seccompiler::BpfThreadMap; -use serde::Serialize; +use semver::Version; +use serde::{Deserialize, Serialize}; use snapshot::Snapshot; use userfaultfd::{FeatureFlags, Uffd, UffdBuilder}; use utils::sock_ctrl_msg::ScmSocket; @@ -93,18 +94,22 @@ impl From<&VmResources> for VmInfo { } /// Contains the necesary state for saving/restoring a microVM. -#[derive(Debug, Default, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Debug, Default, Serialize, Deserialize)] pub struct MicrovmState { /// Miscellaneous VM info. + #[serde(skip)] pub vm_info: VmInfo, /// Memory state. + #[serde(skip)] pub memory_state: GuestMemoryState, /// VM KVM state. + #[serde(skip)] pub vm_state: VmState, /// Vcpu states. + #[serde(skip)] pub vcpu_states: Vec, /// Device states. + #[serde(skip)] pub device_states: DeviceStates, } @@ -158,10 +163,7 @@ pub enum MicrovmStateError { pub enum CreateSnapshotError { /// Cannot get dirty bitmap: {0} DirtyBitmap(VmmError), - /// The virtio devices use a features that is incompatible with older versions of Firecracker: {0} - IncompatibleVirtioFeature(&'static str), - /// Invalid microVM version format - InvalidVersionFormat, + #[rustfmt::skip] /// Cannot translate microVM version to snapshot data version UnsupportedVersion, /// Cannot write memory file: {0} @@ -181,25 +183,20 @@ pub enum CreateSnapshotError { TooManyDevices(usize), } +/// Snapshot version +pub const SNAPSHOT_VERSION: Version = Version::new(1, 0, 0); + /// Creates a Microvm snapshot. pub fn create_snapshot( vmm: &mut Vmm, vm_info: &VmInfo, params: &CreateSnapshotParams, - version_map: VersionMap, ) -> Result<(), CreateSnapshotError> { - let snapshot_data_version = version_map.latest_version(); - let microvm_state = vmm .save_state(vm_info) .map_err(CreateSnapshotError::MicrovmState)?; - snapshot_state_to_file( - µvm_state, - ¶ms.snapshot_path, - snapshot_data_version, - version_map, - )?; + snapshot_state_to_file(µvm_state, ¶ms.snapshot_path)?; snapshot_memory_to_file(vmm, ¶ms.mem_file_path, params.snapshot_type)?; @@ -209,8 +206,6 @@ pub fn create_snapshot( fn snapshot_state_to_file( microvm_state: &MicrovmState, snapshot_path: &Path, - snapshot_data_version: u16, - version_map: VersionMap, ) -> Result<(), CreateSnapshotError> { use self::CreateSnapshotError::*; let mut snapshot_file = OpenOptions::new() @@ -220,7 +215,7 @@ fn snapshot_state_to_file( .open(snapshot_path) .map_err(|err| SnapshotBackingFile("open", err))?; - let mut snapshot = Snapshot::new(version_map, snapshot_data_version); + let snapshot = Snapshot::new(SNAPSHOT_VERSION); snapshot .save(&mut snapshot_file, microvm_state) .map_err(SerializeMicrovmState)?; @@ -428,10 +423,9 @@ pub fn restore_from_snapshot( event_manager: &mut EventManager, seccomp_filters: &BpfThreadMap, params: &LoadSnapshotParams, - version_map: VersionMap, vm_resources: &mut VmResources, ) -> Result>, RestoreFromSnapshotError> { - let microvm_state = snapshot_state_from_file(¶ms.snapshot_path, version_map)?; + let microvm_state = snapshot_state_from_file(¶ms.snapshot_path)?; // Some sanity checks before building the microvm. snapshot_state_sanity_check(µvm_state)?; @@ -482,13 +476,14 @@ pub enum SnapshotStateFromFileError { fn snapshot_state_from_file( snapshot_path: &Path, - version_map: VersionMap, ) -> Result { + let snapshot = Snapshot::new(SNAPSHOT_VERSION); let mut snapshot_reader = File::open(snapshot_path).map_err(SnapshotStateFromFileError::Open)?; let metadata = std::fs::metadata(snapshot_path).map_err(SnapshotStateFromFileError::Meta)?; let snapshot_len = u64_to_usize(metadata.len()); - let (state, _) = Snapshot::load(&mut snapshot_reader, snapshot_len, version_map) + let state: MicrovmState = snapshot + .load_with_version_check(&mut snapshot_reader, snapshot_len) .map_err(SnapshotStateFromFileError::Load)?; Ok(state) } @@ -675,7 +670,7 @@ mod tests { } #[test] - fn test_microvmstate_versionize() { + fn test_microvm_state_snapshot() { let vmm = default_vmm_with_devices(); let states = vmm.mmio_device_manager.save(); @@ -686,36 +681,19 @@ mod tests { assert!(states.vsock_device.is_some()); assert!(states.balloon_device.is_some()); - let memory_state = vmm.guest_memory().describe(); - let vcpu_states = vec![VcpuState::default()]; + let _memory_state = vmm.guest_memory().describe(); + let _vcpu_states = vec![VcpuState::default()]; #[cfg(target_arch = "aarch64")] - let mpidrs = construct_kvm_mpidrs(&vcpu_states); + let _mpidrs = construct_kvm_mpidrs(&_vcpu_states); let microvm_state = MicrovmState { - device_states: states, - memory_state, - vcpu_states, - vm_info: VmInfo { - mem_size_mib: 1u64, - ..Default::default() - }, - #[cfg(target_arch = "aarch64")] - vm_state: vmm.vm.save_state(&mpidrs).unwrap(), - #[cfg(target_arch = "x86_64")] - vm_state: vmm.vm.save_state().unwrap(), + ..Default::default() }; let mut buf = vec![0; 10000]; - let mut version_map = VersionMap::new(); - - version_map - .new_version() - .set_type_version(DeviceStates::type_id(), 2); - microvm_state - .serialize(&mut buf.as_mut_slice(), &version_map, 2) - .unwrap(); + Snapshot::serialize(&mut buf.as_mut_slice(), µvm_state).unwrap(); - let restored_microvm_state = - MicrovmState::deserialize(&mut buf.as_slice(), &version_map, 2).unwrap(); + let restored_microvm_state: MicrovmState = + Snapshot::deserialize(&mut buf.as_slice()).unwrap(); assert_eq!(restored_microvm_state.vm_info, microvm_state.vm_info); assert_eq!( diff --git a/src/vmm/src/rpc_interface.rs b/src/vmm/src/rpc_interface.rs index ec5d583ef2e..ea3579b17f6 100644 --- a/src/vmm/src/rpc_interface.rs +++ b/src/vmm/src/rpc_interface.rs @@ -24,7 +24,6 @@ use crate::logger::{info, warn, LoggerConfig, *}; use crate::mmds::data_store::{self, Mmds}; use crate::persist::{CreateSnapshotError, RestoreFromSnapshotError, VmInfo}; use crate::resources::VmmConfig; -use crate::version_map::VERSION_MAP; use crate::vmm_config::balloon::{ BalloonConfigError, BalloonDeviceConfig, BalloonStats, BalloonUpdateConfig, BalloonUpdateStatsConfig, @@ -576,7 +575,6 @@ impl<'a> PrebootApiController<'a> { self.event_manager, self.seccomp_filters, load_params, - VERSION_MAP.clone(), self.vm_resources, ) .map_err(|err| { @@ -774,12 +772,7 @@ impl RuntimeApiController { let vm_info = VmInfo::from(&self.vm_resources); let create_start_us = utils::time::get_time_us(utils::time::ClockType::Monotonic); - create_snapshot( - &mut locked_vmm, - &vm_info, - create_params, - VERSION_MAP.clone(), - )?; + create_snapshot(&mut locked_vmm, &vm_info, create_params)?; match create_params.snapshot_type { SnapshotType::Full => { @@ -1234,7 +1227,6 @@ mod tests { _: &mut Vmm, _: &VmInfo, _: &CreateSnapshotParams, - _: versionize::VersionMap, ) -> Result<(), CreateSnapshotError> { Ok(()) } @@ -1246,7 +1238,6 @@ mod tests { _: &mut EventManager, _: &BpfThreadMap, _: &LoadSnapshotParams, - _: versionize::VersionMap, _: &mut MockVmRes, ) -> Result>, RestoreFromSnapshotError> { Ok(Arc::new(Mutex::new(MockVmm::default()))) diff --git a/src/vmm/src/version_map.rs b/src/vmm/src/version_map.rs deleted file mode 100644 index e359e082188..00000000000 --- a/src/vmm/src/version_map.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Provides the VersionMap that deals with the microvm state versions. - -use std::collections::HashMap; - -use lazy_static::lazy_static; -use semver::Version; -use versionize::{VersionMap, Versionize}; - -use crate::device_manager::persist::DeviceStates; -use crate::devices::virtio::net::persist::NetConfigSpaceState; -use crate::devices::virtio::persist::{QueueState, VirtioDeviceState}; -use crate::devices::virtio::virtio_block::persist::VirtioBlockState; -use crate::persist::VmInfo; -use crate::vstate::vcpu::VcpuState; -use crate::vstate::vm::VmState; - -/// Snap version for Firecracker v0.23 -#[cfg(target_arch = "x86_64")] -pub const FC_V0_23_SNAP_VERSION: u16 = 1; -/// Snap version for Firecracker v0.24 -pub const FC_V0_24_SNAP_VERSION: u16 = 2; -/// Snap version for Firecracker v0.25 -pub const FC_V0_25_SNAP_VERSION: u16 = 3; -/// Snap version for Firecracker v1.0 -pub const FC_V1_0_SNAP_VERSION: u16 = 4; -/// Snap version for Firecracker v1.1 -pub const FC_V1_1_SNAP_VERSION: u16 = 5; -/// Snap version for Firecracker v1.2 -pub const FC_V1_2_SNAP_VERSION: u16 = 6; -/// Snap version for Firecracker v1.3 -pub const FC_V1_3_SNAP_VERSION: u16 = 7; -/// Snap version for Firecracker v1.4 -pub const FC_V1_4_SNAP_VERSION: u16 = 8; -/// Snap version for Firecracker v1.5 -pub const FC_V1_5_SNAP_VERSION: u16 = 9; -/// Snap version for Firecracker v1.6 -pub const FC_V1_6_SNAP_VERSION: u16 = 10; -/// Snap version for Firecracker v1.7 -pub const FC_V1_7_SNAP_VERSION: u16 = 11; - -lazy_static! { - // Note: until we have a better design, this needs to be updated when the version changes. - /// Static instance used for handling microVM state versions. - pub static ref VERSION_MAP: VersionMap = { - // v0.23 - all structs and root version are set to 1. - let mut version_map = VersionMap::new(); - - // v0.24 state change mappings. - version_map.new_version().set_type_version(DeviceStates::type_id(), 2); - - // v0.25 state change mappings. - version_map.new_version().set_type_version(VirtioBlockState::type_id(), 2); - #[cfg(target_arch = "x86_64")] - version_map.set_type_version(VcpuState::type_id(), 2); - - // v1.0 state change mappings. - version_map.new_version().set_type_version(QueueState::type_id(), 2); - version_map.set_type_version(VirtioBlockState::type_id(), 3); - - // v1.1 state change mappings. - version_map.new_version().set_type_version(DeviceStates::type_id(), 3); - - // v1.2 state change mappings. - version_map.new_version().set_type_version(VmInfo::type_id(), 2); - version_map.set_type_version(NetConfigSpaceState::type_id(), 2); - #[cfg(target_arch = "x86_64")] - version_map.set_type_version(VcpuState::type_id(), 3); - - // v1.3 - no changes introduced, but we need to bump as mapping - // between firecracker minor versions and snapshot versions needs - // to be 1-to-1 (see below) - version_map.new_version(); - - // v1.4 state change mappings. - version_map.new_version().set_type_version(DeviceStates::type_id(), 4); - - // v1.5 state change mappings. - version_map.new_version(); - #[cfg(target_arch = "aarch64")] - version_map.set_type_version(VcpuState::type_id(), 2); - - version_map.set_type_version(VmState::type_id(), 2); - - // v1.6 state change mappings - version_map.new_version(); - - version_map.set_type_version(VirtioDeviceState::type_id(), 2); - version_map.set_type_version(DeviceStates::type_id(), 5); - - // v1.7 state change mappings - version_map.new_version(); - - version_map - }; - - /// Static instance used for creating a 1:1 mapping between Firecracker release version - /// and snapshot data format version. - /// !CAVEAT! - /// This map is supposed to be strictly one-to-one (i.e. bijective) because - /// describe-snapshot inverts it to look up the release that matches the - /// snapshot version. If two versions map to the same snap_version, the - /// results are non-deterministic. - /// This means - /// - Do not insert patch releases here. - /// - Every minor version should be represented here. - /// - When requesting a `target_version`, these are the versions we expect. - pub static ref FC_VERSION_TO_SNAP_VERSION: HashMap = { - let mut mapping = HashMap::new(); - #[cfg(not(target_arch = "aarch64"))] - mapping.insert(Version::new(0, 23, 0), FC_V0_23_SNAP_VERSION); - - mapping.insert(Version::new(0, 24, 0), FC_V0_24_SNAP_VERSION); - mapping.insert(Version::new(0, 25, 0), FC_V0_25_SNAP_VERSION); - mapping.insert(Version::new(1, 0, 0), FC_V1_0_SNAP_VERSION); - mapping.insert(Version::new(1, 1, 0), FC_V1_1_SNAP_VERSION); - mapping.insert(Version::new(1, 2, 0), FC_V1_2_SNAP_VERSION); - mapping.insert(Version::new(1, 3, 0), FC_V1_3_SNAP_VERSION); - mapping.insert(Version::new(1, 4, 0), FC_V1_4_SNAP_VERSION); - mapping.insert(Version::new(1, 5, 0), FC_V1_5_SNAP_VERSION); - mapping.insert(Version::new(1, 6, 0), FC_V1_6_SNAP_VERSION); - mapping.insert(Version::new(1, 7, 0), FC_V1_7_SNAP_VERSION); - - mapping - }; -} diff --git a/src/vmm/tests/integration_tests.rs b/src/vmm/tests/integration_tests.rs index 5b328c791ce..eee48229b89 100644 --- a/src/vmm/tests/integration_tests.rs +++ b/src/vmm/tests/integration_tests.rs @@ -15,7 +15,6 @@ use vmm::utilities::mock_resources::{MockVmResources, NOISY_KERNEL_IMAGE}; #[cfg(target_arch = "x86_64")] use vmm::utilities::test_utils::dirty_tracking_vmm; use vmm::utilities::test_utils::{create_vmm, default_vmm, default_vmm_no_boot}; -use vmm::version_map::VERSION_MAP; use vmm::vmm_config::instance_info::{InstanceInfo, VmState}; use vmm::vmm_config::snapshot::{CreateSnapshotParams, SnapshotType}; use vmm::{DumpCpuConfigError, EventManager, FcExitCode}; @@ -197,13 +196,7 @@ fn verify_create_snapshot(is_diff: bool) -> (TempFile, TempFile) { { let mut locked_vmm = vmm.lock().unwrap(); - persist::create_snapshot( - &mut locked_vmm, - &vm_info, - &snapshot_params, - VERSION_MAP.clone(), - ) - .unwrap(); + persist::create_snapshot(&mut locked_vmm, &vm_info, &snapshot_params).unwrap(); } vmm.lock().unwrap().stop(FcExitCode::Ok); @@ -212,12 +205,8 @@ fn verify_create_snapshot(is_diff: bool) -> (TempFile, TempFile) { let snapshot_path = snapshot_file.as_path().to_path_buf(); let snapshot_file_metadata = std::fs::metadata(snapshot_path).unwrap(); let snapshot_len = snapshot_file_metadata.len() as usize; - let (restored_microvm_state, _) = Snapshot::load::<_, MicrovmState>( - &mut snapshot_file.as_file(), - snapshot_len, - VERSION_MAP.clone(), - ) - .unwrap(); + let (restored_microvm_state, _) = + Snapshot::load::<_, MicrovmState>(&mut snapshot_file.as_file(), snapshot_len).unwrap(); assert_eq!(restored_microvm_state.vm_info, vm_info); @@ -247,12 +236,8 @@ fn verify_load_snapshot(snapshot_file: TempFile, memory_file: TempFile) { let snapshot_file_metadata = snapshot_file.as_file().metadata().unwrap(); let snapshot_len = snapshot_file_metadata.len() as usize; snapshot_file.as_file().seek(SeekFrom::Start(0)).unwrap(); - let (microvm_state, _) = Snapshot::load::<_, MicrovmState>( - &mut snapshot_file.as_file(), - snapshot_len, - VERSION_MAP.clone(), - ) - .unwrap(); + let (microvm_state, _) = + Snapshot::load::<_, MicrovmState>(&mut snapshot_file.as_file(), snapshot_len).unwrap(); let mem = GuestMemoryMmap::from_state( Some(memory_file.as_file()), µvm_state.memory_state, @@ -278,6 +263,7 @@ fn verify_load_snapshot(snapshot_file: TempFile, memory_file: TempFile) { vmm.lock().unwrap().stop(FcExitCode::Ok); } +#[ignore] #[test] fn test_create_and_load_snapshot() { // Create diff snapshot. @@ -297,6 +283,7 @@ fn test_create_and_load_snapshot() { verify_load_snapshot(snapshot_file, memory_file); } +#[ignore] #[test] fn test_snapshot_load_sanity_checks() { use vmm::persist::SnapShotStateSanityCheckError; @@ -351,11 +338,6 @@ fn get_microvm_state_from_snapshot() -> MicrovmState { let snapshot_file_metadata = snapshot_file.as_file().metadata().unwrap(); let snapshot_len = snapshot_file_metadata.len() as usize; snapshot_file.as_file().seek(SeekFrom::Start(0)).unwrap(); - let (state, _) = Snapshot::load( - &mut snapshot_file.as_file(), - snapshot_len, - VERSION_MAP.clone(), - ) - .unwrap(); + let (state, _) = Snapshot::load(&mut snapshot_file.as_file(), snapshot_len).unwrap(); state } diff --git a/tools/bump-version.sh b/tools/bump-version.sh index cecbd11129e..b3244581eb0 100755 --- a/tools/bump-version.sh +++ b/tools/bump-version.sh @@ -26,17 +26,6 @@ fi version=$1 -function check_snapshot_version { - local version=$1 - local snap_version=$(echo $version |cut -f-2 -d. |tr . _) - if ! grep -s FC_V${snap_version}_SNAP_VERSION src/vmm/src/version_map.rs; then - die "I couldn't find FC_V${snap_version}_SNAP_VERSION in src/vmm/src/version_map.rs" - fi -} - -check_snapshot_version "$version" - - # Get current version from the swagger spec. prev_ver=$(get_swagger_version) From 557247bf19f149b6cf5e03fff7c040e506a45fc1 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Wed, 8 Nov 2023 13:06:20 +0000 Subject: [PATCH 02/11] snapshot: serialize VmInfo with serde Derive Serialize & Deserialize for VmInfo and remove Versionize. Now we can serialize this struct in the Firecracker snapshot file. Signed-off-by: Babis Chalios --- .../aarch64/static_cpu_templates/mod.rs | 4 +-- .../x86_64/static_cpu_templates/mod.rs | 6 +--- src/vmm/src/device_manager/persist.rs | 3 +- src/vmm/src/persist.rs | 30 ++++--------------- src/vmm/src/vmm_config/boot_source.rs | 20 ++++++++++--- 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/vmm/src/cpu_config/aarch64/static_cpu_templates/mod.rs b/src/vmm/src/cpu_config/aarch64/static_cpu_templates/mod.rs index 5439de556b4..88159e82e2f 100644 --- a/src/vmm/src/cpu_config/aarch64/static_cpu_templates/mod.rs +++ b/src/vmm/src/cpu_config/aarch64/static_cpu_templates/mod.rs @@ -2,14 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; /// Module with V1N1 CPU template for aarch64 pub mod v1n1; /// Templates available for configuring the supported ARM CPU types. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Versionize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum StaticCpuTemplate { // Needed for compatibility /// Empty0 diff --git a/src/vmm/src/cpu_config/x86_64/static_cpu_templates/mod.rs b/src/vmm/src/cpu_config/x86_64/static_cpu_templates/mod.rs index a267a8b9739..db976ed9658 100644 --- a/src/vmm/src/cpu_config/x86_64/static_cpu_templates/mod.rs +++ b/src/vmm/src/cpu_config/x86_64/static_cpu_templates/mod.rs @@ -3,8 +3,6 @@ use derive_more::Display; use serde::{Deserialize, Serialize}; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; /// Module with C3 CPU template for x86_64 pub mod c3; @@ -19,9 +17,7 @@ pub mod t2s; /// Template types available for configuring the x86 CPU features that map /// to EC2 instances. -#[derive( - Debug, Default, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Versionize, -)] +#[derive(Debug, Default, Display, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum StaticCpuTemplate { /// C3 Template. #[display(fmt = "C3")] diff --git a/src/vmm/src/device_manager/persist.rs b/src/vmm/src/device_manager/persist.rs index 96c04cd601c..b9a1807e686 100644 --- a/src/vmm/src/device_manager/persist.rs +++ b/src/vmm/src/device_manager/persist.rs @@ -900,7 +900,8 @@ mod tests { ], "boot-source": {{ "kernel_image_path": "", - "initrd_path": null + "initrd_path": null, + "boot_args": null }}, "cpu-config": null, "logger": null, diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index 092490dbbc4..0c2c86fd6d6 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -18,8 +18,6 @@ use snapshot::Snapshot; use userfaultfd::{FeatureFlags, Uffd, UffdBuilder}; use utils::sock_ctrl_msg::ScmSocket; use utils::u64_to_usize; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; #[cfg(target_arch = "aarch64")] use crate::arch::aarch64::vcpu::{get_manufacturer_id_from_host, get_manufacturer_id_from_state}; @@ -49,39 +47,18 @@ use crate::{mem_size_mib, vstate, EventManager, Vmm, VmmError}; const FC_V0_23_MAX_DEVICES: u32 = 11; /// Holds information related to the VM that is not part of VmState. -#[derive(Clone, Debug, Default, PartialEq, Eq, Versionize, Serialize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct VmInfo { /// Guest memory size. pub mem_size_mib: u64, /// smt information - #[version(start = 2, default_fn = "def_smt")] pub smt: bool, /// CPU template type - #[version(start = 2, default_fn = "def_cpu_template")] pub cpu_template: StaticCpuTemplate, /// Boot source information. - #[version(start = 2, default_fn = "def_boot_source")] pub boot_source: BootSourceConfig, } -impl VmInfo { - fn def_smt(_: u16) -> bool { - warn!("SMT field not found in snapshot."); - false - } - - fn def_cpu_template(_: u16) -> StaticCpuTemplate { - warn!("CPU template field not found in snapshot."); - StaticCpuTemplate::default() - } - - fn def_boot_source(_: u16) -> BootSourceConfig { - warn!("Boot source information not found in snapshot."); - BootSourceConfig::default() - } -} - impl From<&VmResources> for VmInfo { fn from(value: &VmResources) -> Self { Self { @@ -97,7 +74,6 @@ impl From<&VmResources> for VmInfo { #[derive(Debug, Default, Serialize, Deserialize)] pub struct MicrovmState { /// Miscellaneous VM info. - #[serde(skip)] pub vm_info: VmInfo, /// Memory state. #[serde(skip)] @@ -686,6 +662,10 @@ mod tests { #[cfg(target_arch = "aarch64")] let _mpidrs = construct_kvm_mpidrs(&_vcpu_states); let microvm_state = MicrovmState { + vm_info: VmInfo { + mem_size_mib: 1u64, + ..Default::default() + }, ..Default::default() }; diff --git a/src/vmm/src/vmm_config/boot_source.rs b/src/vmm/src/vmm_config/boot_source.rs index d5f6a1f3bde..e0b4019e318 100644 --- a/src/vmm/src/vmm_config/boot_source.rs +++ b/src/vmm/src/vmm_config/boot_source.rs @@ -5,8 +5,6 @@ use std::fs::File; use std::io; use serde::{Deserialize, Serialize}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; /// Default guest kernel command line: /// - `reboot=k` shut down the guest on reboot, instead of well... rebooting; @@ -23,7 +21,7 @@ pub const DEFAULT_KERNEL_CMDLINE: &str = "reboot=k panic=1 pci=off nomodule 8250 /// Strongly typed data structure used to configure the boot source of the /// microvm. -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Versionize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct BootSourceConfig { /// Path of the kernel image. @@ -32,7 +30,6 @@ pub struct BootSourceConfig { pub initrd_path: Option, /// The boot arguments to pass to the kernel. If this field is uninitialized, /// DEFAULT_KERNEL_CMDLINE is used. - #[serde(skip_serializing_if = "Option::is_none")] pub boot_args: Option, } @@ -100,6 +97,7 @@ impl BootConfig { #[cfg(test)] pub(crate) mod tests { + use snapshot::Snapshot; use utils::tempfile::TempFile; use super::*; @@ -122,4 +120,18 @@ pub(crate) mod tests { [DEFAULT_KERNEL_CMDLINE.as_bytes(), &[b'\0']].concat() ); } + + #[test] + fn test_serde() { + let boot_src_cfg = BootSourceConfig { + boot_args: Some(DEFAULT_KERNEL_CMDLINE.to_string()), + initrd_path: Some("/tmp/initrd".to_string()), + kernel_image_path: "./vmlinux.bin".to_string(), + }; + + let mut snapshot_data = vec![0u8; 1000]; + Snapshot::serialize(&mut snapshot_data.as_mut_slice(), &boot_src_cfg).unwrap(); + let restored_boot_cfg = Snapshot::deserialize(&mut snapshot_data.as_slice()).unwrap(); + assert_eq!(boot_src_cfg, restored_boot_cfg); + } } From b06caba90e86f86bd867fd5c65b99bf7a4c4b998 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Wed, 8 Nov 2023 14:20:57 +0000 Subject: [PATCH 03/11] snapshot: serialize GuestMemoryState with serde Derive Serialize and Deserialize for GuestMemoryState. Also add a unit test for (de)serializing this type with the snapshot crate. Signed-off-by: Babis Chalios --- src/vmm/src/persist.rs | 4 ++-- src/vmm/src/vstate/memory.rs | 38 ++++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index 0c2c86fd6d6..c48f76d6e1a 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -76,7 +76,6 @@ pub struct MicrovmState { /// Miscellaneous VM info. pub vm_info: VmInfo, /// Memory state. - #[serde(skip)] pub memory_state: GuestMemoryState, /// VM KVM state. #[serde(skip)] @@ -657,11 +656,12 @@ mod tests { assert!(states.vsock_device.is_some()); assert!(states.balloon_device.is_some()); - let _memory_state = vmm.guest_memory().describe(); + let memory_state = vmm.guest_memory().describe(); let _vcpu_states = vec![VcpuState::default()]; #[cfg(target_arch = "aarch64")] let _mpidrs = construct_kvm_mpidrs(&_vcpu_states); let microvm_state = MicrovmState { + memory_state, vm_info: VmInfo { mem_size_mib: 1u64, ..Default::default() diff --git a/src/vmm/src/vstate/memory.rs b/src/vmm/src/vstate/memory.rs index cce9a76f7d0..7dd15bc5118 100644 --- a/src/vmm/src/vstate/memory.rs +++ b/src/vmm/src/vstate/memory.rs @@ -8,9 +8,8 @@ use std::fs::File; use std::io::SeekFrom; +use serde::{Deserialize, Serialize}; use utils::{errno, get_page_size, u64_to_usize}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; pub use vm_memory::bitmap::{AtomicBitmap, Bitmap, BitmapSlice, BS}; pub use vm_memory::mmap::MmapRegionBuilder; use vm_memory::mmap::{MmapRegionError, NewBitmap}; @@ -102,8 +101,7 @@ where } /// State of a guest memory region saved to file/buffer. -#[derive(Debug, PartialEq, Eq, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct GuestMemoryRegionState { // This should have been named `base_guest_addr` since it's _guest_ addr, but for // backward compatibility we have to keep this name. At least this comment should help. @@ -116,8 +114,7 @@ pub struct GuestMemoryRegionState { } /// Describes guest memory regions and their snapshot file mappings. -#[derive(Debug, Default, PartialEq, Eq, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GuestMemoryState { /// List of regions. pub regions: Vec, @@ -373,6 +370,7 @@ mod tests { use std::collections::HashMap; use std::io::{Read, Seek}; + use snapshot::Snapshot; use utils::get_page_size; use utils::tempfile::TempFile; @@ -525,6 +523,34 @@ mod tests { } } + fn check_serde(guest_memory: &GuestMemoryMmap) { + let mut snapshot_data = vec![0u8; 10000]; + let original_state = guest_memory.describe(); + Snapshot::serialize(&mut snapshot_data.as_mut_slice(), &original_state).unwrap(); + let restored_state = Snapshot::deserialize(&mut snapshot_data.as_slice()).unwrap(); + assert_eq!(original_state, restored_state); + } + + #[test] + fn test_serde() { + let page_size = get_page_size().unwrap(); + let region_size = page_size * 3; + + // Test with a single region + let guest_memory = + GuestMemoryMmap::from_raw_regions(&[(GuestAddress(0), region_size)], false).unwrap(); + check_serde(&guest_memory); + + // Test with some regions + let regions = vec![ + (GuestAddress(0), region_size), // pages 0-2 + (GuestAddress(region_size as u64), region_size), // pages 3-5 + (GuestAddress(region_size as u64 * 2), region_size), // pages 6-8 + ]; + let guest_memory = GuestMemoryMmap::from_raw_regions(®ions, true).unwrap(); + check_serde(&guest_memory); + } + #[test] fn test_describe() { let page_size: usize = get_page_size().unwrap(); From 32fe5d0b9f9b90014db2f9075409a39ec69a00d9 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Wed, 8 Nov 2023 15:30:54 +0000 Subject: [PATCH 04/11] snapshot: serialize VmState and VcpuState Derive Serialize and Deserialize and remove Versionize support. Fix these two structs together because both depend on kvm-bindings implementing (De)serialize for its types, so updating to the kvm-bindings to the version that introduces serde breaks both of these types. While implementing serde for VcpuState also remove the `msrs` field. According to Versionize this was end-of-life and we are anyway breaking compatibility with previous snapshot versions. Signed-off-by: Babis Chalios --- Cargo.lock | 17 +- Cargo.toml | 2 +- .../arch/aarch64/gic/gicv2/regs/dist_regs.rs | 2 +- .../arch/aarch64/gic/gicv2/regs/icc_regs.rs | 1 - .../arch/aarch64/gic/gicv3/regs/dist_regs.rs | 3 +- .../arch/aarch64/gic/gicv3/regs/icc_regs.rs | 2 - .../aarch64/gic/gicv3/regs/redist_regs.rs | 2 - src/vmm/src/arch/aarch64/gic/regs.rs | 42 +-- src/vmm/src/arch/aarch64/regs.rs | 239 +++--------------- src/vmm/src/cpu_config/templates.rs | 4 +- src/vmm/src/persist.rs | 11 +- src/vmm/src/vstate/vcpu/aarch64.rs | 35 +-- src/vmm/src/vstate/vcpu/x86_64.rs | 34 +-- src/vmm/src/vstate/vm.rs | 33 ++- 14 files changed, 99 insertions(+), 328 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 662c90c0f53..cb172284a61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,10 +711,10 @@ dependencies = [ [[package]] name = "kvm-bindings" version = "0.7.0" -source = "git+https://github.com/firecracker-microvm/kvm-bindings?tag=v0.7.0-1#93af344a93b83b39cdfea0d0a860b3b57d29d28b" +source = "git+https://github.com/firecracker-microvm/kvm-bindings?tag=v0.7.0-2#60cc04e2658646516f4e763eca77fbfa1cf5ec1f" dependencies = [ - "versionize", - "versionize_derive", + "serde", + "serde-big-array", "vmm-sys-util", ] @@ -1130,6 +1130,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.195" @@ -1552,6 +1561,8 @@ checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede" dependencies = [ "bitflags 1.3.2", "libc", + "serde", + "serde_derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d57f997764e..926d1ca330b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,4 @@ panic = "abort" lto = true [patch.crates-io] -kvm-bindings = { git = "https://github.com/firecracker-microvm/kvm-bindings", tag = "v0.7.0-1", features = ["fam-wrappers"] } +kvm-bindings = { git = "https://github.com/firecracker-microvm/kvm-bindings", tag = "v0.7.0-2", features = ["fam-wrappers"] } diff --git a/src/vmm/src/arch/aarch64/gic/gicv2/regs/dist_regs.rs b/src/vmm/src/arch/aarch64/gic/gicv2/regs/dist_regs.rs index 14be488e605..3ec73490038 100644 --- a/src/vmm/src/arch/aarch64/gic/gicv2/regs/dist_regs.rs +++ b/src/vmm/src/arch/aarch64/gic/gicv2/regs/dist_regs.rs @@ -30,7 +30,7 @@ const GICD_SPENDSGIR: DistReg = DistReg::simple(0xF20, 16); // List with relevant distributor registers that we will be restoring. // Order is taken from qemu. // Criteria for the present list of registers: only R/W registers, implementation specific registers -// are not saved. NOTICE: Any changes to this structure require a snapshot version bump. +// are not saved. static VGIC_DIST_REGS: &[DistReg] = &[ GICD_CTLR, GICD_ICENABLER, diff --git a/src/vmm/src/arch/aarch64/gic/gicv2/regs/icc_regs.rs b/src/vmm/src/arch/aarch64/gic/gicv2/regs/icc_regs.rs index e8dbf0ef9e7..aea6cb722b6 100644 --- a/src/vmm/src/arch/aarch64/gic/gicv2/regs/icc_regs.rs +++ b/src/vmm/src/arch/aarch64/gic/gicv2/regs/icc_regs.rs @@ -23,7 +23,6 @@ const GICC_APR2: SimpleReg = SimpleReg::new(0x00D4, 4); const GICC_APR3: SimpleReg = SimpleReg::new(0x00D8, 4); const GICC_APR4: SimpleReg = SimpleReg::new(0x00DC, 4); -// NOTICE: Any changes to this structure require a snapshot version bump. static MAIN_VGIC_ICC_REGS: &[SimpleReg] = &[ GICC_CTLR, GICC_PMR, GICC_BPR, GICC_APBR, GICC_APR1, GICC_APR2, GICC_APR3, GICC_APR4, ]; diff --git a/src/vmm/src/arch/aarch64/gic/gicv3/regs/dist_regs.rs b/src/vmm/src/arch/aarch64/gic/gicv3/regs/dist_regs.rs index 4ae7c337d08..6e0cc8aac23 100644 --- a/src/vmm/src/arch/aarch64/gic/gicv3/regs/dist_regs.rs +++ b/src/vmm/src/arch/aarch64/gic/gicv3/regs/dist_regs.rs @@ -32,8 +32,7 @@ const GICD_IROUTER: DistReg = DistReg::shared_irq(0x6000, 64); // Criteria for the present list of registers: only R/W registers, implementation specific registers // are not saved. GICD_CPENDSGIR and GICD_SPENDSGIR are not saved since these registers are not used // when affinity routing is enabled. Affinity routing GICv3 is enabled by default unless Firecracker -// clears the ICD_CTLR.ARE bit which it does not do. NOTICE: Any changes to this structure require a -// snapshot version bump. +// clears the ICD_CTLR.ARE bit which it does not do. static VGIC_DIST_REGS: &[DistReg] = &[ GICD_CTLR, GICD_STATUSR, diff --git a/src/vmm/src/arch/aarch64/gic/gicv3/regs/icc_regs.rs b/src/vmm/src/arch/aarch64/gic/gicv3/regs/icc_regs.rs index 6ed7df622b1..e455e76ba43 100644 --- a/src/vmm/src/arch/aarch64/gic/gicv3/regs/icc_regs.rs +++ b/src/vmm/src/arch/aarch64/gic/gicv3/regs/icc_regs.rs @@ -29,7 +29,6 @@ const SYS_ICC_AP1R1_EL1: SimpleReg = SimpleReg::sys_icc_ap1rn_el1(1); const SYS_ICC_AP1R2_EL1: SimpleReg = SimpleReg::sys_icc_ap1rn_el1(2); const SYS_ICC_AP1R3_EL1: SimpleReg = SimpleReg::sys_icc_ap1rn_el1(3); -// NOTICE: Any changes to this structure require a snapshot version bump. static MAIN_VGIC_ICC_REGS: &[SimpleReg] = &[ SYS_ICC_SRE_EL1, SYS_ICC_CTLR_EL1, @@ -40,7 +39,6 @@ static MAIN_VGIC_ICC_REGS: &[SimpleReg] = &[ SYS_ICC_BPR1_EL1, ]; -// NOTICE: Any changes to this structure require a snapshot version bump. static AP_VGIC_ICC_REGS: &[SimpleReg] = &[ SYS_ICC_AP0R0_EL1, SYS_ICC_AP0R1_EL1, diff --git a/src/vmm/src/arch/aarch64/gic/gicv3/regs/redist_regs.rs b/src/vmm/src/arch/aarch64/gic/gicv3/regs/redist_regs.rs index 9c966a22d31..1909d9b453b 100644 --- a/src/vmm/src/arch/aarch64/gic/gicv3/regs/redist_regs.rs +++ b/src/vmm/src/arch/aarch64/gic/gicv3/regs/redist_regs.rs @@ -27,7 +27,6 @@ const GICR_IPRIORITYR0: SimpleReg = SimpleReg::new(GICR_SGI_OFFSET + 0x0400, 32) const GICR_ICFGR0: SimpleReg = SimpleReg::new(GICR_SGI_OFFSET + 0x0C00, 8); // List with relevant redistributor registers that we will be restoring. -// NOTICE: Any changes to this structure require a snapshot version bump. static VGIC_RDIST_REGS: &[SimpleReg] = &[ GICR_CTLR, GICR_STATUSR, @@ -37,7 +36,6 @@ static VGIC_RDIST_REGS: &[SimpleReg] = &[ ]; // List with relevant SGI associated redistributor registers that we will be restoring. -// NOTICE: Any changes to this structure require a snapshot version bump. static VGIC_SGI_REGS: &[SimpleReg] = &[ GICR_IGROUPR0, GICR_ICENABLER0, diff --git a/src/vmm/src/arch/aarch64/gic/regs.rs b/src/vmm/src/arch/aarch64/gic/regs.rs index c2e0a303d75..88c4d68c15c 100644 --- a/src/vmm/src/arch/aarch64/gic/regs.rs +++ b/src/vmm/src/arch/aarch64/gic/regs.rs @@ -7,25 +7,24 @@ use std::ops::Range; use kvm_bindings::kvm_device_attr; use kvm_ioctls::DeviceFd; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; +use serde::{Deserialize, Serialize}; use crate::arch::aarch64::gic::GicError; -#[derive(Debug)] -pub struct GicRegState { +#[derive(Debug, Serialize, Deserialize)] +pub struct GicRegState { pub(crate) chunks: Vec, } /// Structure for serializing the state of the Vgic ICC regs -#[derive(Debug, Default, Versionize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct VgicSysRegsState { pub main_icc_regs: Vec>, pub ap_icc_regs: Vec>>, } /// Structure used for serializing the state of the GIC registers. -#[derive(Debug, Default, Versionize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct GicState { /// The state of the distributor registers. pub dist: Vec>, @@ -34,39 +33,12 @@ pub struct GicState { } /// Structure used for serializing the state of the GIC registers for a specific vCPU. -#[derive(Debug, Default, Versionize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct GicVcpuState { pub rdist: Vec>, pub icc: VgicSysRegsState, } -impl Versionize for GicRegState { - fn serialize( - &self, - writer: &mut W, - version_map: &VersionMap, - app_version: u16, - ) -> VersionizeResult<()> { - let chunks = &self.chunks; - assert_eq!(std::mem::size_of_val(chunks), std::mem::size_of::()); - Versionize::serialize(chunks, writer, version_map, app_version) - } - - fn deserialize( - reader: &mut R, - version_map: &VersionMap, - app_version: u16, - ) -> VersionizeResult { - let chunks = Versionize::deserialize(reader, version_map, app_version)?; - assert_eq!(std::mem::size_of_val(&chunks), std::mem::size_of::()); - Ok(Self { chunks }) - } - - fn version() -> u16 { - 1 - } -} - pub(crate) trait MmioReg { fn range(&self) -> Range; @@ -80,7 +52,7 @@ pub(crate) trait MmioReg { pub(crate) trait VgicRegEngine { type Reg: MmioReg; - type RegChunk: Clone + Default + Versionize; + type RegChunk: Clone + Default; fn group() -> u32; diff --git a/src/vmm/src/arch/aarch64/regs.rs b/src/vmm/src/arch/aarch64/regs.rs index 6c0fce0e1f9..0e1dd348897 100644 --- a/src/vmm/src/arch/aarch64/regs.rs +++ b/src/vmm/src/arch/aarch64/regs.rs @@ -6,8 +6,7 @@ // found in the THIRD-PARTY file. use kvm_bindings::*; -use versionize::*; -use versionize_derive::Versionize; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[allow(non_upper_case_globals)] /// PSR (Processor State Register) bits. @@ -73,8 +72,6 @@ macro_rules! arm64_core_reg_id { } pub(crate) use arm64_core_reg_id; -use crate::vstate::memory::ByteValued; - /// This macro computes the ID of a specific ARM64 system register similar to how /// the kernel C macro does. /// https://elixir.bootlin.com/linux/v4.20.17/source/arch/arm64/include/uapi/asm/kvm.h#L203 @@ -195,33 +192,31 @@ pub fn reg_size(reg_id: u64) -> usize { } /// Storage for aarch64 registers with different sizes. -/// For public usage it is wrapped into `Aarch64RegisterVec` -/// which ensures correctness after deserialization. -#[derive(Default, Debug, Clone, PartialEq, Eq, Versionize)] -struct Aarch64RegisterVecInner { +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct Aarch64RegisterVec { ids: Vec, data: Vec, } -impl Aarch64RegisterVecInner { +impl Aarch64RegisterVec { /// Returns the number of elements in the vector. - fn len(&self) -> usize { + pub fn len(&self) -> usize { self.ids.len() } /// Returns true if the vector contains no elements. - fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.ids.is_empty() } /// Appends a register to the vector, copying register data. - fn push(&mut self, reg: Aarch64RegisterRef<'_>) { + pub fn push(&mut self, reg: Aarch64RegisterRef<'_>) { self.ids.push(reg.id); self.data.extend_from_slice(reg.data); } /// Returns an iterator over stored registers. - fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { Aarch64RegisterVecIterator { index: 0, offset: 0, @@ -231,7 +226,7 @@ impl Aarch64RegisterVecInner { } /// Returns an iterator over stored registers that allows register modifications. - fn iter_mut(&mut self) -> impl Iterator { + pub fn iter_mut(&mut self) -> impl Iterator { Aarch64RegisterVecIteratorMut { index: 0, offset: 0, @@ -241,85 +236,42 @@ impl Aarch64RegisterVecInner { } } -/// Wrapper type around `Aarch64RegisterVecInner`. -/// Needed to ensure correctness of inner state after -/// deserialization. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct Aarch64RegisterVec { - inner: Aarch64RegisterVecInner, -} - -impl Aarch64RegisterVec { - /// Returns the number of elements in the vector. - pub fn len(&self) -> usize { - self.inner.len() - } - - /// Returns true if the vector contains no elements. - pub fn is_empty(&self) -> bool { - self.inner.is_empty() - } - - /// Appends a register to the vector, copying register data. - pub fn push(&mut self, reg: Aarch64RegisterRef<'_>) { - self.inner.push(reg); - } - - /// Returns an iterator over stored registers. - pub fn iter(&self) -> impl Iterator { - self.inner.iter() - } - - /// Returns an iterator over stored registers that allows register modifications. - pub fn iter_mut(&mut self) -> impl Iterator { - self.inner.iter_mut() +impl Serialize for Aarch64RegisterVec { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Serialize::serialize(&(&self.ids, &self.data), serializer) } } -impl Versionize for Aarch64RegisterVec { - fn serialize( - &self, - writer: &mut W, - version_map: &VersionMap, - target_version: u16, - ) -> VersionizeResult<()> { - self.inner.serialize(writer, version_map, target_version) - } - - fn deserialize( - reader: &mut R, - version_map: &VersionMap, - source_version: u16, - ) -> VersionizeResult +impl<'de> Deserialize<'de> for Aarch64RegisterVec { + fn deserialize(deserializer: D) -> Result where - Self: Sized, + D: Deserializer<'de>, { - let inner = Aarch64RegisterVecInner::deserialize(reader, version_map, source_version)?; + let (ids, data): (Vec, Vec) = Deserialize::deserialize(deserializer)?; + let mut total_size: usize = 0; - for id in inner.ids.iter() { + for id in ids.iter() { let reg_size = reg_size(*id); - if RegSize::U2048_SIZE < reg_size { - return Err(VersionizeError::Deserialize( - "Failed to deserialize aarch64 registers. Registers bigger then 2048 bits are \ - not supported" - .to_string(), + if reg_size > RegSize::U2048_SIZE { + return Err(serde::de::Error::custom( + "Failed to deserialize aarch64 registers. Registers bigger than 2048 bits are \ + not supported", )); } total_size += reg_size; } - if total_size != inner.data.len() { - Err(VersionizeError::Deserialize( - "Failed to deserialize aarch64 registers. Sum of registers sizes is not equal to \ - registers data length" - .to_string(), - )) - } else { - Ok(Self { inner }) + + if total_size != data.len() { + return Err(serde::de::Error::custom( + "Failed to deserialize aarch64 registers. Sum of register sizes is not equal to \ + registers data length", + )); } - } - fn version() -> u16 { - Aarch64RegisterVecInner::version() + Ok(Aarch64RegisterVec { ids, data }) } } @@ -469,61 +421,6 @@ impl<'a> Aarch64RegisterRefMut<'a> { } } -/// Old definition of a struct describing an aarch64 register. -/// This type is only used to have a backward compatibility -/// with old snapshot versions and should not be used anywhere -/// else. -#[derive(Debug, Clone, Copy, Versionize)] -pub struct Aarch64RegisterOld { - /// ID of the register. - pub id: u64, - /// Register data. - pub data: u128, -} - -impl<'a> TryFrom> for Aarch64RegisterOld { - type Error = &'static str; - - fn try_from(value: Aarch64RegisterRef) -> Result { - let reg = match value.size() { - RegSize::U32 => Self { - id: value.id, - data: u128::from(value.value::()), - }, - RegSize::U64 => Self { - id: value.id, - data: u128::from(value.value::()), - }, - RegSize::U128 => Self { - id: value.id, - data: value.value::(), - }, - _ => return Err("Only 32, 64 and 128 bit wide registers are supported"), - }; - Ok(reg) - } -} - -impl<'a> TryFrom<&'a Aarch64RegisterOld> for Aarch64RegisterRef<'a> { - type Error = &'static str; - - fn try_from(value: &'a Aarch64RegisterOld) -> Result { - // # Safety: - // `self.data` is a valid memory and slice size is valid for this type. - let data_ref = value.data.as_slice(); - let reg_size = reg_size(value.id); - if RegSize::U2048_SIZE < reg_size { - return Err("Registers bigger then 2048 bits are not supported"); - } - match RegSize::from(reg_size) { - RegSize::U32 => Ok(Self::new(value.id, &data_ref[..std::mem::size_of::()])), - RegSize::U64 => Ok(Self::new(value.id, &data_ref[..std::mem::size_of::()])), - RegSize::U128 => Ok(Self::new(value.id, data_ref)), - _ => Err("Only 32, 64 and 128 bit wide registers are supported"), - } - } -} - /// Trait for data types that can represent aarch64 /// register data. pub trait Aarch64RegisterData { @@ -581,6 +478,8 @@ reg_data_array!([u8; 256], 256); #[cfg(test)] mod tests { + use snapshot::Snapshot; + use super::*; #[test] @@ -603,13 +502,9 @@ mod tests { v.push(reg2); let mut buf = vec![0; 10000]; - let version_map = VersionMap::new(); - v.serialize(&mut buf.as_mut_slice(), &version_map, 1) - .unwrap(); - let restored = - ::deserialize(&mut buf.as_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut buf.as_mut_slice(), &v).unwrap(); + let restored: Aarch64RegisterVec = Snapshot::deserialize(&mut buf.as_slice()).unwrap(); for (old, new) in v.iter().zip(restored.iter()) { assert_eq!(old, new); @@ -633,15 +528,12 @@ mod tests { v.push(reg2); let mut buf = vec![0; 10000]; - let version_map = VersionMap::new(); - v.serialize(&mut buf.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut buf.as_mut_slice(), &v).unwrap(); // Total size of registers according IDs are 16 + 16 = 32, // but actual data size is 8 + 16 = 24. - ::deserialize(&mut buf.as_slice(), &version_map, 1) - .unwrap_err(); + Snapshot::deserialize::<_, Aarch64RegisterVec>(&mut buf.as_slice()).unwrap_err(); } #[test] @@ -659,19 +551,16 @@ mod tests { v.push(reg); let mut buf = vec![0; 10000]; - let version_map = VersionMap::new(); - v.serialize(&mut buf.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut buf.as_mut_slice(), &v).unwrap(); // 4096 bit wide registers are not supported. - ::deserialize(&mut buf.as_slice(), &version_map, 1) - .unwrap_err(); + Snapshot::deserialize::<_, Aarch64RegisterVec>(&mut buf.as_slice()).unwrap_err(); } #[test] - fn test_aarch64_register_vec_inner() { - let mut v = Aarch64RegisterVecInner::default(); + fn test_aarch64_register_vec() { + let mut v = Aarch64RegisterVec::default(); let reg1_bytes = 1_u8.to_le_bytes(); let reg1 = Aarch64RegisterRef::new(u64::from(KVM_REG_SIZE_U8), ®1_bytes); @@ -823,48 +712,4 @@ mod tests { let reg_ref = Aarch64RegisterRefMut::new(KVM_REG_SIZE_U64, &mut bytes); assert_eq!(reg_ref.value::(), 69); } - - #[test] - fn test_old_reg_to_reg_ref() { - let old_reg = Aarch64RegisterOld { - id: KVM_REG_SIZE_U64, - data: 69, - }; - - let reg_ref: Aarch64RegisterRef = (&old_reg).try_into().unwrap(); - assert_eq!(old_reg.id, reg_ref.id); - assert_eq!(old_reg.data, u128::from(reg_ref.value::())); - - let old_reg = Aarch64RegisterOld { - id: KVM_REG_SIZE_U256, - data: 69, - }; - - let reg_ref: Result = (&old_reg).try_into(); - reg_ref.unwrap_err(); - - // 4096 bit wide reg ID. - let old_reg = Aarch64RegisterOld { - id: 0x0090000000000000, - data: 69, - }; - - let reg_ref: Result = (&old_reg).try_into(); - reg_ref.unwrap_err(); - } - - #[test] - fn test_reg_ref_to_old_reg() { - let reg_bytes = 69_u64.to_le_bytes(); - let reg_ref = Aarch64RegisterRef::new(KVM_REG_SIZE_U64, ®_bytes); - - let reg: Aarch64RegisterOld = reg_ref.try_into().unwrap(); - assert_eq!(reg.id, reg_ref.id); - assert_eq!(reg.data, u128::from(reg_ref.value::())); - - let reg_ref = Aarch64RegisterRef::new(KVM_REG_SIZE_U256, &[0_u8; 32]); - - let reg: Result = reg_ref.try_into(); - reg.unwrap_err(); - } } diff --git a/src/vmm/src/cpu_config/templates.rs b/src/vmm/src/cpu_config/templates.rs index 599f3386f74..984fd8dcac2 100644 --- a/src/vmm/src/cpu_config/templates.rs +++ b/src/vmm/src/cpu_config/templates.rs @@ -25,8 +25,6 @@ use std::fmt::Debug; pub use common_types::*; use serde::de::Error as SerdeError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; /// Error for GetCpuTemplate trait. #[derive(Debug, thiserror::Error, displaydoc::Display, PartialEq, Eq)] @@ -106,7 +104,7 @@ impl TryFrom<&str> for CustomCpuTemplate { /// Struct to represent user defined kvm capability. /// Users can add or remove kvm capabilities to be checked /// by FC in addition to those FC checks by default. -#[derive(Debug, Clone, Eq, PartialEq, Versionize)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum KvmCapability { /// Add capability to the check list. Add(u32), diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index c48f76d6e1a..ed76b7ef0cd 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -78,10 +78,8 @@ pub struct MicrovmState { /// Memory state. pub memory_state: GuestMemoryState, /// VM KVM state. - #[serde(skip)] pub vm_state: VmState, /// Vcpu states. - #[serde(skip)] pub vcpu_states: Vec, /// Device states. #[serde(skip)] @@ -657,15 +655,20 @@ mod tests { assert!(states.balloon_device.is_some()); let memory_state = vmm.guest_memory().describe(); - let _vcpu_states = vec![VcpuState::default()]; + let vcpu_states = vec![VcpuState::default()]; #[cfg(target_arch = "aarch64")] - let _mpidrs = construct_kvm_mpidrs(&_vcpu_states); + let mpidrs = construct_kvm_mpidrs(&vcpu_states); let microvm_state = MicrovmState { memory_state, + vcpu_states, vm_info: VmInfo { mem_size_mib: 1u64, ..Default::default() }, + #[cfg(target_arch = "aarch64")] + vm_state: vmm.vm.save_state(&mpidrs).unwrap(), + #[cfg(target_arch = "x86_64")] + vm_state: vmm.vm.save_state().unwrap(), ..Default::default() }; diff --git a/src/vmm/src/vstate/vcpu/aarch64.rs b/src/vmm/src/vstate/vcpu/aarch64.rs index e7e928b3bd3..9c4eeb9b44d 100644 --- a/src/vmm/src/vstate/vcpu/aarch64.rs +++ b/src/vmm/src/vstate/vcpu/aarch64.rs @@ -9,12 +9,10 @@ use std::fmt::{Debug, Write}; use kvm_bindings::*; use kvm_ioctls::*; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; +use serde::{Deserialize, Serialize}; use crate::arch::aarch64::regs::{ - arm64_core_reg_id, offset__of, Aarch64RegisterOld, Aarch64RegisterRef, Aarch64RegisterVec, - KVM_REG_ARM_TIMER_CNT, + arm64_core_reg_id, offset__of, Aarch64RegisterVec, KVM_REG_ARM_TIMER_CNT, }; use crate::arch::aarch64::vcpu::{ get_all_registers, get_all_registers_ids, get_mpidr, get_mpstate, get_registers, set_mpstate, @@ -252,15 +250,11 @@ impl KvmVcpu { } /// Structure holding VCPU kvm state. -#[derive(Default, Clone, Versionize)] +#[derive(Default, Clone, Serialize, Deserialize)] pub struct VcpuState { /// Multiprocessing state. pub mp_state: kvm_bindings::kvm_mp_state, - /// Old representation of Vcpu registers. - #[version(end = 2, default_fn = "default_old_regs")] - pub old_regs: Vec, /// Vcpu registers. - #[version(start = 2, de_fn = "de_regs")] pub regs: Aarch64RegisterVec, /// We will be using the mpidr for passing it to the VmState. /// The VmState will give this away for saving restoring the icc and redistributor @@ -269,7 +263,6 @@ pub struct VcpuState { /// kvi states for vcpu initialization. /// If None then use `default_kvi` to obtain /// kvi. - #[version(start = 2, default_fn = "default_kvi")] pub kvi: Option, } @@ -295,28 +288,6 @@ impl Debug for VcpuState { } } -impl VcpuState { - fn default_old_regs(_: u16) -> Vec { - Vec::default() - } - - fn default_kvi(_: u16) -> Option { - None - } - - fn de_regs(&mut self, _source_version: u16) -> VersionizeResult<()> { - let mut regs = Aarch64RegisterVec::default(); - for reg in self.old_regs.iter() { - let reg_ref: Aarch64RegisterRef = reg - .try_into() - .map_err(|e: &str| VersionizeError::Deserialize(e.into()))?; - regs.push(reg_ref); - } - self.regs = regs; - Ok(()) - } -} - #[cfg(test)] mod tests { #![allow(clippy::undocumented_unsafe_blocks)] diff --git a/src/vmm/src/vstate/vcpu/x86_64.rs b/src/vmm/src/vstate/vcpu/x86_64.rs index d14d509d67f..941e13b52f8 100644 --- a/src/vmm/src/vstate/vcpu/x86_64.rs +++ b/src/vmm/src/vstate/vcpu/x86_64.rs @@ -14,8 +14,7 @@ use kvm_bindings::{ }; use kvm_ioctls::{VcpuExit, VcpuFd}; use log::{error, warn}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; +use serde::{Deserialize, Serialize}; use crate::arch::x86_64::interrupts; use crate::arch::x86_64::msr::{create_boot_msr_entries, MsrError}; @@ -403,7 +402,6 @@ impl KvmVcpu { Ok(VcpuState { cpuid, saved_msrs, - msrs: Msrs::new(0)?, debug_regs, lapic, mp_state, @@ -543,17 +541,12 @@ impl KvmVcpu { } } -#[derive(Clone, Versionize)] /// Structure holding VCPU kvm state. -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Clone, Serialize, Deserialize)] pub struct VcpuState { /// CpuId. pub cpuid: CpuId, - /// Msrs. - #[version(end = 3, default_fn = "default_msrs")] - pub msrs: Msrs, /// Saved msrs. - #[version(start = 3, de_fn = "de_saved_msrs")] pub saved_msrs: Vec, /// Debug regs. pub debug_regs: kvm_debugregs, @@ -572,7 +565,6 @@ pub struct VcpuState { /// Xsave. pub xsave: kvm_xsave, /// Tsc khz. - #[version(start = 2, default_fn = "default_tsc_khz")] pub tsc_khz: Option, } @@ -585,7 +577,6 @@ impl Debug for VcpuState { } f.debug_struct("VcpuState") .field("cpuid", &self.cpuid) - .field("msrs", &self.msrs) .field("saved_msrs", &debug_kvm_regs) .field("debug_regs", &self.debug_regs) .field("lapic", &self.lapic) @@ -600,26 +591,6 @@ impl Debug for VcpuState { } } -impl VcpuState { - fn default_tsc_khz(_: u16) -> Option { - warn!("CPU TSC freq not found in snapshot"); - None - } - - fn default_msrs(_source_version: u16) -> Msrs { - // Safe to unwrap since Msrs::new() only returns an error if the number - // of elements exceeds KVM_MAX_MSR_ENTRIES - Msrs::new(0).unwrap() - } - - fn de_saved_msrs(&mut self, source_version: u16) -> VersionizeResult<()> { - if source_version < 3 { - self.saved_msrs.push(self.msrs.clone()); - } - Ok(()) - } -} - #[cfg(test)] mod tests { #![allow(clippy::undocumented_unsafe_blocks)] @@ -642,7 +613,6 @@ mod tests { fn default() -> Self { VcpuState { cpuid: CpuId::new(1).unwrap(), - msrs: Msrs::new(1).unwrap(), saved_msrs: vec![Msrs::new(1).unwrap()], debug_regs: Default::default(), lapic: Default::default(), diff --git a/src/vmm/src/vstate/vm.rs b/src/vmm/src/vstate/vm.rs index 491dd306c1d..da2ec4eac6a 100644 --- a/src/vmm/src/vstate/vm.rs +++ b/src/vmm/src/vstate/vm.rs @@ -16,10 +16,9 @@ use kvm_bindings::{ }; use kvm_bindings::{kvm_userspace_memory_region, KVM_API_VERSION, KVM_MEM_LOG_DIRTY_PAGES}; use kvm_ioctls::{Kvm, VmFd}; +use serde::{Deserialize, Serialize}; #[cfg(target_arch = "x86_64")] use utils::u64_to_usize; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; #[cfg(target_arch = "aarch64")] use crate::arch::aarch64::gic::GICDevice; @@ -316,12 +315,11 @@ impl Vm { /// Structure holding an general specific VM state. #[cfg(target_arch = "aarch64")] -#[derive(Debug, Default, Versionize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct VmState { /// GIC state. pub gic: GicState, /// Additional capabilities that were specified in cpu template. - #[version(start = 2, default_fn = "default_caps")] pub kvm_cap_modifiers: Vec, } @@ -439,9 +437,8 @@ impl Vm { } #[cfg(target_arch = "x86_64")] -#[derive(Default, Versionize)] +#[derive(Default, Deserialize, Serialize)] /// Structure holding VM kvm state. -// NOTICE: Any changes to this structure require a snapshot version bump. pub struct VmState { pitstate: kvm_pit_state2, clock: kvm_clock_data, @@ -452,16 +449,9 @@ pub struct VmState { ioapic: kvm_irqchip, /// Additional capabilities that were specified in cpu template. - #[version(start = 2, default_fn = "default_caps")] pub kvm_cap_modifiers: Vec, } -impl VmState { - fn default_caps(_: u16) -> Vec { - Vec::default() - } -} - #[cfg(target_arch = "x86_64")] impl fmt::Debug for VmState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -478,6 +468,9 @@ impl fmt::Debug for VmState { #[cfg(test)] pub(crate) mod tests { + #[cfg(target_arch = "x86_64")] + use snapshot::Snapshot; + use super::*; use crate::vstate::memory::{GuestAddress, GuestMemoryExtension, GuestMemoryMmap}; @@ -579,6 +572,20 @@ pub(crate) mod tests { vm.restore_state(&vm_state).unwrap_err(); } + #[cfg(target_arch = "x86_64")] + #[test] + fn test_vmstate_serde() { + let mut snapshot_data = vec![0u8; 10000]; + + let (mut vm, _) = setup_vm(0x1000); + vm.setup_irqchip().unwrap(); + let state = vm.save_state().unwrap(); + Snapshot::serialize(&mut snapshot_data.as_mut_slice(), &state).unwrap(); + let restored_state: VmState = Snapshot::deserialize(&mut snapshot_data.as_slice()).unwrap(); + + vm.restore_state(&restored_state).unwrap(); + } + #[test] fn test_set_kvm_memory_regions() { let vm = Vm::new(vec![]).expect("Cannot create new vm"); From ddfb0e7236a208ef76049d3a8c9060bc0aa71891 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Wed, 8 Nov 2023 16:44:38 +0000 Subject: [PATCH 05/11] snapshot: serialize DeviceStates Derive Serialize and Deserialize for DeviceStates and remove Versionnize support. Do this for all device types at the same time, since they all depend on VirtIO related state. Signed-off-by: Babis Chalios --- src/utils/src/net/mac.rs | 4 +- src/vmm/src/arch/mod.rs | 5 +- src/vmm/src/device_manager/mmio.rs | 6 +- src/vmm/src/device_manager/persist.rs | 95 +++---------------- src/vmm/src/devices/virtio/balloon/persist.rs | 21 ++-- src/vmm/src/devices/virtio/block_common.rs | 5 +- src/vmm/src/devices/virtio/net/persist.rs | 47 +++------ src/vmm/src/devices/virtio/persist.rs | 59 +++--------- src/vmm/src/devices/virtio/rng/persist.rs | 14 ++- .../virtio/vhost_user_block/persist.rs | 6 +- .../devices/virtio/virtio_block/persist.rs | 44 ++------- src/vmm/src/devices/virtio/vsock/persist.rs | 23 ++--- src/vmm/src/mmds/persist.rs | 15 ++- src/vmm/src/persist.rs | 9 +- src/vmm/src/rate_limiter/persist.rs | 38 +++----- 15 files changed, 95 insertions(+), 296 deletions(-) diff --git a/src/utils/src/net/mac.rs b/src/utils/src/net/mac.rs index 4d300b3e405..d0f6e3057c4 100644 --- a/src/utils/src/net/mac.rs +++ b/src/utils/src/net/mac.rs @@ -16,14 +16,12 @@ use std::str::FromStr; use serde::de::{Deserialize, Deserializer, Error}; use serde::ser::{Serialize, Serializer}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; /// The number of tuples (the ones separated by ":") contained in a MAC address. pub const MAC_ADDR_LEN: u8 = 6; /// Represents a MAC address -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Versionize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] /// Representation of a MAC address. pub struct MacAddr { bytes: [u8; MAC_ADDR_LEN as usize], diff --git a/src/vmm/src/arch/mod.rs b/src/vmm/src/arch/mod.rs index 15c88ead491..e68bf1ec089 100644 --- a/src/vmm/src/arch/mod.rs +++ b/src/vmm/src/arch/mod.rs @@ -3,8 +3,7 @@ use std::fmt; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; +use serde::{Deserialize, Serialize}; /// Module for aarch64 related functionality. #[cfg(target_arch = "aarch64")] @@ -29,7 +28,7 @@ pub use crate::arch::x86_64::{ }; /// Types of devices that can get attached to this platform. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy, Versionize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)] pub enum DeviceType { /// Device Type: Virtio. Virtio(u32), diff --git a/src/vmm/src/device_manager/mmio.rs b/src/vmm/src/device_manager/mmio.rs index c775d707b9e..4a31a5d06e1 100644 --- a/src/vmm/src/device_manager/mmio.rs +++ b/src/vmm/src/device_manager/mmio.rs @@ -12,8 +12,7 @@ use std::sync::{Arc, Mutex}; use kvm_ioctls::{IoEventAddress, VmFd}; use linux_loader::cmdline as kernel_cmdline; use log::info; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; +use serde::{Deserialize, Serialize}; use vm_allocator::{AddressAllocator, AllocPolicy, IdAllocator}; #[cfg(target_arch = "aarch64")] @@ -65,8 +64,7 @@ pub enum MmioError { pub const MMIO_LEN: u64 = 0x1000; /// Stores the address range and irq allocated to this device. -#[derive(Clone, Debug, PartialEq, Eq, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct MMIODeviceInfo { /// Mmio address at which the device is registered. pub addr: u64, diff --git a/src/vmm/src/device_manager/persist.rs b/src/vmm/src/device_manager/persist.rs index b9a1807e686..f8e5ea55cf0 100644 --- a/src/vmm/src/device_manager/persist.rs +++ b/src/vmm/src/device_manager/persist.rs @@ -9,9 +9,8 @@ use std::sync::{Arc, Mutex}; use event_manager::{MutEventSubscriber, SubscriberOps}; use kvm_ioctls::VmFd; use log::{error, warn}; +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; use vm_allocator::AllocPolicy; use super::mmio::*; @@ -79,8 +78,7 @@ pub enum DevicePersistError { } /// Holds the state of a balloon device connected to the MMIO space. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedBalloonState { /// Device identifier. pub device_id: String, @@ -93,8 +91,7 @@ pub struct ConnectedBalloonState { } /// Holds the state of a virtio block device connected to the MMIO space. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedVirtioBlockState { /// Device identifier. pub device_id: String, @@ -107,8 +104,7 @@ pub struct ConnectedVirtioBlockState { } /// Holds the state of a vhost-user block device connected to the MMIO space. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedVhostUserBlockState { /// Device identifier. pub device_id: String, @@ -121,8 +117,7 @@ pub struct ConnectedVhostUserBlockState { } /// Holds the state of a net device connected to the MMIO space. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedNetState { /// Device identifier. pub device_id: String, @@ -135,8 +130,7 @@ pub struct ConnectedNetState { } /// Holds the state of a vsock device connected to the MMIO space. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedVsockState { /// Device identifier. pub device_id: String, @@ -149,8 +143,7 @@ pub struct ConnectedVsockState { } /// Holds the state of an entropy device connected to the MMIO space. -// NOTICE: Any chages to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedEntropyState { /// Device identifier. pub device_id: String, @@ -164,7 +157,7 @@ pub struct ConnectedEntropyState { /// Holds the state of a legacy device connected to the MMIO space. #[cfg(target_arch = "aarch64")] -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectedLegacyState { /// Device identifier. pub type_: DeviceType, @@ -173,8 +166,7 @@ pub struct ConnectedLegacyState { } /// Holds the MMDS data store version. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, PartialEq, Eq, Versionize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum MmdsVersionState { V1, V2, @@ -199,8 +191,7 @@ impl From for MmdsVersionState { } /// Holds the device states. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Default, Clone, Versionize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct DeviceStates { #[cfg(target_arch = "aarch64")] // State of legacy devices in MMIO space. @@ -208,20 +199,16 @@ pub struct DeviceStates { /// Virtio block device states. pub virtio_block_devices: Vec, /// Vhost-user block device states. - #[version(start = 5, de_fn = "de_vhost_user_block")] pub vhost_user_block_devices: Vec, /// Net device states. pub net_devices: Vec, /// Vsock device state. pub vsock_device: Option, /// Balloon device state. - #[version(start = 2)] pub balloon_device: Option, /// Mmds version. - #[version(start = 3)] pub mmds_version: Option, /// Entropy device state. - #[version(start = 4)] pub entropy_device: Option, } @@ -237,15 +224,6 @@ pub enum SharedDeviceType { Entropy(Arc>), } -impl DeviceStates { - fn de_vhost_user_block(&mut self, source_version: u16) -> VersionizeResult<()> { - if source_version < 5 { - self.vhost_user_block_devices = vec![]; - } - - Ok(()) - } -} pub struct MMIODevManagerConstructorArgs<'a> { pub mem: GuestMemoryMmap, pub vm: &'a VmFd, @@ -668,12 +646,12 @@ impl<'a> Persist<'a> for MMIODeviceManager { #[cfg(test)] mod tests { + use snapshot::Snapshot; use utils::tempfile::TempFile; use super::*; use crate::builder::tests::*; use crate::devices::virtio::block_common::CacheType; - use crate::devices::virtio::net::persist::NetConfigSpaceState; use crate::resources::VmmConfig; use crate::vmm_config::balloon::BalloonDeviceConfig; use crate::vmm_config::entropy::EntropyDeviceConfig; @@ -754,7 +732,6 @@ mod tests { #[test] fn test_device_manager_persistence() { let mut buf = vec![0; 16384]; - let mut version_map = VersionMap::new(); // These need to survive so the restored blocks find them. let _block_files; let mut tmp_sock_file = TempFile::new().unwrap(); @@ -806,56 +783,11 @@ mod tests { uds_path: tmp_sock_file.as_path().to_str().unwrap().to_string(), }; insert_vsock_device(&mut vmm, &mut cmdline, &mut event_manager, vsock_config); - - version_map - .new_version() - .set_type_version(DeviceStates::type_id(), 2); - vmm.mmio_device_manager - .save() - .serialize(&mut buf.as_mut_slice(), &version_map, 2) - .unwrap(); - - version_map - .new_version() - .set_type_version(DeviceStates::type_id(), 3) - .set_type_version(NetConfigSpaceState::type_id(), 2); - - // For snapshot versions that not support persisting the mmds version, it should be - // deserialized as None. The MMIODeviceManager will initialise it as the default if - // there's at least one network device having a MMDS NS. - vmm.mmio_device_manager - .save() - .serialize(&mut buf.as_mut_slice(), &version_map, 2) - .unwrap(); - let device_states: DeviceStates = - DeviceStates::deserialize(&mut buf.as_slice(), &version_map, 2).unwrap(); - assert!(device_states.mmds_version.is_none()); - - vmm.mmio_device_manager - .save() - .serialize(&mut buf.as_mut_slice(), &version_map, 3) - .unwrap(); - // Add an entropy device. let entropy_config = EntropyDeviceConfig::default(); insert_entropy_device(&mut vmm, &mut cmdline, &mut event_manager, entropy_config); - version_map - .new_version() - .set_type_version(DeviceStates::type_id(), 4); - - version_map - .new_version() - .set_type_version(DeviceStates::type_id(), 4) - .set_type_version(NetConfigSpaceState::type_id(), 2); - - vmm.mmio_device_manager - .save() - .serialize(&mut buf.as_mut_slice(), &version_map, 4) - .unwrap(); - let device_states: DeviceStates = - DeviceStates::deserialize(&mut buf.as_slice(), &version_map, 4).unwrap(); - assert!(device_states.entropy_device.is_some()); + Snapshot::serialize(&mut buf.as_mut_slice(), &vmm.mmio_device_manager.save()).unwrap(); // We only want to keep the device map from the original MmioDeviceManager. vmm.mmio_device_manager.soft_clone() @@ -864,8 +796,7 @@ mod tests { let mut event_manager = EventManager::new().expect("Unable to create EventManager"); let vmm = default_vmm(); - let device_states: DeviceStates = - DeviceStates::deserialize(&mut buf.as_slice(), &version_map, 4).unwrap(); + let device_states: DeviceStates = Snapshot::deserialize(&mut buf.as_slice()).unwrap(); let vm_resources = &mut VmResources::default(); let restore_args = MMIODevManagerConstructorArgs { mem: vmm.guest_memory().clone(), diff --git a/src/vmm/src/devices/virtio/balloon/persist.rs b/src/vmm/src/devices/virtio/balloon/persist.rs index cd201fe6589..5efb366903d 100644 --- a/src/vmm/src/devices/virtio/balloon/persist.rs +++ b/src/vmm/src/devices/virtio/balloon/persist.rs @@ -7,10 +7,9 @@ use std::sync::atomic::AtomicU32; use std::sync::Arc; use std::time::Duration; +use serde::{Deserialize, Serialize}; use snapshot::Persist; use timerfd::{SetTimeFlags, TimerState}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; use super::*; use crate::devices::virtio::balloon::device::{BalloonStats, ConfigSpace}; @@ -22,8 +21,7 @@ use crate::vstate::memory::GuestMemoryMmap; /// Information about the balloon config's that are saved /// at snapshot. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BalloonConfigSpaceState { num_pages: u32, actual_pages: u32, @@ -31,8 +29,7 @@ pub struct BalloonConfigSpaceState { /// Information about the balloon stats that are saved /// at snapshot. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BalloonStatsState { swap_in: Option, swap_out: Option, @@ -84,8 +81,7 @@ impl BalloonStatsState { /// Information about the balloon that are saved /// at snapshot. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BalloonState { stats_polling_interval_s: u16, stats_desc_index: Option, @@ -178,6 +174,8 @@ impl Persist<'_> for Balloon { mod tests { use std::sync::atomic::Ordering; + use snapshot::Snapshot; + use super::*; use crate::devices::virtio::device::VirtioDevice; use crate::devices::virtio::test_utils::default_mem; @@ -187,19 +185,16 @@ mod tests { fn test_persistence() { let guest_mem = default_mem(); let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); // Create and save the balloon device. let balloon = Balloon::new(0x42, false, 2, false).unwrap(); - ::save(&balloon) - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &balloon.save()).unwrap(); // Deserialize and restore the balloon device. let restored_balloon = Balloon::restore( BalloonConstructorArgs { mem: guest_mem }, - &BalloonState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), + &Snapshot::deserialize(&mut mem.as_slice()).unwrap(), ) .unwrap(); diff --git a/src/vmm/src/devices/virtio/block_common.rs b/src/vmm/src/devices/virtio/block_common.rs index a4b28113647..0b8488ca8e5 100644 --- a/src/vmm/src/devices/virtio/block_common.rs +++ b/src/vmm/src/devices/virtio/block_common.rs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; /// Configuration options for disk caching. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Versionize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub enum CacheType { /// Flushing mechanic not will be advertised to the guest driver #[default] diff --git a/src/vmm/src/devices/virtio/net/persist.rs b/src/vmm/src/devices/virtio/net/persist.rs index 9f5c4396ba6..0926a5a0d42 100644 --- a/src/vmm/src/devices/virtio/net/persist.rs +++ b/src/vmm/src/devices/virtio/net/persist.rs @@ -7,11 +7,9 @@ use std::io; use std::sync::atomic::AtomicU32; use std::sync::{Arc, Mutex}; -use log::warn; +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use utils::net::mac::{MacAddr, MAC_ADDR_LEN}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; +use utils::net::mac::MacAddr; use super::device::Net; use super::NET_NUM_QUEUES; @@ -28,35 +26,14 @@ use crate::vstate::memory::GuestMemoryMmap; /// Information about the network config's that are saved /// at snapshot. -#[derive(Debug, Default, Clone, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct NetConfigSpaceState { - #[version(end = 2, default_fn = "def_guest_mac_old")] - guest_mac: [u8; MAC_ADDR_LEN as usize], - #[version(start = 2, de_fn = "de_guest_mac_v2")] - guest_mac_v2: Option, -} - -impl NetConfigSpaceState { - fn de_guest_mac_v2(&mut self, version: u16) -> VersionizeResult<()> { - // v1.1 and older versions do not have optional MAC address. - warn!("Optional MAC address will be set to older version."); - if version < 2 { - self.guest_mac_v2 = Some(self.guest_mac.into()); - } - Ok(()) - } - - fn def_guest_mac_old(_: u16) -> [u8; MAC_ADDR_LEN as usize] { - // v1.2 and newer don't use this field anyway - Default::default() - } + guest_mac: Option, } /// Information about the network device that are saved /// at snapshot. -#[derive(Debug, Clone, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetState { id: String, tap_if_name: String, @@ -103,8 +80,7 @@ impl Persist<'_> for Net { tx_rate_limiter_state: self.tx_rate_limiter.save(), mmds_ns: self.mmds_ns.as_ref().map(|mmds| mmds.save()), config_space: NetConfigSpaceState { - guest_mac_v2: self.guest_mac, - guest_mac: Default::default(), + guest_mac: self.guest_mac, }, virtio_state: VirtioDeviceState::from_device(self), } @@ -120,7 +96,7 @@ impl Persist<'_> for Net { let mut net = Net::new( state.id.clone(), &state.tap_if_name, - state.config_space.guest_mac_v2, + state.config_space.guest_mac, rx_rate_limiter, tx_rate_limiter, )?; @@ -164,6 +140,8 @@ impl Persist<'_> for Net { mod tests { use std::sync::atomic::Ordering; + use snapshot::Snapshot; + use super::*; use crate::devices::virtio::device::VirtioDevice; use crate::devices::virtio::net::test_utils::{default_net, default_net_no_mmds}; @@ -172,7 +150,6 @@ mod tests { fn validate_save_and_restore(net: Net, mmds_ds: Option>>) { let guest_mem = default_mem(); let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); let id; let tap_if_name; @@ -182,9 +159,7 @@ mod tests { // Create and save the net device. { - ::save(&net) - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &net.save()).unwrap(); // Save some fields that we want to check later. id = net.id.clone(); @@ -204,7 +179,7 @@ mod tests { mem: guest_mem, mmds: mmds_ds, }, - &NetState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), + &Snapshot::deserialize(&mut mem.as_slice()).unwrap(), ) { Ok(restored_net) => { // Test that virtio specific fields are the same. diff --git a/src/vmm/src/devices/virtio/persist.rs b/src/vmm/src/devices/virtio/persist.rs index 91313f65caf..4293fac01e2 100644 --- a/src/vmm/src/devices/virtio/persist.rs +++ b/src/vmm/src/devices/virtio/persist.rs @@ -7,9 +7,8 @@ use std::num::Wrapping; use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; use crate::devices::virtio::device::VirtioDevice; use crate::devices::virtio::gen::virtio_ring::VIRTIO_RING_F_EVENT_IDX; @@ -25,8 +24,7 @@ pub enum PersistError { } /// Queue information saved in snapshot. -#[derive(Clone, Debug, PartialEq, Eq, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct QueueState { /// The maximal size in elements offered by the device max_size: u16, @@ -50,7 +48,6 @@ pub struct QueueState { next_used: Wrapping, /// The number of added used buffers since last guest kick - #[version(start = 2)] num_added: Wrapping, } @@ -90,8 +87,7 @@ impl Persist<'_> for Queue { } /// State of a VirtioDevice. -#[derive(Clone, Debug, Default, PartialEq, Eq, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct VirtioDeviceState { /// Device type. pub device_type: u32, @@ -102,11 +98,7 @@ pub struct VirtioDeviceState { /// List of queues. pub queues: Vec, /// The MMIO interrupt status. - #[version(start = 2, de_fn = "de_interrupt_status")] pub interrupt_status: u32, - /// The MMIO interrupt status as a usize. - #[version(end = 2)] - pub interrupt_status_old: usize, /// Flag for activated status. pub activated: bool, } @@ -120,7 +112,6 @@ impl VirtioDeviceState { acked_features: device.acked_features(), queues: device.queues().iter().map(Persist::save).collect(), interrupt_status: device.interrupt_status().load(Ordering::Relaxed), - interrupt_status_old: device.interrupt_status().load(Ordering::Relaxed) as usize, activated: device.is_activated(), } } @@ -174,20 +165,10 @@ impl VirtioDeviceState { } Ok(queues) } - - #[allow(clippy::cast_possible_truncation)] - fn de_interrupt_status(&mut self, version: u16) -> VersionizeResult<()> { - // v1 uses a usize type for interrupt status. - if version < 2 { - self.interrupt_status = self.interrupt_status_old as u32; - } - Ok(()) - } } /// Transport information saved in snapshot. -#[derive(Clone, Debug, PartialEq, Eq, Versionize)] -// NOTICE: Any changes to this structure require a snapshot version bump. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct MmioTransportState { // The register where feature bits are stored. features_select: u32, @@ -244,6 +225,7 @@ impl Persist<'_> for MmioTransport { #[cfg(test)] mod tests { + use snapshot::Snapshot; use utils::tempfile::TempFile; use super::*; @@ -337,35 +319,24 @@ mod tests { let queue = Queue::new(128); let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); - queue - .save() - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &queue.save()).unwrap(); - let restored_queue = Queue::restore( - (), - &QueueState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), - ) - .unwrap(); + let restored_queue = + Queue::restore((), &Snapshot::deserialize(&mut mem.as_slice()).unwrap()).unwrap(); assert_eq!(restored_queue, queue); } #[test] - fn test_virtio_device_state_versionize() { + fn test_virtio_device_state_serde() { let dummy = DummyDevice::new(); let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); let state = VirtioDeviceState::from_device(&dummy); - state - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &state).unwrap(); - let restored_state = - VirtioDeviceState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(); + let restored_state: VirtioDeviceState = Snapshot::deserialize(&mut mem.as_slice()).unwrap(); assert_eq!(restored_state, state); } @@ -390,12 +361,8 @@ mod tests { device: Arc>, ) { let mut buf = vec![0; 4096]; - let version_map = VersionMap::new(); - mmio_transport - .save() - .serialize(&mut buf.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut buf.as_mut_slice(), &mmio_transport.save()).unwrap(); let restore_args = MmioTransportConstructorArgs { mem, @@ -404,7 +371,7 @@ mod tests { }; let restored_mmio_transport = MmioTransport::restore( restore_args, - &MmioTransportState::deserialize(&mut buf.as_slice(), &version_map, 1).unwrap(), + &Snapshot::deserialize(&mut buf.as_slice()).unwrap(), ) .unwrap(); diff --git a/src/vmm/src/devices/virtio/rng/persist.rs b/src/vmm/src/devices/virtio/rng/persist.rs index 1d2b139b873..c6de8a1d4d8 100644 --- a/src/vmm/src/devices/virtio/rng/persist.rs +++ b/src/vmm/src/devices/virtio/rng/persist.rs @@ -3,9 +3,8 @@ //! Defines the structures needed for saving/restoring entropy devices. +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; use crate::devices::virtio::persist::{PersistError as VirtioStateError, VirtioDeviceState}; use crate::devices::virtio::queue::FIRECRACKER_MAX_QUEUE_SIZE; @@ -15,7 +14,7 @@ use crate::rate_limiter::persist::RateLimiterState; use crate::rate_limiter::RateLimiter; use crate::vstate::memory::GuestMemoryMmap; -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct EntropyState { virtio_state: VirtioDeviceState, rate_limiter_state: RateLimiterState, @@ -80,6 +79,8 @@ impl Persist<'_> for Entropy { mod tests { use std::sync::atomic::Ordering; + use snapshot::Snapshot; + use super::*; use crate::devices::virtio::device::VirtioDevice; use crate::devices::virtio::rng::device::ENTROPY_DEV_ID; @@ -90,15 +91,12 @@ mod tests { let mut mem = vec![0u8; 4096]; let entropy = Entropy::new(RateLimiter::default()).unwrap(); - let version_map = VersionMap::new(); - ::save(&entropy) - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &entropy.save()).unwrap(); let guest_mem = create_virtio_mem(); let restored = Entropy::restore( EntropyConstructorArgs(guest_mem), - &EntropyState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), + &Snapshot::deserialize(&mut mem.as_slice()).unwrap(), ) .unwrap(); diff --git a/src/vmm/src/devices/virtio/vhost_user_block/persist.rs b/src/vmm/src/devices/virtio/vhost_user_block/persist.rs index d62908531b3..cdf3eef5d00 100644 --- a/src/vmm/src/devices/virtio/vhost_user_block/persist.rs +++ b/src/vmm/src/devices/virtio/vhost_user_block/persist.rs @@ -3,9 +3,8 @@ //! Defines the structures needed for saving/restoring block devices. +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; use super::device::VhostUserBlock; use super::VhostUserBlockError; @@ -14,8 +13,7 @@ use crate::devices::virtio::persist::VirtioDeviceState; use crate::vstate::memory::GuestMemoryMmap; /// vhost-user block device state. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct VhostUserBlockState { id: String, partuuid: Option, diff --git a/src/vmm/src/devices/virtio/virtio_block/persist.rs b/src/vmm/src/devices/virtio/virtio_block/persist.rs index 5e4c65aad4d..ae274175884 100644 --- a/src/vmm/src/devices/virtio/virtio_block/persist.rs +++ b/src/vmm/src/devices/virtio/virtio_block/persist.rs @@ -6,10 +6,9 @@ use std::sync::atomic::AtomicU32; use std::sync::Arc; +use serde::{Deserialize, Serialize}; use snapshot::Persist; use utils::eventfd::EventFd; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; use super::device::DiskProperties; use super::*; @@ -25,8 +24,7 @@ use crate::rate_limiter::RateLimiter; use crate::vstate::memory::GuestMemoryMmap; /// Holds info about block's file engine type. Gets saved in snapshot. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Versionize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum FileEngineTypeState { /// Sync File Engine. // If the snap version does not contain the `FileEngineType`, it must have been snapshotted @@ -56,27 +54,18 @@ impl From for FileEngineType { } /// Holds info about the block device. Gets saved in snapshot. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct VirtioBlockState { id: String, partuuid: Option, - #[version(start = 2, default_fn = "default_cache_type_flush")] cache_type: CacheType, root_device: bool, disk_path: String, virtio_state: VirtioDeviceState, rate_limiter_state: RateLimiterState, - #[version(start = 3)] file_engine_type: FileEngineTypeState, } -impl VirtioBlockState { - fn default_cache_type_flush(_source_version: u16) -> CacheType { - CacheType::Unsafe - } -} - /// Auxiliary structure for creating a device when resuming from a snapshot. #[derive(Debug)] pub struct VirtioBlockConstructorArgs { @@ -184,6 +173,7 @@ impl Persist<'_> for VirtioBlock { mod tests { use std::sync::atomic::Ordering; + use snapshot::Snapshot; use utils::tempfile::TempFile; use super::*; @@ -212,15 +202,8 @@ mod tests { // Save the block device. let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); - ::save(&block) - .serialize(&mut mem.as_mut_slice(), &version_map, 2) - .unwrap(); - - ::save(&block) - .serialize(&mut mem.as_mut_slice(), &version_map, 3) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &block.save()).unwrap(); } #[test] @@ -241,10 +224,6 @@ mod tests { let f = TempFile::new().unwrap(); f.as_file().set_len(0x1000).unwrap(); - let mut version_map = VersionMap::new(); - version_map - .new_version() - .set_type_version(VirtioBlockState::type_id(), 3); if !FileEngineType::Async.is_supported().unwrap() { // Test what happens when restoring an Async engine on a kernel that does not support @@ -272,14 +251,12 @@ mod tests { // Overwrite the engine type state with Async. block_state.file_engine_type = FileEngineTypeState::Async; - block_state - .serialize(&mut mem.as_mut_slice(), &version_map, 2) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &block_state).unwrap(); // Restore the block device. let restored_block = VirtioBlock::restore( VirtioBlockConstructorArgs { mem: default_mem() }, - &VirtioBlockState::deserialize(&mut mem.as_slice(), &version_map, 2).unwrap(), + &Snapshot::deserialize(&mut mem.as_slice()).unwrap(), ) .unwrap(); @@ -311,16 +288,13 @@ mod tests { // Save the block device. let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); - ::save(&block) - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &block.save()).unwrap(); // Restore the block device. let restored_block = VirtioBlock::restore( VirtioBlockConstructorArgs { mem: guest_mem }, - &VirtioBlockState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), + &Snapshot::deserialize(&mut mem.as_slice()).unwrap(), ) .unwrap(); diff --git a/src/vmm/src/devices/virtio/vsock/persist.rs b/src/vmm/src/devices/virtio/vsock/persist.rs index 8c6784c6f50..8930ee781ae 100644 --- a/src/vmm/src/devices/virtio/vsock/persist.rs +++ b/src/vmm/src/devices/virtio/vsock/persist.rs @@ -7,9 +7,8 @@ use std::fmt::Debug; use std::sync::atomic::AtomicU32; use std::sync::Arc; +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use versionize::{VersionMap, Versionize, VersionizeError, VersionizeResult}; -use versionize_derive::Versionize; use super::*; use crate::devices::virtio::device::DeviceState; @@ -19,8 +18,7 @@ use crate::devices::virtio::vsock::TYPE_VSOCK; use crate::vstate::memory::GuestMemoryMmap; /// The Vsock serializable state. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct VsockState { /// The vsock backend state. pub backend: VsockBackendState, @@ -29,8 +27,7 @@ pub struct VsockState { } /// The Vsock frontend serializable state. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct VsockFrontendState { /// Context IDentifier. pub cid: u64, @@ -38,16 +35,14 @@ pub struct VsockFrontendState { } /// An enum for the serializable backend state types. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum VsockBackendState { /// UDS backend state. Uds(VsockUdsState), } /// The Vsock Unix Backend serializable state. -// NOTICE: Any changes to this structure require a snapshot version bump. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct VsockUdsState { /// The path for the UDS socket. pub(crate) path: String, @@ -139,6 +134,7 @@ where #[cfg(test)] pub(crate) mod tests { + use snapshot::Snapshot; use utils::byte_order; use super::device::AVAIL_FEATURES; @@ -181,7 +177,6 @@ pub(crate) mod tests { // Test serialization let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); // Save backend and device state separately. let state = VsockState { @@ -189,11 +184,9 @@ pub(crate) mod tests { frontend: ctx.device.save(), }; - state - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &state).unwrap(); - let restored_state = VsockState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(); + let restored_state: VsockState = Snapshot::deserialize(&mut mem.as_slice()).unwrap(); let mut restored_device = Vsock::restore( VsockConstructorArgs { mem: ctx.mem.clone(), diff --git a/src/vmm/src/mmds/persist.rs b/src/vmm/src/mmds/persist.rs index 2f4a6157a61..b1282b9c2e0 100644 --- a/src/vmm/src/mmds/persist.rs +++ b/src/vmm/src/mmds/persist.rs @@ -6,17 +6,15 @@ use std::net::Ipv4Addr; use std::sync::{Arc, Mutex}; +use serde::{Deserialize, Serialize}; use snapshot::Persist; use utils::net::mac::{MacAddr, MAC_ADDR_LEN}; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; use super::ns::MmdsNetworkStack; use crate::mmds::data_store::Mmds; -// NOTICE: Any changes to this structure require a snapshot version bump. /// State of a MmdsNetworkStack. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MmdsNetworkStackState { mac_addr: [u8; MAC_ADDR_LEN as usize], ipv4_addr: u32, @@ -60,6 +58,8 @@ impl Persist<'_> for MmdsNetworkStack { #[cfg(test)] mod tests { + use snapshot::Snapshot; + use super::*; #[test] @@ -67,15 +67,12 @@ mod tests { let ns = MmdsNetworkStack::new_with_defaults(None, Arc::new(Mutex::new(Mmds::default()))); let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); - ns.save() - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &ns.save()).unwrap(); let restored_ns = MmdsNetworkStack::restore( Arc::new(Mutex::new(Mmds::default())), - &MmdsNetworkStackState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), + &Snapshot::deserialize(&mut mem.as_slice()).unwrap(), ) .unwrap(); diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index ed76b7ef0cd..ef2769d0e0e 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -43,9 +43,6 @@ use crate::vstate::vcpu::{VcpuSendEventError, VcpuState}; use crate::vstate::vm::VmState; use crate::{mem_size_mib, vstate, EventManager, Vmm, VmmError}; -#[cfg(target_arch = "x86_64")] -const FC_V0_23_MAX_DEVICES: u32 = 11; - /// Holds information related to the VM that is not part of VmState. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct VmInfo { @@ -82,7 +79,6 @@ pub struct MicrovmState { /// Vcpu states. pub vcpu_states: Vec, /// Device states. - #[serde(skip)] pub device_states: DeviceStates, } @@ -151,9 +147,6 @@ pub enum CreateSnapshotError { SnapshotBackingFile(&'static str, io::Error), /// Size mismatch when writing diff snapshot on top of base layer: base layer size is {0} but diff layer is size {1}. SnapshotBackingFileLengthMismatch(u64, u64), - #[cfg(target_arch = "x86_64")] - /// Too many devices attached: {0}. The maximum number allowed for the snapshot data version requested is {FC_V0_23_MAX_DEVICES:}. - TooManyDevices(usize), } /// Snapshot version @@ -659,6 +652,7 @@ mod tests { #[cfg(target_arch = "aarch64")] let mpidrs = construct_kvm_mpidrs(&vcpu_states); let microvm_state = MicrovmState { + device_states: states, memory_state, vcpu_states, vm_info: VmInfo { @@ -669,7 +663,6 @@ mod tests { vm_state: vmm.vm.save_state(&mpidrs).unwrap(), #[cfg(target_arch = "x86_64")] vm_state: vmm.vm.save_state().unwrap(), - ..Default::default() }; let mut buf = vec![0; 10000]; diff --git a/src/vmm/src/rate_limiter/persist.rs b/src/vmm/src/rate_limiter/persist.rs index e15775ed6fd..e621433ccf3 100644 --- a/src/vmm/src/rate_limiter/persist.rs +++ b/src/vmm/src/rate_limiter/persist.rs @@ -3,15 +3,13 @@ //! Defines the structures needed for saving/restoring a RateLimiter. +use serde::{Deserialize, Serialize}; use snapshot::Persist; -use versionize::{VersionMap, Versionize, VersionizeResult}; -use versionize_derive::Versionize; use super::*; -// NOTICE: Any changes to this structure require a snapshot version bump. /// State for saving a TokenBucket. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenBucketState { size: u64, one_time_burst: u64, @@ -53,9 +51,8 @@ impl Persist<'_> for TokenBucket { } } -// NOTICE: Any changes to this structure require a snapshot version bump. /// State for saving a RateLimiter. -#[derive(Debug, Clone, Versionize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct RateLimiterState { ops: Option, bandwidth: Option, @@ -95,6 +92,8 @@ impl Persist<'_> for RateLimiter { #[cfg(test)] mod tests { + use snapshot::Snapshot; + use super::*; #[test] @@ -117,16 +116,10 @@ mod tests { // Test serialization. let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); - tb.save() - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); - - let restored_tb = TokenBucket::restore( - (), - &TokenBucketState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), - ) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &tb.save()).unwrap(); + + let restored_tb = + TokenBucket::restore((), &Snapshot::deserialize(&mut mem.as_slice()).unwrap()).unwrap(); assert!(tb.partial_eq(&restored_tb)); } @@ -187,16 +180,9 @@ mod tests { // Test serialization. let mut mem = vec![0; 4096]; - let version_map = VersionMap::new(); - rate_limiter - .save() - .serialize(&mut mem.as_mut_slice(), &version_map, 1) - .unwrap(); - let restored_rate_limiter = RateLimiter::restore( - (), - &RateLimiterState::deserialize(&mut mem.as_slice(), &version_map, 1).unwrap(), - ) - .unwrap(); + Snapshot::serialize(&mut mem.as_mut_slice(), &rate_limiter.save()).unwrap(); + let restored_rate_limiter = + RateLimiter::restore((), &Snapshot::deserialize(&mut mem.as_slice()).unwrap()).unwrap(); assert!(rate_limiter .ops() From 4bf66dd9f1b7b99e5f8b0d2b406aa0916eb4257c Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Thu, 9 Nov 2023 19:33:27 +0000 Subject: [PATCH 06/11] snapshot: remove versionize dependencies Remove versionize and versionize_derive dependencies. Also, re-enable the rust integration tests. Now that we have all of the state serialized with serde these should work again. Signed-off-by: Babis Chalios --- Cargo.lock | 32 ------------------------------ src/utils/Cargo.toml | 4 +--- src/vmm/Cargo.toml | 2 -- src/vmm/tests/integration_tests.rs | 2 -- 4 files changed, 1 insertion(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb172284a61..32124eb40d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,8 +1404,6 @@ dependencies = [ "serde", "serde_json", "thiserror", - "versionize", - "versionize_derive", "vm-memory 0.13.1", "vmm-sys-util", ] @@ -1425,34 +1423,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "versionize" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62929d59c7f6730b7298fcb363760550f4db6e353fbac4076d447d0e82799d6d" -dependencies = [ - "bincode", - "crc64", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn 1.0.109", - "versionize_derive", - "vmm-sys-util", -] - -[[package]] -name = "versionize_derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4971c07ef708cd51003222e509aaed8eae14aeb2907706b01578843195e03a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "vhost" version = "0.10.0" @@ -1544,8 +1514,6 @@ dependencies = [ "timerfd", "userfaultfd", "utils", - "versionize", - "versionize_derive", "vhost", "vm-allocator", "vm-fdt", diff --git a/src/utils/Cargo.toml b/src/utils/Cargo.toml index ea4f6345a97..04fba2eef90 100644 --- a/src/utils/Cargo.toml +++ b/src/utils/Cargo.toml @@ -14,9 +14,7 @@ libc = "0.2.147" serde = { version = "1.0.165", features = ["derive"] } thiserror = "1.0.32" displaydoc = "0.2.4" -versionize = "0.2.0" -versionize_derive = "0.1.6" -vmm-sys-util = "0.12.0" +vmm-sys-util = "0.12.1" vm-memory = { version = "0.13.0", features = ["backend-mmap", "backend-bitmap"] } log-instrument = { path = "../log-instrument", optional = true } diff --git a/src/vmm/Cargo.toml b/src/vmm/Cargo.toml index 9368461f378..3884849d674 100644 --- a/src/vmm/Cargo.toml +++ b/src/vmm/Cargo.toml @@ -26,8 +26,6 @@ timerfd = "1.5.0" thiserror = "1.0.32" displaydoc = "0.2.4" userfaultfd = "0.7.0" -versionize = "0.2.0" -versionize_derive = "0.1.6" vhost = { version = "0.10.0", features = ["vhost-user-frontend"] } vm-allocator = "0.1.0" vm-superio = "0.7.0" diff --git a/src/vmm/tests/integration_tests.rs b/src/vmm/tests/integration_tests.rs index eee48229b89..7489764bc5a 100644 --- a/src/vmm/tests/integration_tests.rs +++ b/src/vmm/tests/integration_tests.rs @@ -263,7 +263,6 @@ fn verify_load_snapshot(snapshot_file: TempFile, memory_file: TempFile) { vmm.lock().unwrap().stop(FcExitCode::Ok); } -#[ignore] #[test] fn test_create_and_load_snapshot() { // Create diff snapshot. @@ -283,7 +282,6 @@ fn test_create_and_load_snapshot() { verify_load_snapshot(snapshot_file, memory_file); } -#[ignore] #[test] fn test_snapshot_load_sanity_checks() { use vmm::persist::SnapShotStateSanityCheckError; From f67822d571cb3674401171cd5ddc89866316de9f Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Fri, 10 Nov 2023 10:11:03 +0000 Subject: [PATCH 07/11] fix(test): --describe-snapshot cmd line test This test relies on the fact that the current Firecracker binary can parse snapshots that were created with a previous Firecracker version. 1.7.0 changes the snapshot format header and its parser only understands the new format. As a result, it can't parse previous snapshots. We fix this by skipping the tests for Firecracker versions previous to 1.7.0. Signed-off-by: Babis Chalios --- tests/framework/artifacts.py | 18 ++++++++++++++++-- .../functional/test_cmd_line_parameters.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/framework/artifacts.py b/tests/framework/artifacts.py index b841a109b9d..3aa564b4a7c 100644 --- a/tests/framework/artifacts.py +++ b/tests/framework/artifacts.py @@ -13,7 +13,7 @@ from framework.defs import ARTIFACT_DIR from framework.properties import global_props -from framework.utils import get_firecracker_version_from_toml +from framework.utils import get_firecracker_version_from_toml, run_cmd from host_tools.cargo_build import get_binary @@ -98,7 +98,21 @@ def version_tuple(self): @property def snapshot_version_tuple(self): """Return the artifact's snapshot version as a tuple: `X.Y.0`.""" - return self.version_tuple[:2] + (0,) + + # Starting from Firecracker v1.6.0, snapshots have their own version that is + # independent of Firecracker versions. For these Firecracker versions, use + # the --snapshot-version Firecracker flag, to figure out which snapshot version + # it supports. + # TODO: remove this check once all version prior to 1.6.0 go out of support. + if packaging.version.parse(self.version) < packaging.version.parse("1.7.0"): + return self.version_tuple[:2] + (0,) + + return ( + run_cmd([self.path, "--snapshot-version"]) + .stdout.strip() + .split("\n")[0] + .split(".") + ) @property def snapshot_version(self): diff --git a/tests/integration_tests/functional/test_cmd_line_parameters.py b/tests/integration_tests/functional/test_cmd_line_parameters.py index 3bd08877b60..62cfdc304fe 100644 --- a/tests/integration_tests/functional/test_cmd_line_parameters.py +++ b/tests/integration_tests/functional/test_cmd_line_parameters.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path +import packaging.version import pytest from framework.utils import run_cmd @@ -20,6 +21,15 @@ def test_describe_snapshot_all_versions( For each release create a snapshot and verify the data version of the snapshot state file. """ + + # TODO: remove this check once all versions prior to 1.6.0 go out of support. + if packaging.version.parse(firecracker_release.version) < packaging.version.parse( + "1.7.0" + ): + pytest.skip( + "We can't parse snapshot files created from Firecracker with version < 1.7.0." + ) + target_version = firecracker_release.snapshot_version vm = microvm_factory.build( guest_kernel, From 434e33859420c42efe94ba6c1fd587a8a389a5b2 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Fri, 10 Nov 2023 10:17:01 +0000 Subject: [PATCH 08/11] fix(test): microVM config after restore We were skipping the serialization of the "boot_args" field of the "boot-source" objectm, if kernel boot arguments were not supported. Now, we return None. This was done so that we have consistent snapshot formats, now that we use serde to serialize microVM state in a snapshot file. Amend this test to expect '"boot_args": None' in the "boot-source" object. Signed-off-by: Babis Chalios --- tests/integration_tests/functional/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration_tests/functional/test_api.py b/tests/integration_tests/functional/test_api.py index 1f398da1cd8..ac5a16d0762 100644 --- a/tests/integration_tests/functional/test_api.py +++ b/tests/integration_tests/functional/test_api.py @@ -1193,6 +1193,7 @@ def test_get_full_config_after_restoring_snapshot(microvm_factory, uvm_nano): expected_cfg["boot-source"] = { "kernel_image_path": uvm_nano.get_jailed_resource(uvm_nano.kernel_file), "initrd_path": None, + "boot_args": None, } # no ipv4 specified during PUT /mmds/config so we expect the default From 603d6d5a827a12544c81b219a3734d7acb97fc39 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Fri, 10 Nov 2023 10:40:26 +0000 Subject: [PATCH 09/11] fix(test): use --snapshot-version for supported snapshot format Snapshot versions are now independent from Firecracker binary versions. Use Firecracker --snapshot-version flag to retrieve the supported snapshot data format version for checking if the created snapshot is supported from the current release. Signed-off-by: Babis Chalios --- .../functional/test_snapshot_basic.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/integration_tests/functional/test_snapshot_basic.py b/tests/integration_tests/functional/test_snapshot_basic.py index b0f3a3a40f4..ec6c5217b8d 100644 --- a/tests/integration_tests/functional/test_snapshot_basic.py +++ b/tests/integration_tests/functional/test_snapshot_basic.py @@ -13,12 +13,7 @@ import host_tools.drive as drive_tools from framework.microvm import SnapshotType -from framework.utils import ( - check_filesystem, - get_firecracker_version_from_toml, - run_cmd, - wait_process_termination, -) +from framework.utils import check_filesystem, run_cmd, wait_process_termination from framework.utils_vsock import ( ECHO_SERVER_PORT, VSOCK_UDS_PATH, @@ -53,18 +48,20 @@ def test_snapshot_current_version(uvm_nano): vm = uvm_nano vm.start() - version = get_firecracker_version_from_toml() - # normalize to a snapshot version - version = f"{version.major}.{version.minor}.0" snapshot = vm.snapshot_full() # Fetch Firecracker binary for the latest version fc_binary = uvm_nano.fc_binary_path + # Get supported snapshot version from Firecracker binary + snapshot_version = ( + run_cmd(f"{fc_binary} --snapshot-version").stdout.strip().splitlines()[0] + ) + # Verify the output of `--describe-snapshot` command line parameter cmd = [str(fc_binary)] + ["--describe-snapshot", str(snapshot.vmstate)] _, stdout, _ = run_cmd(cmd) - assert version in stdout + assert snapshot_version in stdout # Testing matrix: From 74b29f1ac9a02f10482726ea32ebd0a51ab3a135 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Fri, 10 Nov 2023 10:46:27 +0000 Subject: [PATCH 10/11] fix(test): remove tests that are not relevant any more With the new snapshot format we break compatibility with all previous snapshot format versions. This commits removes all the tests that were checking functional correctness and performance of loading older snapshots in the current Firecracker binary. In the future, it might be that two Firecracker binaries support the same snapshot versions. When that happens we should thing of a proper way to test that this works, but the current tests are not fit for purpose. Signed-off-by: Babis Chalios --- .../integration_tests/functional/test_mmds.py | 34 ---- .../functional/test_snapshot_advanced.py | 156 ------------------ .../performance/test_snapshot_perf.py | 82 --------- 3 files changed, 272 deletions(-) delete mode 100644 tests/integration_tests/functional/test_snapshot_advanced.py diff --git a/tests/integration_tests/functional/test_mmds.py b/tests/integration_tests/functional/test_mmds.py index 2a682d24fc8..1673008d11b 100644 --- a/tests/integration_tests/functional/test_mmds.py +++ b/tests/integration_tests/functional/test_mmds.py @@ -10,7 +10,6 @@ import pytest from framework.artifacts import working_version_as_artifact -from framework.properties import global_props from framework.utils import ( configure_mmds, generate_mmds_get_request, @@ -612,39 +611,6 @@ def test_mmds_snapshot(uvm_nano, microvm_factory, version): ) -def test_mmds_older_snapshot( - microvm_factory, guest_kernel, rootfs, firecracker_release -): - """ - Test MMDS behavior restoring older snapshots in the current version. - - Ensures that the MMDS version is persisted or initialised with the default - if the FC version does not support this feature. - """ - - # due to bug fixed in commit 8dab78b - firecracker_version = firecracker_release.version_tuple - if global_props.instance == "m6a.metal" and firecracker_version < (1, 3, 3): - pytest.skip("incompatible with AMD and Firecracker <1.3.3") - - microvm = microvm_factory.build( - guest_kernel, - rootfs, - fc_binary_path=firecracker_release.path, - jailer_binary_path=firecracker_release.jailer, - ) - microvm.spawn() - microvm.basic_config() - microvm.add_net_iface() - - mmds_version = "V2" - _validate_mmds_snapshot( - microvm, - microvm_factory, - mmds_version, - ) - - def test_mmds_v2_negative(test_microvm_with_api): """ Test invalid MMDS GET/PUT requests when using V2. diff --git a/tests/integration_tests/functional/test_snapshot_advanced.py b/tests/integration_tests/functional/test_snapshot_advanced.py deleted file mode 100644 index 2bd26cd56c3..00000000000 --- a/tests/integration_tests/functional/test_snapshot_advanced.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Advanced tests scenarios for snapshot save/restore.""" - -import tempfile - -import pytest -from test_balloon import _test_rss_memory_lower - -import host_tools.drive as drive_tools -from framework.microvm import SnapshotType -from framework.properties import global_props - -# Define 4 scratch drives. -scratch_drives = ["vdb", "vdc", "vdd", "vde"] - - -def test_restore_old_to_current( - microvm_factory, guest_kernel, rootfs_ubuntu_22, firecracker_release -): - """ - Restore snapshots from previous supported versions of Firecracker. - - For each firecracker release: - 1. Snapshot with the past release - 2. Restore with the current build - """ - - # due to bug fixed in commit 8dab78b - firecracker_version = firecracker_release.version_tuple - if global_props.instance == "m6a.metal" and firecracker_version < (1, 3, 3): - pytest.skip("incompatible with AMD and Firecracker <1.3.3") - - # Microvm: 2vCPU 256MB RAM, balloon, 4 disks and 4 net devices. - diff_snapshots = True - vm = microvm_factory.build( - guest_kernel, - rootfs_ubuntu_22, - fc_binary_path=firecracker_release.path, - jailer_binary_path=firecracker_release.jailer, - ) - vm.spawn() - vm.basic_config(track_dirty_pages=True) - snapshot = create_snapshot_helper( - vm, - drives=scratch_drives, - diff_snapshots=diff_snapshots, - balloon=diff_snapshots, - ) - vm = microvm_factory.build() - vm.spawn() - vm.restore_from_snapshot(snapshot, resume=True) - validate_all_devices(vm, diff_snapshots) - print(vm.log_data) - - -def validate_all_devices(microvm, balloon): - """Perform a basic validation for all devices of a microvm.""" - # Test that net devices have connectivity after restore. - for iface in microvm.iface.values(): - print("Testing net device", iface["iface"].dev_name) - microvm.guest_ip = iface["iface"].guest_ip - exit_code, _, _ = microvm.ssh.run("sync") - - # Drop page cache. - # Ensure further reads are going to be served from emulation layer. - cmd = "sync; echo 1 > /proc/sys/vm/drop_caches" - exit_code, _, _ = microvm.ssh.run(cmd) - assert exit_code == 0 - - # Validate checksum of /dev/vdX/test. - # Should be ab893875d697a3145af5eed5309bee26 for 10 pages - # of zeroes. - for drive in list(microvm.disks)[1:]: - # Mount block device. - print("Testing drive ", drive) - cmd = f"mkdir -p /tmp/{drive} ; mount /dev/{drive} /tmp/{drive}" - exit_code, _, _ = microvm.ssh.run(cmd) - assert exit_code == 0 - - # Validate checksum. - cmd = f"md5sum /tmp/{drive}/test | cut -d ' ' -f 1" - exit_code, stdout, _ = microvm.ssh.run(cmd) - assert exit_code == 0 - assert stdout.strip() == "ab893875d697a3145af5eed5309bee26" - print("* checksum OK.") - - if balloon is True: - print("Testing balloon memory reclaim.") - # Call helper fn from balloon integration tests. - _test_rss_memory_lower(microvm) - - -def create_snapshot_helper( - vm, - drives=None, - balloon=False, - diff_snapshots=False, -): - """Create a snapshot with many devices.""" - if diff_snapshots is False: - snapshot_type = SnapshotType.FULL - else: - # Version 0.24 and greater has Diff and balloon support. - snapshot_type = SnapshotType.DIFF - - if balloon: - # Add a memory balloon with stats enabled. - vm.api.balloon.put( - amount_mib=0, deflate_on_oom=True, stats_polling_interval_s=1 - ) - - test_drives = [] if drives is None else drives - - # Add disks. - for scratch in test_drives: - # Add a scratch 64MB RW non-root block device. - scratchdisk = drive_tools.FilesystemFile(tempfile.mktemp(), size=64) - vm.add_drive(scratch, scratchdisk.path) - - # Workaround FilesystemFile destructor removal of file. - scratchdisk.path = None - - for _ in range(4): - vm.add_net_iface() - - vm.start() - - # Iterate and validate connectivity on all ifaces after boot. - for i in range(4): - exit_code, _, _ = vm.ssh_iface(i).run("sync") - assert exit_code == 0 - - # Mount scratch drives in guest. - for blk in test_drives: - # Create mount point and mount each device. - cmd = f"mkdir -p /tmp/mnt/{blk} && mount /dev/{blk} /tmp/mnt/{blk}" - exit_code, _, _ = vm.ssh.run(cmd) - assert exit_code == 0 - - # Create file using dd using O_DIRECT. - # After resume we will compute md5sum on these files. - dd = f"dd if=/dev/zero of=/tmp/mnt/{blk}/test bs=4096 count=10 oflag=direct" - exit_code, _, _ = vm.ssh.run(dd) - assert exit_code == 0 - - # Unmount the device. - cmd = f"umount /dev/{blk}" - exit_code, _, _ = vm.ssh.run(cmd) - assert exit_code == 0 - - snapshot = vm.make_snapshot(snapshot_type) - print("========== Firecracker create snapshot log ==========") - print(vm.log_data) - vm.kill() - return snapshot diff --git a/tests/integration_tests/performance/test_snapshot_perf.py b/tests/integration_tests/performance/test_snapshot_perf.py index bf4f04a1b0b..1fd331f3c5f 100644 --- a/tests/integration_tests/performance/test_snapshot_perf.py +++ b/tests/integration_tests/performance/test_snapshot_perf.py @@ -2,10 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 """Basic tests scenarios for snapshot save/restore.""" -import pytest - -from framework.properties import global_props - # How many latencies do we sample per test. SAMPLE_COUNT = 3 USEC_IN_MSEC = 1000 @@ -23,84 +19,6 @@ def snapshot_create_producer(vm): return value -def snapshot_resume_producer(microvm_factory, snapshot): - """Produce results for snapshot resume tests.""" - - microvm = microvm_factory.build() - microvm.spawn() - microvm.restore_from_snapshot(snapshot, resume=True) - - # Attempt to connect to resumed microvm. - # Verify if guest can run commands. - exit_code, _, _ = microvm.ssh.run("ls") - assert exit_code == 0 - - value = 0 - # Parse all metric data points in search of load_snapshot time. - metrics = microvm.get_all_metrics() - for data_point in metrics: - cur_value = data_point["latencies_us"]["load_snapshot"] / USEC_IN_MSEC - if cur_value > 0: - value = cur_value - break - - print("Latency {value} ms") - return value - - -def test_older_snapshot_resume_latency( - microvm_factory, - guest_kernel_linux_4_14, - rootfs, - firecracker_release, - io_engine, - metrics, -): - """ - Test scenario: Older snapshot load performance measurement. - - With each previous firecracker version, create a snapshot and try to - restore in current version. - """ - - # due to bug fixed in commit 8dab78b - firecracker_version = firecracker_release.version_tuple - if global_props.instance == "m6a.metal" and firecracker_version < (1, 3, 3): - pytest.skip("incompatible with AMD and Firecracker <1.3.3") - - vm = microvm_factory.build( - guest_kernel_linux_4_14, - rootfs, - monitor_memory=False, - fc_binary_path=firecracker_release.path, - jailer_binary_path=firecracker_release.jailer, - ) - vm.spawn() - vm.basic_config(vcpu_count=2, mem_size_mib=512) - vm.add_net_iface() - vm.start() - # Check if guest works. - exit_code, _, _ = vm.ssh.run("ls") - assert exit_code == 0 - snapshot = vm.snapshot_full() - - metrics.set_dimensions( - { - **vm.dimensions, - "io_engine": io_engine, - "performance_test": "test_older_snapshot_resume_latency", - "firecracker_version": firecracker_release.name, - } - ) - - for _ in range(SAMPLE_COUNT): - metrics.put_metric( - "latency", - snapshot_resume_producer(microvm_factory, snapshot), - "Milliseconds", - ) - - def test_snapshot_create_latency( microvm_factory, guest_kernel_linux_4_14, From d58177be9be5fd9674096316268b0cdccf31c243 Mon Sep 17 00:00:00 2001 From: Babis Chalios Date: Tue, 14 Nov 2023 07:57:48 +0000 Subject: [PATCH 11/11] doc: adapt snapshot versioning docs and CHANGELOG Adapt documentation around snapshot versions format and compatibility to remove mentions of Versionize and the effects its usage had to snapshot compatibility. Also, add a CHANGELOG entry regarding the new strategy for snapshot versioning. Signed-off-by: Babis Chalios --- CHANGELOG.md | 13 +++ docs/images/version_graph.png | Bin 24903 -> 0 bytes docs/images/versionize.png | Bin 48235 -> 0 bytes docs/snapshotting/snapshot-support.md | 55 ++--------- docs/snapshotting/versioning.md | 133 +++++++------------------- 5 files changed, 53 insertions(+), 148 deletions(-) delete mode 100644 docs/images/version_graph.png delete mode 100644 docs/images/versionize.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 832e1f7d2ce..7304020cceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `VcpuExit::IoOut`. The average for these VM exits is not emitted since it can be deduced from the available emitted metrics. +### Changed + +- [#4230](https://github.com/firecracker-microvm/firecracker/pull/4230): + Changed microVM snapshot format version strategy. Firecracker snapshot format + now has a version that is independent of Firecracker version. The current + version of the snapshot format is v1.0.0. From now on, the Firecracker binary + will define the snapshot format version it supports and it will only be able + to load snapshots with format that is backwards compatible with that version. + Users can pass the `--snapshot-version` flag to the Firecracker binary to see + its supported snapshot version format. This change renders all previous + Firecracker snapshots (up to Firecracker version v1.6.0) incompatible with + the current Firecracker version. + ## [v1.6.0] ### Added diff --git a/docs/images/version_graph.png b/docs/images/version_graph.png deleted file mode 100644 index 8b636d3791d7f5d8c99f722212286bd3c8ef165f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24903 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 MWo@Nq1)J#q1BBo=VE_OC diff --git a/docs/images/versionize.png b/docs/images/versionize.png deleted file mode 100644 index 4f21be53055c8e0484d6f34301f988e14a8503f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48235 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

m0ofTD-Npr*_zen)~Lecy`8m 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 S%Z4Kj{Ap|Gsh6r<3jQDH98V