From a34d2e1800ae7688bc4ef9fe529f5f7e55e88691 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Thu, 9 Jul 2026 16:06:39 +0500 Subject: [PATCH] Drop cudo backend --- .../_internal/core/backends/configurators.py | 9 - .../_internal/core/backends/cudo/__init__.py | 0 .../core/backends/cudo/api_client.py | 111 ----------- .../_internal/core/backends/cudo/backend.py | 16 -- .../_internal/core/backends/cudo/compute.py | 174 ------------------ .../core/backends/cudo/configurator.py | 63 ------- .../_internal/core/backends/cudo/models.py | 2 + .../core/backends/cudo/test_configurator.py | 37 ---- .../_internal/server/routers/test_backends.py | 1 - 9 files changed, 2 insertions(+), 411 deletions(-) delete mode 100644 src/dstack/_internal/core/backends/cudo/__init__.py delete mode 100644 src/dstack/_internal/core/backends/cudo/api_client.py delete mode 100644 src/dstack/_internal/core/backends/cudo/backend.py delete mode 100644 src/dstack/_internal/core/backends/cudo/compute.py delete mode 100644 src/dstack/_internal/core/backends/cudo/configurator.py delete mode 100644 src/tests/_internal/core/backends/cudo/test_configurator.py diff --git a/src/dstack/_internal/core/backends/configurators.py b/src/dstack/_internal/core/backends/configurators.py index bcc144cf5f..fe30d42e7b 100644 --- a/src/dstack/_internal/core/backends/configurators.py +++ b/src/dstack/_internal/core/backends/configurators.py @@ -44,15 +44,6 @@ except ImportError: pass -try: - from dstack._internal.core.backends.cudo.configurator import ( - CudoConfigurator, - ) - - _CONFIGURATOR_CLASSES.append(CudoConfigurator) -except ImportError: - pass - try: from dstack._internal.core.backends.datacrunch.configurator import ( DataCrunchConfigurator, diff --git a/src/dstack/_internal/core/backends/cudo/__init__.py b/src/dstack/_internal/core/backends/cudo/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/dstack/_internal/core/backends/cudo/api_client.py b/src/dstack/_internal/core/backends/cudo/api_client.py deleted file mode 100644 index dbf006b0fa..0000000000 --- a/src/dstack/_internal/core/backends/cudo/api_client.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import Any, Dict, List, Optional - -import requests -from requests import Response - -from dstack._internal.core.errors import BackendInvalidCredentialsError -from dstack._internal.utils.logging import get_logger -from dstack._internal.utils.ssh import get_public_key_fingerprint - -API_URL = "https://rest.compute.cudo.org/v1" - -logger = get_logger(__name__) - - -class CudoApiClient: - def __init__(self, api_key: str): - self.api_key = api_key - - def validate_api_key(self) -> bool: - try: - self.get_user_details() - except BackendInvalidCredentialsError: - return False - return True - - def get_user_details(self) -> Dict: - resp = self._make_request("GET", "/auth") - return resp.json() - - def create_virtual_machine( - self, - project_id: str, - boot_disk_storage_class: str, - boot_disk_size_gib: int, - book_disk_id: str, - boot_disk_image_id: str, - data_center_id: str, - gpus: int, - machine_type: str, - memory_gib: int, - password: Optional[str], - vcpus: int, - vm_id: str, - customSshKeys, - start_script: Optional[str] = None, - ) -> Dict: - data = { - "bootDisk": { - "storage_class": boot_disk_storage_class, - "size_gib": boot_disk_size_gib, - "id": book_disk_id, - }, - "bootDiskImageId": boot_disk_image_id, - "dataCenterId": data_center_id, - "gpus": gpus, - "machineType": machine_type, - "memoryGib": memory_gib, - "password": password, - "vcpus": vcpus, - "vmId": vm_id, - "startScript": start_script, - "customSshKeys": customSshKeys, - } - resp = self._make_request("POST", f"/projects/{project_id}/vm", data) - return resp.json() - - def terminate_virtual_machine(self, vm_id: str, project_id: str) -> Dict: - resp = self._make_request("POST", f"/projects/{project_id}/vms/{vm_id}/terminate") - return resp.json() - - def _make_request(self, method: str, path: str, data: Any = None) -> Response: - try: - response = requests.request( - method=method, - url=API_URL + path, - json=data, - headers={"Authorization": f"Bearer {self.api_key}"}, - timeout=30, - ) - response.raise_for_status() - return response - except requests.HTTPError as e: - if e.response is not None and e.response.status_code in ( - requests.codes.forbidden, - requests.codes.unauthorized, - ): - raise BackendInvalidCredentialsError(e.response.text) - raise - - def get_or_create_ssh_key(self, public_key: str) -> str: - fingerprint = get_public_key_fingerprint(public_key) - keys = self.list_ssh_keys() - found_keys = [key for key in keys if fingerprint == key["fingerprint"]] - if found_keys: - key = found_keys[0] - return key["id"] - key_id = self.create_ssh_key(public_key) - return key_id - - def list_ssh_keys(self) -> List[Dict]: - resp = self._make_request("GET", "/ssh-keys") - return resp.json()["sshKeys"] - - def create_ssh_key(self, public_key: str) -> str: - data = {"publicKey": public_key} - resp = self._make_request("POST", "/ssh-keys", data) - return resp.json()["id"] - - def get_vm(self, project_id, vm_id) -> Dict: - resp = self._make_request("GET", f"/projects/{project_id}/vms/{vm_id}") - return resp.json() diff --git a/src/dstack/_internal/core/backends/cudo/backend.py b/src/dstack/_internal/core/backends/cudo/backend.py deleted file mode 100644 index b7e724058f..0000000000 --- a/src/dstack/_internal/core/backends/cudo/backend.py +++ /dev/null @@ -1,16 +0,0 @@ -from dstack._internal.core.backends.base.backend import Backend -from dstack._internal.core.backends.cudo.compute import CudoCompute -from dstack._internal.core.backends.cudo.models import CudoConfig -from dstack._internal.core.models.backends.base import BackendType - - -class CudoBackend(Backend): - TYPE = BackendType.CUDO - COMPUTE_CLASS = CudoCompute - - def __init__(self, config: CudoConfig): - self.config = config - self._compute = CudoCompute(self.config) - - def compute(self) -> CudoCompute: - return self._compute diff --git a/src/dstack/_internal/core/backends/cudo/compute.py b/src/dstack/_internal/core/backends/cudo/compute.py deleted file mode 100644 index 0dbddcba0e..0000000000 --- a/src/dstack/_internal/core/backends/cudo/compute.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import List, Optional - -import requests - -from dstack._internal.core.backends.base.backend import Compute -from dstack._internal.core.backends.base.compute import ( - ComputeWithCreateInstanceSupport, - ComputeWithFilteredOffersCached, - ComputeWithInstanceVolumesSupport, - ComputeWithPrivilegedSupport, - generate_unique_instance_name, - get_shim_commands, -) -from dstack._internal.core.backends.base.offers import get_catalog_offers -from dstack._internal.core.backends.cudo.api_client import CudoApiClient -from dstack._internal.core.backends.cudo.models import CudoConfig -from dstack._internal.core.errors import BackendError, NoCapacityError, ProvisioningError -from dstack._internal.core.models.backends.base import BackendType -from dstack._internal.core.models.instances import ( - InstanceAvailability, - InstanceConfiguration, - InstanceOfferWithAvailability, -) -from dstack._internal.core.models.placement import PlacementGroup -from dstack._internal.core.models.runs import JobProvisioningData, Requirements -from dstack._internal.utils.logging import get_logger - -logger = get_logger(__name__) - - -MAX_RESOURCE_NAME_LEN = 30 - - -class CudoCompute( - ComputeWithFilteredOffersCached, - ComputeWithCreateInstanceSupport, - ComputeWithPrivilegedSupport, - ComputeWithInstanceVolumesSupport, - Compute, -): - def __init__(self, config: CudoConfig): - super().__init__() - self.config = config - self.api_client = CudoApiClient(config.creds.api_key) - - def get_offers_by_requirements( - self, requirements: Requirements - ) -> List[InstanceOfferWithAvailability]: - offers = get_catalog_offers( - backend=BackendType.CUDO, - locations=self.config.regions, - requirements=requirements, - ) - offers = [ - offer.with_availability(availability=InstanceAvailability.AVAILABLE) - for offer in offers - # in-hyderabad-1 is known to have provisioning issues - if offer.region not in ["in-hyderabad-1"] - ] - return offers - - def create_instance( - self, - instance_offer: InstanceOfferWithAvailability, - instance_config: InstanceConfiguration, - placement_group: Optional[PlacementGroup], - ) -> JobProvisioningData: - vm_id = generate_unique_instance_name(instance_config, max_length=MAX_RESOURCE_NAME_LEN) - public_keys = instance_config.get_public_keys() - memory_size = round(instance_offer.instance.resources.memory_mib / 1024) - disk_size = round(instance_offer.instance.resources.disk.size_mib / 1024) - gpus_no = len(instance_offer.instance.resources.gpus) - if gpus_no > 0: - # we'll need jq for patching /etc/docker/daemon.json, see get_shim_commands() - commands = install_jq_commands() - else: - commands = install_docker_commands() - commands += get_shim_commands() - - try: - resp_data = self.api_client.create_virtual_machine( - project_id=self.config.project_id, - boot_disk_storage_class="STORAGE_CLASS_NETWORK", - boot_disk_size_gib=disk_size, - book_disk_id=f"{vm_id}_disk_id", - boot_disk_image_id=_get_image_id(gpus_no > 0), - data_center_id=instance_offer.region, - gpus=gpus_no, - machine_type=instance_offer.instance.name, - memory_gib=memory_size, - vcpus=instance_offer.instance.resources.cpus, - vm_id=vm_id, - start_script=" && ".join(commands), - password=None, - customSshKeys=public_keys, - ) - except requests.HTTPError as e: - try: - details = e.response.json() - response_code = details.get("code") - except ValueError: - raise BackendError(e) - - # code 3: There are no hosts available for your specified virtual machine. - if response_code == 3: - raise NoCapacityError(details.get("message")) - # code 9: Vm cannot be assigned ip from network. Network full - if response_code == 9: - raise BackendError(details.get("message")) - # code 6: A disk with that id already exists - if response_code == 6: - raise BackendError(details.get("message")) - - launched_instance = JobProvisioningData( - backend=instance_offer.backend, - instance_type=instance_offer.instance, - instance_id=resp_data["id"], - hostname=None, - internal_ip=None, - region=instance_offer.region, - price=instance_offer.price, - ssh_port=22, - username="root", - ssh_proxy=None, - dockerized=True, - backend_data=None, - ) - return launched_instance - - def terminate_instance( - self, instance_id: str, region: str, backend_data: Optional[str] = None - ) -> None: - try: - self.api_client.terminate_virtual_machine(instance_id, self.config.project_id) - except requests.HTTPError as e: - if e.response is not None and e.response.status_code == requests.codes.not_found: - logger.debug("The instance with name %s not found", instance_id) - return - raise BackendError(e.response.text if e.response is not None else str(e)) - - def update_provisioning_data( - self, - provisioning_data: JobProvisioningData, - project_ssh_public_key: str, - project_ssh_private_key: str, - ): - vm = self.api_client.get_vm(self.config.project_id, provisioning_data.instance_id) - if vm["VM"]["state"] == "ACTIVE": - provisioning_data.hostname = vm["VM"]["publicIpAddress"] - if vm["VM"]["state"] == "FAILED": - raise ProvisioningError("VM entered FAILED state", vm) - - -def _get_image_id(cuda: bool) -> str: - image_name = "ubuntu-2204-nvidia-535-docker-v20241017" if cuda else "ubuntu-2204" - return image_name - - -def install_jq_commands(): - return [ - "export DEBIAN_FRONTEND=noninteractive", - "apt-get --assume-yes install jq", - ] - - -def install_docker_commands(): - return [ - "export DEBIAN_FRONTEND=noninteractive", - "mkdir -p /etc/apt/keyrings", - "curl --max-time 60 -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg", - 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null', - "apt-get update", - "apt-get --assume-yes install docker-ce docker-ce-cli containerd.io docker-compose-plugin", - ] diff --git a/src/dstack/_internal/core/backends/cudo/configurator.py b/src/dstack/_internal/core/backends/cudo/configurator.py deleted file mode 100644 index a4d5b7d94d..0000000000 --- a/src/dstack/_internal/core/backends/cudo/configurator.py +++ /dev/null @@ -1,63 +0,0 @@ -import json - -from dstack._internal.core.backends.base.configurator import ( - BackendRecord, - Configurator, - raise_invalid_credentials_error, -) -from dstack._internal.core.backends.cudo import api_client -from dstack._internal.core.backends.cudo.backend import CudoBackend -from dstack._internal.core.backends.cudo.models import ( - CudoBackendConfig, - CudoBackendConfigWithCreds, - CudoConfig, - CudoCreds, - CudoStoredConfig, -) -from dstack._internal.core.models.backends.base import BackendType - - -class CudoConfigurator( - Configurator[ - CudoBackendConfig, - CudoBackendConfigWithCreds, - ] -): - TYPE = BackendType.CUDO - BACKEND_CLASS = CudoBackend - - def validate_config(self, config: CudoBackendConfigWithCreds, default_creds_enabled: bool): - self._validate_cudo_api_key(config.creds.api_key) - - def create_backend( - self, project_name: str, config: CudoBackendConfigWithCreds - ) -> BackendRecord: - return BackendRecord( - config=CudoStoredConfig( - **CudoBackendConfig.__response__.parse_obj(config).dict() - ).json(), - auth=CudoCreds.parse_obj(config.creds).json(), - ) - - def get_backend_config_with_creds(self, record: BackendRecord) -> CudoBackendConfigWithCreds: - config = self._get_config(record) - return CudoBackendConfigWithCreds.__response__.parse_obj(config) - - def get_backend_config_without_creds(self, record: BackendRecord) -> CudoBackendConfig: - config = self._get_config(record) - return CudoBackendConfig.__response__.parse_obj(config) - - def get_backend(self, record: BackendRecord) -> CudoBackend: - config = self._get_config(record) - return CudoBackend(config=config) - - def _get_config(self, record: BackendRecord) -> CudoConfig: - return CudoConfig.__response__( - **json.loads(record.config), - creds=CudoCreds.parse_raw(record.auth), - ) - - def _validate_cudo_api_key(self, api_key: str): - client = api_client.CudoApiClient(api_key=api_key) - if not client.validate_api_key(): - raise_invalid_credentials_error(fields=[["creds", "api_key"]]) diff --git a/src/dstack/_internal/core/backends/cudo/models.py b/src/dstack/_internal/core/backends/cudo/models.py index e22d9d49af..cec8017619 100644 --- a/src/dstack/_internal/core/backends/cudo/models.py +++ b/src/dstack/_internal/core/backends/cudo/models.py @@ -4,6 +4,8 @@ from dstack._internal.core.models.common import CoreModel +# TODO: Cudo is deprecated and will be removed in the future + class CudoAPIKeyCreds(CoreModel): type: Annotated[Literal["api_key"], Field(description="The type of credentials")] = "api_key" diff --git a/src/tests/_internal/core/backends/cudo/test_configurator.py b/src/tests/_internal/core/backends/cudo/test_configurator.py deleted file mode 100644 index c36b3df0a5..0000000000 --- a/src/tests/_internal/core/backends/cudo/test_configurator.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -import pytest - -from dstack._internal.core.backends.cudo.configurator import CudoConfigurator -from dstack._internal.core.backends.cudo.models import CudoBackendConfigWithCreds, CudoCreds -from dstack._internal.core.errors import BackendInvalidCredentialsError - - -class TestCudoConfigurator: - def test_validate_config_valid(self): - config = CudoBackendConfigWithCreds( - creds=CudoCreds(api_key="valid"), - project_id="project1", - regions=["no-luster-1"], - ) - with patch( - "dstack._internal.core.backends.cudo.api_client.CudoApiClient.validate_api_key" - ) as validate_mock: - validate_mock.return_value = True - CudoConfigurator().validate_config(config, default_creds_enabled=True) - - def test_validate_config_invalid_creds(self): - config = CudoBackendConfigWithCreds( - creds=CudoCreds(api_key="invalid"), - project_id="project1", - regions=["no-luster-1"], - ) - with ( - patch( - "dstack._internal.core.backends.cudo.api_client.CudoApiClient.validate_api_key" - ) as validate_mock, - pytest.raises(BackendInvalidCredentialsError) as exc_info, - ): - validate_mock.return_value = False - CudoConfigurator().validate_config(config, default_creds_enabled=True) - assert exc_info.value.fields == [["creds", "api_key"]] diff --git a/src/tests/_internal/server/routers/test_backends.py b/src/tests/_internal/server/routers/test_backends.py index 23b51c5fc8..9b548ab297 100644 --- a/src/tests/_internal/server/routers/test_backends.py +++ b/src/tests/_internal/server/routers/test_backends.py @@ -87,7 +87,6 @@ async def test_returns_backend_types(self, client: AsyncClient): "azure", "cloudrift", "crusoe", - "cudo", "datacrunch", "digitalocean", "gcp",