Refactor environment and configuration management
- Updated the project structure to store configuration files (.env, models.yaml, extensions.yaml) in the project root instead of the user's home directory. - Enhanced the setup process to automatically copy example files to the project root on first run. - Implemented a migration function to transfer legacy configuration files from the user's home directory to the new project structure. - Revised documentation to reflect changes in file locations and setup instructions. - Improved code readability and maintainability by refactoring path management functions.
This commit is contained in:
+3
-1
@@ -2,6 +2,8 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
models.yaml
|
||||
extensions.yaml
|
||||
|
||||
# SwarmUI checkpoints / LoRA dropped into the app tree
|
||||
Models/**
|
||||
@@ -27,7 +29,7 @@ __pycache__/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# Local CLI state (if someone runs from the repo)
|
||||
# Local CLI state / SSH key (inside the repo, not under %)
|
||||
.gpu-rent/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
|
||||
@@ -29,7 +29,7 @@ chmod +x gpu-rent.sh # один раз на Unix
|
||||
./gpu-rent.sh doctor
|
||||
```
|
||||
|
||||
Ключи: скопируй `env.example` в `%USERPROFILE%\.gpu-rent\.env` (скрипт сделает это сам при первом запуске) и заполни `OS_*` по [`docs/setup.md`](docs/setup.md).
|
||||
Ключи: скопируй `env.example` в `.env` в корне репо (скрипт сделает это сам при первом запуске) и заполни `OS_*` по [`docs/setup.md`](docs/setup.md).
|
||||
|
||||
`up` поднимает compute и ставит SwarmUI нативно (`launch-linux.sh` + systemd). `tunnel` пробрасывает `localhost:17801`. `stop` гасит GPU, диски оставляет.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
│ gpu-rent CLI │
|
||||
│ up / stop / status / doctor / hold │
|
||||
│ tunnel / open — SSH localhost:17801 → VM :7801 │
|
||||
│ state ~/.gpu-rent/state.json │
|
||||
│ state <repo>/.gpu-rent/state.json │
|
||||
│ │
|
||||
│ браузер / MCP / curl API → http://127.0.0.1:17801 │
|
||||
│ локальный SwarmUI → http://127.0.0.1:7801 (не трогаем)
|
||||
@@ -169,5 +169,5 @@ Reconcile: сервер с тегом `gpu-rent` есть, локального
|
||||
| --- | --- |
|
||||
| Python 3.11+ | openstacksdk, paramiko, Typer, Rich, questionary, dotenv |
|
||||
| SSH | paramiko + sshtunnel (Windows без системного `ssh -L`) |
|
||||
| Конфиг | `%USERPROFILE%\.gpu-rent\.env` |
|
||||
| Конфиг | `<repo>/.env` + runtime в `<repo>/.gpu-rent/` |
|
||||
| Лицензия | MIT |
|
||||
|
||||
+4
-4
@@ -48,7 +48,7 @@
|
||||
3. Квота GPU > 0; иначе текст «напишите в поддержку Selectel».
|
||||
4. В сегменте есть хотя бы один flavor из `FLAVOR_PREFERENCE` (не `disabled`).
|
||||
5. Volume type сегмента существует; data 100 GB влезает в квоту дисков.
|
||||
6. SSH-ключ: есть `~/.gpu-rent/id_ed25519` или CLI сможет его создать.
|
||||
6. SSH-ключ: есть `<repo>/.gpu-rent/id_ed25519` или CLI сможет его создать.
|
||||
7. Если задан `CIVITAI_API_TOKEN` — HEAD/лёгкий запрос к `CIVITAI_API_HOST` (дефолт `.red`); манифест парсится.
|
||||
8. `extensions.yaml` парсится, если файл есть.
|
||||
9. Локальные папки: предупреждение, если `Models/` огромный относительно свободного места на будущем диске.
|
||||
@@ -98,7 +98,7 @@ Hold killer: gpu-rent hold
|
||||
|
||||
## Конфигурация
|
||||
|
||||
`%USERPROFILE%\.gpu-rent\.env` / `~/.gpu-rent/.env`:
|
||||
`.env` в корне репозитория (рядом с `env.example`):
|
||||
|
||||
```env
|
||||
OS_AUTH_URL=https://cloud.api.selcloud.ru/identity/v3
|
||||
@@ -154,7 +154,7 @@ Application credential для idle-killer CLI создаёт при bootstrap и
|
||||
|
||||
## State
|
||||
|
||||
`~/.gpu-rent/state.json` — ids, фаза (`idle` / `ready_cloud` / `ready_tunneled` / …), flavor, timestamps create/unshelve (для окна 24 ч). Без паролей. Lockfile, чтобы два `up` не создали два сервера.
|
||||
`<repo>/.gpu-rent/state.json` — ids, фаза (`idle` / `ready_cloud` / `ready_tunneled` / …), flavor, timestamps create/unshelve (для окна 24 ч). Без паролей. Lockfile там же, чтобы два `up` не создали два сервера.
|
||||
|
||||
## Ожидание готовности
|
||||
|
||||
@@ -168,6 +168,6 @@ Application credential для idle-killer CLI создаёт при bootstrap и
|
||||
|
||||
## Windows
|
||||
|
||||
Лаунчеры в корне: `gpu-rent.bat` (cmd / двойной клик), `.\gpu-rent.ps1` (PowerShell). На Unix — `./gpu-rent.sh`. Они создают `.venv` в репозитории и вызывают `python -m gpu_rent`. Пути в приложении через `Path.expanduser`. Ключ генерирует CLI: `~/.gpu-rent/id_ed25519`.
|
||||
Лаунчеры в корне: `gpu-rent.bat` (cmd / двойной клик), `.\gpu-rent.ps1` (PowerShell). На Unix — `./gpu-rent.sh`. Они создают `.venv` в репозитории и вызывают `python -m gpu_rent`. Ключ генерирует CLI: `<repo>/.gpu-rent/id_ed25519`.
|
||||
|
||||
`NOTIFY_READY`: toast через WinRT / `win10toast` (что заработает на 10/11 без админ-прав) + системный звук. Если toast недоступен — только звук и лог, не падать.
|
||||
|
||||
+4
-3
@@ -8,8 +8,8 @@
|
||||
| Сессия | GPU живёт до `stop` **или** простоя (idle-killer **на VM**). Ноут можно закрыть — compute не обязан умереть |
|
||||
| Модели | Пустой `./Models` → не грузим ничего. Появились веса + метадата → на `up` выгрузить эту пару. Civitai-seed отдельно. С сервера не удаляем |
|
||||
| Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена |
|
||||
| Манифест моделей | `~/.gpu-rent/models.yaml`, типы: checkpoint / lora / vae / embedding / controlnet / upscaler. В git только `models.example.yaml` |
|
||||
| Расширения | `~/.gpu-rent/extensions.yaml`: git-репы `swarmui` → `src/Extensions`, `comfy` → DLNodes. Клон на первом bootstrap до старта UI. Пустой файл — стоковый SwarmUI |
|
||||
| Манифест моделей | `<repo>/models.yaml`, типы: checkpoint / lora / vae / embedding / controlnet / upscaler. В git только `models.example.yaml` |
|
||||
| Расширения | `<repo>/extensions.yaml`: git-репы `swarmui` → `src/Extensions`, `comfy` → DLNodes. Клон на первом bootstrap до старта UI. Пустой файл — стоковый SwarmUI |
|
||||
| Autocomplete | До первого старта: скачать word-list в `Data/Autocompletions`, прописать `DefaultUser.AutoComplete.Source`. На каждом `up` сверить GitHub blob sha и обновить файл, если изменился. Дефолт: `tags/danbooru.csv` из a1111-sd-webui-tagcomplete (как в доке SwarmUI) |
|
||||
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
|
||||
| Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` |
|
||||
@@ -20,7 +20,8 @@
|
||||
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
||||
| `up` / `tunnel` | Разделены: `up` только облако, `tunnel` отдельно. Ctrl+C на туннеле не удаляет VM |
|
||||
| Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) |
|
||||
| SSH | CLI генерирует `~/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair |
|
||||
| SSH | CLI генерирует `<repo>/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair |
|
||||
| Локальные файлы | Всё в корне репозитория: `.env`, `models.yaml`, `extensions.yaml`; runtime (`state.json`, lock, SSH) в `<repo>/.gpu-rent/`. Не `%USERPROFILE%\.gpu-rent` |
|
||||
| Локальный порт | **17801** |
|
||||
| MCP-конфиг Cursor | Только сниппет в stdout, файл не трогаем |
|
||||
| `ready` для API | Ждать Idle backend, не только HTTP UI |
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@
|
||||
|
||||
## Манифест
|
||||
|
||||
Пример: [`extensions.example.yaml`](../extensions.example.yaml). Рабочая копия: `~/.gpu-rent/extensions.yaml` (или `EXTENSIONS_MANIFEST`).
|
||||
Пример: [`extensions.example.yaml`](../extensions.example.yaml). Рабочая копия: `extensions.yaml` в корне репо (или `EXTENSIONS_MANIFEST`).
|
||||
|
||||
```yaml
|
||||
swarmui:
|
||||
@@ -52,7 +52,7 @@ Idle-killer ещё не вооружён. Git нужен на VM (`apt` в boots
|
||||
Публичный HTTPS без секретов. Приватный репозиторий:
|
||||
|
||||
- `GIT_TOKEN` в `.env` (GitHub: `x-access-token`; GitLab: `oauth2` / PAT). CLI подставляет в URL **только на VM**, не пишет токен в yaml и не в cloud-init.
|
||||
- Либо отдельный deploy key `~/.gpu-rent/git_deploy` — v1.1, не обязательно сейчас.
|
||||
- Либо отдельный deploy key `<repo>/.gpu-rent/git_deploy` — v1.1, не обязательно сейчас.
|
||||
|
||||
Не использовать `git://`. Только `https://` и `git@` + ключ.
|
||||
|
||||
@@ -63,6 +63,6 @@ Idle-killer ещё не вооружён. Git нужен на VM (`apt` в boots
|
||||
## Конфиг
|
||||
|
||||
```env
|
||||
EXTENSIONS_MANIFEST= # пусто = ~/.gpu-rent/extensions.yaml
|
||||
EXTENSIONS_MANIFEST= # пусто = <repo>/extensions.yaml
|
||||
GIT_TOKEN= # опционально, для приватных https
|
||||
```
|
||||
|
||||
+9
-15
@@ -67,27 +67,21 @@ ComfyUI в `dlbackend` качается всегда. Речь только о *
|
||||
|
||||
## Манифест
|
||||
|
||||
Пример в репозитории: [`models.example.yaml`](../models.example.yaml). Рабочая копия: `~/.gpu-rent/models.yaml` (или путь `MODELS_MANIFEST`).
|
||||
Пример в репозитории: [`models.example.yaml`](../models.example.yaml). Рабочая копия: `models.yaml` в корне репо (или путь `MODELS_MANIFEST`).
|
||||
|
||||
Идентификатор — **Civitai model version id**, не имя файла и не «последняя версия модели» (она плывёт).
|
||||
|
||||
```yaml
|
||||
# ~/.gpu-rent/models.yaml
|
||||
# models.yaml — пример: Krea 2 + LoRA (см. models.example.yaml)
|
||||
checkpoint:
|
||||
- version_id: 2514310
|
||||
- url: https://civitai.red/models/2786499/realism-by-stable-yogi-krea2?modelVersionId=3231611
|
||||
lora:
|
||||
- version_id: 123456
|
||||
vae:
|
||||
- version_id: 789012
|
||||
embedding:
|
||||
- version_id: 111
|
||||
controlnet:
|
||||
- version_id: 222
|
||||
upscaler:
|
||||
- version_id: 333
|
||||
- url: https://civitai.red/models/2268008/realistic-snapshot-z-image-turbo-krea-2?modelVersionId=3084537
|
||||
- url: https://civitai.red/models/2187487/cutifyier?modelVersionId=3107521
|
||||
- url: https://civitai.red/models/1134895/2000s-analog-core?modelVersionId=3103914
|
||||
```
|
||||
|
||||
Допустимо вместо `version_id` поле `url` страницы модели/версии — CLI вынимает id. Хосты `civitai.com`, `civitai.red`, `civitai.green` равнозначны (зелёный редиректит на `.com`). Несколько файлов у версии: берём `files[]` с `"primary": true`, иначе SafeTensor / fp16.
|
||||
Допустимо вместо `url` поле `version_id` (число из `modelVersionId=`). Хосты `civitai.com`, `civitai.red`, `civitai.green` равнозначны (зелёный редиректит на `.com`). Несколько файлов у версии: берём `files[]` с `"primary": true`, иначе SafeTensor / fp16.
|
||||
|
||||
Типы манифеста → каталоги SwarmUI (`ModelRoot` = `Models`):
|
||||
|
||||
@@ -103,7 +97,7 @@ upscaler:
|
||||
|
||||
Тип в yaml задаёт **куда класть**, даже если Civitai назвал иначе. Если API-тип совсем другой — предупреждение, кладём по yaml.
|
||||
|
||||
В git не коммитить рабочий манифест с личными чекпоинтами. В примере — вымышленные id.
|
||||
В git не коммитить рабочий манифест с личными чекпоинтами. В `models.example.yaml` — стартовый набор Krea 2 (чекпоинт + три LoRA); скопируй и дополни под себя.
|
||||
|
||||
## Civitai API (.com / .red)
|
||||
|
||||
@@ -169,6 +163,6 @@ REST не раздвоился: те же `/api/v1/...` на обоих хост
|
||||
```env
|
||||
CIVITAI_API_TOKEN=
|
||||
CIVITAI_API_HOST=civitai.red # полный каталог; civitai.com = только SFW
|
||||
MODELS_MANIFEST= # пусто = ~/.gpu-rent/models.yaml
|
||||
MODELS_MANIFEST= # пусто = <repo>/models.yaml
|
||||
LOCAL_MODELS_DIR= # пусто = <корень приложения>/Models
|
||||
```
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
- [ ] `status`: диск used/free, ₽/час, TTL preempt 24 ч, killer/hold
|
||||
- [x] Второй `up` не создаёт второй GPU
|
||||
- [ ] `resize-data` вверх
|
||||
- [x] Генерация `~/.gpu-rent/id_ed25519` + keypair при первом `up`
|
||||
- [x] Генерация `<repo>/.gpu-rent/id_ed25519` + keypair при первом `up`
|
||||
- [x] `seed-models` на живом диске (идемпотентно, SHA256)
|
||||
- [x] `push` / `push-models`: `Models/`, `Wildcards/`, `CustomWorkflows/`
|
||||
- [x] `pull-output` и `PULL_OUTPUT` на `stop` / повторный `up`
|
||||
|
||||
+9
-8
@@ -26,7 +26,9 @@ chmod +x gpu-rent.sh
|
||||
./gpu-rent.sh --help
|
||||
```
|
||||
|
||||
Первый запуск копирует `env.example` в `%USERPROFILE%\.gpu-rent\.env` / `~/.gpu-rent/.env`, если файла ещё нет. Заполни `OS_*` (шаги ниже). После `git pull`, если изменился `pyproject.toml`, лаунчер переустановит пакет.
|
||||
Первый запуск копирует `env.example` → `.env`, `models.example.yaml` → `models.yaml`, `extensions.example.yaml` → `extensions.yaml` в **корне репозитория**, если файлов ещё нет. Заполни `OS_*` в `.env` (шаги ниже). После `git pull`, если изменился `pyproject.toml`, лаунчер переустановит пакет.
|
||||
|
||||
Секреты и runtime не уезжают в `%USERPROFILE%`: только `<repo>/.env` и `<repo>/.gpu-rent/` (state, SSH-ключ). Оба в `.gitignore`. Если раньше лежало в `~\.gpu-rent\`, CLI один раз перенесёт в проект.
|
||||
|
||||
Ручной венв по желанию (для разработки / pytest):
|
||||
|
||||
@@ -129,9 +131,8 @@ python -m gpu_rent --help
|
||||
Создай каталог и файл **вне git**:
|
||||
|
||||
```powershell
|
||||
mkdir $env:USERPROFILE\.gpu-rent
|
||||
copy env.example $env:USERPROFILE\.gpu-rent\.env
|
||||
notepad $env:USERPROFILE\.gpu-rent\.env
|
||||
copy env.example .env
|
||||
notepad .env
|
||||
```
|
||||
|
||||
Вставь значения из RC + пароль + `GPU_RENT_AZ`. Никогда не коммить `.env`.
|
||||
@@ -163,7 +164,7 @@ openstack flavor list
|
||||
Манифест (не в git со своими id, если не хочешь светить вкусы):
|
||||
|
||||
```powershell
|
||||
copy models.example.yaml $env:USERPROFILE\.gpu-rent\models.yaml
|
||||
copy models.example.yaml models.yaml
|
||||
```
|
||||
|
||||
`version_id: 0` — заглушка, doctor её игнорирует. Нужен **modelVersionId** из URL, не id карточки модели.
|
||||
@@ -180,13 +181,13 @@ copy models.example.yaml $env:USERPROFILE\.gpu-rent\models.yaml
|
||||
2. Fine-grained: доступ только к нужным репам, **Contents: Read**.
|
||||
3. `GIT_TOKEN` в `.env`.
|
||||
|
||||
Скопируй шаблон: `extensions.example.yaml` → `~\.gpu-rent\extensions.yaml`. Пустой файл = стоковый SwarmUI.
|
||||
Скопируй шаблон: `extensions.example.yaml` → `extensions.yaml` в корне репо. Пустой файл = стоковый SwarmUI.
|
||||
|
||||
---
|
||||
|
||||
## 6. SSH
|
||||
|
||||
Ключ **не надо** делать руками. CLI создаст `~\.gpu-rent\id_ed25519` без passphrase и зарегистрирует keypair в OpenStack при первом `up`. `doctor` только проверяет, что это получится.
|
||||
Ключ **не надо** делать руками. CLI создаст `<repo>\.gpu-rent\id_ed25519` без passphrase и зарегистрирует keypair в OpenStack при первом `up`. `doctor` только проверяет, что это получится.
|
||||
|
||||
---
|
||||
|
||||
@@ -197,7 +198,7 @@ copy models.example.yaml $env:USERPROFILE\.gpu-rent\models.yaml
|
||||
- [ ] Тикет на лимит **1× GPU** в нужном сегменте (или квота уже > 0)
|
||||
- [ ] Сервисный пользователь `member` на этот проект, пароль сохранён
|
||||
- [ ] RC скачан на **тот же пул**, где GPU
|
||||
- [ ] `~\.gpu-rent\.env` заполнен (`OS_*` + `GPU_RENT_AZ`)
|
||||
- [ ] `.env` в корне репо заполнен (`OS_*` + `GPU_RENT_AZ`)
|
||||
- [ ] Нет `X-Token` вместо пароля сервисного пользователя
|
||||
- [ ] (опционально) Civitai token + `models.yaml`
|
||||
- [ ] На балансе хватает на диск 100 GB **даже когда GPU выключен**
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copy to %USERPROFILE%\.gpu-rent\.env (never commit the copy)
|
||||
# Copy to .env in this repo (never commit the copy)
|
||||
# How to fill: docs/setup.md
|
||||
# X-Token from the panel does NOT work here.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copy to ~/.gpu-rent/extensions.yaml and put real repo URLs.
|
||||
# Copy to extensions.yaml in this repo and put real repo URLs.
|
||||
# Empty/missing file → no extra extensions, stock SwarmUI.
|
||||
# swarmui = C# repos cloned to src/Extensions
|
||||
# comfy = Python custom nodes cloned to ComfyUI DLNodes
|
||||
|
||||
+12
-4
@@ -73,17 +73,25 @@ if ($needInstall) {
|
||||
Get-Date -Format o | Set-Content -LiteralPath $Stamp -Encoding ascii
|
||||
}
|
||||
|
||||
$GpuHome = Join-Path $env:USERPROFILE ".gpu-rent"
|
||||
$EnvFile = Join-Path $GpuHome ".env"
|
||||
$EnvFile = Join-Path $Root ".env"
|
||||
$Example = Join-Path $Root "env.example"
|
||||
if (-not (Test-Path -LiteralPath $EnvFile)) {
|
||||
New-Item -ItemType Directory -Force -Path $GpuHome | Out-Null
|
||||
if (Test-Path -LiteralPath $Example) {
|
||||
Copy-Item -LiteralPath $Example -Destination $EnvFile
|
||||
Write-Host "gpu-rent: created $EnvFile - fill OS_* (docs/setup.md)"
|
||||
Write-Host "gpu-rent: created .env - fill OS_* (docs/setup.md)"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-IfMissing {
|
||||
param([string]$Src, [string]$Dst, [string]$Label)
|
||||
if ((Test-Path -LiteralPath $Src) -and -not (Test-Path -LiteralPath $Dst)) {
|
||||
Copy-Item -LiteralPath $Src -Destination $Dst
|
||||
Write-Host "gpu-rent: created $Label"
|
||||
}
|
||||
}
|
||||
Copy-IfMissing (Join-Path $Root "models.example.yaml") (Join-Path $Root "models.yaml") "models.yaml"
|
||||
Copy-IfMissing (Join-Path $Root "extensions.example.yaml") (Join-Path $Root "extensions.yaml") "extensions.yaml"
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
& $VenvPy -m gpu_rent @args
|
||||
exit $LASTEXITCODE
|
||||
|
||||
+11
-5
@@ -52,11 +52,17 @@ if [[ ! -f "$STAMP" || "$ROOT/pyproject.toml" -nt "$STAMP" ]]; then
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" >"$STAMP"
|
||||
fi
|
||||
|
||||
GPU_HOME="${HOME}/.gpu-rent"
|
||||
if [[ ! -f "$GPU_HOME/.env" && -f "$ROOT/env.example" ]]; then
|
||||
mkdir -p "$GPU_HOME"
|
||||
cp "$ROOT/env.example" "$GPU_HOME/.env"
|
||||
echo "gpu-rent: created $GPU_HOME/.env - fill OS_* (see docs/setup.md)"
|
||||
if [[ ! -f "$ROOT/.env" && -f "$ROOT/env.example" ]]; then
|
||||
cp "$ROOT/env.example" "$ROOT/.env"
|
||||
echo "gpu-rent: created .env - fill OS_* (see docs/setup.md)"
|
||||
fi
|
||||
if [[ ! -f "$ROOT/models.yaml" && -f "$ROOT/models.example.yaml" ]]; then
|
||||
cp "$ROOT/models.example.yaml" "$ROOT/models.yaml"
|
||||
echo "gpu-rent: created models.yaml"
|
||||
fi
|
||||
if [[ ! -f "$ROOT/extensions.yaml" && -f "$ROOT/extensions.example.yaml" ]]; then
|
||||
cp "$ROOT/extensions.example.yaml" "$ROOT/extensions.yaml"
|
||||
echo "gpu-rent: created extensions.yaml"
|
||||
fi
|
||||
|
||||
exec "$VENV_PY" -m gpu_rent "$@"
|
||||
|
||||
+10
-4
@@ -1,13 +1,19 @@
|
||||
# Copy to ~/.gpu-rent/models.yaml
|
||||
# Copy to models.yaml in this repo (never commit the working copy).
|
||||
# version_id = modelVersionId from the URL (not the model id).
|
||||
# url may be civitai.com or civitai.red — both work.
|
||||
# Requires CIVITAI_API_TOKEN. Empty file → SwarmUI default model.
|
||||
|
||||
checkpoint:
|
||||
- version_id: 0
|
||||
# - url: https://civitai.red/models/123?modelVersionId=456
|
||||
# Realism by Stable Yogi — Krea 2
|
||||
- url: https://civitai.red/models/2786499/realism-by-stable-yogi-krea2?modelVersionId=3231611
|
||||
|
||||
lora:
|
||||
- version_id: 0
|
||||
# Realistic Snapshot — Z-Image Turbo / Krea 2
|
||||
- url: https://civitai.red/models/2268008/realistic-snapshot-z-image-turbo-krea-2?modelVersionId=3084537
|
||||
# Cutifyier
|
||||
- url: https://civitai.red/models/2187487/cutifyier?modelVersionId=3107521
|
||||
# 2000s Analog Core
|
||||
- url: https://civitai.red/models/1134895/2000s-analog-core?modelVersionId=3103914
|
||||
|
||||
# vae:
|
||||
# - version_id: 0
|
||||
|
||||
+15
-10
@@ -1,4 +1,4 @@
|
||||
"""Load ~/.gpu-rent/.env. No secrets in git."""
|
||||
"""Load <project>/.env. No secrets in git."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,10 +10,13 @@ from dotenv import load_dotenv
|
||||
|
||||
from gpu_rent.errors import ConfigError
|
||||
from gpu_rent.paths import (
|
||||
app_root,
|
||||
default_ssh_key_path,
|
||||
detect_app_root,
|
||||
env_path,
|
||||
home_dir,
|
||||
extensions_manifest_path,
|
||||
migrate_legacy_if_needed,
|
||||
models_manifest_path,
|
||||
runtime_dir,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,12 +99,14 @@ def _required(name: str) -> str:
|
||||
|
||||
|
||||
def load_config(*, require_auth: bool = True) -> Config:
|
||||
home_dir().mkdir(parents=True, exist_ok=True)
|
||||
root = app_root()
|
||||
runtime_dir().mkdir(parents=True, exist_ok=True)
|
||||
migrate_legacy_if_needed()
|
||||
|
||||
env_file = env_path()
|
||||
if env_file.is_file():
|
||||
load_dotenv(env_file, override=False)
|
||||
|
||||
app_root = detect_app_root()
|
||||
missing: list[str] = []
|
||||
required = (
|
||||
"OS_AUTH_URL",
|
||||
@@ -119,7 +124,7 @@ def load_config(*, require_auth: bool = True) -> Config:
|
||||
|
||||
if require_auth and missing:
|
||||
raise ConfigError(
|
||||
"В ~/.gpu-rent/.env не хватает: "
|
||||
f"В {env_file} не хватает: "
|
||||
+ ", ".join(missing)
|
||||
+ ". Как заполнить: docs/setup.md (сервисный пользователь, не X-Token)."
|
||||
)
|
||||
@@ -128,16 +133,16 @@ def load_config(*, require_auth: bool = True) -> Config:
|
||||
ssh_key = Path(key_override).expanduser() if key_override else default_ssh_key_path()
|
||||
|
||||
models_manifest = Path(
|
||||
(os.environ.get("MODELS_MANIFEST") or "").strip() or (home_dir() / "models.yaml")
|
||||
(os.environ.get("MODELS_MANIFEST") or "").strip() or str(models_manifest_path())
|
||||
).expanduser()
|
||||
extensions_manifest = Path(
|
||||
(os.environ.get("EXTENSIONS_MANIFEST") or "").strip()
|
||||
or (home_dir() / "extensions.yaml")
|
||||
or str(extensions_manifest_path())
|
||||
).expanduser()
|
||||
|
||||
def _dir(env_name: str, folder: str) -> Path:
|
||||
raw = (os.environ.get(env_name) or "").strip()
|
||||
return Path(raw).expanduser() if raw else (app_root / folder)
|
||||
return Path(raw).expanduser() if raw else (root / folder)
|
||||
|
||||
return Config(
|
||||
os_auth_url=values["OS_AUTH_URL"] or "https://cloud.api.selcloud.ru/identity/v3",
|
||||
@@ -162,7 +167,7 @@ def load_config(*, require_auth: bool = True) -> Config:
|
||||
local_wildcards_dir=_dir("LOCAL_WILDCARDS_DIR", "Wildcards"),
|
||||
local_workflows_dir=_dir("LOCAL_WORKFLOWS_DIR", "CustomWorkflows"),
|
||||
local_output_dir=_dir("LOCAL_OUTPUT_DIR", "Output"),
|
||||
app_root=app_root,
|
||||
app_root=root,
|
||||
autocomplete_enabled=_as_bool(os.environ.get("AUTOCOMPLETE_ENABLED"), True),
|
||||
autocomplete_github_repo=(
|
||||
os.environ.get("AUTOCOMPLETE_GITHUB_REPO") or "DominikDoom/a1111-sd-webui-tagcomplete"
|
||||
|
||||
@@ -27,7 +27,7 @@ from gpu_rent.os_client import (
|
||||
volume_quotas,
|
||||
)
|
||||
from gpu_rent.payload import folder_bytes as _folder_bytes, has_payload as _has_payload
|
||||
from gpu_rent.paths import env_path, home_dir
|
||||
from gpu_rent.paths import env_path, runtime_dir
|
||||
from gpu_rent.ssh_keys import key_ready
|
||||
from gpu_rent.state import load_state
|
||||
|
||||
@@ -42,7 +42,7 @@ class Check:
|
||||
|
||||
def run_doctor() -> list[Check]:
|
||||
checks: list[Check] = []
|
||||
home_dir().mkdir(parents=True, exist_ok=True)
|
||||
runtime_dir().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
env_file = env_path()
|
||||
if env_file.is_file():
|
||||
|
||||
+79
-21
@@ -1,33 +1,91 @@
|
||||
"""Paths: ~/.gpu-rent, app tree, SSH key."""
|
||||
"""Paths: project root, local .gpu-rent runtime, SSH key."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def home_dir() -> Path:
|
||||
return Path.home() / ".gpu-rent"
|
||||
|
||||
|
||||
def env_path() -> Path:
|
||||
return home_dir() / ".env"
|
||||
|
||||
|
||||
def state_path() -> Path:
|
||||
return home_dir() / "state.json"
|
||||
|
||||
|
||||
def lock_path() -> Path:
|
||||
return home_dir() / "gpu-rent.lock"
|
||||
|
||||
|
||||
def default_ssh_key_path() -> Path:
|
||||
return home_dir() / "id_ed25519"
|
||||
|
||||
|
||||
def detect_app_root() -> Path:
|
||||
cwd = Path.cwd().resolve()
|
||||
for candidate in (cwd, *cwd.parents):
|
||||
if (candidate / "Models").is_dir() and (candidate / "docs").is_dir():
|
||||
return candidate
|
||||
return cwd
|
||||
|
||||
|
||||
def app_root() -> Path:
|
||||
return detect_app_root()
|
||||
|
||||
|
||||
def runtime_dir() -> Path:
|
||||
"""State, lock, SSH key — always inside the project (gitignored)."""
|
||||
return app_root() / ".gpu-rent"
|
||||
|
||||
|
||||
# Back-compat alias used across the package.
|
||||
def home_dir() -> Path:
|
||||
return runtime_dir()
|
||||
|
||||
|
||||
def env_path() -> Path:
|
||||
return app_root() / ".env"
|
||||
|
||||
|
||||
def models_manifest_path() -> Path:
|
||||
return app_root() / "models.yaml"
|
||||
|
||||
|
||||
def extensions_manifest_path() -> Path:
|
||||
return app_root() / "extensions.yaml"
|
||||
|
||||
|
||||
def state_path() -> Path:
|
||||
return runtime_dir() / "state.json"
|
||||
|
||||
|
||||
def lock_path() -> Path:
|
||||
return runtime_dir() / "gpu-rent.lock"
|
||||
|
||||
|
||||
def default_ssh_key_path() -> Path:
|
||||
return runtime_dir() / "id_ed25519"
|
||||
|
||||
|
||||
def legacy_user_dir() -> Path:
|
||||
"""Old location (~/.gpu-rent) — only for one-shot migrate."""
|
||||
return Path.home() / ".gpu-rent"
|
||||
|
||||
|
||||
def migrate_legacy_if_needed() -> list[str]:
|
||||
"""Copy ~/.gpu-rent leftovers into the project once. Returns log lines."""
|
||||
legacy = legacy_user_dir()
|
||||
if not legacy.is_dir():
|
||||
return []
|
||||
root = app_root()
|
||||
runtime = runtime_dir()
|
||||
notes: list[str] = []
|
||||
|
||||
pairs = (
|
||||
(legacy / ".env", env_path()),
|
||||
(legacy / "models.yaml", models_manifest_path()),
|
||||
(legacy / "extensions.yaml", extensions_manifest_path()),
|
||||
(legacy / "state.json", state_path()),
|
||||
(legacy / "id_ed25519", default_ssh_key_path()),
|
||||
(legacy / "id_ed25519.pub", default_ssh_key_path().with_suffix(".pub")),
|
||||
)
|
||||
for src, dst in pairs:
|
||||
if not src.is_file() or dst.is_file():
|
||||
continue
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
notes.append(f"перенёс {src} → {dst}")
|
||||
if notes:
|
||||
runtime.mkdir(parents=True, exist_ok=True)
|
||||
marker = runtime / ".migrated-from-user-home"
|
||||
if not marker.is_file():
|
||||
marker.write_text(
|
||||
f"from {legacy}\n" + "\n".join(notes) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return notes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Ed25519 key at ~/.gpu-rent/id_ed25519 (no passphrase)."""
|
||||
"""Ed25519 key at <project>/.gpu-rent/id_ed25519 (no passphrase)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from gpu_rent.paths import home_dir, state_path
|
||||
from gpu_rent.paths import runtime_dir, state_path
|
||||
|
||||
CURRENT_VERSION = 1
|
||||
|
||||
@@ -60,7 +60,7 @@ def load_state() -> SessionState:
|
||||
|
||||
|
||||
def save_state(state: SessionState) -> None:
|
||||
home_dir().mkdir(parents=True, exist_ok=True)
|
||||
runtime_dir().mkdir(parents=True, exist_ok=True)
|
||||
path = state_path()
|
||||
path.write_text(json.dumps(state.to_dict(), indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Keep tests from picking up a real ~/.gpu-rent/.env or OS_* from the shell."""
|
||||
"""Keep tests from picking up a real project .env or OS_* from the shell."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from gpu_rent.config import load_config
|
||||
from gpu_rent.paths import env_path, migrate_legacy_if_needed, models_manifest_path, runtime_dir
|
||||
from gpu_rent.ssh_keys import ensure_ed25519, public_path
|
||||
|
||||
|
||||
@@ -19,6 +20,25 @@ def test_load_config_missing_auth(monkeypatch, tmp_path):
|
||||
assert not cfg.auth_ok
|
||||
assert "OS_USERNAME" in cfg.missing
|
||||
assert cfg.data_volume_size_gb == 100
|
||||
assert cfg.models_manifest == models_manifest_path()
|
||||
assert runtime_dir() == tmp_path / ".gpu-rent"
|
||||
assert env_path() == tmp_path / ".env"
|
||||
|
||||
|
||||
def test_migrate_legacy_user_home(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HOME", str(tmp_path / "user"))
|
||||
monkeypatch.setenv("USERPROFILE", str(tmp_path / "user"))
|
||||
proj = tmp_path / "proj"
|
||||
proj.mkdir()
|
||||
monkeypatch.chdir(proj)
|
||||
legacy = tmp_path / "user" / ".gpu-rent"
|
||||
legacy.mkdir(parents=True)
|
||||
(legacy / "models.yaml").write_text("checkpoint:\n - version_id: 1\n", encoding="utf-8")
|
||||
(legacy / ".env").write_text("OS_USERNAME=x\n", encoding="utf-8")
|
||||
notes = migrate_legacy_if_needed()
|
||||
assert notes
|
||||
assert (proj / "models.yaml").is_file()
|
||||
assert (proj / ".env").is_file()
|
||||
|
||||
|
||||
def test_ssh_key_generate(tmp_path):
|
||||
|
||||
@@ -17,3 +17,14 @@ def test_launchers_require_python_311():
|
||||
sh = (ROOT / "gpu-rent.sh").read_text(encoding="utf-8")
|
||||
assert "3, 11" in ps1 or "3.11" in ps1
|
||||
assert "3, 11" in sh
|
||||
|
||||
|
||||
def test_launchers_seed_project_local_files():
|
||||
ps1 = (ROOT / "gpu-rent.ps1").read_text(encoding="utf-8")
|
||||
sh = (ROOT / "gpu-rent.sh").read_text(encoding="utf-8")
|
||||
assert 'Join-Path $env:USERPROFILE ".gpu-rent"' not in ps1
|
||||
assert "${HOME}/.gpu-rent" not in sh
|
||||
assert "models.yaml" in ps1 and "models.yaml" in sh
|
||||
assert "extensions.yaml" in ps1 and "extensions.yaml" in sh
|
||||
assert 'Join-Path $Root ".env"' in ps1
|
||||
assert "$ROOT/.env" in sh
|
||||
|
||||
Reference in New Issue
Block a user