-
-
Notifications
You must be signed in to change notification settings - Fork 222
add hotaisle backend #2935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add hotaisle backend #2935
Changes from 3 commits
f5f2f2e
b9ca0be
9558c29
1bf72e0
16e77e3
d2846ae
c19065b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Hotaisle backend for dstack |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| from typing import Any, Dict, Optional | ||
|
|
||
| import requests | ||
|
|
||
| from dstack._internal.utils.logging import get_logger | ||
|
|
||
| API_URL = "https://admin.hotaisle.app/api" | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| class HotaisleAPIClient: | ||
| def __init__(self, api_key: str, team_handle: str): | ||
| self.api_key = api_key | ||
| self.team_handle = team_handle | ||
|
|
||
| def validate_api_key(self) -> bool: | ||
| try: | ||
| self._validate_user_and_team() | ||
| return True | ||
| except requests.HTTPError as e: | ||
| if e.response.status_code in [401, 403]: | ||
| return False | ||
| raise e | ||
| except ValueError: | ||
| return False | ||
|
|
||
| def _validate_user_and_team(self) -> None: | ||
| url = f"{API_URL}/user/" | ||
| response = self._make_request("GET", url) | ||
|
|
||
| if response.ok: | ||
| user_data = response.json() | ||
| else: | ||
| response.raise_for_status() | ||
|
jvstme marked this conversation as resolved.
Outdated
|
||
|
|
||
| teams = user_data.get("teams", []) | ||
| if not teams: | ||
| raise ValueError("No Hotaisle teams found for this user") | ||
|
|
||
| available_teams = [team["handle"] for team in teams] | ||
| if self.team_handle not in available_teams: | ||
| raise ValueError(f"Hotaisle Team '{self.team_handle}' not found.") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (optional) This validation is already better than in most our backends, but we can further improve it by validating the roles assigned to the key, so that users can see permission-related errors earlier - when configuring the backend rather than when creating instances. It should be possible to validate everything (the key, the user role, and the team roles) by calling only
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you. Will plan to update it in the next iteration. |
||
|
|
||
| def upload_ssh_key(self, public_key: str) -> bool: | ||
| url = f"{API_URL}/user/ssh_keys/" | ||
| payload = {"authorized_key": public_key} | ||
|
|
||
| response = self._make_request("POST", url, json=payload) | ||
|
|
||
| if response.status_code == 409: | ||
| return True # Key already exists - success | ||
| if not response.ok: | ||
| response.raise_for_status() | ||
| return True | ||
|
|
||
| def create_virtual_machine( | ||
| self, vm_payload: Dict[str, Any], instance_name: str | ||
| ) -> Dict[str, Any]: | ||
| url = f"{API_URL}/teams/{self.team_handle}/virtual_machines/" | ||
| response = self._make_request("POST", url, json=vm_payload) | ||
|
|
||
| if not response.ok: | ||
| response.raise_for_status() | ||
|
|
||
| vm_data = response.json() | ||
| return vm_data | ||
|
jvstme marked this conversation as resolved.
Outdated
|
||
|
|
||
| def get_vm_state(self, vm_name: str) -> str: | ||
| url = f"{API_URL}/teams/{self.team_handle}/virtual_machines/{vm_name}/state/" | ||
| response = self._make_request("GET", url) | ||
|
|
||
| if not response.ok: | ||
| response.raise_for_status() | ||
|
|
||
| state_data = response.json() | ||
| return state_data["state"] | ||
|
|
||
| def terminate_virtual_machine(self, vm_name: str) -> bool: | ||
| url = f"{API_URL}/teams/{self.team_handle}/virtual_machines/{vm_name}/" | ||
| response = self._make_request("DELETE", url) | ||
|
|
||
| if response.status_code == 204: | ||
| return True | ||
| else: | ||
| response.raise_for_status() | ||
|
jvstme marked this conversation as resolved.
Outdated
|
||
|
|
||
| def _make_request( | ||
| self, method: str, url: str, json: Optional[Dict[str, Any]] = None, timeout: int = 30 | ||
| ) -> requests.Response: | ||
| headers = { | ||
| "accept": "application/json", | ||
| "Authorization": self.api_key, | ||
|
jvstme marked this conversation as resolved.
Outdated
|
||
| } | ||
| if json is not None: | ||
| headers["Content-Type"] = "application/json" | ||
|
|
||
| return requests.request( | ||
| method=method, | ||
| url=url, | ||
| headers=headers, | ||
| json=json, | ||
| timeout=timeout, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from dstack._internal.core.backends.base.backend import Backend | ||
| from dstack._internal.core.backends.hotaisle.compute import HotaisleCompute | ||
| from dstack._internal.core.backends.hotaisle.models import HotaisleConfig | ||
| from dstack._internal.core.models.backends.base import BackendType | ||
|
|
||
|
|
||
| class HotaisleBackend(Backend): | ||
| TYPE = BackendType.HOTAISLE | ||
| COMPUTE_CLASS = HotaisleCompute | ||
|
|
||
| def __init__(self, config: HotaisleConfig): | ||
| self.config = config | ||
| self._compute = HotaisleCompute(self.config) | ||
|
|
||
| def compute(self) -> HotaisleCompute: | ||
| return self._compute |
Uh oh!
There was an error while loading. Please reload this page.