Dev #4
+16
-2
@@ -490,7 +490,14 @@ The `firecracker/` package has three separate responsibilities.
|
|||||||
2. `PUT /boot-source`
|
2. `PUT /boot-source`
|
||||||
3. `PUT /drives/rootfs`
|
3. `PUT /drives/rootfs`
|
||||||
4. `PUT /network-interfaces/eth0`
|
4. `PUT /network-interfaces/eth0`
|
||||||
5. `PUT /actions` with `InstanceStart`
|
5. `PUT /entropy` with `{}` (a host-backed virtio RNG device)
|
||||||
|
6. `PUT /actions` with `InstanceStart`
|
||||||
|
|
||||||
|
Every generated config includes `entropy`, including retained-disk boots.
|
||||||
|
An empty object enables the device with default settings; do not skip it with
|
||||||
|
a truthiness check. A missing/invalid configuration or an entropy API failure
|
||||||
|
must prevent `InstanceStart`. Without guest entropy, systemd's random-seed
|
||||||
|
service and OpenSSH can remain in startup while the VM process and IP respond.
|
||||||
|
|
||||||
The guest receives a static IP via the kernel `ip=` boot argument. The netmask
|
The guest receives a static IP via the kernel `ip=` boot argument. The netmask
|
||||||
comes from `Settings.network`, not a hard-coded `/24` value.
|
comes from `Settings.network`, not a hard-coded `/24` value.
|
||||||
@@ -729,7 +736,14 @@ handling, downloads, and integrity-manifest publication.
|
|||||||
(`amd64` to `x86_64`, `arm64` to `aarch64`) and rejects unsupported architectures.
|
(`amd64` to `x86_64`, `arm64` to `aarch64`) and rejects unsupported architectures.
|
||||||
The same architecture selects the Firecracker release archive, guest kernel,
|
The same architecture selects the Firecracker release archive, guest kernel,
|
||||||
native rootfs packages, and architecture-specific boot arguments. The default
|
native rootfs packages, and architecture-specific boot arguments. The default
|
||||||
kernel URL uses Firecracker's `img/quickstart_guide/<architecture>/kernels/vmlinux.bin`.
|
kernel URL is pinned to Firecracker's published
|
||||||
|
`firecracker-ci/v1.15/<architecture>/vmlinux-6.1.155` artifacts. The CI artifact
|
||||||
|
series is independent of the VMM release. Both architecture configs include
|
||||||
|
`CONFIG_HW_RANDOM=y` and `CONFIG_HW_RANDOM_VIRTIO=y`; custom kernels need these
|
||||||
|
built-in drivers for UVM's entropy device. The previous ARM quickstart image
|
||||||
|
was Linux 4.14.174 without the built-in virtio RNG driver. Updating the device
|
||||||
|
configuration alone cannot repair that kernel; existing installs need a forced
|
||||||
|
asset refresh with a matching new trusted kernel checksum, then a fresh VM boot.
|
||||||
An empty `Settings.rootfs_url` selects the built Ubuntu 22.04 (Jammy) template.
|
An empty `Settings.rootfs_url` selects the built Ubuntu 22.04 (Jammy) template.
|
||||||
Explicit kernel/rootfs URLs and trusted digests must match the host architecture. ARM64
|
Explicit kernel/rootfs URLs and trusted digests must match the host architecture. ARM64
|
||||||
uses an uncompressed Linux `Image` (PE format) and an ARM64 ext4 rootfs.
|
uses an uncompressed Linux `Image` (PE format) and an ARM64 ext4 rootfs.
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ Spark, UVM uses ARM64 assets and packages.
|
|||||||
`x86_64`, respectively. Custom `UVM_KERNEL_URL` and `UVM_ROOTFS_URL` overrides
|
`x86_64`, respectively. Custom `UVM_KERNEL_URL` and `UVM_ROOTFS_URL` overrides
|
||||||
must point to assets compatible with the host architecture.
|
must point to assets compatible with the host architecture.
|
||||||
|
|
||||||
|
The default guest kernel is Linux 6.1.155 from Firecracker's published `v1.15`
|
||||||
|
CI artifacts, with built-in `virtio-rng` support. UVM attaches a host-backed
|
||||||
|
entropy device before each guest boot so services such as SSH can initialize
|
||||||
|
their cryptographic random generator. Custom kernels must include
|
||||||
|
`CONFIG_HW_RANDOM=y` and `CONFIG_HW_RANDOM_VIRTIO=y` for this device.
|
||||||
|
|
||||||
|
Older installs used a Linux 4.14 ARM demo kernel and supplied no entropy
|
||||||
|
device. If SSH stays at **Starting OpenBSD Secure Shell server** while ping
|
||||||
|
works, see [SSH startup and guest entropy](USAGE.md#ssh-startup-and-guest-entropy)
|
||||||
|
for the kernel-refresh and retained-VM recovery steps.
|
||||||
|
|
||||||
## Trusted Installation
|
## Trusted Installation
|
||||||
|
|
||||||
Installation requires trusted SHA-256 digests by default. Supply digests from
|
Installation requires trusted SHA-256 digests by default. Supply digests from
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ UVM detects the host architecture and selects matching Firecracker and kernel
|
|||||||
downloads and native Ubuntu packages. NVIDIA DGX Spark uses `aarch64` artifacts
|
downloads and native Ubuntu packages. NVIDIA DGX Spark uses `aarch64` artifacts
|
||||||
and `arm64` packages; the `arm64` architecture alias is also accepted:
|
and `arm64` packages; the `arm64` architecture alias is also accepted:
|
||||||
|
|
||||||
- Kernel: `https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64/kernels/vmlinux.bin`
|
- Kernel: `https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/aarch64/vmlinux-6.1.155`
|
||||||
- Rootfs: Ubuntu 22.04 built with `debootstrap` from `ports.ubuntu.com/ubuntu-ports`.
|
- Rootfs: Ubuntu 22.04 built with `debootstrap` from `ports.ubuntu.com/ubuntu-ports`.
|
||||||
|
|
||||||
Use the installation commands below with checksums for the ARM64 kernel
|
Use the installation commands below with checksums for the ARM64 kernel
|
||||||
@@ -180,6 +180,12 @@ On ARM64, the kernel must be an uncompressed ARM64 Linux `Image` (PE format),
|
|||||||
and the rootfs must be an ARM64 ext4 filesystem. UVM adds `keep_bootcon` to the
|
and the rootfs must be an ARM64 ext4 filesystem. UVM adds `keep_bootcon` to the
|
||||||
guest boot arguments for ARM64 console output.
|
guest boot arguments for ARM64 console output.
|
||||||
|
|
||||||
|
Both architectures default to the pinned Linux 6.1.155 kernel from the published
|
||||||
|
`v1.15` CI artifact series, independently of the Firecracker binary version.
|
||||||
|
These kernels have `CONFIG_HW_RANDOM=y` and `CONFIG_HW_RANDOM_VIRTIO=y` built in.
|
||||||
|
UVM configures Firecracker's entropy device before boot; custom kernels also
|
||||||
|
need these drivers to receive host-backed randomness.
|
||||||
|
|
||||||
### Scenario: Strict First Installation
|
### Scenario: Strict First Installation
|
||||||
|
|
||||||
Obtain trusted checksums independently, then run:
|
Obtain trusted checksums independently, then run:
|
||||||
@@ -1222,6 +1228,58 @@ External firewall integration (including UFW, firewalld, Docker, and libvirt)
|
|||||||
is deferred. A UVM policy readback does not prove that external rules permit
|
is deferred. A UVM policy readback does not prove that external rules permit
|
||||||
traffic, or that they will not later remove UVM's rules.
|
traffic, or that they will not later remove UVM's rules.
|
||||||
|
|
||||||
|
### SSH Startup and Guest Entropy
|
||||||
|
|
||||||
|
`running` checks the Firecracker process, and `enforced` checks the private
|
||||||
|
firewall policy. Neither probes the guest's SSH listener. If ping works but
|
||||||
|
TCP/22 returns `Connection refused`, inspect the guest boot log:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ./uvm.py logs VM --tail 200
|
||||||
|
sudo ./uvm.py inspect VM --json
|
||||||
|
```
|
||||||
|
|
||||||
|
An SSH service stuck at `Starting OpenBSD Secure Shell server...`, together
|
||||||
|
with an unfinished `Load/Save Random Seed` service and no `random: crng init done`,
|
||||||
|
strongly suggests entropy starvation. A login prompt can still appear while
|
||||||
|
SSH is waiting for its cryptographic random generator. On the old ARM demo
|
||||||
|
kernel, `random: fast init done` is not full cryptographic RNG initialization.
|
||||||
|
|
||||||
|
Earlier UVM versions downloaded a Linux 4.14.174 ARM quickstart kernel without
|
||||||
|
the built-in virtio RNG driver and did not attach an entropy device. Recovery
|
||||||
|
requires **both** the updated UVM boot code and an RNG-capable kernel. A restart
|
||||||
|
with the old cached kernel is insufficient.
|
||||||
|
|
||||||
|
From the updated checkout, stop the affected VM before refreshing assets.
|
||||||
|
Stop any other VMs using the same installed Firecracker binary before the
|
||||||
|
forced reinstall as well. Use the Firecracker archive checksum for the configured
|
||||||
|
release and a new trusted checksum for the selected architecture's Linux 6.1.155
|
||||||
|
kernel; the old kernel checksum will not match:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ./uvm.py stop VM
|
||||||
|
sudo env -u UVM_KERNEL_URL \
|
||||||
|
UVM_FIRECRACKER_SHA256='<trusted-firecracker-archive-sha256>' \
|
||||||
|
UVM_KERNEL_SHA256='<trusted-new-kernel-sha256>' \
|
||||||
|
./uvm.py install --force
|
||||||
|
sudo ./uvm.py start VM
|
||||||
|
sudo ./uvm.py logs VM --tail 100
|
||||||
|
sudo ./uvm.py ssh VM
|
||||||
|
```
|
||||||
|
|
||||||
|
This uses the default kernel URL and boots the retained private disk with the
|
||||||
|
updated device configuration. The VM's identity, files, credentials, and SSH
|
||||||
|
host keys are retained. `install --force` also refreshes the shared rootfs
|
||||||
|
template for future creates. If using a custom kernel, supply its URL and
|
||||||
|
matching checksum with the required drivers instead. For an explicitly
|
||||||
|
unverified development installation, retain the existing
|
||||||
|
`UVM_ALLOW_UNVERIFIED_DOWNLOADS=1` setting as described in the installation section.
|
||||||
|
|
||||||
|
Restart a running UVM API server to load the updated code before using dashboard
|
||||||
|
start/restart actions. Look for `Started OpenBSD Secure Shell server` in the new
|
||||||
|
boot log and confirm an actual SSH connection. `start` on an already-running
|
||||||
|
VM only refreshes networking; use a stopped VM or `restart` to attach the device.
|
||||||
|
|
||||||
## 16. Important Limitations
|
## 16. Important Limitations
|
||||||
|
|
||||||
Keep these constraints in mind when deciding whether UVM fits a scenario:
|
Keep these constraints in mind when deciding whether UVM fits a scenario:
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ class ArchitectureSettingsTests(unittest.TestCase):
|
|||||||
settings = factory()
|
settings = factory()
|
||||||
|
|
||||||
base = (
|
base = (
|
||||||
"https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/"
|
"https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/"
|
||||||
f"{architecture}"
|
f"{architecture}"
|
||||||
)
|
)
|
||||||
self.assertEqual(settings.architecture, architecture)
|
self.assertEqual(settings.architecture, architecture)
|
||||||
self.assertEqual(settings.kernel_url, f"{base}/kernels/vmlinux.bin")
|
self.assertEqual(settings.kernel_url, f"{base}/vmlinux-6.1.155")
|
||||||
self.assertEqual(settings.rootfs_url, "")
|
self.assertEqual(settings.rootfs_url, "")
|
||||||
|
|
||||||
def test_preserves_individual_environment_url_overrides_on_arm64(self) -> None:
|
def test_preserves_individual_environment_url_overrides_on_arm64(self) -> None:
|
||||||
kernel = "https://example.com/custom-arm64-kernel"
|
kernel = "https://example.com/custom-arm64-kernel"
|
||||||
rootfs = "https://example.com/custom-arm64-rootfs.ext4"
|
rootfs = "https://example.com/custom-arm64-rootfs.ext4"
|
||||||
base = "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64"
|
base = "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/aarch64"
|
||||||
for overrides in (
|
for overrides in (
|
||||||
{"UVM_KERNEL_URL": kernel},
|
{"UVM_KERNEL_URL": kernel},
|
||||||
{"UVM_ROOTFS_URL": rootfs},
|
{"UVM_ROOTFS_URL": rootfs},
|
||||||
@@ -52,7 +52,7 @@ class ArchitectureSettingsTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
settings.kernel_url,
|
settings.kernel_url,
|
||||||
overrides.get("UVM_KERNEL_URL", f"{base}/kernels/vmlinux.bin"),
|
overrides.get("UVM_KERNEL_URL", f"{base}/vmlinux-6.1.155"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
settings.rootfs_url,
|
settings.rootfs_url,
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
from ipaddress import IPv4Address, IPv4Network
|
from ipaddress import IPv4Address, IPv4Network
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from uvm.config import Settings
|
from uvm.config import Settings
|
||||||
from uvm.domain import VmRecord
|
from uvm.domain import VmRecord
|
||||||
|
from uvm.errors import FirecrackerError
|
||||||
from uvm.firecracker.api import FirecrackerClient
|
from uvm.firecracker.api import FirecrackerClient
|
||||||
from uvm.firecracker.config import build_config, vcpu_count
|
from uvm.firecracker.config import build_config, vcpu_count
|
||||||
|
|
||||||
@@ -45,6 +47,10 @@ class FirecrackerConfigTests(unittest.TestCase):
|
|||||||
self.assertEqual("keep_bootcon" in arguments, architecture != "x86_64")
|
self.assertEqual("keep_bootcon" in arguments, architecture != "x86_64")
|
||||||
self.assertIn("console=ttyS0", arguments)
|
self.assertIn("console=ttyS0", arguments)
|
||||||
self.assertIn("ip=10.42.0.2::10.42.0.1:255.255.255.0::eth0:off", arguments)
|
self.assertIn("ip=10.42.0.2::10.42.0.1:255.255.255.0::eth0:off", arguments)
|
||||||
|
client = RecordingFirecrackerClient()
|
||||||
|
client.configure_and_start(config)
|
||||||
|
self.assertEqual(client.calls[-2], ("PUT", "/entropy", {}))
|
||||||
|
self.assertEqual(client.calls[-1], ("PUT", "/actions", {"action_type": "InstanceStart"}))
|
||||||
|
|
||||||
def test_builds_a_writable_vm_disk_configuration(self) -> None:
|
def test_builds_a_writable_vm_disk_configuration(self) -> None:
|
||||||
vm = VmRecord(
|
vm = VmRecord(
|
||||||
@@ -101,6 +107,7 @@ class FirecrackerConfigTests(unittest.TestCase):
|
|||||||
"/boot-source",
|
"/boot-source",
|
||||||
"/drives/rootfs",
|
"/drives/rootfs",
|
||||||
"/network-interfaces/eth0",
|
"/network-interfaces/eth0",
|
||||||
|
"/entropy",
|
||||||
"/actions",
|
"/actions",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -110,6 +117,36 @@ class FirecrackerConfigTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(client.calls[-1][2], {"action_type": "InstanceStart"})
|
self.assertEqual(client.calls[-1][2], {"action_type": "InstanceStart"})
|
||||||
|
|
||||||
|
def test_entropy_failure_prevents_guest_boot(self) -> None:
|
||||||
|
config = {
|
||||||
|
"machine-config": {"vcpu_count": 1, "mem_size_mib": 512},
|
||||||
|
"boot-source": {},
|
||||||
|
"drives": [{}],
|
||||||
|
"network-interfaces": [{}],
|
||||||
|
"entropy": {},
|
||||||
|
}
|
||||||
|
client = RecordingFirecrackerClient()
|
||||||
|
|
||||||
|
def request(method: str, path: str, body: object) -> None:
|
||||||
|
client.calls.append((method, path, body))
|
||||||
|
if path == "/entropy":
|
||||||
|
raise FirecrackerError("entropy device unavailable")
|
||||||
|
|
||||||
|
with patch.object(client, "_request", side_effect=request):
|
||||||
|
with self.assertRaisesRegex(FirecrackerError, "entropy device unavailable"):
|
||||||
|
client.configure_and_start(config)
|
||||||
|
|
||||||
|
self.assertEqual(client.calls[-1], ("PUT", "/entropy", {}))
|
||||||
|
self.assertNotIn("/actions", [path for _method, path, _body in client.calls])
|
||||||
|
|
||||||
|
for entropy in (None, [], False):
|
||||||
|
with self.subTest(entropy=entropy):
|
||||||
|
client.calls.clear()
|
||||||
|
config["entropy"] = entropy
|
||||||
|
with self.assertRaisesRegex(FirecrackerError, "no valid entropy device"):
|
||||||
|
client.configure_and_start(config)
|
||||||
|
self.assertEqual(client.calls, [])
|
||||||
|
|
||||||
def test_uses_the_configured_network_netmask_in_guest_boot_arguments(self) -> None:
|
def test_uses_the_configured_network_netmask_in_guest_boot_arguments(self) -> None:
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
network=IPv4Network("10.50.0.0/16"),
|
network=IPv4Network("10.50.0.0/16"),
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ class InstallerArchitectureTests(unittest.TestCase):
|
|||||||
payloads = {
|
payloads = {
|
||||||
"https://github.com/firecracker-microvm/firecracker/releases/download/"
|
"https://github.com/firecracker-microvm/firecracker/releases/download/"
|
||||||
f"{version}/firecracker-{version}-{architecture}.tgz": archive_bytes,
|
f"{version}/firecracker-{version}-{architecture}.tgz": archive_bytes,
|
||||||
f"{guest_base}/kernels/vmlinux.bin": kernel_bytes,
|
"https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/"
|
||||||
|
f"{architecture}/vmlinux-6.1.155": kernel_bytes,
|
||||||
f"{guest_base}/rootfs/bionic.rootfs.ext4": rootfs_bytes,
|
f"{guest_base}/rootfs/bionic.rootfs.ext4": rootfs_bytes,
|
||||||
}
|
}
|
||||||
runner = Mock(spec=CommandRunner)
|
runner = Mock(spec=CommandRunner)
|
||||||
|
|||||||
+4
-2
@@ -10,7 +10,9 @@ from pathlib import Path
|
|||||||
from .errors import ConfigurationError
|
from .errors import ConfigurationError
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_GUEST_ASSETS_URL = "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide"
|
# Pin published kernels with built-in virtio-rng support. The artifact series
|
||||||
|
# is independent of the VMM release; the old ARM quickstart kernel lacks this driver.
|
||||||
|
DEFAULT_GUEST_ASSETS_URL = "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15"
|
||||||
MIN_STORAGE_MIB = 30 * 1024
|
MIN_STORAGE_MIB = 30 * 1024
|
||||||
MAX_STORAGE_MIB = (2**63 - 1) // (1024 * 1024)
|
MAX_STORAGE_MIB = (2**63 - 1) // (1024 * 1024)
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ class Settings:
|
|||||||
# Resolve defaults at construction time, while preserving individual URL overrides.
|
# Resolve defaults at construction time, while preserving individual URL overrides.
|
||||||
asset_base = f"{DEFAULT_GUEST_ASSETS_URL}/{architecture}"
|
asset_base = f"{DEFAULT_GUEST_ASSETS_URL}/{architecture}"
|
||||||
if not self.kernel_url:
|
if not self.kernel_url:
|
||||||
object.__setattr__(self, "kernel_url", f"{asset_base}/kernels/vmlinux.bin")
|
object.__setattr__(self, "kernel_url", f"{asset_base}/vmlinux-6.1.155")
|
||||||
|
|
||||||
if self.gateway not in self.network:
|
if self.gateway not in self.network:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
|
|||||||
@@ -35,12 +35,15 @@ class FirecrackerClient:
|
|||||||
boot = config["boot-source"]
|
boot = config["boot-source"]
|
||||||
drives = config["drives"]
|
drives = config["drives"]
|
||||||
interfaces = config["network-interfaces"]
|
interfaces = config["network-interfaces"]
|
||||||
|
entropy = config.get("entropy")
|
||||||
if not isinstance(machine, Mapping) or not isinstance(boot, Mapping):
|
if not isinstance(machine, Mapping) or not isinstance(boot, Mapping):
|
||||||
raise FirecrackerError("invalid Firecracker configuration")
|
raise FirecrackerError("invalid Firecracker configuration")
|
||||||
if not isinstance(drives, list) or not drives:
|
if not isinstance(drives, list) or not drives:
|
||||||
raise FirecrackerError("Firecracker configuration has no root drive")
|
raise FirecrackerError("Firecracker configuration has no root drive")
|
||||||
if not isinstance(interfaces, list) or not interfaces:
|
if not isinstance(interfaces, list) or not interfaces:
|
||||||
raise FirecrackerError("Firecracker configuration has no network interface")
|
raise FirecrackerError("Firecracker configuration has no network interface")
|
||||||
|
if not isinstance(entropy, Mapping):
|
||||||
|
raise FirecrackerError("Firecracker configuration has no valid entropy device")
|
||||||
|
|
||||||
self._request(
|
self._request(
|
||||||
"PUT",
|
"PUT",
|
||||||
@@ -54,6 +57,7 @@ class FirecrackerClient:
|
|||||||
self._request("PUT", "/boot-source", boot)
|
self._request("PUT", "/boot-source", boot)
|
||||||
self._request("PUT", "/drives/rootfs", drives[0])
|
self._request("PUT", "/drives/rootfs", drives[0])
|
||||||
self._request("PUT", "/network-interfaces/eth0", interfaces[0])
|
self._request("PUT", "/network-interfaces/eth0", interfaces[0])
|
||||||
|
self._request("PUT", "/entropy", entropy)
|
||||||
self._request("PUT", "/actions", {"action_type": "InstanceStart"})
|
self._request("PUT", "/actions", {"action_type": "InstanceStart"})
|
||||||
|
|
||||||
def _request(self, method: str, path: str, body: object) -> None:
|
def _request(self, method: str, path: str, body: object) -> None:
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ def build_config(
|
|||||||
"host_dev_name": vm.tap,
|
"host_dev_name": vm.tap,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
# Supply host-backed randomness before guest services such as sshd start.
|
||||||
|
"entropy": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user