Skip to content

Commit aa336b4

Browse files
committed
feat(snapshot): allow block device path override on restore
Allow overriding block device host paths when restoring from a snapshot. This is useful when disk paths are non-deterministic (e.g. container runtimes using devmapper) or when restoring on a different host where the backing file is at a different location. Adds a drive_overrides parameter to the snapshot load API, following the same pattern as the existing network_overrides and vsock_override. Includes Python integration tests that verify the override works with a different file and that an unknown drive_id is rejected. Closes #4014 Signed-off-by: Amit Patil <iamitpatil2001@gmail.com>
1 parent 86aac24 commit aa336b4

File tree

13 files changed

+314
-7
lines changed

13 files changed

+314
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to
1010

1111
### Added
1212

13+
- [#5774](https://github.com/firecracker-microvm/firecracker/pull/5774): Add
14+
support for block device path overriding on snapshot restore.
1315
- [#5323](https://github.com/firecracker-microvm/firecracker/pull/5323): Add
1416
support for Vsock Unix domain socket path overriding on snapshot restore. More
1517
information can be found in the

docs/snapshotting/network-for-clones.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,37 @@ ip addr add 172.16.3.2/30 dev eth0
200200
ip route add default via 172.16.3.1/30 dev eth0
201201
```
202202

203+
### Overriding block device paths
204+
205+
When restoring a VM from a snapshot on a different host or in an environment
206+
where disk paths are non-deterministic (e.g. container runtimes using
207+
devmapper), the block device paths baked into the snapshot state may no longer
208+
be valid. In this case you can use the `drive_overrides` parameter of the
209+
snapshot restore API to specify the new host path for each block device.
210+
211+
For example, if we have a block device with drive ID `rootfs` in the snapshotted
212+
microVM, we can override its path during snapshot resume:
213+
214+
```bash
215+
curl --unix-socket /tmp/firecracker.socket -i \
216+
-X PUT 'http://localhost/snapshot/load' \
217+
-H 'Accept: application/json' \
218+
-H 'Content-Type: application/json' \
219+
-d '{
220+
"snapshot_path": "./snapshot_file",
221+
"mem_backend": {
222+
"backend_path": "./mem_file",
223+
"backend_type": "File"
224+
},
225+
"drive_overrides": [
226+
{
227+
"drive_id": "rootfs",
228+
"path_on_host": "/new/path/to/rootfs.ext4"
229+
}
230+
]
231+
}'
232+
```
233+
203234
# Ingress connectivity
204235

205236
The above setup only provides egress connectivity. If in addition we also want

src/firecracker/src/api_server/request/snapshot.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ fn parse_put_snapshot_load(body: &Body) -> Result<ParsedRequest, RequestError> {
111111
resume_vm: snapshot_config.resume_vm,
112112
network_overrides: snapshot_config.network_overrides,
113113
vsock_override: snapshot_config.vsock_override,
114+
drive_overrides: snapshot_config.drive_overrides,
114115
};
115116

116117
// Construct the `ParsedRequest` object.
@@ -126,7 +127,9 @@ fn parse_put_snapshot_load(body: &Body) -> Result<ParsedRequest, RequestError> {
126127

127128
#[cfg(test)]
128129
mod tests {
129-
use vmm::vmm_config::snapshot::{MemBackendConfig, MemBackendType, NetworkOverride};
130+
use vmm::vmm_config::snapshot::{
131+
DriveOverride, MemBackendConfig, MemBackendType, NetworkOverride,
132+
};
130133

131134
use super::*;
132135
use crate::api_server::parsed_request::tests::{depr_action_from_req, vmm_action_from_request};
@@ -189,6 +192,7 @@ mod tests {
189192
resume_vm: false,
190193
network_overrides: vec![],
191194
vsock_override: None,
195+
drive_overrides: vec![],
192196
};
193197
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
194198
assert!(
@@ -220,6 +224,7 @@ mod tests {
220224
resume_vm: false,
221225
network_overrides: vec![],
222226
vsock_override: None,
227+
drive_overrides: vec![],
223228
};
224229
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
225230
assert!(
@@ -251,6 +256,7 @@ mod tests {
251256
resume_vm: true,
252257
network_overrides: vec![],
253258
vsock_override: None,
259+
drive_overrides: vec![],
254260
};
255261
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
256262
assert!(
@@ -291,6 +297,48 @@ mod tests {
291297
host_dev_name: String::from("vmtap2"),
292298
}],
293299
vsock_override: None,
300+
drive_overrides: vec![],
301+
};
302+
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
303+
assert!(
304+
parsed_request
305+
.parsing_info()
306+
.take_deprecation_message()
307+
.is_none()
308+
);
309+
assert_eq!(
310+
vmm_action_from_request(parsed_request),
311+
VmmAction::LoadSnapshot(expected_config)
312+
);
313+
314+
let body = r#"{
315+
"snapshot_path": "foo",
316+
"mem_backend": {
317+
"backend_path": "bar",
318+
"backend_type": "File"
319+
},
320+
"resume_vm": true,
321+
"drive_overrides": [
322+
{
323+
"drive_id": "rootfs",
324+
"path_on_host": "/new/path/rootfs.ext4"
325+
}
326+
]
327+
}"#;
328+
let expected_config = LoadSnapshotParams {
329+
snapshot_path: PathBuf::from("foo"),
330+
mem_backend: MemBackendConfig {
331+
backend_path: PathBuf::from("bar"),
332+
backend_type: MemBackendType::File,
333+
},
334+
track_dirty_pages: false,
335+
resume_vm: true,
336+
network_overrides: vec![],
337+
vsock_override: None,
338+
drive_overrides: vec![DriveOverride {
339+
drive_id: String::from("rootfs"),
340+
path_on_host: String::from("/new/path/rootfs.ext4"),
341+
}],
294342
};
295343
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
296344
assert!(
@@ -319,6 +367,7 @@ mod tests {
319367
resume_vm: true,
320368
network_overrides: vec![],
321369
vsock_override: None,
370+
drive_overrides: vec![],
322371
};
323372
let parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
324373
assert_eq!(

src/firecracker/swagger/firecracker.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,24 @@ definitions:
16031603
description:
16041604
The new path for the backing Unix Domain Socket.
16051605

1606+
DriveOverride:
1607+
type: object
1608+
description:
1609+
Allows for changing the backing host path of a block device
1610+
during snapshot restore.
1611+
required:
1612+
- drive_id
1613+
- path_on_host
1614+
properties:
1615+
drive_id:
1616+
type: string
1617+
description:
1618+
The ID of the drive to modify
1619+
path_on_host:
1620+
type: string
1621+
description:
1622+
The new path on the host for the block device's backing file
1623+
16061624
SnapshotLoadParams:
16071625
type: object
16081626
description:
@@ -1650,6 +1668,11 @@ definitions:
16501668
for restoring a snapshot with a different socket path than the one used
16511669
when the snapshot was created. For example, when the original socket path
16521670
is no longer available or when deploying to a different environment.
1671+
drive_overrides:
1672+
type: array
1673+
description: Block device host paths to override
1674+
items:
1675+
$ref: "#/definitions/DriveOverride"
16531676

16541677

16551678
TokenBucket:

src/vmm/src/devices/virtio/block/persist.rs

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,24 @@ pub enum BlockState {
2020
impl BlockState {
2121
pub fn is_activated(&self) -> bool {
2222
match self {
23-
BlockState::Virtio(virtio_block_state) => virtio_block_state.virtio_state.activated,
24-
BlockState::VhostUser(vhost_user_block_state) => false,
23+
BlockState::Virtio(state) => state.virtio_state.activated,
24+
BlockState::VhostUser(_) => false,
25+
}
26+
}
27+
28+
/// Returns the drive ID.
29+
pub fn id(&self) -> &str {
30+
match self {
31+
BlockState::Virtio(state) => &state.id,
32+
BlockState::VhostUser(state) => &state.id,
33+
}
34+
}
35+
36+
/// Overrides the host path (disk path or socket path) of the block device.
37+
pub fn set_host_path(&mut self, path: &str) {
38+
match self {
39+
BlockState::Virtio(state) => state.disk_path = path.to_string(),
40+
BlockState::VhostUser(state) => state.socket_path = path.to_string(),
2541
}
2642
}
2743
}
@@ -31,3 +47,90 @@ impl BlockState {
3147
pub struct BlockConstructorArgs {
3248
pub mem: GuestMemoryMmap,
3349
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
use crate::devices::virtio::block::CacheType;
55+
use crate::devices::virtio::block::virtio::persist::FileEngineTypeState;
56+
use crate::devices::virtio::device::VirtioDeviceType;
57+
use crate::devices::virtio::persist::VirtioDeviceState;
58+
use crate::rate_limiter::persist::RateLimiterState;
59+
60+
fn virtio_block_state(id: &str, disk_path: &str, activated: bool) -> VirtioBlockState {
61+
VirtioBlockState {
62+
id: id.to_string(),
63+
partuuid: None,
64+
cache_type: CacheType::Unsafe,
65+
root_device: false,
66+
disk_path: disk_path.to_string(),
67+
virtio_state: VirtioDeviceState {
68+
device_type: VirtioDeviceType::Block,
69+
avail_features: 0,
70+
acked_features: 0,
71+
queues: vec![],
72+
activated,
73+
},
74+
rate_limiter_state: Default::default(),
75+
file_engine_type: FileEngineTypeState::Sync,
76+
}
77+
}
78+
79+
fn vhost_user_block_state(id: &str, socket_path: &str) -> VhostUserBlockState {
80+
VhostUserBlockState {
81+
id: id.to_string(),
82+
partuuid: None,
83+
cache_type: CacheType::Unsafe,
84+
root_device: false,
85+
socket_path: socket_path.to_string(),
86+
vu_acked_protocol_features: 0,
87+
config_space: vec![],
88+
virtio_state: VirtioDeviceState {
89+
device_type: VirtioDeviceType::Block,
90+
avail_features: 0,
91+
acked_features: 0,
92+
queues: vec![],
93+
activated: false,
94+
},
95+
}
96+
}
97+
98+
#[test]
99+
fn test_block_state_id() {
100+
let virtio = BlockState::Virtio(virtio_block_state("rootfs", "/path", false));
101+
assert_eq!(virtio.id(), "rootfs");
102+
103+
let vhost = BlockState::VhostUser(vhost_user_block_state("scratch", "/sock"));
104+
assert_eq!(vhost.id(), "scratch");
105+
}
106+
107+
#[test]
108+
fn test_block_state_is_activated() {
109+
let active = BlockState::Virtio(virtio_block_state("rootfs", "/path", true));
110+
assert!(active.is_activated());
111+
112+
let inactive = BlockState::Virtio(virtio_block_state("rootfs", "/path", false));
113+
assert!(!inactive.is_activated());
114+
115+
// vhost-user always returns false
116+
let vhost = BlockState::VhostUser(vhost_user_block_state("rootfs", "/sock"));
117+
assert!(!vhost.is_activated());
118+
}
119+
120+
#[test]
121+
fn test_block_state_set_host_path() {
122+
let mut virtio = BlockState::Virtio(virtio_block_state("rootfs", "/old/path", false));
123+
virtio.set_host_path("/new/path");
124+
match &virtio {
125+
BlockState::Virtio(state) => assert_eq!(state.disk_path, "/new/path"),
126+
_ => panic!("expected Virtio variant"),
127+
}
128+
129+
let mut vhost = BlockState::VhostUser(vhost_user_block_state("rootfs", "/old/sock"));
130+
vhost.set_host_path("/new/sock");
131+
match &vhost {
132+
BlockState::VhostUser(state) => assert_eq!(state.socket_path, "/new/sock"),
133+
_ => panic!("expected VhostUser variant"),
134+
}
135+
}
136+
}

src/vmm/src/devices/virtio/block/vhost_user/persist.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ use crate::snapshot::Persist;
1515
/// vhost-user block device state.
1616
#[derive(Debug, Clone, Serialize, Deserialize)]
1717
pub struct VhostUserBlockState {
18-
id: String,
18+
pub id: String,
1919
partuuid: Option<String>,
2020
cache_type: CacheType,
2121
root_device: bool,
22-
socket_path: String,
22+
pub socket_path: String,
2323
vu_acked_protocol_features: u64,
2424
config_space: Vec<u8>,
2525
virtio_state: VirtioDeviceState,

src/vmm/src/devices/virtio/block/virtio/persist.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ impl From<FileEngineTypeState> for FileEngineType {
5252
/// Holds info about the block device. Gets saved in snapshot.
5353
#[derive(Debug, Clone, Serialize, Deserialize)]
5454
pub struct VirtioBlockState {
55-
id: String,
55+
pub id: String,
5656
partuuid: Option<String>,
5757
cache_type: CacheType,
5858
root_device: bool,
59-
disk_path: String,
59+
pub disk_path: String,
6060
pub virtio_state: VirtioDeviceState,
6161
rate_limiter_state: RateLimiterState,
6262
file_engine_type: FileEngineTypeState,

src/vmm/src/persist.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,26 @@ pub fn restore_from_snapshot(
408408
.clone_from(&vsock_override.uds_path);
409409
}
410410

411+
for entry in &params.drive_overrides {
412+
microvm_state
413+
.device_states
414+
.mmio_state
415+
.block_devices
416+
.iter_mut()
417+
.map(|device| &mut device.device_state)
418+
.chain(
419+
microvm_state
420+
.device_states
421+
.pci_state
422+
.block_devices
423+
.iter_mut()
424+
.map(|device| &mut device.device_state),
425+
)
426+
.find(|x| x.id() == entry.drive_id)
427+
.map(|device_state| device_state.set_host_path(&entry.path_on_host))
428+
.ok_or(SnapshotStateFromFileError::UnknownBlockDevice)?;
429+
}
430+
411431
let track_dirty_pages = params.track_dirty_pages;
412432

413433
let vcpu_count = microvm_state
@@ -481,6 +501,8 @@ pub enum SnapshotStateFromFileError {
481501
UnknownNetworkDevice,
482502
/// Unknown Vsock Device.
483503
UnknownVsockDevice,
504+
/// Unknown Block Device.
505+
UnknownBlockDevice,
484506
}
485507

486508
fn snapshot_state_from_file(

src/vmm/src/rpc_interface.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,7 @@ mod tests {
12861286
resume_vm: false,
12871287
network_overrides: vec![],
12881288
vsock_override: None,
1289+
drive_overrides: vec![],
12891290
},
12901291
)));
12911292
check_unsupported(runtime_request(VmmAction::SetEntropyDevice(

0 commit comments

Comments
 (0)