From: Vasek Sraier Date: Sun, 21 Feb 2021 16:48:35 +0000 (+0100) Subject: basic foundation of inner APIs X-Git-Tag: v6.0.0a1~234 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=acb91dd253c73e2dca98601f6e09663d06f6787c;p=thirdparty%2Fknot-resolver.git basic foundation of inner APIs --- diff --git a/manager/.flake8 b/manager/.flake8 new file mode 100644 index 000000000..79a16af7e --- /dev/null +++ b/manager/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 \ No newline at end of file diff --git a/manager/knot_resolver_manager/__main__.py b/manager/knot_resolver_manager/__main__.py index bdf6f199c..59af3c5ca 100644 --- a/manager/knot_resolver_manager/__main__.py +++ b/manager/knot_resolver_manager/__main__.py @@ -1,14 +1,33 @@ from aiohttp import web +from knot_resolver_manager.kresd_manager import KresdManager + +from . import confmodel +from . import compat async def hello(_request: web.Request) -> web.Response: return web.Response(text="Hello, world") +async def apply_config(request: web.Request) -> web.Response: + config = await confmodel.parse(await request.text()) + manager: KresdManager = request.app["kresd_manager"] + await manager.apply_config(config) + return web.Response(text="OK") + + def main(): app = web.Application() - app.add_routes([web.get("/", hello)]) + # initialize KresdManager + manager = KresdManager() + compat.asyncio_run(manager.load_system_state()) + app["kresd_manager"] = manager + + # configure routing + app.add_routes([web.get("/", hello), web.post("/config", apply_config)]) + + # run forever web.run_app(app, path="./manager.sock") diff --git a/manager/knot_resolver_manager/compat.py b/manager/knot_resolver_manager/compat.py new file mode 100644 index 000000000..3a1482a17 --- /dev/null +++ b/manager/knot_resolver_manager/compat.py @@ -0,0 +1,47 @@ +# pylint: disable=E1101 + +from asyncio.futures import Future +import sys +import asyncio +import functools +from typing import Awaitable, Coroutine + + +def asyncio_to_thread(func, *args, **kwargs) -> Awaitable: + # version 3.9 and higher, call directly + if sys.version_info.major >= 3 and sys.version_info.minor >= 9: + return asyncio.to_thread(func, *args, **kwargs) + + # earlier versions, run with default executor + else: + loop = asyncio.get_event_loop() + pfunc = functools.partial(func, *args, **kwargs) + return loop.run_in_executor(None, pfunc) + + +def asyncio_create_task(coro: Coroutine, name=None) -> Future: + # version 3.8 and higher, call directly + if sys.version_info.major >= 3 and sys.version_info.minor >= 8: + return asyncio.create_task(coro, name=name) + + # version 3.7 and higher, call directly without the name argument + if sys.version_info.major >= 3 and sys.version_info.minor >= 8: + return asyncio.create_task(coro) + + # earlier versions, use older function + else: + return asyncio.ensure_future(coro) + + +def asyncio_run(coro: Coroutine, debug=None) -> Awaitable: + # ideally copy-paste of this: + # https://github.com/python/cpython/blob/3.9/Lib/asyncio/runners.py#L8 + + # version 3.7 and higher, call directly + if sys.version_info.major >= 3 and sys.version_info.minor >= 7: + return asyncio.run(coro, debug=debug) + + # earlier versions, run with default executor + else: + loop = asyncio.get_event_loop() + return loop.run_until_complete(coro) diff --git a/manager/knot_resolver_manager/confmodel.py b/manager/knot_resolver_manager/confmodel.py new file mode 100644 index 000000000..411a51127 --- /dev/null +++ b/manager/knot_resolver_manager/confmodel.py @@ -0,0 +1,44 @@ +from strictyaml import Map, Str, Int +from strictyaml.parser import load +from strictyaml.representation import YAML + + +_CONFIG_SCHEMA = Map({"lua_config": Str(), "num_workers": Int()}) + + +def _get_config_schema(): + """ + Returns a schema defined using the strictyaml library, that the manager + should accept at it's input. + + If this function does something, that can be cached, it should cache it by + itself. For example, loading the schema from a file is OK, the loaded + parsed schema object should then however be cached in memory. The function + is on purpose non-async and it's expected to return very fast. + """ + return _CONFIG_SCHEMA + + +class ConfigValidationException(Exception): + pass + + +async def _validate_config(config): + """ + Perform runtime value validation of the provided configuration object which + is guaranteed to follow the configuration schema returned by the + `get_config_schema` function. + + Throws a ConfigValidationException in case any errors are found. The error + message should be in the error message of the exception. + """ + + if config["num_workers"] < 0: + raise ConfigValidationException("Number of workers must be non-negative") + + +async def parse(textual_config: str) -> YAML: + schema = _get_config_schema() + conf = load(textual_config, schema) + await _validate_config(conf) + return conf diff --git a/manager/knot_resolver_manager/kresd_manager.py b/manager/knot_resolver_manager/kresd_manager.py new file mode 100644 index 000000000..59a83db7f --- /dev/null +++ b/manager/knot_resolver_manager/kresd_manager.py @@ -0,0 +1,76 @@ +import asyncio +from uuid import uuid4 +from typing import List, Optional +from strictyaml.representation import YAML + + +class Kresd: + def __init__(self, kresd_id: Optional[str] = None): + self._lock = asyncio.Lock() + self._id: str = kresd_id or str(uuid4()) + + # if we got existing id, mark for restart + self._needs_restart: bool = id is not None + + async def is_running(self) -> bool: + raise NotImplementedError() + + async def start(self): + raise NotImplementedError() + + async def stop(self): + raise NotImplementedError() + + async def restart(self): + raise NotImplementedError() + + def mark_for_restart(self): + self._needs_restart = True + + +class KresdManager: + def __init__(self): + self._children: List[Kresd] = [] + self._children_lock = asyncio.Lock() + + async def load_system_state(self): + async with self._children_lock: + await self._collect_already_running_children() + + async def _spawn_new_child(self): + kresd = Kresd() + await kresd.start() + self._children.append(kresd) + + async def _stop_a_child(self): + if len(self._children) == 0: + raise IndexError("Can't stop a kresd when there are no running") + + kresd = self._children.pop() + await kresd.stop() + + async def _collect_already_running_children(self): + raise NotImplementedError() + + async def _rolling_restart(self): + for kresd in self._children: + await kresd.restart() + await asyncio.sleep(1) + + async def _ensure_number_of_children(self, n: int): + # kill children that are not needed + while len(self._children) > n: + await self._stop_a_child() + + # spawn new children if needed + while len(self._children) < n: + await self._spawn_new_child() + + async def _write_config(self, config: YAML): + raise NotImplementedError() + + async def apply_config(self, config: YAML): + async with self._children_lock: + await self._write_config(config) + await self._ensure_number_of_children(config["num_workers"]) + await self._rolling_restart() diff --git a/manager/knot_resolver_manager/systemd.py b/manager/knot_resolver_manager/systemd.py new file mode 100644 index 000000000..3e0b19cb3 --- /dev/null +++ b/manager/knot_resolver_manager/systemd.py @@ -0,0 +1,32 @@ +from typing import List, Union +import dbus +from typing_extensions import Literal + + +def _create_manager_interface(): + bus = dbus.SystemBus() + systemd = bus.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") + + manager = dbus.Interface(systemd, "org.freedesktop.systemd1.Manager") + + return manager + + +def get_unit_file_state( + unit_name: str, +) -> Union[Literal["disabled"], Literal["enabled"]]: + res = str(_create_manager_interface().GetUnitFileState(unit_name)) + assert res == "disabled" or res == "enabled" + return res + + +def list_units() -> List[str]: + return [str(u[0]) for u in _create_manager_interface().ListUnits()] + + +def list_jobs(): + return _create_manager_interface().ListJobs() + + +def restart_unit(unit_name: str): + return _create_manager_interface().RestartUnit(unit_name, "fail") diff --git a/manager/poetry.lock b/manager/poetry.lock index 608aae901..cad3cefd6 100644 --- a/manager/poetry.lock +++ b/manager/poetry.lock @@ -151,6 +151,14 @@ category = "dev" optional = false python-versions = ">=3.6, <3.7" +[[package]] +name = "dbus-python" +version = "1.2.16" +description = "Python bindings for libdbus" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "distlib" version = "0.3.1" @@ -846,7 +854,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.6.12" -content-hash = "90a3b2334875dcde45ebbb46bff45b04e689ef28d37bdc560056e4fe365fde0c" +content-hash = "103e16cdbcee85cc8aa19e806f3a595b7d82bc565f6de0fa7970745c6ef6c1ef" [metadata.files] aiohttp = [ @@ -982,6 +990,9 @@ dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] +dbus-python = [ + {file = "dbus-python-1.2.16.tar.gz", hash = "sha256:11238f1d86c995d8aed2e22f04a1e3779f0d70e587caffeab4857f3c662ed5a4"}, +] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, diff --git a/manager/pyproject.toml b/manager/pyproject.toml index 8dc023b25..ef938a25e 100644 --- a/manager/pyproject.toml +++ b/manager/pyproject.toml @@ -10,6 +10,7 @@ authors = [ python = "^3.6.12" aiohttp = "^3.6.12" strictyaml = "^1.3.2" +dbus-python = "^1.2.16" [tool.poetry.dev-dependencies] pytest = "^5.2" @@ -70,7 +71,9 @@ disable= [ "no-self-use", "raise-missing-from", "too-few-public-methods", - "unused-import", # checked by flake8 + "unused-import", # checked by flake8, + "bad-continuation", # conflicts with black + "consider-using-in", # pyright can't see through in expressions ] [tool.pylint.SIMILARITIES]