main.py 6.74 KB
#!/usr/bin/env python3

# python standard library
import argparse
import logging
from os import environ, path
import ssl
import sys

# this project
from .learnapp import LearnApp, DatabaseUnusableError
from .serve import run_webserver
from .tools import load_yaml
from . import APP_NAME, APP_VERSION


# ----------------------------------------------------------------------------
def parse_cmdline_arguments():
    argparser = argparse.ArgumentParser(
        description='Server for online learning. Students and topics '
        'have to be previously configured. Please read the documentation '
        'included with this software before running the server.'
        )

    argparser.add_argument(
        'conffile', type=str, nargs='*',
        help='Topics configuration file in YAML format.'
        )

    argparser.add_argument(
        '--prefix', type=str, default='.',
        help='Path where the topic directories can be found (default: .)'
        )

    argparser.add_argument(
        '--port', type=int, default=8443,
        help='Port to be used by the HTTPS server (default: 8443)'
        )

    argparser.add_argument(
        '--db', type=str, default='students.db',
        help='SQLite3 database file (default: students.db)'
        )

    argparser.add_argument(
        '--check', action='store_true',
        help='Sanity check questions (can take awhile)'
        )

    argparser.add_argument(
        '--debug', action='store_true',
        help='Enable debug mode'
        )

    argparser.add_argument(
        '--version', action='store_true',
        help='Print version information'
        )

    return argparser.parse_args()


# ----------------------------------------------------------------------------
def get_logger_config(debug=False):
    if debug:
        filename, level = 'logger-debug.yaml', 'DEBUG'
    else:
        filename, level = 'logger.yaml', 'INFO'

    config_dir = environ.get('XDG_CONFIG_HOME', '~/.config/')
    config_file = path.join(path.expanduser(config_dir), APP_NAME, filename)

    default_config = {
        'version': 1,
        'formatters': {
            'standard': {
                'format': '%(asctime)s | %(levelname)-8s | %(message)s',
                'datefmt': '%Y-%m-%d %H:%M:%S',
                },
            },
        'handlers': {
            'default': {
                'level': level,
                'class': 'logging.StreamHandler',
                'formatter': 'standard',
                'stream': 'ext://sys.stdout',
                },
            },
        'loggers': {
            '': {  # configuration for serve.py
                'handlers': ['default'],
                'level': level,
                },
            },
        }
    default_config['loggers'].update({
        APP_NAME+'.'+module: {
            'handlers': ['default'],
            'level': level,
            'propagate': False,
            } for module in ['learnapp', 'models', 'factory', 'tools', 'serve',
                             'questions', 'student']})

    return load_yaml(config_file, default=default_config)


# ----------------------------------------------------------------------------
# Start application and webserver
# ----------------------------------------------------------------------------
def main():
    # --- Commandline argument parsing
    arg = parse_cmdline_arguments()

    if arg.version:
        print(f'{APP_NAME} - {APP_VERSION}\nPython {sys.version}')
        sys.exit(0)

    # --- Setup logging
    logger_config = get_logger_config(arg.debug)
    logging.config.dictConfig(logger_config)

    try:
        logging.config.dictConfig(logger_config)
    except Exception:
        print('An error ocurred while setting up the logging system.')
        sys.exit(1)

    logging.info('====================== Start Logging ======================')

    # --- get SSL certificates
    if 'XDG_DATA_HOME' in environ:
        certs_dir = path.join(environ['XDG_DATA_HOME'], 'certs')
    else:
        certs_dir = path.expanduser('~/.local/share/certs')

    ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    try:
        ssl_ctx.load_cert_chain(path.join(certs_dir, 'cert.pem'),
                                path.join(certs_dir, 'privkey.pem'))
    except FileNotFoundError:
        logging.critical(f'SSL certificates missing in {certs_dir}')
        print('--------------------------------------------------------------')
        print('Certificates should be issued by a certificate authority (CA),')
        print('such as https://letsencrypt.org.                              ')
        print('For testing purposes a selfsigned certificate can be generated')
        print('locally by running:                                           ')
        print('                                                              ')
        print('  openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\   ')
        print('          -out cert.pem -days 365 -nodes                      ')
        print('                                                              ')
        print('Copy the cert.pem and privkey.pem files to:                   ')
        print('                                                              ')
        print(f'  {certs_dir:<62}')
        print('                                                              ')
        print('(See README.md for more information)                          ')
        print('--------------------------------------------------------------')
        sys.exit(1)
    else:
        logging.info('SSL certificates loaded')

    # --- start application
    try:
        learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,
                            check=arg.check)
    except DatabaseUnusableError:
        logging.critical('Failed to start application.')
        print('--------------------------------------------------------------')
        print('Could not find a usable database. Use one of the follwing     ')
        print('commands to initialize:                                       ')
        print('                                                              ')
        print('  initdb-aprendizations --admin            # add admin        ')
        print('  initdb-aprendizations -a 86 "Max Smart"  # add student      ')
        print('  initdb-aprendizations students.csv       # add many students')
        print('--------------------------------------------------------------')
        sys.exit(1)
    except Exception:
        logging.critical('Failed to start application.')
        sys.exit(1)
    else:
        logging.info('Backend application started')

    # --- run webserver forever
    run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug)


# ----------------------------------------------------------------------------
if __name__ == "__main__":
    main()