dstack 0.18.40rc1__py3-none-any.whl → 0.18.42__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 (104) hide show
  1. dstack/_internal/cli/commands/apply.py +8 -5
  2. dstack/_internal/cli/services/configurators/base.py +4 -2
  3. dstack/_internal/cli/services/configurators/fleet.py +21 -9
  4. dstack/_internal/cli/services/configurators/gateway.py +15 -0
  5. dstack/_internal/cli/services/configurators/run.py +6 -5
  6. dstack/_internal/cli/services/configurators/volume.py +15 -0
  7. dstack/_internal/cli/services/repos.py +3 -3
  8. dstack/_internal/cli/utils/fleet.py +44 -33
  9. dstack/_internal/cli/utils/run.py +27 -7
  10. dstack/_internal/cli/utils/volume.py +30 -9
  11. dstack/_internal/core/backends/aws/compute.py +94 -53
  12. dstack/_internal/core/backends/aws/resources.py +22 -12
  13. dstack/_internal/core/backends/azure/compute.py +2 -0
  14. dstack/_internal/core/backends/base/compute.py +20 -2
  15. dstack/_internal/core/backends/gcp/compute.py +32 -24
  16. dstack/_internal/core/backends/gcp/resources.py +0 -15
  17. dstack/_internal/core/backends/oci/compute.py +10 -5
  18. dstack/_internal/core/backends/oci/resources.py +23 -26
  19. dstack/_internal/core/backends/remote/provisioning.py +65 -27
  20. dstack/_internal/core/backends/runpod/compute.py +1 -0
  21. dstack/_internal/core/models/backends/azure.py +3 -1
  22. dstack/_internal/core/models/configurations.py +24 -1
  23. dstack/_internal/core/models/fleets.py +46 -0
  24. dstack/_internal/core/models/instances.py +5 -1
  25. dstack/_internal/core/models/pools.py +4 -1
  26. dstack/_internal/core/models/profiles.py +10 -4
  27. dstack/_internal/core/models/runs.py +23 -3
  28. dstack/_internal/core/models/volumes.py +26 -0
  29. dstack/_internal/core/services/ssh/attach.py +92 -53
  30. dstack/_internal/core/services/ssh/tunnel.py +58 -31
  31. dstack/_internal/proxy/gateway/routers/registry.py +2 -0
  32. dstack/_internal/proxy/gateway/schemas/registry.py +2 -0
  33. dstack/_internal/proxy/gateway/services/registry.py +4 -0
  34. dstack/_internal/proxy/lib/models.py +3 -0
  35. dstack/_internal/proxy/lib/services/service_connection.py +8 -1
  36. dstack/_internal/server/background/tasks/process_instances.py +73 -35
  37. dstack/_internal/server/background/tasks/process_metrics.py +9 -9
  38. dstack/_internal/server/background/tasks/process_running_jobs.py +77 -26
  39. dstack/_internal/server/background/tasks/process_runs.py +2 -12
  40. dstack/_internal/server/background/tasks/process_submitted_jobs.py +121 -49
  41. dstack/_internal/server/background/tasks/process_terminating_jobs.py +14 -3
  42. dstack/_internal/server/background/tasks/process_volumes.py +11 -1
  43. dstack/_internal/server/migrations/versions/1338b788b612_reverse_job_instance_relationship.py +71 -0
  44. dstack/_internal/server/migrations/versions/1e76fb0dde87_add_jobmodel_inactivity_secs.py +32 -0
  45. dstack/_internal/server/migrations/versions/51d45659d574_add_instancemodel_blocks_fields.py +43 -0
  46. dstack/_internal/server/migrations/versions/63c3f19cb184_add_jobterminationreason_inactivity_.py +83 -0
  47. dstack/_internal/server/migrations/versions/a751ef183f27_move_attachment_data_to_volumes_.py +34 -0
  48. dstack/_internal/server/models.py +27 -23
  49. dstack/_internal/server/routers/runs.py +1 -0
  50. dstack/_internal/server/schemas/runner.py +1 -0
  51. dstack/_internal/server/services/backends/configurators/azure.py +34 -8
  52. dstack/_internal/server/services/config.py +9 -0
  53. dstack/_internal/server/services/fleets.py +32 -3
  54. dstack/_internal/server/services/gateways/client.py +9 -1
  55. dstack/_internal/server/services/jobs/__init__.py +217 -45
  56. dstack/_internal/server/services/jobs/configurators/base.py +47 -2
  57. dstack/_internal/server/services/offers.py +96 -10
  58. dstack/_internal/server/services/pools.py +98 -14
  59. dstack/_internal/server/services/proxy/repo.py +17 -3
  60. dstack/_internal/server/services/runner/client.py +9 -6
  61. dstack/_internal/server/services/runner/ssh.py +33 -5
  62. dstack/_internal/server/services/runs.py +48 -179
  63. dstack/_internal/server/services/services/__init__.py +9 -1
  64. dstack/_internal/server/services/volumes.py +68 -9
  65. dstack/_internal/server/statics/index.html +1 -1
  66. dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js → main-2ac66bfcbd2e39830b88.js} +30 -31
  67. dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js.map → main-2ac66bfcbd2e39830b88.js.map} +1 -1
  68. dstack/_internal/server/statics/{main-fc56d1f4af8e57522a1c.css → main-ad5150a441de98cd8987.css} +1 -1
  69. dstack/_internal/server/testing/common.py +130 -61
  70. dstack/_internal/utils/common.py +22 -8
  71. dstack/_internal/utils/env.py +14 -0
  72. dstack/_internal/utils/ssh.py +1 -1
  73. dstack/api/server/_fleets.py +25 -1
  74. dstack/api/server/_runs.py +23 -2
  75. dstack/api/server/_volumes.py +12 -1
  76. dstack/version.py +1 -1
  77. {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/METADATA +1 -1
  78. {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/RECORD +104 -93
  79. tests/_internal/cli/services/configurators/test_profile.py +3 -3
  80. tests/_internal/core/services/ssh/test_tunnel.py +56 -4
  81. tests/_internal/proxy/gateway/routers/test_registry.py +30 -7
  82. tests/_internal/server/background/tasks/test_process_instances.py +138 -20
  83. tests/_internal/server/background/tasks/test_process_metrics.py +12 -0
  84. tests/_internal/server/background/tasks/test_process_running_jobs.py +193 -0
  85. tests/_internal/server/background/tasks/test_process_runs.py +27 -3
  86. tests/_internal/server/background/tasks/test_process_submitted_jobs.py +53 -6
  87. tests/_internal/server/background/tasks/test_process_terminating_jobs.py +135 -17
  88. tests/_internal/server/routers/test_fleets.py +15 -2
  89. tests/_internal/server/routers/test_pools.py +6 -0
  90. tests/_internal/server/routers/test_runs.py +27 -0
  91. tests/_internal/server/routers/test_volumes.py +9 -2
  92. tests/_internal/server/services/jobs/__init__.py +0 -0
  93. tests/_internal/server/services/jobs/configurators/__init__.py +0 -0
  94. tests/_internal/server/services/jobs/configurators/test_base.py +72 -0
  95. tests/_internal/server/services/runner/test_client.py +22 -3
  96. tests/_internal/server/services/test_offers.py +167 -0
  97. tests/_internal/server/services/test_pools.py +109 -1
  98. tests/_internal/server/services/test_runs.py +5 -41
  99. tests/_internal/utils/test_common.py +21 -0
  100. tests/_internal/utils/test_env.py +38 -0
  101. {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/LICENSE.md +0 -0
  102. {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/WHEEL +0 -0
  103. {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/entry_points.txt +0 -0
  104. {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/top_level.txt +0 -0
@@ -6,12 +6,12 @@ from dstack._internal.cli.services.configurators import (
6
6
  get_apply_configurator_class,
7
7
  load_apply_configuration,
8
8
  )
9
- from dstack._internal.cli.services.configurators.base import BaseApplyConfigurator
10
9
  from dstack._internal.cli.services.repos import (
11
10
  init_default_virtual_repo,
12
11
  init_repo,
13
12
  register_init_repo_args,
14
13
  )
14
+ from dstack._internal.cli.utils.common import console
15
15
  from dstack._internal.core.errors import CLIError
16
16
  from dstack._internal.core.models.configurations import ApplyConfigurationType
17
17
 
@@ -92,10 +92,13 @@ class ApplyCommand(APIBaseCommand):
92
92
  configurator_class = get_apply_configurator_class(
93
93
  ApplyConfigurationType(args.help)
94
94
  )
95
- else:
96
- configurator_class = BaseApplyConfigurator
97
- configurator_class.register_args(self._parser)
95
+ configurator_class.register_args(self._parser)
96
+ self._parser.print_help()
97
+ return
98
98
  self._parser.print_help()
99
+ console.print(
100
+ "\nType `dstack apply -h CONFIGURATION_TYPE` to see configuration-specific options.\n"
101
+ )
99
102
  return
100
103
 
101
104
  super()._command(args)
@@ -129,5 +132,5 @@ class ApplyCommand(APIBaseCommand):
129
132
  repo=repo,
130
133
  )
131
134
  except KeyboardInterrupt:
132
- print("\nOperation interrupted by user. Exiting...")
135
+ console.print("\nOperation interrupted by user. Exiting...")
133
136
  exit(0)
@@ -1,7 +1,7 @@
1
1
  import argparse
2
2
  import os
3
3
  from abc import ABC, abstractmethod
4
- from typing import List, Optional, cast
4
+ from typing import List, Optional, Union, cast
5
5
 
6
6
  from dstack._internal.cli.services.args import env_var
7
7
  from dstack._internal.core.errors import ConfigurationError
@@ -14,6 +14,8 @@ from dstack._internal.core.models.envs import Env, EnvSentinel, EnvVarTuple
14
14
  from dstack._internal.core.models.repos.base import Repo
15
15
  from dstack.api._public import Client
16
16
 
17
+ ArgsParser = Union[argparse._ArgumentGroup, argparse.ArgumentParser]
18
+
17
19
 
18
20
  class BaseApplyConfigurator(ABC):
19
21
  TYPE: ApplyConfigurationType
@@ -82,7 +84,7 @@ class BaseApplyConfigurator(ABC):
82
84
 
83
85
  class ApplyEnvVarsConfiguratorMixin:
84
86
  @classmethod
85
- def register_env_args(cls, parser: argparse.ArgumentParser):
87
+ def register_env_args(cls, parser: ArgsParser):
86
88
  parser.add_argument(
87
89
  "-e",
88
90
  "--env",
@@ -41,10 +41,6 @@ logger = get_logger(__name__)
41
41
  class FleetConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
42
42
  TYPE: ApplyConfigurationType = ApplyConfigurationType.FLEET
43
43
 
44
- @classmethod
45
- def register_args(cls, parser: argparse.ArgumentParser):
46
- cls.register_env_args(parser)
47
-
48
44
  def apply_configuration(
49
45
  self,
50
46
  conf: FleetConfiguration,
@@ -185,20 +181,36 @@ class FleetConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
185
181
 
186
182
  console.print(f"Fleet [code]{conf.name}[/] deleted")
187
183
 
184
+ @classmethod
185
+ def register_args(cls, parser: argparse.ArgumentParser):
186
+ configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
187
+ configuration_group.add_argument(
188
+ "-n",
189
+ "--name",
190
+ dest="name",
191
+ help="The fleet name",
192
+ )
193
+ cls.register_env_args(configuration_group)
194
+
188
195
  def apply_args(self, conf: FleetConfiguration, args: argparse.Namespace, unknown: List[str]):
196
+ if args.name:
197
+ conf.name = args.name
189
198
  self.apply_env_vars(conf.env, args)
190
199
  if conf.ssh_config is None and conf.env:
191
200
  raise ConfigurationError("`env` is currently supported for SSH fleets only")
192
201
 
193
202
 
194
203
  def _preprocess_spec(spec: FleetSpec):
195
- if spec.configuration.ssh_config is not None:
196
- spec.configuration.ssh_config.ssh_key = _resolve_ssh_key(
197
- spec.configuration.ssh_config.identity_file
198
- )
199
- for host in spec.configuration.ssh_config.hosts:
204
+ ssh_config = spec.configuration.ssh_config
205
+ if ssh_config is not None:
206
+ ssh_config.ssh_key = _resolve_ssh_key(ssh_config.identity_file)
207
+ if ssh_config.proxy_jump is not None:
208
+ ssh_config.proxy_jump.ssh_key = _resolve_ssh_key(ssh_config.proxy_jump.identity_file)
209
+ for host in ssh_config.hosts:
200
210
  if not isinstance(host, str):
201
211
  host.ssh_key = _resolve_ssh_key(host.identity_file)
212
+ if host.proxy_jump is not None:
213
+ host.proxy_jump.ssh_key = _resolve_ssh_key(host.proxy_jump.identity_file)
202
214
 
203
215
 
204
216
  def _resolve_ssh_key(ssh_key_path: Optional[str]) -> Optional[SSHKey]:
@@ -39,6 +39,7 @@ class GatewayConfigurator(BaseApplyConfigurator):
39
39
  unknown_args: List[str],
40
40
  repo: Optional[Repo] = None,
41
41
  ):
42
+ self.apply_args(conf, configurator_args, unknown_args)
42
43
  spec = GatewaySpec(
43
44
  configuration=conf,
44
45
  configuration_path=configuration_path,
@@ -170,6 +171,20 @@ class GatewayConfigurator(BaseApplyConfigurator):
170
171
 
171
172
  console.print(f"Gateway [code]{conf.name}[/] deleted")
172
173
 
174
+ @classmethod
175
+ def register_args(cls, parser: argparse.ArgumentParser):
176
+ configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
177
+ configuration_group.add_argument(
178
+ "-n",
179
+ "--name",
180
+ dest="name",
181
+ help="The gateway name",
182
+ )
183
+
184
+ def apply_args(self, conf: GatewayConfiguration, args: argparse.Namespace, unknown: List[str]):
185
+ if args.name:
186
+ conf.name = args.name
187
+
173
188
 
174
189
  def _get_plan(api: Client, spec: GatewaySpec) -> GatewayPlan:
175
190
  # TODO: Implement server-side /get_plan with an offer included
@@ -298,20 +298,21 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
298
298
 
299
299
  @classmethod
300
300
  def register_args(cls, parser: argparse.ArgumentParser):
301
- parser.add_argument(
301
+ configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
302
+ configuration_group.add_argument(
302
303
  "-n",
303
304
  "--name",
304
305
  dest="run_name",
305
306
  help="The name of the run. If not specified, a random name is assigned",
306
307
  )
307
- parser.add_argument(
308
+ configuration_group.add_argument(
308
309
  "--max-offers",
309
310
  help="Number of offers to show in the run plan",
310
311
  type=int,
311
312
  default=3,
312
313
  )
313
- cls.register_env_args(parser)
314
- parser.add_argument(
314
+ cls.register_env_args(configuration_group)
315
+ configuration_group.add_argument(
315
316
  "--gpu",
316
317
  type=gpu_spec,
317
318
  help="Request GPU for the run. "
@@ -319,7 +320,7 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
319
320
  dest="gpu_spec",
320
321
  metavar="SPEC",
321
322
  )
322
- parser.add_argument(
323
+ configuration_group.add_argument(
323
324
  "--disk",
324
325
  type=disk_spec,
325
326
  help="Request the size range of disk for the run. Example [code]--disk 100GB..[/].",
@@ -38,6 +38,7 @@ class VolumeConfigurator(BaseApplyConfigurator):
38
38
  unknown_args: List[str],
39
39
  repo: Optional[Repo] = None,
40
40
  ):
41
+ self.apply_args(conf, configurator_args, unknown_args)
41
42
  spec = VolumeSpec(
42
43
  configuration=conf,
43
44
  configuration_path=configuration_path,
@@ -158,6 +159,20 @@ class VolumeConfigurator(BaseApplyConfigurator):
158
159
 
159
160
  console.print(f"Volume [code]{conf.name}[/] deleted")
160
161
 
162
+ @classmethod
163
+ def register_args(cls, parser: argparse.ArgumentParser):
164
+ configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
165
+ configuration_group.add_argument(
166
+ "-n",
167
+ "--name",
168
+ dest="name",
169
+ help="The volume name",
170
+ )
171
+
172
+ def apply_args(self, conf: VolumeConfiguration, args: argparse.Namespace, unknown: List[str]):
173
+ if args.name:
174
+ conf.name = args.name
175
+
161
176
 
162
177
  def _get_plan(api: Client, spec: VolumeSpec) -> VolumePlan:
163
178
  # TODO: Implement server-side /get_plan with an offer included
@@ -1,7 +1,7 @@
1
- from argparse import ArgumentParser, _ArgumentGroup
2
1
  from pathlib import Path
3
- from typing import Optional, Union
2
+ from typing import Optional
4
3
 
4
+ from dstack._internal.cli.services.configurators.base import ArgsParser
5
5
  from dstack._internal.core.errors import CLIError
6
6
  from dstack._internal.core.models.repos.base import Repo, RepoType
7
7
  from dstack._internal.core.models.repos.remote import GitRepoURL, RemoteRepo, RepoError
@@ -12,7 +12,7 @@ from dstack._internal.utils.path import PathLike
12
12
  from dstack.api._public import Client
13
13
 
14
14
 
15
- def register_init_repo_args(parser: Union[ArgumentParser, _ArgumentGroup]):
15
+ def register_init_repo_args(parser: ArgsParser):
16
16
  parser.add_argument(
17
17
  "-t",
18
18
  "--token",
@@ -2,7 +2,7 @@ from typing import List
2
2
 
3
3
  from rich.table import Table
4
4
 
5
- from dstack._internal.cli.utils.common import console
5
+ from dstack._internal.cli.utils.common import add_row_from_dict, console
6
6
  from dstack._internal.core.models.backends.base import BackendType
7
7
  from dstack._internal.core.models.fleets import Fleet, FleetStatus
8
8
  from dstack._internal.core.models.instances import InstanceStatus
@@ -23,6 +23,8 @@ def get_fleets_table(
23
23
  table.add_column("RESERVATION")
24
24
  table.add_column("INSTANCE")
25
25
  table.add_column("BACKEND")
26
+ if verbose:
27
+ table.add_column("REGION")
26
28
  table.add_column("RESOURCES")
27
29
  table.add_column("PRICE")
28
30
  table.add_column("STATUS")
@@ -41,6 +43,10 @@ def get_fleets_table(
41
43
  resources = instance.instance_type.resources.pretty_format(include_spot=True)
42
44
 
43
45
  status = instance.status.value
46
+ total_blocks = instance.total_blocks
47
+ busy_blocks = instance.busy_blocks
48
+ if total_blocks is not None and total_blocks > 1:
49
+ status = f"{busy_blocks}/{total_blocks} {InstanceStatus.BUSY.value}"
44
50
  if (
45
51
  instance.status in [InstanceStatus.IDLE, InstanceStatus.BUSY]
46
52
  and instance.unreachable
@@ -50,40 +56,45 @@ def get_fleets_table(
50
56
  backend = instance.backend or ""
51
57
  if backend == "remote":
52
58
  backend = "ssh"
53
- if instance.region:
54
- backend += f" ({instance.region})"
55
59
 
56
- row = [
57
- fleet.name if i == 0 else "",
58
- ]
59
- if verbose:
60
- row.append(fleet.spec.configuration.reservation or "" if i == 0 else "")
61
- row += [
62
- str(instance.instance_num),
63
- backend,
64
- resources,
65
- f"${instance.price:.4}" if instance.price is not None else "",
66
- status,
67
- format_date(instance.created),
68
- ]
69
- if verbose:
70
- error = ""
71
- if instance.status == InstanceStatus.TERMINATED and instance.termination_reason:
72
- error = f"{instance.termination_reason}"
73
- row.append(error)
74
-
75
- table.add_row(*row)
60
+ region = ""
61
+ if instance.region:
62
+ region = f"{instance.region}"
63
+ if verbose:
64
+ if instance.availability_zone:
65
+ region += f" ({instance.availability_zone})"
66
+ else:
67
+ backend += f" ({instance.region})"
68
+ error = ""
69
+ if instance.status == InstanceStatus.TERMINATED and instance.termination_reason:
70
+ error = f"{instance.termination_reason}"
71
+ row = {
72
+ "FLEET": fleet.name if i == 0 else "",
73
+ "RESERVATION": fleet.spec.configuration.reservation or "" if i == 0 else "",
74
+ "INSTANCE": str(instance.instance_num),
75
+ "BACKEND": backend,
76
+ "REGION": region,
77
+ "RESOURCES": resources,
78
+ "PRICE": f"${instance.price:.4}" if instance.price is not None else "",
79
+ "STATUS": status,
80
+ "CREATED": format_date(instance.created),
81
+ "ERROR": error,
82
+ }
83
+ add_row_from_dict(table, row)
76
84
 
77
85
  if len(fleet.instances) == 0 and fleet.status != FleetStatus.TERMINATING:
78
- row = [
79
- fleet.name,
80
- "-",
81
- "-",
82
- "-",
83
- "-",
84
- "-",
85
- format_date(fleet.created_at),
86
- ]
87
- table.add_row(*row)
86
+ row = {
87
+ "FLEET": fleet.name,
88
+ "RESERVATION": "-",
89
+ "INSTANCE": "-",
90
+ "BACKEND": "-",
91
+ "REGION": "-",
92
+ "RESOURCES": "-",
93
+ "PRICE": "-",
94
+ "STATUS": "-",
95
+ "CREATED": format_date(fleet.created_at),
96
+ "ERROR": "-",
97
+ }
98
+ add_row_from_dict(table, row)
88
99
 
89
100
  return table
@@ -14,7 +14,12 @@ from dstack._internal.core.models.runs import (
14
14
  RunPlan,
15
15
  )
16
16
  from dstack._internal.core.services.profiles import get_termination
17
- from dstack._internal.utils.common import DateFormatter, format_pretty_duration, pretty_date
17
+ from dstack._internal.utils.common import (
18
+ DateFormatter,
19
+ format_duration_multiunit,
20
+ format_pretty_duration,
21
+ pretty_date,
22
+ )
18
23
  from dstack.api import Run
19
24
 
20
25
 
@@ -96,11 +101,14 @@ def print_run_plan(run_plan: RunPlan, offers_limit: int = 3):
96
101
  InstanceAvailability.BUSY,
97
102
  }:
98
103
  availability = offer.availability.value.replace("_", " ").lower()
104
+ instance = offer.instance.name
105
+ if offer.total_blocks > 1:
106
+ instance += f" ({offer.blocks}/{offer.total_blocks})"
99
107
  offers.add_row(
100
108
  f"{i}",
101
109
  offer.backend.replace("remote", "ssh"),
102
110
  offer.region,
103
- offer.instance.name,
111
+ instance,
104
112
  r.pretty_format(),
105
113
  "yes" if r.spot else "no",
106
114
  f"${offer.price:g}",
@@ -155,19 +163,31 @@ def get_runs_table(
155
163
  add_row_from_dict(table, run_row)
156
164
 
157
165
  for job in run.jobs:
166
+ latest_job_submission = job.job_submissions[-1]
167
+ status = latest_job_submission.status.value
168
+ if verbose and latest_job_submission.inactivity_secs:
169
+ inactive_for = format_duration_multiunit(latest_job_submission.inactivity_secs)
170
+ status += f" (inactive for {inactive_for})"
158
171
  job_row: Dict[Union[str, int], Any] = {
159
172
  "NAME": f" replica={job.job_spec.replica_num} job={job.job_spec.job_num}",
160
- "STATUS": job.job_submissions[-1].status,
161
- "SUBMITTED": format_date(job.job_submissions[-1].submitted_at),
173
+ "STATUS": status,
174
+ "SUBMITTED": format_date(latest_job_submission.submitted_at),
162
175
  "ERROR": _get_job_error(job),
163
176
  }
164
- jpd = job.job_submissions[-1].job_provisioning_data
177
+ jpd = latest_job_submission.job_provisioning_data
165
178
  if jpd is not None:
179
+ resources = jpd.instance_type.resources
180
+ instance = jpd.instance_type.name
181
+ jrd = latest_job_submission.job_runtime_data
182
+ if jrd is not None and jrd.offer is not None:
183
+ resources = jrd.offer.instance.resources
184
+ if jrd.offer.total_blocks > 1:
185
+ instance += f" ({jrd.offer.blocks}/{jrd.offer.total_blocks})"
166
186
  job_row.update(
167
187
  {
168
188
  "BACKEND": f"{jpd.backend.value.replace('remote', 'ssh')} ({jpd.region})",
169
- "INSTANCE": jpd.instance_type.name,
170
- "RESOURCES": jpd.instance_type.resources.pretty_format(include_spot=True),
189
+ "INSTANCE": instance,
190
+ "RESOURCES": resources.pretty_format(include_spot=True),
171
191
  "RESERVATION": jpd.reservation,
172
192
  "PRICE": f"${jpd.price:.4}",
173
193
  }
@@ -2,7 +2,7 @@ from typing import List
2
2
 
3
3
  from rich.table import Table
4
4
 
5
- from dstack._internal.cli.utils.common import console
5
+ from dstack._internal.cli.utils.common import add_row_from_dict, console
6
6
  from dstack._internal.core.models.volumes import Volume
7
7
  from dstack._internal.utils.common import DateFormatter, pretty_date
8
8
 
@@ -19,19 +19,40 @@ def get_volumes_table(
19
19
  table = Table(box=None)
20
20
  table.add_column("NAME", no_wrap=True)
21
21
  table.add_column("BACKEND")
22
+ if verbose:
23
+ table.add_column("REGION")
22
24
  table.add_column("STATUS")
25
+ if verbose:
26
+ table.add_column("ATTACHED")
23
27
  table.add_column("CREATED")
24
28
  if verbose:
25
29
  table.add_column("ERROR")
26
30
 
27
31
  for volume in volumes:
28
- renderables = [
29
- volume.name,
30
- f"{volume.configuration.backend.value} ({volume.configuration.region})",
31
- volume.status,
32
- format_date(volume.created_at),
33
- ]
32
+ backend = f"{volume.configuration.backend.value} ({volume.configuration.region})"
33
+ region = volume.configuration.region
34
34
  if verbose:
35
- renderables.append(volume.status_message)
36
- table.add_row(*renderables)
35
+ backend = volume.configuration.backend.value
36
+ if (
37
+ verbose
38
+ and volume.provisioning_data is not None
39
+ and volume.provisioning_data.availability_zone is not None
40
+ ):
41
+ region += f" ({volume.provisioning_data.availability_zone})"
42
+ attached = "-"
43
+ if volume.attachments is not None:
44
+ attached = ", ".join(
45
+ {va.instance.fleet_name for va in volume.attachments if va.instance.fleet_name}
46
+ )
47
+ attached = attached or "-"
48
+ row = {
49
+ "NAME": volume.name,
50
+ "BACKEND": backend,
51
+ "REGION": region,
52
+ "STATUS": volume.status,
53
+ "ATTACHED": attached,
54
+ "CREATED": format_date(volume.created_at),
55
+ "ERROR": volume.status_message,
56
+ }
57
+ add_row_from_dict(table, row)
37
58
  return table