Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions examples/create_vm_with_filesystem_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Example: Creating a MicroVM with Different Filesystem Formats

This example demonstrates how to create microVMs with different rootfs filesystem formats.
The firecracker-python library now supports ext3, ext4, and xfs filesystem formats.
"""

from firecracker import MicroVM

# Example 1: Create a VM with ext4 filesystem (default)
print("Creating VM with ext4 filesystem...")
vm_ext4 = MicroVM(
name="vm-ext4",
image="alpine:latest",
base_rootfs="./rootfs_ext4.img",
rootfs_size="5G",
rootfs_format="ext4", # Explicitly specify ext4 format
kernel_file="/var/lib/firecracker/kernel/vmlinux",
vcpu=2,
memory="2G",
verbose=True,
)

# Build the rootfs with ext4 filesystem
result = vm_ext4.build()
print(f"Result: {result}")

# Example 2: Create a VM with ext3 filesystem
print("\nCreating VM with ext3 filesystem...")
vm_ext3 = MicroVM(
name="vm-ext3",
image="alpine:latest",
base_rootfs="./rootfs_ext3.img",
rootfs_size="5G",
rootfs_format="ext3", # Use ext3 format
kernel_file="/var/lib/firecracker/kernel/vmlinux",
vcpu=2,
memory="2G",
verbose=True,
)

# Build the rootfs with ext3 filesystem
result = vm_ext3.build()
print(f"Result: {result}")

# Example 3: Create a VM with xfs filesystem
print("\nCreating VM with xfs filesystem...")
vm_xfs = MicroVM(
name="vm-xfs",
image="alpine:latest",
base_rootfs="./rootfs_xfs.img",
rootfs_size="5G",
rootfs_format="xfs", # Use xfs format
kernel_file="/var/lib/firecracker/kernel/vmlinux",
vcpu=2,
memory="2G",
verbose=True,
)

# Build the rootfs with xfs filesystem
result = vm_xfs.build()
print(f"Result: {result}")

# Example 4: Create and start a VM with a specific filesystem format
print("\nCreating and starting VM with xfs filesystem...")
vm = MicroVM(
name="running-vm-xfs",
image="ubuntu:24.04",
base_rootfs="./ubuntu_rootfs_xfs.img",
rootfs_size="10G",
rootfs_format="xfs",
kernel_file="/var/lib/firecracker/kernel/vmlinux",
ip_addr="172.16.0.10",
vcpu=2,
memory="4G",
expose_ports=True,
host_port=10222,
dest_port=22,
verbose=True,
)

# Create and start the VM
result = vm.create()
print(f"Result: {result}")

# Clean up (optional)
# vm.delete()

print("\n✓ All examples completed successfully!")
print("\nNotes:")
print("- Supported formats: ext3, ext4, xfs")
print("- Default format: ext4")
print("- The filesystem format is validated during initialization")
print("- Overlayfs also respects the specified format")
5 changes: 3 additions & 2 deletions firecracker/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import os
from dataclasses import dataclass


@dataclass
class MicroVMConfig:
"""Configuration defaults for Firecracker microVMs."""

data_path: str = "/var/lib/firecracker"
binary_path: str = "/usr/local/bin/firecracker"
snapshot_path: str = "/var/lib/firecracker/snapshots"
kernel_file: str = None
rootfs_size: str = "5G"
rootfs_format: str = "ext4"
initrd_file: str = None
init_file: str = "/sbin/init"
base_rootfs: str = None
Expand All @@ -27,4 +28,4 @@ class MicroVMConfig:
host_port: int = None
dest_port: int = None
vsock_enabled: bool = False
vsock_guest_cid: int = 3
vsock_guest_cid: int = 3
75 changes: 63 additions & 12 deletions firecracker/microvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class MicroVM:
image (str, optional): Docker image to use for the MicroVM
base_rootfs (str, optional): Path to the base rootfs file
rootfs_size (str, optional): Size of the rootfs file
rootfs_format (str, optional): Filesystem format for rootfs (ext3, ext4, xfs). Defaults to ext4
overlayfs (bool, optional): Whether to use overlayfs
overlayfs_file (str, optional): Path to the overlayfs file
vcpu (int, optional): Number of vCPUs
Expand Down Expand Up @@ -76,6 +77,7 @@ def __init__(
image: str = None,
base_rootfs: str = None,
rootfs_size: str = None,
rootfs_format: str = None,
overlayfs: bool = False,
overlayfs_file: str = None,
vcpu: int = None,
Expand Down Expand Up @@ -188,10 +190,20 @@ def __init__(
self._rootfs_file = os.path.join(self._rootfs_dir, base_rootfs_name)

self._rootfs_size = rootfs_size or self._config.rootfs_size

# Validate and set rootfs format
supported_formats = ["ext3", "ext4", "xfs"]
self._rootfs_format = rootfs_format or self._config.rootfs_format
if self._rootfs_format not in supported_formats:
raise ValueError(
f"Unsupported rootfs format '{self._rootfs_format}'. "
f"Supported formats are: {', '.join(supported_formats)}"
)

self._overlayfs = overlayfs or self._config.overlayfs
if self._overlayfs:
self._overlayfs_file = overlayfs_file or os.path.join(
self._rootfs_dir, "overlayfs.ext4"
self._rootfs_dir, f"overlayfs.{self._rootfs_format}"
)
self._overlayfs_name = os.path.basename(
self._overlayfs_file.replace("./", "")
Expand Down Expand Up @@ -310,7 +322,12 @@ def build(self):
if not self._docker_image:
return "No Docker image specified for building rootfs"

self._build_rootfs(self._docker_image, self._base_rootfs, self._rootfs_size)
self._build_rootfs(
self._docker_image,
self._base_rootfs,
self._rootfs_size,
self._rootfs_format,
)

return f"Rootfs built at {self._base_rootfs}"

Expand Down Expand Up @@ -369,7 +386,10 @@ def create(
f"Building rootfs from Docker image: {self._docker_image}"
)
self._build_rootfs(
self._docker_image, self._base_rootfs, self._rootfs_size
self._docker_image,
self._base_rootfs,
self._rootfs_size,
self._rootfs_format,
)

self._network.setup(
Expand Down Expand Up @@ -1550,37 +1570,68 @@ def _export_docker_image(self, image: str) -> str:
except Exception as e:
raise VMMError(f"Unexpected error: {e}")

def _build_rootfs(self, image: str, file: str, size: str):
def _build_rootfs(self, image: str, file: str, size: str, fs_format: str = "ext4"):
"""Create a filesystem image from a tar file.

