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
|
@@ -5,6 +5,7 @@ from datetime import datetime, timezone
|
|
|
5
5
|
from typing import Dict, List, Optional, Union
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
|
+
import gpuhunt
|
|
8
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
10
|
|
|
10
11
|
from dstack._internal.core.models.backends.base import BackendType
|
|
@@ -13,14 +14,20 @@ from dstack._internal.core.models.configurations import (
|
|
|
13
14
|
AnyRunConfiguration,
|
|
14
15
|
DevEnvironmentConfiguration,
|
|
15
16
|
)
|
|
17
|
+
from dstack._internal.core.models.envs import Env
|
|
16
18
|
from dstack._internal.core.models.fleets import FleetConfiguration, FleetSpec, FleetStatus
|
|
17
19
|
from dstack._internal.core.models.gateways import GatewayStatus
|
|
18
20
|
from dstack._internal.core.models.instances import (
|
|
21
|
+
Disk,
|
|
22
|
+
Gpu,
|
|
23
|
+
InstanceAvailability,
|
|
19
24
|
InstanceConfiguration,
|
|
25
|
+
InstanceOfferWithAvailability,
|
|
20
26
|
InstanceStatus,
|
|
21
27
|
InstanceType,
|
|
22
28
|
RemoteConnectionInfo,
|
|
23
29
|
Resources,
|
|
30
|
+
SSHKey,
|
|
24
31
|
)
|
|
25
32
|
from dstack._internal.core.models.placement import (
|
|
26
33
|
PlacementGroupConfiguration,
|
|
@@ -311,17 +318,30 @@ async def create_job(
|
|
|
311
318
|
return job
|
|
312
319
|
|
|
313
320
|
|
|
314
|
-
def get_job_provisioning_data(
|
|
321
|
+
def get_job_provisioning_data(
|
|
322
|
+
dockerized: bool = False,
|
|
323
|
+
backend: BackendType = BackendType.AWS,
|
|
324
|
+
region: str = "us-east-1",
|
|
325
|
+
gpu_count: int = 0,
|
|
326
|
+
cpu_count: int = 1,
|
|
327
|
+
memory_gib: float = 0.5,
|
|
328
|
+
spot: bool = False,
|
|
329
|
+
hostname: str = "127.0.0.4",
|
|
330
|
+
internal_ip: Optional[str] = "127.0.0.4",
|
|
331
|
+
) -> JobProvisioningData:
|
|
332
|
+
gpus = [Gpu(name="T4", memory_mib=16384, vendor=gpuhunt.AcceleratorVendor.NVIDIA)] * gpu_count
|
|
315
333
|
return JobProvisioningData(
|
|
316
|
-
backend=
|
|
334
|
+
backend=backend,
|
|
317
335
|
instance_type=InstanceType(
|
|
318
336
|
name="instance",
|
|
319
|
-
resources=Resources(
|
|
337
|
+
resources=Resources(
|
|
338
|
+
cpus=cpu_count, memory_mib=int(memory_gib * 1024), spot=spot, gpus=gpus
|
|
339
|
+
),
|
|
320
340
|
),
|
|
321
341
|
instance_id="instance_id",
|
|
322
|
-
hostname=
|
|
323
|
-
internal_ip=
|
|
324
|
-
region=
|
|
342
|
+
hostname=hostname,
|
|
343
|
+
internal_ip=internal_ip,
|
|
344
|
+
region=region,
|
|
325
345
|
price=10.5,
|
|
326
346
|
username="ubuntu",
|
|
327
347
|
ssh_port=22,
|
|
@@ -337,6 +357,8 @@ def get_job_runtime_data(
|
|
|
337
357
|
gpu: Optional[int] = None,
|
|
338
358
|
memory: Optional[float] = None,
|
|
339
359
|
ports: Optional[dict[int, int]] = None,
|
|
360
|
+
offer: Optional[InstanceOfferWithAvailability] = None,
|
|
361
|
+
volume_names: Optional[list[str]] = None,
|
|
340
362
|
) -> JobRuntimeData:
|
|
341
363
|
return JobRuntimeData(
|
|
342
364
|
network_mode=NetworkMode(network_mode),
|
|
@@ -344,6 +366,8 @@ def get_job_runtime_data(
|
|
|
344
366
|
gpu=gpu,
|
|
345
367
|
memory=Memory(memory) if memory is not None else None,
|
|
346
368
|
ports=ports,
|
|
369
|
+
offer=offer,
|
|
370
|
+
volume_names=volume_names,
|
|
347
371
|
)
|
|
348
372
|
|
|
349
373
|
|
|
@@ -481,56 +505,26 @@ async def create_instance(
|
|
|
481
505
|
termination_idle_time: int = DEFAULT_POOL_TERMINATION_IDLE_TIME,
|
|
482
506
|
region: str = "eu-west",
|
|
483
507
|
remote_connection_info: Optional[RemoteConnectionInfo] = None,
|
|
508
|
+
offer: Optional[InstanceOfferWithAvailability] = None,
|
|
484
509
|
job_provisioning_data: Optional[JobProvisioningData] = None,
|
|
510
|
+
total_blocks: Optional[int] = 1,
|
|
511
|
+
busy_blocks: int = 0,
|
|
485
512
|
name: str = "test_instance",
|
|
486
513
|
volumes: Optional[List[VolumeModel]] = None,
|
|
487
514
|
) -> InstanceModel:
|
|
488
515
|
if instance_id is None:
|
|
489
516
|
instance_id = uuid.uuid4()
|
|
490
517
|
if job_provisioning_data is None:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"description": "",
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
"instance_id": "running_instance.id",
|
|
505
|
-
"ssh_proxy": None,
|
|
506
|
-
"hostname": "running_instance.ip",
|
|
507
|
-
"region": region,
|
|
508
|
-
"price": 0.1,
|
|
509
|
-
"username": "root",
|
|
510
|
-
"ssh_port": 22,
|
|
511
|
-
"dockerized": True,
|
|
512
|
-
"backend_data": None,
|
|
513
|
-
}
|
|
514
|
-
else:
|
|
515
|
-
job_provisioning_data_dict = job_provisioning_data.dict()
|
|
516
|
-
offer = {
|
|
517
|
-
"backend": backend.value,
|
|
518
|
-
"instance": {
|
|
519
|
-
"name": "instance",
|
|
520
|
-
"resources": {
|
|
521
|
-
"cpus": 2,
|
|
522
|
-
"memory_mib": 12000,
|
|
523
|
-
"gpus": [],
|
|
524
|
-
"spot": spot,
|
|
525
|
-
"disk": {"size_mib": 102400},
|
|
526
|
-
"description": "",
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
"region": region,
|
|
530
|
-
"price": 1,
|
|
531
|
-
"availability": "available",
|
|
532
|
-
}
|
|
533
|
-
|
|
518
|
+
job_provisioning_data = get_job_provisioning_data(
|
|
519
|
+
dockerized=True,
|
|
520
|
+
backend=backend,
|
|
521
|
+
region=region,
|
|
522
|
+
spot=spot,
|
|
523
|
+
hostname="running_instance.ip",
|
|
524
|
+
internal_ip=None,
|
|
525
|
+
)
|
|
526
|
+
if offer is None:
|
|
527
|
+
offer = get_instance_offer_with_availability(backend=backend, region=region, spot=spot)
|
|
534
528
|
if profile is None:
|
|
535
529
|
profile = Profile(name="test_name")
|
|
536
530
|
|
|
@@ -561,8 +555,8 @@ async def create_instance(
|
|
|
561
555
|
created_at=created_at,
|
|
562
556
|
started_at=created_at,
|
|
563
557
|
finished_at=finished_at,
|
|
564
|
-
job_provisioning_data=json
|
|
565
|
-
offer=json
|
|
558
|
+
job_provisioning_data=job_provisioning_data.json(),
|
|
559
|
+
offer=offer.json(),
|
|
566
560
|
price=1,
|
|
567
561
|
region=region,
|
|
568
562
|
backend=backend,
|
|
@@ -572,14 +566,85 @@ async def create_instance(
|
|
|
572
566
|
requirements=requirements.json(),
|
|
573
567
|
instance_configuration=instance_configuration.json(),
|
|
574
568
|
remote_connection_info=remote_connection_info.json() if remote_connection_info else None,
|
|
575
|
-
job=job,
|
|
576
569
|
volumes=volumes,
|
|
570
|
+
total_blocks=total_blocks,
|
|
571
|
+
busy_blocks=busy_blocks,
|
|
577
572
|
)
|
|
573
|
+
if job:
|
|
574
|
+
im.jobs.append(job)
|
|
578
575
|
session.add(im)
|
|
579
576
|
await session.commit()
|
|
580
577
|
return im
|
|
581
578
|
|
|
582
579
|
|
|
580
|
+
def get_instance_offer_with_availability(
|
|
581
|
+
backend: BackendType = BackendType.AWS,
|
|
582
|
+
region: str = "eu-west",
|
|
583
|
+
gpu_count: int = 0,
|
|
584
|
+
cpu_count: int = 2,
|
|
585
|
+
memory_gib: float = 12,
|
|
586
|
+
disk_gib: float = 100.0,
|
|
587
|
+
spot: bool = False,
|
|
588
|
+
blocks: int = 1,
|
|
589
|
+
total_blocks: int = 1,
|
|
590
|
+
):
|
|
591
|
+
gpus = [Gpu(name="T4", memory_mib=16384, vendor=gpuhunt.AcceleratorVendor.NVIDIA)] * gpu_count
|
|
592
|
+
return InstanceOfferWithAvailability(
|
|
593
|
+
backend=backend,
|
|
594
|
+
instance=InstanceType(
|
|
595
|
+
name="instance",
|
|
596
|
+
resources=Resources(
|
|
597
|
+
cpus=cpu_count,
|
|
598
|
+
memory_mib=int(memory_gib * 1024),
|
|
599
|
+
gpus=gpus,
|
|
600
|
+
spot=spot,
|
|
601
|
+
disk=Disk(size_mib=int(disk_gib * 1024)),
|
|
602
|
+
description="",
|
|
603
|
+
),
|
|
604
|
+
),
|
|
605
|
+
region=region,
|
|
606
|
+
price=1,
|
|
607
|
+
availability=InstanceAvailability.AVAILABLE,
|
|
608
|
+
blocks=blocks,
|
|
609
|
+
total_blocks=total_blocks,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def get_remote_connection_info(
|
|
614
|
+
host: str = "10.0.0.10",
|
|
615
|
+
port: int = 22,
|
|
616
|
+
ssh_user: str = "ubuntu",
|
|
617
|
+
ssh_keys: Optional[list[SSHKey]] = None,
|
|
618
|
+
env: Optional[Union[Env, dict]] = None,
|
|
619
|
+
):
|
|
620
|
+
if ssh_keys is None:
|
|
621
|
+
ssh_keys = [
|
|
622
|
+
SSHKey(
|
|
623
|
+
public="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6mJxVbNtm0zXgMLvByrhXJCmJRveSrJxLB5/OzcyCk",
|
|
624
|
+
private="""
|
|
625
|
+
-----BEGIN OPENSSH PRIVATE KEY-----
|
|
626
|
+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
|
627
|
+
QyNTUxOQAAACDupicVWzbZtM14DC7wcq4VyQpiUb3kqycSwefzs3MgpAAAAJCiWa5Volmu
|
|
628
|
+
VQAAAAtzc2gtZWQyNTUxOQAAACDupicVWzbZtM14DC7wcq4VyQpiUb3kqycSwefzs3MgpA
|
|
629
|
+
AAAEAncHi4AhS6XdMp5Gzd+IMse/4ekyQ54UngByf0Sp0uH+6mJxVbNtm0zXgMLvByrhXJ
|
|
630
|
+
CmJRveSrJxLB5/OzcyCkAAAACWRlZkBkZWZwYwECAwQ=
|
|
631
|
+
-----END OPENSSH PRIVATE KEY-----
|
|
632
|
+
""",
|
|
633
|
+
)
|
|
634
|
+
]
|
|
635
|
+
if env is None:
|
|
636
|
+
env = Env()
|
|
637
|
+
elif isinstance(env, dict):
|
|
638
|
+
env = Env.parse_obj(env)
|
|
639
|
+
return RemoteConnectionInfo(
|
|
640
|
+
host=host,
|
|
641
|
+
port=port,
|
|
642
|
+
ssh_user=ssh_user,
|
|
643
|
+
ssh_keys=ssh_keys,
|
|
644
|
+
env=env,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
583
648
|
async def create_volume(
|
|
584
649
|
session: AsyncSession,
|
|
585
650
|
project: ProjectModel,
|
dstack/_internal/utils/common.py
CHANGED
|
@@ -157,24 +157,38 @@ def parse_pretty_duration(duration: str) -> int:
|
|
|
157
157
|
return amount * multiplier
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
DURATION_UNITS_DESC = [
|
|
161
|
+
("w", 7 * 24 * 3600),
|
|
162
|
+
("d", 24 * 3600),
|
|
163
|
+
("h", 3600),
|
|
164
|
+
("m", 60),
|
|
165
|
+
("s", 1),
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
160
169
|
def format_pretty_duration(seconds: int) -> str:
|
|
161
170
|
if seconds == 0:
|
|
162
171
|
return "0s"
|
|
163
172
|
if seconds < 0:
|
|
164
173
|
raise ValueError("Seconds cannot be negative")
|
|
165
|
-
|
|
166
|
-
("w", 7 * 24 * 3600),
|
|
167
|
-
("d", 24 * 3600),
|
|
168
|
-
("h", 3600),
|
|
169
|
-
("m", 60),
|
|
170
|
-
("s", 1),
|
|
171
|
-
]
|
|
172
|
-
for unit, multiplier in units:
|
|
174
|
+
for unit, multiplier in DURATION_UNITS_DESC:
|
|
173
175
|
if seconds % multiplier == 0:
|
|
174
176
|
return f"{seconds // multiplier}{unit}"
|
|
175
177
|
return f"{seconds}s" # Fallback to seconds if no larger unit fits perfectly
|
|
176
178
|
|
|
177
179
|
|
|
180
|
+
def format_duration_multiunit(seconds: int) -> str:
|
|
181
|
+
"""90 -> 1m 30s, 4545 -> 1h 15m 45s, etc"""
|
|
182
|
+
if seconds < 0:
|
|
183
|
+
raise ValueError("Seconds cannot be negative")
|
|
184
|
+
result = ""
|
|
185
|
+
for unit, multiplier in DURATION_UNITS_DESC:
|
|
186
|
+
if unit_value := seconds // multiplier:
|
|
187
|
+
result += f" {unit_value}{unit}"
|
|
188
|
+
seconds -= unit_value * multiplier
|
|
189
|
+
return result.lstrip() or "0s"
|
|
190
|
+
|
|
191
|
+
|
|
178
192
|
def sizeof_fmt(num, suffix="B"):
|
|
179
193
|
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
|
180
194
|
if abs(num) < 1024.0:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_bool(name: str, default: bool = False) -> bool:
|
|
5
|
+
try:
|
|
6
|
+
value = os.environ[name]
|
|
7
|
+
except KeyError:
|
|
8
|
+
return default
|
|
9
|
+
value = value.lower()
|
|
10
|
+
if value in ["0", "false", "off"]:
|
|
11
|
+
return False
|
|
12
|
+
if value in ["1", "true", "on"]:
|
|
13
|
+
return True
|
|
14
|
+
raise ValueError(f"Invalid bool value: {name}={value}")
|
dstack/_internal/utils/ssh.py
CHANGED
|
@@ -159,7 +159,7 @@ def get_ssh_config(path: PathLike, host: str) -> Optional[Dict[str, str]]:
|
|
|
159
159
|
return None
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def update_ssh_config(path: PathLike, host: str, options: Dict[str, Union[str, FilePath]]):
|
|
162
|
+
def update_ssh_config(path: PathLike, host: str, options: Dict[str, Union[str, int, FilePath]]):
|
|
163
163
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
164
164
|
with FileLock(str(path) + ".lock"):
|
|
165
165
|
copy_mode = True
|
dstack/api/server/_fleets.py
CHANGED
|
@@ -62,16 +62,29 @@ def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[_ExcludeDict]:
|
|
|
62
62
|
spec_excludes: _ExcludeDict = {}
|
|
63
63
|
configuration_excludes: _ExcludeDict = {}
|
|
64
64
|
profile_excludes: set[str] = set()
|
|
65
|
+
ssh_config_excludes: _ExcludeDict = {}
|
|
66
|
+
ssh_hosts_excludes: set[str] = set()
|
|
65
67
|
|
|
66
68
|
# TODO: Can be removed in 0.19
|
|
67
69
|
if fleet_spec.configuration_path is None:
|
|
68
70
|
spec_excludes["configuration_path"] = True
|
|
69
71
|
if fleet_spec.configuration.ssh_config is not None:
|
|
72
|
+
if fleet_spec.configuration.ssh_config.proxy_jump is None:
|
|
73
|
+
ssh_config_excludes["proxy_jump"] = True
|
|
74
|
+
if all(
|
|
75
|
+
isinstance(h, str) or h.proxy_jump is None
|
|
76
|
+
for h in fleet_spec.configuration.ssh_config.hosts
|
|
77
|
+
):
|
|
78
|
+
ssh_hosts_excludes.add("proxy_jump")
|
|
70
79
|
if all(
|
|
71
80
|
isinstance(h, str) or h.internal_ip is None
|
|
72
81
|
for h in fleet_spec.configuration.ssh_config.hosts
|
|
73
82
|
):
|
|
74
|
-
|
|
83
|
+
ssh_hosts_excludes.add("internal_ip")
|
|
84
|
+
if all(
|
|
85
|
+
isinstance(h, str) or h.blocks == 1 for h in fleet_spec.configuration.ssh_config.hosts
|
|
86
|
+
):
|
|
87
|
+
ssh_hosts_excludes.add("blocks")
|
|
75
88
|
# client >= 0.18.30 / server <= 0.18.29 compatibility tweak
|
|
76
89
|
if fleet_spec.configuration.reservation is None:
|
|
77
90
|
configuration_excludes["reservation"] = True
|
|
@@ -84,7 +97,18 @@ def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[_ExcludeDict]:
|
|
|
84
97
|
# client >= 0.18.38 / server <= 0.18.37 compatibility tweak
|
|
85
98
|
if fleet_spec.profile is not None and fleet_spec.profile.stop_duration is None:
|
|
86
99
|
profile_excludes.add("stop_duration")
|
|
100
|
+
# client >= 0.18.41 / server <= 0.18.40 compatibility tweak
|
|
101
|
+
if fleet_spec.configuration.availability_zones is None:
|
|
102
|
+
configuration_excludes["availability_zones"] = True
|
|
103
|
+
if fleet_spec.profile is not None and fleet_spec.profile.availability_zones is None:
|
|
104
|
+
profile_excludes.add("availability_zones")
|
|
105
|
+
if fleet_spec.configuration.blocks == 1:
|
|
106
|
+
configuration_excludes["blocks"] = True
|
|
87
107
|
|
|
108
|
+
if ssh_hosts_excludes:
|
|
109
|
+
ssh_config_excludes["hosts"] = {"__all__": ssh_hosts_excludes}
|
|
110
|
+
if ssh_config_excludes:
|
|
111
|
+
configuration_excludes["ssh_config"] = ssh_config_excludes
|
|
88
112
|
if configuration_excludes:
|
|
89
113
|
spec_excludes["configuration"] = configuration_excludes
|
|
90
114
|
if profile_excludes:
|
dstack/api/server/_runs.py
CHANGED
|
@@ -7,6 +7,7 @@ from pydantic import parse_obj_as
|
|
|
7
7
|
from dstack._internal.core.models.common import is_core_model_instance
|
|
8
8
|
from dstack._internal.core.models.configurations import (
|
|
9
9
|
STRIP_PREFIX_DEFAULT,
|
|
10
|
+
DevEnvironmentConfiguration,
|
|
10
11
|
ServiceConfiguration,
|
|
11
12
|
)
|
|
12
13
|
from dstack._internal.core.models.pools import Instance
|
|
@@ -82,7 +83,10 @@ class RunsAPIClient(APIClientGroup):
|
|
|
82
83
|
) -> Run:
|
|
83
84
|
plan_input: ApplyRunPlanInput = ApplyRunPlanInput.__response__.parse_obj(plan)
|
|
84
85
|
body = ApplyRunPlanRequest(plan=plan_input, force=force)
|
|
85
|
-
resp = self._request(
|
|
86
|
+
resp = self._request(
|
|
87
|
+
f"/api/project/{project_name}/runs/apply",
|
|
88
|
+
body=body.json(exclude=_get_apply_plan_excludes(plan_input)),
|
|
89
|
+
)
|
|
86
90
|
return parse_obj_as(Run.__response__, resp.json())
|
|
87
91
|
|
|
88
92
|
def submit(self, project_name: str, run_spec: RunSpec) -> Run:
|
|
@@ -121,8 +125,15 @@ class RunsAPIClient(APIClientGroup):
|
|
|
121
125
|
return parse_obj_as(Instance.__response__, resp.json())
|
|
122
126
|
|
|
123
127
|
|
|
128
|
+
def _get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[dict]:
|
|
129
|
+
run_spec_excludes = _get_run_spec_excludes(plan.run_spec)
|
|
130
|
+
if run_spec_excludes is not None:
|
|
131
|
+
return {"plan": run_spec_excludes}
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
124
135
|
def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[dict]:
|
|
125
|
-
spec_excludes: dict[str,
|
|
136
|
+
spec_excludes: dict[str, Any] = {}
|
|
126
137
|
configuration_excludes: dict[str, Any] = {}
|
|
127
138
|
profile_excludes: set[str] = set()
|
|
128
139
|
configuration = run_spec.configuration
|
|
@@ -164,6 +175,16 @@ def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[dict]:
|
|
|
164
175
|
for v in configuration.volumes
|
|
165
176
|
):
|
|
166
177
|
configuration_excludes["volumes"] = {"__all__": {"optional"}}
|
|
178
|
+
# client >= 0.18.41 / server <= 0.18.40 compatibility tweak
|
|
179
|
+
if configuration.availability_zones is None:
|
|
180
|
+
configuration_excludes["availability_zones"] = True
|
|
181
|
+
if profile is not None and profile.availability_zones is None:
|
|
182
|
+
profile_excludes.add("availability_zones")
|
|
183
|
+
if (
|
|
184
|
+
is_core_model_instance(configuration, DevEnvironmentConfiguration)
|
|
185
|
+
and configuration.inactivity_duration is None
|
|
186
|
+
):
|
|
187
|
+
configuration_excludes["inactivity_duration"] = True
|
|
167
188
|
|
|
168
189
|
if configuration_excludes:
|
|
169
190
|
spec_excludes["configuration"] = configuration_excludes
|
dstack/api/server/_volumes.py
CHANGED
|
@@ -27,9 +27,20 @@ class VolumesAPIClient(APIClientGroup):
|
|
|
27
27
|
configuration: VolumeConfiguration,
|
|
28
28
|
) -> Volume:
|
|
29
29
|
body = CreateVolumeRequest(configuration=configuration)
|
|
30
|
-
resp = self._request(
|
|
30
|
+
resp = self._request(
|
|
31
|
+
f"/api/project/{project_name}/volumes/create",
|
|
32
|
+
body=body.json(exclude=_get_volume_configuration_excludes(configuration)),
|
|
33
|
+
)
|
|
31
34
|
return parse_obj_as(Volume.__response__, resp.json())
|
|
32
35
|
|
|
33
36
|
def delete(self, project_name: str, names: List[str]) -> None:
|
|
34
37
|
body = DeleteVolumesRequest(names=names)
|
|
35
38
|
self._request(f"/api/project/{project_name}/volumes/delete", body=body.json())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_volume_configuration_excludes(configuration: VolumeConfiguration) -> dict:
|
|
42
|
+
configuration_excludes = {}
|
|
43
|
+
# client >= 0.18.41 / server <= 0.18.40 compatibility tweak
|
|
44
|
+
if configuration.availability_zone is None:
|
|
45
|
+
configuration_excludes["availability_zone"] = True
|
|
46
|
+
return {"configuration": configuration_excludes}
|
dstack/version.py
CHANGED