osbuild: extract CLI to prepare for additional entrypoints

This extracts the CLI entrypoint into `main_cli.py` and prepares the
codebase for the introduction of additional entrypoints. This should
not contain any functional changes.

The idea behind this is to add `main_api.py` (and maybe more in the
future), which will be similar to `main_cli.py` but contain the
`osbuild-api` entrypoint. This will make all entrypoints nicely symetric
and the only difference will be `setup.py` selecting the right
entrypoint for each executable, as well as `__main__.py` selecting the
entrypoint for the module itself (which we will keep to the CLI for
compatibility).
This commit is contained in:
David Rheinsberg 2020-04-20 10:07:03 +02:00
parent 20cf5dba6a
commit 421414ef0b
4 changed files with 153 additions and 114 deletions

View file

@ -1,3 +1,13 @@
"""OSBuild Module
The `osbuild` module provides access to the internal features of OSBuild. It
provides parsers for the input and output formats of osbuild, access to shared
infrastructure of osbuild stages, as well as a pipeline executor.
The utility module `osbuild.util` provides access to common functionality
independent of osbuild but used across the osbuild codebase.
"""
from .pipeline import Assembler, load, load_build, Pipeline, Stage

View file

@ -1,118 +1,12 @@
import argparse
import json
import os
"""OSBuild Main
This specifies the entrypoint of the osbuild module when run as executable. For
compatibility we will continue to run the CLI.
"""
import sys
import osbuild
RESET = "\033[0m"
BOLD = "\033[1m"
RED = "\033[31m"
def mark_checkpoints(pipeline, checkpoints):
points = set(checkpoints)
def mark_stage(stage):
c = stage.id
if c in points:
stage.checkpoint = True
points.remove(c)
def mark_pipeline(pl):
for stage in pl.stages:
mark_stage(stage)
if pl.build:
mark_pipeline(pl.build)
mark_pipeline(pipeline)
return points
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
def main():
parser = argparse.ArgumentParser(description="Build operating system images")
parser.add_argument("manifest_path", metavar="MANIFEST",
help="json file containing the manifest that should be built, or a '-' to read from stdin")
parser.add_argument("--build-env", metavar="FILE", type=os.path.abspath,
help="json file containing a description of the build environment")
parser.add_argument("--store", metavar="DIRECTORY", type=os.path.abspath,
default=".osbuild",
help="directory where intermediary os trees are stored")
parser.add_argument("--sources", metavar="FILE", type=os.path.abspath,
help="json file containing a dictionary of source configuration")
parser.add_argument("--secrets", metavar="FILE", type=os.path.abspath,
help="json file containing a dictionary of secrets that are passed to sources")
parser.add_argument("-l", "--libdir", metavar="DIRECTORY", type=os.path.abspath,
help="the directory containing stages, assemblers, and the osbuild library")
parser.add_argument("--checkpoint", metavar="ID", action="append", type=str, default=None,
help="stage to commit to the object store during build (can be passed multiple times)")
parser.add_argument("--json", action="store_true",
help="output results in JSON format")
parser.add_argument("--output-directory", metavar="DIRECTORY", type=os.path.abspath,
help="directory where result objects are stored")
args = parser.parse_args()
if args.manifest_path == "-":
f = sys.stdin
else:
f = open(args.manifest_path)
manifest = json.load(f)
f.close()
pipeline = manifest.get("pipeline", {})
sources_options = manifest.get("sources", {})
if args.sources:
with open(args.sources) as f:
sources_options = json.load(f)
pipeline = osbuild.load(pipeline, sources_options)
if args.build_env:
with open(args.build_env) as f:
build_pipeline, runner = osbuild.load_build(json.load(f), sources_options)
pipeline.prepend_build_env(build_pipeline, runner)
secrets = {}
if args.secrets:
with open(args.secrets) as f:
secrets = json.load(f)
if args.checkpoint:
missed = mark_checkpoints(pipeline, args.checkpoint)
if missed:
for checkpoint in missed:
print(f"Checkpoint {BOLD}{checkpoint}{RESET} not found!")
print(f"{RESET}{BOLD}{RED}Failed{RESET}")
return 1
try:
r = pipeline.run(
args.store,
interactive=not args.json,
libdir=args.libdir,
secrets=secrets,
output_directory=args.output_directory
)
except KeyboardInterrupt:
print()
print(f"{RESET}{BOLD}{RED}Aborted{RESET}")
return 130
if args.json:
json.dump(r, sys.stdout)
sys.stdout.write("\n")
else:
if r["success"]:
print("tree id:", pipeline.tree_id)
print("output id:", pipeline.output_id)
else:
print()
print(f"{RESET}{BOLD}{RED}Failed{RESET}")
return 0 if r["success"] else 1
from osbuild.main_cli import main_cli as main
if __name__ == "__main__":

