From e4e26a86985272efaafee2674b3f7e0d765603b6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 16 Jun 2021 17:16:14 +0200 Subject: [PATCH] mkosi: when using cached images, randomize fs and partition uuids explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This addresses an annoying issue when building cached images containing btrfs file systems: the kernel btrfs driver refuses mounting or handling multiple different file systems with the same uuid. This means using an image and building the next at the same time fails — as long as the UUIDs of the newly build image aren't refreshed. This patches makes sure when using a cached image we'll refresh disk, partition and file system uuids. We generate them randomly, exactly like we would have them when using non-cached builds. This also ensures that the partition labels are rewritten when images versions are bumped. (Eventually we should probably start hashing the uuids from the configuration state in some form, to provide a certain level of reproducibility, but for now let's just randomize them.) --- mkosi/__init__.py | 83 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 7361facee..c100c66c3 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -725,11 +725,24 @@ def determine_partition_table(args: CommandLineArguments) -> Tuple[str, bool]: return table, run_sfdisk +def exec_sfdisk(args: CommandLineArguments, f: BinaryIO) -> None: + + table, run_sfdisk = determine_partition_table(args) + + if run_sfdisk: + run(["sfdisk", "--color=never", f.name], input=table.encode("utf-8")) + run(["sync"]) + + args.ran_sfdisk = run_sfdisk + + def create_image(args: CommandLineArguments, for_cache: bool) -> Optional[BinaryIO]: if not args.output_format.is_disk(): return None - with complete_step("Creating partition table", "Created partition table as {.name}") as output: + with complete_step( + "Creating image with partition table", "Created image with partition table as {.name}" + ) as output: f: BinaryIO = cast( BinaryIO, @@ -739,15 +752,63 @@ def create_image(args: CommandLineArguments, for_cache: bool) -> Optional[Binary disable_cow(f.name) f.truncate(image_size(args)) - table, run_sfdisk = determine_partition_table(args) + exec_sfdisk(args, f) - if run_sfdisk: - run(["sfdisk", "--color=never", f.name], input=table.encode("utf-8")) - run(["sync"]) + return f - args.ran_sfdisk = run_sfdisk - return f +def refresh_partition_table(args: CommandLineArguments, f: BinaryIO) -> None: + if not args.output_format.is_disk(): + return + + # Let's refresh all UUIDs and labels to match the new build. This + # is called whenever we reuse a cached image, to ensure that the + # UUIDs/labels of partitions are generated the same way as for + # non-cached builds. Note that we refresh the UUIDs/labels simply + # by invoking sfdisk again. If the build parameters didn't change + # this should have the effect that offsets and sizes should remain + # identical, and we thus only update the UUIDs and labels. + # + # FIXME: One of those days we should generate the UUIDs as hashes + # of the used configuration, so that they remain stable as the + # configuration is identical. + + with complete_step("Refreshing partition table", "Refreshed partition table") as output: + exec_sfdisk(args, f) + + +def refresh_file_system(args: CommandLineArguments, dev: Optional[str], cached: bool) -> None: + + if dev is None: + return + if not cached: + return + + # Similar to refresh_partition_table() but refreshes the UUIDs of + # the file systems themselves. We want that build artifacts from + # cached builds are as similar as possible to those from uncached + # builds, and hence we want to randomize UUIDs explicitly like + # they are for uncached builds. This is particularly relevant for + # btrfs since it prohibits mounting multiple file systems at the + # same time that carry the same UUID. + # + # FIXME: One of those days we should generate the UUIDs as hashes + # of the used configuration, so that they remain stable as the + # configuration is identical. + + with complete_step(f"Refreshing file system {dev}."): + if args.output_format == OutputFormat.gpt_btrfs: + # We use -M instead of -m here, for compatibility with + # older btrfs, where -M didn't exist yet. + run(["btrfstune", "-M", str(uuid.uuid4()), dev]) + elif args.output_format == OutputFormat.gpt_ext4: + # We connect stdin to /dev/null since tune2fs otherwise + # asks an unnecessary safety question on stdin, and we + # don't want that, our script doesn't operate on essential + # file systems anyway, but just our build images. + run(["tune2fs", "-U", "random", dev], stdin=subprocess.DEVNULL) + elif args.output_format == OutputFormat.gpt_xfs: + run(["xfs_admin", "-U", "generate", dev]) def copy_image_temporary(src: str, dir: str) -> BinaryIO: @@ -6174,7 +6235,10 @@ def build_image( # Found existing cache image, exiting build_image return None, None, None, None, None, None, None - if not cached: + if cached: + assert raw is not None + refresh_partition_table(args, raw) + else: raw = create_image(args, for_cache) with attach_image_loopback(args, raw) as loopdev: @@ -6204,6 +6268,9 @@ def build_image( prepare_var(args, encrypted_var, cached) prepare_tmp(args, encrypted_tmp, cached) + for dev in (encrypted_root, encrypted_home, encrypted_srv, encrypted_var, encrypted_tmp): + refresh_file_system(args, dev, cached) + # Mount everything together, but let's not mount the root # dir if we still have to generate the root image here prepare_tree_root(args, root) -- 2.47.2