#!/opt/imh-python/bin/python3.9
# vim: set ts=4 sw=4 expandtab syntax=python:
"""

ngxconf.cli
Command-line functions & CLI entry-point

Copyright (c) 2017-2020 InMotion Hosting, Inc.
http://www.inmotionhosting.com/

@author J. Hipps <jacobh@inmotionhosting.com>

"""

import sys
import os
import time
import logging
import logging.handlers
from argparse import ArgumentParser, Action

import yaml

from ngxconf import builder, fpm, control
from ngxconf.util import parse_user_default_conf, parse_profiles, get_profile, gconf, excepthook, NGXDumper
from ngxconf import __version__, __date__

logger = logging.getLogger('ngxconf')

class SafeStoreAction(Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        super(SafeStoreAction, self).__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        is_safe = False
        if 'SUDO_UID' in os.environ:
            if os.environ['SUDO_UID'] == '0':
                is_safe = True
            else:
                is_safe = False
        else:
            is_safe = True
        if is_safe:
            setattr(namespace, self.dest, values)
        else:
            print("ERROR: Attempted to set dangerous option as a non-root sudoer! Aborting.")
            sys.exit(249)


def setup_logging(clevel=logging.INFO, flevel=logging.DEBUG, logfile='/var/log/ngxconf.log'):
    """
    Setup logging
    """
    logger.setLevel(logging.DEBUG)

    # Console
    con = logging.StreamHandler()
    con.setLevel(clevel)
    con_format = logging.Formatter("%(levelname)s: %(message)s")
    con.setFormatter(con_format)
    logger.addHandler(con)

    # File
    try:
        flog = logging.handlers.WatchedFileHandler(logfile)
        flog.setLevel(flevel)
        flog_format = logging.Formatter("[%(asctime)s] %(name)s (%(process)d): %(levelname)s: %(message)s")
        flog.setFormatter(flog_format)
        logger.addHandler(flog)
    except Exception as e:
        logger.warning("Failed to open logfile: %s", str(e))

def parse_cli(show_help=False):
    """
    Parse CLI arguments
    """
    parser = ArgumentParser(description="Nginx user configuration builder for cPanel")
    parser.set_defaults(rebuildall=False, force=False, reload=False, user=None, outfile=None, defaults=False,
                        skipcheck=False, loglevel=logging.INFO, logfile='/var/log/ngxconf.log', nofpm=False,
                        nohttpd=False)

    parser.add_argument('--rebuildall', '-R', action='store_true',
                        help="Rebuild configuration for all users whose configuration has changed")
    parser.add_argument('--force', '-F', action='store_true',
                        help="Force rebuild even if the configuration has not changed")
    parser.add_argument('--reload', '-r', action='store_true',
                        help="Sends a SIGUSR2 signal to Nginx master process to reload the running config")
    parser.add_argument('--user', '-u', action='store', metavar="USER",
                        help="Specify user. Configuration will only be built for this user")
    parser.add_argument('--defaults', action='store_true',
                        help="Replace all of a user's config files with defaults")
    parser.add_argument('--skipcheck', action='store_true',
                        help="Skip Nginx config validation")
    parser.add_argument('--nofpm', action='store_true', help="Skip PHP-FPM rebuild and reload")
    parser.add_argument('--skipfpmbuild', action='store_true', help="Do not trigger /scripts/php_fpm_config --rebuild")
    parser.add_argument('--nohttpd', action='store_true', help="Skip Apache HTTPd reload")
    parser.add_argument('--skiphttpdbuild', action='store_true', help="Do not trigger /scripts/rebuildhttpdconf")
    parser.add_argument('--fork', action='store_true', help="Fork into the background; suppress all messages to stderr")
    parser.add_argument('--showconf', action='store_true', help="Output combined global configuration")
    parser.add_argument('--showprofile', action='store', metavar="PROF_ID",
                        help="Output combined profile by ID")
    parser.add_argument('--outfile', '-f', action='store', metavar="PATH",
                        help="Specify output file when building configuration for a single user")
    parser.add_argument('--logfile', '-l', action=SafeStoreAction, metavar="PATH",
                        help="Log output file")
    parser.add_argument('--debug', '-d', dest='loglevel', action='store_const', const=logging.DEBUG,
                        help="Enable debug output")
    parser.add_argument('--version', '-v', action='version', version="%s (%s)" % (__version__, __date__))

    if show_help:
        parser.print_help()
        sys.exit(1)

    return parser.parse_args()

def do_fork():
    """
    When --fork is used, ngxconf will fork into the background
    to complete its task
    """
    logger.info("Forking into background...")

    try:
        # first fork
        pid = os.fork()
    except Exception as e:
        logger.error("os.fork() failed, aborting: %s", str(e))
        sys.exit(251)

    if (pid == 0):
        # become parent of session & process group
        os.setsid()
        try:
            # second fork
            pid = os.fork()
        except Exception as e:
            logger.error("os.fork() [2] failed, aborting: %s", str(e))
            sys.exit(251)
        if pid:
            # ... and kill the other parent
            os._exit(0)

        logger.debug("Forked into background. PID: %d", os.getpid())

        try:
            # Redirect stdout & stderr to /dev/null
            sys.stdout.flush()
            sys.stdout = open(os.devnull, 'w')
            sys.stderr.flush()
            sys.stderr = open(os.devnull, 'w')

            # Redirect console logger to /dev/null
            logger.handlers[0].stream = open(os.devnull, 'w')
        except Exception as e:
            logger.error("Failed to redirect output streams: %s", str(e))

    else:
        # otherwise, kill the parent; _exit() so we don't mess with any
        # open file handles or streams
        logger.info("Background job running. Exiting main process (PID %d)", os.getpid())
        os._exit(0)

def _main():
    """
    Entry point
    """
    sys.excepthook = excepthook

    args = parse_cli()

    # NGX-37: suppress all console messages by default when using --fork
    #         to prevent cPanel programs from failing (since they try to parse
    #         JSON via stderr for some stupid reason)
    if args.fork and args.loglevel == logging.INFO:
        args.loglevel = logging.FATAL

    setup_logging(clevel=args.loglevel, flevel=logging.DEBUG, logfile=args.logfile)
    start_run = time.time()

    # Parse global config and additional user config files
    if gconf.apply_user_default_config:
        udefs = parse_user_default_conf()
    else:
        udefs = None
    parse_profiles()

    if args.showconf:
        print("---")
        print(yaml.dump(gconf._conf, Dumper=NGXDumper, default_flow_style=False))
        return 0
    elif args.showprofile:
        tprofile = get_profile(args.showprofile)
        if tprofile is not None:
            print("---")
            print(yaml.dump(tprofile, Dumper=NGXDumper, default_flow_style=False))
            return 0
        else:
            logger.error("Unable to locate profile: '%s'", args.showprofile)
            return 2

    # Check for pause file - skip rebuild if exists
    pause_file = '/var/ngxconf/conf_pause_rebuild'
    if os.path.exists(pause_file):
        logger.info("Pause file found at %s; skipping configuration rebuild", pause_file)
        return 0

    if args.user:
        if args.rebuildall:
            logger.error("Options --rebuildall and --user are mutually-exclusive")
            sys.exit(1)
        if args.fork:
            do_fork()
        changes = builder.rebuild_user(args.user, force=args.force, outfile=args.outfile,
                                       defaults=args.defaults, skip_fpm=args.nofpm, userdef=udefs)
    elif args.rebuildall:
        if args.defaults and not args.force:
            logger.error("CAUTION: Use of --defaults with --rebuildall will obliterate all user-defined settings "
                         "for all users! Use of --force is required. Aborting.")
            sys.exit(1)
        if args.fork:
            do_fork()
        changes = builder.rebuild_all(force=args.force, defaults=args.defaults, skip_fpm=args.nofpm, userdef=udefs)
    else:
        parse_cli(show_help=True)
        return 1

    # Check if services need to be reloaded
    retval = 0
    if not changes:
        retval = 2
    else:
        # Rebuild PHP-FPM config/pools if changes were made,
        # then rebuild/reload Apache
        if args.force or len(changes['fpm']) > 0:
            single_user = False if args.rebuildall else True
            if gconf.fpm_management == 'ngxconf':
                fpm.commit(args.force, single_user=args.user)
                if not args.skiphttpdbuild:
                    control.cp_rebuild_httpd_conf(reload=not args.nohttpd)
            elif gconf.fpm_management == 'cpanel' and not args.skipfpmbuild:
                control.rebuild_phpfpm()
                if args.reload:
                    control.restart_phpfpm()
                if not args.skiphttpdbuild:
                    control.cp_rebuild_httpd_conf(reload=not args.nohttpd)

        # Check/reload Nginx if changes were made
        if args.force or len(changes['nginx']) > 0:
            if not args.skipcheck:
                if not control.check_nginx():
                    retval = 3
            # Reload will only trigger if --reload is used
            if args.reload:
                control.reload_nginx()

    tot_run = time.time() - start_run
    logger.info("Finished run in %02.02f seconds", tot_run)
    return retval

if __name__ == '__main__':
    sys.exit(_main())
