#!/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 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.config import configure, make_yum_dirs, make_queue_dirs
from pylorax.api.compose import test_templates
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
from pylorax.api.yumbase import YumLock

# pylint has trouble with gevent's socket module, doesn't recognize some of its members
# pylint: disable=no-member

VERSION = "{0}-{1}".format(os.path.basename(sys.argv[0]), vernum)

DEFAULT_USER  = "root"
DEFAULT_GROUP = "weldr"

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=DEFAULT_USER, metavar="USER",
                        help="User to use for reduced permissions")
    parser.add_argument("--group", default=DEFAULT_GROUP, 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("--tmp", default="/var/tmp",
                        help="Top level temporary directory")
    parser.add_argument("--proxy", default=None, metavar="PROXY",
                        help="Set proxy for DNF, overrides configuration file setting.")
    parser.add_argument("--no-system-repos", action="store_true", default=False,
                        help="Do not copy over system repos from /etc/yum.repos.d/ at startup")
    parser.add_argument("BLUEPRINTS", metavar="BLUEPRINTS",
                        help="Path to the blueprints")

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

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 = get_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)

    if not os.path.isdir(opts.BLUEPRINTS):
        errors.append("Missing blueprints directory: %s" % opts.BLUEPRINTS)

    # 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("yum", "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 yumbase directories are created
    make_yum_dirs(server.config["COMPOSER_CFG"])

    # Get a YumBase to share with the requests
    try:
        server.config["YUMLOCK"] = YumLock(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["YUMLOCK"].lock:
        server.config["TEMPLATE_ERRORS"] = test_templates(server.config["YUMLOCK"].yb, server.config["COMPOSER_CFG"].get("composer", "share_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)

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