feat: Add guest execution agent and vsock support

- Introduced a standalone guest execution agent to facilitate command execution within the guest VM over Firecracker's vsock.
- Enhanced FirecrackerClient to support vsock configuration, validating guest CID and UDS path.
- Updated configuration to include vsock settings by default.
- Refactored FirecrackerProcessManager to manage runtime sockets more effectively.
- Implemented guest agent installation and provisioning in the image store.
- Added API endpoints for executing commands in the VM and installing the guest agent.
- Improved error handling for guest execution failures and agent availability.
This commit is contained in:
2026-09-25 17:39:06 +05:30
parent dca7f78dae
commit b0bf99a6ee
25 changed files with 1893 additions and 30 deletions
+6
View File
@@ -36,6 +36,7 @@ class FirecrackerClient:
drives = config["drives"]
interfaces = config["network-interfaces"]
entropy = config.get("entropy")
vsock = config.get("vsock")
if not isinstance(machine, Mapping) or not isinstance(boot, Mapping):
raise FirecrackerError("invalid Firecracker configuration")
if not isinstance(drives, list) or not drives:
@@ -44,6 +45,10 @@ class FirecrackerClient:
raise FirecrackerError("Firecracker configuration has no network interface")
if not isinstance(entropy, Mapping):
raise FirecrackerError("Firecracker configuration has no valid entropy device")
if (not isinstance(vsock, Mapping) or type(vsock.get("guest_cid")) is not int
or not 3 <= vsock["guest_cid"] < 2**32 - 1
or not isinstance(vsock.get("uds_path"), str) or not vsock["uds_path"]):
raise FirecrackerError("Firecracker configuration has no valid vsock device")
self._request(
"PUT",
@@ -57,6 +62,7 @@ class FirecrackerClient:
self._request("PUT", "/boot-source", boot)
self._request("PUT", "/drives/rootfs", drives[0])
self._request("PUT", "/network-interfaces/eth0", interfaces[0])
self._request("PUT", "/vsock", vsock)
self._request("PUT", "/entropy", entropy)
self._request("PUT", "/actions", {"action_type": "InstanceStart"})
+3
View File
@@ -72,6 +72,9 @@ def build_config(
],
# Supply host-backed randomness before guest services such as sshd start.
"entropy": {},
# Each Firecracker instance has its own Unix-socket-backed vsock device;
# CIDs need not be allocated across these independent devices.
"vsock": {"guest_cid": 3, "uds_path": str(settings.guest_socket_path(vm.id))},
}
+17 -11
View File
@@ -124,7 +124,7 @@ class FirecrackerProcessManager:
) from error
socket_path = Path(vm.socket)
self._remove_socket(socket_path)
self._remove_runtime_sockets(vm)
log_path = Path(vm.log)
process: _PopenLike | None = None
@@ -275,14 +275,16 @@ class FirecrackerProcessManager:
if vm.pid is None:
if vm.spawn_pending or vm.operation == "spawning" or vm.process_start_time is not None:
return "unknown"
try:
Path(vm.socket).lstat()
except FileNotFoundError:
return "gone"
except OSError:
for socket_path in (Path(vm.socket), self._settings.guest_socket_path(vm.id)):
try:
socket_path.lstat()
except FileNotFoundError:
continue
except OSError:
return "unknown"
# An unowned socket may belong to a child whose identity was never saved.
return "unknown"
# An unowned socket may belong to a child whose identity was never saved.
return "unknown"
return "gone"
stat = self._process_stat(vm.pid)
if stat is None:
@@ -330,14 +332,14 @@ class FirecrackerProcessManager:
f"refusing to stop Firecracker PID {vm.pid}: process identity is ambiguous"
)
if observation == "gone":
self._remove_socket(Path(vm.socket))
self._remove_runtime_sockets(vm)
return
try:
pidfd = _open_pidfd(vm.pid)
except ProcessLookupError:
self._reap_child(vm.pid)
self._remove_socket(Path(vm.socket))
self._remove_runtime_sockets(vm)
return
except (OSError, NotImplementedError) as error:
raise FirecrackerError(
@@ -369,7 +371,7 @@ class FirecrackerProcessManager:
"process identity and socket retained"
)
self._reap_child(vm.pid)
self._remove_socket(Path(vm.socket))
self._remove_runtime_sockets(vm)
except (OSError, NotImplementedError) as error:
raise FirecrackerError(
f"could not safely stop Firecracker process {vm.pid}: {error}"
@@ -455,6 +457,10 @@ class FirecrackerProcessManager:
if process is not None and process.poll() is not None:
self._children.pop(pid, None)
def _remove_runtime_sockets(self, vm: VmRecord) -> None:
self._remove_socket(self._settings.guest_socket_path(vm.id))
self._remove_socket(Path(vm.socket))
@staticmethod
def _remove_socket(socket_path: Path) -> None:
try: