from types import SimpleNamespace import pytest from gpu_rent.cloud import ( _exc_brief, _http_status, _is_create_policy_deny, _wrap, create_gpu_server, ) from gpu_rent.errors import CloudError POLICY_403 = ( "ForbiddenException: 403: Client Error for url: " "https://ru-6.cloud.api.selcloud.ru/compute/v2.1/servers, " "Policy doesn't allow os_compute_api:servers:create to be performed." ) class Forbidden(Exception): def __init__(self, msg: str, http_status: int = 403): super().__init__(msg) self.http_status = http_status def test_http_status_from_attr_and_text(): assert _http_status(Forbidden(POLICY_403)) == 403 assert _http_status(RuntimeError("no status")) is None def test_is_create_policy_deny(): assert _is_create_policy_deny(Forbidden(POLICY_403)) assert not _is_create_policy_deny(RuntimeError("quota exceeded for cores")) def test_exc_brief_strips_url_and_user_data(): dump = ( "BadRequestException: 400: Invalid input for field/attribute user_data. " "Value: #cloud-config\nssh_authorized_keys:\n" " - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFake gpu-rent\n" ) brief = _exc_brief(RuntimeError(dump)) assert "ssh-ed25519" not in brief assert "AAAAC3" not in brief assert "http" not in brief.lower() or "https://" not in brief def test_wrap_403_create_is_funds_not_gpu_quota(): err = _wrap(Forbidden(POLICY_403), "create server") msg = str(err) assert isinstance(err, CloudError) assert "GPU=0" not in msg assert "тикет" not in msg assert "ssh-ed25519" not in msg assert "https://" not in msg assert "Пополни" in msg or "нет средств" in msg or "кончились деньги" in msg def test_wrap_403_create_with_zero_balance(monkeypatch): monkeypatch.setattr("gpu_rent.balance.peek_balance_rub", lambda *_a, **_k: 0.0) msg = str(_wrap(Forbidden(POLICY_403), "create server", api_token="tok")) assert "0 ₽" in msg assert "нет средств" in msg assert "Пополни" in msg assert "GPU=0" not in msg def test_wrap_403_create_with_healthy_balance_is_iam(monkeypatch): monkeypatch.setattr("gpu_rent.balance.peek_balance_rub", lambda *_a, **_k: 1500.0) msg = str(_wrap(Forbidden(POLICY_403), "create server", api_token="tok")) assert "1500 ₽" in msg assert "IAM" in msg assert "GPU=0" not in msg def test_wrap_quota_message_only_when_quota_in_error(): exc = RuntimeError("403 OverLimit: quota exceeded for instances") msg = str(_wrap(exc, "create server")) assert "квота" in msg.lower() def test_create_gpu_server_stops_on_first_403(): calls: list[int] = [] class Compute: def create_server(self, **kw): calls.append(1) raise Forbidden(POLICY_403) logs: list[str] = [] with pytest.raises(CloudError) as caught: create_gpu_server( SimpleNamespace(compute=Compute()), flavor_id="1", net_id="n", sg_name="g", boot_volume_id="b", data_volume_id="d", az="ru-6a", spot=True, public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFake gpu-rent", log=logs.append, ) assert len(calls) == 1 assert logs == [] msg = str(caught.value) assert "GPU=0" not in msg assert "тикет" not in msg assert "ssh-ed25519" not in msg assert "Пополни" in msg or "нет средств" in msg or "кончились деньги" in msg