Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ready notification #66

Merged
merged 9 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ not-my-board is a tool to manage a pool of embedded hardware setups and to
schedule and provide access to those setups over a local network. The concept is
known as a *board farm*.

Check out the **[Documentation]** for installation and usage information
details.
Check out the **[Documentation]** for installation and usage information.

[Documentation]: http://not-my-board.readthedocs.io
30 changes: 23 additions & 7 deletions doc/how-to-guides/set-up-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<my-hub-address>` 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 `<my-hub-address>` with the address or
domain name of the *Hub*):
```{code-block} systemd
:caption: /etc/systemd/system/not-my-board-agent.service
Expand All @@ -26,18 +43,17 @@ domain name of the *Hub*):
Description=Board Farm Agent

[Service]
ExecStart=/usr/local/bin/not-my-board agent https://<my-hub-address>

[Install]
WantedBy=multi-user.target
ExecStart=/usr/local/bin/not-my-board agent --fd 0 https://<my-hub-address>
StandardInput=socket
StandardOutput=journal
```

If authentication is configured in the *Hub*, log in:
```console
$ sudo not-my-board login https://<my-hub-address>
```

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
```
4 changes: 2 additions & 2 deletions doc/installation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Installation

Since you'll need to run `not-my-board` as root user to export USB devices,
you should install it globally in an isolated environment with [`pipx`][1].
Since you'll need to run `not-my-board` as root user for some operations, you
should install it globally in an isolated environment with [`pipx`][1].

[1]: https://pypa.github.io/pipx/

Expand Down
9 changes: 7 additions & 2 deletions doc/reference/cli.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CLI Interface
# Command Line Interface

Here's a description of all the commands and options `not-my-board` supports.

