dstack 0.19.19__py3-none-any.whl → 0.19.21__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/core/backends/__init__.py +0 -65
- dstack/_internal/core/backends/cloudrift/api_client.py +13 -1
- dstack/_internal/core/backends/features.py +64 -0
- dstack/_internal/core/backends/oci/resources.py +5 -5
- dstack/_internal/core/compatibility/fleets.py +2 -0
- dstack/_internal/core/compatibility/runs.py +4 -0
- dstack/_internal/core/models/profiles.py +37 -0
- dstack/_internal/server/app.py +22 -10
- dstack/_internal/server/background/__init__.py +5 -6
- dstack/_internal/server/background/tasks/process_fleets.py +52 -38
- dstack/_internal/server/background/tasks/process_gateways.py +2 -2
- dstack/_internal/server/background/tasks/process_idle_volumes.py +5 -4
- dstack/_internal/server/background/tasks/process_instances.py +62 -48
- dstack/_internal/server/background/tasks/process_metrics.py +9 -2
- dstack/_internal/server/background/tasks/process_placement_groups.py +2 -0
- dstack/_internal/server/background/tasks/process_prometheus_metrics.py +14 -2
- dstack/_internal/server/background/tasks/process_running_jobs.py +129 -124
- dstack/_internal/server/background/tasks/process_runs.py +63 -20
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +12 -10
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +12 -4
- dstack/_internal/server/background/tasks/process_volumes.py +4 -1
- dstack/_internal/server/migrations/versions/50dd7ea98639_index_status_columns.py +55 -0
- dstack/_internal/server/migrations/versions/ec02a26a256c_add_runmodel_next_triggered_at.py +38 -0
- dstack/_internal/server/models.py +16 -16
- dstack/_internal/server/schemas/logs.py +1 -9
- dstack/_internal/server/services/fleets.py +19 -10
- dstack/_internal/server/services/gateways/__init__.py +17 -17
- dstack/_internal/server/services/instances.py +10 -14
- dstack/_internal/server/services/jobs/__init__.py +10 -12
- dstack/_internal/server/services/logs/aws.py +45 -3
- dstack/_internal/server/services/logs/filelog.py +121 -11
- dstack/_internal/server/services/offers.py +3 -3
- dstack/_internal/server/services/projects.py +35 -15
- dstack/_internal/server/services/prometheus/client_metrics.py +3 -0
- dstack/_internal/server/services/prometheus/custom_metrics.py +22 -3
- dstack/_internal/server/services/runs.py +74 -34
- dstack/_internal/server/services/services/__init__.py +4 -1
- dstack/_internal/server/services/users.py +2 -3
- dstack/_internal/server/services/volumes.py +11 -11
- dstack/_internal/server/settings.py +3 -0
- dstack/_internal/server/statics/index.html +1 -1
- dstack/_internal/server/statics/{main-64f8273740c4b52c18f5.js → main-39a767528976f8078166.js} +7 -26
- dstack/_internal/server/statics/{main-64f8273740c4b52c18f5.js.map → main-39a767528976f8078166.js.map} +1 -1
- dstack/_internal/server/statics/{main-d58fc0460cb0eae7cb5c.css → main-8f9ee218d3eb45989682.css} +2 -2
- dstack/_internal/server/testing/common.py +7 -0
- dstack/_internal/server/utils/sentry_utils.py +12 -0
- dstack/_internal/utils/common.py +10 -21
- dstack/_internal/utils/cron.py +5 -0
- dstack/version.py +1 -1
- {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/METADATA +2 -11
- {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/RECORD +54 -49
- {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/WHEEL +0 -0
- {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import itertools
|
|
2
|
+
import json
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
from collections.abc import Generator, Iterable
|
|
4
|
-
from datetime import timezone
|
|
5
5
|
from typing import ClassVar
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
@@ -79,7 +79,7 @@ async def get_instance_metrics(session: AsyncSession) -> Iterable[Metric]:
|
|
|
79
79
|
"dstack_backend": instance.backend.value if instance.backend is not None else "",
|
|
80
80
|
"dstack_gpu": gpu,
|
|
81
81
|
}
|
|
82
|
-
duration = (now - instance.created_at
|
|
82
|
+
duration = (now - instance.created_at).total_seconds()
|
|
83
83
|
metrics.add_sample(_INSTANCE_DURATION, labels, duration)
|
|
84
84
|
metrics.add_sample(_INSTANCE_PRICE, labels, instance.price or 0.0)
|
|
85
85
|
metrics.add_sample(_INSTANCE_GPU_COUNT, labels, gpu_count)
|
|
@@ -166,7 +166,7 @@ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
|
|
|
166
166
|
"dstack_backend": jpd.get_base_backend().value,
|
|
167
167
|
"dstack_gpu": gpus[0].name if gpus else "",
|
|
168
168
|
}
|
|
169
|
-
duration = (now - job.submitted_at
|
|
169
|
+
duration = (now - job.submitted_at).total_seconds()
|
|
170
170
|
metrics.add_sample(_JOB_DURATION, labels, duration)
|
|
171
171
|
metrics.add_sample(_JOB_PRICE, labels, price)
|
|
172
172
|
metrics.add_sample(_JOB_GPU_COUNT, labels, len(gpus))
|
|
@@ -177,6 +177,19 @@ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
|
|
|
177
177
|
metrics.add_sample(_JOB_CPU_TIME, labels, jmp.cpu_usage_micro / 1_000_000)
|
|
178
178
|
metrics.add_sample(_JOB_MEMORY_USAGE, labels, jmp.memory_usage_bytes)
|
|
179
179
|
metrics.add_sample(_JOB_MEMORY_WORKING_SET, labels, jmp.memory_working_set_bytes)
|
|
180
|
+
if gpus:
|
|
181
|
+
gpu_memory_total = gpus[0].memory_mib * 1024 * 1024
|
|
182
|
+
for gpu_num, (gpu_util, gpu_memory_usage) in enumerate(
|
|
183
|
+
zip(
|
|
184
|
+
json.loads(jmp.gpus_util_percent),
|
|
185
|
+
json.loads(jmp.gpus_memory_usage_bytes),
|
|
186
|
+
)
|
|
187
|
+
):
|
|
188
|
+
gpu_labels = labels.copy()
|
|
189
|
+
gpu_labels["dstack_gpu_num"] = gpu_num
|
|
190
|
+
metrics.add_sample(_JOB_GPU_USAGE_RATIO, gpu_labels, gpu_util / 100)
|
|
191
|
+
metrics.add_sample(_JOB_GPU_MEMORY_TOTAL, gpu_labels, gpu_memory_total)
|
|
192
|
+
metrics.add_sample(_JOB_GPU_MEMORY_USAGE, gpu_labels, gpu_memory_usage)
|
|
180
193
|
jpm = job_prometheus_metrics.get(job.id)
|
|
181
194
|
if jpm is not None:
|
|
182
195
|
for metric in text_string_to_metric_families(jpm.text):
|
|
@@ -202,6 +215,9 @@ _JOB_CPU_TIME = "dstack_job_cpu_time_seconds_total"
|
|
|
202
215
|
_JOB_MEMORY_TOTAL = "dstack_job_memory_total_bytes"
|
|
203
216
|
_JOB_MEMORY_USAGE = "dstack_job_memory_usage_bytes"
|
|
204
217
|
_JOB_MEMORY_WORKING_SET = "dstack_job_memory_working_set_bytes"
|
|
218
|
+
_JOB_GPU_USAGE_RATIO = "dstack_job_gpu_usage_ratio"
|
|
219
|
+
_JOB_GPU_MEMORY_TOTAL = "dstack_job_gpu_memory_total_bytes"
|
|
220
|
+
_JOB_GPU_MEMORY_USAGE = "dstack_job_gpu_memory_usage_bytes"
|
|
205
221
|
|
|
206
222
|
|
|
207
223
|
class _Metrics(dict[str, Metric]):
|
|
@@ -259,6 +275,9 @@ class _JobMetrics(_Metrics):
|
|
|
259
275
|
(_JOB_MEMORY_TOTAL, _GAUGE, "Total memory allocated for the job, bytes"),
|
|
260
276
|
(_JOB_MEMORY_USAGE, _GAUGE, "Memory used by the job (including cache), bytes"),
|
|
261
277
|
(_JOB_MEMORY_WORKING_SET, _GAUGE, "Memory used by the job (not including cache), bytes"),
|
|
278
|
+
(_JOB_GPU_USAGE_RATIO, _GAUGE, "Job GPU usage, percent (as 0.0-1.0)"),
|
|
279
|
+
(_JOB_GPU_MEMORY_TOTAL, _GAUGE, "Total GPU memory allocated for the job, bytes"),
|
|
280
|
+
(_JOB_GPU_MEMORY_USAGE, _GAUGE, "GPU memory used by the job, bytes"),
|
|
262
281
|
]
|
|
263
282
|
|
|
264
283
|
|
|
@@ -5,9 +5,10 @@ from datetime import datetime, timezone
|
|
|
5
5
|
from typing import List, Optional
|
|
6
6
|
|
|
7
7
|
import pydantic
|
|
8
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
8
9
|
from sqlalchemy import and_, func, or_, select, update
|
|
9
10
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
-
from sqlalchemy.orm import joinedload
|
|
11
|
+
from sqlalchemy.orm import joinedload
|
|
11
12
|
|
|
12
13
|
import dstack._internal.utils.common as common_utils
|
|
13
14
|
from dstack._internal.core.errors import (
|
|
@@ -42,7 +43,6 @@ from dstack._internal.core.models.runs import (
|
|
|
42
43
|
RunTerminationReason,
|
|
43
44
|
ServiceSpec,
|
|
44
45
|
)
|
|
45
|
-
from dstack._internal.core.models.users import GlobalRole
|
|
46
46
|
from dstack._internal.core.models.volumes import (
|
|
47
47
|
InstanceMountPoint,
|
|
48
48
|
Volume,
|
|
@@ -81,7 +81,7 @@ from dstack._internal.server.services.locking import get_locker, string_to_lock_
|
|
|
81
81
|
from dstack._internal.server.services.logging import fmt
|
|
82
82
|
from dstack._internal.server.services.offers import get_offers_by_requirements
|
|
83
83
|
from dstack._internal.server.services.plugins import apply_plugin_policies
|
|
84
|
-
from dstack._internal.server.services.projects import
|
|
84
|
+
from dstack._internal.server.services.projects import list_user_project_models
|
|
85
85
|
from dstack._internal.server.services.resources import set_resources_defaults
|
|
86
86
|
from dstack._internal.server.services.secrets import get_project_secrets_mapping
|
|
87
87
|
from dstack._internal.server.services.users import get_user_model_by_name
|
|
@@ -115,10 +115,11 @@ async def list_user_runs(
|
|
|
115
115
|
) -> List[Run]:
|
|
116
116
|
if project_name is None and repo_id is not None:
|
|
117
117
|
return []
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
projects = await list_user_project_models(
|
|
119
|
+
session=session,
|
|
120
|
+
user=user,
|
|
121
|
+
only_names=True,
|
|
122
|
+
)
|
|
122
123
|
runs_user = None
|
|
123
124
|
if username is not None:
|
|
124
125
|
runs_user = await get_user_model_by_name(session=session, username=username)
|
|
@@ -217,9 +218,9 @@ async def list_projects_run_models(
|
|
|
217
218
|
res = await session.execute(
|
|
218
219
|
select(RunModel)
|
|
219
220
|
.where(*filters)
|
|
221
|
+
.options(joinedload(RunModel.user).load_only(UserModel.name))
|
|
220
222
|
.order_by(*order_by)
|
|
221
223
|
.limit(limit)
|
|
222
|
-
.options(selectinload(RunModel.user))
|
|
223
224
|
)
|
|
224
225
|
run_models = list(res.scalars().all())
|
|
225
226
|
return run_models
|
|
@@ -511,6 +512,14 @@ async def submit_run(
|
|
|
511
512
|
)
|
|
512
513
|
|
|
513
514
|
submitted_at = common_utils.get_current_datetime()
|
|
515
|
+
initial_status = RunStatus.SUBMITTED
|
|
516
|
+
initial_replicas = 1
|
|
517
|
+
if run_spec.merged_profile.schedule is not None:
|
|
518
|
+
initial_status = RunStatus.PENDING
|
|
519
|
+
initial_replicas = 0
|
|
520
|
+
elif run_spec.configuration.type == "service":
|
|
521
|
+
initial_replicas = run_spec.configuration.replicas.min
|
|
522
|
+
|
|
514
523
|
run_model = RunModel(
|
|
515
524
|
id=uuid.uuid4(),
|
|
516
525
|
project_id=project.id,
|
|
@@ -519,21 +528,20 @@ async def submit_run(
|
|
|
519
528
|
user_id=user.id,
|
|
520
529
|
run_name=run_spec.run_name,
|
|
521
530
|
submitted_at=submitted_at,
|
|
522
|
-
status=
|
|
531
|
+
status=initial_status,
|
|
523
532
|
run_spec=run_spec.json(),
|
|
524
533
|
last_processed_at=submitted_at,
|
|
525
534
|
priority=run_spec.configuration.priority,
|
|
526
535
|
deployment_num=0,
|
|
527
536
|
desired_replica_count=1, # a relevant value will be set in process_runs.py
|
|
537
|
+
next_triggered_at=_get_next_triggered_at(run_spec),
|
|
528
538
|
)
|
|
529
539
|
session.add(run_model)
|
|
530
540
|
|
|
531
|
-
replicas = 1
|
|
532
541
|
if run_spec.configuration.type == "service":
|
|
533
|
-
replicas = run_spec.configuration.replicas.min
|
|
534
542
|
await services.register_service(session, run_model, run_spec)
|
|
535
543
|
|
|
536
|
-
for replica_num in range(
|
|
544
|
+
for replica_num in range(initial_replicas):
|
|
537
545
|
jobs = await get_jobs_from_run_spec(
|
|
538
546
|
run_spec=run_spec,
|
|
539
547
|
secrets=secrets,
|
|
@@ -693,8 +701,8 @@ def run_model_to_run(
|
|
|
693
701
|
id=run_model.id,
|
|
694
702
|
project_name=run_model.project.name,
|
|
695
703
|
user=run_model.user.name,
|
|
696
|
-
submitted_at=run_model.submitted_at
|
|
697
|
-
last_processed_at=run_model.last_processed_at
|
|
704
|
+
submitted_at=run_model.submitted_at,
|
|
705
|
+
last_processed_at=run_model.last_processed_at,
|
|
698
706
|
status=run_model.status,
|
|
699
707
|
status_message=status_message,
|
|
700
708
|
termination_reason=run_model.termination_reason,
|
|
@@ -972,6 +980,12 @@ def _validate_run_spec_and_set_defaults(run_spec: RunSpec):
|
|
|
972
980
|
raise ServerClientError(
|
|
973
981
|
f"Maximum utilization_policy.time_window is {settings.SERVER_METRICS_RUNNING_TTL_SECONDS}s"
|
|
974
982
|
)
|
|
983
|
+
if (
|
|
984
|
+
run_spec.merged_profile.schedule
|
|
985
|
+
and run_spec.configuration.type == "service"
|
|
986
|
+
and run_spec.configuration.replicas.min == 0
|
|
987
|
+
):
|
|
988
|
+
raise ServerClientError("Scheduled services with autoscaling to zero are not supported")
|
|
975
989
|
if run_spec.configuration.priority is None:
|
|
976
990
|
run_spec.configuration.priority = RUN_PRIORITY_DEFAULT
|
|
977
991
|
set_resources_defaults(run_spec.configuration.resources)
|
|
@@ -1059,7 +1073,7 @@ def _check_can_update_configuration(
|
|
|
1059
1073
|
)
|
|
1060
1074
|
|
|
1061
1075
|
|
|
1062
|
-
async def process_terminating_run(session: AsyncSession,
|
|
1076
|
+
async def process_terminating_run(session: AsyncSession, run_model: RunModel):
|
|
1063
1077
|
"""
|
|
1064
1078
|
Used by both `process_runs` and `stop_run` to process a TERMINATING run.
|
|
1065
1079
|
Stops the jobs gracefully and marks them as TERMINATING.
|
|
@@ -1067,44 +1081,54 @@ async def process_terminating_run(session: AsyncSession, run: RunModel):
|
|
|
1067
1081
|
When all jobs are terminated, assigns a finished status to the run.
|
|
1068
1082
|
Caller must acquire the lock on run.
|
|
1069
1083
|
"""
|
|
1070
|
-
assert
|
|
1071
|
-
|
|
1084
|
+
assert run_model.termination_reason is not None
|
|
1085
|
+
run = run_model_to_run(run_model, include_jobs=False)
|
|
1086
|
+
job_termination_reason = run_model.termination_reason.to_job_termination_reason()
|
|
1072
1087
|
|
|
1073
1088
|
unfinished_jobs_count = 0
|
|
1074
|
-
for
|
|
1075
|
-
if
|
|
1089
|
+
for job_model in run_model.jobs:
|
|
1090
|
+
if job_model.status.is_finished():
|
|
1076
1091
|
continue
|
|
1077
1092
|
unfinished_jobs_count += 1
|
|
1078
|
-
if
|
|
1093
|
+
if job_model.status == JobStatus.TERMINATING:
|
|
1079
1094
|
if job_termination_reason == JobTerminationReason.ABORTED_BY_USER:
|
|
1080
1095
|
# Override termination reason so that
|
|
1081
1096
|
# abort actions such as volume force detach are triggered
|
|
1082
|
-
|
|
1097
|
+
job_model.termination_reason = job_termination_reason
|
|
1083
1098
|
continue
|
|
1084
1099
|
|
|
1085
|
-
if
|
|
1100
|
+
if job_model.status == JobStatus.RUNNING and job_termination_reason not in {
|
|
1086
1101
|
JobTerminationReason.ABORTED_BY_USER,
|
|
1087
1102
|
JobTerminationReason.DONE_BY_RUNNER,
|
|
1088
1103
|
}:
|
|
1089
1104
|
# Send a signal to stop the job gracefully
|
|
1090
|
-
await stop_runner(session,
|
|
1091
|
-
delay_job_instance_termination(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1105
|
+
await stop_runner(session, job_model)
|
|
1106
|
+
delay_job_instance_termination(job_model)
|
|
1107
|
+
job_model.status = JobStatus.TERMINATING
|
|
1108
|
+
job_model.termination_reason = job_termination_reason
|
|
1109
|
+
job_model.last_processed_at = common_utils.get_current_datetime()
|
|
1095
1110
|
|
|
1096
1111
|
if unfinished_jobs_count == 0:
|
|
1097
|
-
if
|
|
1112
|
+
if run_model.service_spec is not None:
|
|
1098
1113
|
try:
|
|
1099
|
-
await services.unregister_service(session,
|
|
1114
|
+
await services.unregister_service(session, run_model)
|
|
1100
1115
|
except Exception as e:
|
|
1101
|
-
logger.warning("%s: failed to unregister service: %s", fmt(
|
|
1102
|
-
|
|
1116
|
+
logger.warning("%s: failed to unregister service: %s", fmt(run_model), repr(e))
|
|
1117
|
+
if (
|
|
1118
|
+
run.run_spec.merged_profile.schedule is not None
|
|
1119
|
+
and run_model.termination_reason
|
|
1120
|
+
not in [RunTerminationReason.ABORTED_BY_USER, RunTerminationReason.STOPPED_BY_USER]
|
|
1121
|
+
):
|
|
1122
|
+
run_model.next_triggered_at = _get_next_triggered_at(run.run_spec)
|
|
1123
|
+
run_model.status = RunStatus.PENDING
|
|
1124
|
+
else:
|
|
1125
|
+
run_model.status = run_model.termination_reason.to_status()
|
|
1126
|
+
|
|
1103
1127
|
logger.info(
|
|
1104
1128
|
"%s: run status has changed TERMINATING -> %s, reason: %s",
|
|
1105
|
-
fmt(
|
|
1106
|
-
|
|
1107
|
-
|
|
1129
|
+
fmt(run_model),
|
|
1130
|
+
run_model.status.name,
|
|
1131
|
+
run_model.termination_reason.name,
|
|
1108
1132
|
)
|
|
1109
1133
|
|
|
1110
1134
|
|
|
@@ -1224,3 +1248,19 @@ async def retry_run_replica_jobs(
|
|
|
1224
1248
|
|
|
1225
1249
|
def _remove_job_spec_sensitive_info(spec: JobSpec):
|
|
1226
1250
|
spec.ssh_key = None
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def _get_next_triggered_at(run_spec: RunSpec) -> Optional[datetime]:
|
|
1254
|
+
if run_spec.merged_profile.schedule is None:
|
|
1255
|
+
return None
|
|
1256
|
+
now = common_utils.get_current_datetime()
|
|
1257
|
+
fire_times = []
|
|
1258
|
+
for cron in run_spec.merged_profile.schedule.crons:
|
|
1259
|
+
cron_trigger = CronTrigger.from_crontab(cron, timezone=timezone.utc)
|
|
1260
|
+
fire_times.append(
|
|
1261
|
+
cron_trigger.get_next_fire_time(
|
|
1262
|
+
previous_fire_time=None,
|
|
1263
|
+
now=now,
|
|
1264
|
+
)
|
|
1265
|
+
)
|
|
1266
|
+
return min(fire_times)
|
|
@@ -28,6 +28,7 @@ from dstack._internal.server.models import GatewayModel, JobModel, ProjectModel,
|
|
|
28
28
|
from dstack._internal.server.services.gateways import (
|
|
29
29
|
get_gateway_configuration,
|
|
30
30
|
get_or_add_gateway_connection,
|
|
31
|
+
get_project_default_gateway_model,
|
|
31
32
|
get_project_gateway_model_by_name,
|
|
32
33
|
)
|
|
33
34
|
from dstack._internal.server.services.logging import fmt
|
|
@@ -52,7 +53,9 @@ async def register_service(session: AsyncSession, run_model: RunModel, run_spec:
|
|
|
52
53
|
elif run_spec.configuration.gateway == False:
|
|
53
54
|
gateway = None
|
|
54
55
|
else:
|
|
55
|
-
gateway =
|
|
56
|
+
gateway = await get_project_default_gateway_model(
|
|
57
|
+
session=session, project=run_model.project
|
|
58
|
+
)
|
|
56
59
|
|
|
57
60
|
if gateway is not None:
|
|
58
61
|
service_spec = await _register_service_in_gateway(session, run_model, run_spec, gateway)
|
|
@@ -2,7 +2,6 @@ import hashlib
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import uuid
|
|
5
|
-
from datetime import timezone
|
|
6
5
|
from typing import Awaitable, Callable, List, Optional, Tuple
|
|
7
6
|
|
|
8
7
|
from sqlalchemy import delete, select, update
|
|
@@ -195,7 +194,7 @@ def user_model_to_user(user_model: UserModel) -> User:
|
|
|
195
194
|
return User(
|
|
196
195
|
id=user_model.id,
|
|
197
196
|
username=user_model.name,
|
|
198
|
-
created_at=user_model.created_at
|
|
197
|
+
created_at=user_model.created_at,
|
|
199
198
|
global_role=user_model.global_role,
|
|
200
199
|
email=user_model.email,
|
|
201
200
|
active=user_model.active,
|
|
@@ -207,7 +206,7 @@ def user_model_to_user_with_creds(user_model: UserModel) -> UserWithCreds:
|
|
|
207
206
|
return UserWithCreds(
|
|
208
207
|
id=user_model.id,
|
|
209
208
|
username=user_model.name,
|
|
210
|
-
created_at=user_model.created_at
|
|
209
|
+
created_at=user_model.created_at,
|
|
211
210
|
global_role=user_model.global_role,
|
|
212
211
|
email=user_model.email,
|
|
213
212
|
active=user_model.active,
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from datetime import datetime, timedelta
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
3
|
from typing import List, Optional
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import and_, func, or_, select, update
|
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
7
|
from sqlalchemy.orm import joinedload, selectinload
|
|
8
8
|
|
|
9
|
-
from dstack._internal.core.backends import BACKENDS_WITH_VOLUMES_SUPPORT
|
|
10
9
|
from dstack._internal.core.backends.base.compute import ComputeWithVolumeSupport
|
|
10
|
+
from dstack._internal.core.backends.features import BACKENDS_WITH_VOLUMES_SUPPORT
|
|
11
11
|
from dstack._internal.core.errors import (
|
|
12
12
|
BackendNotAvailable,
|
|
13
13
|
ResourceExistsError,
|
|
14
14
|
ServerClientError,
|
|
15
15
|
)
|
|
16
|
-
from dstack._internal.core.models.users import GlobalRole
|
|
17
16
|
from dstack._internal.core.models.volumes import (
|
|
18
17
|
Volume,
|
|
19
18
|
VolumeAttachment,
|
|
@@ -40,7 +39,7 @@ from dstack._internal.server.services.locking import (
|
|
|
40
39
|
string_to_lock_id,
|
|
41
40
|
)
|
|
42
41
|
from dstack._internal.server.services.plugins import apply_plugin_policies
|
|
43
|
-
from dstack._internal.server.services.projects import
|
|
42
|
+
from dstack._internal.server.services.projects import list_user_project_models
|
|
44
43
|
from dstack._internal.utils import common, random_names
|
|
45
44
|
from dstack._internal.utils.logging import get_logger
|
|
46
45
|
|
|
@@ -57,10 +56,11 @@ async def list_volumes(
|
|
|
57
56
|
limit: int,
|
|
58
57
|
ascending: bool,
|
|
59
58
|
) -> List[Volume]:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
projects = await list_user_project_models(
|
|
60
|
+
session=session,
|
|
61
|
+
user=user,
|
|
62
|
+
only_names=True,
|
|
63
|
+
)
|
|
64
64
|
if project_name is not None:
|
|
65
65
|
projects = [p for p in projects if p.name == project_name]
|
|
66
66
|
volume_models = await list_projects_volume_models(
|
|
@@ -320,15 +320,15 @@ def volume_model_to_volume(volume_model: VolumeModel) -> Volume:
|
|
|
320
320
|
)
|
|
321
321
|
deleted_at = None
|
|
322
322
|
if volume_model.deleted_at is not None:
|
|
323
|
-
deleted_at = volume_model.deleted_at
|
|
323
|
+
deleted_at = volume_model.deleted_at
|
|
324
324
|
volume = Volume(
|
|
325
325
|
name=volume_model.name,
|
|
326
326
|
project_name=volume_model.project.name,
|
|
327
327
|
user=volume_model.user.name,
|
|
328
328
|
configuration=configuration,
|
|
329
329
|
external=configuration.volume_id is not None,
|
|
330
|
-
created_at=volume_model.created_at
|
|
331
|
-
last_processed_at=volume_model.last_processed_at
|
|
330
|
+
created_at=volume_model.created_at,
|
|
331
|
+
last_processed_at=volume_model.last_processed_at,
|
|
332
332
|
status=volume_model.status,
|
|
333
333
|
status_message=volume_model.status_message,
|
|
334
334
|
deleted=volume_model.deleted,
|
|
@@ -93,6 +93,9 @@ DEFAULT_PROJECT_NAME = "main"
|
|
|
93
93
|
|
|
94
94
|
SENTRY_DSN = os.getenv("DSTACK_SENTRY_DSN")
|
|
95
95
|
SENTRY_TRACES_SAMPLE_RATE = float(os.getenv("DSTACK_SENTRY_TRACES_SAMPLE_RATE", 0.1))
|
|
96
|
+
SENTRY_TRACES_BACKGROUND_SAMPLE_RATE = float(
|
|
97
|
+
os.getenv("DSTACK_SENTRY_TRACES_BACKGROUND_SAMPLE_RATE", 0.01)
|
|
98
|
+
)
|
|
96
99
|
SENTRY_PROFILES_SAMPLE_RATE = float(os.getenv("DSTACK_SENTRY_PROFILES_SAMPLE_RATE", 0))
|
|
97
100
|
|
|
98
101
|
DEFAULT_CREDS_DISABLED = os.getenv("DSTACK_DEFAULT_CREDS_DISABLED") 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-39a767528976f8078166.js"></script><link href="/main-8f9ee218d3eb45989682.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>
|