Introduce configuration in server app.

This commit is contained in:
sfigato 2019-08-06 10:04:02 +02:00 committed by blallo
parent 50f0b64182
commit 7d6b445678
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
4 changed files with 152 additions and 16 deletions

View File

@ -1,5 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
from distutils.util import strtobool
import os import os
# TODO: create config module and put this constant there. # TODO: create config module and put this constant there.
BASE_URI = os.environ.get("BOTZ_BASE_URI", "http://localhost") BASE_URI = os.environ.get("BOTZ_BASE_URI", "http://localhost")
DEBUG = strtobool(os.environ.get("BOTZ_DEBUG", "False"))

View File

@ -8,6 +8,7 @@ from aiohttp import web
import base64 import base64
from cryptography import fernet from cryptography import fernet
import logging import logging
import logging.handlers
import os import os
import typing as T import typing as T
@ -17,15 +18,19 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
from bot_z.async_operator import AsyncOperator from bot_z.async_operator import AsyncOperator
from api.rest import routes from api.rest import routes
from api.conf import read_conf
def setup_log() -> logging.Logger: def setup_log(level: str, syslog: bool) -> logging.Logger:
alog = logging.getLogger("api") alog = logging.getLogger("api")
alog.setLevel(os.environ.get("BOTZ_LOGLEVEL", logging.INFO)) alog.setLevel(os.environ.get("BOTZ_LOGLEVEL", level))
h = logging.StreamHandler() h = logging.StreamHandler()
f = logging.Formatter("%(levelname)s [%(name)s] -> %(message)s") f = logging.Formatter("%(levelname)s [%(name)s] -> %(message)s")
h.setFormatter(f) h.setFormatter(f)
alog.addHandler(h) alog.addHandler(h)
if syslog:
sh = logging.handlers.SysLogHandler()
alog.addHandler(sh)
return alog return alog
@ -34,28 +39,63 @@ def init_secret() -> bytes:
return base64.urlsafe_b64decode(fernet_key) return base64.urlsafe_b64decode(fernet_key)
def setup_session(app: web.Application): def setup_session(app: web.Application, secure: bool):
secret = init_secret() secret = init_secret()
setup(app, EncryptedCookieStorage(secret)) setup(
app,
EncryptedCookieStorage(
secret_key=secret, cookie_name="BOTZ_SESSION", httponly=False, secure=secure
),
)
def run(address: T.Text, port: int) -> None: def run(
address: T.Optional[T.Text], port: T.Optional[int], conf_path: T.Optional[T.Text]
) -> None:
"""Application entrypoint.""" """Application entrypoint."""
alog = setup_log() conf = read_conf(conf_path)
# This closure is needed to intercept the to-be-prepared response
# and add the right CORS headers
async def on_prepare_cors(request, response):
response.headers["Access-Control-Allow-Origin"] = conf["http"].get("cors_allow")
response.headers["Access-Control-Allow-Credentials"] = "true"
alog = setup_log(conf["log"]["level"], conf["log"]["syslog"])
alog.debug("conf %s", conf)
app = web.Application(logger=alog) app = web.Application(logger=alog)
setup_session(app) app["base_uri"] = conf["base_uri"]
app["debug"] = conf["debug"]
app["headless"] = conf["headless"]
if conf["http"].get("cors_allow"):
app.on_response_prepare.append(on_prepare_cors)
setup_session(app, conf["http"]["cookie_secure"])
app.add_routes(routes) app.add_routes(routes)
web.run_app(app, host=address, port=port) addr = []
if address is not None:
addr.append(address)
if conf["http"].get("bind_addr"):
addr.extend(conf["http"]["bind_addr"])
if port is None:
port = conf["http"]["port"]
alog.debug("Starting app with: addr -> %s, port -> %d", addr, port)
web.run_app(app, host=addr, port=port)
@click.command() @click.command()
@click.option( @click.option(
"-a", "-a", "--address", type=click.STRING, help="Address to bind the server to."
"--address",
type=click.STRING,
help="Address to bind the server to.",
default="127.0.0.1",
) )
@click.option("-p", "--port", type=click.INT, help="Port to bind to", default=3003) @click.option("-p", "--port", type=click.INT, help="Port to bind to.")
def cli(address: T.Text, port: int) -> None: @click.option(
run(address, port) "-c",
"--conf",
type=click.Path(exists=False),
help="A path to a configuration file.",
)
def cli(
address: T.Optional[T.Text],
port: T.Optional[int] = None,
conf: T.Optional[T.Text] = None,
) -> None:
run(address, port, conf)

