Bump version to 0.2.0 and enhance documentation

- Updated version number in pyproject.toml and __init__.py to 0.2.0.
- Revised README.md to reflect the current state of the project, including usage instructions and setup steps.
- Improved CLI documentation in cli.md, adding details about new commands and their functionalities.
- Enhanced the quick start section in README.md for better clarity on initial setup.
- Updated local folder documentation to clarify file handling and commands.
- Added a new command for listing GPU flavors and improved error handling in the CLI.
- Implemented a watchdog feature in the tunnel to manage server states effectively.
This commit is contained in:
Leonid Pershin
2026-08-21 03:38:01 +03:00
parent 343f741baa
commit a563ae06c4
30 changed files with 1750 additions and 132 deletions
+108 -17
View File
@@ -16,7 +16,7 @@ from rich.table import Table
from gpu_rent import __version__
from gpu_rent.config import load_config
from gpu_rent.doctor import blocking_failed, dry_run_plan, run_doctor
from gpu_rent.errors import GpuRentError, NotReadyError
from gpu_rent.errors import GpuRentError
from gpu_rent.os_client import connect, find_snapshot_by_name, find_tagged_servers
from gpu_rent.session import cmd_stop, cmd_up
from gpu_rent.ssh_ops import interactive_ssh, run_ssh
@@ -58,13 +58,6 @@ def _die(exc: BaseException) -> None:
raise typer.Exit(1)
def _nyi(name: str) -> None:
raise NotReadyError(
f"`{name}` ещё не готов. Уже работают: doctor, dry-run, status, open, up, stop, destroy, ssh, logs, tunnel, seed-*, push, pull-output.\n"
"Ключи: docs/setup.md"
)
def _print_checks(checks) -> int:
table = Table(title="gpu-rent doctor", show_lines=False)
table.add_column("ok")
@@ -79,11 +72,30 @@ def _print_checks(checks) -> int:
failed = blocking_failed(checks)
if failed:
console.print("\n[red]Сессию начинать нельзя.[/red] См. docs/setup.md")
_print_next_steps(failed)
return 1
console.print("\n[green]Можно идти дальше.[/green] Дальше: gpu-rent up --yes")
console.print("Чеклист spike: docs/spike-notes.md")
return 0
def _print_next_steps(failed) -> None:
names = {c.name for c in failed}
console.print("\n[bold]Что сделать дальше[/bold]")
if "env file" in names or any("OS_" in (c.detail or "") for c in failed):
console.print(" 1. copy env.example .env → заполни OS_* (docs/setup.md §3)")
if any(
"квота" in (c.detail or "").lower()
or "quota" in c.name.lower()
or c.name == "GPU quota"
for c in failed
):
console.print(" 2. Тикет в поддержку Selectel — лимит GPU (docs/setup.md §2.3)")
if any(c.name == "flavor" for c in failed):
console.print(" 3. gpu-rent flavors — проверь пул / FLAVOR_PREFERENCE")
console.print(" • Чеклист живого прогона: docs/spike-notes.md")
def _live():
cfg = load_config(require_auth=True)
state = load_state()
@@ -118,12 +130,61 @@ def dry_run() -> None:
console.print("\n[bold]План[/bold]")
for line in dry_run_plan(checks):
console.print(f"{line}")
cfg = load_config(require_auth=False)
if cfg.auth_ok:
try:
from gpu_rent.inventory import looks_like_gpu, rank_flavors
from gpu_rent.os_client import iter_flavors
from gpu_rent.ux import format_flavor_lines
conn = connect(cfg)
flavors = list(iter_flavors(conn))
gpu = [f for f in flavors if looks_like_gpu(f)]
ranked = rank_flavors(gpu or flavors, cfg.flavor_preference)
console.print("\n[bold]Flavors[/bold]")
for line in format_flavor_lines(ranked):
console.print(f" {line}")
except GpuRentError as exc:
console.print(f" flavors: {exc}")
if blocking_failed(checks):
raise typer.Exit(1)
except GpuRentError as exc:
_die(exc)
@app.command()
def flavors() -> None:
"""Живой список GPU flavors по FLAVOR_PREFERENCE."""
try:
from gpu_rent.inventory import looks_like_gpu, rank_flavors, resolve_flavor
from gpu_rent.os_client import iter_flavors
from gpu_rent.ux import format_flavor_lines
cfg = load_config(require_auth=True)
conn = connect(cfg)
all_f = list(iter_flavors(conn))
gpu = [f for f in all_f if looks_like_gpu(f)]
ranked = rank_flavors(gpu or all_f, cfg.flavor_preference)
try:
picked = resolve_flavor(
all_f,
cfg.flavor_preference,
default_id=cfg.default_flavor_id or None,
fallback=cfg.flavor_fallback,
)
except ValueError:
picked = None
for line in format_flavor_lines(ranked, picked):
console.print(line)
console.print(
f"\nspot по умолчанию: {cfg.default_spot} "
f"(обычный: gpu-rent up --no-spot)"
)
console.print("₽ в API нет — смотри панель Selectel.")
except GpuRentError as exc:
_die(exc)
@app.command()
def status() -> None:
"""Локальный state + OpenStack, если .env есть. Туннель не нужен."""
@@ -149,9 +210,31 @@ def status() -> None:
cfg = load_config(require_auth=False)
listening = _port_open(cfg.swarmui_local_port)
table.add_row("туннель", f"localhost:{cfg.swarmui_local_port} {'слушает' if listening else 'нет'}")
table.add_row("₽/час", "нет цены в API — смотри панель / spike")
table.add_row("диск used/free", "нужен SSH на живую VM")
table.add_row("idle-killer", "на VM; локально не видно без SSH")
table.add_row(
"₽ / риски",
f"панель Selectel; диск {cfg.data_volume_size_gb}GB 24/7; "
f"killer {cfg.idle_minutes}м (+{cfg.idle_grace_minutes}м льгота)",
)
if state.floating_ip and cfg.ssh_private_key_path.is_file():
try:
df = run_ssh(
cfg,
state.floating_ip,
"df -h /mnt/swarm_data 2>/dev/null | tail -1",
check=False,
timeout=15,
).strip()
table.add_row("диск used/free", df or "нет df")
from gpu_rent.idle_killer import killer_status_lines
table.add_row("idle-killer", "; ".join(killer_status_lines(cfg, state.floating_ip)))
except GpuRentError as exc:
table.add_row("диск used/free", f"SSH: {exc}")
table.add_row("idle-killer", "нет SSH")
else:
table.add_row("диск used/free", "нужен живой FIP + SSH-ключ")
table.add_row("idle-killer", "нужен SSH на живую VM")
if cfg.auth_ok:
try:
@@ -306,10 +389,15 @@ def hold(
until: Optional[str] = typer.Option(None, "--until"),
clear: bool = typer.Option(False, "--clear"),
) -> None:
"""Отложить idle-killer на VM."""
del minutes, until, clear
"""Отложить idle-killer на VM (файл .gpu-rent-hold-until)."""
try:
_nyi("hold")
from gpu_rent.hold import clear_hold, set_hold
cfg, host = _live()
if clear:
clear_hold(cfg, host, log=lambda m: console.print(m))
return
set_hold(cfg, host, minutes=minutes, until=until, log=lambda m: console.print(m))
except GpuRentError as exc:
_die(exc)
@@ -389,10 +477,13 @@ def seed_extensions_cmd() -> None:
@app.command("resize-data")
def resize_data(gb: int = typer.Option(..., "--gb")) -> None:
del gb
def resize_data(gb: int = typer.Option(..., "--gb", help="Новый размер data volume, GB (только вверх)")) -> None:
"""Cinder extend data volume + resize2fs на VM."""
try:
_nyi("resize-data")
from gpu_rent.resize import resize_data_volume
cfg = load_config(require_auth=True)
resize_data_volume(cfg, gb, log=lambda m: console.print(m))
except GpuRentError as exc:
_die(exc)