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.
- 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
|
@@ -7,11 +7,115 @@ import dstack._internal.server.services.pools as services_pools
|
|
|
7
7
|
from dstack._internal.core.models.backends.base import BackendType
|
|
8
8
|
from dstack._internal.core.models.instances import InstanceStatus, InstanceType, Resources
|
|
9
9
|
from dstack._internal.core.models.pools import Instance
|
|
10
|
+
from dstack._internal.core.models.profiles import Profile
|
|
10
11
|
from dstack._internal.server.models import InstanceModel
|
|
11
|
-
from dstack._internal.server.testing.common import
|
|
12
|
+
from dstack._internal.server.testing.common import (
|
|
13
|
+
create_instance,
|
|
14
|
+
create_pool,
|
|
15
|
+
create_project,
|
|
16
|
+
create_user,
|
|
17
|
+
get_volume,
|
|
18
|
+
get_volume_configuration,
|
|
19
|
+
)
|
|
12
20
|
from dstack._internal.utils.common import get_current_datetime
|
|
13
21
|
|
|
14
22
|
|
|
23
|
+
class TestFilterPoolInstances:
|
|
24
|
+
# TODO: Refactor filter_pool_instances to not depend on InstanceModel and simplify tests
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
|
|
27
|
+
async def test_returns_all_instances(self, test_db, session: AsyncSession):
|
|
28
|
+
user = await create_user(session=session)
|
|
29
|
+
project = await create_project(session=session, owner=user)
|
|
30
|
+
pool = await create_pool(session=session, project=project)
|
|
31
|
+
aws_instance = await create_instance(
|
|
32
|
+
session=session,
|
|
33
|
+
project=project,
|
|
34
|
+
pool=pool,
|
|
35
|
+
backend=BackendType.AWS,
|
|
36
|
+
)
|
|
37
|
+
runpod_instance = await create_instance(
|
|
38
|
+
session=session,
|
|
39
|
+
project=project,
|
|
40
|
+
pool=pool,
|
|
41
|
+
backend=BackendType.RUNPOD,
|
|
42
|
+
)
|
|
43
|
+
instances = [aws_instance, runpod_instance]
|
|
44
|
+
res = services_pools.filter_pool_instances(
|
|
45
|
+
pool_instances=instances,
|
|
46
|
+
profile=Profile(name="test"),
|
|
47
|
+
)
|
|
48
|
+
assert res == instances
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
|
|
52
|
+
async def test_returns_multinode_instances(self, test_db, session: AsyncSession):
|
|
53
|
+
user = await create_user(session=session)
|
|
54
|
+
project = await create_project(session=session, owner=user)
|
|
55
|
+
pool = await create_pool(session=session, project=project)
|
|
56
|
+
aws_instance = await create_instance(
|
|
57
|
+
session=session,
|
|
58
|
+
project=project,
|
|
59
|
+
pool=pool,
|
|
60
|
+
backend=BackendType.AWS,
|
|
61
|
+
)
|
|
62
|
+
runpod_instance = await create_instance(
|
|
63
|
+
session=session,
|
|
64
|
+
project=project,
|
|
65
|
+
pool=pool,
|
|
66
|
+
backend=BackendType.RUNPOD,
|
|
67
|
+
)
|
|
68
|
+
instances = [aws_instance, runpod_instance]
|
|
69
|
+
res = services_pools.filter_pool_instances(
|
|
70
|
+
pool_instances=instances,
|
|
71
|
+
profile=Profile(name="test"),
|
|
72
|
+
multinode=True,
|
|
73
|
+
)
|
|
74
|
+
assert res == [aws_instance]
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
|
|
78
|
+
async def test_returns_volume_instances(self, test_db, session: AsyncSession):
|
|
79
|
+
user = await create_user(session=session)
|
|
80
|
+
project = await create_project(session=session, owner=user)
|
|
81
|
+
pool = await create_pool(session=session, project=project)
|
|
82
|
+
aws_instance = await create_instance(
|
|
83
|
+
session=session,
|
|
84
|
+
project=project,
|
|
85
|
+
pool=pool,
|
|
86
|
+
backend=BackendType.AWS,
|
|
87
|
+
)
|
|
88
|
+
runpod_instance1 = await create_instance(
|
|
89
|
+
session=session,
|
|
90
|
+
project=project,
|
|
91
|
+
pool=pool,
|
|
92
|
+
backend=BackendType.RUNPOD,
|
|
93
|
+
region="eu",
|
|
94
|
+
)
|
|
95
|
+
runpod_instance2 = await create_instance(
|
|
96
|
+
session=session,
|
|
97
|
+
project=project,
|
|
98
|
+
pool=pool,
|
|
99
|
+
backend=BackendType.RUNPOD,
|
|
100
|
+
region="us",
|
|
101
|
+
)
|
|
102
|
+
instances = [aws_instance, runpod_instance1, runpod_instance2]
|
|
103
|
+
res = services_pools.filter_pool_instances(
|
|
104
|
+
pool_instances=instances,
|
|
105
|
+
profile=Profile(name="test"),
|
|
106
|
+
volumes=[
|
|
107
|
+
[
|
|
108
|
+
get_volume(
|
|
109
|
+
configuration=get_volume_configuration(
|
|
110
|
+
backend=BackendType.RUNPOD, region="us"
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
assert res == [runpod_instance2]
|
|
117
|
+
|
|
118
|
+
|
|
15
119
|
class TestGenerateInstanceName:
|
|
16
120
|
@pytest.mark.asyncio
|
|
17
121
|
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
|
|
@@ -66,6 +170,8 @@ class TestInstanceModelToInstance:
|
|
|
66
170
|
created=created,
|
|
67
171
|
region="eu-west-1",
|
|
68
172
|
price=1.0,
|
|
173
|
+
total_blocks=1,
|
|
174
|
+
busy_blocks=0,
|
|
69
175
|
)
|
|
70
176
|
im = InstanceModel(
|
|
71
177
|
id=instance_id,
|
|
@@ -78,6 +184,8 @@ class TestInstanceModelToInstance:
|
|
|
78
184
|
pool=None,
|
|
79
185
|
job_provisioning_data='{"ssh_proxy":null, "backend":"local","hostname":"hostname_test","region":"eu-west","price":1.0,"username":"user1","ssh_port":12345,"dockerized":false,"instance_id":"test_instance","instance_type": {"name": "instance", "resources": {"cpus": 1, "memory_mib": 512, "gpus": [], "spot": false, "disk": {"size_mib": 102400}, "description":""}}}',
|
|
80
186
|
offer='{"price":"LOCAL", "price":1.0, "backend":"local", "region":"eu-west-1", "availability":"available","instance": {"name": "instance", "resources": {"cpus": 1, "memory_mib": 512, "gpus": [], "spot": false, "disk": {"size_mib": 102400}, "description":""}}}',
|
|
187
|
+
total_blocks=1,
|
|
188
|
+
busy_blocks=0,
|
|
81
189
|
)
|
|
82
190
|
instance = services_pools.instance_model_to_instance(im)
|
|
83
191
|
assert instance == expected_instance
|
|
@@ -10,9 +10,9 @@ from dstack._internal.core.models.configurations import ScalingSpec, ServiceConf
|
|
|
10
10
|
from dstack._internal.core.models.profiles import Profile
|
|
11
11
|
from dstack._internal.core.models.resources import Range
|
|
12
12
|
from dstack._internal.core.models.runs import JobStatus, JobTerminationReason, RunStatus
|
|
13
|
-
from dstack._internal.core.models.volumes import VolumeMountPoint
|
|
14
13
|
from dstack._internal.server.models import RunModel
|
|
15
|
-
from dstack._internal.server.services.
|
|
14
|
+
from dstack._internal.server.services.jobs import check_can_attach_job_volumes
|
|
15
|
+
from dstack._internal.server.services.runs import scale_run_replicas
|
|
16
16
|
from dstack._internal.server.testing.common import (
|
|
17
17
|
create_job,
|
|
18
18
|
create_pool,
|
|
@@ -241,19 +241,7 @@ class TestCanAttachRunVolumes:
|
|
|
241
241
|
vol22.configuration.backend = BackendType.AWS
|
|
242
242
|
vol22.configuration.region = "eu-west-2"
|
|
243
243
|
volumes = [[vol11, vol12], [vol21, vol22]]
|
|
244
|
-
|
|
245
|
-
run_name="test_run",
|
|
246
|
-
repo_id="test_repo",
|
|
247
|
-
configuration=ServiceConfiguration(
|
|
248
|
-
port=80,
|
|
249
|
-
commands=[""],
|
|
250
|
-
volumes=[
|
|
251
|
-
VolumeMountPoint(name=["vol11", "vol12"], path="/vol1"),
|
|
252
|
-
VolumeMountPoint(name=["vol21", "vol22"], path="/vol2"),
|
|
253
|
-
],
|
|
254
|
-
),
|
|
255
|
-
)
|
|
256
|
-
check_can_attach_run_volumes(run_spec, volumes)
|
|
244
|
+
check_can_attach_job_volumes(volumes)
|
|
257
245
|
|
|
258
246
|
@pytest.mark.asyncio
|
|
259
247
|
async def test_cannot_attach_different_mount_points_with_different_backends_regions(self):
|
|
@@ -264,20 +252,8 @@ class TestCanAttachRunVolumes:
|
|
|
264
252
|
vol2.configuration.backend = BackendType.AWS
|
|
265
253
|
vol2.configuration.region = "eu-west-2"
|
|
266
254
|
volumes = [[vol1], [vol2]]
|
|
267
|
-
run_spec = get_run_spec(
|
|
268
|
-
run_name="test_run",
|
|
269
|
-
repo_id="test_repo",
|
|
270
|
-
configuration=ServiceConfiguration(
|
|
271
|
-
port=80,
|
|
272
|
-
commands=[""],
|
|
273
|
-
volumes=[
|
|
274
|
-
VolumeMountPoint(name=["vol1"], path="/vol1"),
|
|
275
|
-
VolumeMountPoint(name=["vol2"], path="/vol2"),
|
|
276
|
-
],
|
|
277
|
-
),
|
|
278
|
-
)
|
|
279
255
|
with pytest.raises(ServerClientError):
|
|
280
|
-
|
|
256
|
+
check_can_attach_job_volumes(volumes)
|
|
281
257
|
|
|
282
258
|
@pytest.mark.asyncio
|
|
283
259
|
async def test_cannot_attach_same_volume_at_different_mount_points(self):
|
|
@@ -285,17 +261,5 @@ class TestCanAttachRunVolumes:
|
|
|
285
261
|
vol1.configuration.backend = BackendType.AWS
|
|
286
262
|
vol1.configuration.region = "eu-west-1"
|
|
287
263
|
volumes = [[vol1], [vol1]]
|
|
288
|
-
run_spec = get_run_spec(
|
|
289
|
-
run_name="test_run",
|
|
290
|
-
repo_id="test_repo",
|
|
291
|
-
configuration=ServiceConfiguration(
|
|
292
|
-
port=80,
|
|
293
|
-
commands=[""],
|
|
294
|
-
volumes=[
|
|
295
|
-
VolumeMountPoint(name=["vol1"], path="/vol1"),
|
|
296
|
-
VolumeMountPoint(name=["vol1"], path="/vol2"),
|
|
297
|
-
],
|
|
298
|
-
),
|
|
299
|
-
)
|
|
300
264
|
with pytest.raises(ServerClientError):
|
|
301
|
-
|
|
265
|
+
check_can_attach_job_volumes(volumes)
|
|
@@ -6,6 +6,7 @@ from freezegun import freeze_time
|
|
|
6
6
|
|
|
7
7
|
from dstack._internal.utils.common import (
|
|
8
8
|
concat_url_path,
|
|
9
|
+
format_duration_multiunit,
|
|
9
10
|
local_time,
|
|
10
11
|
make_proxy_url,
|
|
11
12
|
parse_memory,
|
|
@@ -87,6 +88,26 @@ class TestPrettyDate:
|
|
|
87
88
|
assert pretty_date(future_time) == ""
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
class TestFormatDurationMultiunit:
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
("input", "output"),
|
|
94
|
+
[
|
|
95
|
+
(0, "0s"),
|
|
96
|
+
(59, "59s"),
|
|
97
|
+
(60, "1m"),
|
|
98
|
+
(61, "1m 1s"),
|
|
99
|
+
(694861, "1w 1d 1h 1m 1s"),
|
|
100
|
+
(86401, "1d 1s"),
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
def test(self, input: int, output: str) -> None:
|
|
104
|
+
assert format_duration_multiunit(input) == output
|
|
105
|
+
|
|
106
|
+
def test_forbids_negative(self) -> None:
|
|
107
|
+
with pytest.raises(ValueError):
|
|
108
|
+
format_duration_multiunit(-1)
|
|
109
|
+
|
|
110
|
+
|
|
90
111
|
class TestParseMemory:
|
|
91
112
|
@pytest.mark.parametrize(
|
|
92
113
|
"memory,as_units,expected",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from dstack._internal.utils.env import get_bool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize(
|
|
7
|
+
["value", "expected"],
|
|
8
|
+
[
|
|
9
|
+
["0", False],
|
|
10
|
+
["1", True],
|
|
11
|
+
["true", True],
|
|
12
|
+
["True", True],
|
|
13
|
+
["FALSE", False],
|
|
14
|
+
["off", False],
|
|
15
|
+
["ON", True],
|
|
16
|
+
],
|
|
17
|
+
)
|
|
18
|
+
def test_get_bool_is_set(monkeypatch: pytest.MonkeyPatch, value: str, expected: bool):
|
|
19
|
+
monkeypatch.setenv("VAR", value)
|
|
20
|
+
assert get_bool("VAR") is expected
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_get_bool_not_set_default_not_set(monkeypatch: pytest.MonkeyPatch):
|
|
24
|
+
monkeypatch.delenv("VAR", raising=False)
|
|
25
|
+
assert get_bool("VAR") is False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize("default", [False, True])
|
|
29
|
+
def test_get_bool_not_set_default_is_set(monkeypatch: pytest.MonkeyPatch, default: bool):
|
|
30
|
+
monkeypatch.delenv("VAR", raising=False)
|
|
31
|
+
assert get_bool("VAR", default) is default
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize("value", ["", "2", "foo"])
|
|
35
|
+
def test_get_bool_error_value(monkeypatch: pytest.MonkeyPatch, value: str):
|
|
36
|
+
monkeypatch.setenv("VAR", value)
|
|
37
|
+
with pytest.raises(ValueError, match=f"VAR={value}"):
|
|
38
|
+
assert get_bool("VAR")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|