first commit

This commit is contained in:
Leonid Pershin
2026-08-21 02:42:48 +03:00
commit 167d07a733
46 changed files with 3334 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
# Архитектура
## Обзор
```
┌─ локальная машина (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 / качалка в UI → delete self │
│ application credential (compute delete/shelve only) │
│ │
│ 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` | Typer: `up`, `tunnel`, `open`, `status`, `stop`, `destroy`, `logs`, `ssh`, `doctor`, `hold`, `seed-*`, `push-models`, `pull-output`, `resize-data`, `dry-run` |
| `config` | `.env`, валидация, пути к ключам |
| `state` | JSON сессии: ids, фаза, timestamps |
| `os_client` | `openstacksdk`, refresh IAM-токена |
| `inventory` | Flavors/images в сегменте, квоты, **фоллбек flavor** |
| `bootstrap` | Идемпотентный first-boot; после успеха — snapshot boot volume |
| `doctor` | Preflight без mutating compute |
| `civitai_seed` | Манифест + Civitai API на `.red` |
| `models_push` | SFTP `./Models`, `./Wildcards`, `./CustomWorkflows` |
| `output_pull` | Опциональный SFTP с VM `Output/` |
| `git_seed` | Clone `extensions.yaml` |
| `autocomplete_seed` | Word-list + Settings.fds |
| `notify` | Toast/звук при backend Idle |
| `tunnel` | paramiko / sshtunnel, порт **17801** |
| `watchdog` | EXPIRED → unshelve, пока туннель жив |
| `idle_killer` | systemd на VM + hold-файл + «качалка занята» |
| `reconcile` | Сироты по state и тегу `gpu-rent` |
Каталог сервисов — из 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_…`.
## Стейт-машина
```
idle
│ gpu-rent up
provisioning → bootstrapping → seeding_extensions → seeding_autocomplete → seeding_models → waiting_ui
ready_cloud ← compute жив, idle-killer вооружён
│ gpu-rent tunnel (опционально, пока ноут онлайн)
ready_tunneled ← localhost:17801
│ хостер: EXPIRED
restoring (unshelve) → ready_cloud / ready_tunneled
│ stop | idle-killer | destroy
stopping → idle
```
`ready_cloud` ≠ Nova `ACTIVE`. ACTIVE бывает раньше SSH и UI.
## 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` — абсолютное продление. `--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` здесь закрывает туннель, **не** вызывает `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, Typer, Rich, questionary, dotenv |
| SSH | paramiko + sshtunnel (Windows без системного `ssh -L`) |
| Конфиг | `%USERPROFILE%\.gpu-rent\.env` |
| Лицензия | MIT |