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.
- dstack/_internal/cli/commands/apply.py +8 -5
- dstack/_internal/cli/services/configurators/base.py +4 -2
- dstack/_internal/cli/services/configurators/fleet.py +21 -9
- dstack/_internal/cli/services/configurators/gateway.py +15 -0
- dstack/_internal/cli/services/configurators/run.py +6 -5
- dstack/_internal/cli/services/configurators/volume.py +15 -0
- dstack/_internal/cli/services/repos.py +3 -3
- dstack/_internal/cli/utils/fleet.py +44 -33
- dstack/_internal/cli/utils/run.py +27 -7
- dstack/_internal/cli/utils/volume.py +30 -9
- dstack/_internal/core/backends/aws/compute.py +94 -53
- dstack/_internal/core/backends/aws/resources.py +22 -12
- dstack/_internal/core/backends/azure/compute.py +2 -0
- dstack/_internal/core/backends/base/compute.py +20 -2
- dstack/_internal/core/backends/gcp/compute.py +32 -24
- dstack/_internal/core/backends/gcp/resources.py +0 -15
- dstack/_internal/core/backends/oci/compute.py +10 -5
- dstack/_internal/core/backends/oci/resources.py +23 -26
- dstack/_internal/core/backends/remote/provisioning.py +65 -27
- dstack/_internal/core/backends/runpod/compute.py +1 -0
- dstack/_internal/core/models/backends/azure.py +3 -1
- dstack/_internal/core/models/configurations.py +24 -1
- dstack/_internal/core/models/fleets.py +46 -0
- dstack/_internal/core/models/instances.py +5 -1
- dstack/_internal/core/models/pools.py +4 -1
- dstack/_internal/core/models/profiles.py +10 -4
- dstack/_internal/core/models/runs.py +23 -3
- dstack/_internal/core/models/volumes.py +26 -0
- dstack/_internal/core/services/ssh/attach.py +92 -53
- dstack/_internal/core/services/ssh/tunnel.py +58 -31
- dstack/_internal/proxy/gateway/routers/registry.py +2 -0
- dstack/_internal/proxy/gateway/schemas/registry.py +2 -0
- dstack/_internal/proxy/gateway/services/registry.py +4 -0
- dstack/_internal/proxy/lib/models.py +3 -0
- dstack/_internal/proxy/lib/services/service_connection.py +8 -1
- dstack/_internal/server/background/tasks/process_instances.py +73 -35
- dstack/_internal/server/background/tasks/process_metrics.py +9 -9
- dstack/_internal/server/background/tasks/process_running_jobs.py +77 -26
- dstack/_internal/server/background/tasks/process_runs.py +2 -12
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +121 -49
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +14 -3
- dstack/_internal/server/background/tasks/process_volumes.py +11 -1
- dstack/_internal/server/migrations/versions/1338b788b612_reverse_job_instance_relationship.py +71 -0
- dstack/_internal/server/migrations/versions/1e76fb0dde87_add_jobmodel_inactivity_secs.py +32 -0
- dstack/_internal/server/migrations/versions/51d45659d574_add_instancemodel_blocks_fields.py +43 -0
- dstack/_internal/server/migrations/versions/63c3f19cb184_add_jobterminationreason_inactivity_.py +83 -0
- dstack/_internal/server/migrations/versions/a751ef183f27_move_attachment_data_to_volumes_.py +34 -0
- dstack/_internal/server/models.py +27 -23
- dstack/_internal/server/routers/runs.py +1 -0
- dstack/_internal/server/schemas/runner.py +1 -0
- dstack/_internal/server/services/backends/configurators/azure.py +34 -8
- dstack/_internal/server/services/config.py +9 -0
- dstack/_internal/server/services/fleets.py +32 -3
- dstack/_internal/server/services/gateways/client.py +9 -1
- dstack/_internal/server/services/jobs/__init__.py +217 -45
- dstack/_internal/server/services/jobs/configurators/base.py +47 -2
- dstack/_internal/server/services/offers.py +96 -10
- dstack/_internal/server/services/pools.py +98 -14
- dstack/_internal/server/services/proxy/repo.py +17 -3
- dstack/_internal/server/services/runner/client.py +9 -6
- dstack/_internal/server/services/runner/ssh.py +33 -5
- dstack/_internal/server/services/runs.py +48 -179
- dstack/_internal/server/services/services/__init__.py +9 -1
- dstack/_internal/server/services/volumes.py +68 -9
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js → main-2ac66bfcbd2e39830b88.js} +30 -31
- dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js.map → main-2ac66bfcbd2e39830b88.js.map} +1 -1
- dstack/_internal/server/statics/{main-fc56d1f4af8e57522a1c.css → main-ad5150a441de98cd8987.css} +1 -1
- dstack/_internal/server/testing/common.py +130 -61
- dstack/_internal/utils/common.py +22 -8
- dstack/_internal/utils/env.py +14 -0
- dstack/_internal/utils/ssh.py +1 -1
- dstack/api/server/_fleets.py +25 -1
- dstack/api/server/_runs.py +23 -2
- dstack/api/server/_volumes.py +12 -1
- dstack/version.py +1 -1
- {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/METADATA +1 -1
- {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/RECORD +104 -93
- tests/_internal/cli/services/configurators/test_profile.py +3 -3
- tests/_internal/core/services/ssh/test_tunnel.py +56 -4
- tests/_internal/proxy/gateway/routers/test_registry.py +30 -7
- tests/_internal/server/background/tasks/test_process_instances.py +138 -20
- tests/_internal/server/background/tasks/test_process_metrics.py +12 -0
- tests/_internal/server/background/tasks/test_process_running_jobs.py +193 -0
- tests/_internal/server/background/tasks/test_process_runs.py +27 -3
- tests/_internal/server/background/tasks/test_process_submitted_jobs.py +53 -6
- tests/_internal/server/background/tasks/test_process_terminating_jobs.py +135 -17
- tests/_internal/server/routers/test_fleets.py +15 -2
- tests/_internal/server/routers/test_pools.py +6 -0
- tests/_internal/server/routers/test_runs.py +27 -0
- tests/_internal/server/routers/test_volumes.py +9 -2
- tests/_internal/server/services/jobs/__init__.py +0 -0
- tests/_internal/server/services/jobs/configurators/__init__.py +0 -0
- tests/_internal/server/services/jobs/configurators/test_base.py +72 -0
- tests/_internal/server/services/runner/test_client.py +22 -3
- tests/_internal/server/services/test_offers.py +167 -0
- tests/_internal/server/services/test_pools.py +109 -1
- tests/_internal/server/services/test_runs.py +5 -41
- tests/_internal/utils/test_common.py +21 -0
- tests/_internal/utils/test_env.py +38 -0
- {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/LICENSE.md +0 -0
- {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/WHEEL +0 -0
- {dstack-0.18.40rc1.dist-info → dstack-0.18.42.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
161
|
-
"SUBMITTED": format_date(
|
|
173
|
+
"STATUS": status,
|
|
174
|
+
"SUBMITTED": format_date(latest_job_submission.submitted_at),
|
|
162
175
|
"ERROR": _get_job_error(job),
|
|
163
176
|
}
|
|
164
|
-
jpd =
|
|
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":
|
|
170
|
-
"RESOURCES":
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|