osbuild: replace --from and --save with --input and --output

The new arguments are passed to the first, respectively last, stage
and are both directories. --input is read only and can be used to
initialize the first stage. --output is r/w and is where the final
stage should place the produced image.

Signed-off-by: Tom Gundersen <teg@jklm.no>
This commit is contained in:
Tom Gundersen 2019-06-12 16:26:29 +02:00
parent 01aa00837f
commit 40cf349f18
2 changed files with 50 additions and 40 deletions

View file

@ -35,21 +35,21 @@ modifies a file system tree. Pipelines are defined as JSON files like this one:
}
```
`osbuild` runs each of the stages in turn, isolating them into mount and pid
namespaces. It injects the `options` object with a `tree` key pointing to the
file system tree and passes that to the stage via its `stdin`. Each stage has
private `/tmp` and `/var/tmp` directories that are deleted after the stage is
run.
`osbuild` runs each of the stages in turn, isolating them from the host and
from each other, with the exception that the first stage may be given an input
directory, the last stage an output directory and all stages of a given
pipeline are given the same filesystem tree to operate on.
Stages may have side effects: the `io.weldr.qcow2` stage in the above
example packs the tree into a `qcow2` image.
Each stage is passed the (appended) `options` object as JSON over stdin.
The above pipeline has no input and produces a qcow2 image.
## Running
```
osbuild [--from ARCHIVE] [--save ARCHIVE] PIPELINE
osbuild [--input DIRECTORY] [--output DIRECTORY] PIPELINE
```
Runs `PIPELINE`. If `--from` is given, unpacks its contents (`.tar.gz`) into
the tree before running the first stage. If `--save` is given, saves the
contents of the tree in the given archive.
Runs `PIPELINE`. If `--input` is given, the directory is available
read-only in the first stage. If `--output` is given it, it must be empty
and is avialble read-write in the final stage.

68
osbuild
View file

@ -15,20 +15,14 @@ RED = "\033[31m"
@contextlib.contextmanager
def tmpfs(save=None):
def tmpfs():
"""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)
@ -45,21 +39,47 @@ def bindmnt(src_path):
subprocess.run(["umount", dst_path], check=True)
def main(pipeline_path, from_archive, save, sit):
def main(pipeline_path, input_dir, output_dir, sit):
if output_dir and len(os.listdir(output_dir)) != 0:
print()
print(f"{RESET}{BOLD}{RED}Output directory {output_dir} is not empty{RESET}")
return 1
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
with tmpfs() as tree, bindmnt("/") as root:
for i, stage in enumerate(pipeline["stages"], start=1):
name = stage["name"]
options = stage.get("options", {})
options["tree"] = "/tmp/tree"
argv = ["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"]
# Optionally pass the input dir to the first stage
if i == 1 and input_dir:
options["input_dir"] = "/tmp/input"
argv.append(f"--bind-ro={os.getcwd()}/{input_dir}:/tmp/input")
# Pass the tree r/w to all stages but the last one.
# The last stage gets the tree ro and optionally gets an output dir
if i == len(pipeline["stages"]) and output_dir:
options["output_dir"] = "/tmp/output"
argv.append(f"--bind={os.getcwd()}/{output_dir}:/tmp/output")
argv.append("/tmp/run-stage")
if sit:
argv.append("--sit")
argv.append("/tmp/stage")
options_str = json.dumps(options, indent=2)
print()
@ -69,17 +89,7 @@ def main(pipeline_path, from_archive, save, sit):
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"],
subprocess.run(argv,
input=options_str, encoding="utf-8",
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
check=True)
@ -97,10 +107,10 @@ 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("--input", dest="input_dir", metavar="DIRECTORY",
help="provide the contents of DIRECTORY to the first stage")
parser.add_argument("--output", dest="output_dir", metavar="DIRECTORY",
help="provide the empty DIRECTORY as output argument to the last stage")
parser.add_argument("--sit", action="store_true",
help="keep the build environment up when a stage failed")
args = parser.parse_args()