1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

239 lines
6.3 KiB
Python
Raw Normal View History

2020-04-13 01:06:03 -07:00
"""Gunicorn Config File."""
# Standard Library
import sys
import math
import shutil
2020-10-11 15:39:15 -07:00
import logging
2020-04-13 01:06:03 -07:00
import platform
from typing import TYPE_CHECKING
2020-04-13 01:06:03 -07:00
# Third Party
from gunicorn.app.base import BaseApplication # type: ignore
from gunicorn.glogging import Logger # type: ignore
2020-04-13 01:06:03 -07:00
# Local
from .log import log, setup_lib_logging
2021-09-13 14:10:50 -07:00
from .plugins import (
InputPluginManager,
OutputPluginManager,
register_plugin,
init_builtin_plugins,
)
from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__
from .util.frontend import get_node_version
2020-04-13 01:06:03 -07:00
if TYPE_CHECKING:
# Third Party
from gunicorn.arbiter import Arbiter # type: ignore
# Local
from .models.config.devices import Devices
# Ensure the Python version meets the minimum requirements.
2020-04-13 01:06:03 -07:00
pretty_version = ".".join(tuple(str(v) for v in MIN_PYTHON_VERSION))
if sys.version_info < MIN_PYTHON_VERSION:
raise RuntimeError(f"Python {pretty_version}+ is required.")
# Ensure the NodeJS version meets the minimum requirements.
2021-07-03 23:02:14 -07:00
node_major, _, __ = get_node_version()
2021-07-03 23:02:14 -07:00
if node_major != MIN_NODE_VERSION:
raise RuntimeError(f"NodeJS {MIN_NODE_VERSION}+ is required.")
2021-01-28 23:02:25 -07:00
# Project
from hyperglass.compat._asyncio import aiorun
# Local
2021-01-28 23:02:25 -07:00
from .util import cpu_count, clear_redis_cache, format_listen_address
from .cache import SyncCache
2021-01-28 23:02:25 -07:00
from .configuration import (
2020-04-13 01:06:03 -07:00
URL_DEV,
URL_PROD,
CONFIG_PATH,
REDIS_CONFIG,
2021-01-28 23:02:25 -07:00
params,
devices,
2021-09-10 01:18:38 -07:00
ui_params,
2020-04-13 01:06:03 -07:00
)
2021-01-28 23:02:25 -07:00
from .util.frontend import build_frontend
2020-04-13 01:06:03 -07:00
if params.debug:
workers = 1
loglevel = "DEBUG"
else:
workers = cpu_count(2)
loglevel = "WARNING"
2020-10-11 15:39:15 -07:00
class StubbedGunicornLogger(Logger):
"""Custom logging to direct Gunicorn/Uvicorn logs to Loguru/Rich.
See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/
"""
def setup(self, cfg):
"""Override Gunicorn setup."""
handler = logging.NullHandler()
self.error_logger = logging.getLogger("gunicorn.error")
self.error_logger.addHandler(handler)
self.access_logger = logging.getLogger("gunicorn.access")
self.access_logger.addHandler(handler)
self.error_logger.setLevel(loglevel)
self.access_logger.setLevel(loglevel)
def check_redis_instance() -> bool:
"""Ensure Redis is running before starting server."""
2020-04-13 01:06:03 -07:00
cache = SyncCache(db=params.cache.database, **REDIS_CONFIG)
cache.test()
2020-09-28 12:37:44 -07:00
log.debug("Redis is running at: {}:{}", REDIS_CONFIG["host"], REDIS_CONFIG["port"])
2020-04-13 01:06:03 -07:00
return True
2021-01-28 23:02:25 -07:00
async def build_ui() -> bool:
"""Perform a UI build prior to starting the application."""
2020-04-13 01:06:03 -07:00
await build_frontend(
dev_mode=params.developer_mode,
dev_url=URL_DEV,
prod_url=URL_PROD,
2021-09-10 01:18:38 -07:00
params=ui_params,
2020-04-13 01:06:03 -07:00
app_path=CONFIG_PATH,
)
return True
async def clear_cache():
"""Clear the Redis cache on shutdown."""
try:
await clear_redis_cache(db=params.cache.database, config=REDIS_CONFIG)
except RuntimeError as e:
log.error(str(e))
pass
2021-01-28 23:02:25 -07:00
def cache_config() -> bool:
2020-04-15 02:12:01 -07:00
"""Add configuration to Redis cache as a pickled object."""
# Standard Library
2020-04-15 02:12:01 -07:00
import pickle
cache = SyncCache(db=params.cache.database, **REDIS_CONFIG)
cache.set("HYPERGLASS_CONFIG", pickle.dumps(params))
2020-04-15 02:12:01 -07:00
return True
def register_all_plugins(devices: "Devices") -> None:
"""Validate and register configured plugins."""
2021-09-13 14:10:50 -07:00
# Register built-in plugins.
init_builtin_plugins()
# Register external plugins.
2021-09-12 18:27:33 -07:00
for plugin_file, directives in devices.directive_plugins().items():
failures = register_plugin(plugin_file, directives=directives)
for failure in failures:
log.warning(
2021-09-12 15:09:24 -07:00
"Plugin '{}' is not a valid hyperglass plugin, and was not registered", failure,
)
def unregister_all_plugins() -> None:
"""Unregister all plugins."""
for manager in (InputPluginManager, OutputPluginManager):
manager().reset()
def on_starting(server: "Arbiter"):
2020-04-13 01:06:03 -07:00
"""Gunicorn pre-start tasks."""
2021-01-12 00:16:34 -07:00
setup_lib_logging()
2020-10-11 15:39:15 -07:00
2020-04-13 01:06:03 -07:00
python_version = platform.python_version()
required = ".".join((str(v) for v in MIN_PYTHON_VERSION))
2020-09-28 12:37:44 -07:00
log.info("Python {} detected ({} required)", python_version, required)
2020-04-13 01:06:03 -07:00
check_redis_instance()
aiorun(build_ui())
cache_config()
register_all_plugins(devices)
2020-04-13 01:06:03 -07:00
log.success(
"Started hyperglass {v} on http://{h}:{p} with {w} workers",
v=__version__,
2020-04-13 02:26:12 -07:00
h=format_listen_address(params.listen_address),
2020-04-13 01:06:03 -07:00
p=str(params.listen_port),
w=server.app.cfg.settings["workers"].value,
)
def on_exit(server: "Arbiter"):
2020-04-13 01:06:03 -07:00
"""Gunicorn shutdown tasks."""
2020-04-15 02:12:01 -07:00
log.critical("Stopping hyperglass {}", __version__)
async def runner():
2020-07-13 01:54:38 -07:00
if not params.developer_mode:
await clear_cache()
2020-04-15 02:12:01 -07:00
aiorun(runner())
unregister_all_plugins()
2020-04-13 01:06:03 -07:00
class HyperglassWSGI(BaseApplication):
"""Custom gunicorn app."""
def __init__(self, app, options):
"""Initialize custom WSGI."""
self.application = app
self.options = options or {}
super().__init__()
def load_config(self):
"""Load gunicorn config."""
config = {
key: value
for key, value in self.options.items()
if key in self.cfg.settings and value is not None
}
2020-04-15 02:12:01 -07:00
2020-04-13 01:06:03 -07:00
for key, value in config.items():
self.cfg.set(key.lower(), value)
def load(self):
"""Load gunicorn app."""
return self.application
def start(**kwargs):
"""Start hyperglass via gunicorn."""
HyperglassWSGI(
2021-09-12 18:27:33 -07:00
app="hyperglass.api:app",
2020-04-13 01:06:03 -07:00
options={
"worker_class": "uvicorn.workers.UvicornWorker",
"preload": True,
"keepalive": 10,
"command": shutil.which("gunicorn"),
2020-04-13 02:04:35 -07:00
"bind": ":".join(
(format_listen_address(params.listen_address), str(params.listen_port))
),
2020-04-13 01:06:03 -07:00
"workers": workers,
"loglevel": loglevel,
"timeout": math.ceil(params.request_timeout * 1.25),
"on_starting": on_starting,
"on_exit": on_exit,
2020-10-11 15:39:15 -07:00
"logger_class": StubbedGunicornLogger,
"accesslog": "-",
"errorlog": "-",
"logconfig_dict": {"formatters": {"generic": {"format": "%(message)s"}}},
2020-04-13 01:06:03 -07:00
**kwargs,
},
).run()
if __name__ == "__main__":
start()