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.
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
@@ -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 create_project, create_user
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.runs import check_can_attach_run_volumes, scale_run_replicas
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
- run_spec = get_run_spec(
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
- check_can_attach_run_volumes(run_spec, volumes)
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
- check_can_attach_run_volumes(run_spec, volumes)
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")