lorax/src/sbin/lorax-composer
Brian C. Lane bb3d6b1003 Change config and paths
With the API running as weldr the permissions on the yum directories
needs to be accessable to it, as well as the results and queue/new
directories.
2018-01-26 16:53:12 -08:00

251 lines
9.3 KiB
Python
Executable File

#!/usr/bin/python
#
# lorax-composer
#
# Copyright (C) 2017 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")
yum_log = logging.getLogger("yum")
import argparse
import grp
import multiprocessing as mp
import os
import pwd
import sys
from threading import Lock
from gevent import socket
from gevent.wsgi import WSGIServer
from pyanaconda.queue import QueueFactory
from pylorax import vernum
from pylorax.api.config import configure
from pylorax.api.queue import monitor
from pylorax.api.recipes import open_or_create_repo, commit_recipe_directory
from pylorax.api.server import server, GitLock, YumLock
from pylorax.api.yumbase import get_base_object
from pylorax.base import DataHolder
from pylorax.sysutils import joinpaths
VERSION = "{0}-{1}".format(os.path.basename(sys.argv[0]), vernum)
def get_parser():
""" Return the ArgumentParser for lorax-composer"""
parser = argparse.ArgumentParser(description="Lorax Composer API Server",
fromfile_prefix_chars="@")
parser.add_argument("--socket", default="/run/weldr/api.socket", metavar="SOCKET",
help="Path to the socket file to listen on")
parser.add_argument("--user", default="weldr", metavar="USER",
help="User to use for reduced permissions")
parser.add_argument("--group", default="weldr", metavar="GROUP",
help="Group to set ownership of the socket to")
parser.add_argument("--log", dest="logfile", default="/var/log/lorax-composer/composer.log", metavar="LOG",
help="Path to logfile (/var/log/lorax-composer/composer.log)")
parser.add_argument("--mockfiles", default="/var/tmp/bdcs-mockfiles/", metavar="MOCKFILES",
help="Path to JSON files used for /api/mock/ paths (/var/tmp/bdcs-mockfiles/)")
parser.add_argument("--sharedir", type=os.path.abspath, metavar="SHAREDIR",
help="Directory containing all the templates. Overrides config file sharedir")
parser.add_argument("-V", action="store_true", dest="showver",
help="show program's version number and exit")
parser.add_argument("-c", "--config", default="/etc/lorax/composer.conf", metavar="CONFIG",
help="Path to lorax-composer configuration file.")
parser.add_argument( "--releasever", default=None, metavar="STRING",
help="Release version to use for $releasever in yum repository urls" )
parser.add_argument("RECIPES", metavar="RECIPES",
help="Path to the recipes")
return parser
def setup_logging(logfile):
# Setup logging to console and to logfile
log.setLevel(logging.DEBUG)
pylorax_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)
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)
# 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)
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)
# YUM logging
yum_log.setLevel(logging.DEBUG)
logfile = os.path.abspath(os.path.dirname(logfile))+"/yum.log"
fh = logging.FileHandler(filename=logfile)
fh.setLevel(logging.DEBUG)
yum_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())
if __name__ == '__main__':
# parse the arguments
opts = get_parser().parse_args()
if opts.showver:
print(VERSION)
sys.exit(0)
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)
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)
# 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))
if not os.path.isdir(opts.RECIPES):
log.warn("Creating empty recipe directory at %s", opts.RECIPES)
os.makedirs(opts.RECIPES)
if errors:
for e in errors:
log.error(e)
sys.exit(1)
server.config["REPO_DIR"] = opts.RECIPES
repo = open_or_create_repo(server.config["REPO_DIR"])
server.config["GITLOCK"] = GitLock(repo=repo, lock=Lock(), dir=opts.RECIPES)
server.config["COMPOSER_CFG"] = configure(conf_file=opts.config)
# 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)
# Make sure the queue paths are setup correctly
lib_dir = server.config["COMPOSER_CFG"].get("composer", "lib_dir")
for p in ["queue/run", "queue/new", "results"]:
p_dir = joinpaths(lib_dir, p)
if not os.path.exists(p_dir):
log.info("%s does not exist, creating it.", p_dir)
orig_umask = os.umask(0)
os.makedirs(p_dir, 0o770)
os.chown(p_dir, 0, gid)
os.umask(orig_umask)
else:
p_stat = os.stat(p_dir)
if p_stat.st_mode & 0o007 != 0:
errors.append("Incorrect permissions on %s, no 'other' permissions are allowed." % p_dir)
if p_stat.st_gid != gid or p_stat.st_uid != 0:
errors.append("%s should be owned by root:%s" % (p_dir, opts.group))
# 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(1)
# Start queue monitor thread as root
cancel_q = QueueFactory("cancel")
cancel_q.addMessage("cancel", 0)
cfg = DataHolder(composer_dir=lib_dir, uid=uid, gid=gid)
p = mp.Process(target=monitor, args=(cfg, cancel_q))
p.daemon = True
p.start()
# Drop root privileges on the main process
os.setgid(gid)
os.setuid(uid)
log.debug("user is now %s:%s", os.getresuid(), os.getresgid())
# Make sure yumbase directories are created
for p in ["yum_conf", "repo_dir", "cache_dir"]:
p_dir = os.path.dirname(server.config["COMPOSER_CFG"].get("composer", p))
if not os.path.exists(p_dir):
os.makedirs(p_dir)
# Get a YumBase to share with the requests
yb = get_base_object(server.config["COMPOSER_CFG"])
server.config["YUMLOCK"] = YumLock(yb=yb, lock=Lock())
# Import example recipes
commit_recipe_directory(server.config["GITLOCK"].repo, "master", opts.RECIPES)
log.info("Starting %s on %s with recipes from %s", VERSION, opts.socket, opts.RECIPES)
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()