Skip to content

Commit

Permalink
Hub: Load config file on startup
Browse files Browse the repository at this point in the history
Add ASGI lifetime hooks, to get notified on startup and shutdown. Load
the config file on startup and configure the root logger.
  • Loading branch information
holesch committed Apr 20, 2024
1 parent f1692e3 commit e5bc4e6
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 14 deletions.
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ how-to-guides/usb-import
:hidden:
reference/cli
reference/hub-configuration
reference/export-description
reference/import-description
```
Expand Down
23 changes: 23 additions & 0 deletions doc/reference/hub-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Hub Configuration

The *Hub* loads its configuration on startup from
`/etc/not-my-board/not-my-board-hub.toml`. The file format is
[TOML](https://toml.io/en/).

## Settings

### `log_level`

**Type:** String \
**Required:** No

Configures the log level. Can be one of `debug`, `info`, `warning` or `error`.

## Example

Here's an example of a *Hub* configuration:
```{code-block} toml
:caption: /etc/not-my-board/not-my-board-hub.toml
log_level = "info"
```
83 changes: 71 additions & 12 deletions not_my_board/_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ipaddress
import itertools
import logging
import pathlib
import random
import traceback

Expand All @@ -21,37 +22,80 @@
valid_tokens = ("dummy-token-1", "dummy-token-2")


def hub():
def run_hub():
asgineer.run(asgi_app, "uvicorn", ":2092")


async def asgi_app(scope, receive, send):
if scope["type"] == "lifespan":
# asgineer doesn't expose the lifespan hooks. Handle them here
# before handing over to asgineer
await _handle_lifespan(scope, receive, send)
else:
# to_asgi() decorator adds extra arguments
# pylint: disable-next=too-many-function-args
await _handle_request(scope, receive, send)


async def _handle_lifespan(scope, receive, send):
while True:
message = await receive()
if message["type"] == "lifespan.startup":
try:
config_file = pathlib.Path("/etc/not-my-board/not-my-board-hub.toml")
if config_file.exists():
config = util.toml_loads(config_file.read_text())
else:
config = {}

hub = Hub()
await hub.startup(config)
scope["state"]["hub"] = hub
except Exception as err:
await send({"type": "lifespan.startup.failed", "message": str(err)})
else:
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
try:
await hub.shutdown()
except Exception as err:
await send({"type": "lifespan.shutdown.failed", "message": str(err)})
else:
await send({"type": "lifespan.shutdown.complete"})
return
else:
logger.warning("Unknown lifespan message %s", message["type"])


@asgineer.to_asgi
async def asgi_app(request):
async def _handle_request(request):
hub = request.scope["state"]["hub"]

if isinstance(request, asgineer.WebsocketRequest):
if request.path == "/ws-agent":
return await _handle_agent(request)
return await _handle_agent(hub, request)
elif request.path == "/ws-exporter":
return await _handle_exporter(request)
return await _handle_exporter(hub, request)
await request.close()
return
elif isinstance(request, asgineer.HttpRequest):
if request.path == "/api/v1/places":
return await _hub.get_places()
return await hub.get_places()
return 404, {}, "Page not found"


async def _handle_agent(ws):
async def _handle_agent(hub, ws):
await _authorize_ws(ws)
client_ip = ws.scope["client"][0]
server = jsonrpc.Channel(ws.send, ws.receive_iter())
await _hub.agent_communicate(client_ip, server)
await hub.agent_communicate(client_ip, server)


async def _handle_exporter(ws):
async def _handle_exporter(hub, ws):
await _authorize_ws(ws)
client_ip = ws.scope["client"][0]
exporter = jsonrpc.Channel(ws.send, ws.receive_iter())
await _hub.exporter_communicate(client_ip, exporter)
await hub.exporter_communicate(client_ip, exporter)


async def _authorize_ws(ws):
Expand Down Expand Up @@ -80,6 +124,24 @@ class Hub:
def __init__(self):
self._id_generator = itertools.count(start=1)

async def startup(self, config):
if "log_level" in config:
log_level_str = config["log_level"]
log_level_map = {
"debug": logging.DEBUG,
"info": logging.INFO,
"warning": logging.WARNING,
"error": logging.ERROR,
}
log_level = log_level_map[log_level_str]

logging.basicConfig(
format="%(levelname)s: %(name)s: %(message)s", level=log_level
)

async def shutdown(self):
pass

@jsonrpc.hidden
async def get_places(self):
return {"places": [p.dict() for p in self._places.values()]}
Expand Down Expand Up @@ -189,9 +251,6 @@ async def return_reservation(self, place_id):
logger.info("Place returned, but it doesn't exist: %d", place_id)


_hub = Hub()


def _unmap_ip(ip_str):
"""Resolve IPv4-mapped-on-IPv6 to an IPv4 address"""
ip = ipaddress.ip_address(ip_str)
Expand Down
4 changes: 2 additions & 2 deletions not_my_board/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import not_my_board._util as util
from not_my_board._agent import agent
from not_my_board._export import export
from not_my_board._hub import hub
from not_my_board._hub import run_hub

try:
from ..__about__ import __version__
Expand Down Expand Up @@ -144,7 +144,7 @@ def add_cacert_arg(subparser):


def _hub_command(_):
hub()
run_hub()


async def _export_command(args):
Expand Down

0 comments on commit e5bc4e6

Please sign in to comment.