Pick cheapest GPU SKU and pin the VM to an existing disk AZ.

Scan ru-6 by Nova availability zones (a/b/c). First boot uses FLAVOR_SIZE_PRESET=cheap; reruns stay on the disk segment.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 00:06:44 +03:00
co-authored by Cursor
parent 5d0c30f66b
commit 789fa26918
19 changed files with 666 additions and 71 deletions
+55 -3
View File
@@ -9,13 +9,25 @@ from gpu_rent.inventory import (
class FakeFlavor:
def __init__(self, flavor_id: str, name: str, extra=None, disabled: bool = False):
def __init__(
self,
flavor_id: str,
name: str,
extra=None,
disabled: bool = False,
vcpus: int = 8,
ram: int = 32768,
disk: int = 0,
availability_zones=None,
):
self.id = flavor_id
self.name = name
self.extra_specs = extra or {}
self.is_disabled = disabled
self.vcpus = 8
self.ram = 32768
self.vcpus = vcpus
self.ram = ram
self.disk = disk
self.availability_zones = list(availability_zones or [])
def test_match_4090_24_not_48():
@@ -74,6 +86,46 @@ def test_resolve_flavor_no_fallback_requires_default():
assert picked.id == "ok"
def test_rank_cheap_prefers_1gpu_min_sku():
extra = {"aggregate_instance_extra_specs:gpu": "RTX4090", "gpu_memory": "24"}
flavors = [
FakeFlavor("dual", "GL10.8-65536-0-2GPU", extra, vcpus=8, ram=65536),
FakeFlavor("fat", "GL10.8-32768-0-1GPU", extra, vcpus=8, ram=32768),
FakeFlavor("tiny", "GL10.4-16384-0-1GPU", extra, vcpus=4, ram=16384),
]
ranked = rank_flavors(flavors, ("4090-24",), size_preset="cheap")
assert ranked[0].id == "tiny"
assert ranked[0].gpu_count == 1
balanced = rank_flavors(flavors, ("4090-24",), size_preset="balanced")
assert balanced[0].id == "fat"
def test_rank_filters_availability_zone():
extra = {"aggregate_instance_extra_specs:gpu": "RTX4090", "gpu_memory": "24"}
flavors = [
FakeFlavor(
"a",
"GL10.4-16384-0-1GPU",
extra,
vcpus=4,
ram=16384,
availability_zones=["ru-6a"],
),
FakeFlavor(
"h200",
"GL-H200",
{"aggregate_instance_extra_specs:gpu": "H200"},
vcpus=12,
ram=120000,
availability_zones=["ru-6b"],
),
]
in_a = rank_flavors(flavors, ("4090-24", "a5000"), az="ru-6a")
assert [x.id for x in in_a] == ["a"]
in_b = rank_flavors(flavors, ("4090-24",), az="ru-6b")
assert in_b == []
def test_pick_boot_image_prefers_24_580_without_docker():
class Img:
def __init__(self, name):
+58
View File
@@ -0,0 +1,58 @@
from gpu_rent.config import load_config
from gpu_rent.flavor_presets import format_char_impact, normalize_size_preset
from gpu_rent.placement import apply_az_policy, existing_disk_az
from gpu_rent.pools import PoolGpuOffer
from gpu_rent.state import SessionState
def test_normalize_size_preset():
assert normalize_size_preset("RAM") == "ram"
assert normalize_size_preset("nope") == "cheap"
assert "GPU" in "\n".join(format_char_impact())
def test_apply_az_policy_disk_wins(monkeypatch):
monkeypatch.setenv("OS_AUTH_URL", "https://example.invalid/identity/v3")
monkeypatch.setenv("OS_USER_DOMAIN_NAME", "1")
monkeypatch.setenv("OS_USERNAME", "u")
monkeypatch.setenv("OS_PASSWORD", "p")
monkeypatch.setenv("OS_PROJECT_ID", "proj")
monkeypatch.setenv("OS_REGION_NAME", "ru-6")
monkeypatch.setenv("GPU_RENT_AZ", "ru-6b")
cfg = load_config(require_auth=True)
best = PoolGpuOffer(region="ru-6", multizone=True, recommended_az="ru-6a")
placed = apply_az_policy(cfg, disk_az="ru-6a", best=best)
assert placed.reason == "disk"
assert placed.cfg.gpu_rent_az == "ru-6a"
def test_apply_az_policy_first_boot_cheap(monkeypatch):
monkeypatch.setenv("OS_AUTH_URL", "https://example.invalid/identity/v3")
monkeypatch.setenv("OS_USER_DOMAIN_NAME", "1")
monkeypatch.setenv("OS_USERNAME", "u")
monkeypatch.setenv("OS_PASSWORD", "p")
monkeypatch.setenv("OS_PROJECT_ID", "proj")
monkeypatch.setenv("OS_REGION_NAME", "ru-6")
monkeypatch.setenv("GPU_RENT_AZ", "ru-6b")
cfg = load_config(require_auth=True)
best = PoolGpuOffer(region="ru-6", multizone=True, recommended_az="ru-6a")
placed = apply_az_policy(cfg, disk_az=None, best=best)
assert placed.reason == "cheap"
assert placed.cfg.gpu_rent_az == "ru-6a"
def test_existing_disk_az_from_volume():
class Vol:
availability_zone = "ru-6a"
class Storage:
def get_volume(self, vid):
assert vid == "boot-1"
return Vol()
class Conn:
block_storage = Storage()
cfg = load_config(require_auth=False)
state = SessionState(boot_volume_id="boot-1")
assert existing_disk_az(Conn(), cfg, state) == "ru-6a"
+33 -1
View File
@@ -1,4 +1,4 @@
from gpu_rent.pools import PoolGpuOffer, format_pool_scan
from gpu_rent.pools import AzGpuOffer, PoolGpuOffer, format_pool_scan
def test_format_pool_scan_recommends_first_hit():
@@ -14,9 +14,41 @@ def test_format_pool_scan_recommends_first_hit():
matched=[
FlavorInfo("1", "GL-4090", 4, 16384, False, {}, "4090-24"),
],
recommended_az="ru-6a",
segments=[
AzGpuOffer(
az="ru-6a",
gpu_labels_found=["4090-24"],
gpu_types=["RTX4090"],
matched=[FlavorInfo("1", "GL-4090", 4, 16384, False, {}, "4090-24")],
),
AzGpuOffer(az="ru-6b", gpu_labels_found=[], gpu_types=["H200"], matched=[]),
AzGpuOffer(az="ru-6c", gpu_labels_found=[], gpu_types=["H200"], matched=[]),
],
),
]
text = "\n".join(format_pool_scan(offers, ("4090-24", "4090-48")))
assert "ru-6" in text and "multizone" in text
assert "типы:" in text
assert "ru-6a" in text and "ru-6b" in text and "ru-6c" in text
assert "рекомендация:" in text and "OS_REGION_NAME=ru-6" in text
assert "GPU_RENT_AZ=ru-6a" in text
def test_format_pool_scan_pins_disk_az():
from gpu_rent.inventory import FlavorInfo
offers = [
PoolGpuOffer(
region="ru-6",
multizone=True,
gpu_labels_found=["4090-24"],
gpu_types=["RTX4090"],
matched=[FlavorInfo("1", "GL-4090", 4, 16384, False, {}, "4090-24")],
recommended_az="ru-6a",
)
]
text = "\n".join(
format_pool_scan(offers, ("4090-24",), pin_az="ru-6a", size_preset="cheap")
)
assert "диск уже в ru-6a" in text
+2 -2
View File
@@ -29,9 +29,9 @@ def test_prompt_server_plan_with_ranked(monkeypatch, tmp_path):
monkeypatch.setattr(
"gpu_rent.ux.list_ranked_flavors",
lambda flavors, cfg: [f1, f2],
lambda flavors, cfg, **kw: [f1, f2],
)
answers = iter(["2", "200", "2"])
answers = iter(["1", "2", "200", "2"])
def ask(msg: str, default: str = "") -> str:
return next(answers)