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:
+108
-17
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user