dstack 0.19.32__py3-none-any.whl → 0.19.34__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/offer.py +1 -1
- dstack/_internal/cli/services/configurators/run.py +1 -5
- dstack/_internal/core/backends/aws/compute.py +8 -5
- dstack/_internal/core/backends/azure/compute.py +9 -6
- dstack/_internal/core/backends/base/compute.py +40 -17
- dstack/_internal/core/backends/base/offers.py +7 -1
- dstack/_internal/core/backends/datacrunch/compute.py +9 -6
- dstack/_internal/core/backends/gcp/compute.py +151 -6
- dstack/_internal/core/backends/gcp/models.py +10 -0
- dstack/_internal/core/backends/gcp/resources.py +87 -5
- dstack/_internal/core/backends/hotaisle/compute.py +11 -1
- dstack/_internal/core/backends/kubernetes/compute.py +161 -83
- dstack/_internal/core/backends/kubernetes/models.py +4 -2
- dstack/_internal/core/backends/nebius/compute.py +9 -6
- dstack/_internal/core/backends/oci/compute.py +9 -6
- dstack/_internal/core/backends/runpod/compute.py +14 -7
- dstack/_internal/core/backends/vastai/compute.py +3 -1
- dstack/_internal/core/backends/vastai/configurator.py +0 -1
- dstack/_internal/core/compatibility/runs.py +25 -4
- dstack/_internal/core/models/fleets.py +1 -1
- dstack/_internal/core/models/instances.py +2 -1
- dstack/_internal/core/models/profiles.py +1 -1
- dstack/_internal/core/models/runs.py +4 -2
- dstack/_internal/core/models/users.py +10 -0
- dstack/_internal/core/services/configs/__init__.py +1 -0
- dstack/_internal/core/services/ssh/key_manager.py +56 -0
- dstack/_internal/server/background/tasks/process_instances.py +5 -1
- dstack/_internal/server/background/tasks/process_running_jobs.py +1 -0
- dstack/_internal/server/migrations/versions/ff1d94f65b08_user_ssh_key.py +34 -0
- dstack/_internal/server/models.py +6 -0
- dstack/_internal/server/routers/metrics.py +6 -2
- dstack/_internal/server/routers/runs.py +5 -1
- dstack/_internal/server/routers/users.py +21 -2
- dstack/_internal/server/services/jobs/__init__.py +18 -9
- dstack/_internal/server/services/offers.py +1 -0
- dstack/_internal/server/services/runs.py +13 -4
- dstack/_internal/server/services/users.py +35 -2
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/main-720ce3a11140daa480cc.css +3 -0
- dstack/_internal/server/statics/{main-c51afa7f243e24d3e446.js → main-e79754c136f1d8e4e7e6.js} +12632 -8039
- dstack/_internal/server/statics/{main-c51afa7f243e24d3e446.js.map → main-e79754c136f1d8e4e7e6.js.map} +1 -1
- dstack/_internal/server/testing/common.py +4 -0
- dstack/api/_public/__init__.py +8 -11
- dstack/api/_public/repos.py +0 -21
- dstack/api/_public/runs.py +61 -9
- dstack/api/server/__init__.py +4 -0
- dstack/api/server/_users.py +17 -2
- dstack/version.py +2 -2
- {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/METADATA +2 -2
- {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/RECORD +53 -51
- dstack/_internal/server/statics/main-56191fbfe77f49b251de.css +0 -3
- {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/WHEEL +0 -0
- {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -126,6 +126,8 @@ async def create_user(
|
|
|
126
126
|
global_role: GlobalRole = GlobalRole.ADMIN,
|
|
127
127
|
token: Optional[str] = None,
|
|
128
128
|
email: Optional[str] = None,
|
|
129
|
+
ssh_public_key: Optional[str] = None,
|
|
130
|
+
ssh_private_key: Optional[str] = None,
|
|
129
131
|
active: bool = True,
|
|
130
132
|
) -> UserModel:
|
|
131
133
|
if token is None:
|
|
@@ -137,6 +139,8 @@ async def create_user(
|
|
|
137
139
|
token=DecryptedString(plaintext=token),
|
|
138
140
|
token_hash=get_token_hash(token),
|
|
139
141
|
email=email,
|
|
142
|
+
ssh_public_key=ssh_public_key,
|
|
143
|
+
ssh_private_key=ssh_private_key,
|
|
140
144
|
active=active,
|
|
141
145
|
)
|
|
142
146
|
session.add(user)
|
dstack/api/_public/__init__.py
CHANGED
|
@@ -2,11 +2,10 @@ from typing import Optional
|
|
|
2
2
|
|
|
3
3
|
import dstack._internal.core.services.api_client as api_client_service
|
|
4
4
|
from dstack._internal.core.errors import ConfigurationError
|
|
5
|
-
from dstack._internal.core.services.configs import ConfigManager
|
|
6
5
|
from dstack._internal.utils.logging import get_logger
|
|
7
|
-
from dstack._internal.utils.path import PathLike
|
|
6
|
+
from dstack._internal.utils.path import PathLike as PathLike
|
|
8
7
|
from dstack.api._public.backends import BackendCollection
|
|
9
|
-
from dstack.api._public.repos import RepoCollection
|
|
8
|
+
from dstack.api._public.repos import RepoCollection
|
|
10
9
|
from dstack.api._public.runs import RunCollection
|
|
11
10
|
from dstack.api.server import APIClient
|
|
12
11
|
|
|
@@ -35,24 +34,24 @@ class Client:
|
|
|
35
34
|
# Args:
|
|
36
35
|
# api_client: low-level server API client
|
|
37
36
|
# project_name: project name used for runs
|
|
38
|
-
# ssh_identity_file:
|
|
37
|
+
# ssh_identity_file: deprecated and will be removed in 0.19.40
|
|
39
38
|
# """
|
|
40
39
|
self._client = api_client
|
|
41
40
|
self._project = project_name
|
|
42
41
|
self._repos = RepoCollection(api_client, project_name)
|
|
43
42
|
self._backends = BackendCollection(api_client, project_name)
|
|
44
43
|
self._runs = RunCollection(api_client, project_name, self)
|
|
45
|
-
if ssh_identity_file:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if ssh_identity_file is not None:
|
|
45
|
+
logger.warning(
|
|
46
|
+
"[code]ssh_identity_file[/code] in [code]Client[/code] is deprecated and ignored; will be removed"
|
|
47
|
+
" since 0.19.40"
|
|
48
|
+
)
|
|
49
49
|
|
|
50
50
|
@staticmethod
|
|
51
51
|
def from_config(
|
|
52
52
|
project_name: Optional[str] = None,
|
|
53
53
|
server_url: Optional[str] = None,
|
|
54
54
|
user_token: Optional[str] = None,
|
|
55
|
-
ssh_identity_file: Optional[PathLike] = None,
|
|
56
55
|
) -> "Client":
|
|
57
56
|
"""
|
|
58
57
|
Creates a Client using the default configuration from `~/.dstack/config.yml` if it exists.
|
|
@@ -61,7 +60,6 @@ class Client:
|
|
|
61
60
|
project_name: The name of the project. required if `server_url` and `user_token` are specified.
|
|
62
61
|
server_url: The dstack server URL (e.g. `http://localhost:3000/` or `https://sky.dstack.ai`).
|
|
63
62
|
user_token: The dstack user token.
|
|
64
|
-
ssh_identity_file: The private SSH key path for SSH tunneling.
|
|
65
63
|
|
|
66
64
|
Returns:
|
|
67
65
|
A client instance.
|
|
@@ -75,7 +73,6 @@ class Client:
|
|
|
75
73
|
return Client(
|
|
76
74
|
api_client=api_client,
|
|
77
75
|
project_name=project_name,
|
|
78
|
-
ssh_identity_file=ssh_identity_file,
|
|
79
76
|
)
|
|
80
77
|
|
|
81
78
|
@property
|
dstack/api/_public/repos.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
1
|
from typing import Literal, Optional, Union, overload
|
|
3
2
|
|
|
4
3
|
from git import InvalidGitRepositoryError
|
|
@@ -18,7 +17,6 @@ from dstack._internal.core.services.repos import (
|
|
|
18
17
|
get_repo_creds_and_default_branch,
|
|
19
18
|
load_repo,
|
|
20
19
|
)
|
|
21
|
-
from dstack._internal.utils.crypto import generate_rsa_key_pair
|
|
22
20
|
from dstack._internal.utils.logging import get_logger
|
|
23
21
|
from dstack._internal.utils.path import PathLike
|
|
24
22
|
from dstack.api.server import APIClient
|
|
@@ -209,22 +207,3 @@ class RepoCollection:
|
|
|
209
207
|
return method(self._project, repo_id)
|
|
210
208
|
except ResourceNotExistsError:
|
|
211
209
|
return None
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def get_ssh_keypair(key_path: Optional[PathLike], dstack_key_path: Path) -> str:
|
|
215
|
-
"""Returns a path to the private key"""
|
|
216
|
-
if key_path is not None:
|
|
217
|
-
key_path = Path(key_path).expanduser().resolve()
|
|
218
|
-
pub_key = (
|
|
219
|
-
key_path
|
|
220
|
-
if key_path.suffix == ".pub"
|
|
221
|
-
else key_path.with_suffix(key_path.suffix + ".pub")
|
|
222
|
-
)
|
|
223
|
-
private_key = pub_key.with_suffix("")
|
|
224
|
-
if pub_key.exists() and private_key.exists():
|
|
225
|
-
return str(private_key)
|
|
226
|
-
raise ConfigurationError(f"Make sure valid keypair exists: {private_key}(.pub)")
|
|
227
|
-
|
|
228
|
-
if not dstack_key_path.exists():
|
|
229
|
-
generate_rsa_key_pair(private_key_path=dstack_key_path)
|
|
230
|
-
return str(dstack_key_path)
|
dstack/api/_public/runs.py
CHANGED
|
@@ -45,11 +45,14 @@ from dstack._internal.core.models.runs import (
|
|
|
45
45
|
get_service_port,
|
|
46
46
|
)
|
|
47
47
|
from dstack._internal.core.models.runs import Run as RunModel
|
|
48
|
+
from dstack._internal.core.services.configs import ConfigManager
|
|
48
49
|
from dstack._internal.core.services.logs import URLReplacer
|
|
49
50
|
from dstack._internal.core.services.ssh.attach import SSHAttach
|
|
51
|
+
from dstack._internal.core.services.ssh.key_manager import UserSSHKeyManager
|
|
50
52
|
from dstack._internal.core.services.ssh.ports import PortsLock
|
|
51
53
|
from dstack._internal.server.schemas.logs import PollLogsRequest
|
|
52
54
|
from dstack._internal.utils.common import get_or_error, make_proxy_url
|
|
55
|
+
from dstack._internal.utils.crypto import generate_rsa_key_pair
|
|
53
56
|
from dstack._internal.utils.files import create_file_archive
|
|
54
57
|
from dstack._internal.utils.logging import get_logger
|
|
55
58
|
from dstack._internal.utils.path import PathLike, path_in_dir
|
|
@@ -72,16 +75,20 @@ class Run(ABC):
|
|
|
72
75
|
self,
|
|
73
76
|
api_client: APIClient,
|
|
74
77
|
project: str,
|
|
75
|
-
ssh_identity_file: Optional[PathLike],
|
|
76
78
|
run: RunModel,
|
|
77
79
|
ports_lock: Optional[PortsLock] = None,
|
|
80
|
+
ssh_identity_file: Optional[PathLike] = None,
|
|
78
81
|
):
|
|
79
82
|
self._api_client = api_client
|
|
80
83
|
self._project = project
|
|
81
|
-
self._ssh_identity_file = ssh_identity_file
|
|
82
84
|
self._run = run
|
|
83
85
|
self._ports_lock: Optional[PortsLock] = ports_lock
|
|
84
86
|
self._ssh_attach: Optional[SSHAttach] = None
|
|
87
|
+
if ssh_identity_file is not None:
|
|
88
|
+
logger.warning(
|
|
89
|
+
"[code]ssh_identity_file[/code] in [code]Run[/code] is deprecated and ignored; will be removed"
|
|
90
|
+
" since 0.19.40"
|
|
91
|
+
)
|
|
85
92
|
|
|
86
93
|
@property
|
|
87
94
|
def name(self) -> str:
|
|
@@ -270,9 +277,22 @@ class Run(ABC):
|
|
|
270
277
|
Raises:
|
|
271
278
|
dstack.api.PortUsedError: If ports are in use or the run is attached by another process.
|
|
272
279
|
"""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
280
|
+
if not ssh_identity_file:
|
|
281
|
+
config_manager = ConfigManager()
|
|
282
|
+
key_manager = UserSSHKeyManager(self._api_client, config_manager.dstack_ssh_dir)
|
|
283
|
+
if (
|
|
284
|
+
user_key := key_manager.get_user_key()
|
|
285
|
+
) and user_key.public_key == self._run.run_spec.ssh_key_pub:
|
|
286
|
+
ssh_identity_file = user_key.private_key_path
|
|
287
|
+
else:
|
|
288
|
+
if config_manager.dstack_key_path.exists():
|
|
289
|
+
# TODO: Remove since 0.19.40
|
|
290
|
+
logger.debug(f"Using legacy [code]{config_manager.dstack_key_path}[/code].")
|
|
291
|
+
ssh_identity_file = config_manager.dstack_key_path
|
|
292
|
+
else:
|
|
293
|
+
raise ConfigurationError(
|
|
294
|
+
f"User SSH key doesn't match; default SSH key ({config_manager.dstack_key_path}) doesn't exist"
|
|
295
|
+
)
|
|
276
296
|
ssh_identity_file = str(ssh_identity_file)
|
|
277
297
|
|
|
278
298
|
job = self._find_job(replica_num=replica_num, job_num=job_num)
|
|
@@ -434,6 +454,7 @@ class RunCollection:
|
|
|
434
454
|
profile: Optional[Profile] = None,
|
|
435
455
|
configuration_path: Optional[str] = None,
|
|
436
456
|
repo_dir: Optional[str] = None,
|
|
457
|
+
ssh_identity_file: Optional[PathLike] = None,
|
|
437
458
|
) -> RunPlan:
|
|
438
459
|
"""
|
|
439
460
|
Get a run plan.
|
|
@@ -465,6 +486,23 @@ class RunCollection:
|
|
|
465
486
|
if repo_dir is None and configuration.repos:
|
|
466
487
|
repo_dir = configuration.repos[0].path
|
|
467
488
|
|
|
489
|
+
if ssh_identity_file:
|
|
490
|
+
ssh_key_pub = Path(ssh_identity_file).with_suffix(".pub").read_text()
|
|
491
|
+
else:
|
|
492
|
+
config_manager = ConfigManager()
|
|
493
|
+
key_manager = UserSSHKeyManager(self._api_client, config_manager.dstack_ssh_dir)
|
|
494
|
+
if key_manager.get_user_key():
|
|
495
|
+
ssh_key_pub = None # using the server-managed user key
|
|
496
|
+
else:
|
|
497
|
+
if not config_manager.dstack_key_path.exists():
|
|
498
|
+
generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
|
|
499
|
+
logger.warning(
|
|
500
|
+
f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
|
|
501
|
+
" You will only be able to attach to the run from this client."
|
|
502
|
+
" Update the [code]dstack[/] server to [code]0.19.34[/]+ to switch to user keys"
|
|
503
|
+
" automatically replicated to all clients.",
|
|
504
|
+
)
|
|
505
|
+
ssh_key_pub = config_manager.dstack_key_path.with_suffix(".pub").read_text()
|
|
468
506
|
run_spec = RunSpec(
|
|
469
507
|
run_name=configuration.name,
|
|
470
508
|
repo_id=repo.repo_id,
|
|
@@ -477,7 +515,7 @@ class RunCollection:
|
|
|
477
515
|
configuration_path=configuration_path,
|
|
478
516
|
configuration=configuration,
|
|
479
517
|
profile=profile,
|
|
480
|
-
ssh_key_pub=
|
|
518
|
+
ssh_key_pub=ssh_key_pub,
|
|
481
519
|
)
|
|
482
520
|
logger.debug("Getting run plan")
|
|
483
521
|
run_plan = self._api_client.runs.get_plan(self._project, run_spec)
|
|
@@ -546,6 +584,7 @@ class RunCollection:
|
|
|
546
584
|
profile: Optional[Profile] = None,
|
|
547
585
|
configuration_path: Optional[str] = None,
|
|
548
586
|
reserve_ports: bool = True,
|
|
587
|
+
ssh_identity_file: Optional[PathLike] = None,
|
|
549
588
|
) -> Run:
|
|
550
589
|
"""
|
|
551
590
|
Apply the run configuration.
|
|
@@ -567,6 +606,7 @@ class RunCollection:
|
|
|
567
606
|
repo=repo,
|
|
568
607
|
profile=profile,
|
|
569
608
|
configuration_path=configuration_path,
|
|
609
|
+
ssh_identity_file=ssh_identity_file,
|
|
570
610
|
)
|
|
571
611
|
run = self.apply_plan(
|
|
572
612
|
run_plan=run_plan,
|
|
@@ -709,6 +749,20 @@ class RunCollection:
|
|
|
709
749
|
creation_policy=creation_policy,
|
|
710
750
|
idle_duration=idle_duration, # type: ignore[assignment]
|
|
711
751
|
)
|
|
752
|
+
config_manager = ConfigManager()
|
|
753
|
+
key_manager = UserSSHKeyManager(self._api_client, config_manager.dstack_ssh_dir)
|
|
754
|
+
if key_manager.get_user_key():
|
|
755
|
+
ssh_key_pub = None # using the server-managed user key
|
|
756
|
+
else:
|
|
757
|
+
if not config_manager.dstack_key_path.exists():
|
|
758
|
+
generate_rsa_key_pair(private_key_path=config_manager.dstack_key_path)
|
|
759
|
+
logger.warning(
|
|
760
|
+
f"Using legacy [code]{config_manager.dstack_key_path.with_suffix('.pub')}[/code]."
|
|
761
|
+
" You will only be able to attach to the run from this client."
|
|
762
|
+
" Update the [code]dstack[/] server to [code]0.19.34[/]+ to switch to user keys"
|
|
763
|
+
" automatically replicated to all clients.",
|
|
764
|
+
)
|
|
765
|
+
ssh_key_pub = config_manager.dstack_key_path.with_suffix(".pub").read_text()
|
|
712
766
|
run_spec = RunSpec(
|
|
713
767
|
run_name=run_name,
|
|
714
768
|
repo_id=repo.repo_id,
|
|
@@ -718,7 +772,7 @@ class RunCollection:
|
|
|
718
772
|
configuration_path=configuration_path,
|
|
719
773
|
configuration=configuration,
|
|
720
774
|
profile=profile,
|
|
721
|
-
ssh_key_pub=
|
|
775
|
+
ssh_key_pub=ssh_key_pub,
|
|
722
776
|
)
|
|
723
777
|
logger.debug("Getting run plan")
|
|
724
778
|
run_plan = self._api_client.runs.get_plan(self._project, run_spec)
|
|
@@ -800,7 +854,6 @@ class RunCollection:
|
|
|
800
854
|
return Run(
|
|
801
855
|
self._api_client,
|
|
802
856
|
self._project,
|
|
803
|
-
self._client.ssh_identity_file,
|
|
804
857
|
run,
|
|
805
858
|
)
|
|
806
859
|
|
|
@@ -808,7 +861,6 @@ class RunCollection:
|
|
|
808
861
|
return Run(
|
|
809
862
|
self._api_client,
|
|
810
863
|
self._project,
|
|
811
|
-
self._client.ssh_identity_file,
|
|
812
864
|
run,
|
|
813
865
|
ports_lock,
|
|
814
866
|
)
|
dstack/api/server/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import os
|
|
2
3
|
import pprint
|
|
3
4
|
import time
|
|
@@ -121,6 +122,9 @@ class APIClient:
|
|
|
121
122
|
def files(self) -> FilesAPIClient:
|
|
122
123
|
return FilesAPIClient(self._request, self._logger)
|
|
123
124
|
|
|
125
|
+
def get_token_hash(self) -> str:
|
|
126
|
+
return hashlib.sha1(self._token.encode()).hexdigest()[:8]
|
|
127
|
+
|
|
124
128
|
def _request(
|
|
125
129
|
self,
|
|
126
130
|
path: str,
|
dstack/api/server/_users.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
|
-
from pydantic import parse_obj_as
|
|
3
|
+
from pydantic import ValidationError, parse_obj_as
|
|
4
4
|
|
|
5
5
|
from dstack._internal.core.models.users import GlobalRole, User, UserWithCreds
|
|
6
6
|
from dstack._internal.server.schemas.users import (
|
|
@@ -18,8 +18,23 @@ class UsersAPIClient(APIClientGroup):
|
|
|
18
18
|
return parse_obj_as(List[User.__response__], resp.json())
|
|
19
19
|
|
|
20
20
|
def get_my_user(self) -> User:
|
|
21
|
+
"""
|
|
22
|
+
Returns `User` with pre-0.19.33 servers, or `UserWithCreds` with newer servers.
|
|
23
|
+
"""
|
|
24
|
+
|
|
21
25
|
resp = self._request("/api/users/get_my_user")
|
|
22
|
-
|
|
26
|
+
try:
|
|
27
|
+
return parse_obj_as(UserWithCreds.__response__, resp.json())
|
|
28
|
+
except ValidationError as e:
|
|
29
|
+
# Compatibility with pre-0.19.33 server
|
|
30
|
+
if (
|
|
31
|
+
len(e.errors()) == 1
|
|
32
|
+
and e.errors()[0]["loc"] == ("__root__", "creds")
|
|
33
|
+
and e.errors()[0]["type"] == "value_error.missing"
|
|
34
|
+
):
|
|
35
|
+
return parse_obj_as(User.__response__, resp.json())
|
|
36
|
+
else:
|
|
37
|
+
raise
|
|
23
38
|
|
|
24
39
|
def get_user(self, username: str) -> User:
|
|
25
40
|
body = GetUserRequest(username=username)
|
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.34
|
|
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.11
|
|
26
26
|
Requires-Dist: ignore-python>=0.2.0
|
|
27
27
|
Requires-Dist: jsonschema
|
|
28
28
|
Requires-Dist: orjson
|