dstack 0.19.33__py3-none-any.whl → 0.19.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dstack might be problematic. Click here for more details.
- dstack/_internal/core/backends/base/offers.py +3 -1
- dstack/_internal/core/backends/gcp/compute.py +24 -9
- dstack/_internal/core/backends/gcp/models.py +4 -1
- dstack/_internal/core/backends/runpod/compute.py +4 -1
- dstack/_internal/core/compatibility/runs.py +25 -4
- dstack/_internal/core/models/instances.py +2 -1
- dstack/_internal/core/models/runs.py +1 -0
- dstack/_internal/core/services/ssh/key_manager.py +56 -0
- dstack/_internal/server/models.py +3 -0
- dstack/_internal/server/routers/metrics.py +6 -2
- dstack/_internal/server/routers/users.py +7 -0
- dstack/_internal/server/services/jobs/__init__.py +18 -9
- dstack/_internal/server/services/offers.py +1 -0
- dstack/_internal/server/services/runs.py +4 -0
- dstack/_internal/server/services/users.py +3 -3
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js → main-e79754c136f1d8e4e7e6.js} +11 -11
- dstack/_internal/server/statics/{main-97c7e184573ca23f9fe4.js.map → main-e79754c136f1d8e4e7e6.js.map} +1 -1
- dstack/_internal/server/testing/common.py +4 -0
- dstack/api/_public/__init__.py +2 -2
- dstack/api/_public/runs.py +36 -39
- dstack/api/server/__init__.py +4 -0
- dstack/version.py +1 -1
- {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/METADATA +2 -2
- {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/RECORD +28 -27
- {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/WHEEL +0 -0
- {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.33.dist-info → dstack-0.19.34.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -23,7 +23,8 @@ SUPPORTED_GPUHUNT_FLAGS = [
|
|
|
23
23
|
"oci-spot",
|
|
24
24
|
"lambda-arm",
|
|
25
25
|
"gcp-a4",
|
|
26
|
-
"gcp-g4
|
|
26
|
+
"gcp-g4",
|
|
27
|
+
"gcp-dws-calendar-mode",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
|
|
@@ -94,6 +95,7 @@ def catalog_item_to_offer(
|
|
|
94
95
|
),
|
|
95
96
|
region=item.location,
|
|
96
97
|
price=item.price,
|
|
98
|
+
backend_data=item.provider_data,
|
|
97
99
|
)
|
|
98
100
|
|
|
99
101
|
|
|
@@ -90,6 +90,10 @@ RESOURCE_NAME_PATTERN = re.compile(r"[a-z0-9-]+")
|
|
|
90
90
|
TPU_VERSIONS = [tpu.name for tpu in KNOWN_TPUS]
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
class GCPOfferBackendData(CoreModel):
|
|
94
|
+
is_dws_calendar_mode: bool = False
|
|
95
|
+
|
|
96
|
+
|
|
93
97
|
class GCPVolumeDiskBackendData(CoreModel):
|
|
94
98
|
type: Literal["disk"] = "disk"
|
|
95
99
|
disk_type: str
|
|
@@ -142,19 +146,13 @@ class GCPCompute(
|
|
|
142
146
|
offer_keys_to_offers = {}
|
|
143
147
|
offers_with_availability = []
|
|
144
148
|
for offer in offers:
|
|
145
|
-
preview = False
|
|
146
|
-
if offer.instance.name.startswith("g4-standard-"):
|
|
147
|
-
if self.config.preview_features and "g4" in self.config.preview_features:
|
|
148
|
-
preview = True
|
|
149
|
-
else:
|
|
150
|
-
continue
|
|
151
149
|
region = offer.region[:-2] # strip zone
|
|
152
150
|
key = (_unique_instance_name(offer.instance), region)
|
|
153
151
|
if key in offer_keys_to_offers:
|
|
154
152
|
offer_keys_to_offers[key].availability_zones.append(offer.region)
|
|
155
153
|
continue
|
|
156
154
|
availability = InstanceAvailability.NO_QUOTA
|
|
157
|
-
if
|
|
155
|
+
if _has_gpu_quota(quotas[region], offer.instance.resources):
|
|
158
156
|
availability = InstanceAvailability.UNKNOWN
|
|
159
157
|
# todo quotas: cpu, memory, global gpu, tpu
|
|
160
158
|
offer_with_availability = InstanceOfferWithAvailability(
|
|
@@ -202,6 +200,23 @@ class GCPCompute(
|
|
|
202
200
|
modifiers.append(get_offers_disk_modifier(CONFIGURABLE_DISK_SIZE, requirements))
|
|
203
201
|
return modifiers
|
|
204
202
|
|
|
203
|
+
def get_offers_post_filter(
|
|
204
|
+
self, requirements: Requirements
|
|
205
|
+
) -> Optional[Callable[[InstanceOfferWithAvailability], bool]]:
|
|
206
|
+
if requirements.reservation is None:
|
|
207
|
+
|
|
208
|
+
def reserved_offers_filter(offer: InstanceOfferWithAvailability) -> bool:
|
|
209
|
+
"""Remove reserved-only offers"""
|
|
210
|
+
if GCPOfferBackendData.__response__.parse_obj(
|
|
211
|
+
offer.backend_data
|
|
212
|
+
).is_dws_calendar_mode:
|
|
213
|
+
return False
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
return reserved_offers_filter
|
|
217
|
+
|
|
218
|
+
return None
|
|
219
|
+
|
|
205
220
|
def terminate_instance(
|
|
206
221
|
self, instance_id: str, region: str, backend_data: Optional[str] = None
|
|
207
222
|
) -> None:
|
|
@@ -1006,8 +1021,8 @@ def _has_gpu_quota(quotas: Dict[str, float], resources: Resources) -> bool:
|
|
|
1006
1021
|
gpu = resources.gpus[0]
|
|
1007
1022
|
if _is_tpu(gpu.name):
|
|
1008
1023
|
return True
|
|
1009
|
-
if gpu.name in ["B200", "H100"]:
|
|
1010
|
-
# B200, H100 and
|
|
1024
|
+
if gpu.name in ["B200", "H100", "RTXPRO6000"]:
|
|
1025
|
+
# B200, H100, H100_MEGA, and RTXPRO6000 quotas are not returned by `regions_client.list`
|
|
1011
1026
|
return True
|
|
1012
1027
|
quota_name = f"NVIDIA_{gpu.name}_GPUS"
|
|
1013
1028
|
if gpu.name == "A100" and gpu.memory_mib == 80 * 1024:
|
|
@@ -92,7 +92,10 @@ class GCPBackendConfig(CoreModel):
|
|
|
92
92
|
preview_features: Annotated[
|
|
93
93
|
Optional[List[Literal["g4"]]],
|
|
94
94
|
Field(
|
|
95
|
-
description=(
|
|
95
|
+
description=(
|
|
96
|
+
"The list of preview GCP features to enable."
|
|
97
|
+
" There are currently no preview features"
|
|
98
|
+
),
|
|
96
99
|
max_items=1,
|
|
97
100
|
),
|
|
98
101
|
] = None
|
|
@@ -232,9 +232,12 @@ class RunpodCompute(
|
|
|
232
232
|
def create_volume(self, volume: Volume) -> VolumeProvisioningData:
|
|
233
233
|
volume_name = generate_unique_volume_name(volume, max_length=MAX_RESOURCE_NAME_LEN)
|
|
234
234
|
size_gb = volume.configuration.size_gb
|
|
235
|
+
# Runpod regions must be uppercase.
|
|
236
|
+
# Lowercase regions are accepted in the API but they break Runpod in several ways.
|
|
237
|
+
region = volume.configuration.region.upper()
|
|
235
238
|
volume_id = self.api_client.create_network_volume(
|
|
236
239
|
name=volume_name,
|
|
237
|
-
region=
|
|
240
|
+
region=region,
|
|
238
241
|
size=size_gb,
|
|
239
242
|
)
|
|
240
243
|
return VolumeProvisioningData(
|
|
@@ -33,6 +33,8 @@ def get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[IncludeExcludeD
|
|
|
33
33
|
current_resource_excludes["deployment_num"] = True
|
|
34
34
|
if current_resource.fleet is None:
|
|
35
35
|
current_resource_excludes["fleet"] = True
|
|
36
|
+
if current_resource.next_triggered_at is None:
|
|
37
|
+
current_resource_excludes["next_triggered_at"] = True
|
|
36
38
|
apply_plan_excludes["current_resource"] = current_resource_excludes
|
|
37
39
|
current_resource_excludes["run_spec"] = get_run_spec_excludes(current_resource.run_spec)
|
|
38
40
|
job_submissions_excludes: IncludeExcludeDictType = {}
|
|
@@ -47,10 +49,20 @@ def get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[IncludeExcludeD
|
|
|
47
49
|
job_submissions_excludes["job_provisioning_data"] = {
|
|
48
50
|
"instance_type": {"resources": {"cpu_arch"}}
|
|
49
51
|
}
|
|
52
|
+
jrd_offer_excludes = {}
|
|
53
|
+
if any(
|
|
54
|
+
js.job_runtime_data and js.job_runtime_data.offer for js in job_submissions
|
|
55
|
+
) and all(
|
|
56
|
+
not js.job_runtime_data
|
|
57
|
+
or not js.job_runtime_data.offer
|
|
58
|
+
or not js.job_runtime_data.offer.backend_data
|
|
59
|
+
for js in job_submissions
|
|
60
|
+
):
|
|
61
|
+
jrd_offer_excludes["backend_data"] = True
|
|
50
62
|
if all(map(_should_exclude_job_submission_jrd_cpu_arch, job_submissions)):
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
63
|
+
jrd_offer_excludes["instance"] = {"resources": {"cpu_arch"}}
|
|
64
|
+
if jrd_offer_excludes:
|
|
65
|
+
job_submissions_excludes["job_runtime_data"] = {"offer": jrd_offer_excludes}
|
|
54
66
|
if all(js.exit_status is None for js in job_submissions):
|
|
55
67
|
job_submissions_excludes["exit_status"] = True
|
|
56
68
|
if all(js.status_message == "" for js in job_submissions):
|
|
@@ -69,9 +81,18 @@ def get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[IncludeExcludeD
|
|
|
69
81
|
latest_job_submission_excludes["job_provisioning_data"] = {
|
|
70
82
|
"instance_type": {"resources": {"cpu_arch"}}
|
|
71
83
|
}
|
|
84
|
+
latest_job_submission_jrd_offer_excludes = {}
|
|
85
|
+
if (
|
|
86
|
+
latest_job_submission.job_runtime_data
|
|
87
|
+
and latest_job_submission.job_runtime_data.offer
|
|
88
|
+
and not latest_job_submission.job_runtime_data.offer.backend_data
|
|
89
|
+
):
|
|
90
|
+
latest_job_submission_jrd_offer_excludes["backend_data"] = True
|
|
72
91
|
if _should_exclude_job_submission_jrd_cpu_arch(latest_job_submission):
|
|
92
|
+
latest_job_submission_jrd_offer_excludes["instance"] = {"resources": {"cpu_arch"}}
|
|
93
|
+
if latest_job_submission_jrd_offer_excludes:
|
|
73
94
|
latest_job_submission_excludes["job_runtime_data"] = {
|
|
74
|
-
"offer":
|
|
95
|
+
"offer": latest_job_submission_jrd_offer_excludes
|
|
75
96
|
}
|
|
76
97
|
if latest_job_submission.exit_status is None:
|
|
77
98
|
latest_job_submission_excludes["exit_status"] = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Dict, List, Optional
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
import gpuhunt
|
|
@@ -184,6 +184,7 @@ class InstanceOffer(CoreModel):
|
|
|
184
184
|
instance: InstanceType
|
|
185
185
|
region: str
|
|
186
186
|
price: float
|
|
187
|
+
backend_data: dict[str, Any] = {}
|
|
187
188
|
|
|
188
189
|
|
|
189
190
|
class InstanceOfferWithAvailability(InstanceOffer):
|
|
@@ -553,6 +553,7 @@ class Run(CoreModel):
|
|
|
553
553
|
deployment_num: int = 0 # default for compatibility with pre-0.19.14 servers
|
|
554
554
|
error: Optional[str] = None
|
|
555
555
|
deleted: Optional[bool] = None
|
|
556
|
+
next_triggered_at: Optional[datetime] = None
|
|
556
557
|
|
|
557
558
|
def is_deployment_in_progress(self) -> bool:
|
|
558
559
|
return any(
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from dstack._internal.core.models.users import UserWithCreds
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from dstack.api.server import APIClient
|
|
11
|
+
|
|
12
|
+
KEY_REFRESH_RATE = timedelta(minutes=10) # redownload the key periodically in case it was rotated
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class UserSSHKey:
|
|
17
|
+
public_key: str
|
|
18
|
+
private_key_path: Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UserSSHKeyManager:
|
|
22
|
+
def __init__(self, api_client: "APIClient", ssh_keys_dir: Path) -> None:
|
|
23
|
+
self._api_client = api_client
|
|
24
|
+
self._key_path = ssh_keys_dir / api_client.get_token_hash()
|
|
25
|
+
self._pub_key_path = self._key_path.with_suffix(".pub")
|
|
26
|
+
|
|
27
|
+
def get_user_key(self) -> Optional[UserSSHKey]:
|
|
28
|
+
"""
|
|
29
|
+
Return the up-to-date user key, or None if the user has no key (if created before 0.19.33)
|
|
30
|
+
"""
|
|
31
|
+
if (
|
|
32
|
+
not self._key_path.exists()
|
|
33
|
+
or not self._pub_key_path.exists()
|
|
34
|
+
or datetime.now() - datetime.fromtimestamp(self._key_path.stat().st_mtime)
|
|
35
|
+
> KEY_REFRESH_RATE
|
|
36
|
+
):
|
|
37
|
+
if not self._download_user_key():
|
|
38
|
+
return None
|
|
39
|
+
return UserSSHKey(
|
|
40
|
+
public_key=self._pub_key_path.read_text(), private_key_path=self._key_path
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _download_user_key(self) -> bool:
|
|
44
|
+
user = self._api_client.users.get_my_user()
|
|
45
|
+
if not (isinstance(user, UserWithCreds) and user.ssh_public_key and user.ssh_private_key):
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def key_opener(path, flags):
|
|
49
|
+
return os.open(path, flags, 0o600)
|
|
50
|
+
|
|
51
|
+
with open(self._key_path, "w", opener=key_opener) as f:
|
|
52
|
+
f.write(user.ssh_private_key)
|
|
53
|
+
with open(self._pub_key_path, "w") as f:
|
|
54
|
+
f.write(user.ssh_public_key)
|
|
55
|
+
|
|
56
|
+
return True
|
|
@@ -190,6 +190,9 @@ class UserModel(BaseModel):
|
|
|
190
190
|
# deactivated users cannot access API
|
|
191
191
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
192
192
|
|
|
193
|
+
# SSH keys can be null for users created before 0.19.33.
|
|
194
|
+
# Keys for those users are being gradually generated on /get_my_user calls.
|
|
195
|
+
# TODO: make keys required in a future version.
|
|
193
196
|
ssh_private_key: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
194
197
|
ssh_public_key: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
195
198
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from typing import Optional, Tuple
|
|
3
|
+
from uuid import UUID
|
|
3
4
|
|
|
4
5
|
from fastapi import APIRouter, Depends
|
|
5
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -29,6 +30,7 @@ router = APIRouter(
|
|
|
29
30
|
)
|
|
30
31
|
async def get_job_metrics(
|
|
31
32
|
run_name: str,
|
|
33
|
+
run_id: Optional[UUID] = None,
|
|
32
34
|
replica_num: int = 0,
|
|
33
35
|
job_num: int = 0,
|
|
34
36
|
limit: int = 1,
|
|
@@ -39,8 +41,9 @@ async def get_job_metrics(
|
|
|
39
41
|
):
|
|
40
42
|
"""
|
|
41
43
|
Returns job-level metrics such as hardware utilization
|
|
42
|
-
given `run_name`, `replica_num`, and `job_num`.
|
|
43
|
-
If only `run_name` is specified, returns metrics of `(replica_num=0, job_num=0)
|
|
44
|
+
given `run_name`, `run_id`, `replica_num`, and `job_num`.
|
|
45
|
+
If only `run_name` is specified, returns metrics of `(replica_num=0, job_num=0)`
|
|
46
|
+
of the latest run with the given name.
|
|
44
47
|
By default, returns one latest sample. To control time window/number of samples, use
|
|
45
48
|
`limit`, `after`, `before`.
|
|
46
49
|
|
|
@@ -61,6 +64,7 @@ async def get_job_metrics(
|
|
|
61
64
|
session=session,
|
|
62
65
|
project=project,
|
|
63
66
|
run_name=run_name,
|
|
67
|
+
run_id=run_id,
|
|
64
68
|
replica_num=replica_num,
|
|
65
69
|
job_num=job_num,
|
|
66
70
|
)
|
|
@@ -38,8 +38,15 @@ async def list_users(
|
|
|
38
38
|
|
|
39
39
|
@router.post("/get_my_user", response_model=UserWithCreds)
|
|
40
40
|
async def get_my_user(
|
|
41
|
+
session: AsyncSession = Depends(get_session),
|
|
41
42
|
user: UserModel = Depends(Authenticated()),
|
|
42
43
|
):
|
|
44
|
+
if user.ssh_private_key is None or user.ssh_public_key is None:
|
|
45
|
+
# Generate keys for pre-0.19.33 users
|
|
46
|
+
updated_user = await users.refresh_ssh_key(session=session, user=user, username=user.name)
|
|
47
|
+
if updated_user is None:
|
|
48
|
+
raise ResourceNotExistsError()
|
|
49
|
+
user = updated_user
|
|
43
50
|
return CustomORJSONResponse(users.user_model_to_user_with_creds(user))
|
|
44
51
|
|
|
45
52
|
|
|
@@ -97,19 +97,28 @@ def find_job(jobs: List[Job], replica_num: int, job_num: int) -> Job:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
async def get_run_job_model(
|
|
100
|
-
session: AsyncSession,
|
|
100
|
+
session: AsyncSession,
|
|
101
|
+
project: ProjectModel,
|
|
102
|
+
run_name: str,
|
|
103
|
+
run_id: Optional[UUID],
|
|
104
|
+
replica_num: int,
|
|
105
|
+
job_num: int,
|
|
101
106
|
) -> Optional[JobModel]:
|
|
107
|
+
filters = [
|
|
108
|
+
RunModel.project_id == project.id,
|
|
109
|
+
RunModel.run_name == run_name,
|
|
110
|
+
JobModel.replica_num == replica_num,
|
|
111
|
+
JobModel.job_num == job_num,
|
|
112
|
+
]
|
|
113
|
+
if run_id is not None:
|
|
114
|
+
filters.append(RunModel.id == run_id)
|
|
115
|
+
else:
|
|
116
|
+
# Assuming run_name is unique for non-deleted runs
|
|
117
|
+
filters.append(RunModel.deleted == False)
|
|
102
118
|
res = await session.execute(
|
|
103
119
|
select(JobModel)
|
|
104
120
|
.join(JobModel.run)
|
|
105
|
-
.where(
|
|
106
|
-
RunModel.project_id == project.id,
|
|
107
|
-
# assuming run_name is unique for non-deleted runs
|
|
108
|
-
RunModel.run_name == run_name,
|
|
109
|
-
RunModel.deleted == False,
|
|
110
|
-
JobModel.replica_num == replica_num,
|
|
111
|
-
JobModel.job_num == job_num,
|
|
112
|
-
)
|
|
121
|
+
.where(*filters)
|
|
113
122
|
.order_by(JobModel.submission_num.desc())
|
|
114
123
|
.limit(1)
|
|
115
124
|
)
|
|
@@ -715,6 +715,9 @@ def run_model_to_run(
|
|
|
715
715
|
status_message = _get_run_status_message(run_model)
|
|
716
716
|
error = _get_run_error(run_model)
|
|
717
717
|
fleet = _get_run_fleet(run_model)
|
|
718
|
+
next_triggered_at = None
|
|
719
|
+
if not run_model.status.is_finished():
|
|
720
|
+
next_triggered_at = _get_next_triggered_at(run_spec)
|
|
718
721
|
run = Run(
|
|
719
722
|
id=run_model.id,
|
|
720
723
|
project_name=run_model.project.name,
|
|
@@ -734,6 +737,7 @@ def run_model_to_run(
|
|
|
734
737
|
deployment_num=run_model.deployment_num,
|
|
735
738
|
error=error,
|
|
736
739
|
deleted=run_model.deleted,
|
|
740
|
+
next_triggered_at=next_triggered_at,
|
|
737
741
|
)
|
|
738
742
|
run.cost = _get_run_cost(run)
|
|
739
743
|
return run
|
|
@@ -20,8 +20,8 @@ from dstack._internal.core.models.users import (
|
|
|
20
20
|
from dstack._internal.server.models import DecryptedString, UserModel
|
|
21
21
|
from dstack._internal.server.services.permissions import get_default_permissions
|
|
22
22
|
from dstack._internal.server.utils.routers import error_forbidden
|
|
23
|
+
from dstack._internal.utils import crypto
|
|
23
24
|
from dstack._internal.utils.common import run_async
|
|
24
|
-
from dstack._internal.utils.crypto import generate_rsa_key_pair_bytes
|
|
25
25
|
from dstack._internal.utils.logging import get_logger
|
|
26
26
|
|
|
27
27
|
logger = get_logger(__name__)
|
|
@@ -88,7 +88,7 @@ async def create_user(
|
|
|
88
88
|
raise ResourceExistsError()
|
|
89
89
|
if token is None:
|
|
90
90
|
token = str(uuid.uuid4())
|
|
91
|
-
private_bytes, public_bytes = await run_async(generate_rsa_key_pair_bytes, username)
|
|
91
|
+
private_bytes, public_bytes = await run_async(crypto.generate_rsa_key_pair_bytes, username)
|
|
92
92
|
user = UserModel(
|
|
93
93
|
id=uuid.uuid4(),
|
|
94
94
|
name=username,
|
|
@@ -135,7 +135,7 @@ async def refresh_ssh_key(
|
|
|
135
135
|
logger.debug("Refreshing SSH key for user [code]%s[/code]", username)
|
|
136
136
|
if user.global_role != GlobalRole.ADMIN and user.name != username:
|
|
137
137
|
raise error_forbidden()
|
|
138
|
-
private_bytes, public_bytes = await run_async(generate_rsa_key_pair_bytes, username)
|
|
138
|
+
private_bytes, public_bytes = await run_async(crypto.generate_rsa_key_pair_bytes, username)
|
|
139
139
|
await session.execute(
|
|
140
140
|
update(UserModel)
|
|
141
141
|
.where(UserModel.name == username)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>dstack</title><meta name="description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
2
2
|
"/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"><meta name="og:title" content="dstack"><meta name="og:type" content="article"><meta name="og:image" content="/splash_thumbnail.png"><meta name="og:description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
3
|
-
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-97c7e184573ca23f9fe4.js"></script><link href="/main-720ce3a11140daa480cc.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div><script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id="11a9339d-20ce-4ddb-9ba3-1b6e29afe8eb" data-project-name="dstack" data-project-color="rgba(0, 0, 0, 0.87)" data-font-size-lg="0.78rem" data-button-hide="true" data-modal-image="/logo-notext.svg" data-modal-z-index="1100" data-modal-title="Ask me anything" data-font-family='metro-web, Metro, -apple-system, "system-ui", "Segoe UI", Roboto' data-project-logo="/assets/images/kapa.svg" data-modal-disclaimer="This is a custom LLM for dstack with access to Documentation, API references and GitHub issues. This feature is experimental - Give it a try!" data-user-analytics-fingerprint-enabled="true"></script></body></html>
|
|
3
|
+
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-e79754c136f1d8e4e7e6.js"></script><link href="/main-720ce3a11140daa480cc.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div><script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id="11a9339d-20ce-4ddb-9ba3-1b6e29afe8eb" data-project-name="dstack" data-project-color="rgba(0, 0, 0, 0.87)" data-font-size-lg="0.78rem" data-button-hide="true" data-modal-image="/logo-notext.svg" data-modal-z-index="1100" data-modal-title="Ask me anything" data-font-family='metro-web, Metro, -apple-system, "system-ui", "Segoe UI", Roboto' data-project-logo="/assets/images/kapa.svg" data-modal-disclaimer="This is a custom LLM for dstack with access to Documentation, API references and GitHub issues. This feature is experimental - Give it a try!" data-user-analytics-fingerprint-enabled="true"></script></body></html>
|