# Архитектура ## Обзор ``` ┌─ локальная машина (Windows / Linux) ─────────────────────────┐ │ gpu-rent CLI │ │ up / stop / status / doctor / hold │ │ tunnel / open — SSH localhost:17801 → VM :7801 │ │ state /.gpu-rent/state.json │ │ │ │ браузер / MCP / curl API → http://127.0.0.1:17801 │ │ локальный SwarmUI → http://127.0.0.1:7801 (не трогаем) └───────────────────────────────┬──────────────────────────────┘ │ SSH :22 и OpenStack API ▼ ┌─ Selectel, сегмент пула (например ru-7a) ────────────────────┐ │ GPU VM (tag preemptible + gpu-rent) │ │ SwarmUI : 127.0.0.1:7801 │ │ idle-killer: очередь / hold / LLM busy → delete this server │ │ app cred: DELETE/GET только этот server_id (fail closed) │ │ optional: Ollama :11434 (туннель 17811) │ │ │ │ boot volume (network) ОС + NVIDIA + SwarmUI нативно + snapshot │ │ data volume (network) Models, Output, Data, workflows │ │ floating IP только SSH, удаляется на stop │ └──────────────────────────────────────────────────────────────┘ ``` Два сетевых диска. После удаления VM boot-диск снова указывают как `--volume` при create. Разделение **жизни GPU** и **локального туннеля** — следствие [decisions.md](decisions.md): ноут можно закрыть. ## Слои CLI | Модуль | Ответственность | | --- | --- | | `cli` / `term` / `prompts` | Typer + цветной лог + нумерованные меню | | `config` / `varsfile` | `.env` + `gpu-rent.vars`, пути | | `state` | JSON сессии: ids, фаза, timestamps | | `os_client` / `cloud` / `pools` | openstacksdk, ресурсы, скан пулов | | `inventory` / `ux` | Flavors, квоты; preview / ServerPlan (flavor/disk/spot) | | `session` | `cmd_up` / `cmd_stop` / adopt | | `bootstrap` + `remote/bootstrap.sh` | Идемпотентный first-boot; light без apt | | `provision` | extensions, autocomplete, civitai seed, LLM, push, idle-killer arm, start SwarmUI | | `capture` | Инвентарь VM → merge ссылок в локальные манифесты | | `doctor` | Preflight без mutating compute | | `sync_files` | SFTP `Models` / Wildcards / workflows / Output | | `notify` | Toast/звук при backend Idle | | `tunnel` | sshtunnel + Nova EXPIRED watchdog | | `local_watchdog` | Опциональный локальный тик → stop при unclean exit | | `llm_runtime` / `setup_wizard` | Opt-in Ollama + `ollama-models.yaml` | | `idle_killer` / `hold` | systemd на VM + hold-файл | | `ready` / `snapshot` | Idle backend + boot snapshot | | `access_card` | URL / MCP panel после ready | Каталог сервисов — из Keystone, не из выдуманного `api.selectel.ru/v3/`. ## Два диска ### Boot volume Сетевой, тот же сегмент, что VM. Первый раз — GPU-optimized образ **без Docker**. `delete_on_termination=false`. Локальный boot запрещён (preemptible сотрёт ОС). После bootstrap: NVIDIA из образа, `/opt/swarmui` + systemd, unit idle-killer, application credential в `/root/.gpu-rent/` (mode 600). После **первого** успешного `waiting_ui` + backend Idle: один snapshot boot-диска `gpu-rent-boot-ok` (если ещё нет). Следующий create VM может идти из snapshot, не из сырого GPU-образа. Старый snapshot не плодить каждый `up`. ### Data volume Второй BDM при create (не attach после ACTIVE). FS один раз, маркер `/mnt/swarm_data/.gpu-rent-ready`. Первый seed: [extensions.md](extensions.md), [autocomplete.md](autocomplete.md), [models.md](models.md), [local-folders.md](local-folders.md). | На хосте (data volume) | В дереве SwarmUI (`/opt/swarmui`, bind) | | --- | --- | | `/mnt/swarm_data/Models` | `/opt/swarmui/Models` | | `/mnt/swarm_data/Output` | `/opt/swarmui/Output` | | `/mnt/swarm_data/Data` | `/opt/swarmui/Data` | | `/mnt/swarm_data/dlbackend` | `/opt/swarmui/dlbackend` | | `/mnt/swarm_data/Extensions` | `/opt/swarmui/src/Extensions` | | `/mnt/swarm_data/DLNodes` | `/opt/swarmui/src/BuiltinExtensions/ComfyUIBackend/DLNodes` | | `/mnt/swarm_data/CustomWorkflows` | `/opt/swarmui/src/BuiltinExtensions/ComfyUIBackend/CustomWorkflows` | `mkfs` только если нет маркера **и** `blkid` подтвердил пустое устройство по serial/by-id. Не хардкодить `scsi-0Selectel_Volume_…`. Remount bind’ов — только после `systemctl stop swarmui` и обычного `umount`. `umount -l` на живом Comfy позже опустошает вкладку Models (веса на data volume остаются). ## Стейт-машина Фазы в `state.json` (код пишет только эти): ``` idle │ gpu-rent up ▼ provisioning ← create volumes / server │ ▼ bootstrapping ← SSH + bootstrap + seed (пока не ready) │ ▼ ready_cloud ← compute жив, idle-killer вооружён │ │ gpu-rent tunnel (или up с туннелем) ▼ ready_tunneled ← localhost:17801 │ │ stop | idle-killer | local-watchdog | destroy ▼ idle ``` Подшаги seed (extensions / autocomplete / models) идут внутри `bootstrapping`, отдельными фазами в state не пишутся. `ready_cloud` ≠ Nova `ACTIVE` (ACTIVE бывает раньше SSH и UI). На EXPIRED туннель сам делает unshelve → снова `bootstrapping`/`ready_*`. ## Idle-killer (на VM) Пока очередь SwarmUI пуста дольше **30 минут**, скрипт удаляет **этот** compute через OpenStack (диски не трогать). Открытый браузер без джобы жизнь **не** продлевает. Killer молчит: - clone расширений, Civitai-seed, push локальных папок; - первые **45 минут** после ACTIVE или unshelve; - пока backend/ComfyUI ещё не Idle; - пока существует hold: файл `/mnt/swarm_data/.gpu-rent-hold-until` с unix ts (пишет `gpu-rent hold`); - пока SwarmUI качает модель в UI (Model Downloader / активный download — точный JSON на spike). Нет сигнала — пользователь жмёт `hold`. `gpu-rent hold` без аргументов = +`IDLE_MINUTES` от сейчас. `--minutes 90` — hold до now+90 мин (заменяет предыдущий, не складывает). `--until` ISO опционально. `hold --clear` снимает. Потом счётчик 30 минут пустой очереди. Почему не `shutdown -h now`: у Selectel останов из гостя не обязан снять GPU с биллинга. Нужен API delete/shelve. Почему не только локальный CLI: ноут спит — процесса нет — GPU продолжает тарифицироваться. Учётные данные на VM: application credential (или роль без `compute:create`), только delete/shelve. Файл не попадает в Output/Models. Истечение кредов = killer слеп; тогда спасает `gpu-rent status` с ноутбука. Дефолты: `IDLE_MINUTES=30`, `IDLE_GRACE_MINUTES=45`. ## Watchdog туннеля (на ноутбуке) Работает только пока открыт туннель: 1. Refresh IAM-токена (TTL 24 ч, как у preemptible). 2. `EXPIRED` → unshelve → переоткрыть туннель. 3. `ERROR` / нет GPU → выход, не бесконечный recreate. 4. Туннель мёртв при ACTIVE → reconnect. `Ctrl+C` и `Ctrl+D` вызывают `stop` (диски остаются). ## Teardown (`gpu-rent stop`) Compute удаляется через OpenStack без SSH. Optional pull Output — только если VM ещё отвечает по SSH: 1. Если `PULL_OUTPUT` и SSH жив — забрать новые файлы в `./Output` (`--no-pull` пропускает). 2. Закрыть туннель этого процесса, если открыт. 3. Удалить compute. Volumes оставить. 4. Дождаться исчезновения сервера. 5. Удалить floating IP (`KEEP_FLOATING_IP=false`). 6. State → `idle`, сохранить volume ids. `destroy` — то же + диски после `--i-understand-data-loss`. Reconcile: сервер с тегом `gpu-rent` есть, локального процесса нет — это норма (`ready_cloud`). Сирота = нет тега в state и наоборот; `status` показывает «жив, туннеля нет, idle-killer: …». ## Сеть на VM - Private net + subnet + router — один раз на пул, переиспользовать. - SG: ingress TCP/22 с IP оператора. **Не** открывать 7801 наружу. - SwarmUI: `--host 127.0.0.1 --port 7801` (systemd `swarmui`). - Туннель: `127.0.0.1:17801` → `127.0.0.1:7801` на VM. ## Стек | Слой | Выбор | | --- | --- | | Python 3.11+ | openstacksdk, paramiko, sshtunnel, Typer, Rich, dotenv, httpx, pyyaml | | SSH | paramiko + sshtunnel (Windows без системного `ssh -L`) | | Конфиг | `/.env` + `gpu-rent.vars` + runtime в `/.gpu-rent/` | | Лицензия | MIT |