Refactor entrypoint to use click.

This commit is contained in:
sfigato 2019-04-15 18:27:22 +02:00
parent bec6123d47
commit 80fb51f7de
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
4 changed files with 309 additions and 35 deletions

View File

@ -1,4 +1,4 @@
from setuptools import setup from setuptools import setup, find_packages
setup( setup(
@ -13,10 +13,10 @@ setup(
author_email='unit@paranoici.org', author_email='unit@paranoici.org',
package_dir={'': 'src'}, package_dir={'': 'src'},
packages=['phi', 'phi.api', 'phi.ldap'], packages=find_packages(),
scripts=['src/phid'], entry_points={"console_scripts": ["phid=phi.app:cli"]},
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
install_requires=['aiohttp==2.3.8', 'pyYAML', 'ldap3'], install_requires=['aiohttp==2.3.8', 'click==7.0', 'pyYAML', 'ldap3'],
tests_require=['pytest', 'pytest-aiohttp'] tests_require=['pytest', 'pytest-aiohttp']
) )

View File

@ -1,22 +1,214 @@
# -*- encoding: utf-8 -*-
import pkg_resources
from asyncio import get_event_loop from asyncio import get_event_loop
from aiohttp import web from aiohttp import web
import click
from pprint import pformat as pp
import yaml
from phi.config import get_config, merge_config
from phi.logging import setup_logging, get_logger
from phi.api.app import api_app from phi.api.app import api_app
log = get_logger(__name__)
def setup_app(config): def setup_app(config):
loop = get_event_loop() loop = get_event_loop()
app = web.Application(loop=loop) app = web.Application(loop=loop)
app['config'] = config app["config"] = config
api = api_app(config) api = api_app(config)
app.add_subapp('/api', api) app.add_subapp("/api", api)
return app return app
def run_app(app): def run_app(app):
web.run_app(app, web.run_app(
host=app['config']['core']['listen'].get('host', '127.0.0.1'), app,
port=app['config']['core']['listen'].get('port', '8080')) host=app["config"]["core"]["listen"].get("host", "127.0.0.1"),
port=app["config"]["core"]["listen"].get("port", "8080"),
)
def _validate_port(ctx, param, value):
"""
Callback to validate provided port.
"""
if 0 < value or value > 65535:
raise click.BadParameter(
"Provided value is not in the range 0-65535: {} [type: {}]".format(
value, type(value)
)
)
return value
@click.command(help="phid is the main application daemon.")
@click.option(
"--config",
"config_path",
type=click.Path(exists=True),
help="Path to a valid config file.",
)
@click.option(
"-H",
"--host",
type=click.STRING,
default="localhost",
help='Address to which the application bounds. Defaults to "localhost".',
)
@click.option(
"-p",
"--port",
type=click.INT,
default=8080,
help="Port to which the application bounds. Defaults to 8080.",
)
@click.option(
"--ldap-host",
"ldap_host",
type=click.STRING,
default="localhost",
help='Address of the LDAP server to connect to. Defaults to "localhost".',
)
@click.option(
"--ldap-port",
"ldap_port",
type=click.INT,
default=389,
help="Port where is exposed the LDAP server to connect to. Defaults to 389.",
)
@click.option(
"--ldap-crypt",
"ldap_crypt",
is_flag=True,
default=True,
help="Connect to the LDAP server using TLSv1.2. Defaults to True.",
)
@click.option(
"--ldap-tls-validate",
"ldap_tls_validate",
is_flag=True,
default=True,
help="Toggle checking of TLS cert against the provided name. Defaults to True.",
)
@click.option(
"--ldap-tls-ca",
"ldap_tls_ca",
type=click.Path(exists=True),
help="Toggle checking of TLS cert against the provided name. Defaults to True.",
)
@click.option(
"--ldap-base-dn", "ldap_base_dn", type=click.STRING, help="The LDAP base_dn to use."
)
@click.option(
"--ldap-username",
"ldap_username",
type=click.STRING,
help="The username to use to connect to the LDAP server.",
)
@click.option(
"--ldap-password",
"ldap_password",
type=click.STRING,
help="The password to use to connect to the LDAP server. "
"THIS CAN BE READ BY OTHER PROCESSES. NEVER USE IN PRODUCTION!",
)
@click.option(
"--log-conf",
"log_conf",
type=click.Path(exists=True),
help="Path to a yaml configuration for the logger.",
)
@click.option(
"--debug",
"debug",
is_flag=True,
default=False,
help="Set the log level to debug.",
)
def cli(
host,
port,
config_path=None,
ldap_host=None,
ldap_port=None,
ldap_crypt=True,
ldap_tls_validate=True,
ldap_tls_ca=None,
ldap_base_dn=None,
ldap_username=None,
ldap_password=None,
log_conf=None,
debug=False,
):
cli_config = prepare_config_from_cli(
host,
port,
ldap_host,
ldap_port,
ldap_crypt,
ldap_tls_validate,
ldap_tls_ca,
ldap_base_dn,
ldap_username,
ldap_password,
log_conf,
debug,
)
config_file, file_config = get_config(config_path)
config = merge_config(cli_config, file_config)
if debug:
set_to_debug(config)
# Beware that everything happened until now
# could not possibly get logged.
setup_logging(config.get("logging", {}))
if config_file:
log.debug("Config file found at: %s", config_file)
log.debug("{}".format(pp(file_config)))
log.debug("CLI config:\n{}".format(pp(cli_config)))
log.info("Starting app with config:\n{}".format(pp(config)))
app = setup_app(config)
run_app(app)
def prepare_config_from_cli(
host,
port,
ldap_host=None,
ldap_port=None,
ldap_crypt=True,
ldap_tls_validate=True,
ldap_tls_ca=None,
ldap_base_dn=None,
ldap_username=None,
ldap_password=None,
log_conf=None,
debug=False,
):
_core = {"listen": {"host": host, "port": port}}
_ldap = {
"encryption": "TLSv1.2" if ldap_crypt else None,
"validate": ldap_tls_validate,
"ca_certs": ldap_tls_ca,
"username": ldap_username,
"password": ldap_password,
"base_dn": ldap_base_dn,
}
_logging = {}
if log_conf:
with open(log_conf) as l:
_logging = yaml.safe_load(l)
return {"core": _core, "ldap": _ldap, "logging": _logging}
def set_to_debug(conf):
for logger, log_conf in conf["logging"]["loggers"].items():
log_conf["level"] = "DEBUG"
conf["logging"]["loggers"][logger] = log_conf

View File

@ -1,9 +1,66 @@
import os.path import os.path
import pkg_resources
import yaml import yaml
NAME = 'phi' NAME = 'phi'
DEFAULT_CONFIG = {
"core": {
"listen": {
"host": "localhost",
"port": 8080,
}
},
"ldap": {
"host": "localhost",
"port": 389,
"encryption": "TLSv1.2",
"ciphers": "HIGH",
"validate": True,
"ca_certs": pkg_resources.resource_filename(NAME, "openldap/cert.pem"),
"username": None,
"password": None,
"base_dn": None,
"attribute_id": "uid",
"attribute_mail": "mail",
},
"logging": {
"version": 1,
"formatters": {
"default": {"format": "[%(name)s %(levelname)s] %(message)s"}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.FileHandler",
"formatter": "default",
"filename": "phi.log",
}
},
"loggers": {
"phi": {
"level": "INFO",
"handlers": ["console", "file"],
},
"aiohttp": {
"level": "INFO",
"handlers": ["console", "file"],
},
"ldap3": {
"level": "WARNING",
"handlers": ["console", "file"],
}
}
}
}
DUMMY_CONFIG = {"core": {}, "ldap": {}, "logging": {}}
CONFIG_FILE = 'config.yml' CONFIG_FILE = 'config.yml'
CONFIG_PATHS = ['./', CONFIG_PATHS = ['./',
'~/.config/' + NAME + '/', '~/.config/' + NAME + '/',
@ -13,12 +70,18 @@ CONFIG_FILES = [os.path.join(p, CONFIG_FILE)
for p in CONFIG_PATHS] for p in CONFIG_PATHS]
def get_config(): def get_config(config_path=None):
"""Return the path of the found configuration file and its content """Return the path of the found configuration file and its content
:param config_path: optional path to a config file.
:returns: (path, config) :returns: (path, config)
:rtype: (str, dict) :rtype: (str, dict)
""" """
if config_path:
with open(config_path) as c:
config = yaml.safe_load(c)
return config_path, config
for f in CONFIG_FILES: for f in CONFIG_FILES:
try: try:
with open(f, 'r') as c: with open(f, 'r') as c:
@ -30,6 +93,47 @@ def get_config():
# accessible or if the file is not present at all # accessible or if the file is not present at all
# in any of CONFIG_PATHS. # in any of CONFIG_PATHS.
pass pass
else: return None, DUMMY_CONFIG
raise FileNotFoundError("Could not find {} in any of {}."
.format(CONFIG_FILE, ', '.join(CONFIG_PATHS)))
def merge_config(cli_config, file_config):
"""
Merge the cli-provided and file-provided config.
"""
return recursive_merge(cli_config, file_config)
def _init_with_shape_of(element):
if isinstance(element, dict):
return {}
elif isinstance(element, list):
return []
return None
def recursive_merge(main_config, aux_config):
def _recursive_merge(main, aux, default, key, _ret_config):
if isinstance(default, dict):
_sub_conf = {}
for k, v in default.items():
_main = main[k] if k in main else _init_with_shape_of(v)
_aux = aux[k] if k in aux else _init_with_shape_of(v)
_recursive_merge(_main, _aux, v, k, _sub_conf)
_ret_config[key] = _sub_conf
elif isinstance(default, list):
_main = main.copy()
if aux is not None:
_main.extend(aux)
_ret_config[key] = list(set(_main))
else:
if main is not None:
_ret_config[key] = main
elif aux is not None:
_ret_config[key] = aux
else:
_ret_config[key] = default
_config = {}
_recursive_merge(main_config, aux_config, DEFAULT_CONFIG, "ROOT", _config)
return _config["ROOT"]

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python3
from pprint import pformat as pp
from phi.config import get_config
from phi.logging import setup_logging, get_logger
from phi.app import setup_app, run_app
log = get_logger(__name__)
if __name__ == '__main__':
config_file, config = get_config()
# Beware that everything happened until now
# could not possibly get logged.
setup_logging(config.get('logging', {}))
log.info("Found configuration at '{}':\n{}"
.format(config_file, pp(config)))
app = setup_app(config)
run_app(app)