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/vmm.py ADDED
@@ -0,0 +1,508 @@
1
+ import os
2
+ import re
3
+ import json
4
+ from typing import List, Dict
5
+ from firecracker.logger import Logger
6
+ from firecracker.api import Api
7
+ from firecracker.config import MicroVMConfig
8
+ from firecracker.network import NetworkManager
9
+ from firecracker.process import ProcessManager
10
+ from firecracker.utils import requires_id
11
+ from firecracker.exceptions import VMMError
12
+
13
+
14
+ class VMMManager:
15
+ """Manages Virtual Machine Monitor (VMM) instances.
16
+
17
+ Handles the lifecycle and configuration of Firecracker VMM instances,
18
+ including creation, monitoring, and cleanup of VMM processes.
19
+
20
+ Attributes:
21
+ logger (Logger): Logger instance for VMM operations
22
+ """
23
+
24
+ def __init__(self, verbose: bool = False, level: str = "INFO"):
25
+ self._logger = Logger(level=level, verbose=verbose)
26
+ self._config = MicroVMConfig()
27
+ self._config.verbose = verbose
28
+ self._network = NetworkManager(verbose=verbose, level=level)
29
+ self._process = ProcessManager(verbose=verbose, level=level)
30
+ self._api = None
31
+
32
+ def get_api(self, id: str, timeout: int = 5) -> Api:
33
+ """Get an API instance for a given VMM ID.
34
+
35
+ Args:
36
+ id (str): VMM ID
37
+ timeout (int): Request timeout in seconds (default: 5)
38
+
39
+ Returns:
40
+ Api: API client instance
41
+ """
42
+ socket_file = f"{self._config.data_path}/{id}/firecracker.socket"
43
+ return Api(socket_file, timeout=timeout)
44
+
45
+ def create_vmm_json_file(self, id: str, **kwargs):
46
+ """Create a JSON file for a VMM.
47
+
48
+ Args:
49
+ id (str): VMM ID
50
+ **kwargs: Keyword arguments for the VMM
51
+
52
+ Returns:
53
+ str: Path to the created config file
54
+
55
+ Raises:
56
+ VMMError: If file creation fails
57
+ """
58
+ vm_data = {
59
+ "ID": kwargs.get("ID", id),
60
+ "Name": kwargs.get("Name", ""),
61
+ "CreatedAt": kwargs.get("CreatedAt", ""),
62
+ "Rootfs": kwargs.get("Rootfs", self._config.base_rootfs),
63
+ "Kernel": kwargs.get("Kernel", self._config.kernel_file),
64
+ "State": {
65
+ "Pid": kwargs.get("Pid", ""),
66
+ "Running": kwargs.get("Running", True),
67
+ "Paused": kwargs.get("Paused", False),
68
+ },
69
+ "Network": {f"tap_{id}": {"IPAddress": kwargs.get("IPAddress", "")}},
70
+ "Ports": kwargs.get("Ports", {}),
71
+ "Labels": kwargs.get("Labels", {}),
72
+ "LogPath": kwargs.get("LogPath", f"{self._config.data_path}/{id}/logs"),
73
+ }
74
+
75
+ try:
76
+ vmm_dir = f"{self._config.data_path}/{id}"
77
+ os.makedirs(vmm_dir, exist_ok=True)
78
+
79
+ file_path = f"{vmm_dir}/config.json"
80
+ with open(file_path, "w") as json_file:
81
+ json.dump(vm_data, json_file, indent=4)
82
+
83
+ if self._config.verbose:
84
+ self._logger.debug(f"Created VMM config file: {file_path}")
85
+
86
+ return file_path
87
+
88
+ except Exception as e:
89
+ raise VMMError(f"Failed to create VMM config file: {str(e)}") from e
90
+
91
+ def list_vmm(self) -> List[Dict]:
92
+ """List all VMMs using their config.json files.
93
+
94
+ Returns:
95
+ List[Dict]: List of dictionaries containing VMM details
96
+ """
97
+ vmm_list = []
98
+
99
+ running_pids = set(self._process.get_pids())
100
+ has_running_vmms = bool(running_pids)
101
+
102
+ vmm_id_pattern = re.compile(r"^[a-zA-Z0-9]{8}$")
103
+
104
+ data_path = self._config.data_path
105
+
106
+ try:
107
+ # Use listdir with error handling
108
+ vmm_dirs = os.listdir(data_path)
109
+ except OSError as e:
110
+ self._logger.error(f"Failed to read data directory {data_path}: {e}")
111
+ return vmm_list
112
+
113
+ for vmm_id in vmm_dirs:
114
+ # Early validation - skip non-matching IDs
115
+ if not vmm_id_pattern.match(vmm_id):
116
+ continue
117
+
118
+ vmm_path = os.path.join(data_path, vmm_id)
119
+
120
+ config_path = os.path.join(vmm_path, "config.json")
121
+ if not (os.path.isdir(vmm_path) and os.path.exists(config_path)):
122
+ if has_running_vmms and self._config.verbose:
123
+ self._logger.info(f"Config file not found for VMM ID: {vmm_id}")
124
+ continue
125
+
126
+ try:
127
+ with open(config_path, "r") as config_file:
128
+ config_data = json.load(config_file)
129
+
130
+ pid = config_data.get("State", {}).get("Pid", "")
131
+
132
+ if pid and pid in running_pids:
133
+ network_key = f"tap_{vmm_id}"
134
+ network_info = config_data.get("Network", {}).get(network_key, {})
135
+ ports_info = config_data.get("Ports", {})
136
+
137
+ vmm_info = {
138
+ "id": config_data.get("ID", vmm_id),
139
+ "name": config_data.get("Name", ""),
140
+ "pid": pid,
141
+ "ip_addr": network_info.get("IPAddress", ""),
142
+ "state": "Running"
143
+ if config_data.get("State", {}).get("Running", False)
144
+ else "Paused",
145
+ "created_at": config_data.get("CreatedAt", ""),
146
+ "ports": ports_info,
147
+ "labels": config_data.get("Labels", {}),
148
+ }
149
+ vmm_list.append(vmm_info)
150
+
151
+ except (json.JSONDecodeError, IOError) as e:
152
+ if self._config.verbose:
153
+ self._logger.warn(f"Failed to read config for VMM {vmm_id}: {e}")
154
+ continue
155
+
156
+ return vmm_list
157
+
158
+ def find_vmm_by_id(self, id: str) -> str:
159
+ """Find a VMM by ID and return its ID.
160
+
161
+ Args:
162
+ id (str): ID of the VMM to find
163
+
164
+ Returns:
165
+ str: ID of the found VMM or error message
166
+ """
167
+ try:
168
+ vmm_list = self.list_vmm()
169
+ for vmm_info in vmm_list:
170
+ if vmm_info["id"] == id:
171
+ return vmm_info["id"]
172
+
173
+ return f"VMM with ID {id} not found"
174
+
175
+ except Exception as e:
176
+ raise VMMError(f"Error finding VMM by ID: {str(e)}")
177
+
178
+ def find_vmm_by_labels(self, state: str, labels: Dict[str, str]) -> List[str]:
179
+ """Find VMMs by state (Running or Paused) and multiple labels, and return their IDs.
180
+
181
+ Args:
182
+ state (str): State to filter by ('Running' or 'Paused')
183
+ labels (Dict[str, str]): Dictionary of labels to search for
184
+
185
+ Returns:
186
+ List[str]: List of VMM IDs that match the state and all the labels
187
+ """
188
+ try:
189
+ matching_vmm_ids = []
190
+
191
+ vmm_list = self.list_vmm()
192
+
193
+ if not vmm_list:
194
+ return matching_vmm_ids
195
+
196
+ state_matching_vmms = [
197
+ vmm_info for vmm_info in vmm_list if vmm_info["state"] == state
198
+ ]
199
+
200
+ if not state_matching_vmms:
201
+ return matching_vmm_ids
202
+
203
+ for vmm_info in state_matching_vmms:
204
+ vmm_id = vmm_info["id"]
205
+ config_path = os.path.join(
206
+ self._config.data_path, vmm_id, "config.json"
207
+ )
208
+
209
+ if not os.path.exists(config_path):
210
+ continue
211
+
212
+ try:
213
+ with open(config_path, "r") as config_file:
214
+ config_data = json.load(config_file)
215
+
216
+ vmm_labels = config_data.get("Labels", {})
217
+ if all(
218
+ vmm_labels.get(key) == value for key, value in labels.items()
219
+ ):
220
+ vmm_info = {
221
+ "id": config_data.get("ID", vmm_id),
222
+ "name": config_data.get("Name", ""),
223
+ "state": "Running"
224
+ if config_data.get("State", {}).get("Running", False)
225
+ else "Paused",
226
+ "created_at": config_data.get("CreatedAt", ""),
227
+ }
228
+ matching_vmm_ids.append(vmm_info)
229
+
230
+ except (json.JSONDecodeError, IOError) as e:
231
+ if self._config.verbose:
232
+ self._logger.warn(
233
+ f"Failed to read config for VMM {vmm_id}: {e}"
234
+ )
235
+ continue
236
+
237
+ return matching_vmm_ids
238
+
239
+ except Exception as e:
240
+ raise VMMError(f"Error finding VMM by labels: {str(e)}")
241
+
242
+ def update_vmm_state(self, id: str, state: str) -> str:
243
+ """Update VM state (pause/resume).
244
+
245
+ Args:
246
+ state (str): Target state ("Paused" or "Resumed")
247
+
248
+ Returns:
249
+ str: Status message
250
+ """
251
+ try:
252
+ api = self.get_api(id)
253
+ response = api.vm.patch(state=state)
254
+
255
+ if self._config.verbose:
256
+ self._logger.debug(f"Changed VMM {id} state response: {response}")
257
+
258
+ return f"{state} VMM {id} successfully"
259
+
260
+ except Exception as e:
261
+ raise VMMError(f"Failed to {state.lower()} VMM {id}: {str(e)}")
262
+
263
+ finally:
264
+ if api:
265
+ api.close()
266
+
267
+ @requires_id
268
+ def get_vmm_config(self, id: str) -> Dict:
269
+ """Get the configuration for a specific VMM.
270
+
271
+ Args:
272
+ id (str): ID of the VMM to query
273
+
274
+ Returns:
275
+ dict: VMM configuration
276
+
277
+ Raises:
278
+ RuntimeError: If VMM ID is invalid or VMM is not running
279
+ """
280
+ try:
281
+ api = self.get_api(id)
282
+ response = api.vm_config.get().json()
283
+
284
+ if self._config.verbose:
285
+ self._logger.debug(f"VMM {id} configuration response: {response}")
286
+
287
+ return response
288
+
289
+ except Exception as e:
290
+ raise VMMError(f"Failed to get VMM configuration: {str(e)}")
291
+
292
+ finally:
293
+ api.close()
294
+
295
+ def get_vmm_state(self, id: str) -> str:
296
+ """Get the state of a specific VMM.
297
+
298
+ Args:
299
+ id (str): ID of the VMM to query
300
+
301
+ Returns:
302
+ str: VMM state ('Running', 'Paused', 'Unknown', etc.)
303
+
304
+ Raises:
305
+ VMMError: If VMM state cannot be retrieved
306
+ """
307
+ try:
308
+ api = self.get_api(id)
309
+ response = api.describe.get().json()
310
+ state = response.get("state")
311
+
312
+ if isinstance(state, str) and state.strip():
313
+ return state
314
+
315
+ return "Unknown"
316
+
317
+ except Exception as e:
318
+ raise VMMError(f"Failed to get state for VMM {id}: {str(e)}")
319
+
320
+ finally:
321
+ if api:
322
+ api.close()
323
+
324
+ def get_vmm_ip_addr(self, id: str) -> str:
325
+ """Get the IP address of a specific VMM.
326
+
327
+ Args:
328
+ id (str): ID of the VMM to query
329
+
330
+ Returns:
331
+ str: IP address of the VMM
332
+
333
+ Raises:
334
+ VMMError: If no IP address is found or an error occurs after
335
+ retries
336
+ """
337
+ try:
338
+ api = self.get_api(id)
339
+ vmm_config = api.vm_config.get().json()
340
+ boot_args = vmm_config.get("boot-source", {}).get("boot_args", "")
341
+
342
+ ip_match = re.search(r"ip=([0-9.]+)", boot_args)
343
+ if ip_match:
344
+ ip_addr = ip_match.group(1)
345
+ return ip_addr
346
+
347
+ else:
348
+ if self._config.verbose:
349
+ self._logger.info(f"No ip= found in boot-args for VMM {id}")
350
+ return "Unknown"
351
+
352
+ except Exception as e:
353
+ raise VMMError(f"Error while retrieving IP address for VMM {id}: {str(e)}")
354
+
355
+ finally:
356
+ api.close()
357
+
358
+ def check_network_overlap(self, ip_addr: str) -> bool:
359
+ """Check if the network configuration overlaps with another VMM.
360
+
361
+ Args:
362
+ ip_addr (str): IP address to check for overlap
363
+
364
+ Returns:
365
+ bool: True if the IP address overlaps, False otherwise.
366
+ """
367
+ try:
368
+ vmm_list = self.list_vmm()
369
+
370
+ existing_ips = {
371
+ vmm_info["ip_addr"]
372
+ for vmm_info in vmm_list
373
+ if vmm_info.get("ip_addr") and vmm_info["ip_addr"] != "Unknown"
374
+ }
375
+
376
+ return ip_addr in existing_ips
377
+
378
+ except Exception as e:
379
+ raise VMMError(f"Error checking network overlap: {str(e)}")
380
+
381
+ def create_vmm_dir(self, path: str):
382
+ """Create directories for the microVM.
383
+
384
+ Args:
385
+ path (str): Path to the VMM directory to create
386
+ """
387
+ try:
388
+ if not os.path.exists(path):
389
+ os.makedirs(path, exist_ok=True)
390
+ if self._config.verbose:
391
+ self._logger.info(f"Directory {path} is created")
392
+
393
+ except Exception as e:
394
+ raise VMMError(f"Failed to create directory at {path}: {str(e)}")
395
+
396
+ def create_log_file(self, id: str, log_file: str):
397
+ """Create a log file for the microVM.
398
+
399
+ Args:
400
+ log_file (str): Name of the log file to create
401
+ """
402
+ try:
403
+ log_dir = f"{self._config.data_path}/{id}/logs"
404
+
405
+ if not os.path.exists(f"{log_dir}/{log_file}"):
406
+ with open(f"{log_dir}/{log_file}", "w"):
407
+ pass
408
+ if self._config.verbose:
409
+ self._logger.info(f"Log file {log_dir}/{log_file} is created")
410
+
411
+ except Exception as e:
412
+ raise VMMError(f"Unable to create log file at {log_dir}: {str(e)}")
413
+
414
+ def delete_vmm_dir(self, id: str = None):
415
+ """
416
+ Clean up all resources associated with the microVM by removing the
417
+ VMM directory.
418
+
419
+ Args:
420
+ id (str): ID of the VMM to delete
421
+ """
422
+ import shutil
423
+
424
+ try:
425
+ vmm_dir = f"{self._config.data_path}/{id}"
426
+
427
+ if os.path.exists(vmm_dir):
428
+ shutil.rmtree(vmm_dir)
429
+ if self._config.verbose:
430
+ self._logger.info(f"Directory {vmm_dir} is removed")
431
+
432
+ except Exception as e:
433
+ self._logger.error(f"Failed to remove {vmm_dir} directory: {str(e)}")
434
+ raise VMMError(f"Failed to remove {vmm_dir} directory: {str(e)}")
435
+
436
+ def delete_vmm(self, id: str = None) -> str:
437
+ """Delete VMM instances from the config.json file.
438
+
439
+ Args:
440
+ id (str, optional): ID of specific VMM to delete. If None, deletes all VMMs.
441
+
442
+ Returns:
443
+ str: Status message indicating deletion results
444
+ """
445
+ try:
446
+ vmm_list = self.list_vmm()
447
+
448
+ if not vmm_list:
449
+ return "No VMMs found to delete"
450
+
451
+ if id:
452
+ if not any(vmm["id"] == id for vmm in vmm_list):
453
+ return f"VMM with ID {id} not found"
454
+ ids_to_delete = [id]
455
+ else:
456
+ ids_to_delete = [vmm["id"] for vmm in vmm_list]
457
+
458
+ deleted_count = 0
459
+ for vmm_id in ids_to_delete:
460
+ try:
461
+ self.cleanup(vmm_id)
462
+ deleted_count += 1
463
+ if self._config.verbose:
464
+ self._logger.info(f"Removed VMM {vmm_id}")
465
+ except Exception as e:
466
+ self._logger.error(f"Failed to delete VMM {vmm_id}: {e}")
467
+ continue
468
+
469
+ if id:
470
+ return f"VMM {id} {'removed' if deleted_count > 0 else 'not found'}"
471
+ else:
472
+ return f"Deleted {deleted_count} VMM(s)"
473
+
474
+ except Exception as e:
475
+ raise VMMError(f"Error during VMM deletion: {str(e)}")
476
+
477
+ def cleanup(self, id=None):
478
+ """Clean up network and process resources for a VMM."""
479
+ try:
480
+ self._process.stop(id)
481
+ self._network.cleanup(f"tap_{id}")
482
+ self.delete_vmm_dir(id)
483
+
484
+ except Exception as e:
485
+ raise VMMError(f"Failed to cleanup VMM {id}: {str(e)}") from e
486
+
487
+ def socket_file(self, id: str) -> str:
488
+ """Ensure the socket file is ready for use, unlinking if necessary.
489
+
490
+ Returns:
491
+ str: Path to the socket file
492
+
493
+ Raises:
494
+ VMMError: If unable to create or verify the socket file
495
+ """
496
+ try:
497
+ socket_file = f"{self._config.data_path}/{id}/firecracker.socket"
498
+
499
+ if os.path.exists(socket_file):
500
+ os.unlink(socket_file)
501
+ if self._config.verbose:
502
+ self._logger.info(f"Unlinked existing socket file {socket_file}")
503
+
504
+ self.create_vmm_dir(f"{self._config.data_path}/{id}")
505
+ return socket_file
506
+
507
+ except OSError as e:
508
+ raise VMMError(f"Failed to ensure socket file {socket_file}: {e}")