"""User-facing prints: flavor list, cost warnings (no invented ₽).""" from __future__ import annotations from collections.abc import Callable from typing import Any from gpu_rent.config import Config from gpu_rent.inventory import FlavorInfo, looks_like_gpu, rank_flavors, resolve_flavor Log = Callable[[str], None] def list_ranked_flavors(flavors: list[Any], cfg: Config) -> list[FlavorInfo]: gpu = [f for f in flavors if looks_like_gpu(f)] return rank_flavors(gpu or flavors, cfg.flavor_preference) def format_flavor_lines(ranked: list[FlavorInfo], picked: FlavorInfo | None = None) -> list[str]: if not ranked: return ["flavors: пусто (проверь регион / FLAVOR_PREFERENCE)"] lines = ["flavors (по FLAVOR_PREFERENCE):"] for i, info in enumerate(ranked, 1): mark = " ← выберем" if picked and info.id == picked.id else "" ram = f"{info.ram_mb // 1024}GB" if info.ram_mb else "?" vcpu = str(info.vcpus) if info.vcpus is not None else "?" label = info.label or "—" lines.append(f" {i}. [{label}] {info.name} vCPU={vcpu} RAM={ram} id={info.id}{mark}") return lines def cost_and_risk_lines(cfg: Config, *, spot: bool, flavor_name: str) -> list[str]: """Honest billing notes — OpenStack has no ₽ prices.""" return [ f"план: {'preemptible ' if spot else ''}{flavor_name}, data {cfg.data_volume_size_gb} GB", "₽: цены в OpenStack API нет — смотри панель Selectel (GPU ₽/час + диск ₽/мес).", "диск data тарифицируется 24/7, даже когда GPU выключен (stop).", f"idle-killer: {cfg.idle_grace_minutes} мин льготы после boot, потом " f"{cfg.idle_minutes} мин пустой очереди → delete compute. Отложить: gpu-rent hold", "preemptible: хостер может усыпить (~24 ч окно) → EXPIRED; tunnel сам unshelve, " "или gpu-rent up", "Ctrl+C на tunnel GPU не гасит — только gpu-rent stop или idle-killer. " "Опционально: gpu-rent watchdog install — аварийное закрытие окна/ребут " "после grace тоже stop (Ctrl+C по-прежнему detach)", ] def print_up_preview( cfg: Config, flavors: list[Any], *, picked: FlavorInfo, spot: bool, log: Log, ) -> None: ranked = list_ranked_flavors(flavors, cfg) for line in format_flavor_lines(ranked, picked): log(line) log("") for line in cost_and_risk_lines(cfg, spot=spot, flavor_name=picked.name): log(f"! {line}") def resolve_and_preview( cfg: Config, flavors: list[Any], *, explicit: str | None, spot: bool, log: Log, ) -> FlavorInfo: picked = resolve_flavor( flavors, cfg.flavor_preference, explicit=explicit, default_id=cfg.default_flavor_id or None, fallback=cfg.flavor_fallback, ) print_up_preview(cfg, flavors, picked=picked, spot=spot, log=log) return picked