dstack 0.19.17__py3-none-any.whl → 0.19.19__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.
Potentially problematic release.
This version of dstack might be problematic. Click here for more details.
- dstack/_internal/cli/services/configurators/fleet.py +111 -1
- dstack/_internal/cli/services/profile.py +1 -1
- dstack/_internal/core/backends/aws/compute.py +237 -18
- dstack/_internal/core/backends/base/compute.py +20 -2
- dstack/_internal/core/backends/cudo/compute.py +23 -9
- dstack/_internal/core/backends/gcp/compute.py +13 -7
- dstack/_internal/core/backends/lambdalabs/compute.py +2 -1
- dstack/_internal/core/compatibility/fleets.py +12 -11
- dstack/_internal/core/compatibility/gateways.py +9 -8
- dstack/_internal/core/compatibility/logs.py +4 -3
- dstack/_internal/core/compatibility/runs.py +29 -21
- dstack/_internal/core/compatibility/volumes.py +11 -8
- dstack/_internal/core/errors.py +4 -0
- dstack/_internal/core/models/common.py +45 -2
- dstack/_internal/core/models/configurations.py +9 -1
- dstack/_internal/core/models/fleets.py +2 -1
- dstack/_internal/core/models/profiles.py +8 -5
- dstack/_internal/core/models/resources.py +15 -8
- dstack/_internal/core/models/runs.py +41 -138
- dstack/_internal/core/models/volumes.py +14 -0
- dstack/_internal/core/services/diff.py +56 -3
- dstack/_internal/core/services/ssh/attach.py +2 -0
- dstack/_internal/server/app.py +37 -9
- dstack/_internal/server/background/__init__.py +66 -40
- dstack/_internal/server/background/tasks/process_fleets.py +19 -3
- dstack/_internal/server/background/tasks/process_gateways.py +47 -29
- dstack/_internal/server/background/tasks/process_idle_volumes.py +139 -0
- dstack/_internal/server/background/tasks/process_instances.py +13 -2
- dstack/_internal/server/background/tasks/process_placement_groups.py +4 -2
- dstack/_internal/server/background/tasks/process_running_jobs.py +14 -3
- dstack/_internal/server/background/tasks/process_runs.py +8 -4
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +38 -7
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +5 -3
- dstack/_internal/server/background/tasks/process_volumes.py +2 -2
- dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +6 -6
- dstack/_internal/server/migrations/versions/d5863798bf41_add_volumemodel_last_job_processed_at.py +40 -0
- dstack/_internal/server/models.py +1 -0
- dstack/_internal/server/routers/backends.py +23 -16
- dstack/_internal/server/routers/files.py +7 -6
- dstack/_internal/server/routers/fleets.py +47 -36
- dstack/_internal/server/routers/gateways.py +27 -18
- dstack/_internal/server/routers/instances.py +18 -13
- dstack/_internal/server/routers/logs.py +7 -3
- dstack/_internal/server/routers/metrics.py +14 -8
- dstack/_internal/server/routers/projects.py +33 -22
- dstack/_internal/server/routers/repos.py +7 -6
- dstack/_internal/server/routers/runs.py +49 -28
- dstack/_internal/server/routers/secrets.py +20 -15
- dstack/_internal/server/routers/server.py +7 -4
- dstack/_internal/server/routers/users.py +22 -19
- dstack/_internal/server/routers/volumes.py +34 -25
- dstack/_internal/server/schemas/logs.py +2 -2
- dstack/_internal/server/schemas/runs.py +17 -5
- dstack/_internal/server/services/fleets.py +358 -75
- dstack/_internal/server/services/gateways/__init__.py +17 -6
- dstack/_internal/server/services/gateways/client.py +5 -3
- dstack/_internal/server/services/instances.py +8 -0
- dstack/_internal/server/services/jobs/__init__.py +45 -0
- dstack/_internal/server/services/jobs/configurators/base.py +12 -1
- dstack/_internal/server/services/locking.py +104 -13
- dstack/_internal/server/services/logging.py +4 -2
- dstack/_internal/server/services/logs/__init__.py +15 -2
- dstack/_internal/server/services/logs/aws.py +2 -4
- dstack/_internal/server/services/logs/filelog.py +33 -27
- dstack/_internal/server/services/logs/gcp.py +3 -5
- dstack/_internal/server/services/proxy/repo.py +4 -1
- dstack/_internal/server/services/runs.py +139 -72
- dstack/_internal/server/services/services/__init__.py +2 -1
- dstack/_internal/server/services/users.py +3 -1
- dstack/_internal/server/services/volumes.py +15 -2
- dstack/_internal/server/settings.py +25 -6
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-d151637af20f70b2e796.js → main-64f8273740c4b52c18f5.js} +71 -67
- dstack/_internal/server/statics/{main-d151637af20f70b2e796.js.map → main-64f8273740c4b52c18f5.js.map} +1 -1
- dstack/_internal/server/statics/{main-d48635d8fe670d53961c.css → main-d58fc0460cb0eae7cb5c.css} +1 -1
- dstack/_internal/server/testing/common.py +48 -8
- dstack/_internal/server/utils/routers.py +31 -8
- dstack/_internal/utils/json_utils.py +54 -0
- dstack/api/_public/runs.py +13 -2
- dstack/api/server/_runs.py +12 -2
- dstack/version.py +1 -1
- {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/METADATA +17 -14
- {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/RECORD +86 -83
- {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/WHEEL +0 -0
- {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.17.dist-info → dstack-0.19.19.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -24,6 +24,7 @@ from dstack._internal.core.models.instances import (
|
|
|
24
24
|
)
|
|
25
25
|
from dstack._internal.core.models.profiles import (
|
|
26
26
|
CreationPolicy,
|
|
27
|
+
RetryEvent,
|
|
27
28
|
)
|
|
28
29
|
from dstack._internal.core.models.repos.virtual import DEFAULT_VIRTUAL_REPO_ID, VirtualRunRepoData
|
|
29
30
|
from dstack._internal.core.models.runs import (
|
|
@@ -105,6 +106,8 @@ async def list_user_runs(
|
|
|
105
106
|
repo_id: Optional[str],
|
|
106
107
|
username: Optional[str],
|
|
107
108
|
only_active: bool,
|
|
109
|
+
include_jobs: bool,
|
|
110
|
+
job_submissions_limit: Optional[int],
|
|
108
111
|
prev_submitted_at: Optional[datetime],
|
|
109
112
|
prev_run_id: Optional[uuid.UUID],
|
|
110
113
|
limit: int,
|
|
@@ -148,7 +151,14 @@ async def list_user_runs(
|
|
|
148
151
|
runs = []
|
|
149
152
|
for r in run_models:
|
|
150
153
|
try:
|
|
151
|
-
runs.append(
|
|
154
|
+
runs.append(
|
|
155
|
+
run_model_to_run(
|
|
156
|
+
r,
|
|
157
|
+
return_in_api=True,
|
|
158
|
+
include_jobs=include_jobs,
|
|
159
|
+
job_submissions_limit=job_submissions_limit,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
152
162
|
except pydantic.ValidationError:
|
|
153
163
|
pass
|
|
154
164
|
if len(run_models) > len(runs):
|
|
@@ -482,8 +492,9 @@ async def submit_run(
|
|
|
482
492
|
select(func.pg_advisory_xact_lock(string_to_lock_id(lock_namespace)))
|
|
483
493
|
)
|
|
484
494
|
|
|
485
|
-
lock, _ = get_locker().get_lockset(lock_namespace)
|
|
495
|
+
lock, _ = get_locker(get_db().dialect_name).get_lockset(lock_namespace)
|
|
486
496
|
async with lock:
|
|
497
|
+
# FIXME: delete_runs commits, so Postgres lock is released too early.
|
|
487
498
|
if run_spec.run_name is None:
|
|
488
499
|
run_spec.run_name = await _generate_run_name(
|
|
489
500
|
session=session,
|
|
@@ -586,46 +597,29 @@ async def stop_runs(
|
|
|
586
597
|
)
|
|
587
598
|
run_models = res.scalars().all()
|
|
588
599
|
run_ids = sorted([r.id for r in run_models])
|
|
589
|
-
res = await session.execute(select(JobModel).where(JobModel.run_id.in_(run_ids)))
|
|
590
|
-
job_models = res.scalars().all()
|
|
591
|
-
job_ids = sorted([j.id for j in job_models])
|
|
592
600
|
await session.commit()
|
|
593
|
-
async with (
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
601
|
+
async with get_locker(get_db().dialect_name).lock_ctx(RunModel.__tablename__, run_ids):
|
|
602
|
+
res = await session.execute(
|
|
603
|
+
select(RunModel)
|
|
604
|
+
.where(RunModel.id.in_(run_ids))
|
|
605
|
+
.order_by(RunModel.id) # take locks in order
|
|
606
|
+
.with_for_update(key_share=True)
|
|
607
|
+
.execution_options(populate_existing=True)
|
|
608
|
+
)
|
|
609
|
+
run_models = res.scalars().all()
|
|
610
|
+
now = common_utils.get_current_datetime()
|
|
597
611
|
for run_model in run_models:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
run_model = res.scalar_one()
|
|
610
|
-
await session.execute(
|
|
611
|
-
select(JobModel)
|
|
612
|
-
.where(JobModel.run_id == run_model.id)
|
|
613
|
-
.order_by(JobModel.id) # take locks in order
|
|
614
|
-
.with_for_update(key_share=True)
|
|
615
|
-
.execution_options(populate_existing=True)
|
|
616
|
-
)
|
|
617
|
-
if run_model.status.is_finished():
|
|
618
|
-
return
|
|
619
|
-
run_model.status = RunStatus.TERMINATING
|
|
620
|
-
if abort:
|
|
621
|
-
run_model.termination_reason = RunTerminationReason.ABORTED_BY_USER
|
|
622
|
-
else:
|
|
623
|
-
run_model.termination_reason = RunTerminationReason.STOPPED_BY_USER
|
|
624
|
-
# process the run out of turn
|
|
625
|
-
logger.debug("%s: terminating because %s", fmt(run_model), run_model.termination_reason.name)
|
|
626
|
-
await process_terminating_run(session, run_model)
|
|
627
|
-
run_model.last_processed_at = common_utils.get_current_datetime()
|
|
628
|
-
await session.commit()
|
|
612
|
+
if run_model.status.is_finished():
|
|
613
|
+
continue
|
|
614
|
+
run_model.status = RunStatus.TERMINATING
|
|
615
|
+
if abort:
|
|
616
|
+
run_model.termination_reason = RunTerminationReason.ABORTED_BY_USER
|
|
617
|
+
else:
|
|
618
|
+
run_model.termination_reason = RunTerminationReason.STOPPED_BY_USER
|
|
619
|
+
run_model.last_processed_at = now
|
|
620
|
+
# The run will be terminated by process_runs.
|
|
621
|
+
# Terminating synchronously is problematic since it may take a long time.
|
|
622
|
+
await session.commit()
|
|
629
623
|
|
|
630
624
|
|
|
631
625
|
async def delete_runs(
|
|
@@ -642,7 +636,7 @@ async def delete_runs(
|
|
|
642
636
|
run_models = res.scalars().all()
|
|
643
637
|
run_ids = sorted([r.id for r in run_models])
|
|
644
638
|
await session.commit()
|
|
645
|
-
async with get_locker().lock_ctx(RunModel.__tablename__, run_ids):
|
|
639
|
+
async with get_locker(get_db().dialect_name).lock_ctx(RunModel.__tablename__, run_ids):
|
|
646
640
|
res = await session.execute(
|
|
647
641
|
select(RunModel)
|
|
648
642
|
.where(RunModel.id.in_(run_ids))
|
|
@@ -668,51 +662,33 @@ async def delete_runs(
|
|
|
668
662
|
|
|
669
663
|
def run_model_to_run(
|
|
670
664
|
run_model: RunModel,
|
|
671
|
-
|
|
665
|
+
include_jobs: bool = True,
|
|
666
|
+
job_submissions_limit: Optional[int] = None,
|
|
672
667
|
return_in_api: bool = False,
|
|
673
668
|
include_sensitive: bool = False,
|
|
674
669
|
) -> Run:
|
|
675
670
|
jobs: List[Job] = []
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
)
|
|
683
|
-
submissions = []
|
|
684
|
-
job_model = None
|
|
685
|
-
for job_model in job_submissions:
|
|
686
|
-
if include_job_submissions:
|
|
687
|
-
job_submission = job_model_to_job_submission(job_model)
|
|
688
|
-
if return_in_api:
|
|
689
|
-
# Set default non-None values for 0.18 backward-compatibility
|
|
690
|
-
# Remove in 0.19
|
|
691
|
-
if job_submission.job_provisioning_data is not None:
|
|
692
|
-
if job_submission.job_provisioning_data.hostname is None:
|
|
693
|
-
job_submission.job_provisioning_data.hostname = ""
|
|
694
|
-
if job_submission.job_provisioning_data.ssh_port is None:
|
|
695
|
-
job_submission.job_provisioning_data.ssh_port = 22
|
|
696
|
-
submissions.append(job_submission)
|
|
697
|
-
if job_model is not None:
|
|
698
|
-
# Use the spec from the latest submission. Submissions can have different specs
|
|
699
|
-
job_spec = JobSpec.__response__.parse_raw(job_model.job_spec_data)
|
|
700
|
-
if not include_sensitive:
|
|
701
|
-
_remove_job_spec_sensitive_info(job_spec)
|
|
702
|
-
jobs.append(Job(job_spec=job_spec, job_submissions=submissions))
|
|
671
|
+
if include_jobs:
|
|
672
|
+
jobs = _get_run_jobs_with_submissions(
|
|
673
|
+
run_model=run_model,
|
|
674
|
+
job_submissions_limit=job_submissions_limit,
|
|
675
|
+
return_in_api=return_in_api,
|
|
676
|
+
include_sensitive=include_sensitive,
|
|
677
|
+
)
|
|
703
678
|
|
|
704
679
|
run_spec = RunSpec.__response__.parse_raw(run_model.run_spec)
|
|
705
680
|
|
|
706
681
|
latest_job_submission = None
|
|
707
|
-
if
|
|
682
|
+
if len(jobs) > 0 and len(jobs[0].job_submissions) > 0:
|
|
708
683
|
# TODO(egor-s): does it make sense with replicas and multi-node?
|
|
709
|
-
|
|
710
|
-
latest_job_submission = jobs[0].job_submissions[-1]
|
|
684
|
+
latest_job_submission = jobs[0].job_submissions[-1]
|
|
711
685
|
|
|
712
686
|
service_spec = None
|
|
713
687
|
if run_model.service_spec is not None:
|
|
714
688
|
service_spec = ServiceSpec.__response__.parse_raw(run_model.service_spec)
|
|
715
689
|
|
|
690
|
+
status_message = _get_run_status_message(run_model)
|
|
691
|
+
error = _get_run_error(run_model)
|
|
716
692
|
run = Run(
|
|
717
693
|
id=run_model.id,
|
|
718
694
|
project_name=run_model.project.name,
|
|
@@ -720,18 +696,107 @@ def run_model_to_run(
|
|
|
720
696
|
submitted_at=run_model.submitted_at.replace(tzinfo=timezone.utc),
|
|
721
697
|
last_processed_at=run_model.last_processed_at.replace(tzinfo=timezone.utc),
|
|
722
698
|
status=run_model.status,
|
|
699
|
+
status_message=status_message,
|
|
723
700
|
termination_reason=run_model.termination_reason,
|
|
724
701
|
run_spec=run_spec,
|
|
725
702
|
jobs=jobs,
|
|
726
703
|
latest_job_submission=latest_job_submission,
|
|
727
704
|
service=service_spec,
|
|
728
705
|
deployment_num=run_model.deployment_num,
|
|
706
|
+
error=error,
|
|
729
707
|
deleted=run_model.deleted,
|
|
730
708
|
)
|
|
731
709
|
run.cost = _get_run_cost(run)
|
|
732
710
|
return run
|
|
733
711
|
|
|
734
712
|
|
|
713
|
+
def _get_run_jobs_with_submissions(
|
|
714
|
+
run_model: RunModel,
|
|
715
|
+
job_submissions_limit: Optional[int],
|
|
716
|
+
return_in_api: bool = False,
|
|
717
|
+
include_sensitive: bool = False,
|
|
718
|
+
) -> List[Job]:
|
|
719
|
+
jobs: List[Job] = []
|
|
720
|
+
run_jobs = sorted(run_model.jobs, key=lambda j: (j.replica_num, j.job_num, j.submission_num))
|
|
721
|
+
for replica_num, replica_submissions in itertools.groupby(
|
|
722
|
+
run_jobs, key=lambda j: j.replica_num
|
|
723
|
+
):
|
|
724
|
+
for job_num, job_models in itertools.groupby(replica_submissions, key=lambda j: j.job_num):
|
|
725
|
+
submissions = []
|
|
726
|
+
job_model = None
|
|
727
|
+
if job_submissions_limit is not None:
|
|
728
|
+
if job_submissions_limit == 0:
|
|
729
|
+
# Take latest job submission to return its job_spec
|
|
730
|
+
job_models = list(job_models)[-1:]
|
|
731
|
+
else:
|
|
732
|
+
job_models = list(job_models)[-job_submissions_limit:]
|
|
733
|
+
for job_model in job_models:
|
|
734
|
+
if job_submissions_limit != 0:
|
|
735
|
+
job_submission = job_model_to_job_submission(job_model)
|
|
736
|
+
if return_in_api:
|
|
737
|
+
# Set default non-None values for 0.18 backward-compatibility
|
|
738
|
+
# Remove in 0.19
|
|
739
|
+
if job_submission.job_provisioning_data is not None:
|
|
740
|
+
if job_submission.job_provisioning_data.hostname is None:
|
|
741
|
+
job_submission.job_provisioning_data.hostname = ""
|
|
742
|
+
if job_submission.job_provisioning_data.ssh_port is None:
|
|
743
|
+
job_submission.job_provisioning_data.ssh_port = 22
|
|
744
|
+
submissions.append(job_submission)
|
|
745
|
+
if job_model is not None:
|
|
746
|
+
# Use the spec from the latest submission. Submissions can have different specs
|
|
747
|
+
job_spec = JobSpec.__response__.parse_raw(job_model.job_spec_data)
|
|
748
|
+
if not include_sensitive:
|
|
749
|
+
_remove_job_spec_sensitive_info(job_spec)
|
|
750
|
+
jobs.append(Job(job_spec=job_spec, job_submissions=submissions))
|
|
751
|
+
return jobs
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _get_run_status_message(run_model: RunModel) -> str:
|
|
755
|
+
if len(run_model.jobs) == 0:
|
|
756
|
+
return run_model.status.value
|
|
757
|
+
|
|
758
|
+
sorted_job_models = sorted(
|
|
759
|
+
run_model.jobs, key=lambda j: (j.replica_num, j.job_num, j.submission_num)
|
|
760
|
+
)
|
|
761
|
+
job_models_grouped_by_job = list(
|
|
762
|
+
list(jm)
|
|
763
|
+
for _, jm in itertools.groupby(sorted_job_models, key=lambda j: (j.replica_num, j.job_num))
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if all(job_models[-1].status == JobStatus.PULLING for job_models in job_models_grouped_by_job):
|
|
767
|
+
# Show `pulling`` if last job submission of all jobs is pulling
|
|
768
|
+
return "pulling"
|
|
769
|
+
|
|
770
|
+
if run_model.status in [RunStatus.SUBMITTED, RunStatus.PENDING]:
|
|
771
|
+
# Show `retrying` if any job caused the run to retry
|
|
772
|
+
for job_models in job_models_grouped_by_job:
|
|
773
|
+
last_job_spec = JobSpec.__response__.parse_raw(job_models[-1].job_spec_data)
|
|
774
|
+
retry_on_events = last_job_spec.retry.on_events if last_job_spec.retry else []
|
|
775
|
+
last_job_termination_reason = _get_last_job_termination_reason(job_models)
|
|
776
|
+
if (
|
|
777
|
+
last_job_termination_reason
|
|
778
|
+
== JobTerminationReason.FAILED_TO_START_DUE_TO_NO_CAPACITY
|
|
779
|
+
and RetryEvent.NO_CAPACITY in retry_on_events
|
|
780
|
+
):
|
|
781
|
+
# TODO: Show `retrying` for other retry events
|
|
782
|
+
return "retrying"
|
|
783
|
+
|
|
784
|
+
return run_model.status.value
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _get_last_job_termination_reason(job_models: List[JobModel]) -> Optional[JobTerminationReason]:
|
|
788
|
+
for job_model in reversed(job_models):
|
|
789
|
+
if job_model.termination_reason is not None:
|
|
790
|
+
return job_model.termination_reason
|
|
791
|
+
return None
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _get_run_error(run_model: RunModel) -> Optional[str]:
|
|
795
|
+
if run_model.termination_reason is None:
|
|
796
|
+
return None
|
|
797
|
+
return run_model.termination_reason.to_error()
|
|
798
|
+
|
|
799
|
+
|
|
735
800
|
async def _get_pool_offers(
|
|
736
801
|
session: AsyncSession,
|
|
737
802
|
project: ProjectModel,
|
|
@@ -930,6 +995,8 @@ _TYPE_SPECIFIC_CONF_UPDATABLE_FIELDS = {
|
|
|
930
995
|
"replicas",
|
|
931
996
|
"scaling",
|
|
932
997
|
# rolling deployment
|
|
998
|
+
# NOTE: keep this list in sync with the "Rolling deployment" section in services.md
|
|
999
|
+
"port",
|
|
933
1000
|
"resources",
|
|
934
1001
|
"volumes",
|
|
935
1002
|
"docker",
|
|
@@ -22,7 +22,7 @@ from dstack._internal.core.errors import (
|
|
|
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
24
|
from dstack._internal.core.models.instances import SSHConnectionParams
|
|
25
|
-
from dstack._internal.core.models.runs import Run, RunSpec, ServiceModelSpec, ServiceSpec
|
|
25
|
+
from dstack._internal.core.models.runs import JobSpec, Run, RunSpec, ServiceModelSpec, ServiceSpec
|
|
26
26
|
from dstack._internal.server import settings
|
|
27
27
|
from dstack._internal.server.models import GatewayModel, JobModel, ProjectModel, RunModel
|
|
28
28
|
from dstack._internal.server.services.gateways import (
|
|
@@ -179,6 +179,7 @@ async def register_replica(
|
|
|
179
179
|
async with conn.client() as client:
|
|
180
180
|
await client.register_replica(
|
|
181
181
|
run=run,
|
|
182
|
+
job_spec=JobSpec.__response__.parse_raw(job_model.job_spec_data),
|
|
182
183
|
job_submission=job_submission,
|
|
183
184
|
ssh_head_proxy=ssh_head_proxy,
|
|
184
185
|
ssh_head_proxy_private_key=ssh_head_proxy_private_key,
|
|
@@ -44,7 +44,9 @@ async def list_users_for_user(
|
|
|
44
44
|
session: AsyncSession,
|
|
45
45
|
user: UserModel,
|
|
46
46
|
) -> List[User]:
|
|
47
|
-
|
|
47
|
+
if user.global_role == GlobalRole.ADMIN:
|
|
48
|
+
return await list_all_users(session=session)
|
|
49
|
+
return [user_model_to_user(user)]
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
async def list_all_users(
|
|
@@ -223,7 +223,7 @@ async def create_volume(
|
|
|
223
223
|
select(func.pg_advisory_xact_lock(string_to_lock_id(lock_namespace)))
|
|
224
224
|
)
|
|
225
225
|
|
|
226
|
-
lock, _ = get_locker().get_lockset(lock_namespace)
|
|
226
|
+
lock, _ = get_locker(get_db().dialect_name).get_lockset(lock_namespace)
|
|
227
227
|
async with lock:
|
|
228
228
|
if configuration.name is not None:
|
|
229
229
|
volume_model = await get_project_volume_model_by_name(
|
|
@@ -262,7 +262,7 @@ async def delete_volumes(session: AsyncSession, project: ProjectModel, names: Li
|
|
|
262
262
|
volumes_ids = sorted([v.id for v in volume_models])
|
|
263
263
|
await session.commit()
|
|
264
264
|
logger.info("Deleting volumes: %s", [v.name for v in volume_models])
|
|
265
|
-
async with get_locker().lock_ctx(VolumeModel.__tablename__, volumes_ids):
|
|
265
|
+
async with get_locker(get_db().dialect_name).lock_ctx(VolumeModel.__tablename__, volumes_ids):
|
|
266
266
|
# Refetch after lock
|
|
267
267
|
res = await session.execute(
|
|
268
268
|
select(VolumeModel)
|
|
@@ -401,6 +401,19 @@ def _validate_volume_configuration(configuration: VolumeConfiguration):
|
|
|
401
401
|
if configuration.name is not None:
|
|
402
402
|
validate_dstack_resource_name(configuration.name)
|
|
403
403
|
|
|
404
|
+
if configuration.volume_id is not None and configuration.auto_cleanup_duration is not None:
|
|
405
|
+
if (
|
|
406
|
+
isinstance(configuration.auto_cleanup_duration, int)
|
|
407
|
+
and configuration.auto_cleanup_duration > 0
|
|
408
|
+
) or (
|
|
409
|
+
isinstance(configuration.auto_cleanup_duration, str)
|
|
410
|
+
and configuration.auto_cleanup_duration not in ("off", "-1")
|
|
411
|
+
):
|
|
412
|
+
raise ServerClientError(
|
|
413
|
+
"External volumes (with volume_id) do not support auto_cleanup_duration. "
|
|
414
|
+
"Auto-cleanup only works for volumes created and managed by dstack."
|
|
415
|
+
)
|
|
416
|
+
|
|
404
417
|
|
|
405
418
|
async def _delete_volume(session: AsyncSession, project: ProjectModel, volume_model: VolumeModel):
|
|
406
419
|
volume = volume_model_to_volume(volume_model)
|
|
@@ -27,10 +27,27 @@ LOG_FORMAT = os.getenv("DSTACK_SERVER_LOG_FORMAT", "rich").lower()
|
|
|
27
27
|
ALEMBIC_MIGRATIONS_LOCATION = os.getenv(
|
|
28
28
|
"DSTACK_ALEMBIC_MIGRATIONS_LOCATION", "dstack._internal.server:migrations"
|
|
29
29
|
)
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
|
|
31
|
+
# Users may want to increase client pool size to support more concurrent resources
|
|
32
|
+
# if their db supports many connections.
|
|
33
|
+
DB_POOL_SIZE = int(os.getenv("DSTACK_DB_POOL_SIZE", 20))
|
|
34
|
+
DB_MAX_OVERFLOW = int(os.getenv("DSTACK_DB_MAX_OVERFLOW", 20))
|
|
35
|
+
|
|
36
|
+
# Scale the number of background processing tasks
|
|
37
|
+
# allowing to process more resources on one server replica.
|
|
38
|
+
# Not recommended to change on SQLite.
|
|
39
|
+
# DSTACK_DB_POOL_SIZE and DSTACK_DB_MAX_OVERFLOW
|
|
40
|
+
# must be increased proportionally.
|
|
41
|
+
SERVER_BACKGROUND_PROCESSING_FACTOR = int(
|
|
42
|
+
os.getenv("DSTACK_SERVER_BACKGROUND_PROCESSING_FACTOR", 1)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
SERVER_BACKGROUND_PROCESSING_DISABLED = (
|
|
46
|
+
os.getenv("DSTACK_SERVER_BACKGROUND_PROCESSING_DISABLED") is not None
|
|
47
|
+
)
|
|
48
|
+
SERVER_BACKGROUND_PROCESSING_ENABLED = not SERVER_BACKGROUND_PROCESSING_DISABLED
|
|
49
|
+
|
|
50
|
+
SERVER_EXECUTOR_MAX_WORKERS = int(os.getenv("DSTACK_SERVER_EXECUTOR_MAX_WORKERS", 128))
|
|
34
51
|
|
|
35
52
|
MAX_OFFERS_TRIED = int(os.getenv("DSTACK_SERVER_MAX_OFFERS_TRIED", 25))
|
|
36
53
|
|
|
@@ -97,7 +114,9 @@ SERVER_CODE_UPLOAD_LIMIT = int(os.getenv("DSTACK_SERVER_CODE_UPLOAD_LIMIT", 2 *
|
|
|
97
114
|
|
|
98
115
|
SQL_ECHO_ENABLED = os.getenv("DSTACK_SQL_ECHO_ENABLED") is not None
|
|
99
116
|
|
|
117
|
+
SERVER_PROFILING_ENABLED = os.getenv("DSTACK_SERVER_PROFILING_ENABLED") is not None
|
|
118
|
+
|
|
100
119
|
UPDATE_DEFAULT_PROJECT = os.getenv("DSTACK_UPDATE_DEFAULT_PROJECT") is not None
|
|
101
120
|
DO_NOT_UPDATE_DEFAULT_PROJECT = os.getenv("DSTACK_DO_NOT_UPDATE_DEFAULT_PROJECT") is not None
|
|
102
|
-
SKIP_GATEWAY_UPDATE = os.getenv("DSTACK_SKIP_GATEWAY_UPDATE"
|
|
103
|
-
ENABLE_PROMETHEUS_METRICS = os.getenv("DSTACK_ENABLE_PROMETHEUS_METRICS"
|
|
121
|
+
SKIP_GATEWAY_UPDATE = os.getenv("DSTACK_SKIP_GATEWAY_UPDATE") is not None
|
|
122
|
+
ENABLE_PROMETHEUS_METRICS = os.getenv("DSTACK_ENABLE_PROMETHEUS_METRICS") is not None
|
|
@@ -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-64f8273740c4b52c18f5.js"></script><link href="/main-d58fc0460cb0eae7cb5c.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>
|