stages/io.weldr.grub2: add a stage to generate grub2 configuration

The stage populates the tree with grub2 configuration. The API and
semantics is the way we want it, but internally this is a massive
hack.

GRUB2 is only able to run grub2-mkconfig on the image it wants to
configure. The reason is that it will inspect / and /boot to detect
the existing UUIDs and filesystems to use, despite this being
information we already know. In principle, the tool does support
passing this is, but due to several bugs that functionality does
not work.

We therefore create the image we want, copy over the tree, run
grub2-mkconfig in this image, then copy it back over the tree. The
end result is that the files /etc/defaults/grub,
/boot/grub2/grub.cfg and /boot/grub2/grubev are added to the tree.

The alternative would be to do what tools typically do, and just
run grub2-mkconfig on the final image at the time it is being
assembled. We want to avoid this in order to fully split filesystem
tree generation from image assembly. This way we can better control
and verify what ends up on the filesystem which should help with
reprobucibility and reuse of filesystem trees. Above all though,
we want to make sure that we can actually place some guarantees
on what each stage of the image building process actually does,
allowing us to argue about and change it without worrying about
arbitrary fallout.

Signed-off-by: Tom Gundersen <teg@jklm.no>
This commit is contained in:
Tom Gundersen 2019-06-17 14:55:42 +02:00
parent 0646b48bb3
commit 6ae19579c1

95
stages/io.weldr.grub2 Executable file
View file

@ -0,0 +1,95 @@
#!/usr/bin/python3
import contextlib
import json
import os
import shutil
import subprocess
import sys
import tempfile
def tree_size(tree):
size = 0
for root, dirs, files in os.walk(tree):
for entry in files + dirs:
path = os.path.join(root, entry)
size += os.stat(path, follow_symlinks=False).st_size
return size
@contextlib.contextmanager
def mount(source, dest, *options):
os.makedirs(dest, 0o755, True)
subprocess.run(["mount", *options, source, dest], check=True)
try:
yield
finally:
subprocess.run(["umount", "-R", dest], check=True)
@contextlib.contextmanager
def mount_api(dest):
with mount("/dev", f"{dest}/dev", "-o", "rbind"), \
mount("/proc", f"{dest}/proc", "-o", "rbind"), \
mount("/sys", f"{dest}/sys", "-o", "rbind"), \
mount("none", f"{dest}/run", "-t", "tmpfs"):
yield
@contextlib.contextmanager
def loop_device(image):
r = subprocess.run(["losetup", "-f"], stdout=subprocess.PIPE, encoding="utf-8", check=True)
loop = r.stdout.strip()
r = subprocess.run(["losetup", loop, "-P", image], check=True)
try:
yield loop
finally:
subprocess.run(["losetup", "-d", loop], check=True)
def main(tree, input_dir, options):
partition_table_id = options["partition_table_id"]
root_fs_uuid = options["root_fs_uuid"]
# Create the configuration file that determines how grub.cfg is generated.
os.makedirs(f"{tree}/etc/default", exist_ok=True)
with open(f"{tree}/etc/default/grub", "w") as default:
default.write("GRUB_ENABLE_BLSCFG=true")
# Create a working directory on a tmpfs, maybe we should implicitly
# always do this.
with tempfile.TemporaryDirectory() as workdir:
image = f"{workdir}/image.raw"
mountpoint = f"{workdir}/mnt"
# Create an empty image file of the right size
size = int(tree_size(tree) * 1.2)
subprocess.run(["truncate", "--size", str(size), image], check=True)
# Set up the partition table of the image
partition_table = "label: mbr\nlabel-id: {partition_table_id}\nbootable, type=83"
subprocess.run(["sfdisk", "-q", image], input=partition_table, encoding='utf-8', check=True)
# Mount the created image as a loopback device
with loop_device(image) as loop:
# Populate the first partition of the image with an ext4 fs and fill it with the contents of the
# tree we are operating on.
subprocess.run(["mkfs.ext4", "-d", tree, "-U", root_fs_uuid, f"{loop}p1"], check=True)
# Mount the partition. The contents is now exactly the same as the input tree, with the only
# difference that when it inspects its own filesystem it will see what has been configured,
# rather than a tmpfs.
with mount(f"{loop}p1", mountpoint):
# Run the tool in the image we created.
with mount_api(mountpoint):
subprocess.run(["chroot", mountpoint, "grub2-mkconfig", "--output=/boot/grub2/grub.cfg"], check=True)
# Copy the entire contents of the image back on top of the input tree, capturing all the changes
# the tool performed.
for name in os.listdir(mountpoint):
if name == "lost+found":
continue
srcname = os.path.join(mountpoint, name)
dstname = os.path.join(tree, name)
subprocess.run(["cp", "-a", "-T", srcname, dstname], check=True)
if __name__ == '__main__':
args = json.load(sys.stdin)
r = main(args["tree"], args["input_dir"], args["options"])
sys.exit(r)