Expand All @@ -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*
Expand Down Expand Up @@ -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*.
```
Expand Down
17 changes: 13 additions & 4 deletions not_my_board/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import pathlib
import shutil
import socket
import traceback
import urllib.parse
import weakref
Expand All @@ -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):
Expand All @@ -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")
Expand All @@ -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
Expand Down
48 changes: 35 additions & 13 deletions not_my_board/_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,22 @@


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

import uvicorn

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

host = "0.0.0.0" # noqa: S104
port = 2092
s.bind((host, port))

s.listen()
print("ready", flush=True) # noqa: T201

fd = s.fileno()
uvicorn.main([f"--fd={fd}", __name__ + ":asgi_app"])


async def asgi_app(scope, receive, send):
Expand All @@ -46,18 +61,7 @@ async def _handle_lifespan(scope, receive, send):
message = await receive()
if message["type"] == "lifespan.startup":
try:
config_file = os.environ.get("NOT_MY_BOARD_HUB_CONFIG")
if not config_file:
config_file = "/etc/not-my-board/hub.toml"
config_file = pathlib.Path(config_file)

if config_file.exists():
config = util.toml_loads(config_file.read_text())
else:
config = {}

hub = Hub(config, http.Client())
await hub.startup()
hub = await _on_startup()
scope["state"]["hub"] = hub
except Exception as err:
await send({"type": "lifespan.startup.failed", "message": str(err)})
Expand All @@ -75,6 +79,24 @@ async def _handle_lifespan(scope, receive, send):
logger.warning("Unknown lifespan message %s", message["type"])


async def _on_startup():
config_file = os.environ.get("NOT_MY_BOARD_HUB_CONFIG")
if not config_file:
config_file = "/etc/not-my-board/hub.toml"
config_file = pathlib.Path(config_file)

if config_file.exists():
config = util.toml_loads(config_file.read_text())
else:
logger.warning('Config file "%s" not found', config_file)
config = {}

hub = Hub(config, http.Client())
await hub.startup()

return hub


@asgineer.to_asgi
async def _handle_request(request):
hub = request.scope["state"]["hub"]
Expand Down
1 change: 1 addition & 0 deletions not_my_board/_usbip.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ async def _main():
server = util.Server(usbip_server.handle_client, port=args.port)
async with server:
logger.info("listening")
print("ready", flush=True) # noqa: T201
await server.serve_forever()


Expand Down
8 changes: 7 additions & 1 deletion not_my_board/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -168,15 +171,18 @@ async def _export_command(args):
args.hub_url, args.export_description, http_client, token_src
) as exporter:
await exporter.register_place()
print("ready", flush=True)
await exporter.serve_forever()


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_:
if args.fd is None:
print("ready", flush=True)
await agent_.serve_forever()


Expand Down
2 changes: 1 addition & 1 deletion tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ async def port_forward(self, ready_event, proxy, target, local_port):
del self.port_forwards[local_port]


@pytest.fixture()
@pytest.fixture
async def agent_io():
io = FakeAgentIO()
async with agentmodule.Agent(HUB_URL, io, None) as agent:
Expand Down
8 changes: 4 additions & 4 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,12 @@ async def send(self, data):
await self._send(data)


@pytest.fixture()
@pytest.fixture
def http_client():
return FakeHttpClient()


@pytest.fixture()
@pytest.fixture
def hub(http_client):
config = {
"auth": {
Expand All @@ -201,7 +201,7 @@ def hub(http_client):
return hubmodule.Hub(config, http_client)


@pytest.fixture()
@pytest.fixture
def token_store_path():
path = pathlib.Path(__file__).parent / "auth_tokens.json"
path.unlink(missing_ok=True)
Expand Down Expand Up @@ -352,7 +352,7 @@ async def test_permission_lost(hub, http_client, token_store_path, fake_time):
assert "Permission lost" in str(execinfo.value)


@pytest.fixture()
@pytest.fixture
def fake_time(monkeypatch):
fake_time_ = FakeTime()
fake_datetime = fake_time_.fake_datetime()
Expand Down
14 changes: 5 additions & 9 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,14 @@ async def tinyproxy():


async def test_proxy_connect(tinyproxy):
async with sh_task("not-my-board hub", "hub"):
await wait_for_ports(2092)

async with sh_task("not-my-board hub", "hub", wait_ready=True):
client = http.Client(proxies={"http": tinyproxy})
response = await client.get_json("http://127.0.0.1:2092/api/v1/places")
assert response == {"places": []}


async def test_proxy_ignore():
async with sh_task("not-my-board hub", "hub"):
await wait_for_ports(2092)

async with sh_task("not-my-board hub", "hub", wait_ready=True):
client = http.Client(
proxies={"http": "http://non-existing.localhost", "no": "127.0.0.1"}
)
Expand Down Expand Up @@ -183,7 +179,7 @@ async def get_written(self):
return await self._queue.get()


@pytest.fixture()
@pytest.fixture
def fake_server(monkeypatch):
fake_server_ = FakeServer()
monkeypatch.setattr(util, "connect", fake_server_.connect)
Expand Down Expand Up @@ -260,7 +256,7 @@ def _send(self, conn, events):
self._reader.feed(data)


@pytest.fixture()
@pytest.fixture
def fake_time(monkeypatch):
fake_time_ = FakeTime()
fake_datetime = fake_time_.fake_datetime()
Expand Down Expand Up @@ -288,7 +284,7 @@ def add_time(self, time):
self._now += time


@pytest.fixture()
@pytest.fixture
def http_client():
return http.Client(proxies={})

Expand Down
2 changes: 1 addition & 1 deletion tests/test_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
DEFAULT_AGENT_IP = "6.1.1.1"


@pytest.fixture()
@pytest.fixture
def hub():
return hubmodule.Hub()

Expand Down
2 changes: 1 addition & 1 deletion tests/test_jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def is_empty(self):
Fakes = collections.namedtuple("Fakes", ["channel", "api", "transport"])


@pytest.fixture()
@pytest.fixture
async def fakes():
transport = FakeTransport()
api = FakeApi()
Expand Down
Loading
Loading