133
osbuild/main_cli.py Normal file
View file

@ -0,0 +1,133 @@
"""Entrypoints for osbuild
This module contains the application and API entrypoints of `osbuild`, the
command-line-interface to osbuild. The `osbuild_cli()` entrypoint can be safely
used from tests to run the cli.
"""
import argparse
import json
import os
import sys
import osbuild
RESET = "\033[0m"
BOLD = "\033[1m"
RED = "\033[31m"
def mark_checkpoints(pipeline, checkpoints):
points = set(checkpoints)
def mark_stage(stage):
c = stage.id
if c in points:
stage.checkpoint = True
points.remove(c)
def mark_pipeline(pl):
for stage in pl.stages:
mark_stage(stage)
if pl.build:
mark_pipeline(pl.build)
mark_pipeline(pipeline)
return points
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
def osbuild_cli(*, sys_argv=[]):
parser = argparse.ArgumentParser(description="Build operating system images")
parser.add_argument("manifest_path", metavar="MANIFEST",
help="json file containing the manifest that should be built, or a '-' to read from stdin")
parser.add_argument("--build-env", metavar="FILE", type=os.path.abspath,
help="json file containing a description of the build environment")
parser.add_argument("--store", metavar="DIRECTORY", type=os.path.abspath,
default=".osbuild",
help="directory where intermediary os trees are stored")
parser.add_argument("--sources", metavar="FILE", type=os.path.abspath,
help="json file containing a dictionary of source configuration")
parser.add_argument("--secrets", metavar="FILE", type=os.path.abspath,
help="json file containing a dictionary of secrets that are passed to sources")
parser.add_argument("-l", "--libdir", metavar="DIRECTORY", type=os.path.abspath,
help="the directory containing stages, assemblers, and the osbuild library")
parser.add_argument("--checkpoint", metavar="ID", action="append", type=str, default=None,
help="stage to commit to the object store during build (can be passed multiple times)")
parser.add_argument("--json", action="store_true",
help="output results in JSON format")
parser.add_argument("--output-directory", metavar="DIRECTORY", type=os.path.abspath,
help="directory where result objects are stored")
args = parser.parse_args(sys_argv[1:])
if args.manifest_path == "-":
f = sys.stdin
else:
f = open(args.manifest_path)
manifest = json.load(f)
f.close()
pipeline = manifest.get("pipeline", {})
sources_options = manifest.get("sources", {})
if args.sources:
with open(args.sources) as f:
sources_options = json.load(f)
pipeline = osbuild.load(pipeline, sources_options)
if args.build_env:
with open(args.build_env) as f:
build_pipeline, runner = osbuild.load_build(json.load(f), sources_options)
pipeline.prepend_build_env(build_pipeline, runner)
secrets = {}
if args.secrets:
with open(args.secrets) as f:
secrets = json.load(f)
if args.checkpoint:
missed = mark_checkpoints(pipeline, args.checkpoint)
if missed:
for checkpoint in missed:
print(f"Checkpoint {BOLD}{checkpoint}{RESET} not found!")
print(f"{RESET}{BOLD}{RED}Failed{RESET}")
return 1
try:
r = pipeline.run(
args.store,
interactive=not args.json,
libdir=args.libdir,
secrets=secrets,
output_directory=args.output_directory
)
except KeyboardInterrupt:
print()
print(f"{RESET}{BOLD}{RED}Aborted{RESET}")
return 130
if args.json:
json.dump(r, sys.stdout)
sys.stdout.write("\n")
else:
if r["success"]:
print("tree id:", pipeline.tree_id)
print("output id:", pipeline.output_id)
else:
print()
print(f"{RESET}{BOLD}{RED}Failed{RESET}")
return 0 if r["success"] else 1
def main_cli():
"""osbuild-cli entrypoint
This is the entrypoint used by the `osbuild` executable. We simply fetch the
global configuration and parameters necessary and invoke the API entrypoint.
"""
sys.exit(osbuild_cli(sys_argv=sys.argv))

View file

@ -7,6 +7,8 @@ setuptools.setup(
packages=["osbuild", "osbuild.util"],
license='Apache-2.0',
entry_points={
"console_scripts": ["osbuild = osbuild.__main__:main"]
"console_scripts": [
"osbuild = osbuild.main_cli:main_cli"
]
},
)