Mini Shell
#!/opt/imh-python/bin/python3.9
# vim: set ts=4 sw=4 expandtab syntax=python:
"""
ngxutil.cli
Command-line functions & CLI entry-point
@author J. Hipps <jacobh@inmotionhosting.com>
"""
import os
import sys
import json
import logging
import logging.handlers
from urllib.parse import urlparse
from argparse import ArgumentParser, Action
import yaml
from ngxutil.util import format_size, excepthook
from ngxutil import report, cache, sendmail, api, info, profile
from ngxutil import __version__, __date__, email_from_addr
logger = logging.getLogger('ngxutil')
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)
class SafeConstAction(Action):
def __init__(self, option_strings, dest, const, default=None,
required=False, help=None, metavar=None):
super(SafeConstAction, self).__init__(option_strings=option_strings,
dest=dest, nargs=0, const=const, default=default, required=required,
help=help)
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, self.const)
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/ngxutil.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: %(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 status reporting and cache manipulation utility")
parser.set_defaults(user=None, domain=None, loglevel=logging.INFO, logfile="/var/log/ngxutil.log",
email=None, action='stats', outfmt='ansi', logdata=False, resolve=True,
span=None, lines=None, noflux=False, url=None, profile=None)
parser.add_argument('--user', '-u', action='store', metavar="USER",
help="Specify user")
parser.add_argument('--domain', '-d', action='store', metavar="DOMAIN",
help="Specify domain")
parser.add_argument('--email', '-E', action='store', metavar="ADDR",
help="Email report to specified address")
parser.add_argument('--profile', '-P', action='store', metavar="PROFILE",
help="Reset domain configuration to defaults using specified profile. Must also specify user (-u) and domain (-d).")
parser.add_argument('--purge', '-X', action='store_const', dest='action', const='purge',
help="Mode: Purge cache for specified user or page")
parser.add_argument('--purgeall', '-Z', action=SafeConstAction, dest='action', const='purgeall',
help="Mode: Purge ALL cached data, for all users")
parser.add_argument('--stats', '-S', action='store_const', dest='action', const='stats',
help="Mode: Show cache stats [default]")
parser.add_argument('--info', '-I', action='store_const', dest='action', const='info',
help="Mode: Show info for URL")
parser.add_argument('--span', '-t', action='store', type=int, help="Parse specified span from logs (in hours)")
parser.add_argument('--lines', '-z', action='store', type=int, metavar="X",
help="Parse X number of lines from log tail")
parser.add_argument('--logfile', '-l', action=SafeStoreAction, metavar="PATH",
help="Log output file")
parser.add_argument('--noflux', '-x', action='store_true', help="Do not try to fetch data from InfluxDB")
parser.add_argument('--debug', '-D', dest='loglevel', action='store_const', const=logging.DEBUG,
help="Enable debug output")
parser.add_argument('--logdata', action='store_true', help="Return logdata in JSON or YAML output")
parser.add_argument('--json', '-J', dest='outfmt', action='store_const', const='json',
help="Output format: JSON")
parser.add_argument('--yaml', '-Y', dest='outfmt', action='store_const', const='yaml',
help="Output format: YAML")
parser.add_argument('--html', '-H', dest='outfmt', action='store_const', const='html',
help="Output format: HTML")
parser.add_argument('--version', '-v', action='version', version="%s (%s)" % (__version__, __date__))
parser.add_argument('url', nargs='?', metavar="URL_OR_LOGFILE",
help="Target URL when using --info or --purge modes; logfile when using report modes")
if show_help:
parser.print_help()
sys.exit(1)
return parser.parse_args()
def _main():
"""Entry point"""
sys.excepthook = excepthook
args = parse_cli()
setup_logging(clevel=args.loglevel, flevel=logging.DEBUG, logfile=args.logfile)
if args.profile and args.user and args.domain:
profile.set_domain_profile(args.user, args.domain, args.profile)
elif args.action == 'purge':
if args.user and not args.url:
cache.purge_cache_zone(args.user)
else:
if args.url:
if urlparse(args.url).scheme == 'all':
if args.user:
cache.purge_url_all(args.url, args.user)
else:
logger.error("Must specify --user when using all://")
parse_cli(show_help=True)
else:
cache.purge_url(args.url)
else:
logger.error("Must specify --user or url with --purge action")
parse_cli(show_help=True)
elif args.action == 'purgeall':
cache.purge_full_cache()
elif args.action == 'info':
if args.url:
info.check_url(args.url)
else:
logger.error("Must specify URL when using --info")
parse_cli(show_help=True)
elif args.action == 'stats':
infile = os.path.expanduser(args.url) if args.url else '/var/log/nginx/access.log'
if args.outfmt in ('ansi', 'html'):
if args.domain:
vstat = report.repgen_domain(args.domain, span=args.span, noflux=args.noflux, tail=args.lines, infile=infile)
elif args.user:
logger.error("Must specify --domain with --stats action")
parse_cli(show_help=True)
else:
vstat = report.repgen_server(span=args.span, noflux=args.noflux, tail=args.lines, infile=infile)
if vstat:
if args.outfmt == 'html':
print(vstat[1])
else:
print(vstat[0])
if args.email:
sendmail.email_report(args.email, email_from_addr, "Cache Status Report", *vstat)
else:
if args.domain:
vstat = api.get_domain_info(args.domain, include_logdata=args.logdata, noflux=args.noflux, span=args.span, tail=args.lines, infile=infile)
else:
vstat = api.get_server_info(include_logdata=args.logdata, noflux=args.noflux, span=args.span, tail=args.lines, infile=infile)
if args.outfmt == 'json':
outstr = json.dumps(vstat, sort_keys=True, indent=4, separators=(',', ': '))
else:
outstr = yaml.dump(vstat, Dumper=yaml.SafeDumper, default_flow_style=False)
sys.stdout.write(outstr)
sys.stdout.flush()
else:
parse_cli(show_help=True)
if __name__ == '__main__':
_main()
Zerion Mini Shell 1.0