hysn-firecracker-python 1.0.3.post0__py3-none-any.whl
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.
- firecracker/__init__.py +23 -0
- firecracker/_version.py +34 -0
- firecracker/api.py +183 -0
- firecracker/config.py +30 -0
- firecracker/exceptions.py +33 -0
- firecracker/logger.py +98 -0
- firecracker/microvm.py +1738 -0
- firecracker/network.py +1230 -0
- firecracker/process.py +438 -0
- firecracker/scripts.py +53 -0
- firecracker/utils.py +192 -0
- firecracker/vmm.py +508 -0
- hysn_firecracker_python-1.0.3.post0.dist-info/METADATA +246 -0
- hysn_firecracker_python-1.0.3.post0.dist-info/RECORD +18 -0
- hysn_firecracker_python-1.0.3.post0.dist-info/WHEEL +5 -0
- hysn_firecracker_python-1.0.3.post0.dist-info/entry_points.txt +2 -0
- hysn_firecracker_python-1.0.3.post0.dist-info/licenses/LICENSE +21 -0
- hysn_firecracker_python-1.0.3.post0.dist-info/top_level.txt +1 -0
firecracker/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .microvm import MicroVM
|
|
2
|
+
from .api import Api
|
|
3
|
+
from .logger import Logger
|
|
4
|
+
from . import scripts
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
scripts.check_firecracker_binary()
|
|
8
|
+
scripts.create_firecracker_directory()
|
|
9
|
+
except scripts.ConfigurationError as e:
|
|
10
|
+
print(f"Warning: {e}")
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from ._version import version as __version__
|
|
14
|
+
except ImportError:
|
|
15
|
+
__version__ = "0.0.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MicroVM",
|
|
19
|
+
"Api",
|
|
20
|
+
"Logger",
|
|
21
|
+
"scripts",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
firecracker/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.0.3.post0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0, 3, 'post0')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'g79b65ab4e'
|
firecracker/api.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from .exceptions import APIError
|
|
5
|
+
from requests_unixsocket import UnixAdapter
|
|
6
|
+
|
|
7
|
+
DEFAULT_SCHEME = "http://"
|
|
8
|
+
DEFAULT_TIMEOUT = 5
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Session(requests.Session):
|
|
12
|
+
"""An HTTP over UNIX sockets Session with optimized connection pooling"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, timeout=DEFAULT_TIMEOUT):
|
|
15
|
+
"""Create a Session object.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
timeout (int): Request timeout in seconds
|
|
19
|
+
"""
|
|
20
|
+
super().__init__()
|
|
21
|
+
adapter = UnixAdapter(
|
|
22
|
+
pool_connections=20, pool_maxsize=20, max_retries=3, pool_block=True
|
|
23
|
+
)
|
|
24
|
+
self.mount(DEFAULT_SCHEME, adapter)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Resource:
|
|
28
|
+
"""An abstraction over a REST path"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, api, resource, id_field=None):
|
|
31
|
+
"""Initialize a REST resource.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
api (Api): The API client instance
|
|
35
|
+
resource (str): The resource path
|
|
36
|
+
id_field (str, optional): The field name used for resource ID
|
|
37
|
+
"""
|
|
38
|
+
self._api = api
|
|
39
|
+
self.resource = resource
|
|
40
|
+
self.id_field = id_field
|
|
41
|
+
|
|
42
|
+
def get(self, timeout=None):
|
|
43
|
+
"""Make a GET request.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
timeout (int, optional): Request timeout in seconds
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
requests.Response: The HTTP response
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
APIError: If request fails or returns an error response
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
url = self._api.endpoint + self.resource
|
|
56
|
+
request_timeout = (
|
|
57
|
+
timeout if timeout is not None else self._api.get_timeout()
|
|
58
|
+
)
|
|
59
|
+
with self._api.session.get(url, timeout=request_timeout) as res:
|
|
60
|
+
if res.status_code != HTTPStatus.OK:
|
|
61
|
+
json = res.json()
|
|
62
|
+
if "fault_message" in json:
|
|
63
|
+
raise APIError(f"API fault: {json['fault_message']}")
|
|
64
|
+
elif "error" in json:
|
|
65
|
+
raise APIError(f"API error: {json['error']}")
|
|
66
|
+
raise APIError(f"Unexpected response: {res.content}")
|
|
67
|
+
|
|
68
|
+
return res
|
|
69
|
+
|
|
70
|
+
except requests.RequestException as e:
|
|
71
|
+
raise APIError(f"GET request failed: {str(e)}") from e
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
raise APIError(f"Invalid JSON response: {str(e)}") from e
|
|
74
|
+
|
|
75
|
+
def put(self, **kwargs):
|
|
76
|
+
"""Make a PUT request.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
**kwargs: Key-value pairs for the request body
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
requests.Response: The HTTP response
|
|
83
|
+
"""
|
|
84
|
+
path = self.resource
|
|
85
|
+
if self.id_field is not None:
|
|
86
|
+
path += "/" + kwargs[self.id_field]
|
|
87
|
+
return self.request("PUT", path, **kwargs)
|
|
88
|
+
|
|
89
|
+
def patch(self, **kwargs):
|
|
90
|
+
"""Make a PATCH request.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
**kwargs: Key-value pairs for the request body
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
requests.Response: The HTTP response
|
|
97
|
+
"""
|
|
98
|
+
path = self.resource
|
|
99
|
+
if self.id_field is not None:
|
|
100
|
+
path += "/" + kwargs[self.id_field]
|
|
101
|
+
return self.request("PATCH", path, **kwargs)
|
|
102
|
+
|
|
103
|
+
def request(self, method, path, timeout=None, **kwargs):
|
|
104
|
+
"""Make an HTTP request to the Firecracker API.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
method (str): HTTP method (GET, PUT, POST, DELETE, etc.)
|
|
108
|
+
path (str): API endpoint path
|
|
109
|
+
timeout (int, optional): Request timeout in seconds
|
|
110
|
+
**kwargs: Additional arguments to be sent as JSON in request body
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
requests.Response: The HTTP response from the API
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
APIError: If request fails or returns an error response
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
kwargs = {key: val for key, val in kwargs.items() if val is not None}
|
|
120
|
+
url = self._api.endpoint + path
|
|
121
|
+
request_timeout = (
|
|
122
|
+
timeout if timeout is not None else self._api.get_timeout()
|
|
123
|
+
)
|
|
124
|
+
with self._api.session.request(
|
|
125
|
+
method, url, json=kwargs, timeout=request_timeout
|
|
126
|
+
) as res:
|
|
127
|
+
if res.status_code != HTTPStatus.NO_CONTENT:
|
|
128
|
+
json = res.json()
|
|
129
|
+
if "fault_message" in json:
|
|
130
|
+
raise APIError(f"API fault: {json['fault_message']}")
|
|
131
|
+
elif "error" in json:
|
|
132
|
+
raise APIError(f"API error: {json['error']}")
|
|
133
|
+
raise APIError(f"Unexpected response: {res.content}")
|
|
134
|
+
return res
|
|
135
|
+
except requests.RequestException as e:
|
|
136
|
+
raise APIError(f"Request failed: {str(e)}") from e
|
|
137
|
+
except ValueError as e:
|
|
138
|
+
raise APIError(f"Invalid JSON response: {str(e)}") from e
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class Api:
|
|
142
|
+
"""A simple HTTP client for Firecracker API"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, socket_file, timeout=DEFAULT_TIMEOUT):
|
|
145
|
+
"""Initialize API client.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
socket_file (str): Path to Firecracker API socket
|
|
149
|
+
timeout (int): Request timeout in seconds (default: 5)
|
|
150
|
+
"""
|
|
151
|
+
self.socket = socket_file
|
|
152
|
+
self.timeout = timeout
|
|
153
|
+
url_encoded_path = urllib.parse.quote_plus(socket_file)
|
|
154
|
+
self.endpoint = DEFAULT_SCHEME + url_encoded_path
|
|
155
|
+
self.session = Session(timeout=timeout)
|
|
156
|
+
|
|
157
|
+
self.describe = Resource(self, "/")
|
|
158
|
+
self.vm = Resource(self, "/vm")
|
|
159
|
+
self.vm_config = Resource(self, "/vm/config")
|
|
160
|
+
self.actions = Resource(self, "/actions")
|
|
161
|
+
self.boot = Resource(self, "/boot-source")
|
|
162
|
+
self.drive = Resource(self, "/drives", "drive_id")
|
|
163
|
+
self.version = Resource(self, "/version")
|
|
164
|
+
self.logger = Resource(self, "/logger")
|
|
165
|
+
self.machine_config = Resource(self, "/machine-config")
|
|
166
|
+
self.network = Resource(self, "/network-interfaces", "iface_id")
|
|
167
|
+
self.mmds = Resource(self, "/mmds")
|
|
168
|
+
self.mmds_config = Resource(self, "/mmds/config")
|
|
169
|
+
self.create_snapshot = Resource(self, "/snapshot/create")
|
|
170
|
+
self.load_snapshot = Resource(self, "/snapshot/load")
|
|
171
|
+
self.vsock = Resource(self, "/vsock")
|
|
172
|
+
|
|
173
|
+
def get_timeout(self):
|
|
174
|
+
"""Get timeout for API requests.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
int: Timeout value in seconds
|
|
178
|
+
"""
|
|
179
|
+
return getattr(self, "timeout", DEFAULT_TIMEOUT)
|
|
180
|
+
|
|
181
|
+
def close(self):
|
|
182
|
+
"""Close the session to release resources."""
|
|
183
|
+
self.session.close()
|
firecracker/config.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class MicroVMConfig:
|
|
7
|
+
"""Configuration defaults for Firecracker microVMs."""
|
|
8
|
+
data_path: str = "/var/lib/firecracker"
|
|
9
|
+
binary_path: str = "/usr/local/bin/firecracker"
|
|
10
|
+
snapshot_path: str = "/var/lib/firecracker/snapshots"
|
|
11
|
+
kernel_file: str = None
|
|
12
|
+
rootfs_size: str = "5G"
|
|
13
|
+
initrd_file: str = None
|
|
14
|
+
init_file: str = "/sbin/init"
|
|
15
|
+
base_rootfs: str = None
|
|
16
|
+
overlayfs: bool = False
|
|
17
|
+
overlayfs_file: str = None
|
|
18
|
+
ip_addr: str = "172.16.0.2"
|
|
19
|
+
mmds_enabled: bool = False
|
|
20
|
+
mmds_ip: str = "169.254.169.254"
|
|
21
|
+
user_data: str = None
|
|
22
|
+
vcpu: int = 1
|
|
23
|
+
memory: int = 512
|
|
24
|
+
verbose: bool = False
|
|
25
|
+
ssh_user: str = "root"
|
|
26
|
+
expose_ports: bool = False
|
|
27
|
+
host_port: int = None
|
|
28
|
+
dest_port: int = None
|
|
29
|
+
vsock_enabled: bool = False
|
|
30
|
+
vsock_guest_cid: int = 3
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Custom exceptions for the Firecracker library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FirecrackerError(Exception):
|
|
5
|
+
"""Base exception for all Firecracker errors."""
|
|
6
|
+
def __init__(self, message: str, *args, **kwargs):
|
|
7
|
+
self.message = message
|
|
8
|
+
super().__init__(message, *args)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NetworkError(FirecrackerError):
|
|
12
|
+
"""Network-related errors."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigurationError(FirecrackerError):
|
|
17
|
+
"""Configuration-related errors."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VMMError(FirecrackerError):
|
|
22
|
+
"""VMM operation errors."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class APIError(FirecrackerError):
|
|
27
|
+
"""API-related errors."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProcessError(FirecrackerError):
|
|
32
|
+
"""Process management errors."""
|
|
33
|
+
pass
|
firecracker/logger.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Logger:
|
|
5
|
+
"""Custom logger class for MicroVM operations."""
|
|
6
|
+
COLORS = {
|
|
7
|
+
"INFO": "\033[0m",
|
|
8
|
+
"ERROR": "\033[91m",
|
|
9
|
+
"WARNING": "\033[93m",
|
|
10
|
+
"DEBUG": "\033[94m",
|
|
11
|
+
}
|
|
12
|
+
RESET = "\033[0m"
|
|
13
|
+
|
|
14
|
+
LEVEL_MAP = {
|
|
15
|
+
"DEBUG": logging.DEBUG,
|
|
16
|
+
"INFO": logging.INFO,
|
|
17
|
+
"WARN": logging.WARNING,
|
|
18
|
+
"ERROR": logging.ERROR,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def __init__(self, level: str = "INFO", verbose: bool = False):
|
|
22
|
+
"""Initialize the logger with custom configuration.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
level (str): Initial log level (INFO, ERROR, WARN, DEBUG)
|
|
26
|
+
verbose (bool): Enable verbose (DEBUG) logging
|
|
27
|
+
"""
|
|
28
|
+
self.logger = logging.getLogger('microvm')
|
|
29
|
+
self.logger.propagate = False
|
|
30
|
+
self.verbose = verbose
|
|
31
|
+
|
|
32
|
+
for handler in self.logger.handlers[:]:
|
|
33
|
+
self.logger.removeHandler(handler)
|
|
34
|
+
|
|
35
|
+
console_handler = logging.StreamHandler()
|
|
36
|
+
formatter = logging.Formatter(
|
|
37
|
+
"\r[%(asctime)s.%(msecs)03d] [%(colored_levelname)s] %(message)s",
|
|
38
|
+
"%Y-%m-%dT%H:%M:%S"
|
|
39
|
+
)
|
|
40
|
+
console_handler.addFilter(self._add_colored_levelname)
|
|
41
|
+
console_handler.setFormatter(formatter)
|
|
42
|
+
self.logger.addHandler(console_handler)
|
|
43
|
+
|
|
44
|
+
self.set_level(level)
|
|
45
|
+
|
|
46
|
+
def _add_colored_levelname(self, record):
|
|
47
|
+
"""Add colored levelname to the log record."""
|
|
48
|
+
level = record.levelname
|
|
49
|
+
if level == "INFO" and getattr(record, "success", False):
|
|
50
|
+
level = "SUCCESS"
|
|
51
|
+
color = self.COLORS.get(level, self.COLORS["INFO"])
|
|
52
|
+
record.colored_levelname = f"{color}{level}{self.RESET}"
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
def set_level(self, level: str):
|
|
56
|
+
"""Set the logging level.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
level (str): Log level to set (INFO, ERROR, WARNING, DEBUG)
|
|
60
|
+
"""
|
|
61
|
+
level = level.upper()
|
|
62
|
+
logging_level = self.LEVEL_MAP.get(level, logging.INFO)
|
|
63
|
+
self.logger.setLevel(logging_level)
|
|
64
|
+
self.current_level = level
|
|
65
|
+
|
|
66
|
+
def __call__(self, level: str, message: str):
|
|
67
|
+
"""Log a message at the specified level.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
level (str): Level to log at (INFO, ERROR, WARNING, DEBUG)
|
|
71
|
+
message (str): Message to log
|
|
72
|
+
"""
|
|
73
|
+
level = level.upper()
|
|
74
|
+
if level not in self.LEVEL_MAP:
|
|
75
|
+
level = "INFO" # Default to INFO for unknown levels
|
|
76
|
+
|
|
77
|
+
msg_level = self.LEVEL_MAP[level]
|
|
78
|
+
current_level = self.LEVEL_MAP[self.current_level]
|
|
79
|
+
|
|
80
|
+
if msg_level >= current_level:
|
|
81
|
+
log_method = getattr(self.logger, level.lower())
|
|
82
|
+
log_method(message)
|
|
83
|
+
|
|
84
|
+
def info(self, message: str):
|
|
85
|
+
"""Log an info message."""
|
|
86
|
+
self("INFO", message)
|
|
87
|
+
|
|
88
|
+
def error(self, message: str):
|
|
89
|
+
"""Log an error message."""
|
|
90
|
+
self("ERROR", message)
|
|
91
|
+
|
|
92
|
+
def warn(self, message: str):
|
|
93
|
+
"""Log a warning message."""
|
|
94
|
+
self("WARN", message)
|
|
95
|
+
|
|
96
|
+
def debug(self, message: str):
|
|
97
|
+
"""Log a debug message."""
|
|
98
|
+
self("DEBUG", message)
|