diff --git a/README.md b/README.md index ccda7ea..4f0661a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ .\gpu-rent.ps1 doctor # preflight .\gpu-rent.ps1 flavors # что выберет фоллбек .\gpu-rent.ps1 dry-run # план без create -.\gpu-rent.ps1 up --yes # GPU + SwarmUI + seed -.\gpu-rent.ps1 tunnel # UI на :17801 (Ctrl+C ≠ stop) +.\gpu-rent.ps1 up --yes # GPU + SwarmUI + seed + туннель :17801 +.\gpu-rent.ps1 up --yes --no-tunnel # только облако +.\gpu-rent.ps1 tunnel --open # снова открыть UI, если туннель закрыли .\gpu-rent.ps1 hold # отложить idle-killer .\gpu-rent.ps1 stop # гасить compute, диски оставить ``` @@ -38,8 +39,10 @@ Unix: `./gpu-rent.sh …` (один раз `chmod +x gpu-rent.sh`). | --- | --- | | `doctor` | Keystone, квота GPU, flavor, Civitai, манифесты | | `flavors` | Живой список по `FLAVOR_PREFERENCE` | -| `up` / `up --yes` / `up --no-spot` / `up --flavor ID` | Поднять GPU | -| `tunnel` / `tunnel --open` | SSH-проброс; watchdog на EXPIRED | +| `up` / `up --yes` | Поднять GPU + туннель :17801 (браузер). Ctrl+C = туннель off | +| `up --no-tunnel` | Только облако | +| `up --no-spot` / `up --flavor ID` | Обычный тариф / явный flavor | +| `tunnel` / `tunnel --open` | Повторный проброс; watchdog на EXPIRED | | `open` | Браузер на 17801 | | `hold` / `hold --minutes 90` / `hold --clear` | Пауза idle-killer | | `status` | Nova + диск + killer | diff --git a/docs/cli.md b/docs/cli.md index 01575a0..29df31e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -8,10 +8,11 @@ | --- | --- | | `gpu-rent flavors` | Скан `SCAN_POOLS` (ru-6 multizone…) × `FLAVOR_PREFERENCE`, затем список в текущем `OS_REGION_NAME` | | `gpu-rent doctor` | Preflight **без** create: Keystone, квота GPU, flavor в AZ, диски, Civitai token+`.red`, манифесты, SSH-ключ. Код выхода ≠ 0, если сессию нельзя начать | -| `gpu-rent up` | Preflight → create/unshelve (с фоллбеком flavor) → extensions → autocomplete → Civitai seed → push локальных папок → ждать UI → toast/звук | +| `gpu-rent up` / `up --yes` | Preflight → create/unshelve → bootstrap → **туннель** `localhost:17801` + URL панели; Ctrl+C закрывает туннель | +| `gpu-rent up --no-tunnel` | Только облако + bootstrap, без локального проброса | | `gpu-rent up --no-spot` | Обычный (не preemptible) сервер | | `gpu-rent up --flavor … --yes` | Без вопросов. `--flavor` бьёт список фоллбека. `--yes` без `--flavor` берёт первый доступный из `FLAVOR_PREFERENCE` | -| `gpu-rent tunnel` | SSH-проброс `localhost:17801` → VM `:7801`. Блокируется. Ctrl+C = закрыть туннель, GPU **оставить** | +| `gpu-rent tunnel` | Повторный SSH-проброс, если `up --no-tunnel` или туннель уже закрыли. Ctrl+C = закрыть туннель, GPU **оставить** | | `gpu-rent tunnel --open` | Туннель + сразу открыть браузер на 17801 | | `gpu-rent open` | Открыть браузер на `http://127.0.0.1:17801`. Туннель уже должен слушать порт | | `gpu-rent status` | State + Nova + диск used/free + ₽/час + сколько до 24 ч preempt + killer/hold. Туннель не обязателен | diff --git a/docs/decisions.md b/docs/decisions.md index 951cad4..15cebd7 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -19,7 +19,7 @@ | Floating IP | `KEEP_FLOATING_IP=false`: выделить на `up`, удалить на `stop` | | Idle-killer | 30 минут пустой очереди генерации. Открытый браузер не продлевает жизнь | | Льгота после boot | Killer молчит во время clone/seed/push, hold, качалки в UI SwarmUI, пока backend не Idle, и 45 мин после ACTIVE/unshelve | -| `up` / `tunnel` | Разделены: `up` только облако, `tunnel` отдельно. Ctrl+C на туннеле не удаляет VM | +| `up` / `tunnel` | `up` по умолчанию после ready открывает туннель `:17801`, печатает URL и ждёт. `--no-tunnel` — только облако. Ctrl+C на туннеле GPU не гасит (`stop` отдельно). Команда `tunnel` остаётся для повторного входа | | Data-диск | Старт **100 GB**, рост через resize вверх (вниз Selectel не умеет) | | SSH | CLI генерирует `/.gpu-rent/id_ed25519` без passphrase и сам регистрирует keypair | | Локальные файлы | Всё в корне репозитория: `.env`, `models.yaml`, `extensions.yaml`; runtime (`state.json`, lock, SSH) в `/.gpu-rent/`. Не `%USERPROFILE%\.gpu-rent` | diff --git a/src/gpu_rent/cli.py b/src/gpu_rent/cli.py index 44da38f..c817ff0 100644 --- a/src/gpu_rent/cli.py +++ b/src/gpu_rent/cli.py @@ -75,6 +75,7 @@ def _print_checks(checks) -> int: _print_next_steps(failed) return 1 console.print("\n[green]Можно идти дальше.[/green] Дальше: gpu-rent up --yes") + console.print(" (туннель :17801 по умолчанию; только облако: --no-tunnel)") console.print("Чеклист spike: docs/spike-notes.md") return 0 @@ -290,8 +291,18 @@ def up( flavor: Optional[str] = typer.Option(None, "--flavor", help="Flavor id, без фоллбека"), yes: bool = typer.Option(False, "--yes", help="Без вопросов"), adopt: bool = typer.Option(False, "--adopt", help="Подхватить тег gpu-rent без state"), + no_tunnel: bool = typer.Option( + False, + "--no-tunnel", + help="Только облако + bootstrap, без локального туннеля", + ), + open_browser: bool = typer.Option( + True, + "--open/--no-open", + help="После туннеля открыть браузер на 17801 (по умолчанию да)", + ), ) -> None: - """Create/unshelve GPU и bootstrap SwarmUI (без Docker).""" + """Create/unshelve GPU, bootstrap SwarmUI, по умолчанию туннель на :17801.""" try: checks = run_doctor() code = _print_checks(checks) @@ -302,7 +313,7 @@ def up( def confirm(msg: str) -> bool: return typer.confirm(msg) - cmd_up( + state = cmd_up( cfg, no_spot=no_spot, flavor=flavor, @@ -311,6 +322,27 @@ def up( confirm=confirm, log=lambda m: console.print(m), ) + if no_tunnel: + console.print( + f"[bold]готово[/bold] (без туннеля). " + f"UI: gpu-rent tunnel --open | stop: gpu-rent stop" + ) + if state.floating_ip: + console.print(f"FIP {state.floating_ip}") + return + + if not state.floating_ip: + raise GpuRentError("нет floating IP после up — туннель не открыть") + console.print( + f"[bold]панель[/bold] http://127.0.0.1:{cfg.swarmui_local_port} " + "(Ctrl+C = закрыть туннель, GPU жив → gpu-rent stop)" + ) + run_tunnel( + cfg, + state.floating_ip, + open_browser=open_browser, + log=lambda m: console.print(m), + ) except GpuRentError as exc: _die(exc) diff --git a/src/gpu_rent/doctor.py b/src/gpu_rent/doctor.py index f12fa33..f3c9bd2 100644 --- a/src/gpu_rent/doctor.py +++ b/src/gpu_rent/doctor.py @@ -363,8 +363,8 @@ def dry_run_plan(checks: list[Check]) -> list[str]: f"idle-killer: {cfg.idle_minutes} мин пустой очереди, льгота {cfg.idle_grace_minutes} мин", "₽: в API нет — смотри панель; диск 24/7 даже после stop", f"туннель: localhost:{cfg.swarmui_local_port} -> VM :7801", - "gpu-rent up --yes создаст сеть/диски/compute и поставит SwarmUI (если doctor зелёный и квота GPU > 0)", - "после up: gpu-rent tunnel (Ctrl+C не гасит GPU)", + "gpu-rent up --yes создаст GPU + SwarmUI и откроет туннель :17801 (Ctrl+C не гасит GPU)", + "только облако без туннеля: gpu-rent up --yes --no-tunnel", ] flavor = next((c.detail for c in checks if c.name == "flavor" and c.ok), None) if flavor: diff --git a/src/gpu_rent/notify.py b/src/gpu_rent/notify.py index ec70674..3dd7944 100644 --- a/src/gpu_rent/notify.py +++ b/src/gpu_rent/notify.py @@ -74,7 +74,7 @@ def print_mcp_snippet(cfg: Config, log: Log) -> None: log("}") log("") log(f"SwarmUI на VM: 127.0.0.1:7801 (только через туннель)") - log(f"Локально: gpu-rent tunnel") + log(f"Локально: уже в up (или gpu-rent tunnel)") log(f"Браузер: gpu-rent open → http://127.0.0.1:{port}") log(f"API: http://127.0.0.1:{port}/API/") log(f"MCP: http://127.0.0.1:{port}/mcp")