Files
viktor 7e798bb68f Полное покрытие API
Клиент дописан до всех двадцати семи эндпоинтов платформы.

Добавлено:
- регионы, образы с разбивкой на системы и готовые сборки, вторая версия API образов
- все действия над сервером: старт, стоп, перезагрузка, переустановка, смена тарифа,
  клонирование, откат, консоль, бэкапы
- снимки целиком, включая ожидание готовности и поиск поднятых из них серверов
- ключи доступа, IP-адреса с обратными записями, приватные сети, kubeconfig
- история операций, биллинг, расход в час и в сутки
- сборка и проверка slug тарифа по правилам платформы
- ошибки разложены по смыслу, чтобы ловить прицельно

Claude-Session: https://claude.ai/code/session_01Loz5stgQZH6ofBdMqTxiJu
2026-08-23 18:30:07 +09:00

135 строки
6.5 KiB
Python

"""Справочники платформы и сборка slug'ов.
Часть этих данных API не отдаёт вовсе, часть отдаёт неполно, поэтому держим
их тут. Проверено 23.08.2026, при расхождении верить панели управления,
раздел настроек, пункт со slug'ами для API и Terraform.
"""
from __future__ import annotations
# Регионы. Список подтверждён самой платформой в тексте ошибки валидации.
REGIONS = {
"openstack-msk1": "Москва",
"openstack-msk2": "Москва-2",
"openstack-msk3": "Москва-3",
"openstack-spb1": "Санкт-Петербург",
"openstack-sam1": "Самара",
"openstack-fz1": "Москва ФЗ-152",
"msk1": "Москва, архивные тарифы",
}
# Линейки обычных тарифов
PLAN_LINES = {
"std": "Стандартный",
"stdp": "Стандартный плюс",
"hp": "Производительный",
"hps": "Производительный ФЗ-152",
"ddc": "Выделенный CPU",
"high": "Высокочастотный",
}
# Допустимые сочетания ядер и памяти по линейкам
CPU_RAM = {
"std": {1: [1, 2, 3, 4], 2: [2, 3, 6, 8], 3: [3, 4, 6, 8, 10],
4: [4, 6, 8, 10, 12, 16], 6: [6, 8, 10, 12, 16],
8: [8, 10, 12, 16, 20, 24, 32], 10: [10, 12, 16, 20, 24, 32, 40],
12: [12, 16, 20, 24, 32, 40, 48], 16: [16, 20, 24, 32, 40, 48],
20: [20, 24, 32, 40, 48], 24: [24, 32, 40, 48]},
"hp": {1: [1, 2, 3, 4], 2: [2, 3, 6, 8], 3: [3, 4, 6, 8, 10],
4: [4, 6, 8, 10, 12, 16], 6: [6, 8, 10, 12, 16],
8: [8, 10, 12, 16, 20, 24, 32], 10: [10, 12, 16, 20, 24, 32, 40],
12: [12, 16, 20, 24, 32, 40, 48], 16: [16, 20, 24, 32, 40, 48],
20: [20, 24, 32, 40, 48, 56, 64], 24: [24, 32, 40, 48, 56, 64, 72],
32: [32, 40, 48, 56, 64, 72, 80], 40: [40, 48, 56, 64, 72, 80],
48: [48, 56, 64, 72, 80]},
"high": {1: [1, 2, 3, 4], 2: [2, 3, 6, 8], 3: [3, 4, 6, 8, 10],
4: [4, 6, 8, 10, 12, 16], 6: [6, 8, 10, 12, 16],
8: [8, 10, 12, 16, 20, 24, 32], 10: [10, 12, 16, 20, 24, 32],
12: [12, 16, 20, 24, 32]},
}
CPU_RAM["stdp"] = CPU_RAM["std"]
CPU_RAM["hps"] = CPU_RAM["hp"]
CPU_RAM["ddc"] = CPU_RAM["hp"]
# Диапазон диска по линейкам, гигабайты, шаг десять
DISK_RANGE = {
"std": (10, 480), "stdp": (10, 480),
"hp": (10, 960), "hps": (10, 960), "ddc": (10, 960),
"high": (20, 320),
}
# Тарифы с видеокартой. Свои конфигурации для них собирать нельзя,
# только эти готовые. Цены в рублях за час на 23.08.2026.
GPU_SIZES = {
"c8-m32-d128-g1-a4000-gpu": ("A4000 16 ГБ", 8, 32, 128, 37.80),
"c16-m64-d256-g1-a4000-gpu": ("A4000 16 ГБ", 16, 64, 256, 51.87),
"c16-m64-d256-g1-a5000-gpu": ("A5000 24 ГБ", 16, 64, 256, 57.00),
"c24-m128-d512-g1-a5000-gpu": ("A5000 24 ГБ", 24, 128, 512, 82.79),
"c24-m128-d512-g2-a5000-gpu": ("2x A5000 24 ГБ", 24, 128, 512, 111.65),
"c32-m256-d1024-g1-a5000-gpu": ("A5000 24 ГБ", 32, 256, 1024, 132.01),
"c48-m384-d1024-g2-a5000-gpu": ("2x A5000 24 ГБ", 48, 384, 1024, 203.30),
"c16-m128-d512-g1-a100-gpu": ("A100 80 ГБ", 16, 128, 512, 222.14),
"c32-m256-d1024-g1-a100-gpu": ("A100 80 ГБ", 32, 256, 1024, 273.72),
"c64-m512-d1024-g2-a100-gpu": ("2x A100 80 ГБ", 64, 512, 1024, 529.14),
"c24-m96-d384-g1-rtx6000-gpu": ("RTX 6000 Blackwell", 24, 96, 384, 205.07),
"c32-m128-d512-g1-rtx6000-gpu": ("RTX 6000 Blackwell", 32, 128, 512, 223.43),
"c64-m192-d1024-g1-rtx6000-gpu": ("RTX 6000 Blackwell", 64, 192, 1024, 271.15),
"c32-m128-d1024-g2-rtx6000-gpu": ("2x RTX 6000 Blackwell", 32, 128, 1024, 382.58),
}
# Типы образов в API
IMAGE_TYPES = ("distribution", "application", "snapshot", "backup", "restore")
# Действия над сервером
ACTIONS = ("start", "stop", "reboot", "rebuild", "password_reset", "resize",
"generate_vnc_link", "snapshot", "enable_backups", "disable_backups",
"restore", "clone", "resize_isp_license")
ISP_LICENSES = ("isp_lite6", "isp_pro6", "isp_host6")
def build_size(cpu: int, ram: int, disk: int, line: str = "hp") -> str:
"""Собирает slug тарифа и проверяет сочетание по правилам платформы.
build_size(4, 8, 120, "hp") -> 'c4-m8-d120-hp'
"""
if line not in PLAN_LINES:
raise ValueError(f"нет линейки {line}, есть {', '.join(PLAN_LINES)}")
allowed = CPU_RAM.get(line, {})
if cpu not in allowed:
raise ValueError(f"в линейке {line} нет варианта на {cpu} ядер, "
f"доступны {sorted(allowed)}")
if ram not in allowed[cpu]:
raise ValueError(f"при {cpu} ядрах в линейке {line} память может быть "
f"{allowed[cpu]}, а не {ram}")
low, high = DISK_RANGE[line]
if not (low <= disk <= high) or disk % 10:
raise ValueError(f"диск в линейке {line} от {low} до {high} ГБ с шагом 10, а не {disk}")
return f"c{cpu}-m{ram}-d{disk}-{line}"
def parse_size(slug: str) -> dict:
"""Разбирает slug обратно в понятные значения."""
if slug in GPU_SIZES:
name, cpu, ram, disk, price = GPU_SIZES[slug]
return {"slug": slug, "gpu": name, "vcpus": cpu, "memory_gb": ram,
"disk_gb": disk, "price_hour": price, "line": "gpu"}
parts = slug.split("-")
try:
return {
"slug": slug,
"vcpus": int(parts[0].lstrip("c")),
"memory_gb": int(parts[1].lstrip("m")),
"disk_gb": int(parts[2].lstrip("d")),
"line": parts[3] if len(parts) > 3 else "",
"gpu": None,
}
except (IndexError, ValueError):
return {"slug": slug}
def gpu_catalog() -> list[dict]:
"""Тарифы с видеокартой списком, отсортированные по цене."""
out = [{"slug": s, "gpu": v[0], "vcpus": v[1], "memory_gb": v[2],
"disk_gb": v[3], "price_hour": v[4]} for s, v in GPU_SIZES.items()]
return sorted(out, key=lambda x: x["price_hour"])