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:
parent
0646b48bb3
commit
6ae19579c1
1 changed files with 95 additions and 0 deletions
95
stages/io.weldr.grub2
Executable file
95
stages/io.weldr.grub2
Executable 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue