"""Справочники платформы и сборка 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"])