93
api/conf.py Normal file
View File

@ -0,0 +1,93 @@
# -*- encoding: utf-8 -*-
import os
from pprint import pprint
import typing as T
import yaml
from api import DEBUG, BASE_URI
def read_conf(path: T.Optional[T.Text]) -> T.Dict:
"""
Read the configuration from the provided path.
Such configuration may provide the following information:
---
base_uri: <base uri of the target instance>
debug: <bool, set debug on, defaults to false>
headless: <bool, use headless mode, defaults to true>
log:
level: <may be DEBUG, INFO, WARN, ..., defaults at INFO>
syslog: <bool, whether to redirect to standard syslog, defaults to false>
http:
bind_addr: <a list of addresses to bind to>
port: <int, the port to bind to>
cookie_name: <defaults to BOTZ_SESSION>
cookie_secure: <bool, whether to set Secure cookie flag, defaults to true>
cors_allow: <an optional single allowed Cross Origin domain>
"""
if path is None:
path = seek_path()
if path is not None:
with open(path) as f:
conf = yaml.safe_load(f)
else:
conf = {}
if "base_uri" not in conf:
if BASE_URI is None:
raise RuntimeError("Missing base_uri")
conf["base_uri"] = BASE_URI
if "debug" not in conf:
conf["debug"] = DEBUG
if "headless" not in conf:
conf["headless"] = True
conf = validate_log_conf(conf)
conf = validate_http_log(conf)
pprint(conf)
return conf
def seek_path() -> T.Optional[T.Text]:
"""
Seeks the path to a config file, in the following order:
- $BOTZ_CONFIG
- $PWD/.botz.yaml
- ~/.botz.yaml
- /etc/botz/conf.yaml
"""
paths = [
os.path.join(os.path.curdir, ".botz.yaml"),
os.path.expanduser("~/.botz.yaml"),
"/etc/botz/conf.yaml",
]
env = os.environ.get("BOTZ_CONFIG")
if env is not None:
paths.insert(0, env)
for path in paths:
if os.path.exists(path):
return path
return None
def validate_log_conf(conf: T.Dict[T.Text, T.Any]) -> T.Dict[T.Text, T.Any]:
if "log" not in conf:
conf["log"] = {}
if conf["log"].get("level") is None:
conf["log"]["level"] = "INFO"
if conf["log"].get("syslog") is None:
conf["log"]["syslog"] = False
return conf
def validate_http_log(conf: T.Dict[T.Text, T.Any]) -> T.Dict[T.Text, T.Any]:
if "http" not in conf:
conf["http"] = {}
if conf["http"].get("bind_addr") is None:
conf["http"]["bind_addr"] = ["127.0.0.1"]
if conf["http"].get("port") is None:
conf["http"]["port"] = 3003
if conf["http"].get("cookie_name") is None:
conf["http"]["cookie_name"] = "BOTZ_SESSION"
if conf["http"].get("cookie_secure") is None:
conf["http"]["cookie_secure"] = True
return conf

View File

@ -40,6 +40,7 @@ requirements = [
"cchardet", "cchardet",
"aiohttp-session==2.7.0", "aiohttp-session==2.7.0",
"cryptography==2.7", "cryptography==2.7",
"PyYAML==5.1.2",
] ]
setup_requirements = [] # type: T.List[str] setup_requirements = [] # type: T.List[str]