]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
basic foundation of inner APIs
authorVasek Sraier <git@vakabus.cz>
Sun, 21 Feb 2021 16:48:35 +0000 (17:48 +0100)
committerAleš Mrázek <ales.mrazek@nic.cz>
Fri, 8 Apr 2022 14:17:51 +0000 (16:17 +0200)
manager/.flake8 [new file with mode: 0644]
manager/knot_resolver_manager/__main__.py
manager/knot_resolver_manager/compat.py [new file with mode: 0644]
manager/knot_resolver_manager/confmodel.py [new file with mode: 0644]
manager/knot_resolver_manager/kresd_manager.py [new file with mode: 0644]
manager/knot_resolver_manager/systemd.py [new file with mode: 0644]
manager/poetry.lock
manager/pyproject.toml

diff --git a/manager/.flake8 b/manager/.flake8
new file mode 100644 (file)
index 0000000..79a16af
--- /dev/null
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 120
\ No newline at end of file
index bdf6f199c1a3d0ab3ce3248d8e6681fac2fb0533..59af3c5caca76295a59b988cb9e2e3b24fb76841 100644 (file)
@@ -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 (file)
index 0000000..3a1482a
--- /dev/null
@@ -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 (file)
index 0000000..411a511
--- /dev/null
@@ -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 (file)
index 0000000..59a83db
--- /dev/null
@@ -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 (file)
index 0000000..3e0b19c
--- /dev/null
@@ -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")
index 608aae901c9f6a3b9e8663e30d65d37c11f79879..cad3cefd6a96ae0696348ec6df81e0137ae410c0 100644 (file)
@@ -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"},
index 8dc023b256f69db4905bbcfe3a09e3b843bed7e6..ef938a25e37e1b3c7c58d5c841eace62695cd2ab 100644 (file)
@@ -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]