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.

Files changed (54) hide show
  1. dstack/_internal/cli/commands/offer.py +1 -1
  2. dstack/_internal/cli/services/configurators/run.py +1 -5
  3. dstack/_internal/core/backends/aws/compute.py +8 -5
  4. dstack/_internal/core/backends/azure/compute.py +9 -6
  5. dstack/_internal/core/backends/base/compute.py +40 -17
  6. dstack/_internal/core/backends/base/offers.py +7 -1
  7. dstack/_internal/core/backends/datacrunch/compute.py +9 -6
  8. dstack/_internal/core/backends/gcp/compute.py +151 -6
  9. dstack/_internal/core/backends/gcp/models.py +10 -0
  10. dstack/_internal/core/backends/gcp/resources.py +87 -5
  11. dstack/_internal/core/backends/hotaisle/compute.py +11 -1
  12. dstack/_internal/core/backends/kubernetes/compute.py +161 -83
  13. dstack/_internal/core/backends/kubernetes/models.py +4 -2
  14. dstack/_internal/core/backends/nebius/compute.py +9 -6
  15. dstack/_internal/core/backends/oci/compute.py +9 -6
  16. dstack/_internal/core/backends/runpod/compute.py +14 -7
  17. dstack/_internal/core/backends/vastai/compute.py +3 -1
  18. dstack/_internal/core/backends/vastai/configurator.py +0 -1
  19. dstack/_internal/core/compatibility/runs.py +25 -4
  20. dstack/_internal/core/models/fleets.py +1 -1
  21. dstack/_internal/core/models/instances.py +2 -1
  22. dstack/_internal/core/models/profiles.py +1 -1
  23. dstack/_internal/core/models/runs.py +4 -2
  24. dstack/_internal/core/models/users.py +10 -0
  25. dstack/_internal/core/services/configs/__init__.py +1 -0
  26. dstack/_internal/core/services/ssh/key_manager.py +56 -0
  27. dstack/_internal/server/background/tasks/process_instances.py +5 -1
  28. dstack/_internal/server/background/tasks/process_running_jobs.py +1 -0
  29. dstack/_internal/server/migrations/versions/ff1d94f65b08_user_ssh_key.py +34 -0
  30. dstack/_internal/server/models.py +6 -0
  31. dstack/_internal/server/routers/metrics.py +6 -2
  32. dstack/_internal/server/routers/runs.py +5 -1
  33. dstack/_internal/server/routers/users.py +21 -2
  34. dstack/_internal/server/services/jobs/__init__.py +18 -9
  35. dstack/_internal/server/services/offers.py +1 -0
  36. dstack/_internal/server/services/runs.py +13 -4
  37. dstack/_internal/server/services/users.py +35 -2
  38. dstack/_internal/server/statics/index.html +1 -1
  39. dstack/_internal/server/statics/main-720ce3a11140daa480cc.css +3 -0
  40. dstack/_internal/server/statics/{main-c51afa7f243e24d3e446.js → main-e79754c136f1d8e4e7e6.js} +12632 -8039
  41. dstack/_internal/server/statics/{main-c51afa7f243e24d3e446.js.map → main-e79754c136f1d8e4e7e6.js.map} +1 -1
  42. dstack/_internal/server/testing/common.py +4 -0
  43. dstack/api/_public/__init__.py +8 -11
  44. dstack/api/_public/repos.py +0 -21
  45. dstack/api/_public/runs.py +61 -9
  46. dstack/api/server/__init__.py +4 -0
  47. dstack/api/server/_users.py +17 -2
  48. dstack/version.py +2 -2
  49. {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/METADATA +2 -2
  50. {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/RECORD +53 -51
  51. dstack/_internal/server/statics/main-56191fbfe77f49b251de.css +0 -3
  52. {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/WHEEL +0 -0
  53. {dstack-0.19.32.dist-info → dstack-0.19.34.dist-info}/entry_points.txt +0 -0
  54. {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)
@@ -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, get_ssh_keypair
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: SSH keypair to access instances
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
- self.ssh_identity_file = str(ssh_identity_file)
47
- else:
48
- self.ssh_identity_file = get_ssh_keypair(None, ConfigManager().dstack_key_path)
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
@@ -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)
@@ -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
- ssh_identity_file = ssh_identity_file or self._ssh_identity_file
274
- if ssh_identity_file is None:
275
- raise ConfigurationError("SSH identity file is required to attach to the run")
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=Path(self._client.ssh_identity_file + ".pub").read_text().strip(),
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=Path(self._client.ssh_identity_file + ".pub").read_text().strip(),
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
  )
@@ -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,
@@ -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
- return parse_obj_as(User.__response__, resp.json())
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,4 +1,4 @@
1
- __version__ = "0.19.32"
1
+ __version__ = "0.19.34"
2
2
  __is_release__ = True
3
- base_image = "0.11rc2"
3
+ base_image = "0.11"
4
4
  base_image_ubuntu_version = "22.04"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.32
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.8
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