dstack 0.19.4rc3__py3-none-any.whl → 0.19.6rc1__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 +22 -20
- dstack/_internal/cli/commands/offer.py +116 -0
- dstack/_internal/cli/main.py +2 -0
- dstack/_internal/cli/services/configurators/base.py +1 -2
- dstack/_internal/cli/services/configurators/fleet.py +43 -20
- dstack/_internal/cli/services/configurators/run.py +3 -3
- dstack/_internal/cli/utils/run.py +43 -38
- dstack/_internal/core/backends/aws/auth.py +1 -2
- dstack/_internal/core/backends/aws/compute.py +24 -9
- dstack/_internal/core/backends/aws/configurator.py +2 -3
- dstack/_internal/core/backends/aws/resources.py +10 -0
- dstack/_internal/core/backends/azure/auth.py +1 -2
- dstack/_internal/core/backends/azure/compute.py +15 -5
- dstack/_internal/core/backends/azure/configurator.py +4 -5
- dstack/_internal/core/backends/azure/resources.py +14 -0
- dstack/_internal/core/backends/base/compute.py +99 -31
- dstack/_internal/core/backends/gcp/auth.py +1 -2
- dstack/_internal/core/backends/gcp/compute.py +58 -14
- dstack/_internal/core/backends/gcp/configurator.py +2 -3
- dstack/_internal/core/backends/gcp/features/tcpx.py +31 -0
- dstack/_internal/core/backends/gcp/resources.py +10 -0
- dstack/_internal/core/backends/nebius/compute.py +6 -2
- dstack/_internal/core/backends/nebius/configurator.py +4 -10
- dstack/_internal/core/backends/nebius/models.py +14 -1
- dstack/_internal/core/backends/nebius/resources.py +91 -10
- dstack/_internal/core/backends/oci/auth.py +1 -2
- dstack/_internal/core/backends/oci/configurator.py +1 -2
- dstack/_internal/core/backends/runpod/compute.py +1 -1
- dstack/_internal/core/errors.py +4 -0
- dstack/_internal/core/models/common.py +2 -14
- dstack/_internal/core/models/configurations.py +24 -2
- dstack/_internal/core/models/envs.py +2 -2
- dstack/_internal/core/models/fleets.py +34 -3
- dstack/_internal/core/models/gateways.py +18 -4
- dstack/_internal/core/models/instances.py +2 -1
- dstack/_internal/core/models/profiles.py +12 -0
- dstack/_internal/core/models/runs.py +6 -0
- dstack/_internal/core/models/secrets.py +1 -1
- dstack/_internal/core/models/volumes.py +17 -1
- dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +3 -3
- dstack/_internal/proxy/gateway/services/nginx.py +0 -1
- dstack/_internal/proxy/gateway/services/registry.py +0 -1
- dstack/_internal/server/background/tasks/process_instances.py +12 -9
- dstack/_internal/server/background/tasks/process_running_jobs.py +66 -15
- dstack/_internal/server/routers/fleets.py +22 -0
- dstack/_internal/server/routers/runs.py +1 -0
- dstack/_internal/server/schemas/fleets.py +12 -2
- dstack/_internal/server/schemas/runner.py +6 -0
- dstack/_internal/server/schemas/runs.py +3 -0
- dstack/_internal/server/services/docker.py +1 -2
- dstack/_internal/server/services/fleets.py +30 -12
- dstack/_internal/server/services/gateways/__init__.py +1 -0
- dstack/_internal/server/services/instances.py +3 -1
- dstack/_internal/server/services/jobs/__init__.py +1 -2
- dstack/_internal/server/services/jobs/configurators/base.py +17 -8
- dstack/_internal/server/services/locking.py +16 -1
- dstack/_internal/server/services/projects.py +1 -2
- dstack/_internal/server/services/proxy/repo.py +1 -2
- dstack/_internal/server/services/runner/client.py +3 -0
- dstack/_internal/server/services/runs.py +19 -16
- dstack/_internal/server/services/services/__init__.py +1 -2
- dstack/_internal/server/services/volumes.py +29 -2
- dstack/_internal/server/statics/00a6e1fb461ed2929fb9.png +0 -0
- dstack/_internal/server/statics/0cae4d9f0a36034984a7.png +0 -0
- dstack/_internal/server/statics/391de232cc0e30cae513.png +0 -0
- dstack/_internal/server/statics/4e0eead8c1a73689ef9d.svg +1 -0
- dstack/_internal/server/statics/544afa2f63428c2235b0.png +0 -0
- dstack/_internal/server/statics/54a4f50f74c6b9381530.svg +7 -0
- dstack/_internal/server/statics/68dd1360a7d2611e0132.svg +4 -0
- dstack/_internal/server/statics/69544b4c81973b54a66f.png +0 -0
- dstack/_internal/server/statics/77a8b02b17af19e39266.png +0 -0
- dstack/_internal/server/statics/83a93a8871c219104367.svg +9 -0
- dstack/_internal/server/statics/8f28bb8e9999e5e6a48b.svg +4 -0
- dstack/_internal/server/statics/9124086961ab8c366bc4.svg +9 -0
- dstack/_internal/server/statics/9a9ebaeb54b025dbac0a.svg +5 -0
- dstack/_internal/server/statics/a3428392dc534f3b15c4.svg +7 -0
- dstack/_internal/server/statics/ae22625574d69361f72c.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-144x144.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-192x192.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-256x256.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-36x36.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-384x384.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-48x48.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-512x512.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-72x72.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-96x96.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-1024x1024.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-114x114.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-120x120.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-144x144.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-152x152.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-167x167.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-180x180.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-57x57.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-60x60.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-72x72.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-76x76.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-precomposed.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1125x2436.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1136x640.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1170x2532.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1179x2556.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2208.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2688.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1284x2778.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1290x2796.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1334x750.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1488x2266.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1536x2048.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1620x2160.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1640x2160.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2224.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2388.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1792x828.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x1536.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x2732.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1620.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1640.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2208x1242.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2224x1668.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2266x1488.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2388x1668.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2436x1125.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2532x1170.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2556x1179.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2688x1242.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2732x2048.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2778x1284.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2796x1290.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-640x1136.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-750x1334.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-828x1792.png +0 -0
- dstack/_internal/server/statics/assets/browserconfig.xml +12 -0
- dstack/_internal/server/statics/assets/favicon-16x16.png +0 -0
- dstack/_internal/server/statics/assets/favicon-32x32.png +0 -0
- dstack/_internal/server/statics/assets/favicon-48x48.png +0 -0
- dstack/_internal/server/statics/assets/favicon.ico +0 -0
- dstack/_internal/server/statics/assets/manifest.webmanifest +67 -0
- dstack/_internal/server/statics/assets/mstile-144x144.png +0 -0
- dstack/_internal/server/statics/assets/mstile-150x150.png +0 -0
- dstack/_internal/server/statics/assets/mstile-310x150.png +0 -0
- dstack/_internal/server/statics/assets/mstile-310x310.png +0 -0
- dstack/_internal/server/statics/assets/mstile-70x70.png +0 -0
- dstack/_internal/server/statics/assets/yandex-browser-50x50.png +0 -0
- dstack/_internal/server/statics/assets/yandex-browser-manifest.json +9 -0
- dstack/_internal/server/statics/b7ae68f44193474fc578.png +0 -0
- dstack/_internal/server/statics/d2f008c75b2b5b191f3f.png +0 -0
- dstack/_internal/server/statics/d44c33e1b92e05c379fd.png +0 -0
- dstack/_internal/server/statics/dd43ff0552815179d7ab.png +0 -0
- dstack/_internal/server/statics/dd4e7166c0b9aac197d7.png +0 -0
- dstack/_internal/server/statics/e30b27916930d43d2271.png +0 -0
- dstack/_internal/server/statics/e467d7d60aae81ab198b.svg +6 -0
- dstack/_internal/server/statics/eb9b344b73818fe2b71a.png +0 -0
- dstack/_internal/server/statics/f517dd626eb964120de0.png +0 -0
- dstack/_internal/server/statics/f958aecddee5d8e3222c.png +0 -0
- dstack/_internal/server/statics/index.html +3 -0
- dstack/_internal/server/statics/main-8f9c66f404e9c7e7e020.css +3 -0
- dstack/_internal/server/statics/main-b4f65323f5df007e1664.js +136480 -0
- dstack/_internal/server/statics/main-b4f65323f5df007e1664.js.map +1 -0
- dstack/_internal/server/statics/manifest.json +16 -0
- dstack/_internal/server/statics/robots.txt +3 -0
- dstack/_internal/server/statics/static/media/entraID.d65d1f3e9486a8e56d24fc07b3230885.svg +9 -0
- dstack/_internal/server/statics/static/media/github.1f7102513534c83a9d8d735d2b8c12a2.svg +3 -0
- dstack/_internal/server/statics/static/media/logo.f602feeb138844eda97c8cb641461448.svg +124 -0
- dstack/_internal/server/statics/static/media/okta.12f178e6873a1100965f2a4dbd18fcec.svg +2 -0
- dstack/_internal/server/statics/static/media/theme.3994c817bb7dda191c1c9640dee0bf42.svg +3 -0
- dstack/_internal/server/testing/common.py +10 -0
- dstack/_internal/utils/tags.py +42 -0
- dstack/api/server/__init__.py +3 -1
- dstack/api/server/_fleets.py +52 -9
- dstack/api/server/_gateways.py +17 -2
- dstack/api/server/_runs.py +34 -11
- dstack/api/server/_volumes.py +2 -3
- dstack/version.py +1 -1
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/METADATA +2 -2
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/RECORD +180 -76
- dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/00-log-format.conf +0 -1
- dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/entrypoint.jinja2 +0 -27
- dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +0 -88
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/WHEEL +0 -0
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -2,9 +2,10 @@ import logging
|
|
|
2
2
|
import time
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from collections.abc import Container as ContainerT
|
|
5
|
-
from collections.abc import Generator
|
|
5
|
+
from collections.abc import Generator, Iterable, Sequence
|
|
6
6
|
from contextlib import contextmanager
|
|
7
7
|
from tempfile import NamedTemporaryFile
|
|
8
|
+
from typing import Optional
|
|
8
9
|
|
|
9
10
|
from nebius.aio.authorization.options import options_to_metadata
|
|
10
11
|
from nebius.aio.operation import Operation as SDKOperation
|
|
@@ -40,7 +41,11 @@ from nebius.api.nebius.iam.v1 import (
|
|
|
40
41
|
from nebius.api.nebius.vpc.v1 import ListSubnetsRequest, Subnet, SubnetServiceClient
|
|
41
42
|
from nebius.sdk import SDK
|
|
42
43
|
|
|
43
|
-
from dstack._internal.core.backends.
|
|
44
|
+
from dstack._internal.core.backends.base.configurator import raise_invalid_credentials_error
|
|
45
|
+
from dstack._internal.core.backends.nebius.models import (
|
|
46
|
+
DEFAULT_PROJECT_NAME_PREFIX,
|
|
47
|
+
NebiusServiceAccountCreds,
|
|
48
|
+
)
|
|
44
49
|
from dstack._internal.core.errors import BackendError, NoCapacityError
|
|
45
50
|
from dstack._internal.utils.event_loop import DaemonEventLoop
|
|
46
51
|
from dstack._internal.utils.logging import get_logger
|
|
@@ -110,7 +115,37 @@ def wait_for_operation(
|
|
|
110
115
|
LOOP.await_(op.update(timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD))
|
|
111
116
|
|
|
112
117
|
|
|
113
|
-
def get_region_to_project_id_map(
|
|
118
|
+
def get_region_to_project_id_map(
|
|
119
|
+
sdk: SDK, configured_regions: Optional[list[str]], configured_project_ids: Optional[list[str]]
|
|
120
|
+
) -> dict[str, str]:
|
|
121
|
+
"""Validate backend settings and build region->project_id map"""
|
|
122
|
+
|
|
123
|
+
projects = list_tenant_projects(sdk)
|
|
124
|
+
if configured_regions:
|
|
125
|
+
validate_regions(
|
|
126
|
+
configured=set(configured_regions), available={p.status.region for p in projects}
|
|
127
|
+
)
|
|
128
|
+
if configured_project_ids is not None:
|
|
129
|
+
return _get_region_to_configured_project_id_map(
|
|
130
|
+
projects, configured_project_ids, configured_regions
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
return _get_region_to_default_project_id_map(projects, configured_regions)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_regions(configured: set[str], available: set[str]) -> None:
|
|
137
|
+
if invalid := set(configured) - available:
|
|
138
|
+
raise_invalid_credentials_error(
|
|
139
|
+
fields=[["regions"]],
|
|
140
|
+
details=(
|
|
141
|
+
f"Configured regions {invalid} do not exist in this Nebius tenancy."
|
|
142
|
+
" Omit `regions` to use all regions or select some of the available regions:"
|
|
143
|
+
f" {available}"
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def list_tenant_projects(sdk: SDK) -> Sequence[Container]:
|
|
114
149
|
tenants = LOOP.await_(
|
|
115
150
|
TenantServiceClient(sdk).list(
|
|
116
151
|
ListTenantsRequest(), timeout=REQUEST_TIMEOUT, metadata=REQUEST_MD
|
|
@@ -126,26 +161,72 @@ def get_region_to_project_id_map(sdk: SDK) -> dict[str, str]:
|
|
|
126
161
|
metadata=REQUEST_MD,
|
|
127
162
|
)
|
|
128
163
|
)
|
|
164
|
+
return projects.items
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _get_region_to_default_project_id_map(
|
|
168
|
+
all_tenant_projects: Iterable[Container], configured_regions: Optional[list[str]]
|
|
169
|
+
) -> dict[str, str]:
|
|
129
170
|
region_to_projects: defaultdict[str, list[Container]] = defaultdict(list)
|
|
130
|
-
for project in
|
|
171
|
+
for project in all_tenant_projects:
|
|
131
172
|
region_to_projects[project.status.region].append(project)
|
|
132
173
|
region_to_project_id = {}
|
|
133
174
|
for region, region_projects in region_to_projects.items():
|
|
175
|
+
if configured_regions and region not in configured_regions:
|
|
176
|
+
continue
|
|
134
177
|
if len(region_projects) != 1:
|
|
135
|
-
# Currently, there can only be one project per region.
|
|
136
|
-
# This condition is implemented just in case Nebius suddenly allows more projects.
|
|
137
178
|
region_projects = [
|
|
138
|
-
p
|
|
179
|
+
p
|
|
180
|
+
for p in region_projects
|
|
181
|
+
if p.metadata.name.startswith(DEFAULT_PROJECT_NAME_PREFIX)
|
|
139
182
|
]
|
|
140
183
|
if len(region_projects) != 1:
|
|
141
|
-
|
|
142
|
-
"
|
|
184
|
+
raise_invalid_credentials_error(
|
|
185
|
+
["regions"],
|
|
186
|
+
(
|
|
187
|
+
f"Could not find the default project in region {region}."
|
|
188
|
+
" Consider setting the `projects` property in backend settings"
|
|
189
|
+
),
|
|
143
190
|
)
|
|
144
|
-
continue
|
|
145
191
|
region_to_project_id[region] = region_projects[0].metadata.id
|
|
146
192
|
return region_to_project_id
|
|
147
193
|
|
|
148
194
|
|
|
195
|
+
def _get_region_to_configured_project_id_map(
|
|
196
|
+
all_tenant_projects: Iterable[Container],
|
|
197
|
+
configured_project_ids: list[str],
|
|
198
|
+
configured_regions: Optional[list[str]],
|
|
199
|
+
) -> dict[str, str]:
|
|
200
|
+
project_id_to_project = {p.metadata.id: p for p in all_tenant_projects}
|
|
201
|
+
region_to_project_id = {}
|
|
202
|
+
for project_id in configured_project_ids:
|
|
203
|
+
project = project_id_to_project.get(project_id)
|
|
204
|
+
if project is None:
|
|
205
|
+
raise_invalid_credentials_error(
|
|
206
|
+
["projects"],
|
|
207
|
+
f"Configured project ID {project_id!r} not found in this Nebius tenancy",
|
|
208
|
+
)
|
|
209
|
+
duplicate_project_id = region_to_project_id.get(project.status.region)
|
|
210
|
+
if duplicate_project_id:
|
|
211
|
+
raise_invalid_credentials_error(
|
|
212
|
+
["projects"],
|
|
213
|
+
(
|
|
214
|
+
f"Configured projects {project_id} and {duplicate_project_id}"
|
|
215
|
+
f" both belong to the same region {project.status.region}."
|
|
216
|
+
" Only one project per region is allowed"
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
region_to_project_id[project.status.region] = project_id
|
|
220
|
+
if configured_regions:
|
|
221
|
+
# only filter by region after validating all project IDs
|
|
222
|
+
return {
|
|
223
|
+
region: project_id
|
|
224
|
+
for region, project_id in region_to_project_id.items()
|
|
225
|
+
if region in configured_regions
|
|
226
|
+
}
|
|
227
|
+
return region_to_project_id
|
|
228
|
+
|
|
229
|
+
|
|
149
230
|
def get_default_subnet(sdk: SDK, project_id: str) -> Subnet:
|
|
150
231
|
subnets = LOOP.await_(
|
|
151
232
|
SubnetServiceClient(sdk).list(
|
|
@@ -3,11 +3,10 @@ from typing_extensions import Any, Mapping
|
|
|
3
3
|
|
|
4
4
|
from dstack._internal.core.backends.oci.exceptions import any_oci_exception
|
|
5
5
|
from dstack._internal.core.backends.oci.models import AnyOCICreds, OCIDefaultCreds
|
|
6
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def get_client_config(creds: AnyOCICreds) -> Mapping[str, Any]:
|
|
10
|
-
if
|
|
9
|
+
if isinstance(creds, OCIDefaultCreds):
|
|
11
10
|
return oci.config.from_file(file_location=creds.file, profile_name=creds.profile)
|
|
12
11
|
return creds.dict(exclude={"type"})
|
|
13
12
|
|
|
@@ -27,7 +27,6 @@ from dstack._internal.core.errors import ServerClientError
|
|
|
27
27
|
from dstack._internal.core.models.backends.base import (
|
|
28
28
|
BackendType,
|
|
29
29
|
)
|
|
30
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
31
30
|
|
|
32
31
|
# where dstack images are published
|
|
33
32
|
SUPPORTED_REGIONS = frozenset(
|
|
@@ -48,7 +47,7 @@ class OCIConfigurator(Configurator):
|
|
|
48
47
|
BACKEND_CLASS = OCIBackend
|
|
49
48
|
|
|
50
49
|
def validate_config(self, config: OCIBackendConfigWithCreds, default_creds_enabled: bool):
|
|
51
|
-
if
|
|
50
|
+
if isinstance(config.creds, OCIDefaultCreds) and not default_creds_enabled:
|
|
52
51
|
raise_invalid_credentials_error(
|
|
53
52
|
fields=[["creds"]],
|
|
54
53
|
details="Default credentials are forbidden by dstack settings",
|
|
@@ -260,7 +260,7 @@ class RunpodCompute(
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
def _get_docker_args(authorized_keys: List[str]) -> str:
|
|
263
|
-
commands = get_docker_commands(authorized_keys
|
|
263
|
+
commands = get_docker_commands(authorized_keys)
|
|
264
264
|
command = " && ".join(commands)
|
|
265
265
|
docker_args = {"cmd": [command], "entrypoint": ["/bin/sh", "-c"]}
|
|
266
266
|
docker_args_escaped = json.dumps(json.dumps(docker_args)).strip('"')
|
dstack/_internal/core/errors.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Union
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
6
|
from pydantic_duality import DualBaseModel
|
|
7
|
-
from typing_extensions import Annotated
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
# DualBaseModel creates two classes for the model:
|
|
@@ -74,15 +74,3 @@ class ApplyAction(str, Enum):
|
|
|
74
74
|
class NetworkMode(str, Enum):
|
|
75
75
|
HOST = "host"
|
|
76
76
|
BRIDGE = "bridge"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
_CM = TypeVar("_CM", bound=CoreModel)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def is_core_model_instance(instance: Any, class_: Type[_CM]) -> TypeGuard[_CM]:
|
|
83
|
-
"""
|
|
84
|
-
Implements isinstance check for CoreModel such that
|
|
85
|
-
models parsed with MyModel.__response__ pass the check against MyModel.
|
|
86
|
-
See https://github.com/dstackai/dstack/issues/1124
|
|
87
|
-
"""
|
|
88
|
-
return isinstance(instance, class_) or isinstance(instance, class_.__response__)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from collections import Counter
|
|
3
3
|
from enum import Enum
|
|
4
|
+
from pathlib import PurePosixPath
|
|
4
5
|
from typing import Any, Dict, List, Optional, Union
|
|
5
6
|
|
|
6
7
|
from pydantic import Field, ValidationError, conint, constr, root_validator, validator
|
|
@@ -210,6 +211,16 @@ class BaseRunConfiguration(CoreModel):
|
|
|
210
211
|
Env,
|
|
211
212
|
Field(description="The mapping or the list of environment variables"),
|
|
212
213
|
] = Env()
|
|
214
|
+
shell: Annotated[
|
|
215
|
+
Optional[str],
|
|
216
|
+
Field(
|
|
217
|
+
description=(
|
|
218
|
+
"The shell used to run commands."
|
|
219
|
+
" Allowed values are `sh`, `bash`, or an absolute path, e.g., `/usr/bin/zsh`."
|
|
220
|
+
" Defaults to `/bin/sh` if the `image` is specified, `/bin/bash` otherwise"
|
|
221
|
+
)
|
|
222
|
+
),
|
|
223
|
+
] = None
|
|
213
224
|
# deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
|
|
214
225
|
setup: CommandsList = []
|
|
215
226
|
resources: Annotated[
|
|
@@ -244,6 +255,17 @@ class BaseRunConfiguration(CoreModel):
|
|
|
244
255
|
UnixUser.parse(v)
|
|
245
256
|
return v
|
|
246
257
|
|
|
258
|
+
@validator("shell")
|
|
259
|
+
def validate_shell(cls, v) -> Optional[str]:
|
|
260
|
+
if v is None:
|
|
261
|
+
return None
|
|
262
|
+
if v in ["sh", "bash"]:
|
|
263
|
+
return v
|
|
264
|
+
path = PurePosixPath(v)
|
|
265
|
+
if path.is_absolute():
|
|
266
|
+
return v
|
|
267
|
+
raise ValueError("The value must be `sh`, `bash`, or an absolute path")
|
|
268
|
+
|
|
247
269
|
|
|
248
270
|
class BaseRunConfigurationWithPorts(BaseRunConfiguration):
|
|
249
271
|
ports: Annotated[
|
|
@@ -261,7 +283,7 @@ class BaseRunConfigurationWithPorts(BaseRunConfiguration):
|
|
|
261
283
|
|
|
262
284
|
|
|
263
285
|
class BaseRunConfigurationWithCommands(BaseRunConfiguration):
|
|
264
|
-
commands: Annotated[CommandsList, Field(description="The
|
|
286
|
+
commands: Annotated[CommandsList, Field(description="The shell commands to run")] = []
|
|
265
287
|
|
|
266
288
|
@root_validator
|
|
267
289
|
def check_image_or_commands_present(cls, values):
|
|
@@ -276,7 +298,7 @@ class DevEnvironmentConfigurationParams(CoreModel):
|
|
|
276
298
|
Field(description="The IDE to run. Supported values include `vscode` and `cursor`"),
|
|
277
299
|
]
|
|
278
300
|
version: Annotated[Optional[str], Field(description="The version of the IDE")] = None
|
|
279
|
-
init: Annotated[CommandsList, Field(description="The
|
|
301
|
+
init: Annotated[CommandsList, Field(description="The shell commands to run on startup")] = []
|
|
280
302
|
inactivity_duration: Annotated[
|
|
281
303
|
Optional[Union[Literal["off"], int, bool, str]],
|
|
282
304
|
Field(
|
|
@@ -4,7 +4,7 @@ from typing import Dict, Iterable, Iterator, List, Mapping, NamedTuple, Tuple, U
|
|
|
4
4
|
from pydantic import BaseModel, Field, validator
|
|
5
5
|
from typing_extensions import Annotated, Self
|
|
6
6
|
|
|
7
|
-
from dstack._internal.core.models.common import CoreModel
|
|
7
|
+
from dstack._internal.core.models.common import CoreModel
|
|
8
8
|
|
|
9
9
|
# VAR_NAME=VALUE, VAR_NAME=, or VAR_NAME
|
|
10
10
|
_ENV_STRING_REGEX = r"^([a-zA-Z_][a-zA-Z0-9_]*)(=.*$|$)"
|
|
@@ -118,7 +118,7 @@ class Env(BaseModel):
|
|
|
118
118
|
unresolved: List[str] = []
|
|
119
119
|
dct: Dict[str, str] = {}
|
|
120
120
|
for k, v in self.items():
|
|
121
|
-
if
|
|
121
|
+
if isinstance(v, EnvSentinel):
|
|
122
122
|
unresolved.append(k)
|
|
123
123
|
else:
|
|
124
124
|
# cast is required since TypeGuard is for positive cases only
|
|
@@ -21,6 +21,7 @@ from dstack._internal.core.models.profiles import (
|
|
|
21
21
|
)
|
|
22
22
|
from dstack._internal.core.models.resources import Range, ResourcesSpec
|
|
23
23
|
from dstack._internal.utils.json_schema import add_extra_schema_types
|
|
24
|
+
from dstack._internal.utils.tags import tags_validator
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class FleetStatus(str, Enum):
|
|
@@ -249,7 +250,18 @@ class FleetProps(CoreModel):
|
|
|
249
250
|
|
|
250
251
|
|
|
251
252
|
class FleetConfiguration(InstanceGroupParams, FleetProps):
|
|
252
|
-
|
|
253
|
+
tags: Annotated[
|
|
254
|
+
Optional[Dict[str, str]],
|
|
255
|
+
Field(
|
|
256
|
+
description=(
|
|
257
|
+
"The custom tags to associate with the resource."
|
|
258
|
+
" The tags are also propagated to the underlying backend resources."
|
|
259
|
+
" If there is a conflict with backend-level tags, does not override them"
|
|
260
|
+
)
|
|
261
|
+
),
|
|
262
|
+
] = None
|
|
263
|
+
|
|
264
|
+
_validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
|
|
253
265
|
|
|
254
266
|
|
|
255
267
|
class FleetSpec(CoreModel):
|
|
@@ -300,7 +312,26 @@ class FleetPlan(CoreModel):
|
|
|
300
312
|
project_name: str
|
|
301
313
|
user: str
|
|
302
314
|
spec: FleetSpec
|
|
303
|
-
|
|
315
|
+
effective_spec: Optional[FleetSpec] = None
|
|
316
|
+
current_resource: Optional[Fleet] = None
|
|
304
317
|
offers: List[InstanceOfferWithAvailability]
|
|
305
318
|
total_offers: int
|
|
306
|
-
max_offer_price: Optional[float]
|
|
319
|
+
max_offer_price: Optional[float] = None
|
|
320
|
+
|
|
321
|
+
def get_effective_spec(self) -> FleetSpec:
|
|
322
|
+
if self.effective_spec is not None:
|
|
323
|
+
return self.effective_spec
|
|
324
|
+
return self.spec
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class ApplyFleetPlanInput(CoreModel):
|
|
328
|
+
spec: FleetSpec
|
|
329
|
+
current_resource: Annotated[
|
|
330
|
+
Optional[Fleet],
|
|
331
|
+
Field(
|
|
332
|
+
description=(
|
|
333
|
+
"The expected current resource."
|
|
334
|
+
" If the resource has changed, the apply fails unless `force: true`."
|
|
335
|
+
)
|
|
336
|
+
),
|
|
337
|
+
] = None
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Optional, Union
|
|
3
|
+
from typing import Dict, Optional, Union
|
|
4
4
|
|
|
5
|
-
from pydantic import Field
|
|
5
|
+
from pydantic import Field, validator
|
|
6
6
|
from typing_extensions import Annotated, Literal
|
|
7
7
|
|
|
8
8
|
from dstack._internal.core.models.backends.base import BackendType
|
|
9
9
|
from dstack._internal.core.models.common import CoreModel
|
|
10
|
+
from dstack._internal.utils.tags import tags_validator
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class GatewayStatus(str, Enum):
|
|
@@ -57,6 +58,18 @@ class GatewayConfiguration(CoreModel):
|
|
|
57
58
|
Optional[AnyGatewayCertificate],
|
|
58
59
|
Field(description="The SSL certificate configuration. Defaults to `type: lets-encrypt`"),
|
|
59
60
|
] = LetsEncryptGatewayCertificate()
|
|
61
|
+
tags: Annotated[
|
|
62
|
+
Optional[Dict[str, str]],
|
|
63
|
+
Field(
|
|
64
|
+
description=(
|
|
65
|
+
"The custom tags to associate with the gateway."
|
|
66
|
+
" The tags are also propagated to the underlying backend resources."
|
|
67
|
+
" If there is a conflict with backend-level tags, does not override them"
|
|
68
|
+
)
|
|
69
|
+
),
|
|
70
|
+
] = None
|
|
71
|
+
|
|
72
|
+
_validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
|
|
60
73
|
|
|
61
74
|
|
|
62
75
|
class GatewaySpec(CoreModel):
|
|
@@ -88,7 +101,7 @@ class GatewayPlan(CoreModel):
|
|
|
88
101
|
project_name: str
|
|
89
102
|
user: str
|
|
90
103
|
spec: GatewaySpec
|
|
91
|
-
current_resource: Optional[Gateway]
|
|
104
|
+
current_resource: Optional[Gateway] = None
|
|
92
105
|
|
|
93
106
|
|
|
94
107
|
class GatewayComputeConfiguration(CoreModel):
|
|
@@ -98,7 +111,8 @@ class GatewayComputeConfiguration(CoreModel):
|
|
|
98
111
|
region: str
|
|
99
112
|
public_ip: bool
|
|
100
113
|
ssh_key_pub: str
|
|
101
|
-
certificate: Optional[AnyGatewayCertificate]
|
|
114
|
+
certificate: Optional[AnyGatewayCertificate] = None
|
|
115
|
+
tags: Optional[Dict[str, str]] = None
|
|
102
116
|
|
|
103
117
|
|
|
104
118
|
class GatewayProvisioningData(CoreModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import List, Optional
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
6
|
import gpuhunt
|
|
@@ -108,6 +108,7 @@ class InstanceConfiguration(CoreModel):
|
|
|
108
108
|
placement_group_name: Optional[str] = None
|
|
109
109
|
reservation: Optional[str] = None
|
|
110
110
|
volumes: Optional[List[Volume]] = None
|
|
111
|
+
tags: Optional[Dict[str, str]] = None
|
|
111
112
|
|
|
112
113
|
def get_public_keys(self) -> List[str]:
|
|
113
114
|
return [ssh_key.public.strip() for ssh_key in self.ssh_keys]
|
|
@@ -6,6 +6,7 @@ from typing_extensions import Annotated, Literal
|
|
|
6
6
|
|
|
7
7
|
from dstack._internal.core.models.backends.base import BackendType
|
|
8
8
|
from dstack._internal.core.models.common import CoreModel, Duration
|
|
9
|
+
from dstack._internal.utils.tags import tags_validator
|
|
9
10
|
|
|
10
11
|
DEFAULT_RETRY_DURATION = 3600
|
|
11
12
|
|
|
@@ -243,6 +244,16 @@ class ProfileParams(CoreModel):
|
|
|
243
244
|
fleets: Annotated[
|
|
244
245
|
Optional[list[str]], Field(description="The fleets considered for reuse")
|
|
245
246
|
] = None
|
|
247
|
+
tags: Annotated[
|
|
248
|
+
Optional[Dict[str, str]],
|
|
249
|
+
Field(
|
|
250
|
+
description=(
|
|
251
|
+
"The custom tags to associate with the resource."
|
|
252
|
+
" The tags are also propagated to the underlying backend resources."
|
|
253
|
+
" If there is a conflict with backend-level tags, does not override them"
|
|
254
|
+
)
|
|
255
|
+
),
|
|
256
|
+
] = None
|
|
246
257
|
|
|
247
258
|
# Deprecated and unused. Left for compatibility with 0.18 clients.
|
|
248
259
|
pool_name: Annotated[Optional[str], Field(exclude=True)] = None
|
|
@@ -269,6 +280,7 @@ class ProfileParams(CoreModel):
|
|
|
269
280
|
_validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
|
|
270
281
|
parse_idle_duration
|
|
271
282
|
)
|
|
283
|
+
_validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
|
|
272
284
|
|
|
273
285
|
|
|
274
286
|
class ProfileProps(CoreModel):
|
|
@@ -455,10 +455,16 @@ class RunPlan(CoreModel):
|
|
|
455
455
|
project_name: str
|
|
456
456
|
user: str
|
|
457
457
|
run_spec: RunSpec
|
|
458
|
+
effective_run_spec: Optional[RunSpec] = None
|
|
458
459
|
job_plans: List[JobPlan]
|
|
459
460
|
current_resource: Optional[Run] = None
|
|
460
461
|
action: ApplyAction
|
|
461
462
|
|
|
463
|
+
def get_effective_run_spec(self) -> RunSpec:
|
|
464
|
+
if self.effective_run_spec is not None:
|
|
465
|
+
return self.effective_run_spec
|
|
466
|
+
return self.run_spec
|
|
467
|
+
|
|
462
468
|
|
|
463
469
|
class ApplyRunPlanInput(CoreModel):
|
|
464
470
|
run_spec: RunSpec
|
|
@@ -2,7 +2,7 @@ import uuid
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
|
-
from typing import List, Literal, Optional, Tuple, Union
|
|
5
|
+
from typing import Dict, List, Literal, Optional, Tuple, Union
|
|
6
6
|
|
|
7
7
|
from pydantic import Field, validator
|
|
8
8
|
from typing_extensions import Annotated, Self
|
|
@@ -11,6 +11,7 @@ from dstack._internal.core.models.backends.base import BackendType
|
|
|
11
11
|
from dstack._internal.core.models.common import CoreModel
|
|
12
12
|
from dstack._internal.core.models.resources import Memory
|
|
13
13
|
from dstack._internal.utils.common import get_or_error
|
|
14
|
+
from dstack._internal.utils.tags import tags_validator
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class VolumeStatus(str, Enum):
|
|
@@ -43,6 +44,18 @@ class VolumeConfiguration(CoreModel):
|
|
|
43
44
|
Optional[str],
|
|
44
45
|
Field(description="The volume ID. Must be specified when registering external volumes"),
|
|
45
46
|
] = None
|
|
47
|
+
tags: Annotated[
|
|
48
|
+
Optional[Dict[str, str]],
|
|
49
|
+
Field(
|
|
50
|
+
description=(
|
|
51
|
+
"The custom tags to associate with the volume."
|
|
52
|
+
" The tags are also propagated to the underlying backend resources."
|
|
53
|
+
" If there is a conflict with backend-level tags, does not override them"
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
] = None
|
|
57
|
+
|
|
58
|
+
_validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
|
|
46
59
|
|
|
47
60
|
@property
|
|
48
61
|
def size_gb(self) -> int:
|
|
@@ -91,11 +104,14 @@ class Volume(CoreModel):
|
|
|
91
104
|
configuration: VolumeConfiguration
|
|
92
105
|
external: bool
|
|
93
106
|
created_at: datetime
|
|
107
|
+
last_processed_at: datetime
|
|
94
108
|
status: VolumeStatus
|
|
95
109
|
status_message: Optional[str] = None
|
|
96
110
|
deleted: bool
|
|
111
|
+
deleted_at: Optional[datetime] = None
|
|
97
112
|
volume_id: Optional[str] = None # id of the volume in the cloud
|
|
98
113
|
provisioning_data: Optional[VolumeProvisioningData] = None
|
|
114
|
+
cost: float = 0
|
|
99
115
|
attachments: Optional[List[VolumeAttachment]] = None
|
|
100
116
|
# attachment_data is deprecated in favor of attachments.
|
|
101
117
|
# It's only set for volumes that were attached before attachments.
|
|
@@ -3,7 +3,7 @@ limit_req_zone {{ zone.key }} zone={{ zone.name }}:10m rate={{ zone.rpm }}r/m;
|
|
|
3
3
|
{% endfor %}
|
|
4
4
|
|
|
5
5
|
{% if replicas %}
|
|
6
|
-
upstream {{
|
|
6
|
+
upstream {{ domain }}.upstream {
|
|
7
7
|
{% for replica in replicas %}
|
|
8
8
|
server unix:{{ replica.socket }}; # replica {{ replica.id }}
|
|
9
9
|
{% endfor %}
|
|
@@ -37,7 +37,7 @@ server {
|
|
|
37
37
|
|
|
38
38
|
{% if replicas %}
|
|
39
39
|
location @websocket {
|
|
40
|
-
proxy_pass http://{{
|
|
40
|
+
proxy_pass http://{{ domain }}.upstream;
|
|
41
41
|
proxy_set_header X-Real-IP $remote_addr;
|
|
42
42
|
proxy_set_header Host $host;
|
|
43
43
|
proxy_http_version 1.1;
|
|
@@ -46,7 +46,7 @@ server {
|
|
|
46
46
|
proxy_read_timeout 300s;
|
|
47
47
|
}
|
|
48
48
|
location @ {
|
|
49
|
-
proxy_pass http://{{
|
|
49
|
+
proxy_pass http://{{ domain }}.upstream;
|
|
50
50
|
proxy_set_header X-Real-IP $remote_addr;
|
|
51
51
|
proxy_set_header Host $host;
|
|
52
52
|
proxy_read_timeout 300s;
|
|
@@ -327,7 +327,6 @@ async def get_nginx_service_config(
|
|
|
327
327
|
domain=service.domain_safe,
|
|
328
328
|
https=service.https_safe,
|
|
329
329
|
project_name=service.project_name,
|
|
330
|
-
run_name=service.run_name,
|
|
331
330
|
auth=service.auth,
|
|
332
331
|
client_max_body_size=service.client_max_body_size,
|
|
333
332
|
access_log_path=ACCESS_LOG_PATH,
|
|
@@ -17,11 +17,11 @@ from dstack._internal.core.backends import (
|
|
|
17
17
|
BACKENDS_WITH_PLACEMENT_GROUPS_SUPPORT,
|
|
18
18
|
)
|
|
19
19
|
from dstack._internal.core.backends.base.compute import (
|
|
20
|
-
DSTACK_RUNNER_BINARY_PATH,
|
|
21
|
-
DSTACK_SHIM_BINARY_PATH,
|
|
22
|
-
DSTACK_WORKING_DIR,
|
|
23
20
|
ComputeWithCreateInstanceSupport,
|
|
24
21
|
ComputeWithPlacementGroupSupport,
|
|
22
|
+
get_dstack_runner_binary_path,
|
|
23
|
+
get_dstack_shim_binary_path,
|
|
24
|
+
get_dstack_working_dir,
|
|
25
25
|
get_shim_env,
|
|
26
26
|
get_shim_pre_start_commands,
|
|
27
27
|
)
|
|
@@ -411,23 +411,26 @@ def _deploy_instance(
|
|
|
411
411
|
except ValueError as e:
|
|
412
412
|
raise ProvisioningError(f"Invalid Env: {e}") from e
|
|
413
413
|
shim_envs.update(fleet_configuration_envs)
|
|
414
|
-
|
|
414
|
+
dstack_working_dir = get_dstack_working_dir()
|
|
415
|
+
dstack_shim_binary_path = get_dstack_shim_binary_path()
|
|
416
|
+
dstack_runner_binary_path = get_dstack_runner_binary_path()
|
|
417
|
+
upload_envs(client, dstack_working_dir, shim_envs)
|
|
415
418
|
logger.debug("The dstack-shim environment variables have been installed")
|
|
416
419
|
|
|
417
420
|
# Ensure we have fresh versions of host info.json and dstack-runner
|
|
418
|
-
remove_host_info_if_exists(client,
|
|
419
|
-
remove_dstack_runner_if_exists(client,
|
|
421
|
+
remove_host_info_if_exists(client, dstack_working_dir)
|
|
422
|
+
remove_dstack_runner_if_exists(client, dstack_runner_binary_path)
|
|
420
423
|
|
|
421
424
|
# Run dstack-shim as a systemd service
|
|
422
425
|
run_shim_as_systemd_service(
|
|
423
426
|
client=client,
|
|
424
|
-
binary_path=
|
|
425
|
-
working_dir=
|
|
427
|
+
binary_path=dstack_shim_binary_path,
|
|
428
|
+
working_dir=dstack_working_dir,
|
|
426
429
|
dev=settings.DSTACK_VERSION is None,
|
|
427
430
|
)
|
|
428
431
|
|
|
429
432
|
# Get host info
|
|
430
|
-
host_info = get_host_info(client,
|
|
433
|
+
host_info = get_host_info(client, dstack_working_dir)
|
|
431
434
|
logger.debug("Received a host_info %s", host_info)
|
|
432
435
|
|
|
433
436
|
raw_health = get_shim_healthcheck(client)
|