Mini Shell
#!/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 yaml import CDumper, CLoader
from ngxconf import builder, fpm, control
from ngxconf.util import parse_user_default_conf, parse_profiles, get_profile, gconf, excepthook
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=CDumper, 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=CDumper, default_flow_style=False))
return 0
else:
logger.error("Unable to locate profile: '%s'", args.showprofile)
return 2
elif 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())
Zerion Mini Shell 1.0