dstack 0.19.25rc1__py3-none-any.whl → 0.19.27__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 +2 -2
- dstack/_internal/cli/commands/apply.py +3 -61
- dstack/_internal/cli/commands/attach.py +1 -1
- dstack/_internal/cli/commands/completion.py +1 -1
- dstack/_internal/cli/commands/delete.py +2 -2
- dstack/_internal/cli/commands/fleet.py +1 -1
- dstack/_internal/cli/commands/gateway.py +2 -2
- dstack/_internal/cli/commands/init.py +56 -24
- dstack/_internal/cli/commands/logs.py +1 -1
- dstack/_internal/cli/commands/metrics.py +1 -1
- dstack/_internal/cli/commands/offer.py +45 -7
- dstack/_internal/cli/commands/project.py +2 -2
- dstack/_internal/cli/commands/secrets.py +2 -2
- dstack/_internal/cli/commands/server.py +1 -1
- dstack/_internal/cli/commands/stop.py +1 -1
- dstack/_internal/cli/commands/volume.py +1 -1
- dstack/_internal/cli/main.py +2 -2
- dstack/_internal/cli/services/completion.py +2 -2
- dstack/_internal/cli/services/configurators/__init__.py +6 -2
- dstack/_internal/cli/services/configurators/base.py +6 -7
- dstack/_internal/cli/services/configurators/fleet.py +1 -3
- dstack/_internal/cli/services/configurators/gateway.py +2 -4
- dstack/_internal/cli/services/configurators/run.py +293 -58
- dstack/_internal/cli/services/configurators/volume.py +2 -4
- dstack/_internal/cli/services/profile.py +1 -1
- dstack/_internal/cli/services/repos.py +35 -48
- 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/aws/configurator.py +11 -7
- dstack/_internal/core/backends/azure/configurator.py +11 -7
- dstack/_internal/core/backends/base/compute.py +33 -5
- dstack/_internal/core/backends/base/configurator.py +25 -13
- dstack/_internal/core/backends/base/offers.py +2 -0
- dstack/_internal/core/backends/cloudrift/configurator.py +13 -7
- dstack/_internal/core/backends/configurators.py +15 -0
- dstack/_internal/core/backends/cudo/configurator.py +11 -7
- dstack/_internal/core/backends/datacrunch/compute.py +5 -1
- dstack/_internal/core/backends/datacrunch/configurator.py +13 -7
- 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/gcp/configurator.py +11 -7
- dstack/_internal/core/backends/hotaisle/api_client.py +25 -33
- dstack/_internal/core/backends/hotaisle/compute.py +1 -6
- dstack/_internal/core/backends/hotaisle/configurator.py +13 -7
- dstack/_internal/core/backends/kubernetes/configurator.py +13 -7
- dstack/_internal/core/backends/lambdalabs/configurator.py +11 -7
- dstack/_internal/core/backends/models.py +7 -0
- dstack/_internal/core/backends/nebius/compute.py +1 -8
- dstack/_internal/core/backends/nebius/configurator.py +11 -7
- dstack/_internal/core/backends/nebius/resources.py +21 -11
- dstack/_internal/core/backends/oci/compute.py +4 -5
- dstack/_internal/core/backends/oci/configurator.py +11 -7
- dstack/_internal/core/backends/runpod/configurator.py +11 -7
- dstack/_internal/core/backends/template/configurator.py.jinja +11 -7
- dstack/_internal/core/backends/tensordock/configurator.py +13 -7
- dstack/_internal/core/backends/vastai/configurator.py +11 -7
- dstack/_internal/core/backends/vultr/compute.py +1 -5
- dstack/_internal/core/backends/vultr/configurator.py +11 -4
- dstack/_internal/core/compatibility/fleets.py +5 -0
- dstack/_internal/core/compatibility/gpus.py +13 -0
- dstack/_internal/core/compatibility/runs.py +9 -1
- dstack/_internal/core/models/backends/base.py +5 -1
- dstack/_internal/core/models/common.py +3 -3
- dstack/_internal/core/models/configurations.py +191 -32
- dstack/_internal/core/models/files.py +1 -1
- dstack/_internal/core/models/fleets.py +80 -3
- dstack/_internal/core/models/profiles.py +41 -11
- dstack/_internal/core/models/resources.py +46 -42
- dstack/_internal/core/models/runs.py +28 -5
- dstack/_internal/core/services/configs/__init__.py +6 -3
- dstack/_internal/core/services/profiles.py +2 -2
- dstack/_internal/core/services/repos.py +86 -79
- dstack/_internal/core/services/ssh/ports.py +1 -1
- dstack/_internal/proxy/lib/deps.py +6 -2
- dstack/_internal/server/app.py +22 -17
- dstack/_internal/server/background/tasks/process_fleets.py +109 -13
- dstack/_internal/server/background/tasks/process_gateways.py +4 -1
- dstack/_internal/server/background/tasks/process_instances.py +22 -73
- dstack/_internal/server/background/tasks/process_probes.py +1 -1
- dstack/_internal/server/background/tasks/process_running_jobs.py +12 -4
- dstack/_internal/server/background/tasks/process_runs.py +3 -1
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +67 -44
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +2 -2
- dstack/_internal/server/background/tasks/process_volumes.py +1 -1
- dstack/_internal/server/db.py +8 -4
- dstack/_internal/server/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
- dstack/_internal/server/models.py +6 -2
- dstack/_internal/server/routers/gpus.py +1 -6
- dstack/_internal/server/schemas/runner.py +11 -0
- dstack/_internal/server/services/backends/__init__.py +14 -8
- dstack/_internal/server/services/backends/handlers.py +6 -1
- dstack/_internal/server/services/docker.py +5 -5
- dstack/_internal/server/services/fleets.py +37 -38
- dstack/_internal/server/services/gateways/__init__.py +2 -0
- dstack/_internal/server/services/gateways/client.py +5 -2
- dstack/_internal/server/services/gateways/connection.py +1 -1
- dstack/_internal/server/services/gpus.py +50 -49
- dstack/_internal/server/services/instances.py +44 -4
- dstack/_internal/server/services/jobs/__init__.py +15 -4
- dstack/_internal/server/services/jobs/configurators/base.py +53 -17
- dstack/_internal/server/services/jobs/configurators/dev.py +9 -4
- dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +6 -8
- dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +7 -9
- dstack/_internal/server/services/jobs/configurators/service.py +1 -3
- dstack/_internal/server/services/jobs/configurators/task.py +3 -3
- dstack/_internal/server/services/locking.py +5 -5
- dstack/_internal/server/services/logging.py +10 -2
- dstack/_internal/server/services/logs/__init__.py +8 -6
- dstack/_internal/server/services/logs/aws.py +330 -327
- dstack/_internal/server/services/logs/filelog.py +7 -6
- dstack/_internal/server/services/logs/gcp.py +141 -139
- dstack/_internal/server/services/plugins.py +1 -1
- dstack/_internal/server/services/projects.py +2 -5
- dstack/_internal/server/services/proxy/repo.py +5 -1
- dstack/_internal/server/services/requirements/__init__.py +0 -0
- dstack/_internal/server/services/requirements/combine.py +259 -0
- dstack/_internal/server/services/runner/client.py +7 -0
- dstack/_internal/server/services/runs.py +17 -1
- dstack/_internal/server/services/services/__init__.py +8 -2
- dstack/_internal/server/services/services/autoscalers.py +2 -0
- dstack/_internal/server/services/ssh.py +2 -1
- dstack/_internal/server/services/storage/__init__.py +5 -6
- dstack/_internal/server/services/storage/gcs.py +49 -49
- dstack/_internal/server/services/storage/s3.py +52 -52
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-d151b300fcac3933213d.js → main-4eecc75fbe64067eb1bc.js} +1146 -899
- dstack/_internal/server/statics/{main-d151b300fcac3933213d.js.map → main-4eecc75fbe64067eb1bc.js.map} +1 -1
- dstack/_internal/server/statics/{main-aec4762350e34d6fbff9.css → main-56191c63d516fd0041c4.css} +1 -1
- dstack/_internal/server/testing/common.py +7 -4
- dstack/_internal/server/utils/logging.py +3 -3
- dstack/_internal/server/utils/provisioning.py +3 -3
- dstack/_internal/utils/json_schema.py +3 -1
- dstack/_internal/utils/path.py +8 -1
- dstack/_internal/utils/ssh.py +7 -0
- dstack/_internal/utils/typing.py +14 -0
- dstack/api/_public/repos.py +62 -8
- dstack/api/_public/runs.py +19 -8
- dstack/api/server/__init__.py +17 -19
- dstack/api/server/_gpus.py +2 -1
- dstack/api/server/_group.py +4 -3
- dstack/api/server/_repos.py +20 -3
- dstack/plugins/builtin/rest_plugin/_plugin.py +1 -0
- dstack/version.py +1 -1
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/METADATA +2 -2
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/RECORD +160 -142
- dstack/api/huggingface/__init__.py +0 -73
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/WHEEL +0 -0
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.25rc1.dist-info → dstack-0.19.27.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -28,6 +28,7 @@ from dstack._internal.core.models.configurations import (
|
|
|
28
28
|
from dstack._internal.core.models.envs import Env
|
|
29
29
|
from dstack._internal.core.models.fleets import (
|
|
30
30
|
FleetConfiguration,
|
|
31
|
+
FleetNodesSpec,
|
|
31
32
|
FleetSpec,
|
|
32
33
|
FleetStatus,
|
|
33
34
|
InstanceGroupPlacement,
|
|
@@ -60,7 +61,7 @@ from dstack._internal.core.models.profiles import (
|
|
|
60
61
|
)
|
|
61
62
|
from dstack._internal.core.models.repos.base import RepoType
|
|
62
63
|
from dstack._internal.core.models.repos.local import LocalRunRepoData
|
|
63
|
-
from dstack._internal.core.models.resources import CPUSpec, Memory,
|
|
64
|
+
from dstack._internal.core.models.resources import CPUSpec, Memory, ResourcesSpec
|
|
64
65
|
from dstack._internal.core.models.runs import (
|
|
65
66
|
JobProvisioningData,
|
|
66
67
|
JobRuntimeData,
|
|
@@ -271,7 +272,7 @@ def get_run_spec(
|
|
|
271
272
|
repo_id=repo_id,
|
|
272
273
|
repo_data=LocalRunRepoData(repo_dir="/"),
|
|
273
274
|
repo_code_hash=None,
|
|
274
|
-
working_dir=
|
|
275
|
+
working_dir=None,
|
|
275
276
|
configuration_path=configuration_path,
|
|
276
277
|
configuration=configuration or DevEnvironmentConfiguration(ide="vscode"),
|
|
277
278
|
profile=profile,
|
|
@@ -284,6 +285,7 @@ async def create_run(
|
|
|
284
285
|
project: ProjectModel,
|
|
285
286
|
repo: RepoModel,
|
|
286
287
|
user: UserModel,
|
|
288
|
+
fleet: Optional[FleetModel] = None,
|
|
287
289
|
run_name: str = "test-run",
|
|
288
290
|
status: RunStatus = RunStatus.SUBMITTED,
|
|
289
291
|
termination_reason: Optional[RunTerminationReason] = None,
|
|
@@ -309,6 +311,7 @@ async def create_run(
|
|
|
309
311
|
project_id=project.id,
|
|
310
312
|
repo_id=repo.id,
|
|
311
313
|
user_id=user.id,
|
|
314
|
+
fleet_id=fleet.id if fleet else None,
|
|
312
315
|
submitted_at=submitted_at,
|
|
313
316
|
run_name=run_name,
|
|
314
317
|
status=status,
|
|
@@ -573,13 +576,13 @@ def get_fleet_spec(conf: Optional[FleetConfiguration] = None) -> FleetSpec:
|
|
|
573
576
|
return FleetSpec(
|
|
574
577
|
configuration=conf,
|
|
575
578
|
configuration_path="fleet.dstack.yml",
|
|
576
|
-
profile=Profile(
|
|
579
|
+
profile=Profile(),
|
|
577
580
|
)
|
|
578
581
|
|
|
579
582
|
|
|
580
583
|
def get_fleet_configuration(
|
|
581
584
|
name: str = "test-fleet",
|
|
582
|
-
nodes:
|
|
585
|
+
nodes: FleetNodesSpec = FleetNodesSpec(min=1, target=1, max=1),
|
|
583
586
|
placement: Optional[InstanceGroupPlacement] = None,
|
|
584
587
|
) -> FleetConfiguration:
|
|
585
588
|
return FleetConfiguration(
|
|
@@ -31,15 +31,15 @@ def configure_logging():
|
|
|
31
31
|
rename_fields={"name": "logger", "asctime": "timestamp", "levelname": "level"},
|
|
32
32
|
),
|
|
33
33
|
}
|
|
34
|
-
handlers = {
|
|
34
|
+
handlers: dict[str, logging.Handler] = {
|
|
35
35
|
"rich": DstackRichHandler(console=console),
|
|
36
36
|
"standard": logging.StreamHandler(stream=sys.stdout),
|
|
37
37
|
"json": logging.StreamHandler(stream=sys.stdout),
|
|
38
38
|
}
|
|
39
39
|
if settings.LOG_FORMAT not in formatters:
|
|
40
40
|
raise ValueError(f"Invalid settings.LOG_FORMAT: {settings.LOG_FORMAT}")
|
|
41
|
-
formatter = formatters
|
|
42
|
-
handler = handlers
|
|
41
|
+
formatter = formatters[settings.LOG_FORMAT]
|
|
42
|
+
handler = handlers[settings.LOG_FORMAT]
|
|
43
43
|
handler.setFormatter(formatter)
|
|
44
44
|
handler.addFilter(AsyncioCancelledErrorFilter())
|
|
45
45
|
root_logger = logging.getLogger(None)
|
|
@@ -312,10 +312,10 @@ def get_paramiko_connection(
|
|
|
312
312
|
with proxy_ctx as proxy_client, paramiko.SSHClient() as client:
|
|
313
313
|
proxy_channel: Optional[paramiko.Channel] = None
|
|
314
314
|
if proxy_client is not None:
|
|
315
|
+
transport = proxy_client.get_transport()
|
|
316
|
+
assert transport is not None
|
|
315
317
|
try:
|
|
316
|
-
proxy_channel =
|
|
317
|
-
"direct-tcpip", (host, port), ("", 0)
|
|
318
|
-
)
|
|
318
|
+
proxy_channel = transport.open_channel("direct-tcpip", (host, port), ("", 0))
|
|
319
319
|
except (paramiko.SSHException, OSError) as e:
|
|
320
320
|
raise ProvisioningError(f"Proxy channel failed: {e}") from e
|
|
321
321
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
@@ -3,7 +3,9 @@ def add_extra_schema_types(schema_property: dict, extra_types: list[dict]):
|
|
|
3
3
|
refs = [schema_property.pop("allOf")[0]]
|
|
4
4
|
elif "anyOf" in schema_property:
|
|
5
5
|
refs = schema_property.pop("anyOf")
|
|
6
|
-
|
|
6
|
+
elif "type" in schema_property:
|
|
7
7
|
refs = [{"type": schema_property.pop("type")}]
|
|
8
|
+
else:
|
|
9
|
+
refs = [{"$ref": schema_property.pop("$ref")}]
|
|
8
10
|
refs.extend(extra_types)
|
|
9
11
|
schema_property["anyOf"] = refs
|
dstack/_internal/utils/path.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from pathlib import Path, PurePath
|
|
3
|
+
from pathlib import Path, PurePath, PurePosixPath
|
|
4
4
|
from typing import Union
|
|
5
5
|
|
|
6
6
|
PathLike = Union[str, os.PathLike]
|
|
@@ -48,3 +48,10 @@ def resolve_relative_path(path: PathLike) -> PurePath:
|
|
|
48
48
|
return normalize_path(path)
|
|
49
49
|
except ValueError:
|
|
50
50
|
raise ValueError("Path is outside of the repo")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_absolute_posix_path(path: PathLike) -> bool:
|
|
54
|
+
# Passing Windows path leads to undefined behavior
|
|
55
|
+
if str(path).startswith("~"):
|
|
56
|
+
return True
|
|
57
|
+
return PurePosixPath(path).is_absolute()
|
dstack/_internal/utils/ssh.py
CHANGED
|
@@ -50,6 +50,13 @@ def make_ssh_command_for_git(identity_file: PathLike) -> str:
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def make_git_env(*, identity_file: Optional[PathLike] = None) -> dict[str, str]:
|
|
54
|
+
env: dict[str, str] = {"GIT_TERMINAL_PROMPT": "0"}
|
|
55
|
+
if identity_file is not None:
|
|
56
|
+
env["GIT_SSH_COMMAND"] = make_ssh_command_for_git(identity_file)
|
|
57
|
+
return env
|
|
58
|
+
|
|
59
|
+
|
|
53
60
|
def try_ssh_key_passphrase(identity_file: PathLike, passphrase: str = "") -> bool:
|
|
54
61
|
ssh_keygen = find_ssh_util("ssh-keygen")
|
|
55
62
|
if ssh_keygen is None:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Any, Protocol, TypeVar, Union
|
|
2
|
+
|
|
3
|
+
_T_contra = TypeVar("_T_contra", contravariant=True)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SupportsDunderLT(Protocol[_T_contra]):
|
|
7
|
+
def __lt__(self, other: _T_contra, /) -> bool: ...
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SupportsDunderGT(Protocol[_T_contra]):
|
|
11
|
+
def __gt__(self, other: _T_contra, /) -> bool: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SupportsRichComparison = Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]
|
dstack/api/_public/repos.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Optional, Union
|
|
2
|
+
from typing import Literal, Optional, Union, overload
|
|
3
3
|
|
|
4
4
|
from git import InvalidGitRepositoryError
|
|
5
5
|
|
|
6
6
|
from dstack._internal.core.errors import ConfigurationError, ResourceNotExistsError
|
|
7
|
-
from dstack._internal.core.models.repos import
|
|
7
|
+
from dstack._internal.core.models.repos import (
|
|
8
|
+
LocalRepo,
|
|
9
|
+
RemoteRepo,
|
|
10
|
+
RemoteRepoCreds,
|
|
11
|
+
RepoHead,
|
|
12
|
+
RepoHeadWithCreds,
|
|
13
|
+
)
|
|
8
14
|
from dstack._internal.core.models.repos.base import Repo, RepoType
|
|
9
15
|
from dstack._internal.core.services.configs import ConfigManager
|
|
10
16
|
from dstack._internal.core.services.repos import (
|
|
11
17
|
InvalidRepoCredentialsError,
|
|
12
|
-
|
|
18
|
+
get_repo_creds_and_default_branch,
|
|
13
19
|
load_repo,
|
|
14
20
|
)
|
|
15
21
|
from dstack._internal.utils.crypto import generate_rsa_key_pair
|
|
@@ -34,6 +40,7 @@ class RepoCollection:
|
|
|
34
40
|
repo: Repo,
|
|
35
41
|
git_identity_file: Optional[PathLike] = None,
|
|
36
42
|
oauth_token: Optional[str] = None,
|
|
43
|
+
creds: Optional[RemoteRepoCreds] = None,
|
|
37
44
|
):
|
|
38
45
|
"""
|
|
39
46
|
Initializes the repo and configures its credentials in the project.
|
|
@@ -65,11 +72,13 @@ class RepoCollection:
|
|
|
65
72
|
repo: The repo to initialize.
|
|
66
73
|
git_identity_file: The private SSH key path for accessing the remote repo.
|
|
67
74
|
oauth_token: The GitHub OAuth token to access the remote repo.
|
|
75
|
+
creds: Optional prepared repo credentials. If specified, both `git_identity_file`
|
|
76
|
+
and `oauth_token` are ignored.
|
|
68
77
|
"""
|
|
69
|
-
creds
|
|
70
|
-
|
|
78
|
+
if creds is None and isinstance(repo, RemoteRepo):
|
|
79
|
+
assert repo.repo_url is not None
|
|
71
80
|
try:
|
|
72
|
-
creds =
|
|
81
|
+
creds, _ = get_repo_creds_and_default_branch(
|
|
73
82
|
repo_url=repo.repo_url,
|
|
74
83
|
identity_file=git_identity_file,
|
|
75
84
|
oauth_token=oauth_token,
|
|
@@ -140,22 +149,67 @@ class RepoCollection:
|
|
|
140
149
|
def is_initialized(
|
|
141
150
|
self,
|
|
142
151
|
repo: Repo,
|
|
152
|
+
by_user: bool = False,
|
|
143
153
|
) -> bool:
|
|
144
154
|
"""
|
|
145
|
-
Checks if the
|
|
155
|
+
Checks if the repo is initialized in the project
|
|
146
156
|
|
|
147
157
|
Args:
|
|
148
158
|
repo: The repo to check.
|
|
159
|
+
by_user: Require the remote repo to be initialized by the user, that is, to have
|
|
160
|
+
the user's credentials. Ignored for other repo types.
|
|
149
161
|
|
|
150
162
|
Returns:
|
|
151
163
|
Whether the repo is initialized or not.
|
|
152
164
|
"""
|
|
165
|
+
if isinstance(repo, RemoteRepo) and by_user:
|
|
166
|
+
return self._is_initialized_by_user(repo)
|
|
153
167
|
try:
|
|
154
|
-
self._api_client.repos.get(self._project, repo.repo_id
|
|
168
|
+
self._api_client.repos.get(self._project, repo.repo_id)
|
|
155
169
|
return True
|
|
156
170
|
except ResourceNotExistsError:
|
|
157
171
|
return False
|
|
158
172
|
|
|
173
|
+
def _is_initialized_by_user(self, repo: RemoteRepo) -> bool:
|
|
174
|
+
try:
|
|
175
|
+
repo_head = self._api_client.repos.get_with_creds(self._project, repo.repo_id)
|
|
176
|
+
except ResourceNotExistsError:
|
|
177
|
+
return False
|
|
178
|
+
# This works because:
|
|
179
|
+
# - RepoCollection.init() always submits RemoteRepoCreds for remote repos, even if
|
|
180
|
+
# the repo is public
|
|
181
|
+
# - Server returns creds only if there is RepoCredsModel for the user (or legacy
|
|
182
|
+
# shared creds in RepoModel)
|
|
183
|
+
# TODO: add an API method with the same logic returning a bool value?
|
|
184
|
+
return repo_head.repo_creds is not None
|
|
185
|
+
|
|
186
|
+
@overload
|
|
187
|
+
def get(self, repo_id: str, *, with_creds: Literal[False] = False) -> Optional[RepoHead]: ...
|
|
188
|
+
|
|
189
|
+
@overload
|
|
190
|
+
def get(self, repo_id: str, *, with_creds: Literal[True]) -> Optional[RepoHeadWithCreds]: ...
|
|
191
|
+
|
|
192
|
+
def get(
|
|
193
|
+
self, repo_id: str, *, with_creds: bool = False
|
|
194
|
+
) -> Optional[Union[RepoHead, RepoHeadWithCreds]]:
|
|
195
|
+
"""
|
|
196
|
+
Returns the repo by `repo_id`
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
repo_id: The repo ID.
|
|
200
|
+
with_creds: include repo credentials in the response.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The repo or `None` if the repo is not found.
|
|
204
|
+
"""
|
|
205
|
+
method = self._api_client.repos.get
|
|
206
|
+
if with_creds:
|
|
207
|
+
method = self._api_client.repos.get_with_creds
|
|
208
|
+
try:
|
|
209
|
+
return method(self._project, repo_id)
|
|
210
|
+
except ResourceNotExistsError:
|
|
211
|
+
return None
|
|
212
|
+
|
|
159
213
|
|
|
160
214
|
def get_ssh_keypair(key_path: Optional[PathLike], dstack_key_path: Path) -> str:
|
|
161
215
|
"""Returns a path to the private key"""
|
dstack/api/_public/runs.py
CHANGED
|
@@ -23,7 +23,7 @@ from dstack._internal.core.models.configurations import (
|
|
|
23
23
|
PortMapping,
|
|
24
24
|
ServiceConfiguration,
|
|
25
25
|
)
|
|
26
|
-
from dstack._internal.core.models.files import FileArchiveMapping
|
|
26
|
+
from dstack._internal.core.models.files import FileArchiveMapping
|
|
27
27
|
from dstack._internal.core.models.profiles import (
|
|
28
28
|
CreationPolicy,
|
|
29
29
|
Profile,
|
|
@@ -433,21 +433,28 @@ class RunCollection:
|
|
|
433
433
|
repo: Optional[Repo] = None,
|
|
434
434
|
profile: Optional[Profile] = None,
|
|
435
435
|
configuration_path: Optional[str] = None,
|
|
436
|
+
repo_dir: Optional[str] = None,
|
|
436
437
|
) -> RunPlan:
|
|
437
438
|
"""
|
|
438
439
|
Get a run plan.
|
|
439
|
-
Use this method to see the run plan before applying the
|
|
440
|
+
Use this method to see the run plan before applying the configuration.
|
|
440
441
|
|
|
441
442
|
Args:
|
|
442
443
|
configuration (Union[Task, Service, DevEnvironment]): The run configuration.
|
|
443
444
|
repo (Union[LocalRepo, RemoteRepo, VirtualRepo, None]):
|
|
444
445
|
The repo to use for the run. Pass `None` if repo is not needed.
|
|
445
446
|
profile: The profile to use for the run.
|
|
446
|
-
configuration_path: The path to the configuration file. Omit if the configuration
|
|
447
|
+
configuration_path: The path to the configuration file. Omit if the configuration
|
|
448
|
+
is not loaded from a file.
|
|
449
|
+
repo_dir: The path of the cloned repo inside the run container. If not set,
|
|
450
|
+
defaults first to the `repos[0].path` property of the configuration (for remote
|
|
451
|
+
repos only), then to `/workflow`.
|
|
447
452
|
|
|
448
453
|
Returns:
|
|
449
454
|
Run plan.
|
|
450
455
|
"""
|
|
456
|
+
# XXX: not using the LEGACY_REPO_DIR const in the docstring above, as the docs generator,
|
|
457
|
+
# apparently, doesn't support f-strings (f"""...""").
|
|
451
458
|
if repo is None:
|
|
452
459
|
repo = VirtualRepo()
|
|
453
460
|
repo_code_hash = None
|
|
@@ -455,11 +462,17 @@ class RunCollection:
|
|
|
455
462
|
with _prepare_code_file(repo) as (_, repo_code_hash):
|
|
456
463
|
pass
|
|
457
464
|
|
|
465
|
+
if repo_dir is None and configuration.repos:
|
|
466
|
+
repo_dir = configuration.repos[0].path
|
|
467
|
+
|
|
458
468
|
run_spec = RunSpec(
|
|
459
469
|
run_name=configuration.name,
|
|
460
470
|
repo_id=repo.repo_id,
|
|
461
471
|
repo_data=repo.run_repo_data,
|
|
462
472
|
repo_code_hash=repo_code_hash,
|
|
473
|
+
repo_dir=repo_dir,
|
|
474
|
+
# Server doesn't use this field since 0.19.27, but we still send it for compatibility
|
|
475
|
+
# with older servers
|
|
463
476
|
working_dir=configuration.working_dir,
|
|
464
477
|
configuration_path=configuration_path,
|
|
465
478
|
configuration=configuration,
|
|
@@ -499,7 +512,6 @@ class RunCollection:
|
|
|
499
512
|
|
|
500
513
|
self._validate_configuration_files(configuration, run_spec.configuration_path)
|
|
501
514
|
for file_mapping in configuration.files:
|
|
502
|
-
assert isinstance(file_mapping, FilePathMapping)
|
|
503
515
|
with tempfile.TemporaryFile("w+b") as fp:
|
|
504
516
|
try:
|
|
505
517
|
archive_hash = create_file_archive(file_mapping.local_path, fp)
|
|
@@ -691,11 +703,11 @@ class RunCollection:
|
|
|
691
703
|
spot_policy=spot_policy,
|
|
692
704
|
retry=None,
|
|
693
705
|
utilization_policy=utilization_policy,
|
|
694
|
-
max_duration=max_duration,
|
|
695
|
-
stop_duration=stop_duration,
|
|
706
|
+
max_duration=max_duration, # type: ignore[assignment]
|
|
707
|
+
stop_duration=stop_duration, # type: ignore[assignment]
|
|
696
708
|
max_price=max_price,
|
|
697
709
|
creation_policy=creation_policy,
|
|
698
|
-
idle_duration=idle_duration,
|
|
710
|
+
idle_duration=idle_duration, # type: ignore[assignment]
|
|
699
711
|
)
|
|
700
712
|
run_spec = RunSpec(
|
|
701
713
|
run_name=run_name,
|
|
@@ -812,7 +824,6 @@ class RunCollection:
|
|
|
812
824
|
if configuration_path is not None:
|
|
813
825
|
base_dir = Path(configuration_path).expanduser().resolve().parent
|
|
814
826
|
for file_mapping in configuration.files:
|
|
815
|
-
assert isinstance(file_mapping, FilePathMapping)
|
|
816
827
|
path = Path(file_mapping.local_path).expanduser()
|
|
817
828
|
if not path.is_absolute():
|
|
818
829
|
if base_dir is None:
|
dstack/api/server/__init__.py
CHANGED
|
@@ -27,9 +27,6 @@ from dstack.api.server._secrets import SecretsAPIClient
|
|
|
27
27
|
from dstack.api.server._users import UsersAPIClient
|
|
28
28
|
from dstack.api.server._volumes import VolumesAPIClient
|
|
29
29
|
|
|
30
|
-
logger = get_logger(__name__)
|
|
31
|
-
|
|
32
|
-
|
|
33
30
|
_MAX_RETRIES = 3
|
|
34
31
|
_RETRY_INTERVAL = 1
|
|
35
32
|
|
|
@@ -66,6 +63,7 @@ class APIClient:
|
|
|
66
63
|
client_api_version = os.getenv("DSTACK_CLIENT_API_VERSION", version.__version__)
|
|
67
64
|
if client_api_version is not None:
|
|
68
65
|
self._s.headers.update({"X-API-VERSION": client_api_version})
|
|
66
|
+
self._logger = get_logger(__name__)
|
|
69
67
|
|
|
70
68
|
@property
|
|
71
69
|
def base_url(self) -> str:
|
|
@@ -73,55 +71,55 @@ class APIClient:
|
|
|
73
71
|
|
|
74
72
|
@property
|
|
75
73
|
def users(self) -> UsersAPIClient:
|
|
76
|
-
return UsersAPIClient(self._request)
|
|
74
|
+
return UsersAPIClient(self._request, self._logger)
|
|
77
75
|
|
|
78
76
|
@property
|
|
79
77
|
def projects(self) -> ProjectsAPIClient:
|
|
80
|
-
return ProjectsAPIClient(self._request)
|
|
78
|
+
return ProjectsAPIClient(self._request, self._logger)
|
|
81
79
|
|
|
82
80
|
@property
|
|
83
81
|
def backends(self) -> BackendsAPIClient:
|
|
84
|
-
return BackendsAPIClient(self._request)
|
|
82
|
+
return BackendsAPIClient(self._request, self._logger)
|
|
85
83
|
|
|
86
84
|
@property
|
|
87
85
|
def fleets(self) -> FleetsAPIClient:
|
|
88
|
-
return FleetsAPIClient(self._request)
|
|
86
|
+
return FleetsAPIClient(self._request, self._logger)
|
|
89
87
|
|
|
90
88
|
@property
|
|
91
89
|
def repos(self) -> ReposAPIClient:
|
|
92
|
-
return ReposAPIClient(self._request)
|
|
90
|
+
return ReposAPIClient(self._request, self._logger)
|
|
93
91
|
|
|
94
92
|
@property
|
|
95
93
|
def runs(self) -> RunsAPIClient:
|
|
96
|
-
return RunsAPIClient(self._request)
|
|
94
|
+
return RunsAPIClient(self._request, self._logger)
|
|
97
95
|
|
|
98
96
|
@property
|
|
99
97
|
def gpus(self) -> GpusAPIClient:
|
|
100
|
-
return GpusAPIClient(self._request)
|
|
98
|
+
return GpusAPIClient(self._request, self._logger)
|
|
101
99
|
|
|
102
100
|
@property
|
|
103
101
|
def metrics(self) -> MetricsAPIClient:
|
|
104
|
-
return MetricsAPIClient(self._request)
|
|
102
|
+
return MetricsAPIClient(self._request, self._logger)
|
|
105
103
|
|
|
106
104
|
@property
|
|
107
105
|
def logs(self) -> LogsAPIClient:
|
|
108
|
-
return LogsAPIClient(self._request)
|
|
106
|
+
return LogsAPIClient(self._request, self._logger)
|
|
109
107
|
|
|
110
108
|
@property
|
|
111
109
|
def secrets(self) -> SecretsAPIClient:
|
|
112
|
-
return SecretsAPIClient(self._request)
|
|
110
|
+
return SecretsAPIClient(self._request, self._logger)
|
|
113
111
|
|
|
114
112
|
@property
|
|
115
113
|
def gateways(self) -> GatewaysAPIClient:
|
|
116
|
-
return GatewaysAPIClient(self._request)
|
|
114
|
+
return GatewaysAPIClient(self._request, self._logger)
|
|
117
115
|
|
|
118
116
|
@property
|
|
119
117
|
def volumes(self) -> VolumesAPIClient:
|
|
120
|
-
return VolumesAPIClient(self._request)
|
|
118
|
+
return VolumesAPIClient(self._request, self._logger)
|
|
121
119
|
|
|
122
120
|
@property
|
|
123
121
|
def files(self) -> FilesAPIClient:
|
|
124
|
-
return FilesAPIClient(self._request)
|
|
122
|
+
return FilesAPIClient(self._request, self._logger)
|
|
125
123
|
|
|
126
124
|
def _request(
|
|
127
125
|
self,
|
|
@@ -136,20 +134,20 @@ class APIClient:
|
|
|
136
134
|
kwargs.setdefault("headers", {})["Content-Type"] = "application/json"
|
|
137
135
|
kwargs["data"] = body
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
self._logger.debug("POST /%s", path)
|
|
140
138
|
for _ in range(_MAX_RETRIES):
|
|
141
139
|
try:
|
|
142
140
|
# TODO: set adequate timeout here or everywhere the method is used
|
|
143
141
|
resp = self._s.request(method, f"{self._base_url}/{path}", **kwargs)
|
|
144
142
|
break
|
|
145
143
|
except requests.exceptions.ConnectionError as e:
|
|
146
|
-
|
|
144
|
+
self._logger.debug("Could not connect to server: %s", e)
|
|
147
145
|
time.sleep(_RETRY_INTERVAL)
|
|
148
146
|
else:
|
|
149
147
|
raise ClientError(f"Failed to connect to dstack server {self._base_url}")
|
|
150
148
|
|
|
151
149
|
if 400 <= resp.status_code < 600:
|
|
152
|
-
|
|
150
|
+
self._logger.debug(
|
|
153
151
|
"Error requesting %s. Status: %s. Headers: %s. Body: %s",
|
|
154
152
|
resp.request.url,
|
|
155
153
|
resp.status_code,
|
dstack/api/server/_gpus.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import List, Optional
|
|
|
2
2
|
|
|
3
3
|
from pydantic import parse_obj_as
|
|
4
4
|
|
|
5
|
+
from dstack._internal.core.compatibility.gpus import get_list_gpus_excludes
|
|
5
6
|
from dstack._internal.core.models.runs import RunSpec
|
|
6
7
|
from dstack._internal.server.schemas.gpus import GpuGroup, ListGpusRequest, ListGpusResponse
|
|
7
8
|
from dstack.api.server._group import APIClientGroup
|
|
@@ -17,6 +18,6 @@ class GpusAPIClient(APIClientGroup):
|
|
|
17
18
|
body = ListGpusRequest(run_spec=run_spec, group_by=group_by)
|
|
18
19
|
resp = self._request(
|
|
19
20
|
f"/api/project/{project_name}/gpus/list",
|
|
20
|
-
body=body.json(),
|
|
21
|
+
body=body.json(exclude=get_list_gpus_excludes(body)),
|
|
21
22
|
)
|
|
22
23
|
return parse_obj_as(ListGpusResponse, resp.json()).gpus
|
dstack/api/server/_group.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from logging import Logger
|
|
1
2
|
from typing import Optional
|
|
2
3
|
|
|
3
4
|
import requests
|
|
@@ -12,10 +13,10 @@ class APIRequest(Protocol):
|
|
|
12
13
|
raise_for_status: bool = True,
|
|
13
14
|
method: str = "POST",
|
|
14
15
|
**kwargs,
|
|
15
|
-
) -> requests.Response:
|
|
16
|
-
pass
|
|
16
|
+
) -> requests.Response: ...
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class APIClientGroup:
|
|
20
|
-
def __init__(self, _request: APIRequest):
|
|
20
|
+
def __init__(self, _request: APIRequest, _logger: Logger):
|
|
21
21
|
self._request = _request
|
|
22
|
+
self._logger = _logger
|
dstack/api/server/_repos.py
CHANGED
|
@@ -2,7 +2,12 @@ from typing import BinaryIO, List, Optional
|
|
|
2
2
|
|
|
3
3
|
from pydantic import parse_obj_as
|
|
4
4
|
|
|
5
|
-
from dstack._internal.core.models.repos import
|
|
5
|
+
from dstack._internal.core.models.repos import (
|
|
6
|
+
AnyRepoInfo,
|
|
7
|
+
RemoteRepoCreds,
|
|
8
|
+
RepoHead,
|
|
9
|
+
RepoHeadWithCreds,
|
|
10
|
+
)
|
|
6
11
|
from dstack._internal.server.schemas.repos import (
|
|
7
12
|
DeleteReposRequest,
|
|
8
13
|
GetRepoRequest,
|
|
@@ -16,11 +21,23 @@ class ReposAPIClient(APIClientGroup):
|
|
|
16
21
|
resp = self._request(f"/api/project/{project_name}/repos/list")
|
|
17
22
|
return parse_obj_as(List[RepoHead.__response__], resp.json())
|
|
18
23
|
|
|
19
|
-
def get(
|
|
20
|
-
|
|
24
|
+
def get(
|
|
25
|
+
self, project_name: str, repo_id: str, include_creds: Optional[bool] = None
|
|
26
|
+
) -> RepoHead:
|
|
27
|
+
if include_creds is not None:
|
|
28
|
+
self._logger.warning(
|
|
29
|
+
"`include_creds` argument is deprecated and has no effect, `get()` always returns"
|
|
30
|
+
" the repo without creds. Use `get_with_creds()` to get the repo with creds"
|
|
31
|
+
)
|
|
32
|
+
body = GetRepoRequest(repo_id=repo_id, include_creds=False)
|
|
21
33
|
resp = self._request(f"/api/project/{project_name}/repos/get", body=body.json())
|
|
22
34
|
return parse_obj_as(RepoHead.__response__, resp.json())
|
|
23
35
|
|
|
36
|
+
def get_with_creds(self, project_name: str, repo_id: str) -> RepoHeadWithCreds:
|
|
37
|
+
body = GetRepoRequest(repo_id=repo_id, include_creds=True)
|
|
38
|
+
resp = self._request(f"/api/project/{project_name}/repos/get", body=body.json())
|
|
39
|
+
return parse_obj_as(RepoHeadWithCreds.__response__, resp.json())
|
|
40
|
+
|
|
24
41
|
def init(
|
|
25
42
|
self,
|
|
26
43
|
project_name: str,
|
|
@@ -86,6 +86,7 @@ class CustomApplyPolicy(ApplyPolicy):
|
|
|
86
86
|
spec: ApplySpec,
|
|
87
87
|
excludes: Optional[Dict] = None,
|
|
88
88
|
) -> ApplySpec:
|
|
89
|
+
spec_json = None
|
|
89
90
|
try:
|
|
90
91
|
spec_request = request_cls(user=user, project=project, spec=spec)
|
|
91
92
|
spec_json = self._call_plugin_service(spec_request, endpoint, excludes)
|
dstack/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dstack
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.27
|
|
4
4
|
Summary: dstack is an open-source orchestration engine for running AI workloads on any cloud or on-premises.
|
|
5
5
|
Project-URL: Homepage, https://dstack.ai
|
|
6
6
|
Project-URL: Source, https://github.com/dstackai/dstack
|
|
@@ -22,7 +22,7 @@ Requires-Dist: cryptography
|
|
|
22
22
|
Requires-Dist: cursor
|
|
23
23
|
Requires-Dist: filelock
|
|
24
24
|
Requires-Dist: gitpython
|
|
25
|
-
Requires-Dist: gpuhunt==0.1.
|
|
25
|
+
Requires-Dist: gpuhunt==0.1.8
|
|
26
26
|
Requires-Dist: ignore-python>=0.2.0
|
|
27
27
|
Requires-Dist: jsonschema
|
|
28
28
|
Requires-Dist: orjson
|