From f7370128e1ec31e4fb46d6624f527ec8433b5326 Mon Sep 17 00:00:00 2001 From: Simon Holesch Date: Fri, 16 Aug 2024 10:23:30 +0200 Subject: [PATCH] Agent: Support systemd Socket Activation With socket activation, the ready notification isn't useful, because the socket is ready before the Agent is started. Disable the ready notification in that case. --- doc/how-to-guides/set-up-agent.md | 30 +++++++++++++++++++++++------- doc/reference/cli.md | 7 ++++++- not_my_board/_agent.py | 17 +++++++++++++---- not_my_board/cli/__init__.py | 8 ++++++-- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/doc/how-to-guides/set-up-agent.md b/doc/how-to-guides/set-up-agent.md index 42c09ce..6b28cd3 100644 --- a/doc/how-to-guides/set-up-agent.md +++ b/doc/how-to-guides/set-up-agent.md @@ -17,7 +17,24 @@ Log out and log back in again for the changes to take effect. ## Configuring the Service -Create a `systemd` service file (replace `` with the address or +Configure `systemd` to create and listen on a Unix domain socket: +```{code-block} systemd +:caption: /etc/systemd/system/not-my-board-agent.socket + +[Unit] +Description=Board Farm Agent Socket + +[Socket] +ListenStream=/run/not-my-board-agent.sock +SocketGroup=not-my-board +SocketMode=0660 + +[Install] +WantedBy=sockets.target +``` + +Then create a `systemd` service, that is started, the fist time a `not-my-board` +command connects to the *Agent* (replace `` with the address or domain name of the *Hub*): ```{code-block} systemd :caption: /etc/systemd/system/not-my-board-agent.service @@ -26,10 +43,9 @@ domain name of the *Hub*): Description=Board Farm Agent [Service] -ExecStart=/usr/local/bin/not-my-board agent https:// - -[Install] -WantedBy=multi-user.target +ExecStart=/usr/local/bin/not-my-board agent --fd 0 https:// +StandardInput=socket +StandardOutput=journal ``` If authentication is configured in the *Hub*, log in: @@ -37,7 +53,7 @@ If authentication is configured in the *Hub*, log in: $ sudo not-my-board login https:// ``` -Enable and start the service: +Enable and start the socket: ```console -$ sudo systemctl enable --now not-my-board-agent +$ sudo systemctl enable --now not-my-board-agent.socket ``` diff --git a/doc/reference/cli.md b/doc/reference/cli.md index 62fcd8f..29e47eb 100644 --- a/doc/reference/cli.md +++ b/doc/reference/cli.md @@ -11,7 +11,7 @@ Here's a description of all the commands and options `not-my-board` supports. **`export`** \[**`-h`**|**`--help`**\] \[**`--cacert`** *cacert*\] \[**`--token-cmd`** *token_cmd*\] *hub_url* *export_description* : Make connected boards and equipment available in the board farm. -**`agent`** \[**`-h`**|**`--help`**\] \[**`--cacert`** *cacert*\] \[**`--token-cmd`** *token_cmd*\] *hub_url* +**`agent`** \[**`-h`**|**`--help`**\] \[**`--cacert`** *cacert*\] \[**`--token-cmd`** *token_cmd*\] \[**`--fd`** *fd*\] *hub_url* : Start an *Agent*. **`login`** \[**`-h`**|**`--help`**\] \[**`-v`**|**`--verbose`**\] \[**`--cacert`** *cacert*\] *hub_url* @@ -66,6 +66,11 @@ This option is an alternative to the `login` command. It can be used in non-interactive environments. ``` +```{option} --fd fd +Use file descriptor *fd*, instead of creating the listening socket. Should be a +Unix domain socket with the address `/run/not-my-board-agent.sock`. +``` + ```{option} hub_url HTTP or HTTPS URL of the *Hub*. ``` diff --git a/not_my_board/_agent.py b/not_my_board/_agent.py index 864ea2c..fc5fa21 100644 --- a/not_my_board/_agent.py +++ b/not_my_board/_agent.py @@ -7,6 +7,7 @@ import logging import pathlib import shutil +import socket import traceback import urllib.parse import weakref @@ -24,9 +25,10 @@ class AgentIO: - def __init__(self, hub_url, http_client): + def __init__(self, hub_url, http_client, unix_server_fd=None): self._hub_url = hub_url self._http = http_client + self._unix_server_fd = unix_server_fd @contextlib.asynccontextmanager async def hub_rpc(self): @@ -36,10 +38,15 @@ async def hub_rpc(self): @contextlib.asynccontextmanager async def unix_server(self, api_obj): - socket_path = pathlib.Path("/run") / "not-my-board-agent.sock" + if self._unix_server_fd is not None: + s = socket.socket(fileno=self._unix_server_fd) + else: + socket_path = pathlib.Path("/run") / "not-my-board-agent.sock" + if socket_path.is_socket(): + socket_path.unlink(missing_ok=True) - connection_handler = functools.partial(self._handle_unix_client, api_obj) - async with util.UnixServer(connection_handler, socket_path) as unix_server: + s = socket.socket(family=socket.AF_UNIX) + s.bind(socket_path.as_posix()) socket_path.chmod(0o660) try: shutil.chown(socket_path, group="not-my-board") @@ -48,6 +55,8 @@ async def unix_server(self, api_obj): 'Failed to change group on agent socket "%s": %s', socket_path, e ) + connection_handler = functools.partial(self._handle_unix_client, api_obj) + async with util.UnixServer(connection_handler, sock=s) as unix_server: yield unix_server @staticmethod diff --git a/not_my_board/cli/__init__.py b/not_my_board/cli/__init__.py index 764b92c..bfbbdd6 100644 --- a/not_my_board/cli/__init__.py +++ b/not_my_board/cli/__init__.py @@ -72,6 +72,9 @@ def add_cacert_arg(subparser): subparser.set_defaults(verbose=True) add_cacert_arg(subparser) subparser.add_argument("--token-cmd", help="generate ID tokens with shell command") + subparser.add_argument( + "--fd", type=int, help="listen on socket from this file descriptor" + ) subparser.add_argument("hub_url", help="http(s) URL of the hub") subparser = add_subcommand("reserve", help="reserve a place") @@ -174,11 +177,12 @@ async def _export_command(args): async def _agent_command(args): http_client = http.Client(args.cacert) - io = agent.AgentIO(args.hub_url, http_client) + io = agent.AgentIO(args.hub_url, http_client, args.fd) token_src = _token_src(args, http_client) async with agent.Agent(args.hub_url, io, token_src) as agent_: - print("ready", flush=True) + if agrs.fd is None: + print("ready", flush=True) await agent_.serve_forever()