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.

Files changed (183) hide show
  1. dstack/_internal/cli/commands/attach.py +22 -20
  2. dstack/_internal/cli/commands/offer.py +116 -0
  3. dstack/_internal/cli/main.py +2 -0
  4. dstack/_internal/cli/services/configurators/base.py +1 -2
  5. dstack/_internal/cli/services/configurators/fleet.py +43 -20
  6. dstack/_internal/cli/services/configurators/run.py +3 -3
  7. dstack/_internal/cli/utils/run.py +43 -38
  8. dstack/_internal/core/backends/aws/auth.py +1 -2
  9. dstack/_internal/core/backends/aws/compute.py +24 -9
  10. dstack/_internal/core/backends/aws/configurator.py +2 -3
  11. dstack/_internal/core/backends/aws/resources.py +10 -0
  12. dstack/_internal/core/backends/azure/auth.py +1 -2
  13. dstack/_internal/core/backends/azure/compute.py +15 -5
  14. dstack/_internal/core/backends/azure/configurator.py +4 -5
  15. dstack/_internal/core/backends/azure/resources.py +14 -0
  16. dstack/_internal/core/backends/base/compute.py +99 -31
  17. dstack/_internal/core/backends/gcp/auth.py +1 -2
  18. dstack/_internal/core/backends/gcp/compute.py +58 -14
  19. dstack/_internal/core/backends/gcp/configurator.py +2 -3
  20. dstack/_internal/core/backends/gcp/features/tcpx.py +31 -0
  21. dstack/_internal/core/backends/gcp/resources.py +10 -0
  22. dstack/_internal/core/backends/nebius/compute.py +6 -2
  23. dstack/_internal/core/backends/nebius/configurator.py +4 -10
  24. dstack/_internal/core/backends/nebius/models.py +14 -1
  25. dstack/_internal/core/backends/nebius/resources.py +91 -10
  26. dstack/_internal/core/backends/oci/auth.py +1 -2
  27. dstack/_internal/core/backends/oci/configurator.py +1 -2
  28. dstack/_internal/core/backends/runpod/compute.py +1 -1
  29. dstack/_internal/core/errors.py +4 -0
  30. dstack/_internal/core/models/common.py +2 -14
  31. dstack/_internal/core/models/configurations.py +24 -2
  32. dstack/_internal/core/models/envs.py +2 -2
  33. dstack/_internal/core/models/fleets.py +34 -3
  34. dstack/_internal/core/models/gateways.py +18 -4
  35. dstack/_internal/core/models/instances.py +2 -1
  36. dstack/_internal/core/models/profiles.py +12 -0
  37. dstack/_internal/core/models/runs.py +6 -0
  38. dstack/_internal/core/models/secrets.py +1 -1
  39. dstack/_internal/core/models/volumes.py +17 -1
  40. dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +3 -3
  41. dstack/_internal/proxy/gateway/services/nginx.py +0 -1
  42. dstack/_internal/proxy/gateway/services/registry.py +0 -1
  43. dstack/_internal/server/background/tasks/process_instances.py +12 -9
  44. dstack/_internal/server/background/tasks/process_running_jobs.py +66 -15
  45. dstack/_internal/server/routers/fleets.py +22 -0
  46. dstack/_internal/server/routers/runs.py +1 -0
  47. dstack/_internal/server/schemas/fleets.py +12 -2
  48. dstack/_internal/server/schemas/runner.py +6 -0
  49. dstack/_internal/server/schemas/runs.py +3 -0
  50. dstack/_internal/server/services/docker.py +1 -2
  51. dstack/_internal/server/services/fleets.py +30 -12
  52. dstack/_internal/server/services/gateways/__init__.py +1 -0
  53. dstack/_internal/server/services/instances.py +3 -1
  54. dstack/_internal/server/services/jobs/__init__.py +1 -2
  55. dstack/_internal/server/services/jobs/configurators/base.py +17 -8
  56. dstack/_internal/server/services/locking.py +16 -1
  57. dstack/_internal/server/services/projects.py +1 -2
  58. dstack/_internal/server/services/proxy/repo.py +1 -2
  59. dstack/_internal/server/services/runner/client.py +3 -0
  60. dstack/_internal/server/services/runs.py +19 -16
  61. dstack/_internal/server/services/services/__init__.py +1 -2
  62. dstack/_internal/server/services/volumes.py +29 -2
  63. dstack/_internal/server/statics/00a6e1fb461ed2929fb9.png +0 -0
  64. dstack/_internal/server/statics/0cae4d9f0a36034984a7.png +0 -0
  65. dstack/_internal/server/statics/391de232cc0e30cae513.png +0 -0
  66. dstack/_internal/server/statics/4e0eead8c1a73689ef9d.svg +1 -0
  67. dstack/_internal/server/statics/544afa2f63428c2235b0.png +0 -0
  68. dstack/_internal/server/statics/54a4f50f74c6b9381530.svg +7 -0
  69. dstack/_internal/server/statics/68dd1360a7d2611e0132.svg +4 -0
  70. dstack/_internal/server/statics/69544b4c81973b54a66f.png +0 -0
  71. dstack/_internal/server/statics/77a8b02b17af19e39266.png +0 -0
  72. dstack/_internal/server/statics/83a93a8871c219104367.svg +9 -0
  73. dstack/_internal/server/statics/8f28bb8e9999e5e6a48b.svg +4 -0
  74. dstack/_internal/server/statics/9124086961ab8c366bc4.svg +9 -0
  75. dstack/_internal/server/statics/9a9ebaeb54b025dbac0a.svg +5 -0
  76. dstack/_internal/server/statics/a3428392dc534f3b15c4.svg +7 -0
  77. dstack/_internal/server/statics/ae22625574d69361f72c.png +0 -0
  78. dstack/_internal/server/statics/assets/android-chrome-144x144.png +0 -0
  79. dstack/_internal/server/statics/assets/android-chrome-192x192.png +0 -0
  80. dstack/_internal/server/statics/assets/android-chrome-256x256.png +0 -0
  81. dstack/_internal/server/statics/assets/android-chrome-36x36.png +0 -0
  82. dstack/_internal/server/statics/assets/android-chrome-384x384.png +0 -0
  83. dstack/_internal/server/statics/assets/android-chrome-48x48.png +0 -0
  84. dstack/_internal/server/statics/assets/android-chrome-512x512.png +0 -0
  85. dstack/_internal/server/statics/assets/android-chrome-72x72.png +0 -0
  86. dstack/_internal/server/statics/assets/android-chrome-96x96.png +0 -0
  87. dstack/_internal/server/statics/assets/apple-touch-icon-1024x1024.png +0 -0
  88. dstack/_internal/server/statics/assets/apple-touch-icon-114x114.png +0 -0
  89. dstack/_internal/server/statics/assets/apple-touch-icon-120x120.png +0 -0
  90. dstack/_internal/server/statics/assets/apple-touch-icon-144x144.png +0 -0
  91. dstack/_internal/server/statics/assets/apple-touch-icon-152x152.png +0 -0
  92. dstack/_internal/server/statics/assets/apple-touch-icon-167x167.png +0 -0
  93. dstack/_internal/server/statics/assets/apple-touch-icon-180x180.png +0 -0
  94. dstack/_internal/server/statics/assets/apple-touch-icon-57x57.png +0 -0
  95. dstack/_internal/server/statics/assets/apple-touch-icon-60x60.png +0 -0
  96. dstack/_internal/server/statics/assets/apple-touch-icon-72x72.png +0 -0
  97. dstack/_internal/server/statics/assets/apple-touch-icon-76x76.png +0 -0
  98. dstack/_internal/server/statics/assets/apple-touch-icon-precomposed.png +0 -0
  99. dstack/_internal/server/statics/assets/apple-touch-icon.png +0 -0
  100. dstack/_internal/server/statics/assets/apple-touch-startup-image-1125x2436.png +0 -0
  101. dstack/_internal/server/statics/assets/apple-touch-startup-image-1136x640.png +0 -0
  102. dstack/_internal/server/statics/assets/apple-touch-startup-image-1170x2532.png +0 -0
  103. dstack/_internal/server/statics/assets/apple-touch-startup-image-1179x2556.png +0 -0
  104. dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2208.png +0 -0
  105. dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2688.png +0 -0
  106. dstack/_internal/server/statics/assets/apple-touch-startup-image-1284x2778.png +0 -0
  107. dstack/_internal/server/statics/assets/apple-touch-startup-image-1290x2796.png +0 -0
  108. dstack/_internal/server/statics/assets/apple-touch-startup-image-1334x750.png +0 -0
  109. dstack/_internal/server/statics/assets/apple-touch-startup-image-1488x2266.png +0 -0
  110. dstack/_internal/server/statics/assets/apple-touch-startup-image-1536x2048.png +0 -0
  111. dstack/_internal/server/statics/assets/apple-touch-startup-image-1620x2160.png +0 -0
  112. dstack/_internal/server/statics/assets/apple-touch-startup-image-1640x2160.png +0 -0
  113. dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2224.png +0 -0
  114. dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2388.png +0 -0
  115. dstack/_internal/server/statics/assets/apple-touch-startup-image-1792x828.png +0 -0
  116. dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x1536.png +0 -0
  117. dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x2732.png +0 -0
  118. dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1620.png +0 -0
  119. dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1640.png +0 -0
  120. dstack/_internal/server/statics/assets/apple-touch-startup-image-2208x1242.png +0 -0
  121. dstack/_internal/server/statics/assets/apple-touch-startup-image-2224x1668.png +0 -0
  122. dstack/_internal/server/statics/assets/apple-touch-startup-image-2266x1488.png +0 -0
  123. dstack/_internal/server/statics/assets/apple-touch-startup-image-2388x1668.png +0 -0
  124. dstack/_internal/server/statics/assets/apple-touch-startup-image-2436x1125.png +0 -0
  125. dstack/_internal/server/statics/assets/apple-touch-startup-image-2532x1170.png +0 -0
  126. dstack/_internal/server/statics/assets/apple-touch-startup-image-2556x1179.png +0 -0
  127. dstack/_internal/server/statics/assets/apple-touch-startup-image-2688x1242.png +0 -0
  128. dstack/_internal/server/statics/assets/apple-touch-startup-image-2732x2048.png +0 -0
  129. dstack/_internal/server/statics/assets/apple-touch-startup-image-2778x1284.png +0 -0
  130. dstack/_internal/server/statics/assets/apple-touch-startup-image-2796x1290.png +0 -0
  131. dstack/_internal/server/statics/assets/apple-touch-startup-image-640x1136.png +0 -0
  132. dstack/_internal/server/statics/assets/apple-touch-startup-image-750x1334.png +0 -0
  133. dstack/_internal/server/statics/assets/apple-touch-startup-image-828x1792.png +0 -0
  134. dstack/_internal/server/statics/assets/browserconfig.xml +12 -0
  135. dstack/_internal/server/statics/assets/favicon-16x16.png +0 -0
  136. dstack/_internal/server/statics/assets/favicon-32x32.png +0 -0
  137. dstack/_internal/server/statics/assets/favicon-48x48.png +0 -0
  138. dstack/_internal/server/statics/assets/favicon.ico +0 -0
  139. dstack/_internal/server/statics/assets/manifest.webmanifest +67 -0
  140. dstack/_internal/server/statics/assets/mstile-144x144.png +0 -0
  141. dstack/_internal/server/statics/assets/mstile-150x150.png +0 -0
  142. dstack/_internal/server/statics/assets/mstile-310x150.png +0 -0
  143. dstack/_internal/server/statics/assets/mstile-310x310.png +0 -0
  144. dstack/_internal/server/statics/assets/mstile-70x70.png +0 -0
  145. dstack/_internal/server/statics/assets/yandex-browser-50x50.png +0 -0
  146. dstack/_internal/server/statics/assets/yandex-browser-manifest.json +9 -0
  147. dstack/_internal/server/statics/b7ae68f44193474fc578.png +0 -0
  148. dstack/_internal/server/statics/d2f008c75b2b5b191f3f.png +0 -0
  149. dstack/_internal/server/statics/d44c33e1b92e05c379fd.png +0 -0
  150. dstack/_internal/server/statics/dd43ff0552815179d7ab.png +0 -0
  151. dstack/_internal/server/statics/dd4e7166c0b9aac197d7.png +0 -0
  152. dstack/_internal/server/statics/e30b27916930d43d2271.png +0 -0
  153. dstack/_internal/server/statics/e467d7d60aae81ab198b.svg +6 -0
  154. dstack/_internal/server/statics/eb9b344b73818fe2b71a.png +0 -0
  155. dstack/_internal/server/statics/f517dd626eb964120de0.png +0 -0
  156. dstack/_internal/server/statics/f958aecddee5d8e3222c.png +0 -0
  157. dstack/_internal/server/statics/index.html +3 -0
  158. dstack/_internal/server/statics/main-8f9c66f404e9c7e7e020.css +3 -0
  159. dstack/_internal/server/statics/main-b4f65323f5df007e1664.js +136480 -0
  160. dstack/_internal/server/statics/main-b4f65323f5df007e1664.js.map +1 -0
  161. dstack/_internal/server/statics/manifest.json +16 -0
  162. dstack/_internal/server/statics/robots.txt +3 -0
  163. dstack/_internal/server/statics/static/media/entraID.d65d1f3e9486a8e56d24fc07b3230885.svg +9 -0
  164. dstack/_internal/server/statics/static/media/github.1f7102513534c83a9d8d735d2b8c12a2.svg +3 -0
  165. dstack/_internal/server/statics/static/media/logo.f602feeb138844eda97c8cb641461448.svg +124 -0
  166. dstack/_internal/server/statics/static/media/okta.12f178e6873a1100965f2a4dbd18fcec.svg +2 -0
  167. dstack/_internal/server/statics/static/media/theme.3994c817bb7dda191c1c9640dee0bf42.svg +3 -0
  168. dstack/_internal/server/testing/common.py +10 -0
  169. dstack/_internal/utils/tags.py +42 -0
  170. dstack/api/server/__init__.py +3 -1
  171. dstack/api/server/_fleets.py +52 -9
  172. dstack/api/server/_gateways.py +17 -2
  173. dstack/api/server/_runs.py +34 -11
  174. dstack/api/server/_volumes.py +2 -3
  175. dstack/version.py +1 -1
  176. {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/METADATA +2 -2
  177. {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/RECORD +180 -76
  178. dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/00-log-format.conf +0 -1
  179. dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/entrypoint.jinja2 +0 -27
  180. dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +0 -88
  181. {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/WHEEL +0 -0
  182. {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/entry_points.txt +0 -0
  183. {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.nebius.models import NebiusServiceAccountCreds
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(sdk: SDK) -> dict[str, str]:
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 projects.items:
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 for p in region_projects if p.metadata.name.startswith("default-project")
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
- logger.warning(
142
- "Could not find the default project in region %s, tenant %s", region, tenant_id
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 is_core_model_instance(creds, OCIDefaultCreds):
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 is_core_model_instance(config.creds, OCIDefaultCreds) and not default_creds_enabled:
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, False)
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('"')
@@ -18,6 +18,10 @@ class ClientError(DstackError):
18
18
  pass
19
19
 
20
20
 
21
+ class URLNotFoundError(ClientError):
22
+ pass
23
+
24
+
21
25
  class ServerClientErrorCode(str, enum.Enum):
22
26
  UNSPECIFIED_ERROR = "error"
23
27
  RESOURCE_EXISTS = "resource_exists"
@@ -1,10 +1,10 @@
1
1
  import re
2
2
  from enum import Enum
3
- from typing import Any, Type, TypeVar, Union
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, TypeGuard
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 bash commands to run")] = []
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 bash commands to run on startup")] = []
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, is_core_model_instance
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 is_core_model_instance(v, EnvSentinel):
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
- pass
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
- current_resource: Optional[Fleet]
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
@@ -6,4 +6,4 @@ class Secret(CoreModel):
6
6
  value: str
7
7
 
8
8
  def __str__(self) -> str:
9
- return f'Secret(name="{self.name}", value={"*"*len(self.value)})'
9
+ return f'Secret(name="{self.name}", value={"*" * len(self.value)})'
@@ -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 {{ run_name }} {
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://{{ run_name }};
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://{{ run_name }};
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;
@@ -58,7 +58,6 @@ class LocationConfig(BaseModel):
58
58
  class ServiceConfig(SiteConfig):
59
59
  type: Literal["service"] = "service"
60
60
  project_name: str
61
- run_name: str
62
61
  auth: bool
63
62
  client_max_body_size: int
64
63
  access_log_path: Path
@@ -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
- upload_envs(client, DSTACK_WORKING_DIR, shim_envs)
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, DSTACK_WORKING_DIR)
419
- remove_dstack_runner_if_exists(client, DSTACK_RUNNER_BINARY_PATH)
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=DSTACK_SHIM_BINARY_PATH,
425
- working_dir=DSTACK_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, DSTACK_WORKING_DIR)
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)