diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd555d8..6dd2c80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,17 @@ jobs: - run: pip install -e ".[dev]" - run: pytest + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: pip install -e ".[docs]" + - run: sphinx-build -b html -W --keep-going docs docs/_build/html + build: name: Build (${{ matrix.os }}) needs: [format, typecheck, test] diff --git a/Makefile b/Makefile index eabae8f..704cf30 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all client server cli test clean i18n-extract i18n-init i18n-update i18n-compile +.PHONY: all client server cli test clean docs docs-clean i18n-extract i18n-init i18n-update i18n-compile # CLI localization (nexus-cli strings only; daemon/server logs stay in English). LOCALE_DIR := src/nexus_sync/locale @@ -38,5 +38,12 @@ i18n-compile: test: pytest +# Build the Sphinx HTML documentation into docs/_build/html. +docs: + sphinx-build -b html docs docs/_build/html + +docs-clean: + rm -rf docs/_build + clean: - rm -rf build dist *.spec + rm -rf build dist *.spec docs/_build diff --git a/docs/api.md b/docs/api.md index 3b964e3..c07f205 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,15 +2,15 @@ ## Transport -- Протокол: HTTPS. -- Формат тела запроса и ответа: JSON. -- Префикс API: `/api/v1`. -- Формат времени: RFC 3339 / ISO 8601, например `2026-05-24T13:20:30Z`. -- Авторизация клиента: bearer token в заголовке `Authorization`. +- Protocol: HTTPS. +- Request and response body format: JSON. +- API prefix: `/api/v1`. +- Time format: RFC 3339 / ISO 8601, e.g. `2026-05-24T13:20:30Z`. +- Client authorization: bearer token in the `Authorization` header. -Пример заголовков: +Example headers: -```http +```text Content-Type: application/json Authorization: Bearer ``` @@ -19,12 +19,12 @@ Authorization: Bearer ### `POST /api/v1/client/heartbeat` -Клиент вызывает эту ручку на каждом polling tick. Одна и та же ручка -используется для трёх сценариев: +The client calls this endpoint on every polling tick. The same endpoint serves +three scenarios: -- сообщение "я жив"; -- регулярное обновление состояния; -- отправка результата последней завершённой команды. +- an "I'm alive" message; +- a regular state update; +- reporting the result of the last finished command. ### Request @@ -45,19 +45,19 @@ Authorization: Bearer } ``` -Поля: +Fields: -- `client_id` - стабильный идентификатор, заданный при настройке клиента. Он не - должен меняться при каждом рестарте. -- `observed_at` - момент, когда клиент подготовил payload. -- `client.hostname` - текущий hostname машины. -- `client.platform` - платформа клиента. Желательно использовать названия, - близкие к Python/platform: `linux`, `darwin`, `windows`. -- `client.version` - версия nexus-sync client. -- `state` - намеренно маленький объект. Может быть сюда позже можно добавить IP, - disk usage, memory, battery status и другие метрики. -- `last_command_result` - `null`, если клиенту нечего нового сообщать о - выполнении команды. +- `client_id` - a stable identifier set during client setup. It must not change + on every restart. +- `observed_at` - the moment the client prepared the payload. +- `client.hostname` - the machine's current hostname. +- `client.platform` - the client platform. Prefer names close to + Python/platform: `linux`, `darwin`, `windows`. +- `client.version` - the nexus-sync client version. +- `state` - an intentionally small object. IP, disk usage, memory, battery + status and other metrics may be added here later. +- `last_command_result` - `null` if the client has nothing new to report about + command execution. ### Request with command result @@ -86,17 +86,16 @@ Authorization: Bearer } ``` -Поля результата: +Result fields: -- `command_id` должен совпадать с id команды, которую сервер ранее вернул - клиенту. -- `status` принимает одно из значений: `succeeded`, `failed`, `timed_out`, - `rejected`. -- `return_code` - exit code процесса, если процесс был запущен. Для - `timed_out` или `rejected` поле может быть `null`, если финального exit code - нет. -- `stdout` и `stderr` клиент должен ограничивать по размеру перед отправкой. - Конкретный лимит байт в этом контракте пока не фиксируется. +- `command_id` must match the id of the command the server previously returned + to the client. +- `status` takes one of: `succeeded`, `failed`, `timed_out`, `rejected`. +- `return_code` - the process exit code, if the process was started. For + `timed_out` or `rejected` the field may be `null` when there is no final exit + code. +- `stdout` and `stderr` must be size-limited by the client before sending. This + contract does not yet fix a specific byte limit. ### Response without command @@ -126,36 +125,37 @@ Authorization: Bearer } ``` -Поля команды: +Command fields: -- `id` генерируется сервером и должен быть уникальным. -- `kind` описывает тип executor. Пока определён только `exec`. -- `name` - имя command preset. -- `args` содержит аргументы, специфичные для конкретного preset. -- `timeout_seconds` - максимальное время выполнения, которое сервер допускает - для этой команды. +- `id` is generated by the server and must be unique. +- `kind` describes the executor type. Only `exec` is defined so far. +- `name` - the command preset name. +- `args` holds arguments specific to a particular preset. +- `timeout_seconds` - the maximum execution time the server allows for this + command. -Клиент обязан выполнять только те команды, которые он знает и локально -разрешает. Неизвестные или запрещённые команды нужно возвращать как `rejected`. +The client must run only commands it knows and locally allows. Unknown or +forbidden commands must be returned as `rejected`. ## Server-side API -Эти ручки нужны серверной части/админке, чтобы видеть клиентов и ставить им -команды в очередь. Клиенты напрямую используют только heartbeat. +These endpoints are for the server/admin side, to see clients and queue commands +for them. Clients directly use only the heartbeat. ### `GET /api/v1/server/clients` -Возвращает список известных клиентов с последними heartbeat-данными и +Returns the list of known clients with their latest heartbeat data and `available_commands`. ### `GET /api/v1/server/clients/{client_id}` -Возвращает одного клиента или `404`, если сервер ещё не видел этот `client_id`. +Returns a single client, or `404` if the server has not seen this `client_id` +yet. ### `POST /api/v1/server/clients/{client_id}/commands` -Создаёт pending-команду для клиента. Клиент получит её на следующем heartbeat. -Эта ручка принимает имя команды напрямую. +Creates a pending command for the client. The client receives it on its next +heartbeat. This endpoint accepts the command name directly. Request: @@ -188,66 +188,66 @@ Response: ### `GET /api/v1/server/commands/{command_id}` -Возвращает команду и, если клиент уже отчитался, поле `result`. +Returns the command and, if the client has already reported, the `result` field. ## Command lifecycle -Жизненный цикл команды: +Command lifecycle: -1. `pending`: команда создана на сервере и ещё не доставлена клиенту. -2. `delivered`: команда была возвращена клиенту в heartbeat response. -3. `succeeded`: клиент сообщил об успешном выполнении. -4. `failed`: клиент сообщил об ошибке выполнения. -5. `timed_out`: клиент сообщил о timeout. -6. `rejected`: клиент отказался выполнять команду. +1. `pending`: the command was created on the server and not yet delivered to the + client. +2. `delivered`: the command was returned to the client in a heartbeat response. +3. `succeeded`: the client reported successful execution. +4. `failed`: the client reported an execution error. +5. `timed_out`: the client reported a timeout. +6. `rejected`: the client refused to run the command. -Терминальные статусы: `succeeded`, `failed`, `timed_out`, `rejected`. +Terminal statuses: `succeeded`, `failed`, `timed_out`, `rejected`. ## HTTP statuses -Heartbeat endpoint должен использовать такие HTTP-статусы: +The heartbeat endpoint should use these HTTP statuses: -- `200 OK`: heartbeat принят; тело ответа соответствует контракту выше. -- `400 Bad Request`: некорректный JSON или невалидные значения полей. -- `401 Unauthorized`: bearer token отсутствует или невалиден. -- `403 Forbidden`: token валиден, но не имеет права действовать как этот +- `200 OK`: heartbeat accepted; the response body matches the contract above. +- `400 Bad Request`: malformed JSON or invalid field values. +- `401 Unauthorized`: the bearer token is missing or invalid. +- `403 Forbidden`: the token is valid but not allowed to act as this `client_id`. -- `409 Conflict`: результат ссылается на неизвестную, уже терминальную или - несовместимую команду. -- `429 Too Many Requests`: клиент опрашивает сервер слишком часто. -- `500 Internal Server Error`: неожиданная ошибка сервера. +- `409 Conflict`: the result references an unknown, already terminal, or + incompatible command. +- `429 Too Many Requests`: the client polls the server too often. +- `500 Internal Server Error`: an unexpected server error. -Состояние выполнения команды выражается полями в JSON, а не HTTP-ошибками. -Например, команда с exit code `1` всё равно отправляется через успешный +Command execution state is expressed via JSON fields, not HTTP errors. For +example, a command with exit code `1` is still delivered over a successful heartbeat request. ## Polling rules -Ответ сервера содержит `next_poll_after_seconds`. +The server response contains `next_poll_after_seconds`. -Поведение: +Behavior: -- обычный polling interval без команды: 60 секунд; -- polling interval после получения команды: 10 секунд; -- клиентский минимальный interval: 5 секунд; -- клиентский максимальный interval: 300 секунд. +- normal polling interval without a command: 60 seconds; +- polling interval after receiving a command: 10 seconds; +- client minimum interval: 5 seconds; +- client maximum interval: 300 seconds. -Клиент должен воспринимать значение сервера как рекомендацию и зажимать его в -локальные min/max границы. +The client should treat the server value as a recommendation and clamp it to its +local min/max bounds. ## Security constraints -Необходимо соблюдать эти ограничения: +The following constraints must be respected: -- не выполнять произвольные shell-строки, полученные от сервера; -- выполнять только локально известные command presets; -- требовать bearer token для клиентских endpoint'ов; -- включать `command_id` в каждый результат, чтобы не было неоднозначного - сопоставления; -- хранить command output как логи, а не как доверенные управляющие данные; -- ограничивать размер command output перед отправкой на сервер; -- выполнять каждую команду с timeout. +- do not run arbitrary shell strings received from the server; +- run only locally known command presets; +- require a bearer token for client endpoints; +- include `command_id` in every result to avoid ambiguous matching; +- store command output as logs, not as trusted control data; +- limit the size of command output before sending it to the server; +- run every command with a timeout. -Эти ограничения намеренно являются частью контракта, потому что проект -занимается удалённым управлением машинами. Дешевле строить первую реализацию -вокруг них, чем добавлять их задним числом. +These constraints are intentionally part of the contract because the project +deals with remote machine management. It is cheaper to build the first +implementation around them than to add them after the fact. diff --git a/docs/cli.md b/docs/cli.md index b3fd5c2..2b2bd82 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,4 +1,4 @@ -# nexus-cli +# CLI `nexus-cli` is a small non-interactive CLI for the nexus-sync server API. diff --git a/docs/client.md b/docs/client.md index 9f089f1..120be10 100644 --- a/docs/client.md +++ b/docs/client.md @@ -1,14 +1,17 @@ # Client -Основная суть - кидает на известный по ip/домену сервак свой ключ и базовую информацию о себе: hostname, местное время (что-то ещё?) +The core idea - it sends its key and basic info about itself (hostname, local +time, anything else?) to a server known by ip/domain. -В ответ может получить как простое "ок", так и команду для выполнения +In response it can get either a plain "ok" or a command to execute. -Запросы на сервер будет кидать с некоторой частотой, постепенно возрастающей, но резко снижающейся при получении команды в ответе (вдруг надо ещё что-то выполнить) +It sends requests to the server at a certain frequency, gradually increasing but +dropping sharply when a command is received in the response (in case something +else needs to be executed). -Есть установление нижнего предела на частоту (условная 1/минута) +There is a lower bound on the frequency (roughly 1/minute). -Пример работы +Example flow ``` ... @@ -25,18 +28,18 @@ client -> server: hello!, i'm $(hostname), uuid= , ts=, stdout= , stderr= , ... *waits 23s* ``` -Возможно нужна доп инфа о работе команды (код ошибки как минимум) +Additional info about command execution may be needed (at least the error code). -+ возможно на клиенте стоит ограничить набор допустимых команд -+ стоит явно задуматься о шифровании/идентификации сервера ++ the set of allowed commands should probably be limited on the client ++ encryption / server identification should be considered explicitly -## Разрешённые команды +## Allowed commands -Клиент исполняет только команды, описанные в локальном YAML config-файле. -Серверу отправляются только `name` и `description`; поле `cmd` остаётся только -на клиенте и не управляется сервером. +The client runs only the commands described in its local YAML config file. Only +`name` and `description` are sent to the server; the `cmd` field stays on the +client and is not controlled by the server. -Пример: +Example: ```yaml server_url: "http://127.0.0.1:5852" @@ -52,6 +55,6 @@ allowed_commands: logging_level: "INFO" ``` -Файл ищется как `nexus.yml`/`nexus.yaml` в текущей директории, затем в -`$XDG_CONFIG_HOME`, затем в `~/.config`, затем как +The file is looked up as `nexus.yml`/`nexus.yaml` in the current directory, then +in `$XDG_CONFIG_HOME`, then in `~/.config`, then as `~/.config/nexus/config.yml`/`.yaml`. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..1ebabc9 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,39 @@ +"""Sphinx configuration for the nexus-sync documentation.""" + +import os +import sys + +# Make the package importable for autodoc (sources live under src/). +sys.path.insert(0, os.path.abspath("../src")) + +project = "nexus-sync" +author = "nexus-sync" +copyright = "2026, nexus-sync" +release = "0.1.0" + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", +] + +# Pull docstrings even from undocumented members so the API pages are useful +# while docstring coverage grows. +autosummary_generate = True +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, +} +autodoc_typehints = "description" + +# MyST so the existing Markdown guides render as-is. +myst_enable_extensions = ["colon_fence", "deflist"] +source_suffix = {".md": "markdown", ".rst": "restructuredtext"} + +exclude_patterns = ["_build", ".DS_Store", "Thumbs.db", "archive"] + +html_theme = "furo" +html_title = "nexus-sync" diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2fdd304 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,24 @@ +# nexus-sync + +A utility that allows you to manage your computers from a centralized server. + +nexus-sync has three parts: a central **server** (FastAPI), a **client** agent +that runs on each managed machine and executes a whitelist of commands, and +**nexus-cli**, the operator's command-line tool. + +```{toctree} +:maxdepth: 2 +:caption: Guide + +api +cli +client +server +``` + +```{toctree} +:maxdepth: 2 +:caption: Reference + +reference +``` diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..0fe61c8 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,43 @@ +# API Reference + +Auto-generated from the source docstrings. + +## CLI (`nexus-cli`) + +```{eval-rst} +.. automodule:: nexus_sync.cli.__main__ +``` + +## Client + +```{eval-rst} +.. automodule:: nexus_sync.client.runtime + +.. automodule:: nexus_sync.client.execute +``` + +## Server + +```{eval-rst} +.. automodule:: nexus_sync.server.app + +.. automodule:: nexus_sync.server.store + +.. automodule:: nexus_sync.server.sqlalchemy_store + +.. automodule:: nexus_sync.server.config +``` + +## Common + +```{eval-rst} +.. automodule:: nexus_sync.common.models +``` + +## Utilities + +```{eval-rst} +.. automodule:: nexus_sync.i18n + +.. automodule:: nexus_sync.utils.log_config +``` diff --git a/docs/server.md b/docs/server.md index a71056a..77193e7 100644 --- a/docs/server.md +++ b/docs/server.md @@ -1,50 +1,59 @@ # Server TODO: -- описать формально все API -- добавить сами команды, возможно сделать их в виде пресетов для платформы +- formally describe the whole API +- add the commands themselves, possibly as per-platform presets -## Основная суть +## Core idea -- API ручки. Доступ чисто по ssl (но это уже зона ответственности nginx) -- клиенты по своему некоторому ключу будут авторизовываться. - - проблема ручной настройки (но она тут будто минимальная, не так плохо) -- когда клиент что-либо присылает, в ответ ему надо кинуть команды на исполнение, если есть -- со стороны клиента должен быть настроен trust к серверу +- API endpoints. Access purely over SSL (but that's already nginx's + responsibility). +- clients authorize with some key of their own. + - the manual setup problem (but it seems minimal here, not too bad) +- when a client sends anything, the response should include commands to execute, + if any. +- the client side must be configured to trust the server. -Для сервера необходимо несколько настроек +The server needs several settings -- лимит на кол-во запросов (от одного клиента, условно) -- лимит на хранение инфо (и хранить ли старое? да, логи) - - лимит на одного юзера - - общий лимит -- кастомные команды -- отслеживание получения/выполнение команды сервером, повторные попытки, лимит попыток +- a request rate limit (per single client, roughly) +- a limit on stored info (and whether to keep old data? yes, logs) + - per-user limit + - global limit +- custom commands +- tracking command delivery/execution by the server, retries, attempt limit -## Возможности (API ручки) +## Capabilities (API endpoints) -'Понятия не имею, как описывать эти ручки (точнее, нет желания правильно их описывать)' +'No idea how to describe these endpoints (or rather, no desire to describe them +properly)' -### Ручки для админа +### Admin endpoints -Все пользователи будут считаться админами для удобства. Остальных нет +All users are treated as admins for convenience. There are no others. -1. получить список всех клиентов - - с отдельным параметром "только активированные" -2. получить данные по клиенту - - сюда включается вся инфа о его показателях, когда был в сети + какие команды на него доступны (да, будем их ограничивать, возможно по модели zero-trust) -3. выполнить какую-то команду - - тут вопрос в том, будем ли мы ждать ответа от нашего клиента (вряд ли) -4. получить токен, авторизация -5. добавить кастомную команду (для клиента) - - проблема в системах, если хочется универсального добавления. Выход - добавлять только для одного клиента (лучше во всём будет) +1. get the list of all clients + - with a separate "only activated" parameter +2. get data for a client + - this includes all info about its metrics, when it was last online, and which + commands are available for it (yes, we'll restrict them, possibly via a + zero-trust model) +3. run some command + - the question here is whether we'll wait for a response from our client + (probably not) +4. obtain a token, authorization +5. add a custom command (for a client) + - the problem is with systems if you want universal addition. The way out is + to add it only for a single client (better all around) -### Ручки для клиентов +### Client endpoints -Не уверен даже, что больше одной нужно будет +Not even sure more than one will be needed -1. Прислать информацию - - просто стучится со своим uuid и кидает, что знает -2. Инфо о сервере? - - возможно фетчить айпи (чтобы стучаться, если сертификат на месте), другие домены - - какие-то особые "правила", если пакет с их установкой потеряется (сам то пакет вряд ли потеряется, мы об этом узнаем сразу) +1. Send information + - just knocks with its uuid and sends what it knows +2. Info about the server? + - possibly fetch the ip (to knock on, if the certificate is in place), other + domains + - some special "rules" if the package with their installation gets lost (the + package itself is unlikely to be lost, we'll know immediately) diff --git a/pyproject.toml b/pyproject.toml index 1527625..3d91abe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,11 @@ dev = [ "pre-commit>=4.0", "Babel>=2.14.0" ] +docs = [ + "sphinx>=7.0", + "myst-parser>=2.0", + "furo>=2024.1.29" +] [project.scripts] nexus-cli = "nexus_sync.cli.__main__:main"