Skip to main content

openvmm_helpers/
snapshot.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Snapshot manifest types and I/O functions for saving/restoring VM snapshots.
5
6use mesh::payload::Protobuf;
7use mesh::payload::Timestamp;
8
9pub mod publish;
10pub mod restore;
11
12pub use publish::write_snapshot;
13pub use restore::read_snapshot;
14
15/// Current manifest format version. Bump when making incompatible changes.
16pub const MANIFEST_VERSION: u32 = 1;
17
18/// Manifest describing a VM snapshot.
19#[derive(Clone, Protobuf)]
20#[mesh(package = "openvmm.snapshot")]
21pub struct SnapshotManifest {
22    /// Manifest format version.
23    #[mesh(1)]
24    pub version: u32,
25    /// When the snapshot was created.
26    #[mesh(2)]
27    pub created_at: Timestamp,
28    /// OpenVMM version that created the snapshot.
29    #[mesh(3)]
30    pub openvmm_version: String,
31    /// Guest RAM size in bytes.
32    #[mesh(4)]
33    pub memory_size_bytes: u64,
34    /// Number of virtual processors.
35    #[mesh(5)]
36    pub vp_count: u32,
37    /// Page size in bytes.
38    #[mesh(6)]
39    pub page_size: u32,
40    /// Architecture string ("x86_64" or "aarch64").
41    #[mesh(7)]
42    pub architecture: String,
43}
44
45/// Validate that a snapshot manifest is compatible with the running VM config.
46///
47/// Checks version, architecture, memory size, VP count, and page size.
48/// Returns `Ok(())` if the manifest matches, or an error describing the
49/// first mismatch found.
50pub fn validate_manifest(
51    manifest: &SnapshotManifest,
52    expected_arch: &str,
53    expected_memory_size: u64,
54    expected_vp_count: u32,
55    expected_page_size: u32,
56) -> anyhow::Result<()> {
57    if manifest.version != MANIFEST_VERSION {
58        anyhow::bail!(
59            "snapshot manifest version {} is not supported (expected {})",
60            manifest.version,
61            MANIFEST_VERSION,
62        );
63    }
64
65    if manifest.architecture != expected_arch {
66        anyhow::bail!(
67            "snapshot architecture '{}' doesn't match expected '{}'",
68            manifest.architecture,
69            expected_arch,
70        );
71    }
72
73    if manifest.memory_size_bytes != expected_memory_size {
74        anyhow::bail!(
75            "snapshot memory size ({} bytes) doesn't match expected ({} bytes)",
76            manifest.memory_size_bytes,
77            expected_memory_size,
78        );
79    }
80
81    if manifest.vp_count != expected_vp_count {
82        anyhow::bail!(
83            "snapshot VP count ({}) doesn't match expected ({})",
84            manifest.vp_count,
85            expected_vp_count,
86        );
87    }
88
89    if manifest.page_size != expected_page_size {
90        anyhow::bail!(
91            "snapshot page size ({}) doesn't match expected ({})",
92            manifest.page_size,
93            expected_page_size,
94        );
95    }
96
97    Ok(())
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    /// Helper: build a test manifest with sensible defaults.
105    fn test_manifest() -> SnapshotManifest {
106        SnapshotManifest {
107            version: MANIFEST_VERSION,
108            created_at: Timestamp {
109                seconds: 1234567890,
110                nanos: 0,
111            },
112            openvmm_version: "test-0.1.0".to_string(),
113            memory_size_bytes: 1024,
114            vp_count: 2,
115            page_size: 4096,
116            architecture: "x86_64".to_string(),
117        }
118    }
119
120    #[test]
121    fn write_read_roundtrip() {
122        let dir = tempfile::tempdir().unwrap();
123        let snap_dir = dir.path().join("snap");
124
125        // Create a fake memory backing file in the same directory (same fs).
126        let mem_path = dir.path().join("memory.bin");
127        std::fs::write(&mem_path, b"FAKEMEM").unwrap();
128
129        let manifest = test_manifest();
130        let state = b"saved-state-data";
131
132        write_snapshot(&snap_dir, &manifest, state, &mem_path).unwrap();
133
134        let (read_manifest, read_state) = read_snapshot(&snap_dir).unwrap();
135        assert_eq!(read_manifest.version, manifest.version);
136        assert_eq!(read_manifest.memory_size_bytes, manifest.memory_size_bytes);
137        assert_eq!(read_manifest.vp_count, manifest.vp_count);
138        assert_eq!(read_manifest.architecture, manifest.architecture);
139        assert_eq!(read_state, state);
140
141        // memory.bin should exist in the snapshot directory.
142        assert!(snap_dir.join("memory.bin").exists());
143    }
144
145    #[test]
146    fn write_snapshot_creates_dir() {
147        let dir = tempfile::tempdir().unwrap();
148        let snap_dir = dir.path().join("a").join("b").join("c");
149
150        let mem_path = dir.path().join("memory.bin");
151        std::fs::write(&mem_path, b"MEM").unwrap();
152
153        write_snapshot(&snap_dir, &test_manifest(), b"state", &mem_path).unwrap();
154
155        assert!(snap_dir.join("manifest.bin").exists());
156        assert!(snap_dir.join("state.bin").exists());
157        assert!(snap_dir.join("memory.bin").exists());
158    }
159
160    #[test]
161    fn write_snapshot_same_memory_path() {
162        // When the memory backing file IS <snap_dir>/memory.bin, the function
163        // should detect the collision and skip the hard-link.
164        let dir = tempfile::tempdir().unwrap();
165        let snap_dir = dir.path().join("snap");
166        std::fs::create_dir_all(&snap_dir).unwrap();
167
168        let mem_path = snap_dir.join("memory.bin");
169        std::fs::write(&mem_path, b"SAMEFILE").unwrap();
170
171        // Should succeed without error.
172        write_snapshot(&snap_dir, &test_manifest(), b"state", &mem_path).unwrap();
173
174        // The file content should be unchanged.
175        assert_eq!(std::fs::read(&mem_path).unwrap(), b"SAMEFILE");
176    }
177
178    #[test]
179    fn read_snapshot_missing_file() {
180        let dir = tempfile::tempdir().unwrap();
181        // No files written — read should fail.
182        let result = read_snapshot(dir.path());
183        assert!(result.is_err());
184    }
185
186    #[test]
187    fn validate_manifest_ok() {
188        let manifest = test_manifest();
189        validate_manifest(&manifest, "x86_64", 1024, 2, 4096).unwrap();
190    }
191
192    #[test]
193    fn validate_manifest_wrong_arch() {
194        let manifest = test_manifest();
195        let err = validate_manifest(&manifest, "aarch64", 1024, 2, 4096).unwrap_err();
196        assert!(
197            err.to_string().contains("architecture"),
198            "unexpected error: {err}"
199        );
200    }
201
202    #[test]
203    fn validate_manifest_wrong_memory_size() {
204        let manifest = test_manifest();
205        let err = validate_manifest(&manifest, "x86_64", 9999, 2, 4096).unwrap_err();
206        assert!(
207            err.to_string().contains("memory size"),
208            "unexpected error: {err}"
209        );
210    }
211
212    #[test]
213    fn validate_manifest_wrong_vp_count() {
214        let manifest = test_manifest();
215        let err = validate_manifest(&manifest, "x86_64", 1024, 99, 4096).unwrap_err();
216        assert!(
217            err.to_string().contains("VP count"),
218            "unexpected error: {err}"
219        );
220    }
221
222    #[test]
223    fn validate_manifest_wrong_page_size() {
224        let manifest = test_manifest();
225        let err = validate_manifest(&manifest, "x86_64", 1024, 2, 65536).unwrap_err();
226        assert!(
227            err.to_string().contains("page size"),
228            "unexpected error: {err}"
229        );
230    }
231
232    #[test]
233    fn validate_manifest_wrong_version() {
234        let mut manifest = test_manifest();
235        manifest.version = 999;
236        let err = validate_manifest(&manifest, "x86_64", 1024, 2, 4096).unwrap_err();
237        assert!(
238            err.to_string().contains("version"),
239            "unexpected error: {err}"
240        );
241    }
242}