pyrecracker 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrecracker-0.5.1/PKG-INFO +52 -0
- pyrecracker-0.5.1/README.md +37 -0
- pyrecracker-0.5.1/pyproject.toml +26 -0
- pyrecracker-0.5.1/src/pyrecracker/__init__.py +0 -0
- pyrecracker-0.5.1/src/pyrecracker/client.py +175 -0
- pyrecracker-0.5.1/src/pyrecracker/client_types.py +218 -0
- pyrecracker-0.5.1/src/pyrecracker/cmd.py +132 -0
- pyrecracker-0.5.1/src/pyrecracker/host_env.py +337 -0
- pyrecracker-0.5.1/src/pyrecracker/vm.py +510 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrecracker
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Python module for creating and controlling micro VMs with Firecracker
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Author: Jake Jongewaard
|
|
7
|
+
Author-email: jjongewaard1@gmail.com
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: requests-unixsocket (>=0.4.1,<0.5.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Pyrecracker
|
|
17
|
+
Pyrcracker is a python module that can be used to create, run, and manage the lifecycle of micro virtual machines using Firecracker. Pyrecracker gives a simple to use API for VM management that can be used from within python based applications.
|
|
18
|
+
|
|
19
|
+
## Supported Operating Systems
|
|
20
|
+
The following are the operating systems that pyrecracker has been tested on:
|
|
21
|
+
- Ubuntu 24.04 LTS (with nested virtualization enabled)
|
|
22
|
+
|
|
23
|
+
## Installing
|
|
24
|
+
If using pip, pyrecracker can be installed with:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install pyrecracker
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If using poetry, pyrecracker can be installed with:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
poetry add pyrecracker
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Building
|
|
37
|
+
This project uses poetry for building and packaging. Pyrecracker can be built with the following command:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
poetry build
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This will generate the project's tar.gz and .whl files in the `./dist` directory.
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
Unit tests can be run with the following:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
poetry run pytest
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Examples on using pyrecracker can be found in the `./examples` directory.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Pyrecracker
|
|
2
|
+
Pyrcracker is a python module that can be used to create, run, and manage the lifecycle of micro virtual machines using Firecracker. Pyrecracker gives a simple to use API for VM management that can be used from within python based applications.
|
|
3
|
+
|
|
4
|
+
## Supported Operating Systems
|
|
5
|
+
The following are the operating systems that pyrecracker has been tested on:
|
|
6
|
+
- Ubuntu 24.04 LTS (with nested virtualization enabled)
|
|
7
|
+
|
|
8
|
+
## Installing
|
|
9
|
+
If using pip, pyrecracker can be installed with:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install pyrecracker
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If using poetry, pyrecracker can be installed with:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
poetry add pyrecracker
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Building
|
|
22
|
+
This project uses poetry for building and packaging. Pyrecracker can be built with the following command:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
poetry build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This will generate the project's tar.gz and .whl files in the `./dist` directory.
|
|
29
|
+
|
|
30
|
+
## Testing
|
|
31
|
+
Unit tests can be run with the following:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
poetry run pytest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Examples on using pyrecracker can be found in the `./examples` directory.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyrecracker"
|
|
3
|
+
version = "0.5.1"
|
|
4
|
+
description = "Python module for creating and controlling micro VMs with Firecracker"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Jake Jongewaard",email = "jjongewaard1@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"requests-unixsocket (>=0.4.1,<0.5.0)"
|
|
12
|
+
]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
|
|
15
|
+
[tool.poetry]
|
|
16
|
+
packages = [{include = "pyrecracker", from = "src"}]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
20
|
+
build-backend = "poetry.core.masonry.api"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest (>=9.0.2,<10.0.0)",
|
|
25
|
+
"pytest-mock (>=3.15.1,<4.0.0)"
|
|
26
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import requests_unixsocket
|
|
6
|
+
from requests.exceptions import HTTPError
|
|
7
|
+
|
|
8
|
+
from pyrecracker.client_types import (
|
|
9
|
+
VM,
|
|
10
|
+
MachineConfiguration,
|
|
11
|
+
BootSource,
|
|
12
|
+
Drive,
|
|
13
|
+
InstanceActionInfo,
|
|
14
|
+
NetworkInterface,
|
|
15
|
+
SnapshotCreateParams,
|
|
16
|
+
SnapshotLoadParams
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FirecrackerClient:
|
|
24
|
+
"""
|
|
25
|
+
A client for interacting with the Firecracker HTTP API via a Unix socket.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, socket_path: str) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize the FirecrackerClient with the path to the Unix socket.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
socket_path (str): The path to the Firecracker API Unix socket.
|
|
34
|
+
"""
|
|
35
|
+
self.__session = requests_unixsocket.Session()
|
|
36
|
+
self.__socket_path = socket_path
|
|
37
|
+
self.__socket_url = f"http+unix://{socket_path.replace('/', '%2F')}"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def socket_path(self) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Get the socket path for the Firecracker API.
|
|
43
|
+
"""
|
|
44
|
+
return self.__socket_path
|
|
45
|
+
|
|
46
|
+
def __put(self, endpoint: str, data: dict) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Helper method to send a PUT request to the Firecracker API.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
endpoint (str): The API endpoint to which the request should be sent.
|
|
52
|
+
data (dict): The JSON data to be included in the PUT request.
|
|
53
|
+
Raises:
|
|
54
|
+
HTTPError: If the PUT request receives with an HTTP error response.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
response = self.__session.put(f"{self.__socket_url}/{endpoint}", json=data)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
except HTTPError as err:
|
|
60
|
+
log.error(f"Error occurred while sending PUT request to {endpoint}: {err}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
def __patch(self, endpoint: str, data: dict) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Helper method to send a PATCH request to the Firecracker API.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
endpoint (str): The API endpoint to which the request should be sent.
|
|
69
|
+
data (dict): The JSON data to be included in the PATCH request.
|
|
70
|
+
Raises:
|
|
71
|
+
HTTPError: If the PATCH request receives with an HTTP error response.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
response = self.__session.patch(f"{self.__socket_url}/{endpoint}", json=data)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
except HTTPError as err:
|
|
77
|
+
log.error(f"Error occurred while sending PATCH request to {endpoint}: {err}")
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
def __body_to_dict(self, body: Any) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Helper method to convert a dataclass instance to a dictionary.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
body (Any): The dataclass instance to be converted.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
dict: The dictionary representation of the dataclass instance.
|
|
89
|
+
"""
|
|
90
|
+
return asdict(
|
|
91
|
+
body,
|
|
92
|
+
dict_factory=lambda x: {k: v for (k, v) in x if v is not None}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def put_machine_config(self, machine_config: MachineConfiguration) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Configure the machine with the specified number of vCPUs and memory size.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
vcpu_count (int): The number of virtual CPUs to allocate to the machine.
|
|
101
|
+
mem_size_mib (int): The amount of memory in MiB to allocate to the machine.
|
|
102
|
+
ht_enabled (bool): Whether to enable hyper-threading for the machine.
|
|
103
|
+
"""
|
|
104
|
+
data = self.__body_to_dict(machine_config)
|
|
105
|
+
self.__put("machine-config", data)
|
|
106
|
+
|
|
107
|
+
def put_boot_source(self, boot_source: BootSource) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Configure the linux kernel boot source for the machine.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
kernel_image_path (str): The path to the kernel image to be used as the boot source.
|
|
113
|
+
boot_args (str): The kernel command line arguments to be passed to the kernel on boot.
|
|
114
|
+
"""
|
|
115
|
+
data = self.__body_to_dict(boot_source)
|
|
116
|
+
self.__put("boot-source", data)
|
|
117
|
+
|
|
118
|
+
def put_drives(self, drive: Drive) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Configure a root filesystem drive for the machine.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
drive_id (str): The identifier inside firecracker for the drive to be configured.
|
|
124
|
+
path_on_host (str): The path on the host where the drive's backing file is located.
|
|
125
|
+
is_root_device (str): True if disk should be the root file drive else False
|
|
126
|
+
is_read_only (bool): Whether the drive should be configured as read-only.
|
|
127
|
+
"""
|
|
128
|
+
data = self.__body_to_dict(drive)
|
|
129
|
+
self.__put(f"drives/{drive.drive_id}", data)
|
|
130
|
+
|
|
131
|
+
def put_network_interfaces(self, network_interface: NetworkInterface) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Configure a network interface for the machine.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
network_interface (NetworkInterface): The network interface configuration.
|
|
137
|
+
"""
|
|
138
|
+
data = self.__body_to_dict(network_interface)
|
|
139
|
+
self.__put(f"network-interfaces/{network_interface.iface_id}", data)
|
|
140
|
+
|
|
141
|
+
def put_actions(self, instance_action_info: InstanceActionInfo) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Send an action request to the Firecracker API.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
action_type (str): The type of action to be performed. Valid values are "InstanceStart" and "InstanceStop".
|
|
147
|
+
"""
|
|
148
|
+
self.__put("actions", self.__body_to_dict(instance_action_info))
|
|
149
|
+
|
|
150
|
+
def patch_vm(self, vm: VM) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Configure the VM with the specified configuration.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
vm (VM): The VM configuration to be applied.
|
|
156
|
+
"""
|
|
157
|
+
self.__patch("vm", self.__body_to_dict(vm))
|
|
158
|
+
|
|
159
|
+
def put_snapshot_create(self, snapshot_create_params: SnapshotCreateParams) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Create a snapshot of the VM.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
snapshot_create_params (SnapshotCreateParams): The parameters for creating the snapshot.
|
|
165
|
+
"""
|
|
166
|
+
self.__put("snapshot/create", self.__body_to_dict(snapshot_create_params))
|
|
167
|
+
|
|
168
|
+
def put_snapshot_load(self, snapshot_load_params: SnapshotLoadParams) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Load a snapshot of the VM.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
snapshot_load_params (SnapshotLoadParams): The parameters for loading the snapshot.
|
|
174
|
+
"""
|
|
175
|
+
self.__put("snapshot/load", self.__body_to_dict(snapshot_load_params))
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class BootSource:
|
|
8
|
+
"""
|
|
9
|
+
Represents the request body for the Firecracker BootSource API.
|
|
10
|
+
Maps 1-to-1 with the BootSource definition in the Firecracker Swagger spec.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
kernel_image_path (str): Path to the kernel image on the host.
|
|
14
|
+
boot_args (Optional[str]): Kernel boot arguments.
|
|
15
|
+
initrd_path (Optional[str]): Path to the initrd image on the host.
|
|
16
|
+
"""
|
|
17
|
+
kernel_image_path: str
|
|
18
|
+
boot_args: Optional[str] = None
|
|
19
|
+
initrd_path: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HugePages(StrEnum):
|
|
23
|
+
NONE = "None"
|
|
24
|
+
TWO_MIB = "2M"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MachineConfiguration:
|
|
29
|
+
"""
|
|
30
|
+
Represents the request body for the Firecracker Machine Configuration API.
|
|
31
|
+
Maps 1-to-1 with the MachineConfiguration definition in the Firecracker Swagger spec.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
mem_size_mib (int): Memory size in MiB.
|
|
35
|
+
vcpu_count (int): Number of vCPUs (1-32).
|
|
36
|
+
smt (Optional[bool]): Simultaneous multithreading enabled.
|
|
37
|
+
track_dirty_pages (Optional[bool]): Track dirty pages for live migration.
|
|
38
|
+
huge_pages (Optional[HugePages]): Use huge pages.
|
|
39
|
+
"""
|
|
40
|
+
mem_size_mib: int
|
|
41
|
+
vcpu_count: int
|
|
42
|
+
#cpu_template: Optional[CPUTemplate] = None will have to be a ref to another dataclass
|
|
43
|
+
smt: Optional[bool] = None
|
|
44
|
+
track_dirty_pages: Optional[bool] = None
|
|
45
|
+
huge_pages: Optional[HugePages] = None
|
|
46
|
+
|
|
47
|
+
def __post__init__(self):
|
|
48
|
+
if self.vcpu_count < 1 or self.vcpu_count > 32:
|
|
49
|
+
raise ValueError("vcpu_count must be between 1 and 32")
|
|
50
|
+
if self.huge_pages is not None:
|
|
51
|
+
if self.huge_pages not in [HugePages.NONE, HugePages.TWO_MIB]:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"MachineConfiguration.huge_pages must be either"
|
|
54
|
+
f" '{HugePages.NONE}' or '{HugePages.TWO_MIB}'"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CacheType(StrEnum):
|
|
59
|
+
UNSAFE = "Unsafe"
|
|
60
|
+
WRITEBACK = "Writeback"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IOEngine(StrEnum):
|
|
64
|
+
SYNC = "Sync"
|
|
65
|
+
ASYNC = "Async"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Drive:
|
|
70
|
+
"""
|
|
71
|
+
Represents the request body for the Firecracker Drive API.
|
|
72
|
+
Maps 1-to-1 with the Drive definition in the Firecracker Swagger spec.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
drive_id (str): Unique identifier for the drive.
|
|
76
|
+
is_root_device (bool): Whether this drive is the root device.
|
|
77
|
+
partuuid (Optional[str]): Partition UUID.
|
|
78
|
+
cache_type (Optional[CacheType]): Cache type.
|
|
79
|
+
is_read_only (Optional[bool]): If the drive is read-only.
|
|
80
|
+
path_on_host (Optional[str]): Path to the drive on the host.
|
|
81
|
+
io_engine (Optional[IOEngine]): IO engine.
|
|
82
|
+
socket (Optional[str]): Path to the drive's socket.
|
|
83
|
+
"""
|
|
84
|
+
drive_id: str
|
|
85
|
+
is_root_device: bool
|
|
86
|
+
partuuid: Optional[str] = None
|
|
87
|
+
cache_type: Optional[CacheType] = None
|
|
88
|
+
is_read_only: Optional[bool] = None
|
|
89
|
+
path_on_host: Optional[str] = None
|
|
90
|
+
#rate_limiter: Optional[RateLimiter] = None This needs to be a ref to another dataclass
|
|
91
|
+
io_engine: Optional[IOEngine] = None
|
|
92
|
+
socket: Optional[str] = None
|
|
93
|
+
|
|
94
|
+
def __post__init__(self):
|
|
95
|
+
if self.cache_type is not None:
|
|
96
|
+
if self.cache_type not in [CacheType.UNSAFE, CacheType.WRITEBACK]:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"DriveBody.cache_type must be either '{CacheType.UNSAFE}' or '{CacheType.WRITEBACK}'"
|
|
99
|
+
)
|
|
100
|
+
if self.io_engine is not None:
|
|
101
|
+
if self.io_engine not in [IOEngine.SYNC, IOEngine.ASYNC]:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"DriveBody.io_engine must be either '{IOEngine.SYNC}' or '{IOEngine.ASYNC}'"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ActionType(StrEnum):
|
|
108
|
+
FLUSH_METRICS = "FlushMetrics"
|
|
109
|
+
INSTANCE_START = "InstanceStart"
|
|
110
|
+
SEND_CTRL_ALT_DEL = "SendCtrlAltDel"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class InstanceActionInfo:
|
|
115
|
+
"""
|
|
116
|
+
Represents the request body for the Firecracker Instance Action API.
|
|
117
|
+
Maps 1-to-1 with the InstanceActionInfo definition in the Firecracker Swagger spec.
|
|
118
|
+
|
|
119
|
+
Attributes:
|
|
120
|
+
action_type (ActionType): Action type to execute.
|
|
121
|
+
"""
|
|
122
|
+
action_type: ActionType
|
|
123
|
+
|
|
124
|
+
def __post__init__(self):
|
|
125
|
+
if self.action_type not in [
|
|
126
|
+
ActionType.FLUSH_METRICS,
|
|
127
|
+
ActionType.INSTANCE_START,
|
|
128
|
+
ActionType.SEND_CTRL_ALT_DEL
|
|
129
|
+
]:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"InstanceActionInfo.action_type must be one of '{ActionType.FLUSH_METRICS}', "
|
|
132
|
+
f"'{ActionType.INSTANCE_START}', or '{ActionType.SEND_CTRL_ALT_DEL}'"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class NetworkInterface:
|
|
138
|
+
"""
|
|
139
|
+
Represents the request body for the Firecracker Network Interface API.
|
|
140
|
+
|
|
141
|
+
Attributes:
|
|
142
|
+
host_dev_name (str): Host level path for the guest network interface (e.g. tap0).
|
|
143
|
+
iface_id (str): Unique identifier for the network interface within Firecracker.
|
|
144
|
+
guest_mac (Optional[str]): MAC address to be assigned to the guest network interface.
|
|
145
|
+
"""
|
|
146
|
+
host_dev_name: str
|
|
147
|
+
iface_id: str
|
|
148
|
+
guest_mac: Optional[str] = None
|
|
149
|
+
#rx_rate_limiter: Optional[RateLimiter] = None This needs to be a ref
|
|
150
|
+
#tx_rate_limiter: Optional[RateLimiter] = None This needs to be a ref
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class VMState(StrEnum):
|
|
154
|
+
PAUSED = "Paused"
|
|
155
|
+
RESUMED = "Resumed"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class VM:
|
|
160
|
+
"""
|
|
161
|
+
Represents the state of a Firecracker microVM.
|
|
162
|
+
|
|
163
|
+
Attributes:
|
|
164
|
+
state (VMState): The current state of the microVM
|
|
165
|
+
"""
|
|
166
|
+
state: VMState
|
|
167
|
+
|
|
168
|
+
def __post__init__(self):
|
|
169
|
+
if self.state not in [VMState.PAUSED, VMState.RESUMED]:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"VM.state must be one of '{VMState.PAUSED}' or '{VMState.RESUMED}'"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SnapshotType(StrEnum):
|
|
176
|
+
FULL = "Full"
|
|
177
|
+
DIFF = "Diff"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class SnapshotCreateParams:
|
|
182
|
+
"""
|
|
183
|
+
Represents the request body for the Firecracker Create Snapshot API.
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
mem_file_path (str): The path on the host where the memory file will be stored.
|
|
187
|
+
snapshot_path (str): The path on the host where the snapshot will be stored.
|
|
188
|
+
snapshot_type (Optional[SnapshotType]): The type of snapshot to create ('Full' or 'Diff').
|
|
189
|
+
"""
|
|
190
|
+
snapshot_path: str
|
|
191
|
+
mem_file_path: str
|
|
192
|
+
snapshot_type: Optional[SnapshotType] = None
|
|
193
|
+
|
|
194
|
+
def __post__init__(self):
|
|
195
|
+
if self.snapshot_type not in [SnapshotType.FULL, SnapshotType.DIFF]:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"SnapshotCreateParams.snapshot_type must be either '{SnapshotType.FULL}' or '{SnapshotType.DIFF}'"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class SnapshotLoadParams:
|
|
203
|
+
"""
|
|
204
|
+
Represents the request body for the Firecracker Load Snapshot API.
|
|
205
|
+
|
|
206
|
+
Attributes:
|
|
207
|
+
snapshot_path (str): Path to the file that contains the microVM state to be loaded.
|
|
208
|
+
track_dirty_pages (Optional[bool]): Whether to track dirty pages after loading the snapshot.
|
|
209
|
+
mem_file_path (Optional[str]): The path on the host that contains the guest memory to be loaded.
|
|
210
|
+
resume_vm (Optional[bool]): Whether to resume the VM immediately after loading the snapshot.
|
|
211
|
+
"""
|
|
212
|
+
snapshot_path: str
|
|
213
|
+
track_dirty_pages: Optional[bool] = None
|
|
214
|
+
mem_file_path: Optional[str] = None
|
|
215
|
+
# mem_backend: Optional[MemoryBackend] = None This needs to be a ref to another dataclass
|
|
216
|
+
resume_vm: Optional[bool] = None
|
|
217
|
+
# network_overrides: Optional[List[NetworkOverride]] = None This needs to be a ref to a list of dataclasses
|
|
218
|
+
# vsock_override: Optional[VsockOverride] = None This needs to be a ref to another dataclass
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from functools import singledispatchmethod
|
|
3
|
+
from typing import Optional, Self
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommandError(Exception):
|
|
8
|
+
"""Custom exception for command execution errors."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Command:
|
|
13
|
+
"""
|
|
14
|
+
A class to build and execute shell commands. This class supports
|
|
15
|
+
adding arguments by chaining calls to `add_arg` or `add_args`.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
__name (str): The base command to execute.
|
|
19
|
+
__command_list (list[str]): The list of command components to be executed.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, name: str, sudo: bool = False) -> None:
|
|
22
|
+
self.__name: str = name
|
|
23
|
+
self.__command_list: list[str] = ["sudo", self.__name] if sudo else [self.__name]
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Returns the full command as a string.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
str: The full command string.
|
|
31
|
+
"""
|
|
32
|
+
return " ".join(self.__command_list)
|
|
33
|
+
|
|
34
|
+
def add_arg(self, arg: str) -> Self:
|
|
35
|
+
"""
|
|
36
|
+
Adds a single argument to the command's command list. This method
|
|
37
|
+
can be chained to add multiple arguments.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Command: The Command instance with the added argument.
|
|
41
|
+
"""
|
|
42
|
+
self.__command_list.append(arg)
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
@singledispatchmethod
|
|
46
|
+
def add_args(self, args) -> Self:
|
|
47
|
+
"""
|
|
48
|
+
Adds multiple arguments to the command's command list. This method
|
|
49
|
+
can be chained to add multiple arguments.
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError("Unsupported type for add_args")
|
|
52
|
+
|
|
53
|
+
@add_args.register
|
|
54
|
+
def _(self, args: list) -> Self:
|
|
55
|
+
"""
|
|
56
|
+
Adds a list of arguments to the command's command list. This method
|
|
57
|
+
can be chained to add multiple arguments.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
args (list[str]): A list of arguments to add to the command.
|
|
61
|
+
Returns:
|
|
62
|
+
Command: The Command instance with the added arguments.
|
|
63
|
+
"""
|
|
64
|
+
self.__command_list.extend(args)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
@add_args.register
|
|
68
|
+
def _(self, args: str) -> Self:
|
|
69
|
+
"""
|
|
70
|
+
Adds a string of arguments to the command's command list. The string
|
|
71
|
+
is split into individual arguments based on whitespace. This method can
|
|
72
|
+
be chained to add multiple arguments.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
args (str): A string of arguments to add to the command.
|
|
76
|
+
Returns:
|
|
77
|
+
Command: The Command instance with the added arguments.
|
|
78
|
+
"""
|
|
79
|
+
args_list = args.split()
|
|
80
|
+
self.__command_list.extend(args_list)
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def run(self) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Executes the command using subprocess.run.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
RuntimeError: If the command execution fails, a RuntimeError is raised with the command error code.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
result = subprocess.run(self.__command_list, check=True)
|
|
92
|
+
if result.returncode != 0:
|
|
93
|
+
error_message = f"Command '{str(self)}' failed with exit code {result.returncode} and output: {result.stdout}"
|
|
94
|
+
raise CommandError(error_message)
|
|
95
|
+
except subprocess.CalledProcessError as e:
|
|
96
|
+
error_message = f"Command '{str(self)}' failed with exit code {e.returncode}"
|
|
97
|
+
raise CommandError(error_message) from e
|
|
98
|
+
|
|
99
|
+
def popen(self, log_file_path: Optional[str] = None) -> subprocess.Popen:
|
|
100
|
+
"""
|
|
101
|
+
Executes the command by spawning a background process using subprocess.Popen.
|
|
102
|
+
Returns the Popen object to allow the caller to maintain a reference to the process.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
subprocess.Popen: The Popen object representing the spawned process.
|
|
106
|
+
log_file_path (str): The path to the log file where the command's
|
|
107
|
+
output will be written.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
RuntimeError: If the command execution fails, a RuntimeError is raised with the command error code.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
if log_file_path is None:
|
|
114
|
+
process = subprocess.Popen(
|
|
115
|
+
self.__command_list,
|
|
116
|
+
stdin=subprocess.DEVNULL,
|
|
117
|
+
stdout=subprocess.DEVNULL
|
|
118
|
+
)
|
|
119
|
+
return process
|
|
120
|
+
path = Path(log_file_path)
|
|
121
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
with open(path, "w") as log_file:
|
|
123
|
+
process = subprocess.Popen(
|
|
124
|
+
self.__command_list,
|
|
125
|
+
stdin=subprocess.DEVNULL,
|
|
126
|
+
stdout=log_file,
|
|
127
|
+
stderr=log_file
|
|
128
|
+
)
|
|
129
|
+
return process
|
|
130
|
+
except subprocess.CalledProcessError as e:
|
|
131
|
+
error_message = f"Command '{str(self)}' failed with exit code {e.returncode}"
|
|
132
|
+
raise CommandError(error_message) from e
|