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
|
@@ -29,7 +29,6 @@ from dstack._internal.core.models.runs import (
|
|
|
29
29
|
ApplyRunPlanInput,
|
|
30
30
|
Job,
|
|
31
31
|
JobPlan,
|
|
32
|
-
JobProvisioningData,
|
|
33
32
|
JobSpec,
|
|
34
33
|
JobStatus,
|
|
35
34
|
JobSubmission,
|
|
@@ -45,8 +44,6 @@ from dstack._internal.core.models.users import GlobalRole
|
|
|
45
44
|
from dstack._internal.core.models.volumes import (
|
|
46
45
|
InstanceMountPoint,
|
|
47
46
|
Volume,
|
|
48
|
-
VolumeMountPoint,
|
|
49
|
-
VolumeStatus,
|
|
50
47
|
)
|
|
51
48
|
from dstack._internal.core.services import validate_dstack_resource_name
|
|
52
49
|
from dstack._internal.core.services.diff import diff_models
|
|
@@ -58,15 +55,15 @@ from dstack._internal.server.models import (
|
|
|
58
55
|
RepoModel,
|
|
59
56
|
RunModel,
|
|
60
57
|
UserModel,
|
|
61
|
-
VolumeModel,
|
|
62
58
|
)
|
|
63
59
|
from dstack._internal.server.services import repos as repos_services
|
|
64
60
|
from dstack._internal.server.services import services
|
|
65
|
-
from dstack._internal.server.services import volumes as volumes_services
|
|
66
61
|
from dstack._internal.server.services.docker import is_valid_docker_volume_target
|
|
67
62
|
from dstack._internal.server.services.jobs import (
|
|
63
|
+
check_can_attach_job_volumes,
|
|
68
64
|
delay_job_instance_termination,
|
|
69
65
|
get_instances_ids_with_detaching_volumes,
|
|
66
|
+
get_job_configured_volumes,
|
|
70
67
|
get_jobs_from_run_spec,
|
|
71
68
|
group_jobs_by_replica_latest,
|
|
72
69
|
job_model_to_job_submission,
|
|
@@ -80,6 +77,7 @@ from dstack._internal.server.services.pools import (
|
|
|
80
77
|
get_instance_offer,
|
|
81
78
|
get_or_create_pool_by_name,
|
|
82
79
|
get_pool_instances,
|
|
80
|
+
get_shared_pool_instances_with_offers,
|
|
83
81
|
)
|
|
84
82
|
from dstack._internal.server.services.projects import list_project_models, list_user_project_models
|
|
85
83
|
from dstack._internal.server.services.users import get_user_model_by_name
|
|
@@ -164,7 +162,8 @@ async def list_projects_run_models(
|
|
|
164
162
|
limit: int,
|
|
165
163
|
ascending: bool,
|
|
166
164
|
) -> List[RunModel]:
|
|
167
|
-
filters = [
|
|
165
|
+
filters = []
|
|
166
|
+
filters.append(RunModel.project_id.in_(p.id for p in projects))
|
|
168
167
|
if repo is not None:
|
|
169
168
|
filters.append(RunModel.repo_id == repo.id)
|
|
170
169
|
if runs_user is not None:
|
|
@@ -300,10 +299,11 @@ async def get_plan(
|
|
|
300
299
|
# TODO(egor-s): do we need to generate all replicas here?
|
|
301
300
|
jobs = await get_jobs_from_run_spec(run_spec, replica_num=0)
|
|
302
301
|
|
|
303
|
-
volumes = await
|
|
302
|
+
volumes = await get_job_configured_volumes(
|
|
304
303
|
session=session,
|
|
305
304
|
project=project,
|
|
306
305
|
run_spec=run_spec,
|
|
306
|
+
job_num=0,
|
|
307
307
|
)
|
|
308
308
|
|
|
309
309
|
pool = await get_or_create_pool_by_name(
|
|
@@ -450,7 +450,7 @@ async def submit_run(
|
|
|
450
450
|
else:
|
|
451
451
|
await delete_runs(session=session, project=project, runs_names=[run_spec.run_name])
|
|
452
452
|
|
|
453
|
-
await
|
|
453
|
+
await _validate_run(
|
|
454
454
|
session=session,
|
|
455
455
|
user=user,
|
|
456
456
|
project=project,
|
|
@@ -677,27 +677,40 @@ async def _get_pool_offers(
|
|
|
677
677
|
run_spec: RunSpec,
|
|
678
678
|
job: Job,
|
|
679
679
|
volumes: List[List[Volume]],
|
|
680
|
-
) ->
|
|
680
|
+
) -> list[InstanceOfferWithAvailability]:
|
|
681
|
+
pool_offers: list[InstanceOfferWithAvailability] = []
|
|
682
|
+
|
|
681
683
|
detaching_instances_ids = await get_instances_ids_with_detaching_volumes(session)
|
|
682
684
|
pool_instances = [i for i in get_pool_instances(pool) if i.id not in detaching_instances_ids]
|
|
683
|
-
|
|
685
|
+
multinode = job.job_spec.jobs_per_replica > 1
|
|
686
|
+
|
|
687
|
+
if not multinode:
|
|
688
|
+
shared_instances_with_offers = get_shared_pool_instances_with_offers(
|
|
689
|
+
pool_instances=pool_instances,
|
|
690
|
+
profile=run_spec.merged_profile,
|
|
691
|
+
requirements=job.job_spec.requirements,
|
|
692
|
+
volumes=volumes,
|
|
693
|
+
)
|
|
694
|
+
for _, offer in shared_instances_with_offers:
|
|
695
|
+
pool_offers.append(offer)
|
|
696
|
+
|
|
697
|
+
nonshared_instances = filter_pool_instances(
|
|
684
698
|
pool_instances=pool_instances,
|
|
685
699
|
profile=run_spec.merged_profile,
|
|
686
700
|
requirements=job.job_spec.requirements,
|
|
687
|
-
multinode=
|
|
701
|
+
multinode=multinode,
|
|
688
702
|
volumes=volumes,
|
|
703
|
+
shared=False,
|
|
689
704
|
)
|
|
690
|
-
|
|
691
|
-
for instance in pool_filtered_instances:
|
|
705
|
+
for instance in nonshared_instances:
|
|
692
706
|
offer = get_instance_offer(instance)
|
|
693
707
|
if offer is None:
|
|
694
708
|
continue
|
|
695
709
|
offer.availability = InstanceAvailability.BUSY
|
|
696
710
|
if instance.status == InstanceStatus.IDLE:
|
|
697
711
|
offer.availability = InstanceAvailability.IDLE
|
|
698
|
-
if instance.unreachable:
|
|
699
|
-
offer.availability = InstanceAvailability.NOT_AVAILABLE
|
|
700
712
|
pool_offers.append(offer)
|
|
713
|
+
|
|
701
714
|
pool_offers.sort(key=lambda offer: offer.price)
|
|
702
715
|
return pool_offers
|
|
703
716
|
|
|
@@ -722,186 +735,42 @@ async def _generate_run_name(
|
|
|
722
735
|
idx += 1
|
|
723
736
|
|
|
724
737
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
run_spec: RunSpec,
|
|
730
|
-
):
|
|
731
|
-
volumes = await get_run_volumes(
|
|
732
|
-
session=session,
|
|
733
|
-
project=project,
|
|
734
|
-
run_spec=run_spec,
|
|
735
|
-
)
|
|
736
|
-
check_can_attach_run_volumes(
|
|
737
|
-
run_spec=run_spec,
|
|
738
|
-
volumes=volumes,
|
|
738
|
+
def check_run_spec_requires_instance_mounts(run_spec: RunSpec) -> bool:
|
|
739
|
+
return any(
|
|
740
|
+
is_core_model_instance(mp, InstanceMountPoint) and not mp.optional
|
|
741
|
+
for mp in run_spec.configuration.volumes
|
|
739
742
|
)
|
|
740
743
|
|
|
741
744
|
|
|
742
|
-
async def
|
|
745
|
+
async def _validate_run(
|
|
743
746
|
session: AsyncSession,
|
|
747
|
+
user: UserModel,
|
|
744
748
|
project: ProjectModel,
|
|
745
749
|
run_spec: RunSpec,
|
|
746
|
-
)
|
|
747
|
-
|
|
748
|
-
Returns list of run volumes grouped by mount points.
|
|
749
|
-
"""
|
|
750
|
-
volume_models = await get_run_volume_models(
|
|
750
|
+
):
|
|
751
|
+
await _validate_run_volumes(
|
|
751
752
|
session=session,
|
|
752
753
|
project=project,
|
|
753
754
|
run_spec=run_spec,
|
|
754
755
|
)
|
|
755
|
-
return [
|
|
756
|
-
[volumes_services.volume_model_to_volume(v) for v in mount_point_volume_models]
|
|
757
|
-
for mount_point_volume_models in volume_models
|
|
758
|
-
]
|
|
759
756
|
|
|
760
757
|
|
|
761
|
-
async def
|
|
758
|
+
async def _validate_run_volumes(
|
|
762
759
|
session: AsyncSession,
|
|
763
760
|
project: ProjectModel,
|
|
764
761
|
run_spec: RunSpec,
|
|
765
|
-
) -> List[List[VolumeModel]]:
|
|
766
|
-
"""
|
|
767
|
-
Returns list of run volume models grouped by mount points.
|
|
768
|
-
"""
|
|
769
|
-
if len(run_spec.configuration.volumes) == 0:
|
|
770
|
-
return []
|
|
771
|
-
volume_models = []
|
|
772
|
-
for mount_point in run_spec.configuration.volumes:
|
|
773
|
-
if not is_core_model_instance(mount_point, VolumeMountPoint):
|
|
774
|
-
continue
|
|
775
|
-
if isinstance(mount_point.name, str):
|
|
776
|
-
names = [mount_point.name]
|
|
777
|
-
else:
|
|
778
|
-
names = mount_point.name
|
|
779
|
-
mount_point_volume_models = []
|
|
780
|
-
for name in names:
|
|
781
|
-
volume_model = await volumes_services.get_project_volume_model_by_name(
|
|
782
|
-
session=session,
|
|
783
|
-
project=project,
|
|
784
|
-
name=name,
|
|
785
|
-
)
|
|
786
|
-
if volume_model is None:
|
|
787
|
-
raise ResourceNotExistsError(f"Volume {mount_point.name} not found")
|
|
788
|
-
mount_point_volume_models.append(volume_model)
|
|
789
|
-
volume_models.append(mount_point_volume_models)
|
|
790
|
-
return volume_models
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
def check_can_attach_run_volumes(
|
|
794
|
-
run_spec: RunSpec,
|
|
795
|
-
volumes: List[List[Volume]],
|
|
796
762
|
):
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
""
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
regions = {v.configuration.region for v in mount_point_volumes}
|
|
809
|
-
if backends != expected_backends:
|
|
810
|
-
raise ServerClientError(
|
|
811
|
-
"Volumes from different backends specified for different mount points"
|
|
812
|
-
)
|
|
813
|
-
if regions != expected_regions:
|
|
814
|
-
raise ServerClientError(
|
|
815
|
-
"Volumes from different regions specified for different mount points"
|
|
816
|
-
)
|
|
817
|
-
for volume in mount_point_volumes:
|
|
818
|
-
if volume.status != VolumeStatus.ACTIVE:
|
|
819
|
-
raise ServerClientError(f"Cannot mount volumes that are not active: {volume.name}")
|
|
820
|
-
volumes_names = [v.name for vs in volumes for v in vs]
|
|
821
|
-
if len(volumes_names) != len(set(volumes_names)):
|
|
822
|
-
raise ServerClientError("Cannot attach the same volume at different mount points")
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
async def get_job_volumes(
|
|
826
|
-
session: AsyncSession,
|
|
827
|
-
project: ProjectModel,
|
|
828
|
-
run_spec: RunSpec,
|
|
829
|
-
job_provisioning_data: JobProvisioningData,
|
|
830
|
-
) -> List[Volume]:
|
|
831
|
-
"""
|
|
832
|
-
Returns volumes attached to the job.
|
|
833
|
-
"""
|
|
834
|
-
run_volumes = await get_run_volumes(
|
|
835
|
-
session=session,
|
|
836
|
-
project=project,
|
|
837
|
-
run_spec=run_spec,
|
|
838
|
-
)
|
|
839
|
-
job_volumes = []
|
|
840
|
-
for mount_point_volumes in run_volumes:
|
|
841
|
-
job_volumes.append(get_job_mount_point_volume(mount_point_volumes, job_provisioning_data))
|
|
842
|
-
return job_volumes
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
def get_job_mount_point_volume(
|
|
846
|
-
volumes: List[Volume],
|
|
847
|
-
job_provisioning_data: JobProvisioningData,
|
|
848
|
-
) -> Volume:
|
|
849
|
-
"""
|
|
850
|
-
Returns the volume attached to the job among the list of possible mount point volumes.
|
|
851
|
-
"""
|
|
852
|
-
for volume in volumes:
|
|
853
|
-
if (
|
|
854
|
-
volume.configuration.backend != job_provisioning_data.get_base_backend()
|
|
855
|
-
or volume.configuration.region != job_provisioning_data.region
|
|
856
|
-
):
|
|
857
|
-
continue
|
|
858
|
-
if (
|
|
859
|
-
volume.provisioning_data is not None
|
|
860
|
-
and volume.provisioning_data.availability_zone is not None
|
|
861
|
-
and job_provisioning_data.availability_zone is not None
|
|
862
|
-
and volume.provisioning_data.availability_zone
|
|
863
|
-
!= job_provisioning_data.availability_zone
|
|
864
|
-
):
|
|
865
|
-
continue
|
|
866
|
-
return volume
|
|
867
|
-
raise ServerClientError("Failed to find an eligible volume for the mount point")
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
def get_offer_volumes(
|
|
871
|
-
volumes: List[List[Volume]],
|
|
872
|
-
offer: InstanceOfferWithAvailability,
|
|
873
|
-
) -> List[Volume]:
|
|
874
|
-
"""
|
|
875
|
-
Returns volumes suitable for the offer for each mount point.
|
|
876
|
-
"""
|
|
877
|
-
offer_volumes = []
|
|
878
|
-
for mount_point_volumes in volumes:
|
|
879
|
-
offer_volumes.append(get_offer_mount_point_volume(mount_point_volumes, offer))
|
|
880
|
-
return offer_volumes
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
def get_offer_mount_point_volume(
|
|
884
|
-
volumes: List[Volume],
|
|
885
|
-
offer: InstanceOfferWithAvailability,
|
|
886
|
-
) -> Volume:
|
|
887
|
-
"""
|
|
888
|
-
Returns the first suitable volume for the offer among possible mount point volumes.
|
|
889
|
-
"""
|
|
890
|
-
for volume in volumes:
|
|
891
|
-
if (
|
|
892
|
-
volume.configuration.backend != offer.backend
|
|
893
|
-
or volume.configuration.region != offer.region
|
|
894
|
-
):
|
|
895
|
-
continue
|
|
896
|
-
return volume
|
|
897
|
-
raise ServerClientError("Failed to find an eligible volume for the mount point")
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
def check_run_spec_requires_instance_mounts(run_spec: RunSpec) -> bool:
|
|
901
|
-
return any(
|
|
902
|
-
is_core_model_instance(mp, InstanceMountPoint) and not mp.optional
|
|
903
|
-
for mp in run_spec.configuration.volumes
|
|
904
|
-
)
|
|
763
|
+
# The volumes validation should be done here and not in job configurator
|
|
764
|
+
# since potentially we may need to validate volumes for jobs/replicas
|
|
765
|
+
# that won't be created immediately (e.g. range of replicas or nodes).
|
|
766
|
+
nodes = 1
|
|
767
|
+
if run_spec.configuration.type == "task":
|
|
768
|
+
nodes = run_spec.configuration.nodes
|
|
769
|
+
for job_num in range(nodes):
|
|
770
|
+
volumes = await get_job_configured_volumes(
|
|
771
|
+
session=session, project=project, run_spec=run_spec, job_num=job_num
|
|
772
|
+
)
|
|
773
|
+
check_can_attach_job_volumes(volumes=volumes)
|
|
905
774
|
|
|
906
775
|
|
|
907
776
|
async def _get_run_repo_or_error(
|
|
@@ -21,6 +21,7 @@ from dstack._internal.core.errors import (
|
|
|
21
21
|
from dstack._internal.core.models.common import is_core_model_instance
|
|
22
22
|
from dstack._internal.core.models.configurations import SERVICE_HTTPS_DEFAULT, ServiceConfiguration
|
|
23
23
|
from dstack._internal.core.models.gateways import GatewayConfiguration, GatewayStatus
|
|
24
|
+
from dstack._internal.core.models.instances import SSHConnectionParams
|
|
24
25
|
from dstack._internal.core.models.runs import Run, RunSpec, ServiceModelSpec, ServiceSpec
|
|
25
26
|
from dstack._internal.server import settings
|
|
26
27
|
from dstack._internal.server.models import GatewayModel, JobModel, ProjectModel, RunModel
|
|
@@ -155,7 +156,12 @@ def get_service_spec(
|
|
|
155
156
|
|
|
156
157
|
|
|
157
158
|
async def register_replica(
|
|
158
|
-
session: AsyncSession,
|
|
159
|
+
session: AsyncSession,
|
|
160
|
+
gateway_id: Optional[uuid.UUID],
|
|
161
|
+
run: Run,
|
|
162
|
+
job_model: JobModel,
|
|
163
|
+
ssh_head_proxy: Optional[SSHConnectionParams],
|
|
164
|
+
ssh_head_proxy_private_key: Optional[str],
|
|
159
165
|
):
|
|
160
166
|
if gateway_id is None: # in-server proxy
|
|
161
167
|
return
|
|
@@ -167,6 +173,8 @@ async def register_replica(
|
|
|
167
173
|
await client.register_replica(
|
|
168
174
|
run=run,
|
|
169
175
|
job_submission=job_submission,
|
|
176
|
+
ssh_head_proxy=ssh_head_proxy,
|
|
177
|
+
ssh_head_proxy_private_key=ssh_head_proxy_private_key,
|
|
170
178
|
)
|
|
171
179
|
logger.info("%s: replica is registered for service %s", fmt(job_model), run.id.hex)
|
|
172
180
|
except (httpx.RequestError, SSHError) as e:
|
|
@@ -15,19 +15,28 @@ from dstack._internal.core.errors import (
|
|
|
15
15
|
from dstack._internal.core.models.users import GlobalRole
|
|
16
16
|
from dstack._internal.core.models.volumes import (
|
|
17
17
|
Volume,
|
|
18
|
+
VolumeAttachment,
|
|
18
19
|
VolumeAttachmentData,
|
|
19
20
|
VolumeConfiguration,
|
|
21
|
+
VolumeInstance,
|
|
20
22
|
VolumeProvisioningData,
|
|
21
23
|
VolumeStatus,
|
|
22
24
|
)
|
|
23
25
|
from dstack._internal.core.services import validate_dstack_resource_name
|
|
24
26
|
from dstack._internal.server.db import get_db
|
|
25
|
-
from dstack._internal.server.models import
|
|
27
|
+
from dstack._internal.server.models import (
|
|
28
|
+
InstanceModel,
|
|
29
|
+
ProjectModel,
|
|
30
|
+
UserModel,
|
|
31
|
+
VolumeAttachmentModel,
|
|
32
|
+
VolumeModel,
|
|
33
|
+
)
|
|
26
34
|
from dstack._internal.server.services import backends as backends_services
|
|
27
35
|
from dstack._internal.server.services.locking import (
|
|
28
36
|
get_locker,
|
|
29
37
|
string_to_lock_id,
|
|
30
38
|
)
|
|
39
|
+
from dstack._internal.server.services.pools import get_instance_provisioning_data
|
|
31
40
|
from dstack._internal.server.services.projects import list_project_models, list_user_project_models
|
|
32
41
|
from dstack._internal.utils import common, random_names
|
|
33
42
|
from dstack._internal.utils.logging import get_logger
|
|
@@ -106,8 +115,13 @@ async def list_projects_volume_models(
|
|
|
106
115
|
.order_by(*order_by)
|
|
107
116
|
.limit(limit)
|
|
108
117
|
.options(joinedload(VolumeModel.user))
|
|
118
|
+
.options(
|
|
119
|
+
joinedload(VolumeModel.attachments)
|
|
120
|
+
.joinedload(VolumeAttachmentModel.instance)
|
|
121
|
+
.joinedload(InstanceModel.fleet)
|
|
122
|
+
)
|
|
109
123
|
)
|
|
110
|
-
volume_models = list(res.scalars().all())
|
|
124
|
+
volume_models = list(res.unique().scalars().all())
|
|
111
125
|
return volume_models
|
|
112
126
|
|
|
113
127
|
|
|
@@ -134,9 +148,16 @@ async def list_project_volume_models(
|
|
|
134
148
|
if not include_deleted:
|
|
135
149
|
filters.append(VolumeModel.deleted == False)
|
|
136
150
|
res = await session.execute(
|
|
137
|
-
select(VolumeModel)
|
|
151
|
+
select(VolumeModel)
|
|
152
|
+
.where(*filters)
|
|
153
|
+
.options(joinedload(VolumeModel.user))
|
|
154
|
+
.options(
|
|
155
|
+
joinedload(VolumeModel.attachments)
|
|
156
|
+
.joinedload(VolumeAttachmentModel.instance)
|
|
157
|
+
.joinedload(InstanceModel.fleet)
|
|
158
|
+
)
|
|
138
159
|
)
|
|
139
|
-
return list(res.scalars().all())
|
|
160
|
+
return list(res.unique().scalars().all())
|
|
140
161
|
|
|
141
162
|
|
|
142
163
|
async def get_volume_by_name(
|
|
@@ -163,9 +184,16 @@ async def get_project_volume_model_by_name(
|
|
|
163
184
|
if not include_deleted:
|
|
164
185
|
filters.append(VolumeModel.deleted == False)
|
|
165
186
|
res = await session.execute(
|
|
166
|
-
select(VolumeModel)
|
|
187
|
+
select(VolumeModel)
|
|
188
|
+
.where(*filters)
|
|
189
|
+
.options(joinedload(VolumeModel.user))
|
|
190
|
+
.options(
|
|
191
|
+
joinedload(VolumeModel.attachments)
|
|
192
|
+
.joinedload(VolumeAttachmentModel.instance)
|
|
193
|
+
.joinedload(InstanceModel.fleet)
|
|
194
|
+
)
|
|
167
195
|
)
|
|
168
|
-
return res.scalar_one_or_none()
|
|
196
|
+
return res.unique().scalar_one_or_none()
|
|
169
197
|
|
|
170
198
|
|
|
171
199
|
async def create_volume(
|
|
@@ -205,10 +233,10 @@ async def create_volume(
|
|
|
205
233
|
project=project,
|
|
206
234
|
status=VolumeStatus.SUBMITTED,
|
|
207
235
|
configuration=configuration.json(),
|
|
236
|
+
attachments=[],
|
|
208
237
|
)
|
|
209
238
|
session.add(volume_model)
|
|
210
239
|
await session.commit()
|
|
211
|
-
await session.refresh(volume_model)
|
|
212
240
|
return volume_model_to_volume(volume_model)
|
|
213
241
|
|
|
214
242
|
|
|
@@ -234,13 +262,13 @@ async def delete_volumes(session: AsyncSession, project: ProjectModel, names: Li
|
|
|
234
262
|
VolumeModel.deleted == False,
|
|
235
263
|
)
|
|
236
264
|
.options(selectinload(VolumeModel.user))
|
|
237
|
-
.options(selectinload(VolumeModel.
|
|
265
|
+
.options(selectinload(VolumeModel.attachments))
|
|
238
266
|
.execution_options(populate_existing=True)
|
|
239
267
|
.with_for_update()
|
|
240
268
|
)
|
|
241
269
|
volume_models = res.scalars().unique().all()
|
|
242
270
|
for volume_model in volume_models:
|
|
243
|
-
if len(volume_model.
|
|
271
|
+
if len(volume_model.attachments) > 0:
|
|
244
272
|
raise ServerClientError(
|
|
245
273
|
f"Failed to delete volume {volume_model.name}. Volume is in use."
|
|
246
274
|
)
|
|
@@ -270,6 +298,15 @@ def volume_model_to_volume(volume_model: VolumeModel) -> Volume:
|
|
|
270
298
|
# Initially VolumeProvisionigData lacked backend
|
|
271
299
|
if vpd is not None and vpd.backend is None:
|
|
272
300
|
vpd.backend = configuration.backend
|
|
301
|
+
attachments = []
|
|
302
|
+
for volume_attachment_model in volume_model.attachments:
|
|
303
|
+
instance = volume_attachment_model.instance
|
|
304
|
+
attachments.append(
|
|
305
|
+
VolumeAttachment(
|
|
306
|
+
instance=instance_model_to_volume_instance(instance),
|
|
307
|
+
attachment_data=get_attachment_data(volume_attachment_model),
|
|
308
|
+
)
|
|
309
|
+
)
|
|
273
310
|
return Volume(
|
|
274
311
|
name=volume_model.name,
|
|
275
312
|
project_name=volume_model.project.name,
|
|
@@ -282,6 +319,7 @@ def volume_model_to_volume(volume_model: VolumeModel) -> Volume:
|
|
|
282
319
|
deleted=volume_model.deleted,
|
|
283
320
|
volume_id=vpd.volume_id if vpd is not None else None,
|
|
284
321
|
provisioning_data=vpd,
|
|
322
|
+
attachments=attachments,
|
|
285
323
|
attachment_data=vad,
|
|
286
324
|
id=volume_model.id,
|
|
287
325
|
)
|
|
@@ -303,6 +341,27 @@ def get_volume_attachment_data(volume_model: VolumeModel) -> Optional[VolumeAtta
|
|
|
303
341
|
return VolumeAttachmentData.__response__.parse_raw(volume_model.volume_attachment_data)
|
|
304
342
|
|
|
305
343
|
|
|
344
|
+
def get_attachment_data(
|
|
345
|
+
volume_attachment_model: VolumeAttachmentModel,
|
|
346
|
+
) -> Optional[VolumeAttachmentData]:
|
|
347
|
+
if volume_attachment_model.attachment_data is None:
|
|
348
|
+
return None
|
|
349
|
+
return VolumeAttachmentData.__response__.parse_raw(volume_attachment_model.attachment_data)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def instance_model_to_volume_instance(instance_model: InstanceModel) -> VolumeInstance:
|
|
353
|
+
instance_id = None
|
|
354
|
+
jpd = get_instance_provisioning_data(instance_model)
|
|
355
|
+
if jpd is not None:
|
|
356
|
+
instance_id = jpd.instance_id
|
|
357
|
+
return VolumeInstance(
|
|
358
|
+
name=instance_model.name,
|
|
359
|
+
fleet_name=instance_model.fleet.name if instance_model.fleet else None,
|
|
360
|
+
instance_num=instance_model.instance_num,
|
|
361
|
+
instance_id=instance_id,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
306
365
|
async def generate_volume_name(session: AsyncSession, project: ProjectModel) -> str:
|
|
307
366
|
volume_models = await list_project_volume_models(session=session, project=project)
|
|
308
367
|
names = {v.name for v in volume_models}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>dstack</title><meta name="description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
2
2
|
"/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"><meta name="og:title" content="dstack"><meta name="og:type" content="article"><meta name="og:image" content="/splash_thumbnail.png"><meta name="og:description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
|
|
3
|
-
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-
|
|
3
|
+
"><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-2ac66bfcbd2e39830b88.js"></script><link href="/main-ad5150a441de98cd8987.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>
|