dstack 0.19.16__py3-none-any.whl → 0.19.18__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 (80) hide show
  1. dstack/_internal/cli/commands/secrets.py +92 -0
  2. dstack/_internal/cli/main.py +2 -0
  3. dstack/_internal/cli/services/completion.py +5 -0
  4. dstack/_internal/cli/services/configurators/fleet.py +13 -1
  5. dstack/_internal/cli/services/configurators/run.py +59 -17
  6. dstack/_internal/cli/utils/secrets.py +25 -0
  7. dstack/_internal/core/backends/__init__.py +10 -4
  8. dstack/_internal/core/backends/aws/compute.py +237 -18
  9. dstack/_internal/core/backends/base/compute.py +20 -2
  10. dstack/_internal/core/backends/cudo/compute.py +23 -9
  11. dstack/_internal/core/backends/gcp/compute.py +13 -7
  12. dstack/_internal/core/backends/lambdalabs/compute.py +2 -1
  13. dstack/_internal/core/compatibility/fleets.py +12 -11
  14. dstack/_internal/core/compatibility/gateways.py +9 -8
  15. dstack/_internal/core/compatibility/logs.py +4 -3
  16. dstack/_internal/core/compatibility/runs.py +41 -17
  17. dstack/_internal/core/compatibility/volumes.py +9 -8
  18. dstack/_internal/core/errors.py +4 -0
  19. dstack/_internal/core/models/common.py +7 -0
  20. dstack/_internal/core/models/configurations.py +11 -0
  21. dstack/_internal/core/models/files.py +67 -0
  22. dstack/_internal/core/models/runs.py +14 -0
  23. dstack/_internal/core/models/secrets.py +9 -2
  24. dstack/_internal/core/services/diff.py +36 -3
  25. dstack/_internal/server/app.py +22 -0
  26. dstack/_internal/server/background/__init__.py +61 -37
  27. dstack/_internal/server/background/tasks/process_fleets.py +19 -3
  28. dstack/_internal/server/background/tasks/process_gateways.py +1 -1
  29. dstack/_internal/server/background/tasks/process_instances.py +13 -2
  30. dstack/_internal/server/background/tasks/process_placement_groups.py +4 -2
  31. dstack/_internal/server/background/tasks/process_running_jobs.py +123 -15
  32. dstack/_internal/server/background/tasks/process_runs.py +23 -7
  33. dstack/_internal/server/background/tasks/process_submitted_jobs.py +36 -7
  34. dstack/_internal/server/background/tasks/process_terminating_jobs.py +5 -3
  35. dstack/_internal/server/background/tasks/process_volumes.py +2 -2
  36. dstack/_internal/server/migrations/versions/5f1707c525d2_add_filearchivemodel.py +39 -0
  37. dstack/_internal/server/migrations/versions/644b8a114187_add_secretmodel.py +49 -0
  38. dstack/_internal/server/models.py +33 -0
  39. dstack/_internal/server/routers/files.py +67 -0
  40. dstack/_internal/server/routers/secrets.py +57 -15
  41. dstack/_internal/server/schemas/files.py +5 -0
  42. dstack/_internal/server/schemas/runner.py +2 -0
  43. dstack/_internal/server/schemas/secrets.py +7 -11
  44. dstack/_internal/server/services/backends/__init__.py +1 -1
  45. dstack/_internal/server/services/files.py +91 -0
  46. dstack/_internal/server/services/fleets.py +5 -4
  47. dstack/_internal/server/services/gateways/__init__.py +4 -2
  48. dstack/_internal/server/services/jobs/__init__.py +19 -8
  49. dstack/_internal/server/services/jobs/configurators/base.py +25 -3
  50. dstack/_internal/server/services/jobs/configurators/dev.py +3 -3
  51. dstack/_internal/server/services/locking.py +101 -12
  52. dstack/_internal/server/services/proxy/repo.py +3 -0
  53. dstack/_internal/server/services/runner/client.py +8 -0
  54. dstack/_internal/server/services/runs.py +76 -47
  55. dstack/_internal/server/services/secrets.py +204 -0
  56. dstack/_internal/server/services/storage/base.py +21 -0
  57. dstack/_internal/server/services/storage/gcs.py +28 -6
  58. dstack/_internal/server/services/storage/s3.py +27 -9
  59. dstack/_internal/server/services/volumes.py +2 -2
  60. dstack/_internal/server/settings.py +19 -5
  61. dstack/_internal/server/statics/index.html +1 -1
  62. dstack/_internal/server/statics/{main-a4eafa74304e587d037c.js → main-d1ac2e8c38ed5f08a114.js} +86 -34
  63. dstack/_internal/server/statics/{main-a4eafa74304e587d037c.js.map → main-d1ac2e8c38ed5f08a114.js.map} +1 -1
  64. dstack/_internal/server/statics/{main-f53d6d0d42f8d61df1de.css → main-d58fc0460cb0eae7cb5c.css} +1 -1
  65. dstack/_internal/server/statics/static/media/google.b194b06fafd0a52aeb566922160ea514.svg +1 -0
  66. dstack/_internal/server/testing/common.py +50 -8
  67. dstack/_internal/settings.py +4 -0
  68. dstack/_internal/utils/files.py +69 -0
  69. dstack/_internal/utils/nested_list.py +47 -0
  70. dstack/_internal/utils/path.py +12 -4
  71. dstack/api/_public/runs.py +67 -7
  72. dstack/api/server/__init__.py +6 -0
  73. dstack/api/server/_files.py +18 -0
  74. dstack/api/server/_secrets.py +15 -15
  75. dstack/version.py +1 -1
  76. {dstack-0.19.16.dist-info → dstack-0.19.18.dist-info}/METADATA +13 -13
  77. {dstack-0.19.16.dist-info → dstack-0.19.18.dist-info}/RECORD +80 -67
  78. {dstack-0.19.16.dist-info → dstack-0.19.18.dist-info}/WHEEL +0 -0
  79. {dstack-0.19.16.dist-info → dstack-0.19.18.dist-info}/entry_points.txt +0 -0
  80. {dstack-0.19.16.dist-info → dstack-0.19.18.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,92 @@
1
+ import argparse
2
+
3
+ from dstack._internal.cli.commands import APIBaseCommand
4
+ from dstack._internal.cli.services.completion import SecretNameCompleter
5
+ from dstack._internal.cli.utils.common import (
6
+ confirm_ask,
7
+ console,
8
+ )
9
+ from dstack._internal.cli.utils.secrets import print_secrets_table
10
+
11
+
12
+ class SecretCommand(APIBaseCommand):
13
+ NAME = "secret"
14
+ DESCRIPTION = "Manage secrets"
15
+
16
+ def _register(self):
17
+ super()._register()
18
+ self._parser.set_defaults(subfunc=self._list)
19
+ subparsers = self._parser.add_subparsers(dest="action")
20
+
21
+ list_parser = subparsers.add_parser(
22
+ "list", help="List secrets", formatter_class=self._parser.formatter_class
23
+ )
24
+ list_parser.set_defaults(subfunc=self._list)
25
+
26
+ get_parser = subparsers.add_parser(
27
+ "get", help="Get secret value", formatter_class=self._parser.formatter_class
28
+ )
29
+ get_parser.add_argument(
30
+ "name",
31
+ help="The name of the secret",
32
+ ).completer = SecretNameCompleter()
33
+ get_parser.set_defaults(subfunc=self._get)
34
+
35
+ set_parser = subparsers.add_parser(
36
+ "set", help="Set secret", formatter_class=self._parser.formatter_class
37
+ )
38
+ set_parser.add_argument(
39
+ "name",
40
+ help="The name of the secret",
41
+ )
42
+ set_parser.add_argument(
43
+ "value",
44
+ help="The value of the secret",
45
+ )
46
+ set_parser.set_defaults(subfunc=self._set)
47
+
48
+ delete_parser = subparsers.add_parser(
49
+ "delete",
50
+ help="Delete secrets",
51
+ formatter_class=self._parser.formatter_class,
52
+ )
53
+ delete_parser.add_argument(
54
+ "name",
55
+ help="The name of the secret",
56
+ ).completer = SecretNameCompleter()
57
+ delete_parser.add_argument(
58
+ "-y", "--yes", help="Don't ask for confirmation", action="store_true"
59
+ )
60
+ delete_parser.set_defaults(subfunc=self._delete)
61
+
62
+ def _command(self, args: argparse.Namespace):
63
+ super()._command(args)
64
+ args.subfunc(args)
65
+
66
+ def _list(self, args: argparse.Namespace):
67
+ secrets = self.api.client.secrets.list(self.api.project)
68
+ print_secrets_table(secrets)
69
+
70
+ def _get(self, args: argparse.Namespace):
71
+ secret = self.api.client.secrets.get(self.api.project, name=args.name)
72
+ print_secrets_table([secret])
73
+
74
+ def _set(self, args: argparse.Namespace):
75
+ self.api.client.secrets.create_or_update(
76
+ self.api.project,
77
+ name=args.name,
78
+ value=args.value,
79
+ )
80
+ console.print("[grey58]OK[/]")
81
+
82
+ def _delete(self, args: argparse.Namespace):
83
+ if not args.yes and not confirm_ask(f"Delete the secret [code]{args.name}[/]?"):
84
+ console.print("\nExiting...")
85
+ return
86
+
87
+ with console.status("Deleting secret..."):
88
+ self.api.client.secrets.delete(
89
+ project_name=self.api.project,
90
+ names=[args.name],
91
+ )
92
+ console.print("[grey58]OK[/]")
@@ -17,6 +17,7 @@ from dstack._internal.cli.commands.metrics import MetricsCommand
17
17
  from dstack._internal.cli.commands.offer import OfferCommand
18
18
  from dstack._internal.cli.commands.project import ProjectCommand
19
19
  from dstack._internal.cli.commands.ps import PsCommand
20
+ from dstack._internal.cli.commands.secrets import SecretCommand
20
21
  from dstack._internal.cli.commands.server import ServerCommand
21
22
  from dstack._internal.cli.commands.stats import StatsCommand
22
23
  from dstack._internal.cli.commands.stop import StopCommand
@@ -72,6 +73,7 @@ def main():
72
73
  MetricsCommand.register(subparsers)
73
74
  ProjectCommand.register(subparsers)
74
75
  PsCommand.register(subparsers)
76
+ SecretCommand.register(subparsers)
75
77
  ServerCommand.register(subparsers)
76
78
  StatsCommand.register(subparsers)
77
79
  StopCommand.register(subparsers)
@@ -75,6 +75,11 @@ class GatewayNameCompleter(BaseAPINameCompleter):
75
75
  return [r.name for r in api.client.gateways.list(api.project)]
76
76
 
77
77
 
78
+ class SecretNameCompleter(BaseAPINameCompleter):
79
+ def fetch_resource_names(self, api: Client) -> Iterable[str]:
80
+ return [r.name for r in api.client.secrets.list(api.project)]
81
+
82
+
78
83
  class ProjectNameCompleter(BaseCompleter):
79
84
  """
80
85
  Completer for local project names.
@@ -35,6 +35,7 @@ from dstack._internal.core.models.fleets import (
35
35
  )
36
36
  from dstack._internal.core.models.instances import InstanceAvailability, InstanceStatus, SSHKey
37
37
  from dstack._internal.core.models.repos.base import Repo
38
+ from dstack._internal.core.services.diff import diff_models
38
39
  from dstack._internal.utils.common import local_time
39
40
  from dstack._internal.utils.logging import get_logger
40
41
  from dstack._internal.utils.ssh import convert_ssh_key_to_pem, generate_public_key, pkey_from_str
@@ -82,7 +83,18 @@ class FleetConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
82
83
  confirm_message += "Create the fleet?"
83
84
  else:
84
85
  action_message += f"Found fleet [code]{plan.spec.configuration.name}[/]."
85
- if plan.current_resource.spec.configuration == plan.spec.configuration:
86
+ diff = diff_models(
87
+ old=plan.current_resource.spec.configuration,
88
+ new=plan.spec.configuration,
89
+ ignore={
90
+ "ssh_config": {
91
+ "ssh_key": True,
92
+ "proxy_jump": {"ssh_key"},
93
+ "hosts": {"__all__": {"ssh_key": True, "proxy_jump": {"ssh_key"}}},
94
+ }
95
+ },
96
+ )
97
+ if not diff:
86
98
  if command_args.yes and not command_args.force:
87
99
  # --force is required only with --yes,
88
100
  # otherwise we may ask for force apply interactively.
@@ -41,12 +41,13 @@ from dstack._internal.core.models.configurations import (
41
41
  )
42
42
  from dstack._internal.core.models.repos.base import Repo
43
43
  from dstack._internal.core.models.resources import CPUSpec
44
- from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunStatus
44
+ from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunSpec, RunStatus
45
45
  from dstack._internal.core.services.configs import ConfigManager
46
46
  from dstack._internal.core.services.diff import diff_models
47
47
  from dstack._internal.utils.common import local_time
48
48
  from dstack._internal.utils.interpolator import InterpolatorError, VariablesInterpolator
49
49
  from dstack._internal.utils.logging import get_logger
50
+ from dstack._internal.utils.nested_list import NestedList, NestedListItem
50
51
  from dstack.api._public.repos import get_ssh_keypair
51
52
  from dstack.api._public.runs import Run
52
53
  from dstack.api.utils import load_profile
@@ -102,25 +103,20 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
102
103
  confirm_message = f"Submit the run [code]{conf.name}[/]?"
103
104
  stop_run_name = None
104
105
  if run_plan.current_resource is not None:
105
- changed_fields = []
106
- if run_plan.action == ApplyAction.UPDATE:
107
- diff = diff_models(
108
- run_plan.get_effective_run_spec().configuration,
109
- run_plan.current_resource.run_spec.configuration,
110
- )
111
- changed_fields = list(diff.keys())
112
- if run_plan.action == ApplyAction.UPDATE and len(changed_fields) > 0:
106
+ diff = render_run_spec_diff(
107
+ run_plan.get_effective_run_spec(),
108
+ run_plan.current_resource.run_spec,
109
+ )
110
+ if run_plan.action == ApplyAction.UPDATE and diff is not None:
113
111
  console.print(
114
112
  f"Active run [code]{conf.name}[/] already exists."
115
- " Detected configuration changes that can be updated in-place:"
116
- f" {changed_fields}"
113
+ f" Detected changes that [code]can[/] be updated in-place:\n{diff}"
117
114
  )
118
115
  confirm_message = "Update the run?"
119
- elif run_plan.action == ApplyAction.UPDATE and len(changed_fields) == 0:
116
+ elif run_plan.action == ApplyAction.UPDATE and diff is None:
120
117
  stop_run_name = run_plan.current_resource.run_spec.run_name
121
118
  console.print(
122
- f"Active run [code]{conf.name}[/] already exists."
123
- " Detected no configuration changes."
119
+ f"Active run [code]{conf.name}[/] already exists. Detected no changes."
124
120
  )
125
121
  if command_args.yes and not command_args.force:
126
122
  console.print("Use --force to apply anyway.")
@@ -129,7 +125,8 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
129
125
  elif not run_plan.current_resource.status.is_finished():
130
126
  stop_run_name = run_plan.current_resource.run_spec.run_name
131
127
  console.print(
132
- f"Active run [code]{conf.name}[/] already exists and cannot be updated in-place."
128
+ f"Active run [code]{conf.name}[/] already exists."
129
+ f" Detected changes that [error]cannot[/] be updated in-place:\n{diff}"
133
130
  )
134
131
  confirm_message = "Stop and override the run?"
135
132
 
@@ -398,9 +395,10 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
398
395
  else:
399
396
  has_amd_gpu = vendor == gpuhunt.AcceleratorVendor.AMD
400
397
  has_tt_gpu = vendor == gpuhunt.AcceleratorVendor.TENSTORRENT
401
- if has_amd_gpu and conf.image is None:
398
+ # When docker=True, the system uses Docker-in-Docker image, so no custom image is required
399
+ if has_amd_gpu and conf.image is None and conf.docker is not True:
402
400
  raise ConfigurationError("`image` is required if `resources.gpu.vendor` is `amd`")
403
- if has_tt_gpu and conf.image is None:
401
+ if has_tt_gpu and conf.image is None and conf.docker is not True:
404
402
  raise ConfigurationError(
405
403
  "`image` is required if `resources.gpu.vendor` is `tenstorrent`"
406
404
  )
@@ -610,3 +608,47 @@ def _run_resubmitted(run: Run, current_job_submission: Optional[JobSubmission])
610
608
  not run.status.is_finished()
611
609
  and run._run.latest_job_submission.submitted_at > current_job_submission.submitted_at
612
610
  )
611
+
612
+
613
+ def render_run_spec_diff(old_spec: RunSpec, new_spec: RunSpec) -> Optional[str]:
614
+ changed_spec_fields = list(diff_models(old_spec, new_spec))
615
+ if not changed_spec_fields:
616
+ return None
617
+ friendly_spec_field_names = {
618
+ "repo_id": "Repo ID",
619
+ "repo_code_hash": "Repo files",
620
+ "repo_data": "Repo state (branch, commit, or other)",
621
+ "ssh_key_pub": "Public SSH key",
622
+ }
623
+ nested_list = NestedList()
624
+ for spec_field in changed_spec_fields:
625
+ if spec_field == "merged_profile":
626
+ continue
627
+ elif spec_field == "configuration":
628
+ if type(old_spec.configuration) is not type(new_spec.configuration):
629
+ item = NestedListItem("Configuration type")
630
+ else:
631
+ item = NestedListItem(
632
+ "Configuration properties:",
633
+ children=[
634
+ NestedListItem(field)
635
+ for field in diff_models(old_spec.configuration, new_spec.configuration)
636
+ ],
637
+ )
638
+ elif spec_field == "profile":
639
+ if type(old_spec.profile) is not type(new_spec.profile):
640
+ item = NestedListItem("Profile")
641
+ else:
642
+ item = NestedListItem(
643
+ "Profile properties:",
644
+ children=[
645
+ NestedListItem(field)
646
+ for field in diff_models(old_spec.profile, new_spec.profile)
647
+ ],
648
+ )
649
+ elif spec_field in friendly_spec_field_names:
650
+ item = NestedListItem(friendly_spec_field_names[spec_field])
651
+ else:
652
+ item = NestedListItem(spec_field.replace("_", " ").capitalize())
653
+ nested_list.children.append(item)
654
+ return nested_list.render()
@@ -0,0 +1,25 @@
1
+ from typing import List
2
+
3
+ from rich.table import Table
4
+
5
+ from dstack._internal.cli.utils.common import add_row_from_dict, console
6
+ from dstack._internal.core.models.secrets import Secret
7
+
8
+
9
+ def print_secrets_table(secrets: List[Secret]) -> None:
10
+ console.print(get_secrets_table(secrets))
11
+ console.print()
12
+
13
+
14
+ def get_secrets_table(secrets: List[Secret]) -> Table:
15
+ table = Table(box=None)
16
+ table.add_column("NAME", no_wrap=True)
17
+ table.add_column("VALUE")
18
+
19
+ for secret in secrets:
20
+ row = {
21
+ "NAME": secret.name,
22
+ "VALUE": secret.value or "*" * 6,
23
+ }
24
+ add_row_from_dict(table, row)
25
+ return table
@@ -9,18 +9,25 @@ from dstack._internal.core.backends.base.compute import (
9
9
  )
10
10
  from dstack._internal.core.backends.base.configurator import Configurator
11
11
  from dstack._internal.core.backends.configurators import list_available_configurator_classes
12
+ from dstack._internal.core.backends.local.compute import LocalCompute
12
13
  from dstack._internal.core.models.backends.base import BackendType
14
+ from dstack._internal.settings import LOCAL_BACKEND_ENABLED
13
15
 
14
16
 
15
17
  def _get_backends_with_compute_feature(
16
18
  configurator_classes: list[type[Configurator]],
17
19
  compute_feature_class: type,
18
20
  ) -> list[BackendType]:
21
+ backend_types_and_computes = [
22
+ (configurator_class.TYPE, configurator_class.BACKEND_CLASS.COMPUTE_CLASS)
23
+ for configurator_class in configurator_classes
24
+ ]
25
+ if LOCAL_BACKEND_ENABLED:
26
+ backend_types_and_computes.append((BackendType.LOCAL, LocalCompute))
19
27
  backend_types = []
20
- for configurator_class in configurator_classes:
21
- compute_class = configurator_class.BACKEND_CLASS.COMPUTE_CLASS
28
+ for backend_type, compute_class in backend_types_and_computes:
22
29
  if issubclass(compute_class, compute_feature_class):
23
- backend_types.append(configurator_class.TYPE)
30
+ backend_types.append(backend_type)
24
31
  return backend_types
25
32
 
26
33
 
@@ -28,7 +35,6 @@ _configurator_classes = list_available_configurator_classes()
28
35
 
29
36
 
30
37
  # The following backend lists do not include unavailable backends (i.e. backends missing deps).
31
- # TODO: Add LocalBackend to lists if it's enabled
32
38
  BACKENDS_WITH_CREATE_INSTANCE_SUPPORT = _get_backends_with_compute_feature(
33
39
  configurator_classes=_configurator_classes,
34
40
  compute_feature_class=ComputeWithCreateInstanceSupport,