dstack 0.19.26__py3-none-any.whl → 0.19.28__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/__init__.py +11 -8
- dstack/_internal/cli/commands/apply.py +6 -3
- dstack/_internal/cli/commands/completion.py +3 -1
- dstack/_internal/cli/commands/config.py +1 -0
- dstack/_internal/cli/commands/init.py +4 -4
- dstack/_internal/cli/commands/offer.py +1 -1
- dstack/_internal/cli/commands/project.py +1 -0
- dstack/_internal/cli/commands/server.py +2 -2
- dstack/_internal/cli/main.py +1 -1
- dstack/_internal/cli/services/configurators/base.py +2 -4
- dstack/_internal/cli/services/configurators/fleet.py +4 -5
- dstack/_internal/cli/services/configurators/gateway.py +3 -5
- dstack/_internal/cli/services/configurators/run.py +165 -43
- dstack/_internal/cli/services/configurators/volume.py +3 -5
- dstack/_internal/cli/services/repos.py +1 -18
- dstack/_internal/core/backends/amddevcloud/__init__.py +1 -0
- dstack/_internal/core/backends/amddevcloud/backend.py +16 -0
- dstack/_internal/core/backends/amddevcloud/compute.py +5 -0
- dstack/_internal/core/backends/amddevcloud/configurator.py +29 -0
- dstack/_internal/core/backends/aws/compute.py +6 -1
- dstack/_internal/core/backends/base/compute.py +33 -5
- dstack/_internal/core/backends/base/offers.py +2 -0
- dstack/_internal/core/backends/configurators.py +15 -0
- dstack/_internal/core/backends/digitalocean/__init__.py +1 -0
- dstack/_internal/core/backends/digitalocean/backend.py +16 -0
- dstack/_internal/core/backends/digitalocean/compute.py +5 -0
- dstack/_internal/core/backends/digitalocean/configurator.py +31 -0
- dstack/_internal/core/backends/digitalocean_base/__init__.py +1 -0
- dstack/_internal/core/backends/digitalocean_base/api_client.py +104 -0
- dstack/_internal/core/backends/digitalocean_base/backend.py +5 -0
- dstack/_internal/core/backends/digitalocean_base/compute.py +173 -0
- dstack/_internal/core/backends/digitalocean_base/configurator.py +57 -0
- dstack/_internal/core/backends/digitalocean_base/models.py +43 -0
- dstack/_internal/core/backends/gcp/compute.py +32 -8
- dstack/_internal/core/backends/hotaisle/api_client.py +25 -33
- dstack/_internal/core/backends/hotaisle/compute.py +1 -6
- dstack/_internal/core/backends/models.py +7 -0
- dstack/_internal/core/backends/nebius/compute.py +0 -7
- dstack/_internal/core/backends/oci/compute.py +4 -5
- dstack/_internal/core/backends/vultr/compute.py +1 -5
- dstack/_internal/core/compatibility/fleets.py +5 -0
- dstack/_internal/core/compatibility/runs.py +10 -1
- dstack/_internal/core/models/backends/base.py +5 -1
- dstack/_internal/core/models/common.py +67 -43
- dstack/_internal/core/models/configurations.py +109 -69
- dstack/_internal/core/models/files.py +1 -1
- dstack/_internal/core/models/fleets.py +115 -25
- dstack/_internal/core/models/instances.py +5 -5
- dstack/_internal/core/models/profiles.py +66 -47
- dstack/_internal/core/models/repos/remote.py +21 -16
- dstack/_internal/core/models/resources.py +69 -65
- dstack/_internal/core/models/runs.py +41 -14
- dstack/_internal/core/services/repos.py +85 -80
- dstack/_internal/server/app.py +5 -0
- dstack/_internal/server/background/tasks/process_fleets.py +117 -13
- dstack/_internal/server/background/tasks/process_instances.py +12 -71
- dstack/_internal/server/background/tasks/process_running_jobs.py +2 -0
- dstack/_internal/server/background/tasks/process_runs.py +2 -0
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +48 -16
- dstack/_internal/server/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
- dstack/_internal/server/models.py +11 -7
- dstack/_internal/server/schemas/gateways.py +10 -9
- dstack/_internal/server/schemas/runner.py +1 -0
- dstack/_internal/server/services/backends/handlers.py +2 -0
- dstack/_internal/server/services/docker.py +8 -7
- dstack/_internal/server/services/fleets.py +23 -25
- dstack/_internal/server/services/instances.py +3 -3
- dstack/_internal/server/services/jobs/configurators/base.py +46 -6
- dstack/_internal/server/services/jobs/configurators/dev.py +4 -4
- dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +3 -5
- dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +4 -6
- dstack/_internal/server/services/jobs/configurators/service.py +0 -3
- dstack/_internal/server/services/jobs/configurators/task.py +0 -3
- dstack/_internal/server/services/projects.py +52 -1
- dstack/_internal/server/services/runs.py +16 -0
- dstack/_internal/server/settings.py +46 -0
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-aec4762350e34d6fbff9.css → main-5e0d56245c4bd241ec27.css} +1 -1
- dstack/_internal/server/statics/{main-d151b300fcac3933213d.js → main-a2a16772fbf11a14d191.js} +1215 -998
- dstack/_internal/server/statics/{main-d151b300fcac3933213d.js.map → main-a2a16772fbf11a14d191.js.map} +1 -1
- dstack/_internal/server/testing/common.py +6 -3
- dstack/_internal/utils/env.py +85 -11
- dstack/_internal/utils/path.py +8 -1
- dstack/_internal/utils/ssh.py +7 -0
- dstack/api/_public/repos.py +41 -6
- dstack/api/_public/runs.py +14 -1
- dstack/version.py +1 -1
- {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/METADATA +2 -2
- {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/RECORD +92 -78
- dstack/_internal/server/statics/static/media/github.1f7102513534c83a9d8d735d2b8c12a2.svg +0 -3
- {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/WHEEL +0 -0
- {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.26.dist-info → dstack-0.19.28.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -10,12 +10,23 @@ from pydantic import Field, ValidationError, conint, constr, root_validator, val
|
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
12
|
from dstack._internal.core.errors import ConfigurationError
|
|
13
|
-
from dstack._internal.core.models.common import
|
|
13
|
+
from dstack._internal.core.models.common import (
|
|
14
|
+
CoreConfig,
|
|
15
|
+
CoreModel,
|
|
16
|
+
Duration,
|
|
17
|
+
RegistryAuth,
|
|
18
|
+
generate_dual_core_model,
|
|
19
|
+
)
|
|
14
20
|
from dstack._internal.core.models.envs import Env
|
|
15
21
|
from dstack._internal.core.models.files import FilePathMapping
|
|
16
22
|
from dstack._internal.core.models.fleets import FleetConfiguration
|
|
17
23
|
from dstack._internal.core.models.gateways import GatewayConfiguration
|
|
18
|
-
from dstack._internal.core.models.profiles import
|
|
24
|
+
from dstack._internal.core.models.profiles import (
|
|
25
|
+
ProfileParams,
|
|
26
|
+
ProfileParamsConfig,
|
|
27
|
+
parse_duration,
|
|
28
|
+
parse_off_duration,
|
|
29
|
+
)
|
|
19
30
|
from dstack._internal.core.models.resources import Range, ResourcesSpec
|
|
20
31
|
from dstack._internal.core.models.services import AnyModel, OpenAIChatModel
|
|
21
32
|
from dstack._internal.core.models.unix import UnixUser
|
|
@@ -34,7 +45,7 @@ STRIP_PREFIX_DEFAULT = True
|
|
|
34
45
|
RUN_PRIOTIRY_MIN = 0
|
|
35
46
|
RUN_PRIOTIRY_MAX = 100
|
|
36
47
|
RUN_PRIORITY_DEFAULT = 0
|
|
37
|
-
|
|
48
|
+
LEGACY_REPO_DIR = "/workflow"
|
|
38
49
|
MIN_PROBE_TIMEOUT = 1
|
|
39
50
|
MIN_PROBE_INTERVAL = 1
|
|
40
51
|
DEFAULT_PROBE_URL = "/"
|
|
@@ -112,8 +123,15 @@ class RepoSpec(CoreModel):
|
|
|
112
123
|
Optional[str],
|
|
113
124
|
Field(description="The commit hash"),
|
|
114
125
|
] = None
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
path: Annotated[
|
|
127
|
+
Optional[str],
|
|
128
|
+
Field(
|
|
129
|
+
description=(
|
|
130
|
+
"The repo path inside the run container. Relative paths are resolved"
|
|
131
|
+
f" relative to the working directory. Defaults to `{LEGACY_REPO_DIR}`"
|
|
132
|
+
)
|
|
133
|
+
),
|
|
134
|
+
] = None
|
|
117
135
|
|
|
118
136
|
@classmethod
|
|
119
137
|
def parse(cls, v: str) -> Self:
|
|
@@ -149,6 +167,14 @@ class RepoSpec(CoreModel):
|
|
|
149
167
|
raise ValueError("Either `local_path` or `url` must be specified")
|
|
150
168
|
return values
|
|
151
169
|
|
|
170
|
+
@validator("path")
|
|
171
|
+
def validate_path(cls, v: Optional[str]) -> Optional[str]:
|
|
172
|
+
if v is None:
|
|
173
|
+
return v
|
|
174
|
+
if v.startswith("~") and PurePosixPath(v).parts[0] != "~":
|
|
175
|
+
raise ValueError("`~username` syntax is not supported")
|
|
176
|
+
return v
|
|
177
|
+
|
|
152
178
|
|
|
153
179
|
class ScalingSpec(CoreModel):
|
|
154
180
|
metric: Annotated[
|
|
@@ -261,7 +287,20 @@ class HTTPHeaderSpec(CoreModel):
|
|
|
261
287
|
]
|
|
262
288
|
|
|
263
289
|
|
|
264
|
-
class
|
|
290
|
+
class ProbeConfigConfig(CoreConfig):
|
|
291
|
+
@staticmethod
|
|
292
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
293
|
+
add_extra_schema_types(
|
|
294
|
+
schema["properties"]["timeout"],
|
|
295
|
+
extra_types=[{"type": "string"}],
|
|
296
|
+
)
|
|
297
|
+
add_extra_schema_types(
|
|
298
|
+
schema["properties"]["interval"],
|
|
299
|
+
extra_types=[{"type": "string"}],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ProbeConfig(generate_dual_core_model(ProbeConfigConfig)):
|
|
265
304
|
type: Literal["http"] # expect other probe types in the future, namely `exec`
|
|
266
305
|
url: Annotated[
|
|
267
306
|
Optional[str], Field(description=f"The URL to request. Defaults to `{DEFAULT_PROBE_URL}`")
|
|
@@ -316,18 +355,6 @@ class ProbeConfig(CoreModel):
|
|
|
316
355
|
),
|
|
317
356
|
] = None
|
|
318
357
|
|
|
319
|
-
class Config(CoreModel.Config):
|
|
320
|
-
@staticmethod
|
|
321
|
-
def schema_extra(schema: Dict[str, Any]):
|
|
322
|
-
add_extra_schema_types(
|
|
323
|
-
schema["properties"]["timeout"],
|
|
324
|
-
extra_types=[{"type": "string"}],
|
|
325
|
-
)
|
|
326
|
-
add_extra_schema_types(
|
|
327
|
-
schema["properties"]["interval"],
|
|
328
|
-
extra_types=[{"type": "string"}],
|
|
329
|
-
)
|
|
330
|
-
|
|
331
358
|
@validator("timeout", pre=True)
|
|
332
359
|
def parse_timeout(cls, v: Optional[Union[int, str]]) -> Optional[int]:
|
|
333
360
|
if v is None:
|
|
@@ -366,6 +393,19 @@ class ProbeConfig(CoreModel):
|
|
|
366
393
|
return values
|
|
367
394
|
|
|
368
395
|
|
|
396
|
+
class BaseRunConfigurationConfig(CoreConfig):
|
|
397
|
+
@staticmethod
|
|
398
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
399
|
+
add_extra_schema_types(
|
|
400
|
+
schema["properties"]["volumes"]["items"],
|
|
401
|
+
extra_types=[{"type": "string"}],
|
|
402
|
+
)
|
|
403
|
+
add_extra_schema_types(
|
|
404
|
+
schema["properties"]["files"]["items"],
|
|
405
|
+
extra_types=[{"type": "string"}],
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
369
409
|
class BaseRunConfiguration(CoreModel):
|
|
370
410
|
type: Literal["none"]
|
|
371
411
|
name: Annotated[
|
|
@@ -380,7 +420,7 @@ class BaseRunConfiguration(CoreModel):
|
|
|
380
420
|
Field(
|
|
381
421
|
description=(
|
|
382
422
|
"The user inside the container, `user_name_or_id[:group_name_or_id]`"
|
|
383
|
-
" (e.g., `ubuntu`, `1000:1000`). Defaults to the default `image`
|
|
423
|
+
" (e.g., `ubuntu`, `1000:1000`). Defaults to the default user from the `image`"
|
|
384
424
|
)
|
|
385
425
|
),
|
|
386
426
|
] = None
|
|
@@ -390,9 +430,8 @@ class BaseRunConfiguration(CoreModel):
|
|
|
390
430
|
Optional[str],
|
|
391
431
|
Field(
|
|
392
432
|
description=(
|
|
393
|
-
"The path to the working directory inside the container."
|
|
394
|
-
f"
|
|
395
|
-
' Defaults to `"."` '
|
|
433
|
+
"The absolute path to the working directory inside the container."
|
|
434
|
+
f" Defaults to `{LEGACY_REPO_DIR}`"
|
|
396
435
|
)
|
|
397
436
|
),
|
|
398
437
|
] = None
|
|
@@ -470,18 +509,6 @@ class BaseRunConfiguration(CoreModel):
|
|
|
470
509
|
# deprecated since 0.18.31; task, service -- no effect; dev-environment -- executed right before `init`
|
|
471
510
|
setup: CommandsList = []
|
|
472
511
|
|
|
473
|
-
class Config(CoreModel.Config):
|
|
474
|
-
@staticmethod
|
|
475
|
-
def schema_extra(schema: Dict[str, Any]):
|
|
476
|
-
add_extra_schema_types(
|
|
477
|
-
schema["properties"]["volumes"]["items"],
|
|
478
|
-
extra_types=[{"type": "string"}],
|
|
479
|
-
)
|
|
480
|
-
add_extra_schema_types(
|
|
481
|
-
schema["properties"]["files"]["items"],
|
|
482
|
-
extra_types=[{"type": "string"}],
|
|
483
|
-
)
|
|
484
|
-
|
|
485
512
|
@validator("python", pre=True, always=True)
|
|
486
513
|
def convert_python(cls, v, values) -> Optional[PythonVersion]:
|
|
487
514
|
if v is not None and values.get("image"):
|
|
@@ -607,20 +634,25 @@ class DevEnvironmentConfigurationParams(CoreModel):
|
|
|
607
634
|
return None
|
|
608
635
|
|
|
609
636
|
|
|
637
|
+
class DevEnvironmentConfigurationConfig(
|
|
638
|
+
ProfileParamsConfig,
|
|
639
|
+
BaseRunConfigurationConfig,
|
|
640
|
+
):
|
|
641
|
+
@staticmethod
|
|
642
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
643
|
+
ProfileParamsConfig.schema_extra(schema)
|
|
644
|
+
BaseRunConfigurationConfig.schema_extra(schema)
|
|
645
|
+
|
|
646
|
+
|
|
610
647
|
class DevEnvironmentConfiguration(
|
|
611
648
|
ProfileParams,
|
|
612
649
|
BaseRunConfiguration,
|
|
613
650
|
ConfigurationWithPortsParams,
|
|
614
651
|
DevEnvironmentConfigurationParams,
|
|
652
|
+
generate_dual_core_model(DevEnvironmentConfigurationConfig),
|
|
615
653
|
):
|
|
616
654
|
type: Literal["dev-environment"] = "dev-environment"
|
|
617
655
|
|
|
618
|
-
class Config(ProfileParams.Config, BaseRunConfiguration.Config):
|
|
619
|
-
@staticmethod
|
|
620
|
-
def schema_extra(schema: Dict[str, Any]):
|
|
621
|
-
ProfileParams.Config.schema_extra(schema)
|
|
622
|
-
BaseRunConfiguration.Config.schema_extra(schema)
|
|
623
|
-
|
|
624
656
|
@validator("entrypoint")
|
|
625
657
|
def validate_entrypoint(cls, v: Optional[str]) -> Optional[str]:
|
|
626
658
|
if v is not None:
|
|
@@ -632,20 +664,38 @@ class TaskConfigurationParams(CoreModel):
|
|
|
632
664
|
nodes: Annotated[int, Field(description="Number of nodes", ge=1)] = 1
|
|
633
665
|
|
|
634
666
|
|
|
667
|
+
class TaskConfigurationConfig(
|
|
668
|
+
ProfileParamsConfig,
|
|
669
|
+
BaseRunConfigurationConfig,
|
|
670
|
+
):
|
|
671
|
+
@staticmethod
|
|
672
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
673
|
+
ProfileParamsConfig.schema_extra(schema)
|
|
674
|
+
BaseRunConfigurationConfig.schema_extra(schema)
|
|
675
|
+
|
|
676
|
+
|
|
635
677
|
class TaskConfiguration(
|
|
636
678
|
ProfileParams,
|
|
637
679
|
BaseRunConfiguration,
|
|
638
680
|
ConfigurationWithCommandsParams,
|
|
639
681
|
ConfigurationWithPortsParams,
|
|
640
682
|
TaskConfigurationParams,
|
|
683
|
+
generate_dual_core_model(TaskConfigurationConfig),
|
|
641
684
|
):
|
|
642
685
|
type: Literal["task"] = "task"
|
|
643
686
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
687
|
+
|
|
688
|
+
class ServiceConfigurationParamsConfig(CoreConfig):
|
|
689
|
+
@staticmethod
|
|
690
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
691
|
+
add_extra_schema_types(
|
|
692
|
+
schema["properties"]["replicas"],
|
|
693
|
+
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
694
|
+
)
|
|
695
|
+
add_extra_schema_types(
|
|
696
|
+
schema["properties"]["model"],
|
|
697
|
+
extra_types=[{"type": "string"}],
|
|
698
|
+
)
|
|
649
699
|
|
|
650
700
|
|
|
651
701
|
class ServiceConfigurationParams(CoreModel):
|
|
@@ -705,18 +755,6 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
705
755
|
Field(description="List of probes used to determine job health"),
|
|
706
756
|
] = []
|
|
707
757
|
|
|
708
|
-
class Config(CoreModel.Config):
|
|
709
|
-
@staticmethod
|
|
710
|
-
def schema_extra(schema: Dict[str, Any]):
|
|
711
|
-
add_extra_schema_types(
|
|
712
|
-
schema["properties"]["replicas"],
|
|
713
|
-
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
714
|
-
)
|
|
715
|
-
add_extra_schema_types(
|
|
716
|
-
schema["properties"]["model"],
|
|
717
|
-
extra_types=[{"type": "string"}],
|
|
718
|
-
)
|
|
719
|
-
|
|
720
758
|
@validator("port")
|
|
721
759
|
def convert_port(cls, v) -> PortMapping:
|
|
722
760
|
if isinstance(v, int):
|
|
@@ -783,25 +821,27 @@ class ServiceConfigurationParams(CoreModel):
|
|
|
783
821
|
return v
|
|
784
822
|
|
|
785
823
|
|
|
824
|
+
class ServiceConfigurationConfig(
|
|
825
|
+
ProfileParamsConfig,
|
|
826
|
+
BaseRunConfigurationConfig,
|
|
827
|
+
ServiceConfigurationParamsConfig,
|
|
828
|
+
):
|
|
829
|
+
@staticmethod
|
|
830
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
831
|
+
ProfileParamsConfig.schema_extra(schema)
|
|
832
|
+
BaseRunConfigurationConfig.schema_extra(schema)
|
|
833
|
+
ServiceConfigurationParamsConfig.schema_extra(schema)
|
|
834
|
+
|
|
835
|
+
|
|
786
836
|
class ServiceConfiguration(
|
|
787
837
|
ProfileParams,
|
|
788
838
|
BaseRunConfiguration,
|
|
789
839
|
ConfigurationWithCommandsParams,
|
|
790
840
|
ServiceConfigurationParams,
|
|
841
|
+
generate_dual_core_model(ServiceConfigurationConfig),
|
|
791
842
|
):
|
|
792
843
|
type: Literal["service"] = "service"
|
|
793
844
|
|
|
794
|
-
class Config(
|
|
795
|
-
ProfileParams.Config,
|
|
796
|
-
BaseRunConfiguration.Config,
|
|
797
|
-
ServiceConfigurationParams.Config,
|
|
798
|
-
):
|
|
799
|
-
@staticmethod
|
|
800
|
-
def schema_extra(schema: Dict[str, Any]):
|
|
801
|
-
ProfileParams.Config.schema_extra(schema)
|
|
802
|
-
BaseRunConfiguration.Config.schema_extra(schema)
|
|
803
|
-
ServiceConfigurationParams.Config.schema_extra(schema)
|
|
804
|
-
|
|
805
845
|
|
|
806
846
|
AnyRunConfiguration = Union[DevEnvironmentConfiguration, TaskConfiguration, ServiceConfiguration]
|
|
807
847
|
|
|
@@ -862,7 +902,7 @@ class DstackConfiguration(CoreModel):
|
|
|
862
902
|
Field(discriminator="type"),
|
|
863
903
|
]
|
|
864
904
|
|
|
865
|
-
class Config(
|
|
905
|
+
class Config(CoreConfig):
|
|
866
906
|
json_loads = orjson.loads
|
|
867
907
|
json_dumps = pydantic_orjson_dumps_with_indent
|
|
868
908
|
|
|
@@ -2,13 +2,18 @@ import ipaddress
|
|
|
2
2
|
import uuid
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Any, Dict, List, Optional,
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
from pydantic import Field, root_validator, validator
|
|
8
8
|
from typing_extensions import Annotated, Literal
|
|
9
9
|
|
|
10
10
|
from dstack._internal.core.models.backends.base import BackendType
|
|
11
|
-
from dstack._internal.core.models.common import
|
|
11
|
+
from dstack._internal.core.models.common import (
|
|
12
|
+
ApplyAction,
|
|
13
|
+
CoreConfig,
|
|
14
|
+
CoreModel,
|
|
15
|
+
generate_dual_core_model,
|
|
16
|
+
)
|
|
12
17
|
from dstack._internal.core.models.envs import Env
|
|
13
18
|
from dstack._internal.core.models.instances import Instance, InstanceOfferWithAvailability, SSHKey
|
|
14
19
|
from dstack._internal.core.models.profiles import (
|
|
@@ -19,7 +24,7 @@ from dstack._internal.core.models.profiles import (
|
|
|
19
24
|
TerminationPolicy,
|
|
20
25
|
parse_idle_duration,
|
|
21
26
|
)
|
|
22
|
-
from dstack._internal.core.models.resources import
|
|
27
|
+
from dstack._internal.core.models.resources import ResourcesSpec
|
|
23
28
|
from dstack._internal.utils.common import list_enum_values_for_annotation
|
|
24
29
|
from dstack._internal.utils.json_schema import add_extra_schema_types
|
|
25
30
|
from dstack._internal.utils.tags import tags_validator
|
|
@@ -141,6 +146,82 @@ class SSHParams(CoreModel):
|
|
|
141
146
|
return value
|
|
142
147
|
|
|
143
148
|
|
|
149
|
+
class FleetNodesSpec(CoreModel):
|
|
150
|
+
min: Annotated[
|
|
151
|
+
int, Field(description=("The minimum number of instances to maintain in the fleet"))
|
|
152
|
+
]
|
|
153
|
+
target: Annotated[
|
|
154
|
+
int,
|
|
155
|
+
Field(
|
|
156
|
+
description=(
|
|
157
|
+
"The number of instances to provision on fleet apply. `min` <= `target` <= `max`"
|
|
158
|
+
" Defaults to `min`"
|
|
159
|
+
)
|
|
160
|
+
),
|
|
161
|
+
]
|
|
162
|
+
max: Annotated[
|
|
163
|
+
Optional[int],
|
|
164
|
+
Field(
|
|
165
|
+
description=(
|
|
166
|
+
"The maximum number of instances allowed in the fleet. Unlimited if not specified"
|
|
167
|
+
)
|
|
168
|
+
),
|
|
169
|
+
] = None
|
|
170
|
+
|
|
171
|
+
def dict(self, *args, **kwargs) -> Dict:
|
|
172
|
+
# super() does not work with pydantic-duality
|
|
173
|
+
res = CoreModel.dict(self, *args, **kwargs)
|
|
174
|
+
# For backward compatibility with old clients
|
|
175
|
+
# that do not ignore extra fields due to https://github.com/dstackai/dstack/issues/3066
|
|
176
|
+
if "target" in res and res["target"] == res["min"]:
|
|
177
|
+
del res["target"]
|
|
178
|
+
return res
|
|
179
|
+
|
|
180
|
+
@root_validator(pre=True)
|
|
181
|
+
def set_min_and_target_defaults(cls, values):
|
|
182
|
+
min_ = values.get("min")
|
|
183
|
+
target = values.get("target")
|
|
184
|
+
if min_ is None:
|
|
185
|
+
values["min"] = 0
|
|
186
|
+
if target is None:
|
|
187
|
+
values["target"] = values["min"]
|
|
188
|
+
return values
|
|
189
|
+
|
|
190
|
+
@validator("min")
|
|
191
|
+
def validate_min(cls, v: int) -> int:
|
|
192
|
+
if v < 0:
|
|
193
|
+
raise ValueError("min cannot be negative")
|
|
194
|
+
return v
|
|
195
|
+
|
|
196
|
+
@root_validator(skip_on_failure=True)
|
|
197
|
+
def _post_validate_ranges(cls, values):
|
|
198
|
+
min_ = values["min"]
|
|
199
|
+
target = values["target"]
|
|
200
|
+
max_ = values.get("max")
|
|
201
|
+
if target < min_:
|
|
202
|
+
raise ValueError("target must not be be less than min")
|
|
203
|
+
if max_ is not None and max_ < min_:
|
|
204
|
+
raise ValueError("max must not be less than min")
|
|
205
|
+
if max_ is not None and max_ < target:
|
|
206
|
+
raise ValueError("max must not be less than target")
|
|
207
|
+
return values
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class InstanceGroupParamsConfig(CoreConfig):
|
|
211
|
+
@staticmethod
|
|
212
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
213
|
+
del schema["properties"]["termination_policy"]
|
|
214
|
+
del schema["properties"]["termination_idle_time"]
|
|
215
|
+
add_extra_schema_types(
|
|
216
|
+
schema["properties"]["nodes"],
|
|
217
|
+
extra_types=[{"type": "integer"}, {"type": "string"}],
|
|
218
|
+
)
|
|
219
|
+
add_extra_schema_types(
|
|
220
|
+
schema["properties"]["idle_duration"],
|
|
221
|
+
extra_types=[{"type": "string"}],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
144
225
|
class InstanceGroupParams(CoreModel):
|
|
145
226
|
env: Annotated[
|
|
146
227
|
Env,
|
|
@@ -151,7 +232,9 @@ class InstanceGroupParams(CoreModel):
|
|
|
151
232
|
Field(description="The parameters for adding instances via SSH"),
|
|
152
233
|
] = None
|
|
153
234
|
|
|
154
|
-
nodes: Annotated[
|
|
235
|
+
nodes: Annotated[
|
|
236
|
+
Optional[FleetNodesSpec], Field(description="The number of instances in cloud fleet")
|
|
237
|
+
] = None
|
|
155
238
|
placement: Annotated[
|
|
156
239
|
Optional[InstanceGroupPlacement],
|
|
157
240
|
Field(description="The placement of instances: `any` or `cluster`"),
|
|
@@ -234,19 +317,15 @@ class InstanceGroupParams(CoreModel):
|
|
|
234
317
|
termination_policy: Annotated[Optional[TerminationPolicy], Field(exclude=True)] = None
|
|
235
318
|
termination_idle_time: Annotated[Optional[Union[str, int]], Field(exclude=True)] = None
|
|
236
319
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
add_extra_schema_types(
|
|
247
|
-
schema["properties"]["idle_duration"],
|
|
248
|
-
extra_types=[{"type": "string"}],
|
|
249
|
-
)
|
|
320
|
+
@validator("nodes", pre=True)
|
|
321
|
+
def parse_nodes(cls, v: Optional[Union[dict, str]]) -> Optional[dict]:
|
|
322
|
+
if isinstance(v, str) and ".." in v:
|
|
323
|
+
v = v.replace(" ", "")
|
|
324
|
+
min, max = v.split("..")
|
|
325
|
+
return dict(min=min or None, max=max or None)
|
|
326
|
+
elif isinstance(v, str) or isinstance(v, int):
|
|
327
|
+
return dict(min=v, max=v)
|
|
328
|
+
return v
|
|
250
329
|
|
|
251
330
|
_validate_idle_duration = validator("idle_duration", pre=True, allow_reuse=True)(
|
|
252
331
|
parse_idle_duration
|
|
@@ -258,7 +337,17 @@ class FleetProps(CoreModel):
|
|
|
258
337
|
name: Annotated[Optional[str], Field(description="The fleet name")] = None
|
|
259
338
|
|
|
260
339
|
|
|
261
|
-
class
|
|
340
|
+
class FleetConfigurationConfig(InstanceGroupParamsConfig):
|
|
341
|
+
@staticmethod
|
|
342
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
343
|
+
InstanceGroupParamsConfig.schema_extra(schema)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class FleetConfiguration(
|
|
347
|
+
InstanceGroupParams,
|
|
348
|
+
FleetProps,
|
|
349
|
+
generate_dual_core_model(FleetConfigurationConfig),
|
|
350
|
+
):
|
|
262
351
|
tags: Annotated[
|
|
263
352
|
Optional[Dict[str, str]],
|
|
264
353
|
Field(
|
|
@@ -273,7 +362,14 @@ class FleetConfiguration(InstanceGroupParams, FleetProps):
|
|
|
273
362
|
_validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
|
|
274
363
|
|
|
275
364
|
|
|
276
|
-
class
|
|
365
|
+
class FleetSpecConfig(CoreConfig):
|
|
366
|
+
@staticmethod
|
|
367
|
+
def schema_extra(schema: Dict[str, Any]):
|
|
368
|
+
prop = schema.get("properties", {})
|
|
369
|
+
prop.pop("merged_profile", None)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class FleetSpec(generate_dual_core_model(FleetSpecConfig)):
|
|
277
373
|
configuration: FleetConfiguration
|
|
278
374
|
configuration_path: Optional[str] = None
|
|
279
375
|
profile: Profile
|
|
@@ -283,12 +379,6 @@ class FleetSpec(CoreModel):
|
|
|
283
379
|
# TODO: make merged_profile a computed field after migrating to pydanticV2
|
|
284
380
|
merged_profile: Annotated[Profile, Field(exclude=True)] = None
|
|
285
381
|
|
|
286
|
-
class Config(CoreModel.Config):
|
|
287
|
-
@staticmethod
|
|
288
|
-
def schema_extra(schema: Dict[str, Any], model: Type) -> None:
|
|
289
|
-
prop = schema.get("properties", {})
|
|
290
|
-
prop.pop("merged_profile", None)
|
|
291
|
-
|
|
292
382
|
@root_validator
|
|
293
383
|
def _merged_profile(cls, values) -> Dict:
|
|
294
384
|
try:
|
|
@@ -7,7 +7,10 @@ import gpuhunt
|
|
|
7
7
|
from pydantic import root_validator
|
|
8
8
|
|
|
9
9
|
from dstack._internal.core.models.backends.base import BackendType
|
|
10
|
-
from dstack._internal.core.models.common import
|
|
10
|
+
from dstack._internal.core.models.common import (
|
|
11
|
+
CoreModel,
|
|
12
|
+
FrozenCoreModel,
|
|
13
|
+
)
|
|
11
14
|
from dstack._internal.core.models.envs import Env
|
|
12
15
|
from dstack._internal.core.models.health import HealthStatus
|
|
13
16
|
from dstack._internal.core.models.volumes import Volume
|
|
@@ -117,14 +120,11 @@ class InstanceType(CoreModel):
|
|
|
117
120
|
resources: Resources
|
|
118
121
|
|
|
119
122
|
|
|
120
|
-
class SSHConnectionParams(
|
|
123
|
+
class SSHConnectionParams(FrozenCoreModel):
|
|
121
124
|
hostname: str
|
|
122
125
|
username: str
|
|
123
126
|
port: int
|
|
124
127
|
|
|
125
|
-
class Config(CoreModel.Config):
|
|
126
|
-
frozen = True
|
|
127
|
-
|
|
128
128
|
|
|
129
129
|
class SSHKey(CoreModel):
|
|
130
130
|
public: str
|