1
0
Fork 1
mirror of https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git synced 2025-07-20 02:55:07 +03:00
pmbootstrap/pmb/install/format.py
Casey Connolly 18d912d53d WIP: install: rootless disk image
Refactor the install code to stop using loop devices and instead create
and manipulate a disk image directly. Both ext4 and vfat have mechanisms
for formatting and populating partitions at an offset inside an image,
other filesystems likely do as well but so far have not been implemented
or tested.

With this "pmbootstrap install" works for standard EFI disk images (e.g.
QEMU, X64 or trailblazer) entirely rootless.

Since the creation of the disk images happens in the same user namespace
as everything else, the resulting disk images have correct ownership and
permissions even though from the host perspective they are all subuids.

This gets image building working properly *for the default case*. We can
now build disk images! In particular, we can build disk images with a 4k
sector size even on a host with a 512 byte sector size (or block size in
the filesystem). This is surprisingly hard for some reason since not all
libfdisk tools have the right flags. Thankfully sfdisk does.

In addition, we now generate UUIDs ourselves, to break the loop between
generating fstab and running mkfs (since we also populate the disk image
/with/ mkfs, we need to already know the UUID when we run it...).

Signed-off-by: Casey Connolly <kcxt@postmarketos.org>
2025-07-11 19:35:04 +02:00

298 lines
11 KiB
Python

# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
from pmb.helpers import logging
from pmb.helpers.devices import get_device_category_by_name
import pmb.chroot
import pmb.chroot.apk
from pmb.core import Chroot
from pmb.core.context import get_context
from pmb.types import PartitionLayout, PmbArgs, PathString
import os
from pathlib import Path
def install_fsprogs(filesystem: str) -> None:
"""Install the package required to format a specific filesystem."""
fsprogs = pmb.config.filesystems.get(filesystem)
if not fsprogs:
raise RuntimeError(f"Unsupported filesystem: {filesystem}")
pmb.chroot.apk.install([fsprogs], Chroot.native())
def format_and_mount_boot(layout: PartitionLayout) -> None:
"""
:param device: boot partition on install block device (e.g. /dev/installp1)
When adjusting this function, make sure to also adjust
ondev-prepare-internal-storage.sh in postmarketos-ondev.git!
"""
pmb.chroot.apk.install(["mtools"], Chroot.native(), build=False, quiet=True)
deviceinfo = pmb.parse.deviceinfo()
filesystem = deviceinfo.boot_filesystem or "ext2"
layout.boot.filesystem = filesystem
offset_sectors = deviceinfo.boot_part_start
offset_bytes = layout.boot.offset
boot_path = "/mnt/rootfs/boot"
install_fsprogs(filesystem)
logging.info(f"(native) format {layout.boot.path} (boot, {filesystem})")
# mkfs.fat takes offset in sectors! wtf...
if filesystem == "fat16":
pmb.chroot.root(
[
"mkfs.fat",
"-F",
"16",
"-i",
layout.boot.uuid.replace("-", ""),
"--offset",
str(offset_sectors),
"-n",
layout.boot.partition_label,
layout.boot.path,
]
)
elif filesystem == "fat32":
pmb.chroot.root(
[
"mkfs.fat",
"-F",
"32",
"-i",
layout.boot.uuid.replace("-", ""),
"--offset",
str(offset_sectors),
"-n",
layout.boot.partition_label,
layout.boot.path,
]
)
elif filesystem == "ext2":
pmb.chroot.root(
[
"mkfs.ext2",
"-d",
boot_path,
"-U",
layout.boot.uuid,
"-F",
"-q",
"-E",
f"offset={offset_bytes}",
"-L",
layout.boot.partition_label,
layout.boot.path,
f"{round(layout.boot.size / 1024)}k",
]
)
elif filesystem == "btrfs":
raise ValueError("BTRFS not yet supported with new sandbox")
pmb.chroot.root(["mkfs.btrfs", "-f", "-q", "-L", layout.boot.partition_label, layout.boot.path])
else:
raise RuntimeError("Filesystem " + filesystem + " is not supported!")
# Copy in the filesystem
if filesystem.startswith("fat"):
contents = [
path.relative_to(Chroot.native().path)
for path in (Chroot.native() / boot_path).glob("*")
]
pmb.chroot.root(
["mcopy", "-i", f"{layout.boot.path}@@{offset_bytes}", "-s", *contents, "::"]
)
def format_luks_root(args: PmbArgs, layout: PartitionLayout, device: str) -> None:
"""
:param device: root partition on install block device (e.g. /dev/installp2)
"""
mountpoint = "/dev/mapper/pm_crypt"
logging.info(f"(native) format {device} (root, luks), mount to {mountpoint}")
logging.info(" *** TYPE IN THE FULL DISK ENCRYPTION PASSWORD (TWICE!) ***")
# Avoid cryptsetup warning about missing locking directory
pmb.chroot.root(["mkdir", "-p", "/run/cryptsetup"])
format_cmd = [
"cryptsetup",
"luksFormat",
"-q",
"--cipher",
args.cipher,
"--iter-time",
args.iter_time,
"--use-random",
"--offset",
str(layout.root.offset),
device,
]
open_cmd = ["cryptsetup", "luksOpen"]
path_outside = None
fde_key = os.environ.get("PMB_FDE_PASSWORD", None)
if fde_key:
# Write passphrase to a temp file, to avoid printing it in any log
path = tempfile.mktemp(dir="/tmp")
path_outside = Chroot.native() / path
with open(path_outside, "w", encoding="utf-8") as handle:
handle.write(f"{fde_key}")
format_cmd += [str(path)]
open_cmd += ["--key-file", str(path)]
try:
pmb.chroot.root(format_cmd, output="interactive")
pmb.chroot.root([*open_cmd, device, "pm_crypt"], output="interactive")
finally:
if path_outside:
os.unlink(path_outside)
if not (Chroot.native() / mountpoint).exists():
raise RuntimeError("Failed to open cryptdevice!")
def prepare_btrfs_subvolumes(args: PmbArgs, device: str, mountpoint: str) -> None:
"""
Create separate subvolumes if root filesystem is btrfs.
This lets us do snapshots and rollbacks of relevant parts
of the filesystem.
/var contains logs, VMs, containers, flatpaks; and shouldn't roll back,
/root is root's home directory and shouldn't roll back,
/tmp has temporary files, snapshotting them is unnecessary,
/srv contains data for web and FTP servers, and shouldn't roll back,
/snapshots should be a separate subvol so that changing the root subvol
doesn't affect snapshots
"""
subvolume_list = ["@", "@home", "@root", "@snapshots", "@srv", "@tmp", "@var"]
for subvolume in subvolume_list:
pmb.chroot.root(["btrfs", "subvol", "create", f"{mountpoint}/{subvolume}"])
# Set the default root subvolume to be separate from top level btrfs
# subvol. This lets us easily swap out current root subvol with an
# earlier snapshot.
pmb.chroot.root(["btrfs", "subvol", "set-default", f"{mountpoint}/@"])
# Make directories to mount subvols onto
pmb.chroot.root(["umount", mountpoint])
pmb.chroot.root(["mount", device, mountpoint])
pmb.chroot.root(
[
"mkdir",
f"{mountpoint}/home",
f"{mountpoint}/root",
f"{mountpoint}/.snapshots",
f"{mountpoint}/srv",
f"{mountpoint}/var",
]
)
# snapshots contain sensitive information,
# and should only be readable by root.
pmb.chroot.root(["chmod", "700", f"{mountpoint}/root"])
pmb.chroot.root(["chmod", "700", f"{mountpoint}/.snapshots"])
# Mount subvols
pmb.chroot.root(["mount", "-o", "subvol=@var", device, f"{mountpoint}/var"])
pmb.chroot.root(["mount", "-o", "subvol=@home", device, f"{mountpoint}/home"])
pmb.chroot.root(["mount", "-o", "subvol=@root", device, f"{mountpoint}/root"])
pmb.chroot.root(["mount", "-o", "subvol=@srv", device, f"{mountpoint}/srv"])
pmb.chroot.root(["mount", "-o", "subvol=@snapshots", device, f"{mountpoint}/.snapshots"])
# Disable CoW for /var, to avoid write multiplication
# and slowdown on databases, containers and VM images.
pmb.chroot.root(["chattr", "+C", f"{mountpoint}/var"])
def format_and_mount_root(args: PmbArgs, layout: PartitionLayout) -> None:
"""
:param layout: disk image layout
"""
# Format
if not args.rsync:
filesystem = layout.root.filesystem
layout.root.filesystem = filesystem
rootfs = Path("/mnt/rootfs")
# Bind mount an empty path over /boot so we don't include it in the root partition
# FIXME: better way to check if running with --single-partition
if len(layout) > 1:
empty_dir = Path("/tmp/empty")
pmb.mount.bind(empty_dir, Chroot.native() / rootfs / "boot")
if filesystem != "ext4":
raise RuntimeError(
"Only EXT4 supports offset parameter for writing directly to disk image!"
)
if filesystem == "ext4":
device_category = get_device_category_by_name(get_context().config.device)
if device_category.allows_downstream_ports():
# Some downstream kernels don't support metadata_csum (#1364).
# When changing the options of mkfs.ext4, also change them in the
# recovery zip code (see 'grep -r mkfs\.ext4')!
category_opts = ["-O", "^metadata_csum"]
else:
category_opts = []
# Some downstream kernels don't support metadata_csum (#1364).
# When changing the options of mkfs.ext4, also change them in the
# recovery zip code (see 'grep -r mkfs\.ext4')!
mkfs_root_args = [
"mkfs.ext4",
"-d",
rootfs,
"-F",
"-q",
"-L",
layout.root.partition_label,
"-U",
layout.root.uuid,
*category_opts,
]
# pmb#2568: tell mkfs.ext4 to make a filesystem with enough
# indoes that we don't run into "out of space" errors
mkfs_root_args = [*mkfs_root_args, "-i", "16384"]
if not layout.split:
mkfs_root_args = [*mkfs_root_args, "-E", f"offset={layout.root.offset}"]
elif filesystem == "f2fs":
mkfs_root_args = ["mkfs.f2fs", "-f", "-l", layout.root.partition_label]
elif filesystem == "btrfs":
mkfs_root_args = ["mkfs.btrfs", "-f", "-L", layout.root.partition_label]
else:
raise RuntimeError(f"Don't know how to format {filesystem}!")
install_fsprogs(filesystem)
logging.info(f"(native) format {layout.root.path} (root, {filesystem})")
pmb.chroot.root([*mkfs_root_args, layout.root.path, f"{round(layout.root.size / 1024)}k"])
# Unmount the empty dir we mounted over /boot
pmb.mount.umount_all(Chroot.native() / rootfs / "boot")
# FIXME: btrfs borked
# if not args.rsync and filesystem == "btrfs":
# # Make flat btrfs subvolume layout
# prepare_btrfs_subvolumes(args, device, mountpoint)
def format(
args: PmbArgs,
layout: PartitionLayout | None,
rootfs: Path,
disk: PathString | None,
) -> None:
"""
:param layout: partition layout from get_partition_layout() or None
:param boot_label: label of the boot partition (e.g. "pmOS_boot")
:param root_label: label of the root partition (e.g. "pmOS_root")
:param disk: path to disk block device (e.g. /dev/mmcblk0) or None
"""
# FIXME: do this elsewhere?
pmb.mount.bind(rootfs, Chroot.native().path / "mnt/rootfs")
# FIXME: probably broken because luksOpen uses loop under the hood, needs testing...
# root_dev = "/dev/mapper/pm_crypt"
format_and_mount_root(args, layout)
# FIXME: better way to check if we are running with --single-partition
if len(layout) > 1:
format_and_mount_boot(layout)