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.
108 lines
4 KiB
Python
Executable file
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)))
|