1
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
# SPDX-License-Identifier: Apache-2.0
3
+
3
4
"""Classes for working with microVMs.
4
5
5
6
This module defines `Microvm`, which can be used to create, test drive, and
8
9
- Use the Firecracker Open API spec to populate Microvm API resource URLs.
9
10
"""
10
11
12
+ # pylint:disable=too-many-lines
13
+
11
14
import json
12
15
import logging
13
16
import os
18
21
import uuid
19
22
import weakref
20
23
from collections import namedtuple
24
+ from dataclasses import dataclass
25
+ from enum import Enum
21
26
from functools import lru_cache
22
27
from pathlib import Path
23
28
from threading import Lock
59
64
data_lock = Lock ()
60
65
61
66
67
+ class SnapshotType (Enum ):
68
+ """Supported snapshot types."""
69
+
70
+ FULL = "FULL"
71
+ DIFF = "DIFF"
72
+
73
+ def __repr__ (self ):
74
+ cls_name = self .__class__ .__name__
75
+ return f"{ cls_name } .{ self .name } "
76
+
77
+
78
+ def hardlink_or_copy (src , dst ):
79
+ """If src and dst are in the same device, hardlink. Otherwise, copy."""
80
+ dst .touch (exist_ok = False )
81
+ if dst .stat ().st_dev == src .stat ().st_dev :
82
+ dst .unlink ()
83
+ dst .hardlink_to (src )
84
+ else :
85
+ shutil .copyfile (src , dst )
86
+
87
+
88
+ @dataclass (frozen = True , repr = True )
89
+ class Snapshot :
90
+ """A Firecracker snapshot"""
91
+
92
+ vmstate : Path
93
+ mem : Path
94
+ net_ifaces : list
95
+ disks : dict
96
+ ssh_key : Path
97
+ snapshot_type : str
98
+
99
+ @property
100
+ def is_diff (self ) -> bool :
101
+ """Is this a DIFF snapshot?"""
102
+ return self .snapshot_type == SnapshotType .DIFF
103
+
104
+ def rebase_snapshot (self , base ):
105
+ """Rebases current incremental snapshot onto a specified base layer."""
106
+ if not self .is_diff :
107
+ raise ValueError ("Can only rebase DIFF snapshots" )
108
+ build_tools .run_rebase_snap_bin (base .mem , self .mem )
109
+ new_args = self .__dict__ | {"mem" : base .mem }
110
+ return Snapshot (** new_args )
111
+
112
+ @classmethod
113
+ # TBD when Python 3.11: -> Self
114
+ def load_from (cls , src : Path ) -> "Snapshot" :
115
+ """Load a snapshot saved with `save_to`"""
116
+ snap_json = src / "snapshot.json"
117
+ obj = json .loads (snap_json .read_text ())
118
+ return cls (
119
+ vmstate = src / obj ["vmstate" ],
120
+ mem = src / obj ["mem" ],
121
+ net_ifaces = [NetIfaceConfig (** d ) for d in obj ["net_ifaces" ]],
122
+ disks = {dsk : src / p for dsk , p in obj ["disks" ].items ()},
123
+ ssh_key = src / obj ["ssh_key" ],
124
+ snapshot_type = obj ["snapshot_type" ],
125
+ )
126
+
127
+ def save_to (self , dst : Path ):
128
+ """Serialize snapshot details to `dst`
129
+
130
+ Deserialize the snapshot with `load_from`
131
+ """
132
+ for path in [self .vmstate , self .mem , self .ssh_key ]:
133
+ new_path = dst / path .name
134
+ hardlink_or_copy (path , new_path )
135
+ new_disks = {}
136
+ for disk_id , path in self .disks .items ():
137
+ new_path = dst / path .name
138
+ hardlink_or_copy (path , new_path )
139
+ new_disks [disk_id ] = new_path .name
140
+ obj = {
141
+ "vmstate" : self .vmstate .name ,
142
+ "mem" : self .mem .name ,
143
+ "net_ifaces" : [x .__dict__ for x in self .net_ifaces ],
144
+ "disks" : new_disks ,
145
+ "ssh_key" : self .ssh_key .name ,
146
+ "snapshot_type" : self .snapshot_type ,
147
+ }
148
+ snap_json = dst / "snapshot.json"
149
+ snap_json .write_text (json .dumps (obj ))
150
+
151
+ def delete (self ):
152
+ """Delete the backing files from disk."""
153
+ self .mem .unlink ()
154
+ self .vmstate .unlink ()
155
+
156
+
62
157
# pylint: disable=R0904
63
158
class Microvm :
64
159
"""Class to represent a Firecracker microvm.
@@ -82,7 +177,6 @@ def __init__(
82
177
monitor_memory = True ,
83
178
bin_cloner_path = None ,
84
179
):
85
- # pylint: disable=too-many-statements
86
180
"""Set up microVM attributes, paths, and data structures."""
87
181
# pylint: disable=too-many-statements
88
182
# Unique identifier for this machine.
@@ -750,60 +844,114 @@ def start(self, check=True):
750
844
except KeyError :
751
845
assert self .started is True
752
846
847
+ def pause (self ):
848
+ """Pauses the microVM"""
849
+ response = self .vm .patch (state = "Paused" )
850
+ assert self .api_session .is_status_no_content (response .status_code )
851
+
852
+ def resume (self ):
853
+ """Resume the microVM"""
854
+ response = self .vm .patch (state = "Resumed" )
855
+ assert self .api_session .is_status_no_content (response .status_code )
856
+
753
857
def pause_to_snapshot (
754
- self , mem_file_path = None , snapshot_path = None , diff = False , version = None
858
+ self ,
859
+ mem_file_path ,
860
+ snapshot_path ,
861
+ diff = False ,
862
+ version = None ,
755
863
):
756
864
"""Pauses the microVM, and creates snapshot.
757
865
758
866
This function validates that the microVM pauses successfully and
759
867
creates a snapshot.
760
868
"""
761
- assert mem_file_path is not None , "Please specify mem_file_path."
762
- assert snapshot_path is not None , "Please specify snapshot_path."
763
-
764
- response = self .vm .patch (state = "Paused" )
765
- assert self .api_session .is_status_no_content (response .status_code )
869
+ self .pause ()
766
870
767
871
response = self .snapshot .create (
768
- mem_file_path = mem_file_path ,
769
- snapshot_path = snapshot_path ,
872
+ mem_file_path = str ( mem_file_path ) ,
873
+ snapshot_path = str ( snapshot_path ) ,
770
874
diff = diff ,
771
875
version = version ,
772
876
)
773
877
assert self .api_session .is_status_no_content (
774
878
response .status_code
775
879
), response .text
776
880
881
+ def make_snapshot (self , snapshot_type : str , target_version : str = None ):
882
+ """Create a Snapshot object from a microvm."""
883
+ vmstate_path = "vmstate"
884
+ mem_path = "mem"
885
+ self .pause_to_snapshot (
886
+ mem_file_path = mem_path ,
887
+ snapshot_path = vmstate_path ,
888
+ diff = snapshot_type == "DIFF" ,
889
+ version = target_version ,
890
+ )
891
+ root = Path (self .chroot ())
892
+ return Snapshot (
893
+ vmstate = root / vmstate_path ,
894
+ mem = root / mem_path ,
895
+ disks = self .disks ,
896
+ net_ifaces = [x ["iface" ] for ifname , x in self .iface .items ()],
897
+ ssh_key = self .ssh_key ,
898
+ snapshot_type = snapshot_type ,
899
+ )
900
+
901
+ def snapshot_diff (self , target_version : str = None ):
902
+ """Make a DIFF snapshot"""
903
+ return self .make_snapshot ("DIFF" , target_version )
904
+
905
+ def snapshot_full (self , target_version : str = None ):
906
+ """Make a FULL snapshot"""
907
+ return self .make_snapshot ("FULL" , target_version )
908
+
777
909
def restore_from_snapshot (
778
910
self ,
779
- * ,
780
- snapshot_mem : Path ,
781
- snapshot_vmstate : Path ,
782
- snapshot_disks : list [Path ],
783
- snapshot_is_diff : bool = False ,
911
+ snapshot : Snapshot ,
912
+ resume : bool = False ,
913
+ uffd_path : Path = None ,
784
914
):
785
- """
786
- Restores a snapshot, and resumes the microvm
787
- """
788
-
789
- # Hardlink all the snapshot files into the microvm jail.
790
- jailed_mem = self .create_jailed_resource (snapshot_mem )
791
- jailed_vmstate = self .create_jailed_resource (snapshot_vmstate )
792
-
915
+ """Restore a snapshot"""
916
+ # Move all the snapshot files into the microvm jail.
917
+ # Use different names so a snapshot doesn't overwrite our original snapshot.
918
+ chroot = Path (self .chroot ())
919
+ mem_src = chroot / snapshot .mem .with_suffix (".src" ).name
920
+ hardlink_or_copy (snapshot .mem , mem_src )
921
+ vmstate_src = chroot / snapshot .vmstate .with_suffix (".src" ).name
922
+ hardlink_or_copy (snapshot .vmstate , vmstate_src )
923
+ jailed_mem = Path ("/" ) / mem_src .name
924
+ jailed_vmstate = Path ("/" ) / vmstate_src .name
925
+
926
+ snapshot_disks = [v for k , v in snapshot .disks .items ()]
793
927
assert len (snapshot_disks ) > 0 , "Snapshot requires at least one disk."
794
928
jailed_disks = []
795
929
for disk in snapshot_disks :
796
930
jailed_disks .append (self .create_jailed_resource (disk ))
931
+ self .disks = snapshot .disks
932
+ self .ssh_key = snapshot .ssh_key
933
+
934
+ # Create network interfaces.
935
+ for iface in snapshot .net_ifaces :
936
+ self .add_net_iface (iface , api = False )
937
+
938
+ mem_backend = {"type" : "File" , "path" : str (jailed_mem )}
939
+ if uffd_path is not None :
940
+ mem_backend = {"type" : "Uffd" , "path" : str (uffd_path )}
797
941
798
942
response = self .snapshot .load (
799
- mem_file_path = jailed_mem ,
800
- snapshot_path = jailed_vmstate ,
801
- diff = snapshot_is_diff ,
802
- resume = True ,
943
+ mem_backend = mem_backend ,
944
+ snapshot_path = str ( jailed_vmstate ) ,
945
+ diff = snapshot . is_diff ,
946
+ resume = resume ,
803
947
)
804
948
assert response .ok , response .content
805
949
return True
806
950
951
+ def restore_from_path (self , snap_dir : Path , ** kwargs ):
952
+ """Restore snapshot from a path"""
953
+ return self .restore_from_snapshot (Snapshot .load_from (snap_dir ), ** kwargs )
954
+
807
955
@lru_cache
808
956
def ssh_iface (self , iface_idx = 0 ):
809
957
"""Return a cached SSH connection on a given interface id."""
0 commit comments