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
+16
View File
@@ -0,0 +1,16 @@
from gpu_rent.bootstrap import bootstrap_script
def test_bootstrap_script_is_native_swarmui():
script = bootstrap_script()
assert "docker.io" not in script
assert "docker run" not in script
assert "nvidia-container" not in script
assert "/opt/swarmui" in script
assert "/mnt/swarm_data" in script
assert "launch-linux.sh --launch_mode none --host 127.0.0.1 --port 7801" in script
assert "systemctl enable swarmui" in script
assert "mkfs.ext4" in script
assert "apt-get upgrade" not in script
assert ".gpu-rent-ready" in script
assert "src/BuiltinExtensions/ComfyUIBackend/DLNodes" in script
+43
View File
@@ -0,0 +1,43 @@
from gpu_rent.cloud import ensure_floating_ip, server_floating_ip, server_status
from gpu_rent.lock import SessionLock
from gpu_rent.paths import lock_path
class Obj:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def test_server_floating_ip():
server = Obj(
status="ACTIVE",
addresses={"gpu-rent": [{"addr": "192.168.77.10", "OS-EXT-IPS:type": "fixed"}, {"addr": "203.0.113.9", "OS-EXT-IPS:type": "floating"}]},
)
assert server_status(server) == "ACTIVE"
assert server_floating_ip(server) == "203.0.113.9"
def test_lock_roundtrip():
with SessionLock():
assert lock_path().is_file()
assert not lock_path().is_file()
def test_ensure_floating_ip_reuses_existing():
class Net:
def ports(self, device_id=None):
return [Obj(id="p1")]
def update_ip(self, existing_id, port_id=None):
assert existing_id == "old-fip"
assert port_id == "p1"
return Obj(floating_ip_address="9.9.9.9")
def create_ip(self, floating_network_id=None):
raise AssertionError("must reuse FIP")
conn = Obj(network=Net())
server = Obj(id="s1", addresses={})
ip, fip_id = ensure_floating_ip(conn, server, "old-fip", "9.9.9.9", lambda m: None)
assert ip == "9.9.9.9"
assert fip_id == "old-fip"
+33
View File
@@ -4,6 +4,7 @@ from gpu_rent.inventory import (
match_label,
pick_boot_image,
rank_flavors,
resolve_flavor,
)
@@ -41,6 +42,38 @@ def test_gpu_quota_from_compute():
assert gpu_quota_from_compute({"GPU_limit": 2}) == 2
def test_resolve_flavor_explicit_and_preference():
flavors = [
FakeFlavor("d", "RTX 4090 24GB", disabled=True),
FakeFlavor("a5", "RTX A5000 24GB"),
FakeFlavor("ok", "GPU 1x RTX 4090 24GB"),
]
picked = resolve_flavor(flavors, ("4090-24", "a5000"))
assert picked.id == "ok"
explicit = resolve_flavor(flavors, ("a5000",), explicit="a5")
assert explicit.id == "a5"
def test_resolve_flavor_prefers_list_over_default_id():
flavors = [
FakeFlavor("a5", "RTX A5000 24GB"),
FakeFlavor("ok", "GPU 1x RTX 4090 24GB"),
]
picked = resolve_flavor(flavors, ("4090-24", "a5000"), default_id="a5", fallback=True)
assert picked.id == "ok"
def test_resolve_flavor_no_fallback_requires_default():
flavors = [FakeFlavor("ok", "GPU 1x RTX 4090 24GB")]
try:
resolve_flavor(flavors, ("4090-24",), fallback=False)
raise AssertionError("expected ValueError")
except ValueError:
pass
picked = resolve_flavor(flavors, ("a5000",), default_id="ok", fallback=False)
assert picked.id == "ok"
def test_pick_boot_image_prefers_24_580_without_docker():
class Img:
def __init__(self, name):
+19
View File
@@ -0,0 +1,19 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_launcher_files_exist():
for name in ("gpu-rent.bat", "gpu-rent.ps1", "gpu-rent.sh"):
path = ROOT / name
assert path.is_file(), name
text = path.read_text(encoding="utf-8")
assert ".venv" in text or "gpu-rent.ps1" in text
assert "gpu_rent" in text or "gpu-rent.ps1" in text
def test_launchers_require_python_311():
ps1 = (ROOT / "gpu-rent.ps1").read_text(encoding="utf-8")
sh = (ROOT / "gpu-rent.sh").read_text(encoding="utf-8")
assert "3, 11" in ps1 or "3.11" in ps1
assert "3, 11" in sh
+132
View File
@@ -0,0 +1,132 @@
import pytest
from gpu_rent.config import load_config
from gpu_rent.errors import CloudError, GpuRentError
from gpu_rent.lock import SessionLock
from gpu_rent.paths import lock_path
from gpu_rent.session import cmd_stop, cmd_up
from gpu_rent.state import SessionState, save_state
class Server:
def __init__(self, status="ACTIVE", server_id="s1"):
self.id = server_id
self.name = "gpu-rent"
self.status = status
self.flavor = {"id": "f1"}
self.addresses = {
"gpu-rent": [{"addr": "203.0.113.9", "OS-EXT-IPS:type": "floating"}]
}
def _cfg(monkeypatch):
monkeypatch.setenv("OS_AUTH_URL", "https://example.invalid/identity/v3")
monkeypatch.setenv("OS_USER_DOMAIN_NAME", "999")
monkeypatch.setenv("OS_USERNAME", "svc")
monkeypatch.setenv("OS_PASSWORD", "secret")
monkeypatch.setenv("OS_PROJECT_ID", "proj")
monkeypatch.setenv("OS_REGION_NAME", "ru-7")
monkeypatch.setenv("GPU_RENT_AZ", "ru-7a")
return load_config(require_auth=True)
def test_cmd_up_refuses_zero_gpu_quota(monkeypatch):
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
monkeypatch.setattr("gpu_rent.session.compute_quotas", lambda conn: {"gpu": 0})
with pytest.raises(CloudError, match="квота GPU"):
cmd_up(_cfg(monkeypatch), yes=True)
def test_cmd_up_does_not_create_second_gpu(monkeypatch):
created = []
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
monkeypatch.setattr("gpu_rent.session.compute_quotas", lambda conn: {"gpu": 1})
monkeypatch.setattr("gpu_rent.session.pick_existing_server", lambda conn: Server())
monkeypatch.setattr(
"gpu_rent.session.ensure_floating_ip",
lambda conn, server, existing_id, existing_addr, log: ("203.0.113.9", "fip1"),
)
monkeypatch.setattr("gpu_rent.session.wait_ssh", lambda cfg, host, timeout=420.0: None)
monkeypatch.setattr("gpu_rent.session.run_bootstrap", lambda cfg, host, log: None)
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log: None)
monkeypatch.setattr(
"gpu_rent.session.create_gpu_server",
lambda *a, **k: created.append("created") or Server(),
)
state = cmd_up(_cfg(monkeypatch), yes=True)
assert created == []
assert state.server_id == "s1"
assert state.phase == "ready_cloud"
assert state.floating_ip == "203.0.113.9"
assert state.bootstrapped is True
def test_cmd_up_unshelves_expired(monkeypatch):
unshelved = []
created = []
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
monkeypatch.setattr("gpu_rent.session.compute_quotas", lambda conn: {"gpu": 1})
monkeypatch.setattr(
"gpu_rent.session.pick_existing_server", lambda conn: Server(status="EXPIRED")
)
monkeypatch.setattr(
"gpu_rent.session.unshelve",
lambda conn, server, log: unshelved.append(server.id) or Server(status="ACTIVE"),
)
monkeypatch.setattr(
"gpu_rent.session.ensure_floating_ip",
lambda conn, server, existing_id, existing_addr, log: ("203.0.113.9", "fip1"),
)
monkeypatch.setattr("gpu_rent.session.wait_ssh", lambda cfg, host, timeout=420.0: None)
monkeypatch.setattr("gpu_rent.session.run_bootstrap", lambda cfg, host, log: None)
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log: None)
monkeypatch.setattr(
"gpu_rent.session.create_gpu_server",
lambda *a, **k: created.append("created"),
)
state = cmd_up(_cfg(monkeypatch), yes=True)
assert unshelved == ["s1"]
assert created == []
assert state.unshelved_at
assert state.phase == "ready_cloud"
assert state.bootstrapped is True
def test_cmd_stop_deletes_compute_keeps_disks(monkeypatch):
deleted = []
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: object())
monkeypatch.setattr("gpu_rent.session.pick_existing_server", lambda conn: Server())
monkeypatch.setattr(
"gpu_rent.session.delete_server",
lambda conn, server, log: deleted.append(server.id),
)
monkeypatch.setattr(
"gpu_rent.session.delete_floating_ip",
lambda conn, fip_id, address, log: deleted.append("fip"),
)
save_state(
SessionState(server_id="s1", boot_volume_id="b1", data_volume_id="d1", floating_ip="1.1.1.1")
)
class Conn:
class compute:
@staticmethod
def get_server(sid):
return Server(server_id=sid)
monkeypatch.setattr("gpu_rent.session.connect", lambda cfg: Conn())
state = cmd_stop(_cfg(monkeypatch))
assert "s1" in deleted
assert state.phase == "idle"
assert state.server_id is None
assert state.boot_volume_id == "b1"
assert state.data_volume_id == "d1"
def test_lock_busy(monkeypatch):
lock_path().parent.mkdir(parents=True, exist_ok=True)
lock_path().write_text("1", encoding="utf-8")
monkeypatch.setattr("gpu_rent.lock._pid_alive", lambda pid: True)
monkeypatch.setattr("gpu_rent.lock.os.getpid", lambda: 99)
with pytest.raises(GpuRentError, match="уже работает"):
SessionLock().__enter__()