Args:
image (str): Docker image name
file (str): Path to the output image file
size (str): Size of the image file
fs_format (str): Filesystem format (ext3, ext4, or xfs). Defaults to ext4

Returns:
str: Path to the created image file
"""
tmp_dir = None
try:
# Validate filesystem format
supported_formats = ["ext3", "ext4", "xfs"]
if fs_format not in supported_formats:
raise ValueError(
f"Unsupported filesystem format '{fs_format}'. "
f"Supported formats are: {', '.join(supported_formats)}"
)

self._download_docker(image)
tar_file = self._export_docker_image(image)

if not tar_file or not os.path.exists(tar_file):
return f"Failed to export Docker image {image}"

# Allocate file
run(f"fallocate -l {size} {file}")
if self._config.verbose:
self._logger.debug(f"Image file created: {file}")

run(f"mkfs.ext4 {file}")
if self._config.verbose:
self._logger.debug(f"Formatting filesystem: {file} with size {size}")

run(f"e2fsck -f -y {file}")
if self._config.verbose:
self._logger.debug(f"Filesystem check completed for: {file}")
# Format filesystem based on the specified format
if fs_format == "xfs":
run(f"mkfs.xfs -f {file}")
if self._config.verbose:
self._logger.debug(
f"Formatting filesystem: {file} with xfs (size {size})"
)
elif fs_format == "ext3":
run(f"mkfs.ext3 -F {file}")
if self._config.verbose:
self._logger.debug(
f"Formatting filesystem: {file} with ext3 (size {size})"
)
# Check filesystem for ext3
run(f"e2fsck -f -y {file}")
if self._config.verbose:
self._logger.debug(f"Filesystem check completed for: {file}")
else: # ext4 (default)
run(f"mkfs.ext4 -F {file}")
if self._config.verbose:
self._logger.debug(
f"Formatting filesystem: {file} with ext4 (size {size})"
)
# Check filesystem for ext4
run(f"e2fsck -f -y {file}")
if self._config.verbose:
self._logger.debug(f"Filesystem check completed for: {file}")

# Mount and extract
tmp_dir = tempfile.mkdtemp()
run(f"mount -o loop {file} {tmp_dir}")

Expand All @@ -1599,7 +1650,7 @@ def _build_rootfs(self, image: str, file: str, size: str):
)

if self._config.verbose:
self._logger.info("Build rootfs completed")
self._logger.info(f"Build rootfs completed with {fs_format} filesystem")

except Exception as e:
if tmp_dir:
Expand Down
124 changes: 124 additions & 0 deletions tests/test_rootfs_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3

import pytest
from firecracker import MicroVM

# Use the same paths as in conftest.py
KERNEL_FILE = "/var/lib/firecracker/kernel/vmlinux"
BASE_ROOTFS = "./rootfs.img"


class TestRootfsFormat:
"""Test cases for rootfs filesystem format support"""

def test_default_rootfs_format(self):
"""Test that default rootfs format is ext4"""
vm = MicroVM(kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS)
assert vm._rootfs_format == "ext4"

def test_ext4_rootfs_format(self):
"""Test creating VM with ext4 filesystem"""
vm = MicroVM(
kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format="ext4"
)
assert vm._rootfs_format == "ext4"

def test_ext3_rootfs_format(self):
"""Test creating VM with ext3 filesystem"""
vm = MicroVM(
kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format="ext3"
)
assert vm._rootfs_format == "ext3"

def test_xfs_rootfs_format(self):
"""Test creating VM with xfs filesystem"""
vm = MicroVM(
kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format="xfs"
)
assert vm._rootfs_format == "xfs"

def test_invalid_rootfs_format(self):
"""Test that invalid filesystem format raises ValueError"""
with pytest.raises(
ValueError,
match=r"Unsupported rootfs format 'btrfs'. Supported formats are: ext3, ext4, xfs",
):
MicroVM(
kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format="btrfs"
)

def test_overlayfs_file_extension_matches_format(self):
"""Test that overlayfs file extension matches the rootfs format"""
vm = MicroVM(
kernel_file=KERNEL_FILE,
base_rootfs=BASE_ROOTFS,
rootfs_format="xfs",
overlayfs=True,
)
assert vm._rootfs_format == "xfs"
assert vm._overlayfs_file.endswith(".xfs")

def test_overlayfs_ext3_format(self):
"""Test overlayfs with ext3 format"""
vm = MicroVM(
kernel_file=KERNEL_FILE,
base_rootfs=BASE_ROOTFS,
rootfs_format="ext3",
overlayfs=True,
)
assert vm._rootfs_format == "ext3"
assert vm._overlayfs_file.endswith(".ext3")

def test_build_rootfs_format_validation_ext4(self):
"""Test that ext4 format is properly validated in _build_rootfs"""
vm = MicroVM(
image="alpine:latest",
base_rootfs="./test_rootfs_ext4.img",
rootfs_format="ext4",
verbose=True,
)
# Just verify the format is set correctly
assert vm._rootfs_format == "ext4"

def test_build_rootfs_format_validation_ext3(self):
"""Test that ext3 format is properly validated in _build_rootfs"""
vm = MicroVM(
image="alpine:latest",
base_rootfs="./test_rootfs_ext3.img",
rootfs_format="ext3",
verbose=True,
)
# Just verify the format is set correctly
assert vm._rootfs_format == "ext3"

def test_build_rootfs_format_validation_xfs(self):
"""Test that xfs format is properly validated in _build_rootfs"""
vm = MicroVM(
image="alpine:latest",
base_rootfs="./test_rootfs_xfs.img",
rootfs_format="xfs",
verbose=True,
)
# Just verify the format is set correctly
assert vm._rootfs_format == "xfs"

def test_case_sensitivity_rootfs_format(self):
"""Test that rootfs format is case-sensitive (lowercase only)"""
with pytest.raises(ValueError, match=r"Unsupported rootfs format 'EXT4'"):
MicroVM(
kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format="EXT4"
)

def test_none_rootfs_format_uses_default(self):
"""Test that None for rootfs format uses the default (ext4)"""
vm = MicroVM(
kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format=None
)
# Should default to ext4
assert vm._rootfs_format == "ext4"

def test_empty_rootfs_format_uses_default(self):
"""Test that empty string for rootfs format uses the default (ext4)"""
vm = MicroVM(kernel_file=KERNEL_FILE, base_rootfs=BASE_ROOTFS, rootfs_format="")
# Empty string should default to ext4 (from config)
assert vm._rootfs_format == "ext4"