Rather than using unshare, we use nspawn as it gives us more isolation for free. We are not sure if we will end up with this in the end, but for the time being let's see how well it works for us. We have to do a work-around as nspawn refuses to spawn with the current root as the directory, even in read-only mode, so we bindmount it first and use the bindmount, in order to trick nspawn.
103 lines
3.5 KiB
Python
Executable file
103 lines
3.5 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
import argparse
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
RESET = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
RED = "\033[31m"
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def tmpfs(save=None):
|
|
"""A contextmanager that mounts a tmpfs and returns its location.
|
|
|
|
If `save` is given, it should contain the name of an .tar.gz archive to
|
|
which the contents of the tmpfs will be written.
|
|
"""
|
|
|
|
with tempfile.TemporaryDirectory(prefix="osbuild-tree-", dir=os.getcwd()) as path:
|
|
subprocess.run(["mount", "-t", "tmpfs", "tmpfs", path], check=True)
|
|
try:
|
|
yield path
|
|
if save:
|
|
print(f"Saving tree to {save}...")
|
|
subprocess.run(["tar", "-czf", save, "-C", path, "."], stdout=subprocess.DEVNULL, check=True)
|
|
finally:
|
|
subprocess.run(["umount", path], check=True)
|
|
|
|
@contextlib.contextmanager
|
|
def bindmnt(src_path):
|
|
"""A contextmanager that mindmounts a path read-only and returns its location.
|
|
"""
|
|
|
|
with tempfile.TemporaryDirectory(prefix="osbuild-build-", dir=os.getcwd()) as dst_path:
|
|
subprocess.run(["mount", "-o", "bind,ro", src_path, dst_path], check=True)
|
|
try:
|
|
yield dst_path
|
|
finally:
|
|
subprocess.run(["umount", dst_path], check=True)
|
|
|
|
|
|
def main(pipeline_path, from_archive, save):
|
|
with open(pipeline_path) as f:
|
|
pipeline = json.load(f)
|
|
|
|
with tmpfs(save) as tree, bindmnt("/") as root:
|
|
if from_archive:
|
|
r = subprocess.run(["tar", "-xzf", from_archive, "-C", tree])
|
|
if r.returncode != 0:
|
|
return
|
|
|
|
for i, stage in enumerate(pipeline["stages"], start=1):
|
|
name = stage["name"]
|
|
options = stage.get("options", {})
|
|
options["tree"] = "/tmp/tree"
|
|
options["state"] = "/tmp/state"
|
|
|
|
options_str = json.dumps(options, indent=2)
|
|
|
|
r = subprocess.run(["mkdir", "-p", f"{os.getcwd()}/state/{name}"])
|
|
if r.returncode != 0:
|
|
return
|
|
|
|
print()
|
|
print(f"{RESET}{BOLD}{i}. {name}{RESET} {options_str}")
|
|
print()
|
|
|
|
try:
|
|
subprocess.run(["systemd-nspawn",
|
|
"--link-journal=no",
|
|
f"--directory={root}",
|
|
f"--bind={tree}:/tmp/tree",
|
|
f"--bind={os.getcwd()}/state/{name}:/tmp/state",
|
|
f"--bind={os.getcwd()}/stages/{name}:/tmp/stage",
|
|
"/tmp/stage"],
|
|
input=options_str, encoding="utf-8", check=True)
|
|
except KeyboardInterrupt:
|
|
print()
|
|
print(f"{RESET}{BOLD}{RED}Aborted{RESET}")
|
|
return 130
|
|
except subprocess.CalledProcessError as error:
|
|
print()
|
|
print(f"{RESET}{BOLD}{RED}{name} failed with code {error.returncode}{RESET}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Build operating system images")
|
|
parser.add_argument("pipeline_path", metavar="PIPELINE",
|
|
help="json file containing the pipeline that should be built")
|
|
parser.add_argument("--save", metavar="ARCHIVE",
|
|
help="save the resulting tree to ARCHIVE")
|
|
parser.add_argument("--from", dest="from_archive", metavar="ARCHIVE",
|
|
help="initialize the tree from ARCHIVE")
|
|
args = parser.parse_args()
|
|
|
|
sys.exit(main(**vars(args)))
|