debian-forge/osbuild
Lars Karlitski 01aa00837f osbuild: drop state
Stages should be as stateless as possible. Don't provide an easy way out
of that.

Only the dnf stage used stage to save the dnf cache. That's only useful
during development and can be solved by pointing to a local repo mirror.
2019-06-12 15:23:45 +02:00

108 lines
4 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, sit):
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_str = json.dumps(options, indent=2)
print()
print(f"{RESET}{BOLD}{i}. {name}{RESET} {options_str}")
print("Inspect with:")
print(f"\t# nsenter -a --wd=/root -t `machinectl show {os.path.basename(root)} -p Leader --value`")
print()
try:
opts = ["--sit"] if sit else []
subprocess.run(["systemd-nspawn",
"--as-pid2",
"--link-journal=no",
"--volatile=yes",
f"--directory={root}",
f"--bind={tree}:/tmp/tree",
f"--bind={os.getcwd()}/run-stage:/tmp/run-stage",
f"--bind={os.getcwd()}/stages/{name}:/tmp/stage",
"--bind=/etc/pki",
"/tmp/run-stage", *opts, "/tmp/stage"],
input=options_str, encoding="utf-8",
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
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")
parser.add_argument("--sit", action="store_true",
help="keep the build environment up when a stage failed")
args = parser.parse_args()
sys.exit(main(**vars(args)))