diff --git a/src/firecracker/src/api_server/request/snapshot.rs b/src/firecracker/src/api_server/request/snapshot.rs index 46b0ae95e542..d043a248ecb1 100644 --- a/src/firecracker/src/api_server/request/snapshot.rs +++ b/src/firecracker/src/api_server/request/snapshot.rs @@ -121,7 +121,7 @@ fn parse_put_snapshot_load(body: &Body) -> Result { #[cfg(test)] mod tests { - use vmm::vmm_config::snapshot::{MemBackendConfig, MemBackendType}; + use vmm::vmm_config::snapshot::{MemBackendConfig, MemBackendType, NetworkOverride}; use super::*; use crate::api_server::parsed_request::tests::{depr_action_from_req, vmm_action_from_request}; @@ -182,6 +182,7 @@ mod tests { }, enable_diff_snapshots: false, resume_vm: false, + network_overrides: vec![], }; let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap(); assert!(parsed_request @@ -209,6 +210,7 @@ mod tests { }, enable_diff_snapshots: true, resume_vm: false, + network_overrides: vec![], }; let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap(); assert!(parsed_request @@ -236,6 +238,44 @@ mod tests { }, enable_diff_snapshots: false, resume_vm: true, + network_overrides: vec![], + }; + let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap(); + assert!(parsed_request + .parsing_info() + .take_deprecation_message() + .is_none()); + assert_eq!( + vmm_action_from_request(parsed_request), + VmmAction::LoadSnapshot(expected_config) + ); + + let body = r#"{ + "snapshot_path": "foo", + "mem_backend": { + "backend_path": "bar", + "backend_type": "Uffd" + }, + "resume_vm": true, + "network_overrides": [ + { + "iface_id": "eth0", + "host_dev_name": "vmtap2" + } + ] + }"#; + let expected_config = LoadSnapshotParams { + snapshot_path: PathBuf::from("foo"), + mem_backend: MemBackendConfig { + backend_path: PathBuf::from("bar"), + backend_type: MemBackendType::Uffd, + }, + enable_diff_snapshots: false, + resume_vm: true, + network_overrides: vec![NetworkOverride { + iface_id: String::from("eth0"), + host_dev_name: String::from("vmtap2"), + }], }; let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap(); assert!(parsed_request @@ -260,6 +300,7 @@ mod tests { }, enable_diff_snapshots: false, resume_vm: true, + network_overrides: vec![], }; let parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap(); assert_eq!( diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index ab3a16774dcd..0acf3b3ab0e7 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -405,7 +405,10 @@ pub fn restore_from_snapshot( .iter_mut() .find(|x| x.device_state.id == entry.iface_id) { - device.device_state.tap_if_name = entry.host_dev_name.clone(); + device + .device_state + .tap_if_name + .clone_from(&entry.host_dev_name); } else { return Err(BuildMicrovmFromSnapshotError::UnknownNetworkDevice.into()); } diff --git a/tests/framework/microvm.py b/tests/framework/microvm.py index 969a6452ce43..98fd44552b8b 100644 --- a/tests/framework/microvm.py +++ b/tests/framework/microvm.py @@ -26,7 +26,7 @@ from enum import Enum from functools import lru_cache from pathlib import Path -from typing import Optional +from typing import Dict, Optional from tenacity import retry, stop_after_attempt, wait_fixed @@ -925,6 +925,7 @@ def restore_from_snapshot( snapshot: Snapshot, resume: bool = False, uffd_path: Path = None, + rename_interfaces: Dict[str, str] = None, ): """Restore a snapshot""" jailed_snapshot = snapshot.copy_to_chroot(Path(self.chroot())) @@ -947,11 +948,19 @@ def restore_from_snapshot( if uffd_path is not None: mem_backend = {"backend_type": "Uffd", "backend_path": str(uffd_path)} + iface_overrides = [] + if rename_interfaces: + iface_overrides = [ + {"iface_id": k, "host_dev_name": v} + for k, v in rename_interfaces.items() + ] + self.api.snapshot_load.put( mem_backend=mem_backend, snapshot_path=str(jailed_vmstate), enable_diff_snapshots=snapshot.is_diff, resume_vm=resume, + network_overrides=iface_overrides, ) return jailed_snapshot @@ -992,13 +1001,13 @@ def thread_backtraces(self): ) return "\n".join(backtraces) - def wait_for_up(self, timeout=10): + def wait_for_up(self, timeout=10, iface=0): """Wait for guest running inside the microVM to come up and respond. :param timeout: seconds to wait. """ try: - rc, stdout, stderr = self.ssh.run("true", timeout) + rc, stdout, stderr = self.ssh_iface(iface).run("true", timeout) except subprocess.TimeoutExpired: print( f"Remote command did not respond within {timeout}s\n\n" diff --git a/tests/integration_tests/functional/test_snapshot_basic.py b/tests/integration_tests/functional/test_snapshot_basic.py index 998d5d027af2..34ed3a249aa4 100644 --- a/tests/integration_tests/functional/test_snapshot_basic.py +++ b/tests/integration_tests/functional/test_snapshot_basic.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 """Basic tests scenarios for snapshot save/restore.""" +import dataclasses import filecmp import logging import os @@ -13,6 +14,7 @@ import pytest import host_tools.drive as drive_tools +import host_tools.network as net_tools from framework.microvm import SnapshotType from framework.utils import check_filesystem, check_output from framework.utils_vsock import ( @@ -577,3 +579,31 @@ def test_vmgenid(guest_kernel_linux_6_1, rootfs, microvm_factory, snapshot_type) # Update the base for next iteration base_snapshot = snapshot + + +def test_snapshot_rename_interface(uvm_nano, microvm_factory): + """ + Test that we can restore a snapshot and point its interface to a + different host interface. + """ + base_iface = net_tools.NetIfaceConfig.with_id(0) + + vm = uvm_nano + iface1 = dataclasses.replace(base_iface, tap_name="tap1") + vm.add_net_iface(iface=iface1) + # Create an interface but don't attach it to the device + vm.start() + vm.wait_for_up() + + snapshot = vm.snapshot_full() + + restored_vm = microvm_factory.build() + restored_vm.spawn() + iface2 = dataclasses.replace(base_iface, tap_name="tap2") + snapshot.net_ifaces.clear() + snapshot.net_ifaces.append(iface2) + restored_vm.restore_from_snapshot( + snapshot, rename_interfaces={base_iface.dev_name: "tap2"} + ) + restored_vm.resume() + restored_vm.wait_for_up()