dstack 0.19.12rc1__py3-none-any.whl → 0.19.14__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/cli/commands/attach.py +4 -4
- dstack/_internal/cli/services/configurators/run.py +44 -47
- dstack/_internal/cli/utils/run.py +31 -31
- dstack/_internal/core/backends/aws/compute.py +22 -9
- dstack/_internal/core/backends/aws/resources.py +26 -0
- dstack/_internal/core/backends/base/offers.py +0 -1
- dstack/_internal/core/backends/template/configurator.py.jinja +1 -6
- dstack/_internal/core/backends/template/models.py.jinja +4 -0
- dstack/_internal/core/compatibility/__init__.py +0 -0
- dstack/_internal/core/compatibility/fleets.py +72 -0
- dstack/_internal/core/compatibility/gateways.py +34 -0
- dstack/_internal/core/compatibility/runs.py +131 -0
- dstack/_internal/core/compatibility/volumes.py +32 -0
- dstack/_internal/core/models/configurations.py +1 -1
- dstack/_internal/core/models/fleets.py +6 -1
- dstack/_internal/core/models/instances.py +51 -12
- dstack/_internal/core/models/profiles.py +43 -3
- dstack/_internal/core/models/projects.py +1 -0
- dstack/_internal/core/models/repos/local.py +3 -3
- dstack/_internal/core/models/runs.py +139 -43
- dstack/_internal/server/app.py +46 -1
- dstack/_internal/server/background/tasks/process_running_jobs.py +92 -15
- dstack/_internal/server/background/tasks/process_runs.py +163 -80
- dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +42 -0
- dstack/_internal/server/migrations/versions/35f732ee4cf5_add_projectmodel_is_public.py +39 -0
- dstack/_internal/server/models.py +4 -0
- dstack/_internal/server/routers/projects.py +4 -3
- dstack/_internal/server/routers/prometheus.py +4 -1
- dstack/_internal/server/schemas/projects.py +1 -0
- dstack/_internal/server/security/permissions.py +36 -0
- dstack/_internal/server/services/jobs/__init__.py +1 -0
- dstack/_internal/server/services/jobs/configurators/base.py +11 -7
- dstack/_internal/server/services/projects.py +54 -1
- dstack/_internal/server/services/runner/client.py +4 -1
- dstack/_internal/server/services/runs.py +49 -29
- dstack/_internal/server/services/services/__init__.py +19 -0
- dstack/_internal/server/services/services/autoscalers.py +37 -26
- dstack/_internal/server/services/storage/__init__.py +38 -0
- dstack/_internal/server/services/storage/base.py +27 -0
- dstack/_internal/server/services/storage/gcs.py +44 -0
- dstack/_internal/server/services/{storage.py → storage/s3.py} +4 -27
- dstack/_internal/server/settings.py +7 -3
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-5b9786c955b42bf93581.js → main-0ac1e1583684417ae4d1.js} +1695 -62
- dstack/_internal/server/statics/{main-5b9786c955b42bf93581.js.map → main-0ac1e1583684417ae4d1.js.map} +1 -1
- dstack/_internal/server/statics/{main-8f9c66f404e9c7e7e020.css → main-f39c418b05fe14772dd8.css} +1 -1
- dstack/_internal/server/testing/common.py +11 -1
- dstack/_internal/settings.py +3 -0
- dstack/_internal/utils/common.py +4 -0
- dstack/api/_public/runs.py +14 -5
- dstack/api/server/_fleets.py +9 -69
- dstack/api/server/_gateways.py +3 -14
- dstack/api/server/_projects.py +2 -2
- dstack/api/server/_runs.py +4 -116
- dstack/api/server/_volumes.py +3 -14
- dstack/plugins/builtin/rest_plugin/_plugin.py +24 -5
- dstack/version.py +2 -2
- {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/METADATA +1 -1
- {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/RECORD +62 -52
- {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/WHEEL +0 -0
- {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -140,6 +140,7 @@ async def create_project(
|
|
|
140
140
|
created_at: datetime = datetime(2023, 1, 2, 3, 4, tzinfo=timezone.utc),
|
|
141
141
|
ssh_private_key: str = "",
|
|
142
142
|
ssh_public_key: str = "",
|
|
143
|
+
is_public: bool = False,
|
|
143
144
|
) -> ProjectModel:
|
|
144
145
|
if owner is None:
|
|
145
146
|
owner = await create_user(session=session, name="test_owner")
|
|
@@ -149,6 +150,7 @@ async def create_project(
|
|
|
149
150
|
created_at=created_at,
|
|
150
151
|
ssh_private_key=ssh_private_key,
|
|
151
152
|
ssh_public_key=ssh_public_key,
|
|
153
|
+
is_public=is_public,
|
|
152
154
|
)
|
|
153
155
|
session.add(project)
|
|
154
156
|
await session.commit()
|
|
@@ -263,6 +265,7 @@ async def create_run(
|
|
|
263
265
|
run_id: Optional[UUID] = None,
|
|
264
266
|
deleted: bool = False,
|
|
265
267
|
priority: int = 0,
|
|
268
|
+
deployment_num: int = 0,
|
|
266
269
|
) -> RunModel:
|
|
267
270
|
if run_spec is None:
|
|
268
271
|
run_spec = get_run_spec(
|
|
@@ -284,6 +287,8 @@ async def create_run(
|
|
|
284
287
|
last_processed_at=submitted_at,
|
|
285
288
|
jobs=[],
|
|
286
289
|
priority=priority,
|
|
290
|
+
deployment_num=deployment_num,
|
|
291
|
+
desired_replica_count=1,
|
|
287
292
|
)
|
|
288
293
|
session.add(run)
|
|
289
294
|
await session.commit()
|
|
@@ -303,18 +308,23 @@ async def create_job(
|
|
|
303
308
|
instance: Optional[InstanceModel] = None,
|
|
304
309
|
job_num: int = 0,
|
|
305
310
|
replica_num: int = 0,
|
|
311
|
+
deployment_num: Optional[int] = None,
|
|
306
312
|
instance_assigned: bool = False,
|
|
307
313
|
disconnected_at: Optional[datetime] = None,
|
|
308
314
|
) -> JobModel:
|
|
315
|
+
if deployment_num is None:
|
|
316
|
+
deployment_num = run.deployment_num
|
|
309
317
|
run_spec = RunSpec.parse_raw(run.run_spec)
|
|
310
318
|
job_spec = (await get_job_specs_from_run_spec(run_spec, replica_num=replica_num))[0]
|
|
319
|
+
job_spec.job_num = job_num
|
|
311
320
|
job = JobModel(
|
|
312
321
|
project_id=run.project_id,
|
|
313
322
|
run_id=run.id,
|
|
314
323
|
run_name=run.run_name,
|
|
315
324
|
job_num=job_num,
|
|
316
|
-
job_name=run.run_name + f"-
|
|
325
|
+
job_name=run.run_name + f"-{job_num}-{replica_num}",
|
|
317
326
|
replica_num=replica_num,
|
|
327
|
+
deployment_num=deployment_num,
|
|
318
328
|
submission_num=submission_num,
|
|
319
329
|
submitted_at=submitted_at,
|
|
320
330
|
last_processed_at=last_processed_at,
|
dstack/_internal/settings.py
CHANGED
|
@@ -14,6 +14,9 @@ DSTACK_USE_LATEST_FROM_BRANCH = os.getenv("DSTACK_USE_LATEST_FROM_BRANCH") is no
|
|
|
14
14
|
|
|
15
15
|
DSTACK_BASE_IMAGE = os.getenv("DSTACK_BASE_IMAGE", "dstackai/base")
|
|
16
16
|
DSTACK_BASE_IMAGE_VERSION = os.getenv("DSTACK_BASE_IMAGE_VERSION", version.base_image)
|
|
17
|
+
DSTACK_BASE_IMAGE_UBUNTU_VERSION = os.getenv(
|
|
18
|
+
"DSTACK_BASE_IMAGE_UBUNTU_VERSION", version.base_image_ubuntu_version
|
|
19
|
+
)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class FeatureFlags:
|
dstack/_internal/utils/common.py
CHANGED
|
@@ -314,3 +314,7 @@ def make_proxy_url(server_url: str, proxy_url: str) -> str:
|
|
|
314
314
|
path=concat_url_path(server.path, proxy.path),
|
|
315
315
|
)
|
|
316
316
|
return proxy.geturl()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def list_enum_values_for_annotation(enum_class: type[enum.Enum]) -> str:
|
|
320
|
+
return ", ".join(f"`{e.value}`" for e in enum_class)
|
dstack/api/_public/runs.py
CHANGED
|
@@ -31,6 +31,7 @@ from dstack._internal.core.models.resources import ResourcesSpec
|
|
|
31
31
|
from dstack._internal.core.models.runs import (
|
|
32
32
|
Job,
|
|
33
33
|
JobSpec,
|
|
34
|
+
JobStatus,
|
|
34
35
|
RunPlan,
|
|
35
36
|
RunSpec,
|
|
36
37
|
RunStatus,
|
|
@@ -184,7 +185,7 @@ class Run(ABC):
|
|
|
184
185
|
self,
|
|
185
186
|
start_time: Optional[datetime] = None,
|
|
186
187
|
diagnose: bool = False,
|
|
187
|
-
replica_num: int =
|
|
188
|
+
replica_num: Optional[int] = None,
|
|
188
189
|
job_num: int = 0,
|
|
189
190
|
) -> Iterable[bytes]:
|
|
190
191
|
"""
|
|
@@ -246,7 +247,7 @@ class Run(ABC):
|
|
|
246
247
|
ssh_identity_file: Optional[PathLike] = None,
|
|
247
248
|
bind_address: Optional[str] = None,
|
|
248
249
|
ports_overrides: Optional[List[PortMapping]] = None,
|
|
249
|
-
replica_num: int =
|
|
250
|
+
replica_num: Optional[int] = None,
|
|
250
251
|
job_num: int = 0,
|
|
251
252
|
) -> bool:
|
|
252
253
|
"""
|
|
@@ -254,6 +255,7 @@ class Run(ABC):
|
|
|
254
255
|
|
|
255
256
|
Args:
|
|
256
257
|
ssh_identity_file: SSH keypair to access instances.
|
|
258
|
+
replica_num: replica_num or None to attach to any running replica.
|
|
257
259
|
|
|
258
260
|
Raises:
|
|
259
261
|
dstack.api.PortUsedError: If ports are in use or the run is attached by another process.
|
|
@@ -265,7 +267,9 @@ class Run(ABC):
|
|
|
265
267
|
|
|
266
268
|
job = self._find_job(replica_num=replica_num, job_num=job_num)
|
|
267
269
|
if job is None:
|
|
268
|
-
|
|
270
|
+
replica_repr = replica_num if replica_num is not None else "<any running>"
|
|
271
|
+
raise ClientError(f"Failed to find replica={replica_repr} job={job_num}")
|
|
272
|
+
replica_num = job.job_spec.replica_num
|
|
269
273
|
|
|
270
274
|
name = self.name
|
|
271
275
|
if replica_num != 0 or job_num != 0:
|
|
@@ -358,9 +362,14 @@ class Run(ABC):
|
|
|
358
362
|
self._ssh_attach.detach()
|
|
359
363
|
self._ssh_attach = None
|
|
360
364
|
|
|
361
|
-
def _find_job(self, replica_num: int, job_num: int) -> Optional[Job]:
|
|
365
|
+
def _find_job(self, replica_num: Optional[int], job_num: int) -> Optional[Job]:
|
|
362
366
|
for j in self._run.jobs:
|
|
363
|
-
if
|
|
367
|
+
if (
|
|
368
|
+
replica_num is not None
|
|
369
|
+
and j.job_spec.replica_num == replica_num
|
|
370
|
+
or replica_num is None
|
|
371
|
+
and j.job_submissions[-1].status == JobStatus.RUNNING
|
|
372
|
+
) and j.job_spec.job_num == job_num:
|
|
364
373
|
return j
|
|
365
374
|
return None
|
|
366
375
|
|
dstack/api/server/_fleets.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import List, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import parse_obj_as
|
|
4
4
|
|
|
5
|
+
from dstack._internal.core.compatibility.fleets import (
|
|
6
|
+
get_apply_plan_excludes,
|
|
7
|
+
get_create_fleet_excludes,
|
|
8
|
+
get_get_plan_excludes,
|
|
9
|
+
)
|
|
5
10
|
from dstack._internal.core.models.fleets import ApplyFleetPlanInput, Fleet, FleetPlan, FleetSpec
|
|
6
|
-
from dstack._internal.core.models.instances import Instance
|
|
7
11
|
from dstack._internal.server.schemas.fleets import (
|
|
8
12
|
ApplyFleetPlanRequest,
|
|
9
13
|
CreateFleetRequest,
|
|
@@ -34,7 +38,7 @@ class FleetsAPIClient(APIClientGroup):
|
|
|
34
38
|
spec: FleetSpec,
|
|
35
39
|
) -> FleetPlan:
|
|
36
40
|
body = GetFleetPlanRequest(spec=spec)
|
|
37
|
-
body_json = body.json(exclude=
|
|
41
|
+
body_json = body.json(exclude=get_get_plan_excludes(spec))
|
|
38
42
|
resp = self._request(f"/api/project/{project_name}/fleets/get_plan", body=body_json)
|
|
39
43
|
return parse_obj_as(FleetPlan.__response__, resp.json())
|
|
40
44
|
|
|
@@ -46,7 +50,7 @@ class FleetsAPIClient(APIClientGroup):
|
|
|
46
50
|
) -> Fleet:
|
|
47
51
|
plan_input = ApplyFleetPlanInput.__response__.parse_obj(plan)
|
|
48
52
|
body = ApplyFleetPlanRequest(plan=plan_input, force=force)
|
|
49
|
-
body_json = body.json(exclude=
|
|
53
|
+
body_json = body.json(exclude=get_apply_plan_excludes(plan_input))
|
|
50
54
|
resp = self._request(f"/api/project/{project_name}/fleets/apply", body=body_json)
|
|
51
55
|
return parse_obj_as(Fleet.__response__, resp.json())
|
|
52
56
|
|
|
@@ -66,70 +70,6 @@ class FleetsAPIClient(APIClientGroup):
|
|
|
66
70
|
spec: FleetSpec,
|
|
67
71
|
) -> Fleet:
|
|
68
72
|
body = CreateFleetRequest(spec=spec)
|
|
69
|
-
body_json = body.json(exclude=
|
|
73
|
+
body_json = body.json(exclude=get_create_fleet_excludes(spec))
|
|
70
74
|
resp = self._request(f"/api/project/{project_name}/fleets/create", body=body_json)
|
|
71
75
|
return parse_obj_as(Fleet.__response__, resp.json())
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _get_get_plan_excludes(fleet_spec: FleetSpec) -> Dict:
|
|
75
|
-
get_plan_excludes = {}
|
|
76
|
-
spec_excludes = _get_fleet_spec_excludes(fleet_spec)
|
|
77
|
-
if spec_excludes:
|
|
78
|
-
get_plan_excludes["spec"] = spec_excludes
|
|
79
|
-
return get_plan_excludes
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _get_apply_plan_excludes(plan_input: ApplyFleetPlanInput) -> Dict:
|
|
83
|
-
apply_plan_excludes = {}
|
|
84
|
-
spec_excludes = _get_fleet_spec_excludes(plan_input.spec)
|
|
85
|
-
if spec_excludes:
|
|
86
|
-
apply_plan_excludes["spec"] = apply_plan_excludes
|
|
87
|
-
current_resource = plan_input.current_resource
|
|
88
|
-
if current_resource is not None:
|
|
89
|
-
current_resource_excludes = {}
|
|
90
|
-
apply_plan_excludes["current_resource"] = current_resource_excludes
|
|
91
|
-
if all(map(_should_exclude_instance_cpu_arch, current_resource.instances)):
|
|
92
|
-
current_resource_excludes["instances"] = {
|
|
93
|
-
"__all__": {"instance_type": {"resources": {"cpu_arch"}}}
|
|
94
|
-
}
|
|
95
|
-
return {"plan": apply_plan_excludes}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _should_exclude_instance_cpu_arch(instance: Instance) -> bool:
|
|
99
|
-
try:
|
|
100
|
-
return instance.instance_type.resources.cpu_arch is None
|
|
101
|
-
except AttributeError:
|
|
102
|
-
return True
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _get_create_fleet_excludes(fleet_spec: FleetSpec) -> Dict:
|
|
106
|
-
create_fleet_excludes = {}
|
|
107
|
-
spec_excludes = _get_fleet_spec_excludes(fleet_spec)
|
|
108
|
-
if spec_excludes:
|
|
109
|
-
create_fleet_excludes["spec"] = spec_excludes
|
|
110
|
-
return create_fleet_excludes
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[Dict]:
|
|
114
|
-
"""
|
|
115
|
-
Returns `fleet_spec` exclude mapping to exclude certain fields from the request.
|
|
116
|
-
Use this method to exclude new fields when they are not set to keep
|
|
117
|
-
clients backward-compatibility with older servers.
|
|
118
|
-
"""
|
|
119
|
-
spec_excludes: Dict[str, Any] = {}
|
|
120
|
-
configuration_excludes: Dict[str, Any] = {}
|
|
121
|
-
profile_excludes: set[str] = set()
|
|
122
|
-
profile = fleet_spec.profile
|
|
123
|
-
if profile.fleets is None:
|
|
124
|
-
profile_excludes.add("fleets")
|
|
125
|
-
if fleet_spec.configuration.tags is None:
|
|
126
|
-
configuration_excludes["tags"] = True
|
|
127
|
-
if profile.tags is None:
|
|
128
|
-
profile_excludes.add("tags")
|
|
129
|
-
if configuration_excludes:
|
|
130
|
-
spec_excludes["configuration"] = configuration_excludes
|
|
131
|
-
if profile_excludes:
|
|
132
|
-
spec_excludes["profile"] = profile_excludes
|
|
133
|
-
if spec_excludes:
|
|
134
|
-
return spec_excludes
|
|
135
|
-
return None
|
dstack/api/server/_gateways.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import List
|
|
2
2
|
|
|
3
3
|
from pydantic import parse_obj_as
|
|
4
4
|
|
|
5
|
+
from dstack._internal.core.compatibility.gateways import get_create_gateway_excludes
|
|
5
6
|
from dstack._internal.core.models.gateways import Gateway, GatewayConfiguration
|
|
6
7
|
from dstack._internal.server.schemas.gateways import (
|
|
7
8
|
CreateGatewayRequest,
|
|
@@ -31,7 +32,7 @@ class GatewaysAPIClient(APIClientGroup):
|
|
|
31
32
|
body = CreateGatewayRequest(configuration=configuration)
|
|
32
33
|
resp = self._request(
|
|
33
34
|
f"/api/project/{project_name}/gateways/create",
|
|
34
|
-
body=body.json(exclude=
|
|
35
|
+
body=body.json(exclude=get_create_gateway_excludes(configuration)),
|
|
35
36
|
)
|
|
36
37
|
return parse_obj_as(Gateway.__response__, resp.json())
|
|
37
38
|
|
|
@@ -51,15 +52,3 @@ class GatewaysAPIClient(APIClientGroup):
|
|
|
51
52
|
f"/api/project/{project_name}/gateways/set_wildcard_domain", body=body.json()
|
|
52
53
|
)
|
|
53
54
|
return parse_obj_as(Gateway.__response__, resp.json())
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _get_gateway_configuration_excludes(configuration: GatewayConfiguration) -> Dict:
|
|
57
|
-
"""
|
|
58
|
-
Returns `configuration` exclude mapping to exclude certain fields from the request.
|
|
59
|
-
Use this method to exclude new fields when they are not set to keep
|
|
60
|
-
clients backward-compatibility with older servers.
|
|
61
|
-
"""
|
|
62
|
-
configuration_excludes = {}
|
|
63
|
-
if configuration.tags is None:
|
|
64
|
-
configuration_excludes["tags"] = True
|
|
65
|
-
return {"configuration": configuration_excludes}
|
dstack/api/server/_projects.py
CHANGED
|
@@ -17,8 +17,8 @@ class ProjectsAPIClient(APIClientGroup):
|
|
|
17
17
|
resp = self._request("/api/projects/list")
|
|
18
18
|
return parse_obj_as(List[Project.__response__], resp.json())
|
|
19
19
|
|
|
20
|
-
def create(self, project_name: str) -> Project:
|
|
21
|
-
body = CreateProjectRequest(project_name=project_name)
|
|
20
|
+
def create(self, project_name: str, is_public: bool = False) -> Project:
|
|
21
|
+
body = CreateProjectRequest(project_name=project_name, is_public=is_public)
|
|
22
22
|
resp = self._request("/api/projects/create", body=body.json())
|
|
23
23
|
return parse_obj_as(Project.__response__, resp.json())
|
|
24
24
|
|
dstack/api/server/_runs.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
5
|
from pydantic import parse_obj_as
|
|
6
6
|
|
|
7
|
-
from dstack._internal.core.
|
|
7
|
+
from dstack._internal.core.compatibility.runs import get_apply_plan_excludes, get_get_plan_excludes
|
|
8
8
|
from dstack._internal.core.models.runs import (
|
|
9
9
|
ApplyRunPlanInput,
|
|
10
|
-
JobSubmission,
|
|
11
10
|
Run,
|
|
12
11
|
RunPlan,
|
|
13
12
|
RunSpec,
|
|
@@ -60,7 +59,7 @@ class RunsAPIClient(APIClientGroup):
|
|
|
60
59
|
body = GetRunPlanRequest(run_spec=run_spec, max_offers=max_offers)
|
|
61
60
|
resp = self._request(
|
|
62
61
|
f"/api/project/{project_name}/runs/get_plan",
|
|
63
|
-
body=body.json(exclude=
|
|
62
|
+
body=body.json(exclude=get_get_plan_excludes(body)),
|
|
64
63
|
)
|
|
65
64
|
return parse_obj_as(RunPlan.__response__, resp.json())
|
|
66
65
|
|
|
@@ -74,7 +73,7 @@ class RunsAPIClient(APIClientGroup):
|
|
|
74
73
|
body = ApplyRunPlanRequest(plan=plan_input, force=force)
|
|
75
74
|
resp = self._request(
|
|
76
75
|
f"/api/project/{project_name}/runs/apply",
|
|
77
|
-
body=body.json(exclude=
|
|
76
|
+
body=body.json(exclude=get_apply_plan_excludes(plan_input)),
|
|
78
77
|
)
|
|
79
78
|
return parse_obj_as(Run.__response__, resp.json())
|
|
80
79
|
|
|
@@ -85,114 +84,3 @@ class RunsAPIClient(APIClientGroup):
|
|
|
85
84
|
def delete(self, project_name: str, runs_names: List[str]):
|
|
86
85
|
body = DeleteRunsRequest(runs_names=runs_names)
|
|
87
86
|
self._request(f"/api/project/{project_name}/runs/delete", body=body.json())
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[Dict]:
|
|
91
|
-
"""
|
|
92
|
-
Returns `plan` exclude mapping to exclude certain fields from the request.
|
|
93
|
-
Use this method to exclude new fields when they are not set to keep
|
|
94
|
-
clients backward-compatibility with older servers.
|
|
95
|
-
"""
|
|
96
|
-
apply_plan_excludes = {}
|
|
97
|
-
run_spec_excludes = _get_run_spec_excludes(plan.run_spec)
|
|
98
|
-
if run_spec_excludes is not None:
|
|
99
|
-
apply_plan_excludes["run_spec"] = run_spec_excludes
|
|
100
|
-
current_resource = plan.current_resource
|
|
101
|
-
if current_resource is not None:
|
|
102
|
-
current_resource_excludes = {}
|
|
103
|
-
apply_plan_excludes["current_resource"] = current_resource_excludes
|
|
104
|
-
current_resource_excludes["run_spec"] = _get_run_spec_excludes(current_resource.run_spec)
|
|
105
|
-
job_submissions_excludes = {}
|
|
106
|
-
current_resource_excludes["jobs"] = {
|
|
107
|
-
"__all__": {"job_submissions": {"__all__": job_submissions_excludes}}
|
|
108
|
-
}
|
|
109
|
-
job_submissions = [js for j in current_resource.jobs for js in j.job_submissions]
|
|
110
|
-
if all(map(_should_exclude_job_submission_jpd_cpu_arch, job_submissions)):
|
|
111
|
-
job_submissions_excludes["job_provisioning_data"] = {
|
|
112
|
-
"instance_type": {"resources": {"cpu_arch"}}
|
|
113
|
-
}
|
|
114
|
-
if all(map(_should_exclude_job_submission_jrd_cpu_arch, job_submissions)):
|
|
115
|
-
job_submissions_excludes["job_runtime_data"] = {
|
|
116
|
-
"offer": {"instance": {"resources": {"cpu_arch"}}}
|
|
117
|
-
}
|
|
118
|
-
if all(js.exit_status is None for js in job_submissions):
|
|
119
|
-
job_submissions_excludes["exit_status"] = True
|
|
120
|
-
latest_job_submission = current_resource.latest_job_submission
|
|
121
|
-
if latest_job_submission is not None:
|
|
122
|
-
latest_job_submission_excludes = {}
|
|
123
|
-
current_resource_excludes["latest_job_submission"] = latest_job_submission_excludes
|
|
124
|
-
if _should_exclude_job_submission_jpd_cpu_arch(latest_job_submission):
|
|
125
|
-
latest_job_submission_excludes["job_provisioning_data"] = {
|
|
126
|
-
"instance_type": {"resources": {"cpu_arch"}}
|
|
127
|
-
}
|
|
128
|
-
if _should_exclude_job_submission_jrd_cpu_arch(latest_job_submission):
|
|
129
|
-
latest_job_submission_excludes["job_runtime_data"] = {
|
|
130
|
-
"offer": {"instance": {"resources": {"cpu_arch"}}}
|
|
131
|
-
}
|
|
132
|
-
if latest_job_submission.exit_status is None:
|
|
133
|
-
latest_job_submission_excludes["exit_status"] = True
|
|
134
|
-
return {"plan": apply_plan_excludes}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _should_exclude_job_submission_jpd_cpu_arch(job_submission: JobSubmission) -> bool:
|
|
138
|
-
try:
|
|
139
|
-
return job_submission.job_provisioning_data.instance_type.resources.cpu_arch is None
|
|
140
|
-
except AttributeError:
|
|
141
|
-
return True
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _should_exclude_job_submission_jrd_cpu_arch(job_submission: JobSubmission) -> bool:
|
|
145
|
-
try:
|
|
146
|
-
return job_submission.job_runtime_data.offer.instance.resources.cpu_arch is None
|
|
147
|
-
except AttributeError:
|
|
148
|
-
return True
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _get_get_plan_excludes(request: GetRunPlanRequest) -> Optional[Dict]:
|
|
152
|
-
"""
|
|
153
|
-
Excludes new fields when they are not set to keep
|
|
154
|
-
clients backward-compatibility with older servers.
|
|
155
|
-
"""
|
|
156
|
-
get_plan_excludes = {}
|
|
157
|
-
run_spec_excludes = _get_run_spec_excludes(request.run_spec)
|
|
158
|
-
if run_spec_excludes is not None:
|
|
159
|
-
get_plan_excludes["run_spec"] = run_spec_excludes
|
|
160
|
-
if request.max_offers is None:
|
|
161
|
-
get_plan_excludes["max_offers"] = True
|
|
162
|
-
return get_plan_excludes
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[Dict]:
|
|
166
|
-
"""
|
|
167
|
-
Returns `run_spec` exclude mapping to exclude certain fields from the request.
|
|
168
|
-
Use this method to exclude new fields when they are not set to keep
|
|
169
|
-
clients backward-compatibility with older servers.
|
|
170
|
-
"""
|
|
171
|
-
spec_excludes: dict[str, Any] = {}
|
|
172
|
-
configuration_excludes: dict[str, Any] = {}
|
|
173
|
-
profile_excludes: set[str] = set()
|
|
174
|
-
configuration = run_spec.configuration
|
|
175
|
-
profile = run_spec.profile
|
|
176
|
-
|
|
177
|
-
if configuration.fleets is None:
|
|
178
|
-
configuration_excludes["fleets"] = True
|
|
179
|
-
if profile is not None and profile.fleets is None:
|
|
180
|
-
profile_excludes.add("fleets")
|
|
181
|
-
if configuration.tags is None:
|
|
182
|
-
configuration_excludes["tags"] = True
|
|
183
|
-
if profile is not None and profile.tags is None:
|
|
184
|
-
profile_excludes.add("tags")
|
|
185
|
-
if isinstance(configuration, ServiceConfiguration) and not configuration.rate_limits:
|
|
186
|
-
configuration_excludes["rate_limits"] = True
|
|
187
|
-
if configuration.shell is None:
|
|
188
|
-
configuration_excludes["shell"] = True
|
|
189
|
-
if configuration.priority is None:
|
|
190
|
-
configuration_excludes["priority"] = True
|
|
191
|
-
|
|
192
|
-
if configuration_excludes:
|
|
193
|
-
spec_excludes["configuration"] = configuration_excludes
|
|
194
|
-
if profile_excludes:
|
|
195
|
-
spec_excludes["profile"] = profile_excludes
|
|
196
|
-
if spec_excludes:
|
|
197
|
-
return spec_excludes
|
|
198
|
-
return None
|
dstack/api/server/_volumes.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import List
|
|
2
2
|
|
|
3
3
|
from pydantic import parse_obj_as
|
|
4
4
|
|
|
5
|
+
from dstack._internal.core.compatibility.volumes import get_create_volume_excludes
|
|
5
6
|
from dstack._internal.core.models.volumes import Volume, VolumeConfiguration
|
|
6
7
|
from dstack._internal.server.schemas.volumes import (
|
|
7
8
|
CreateVolumeRequest,
|
|
@@ -29,22 +30,10 @@ class VolumesAPIClient(APIClientGroup):
|
|
|
29
30
|
body = CreateVolumeRequest(configuration=configuration)
|
|
30
31
|
resp = self._request(
|
|
31
32
|
f"/api/project/{project_name}/volumes/create",
|
|
32
|
-
body=body.json(exclude=
|
|
33
|
+
body=body.json(exclude=get_create_volume_excludes(configuration)),
|
|
33
34
|
)
|
|
34
35
|
return parse_obj_as(Volume.__response__, resp.json())
|
|
35
36
|
|
|
36
37
|
def delete(self, project_name: str, names: List[str]) -> None:
|
|
37
38
|
body = DeleteVolumesRequest(names=names)
|
|
38
39
|
self._request(f"/api/project/{project_name}/volumes/delete", body=body.json())
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _get_volume_configuration_excludes(configuration: VolumeConfiguration) -> Dict:
|
|
42
|
-
"""
|
|
43
|
-
Returns `configuration` exclude mapping to exclude certain fields from the request.
|
|
44
|
-
Use this method to exclude new fields when they are not set to keep
|
|
45
|
-
clients backward-compatibility with older servers.
|
|
46
|
-
"""
|
|
47
|
-
configuration_excludes = {}
|
|
48
|
-
if configuration.tags is None:
|
|
49
|
-
configuration_excludes["tags"] = True
|
|
50
|
-
return {"configuration": configuration_excludes}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
from typing import Type
|
|
3
|
+
from typing import Dict, Optional, Type
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
|
|
8
|
+
from dstack._internal.core.compatibility.fleets import get_fleet_spec_excludes
|
|
9
|
+
from dstack._internal.core.compatibility.gateways import get_gateway_spec_excludes
|
|
10
|
+
from dstack._internal.core.compatibility.runs import get_run_spec_excludes
|
|
11
|
+
from dstack._internal.core.compatibility.volumes import get_volume_spec_excludes
|
|
8
12
|
from dstack._internal.core.errors import ServerClientError
|
|
9
13
|
from dstack._internal.core.models.fleets import FleetSpec
|
|
10
14
|
from dstack._internal.core.models.gateways import GatewaySpec
|
|
@@ -44,12 +48,17 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
44
48
|
logger.error(f"Plugin service rejected apply request: {response.error}")
|
|
45
49
|
raise ServerClientError(f"Apply request rejected: {response.error}")
|
|
46
50
|
|
|
47
|
-
def _call_plugin_service(
|
|
51
|
+
def _call_plugin_service(
|
|
52
|
+
self,
|
|
53
|
+
spec_request: SpecApplyRequest,
|
|
54
|
+
endpoint: str,
|
|
55
|
+
excludes: Optional[Dict],
|
|
56
|
+
) -> ApplySpec:
|
|
48
57
|
response = None
|
|
49
58
|
try:
|
|
50
59
|
response = requests.post(
|
|
51
60
|
f"{self._plugin_service_uri}{endpoint}",
|
|
52
|
-
json=spec_request.dict(),
|
|
61
|
+
json=spec_request.dict(exclude={"spec": excludes}),
|
|
53
62
|
headers={"accept": "application/json", "Content-Type": "application/json"},
|
|
54
63
|
timeout=PLUGIN_REQUEST_TIMEOUT_SEC,
|
|
55
64
|
)
|
|
@@ -75,10 +84,11 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
75
84
|
user: str,
|
|
76
85
|
project: str,
|
|
77
86
|
spec: ApplySpec,
|
|
87
|
+
excludes: Optional[Dict] = None,
|
|
78
88
|
) -> ApplySpec:
|
|
79
89
|
try:
|
|
80
90
|
spec_request = request_cls(user=user, project=project, spec=spec)
|
|
81
|
-
spec_json = self._call_plugin_service(spec_request, endpoint)
|
|
91
|
+
spec_json = self._call_plugin_service(spec_request, endpoint, excludes)
|
|
82
92
|
response = response_cls(**spec_json)
|
|
83
93
|
self._check_request_rejected(response)
|
|
84
94
|
return response.spec
|
|
@@ -88,7 +98,13 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
88
98
|
|
|
89
99
|
def on_run_apply(self, user: str, project: str, spec: RunSpec) -> RunSpec:
|
|
90
100
|
return self._on_apply(
|
|
91
|
-
RunSpecRequest,
|
|
101
|
+
RunSpecRequest,
|
|
102
|
+
RunSpecResponse,
|
|
103
|
+
"/apply_policies/on_run_apply",
|
|
104
|
+
user,
|
|
105
|
+
project,
|
|
106
|
+
spec,
|
|
107
|
+
excludes=get_run_spec_excludes(spec),
|
|
92
108
|
)
|
|
93
109
|
|
|
94
110
|
def on_fleet_apply(self, user: str, project: str, spec: FleetSpec) -> FleetSpec:
|
|
@@ -99,6 +115,7 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
99
115
|
user,
|
|
100
116
|
project,
|
|
101
117
|
spec,
|
|
118
|
+
excludes=get_fleet_spec_excludes(spec),
|
|
102
119
|
)
|
|
103
120
|
|
|
104
121
|
def on_volume_apply(self, user: str, project: str, spec: VolumeSpec) -> VolumeSpec:
|
|
@@ -109,6 +126,7 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
109
126
|
user,
|
|
110
127
|
project,
|
|
111
128
|
spec,
|
|
129
|
+
excludes=get_volume_spec_excludes(spec),
|
|
112
130
|
)
|
|
113
131
|
|
|
114
132
|
def on_gateway_apply(self, user: str, project: str, spec: GatewaySpec) -> GatewaySpec:
|
|
@@ -119,6 +137,7 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
119
137
|
user,
|
|
120
138
|
project,
|
|
121
139
|
spec,
|
|
140
|
+
excludes=get_gateway_spec_excludes(spec),
|
|
122
141
|
)
|
|
123
142
|
|
|
124
143
|
|
dstack/version.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
__version__ = "0.19.
|
|
1
|
+
__version__ = "0.19.14"
|
|
2
2
|
__is_release__ = True
|
|
3
|
-
base_image = "0.
|
|
3
|
+
base_image = "0.10" base_image_ubuntu_version = "22.04"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dstack
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.14
|
|
4
4
|
Summary: dstack is an open-source orchestration engine for running AI workloads on any cloud or on-premises.
|
|
5
5
|
Project-URL: Homepage, https://dstack.ai
|
|
6
6
|
Project-URL: Source, https://github.com/dstackai/dstack
|