Add package data for GPU rent and update CLI documentation

- Added package data configuration for the 'gpu_rent' package in pyproject.toml.
- Updated README.md to include usage instructions for Windows and Unix launchers.
- Enhanced CLI documentation in cli.md to reflect new commands and their functionalities.
- Revised setup.md to clarify installation steps and environment setup.
- Improved error handling and command descriptions in the CLI implementation.
- Added new functions for model version handling and flavor resolution in the codebase.
- Updated state management to include additional properties for better tracking.
This commit is contained in:
Leonid Pershin
2026-08-21 03:06:51 +03:00
parent 167d07a733
commit 615cf81493
34 changed files with 2733 additions and 117 deletions
+282
View File
@@ -0,0 +1,282 @@
"""up / stop / destroy: GPU lifetime; SwarmUI bootstrap after SSH."""
from __future__ import annotations
from collections.abc import Callable
from gpu_rent.cloud import (
create_gpu_server,
delete_floating_ip,
delete_server,
ensure_boot_volume,
ensure_data_volume,
ensure_floating_ip,
ensure_keypair,
ensure_network,
ensure_security_group,
guess_operator_cidr,
pick_existing_server,
server_status,
unshelve,
)
from gpu_rent.bootstrap import run_bootstrap
from gpu_rent.provision import provision_vm
from gpu_rent.config import Config
from gpu_rent.errors import CloudError, GpuRentError
from gpu_rent.inventory import (
gpu_quota_from_compute,
pick_boot_image,
pick_volume_type,
resolve_flavor,
)
from gpu_rent.lock import SessionLock
from gpu_rent.os_client import (
KEYPAIR_NAME,
SG_NAME,
compute_quotas,
connect,
iter_flavors,
iter_images,
iter_volume_types,
)
from gpu_rent.ssh_keys import ensure_ed25519
from gpu_rent.ssh_ops import wait_ssh
from gpu_rent.state import SessionState, load_state, save_state, utc_now
Log = Callable[[str], None]
def _log_default(msg: str) -> None:
print(msg)
def _require_gpu_quota(conn) -> None:
quota = compute_quotas(conn)
limit = gpu_quota_from_compute(quota)
if limit is not None and limit <= 0:
raise CloudError(
"квота GPU = 0. Напиши в поддержку Selectel (текст в docs/setup.md). "
"CLI не создаст сервер."
)
def _bind_access(conn, server, state: SessionState, cfg: Config, log: Log) -> SessionState:
ip, fip_id = ensure_floating_ip(
conn, server, state.floating_ip_id, state.floating_ip, log
)
state.floating_ip = ip
if fip_id:
state.floating_ip_id = fip_id
save_state(state)
wait_ssh(cfg, ip)
log(f"SSH {cfg.ssh_user}@{ip}")
run_bootstrap(cfg, ip, log)
provision_vm(cfg, ip, log)
state.bootstrapped = True
save_state(state)
log("gpu-rent tunnel — UI на localhost:17801")
log("gpu-rent stop — удалить compute, диски оставить")
return state
def adopt_server(cfg: Config, log: Log = _log_default) -> SessionState:
conn = connect(cfg)
server = pick_existing_server(conn)
if not server:
raise CloudError("нечего adopt: нет сервера с тегом/именем gpu-rent")
state = load_state()
state.server_id = server.id
state.server_name = getattr(server, "name", None)
state.flavor_id = getattr(server, "flavor_id", None) or (
(server.flavor or {}).get("id") if isinstance(getattr(server, "flavor", None), dict) else None
)
state.phase = "ready_cloud"
state.availability_zone = cfg.gpu_rent_az
save_state(state)
log(f"подхватили {server.id} статус {server_status(server)}")
if server_status(server) == "ACTIVE":
_bind_access(conn, server, state, cfg, log)
return state
def cmd_up(
cfg: Config,
*,
no_spot: bool = False,
flavor: str | None = None,
yes: bool = False,
adopt: bool = False,
confirm: Callable[[str], bool] | None = None,
log: Log = _log_default,
) -> SessionState:
with SessionLock():
if adopt:
return adopt_server(cfg, log=log)
conn = connect(cfg)
_require_gpu_quota(conn)
state = load_state()
existing = pick_existing_server(conn)
if existing:
status = server_status(existing)
state.server_id = existing.id
state.server_name = getattr(existing, "name", None)
if status == "ACTIVE":
log("сервер уже ACTIVE — второй GPU не создаём")
state.phase = "ready_cloud"
save_state(state)
_bind_access(conn, existing, state, cfg, log)
return state
if status in {"EXPIRED", "SHELVED", "SHELVED_OFFLOADED"}:
existing = unshelve(conn, existing, log)
state.server_id = existing.id
state.phase = "ready_cloud"
state.unshelved_at = utc_now()
save_state(state)
_bind_access(conn, existing, state, cfg, log)
return state
raise CloudError(f"сервер gpu-rent в статусе {status} — разбери в панели")
flavors = list(iter_flavors(conn))
try:
picked = resolve_flavor(
flavors,
cfg.flavor_preference,
explicit=flavor,
default_id=cfg.default_flavor_id or None,
fallback=cfg.flavor_fallback,
)
except ValueError as exc:
raise CloudError(str(exc)) from exc
image = pick_boot_image(list(iter_images(conn)))
if not image:
raise CloudError("нет GPU-образа Ubuntu Driver 580/535 в Glance")
vtype = pick_volume_type(list(iter_volume_types(conn)), cfg.gpu_rent_az)
spot = cfg.default_spot and not no_spot
prompt = (
f"Создать {'preemptible ' if spot else ''}GPU {picked.name} "
f"в {cfg.gpu_rent_az}, образ {getattr(image, 'name', image.id)}, "
f"data {cfg.data_volume_size_gb} GB. Диск тарифицируется всегда. Продолжить?"
)
if not yes:
ok = confirm(prompt) if confirm else False
if not ok:
raise GpuRentError("отменено")
private_path, pub = ensure_ed25519(cfg.ssh_private_key_path)
del private_path
public_key = pub.read_text(encoding="utf-8")
ensure_keypair(conn, public_key, log)
net, _subnet = ensure_network(conn, log)
cidr = guess_operator_cidr()
if cidr == "0.0.0.0/0":
log("не удалось узнать твой IP — SG откроет SSH с 0.0.0.0/0")
sg = ensure_security_group(conn, cidr, log)
boot = ensure_boot_volume(
conn,
az=cfg.gpu_rent_az,
volume_type=vtype,
image_id=image.id,
snapshot_name=cfg.boot_snapshot_name,
existing_id=state.boot_volume_id or cfg.boot_volume_id,
log=log,
)
data = ensure_data_volume(
conn,
az=cfg.gpu_rent_az,
volume_type=vtype,
size_gb=cfg.data_volume_size_gb,
existing_id=state.data_volume_id or cfg.data_volume_id,
log=log,
)
state.phase = "provisioning"
state.flavor_id = picked.id
state.flavor_name = picked.name
state.boot_volume_id = boot.id
state.data_volume_id = data.id
state.image_id = image.id
state.network_id = net.id
state.security_group_id = sg.id
state.availability_zone = cfg.gpu_rent_az
state.spot = spot
state.keypair_name = KEYPAIR_NAME
save_state(state)
server = create_gpu_server(
conn,
flavor_id=picked.id,
net_id=net.id,
sg_name=SG_NAME,
boot_volume_id=boot.id,
data_volume_id=data.id,
az=cfg.gpu_rent_az,
spot=spot,
log=log,
)
state.server_id = server.id
state.server_name = getattr(server, "name", None)
state.created_at = utc_now()
state.unshelved_at = None
state.phase = "ready_cloud"
save_state(state)
_bind_access(conn, server, state, cfg, log)
return state
def cmd_stop(
cfg: Config,
*,
destroy_disks: bool = False,
no_pull: bool = False,
log: Log = _log_default,
) -> SessionState:
with SessionLock():
conn = connect(cfg)
state = load_state()
if cfg.pull_output and not no_pull and state.floating_ip:
try:
from gpu_rent.sync_files import pull_tree
pull_tree(cfg, state.floating_ip, "/mnt/swarm_data/Output", cfg.local_output_dir, log)
except Exception as exc:
log(f"pull Output не удался: {exc}")
server = None
if state.server_id:
try:
server = conn.compute.get_server(state.server_id)
except Exception:
server = None
if server is None:
server = pick_existing_server(conn)
if server:
delete_server(conn, server, log)
else:
log("compute уже нет")
if not cfg.keep_floating_ip:
delete_floating_ip(conn, state.floating_ip_id, state.floating_ip, log)
state.floating_ip = None
state.floating_ip_id = None
if destroy_disks:
for vid in (state.data_volume_id, state.boot_volume_id):
if not vid:
continue
try:
conn.block_storage.delete_volume(vid, ignore_missing=True)
log(f"удалён том {vid}")
except Exception as exc:
log(f"том {vid} не удалился: {exc}")
state.boot_volume_id = None
state.data_volume_id = None
state.server_id = None
state.server_name = None
state.phase = "idle"
save_state(state)
log("фаза idle" + ("" if destroy_disks else " (диски на месте)"))
return state