#!/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")

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
from pylorax.api.compose import test_templates
from pylorax.api.dnfbase import DNFLock, check_repos
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

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)

    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)

    # 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)
    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)

    # 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])

    # 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")

    # 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)

    # 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)

    # 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")

    # Make sure dnf directories are created
    make_dnf_dirs(server.config["COMPOSER_CFG"])

    # 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"))
        if not check_repos(server.config["DNFLOCK"].dbo):
            server.config["TEMPLATE_ERRORS"].append("Compose needs non-CDN repos enabled. Anaconda will fail to run.")

    # 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)

    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()
