From: Vasek Sraier Date: Wed, 7 Apr 2021 19:13:40 +0000 (+0200) Subject: manager: preparation for multiple service manager backends X-Git-Tag: v6.0.0a1~181 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=63d6e93d0c8acf132e2036b53cae7d4d072144a0;p=thirdparty%2Fknot-resolver.git manager: preparation for multiple service manager backends --- diff --git a/manager/knot_resolver_manager/__main__.py b/manager/knot_resolver_manager/__main__.py index 2223f775e..a92712b9b 100644 --- a/manager/knot_resolver_manager/__main__.py +++ b/manager/knot_resolver_manager/__main__.py @@ -11,15 +11,23 @@ from .utils import ignore_exceptions # when changing this, change the help message in main() _SOCKET_PATH = "/tmp/manager.sock" +_MANAGER = "kres_manager" -async def hello(_request: web.Request) -> web.Response: - return web.Response(text="Hello, world") +async def index(_request: web.Request) -> web.Response: + return web.Response(text="Knot Resolver Manager is running! The configuration endpoint is at /config") async def apply_config(request: web.Request) -> web.Response: + manager: KresManager = request.app[_MANAGER] + if manager is None: + # handle the case when the manager is not yet initialized + return web.Response( + status=503, headers={"Retry-After": "3"}, text="Knot Resolver Manager is not yet fully initialized" + ) + + # process the request config = KresConfig.from_json(await request.text()) - manager: KresManager = request.app["kres_manager"] await manager.apply_config(config) return web.Response(text="OK") @@ -36,12 +44,11 @@ def main(listen: Optional[str], config: Optional[str]): app = web.Application() # initialize KresManager - manager = KresManager() - app["kres_manager"] = manager + app[_MANAGER] = None async def init_manager(app: web.Application): - manager = app["kres_manager"] - await manager.load_system_state() + manager = await KresManager.create() + app[_MANAGER] = manager if config is not None: # TODO Use config loaded from the file system pass @@ -49,7 +56,7 @@ def main(listen: Optional[str], config: Optional[str]): app.on_startup.append(init_manager) # configure routing - app.add_routes([web.get("/", hello), web.post("/config", apply_config)]) + app.add_routes([web.get("/", index), web.post("/config", apply_config)]) # run forever, listen at the appropriate place maybe_port = ignore_exceptions(None, ValueError, TypeError)(int)(listen) diff --git a/manager/knot_resolver_manager/kres_manager.py b/manager/knot_resolver_manager/kres_manager.py index ab8910ee4..f6d3f4f91 100644 --- a/manager/knot_resolver_manager/kres_manager.py +++ b/manager/knot_resolver_manager/kres_manager.py @@ -1,46 +1,41 @@ import asyncio -from typing import List, Optional -from uuid import uuid4 +from typing import Any, List, Type -from . import compat, configuration, systemd -from .datamodel import KresConfig - - -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 +from knot_resolver_manager.kresd_controller import BaseKresdController, get_best_controller_implementation - async def is_running(self) -> bool: - raise NotImplementedError() +from . import configuration +from .datamodel import KresConfig - async def start(self): - await compat.asyncio.to_thread(systemd.start_unit, f"kresd@{self._id}.service") - async def stop(self): - await compat.asyncio.to_thread(systemd.stop_unit, f"kresd@{self._id}.service") +class KresManager: + """ + Core of the whole operation. Orchestrates individual instances under some + service manager like systemd. - async def restart(self): - await compat.asyncio.to_thread(systemd.restart_unit, f"kresd@{self._id}.service") + Instantiate with `KresManager.create()`, not with the usual constructor! + """ - def mark_for_restart(self): - self._needs_restart = True + @classmethod + async def create(cls: Type["KresManager"], *args: Any, **kwargs: Any) -> "KresManager": + obj = cls() + await obj._async_init(*args, **kwargs) # pylint: disable=protected-access + return obj + async def _async_init(self): + self._controller = await get_best_controller_implementation() + await self.load_system_state() -class KresManager: def __init__(self): - self._children: List[Kresd] = [] + self._children: List[BaseKresdController] = [] self._children_lock = asyncio.Lock() + self._controller: Type[BaseKresdController] 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() + kresd = self._controller() await kresd.start() self._children.append(kresd) @@ -52,12 +47,7 @@ class KresManager: await kresd.stop() async def _collect_already_running_children(self): - units = await compat.asyncio.to_thread(systemd.list_units) - for unit in units: - u: str = unit - if u.startswith("kresd@") and u.endswith(".service"): - iden = u.replace("kresd@", "").replace(".service", "") - self._children.append(Kresd(kresd_id=iden)) + self._children.extend(await self._controller.get_all_running_instances()) async def _rolling_restart(self): for kresd in self._children: diff --git a/manager/knot_resolver_manager/kresd_controller/__init__.py b/manager/knot_resolver_manager/kresd_controller/__init__.py new file mode 100644 index 000000000..302072312 --- /dev/null +++ b/manager/knot_resolver_manager/kresd_controller/__init__.py @@ -0,0 +1,22 @@ +import asyncio +from typing import Type + +from knot_resolver_manager.kresd_controller.base import BaseKresdController +from knot_resolver_manager.kresd_controller.systemd import SystemdKresdController + + +# In this tuple, every supported controller should be listed. In the order of preference (preferred first) +_registered_controllers = (SystemdKresdController,) + + +async def get_best_controller_implementation() -> Type[BaseKresdController]: + # check all controllers concurrently + res = await asyncio.gather(*(cont.is_controller_available() for cont in _registered_controllers)) + + # take the first one on the list which is available + for avail, controller in zip(res, _registered_controllers): + if avail: + return controller + + # or fail + raise LookupError("Can't find any available service manager!") diff --git a/manager/knot_resolver_manager/kresd_controller/base.py b/manager/knot_resolver_manager/kresd_controller/base.py new file mode 100644 index 000000000..575762e44 --- /dev/null +++ b/manager/knot_resolver_manager/kresd_controller/base.py @@ -0,0 +1,34 @@ +import asyncio +from typing import Iterable, Optional +from uuid import uuid4 + + +class BaseKresdController: + """ + The common Kresd Controller interface. This is what KresManager requires and what has to be implemented by all + controllers. + """ + + def __init__(self, kresd_id: Optional[str] = None): + self._lock = asyncio.Lock() + self.id: str = kresd_id or str(uuid4()) + + @staticmethod + async def is_controller_available() -> bool: + raise NotImplementedError() + + async def is_running(self) -> bool: + raise NotImplementedError() + + async def start(self) -> None: + raise NotImplementedError() + + async def stop(self) -> None: + raise NotImplementedError() + + async def restart(self) -> None: + raise NotImplementedError() + + @staticmethod + async def get_all_running_instances() -> Iterable["BaseKresdController"]: + raise NotImplementedError() diff --git a/manager/knot_resolver_manager/kresd_controller/systemd/__init__.py b/manager/knot_resolver_manager/kresd_controller/systemd/__init__.py new file mode 100644 index 000000000..39b564813 --- /dev/null +++ b/manager/knot_resolver_manager/kresd_controller/systemd/__init__.py @@ -0,0 +1,36 @@ +from typing import Iterable, List + +from knot_resolver_manager import compat +from knot_resolver_manager.kresd_controller.base import BaseKresdController + +from . import dbus_api as systemd + + +class SystemdKresdController(BaseKresdController): + async def is_running(self) -> bool: + raise NotImplementedError() + + async def start(self): + await compat.asyncio.to_thread(systemd.start_unit, f"kresd@{self.id}.service") + + async def stop(self): + await compat.asyncio.to_thread(systemd.stop_unit, f"kresd@{self.id}.service") + + async def restart(self): + await compat.asyncio.to_thread(systemd.restart_unit, f"kresd@{self.id}.service") + + @staticmethod + async def is_controller_available() -> bool: + # TODO: implement a proper check + return True + + @staticmethod + async def get_all_running_instances() -> Iterable["BaseKresdController"]: + res: List[SystemdKresdController] = [] + units = await compat.asyncio.to_thread(systemd.list_units) + for unit in units: + u: str = unit + if u.startswith("kresd@") and u.endswith(".service"): + iden = u.replace("kresd@", "").replace(".service", "") + res.append(SystemdKresdController(kresd_id=iden)) + return res diff --git a/manager/knot_resolver_manager/systemd.py b/manager/knot_resolver_manager/kresd_controller/systemd/dbus_api.py similarity index 100% rename from manager/knot_resolver_manager/systemd.py rename to manager/knot_resolver_manager/kresd_controller/systemd/dbus_api.py