dstack 0.18.40rc1__py3-none-any.whl → 0.18.41__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 +21 -9
- dstack/_internal/core/backends/aws/compute.py +92 -52
- 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 +30 -23
- 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 +20 -0
- dstack/_internal/core/models/volumes.py +3 -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 +72 -33
- dstack/_internal/server/background/tasks/process_metrics.py +9 -9
- dstack/_internal/server/background/tasks/process_running_jobs.py +73 -26
- dstack/_internal/server/background/tasks/process_runs.py +2 -12
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +109 -42
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +1 -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/models.py +10 -4
- 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 +27 -2
- dstack/_internal/server/services/gateways/client.py +9 -1
- dstack/_internal/server/services/jobs/__init__.py +215 -43
- dstack/_internal/server/services/jobs/configurators/base.py +47 -2
- dstack/_internal/server/services/offers.py +91 -5
- dstack/_internal/server/services/pools.py +95 -11
- dstack/_internal/server/services/proxy/repo.py +17 -3
- dstack/_internal/server/services/runner/client.py +1 -1
- 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/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 +117 -52
- 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.41.dist-info}/METADATA +1 -1
- {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/RECORD +98 -89
- 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 +192 -0
- tests/_internal/server/background/tasks/test_process_runs.py +27 -3
- tests/_internal/server/background/tasks/test_process_submitted_jobs.py +48 -3
- tests/_internal/server/background/tasks/test_process_terminating_jobs.py +126 -13
- 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/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/test_pools.py +4 -0
- 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.41.dist-info}/LICENSE.md +0 -0
- {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/WHEEL +0 -0
- {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/entry_points.txt +0 -0
- {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/top_level.txt +0 -0
|
@@ -340,6 +340,7 @@ class TestCreateFleet:
|
|
|
340
340
|
},
|
|
341
341
|
"backends": None,
|
|
342
342
|
"regions": None,
|
|
343
|
+
"availability_zones": None,
|
|
343
344
|
"instance_types": None,
|
|
344
345
|
"spot_policy": None,
|
|
345
346
|
"retry": None,
|
|
@@ -350,10 +351,12 @@ class TestCreateFleet:
|
|
|
350
351
|
"type": "fleet",
|
|
351
352
|
"name": "test-fleet",
|
|
352
353
|
"reservation": None,
|
|
354
|
+
"blocks": 1,
|
|
353
355
|
},
|
|
354
356
|
"profile": {
|
|
355
357
|
"backends": None,
|
|
356
358
|
"regions": None,
|
|
359
|
+
"availability_zones": None,
|
|
357
360
|
"instance_types": None,
|
|
358
361
|
"spot_policy": None,
|
|
359
362
|
"retry": None,
|
|
@@ -393,15 +396,18 @@ class TestCreateFleet:
|
|
|
393
396
|
"pool_name": None,
|
|
394
397
|
"backend": None,
|
|
395
398
|
"region": None,
|
|
399
|
+
"availability_zone": None,
|
|
396
400
|
"instance_type": None,
|
|
397
401
|
"price": None,
|
|
402
|
+
"total_blocks": 1,
|
|
403
|
+
"busy_blocks": 0,
|
|
398
404
|
}
|
|
399
405
|
],
|
|
400
406
|
}
|
|
401
407
|
res = await session.execute(select(FleetModel))
|
|
402
408
|
assert res.scalar_one()
|
|
403
409
|
res = await session.execute(select(InstanceModel))
|
|
404
|
-
assert res.scalar_one()
|
|
410
|
+
assert res.unique().scalar_one()
|
|
405
411
|
|
|
406
412
|
@pytest.mark.asyncio
|
|
407
413
|
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
|
|
@@ -444,6 +450,7 @@ class TestCreateFleet:
|
|
|
444
450
|
"port": None,
|
|
445
451
|
"identity_file": None,
|
|
446
452
|
"ssh_key": None, # should not return ssh_key
|
|
453
|
+
"proxy_jump": None,
|
|
447
454
|
"hosts": ["1.1.1.1"],
|
|
448
455
|
"network": None,
|
|
449
456
|
},
|
|
@@ -458,6 +465,7 @@ class TestCreateFleet:
|
|
|
458
465
|
},
|
|
459
466
|
"backends": None,
|
|
460
467
|
"regions": None,
|
|
468
|
+
"availability_zones": None,
|
|
461
469
|
"instance_types": None,
|
|
462
470
|
"spot_policy": None,
|
|
463
471
|
"retry": None,
|
|
@@ -468,10 +476,12 @@ class TestCreateFleet:
|
|
|
468
476
|
"type": "fleet",
|
|
469
477
|
"name": spec.configuration.name,
|
|
470
478
|
"reservation": None,
|
|
479
|
+
"blocks": 1,
|
|
471
480
|
},
|
|
472
481
|
"profile": {
|
|
473
482
|
"backends": None,
|
|
474
483
|
"regions": None,
|
|
484
|
+
"availability_zones": None,
|
|
475
485
|
"instance_types": None,
|
|
476
486
|
"spot_policy": None,
|
|
477
487
|
"retry": None,
|
|
@@ -522,14 +532,17 @@ class TestCreateFleet:
|
|
|
522
532
|
"termination_reason": None,
|
|
523
533
|
"created": "2023-01-02T03:04:00+00:00",
|
|
524
534
|
"region": "remote",
|
|
535
|
+
"availability_zone": None,
|
|
525
536
|
"price": 0.0,
|
|
537
|
+
"total_blocks": 1,
|
|
538
|
+
"busy_blocks": 0,
|
|
526
539
|
}
|
|
527
540
|
],
|
|
528
541
|
}
|
|
529
542
|
res = await session.execute(select(FleetModel))
|
|
530
543
|
assert res.scalar_one()
|
|
531
544
|
res = await session.execute(select(InstanceModel))
|
|
532
|
-
instance = res.scalar_one()
|
|
545
|
+
instance = res.unique().scalar_one()
|
|
533
546
|
assert instance.remote_connection_info is not None
|
|
534
547
|
|
|
535
548
|
@pytest.mark.asyncio
|
|
@@ -332,7 +332,10 @@ class TestShowPool:
|
|
|
332
332
|
"created": "2023-01-02T03:04:00+00:00",
|
|
333
333
|
"pool_name": None,
|
|
334
334
|
"region": "en",
|
|
335
|
+
"availability_zone": None,
|
|
335
336
|
"price": 1,
|
|
337
|
+
"total_blocks": 1,
|
|
338
|
+
"busy_blocks": 0,
|
|
336
339
|
}
|
|
337
340
|
],
|
|
338
341
|
}
|
|
@@ -503,7 +506,10 @@ class TestRemoveInstance:
|
|
|
503
506
|
"created": "2023-01-02T03:04:00+00:00",
|
|
504
507
|
"pool_name": None,
|
|
505
508
|
"region": "en",
|
|
509
|
+
"availability_zone": None,
|
|
506
510
|
"price": 1,
|
|
511
|
+
"total_blocks": 1,
|
|
512
|
+
"busy_blocks": 0,
|
|
507
513
|
}
|
|
508
514
|
],
|
|
509
515
|
}
|
|
@@ -18,6 +18,7 @@ from dstack._internal.core.models.gateways import GatewayStatus
|
|
|
18
18
|
from dstack._internal.core.models.instances import (
|
|
19
19
|
InstanceAvailability,
|
|
20
20
|
InstanceOfferWithAvailability,
|
|
21
|
+
InstanceStatus,
|
|
21
22
|
InstanceType,
|
|
22
23
|
Resources,
|
|
23
24
|
)
|
|
@@ -47,7 +48,9 @@ from dstack._internal.server.testing.common import (
|
|
|
47
48
|
create_backend,
|
|
48
49
|
create_gateway,
|
|
49
50
|
create_gateway_compute,
|
|
51
|
+
create_instance,
|
|
50
52
|
create_job,
|
|
53
|
+
create_pool,
|
|
51
54
|
create_project,
|
|
52
55
|
create_repo,
|
|
53
56
|
create_run,
|
|
@@ -85,6 +88,7 @@ def get_dev_env_run_plan_dict(
|
|
|
85
88
|
"working_dir": None,
|
|
86
89
|
"home_dir": "/root",
|
|
87
90
|
"ide": "vscode",
|
|
91
|
+
"inactivity_duration": None,
|
|
88
92
|
"version": None,
|
|
89
93
|
"image": None,
|
|
90
94
|
"user": None,
|
|
@@ -107,6 +111,7 @@ def get_dev_env_run_plan_dict(
|
|
|
107
111
|
"volumes": [json.loads(v.json()) for v in volumes],
|
|
108
112
|
"backends": ["local", "aws", "azure", "gcp", "lambda", "runpod"],
|
|
109
113
|
"regions": ["us"],
|
|
114
|
+
"availability_zones": None,
|
|
110
115
|
"instance_types": None,
|
|
111
116
|
"creation_policy": None,
|
|
112
117
|
"instance_name": None,
|
|
@@ -127,6 +132,7 @@ def get_dev_env_run_plan_dict(
|
|
|
127
132
|
"profile": {
|
|
128
133
|
"backends": ["local", "aws", "azure", "gcp", "lambda", "runpod"],
|
|
129
134
|
"regions": ["us"],
|
|
135
|
+
"availability_zones": None,
|
|
130
136
|
"instance_types": None,
|
|
131
137
|
"creation_policy": None,
|
|
132
138
|
"default": False,
|
|
@@ -198,6 +204,7 @@ def get_dev_env_run_plan_dict(
|
|
|
198
204
|
"reservation": None,
|
|
199
205
|
},
|
|
200
206
|
"retry": None,
|
|
207
|
+
"volumes": volumes,
|
|
201
208
|
"retry_policy": {"retry": False, "duration": None},
|
|
202
209
|
"working_dir": ".",
|
|
203
210
|
},
|
|
@@ -238,6 +245,7 @@ def get_dev_env_run_dict(
|
|
|
238
245
|
"home_dir": "/root",
|
|
239
246
|
"working_dir": None,
|
|
240
247
|
"ide": "vscode",
|
|
248
|
+
"inactivity_duration": None,
|
|
241
249
|
"version": None,
|
|
242
250
|
"image": None,
|
|
243
251
|
"user": None,
|
|
@@ -260,6 +268,7 @@ def get_dev_env_run_dict(
|
|
|
260
268
|
"volumes": [],
|
|
261
269
|
"backends": ["local", "aws", "azure", "gcp", "lambda"],
|
|
262
270
|
"regions": ["us"],
|
|
271
|
+
"availability_zones": None,
|
|
263
272
|
"instance_types": None,
|
|
264
273
|
"creation_policy": None,
|
|
265
274
|
"instance_name": None,
|
|
@@ -280,6 +289,7 @@ def get_dev_env_run_dict(
|
|
|
280
289
|
"profile": {
|
|
281
290
|
"backends": ["local", "aws", "azure", "gcp", "lambda"],
|
|
282
291
|
"regions": ["us"],
|
|
292
|
+
"availability_zones": None,
|
|
283
293
|
"instance_types": None,
|
|
284
294
|
"creation_policy": None,
|
|
285
295
|
"default": False,
|
|
@@ -351,6 +361,7 @@ def get_dev_env_run_dict(
|
|
|
351
361
|
"reservation": None,
|
|
352
362
|
},
|
|
353
363
|
"retry": None,
|
|
364
|
+
"volumes": [],
|
|
354
365
|
"retry_policy": {"retry": False, "duration": None},
|
|
355
366
|
"working_dir": ".",
|
|
356
367
|
},
|
|
@@ -361,6 +372,7 @@ def get_dev_env_run_dict(
|
|
|
361
372
|
"submitted_at": submitted_at,
|
|
362
373
|
"last_processed_at": last_processed_at,
|
|
363
374
|
"finished_at": finished_at,
|
|
375
|
+
"inactivity_secs": None,
|
|
364
376
|
"status": "submitted",
|
|
365
377
|
"termination_reason": None,
|
|
366
378
|
"termination_reason_message": None,
|
|
@@ -375,6 +387,7 @@ def get_dev_env_run_dict(
|
|
|
375
387
|
"submission_num": 0,
|
|
376
388
|
"submitted_at": submitted_at,
|
|
377
389
|
"last_processed_at": last_processed_at,
|
|
390
|
+
"inactivity_secs": None,
|
|
378
391
|
"finished_at": finished_at,
|
|
379
392
|
"status": "submitted",
|
|
380
393
|
"termination_reason": None,
|
|
@@ -487,6 +500,7 @@ class TestListRuns:
|
|
|
487
500
|
"submitted_at": run1_submitted_at.isoformat(),
|
|
488
501
|
"last_processed_at": run1_submitted_at.isoformat(),
|
|
489
502
|
"finished_at": None,
|
|
503
|
+
"inactivity_secs": None,
|
|
490
504
|
"status": "submitted",
|
|
491
505
|
"termination_reason": None,
|
|
492
506
|
"termination_reason_message": None,
|
|
@@ -502,6 +516,7 @@ class TestListRuns:
|
|
|
502
516
|
"submitted_at": run1_submitted_at.isoformat(),
|
|
503
517
|
"last_processed_at": run1_submitted_at.isoformat(),
|
|
504
518
|
"finished_at": None,
|
|
519
|
+
"inactivity_secs": None,
|
|
505
520
|
"status": "submitted",
|
|
506
521
|
"termination_reason_message": None,
|
|
507
522
|
"termination_reason": None,
|
|
@@ -1303,11 +1318,20 @@ class TestStopRuns:
|
|
|
1303
1318
|
user=user,
|
|
1304
1319
|
status=RunStatus.RUNNING,
|
|
1305
1320
|
)
|
|
1321
|
+
pool = await create_pool(session=session, project=project)
|
|
1322
|
+
instance = await create_instance(
|
|
1323
|
+
session=session,
|
|
1324
|
+
project=project,
|
|
1325
|
+
pool=pool,
|
|
1326
|
+
status=InstanceStatus.BUSY,
|
|
1327
|
+
)
|
|
1306
1328
|
job = await create_job(
|
|
1307
1329
|
session=session,
|
|
1308
1330
|
run=run,
|
|
1309
1331
|
job_provisioning_data=get_job_provisioning_data(),
|
|
1310
1332
|
status=JobStatus.RUNNING,
|
|
1333
|
+
instance=instance,
|
|
1334
|
+
instance_assigned=True,
|
|
1311
1335
|
)
|
|
1312
1336
|
with patch("dstack._internal.server.services.jobs._stop_runner") as stop_runner:
|
|
1313
1337
|
response = await client.post(
|
|
@@ -1533,7 +1557,10 @@ class TestCreateInstance:
|
|
|
1533
1557
|
"created": result["created"],
|
|
1534
1558
|
"pool_name": None,
|
|
1535
1559
|
"region": None,
|
|
1560
|
+
"availability_zone": None,
|
|
1536
1561
|
"price": None,
|
|
1562
|
+
"total_blocks": 1,
|
|
1563
|
+
"busy_blocks": 0,
|
|
1537
1564
|
}
|
|
1538
1565
|
assert result == expected
|
|
1539
1566
|
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from dstack._internal.core.errors import ServerClientError
|
|
6
|
+
from dstack._internal.core.models.volumes import InstanceMountPoint, MountPoint, VolumeMountPoint
|
|
7
|
+
from dstack._internal.server.services.jobs.configurators.base import interpolate_job_volumes
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestInterpolateJobVolumes:
|
|
11
|
+
@pytest.mark.parametrize(
|
|
12
|
+
["run_volumes", "job_num", "job_volumes"],
|
|
13
|
+
[
|
|
14
|
+
pytest.param(
|
|
15
|
+
[VolumeMountPoint(name="volume", path="/volume")],
|
|
16
|
+
0,
|
|
17
|
+
[VolumeMountPoint(name=["volume"], path="/volume")],
|
|
18
|
+
id="no_interpolation",
|
|
19
|
+
),
|
|
20
|
+
pytest.param(
|
|
21
|
+
[InstanceMountPoint(instance_path="/volume", path="/volume")],
|
|
22
|
+
0,
|
|
23
|
+
[InstanceMountPoint(instance_path="/volume", path="/volume")],
|
|
24
|
+
id="instance_mount",
|
|
25
|
+
),
|
|
26
|
+
pytest.param(
|
|
27
|
+
[
|
|
28
|
+
VolumeMountPoint(
|
|
29
|
+
name="job${{dstack.job_num}}-rank${{dstack.node_rank}}", path="/volume"
|
|
30
|
+
)
|
|
31
|
+
],
|
|
32
|
+
2,
|
|
33
|
+
[VolumeMountPoint(name=["job2-rank2"], path="/volume")],
|
|
34
|
+
id="job_num_and_node_rank",
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
def test_interpolates_volumes(
|
|
39
|
+
self,
|
|
40
|
+
run_volumes: list[Union[MountPoint, str]],
|
|
41
|
+
job_num: int,
|
|
42
|
+
job_volumes: list[MountPoint],
|
|
43
|
+
):
|
|
44
|
+
assert interpolate_job_volumes(run_volumes, job_num) == job_volumes
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize(
|
|
47
|
+
["run_volumes", "job_num"],
|
|
48
|
+
[
|
|
49
|
+
pytest.param(
|
|
50
|
+
[VolumeMountPoint(name="${{}", path="/volume")],
|
|
51
|
+
0,
|
|
52
|
+
id="invalid_syntax",
|
|
53
|
+
),
|
|
54
|
+
pytest.param(
|
|
55
|
+
[VolumeMountPoint(name="${{ unknown.namespace }}", path="/volume")],
|
|
56
|
+
0,
|
|
57
|
+
id="unknown_namespace",
|
|
58
|
+
),
|
|
59
|
+
pytest.param(
|
|
60
|
+
[VolumeMountPoint(name="${{ dstack.var }}", path="/volume")],
|
|
61
|
+
0,
|
|
62
|
+
id="unknown_var",
|
|
63
|
+
),
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
def test_raises_server_client_error(
|
|
67
|
+
self,
|
|
68
|
+
run_volumes: list[Union[MountPoint, str]],
|
|
69
|
+
job_num: int,
|
|
70
|
+
):
|
|
71
|
+
with pytest.raises(ServerClientError):
|
|
72
|
+
assert interpolate_job_volumes(run_volumes, job_num)
|
|
@@ -66,6 +66,8 @@ class TestInstanceModelToInstance:
|
|
|
66
66
|
created=created,
|
|
67
67
|
region="eu-west-1",
|
|
68
68
|
price=1.0,
|
|
69
|
+
total_blocks=1,
|
|
70
|
+
busy_blocks=0,
|
|
69
71
|
)
|
|
70
72
|
im = InstanceModel(
|
|
71
73
|
id=instance_id,
|
|
@@ -78,6 +80,8 @@ class TestInstanceModelToInstance:
|
|
|
78
80
|
pool=None,
|
|
79
81
|
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
82
|
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":""}}}',
|
|
83
|
+
total_blocks=1,
|
|
84
|
+
busy_blocks=0,
|
|
81
85
|
)
|
|
82
86
|
instance = services_pools.instance_model_to_instance(im)
|
|
83
87
|
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
|