entrypoint.py ------------- Allow the user to set the port number also for the remote worker Allow the user to set the binding address for the composer api or remote worker api Set the default port of the composer API to 8080 Dockerfile-ubi -------------- Remove setting the port for the composer API since the default is already 8080
387 lines
13 KiB
Python
387 lines
13 KiB
Python
"""entrypoint - Containerized OSBuild Composer
|
|
|
|
This provides the entrypoint for a containerized osbuild-composer image. It
|
|
spawns `osbuild-composer` on start and manages it until it exits. The main
|
|
purpose of this entrypoint is to prepare everything to be usable from within
|
|
a container.
|
|
"""
|
|
|
|
import argparse
|
|
import contextlib
|
|
import os
|
|
import pathlib
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
class Cli(contextlib.AbstractContextManager):
|
|
"""Command Line Interface"""
|
|
|
|
def __init__(self, argv):
|
|
self.args = None
|
|
self._argv = argv
|
|
self._exitstack = None
|
|
self._parser = None
|
|
|
|
def _parse_args(self):
|
|
self._parser = argparse.ArgumentParser(
|
|
add_help=True,
|
|
allow_abbrev=False,
|
|
argument_default=None,
|
|
description="Containerized OSBuild Composer",
|
|
prog="container/osbuild-composer",
|
|
)
|
|
|
|
# --[no-]composer-api
|
|
self._parser.add_argument(
|
|
"--composer-api",
|
|
action="store_true",
|
|
dest="composer_api",
|
|
help="Enable the composer-API",
|
|
)
|
|
self._parser.add_argument(
|
|
"--no-composer-api",
|
|
action="store_false",
|
|
dest="composer_api",
|
|
help="Disable the composer-API",
|
|
)
|
|
self._parser.add_argument(
|
|
"--composer-api-port",
|
|
type=int,
|
|
default=8080,
|
|
dest="composer_api_port",
|
|
help="Port which the composer-API listens on",
|
|
)
|
|
self._parser.add_argument(
|
|
"--composer-api-bind-address",
|
|
type=str,
|
|
default="::",
|
|
dest="composer_api_bind_address",
|
|
help="Bind the composer API to the specified address",
|
|
)
|
|
|
|
# --[no-]local-worker-api
|
|
self._parser.add_argument(
|
|
"--local-worker-api",
|
|
action="store_true",
|
|
dest="local_worker_api",
|
|
help="Enable the local-worker-API",
|
|
)
|
|
self._parser.add_argument(
|
|
"--no-local-worker-api",
|
|
action="store_false",
|
|
dest="local_worker_api",
|
|
help="Disable the local-worker-API",
|
|
)
|
|
|
|
# --[no-]remote-worker-api
|
|
self._parser.add_argument(
|
|
"--remote-worker-api",
|
|
action="store_true",
|
|
dest="remote_worker_api",
|
|
help="Enable the remote-worker-API",
|
|
)
|
|
self._parser.add_argument(
|
|
"--no-remote-worker-api",
|
|
action="store_false",
|
|
dest="remote_worker_api",
|
|
help="Disable the remote-worker-API",
|
|
)
|
|
self._parser.add_argument(
|
|
"--remote-worker-api-port",
|
|
type=int,
|
|
default=8700,
|
|
dest="remote_worker_api_port",
|
|
help="Port which the remote-worker API listens on",
|
|
)
|
|
self._parser.add_argument(
|
|
"--remote-worker-api-bind-address",
|
|
type=str,
|
|
default="::",
|
|
dest="remote_worker_api_bind_address",
|
|
help="Bind the remote worker API to the specified address",
|
|
)
|
|
|
|
# --[no-]weldr-api
|
|
self._parser.add_argument(
|
|
"--weldr-api",
|
|
action="store_true",
|
|
dest="weldr_api",
|
|
help="Enable the weldr-API",
|
|
)
|
|
self._parser.add_argument(
|
|
"--no-weldr-api",
|
|
action="store_false",
|
|
dest="weldr_api",
|
|
help="Disable the weldr-API",
|
|
)
|
|
|
|
# --[no-]dnf-json
|
|
self._parser.add_argument(
|
|
"--dnf-json",
|
|
action="store_true",
|
|
dest="dnf_json",
|
|
help="Enable dnf-json",
|
|
)
|
|
self._parser.add_argument(
|
|
"--no-dnf-json",
|
|
action="store_false",
|
|
dest="dnf_json",
|
|
help="Disable dnf-json",
|
|
)
|
|
self._parser.add_argument(
|
|
"--dnf-json-port",
|
|
type=int,
|
|
default=0,
|
|
dest="dnf_json_port",
|
|
help="Specify the port dnf-json should listen on",
|
|
)
|
|
|
|
self._parser.set_defaults(
|
|
builtin_worker=False,
|
|
composer_api=False,
|
|
local_worker_api=False,
|
|
remote_worker_api=False,
|
|
weldr_api=False,
|
|
dnf_json=False,
|
|
)
|
|
|
|
return self._parser.parse_args(self._argv[1:])
|
|
|
|
def __enter__(self):
|
|
self._exitstack = contextlib.ExitStack()
|
|
self.args = self._parse_args()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
self._exitstack.close()
|
|
self._exitstack = None
|
|
|
|
def _prepare_sockets(self):
|
|
# Prepare all the API sockets that osbuild-composer expectes, and make
|
|
# sure to pass them according to the systemd socket-activation API.
|
|
#
|
|
# Note that we rely on this being called early, so we get the correct
|
|
# FD numbers assigned. We need FD-#3 onwards for compatibility with
|
|
# socket activation (because python `subprocess.Popen` does not support
|
|
# renumbering the sockets we pass down).
|
|
|
|
index = 3
|
|
sockets = []
|
|
names = []
|
|
|
|
# osbuild-composer.socket
|
|
if self.args.weldr_api:
|
|
print("Create weldr-api socket", file=sys.stderr)
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
self._exitstack.enter_context(contextlib.closing(sock))
|
|
sock.bind("/run/weldr/api.socket")
|
|
sock.listen()
|
|
sockets.append(sock)
|
|
names.append("osbuild-composer.socket")
|
|
|
|
assert(sock.fileno() == index)
|
|
index += 1
|
|
|
|
# osbuild-composer-api.socket
|
|
if self.args.composer_api:
|
|
print("Create composer-api socket on port {}".format(self.args.composer_api_port) , file=sys.stderr)
|
|
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
self._exitstack.enter_context(contextlib.closing(sock))
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
sock.bind((self.args.composer_api_bind_address, self.args.composer_api_port))
|
|
sock.listen()
|
|
sockets.append(sock)
|
|
names.append("osbuild-composer-api.socket")
|
|
|
|
assert(sock.fileno() == index)
|
|
index += 1
|
|
|
|
# osbuild-local-worker.socket
|
|
if self.args.local_worker_api:
|
|
print("Create local-worker-api socket", file=sys.stderr)
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
self._exitstack.enter_context(contextlib.closing(sock))
|
|
sock.bind("/run/osbuild-composer/job.socket")
|
|
sock.listen()
|
|
sockets.append(sock)
|
|
names.append("osbuild-local-worker.socket")
|
|
|
|
assert(sock.fileno() == index)
|
|
index += 1
|
|
|
|
# osbuild-remote-worker.socket
|
|
if self.args.remote_worker_api:
|
|
print(f"Create remote-worker-api socket on address [{self.args.remote_worker_api_bind_address}]:{self.args.remote_worker_api_port}", file=sys.stderr)
|
|
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
self._exitstack.enter_context(contextlib.closing(sock))
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
sock.bind((self.args.remote_worker_api_bind_address, self.args.remote_worker_api_port))
|
|
sock.listen(256)
|
|
sockets.append(sock)
|
|
names.append("osbuild-remote-worker.socket")
|
|
|
|
assert(sock.fileno() == index)
|
|
index += 1
|
|
|
|
# Prepare FD environment for the child process.
|
|
os.environ["LISTEN_FDS"] = str(len(sockets))
|
|
os.environ["LISTEN_FDNAMES"] = ":".join(names)
|
|
|
|
return sockets
|
|
|
|
@staticmethod
|
|
def _spawn_worker():
|
|
cmd = [
|
|
"/usr/libexec/osbuild-composer/osbuild-worker",
|
|
"-unix",
|
|
"/run/osbuild-composer/job.socket",
|
|
]
|
|
|
|
env = os.environ.copy()
|
|
env["CACHE_DIRECTORY"] = "/var/cache/osbuild-worker"
|
|
env["STATE_DIRECTORY"] = "/var/lib/osbuild-worker"
|
|
|
|
return subprocess.Popen(
|
|
cmd,
|
|
cwd="/",
|
|
env=env,
|
|
stdin=subprocess.DEVNULL,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
|
|
@staticmethod
|
|
def _spawn_composer(sockets):
|
|
cmd = [
|
|
"/usr/libexec/osbuild-composer/osbuild-composer",
|
|
"-verbose",
|
|
]
|
|
|
|
# Prepare the environment for osbuild-composer. Note that we cannot use
|
|
# the `env` parameter of `subprocess.Popen()`, because it conflicts
|
|
# with the `preexec_fn=` parameter. Therefore, we have to modify the
|
|
# caller's environment.
|
|
os.environ["CACHE_DIRECTORY"] = "/var/cache/osbuild-composer"
|
|
os.environ["STATE_DIRECTORY"] = "/var/lib/osbuild-composer"
|
|
|
|
# We need to set `LISTEN_PID=` to the target PID. The only way python
|
|
# allows us to do this is to hook into `preexec_fn=`, which is executed
|
|
# by `subprocess.Popen()` after forking, but before executing the new
|
|
# executable.
|
|
preexec_setenv = lambda: os.putenv("LISTEN_PID", str(os.getpid()))
|
|
|
|
return subprocess.Popen(
|
|
cmd,
|
|
cwd="/usr/libexec/osbuild-composer",
|
|
stdin=subprocess.DEVNULL,
|
|
stderr=subprocess.STDOUT,
|
|
pass_fds=[sock.fileno() for sock in sockets],
|
|
preexec_fn=preexec_setenv,
|
|
)
|
|
|
|
def _spawn_dnf_json(self):
|
|
cmd = [
|
|
"/usr/libexec/osbuild-composer/dnf-json",
|
|
]
|
|
|
|
if self.args.dnf_json_port:
|
|
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
self._exitstack.enter_context(contextlib.closing(sock))
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
sock.bind(("::", self.args.dnf_json_port))
|
|
sock.listen()
|
|
else:
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
self._exitstack.enter_context(contextlib.closing(sock))
|
|
os.makedirs("/run/osbuild-dnf-json/", exist_ok=True)
|
|
sock.bind("/run/osbuild-dnf-json/api.sock")
|
|
sock.listen()
|
|
|
|
dnfenv = os.environ.copy()
|
|
dnfenv["LISTEN_FDS"] = "1"
|
|
dnfenv["LISTEN_FD"] = str(sock.fileno())
|
|
|
|
return subprocess.Popen(
|
|
cmd,
|
|
cwd="/usr/libexec/osbuild-composer",
|
|
stdin=subprocess.DEVNULL,
|
|
stderr=subprocess.STDOUT,
|
|
env=dnfenv,
|
|
pass_fds=[sock.fileno()]
|
|
)
|
|
|
|
def run(self):
|
|
"""Program Runtime"""
|
|
|
|
proc_composer = None
|
|
proc_worker = None
|
|
proc_dnf_json = None
|
|
res = 0
|
|
sockets = self._prepare_sockets()
|
|
|
|
def handler(signum, frame):
|
|
proc_composer.terminate()
|
|
proc_worker.terminate()
|
|
proc_dnf_json.terminate()
|
|
|
|
signal.signal(signal.SIGTERM, handler)
|
|
|
|
liveness = pathlib.Path('/tmp/osbuild-composer-live')
|
|
|
|
liveness.touch()
|
|
|
|
try:
|
|
if self.args.builtin_worker:
|
|
proc_worker = self._spawn_worker()
|
|
|
|
if self.args.dnf_json:
|
|
proc_dnf_json = self._spawn_dnf_json()
|
|
|
|
if any([self.args.weldr_api, self.args.composer_api, self.args.local_worker_api, self.args.remote_worker_api]):
|
|
proc_composer = self._spawn_composer(sockets)
|
|
|
|
if proc_composer:
|
|
res = proc_composer.wait()
|
|
|
|
if proc_worker:
|
|
if proc_composer:
|
|
proc_worker.terminate()
|
|
proc_worker.wait()
|
|
|
|
if proc_dnf_json:
|
|
if proc_composer:
|
|
proc_dnf_json.terminate()
|
|
proc_dnf_json.wait()
|
|
|
|
except KeyboardInterrupt:
|
|
if proc_composer:
|
|
proc_composer.terminate()
|
|
res = proc_composer.wait()
|
|
if proc_worker:
|
|
proc_worker.terminate()
|
|
proc_worker.wait()
|
|
if proc_dnf_json:
|
|
proc_dnf_json.terminate()
|
|
proc_dnf_json.wait()
|
|
except:
|
|
if proc_worker:
|
|
proc_worker.kill()
|
|
if proc_dnf_json:
|
|
proc_dnf_json.kill()
|
|
if proc_composer:
|
|
proc_composer.kill()
|
|
raise
|
|
finally:
|
|
liveness.unlink()
|
|
|
|
return res
|
|
|
|
|
|
if __name__ == "__main__":
|
|
with Cli(sys.argv) as global_main:
|
|
sys.exit(global_main.run())
|