lorax/src/sbin/lorax-composer

290 lines
10 KiB
Python
Executable File

#!/usr/bin/python3
#
# lorax-composer
#
# Copyright (C) 2017-2018 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
log = logging.getLogger("lorax-composer")
program_log = logging.getLogger("program")
pylorax_log = logging.getLogger("pylorax")
server_log = logging.getLogger("server")
dnf_log = logging.getLogger("dnf")
lifted_log = logging.getLogger("lifted")
import grp
import os
import pwd
import sys
import subprocess
import tempfile
from threading import Lock
from gevent import socket
from gevent.pywsgi import WSGIServer
from pylorax import vernum, log_selinux_state
from pylorax.api.cmdline import lorax_composer_parser
from pylorax.api.config import configure, make_dnf_dirs, make_queue_dirs, make_owned_dir
from pylorax.api.compose import test_templates
from pylorax.api.dnfbase import DNFLock
from pylorax.api.queue import start_queue_monitor
from pylorax.api.recipes import open_or_create_repo, commit_recipe_directory
from pylorax.api.server import server, GitLock
import lifted.config
from lifted.queue import start_upload_monitor
VERSION = "{0}-{1}".format(os.path.basename(sys.argv[0]), vernum)
def setup_logging(logfile):
# Setup logging to console and to logfile
log.setLevel(logging.DEBUG)
pylorax_log.setLevel(logging.DEBUG)
lifted_log.setLevel(logging.DEBUG)
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
fmt = logging.Formatter("%(asctime)s: %(message)s")
sh.setFormatter(fmt)
log.addHandler(sh)
pylorax_log.addHandler(sh)
lifted_log.addHandler(sh)
fh = logging.FileHandler(filename=logfile)
fh.setLevel(logging.DEBUG)
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
fh.setFormatter(fmt)
log.addHandler(fh)
pylorax_log.addHandler(fh)
lifted_log.addHandler(fh)
# External program output log
program_log.setLevel(logging.DEBUG)
logfile = os.path.abspath(os.path.dirname(logfile))+"/program.log"
fh = logging.FileHandler(filename=logfile)
fh.setLevel(logging.DEBUG)
fmt = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
fh.setFormatter(fmt)
program_log.addHandler(fh)
# Server request logging
server_log.setLevel(logging.DEBUG)
logfile = os.path.abspath(os.path.dirname(logfile))+"/server.log"
fh = logging.FileHandler(filename=logfile)
fh.setLevel(logging.DEBUG)
server_log.addHandler(fh)
# DNF logging
dnf_log.setLevel(logging.DEBUG)
logfile = os.path.abspath(os.path.dirname(logfile))+"/dnf.log"
fh = logging.FileHandler(filename=logfile)
fh.setLevel(logging.DEBUG)
fmt = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
fh.setFormatter(fmt)
dnf_log.addHandler(fh)
class LogWrapper(object):
"""Wrapper for the WSGIServer which only calls write()"""
def __init__(self, log_obj):
self.log = log_obj
def write(self, msg):
"""Log everything as INFO"""
self.log.info(msg.strip())
def make_pidfile(pid_path="/run/lorax-composer.pid"):
"""Check for a running instance of lorax-composer
:param pid_path: Path to the pid file
:type pid_path: str
:returns: False if there is already a running lorax-composer, True otherwise
:rtype: bool
This will look for an existing pid file, and if found read the PID and check to
see if it is really lorax-composer running, or if it is a stale pid.
It will create a new pid file if there isn't already one, or if the PID is stale.
"""
if os.path.exists(pid_path):
try:
pid = int(open(pid_path, "r").read())
cmdline = open("/proc/%s/cmdline" % pid, "r").read()
if "lorax-composer" in cmdline:
return False
except (IOError, ValueError):
pass
open(pid_path, "w").write(str(os.getpid()))
return True
if __name__ == '__main__':
# parse the arguments
opts = lorax_composer_parser().parse_args()
if opts.showver:
print(VERSION)
sys.exit(0)
tempfile.tempdir = opts.tmp
logpath = os.path.abspath(os.path.dirname(opts.logfile))
if not os.path.isdir(logpath):
os.makedirs(logpath)
setup_logging(opts.logfile)
log.debug("opts=%s", opts)
log_selinux_state()
if not make_pidfile():
log.error("PID file exists, lorax-composer already running. Quitting.")
sys.exit(1)
errors = []
# Check to make sure the user exists and get its uid
try:
uid = pwd.getpwnam(opts.user).pw_uid
except KeyError:
errors.append("Missing user '%s'" % opts.user)
# Check to make sure the group exists and get its gid
try:
gid = grp.getgrnam(opts.group).gr_gid
except KeyError:
errors.append("Missing group '%s'" % opts.group)
# No point in continuing if there are uid or gid errors
if errors:
for e in errors:
log.error(e)
sys.exit(1)
errors = []
# Check the socket path to make sure it exists, and that ownership and permissions are correct.
socket_dir = os.path.dirname(opts.socket)
if not os.path.exists(socket_dir):
# Create the directory and set permissions and ownership
os.makedirs(socket_dir, 0o750)
os.chown(socket_dir, 0, gid)
sockdir_stat = os.stat(socket_dir)
if sockdir_stat.st_mode & 0o007 != 0:
errors.append("Incorrect permissions on %s, no 'other' permissions are allowed." % socket_dir)
if sockdir_stat.st_gid != gid or sockdir_stat.st_uid != 0:
errors.append("%s should be owned by root:%s" % (socket_dir, opts.group))
# No point in continuing if there are ownership or permission errors
if errors:
for e in errors:
log.error(e)
sys.exit(1)
server.config["COMPOSER_CFG"] = configure(conf_file=opts.config)
server.config["COMPOSER_CFG"].set("composer", "tmp", opts.tmp)
# If the user passed in a releasever set it in the configuration
if opts.releasever:
server.config["COMPOSER_CFG"].set("composer", "releasever", opts.releasever)
# Override the default sharedir
if opts.sharedir:
server.config["COMPOSER_CFG"].set("composer", "share_dir", opts.sharedir)
# Override the config file's DNF proxy setting
if opts.proxy:
server.config["COMPOSER_CFG"].set("dnf", "proxy", opts.proxy)
# Override using system repos
if opts.no_system_repos:
server.config["COMPOSER_CFG"].set("repos", "use_system_repos", "0")
# Setup the lifted configuration settings
lifted.config.configure(server.config["COMPOSER_CFG"])
# Make sure the queue paths are setup correctly, exit on errors
errors = make_queue_dirs(server.config["COMPOSER_CFG"], gid)
if errors:
for e in errors:
log.error(e)
sys.exit(1)
# Make sure dnf directories are created (owned by user:group)
make_dnf_dirs(server.config["COMPOSER_CFG"], uid, gid)
# Make sure the git repo can be accessed by the API uid/gid
if os.path.exists(opts.BLUEPRINTS):
repodir_stat = os.stat(opts.BLUEPRINTS)
if repodir_stat.st_gid != gid or repodir_stat.st_uid != uid:
subprocess.call(["chown", "-R", "%s:%s" % (opts.user, opts.group), opts.BLUEPRINTS])
else:
make_owned_dir(opts.BLUEPRINTS, uid, gid)
# Did systemd pass any extra fds (for socket activation)?
try:
fds = int(os.environ['LISTEN_FDS'])
except (ValueError, KeyError):
fds = 0
if fds == 1:
# Inherit the fd passed by systemd
listener = socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
elif fds > 1:
log.error("lorax-composer only supports inheriting 1 fd from systemd.")
sys.exit(1)
else:
# Setup the Unix Domain Socket, remove old one, set ownership and permissions
if os.path.exists(opts.socket):
os.unlink(opts.socket)
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listener.bind(opts.socket)
os.chmod(opts.socket, 0o660)
os.chown(opts.socket, 0, gid)
listener.listen(socket.SOMAXCONN)
start_queue_monitor(server.config["COMPOSER_CFG"], uid, gid)
start_upload_monitor(server.config["COMPOSER_CFG"]["upload"])
# Change user and group on the main process. Note that this still happens even if
# --user and --group were passed in, but changing to the same user should be fine.
os.setgid(gid)
os.setuid(uid)
log.debug("user is now %s:%s", os.getresuid(), os.getresgid())
# Switch to a home directory we can access (libgit2 uses this to look for .gitconfig)
os.environ["HOME"] = server.config["COMPOSER_CFG"].get("composer", "lib_dir")
# Setup access to the git repo
server.config["REPO_DIR"] = opts.BLUEPRINTS
repo = open_or_create_repo(server.config["REPO_DIR"])
server.config["GITLOCK"] = GitLock(repo=repo, lock=Lock(), dir=opts.BLUEPRINTS)
# Import example blueprints
commit_recipe_directory(server.config["GITLOCK"].repo, "master", opts.BLUEPRINTS)
# Get a dnf.Base to share with the requests
try:
server.config["DNFLOCK"] = DNFLock(server.config["COMPOSER_CFG"])
except RuntimeError:
# Error has already been logged. Just exit cleanly.
sys.exit(1)
# Depsolve the templates and make a note of the failures for /api/status to report
with server.config["DNFLOCK"].lock:
server.config["TEMPLATE_ERRORS"] = test_templates(server.config["DNFLOCK"].dbo, server.config["COMPOSER_CFG"].get("composer", "share_dir"))
log.info("Starting %s on %s with blueprints from %s", VERSION, opts.socket, opts.BLUEPRINTS)
http_server = WSGIServer(listener, server, log=LogWrapper(server_log))
# The server writes directly to a file object, so point to our log directory
http_server.serve_forever()