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.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
models.yaml
|
||||||
|
extensions.yaml
|
||||||
|
|
||||||
# SwarmUI checkpoints / LoRA dropped into the app tree
|
# SwarmUI checkpoints / LoRA dropped into the app tree
|
||||||
Models/**
|
Models/**
|
||||||
@@ -27,7 +29,7 @@ __pycache__/
|
|||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|
||||||
# Local CLI state (if someone runs from the repo)
|
# Local CLI state / SSH key (inside the repo, not under %)
|
||||||
.gpu-rent/
|
.gpu-rent/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ chmod +x gpu-rent.sh # один раз на Unix
|
|||||||
./gpu-rent.sh doctor
|
./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, диски оставляет.
|
`up` поднимает compute и ставит SwarmUI нативно (`launch-linux.sh` + systemd). `tunnel` пробрасывает `localhost:17801`. `stop` гасит GPU, диски оставляет.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
│ gpu-rent CLI │
|
│ gpu-rent CLI │
|
||||||
│ up / stop / status / doctor / hold │
|
│ up / stop / status / doctor / hold │
|
||||||
│ tunnel / open — SSH localhost:17801 → VM :7801 │
|
│ 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 │
|
│ браузер / MCP / curl API → http://127.0.0.1:17801 │
|
||||||
│ локальный SwarmUI → http://127.0.0.1:7801 (не трогаем)
|
│ локальный SwarmUI → http://127.0.0.1:7801 (не трогаем)
|
||||||
@@ -169,5 +169,5 @@ Reconcile: сервер с тегом `gpu-rent` есть, локального
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Python 3.11+ | openstacksdk, paramiko, Typer, Rich, questionary, dotenv |
|
| Python 3.11+ | openstacksdk, paramiko, Typer, Rich, questionary, dotenv |
|
||||||
| SSH | paramiko + sshtunnel (Windows без системного `ssh -L`) |
|
| SSH | paramiko + sshtunnel (Windows без системного `ssh -L`) |
|
||||||
| Конфиг | `%USERPROFILE%\.gpu-rent\.env` |
|
| Конфиг | `<repo>/.env` + runtime в `<repo>/.gpu-rent/` |
|
||||||
| Лицензия | MIT |
|
| Лицензия | MIT |
|
||||||
|
|||||||
+4
-4
@@ -48,7 +48,7 @@
|
|||||||
3. Квота GPU > 0; иначе текст «напишите в поддержку Selectel».
|
3. Квота GPU > 0; иначе текст «напишите в поддержку Selectel».
|
||||||
4. В сегменте есть хотя бы один flavor из `FLAVOR_PREFERENCE` (не `disabled`).
|
4. В сегменте есть хотя бы один flavor из `FLAVOR_PREFERENCE` (не `disabled`).
|
||||||
5. Volume type сегмента существует; data 100 GB влезает в квоту дисков.
|
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`); манифест парсится.
|
7. Если задан `CIVITAI_API_TOKEN` — HEAD/лёгкий запрос к `CIVITAI_API_HOST` (дефолт `.red`); манифест парсится.
|
||||||
8. `extensions.yaml` парсится, если файл есть.
|
8. `extensions.yaml` парсится, если файл есть.
|
||||||
9. Локальные папки: предупреждение, если `Models/` огромный относительно свободного места на будущем диске.
|
9. Локальные папки: предупреждение, если `Models/` огромный относительно свободного места на будущем диске.
|
||||||
@@ -98,7 +98,7 @@ Hold killer: gpu-rent hold
|
|||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
`%USERPROFILE%\.gpu-rent\.env` / `~/.gpu-rent/.env`:
|
`.env` в корне репозитория (рядом с `env.example`):
|
||||||
|
|
||||||
```env
|
```env
|
||||||
OS_AUTH_URL=https://cloud.api.selcloud.ru/identity/v3
|
OS_AUTH_URL=https://cloud.api.selcloud.ru/identity/v3
|
||||||
@@ -154,7 +154,7 @@ Application credential для idle-killer CLI создаёт при bootstrap и
|
|||||||
|
|
||||||
## State
|
## 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
|
## 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 недоступен — только звук и лог, не падать.
|
`NOTIFY_READY`: toast через WinRT / `win10toast` (что заработает на 10/11 без админ-прав) + системный звук. Если toast недоступен — только звук и лог, не падать.
|
||||||
|
|||||||
+4
-3
@@ -8,8 +8,8 @@
|
|||||||
| Сессия | GPU живёт до `stop` **или** простоя (idle-killer **на VM**). Ноут можно закрыть — compute не обязан умереть |
|
| Сессия | GPU живёт до `stop` **или** простоя (idle-killer **на VM**). Ноут можно закрыть — compute не обязан умереть |
|
||||||
| Модели | Пустой `./Models` → не грузим ничего. Появились веса + метадата → на `up` выгрузить эту пару. Civitai-seed отдельно. С сервера не удаляем |
|
| Модели | Пустой `./Models` → не грузим ничего. Появились веса + метадата → на `up` выгрузить эту пару. Civitai-seed отдельно. С сервера не удаляем |
|
||||||
| Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена |
|
| 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` |
|
| Манифест моделей | `<repo>/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>/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) |
|
| Autocomplete | До первого старта: скачать word-list в `Data/Autocompletions`, прописать `DefaultUser.AutoComplete.Source`. На каждом `up` сверить GitHub blob sha и обновить файл, если изменился. Дефолт: `tags/danbooru.csv` из a1111-sd-webui-tagcomplete (как в доке SwarmUI) |
|
||||||
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
|
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
|
||||||
| Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` |
|
| Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` |
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
| Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve |
|
||||||
| `up` / `tunnel` | Разделены: `up` только облако, `tunnel` отдельно. Ctrl+C на туннеле не удаляет VM |
|
| `up` / `tunnel` | Разделены: `up` только облако, `tunnel` отдельно. Ctrl+C на туннеле не удаляет VM |
|
||||||
| Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) |
|
| 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** |
|
| Локальный порт | **17801** |
|
||||||
| MCP-конфиг Cursor | Только сниппет в stdout, файл не трогаем |
|
| MCP-конфиг Cursor | Только сниппет в stdout, файл не трогаем |
|
||||||
| `ready` для API | Ждать Idle backend, не только HTTP UI |
|
| `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
|
```yaml
|
||||||
swarmui:
|
swarmui:
|
||||||
@@ -52,7 +52,7 @@ Idle-killer ещё не вооружён. Git нужен на VM (`apt` в boots
|
|||||||
Публичный HTTPS без секретов. Приватный репозиторий:
|
Публичный HTTPS без секретов. Приватный репозиторий:
|
||||||
|
|
||||||
- `GIT_TOKEN` в `.env` (GitHub: `x-access-token`; GitLab: `oauth2` / PAT). CLI подставляет в URL **только на VM**, не пишет токен в yaml и не в cloud-init.
|
- `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@` + ключ.
|
Не использовать `git://`. Только `https://` и `git@` + ключ.
|
||||||
|
|
||||||
@@ -63,6 +63,6 @@ Idle-killer ещё не вооружён. Git нужен на VM (`apt` в boots
|
|||||||
## Конфиг
|
## Конфиг
|
||||||
|
|
||||||
```env
|
```env
|
||||||
EXTENSIONS_MANIFEST= # пусто = ~/.gpu-rent/extensions.yaml
|
EXTENSIONS_MANIFEST= # пусто = <repo>/extensions.yaml
|
||||||
GIT_TOKEN= # опционально, для приватных https
|
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**, не имя файла и не «последняя версия модели» (она плывёт).
|
Идентификатор — **Civitai model version id**, не имя файла и не «последняя версия модели» (она плывёт).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ~/.gpu-rent/models.yaml
|
# models.yaml — пример: Krea 2 + LoRA (см. models.example.yaml)
|
||||||
checkpoint:
|
checkpoint:
|
||||||
- version_id: 2514310
|
- url: https://civitai.red/models/2786499/realism-by-stable-yogi-krea2?modelVersionId=3231611
|
||||||
lora:
|
lora:
|
||||||
- version_id: 123456
|
- url: https://civitai.red/models/2268008/realistic-snapshot-z-image-turbo-krea-2?modelVersionId=3084537
|
||||||
vae:
|
- url: https://civitai.red/models/2187487/cutifyier?modelVersionId=3107521
|
||||||
- version_id: 789012
|
- url: https://civitai.red/models/1134895/2000s-analog-core?modelVersionId=3103914
|
||||||
embedding:
|
|
||||||
- version_id: 111
|
|
||||||
controlnet:
|
|
||||||
- version_id: 222
|
|
||||||
upscaler:
|
|
||||||
- version_id: 333
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Допустимо вместо `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`):
|
Типы манифеста → каталоги SwarmUI (`ModelRoot` = `Models`):
|
||||||
|
|
||||||
@@ -103,7 +97,7 @@ upscaler:
|
|||||||
|
|
||||||
Тип в yaml задаёт **куда класть**, даже если Civitai назвал иначе. Если API-тип совсем другой — предупреждение, кладём по yaml.
|
Тип в yaml задаёт **куда класть**, даже если Civitai назвал иначе. Если API-тип совсем другой — предупреждение, кладём по yaml.
|
||||||
|
|
||||||
В git не коммитить рабочий манифест с личными чекпоинтами. В примере — вымышленные id.
|
В git не коммитить рабочий манифест с личными чекпоинтами. В `models.example.yaml` — стартовый набор Krea 2 (чекпоинт + три LoRA); скопируй и дополни под себя.
|
||||||
|
|
||||||
## Civitai API (.com / .red)
|
## Civitai API (.com / .red)
|
||||||
|
|
||||||
@@ -169,6 +163,6 @@ REST не раздвоился: те же `/api/v1/...` на обоих хост
|
|||||||
```env
|
```env
|
||||||
CIVITAI_API_TOKEN=
|
CIVITAI_API_TOKEN=
|
||||||
CIVITAI_API_HOST=civitai.red # полный каталог; civitai.com = только SFW
|
CIVITAI_API_HOST=civitai.red # полный каталог; civitai.com = только SFW
|
||||||
MODELS_MANIFEST= # пусто = ~/.gpu-rent/models.yaml
|
MODELS_MANIFEST= # пусто = <repo>/models.yaml
|
||||||
LOCAL_MODELS_DIR= # пусто = <корень приложения>/Models
|
LOCAL_MODELS_DIR= # пусто = <корень приложения>/Models
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@
|
|||||||
- [ ] `status`: диск used/free, ₽/час, TTL preempt 24 ч, killer/hold
|
- [ ] `status`: диск used/free, ₽/час, TTL preempt 24 ч, killer/hold
|
||||||
- [x] Второй `up` не создаёт второй GPU
|
- [x] Второй `up` не создаёт второй GPU
|
||||||
- [ ] `resize-data` вверх
|
- [ ] `resize-data` вверх
|
||||||
- [x] Генерация `~/.gpu-rent/id_ed25519` + keypair при первом `up`
|
- [x] Генерация `<repo>/.gpu-rent/id_ed25519` + keypair при первом `up`
|
||||||
- [x] `seed-models` на живом диске (идемпотентно, SHA256)
|
- [x] `seed-models` на живом диске (идемпотентно, SHA256)
|
||||||
- [x] `push` / `push-models`: `Models/`, `Wildcards/`, `CustomWorkflows/`
|
- [x] `push` / `push-models`: `Models/`, `Wildcards/`, `CustomWorkflows/`
|
||||||
- [x] `pull-output` и `PULL_OUTPUT` на `stop` / повторный `up`
|
- [x] `pull-output` и `PULL_OUTPUT` на `stop` / повторный `up`
|
||||||
|
|||||||
+9
-8
@@ -26,7 +26,9 @@ chmod +x gpu-rent.sh
|
|||||||
./gpu-rent.sh --help
|
./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):
|
Ручной венв по желанию (для разработки / pytest):
|
||||||
|
|
||||||
@@ -129,9 +131,8 @@ python -m gpu_rent --help
|
|||||||
Создай каталог и файл **вне git**:
|
Создай каталог и файл **вне git**:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
mkdir $env:USERPROFILE\.gpu-rent
|
copy env.example .env
|
||||||
copy env.example $env:USERPROFILE\.gpu-rent\.env
|
notepad .env
|
||||||
notepad $env:USERPROFILE\.gpu-rent\.env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Вставь значения из RC + пароль + `GPU_RENT_AZ`. Никогда не коммить `.env`.
|
Вставь значения из RC + пароль + `GPU_RENT_AZ`. Никогда не коммить `.env`.
|
||||||
@@ -163,7 +164,7 @@ openstack flavor list
|
|||||||
Манифест (не в git со своими id, если не хочешь светить вкусы):
|
Манифест (не в git со своими id, если не хочешь светить вкусы):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
copy models.example.yaml $env:USERPROFILE\.gpu-rent\models.yaml
|
copy models.example.yaml models.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
`version_id: 0` — заглушка, doctor её игнорирует. Нужен **modelVersionId** из URL, не id карточки модели.
|
`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**.
|
2. Fine-grained: доступ только к нужным репам, **Contents: Read**.
|
||||||
3. `GIT_TOKEN` в `.env`.
|
3. `GIT_TOKEN` в `.env`.
|
||||||
|
|
||||||
Скопируй шаблон: `extensions.example.yaml` → `~\.gpu-rent\extensions.yaml`. Пустой файл = стоковый SwarmUI.
|
Скопируй шаблон: `extensions.example.yaml` → `extensions.yaml` в корне репо. Пустой файл = стоковый SwarmUI.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. SSH
|
## 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)
|
- [ ] Тикет на лимит **1× GPU** в нужном сегменте (или квота уже > 0)
|
||||||
- [ ] Сервисный пользователь `member` на этот проект, пароль сохранён
|
- [ ] Сервисный пользователь `member` на этот проект, пароль сохранён
|
||||||
- [ ] RC скачан на **тот же пул**, где GPU
|
- [ ] RC скачан на **тот же пул**, где GPU
|
||||||
- [ ] `~\.gpu-rent\.env` заполнен (`OS_*` + `GPU_RENT_AZ`)
|
- [ ] `.env` в корне репо заполнен (`OS_*` + `GPU_RENT_AZ`)
|
||||||
- [ ] Нет `X-Token` вместо пароля сервисного пользователя
|
- [ ] Нет `X-Token` вместо пароля сервисного пользователя
|
||||||
- [ ] (опционально) Civitai token + `models.yaml`
|
- [ ] (опционально) Civitai token + `models.yaml`
|
||||||
- [ ] На балансе хватает на диск 100 GB **даже когда GPU выключен**
|
- [ ] На балансе хватает на диск 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
|
# How to fill: docs/setup.md
|
||||||
# X-Token from the panel does NOT work here.
|
# 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.
|
# Empty/missing file → no extra extensions, stock SwarmUI.
|
||||||
# swarmui = C# repos cloned to src/Extensions
|
# swarmui = C# repos cloned to src/Extensions
|
||||||
# comfy = Python custom nodes cloned to ComfyUI DLNodes
|
# 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
|
Get-Date -Format o | Set-Content -LiteralPath $Stamp -Encoding ascii
|
||||||
}
|
}
|
||||||
|
|
||||||
$GpuHome = Join-Path $env:USERPROFILE ".gpu-rent"
|
$EnvFile = Join-Path $Root ".env"
|
||||||
$EnvFile = Join-Path $GpuHome ".env"
|
|
||||||
$Example = Join-Path $Root "env.example"
|
$Example = Join-Path $Root "env.example"
|
||||||
if (-not (Test-Path -LiteralPath $EnvFile)) {
|
if (-not (Test-Path -LiteralPath $EnvFile)) {
|
||||||
New-Item -ItemType Directory -Force -Path $GpuHome | Out-Null
|
|
||||||
if (Test-Path -LiteralPath $Example) {
|
if (Test-Path -LiteralPath $Example) {
|
||||||
Copy-Item -LiteralPath $Example -Destination $EnvFile
|
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"
|
$ErrorActionPreference = "Continue"
|
||||||
& $VenvPy -m gpu_rent @args
|
& $VenvPy -m gpu_rent @args
|
||||||
exit $LASTEXITCODE
|
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"
|
date -u +"%Y-%m-%dT%H:%M:%SZ" >"$STAMP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
GPU_HOME="${HOME}/.gpu-rent"
|
if [[ ! -f "$ROOT/.env" && -f "$ROOT/env.example" ]]; then
|
||||||
if [[ ! -f "$GPU_HOME/.env" && -f "$ROOT/env.example" ]]; then
|
cp "$ROOT/env.example" "$ROOT/.env"
|
||||||
mkdir -p "$GPU_HOME"
|
echo "gpu-rent: created .env - fill OS_* (see docs/setup.md)"
|
||||||
cp "$ROOT/env.example" "$GPU_HOME/.env"
|
fi
|
||||||
echo "gpu-rent: created $GPU_HOME/.env - fill OS_* (see docs/setup.md)"
|
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
|
fi
|
||||||
|
|
||||||
exec "$VENV_PY" -m gpu_rent "$@"
|
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).
|
# version_id = modelVersionId from the URL (not the model id).
|
||||||
# url may be civitai.com or civitai.red — both work.
|
# url may be civitai.com or civitai.red — both work.
|
||||||
# Requires CIVITAI_API_TOKEN. Empty file → SwarmUI default model.
|
# Requires CIVITAI_API_TOKEN. Empty file → SwarmUI default model.
|
||||||
|
|
||||||
checkpoint:
|
checkpoint:
|
||||||
- version_id: 0
|
# Realism by Stable Yogi — Krea 2
|
||||||
# - url: https://civitai.red/models/123?modelVersionId=456
|
- url: https://civitai.red/models/2786499/realism-by-stable-yogi-krea2?modelVersionId=3231611
|
||||||
|
|
||||||
lora:
|
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:
|
# vae:
|
||||||
# - version_id: 0
|
# - 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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -10,10 +10,13 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
from gpu_rent.errors import ConfigError
|
from gpu_rent.errors import ConfigError
|
||||||
from gpu_rent.paths import (
|
from gpu_rent.paths import (
|
||||||
|
app_root,
|
||||||
default_ssh_key_path,
|
default_ssh_key_path,
|
||||||
detect_app_root,
|
|
||||||
env_path,
|
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:
|
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()
|
env_file = env_path()
|
||||||
if env_file.is_file():
|
if env_file.is_file():
|
||||||
load_dotenv(env_file, override=False)
|
load_dotenv(env_file, override=False)
|
||||||
|
|
||||||
app_root = detect_app_root()
|
|
||||||
missing: list[str] = []
|
missing: list[str] = []
|
||||||
required = (
|
required = (
|
||||||
"OS_AUTH_URL",
|
"OS_AUTH_URL",
|
||||||
@@ -119,7 +124,7 @@ def load_config(*, require_auth: bool = True) -> Config:
|
|||||||
|
|
||||||
if require_auth and missing:
|
if require_auth and missing:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"В ~/.gpu-rent/.env не хватает: "
|
f"В {env_file} не хватает: "
|
||||||
+ ", ".join(missing)
|
+ ", ".join(missing)
|
||||||
+ ". Как заполнить: docs/setup.md (сервисный пользователь, не X-Token)."
|
+ ". Как заполнить: 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()
|
ssh_key = Path(key_override).expanduser() if key_override else default_ssh_key_path()
|
||||||
|
|
||||||
models_manifest = 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()
|
).expanduser()
|
||||||
extensions_manifest = Path(
|
extensions_manifest = Path(
|
||||||
(os.environ.get("EXTENSIONS_MANIFEST") or "").strip()
|
(os.environ.get("EXTENSIONS_MANIFEST") or "").strip()
|
||||||
or (home_dir() / "extensions.yaml")
|
or str(extensions_manifest_path())
|
||||||
).expanduser()
|
).expanduser()
|
||||||
|
|
||||||
def _dir(env_name: str, folder: str) -> Path:
|
def _dir(env_name: str, folder: str) -> Path:
|
||||||
raw = (os.environ.get(env_name) or "").strip()
|
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(
|
return Config(
|
||||||
os_auth_url=values["OS_AUTH_URL"] or "https://cloud.api.selcloud.ru/identity/v3",
|
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_wildcards_dir=_dir("LOCAL_WILDCARDS_DIR", "Wildcards"),
|
||||||
local_workflows_dir=_dir("LOCAL_WORKFLOWS_DIR", "CustomWorkflows"),
|
local_workflows_dir=_dir("LOCAL_WORKFLOWS_DIR", "CustomWorkflows"),
|
||||||
local_output_dir=_dir("LOCAL_OUTPUT_DIR", "Output"),
|
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_enabled=_as_bool(os.environ.get("AUTOCOMPLETE_ENABLED"), True),
|
||||||
autocomplete_github_repo=(
|
autocomplete_github_repo=(
|
||||||
os.environ.get("AUTOCOMPLETE_GITHUB_REPO") or "DominikDoom/a1111-sd-webui-tagcomplete"
|
os.environ.get("AUTOCOMPLETE_GITHUB_REPO") or "DominikDoom/a1111-sd-webui-tagcomplete"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from gpu_rent.os_client import (
|
|||||||
volume_quotas,
|
volume_quotas,
|
||||||
)
|
)
|
||||||
from gpu_rent.payload import folder_bytes as _folder_bytes, has_payload as _has_payload
|
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.ssh_keys import key_ready
|
||||||
from gpu_rent.state import load_state
|
from gpu_rent.state import load_state
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class Check:
|
|||||||
|
|
||||||
def run_doctor() -> list[Check]:
|
def run_doctor() -> list[Check]:
|
||||||
checks: 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()
|
env_file = env_path()
|
||||||
if env_file.is_file():
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
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:
|
def detect_app_root() -> Path:
|
||||||
cwd = Path.cwd().resolve()
|
cwd = Path.cwd().resolve()
|
||||||
for candidate in (cwd, *cwd.parents):
|
for candidate in (cwd, *cwd.parents):
|
||||||
if (candidate / "Models").is_dir() and (candidate / "docs").is_dir():
|
if (candidate / "Models").is_dir() and (candidate / "docs").is_dir():
|
||||||
return candidate
|
return candidate
|
||||||
return cwd
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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
|
CURRENT_VERSION = 1
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ def load_state() -> SessionState:
|
|||||||
|
|
||||||
|
|
||||||
def save_state(state: SessionState) -> None:
|
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 = state_path()
|
||||||
path.write_text(json.dumps(state.to_dict(), indent=2) + "\n", encoding="utf-8")
|
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
|
import pytest
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from gpu_rent.config import load_config
|
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
|
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 not cfg.auth_ok
|
||||||
assert "OS_USERNAME" in cfg.missing
|
assert "OS_USERNAME" in cfg.missing
|
||||||
assert cfg.data_volume_size_gb == 100
|
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):
|
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")
|
sh = (ROOT / "gpu-rent.sh").read_text(encoding="utf-8")
|
||||||
assert "3, 11" in ps1 or "3.11" in ps1
|
assert "3, 11" in ps1 or "3.11" in ps1
|
||||||
assert "3, 11" in sh
|
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