dstack 0.0.9__py3-none-any.whl → 0.20.7__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/__init__.py +80 -0
- dstack/_internal/cli/commands/apply.py +100 -0
- dstack/_internal/cli/commands/attach.py +161 -0
- dstack/_internal/cli/commands/completion.py +22 -0
- dstack/_internal/cli/commands/delete.py +44 -0
- dstack/_internal/cli/commands/event.py +168 -0
- dstack/_internal/cli/commands/fleet.py +161 -0
- dstack/_internal/cli/commands/gateway.py +159 -0
- dstack/_internal/cli/commands/init.py +64 -0
- dstack/_internal/cli/commands/login.py +352 -0
- dstack/_internal/cli/commands/logs.py +62 -0
- dstack/_internal/cli/commands/metrics.py +153 -0
- dstack/_internal/cli/commands/offer.py +146 -0
- dstack/_internal/cli/commands/project.py +259 -0
- dstack/_internal/cli/commands/ps.py +81 -0
- dstack/_internal/cli/commands/run.py +69 -0
- dstack/_internal/cli/commands/secrets.py +92 -0
- dstack/_internal/cli/commands/server.py +96 -0
- dstack/_internal/cli/commands/stop.py +26 -0
- dstack/_internal/cli/commands/volume.py +117 -0
- dstack/_internal/cli/main.py +101 -0
- dstack/_internal/cli/models/gateways.py +16 -0
- dstack/_internal/cli/models/offers.py +47 -0
- dstack/_internal/cli/models/runs.py +16 -0
- dstack/_internal/cli/services/args.py +31 -0
- dstack/_internal/cli/services/completion.py +91 -0
- dstack/_internal/cli/services/configurators/__init__.py +86 -0
- dstack/_internal/cli/services/configurators/base.py +103 -0
- dstack/_internal/cli/services/configurators/fleet.py +475 -0
- dstack/_internal/cli/services/configurators/gateway.py +231 -0
- dstack/_internal/cli/services/configurators/run.py +882 -0
- dstack/_internal/cli/services/configurators/volume.py +222 -0
- dstack/_internal/cli/services/events.py +68 -0
- dstack/_internal/cli/services/profile.py +182 -0
- dstack/_internal/cli/services/repos.py +71 -0
- dstack/_internal/cli/services/resources.py +54 -0
- dstack/_internal/cli/utils/common.py +159 -0
- dstack/_internal/cli/utils/fleet.py +106 -0
- dstack/_internal/cli/utils/gateway.py +56 -0
- dstack/_internal/cli/utils/gpu.py +178 -0
- dstack/_internal/cli/utils/rich.py +156 -0
- dstack/_internal/cli/utils/run.py +517 -0
- dstack/_internal/cli/utils/secrets.py +25 -0
- dstack/_internal/cli/utils/updates.py +98 -0
- dstack/_internal/cli/utils/volume.py +58 -0
- dstack/_internal/compat.py +3 -0
- dstack/_internal/core/backends/amddevcloud/__init__.py +1 -0
- dstack/_internal/core/backends/amddevcloud/backend.py +16 -0
- dstack/_internal/core/backends/amddevcloud/compute.py +5 -0
- dstack/_internal/core/backends/amddevcloud/configurator.py +29 -0
- dstack/_internal/core/backends/aws/auth.py +30 -0
- dstack/_internal/core/backends/aws/backend.py +31 -0
- dstack/_internal/core/backends/aws/compute.py +1153 -0
- dstack/_internal/core/backends/aws/configurator.py +191 -0
- dstack/_internal/core/backends/aws/models.py +135 -0
- dstack/_internal/core/backends/aws/resources.py +700 -0
- dstack/_internal/core/backends/azure/auth.py +39 -0
- dstack/_internal/core/backends/azure/backend.py +21 -0
- dstack/_internal/core/backends/azure/compute.py +676 -0
- dstack/_internal/core/backends/azure/configurator.py +472 -0
- dstack/_internal/core/backends/azure/models.py +98 -0
- dstack/_internal/core/backends/azure/resources.py +116 -0
- dstack/_internal/core/backends/azure/utils.py +42 -0
- dstack/_internal/core/backends/base/backend.py +18 -0
- dstack/_internal/core/backends/base/compute.py +1101 -0
- dstack/_internal/core/backends/base/configurator.py +117 -0
- dstack/_internal/core/backends/base/models.py +24 -0
- dstack/_internal/core/backends/base/offers.py +232 -0
- dstack/_internal/core/backends/cloudrift/api_client.py +220 -0
- dstack/_internal/core/backends/cloudrift/backend.py +16 -0
- dstack/_internal/core/backends/cloudrift/compute.py +138 -0
- dstack/_internal/core/backends/cloudrift/configurator.py +72 -0
- dstack/_internal/core/backends/cloudrift/models.py +40 -0
- dstack/_internal/core/backends/configurators.py +181 -0
- dstack/_internal/core/backends/cudo/__init__.py +0 -0
- dstack/_internal/core/backends/cudo/api_client.py +111 -0
- dstack/_internal/core/backends/cudo/backend.py +16 -0
- dstack/_internal/core/backends/cudo/compute.py +174 -0
- dstack/_internal/core/backends/cudo/configurator.py +63 -0
- dstack/_internal/core/backends/cudo/models.py +37 -0
- dstack/_internal/core/backends/datacrunch/__init__.py +1 -0
- dstack/_internal/core/backends/datacrunch/backend.py +18 -0
- dstack/_internal/core/backends/datacrunch/compute.py +8 -0
- dstack/_internal/core/backends/datacrunch/configurator.py +17 -0
- dstack/_internal/core/backends/digitalocean/__init__.py +1 -0
- dstack/_internal/core/backends/digitalocean/backend.py +16 -0
- dstack/_internal/core/backends/digitalocean/compute.py +5 -0
- dstack/_internal/core/backends/digitalocean/configurator.py +31 -0
- dstack/_internal/core/backends/digitalocean_base/__init__.py +1 -0
- dstack/_internal/core/backends/digitalocean_base/api_client.py +104 -0
- dstack/_internal/core/backends/digitalocean_base/backend.py +5 -0
- dstack/_internal/core/backends/digitalocean_base/compute.py +174 -0
- dstack/_internal/core/backends/digitalocean_base/configurator.py +57 -0
- dstack/_internal/core/backends/digitalocean_base/models.py +43 -0
- dstack/_internal/core/backends/dstack/__init__.py +0 -0
- dstack/_internal/core/backends/dstack/models.py +26 -0
- dstack/_internal/core/backends/features.py +74 -0
- dstack/_internal/core/backends/gcp/__init__.py +0 -0
- dstack/_internal/core/backends/gcp/auth.py +57 -0
- dstack/_internal/core/backends/gcp/backend.py +17 -0
- dstack/_internal/core/backends/gcp/compute.py +1257 -0
- dstack/_internal/core/backends/gcp/configurator.py +206 -0
- dstack/_internal/core/backends/gcp/features/__init__.py +0 -0
- dstack/_internal/core/backends/gcp/features/tcpx.py +65 -0
- dstack/_internal/core/backends/gcp/models.py +160 -0
- dstack/_internal/core/backends/gcp/resources.py +585 -0
- dstack/_internal/core/backends/hotaisle/__init__.py +1 -0
- dstack/_internal/core/backends/hotaisle/api_client.py +101 -0
- dstack/_internal/core/backends/hotaisle/backend.py +16 -0
- dstack/_internal/core/backends/hotaisle/compute.py +188 -0
- dstack/_internal/core/backends/hotaisle/configurator.py +66 -0
- dstack/_internal/core/backends/hotaisle/models.py +45 -0
- dstack/_internal/core/backends/kubernetes/__init__.py +0 -0
- dstack/_internal/core/backends/kubernetes/backend.py +16 -0
- dstack/_internal/core/backends/kubernetes/compute.py +1077 -0
- dstack/_internal/core/backends/kubernetes/configurator.py +61 -0
- dstack/_internal/core/backends/kubernetes/models.py +71 -0
- dstack/_internal/core/backends/kubernetes/utils.py +81 -0
- dstack/_internal/core/backends/lambdalabs/__init__.py +0 -0
- dstack/_internal/core/backends/lambdalabs/api_client.py +87 -0
- dstack/_internal/core/backends/lambdalabs/backend.py +17 -0
- dstack/_internal/core/backends/lambdalabs/compute.py +233 -0
- dstack/_internal/core/backends/lambdalabs/configurator.py +65 -0
- dstack/_internal/core/backends/lambdalabs/models.py +37 -0
- dstack/_internal/core/backends/local/__init__.py +0 -0
- dstack/_internal/core/backends/local/backend.py +14 -0
- dstack/_internal/core/backends/local/compute.py +130 -0
- dstack/_internal/core/backends/models.py +158 -0
- dstack/_internal/core/backends/nebius/__init__.py +0 -0
- dstack/_internal/core/backends/nebius/backend.py +16 -0
- dstack/_internal/core/backends/nebius/compute.py +401 -0
- dstack/_internal/core/backends/nebius/configurator.py +98 -0
- dstack/_internal/core/backends/nebius/models.py +185 -0
- dstack/_internal/core/backends/nebius/resources.py +433 -0
- dstack/_internal/core/backends/oci/__init__.py +0 -0
- dstack/_internal/core/backends/oci/auth.py +21 -0
- dstack/_internal/core/backends/oci/backend.py +16 -0
- dstack/_internal/core/backends/oci/compute.py +209 -0
- dstack/_internal/core/backends/oci/configurator.py +156 -0
- dstack/_internal/core/backends/oci/exceptions.py +15 -0
- dstack/_internal/core/backends/oci/models.py +87 -0
- dstack/_internal/core/backends/oci/region.py +86 -0
- dstack/_internal/core/backends/oci/resources.py +836 -0
- dstack/_internal/core/backends/runpod/__init__.py +0 -0
- dstack/_internal/core/backends/runpod/api_client.py +627 -0
- dstack/_internal/core/backends/runpod/backend.py +16 -0
- dstack/_internal/core/backends/runpod/compute.py +444 -0
- dstack/_internal/core/backends/runpod/configurator.py +63 -0
- dstack/_internal/core/backends/runpod/models.py +54 -0
- dstack/_internal/core/backends/template/__init__.py +0 -0
- dstack/_internal/core/backends/template/backend.py.jinja +16 -0
- dstack/_internal/core/backends/template/compute.py.jinja +95 -0
- dstack/_internal/core/backends/template/configurator.py.jinja +69 -0
- dstack/_internal/core/backends/template/models.py.jinja +62 -0
- dstack/_internal/core/backends/tensordock/models.py +40 -0
- dstack/_internal/core/backends/vastai/__init__.py +0 -0
- dstack/_internal/core/backends/vastai/api_client.py +143 -0
- dstack/_internal/core/backends/vastai/backend.py +16 -0
- dstack/_internal/core/backends/vastai/compute.py +141 -0
- dstack/_internal/core/backends/vastai/configurator.py +69 -0
- dstack/_internal/core/backends/vastai/models.py +37 -0
- dstack/_internal/core/backends/verda/__init__.py +0 -0
- dstack/_internal/core/backends/verda/backend.py +16 -0
- dstack/_internal/core/backends/verda/compute.py +266 -0
- dstack/_internal/core/backends/verda/configurator.py +73 -0
- dstack/_internal/core/backends/verda/models.py +38 -0
- dstack/_internal/core/backends/vultr/__init__.py +0 -0
- dstack/_internal/core/backends/vultr/api_client.py +116 -0
- dstack/_internal/core/backends/vultr/backend.py +16 -0
- dstack/_internal/core/backends/vultr/compute.py +167 -0
- dstack/_internal/core/backends/vultr/configurator.py +71 -0
- dstack/_internal/core/backends/vultr/models.py +34 -0
- dstack/_internal/core/compatibility/__init__.py +0 -0
- dstack/_internal/core/compatibility/events.py +13 -0
- dstack/_internal/core/compatibility/fleets.py +58 -0
- dstack/_internal/core/compatibility/gateways.py +39 -0
- dstack/_internal/core/compatibility/gpus.py +13 -0
- dstack/_internal/core/compatibility/logs.py +14 -0
- dstack/_internal/core/compatibility/runs.py +86 -0
- dstack/_internal/core/compatibility/volumes.py +37 -0
- dstack/_internal/core/consts.py +8 -0
- dstack/_internal/core/errors.py +160 -0
- dstack/_internal/core/models/__init__.py +0 -0
- dstack/_internal/core/models/auth.py +28 -0
- dstack/_internal/core/models/backends/__init__.py +0 -0
- dstack/_internal/core/models/backends/base.py +48 -0
- dstack/_internal/core/models/common.py +143 -0
- dstack/_internal/core/models/compute_groups.py +39 -0
- dstack/_internal/core/models/config.py +28 -0
- dstack/_internal/core/models/configurations.py +1123 -0
- dstack/_internal/core/models/envs.py +149 -0
- dstack/_internal/core/models/events.py +98 -0
- dstack/_internal/core/models/files.py +67 -0
- dstack/_internal/core/models/fleets.py +437 -0
- dstack/_internal/core/models/gateways.py +146 -0
- dstack/_internal/core/models/gpus.py +45 -0
- dstack/_internal/core/models/health.py +28 -0
- dstack/_internal/core/models/instances.py +346 -0
- dstack/_internal/core/models/logs.py +27 -0
- dstack/_internal/core/models/metrics.py +14 -0
- dstack/_internal/core/models/placement.py +27 -0
- dstack/_internal/core/models/profiles.py +431 -0
- dstack/_internal/core/models/projects.py +46 -0
- dstack/_internal/core/models/repos/__init__.py +34 -0
- dstack/_internal/core/models/repos/base.py +36 -0
- dstack/_internal/core/models/repos/local.py +96 -0
- dstack/_internal/core/models/repos/remote.py +341 -0
- dstack/_internal/core/models/repos/virtual.py +85 -0
- dstack/_internal/core/models/resources.py +424 -0
- dstack/_internal/core/models/routers.py +24 -0
- dstack/_internal/core/models/runs.py +618 -0
- dstack/_internal/core/models/secrets.py +16 -0
- dstack/_internal/core/models/server.py +7 -0
- dstack/_internal/core/models/services.py +76 -0
- dstack/_internal/core/models/unix.py +53 -0
- dstack/_internal/core/models/users.py +60 -0
- dstack/_internal/core/models/volumes.py +221 -0
- dstack/_internal/core/services/__init__.py +16 -0
- dstack/_internal/core/services/api_client.py +15 -0
- dstack/_internal/core/services/configs/__init__.py +116 -0
- dstack/_internal/core/services/diff.py +71 -0
- dstack/_internal/core/services/logs.py +58 -0
- dstack/_internal/core/services/profiles.py +46 -0
- dstack/_internal/core/services/repos.py +236 -0
- dstack/_internal/core/services/ssh/__init__.py +27 -0
- dstack/_internal/core/services/ssh/attach.py +241 -0
- dstack/_internal/core/services/ssh/client.py +113 -0
- dstack/_internal/core/services/ssh/key_manager.py +53 -0
- dstack/_internal/core/services/ssh/ports.py +89 -0
- dstack/_internal/core/services/ssh/tunnel.py +337 -0
- dstack/_internal/proxy/__init__.py +8 -0
- dstack/_internal/proxy/gateway/__init__.py +0 -0
- dstack/_internal/proxy/gateway/app.py +89 -0
- dstack/_internal/proxy/gateway/auth.py +26 -0
- dstack/_internal/proxy/gateway/const.py +7 -0
- dstack/_internal/proxy/gateway/deps.py +73 -0
- dstack/_internal/proxy/gateway/main.py +17 -0
- dstack/_internal/proxy/gateway/models.py +23 -0
- dstack/_internal/proxy/gateway/repo/__init__.py +0 -0
- dstack/_internal/proxy/gateway/repo/repo.py +121 -0
- dstack/_internal/proxy/gateway/repo/state_v1.py +164 -0
- dstack/_internal/proxy/gateway/resources/nginx/00-log-format.conf +11 -0
- dstack/_internal/proxy/gateway/resources/nginx/entrypoint.jinja2 +27 -0
- dstack/_internal/proxy/gateway/resources/nginx/router_workers.jinja2 +23 -0
- dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +105 -0
- dstack/_internal/proxy/gateway/routers/__init__.py +0 -0
- dstack/_internal/proxy/gateway/routers/auth.py +10 -0
- dstack/_internal/proxy/gateway/routers/config.py +28 -0
- dstack/_internal/proxy/gateway/routers/registry.py +124 -0
- dstack/_internal/proxy/gateway/routers/stats.py +18 -0
- dstack/_internal/proxy/gateway/schemas/__init__.py +0 -0
- dstack/_internal/proxy/gateway/schemas/common.py +5 -0
- dstack/_internal/proxy/gateway/schemas/config.py +9 -0
- dstack/_internal/proxy/gateway/schemas/registry.py +63 -0
- dstack/_internal/proxy/gateway/schemas/stats.py +15 -0
- dstack/_internal/proxy/gateway/services/__init__.py +0 -0
- dstack/_internal/proxy/gateway/services/model_routers/__init__.py +18 -0
- dstack/_internal/proxy/gateway/services/model_routers/base.py +91 -0
- dstack/_internal/proxy/gateway/services/model_routers/sglang.py +269 -0
- dstack/_internal/proxy/gateway/services/nginx.py +455 -0
- dstack/_internal/proxy/gateway/services/registry.py +426 -0
- dstack/_internal/proxy/gateway/services/server_client.py +95 -0
- dstack/_internal/proxy/gateway/services/stats.py +170 -0
- dstack/_internal/proxy/gateway/testing/__init__.py +0 -0
- dstack/_internal/proxy/gateway/testing/common.py +13 -0
- dstack/_internal/proxy/lib/__init__.py +0 -0
- dstack/_internal/proxy/lib/auth.py +7 -0
- dstack/_internal/proxy/lib/deps.py +106 -0
- dstack/_internal/proxy/lib/errors.py +14 -0
- dstack/_internal/proxy/lib/models.py +112 -0
- dstack/_internal/proxy/lib/repo.py +27 -0
- dstack/_internal/proxy/lib/routers/__init__.py +0 -0
- dstack/_internal/proxy/lib/routers/model_proxy.py +102 -0
- dstack/_internal/proxy/lib/schemas/__init__.py +0 -0
- dstack/_internal/proxy/lib/schemas/model_proxy.py +77 -0
- dstack/_internal/proxy/lib/services/__init__.py +0 -0
- dstack/_internal/proxy/lib/services/model_proxy/__init__.py +0 -0
- dstack/_internal/proxy/lib/services/model_proxy/clients/__init__.py +0 -0
- dstack/_internal/proxy/lib/services/model_proxy/clients/base.py +18 -0
- dstack/_internal/proxy/lib/services/model_proxy/clients/openai.py +67 -0
- dstack/_internal/proxy/lib/services/model_proxy/clients/tgi.py +208 -0
- dstack/_internal/proxy/lib/services/model_proxy/model_proxy.py +23 -0
- dstack/_internal/proxy/lib/services/service_connection.py +160 -0
- dstack/_internal/proxy/lib/testing/__init__.py +0 -0
- dstack/_internal/proxy/lib/testing/auth.py +11 -0
- dstack/_internal/proxy/lib/testing/common.py +51 -0
- dstack/_internal/server/__init__.py +0 -0
- dstack/_internal/server/alembic.ini +100 -0
- dstack/_internal/server/app.py +432 -0
- dstack/_internal/server/background/__init__.py +142 -0
- dstack/_internal/server/background/tasks/__init__.py +0 -0
- dstack/_internal/server/background/tasks/common.py +24 -0
- dstack/_internal/server/background/tasks/process_compute_groups.py +167 -0
- dstack/_internal/server/background/tasks/process_events.py +17 -0
- dstack/_internal/server/background/tasks/process_fleets.py +289 -0
- dstack/_internal/server/background/tasks/process_gateways.py +188 -0
- dstack/_internal/server/background/tasks/process_idle_volumes.py +145 -0
- dstack/_internal/server/background/tasks/process_instances.py +1186 -0
- dstack/_internal/server/background/tasks/process_metrics.py +172 -0
- dstack/_internal/server/background/tasks/process_placement_groups.py +104 -0
- dstack/_internal/server/background/tasks/process_probes.py +164 -0
- dstack/_internal/server/background/tasks/process_prometheus_metrics.py +150 -0
- dstack/_internal/server/background/tasks/process_running_jobs.py +1238 -0
- dstack/_internal/server/background/tasks/process_runs.py +842 -0
- dstack/_internal/server/background/tasks/process_submitted_jobs.py +1106 -0
- dstack/_internal/server/background/tasks/process_terminating_jobs.py +108 -0
- dstack/_internal/server/background/tasks/process_volumes.py +129 -0
- dstack/_internal/server/compatibility/__init__.py +0 -0
- dstack/_internal/server/compatibility/common.py +20 -0
- dstack/_internal/server/compatibility/gpus.py +22 -0
- dstack/_internal/server/db.py +127 -0
- dstack/_internal/server/deps.py +19 -0
- dstack/_internal/server/main.py +4 -0
- dstack/_internal/server/migrations/__init__.py +0 -0
- dstack/_internal/server/migrations/env.py +112 -0
- dstack/_internal/server/migrations/script.py.mako +28 -0
- dstack/_internal/server/migrations/versions/006512f572b4_add_projects_original_name.py +38 -0
- dstack/_internal/server/migrations/versions/065588ec72b8_add_vultr_to_backendtype_enum.py +81 -0
- dstack/_internal/server/migrations/versions/06e977bc61c7_add_usermodel_deleted_and_original_name.py +45 -0
- dstack/_internal/server/migrations/versions/0e33559e16ed_update_instancestatus.py +64 -0
- dstack/_internal/server/migrations/versions/112753bc17dd_remove_nullable_fields.py +50 -0
- dstack/_internal/server/migrations/versions/1338b788b612_reverse_job_instance_relationship.py +71 -0
- dstack/_internal/server/migrations/versions/14f2cb002fc2_add_jobmodel_removed_flag.py +44 -0
- dstack/_internal/server/migrations/versions/1a48dfe44a40_rework_termination_handling.py +42 -0
- dstack/_internal/server/migrations/versions/1aa9638ad963_added_email_index.py +31 -0
- dstack/_internal/server/migrations/versions/1e3fb39ef74b_add_remote_connection_details.py +26 -0
- dstack/_internal/server/migrations/versions/1e76fb0dde87_add_jobmodel_inactivity_secs.py +32 -0
- dstack/_internal/server/migrations/versions/20166748b60c_add_jobmodel_disconnected_at.py +100 -0
- dstack/_internal/server/migrations/versions/22d74df9897e_add_events_and_event_targets.py +99 -0
- dstack/_internal/server/migrations/versions/23e01c56279a_make_blob_nullable.py +32 -0
- dstack/_internal/server/migrations/versions/2498ab323443_add_fleetmodel_consolidation_attempt_.py +44 -0
- dstack/_internal/server/migrations/versions/252d3743b641_.py +40 -0
- dstack/_internal/server/migrations/versions/25479f540245_add_probes.py +43 -0
- dstack/_internal/server/migrations/versions/27d3e55759fa_add_pools.py +152 -0
- dstack/_internal/server/migrations/versions/29826f417010_remove_instancemodel_retry_policy.py +34 -0
- dstack/_internal/server/migrations/versions/29c08c6a8cb3_.py +36 -0
- dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +42 -0
- dstack/_internal/server/migrations/versions/35f732ee4cf5_add_projectmodel_is_public.py +39 -0
- dstack/_internal/server/migrations/versions/3cf77fb8bcf1_store_repo_clone_url.py +85 -0
- dstack/_internal/server/migrations/versions/3d7f6c2ec000_add_jobmodel_registered.py +28 -0
- dstack/_internal/server/migrations/versions/3dbdce90d0e0_fix_code_uq_constraint.py +33 -0
- dstack/_internal/server/migrations/versions/48ad3ecbaea2_do_not_delete_projects_and_runs.py +46 -0
- dstack/_internal/server/migrations/versions/4ae1a5b0e7f1_add_run_list_index.py +34 -0
- dstack/_internal/server/migrations/versions/4b4319398164_introduce_runs_processing.py +144 -0
- dstack/_internal/server/migrations/versions/50dd7ea98639_index_status_columns.py +55 -0
- dstack/_internal/server/migrations/versions/51d45659d574_add_instancemodel_blocks_fields.py +43 -0
- dstack/_internal/server/migrations/versions/54a77e19c64c_add_manager_project_role.py +67 -0
- dstack/_internal/server/migrations/versions/555138b1f77f_change_instancemodel_for_asynchronous_.py +61 -0
- dstack/_internal/server/migrations/versions/58aa5162dcc3_add_gatewaymodel_configuration.py +32 -0
- dstack/_internal/server/migrations/versions/5ad8debc8fe6_fixes_for_psql.py +329 -0
- dstack/_internal/server/migrations/versions/5ec538b70e71_replace_instansestatus.py +31 -0
- dstack/_internal/server/migrations/versions/5f1707c525d2_add_filearchivemodel.py +39 -0
- dstack/_internal/server/migrations/versions/5fd659afca82_add_ix_instances_fleet_id.py +31 -0
- dstack/_internal/server/migrations/versions/60e444118b6d_add_jobprometheusmetrics.py +40 -0
- dstack/_internal/server/migrations/versions/63c3f19cb184_add_jobterminationreason_inactivity_.py +83 -0
- dstack/_internal/server/migrations/versions/644b8a114187_add_secretmodel.py +49 -0
- dstack/_internal/server/migrations/versions/686fb8341ea5_add_user_emails.py +32 -0
- dstack/_internal/server/migrations/versions/6c1a9d6530ee_add_jobmodel_exit_status.py +26 -0
- dstack/_internal/server/migrations/versions/706e0acc3a7d_add_runmodel_desired_replica_counts.py +26 -0
- dstack/_internal/server/migrations/versions/710e5b3fac8f_add_encryption.py +54 -0
- dstack/_internal/server/migrations/versions/728b1488b1b4_add_instance_health.py +50 -0
- dstack/_internal/server/migrations/versions/74a1f55209bd_store_enums_as_strings.py +484 -0
- dstack/_internal/server/migrations/versions/7b24b1c8eba7_add_instancemodel_last_processed_at.py +68 -0
- dstack/_internal/server/migrations/versions/7ba3b59d7ca6_add_runmodel_resubmission_attempt.py +35 -0
- dstack/_internal/server/migrations/versions/7bc2586e8b9e_make_instancemodel_pool_id_optional.py +36 -0
- dstack/_internal/server/migrations/versions/7d1ec2b920ac_add_computegroupmodel.py +91 -0
- dstack/_internal/server/migrations/versions/803c7e9ed85d_add_jobmodel_job_runtime_data.py +32 -0
- dstack/_internal/server/migrations/versions/82b32a135ea2_.py +58 -0
- dstack/_internal/server/migrations/versions/866ec1d67184_replace_retrypolicy_limit_with_.py +93 -0
- dstack/_internal/server/migrations/versions/903c91e24634_add_instances_termination_reason_message.py +34 -0
- dstack/_internal/server/migrations/versions/91a12fff6c76_add_repocredsmodel.py +43 -0
- dstack/_internal/server/migrations/versions/91ac5e543037_extend_repos_creds_column.py +36 -0
- dstack/_internal/server/migrations/versions/98cd9c8b5927_add_volumemodel.py +73 -0
- dstack/_internal/server/migrations/versions/98d1b92988bc_add_jobterminationreason_terminated_due_.py +140 -0
- dstack/_internal/server/migrations/versions/99b4c8c954ea_add_termination_reason_message.py +71 -0
- dstack/_internal/server/migrations/versions/9eea6af28e10_added_fail_reason_for_instancemodel.py +36 -0
- dstack/_internal/server/migrations/versions/__init__.py +0 -0
- dstack/_internal/server/migrations/versions/a060e2440936_.py +206 -0
- dstack/_internal/server/migrations/versions/a751ef183f27_move_attachment_data_to_volumes_.py +34 -0
- dstack/_internal/server/migrations/versions/a7b46c073fa1_add_placementgroupmodel.py +58 -0
- dstack/_internal/server/migrations/versions/afbc600ff2b2_add_created_at_to_usermodel_and_.py +102 -0
- dstack/_internal/server/migrations/versions/b4d6ad60db08_add_instancemodel_unreachable.py +37 -0
- dstack/_internal/server/migrations/versions/b88d55c2a07d_replace_instancestatus_ready.py +21 -0
- dstack/_internal/server/migrations/versions/bc8ca4a505c6_store_backendtype_as_string.py +171 -0
- dstack/_internal/server/migrations/versions/bca2fdf130bf_add_runmodel_priority.py +34 -0
- dstack/_internal/server/migrations/versions/bfba43f6def2_.py +32 -0
- dstack/_internal/server/migrations/versions/c00090eaef21_support_fleets.py +108 -0
- dstack/_internal/server/migrations/versions/c154eece89da_add_fields_for_async_gateway_creation.py +74 -0
- dstack/_internal/server/migrations/versions/c20626d03cfb_add_jobmetricspoint.py +43 -0
- dstack/_internal/server/migrations/versions/c48df7985d57_add_instance_termination_retries.py +38 -0
- dstack/_internal/server/migrations/versions/c83d45f9a971_replace_string_with_text.py +150 -0
- dstack/_internal/server/migrations/versions/d0bb68e48b9f_add_project_owners_and_quotas.py +106 -0
- dstack/_internal/server/migrations/versions/d3e8af4786fa_gateway_compute_flag_deleted.py +34 -0
- dstack/_internal/server/migrations/versions/d4d9dc26cf58_add_ix_jobs_run_id.py +31 -0
- dstack/_internal/server/migrations/versions/d5863798bf41_add_volumemodel_last_job_processed_at.py +40 -0
- dstack/_internal/server/migrations/versions/d6b11105f659_add_usermodel_active.py +36 -0
- dstack/_internal/server/migrations/versions/da574e93fee0_add_jobmodel_volumes_detached_at.py +40 -0
- dstack/_internal/server/migrations/versions/dfffd6a1165c_add_fields_for_gateways_behind_alb.py +36 -0
- dstack/_internal/server/migrations/versions/e2d08cd1b8d9_add_jobmodel_fleet.py +41 -0
- dstack/_internal/server/migrations/versions/e3b7db07727f_add_gatewaycomputemodel_app_updated_at.py +61 -0
- dstack/_internal/server/migrations/versions/e6391ca6a264_separate_gateways_from_compute.py +72 -0
- dstack/_internal/server/migrations/versions/ea60480f82bb_add_membermodel_member_num.py +32 -0
- dstack/_internal/server/migrations/versions/ec02a26a256c_add_runmodel_next_triggered_at.py +38 -0
- dstack/_internal/server/migrations/versions/ed0ca30e13bb_migrate_instancestatus_provisioning.py +29 -0
- dstack/_internal/server/migrations/versions/fe72c4de8376_add_gateways.py +81 -0
- dstack/_internal/server/migrations/versions/ff1d94f65b08_user_ssh_key.py +34 -0
- dstack/_internal/server/migrations/versions/ffa99edd1988_add_jobterminationreason_max_duration_.py +81 -0
- dstack/_internal/server/models.py +930 -0
- dstack/_internal/server/routers/__init__.py +0 -0
- dstack/_internal/server/routers/auth.py +34 -0
- dstack/_internal/server/routers/backends.py +142 -0
- dstack/_internal/server/routers/events.py +60 -0
- dstack/_internal/server/routers/files.py +68 -0
- dstack/_internal/server/routers/fleets.py +202 -0
- dstack/_internal/server/routers/gateways.py +109 -0
- dstack/_internal/server/routers/gpus.py +32 -0
- dstack/_internal/server/routers/instances.py +77 -0
- dstack/_internal/server/routers/logs.py +34 -0
- dstack/_internal/server/routers/metrics.py +82 -0
- dstack/_internal/server/routers/projects.py +205 -0
- dstack/_internal/server/routers/prometheus.py +35 -0
- dstack/_internal/server/routers/repos.py +118 -0
- dstack/_internal/server/routers/runs.py +216 -0
- dstack/_internal/server/routers/secrets.py +86 -0
- dstack/_internal/server/routers/server.py +19 -0
- dstack/_internal/server/routers/users.py +158 -0
- dstack/_internal/server/routers/volumes.py +122 -0
- dstack/_internal/server/schemas/__init__.py +0 -0
- dstack/_internal/server/schemas/auth.py +83 -0
- dstack/_internal/server/schemas/backends.py +16 -0
- dstack/_internal/server/schemas/common.py +9 -0
- dstack/_internal/server/schemas/events.py +211 -0
- dstack/_internal/server/schemas/files.py +5 -0
- dstack/_internal/server/schemas/fleets.py +49 -0
- dstack/_internal/server/schemas/gateways.py +31 -0
- dstack/_internal/server/schemas/gpus.py +26 -0
- dstack/_internal/server/schemas/health/__init__.py +0 -0
- dstack/_internal/server/schemas/health/dcgm.py +56 -0
- dstack/_internal/server/schemas/instances.py +47 -0
- dstack/_internal/server/schemas/logs.py +17 -0
- dstack/_internal/server/schemas/projects.py +81 -0
- dstack/_internal/server/schemas/repos.py +24 -0
- dstack/_internal/server/schemas/runner.py +269 -0
- dstack/_internal/server/schemas/runs.py +66 -0
- dstack/_internal/server/schemas/secrets.py +16 -0
- dstack/_internal/server/schemas/users.py +72 -0
- dstack/_internal/server/schemas/volumes.py +29 -0
- dstack/_internal/server/security/__init__.py +0 -0
- dstack/_internal/server/security/permissions.py +251 -0
- dstack/_internal/server/services/__init__.py +0 -0
- dstack/_internal/server/services/auth.py +77 -0
- dstack/_internal/server/services/backends/__init__.py +404 -0
- dstack/_internal/server/services/backends/handlers.py +105 -0
- dstack/_internal/server/services/compute_groups.py +22 -0
- dstack/_internal/server/services/config.py +279 -0
- dstack/_internal/server/services/docker.py +162 -0
- dstack/_internal/server/services/encryption/__init__.py +102 -0
- dstack/_internal/server/services/encryption/keys/__init__.py +0 -0
- dstack/_internal/server/services/encryption/keys/aes.py +68 -0
- dstack/_internal/server/services/encryption/keys/base.py +19 -0
- dstack/_internal/server/services/encryption/keys/identity.py +28 -0
- dstack/_internal/server/services/events.py +477 -0
- dstack/_internal/server/services/files.py +91 -0
- dstack/_internal/server/services/fleets.py +1224 -0
- dstack/_internal/server/services/gateways/__init__.py +686 -0
- dstack/_internal/server/services/gateways/client.py +209 -0
- dstack/_internal/server/services/gateways/connection.py +139 -0
- dstack/_internal/server/services/gateways/pool.py +58 -0
- dstack/_internal/server/services/gpus.py +387 -0
- dstack/_internal/server/services/instances.py +731 -0
- dstack/_internal/server/services/jobs/__init__.py +840 -0
- dstack/_internal/server/services/jobs/configurators/__init__.py +0 -0
- dstack/_internal/server/services/jobs/configurators/base.py +469 -0
- dstack/_internal/server/services/jobs/configurators/dev.py +69 -0
- dstack/_internal/server/services/jobs/configurators/extensions/__init__.py +0 -0
- dstack/_internal/server/services/jobs/configurators/extensions/base.py +15 -0
- dstack/_internal/server/services/jobs/configurators/extensions/cursor.py +42 -0
- dstack/_internal/server/services/jobs/configurators/extensions/vscode.py +42 -0
- dstack/_internal/server/services/jobs/configurators/extensions/windsurf.py +43 -0
- dstack/_internal/server/services/jobs/configurators/service.py +28 -0
- dstack/_internal/server/services/jobs/configurators/task.py +39 -0
- dstack/_internal/server/services/locking.py +187 -0
- dstack/_internal/server/services/logging.py +29 -0
- dstack/_internal/server/services/logs/__init__.py +122 -0
- dstack/_internal/server/services/logs/aws.py +373 -0
- dstack/_internal/server/services/logs/base.py +47 -0
- dstack/_internal/server/services/logs/filelog.py +261 -0
- dstack/_internal/server/services/logs/fluentbit.py +329 -0
- dstack/_internal/server/services/logs/gcp.py +181 -0
- dstack/_internal/server/services/metrics.py +172 -0
- dstack/_internal/server/services/offers.py +249 -0
- dstack/_internal/server/services/permissions.py +37 -0
- dstack/_internal/server/services/placement.py +234 -0
- dstack/_internal/server/services/plugins.py +109 -0
- dstack/_internal/server/services/probes.py +10 -0
- dstack/_internal/server/services/projects.py +835 -0
- dstack/_internal/server/services/prometheus/__init__.py +0 -0
- dstack/_internal/server/services/prometheus/client_metrics.py +55 -0
- dstack/_internal/server/services/prometheus/custom_metrics.py +327 -0
- dstack/_internal/server/services/proxy/__init__.py +3 -0
- dstack/_internal/server/services/proxy/auth.py +12 -0
- dstack/_internal/server/services/proxy/deps.py +18 -0
- dstack/_internal/server/services/proxy/repo.py +189 -0
- dstack/_internal/server/services/proxy/routers/__init__.py +0 -0
- dstack/_internal/server/services/proxy/routers/service_proxy.py +49 -0
- dstack/_internal/server/services/proxy/services/__init__.py +0 -0
- dstack/_internal/server/services/proxy/services/service_proxy.py +135 -0
- dstack/_internal/server/services/repos.py +362 -0
- dstack/_internal/server/services/requirements/__init__.py +0 -0
- dstack/_internal/server/services/requirements/combine.py +260 -0
- dstack/_internal/server/services/resources.py +21 -0
- dstack/_internal/server/services/runner/__init__.py +0 -0
- dstack/_internal/server/services/runner/client.py +646 -0
- dstack/_internal/server/services/runner/ssh.py +128 -0
- dstack/_internal/server/services/runs/__init__.py +1026 -0
- dstack/_internal/server/services/runs/plan.py +703 -0
- dstack/_internal/server/services/runs/replicas.py +317 -0
- dstack/_internal/server/services/runs/spec.py +191 -0
- dstack/_internal/server/services/secrets.py +245 -0
- dstack/_internal/server/services/services/__init__.py +345 -0
- dstack/_internal/server/services/services/autoscalers.py +140 -0
- dstack/_internal/server/services/services/options.py +53 -0
- dstack/_internal/server/services/ssh.py +67 -0
- dstack/_internal/server/services/storage/__init__.py +37 -0
- dstack/_internal/server/services/storage/base.py +48 -0
- dstack/_internal/server/services/storage/gcs.py +66 -0
- dstack/_internal/server/services/storage/s3.py +69 -0
- dstack/_internal/server/services/users.py +461 -0
- dstack/_internal/server/services/volumes.py +496 -0
- dstack/_internal/server/settings.py +161 -0
- dstack/_internal/server/statics/00a6e1fb461ed2929fb9.png +0 -0
- dstack/_internal/server/statics/0cae4d9f0a36034984a7.png +0 -0
- dstack/_internal/server/statics/391de232cc0e30cae513.png +0 -0
- dstack/_internal/server/statics/4e0eead8c1a73689ef9d.svg +1 -0
- dstack/_internal/server/statics/544afa2f63428c2235b0.png +0 -0
- dstack/_internal/server/statics/54a4f50f74c6b9381530.svg +7 -0
- dstack/_internal/server/statics/68dd1360a7d2611e0132.svg +4 -0
- dstack/_internal/server/statics/69544b4c81973b54a66f.png +0 -0
- dstack/_internal/server/statics/77a8b02b17af19e39266.png +0 -0
- dstack/_internal/server/statics/83a93a8871c219104367.svg +9 -0
- dstack/_internal/server/statics/8f28bb8e9999e5e6a48b.svg +4 -0
- dstack/_internal/server/statics/9124086961ab8c366bc4.svg +9 -0
- dstack/_internal/server/statics/9a9ebaeb54b025dbac0a.svg +5 -0
- dstack/_internal/server/statics/a3428392dc534f3b15c4.svg +7 -0
- dstack/_internal/server/statics/ae22625574d69361f72c.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-144x144.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-192x192.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-256x256.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-36x36.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-384x384.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-48x48.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-512x512.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-72x72.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-96x96.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-1024x1024.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-114x114.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-120x120.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-144x144.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-152x152.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-167x167.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-180x180.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-57x57.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-60x60.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-72x72.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-76x76.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-precomposed.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1125x2436.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1136x640.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1170x2532.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1179x2556.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2208.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2688.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1284x2778.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1290x2796.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1334x750.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1488x2266.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1536x2048.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1620x2160.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1640x2160.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2224.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2388.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1792x828.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x1536.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x2732.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1620.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1640.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2208x1242.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2224x1668.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2266x1488.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2388x1668.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2436x1125.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2532x1170.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2556x1179.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2688x1242.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2732x2048.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2778x1284.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2796x1290.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-640x1136.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-750x1334.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-828x1792.png +0 -0
- dstack/_internal/server/statics/assets/browserconfig.xml +12 -0
- dstack/_internal/server/statics/assets/favicon-16x16.png +0 -0
- dstack/_internal/server/statics/assets/favicon-32x32.png +0 -0
- dstack/_internal/server/statics/assets/favicon-48x48.png +0 -0
- dstack/_internal/server/statics/assets/favicon.ico +0 -0
- dstack/{dashboard/statics/assets/manifest.json → _internal/server/statics/assets/manifest.webmanifest} +18 -9
- dstack/_internal/server/statics/assets/mstile-144x144.png +0 -0
- dstack/_internal/server/statics/assets/mstile-150x150.png +0 -0
- dstack/_internal/server/statics/assets/mstile-310x150.png +0 -0
- dstack/_internal/server/statics/assets/mstile-310x310.png +0 -0
- dstack/_internal/server/statics/assets/mstile-70x70.png +0 -0
- dstack/_internal/server/statics/assets/yandex-browser-50x50.png +0 -0
- dstack/_internal/server/statics/b7ae68f44193474fc578.png +0 -0
- dstack/_internal/server/statics/d2f008c75b2b5b191f3f.png +0 -0
- dstack/_internal/server/statics/d44c33e1b92e05c379fd.png +0 -0
- dstack/_internal/server/statics/dd43ff0552815179d7ab.png +0 -0
- dstack/_internal/server/statics/dd4e7166c0b9aac197d7.png +0 -0
- dstack/_internal/server/statics/e30b27916930d43d2271.png +0 -0
- dstack/_internal/server/statics/e467d7d60aae81ab198b.svg +6 -0
- dstack/_internal/server/statics/eb9b344b73818fe2b71a.png +0 -0
- dstack/_internal/server/statics/f517dd626eb964120de0.png +0 -0
- dstack/_internal/server/statics/f958aecddee5d8e3222c.png +0 -0
- dstack/_internal/server/statics/index.html +3 -0
- dstack/_internal/server/statics/logo-notext.svg +116 -0
- dstack/_internal/server/statics/main-2e6967bad9f29395eea6.css +3 -0
- dstack/_internal/server/statics/main-7dc0f6d20b8b41659acc.js +155547 -0
- dstack/_internal/server/statics/main-7dc0f6d20b8b41659acc.js.map +1 -0
- dstack/{dashboard → _internal/server}/statics/manifest.json +2 -2
- dstack/_internal/server/statics/static/media/entraID.d65d1f3e9486a8e56d24fc07b3230885.svg +9 -0
- dstack/_internal/server/statics/static/media/google.b194b06fafd0a52aeb566922160ea514.svg +1 -0
- dstack/{dashboard/statics/static/media/logo.f9d7170678f68f796e270698633770ec.svg → _internal/server/statics/static/media/logo.f602feeb138844eda97c8cb641461448.svg} +8 -6
- dstack/_internal/server/statics/static/media/okta.12f178e6873a1100965f2a4dbd18fcec.svg +2 -0
- dstack/_internal/server/statics/static/media/theme.3994c817bb7dda191c1c9640dee0bf42.svg +3 -0
- dstack/_internal/server/testing/__init__.py +0 -0
- dstack/_internal/server/testing/common.py +1220 -0
- dstack/_internal/server/testing/conf.py +53 -0
- dstack/_internal/server/testing/matchers.py +31 -0
- dstack/_internal/server/utils/__init__.py +0 -0
- dstack/_internal/server/utils/common.py +55 -0
- dstack/_internal/server/utils/logging.py +51 -0
- dstack/_internal/server/utils/provisioning.py +368 -0
- dstack/_internal/server/utils/routers.py +166 -0
- dstack/_internal/server/utils/sentry_utils.py +24 -0
- dstack/_internal/settings.py +49 -0
- dstack/_internal/utils/__init__.py +0 -0
- dstack/_internal/utils/common.py +318 -0
- dstack/_internal/utils/cron.py +5 -0
- dstack/_internal/utils/crypto.py +40 -0
- dstack/_internal/utils/env.py +88 -0
- dstack/_internal/utils/event_loop.py +30 -0
- dstack/_internal/utils/files.py +69 -0
- dstack/_internal/utils/gpu.py +59 -0
- dstack/_internal/utils/hash.py +31 -0
- dstack/_internal/utils/interpolator.py +91 -0
- dstack/_internal/utils/json_schema.py +11 -0
- dstack/_internal/utils/json_utils.py +54 -0
- dstack/_internal/utils/logging.py +5 -0
- dstack/_internal/utils/nested_list.py +47 -0
- dstack/_internal/utils/network.py +50 -0
- dstack/_internal/utils/path.py +57 -0
- dstack/_internal/utils/random_names.py +258 -0
- dstack/_internal/utils/ssh.py +346 -0
- dstack/_internal/utils/tags.py +42 -0
- dstack/_internal/utils/typing.py +14 -0
- dstack/_internal/utils/version.py +22 -0
- dstack/api/__init__.py +46 -0
- dstack/api/_public/__init__.py +96 -0
- dstack/api/_public/backends.py +42 -0
- dstack/api/_public/common.py +5 -0
- dstack/api/_public/repos.py +202 -0
- dstack/api/_public/runs.py +714 -0
- dstack/api/server/__init__.py +206 -0
- dstack/api/server/_auth.py +30 -0
- dstack/api/server/_backends.py +38 -0
- dstack/api/server/_events.py +64 -0
- dstack/api/server/_files.py +18 -0
- dstack/api/server/_fleets.py +82 -0
- dstack/api/server/_gateways.py +54 -0
- dstack/api/server/_gpus.py +27 -0
- dstack/api/server/_group.py +22 -0
- dstack/api/server/_logs.py +15 -0
- dstack/api/server/_metrics.py +23 -0
- dstack/api/server/_projects.py +124 -0
- dstack/api/server/_repos.py +64 -0
- dstack/api/server/_runs.py +102 -0
- dstack/api/server/_secrets.py +36 -0
- dstack/api/server/_users.py +82 -0
- dstack/api/server/_volumes.py +39 -0
- dstack/api/server/utils.py +34 -0
- dstack/api/utils.py +105 -0
- dstack/core/__init__.py +0 -0
- dstack/plugins/__init__.py +8 -0
- dstack/plugins/_base.py +72 -0
- dstack/plugins/_models.py +8 -0
- dstack/plugins/_utils.py +19 -0
- dstack/plugins/builtin/__init__.py +0 -0
- dstack/plugins/builtin/rest_plugin/__init__.py +18 -0
- dstack/plugins/builtin/rest_plugin/_models.py +48 -0
- dstack/plugins/builtin/rest_plugin/_plugin.py +147 -0
- dstack/version.py +3 -1
- dstack-0.20.7.dist-info/METADATA +519 -0
- dstack-0.20.7.dist-info/RECORD +720 -0
- {dstack-0.0.9.dist-info → dstack-0.20.7.dist-info}/WHEEL +1 -2
- dstack-0.20.7.dist-info/entry_points.txt +2 -0
- dstack-0.20.7.dist-info/licenses/LICENSE.md +353 -0
- dstack/aws/__init__.py +0 -180
- dstack/aws/artifacts.py +0 -111
- dstack/aws/config.py +0 -40
- dstack/aws/jobs.py +0 -245
- dstack/aws/logs.py +0 -186
- dstack/aws/repos.py +0 -137
- dstack/aws/run_names.py +0 -17
- dstack/aws/runners.py +0 -693
- dstack/aws/runs.py +0 -79
- dstack/aws/secrets.py +0 -99
- dstack/aws/tags.py +0 -138
- dstack/backend.py +0 -299
- dstack/cli/app.py +0 -41
- dstack/cli/artifacts.py +0 -87
- dstack/cli/common.py +0 -57
- dstack/cli/config.py +0 -194
- dstack/cli/dashboard.py +0 -26
- dstack/cli/delete.py +0 -49
- dstack/cli/init.py +0 -33
- dstack/cli/logs.py +0 -87
- dstack/cli/main.py +0 -81
- dstack/cli/restart.py +0 -43
- dstack/cli/run.py +0 -223
- dstack/cli/schema.py +0 -46
- dstack/cli/secrets.py +0 -97
- dstack/cli/status.py +0 -140
- dstack/cli/stop.py +0 -53
- dstack/cli/tags.py +0 -100
- dstack/config.py +0 -80
- dstack/dashboard/artifacts.py +0 -26
- dstack/dashboard/logs.py +0 -73
- dstack/dashboard/main.py +0 -45
- dstack/dashboard/repos.py +0 -41
- dstack/dashboard/runs.py +0 -140
- dstack/dashboard/secrets.py +0 -53
- dstack/dashboard/statics/4d6a4e032505c1efd23c.png +0 -0
- dstack/dashboard/statics/7e018c3e5566d7c349a8.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-144x144.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-192x192.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-256x256.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-36x36.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-384x384.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-48x48.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-512x512.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-72x72.png +0 -0
- dstack/dashboard/statics/assets/android-chrome-96x96.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-1024x1024.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-114x114.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-120x120.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-144x144.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-152x152.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-167x167.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-180x180.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-57x57.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-60x60.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-72x72.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-76x76.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon-precomposed.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-icon.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1125x2436.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1136x640.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1242x2208.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1242x2688.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1334x750.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1536x2048.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1620x2160.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1668x2224.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1668x2388.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-1792x828.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2048x1536.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2048x2732.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2160x1620.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2208x1242.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2224x1668.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2388x1668.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2436x1125.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2688x1242.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-2732x2048.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-640x1136.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-750x1334.png +0 -0
- dstack/dashboard/statics/assets/apple-touch-startup-image-828x1792.png +0 -0
- dstack/dashboard/statics/assets/browserconfig.xml +0 -15
- dstack/dashboard/statics/assets/coast-228x228.png +0 -0
- dstack/dashboard/statics/assets/favicon-16x16.png +0 -0
- dstack/dashboard/statics/assets/favicon-32x32.png +0 -0
- dstack/dashboard/statics/assets/favicon-48x48.png +0 -0
- dstack/dashboard/statics/assets/favicon.ico +0 -0
- dstack/dashboard/statics/assets/firefox_app_128x128.png +0 -0
- dstack/dashboard/statics/assets/firefox_app_512x512.png +0 -0
- dstack/dashboard/statics/assets/firefox_app_60x60.png +0 -0
- dstack/dashboard/statics/assets/manifest.webapp +0 -14
- dstack/dashboard/statics/assets/mstile-144x144.png +0 -0
- dstack/dashboard/statics/assets/mstile-150x150.png +0 -0
- dstack/dashboard/statics/assets/mstile-310x150.png +0 -0
- dstack/dashboard/statics/assets/mstile-310x310.png +0 -0
- dstack/dashboard/statics/assets/mstile-70x70.png +0 -0
- dstack/dashboard/statics/assets/yandex-browser-50x50.png +0 -0
- dstack/dashboard/statics/d0f71e48806e25d72553.png +0 -0
- dstack/dashboard/statics/index.html +0 -7
- dstack/dashboard/statics/main-1d87e34eb0454da8ebb4.js +0 -3
- dstack/dashboard/statics/main-1d87e34eb0454da8ebb4.js.LICENSE.txt +0 -102
- dstack/dashboard/statics/main-1d87e34eb0454da8ebb4.js.map +0 -1
- dstack/dashboard/statics/main.css +0 -5058
- dstack/dashboard/statics/splash_thumbnail.png +0 -0
- dstack/dashboard/statics/static/media/check.3f68ffc787a15c0476793a6d18ecb71a.svg +0 -3
- dstack/dashboard/statics/static/media/chevron-down.bfd8f22c4a5db4d443e76bca3b02f334.svg +0 -3
- dstack/dashboard/statics/static/media/chevron-up.bade0c5d82d741cead615813264140c9.svg +0 -3
- dstack/dashboard/statics/static/media/clock.583b744f29b9d143718a55e7c35fe38e.svg +0 -3
- dstack/dashboard/statics/static/media/close.a8bb9e47361b03a3b5084dad676ba1da.svg +0 -3
- dstack/dashboard/statics/static/media/content-copy.73f5f2a175094757758e315243a4111e.svg +0 -3
- dstack/dashboard/statics/static/media/delete-outline.6a8abf4e4f9cb777781967efd56efe9b.svg +0 -3
- dstack/dashboard/statics/static/media/dots-vertical.82fc618192e0c7dc4d615ff93269246a.svg +0 -3
- dstack/dashboard/statics/static/media/earth.1ad57c7f59f4be5c8bb2fa00439c3149.svg +0 -3
- dstack/dashboard/statics/static/media/email.320bc3af24a5f1bb41ebd85f66a5dd70.svg +0 -3
- dstack/dashboard/statics/static/media/external-link.99b88e699c15afb820a1779d9a2261ed.svg +0 -3
- dstack/dashboard/statics/static/media/eye-off-outline.5b4afb7ad624a44dd307518ff93d1faa.svg +0 -3
- dstack/dashboard/statics/static/media/eye-outline.ca41708feaaed1edb15c5fff021fbafe.svg +0 -3
- dstack/dashboard/statics/static/media/file-download-outline.3634b41923ba79b297ff294ef898661c.svg +0 -3
- dstack/dashboard/statics/static/media/folder-outline.33378387af61821dd1207e4b2d061a07.svg +0 -3
- dstack/dashboard/statics/static/media/github-circle.1bb85d171c31a3c2eebad07319377171.svg +0 -3
- dstack/dashboard/statics/static/media/infinity.915f92939afc0a37f94adba211ceb172.svg +0 -3
- dstack/dashboard/statics/static/media/layers.b4b02cea267a617d7aa44c2719250c89.svg +0 -3
- dstack/dashboard/statics/static/media/linkedin.1c52fae553eee54397f0e63a79455a5e.svg +0 -3
- dstack/dashboard/statics/static/media/loading.e466be7b2c1f0ac9e7e51ca929d0e37d.svg +0 -3
- dstack/dashboard/statics/static/media/lock.4a4c7768d0fa60c716609ddc483470ef.svg +0 -3
- dstack/dashboard/statics/static/media/magnify.0c803314d039d21f3cb1504ccd1437a4.svg +0 -3
- dstack/dashboard/statics/static/media/mark.3f68ffc787a15c0476793a6d18ecb71a.svg +0 -3
- dstack/dashboard/statics/static/media/menu-close.3ee84714181017c6ff837830297c8437.svg +0 -3
- dstack/dashboard/statics/static/media/menu.922f81e0972fbcbb5adcd8def20c86a3.svg +0 -3
- dstack/dashboard/statics/static/media/pencil.f706a3b9dcbff4959a91bf72e1e6324f.svg +0 -3
- dstack/dashboard/statics/static/media/refresh.a80edb948e98b322cd73b67814a57a48.svg +0 -3
- dstack/dashboard/statics/static/media/shape-plus.63b093c7f4b44c3def774f30fcfbceca.svg +0 -3
- dstack/dashboard/statics/static/media/slack.ec2fca99c6b944950ac65404ddd26880.svg +0 -4
- dstack/dashboard/statics/static/media/small-logo.b9cc8d09f646a553e65fa336dafd8b10.svg +0 -116
- dstack/dashboard/statics/static/media/source-branch.b8d22cfc42a7bed81f0fc08130818e85.svg +0 -3
- dstack/dashboard/statics/static/media/source-commit.be2bb53c081b9b6836adffccc0b8d3e6.svg +0 -3
- dstack/dashboard/statics/static/media/stop.11488ff1437ad929476be8924a3b7075.svg +0 -3
- dstack/dashboard/statics/static/media/tag-minus.15680a815b0b8d027e973c84832c05e6.svg +0 -3
- dstack/dashboard/statics/static/media/tag-outline.19b0bf86a8afd7d6d9c716e9a91d94ca.svg +0 -3
- dstack/dashboard/statics/static/media/twitter.4af18861c84a2f3044c7546b55d5739c.svg +0 -3
- dstack/dashboard/tags.py +0 -119
- dstack/jobs.py +0 -255
- dstack/providers/__init__.py +0 -316
- dstack/providers/_python/main.py +0 -88
- dstack/providers/_tensorboard/main.py +0 -93
- dstack/providers/_torchrun/main.py +0 -121
- dstack/providers/bash/main.py +0 -90
- dstack/providers/code/main.py +0 -95
- dstack/providers/docker/main.py +0 -79
- dstack/providers/lab/main.py +0 -95
- dstack/providers/notebook/main.py +0 -90
- dstack/random_name.py +0 -29
- dstack/repo.py +0 -135
- dstack/runners.py +0 -35
- dstack/util.py +0 -15
- dstack-0.0.9.dist-info/METADATA +0 -176
- dstack-0.0.9.dist-info/RECORD +0 -179
- dstack-0.0.9.dist-info/entry_points.txt +0 -3
- dstack-0.0.9.dist-info/top_level.txt +0 -2
- tests/test_config.py +0 -70
- /dstack/{cli → _internal}/__init__.py +0 -0
- /dstack/{dashboard → _internal/cli}/__init__.py +0 -0
- /dstack/{providers/_python → _internal/cli/models}/__init__.py +0 -0
- /dstack/{providers/_tensorboard → _internal/cli/services}/__init__.py +0 -0
- /dstack/{providers/_torchrun → _internal/cli/utils}/__init__.py +0 -0
- /dstack/{providers/bash → _internal/core}/__init__.py +0 -0
- /dstack/{providers/code → _internal/core/backends}/__init__.py +0 -0
- /dstack/{providers/docker → _internal/core/backends/aws}/__init__.py +0 -0
- /dstack/{providers/lab → _internal/core/backends/azure}/__init__.py +0 -0
- /dstack/{providers/notebook → _internal/core/backends/base}/__init__.py +0 -0
- {tests → dstack/_internal/core/backends/cloudrift}/__init__.py +0 -0
- /dstack/{dashboard → _internal/server}/statics/assets/yandex-browser-manifest.json +0 -0
- /dstack/{dashboard → _internal/server}/statics/robots.txt +0 -0
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import List, Literal, Optional, Tuple, TypeVar, Union, cast
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import and_, func, or_, select
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
from sqlalchemy.orm import aliased, joinedload, selectinload
|
|
10
|
+
|
|
11
|
+
from dstack._internal.core.backends.base.backend import Backend
|
|
12
|
+
from dstack._internal.core.backends.features import BACKENDS_WITH_CREATE_INSTANCE_SUPPORT
|
|
13
|
+
from dstack._internal.core.errors import (
|
|
14
|
+
ForbiddenError,
|
|
15
|
+
ResourceExistsError,
|
|
16
|
+
ServerClientError,
|
|
17
|
+
)
|
|
18
|
+
from dstack._internal.core.models.common import ApplyAction, CoreModel
|
|
19
|
+
from dstack._internal.core.models.envs import Env
|
|
20
|
+
from dstack._internal.core.models.fleets import (
|
|
21
|
+
ApplyFleetPlanInput,
|
|
22
|
+
Fleet,
|
|
23
|
+
FleetConfiguration,
|
|
24
|
+
FleetPlan,
|
|
25
|
+
FleetSpec,
|
|
26
|
+
FleetStatus,
|
|
27
|
+
InstanceGroupPlacement,
|
|
28
|
+
SSHHostParams,
|
|
29
|
+
SSHParams,
|
|
30
|
+
)
|
|
31
|
+
from dstack._internal.core.models.instances import (
|
|
32
|
+
InstanceOfferWithAvailability,
|
|
33
|
+
InstanceStatus,
|
|
34
|
+
InstanceTerminationReason,
|
|
35
|
+
RemoteConnectionInfo,
|
|
36
|
+
SSHConnectionParams,
|
|
37
|
+
SSHKey,
|
|
38
|
+
)
|
|
39
|
+
from dstack._internal.core.models.placement import PlacementGroup
|
|
40
|
+
from dstack._internal.core.models.profiles import (
|
|
41
|
+
Profile,
|
|
42
|
+
SpotPolicy,
|
|
43
|
+
)
|
|
44
|
+
from dstack._internal.core.models.projects import Project
|
|
45
|
+
from dstack._internal.core.models.resources import ResourcesSpec
|
|
46
|
+
from dstack._internal.core.models.runs import (
|
|
47
|
+
JobProvisioningData,
|
|
48
|
+
Requirements,
|
|
49
|
+
RunStatus,
|
|
50
|
+
get_policy_map,
|
|
51
|
+
)
|
|
52
|
+
from dstack._internal.core.models.users import GlobalRole
|
|
53
|
+
from dstack._internal.core.services import validate_dstack_resource_name
|
|
54
|
+
from dstack._internal.core.services.diff import ModelDiff, copy_model, diff_models
|
|
55
|
+
from dstack._internal.server.db import get_db, is_db_postgres, is_db_sqlite
|
|
56
|
+
from dstack._internal.server.models import (
|
|
57
|
+
FleetModel,
|
|
58
|
+
InstanceModel,
|
|
59
|
+
JobModel,
|
|
60
|
+
MemberModel,
|
|
61
|
+
ProjectModel,
|
|
62
|
+
RunModel,
|
|
63
|
+
UserModel,
|
|
64
|
+
)
|
|
65
|
+
from dstack._internal.server.services import events
|
|
66
|
+
from dstack._internal.server.services import instances as instances_services
|
|
67
|
+
from dstack._internal.server.services import offers as offers_services
|
|
68
|
+
from dstack._internal.server.services.instances import (
|
|
69
|
+
get_instance_remote_connection_info,
|
|
70
|
+
list_active_remote_instances,
|
|
71
|
+
switch_instance_status,
|
|
72
|
+
)
|
|
73
|
+
from dstack._internal.server.services.locking import (
|
|
74
|
+
get_locker,
|
|
75
|
+
string_to_lock_id,
|
|
76
|
+
)
|
|
77
|
+
from dstack._internal.server.services.plugins import apply_plugin_policies
|
|
78
|
+
from dstack._internal.server.services.projects import (
|
|
79
|
+
get_member,
|
|
80
|
+
get_member_permissions,
|
|
81
|
+
list_user_project_models,
|
|
82
|
+
project_model_to_project,
|
|
83
|
+
)
|
|
84
|
+
from dstack._internal.server.services.resources import set_resources_defaults
|
|
85
|
+
from dstack._internal.utils import random_names
|
|
86
|
+
from dstack._internal.utils.logging import get_logger
|
|
87
|
+
from dstack._internal.utils.ssh import pkey_from_str
|
|
88
|
+
|
|
89
|
+
logger = get_logger(__name__)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def switch_fleet_status(
|
|
93
|
+
session: AsyncSession,
|
|
94
|
+
fleet_model: FleetModel,
|
|
95
|
+
new_status: FleetStatus,
|
|
96
|
+
actor: events.AnyActor = events.SystemActor(),
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Switch fleet status.
|
|
100
|
+
"""
|
|
101
|
+
old_status = fleet_model.status
|
|
102
|
+
if old_status == new_status:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
fleet_model.status = new_status
|
|
106
|
+
|
|
107
|
+
msg = f"Fleet status changed {old_status.upper()} -> {new_status.upper()}"
|
|
108
|
+
events.emit(session, msg, actor=actor, targets=[events.Target.from_model(fleet_model)])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def list_projects_with_no_active_fleets(
|
|
112
|
+
session: AsyncSession,
|
|
113
|
+
user: UserModel,
|
|
114
|
+
) -> List[Project]:
|
|
115
|
+
"""
|
|
116
|
+
Returns all projects where the user is a member that have no active fleets.
|
|
117
|
+
|
|
118
|
+
Active fleets are those with `deleted == False`. Projects with only deleted fleets
|
|
119
|
+
(or no fleets) are included. Deleted projects are excluded.
|
|
120
|
+
|
|
121
|
+
Applies to all users (both regular users and admins require membership).
|
|
122
|
+
"""
|
|
123
|
+
active_fleet_alias = aliased(FleetModel)
|
|
124
|
+
member_alias = aliased(MemberModel)
|
|
125
|
+
|
|
126
|
+
query = (
|
|
127
|
+
select(ProjectModel)
|
|
128
|
+
.join(
|
|
129
|
+
member_alias,
|
|
130
|
+
and_(
|
|
131
|
+
member_alias.project_id == ProjectModel.id,
|
|
132
|
+
member_alias.user_id == user.id,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
.outerjoin(
|
|
136
|
+
active_fleet_alias,
|
|
137
|
+
and_(
|
|
138
|
+
active_fleet_alias.project_id == ProjectModel.id,
|
|
139
|
+
active_fleet_alias.deleted == False,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
.where(
|
|
143
|
+
ProjectModel.deleted == False,
|
|
144
|
+
active_fleet_alias.id.is_(None),
|
|
145
|
+
)
|
|
146
|
+
.order_by(ProjectModel.created_at)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
res = await session.execute(query)
|
|
150
|
+
project_models = list(res.scalars().unique().all())
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
project_model_to_project(p, include_backends=False, include_members=False)
|
|
154
|
+
for p in project_models
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def list_fleets(
|
|
159
|
+
session: AsyncSession,
|
|
160
|
+
user: UserModel,
|
|
161
|
+
project_name: Optional[str],
|
|
162
|
+
only_active: bool,
|
|
163
|
+
prev_created_at: Optional[datetime],
|
|
164
|
+
prev_id: Optional[uuid.UUID],
|
|
165
|
+
limit: int,
|
|
166
|
+
ascending: bool,
|
|
167
|
+
) -> List[Fleet]:
|
|
168
|
+
projects = await list_user_project_models(
|
|
169
|
+
session=session,
|
|
170
|
+
user=user,
|
|
171
|
+
only_names=True,
|
|
172
|
+
)
|
|
173
|
+
if project_name is not None:
|
|
174
|
+
projects = [p for p in projects if p.name == project_name]
|
|
175
|
+
fleet_models = await list_projects_fleet_models(
|
|
176
|
+
session=session,
|
|
177
|
+
projects=projects,
|
|
178
|
+
only_active=only_active,
|
|
179
|
+
prev_created_at=prev_created_at,
|
|
180
|
+
prev_id=prev_id,
|
|
181
|
+
limit=limit,
|
|
182
|
+
ascending=ascending,
|
|
183
|
+
)
|
|
184
|
+
return [fleet_model_to_fleet(v) for v in fleet_models]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def list_projects_fleet_models(
|
|
188
|
+
session: AsyncSession,
|
|
189
|
+
projects: List[ProjectModel],
|
|
190
|
+
only_active: bool,
|
|
191
|
+
prev_created_at: Optional[datetime],
|
|
192
|
+
prev_id: Optional[uuid.UUID],
|
|
193
|
+
limit: int,
|
|
194
|
+
ascending: bool,
|
|
195
|
+
) -> List[FleetModel]:
|
|
196
|
+
filters = []
|
|
197
|
+
filters.append(FleetModel.project_id.in_(p.id for p in projects))
|
|
198
|
+
if only_active:
|
|
199
|
+
filters.append(FleetModel.deleted == False)
|
|
200
|
+
if prev_created_at is not None:
|
|
201
|
+
if ascending:
|
|
202
|
+
if prev_id is None:
|
|
203
|
+
filters.append(FleetModel.created_at > prev_created_at)
|
|
204
|
+
else:
|
|
205
|
+
filters.append(
|
|
206
|
+
or_(
|
|
207
|
+
FleetModel.created_at > prev_created_at,
|
|
208
|
+
and_(FleetModel.created_at == prev_created_at, FleetModel.id < prev_id),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
if prev_id is None:
|
|
213
|
+
filters.append(FleetModel.created_at < prev_created_at)
|
|
214
|
+
else:
|
|
215
|
+
filters.append(
|
|
216
|
+
or_(
|
|
217
|
+
FleetModel.created_at < prev_created_at,
|
|
218
|
+
and_(FleetModel.created_at == prev_created_at, FleetModel.id > prev_id),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
order_by = (FleetModel.created_at.desc(), FleetModel.id)
|
|
222
|
+
if ascending:
|
|
223
|
+
order_by = (FleetModel.created_at.asc(), FleetModel.id.desc())
|
|
224
|
+
res = await session.execute(
|
|
225
|
+
select(FleetModel)
|
|
226
|
+
.where(*filters)
|
|
227
|
+
.order_by(*order_by)
|
|
228
|
+
.limit(limit)
|
|
229
|
+
.options(joinedload(FleetModel.instances.and_(InstanceModel.deleted == False)))
|
|
230
|
+
)
|
|
231
|
+
fleet_models = list(res.unique().scalars().all())
|
|
232
|
+
return fleet_models
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
async def list_project_fleets(
|
|
236
|
+
session: AsyncSession,
|
|
237
|
+
project: ProjectModel,
|
|
238
|
+
names: Optional[List[str]] = None,
|
|
239
|
+
) -> List[Fleet]:
|
|
240
|
+
fleet_models = await list_project_fleet_models(session=session, project=project, names=names)
|
|
241
|
+
return [fleet_model_to_fleet(v) for v in fleet_models]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def list_project_fleet_models(
|
|
245
|
+
session: AsyncSession,
|
|
246
|
+
project: ProjectModel,
|
|
247
|
+
names: Optional[List[str]] = None,
|
|
248
|
+
include_deleted: bool = False,
|
|
249
|
+
) -> List[FleetModel]:
|
|
250
|
+
filters = [
|
|
251
|
+
FleetModel.project_id == project.id,
|
|
252
|
+
]
|
|
253
|
+
if names is not None:
|
|
254
|
+
filters.append(FleetModel.name.in_(names))
|
|
255
|
+
if not include_deleted:
|
|
256
|
+
filters.append(FleetModel.deleted == False)
|
|
257
|
+
res = await session.execute(
|
|
258
|
+
select(FleetModel)
|
|
259
|
+
.where(*filters)
|
|
260
|
+
.options(joinedload(FleetModel.instances.and_(InstanceModel.deleted == False)))
|
|
261
|
+
)
|
|
262
|
+
return list(res.unique().scalars().all())
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def get_fleet(
|
|
266
|
+
session: AsyncSession,
|
|
267
|
+
project: ProjectModel,
|
|
268
|
+
name: Optional[str] = None,
|
|
269
|
+
fleet_id: Optional[uuid.UUID] = None,
|
|
270
|
+
include_sensitive: bool = False,
|
|
271
|
+
) -> Optional[Fleet]:
|
|
272
|
+
if fleet_id is not None:
|
|
273
|
+
fleet_model = await get_project_fleet_model_by_id(
|
|
274
|
+
session=session, project=project, fleet_id=fleet_id
|
|
275
|
+
)
|
|
276
|
+
elif name is not None:
|
|
277
|
+
fleet_model = await get_project_fleet_model_by_name(
|
|
278
|
+
session=session, project=project, name=name
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
raise ServerClientError("name or id must be specified")
|
|
282
|
+
if fleet_model is None:
|
|
283
|
+
return None
|
|
284
|
+
return fleet_model_to_fleet(fleet_model, include_sensitive=include_sensitive)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def get_project_fleet_model_by_id(
|
|
288
|
+
session: AsyncSession,
|
|
289
|
+
project: ProjectModel,
|
|
290
|
+
fleet_id: uuid.UUID,
|
|
291
|
+
) -> Optional[FleetModel]:
|
|
292
|
+
filters = [
|
|
293
|
+
FleetModel.id == fleet_id,
|
|
294
|
+
FleetModel.project_id == project.id,
|
|
295
|
+
]
|
|
296
|
+
res = await session.execute(
|
|
297
|
+
select(FleetModel)
|
|
298
|
+
.where(*filters)
|
|
299
|
+
.options(joinedload(FleetModel.instances.and_(InstanceModel.deleted == False)))
|
|
300
|
+
)
|
|
301
|
+
return res.unique().scalar_one_or_none()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def get_project_fleet_model_by_name(
|
|
305
|
+
session: AsyncSession,
|
|
306
|
+
project: ProjectModel,
|
|
307
|
+
name: str,
|
|
308
|
+
include_deleted: bool = False,
|
|
309
|
+
) -> Optional[FleetModel]:
|
|
310
|
+
filters = [
|
|
311
|
+
FleetModel.name == name,
|
|
312
|
+
FleetModel.project_id == project.id,
|
|
313
|
+
]
|
|
314
|
+
if not include_deleted:
|
|
315
|
+
filters.append(FleetModel.deleted == False)
|
|
316
|
+
res = await session.execute(
|
|
317
|
+
select(FleetModel)
|
|
318
|
+
.where(*filters)
|
|
319
|
+
.options(joinedload(FleetModel.instances.and_(InstanceModel.deleted == False)))
|
|
320
|
+
)
|
|
321
|
+
return res.unique().scalar_one_or_none()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def get_plan(
|
|
325
|
+
session: AsyncSession,
|
|
326
|
+
project: ProjectModel,
|
|
327
|
+
user: UserModel,
|
|
328
|
+
spec: FleetSpec,
|
|
329
|
+
) -> FleetPlan:
|
|
330
|
+
# Spec must be copied by parsing to calculate merged_profile
|
|
331
|
+
effective_spec = copy_model(spec)
|
|
332
|
+
effective_spec = await apply_plugin_policies(
|
|
333
|
+
user=user.name,
|
|
334
|
+
project=project.name,
|
|
335
|
+
spec=effective_spec,
|
|
336
|
+
)
|
|
337
|
+
# Spec must be copied by parsing to calculate merged_profile
|
|
338
|
+
effective_spec = copy_model(effective_spec)
|
|
339
|
+
_validate_fleet_spec_and_set_defaults(effective_spec)
|
|
340
|
+
|
|
341
|
+
action = ApplyAction.CREATE
|
|
342
|
+
current_fleet: Optional[Fleet] = None
|
|
343
|
+
current_fleet_id: Optional[uuid.UUID] = None
|
|
344
|
+
|
|
345
|
+
if effective_spec.configuration.name is not None:
|
|
346
|
+
current_fleet = await get_fleet(
|
|
347
|
+
session=session,
|
|
348
|
+
project=project,
|
|
349
|
+
name=effective_spec.configuration.name,
|
|
350
|
+
include_sensitive=True,
|
|
351
|
+
)
|
|
352
|
+
if current_fleet is not None:
|
|
353
|
+
_set_fleet_spec_defaults(current_fleet.spec)
|
|
354
|
+
if _can_update_fleet_spec(current_fleet.spec, effective_spec):
|
|
355
|
+
action = ApplyAction.UPDATE
|
|
356
|
+
current_fleet_id = current_fleet.id
|
|
357
|
+
await _check_ssh_hosts_not_yet_added(session, effective_spec, current_fleet_id)
|
|
358
|
+
|
|
359
|
+
offers = []
|
|
360
|
+
if effective_spec.configuration.ssh_config is None:
|
|
361
|
+
offers_with_backends = await get_create_instance_offers(
|
|
362
|
+
project=project,
|
|
363
|
+
profile=effective_spec.merged_profile,
|
|
364
|
+
requirements=get_fleet_requirements(effective_spec),
|
|
365
|
+
fleet_spec=effective_spec,
|
|
366
|
+
blocks=effective_spec.configuration.blocks,
|
|
367
|
+
)
|
|
368
|
+
offers = [offer for _, offer in offers_with_backends]
|
|
369
|
+
|
|
370
|
+
_remove_fleet_spec_sensitive_info(effective_spec)
|
|
371
|
+
if current_fleet is not None:
|
|
372
|
+
_remove_fleet_spec_sensitive_info(current_fleet.spec)
|
|
373
|
+
plan = FleetPlan(
|
|
374
|
+
project_name=project.name,
|
|
375
|
+
user=user.name,
|
|
376
|
+
spec=spec,
|
|
377
|
+
effective_spec=effective_spec,
|
|
378
|
+
current_resource=current_fleet,
|
|
379
|
+
offers=offers[:50],
|
|
380
|
+
total_offers=len(offers),
|
|
381
|
+
max_offer_price=max((offer.price for offer in offers), default=None),
|
|
382
|
+
action=action,
|
|
383
|
+
)
|
|
384
|
+
return plan
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def get_create_instance_offers(
|
|
388
|
+
project: ProjectModel,
|
|
389
|
+
profile: Profile,
|
|
390
|
+
requirements: Requirements,
|
|
391
|
+
placement_group: Optional[PlacementGroup] = None,
|
|
392
|
+
fleet_spec: Optional[FleetSpec] = None,
|
|
393
|
+
fleet_model: Optional[FleetModel] = None,
|
|
394
|
+
blocks: Union[int, Literal["auto"]] = 1,
|
|
395
|
+
exclude_not_available: bool = False,
|
|
396
|
+
) -> List[Tuple[Backend, InstanceOfferWithAvailability]]:
|
|
397
|
+
multinode = False
|
|
398
|
+
master_job_provisioning_data = None
|
|
399
|
+
if fleet_spec is not None:
|
|
400
|
+
multinode = fleet_spec.configuration.placement == InstanceGroupPlacement.CLUSTER
|
|
401
|
+
if fleet_model is not None:
|
|
402
|
+
fleet = fleet_model_to_fleet(fleet_model)
|
|
403
|
+
multinode = fleet.spec.configuration.placement == InstanceGroupPlacement.CLUSTER
|
|
404
|
+
for instance in fleet_model.instances:
|
|
405
|
+
jpd = instances_services.get_instance_provisioning_data(instance)
|
|
406
|
+
if jpd is not None:
|
|
407
|
+
master_job_provisioning_data = jpd
|
|
408
|
+
break
|
|
409
|
+
|
|
410
|
+
offers = await offers_services.get_offers_by_requirements(
|
|
411
|
+
project=project,
|
|
412
|
+
profile=profile,
|
|
413
|
+
requirements=requirements,
|
|
414
|
+
exclude_not_available=exclude_not_available,
|
|
415
|
+
multinode=multinode,
|
|
416
|
+
master_job_provisioning_data=master_job_provisioning_data,
|
|
417
|
+
placement_group=placement_group,
|
|
418
|
+
blocks=blocks,
|
|
419
|
+
)
|
|
420
|
+
offers = [
|
|
421
|
+
(backend, offer)
|
|
422
|
+
for backend, offer in offers
|
|
423
|
+
if offer.backend in BACKENDS_WITH_CREATE_INSTANCE_SUPPORT
|
|
424
|
+
]
|
|
425
|
+
return offers
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def apply_plan(
|
|
429
|
+
session: AsyncSession,
|
|
430
|
+
user: UserModel,
|
|
431
|
+
project: ProjectModel,
|
|
432
|
+
plan: ApplyFleetPlanInput,
|
|
433
|
+
force: bool,
|
|
434
|
+
) -> Fleet:
|
|
435
|
+
spec = await apply_plugin_policies(
|
|
436
|
+
user=user.name,
|
|
437
|
+
project=project.name,
|
|
438
|
+
spec=plan.spec,
|
|
439
|
+
)
|
|
440
|
+
# Spec must be copied by parsing to calculate merged_profile
|
|
441
|
+
spec = copy_model(spec)
|
|
442
|
+
_validate_fleet_spec_and_set_defaults(spec)
|
|
443
|
+
|
|
444
|
+
if spec.configuration.ssh_config is not None:
|
|
445
|
+
_check_can_manage_ssh_fleets(user=user, project=project)
|
|
446
|
+
|
|
447
|
+
configuration = spec.configuration
|
|
448
|
+
if configuration.name is None:
|
|
449
|
+
return await _create_fleet(
|
|
450
|
+
session=session,
|
|
451
|
+
project=project,
|
|
452
|
+
user=user,
|
|
453
|
+
spec=spec,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
fleet_model = await get_project_fleet_model_by_name(
|
|
457
|
+
session=session,
|
|
458
|
+
project=project,
|
|
459
|
+
name=configuration.name,
|
|
460
|
+
)
|
|
461
|
+
if fleet_model is None:
|
|
462
|
+
return await _create_fleet(
|
|
463
|
+
session=session,
|
|
464
|
+
project=project,
|
|
465
|
+
user=user,
|
|
466
|
+
spec=spec,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
instances_ids = sorted(i.id for i in fleet_model.instances if not i.deleted)
|
|
470
|
+
await session.commit()
|
|
471
|
+
async with (
|
|
472
|
+
get_locker(get_db().dialect_name).lock_ctx(FleetModel.__tablename__, [fleet_model.id]),
|
|
473
|
+
get_locker(get_db().dialect_name).lock_ctx(InstanceModel.__tablename__, instances_ids),
|
|
474
|
+
):
|
|
475
|
+
# Refetch after lock
|
|
476
|
+
# TODO: Lock instances with FOR UPDATE?
|
|
477
|
+
res = await session.execute(
|
|
478
|
+
select(FleetModel)
|
|
479
|
+
.where(
|
|
480
|
+
FleetModel.project_id == project.id,
|
|
481
|
+
FleetModel.id == fleet_model.id,
|
|
482
|
+
FleetModel.deleted == False,
|
|
483
|
+
)
|
|
484
|
+
.options(
|
|
485
|
+
selectinload(FleetModel.instances)
|
|
486
|
+
.joinedload(InstanceModel.jobs)
|
|
487
|
+
.load_only(JobModel.id)
|
|
488
|
+
)
|
|
489
|
+
.options(selectinload(FleetModel.runs))
|
|
490
|
+
.execution_options(populate_existing=True)
|
|
491
|
+
.order_by(FleetModel.id) # take locks in order
|
|
492
|
+
.with_for_update(key_share=True)
|
|
493
|
+
)
|
|
494
|
+
fleet_model = res.scalars().unique().one_or_none()
|
|
495
|
+
if fleet_model is not None:
|
|
496
|
+
return await _update_fleet(
|
|
497
|
+
session=session,
|
|
498
|
+
user=user,
|
|
499
|
+
project=project,
|
|
500
|
+
spec=spec,
|
|
501
|
+
current_resource=plan.current_resource,
|
|
502
|
+
force=force,
|
|
503
|
+
fleet_model=fleet_model,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return await _create_fleet(
|
|
507
|
+
session=session,
|
|
508
|
+
project=project,
|
|
509
|
+
user=user,
|
|
510
|
+
spec=spec,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
async def create_fleet(
|
|
515
|
+
session: AsyncSession,
|
|
516
|
+
project: ProjectModel,
|
|
517
|
+
user: UserModel,
|
|
518
|
+
spec: FleetSpec,
|
|
519
|
+
) -> Fleet:
|
|
520
|
+
spec = await apply_plugin_policies(
|
|
521
|
+
user=user.name,
|
|
522
|
+
project=project.name,
|
|
523
|
+
spec=spec,
|
|
524
|
+
)
|
|
525
|
+
# Spec must be copied by parsing to calculate merged_profile
|
|
526
|
+
spec = copy_model(spec)
|
|
527
|
+
_validate_fleet_spec_and_set_defaults(spec)
|
|
528
|
+
|
|
529
|
+
if spec.configuration.ssh_config is not None:
|
|
530
|
+
_check_can_manage_ssh_fleets(user=user, project=project)
|
|
531
|
+
|
|
532
|
+
return await _create_fleet(session=session, project=project, user=user, spec=spec)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def create_fleet_instance_model(
|
|
536
|
+
session: AsyncSession,
|
|
537
|
+
project: ProjectModel,
|
|
538
|
+
username: str,
|
|
539
|
+
spec: FleetSpec,
|
|
540
|
+
instance_num: int,
|
|
541
|
+
) -> InstanceModel:
|
|
542
|
+
profile = spec.merged_profile
|
|
543
|
+
requirements = get_fleet_requirements(spec)
|
|
544
|
+
instance_model = instances_services.create_instance_model(
|
|
545
|
+
session=session,
|
|
546
|
+
project=project,
|
|
547
|
+
username=username,
|
|
548
|
+
profile=profile,
|
|
549
|
+
requirements=requirements,
|
|
550
|
+
instance_name=f"{spec.configuration.name}-{instance_num}",
|
|
551
|
+
instance_num=instance_num,
|
|
552
|
+
reservation=spec.merged_profile.reservation,
|
|
553
|
+
blocks=spec.configuration.blocks,
|
|
554
|
+
tags=spec.configuration.tags,
|
|
555
|
+
)
|
|
556
|
+
return instance_model
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
async def create_fleet_ssh_instance_model(
|
|
560
|
+
project: ProjectModel,
|
|
561
|
+
spec: FleetSpec,
|
|
562
|
+
ssh_params: SSHParams,
|
|
563
|
+
env: Env,
|
|
564
|
+
instance_num: int,
|
|
565
|
+
host: Union[SSHHostParams, str],
|
|
566
|
+
) -> InstanceModel:
|
|
567
|
+
if isinstance(host, str):
|
|
568
|
+
hostname = host
|
|
569
|
+
ssh_user = ssh_params.user
|
|
570
|
+
ssh_key = ssh_params.ssh_key
|
|
571
|
+
port = ssh_params.port
|
|
572
|
+
proxy_jump = ssh_params.proxy_jump
|
|
573
|
+
internal_ip = None
|
|
574
|
+
blocks = 1
|
|
575
|
+
else:
|
|
576
|
+
hostname = host.hostname
|
|
577
|
+
ssh_user = host.user or ssh_params.user
|
|
578
|
+
ssh_key = host.ssh_key or ssh_params.ssh_key
|
|
579
|
+
port = host.port or ssh_params.port
|
|
580
|
+
proxy_jump = host.proxy_jump or ssh_params.proxy_jump
|
|
581
|
+
internal_ip = host.internal_ip
|
|
582
|
+
blocks = host.blocks
|
|
583
|
+
|
|
584
|
+
if ssh_user is None or ssh_key is None:
|
|
585
|
+
# This should not be reachable but checked by fleet spec validation
|
|
586
|
+
raise ServerClientError("ssh key or user not specified")
|
|
587
|
+
|
|
588
|
+
if proxy_jump is not None:
|
|
589
|
+
assert proxy_jump.ssh_key is not None
|
|
590
|
+
ssh_proxy = SSHConnectionParams(
|
|
591
|
+
hostname=proxy_jump.hostname,
|
|
592
|
+
port=proxy_jump.port or 22,
|
|
593
|
+
username=proxy_jump.user,
|
|
594
|
+
)
|
|
595
|
+
ssh_proxy_keys = [proxy_jump.ssh_key]
|
|
596
|
+
else:
|
|
597
|
+
ssh_proxy = None
|
|
598
|
+
ssh_proxy_keys = None
|
|
599
|
+
|
|
600
|
+
instance_model = await instances_services.create_ssh_instance_model(
|
|
601
|
+
project=project,
|
|
602
|
+
instance_name=f"{spec.configuration.name}-{instance_num}",
|
|
603
|
+
instance_num=instance_num,
|
|
604
|
+
region="remote",
|
|
605
|
+
host=hostname,
|
|
606
|
+
ssh_user=ssh_user,
|
|
607
|
+
ssh_keys=[ssh_key],
|
|
608
|
+
ssh_proxy=ssh_proxy,
|
|
609
|
+
ssh_proxy_keys=ssh_proxy_keys,
|
|
610
|
+
env=env,
|
|
611
|
+
internal_ip=internal_ip,
|
|
612
|
+
instance_network=ssh_params.network,
|
|
613
|
+
port=port or 22,
|
|
614
|
+
blocks=blocks,
|
|
615
|
+
)
|
|
616
|
+
return instance_model
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
async def delete_fleets(
|
|
620
|
+
session: AsyncSession,
|
|
621
|
+
project: ProjectModel,
|
|
622
|
+
user: UserModel,
|
|
623
|
+
names: List[str],
|
|
624
|
+
instance_nums: Optional[List[int]] = None,
|
|
625
|
+
):
|
|
626
|
+
res = await session.execute(
|
|
627
|
+
select(FleetModel.id)
|
|
628
|
+
.where(
|
|
629
|
+
FleetModel.project_id == project.id,
|
|
630
|
+
FleetModel.name.in_(names),
|
|
631
|
+
FleetModel.deleted == False,
|
|
632
|
+
)
|
|
633
|
+
.order_by(FleetModel.id) # take locks in order
|
|
634
|
+
.with_for_update(key_share=True)
|
|
635
|
+
)
|
|
636
|
+
fleets_ids = list(res.scalars().unique().all())
|
|
637
|
+
res = await session.execute(
|
|
638
|
+
select(InstanceModel.id)
|
|
639
|
+
.where(
|
|
640
|
+
InstanceModel.fleet_id.in_(fleets_ids),
|
|
641
|
+
InstanceModel.deleted == False,
|
|
642
|
+
)
|
|
643
|
+
.order_by(InstanceModel.id) # take locks in order
|
|
644
|
+
.with_for_update(key_share=True)
|
|
645
|
+
)
|
|
646
|
+
instances_ids = list(res.scalars().unique().all())
|
|
647
|
+
if is_db_sqlite():
|
|
648
|
+
# Start new transaction to see committed changes after lock
|
|
649
|
+
await session.commit()
|
|
650
|
+
async with (
|
|
651
|
+
get_locker(get_db().dialect_name).lock_ctx(FleetModel.__tablename__, fleets_ids),
|
|
652
|
+
get_locker(get_db().dialect_name).lock_ctx(InstanceModel.__tablename__, instances_ids),
|
|
653
|
+
):
|
|
654
|
+
# Refetch after lock.
|
|
655
|
+
# TODO: Do not lock fleet when deleting only instances.
|
|
656
|
+
res = await session.execute(
|
|
657
|
+
select(FleetModel)
|
|
658
|
+
.where(FleetModel.id.in_(fleets_ids))
|
|
659
|
+
.options(
|
|
660
|
+
joinedload(FleetModel.instances.and_(InstanceModel.id.in_(instances_ids)))
|
|
661
|
+
.joinedload(InstanceModel.jobs)
|
|
662
|
+
.load_only(JobModel.id)
|
|
663
|
+
)
|
|
664
|
+
.options(
|
|
665
|
+
joinedload(
|
|
666
|
+
FleetModel.runs.and_(RunModel.status.not_in(RunStatus.finished_statuses()))
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
.execution_options(populate_existing=True)
|
|
670
|
+
)
|
|
671
|
+
fleet_models = res.scalars().unique().all()
|
|
672
|
+
fleets = [fleet_model_to_fleet(m) for m in fleet_models]
|
|
673
|
+
for fleet in fleets:
|
|
674
|
+
if fleet.spec.configuration.ssh_config is not None:
|
|
675
|
+
_check_can_manage_ssh_fleets(user=user, project=project)
|
|
676
|
+
if instance_nums is None:
|
|
677
|
+
logger.info("Deleting fleets: %s", [f.name for f in fleet_models])
|
|
678
|
+
else:
|
|
679
|
+
logger.info(
|
|
680
|
+
"Deleting fleets %s instances %s", [f.name for f in fleet_models], instance_nums
|
|
681
|
+
)
|
|
682
|
+
for fleet_model in fleet_models:
|
|
683
|
+
_terminate_fleet_instances(
|
|
684
|
+
session=session, fleet_model=fleet_model, instance_nums=instance_nums, actor=user
|
|
685
|
+
)
|
|
686
|
+
# TERMINATING fleets are deleted by process_fleets after instances are terminated
|
|
687
|
+
if instance_nums is None:
|
|
688
|
+
switch_fleet_status(
|
|
689
|
+
session,
|
|
690
|
+
fleet_model,
|
|
691
|
+
FleetStatus.TERMINATING,
|
|
692
|
+
actor=events.UserActor.from_user(user),
|
|
693
|
+
)
|
|
694
|
+
await session.commit()
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def fleet_model_to_fleet(
|
|
698
|
+
fleet_model: FleetModel,
|
|
699
|
+
include_deleted_instances: bool = False,
|
|
700
|
+
include_sensitive: bool = False,
|
|
701
|
+
) -> Fleet:
|
|
702
|
+
instance_models = fleet_model.instances
|
|
703
|
+
if not include_deleted_instances:
|
|
704
|
+
instance_models = [i for i in instance_models if not i.deleted]
|
|
705
|
+
instances = [instances_services.instance_model_to_instance(i) for i in instance_models]
|
|
706
|
+
instances = sorted(instances, key=lambda i: i.instance_num)
|
|
707
|
+
spec = get_fleet_spec(fleet_model)
|
|
708
|
+
if not include_sensitive:
|
|
709
|
+
_remove_fleet_spec_sensitive_info(spec)
|
|
710
|
+
return Fleet(
|
|
711
|
+
id=fleet_model.id,
|
|
712
|
+
name=fleet_model.name,
|
|
713
|
+
project_name=fleet_model.project.name,
|
|
714
|
+
spec=spec,
|
|
715
|
+
created_at=fleet_model.created_at,
|
|
716
|
+
status=fleet_model.status,
|
|
717
|
+
status_message=fleet_model.status_message,
|
|
718
|
+
instances=instances,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def get_fleet_spec(fleet_model: FleetModel) -> FleetSpec:
|
|
723
|
+
return FleetSpec.__response__.parse_raw(fleet_model.spec)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
async def generate_fleet_name(session: AsyncSession, project: ProjectModel) -> str:
|
|
727
|
+
res = await session.execute(
|
|
728
|
+
select(FleetModel.name).where(
|
|
729
|
+
FleetModel.project_id == project.id,
|
|
730
|
+
FleetModel.deleted == False,
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
names = set(res.scalars().all())
|
|
734
|
+
while True:
|
|
735
|
+
name = random_names.generate_name()
|
|
736
|
+
if name not in names:
|
|
737
|
+
return name
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def is_fleet_in_use(fleet_model: FleetModel, instance_nums: Optional[List[int]] = None) -> bool:
|
|
741
|
+
instances_in_use = [i for i in fleet_model.instances if i.jobs and not i.deleted]
|
|
742
|
+
selected_instance_in_use = instances_in_use
|
|
743
|
+
if instance_nums is not None:
|
|
744
|
+
selected_instance_in_use = [i for i in instances_in_use if i.instance_num in instance_nums]
|
|
745
|
+
active_runs = [r for r in fleet_model.runs if not r.status.is_finished()]
|
|
746
|
+
return len(selected_instance_in_use) > 0 or len(instances_in_use) == 0 and len(active_runs) > 0
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def is_fleet_empty(fleet_model: FleetModel) -> bool:
|
|
750
|
+
active_instances = [i for i in fleet_model.instances if not i.deleted]
|
|
751
|
+
return len(active_instances) == 0
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def is_cloud_cluster(fleet_model: FleetModel) -> bool:
|
|
755
|
+
fleet = fleet_model_to_fleet(fleet_model)
|
|
756
|
+
return (
|
|
757
|
+
fleet.spec.configuration.placement == InstanceGroupPlacement.CLUSTER
|
|
758
|
+
and fleet.spec.configuration.ssh_config is None
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def get_fleet_requirements(fleet_spec: FleetSpec) -> Requirements:
|
|
763
|
+
profile = fleet_spec.merged_profile
|
|
764
|
+
requirements = Requirements(
|
|
765
|
+
resources=fleet_spec.configuration.resources or ResourcesSpec(),
|
|
766
|
+
max_price=profile.max_price,
|
|
767
|
+
spot=get_policy_map(profile.spot_policy, default=SpotPolicy.ONDEMAND),
|
|
768
|
+
reservation=fleet_spec.configuration.reservation,
|
|
769
|
+
multinode=fleet_spec.configuration.placement == InstanceGroupPlacement.CLUSTER,
|
|
770
|
+
)
|
|
771
|
+
return requirements
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def get_next_instance_num(taken_instance_nums: set[int]) -> int:
|
|
775
|
+
if not taken_instance_nums:
|
|
776
|
+
return 0
|
|
777
|
+
min_instance_num = min(taken_instance_nums)
|
|
778
|
+
if min_instance_num > 0:
|
|
779
|
+
return 0
|
|
780
|
+
instance_num = min_instance_num + 1
|
|
781
|
+
while True:
|
|
782
|
+
if instance_num not in taken_instance_nums:
|
|
783
|
+
return instance_num
|
|
784
|
+
instance_num += 1
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def get_fleet_master_instance_provisioning_data(
|
|
788
|
+
fleet_model: FleetModel,
|
|
789
|
+
fleet_spec: FleetSpec,
|
|
790
|
+
) -> Optional[JobProvisioningData]:
|
|
791
|
+
master_instance_provisioning_data = None
|
|
792
|
+
if fleet_spec.configuration.placement == InstanceGroupPlacement.CLUSTER:
|
|
793
|
+
# Offers for master jobs must be in the same cluster as existing instances.
|
|
794
|
+
fleet_instance_models = [im for im in fleet_model.instances if not im.deleted]
|
|
795
|
+
if len(fleet_instance_models) > 0:
|
|
796
|
+
master_instance_model = fleet_instance_models[0]
|
|
797
|
+
master_instance_provisioning_data = JobProvisioningData.__response__.parse_raw(
|
|
798
|
+
master_instance_model.job_provisioning_data
|
|
799
|
+
)
|
|
800
|
+
return master_instance_provisioning_data
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def can_create_new_cloud_instance_in_fleet(fleet: Fleet) -> bool:
|
|
804
|
+
if fleet.spec.configuration.ssh_config is not None:
|
|
805
|
+
return False
|
|
806
|
+
active_instances = [i for i in fleet.instances if i.status.is_active()]
|
|
807
|
+
# nodes.max is a soft limit that can be exceeded when provisioning concurrently.
|
|
808
|
+
# The fleet consolidation logic will remove redundant nodes eventually.
|
|
809
|
+
if (
|
|
810
|
+
fleet.spec.configuration.nodes is not None
|
|
811
|
+
and fleet.spec.configuration.nodes.max is not None
|
|
812
|
+
and len(active_instances) >= fleet.spec.configuration.nodes.max
|
|
813
|
+
):
|
|
814
|
+
return False
|
|
815
|
+
return True
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def check_can_create_new_cloud_instance_in_fleet(fleet: Fleet):
|
|
819
|
+
if not can_create_new_cloud_instance_in_fleet(fleet):
|
|
820
|
+
raise ValueError("Cannot fit new cloud instance into fleet")
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
async def _create_fleet(
|
|
824
|
+
session: AsyncSession,
|
|
825
|
+
project: ProjectModel,
|
|
826
|
+
user: UserModel,
|
|
827
|
+
spec: FleetSpec,
|
|
828
|
+
) -> Fleet:
|
|
829
|
+
lock_namespace = f"fleet_names_{project.name}"
|
|
830
|
+
if is_db_sqlite():
|
|
831
|
+
# Start new transaction to see committed changes after lock
|
|
832
|
+
await session.commit()
|
|
833
|
+
elif is_db_postgres():
|
|
834
|
+
await session.execute(
|
|
835
|
+
select(func.pg_advisory_xact_lock(string_to_lock_id(lock_namespace)))
|
|
836
|
+
)
|
|
837
|
+
lock, _ = get_locker(get_db().dialect_name).get_lockset(lock_namespace)
|
|
838
|
+
async with lock:
|
|
839
|
+
if spec.configuration.name is not None:
|
|
840
|
+
fleet_model = await get_project_fleet_model_by_name(
|
|
841
|
+
session=session,
|
|
842
|
+
project=project,
|
|
843
|
+
name=spec.configuration.name,
|
|
844
|
+
)
|
|
845
|
+
if fleet_model is not None:
|
|
846
|
+
raise ResourceExistsError()
|
|
847
|
+
else:
|
|
848
|
+
spec.configuration.name = await generate_fleet_name(session=session, project=project)
|
|
849
|
+
|
|
850
|
+
fleet_model = FleetModel(
|
|
851
|
+
id=uuid.uuid4(),
|
|
852
|
+
name=spec.configuration.name,
|
|
853
|
+
project=project,
|
|
854
|
+
status=FleetStatus.ACTIVE,
|
|
855
|
+
spec=spec.json(),
|
|
856
|
+
instances=[],
|
|
857
|
+
)
|
|
858
|
+
session.add(fleet_model)
|
|
859
|
+
events.emit(
|
|
860
|
+
session,
|
|
861
|
+
f"Fleet created. Status: {fleet_model.status.upper()}",
|
|
862
|
+
actor=events.UserActor.from_user(user),
|
|
863
|
+
targets=[events.Target.from_model(fleet_model)],
|
|
864
|
+
)
|
|
865
|
+
if spec.configuration.ssh_config is not None:
|
|
866
|
+
for i, host in enumerate(spec.configuration.ssh_config.hosts):
|
|
867
|
+
instance_model = await create_fleet_ssh_instance_model(
|
|
868
|
+
project=project,
|
|
869
|
+
spec=spec,
|
|
870
|
+
ssh_params=spec.configuration.ssh_config,
|
|
871
|
+
env=spec.configuration.env,
|
|
872
|
+
instance_num=i,
|
|
873
|
+
host=host,
|
|
874
|
+
)
|
|
875
|
+
events.emit(
|
|
876
|
+
session,
|
|
877
|
+
(
|
|
878
|
+
"Instance created on fleet submission."
|
|
879
|
+
f" Status: {instance_model.status.upper()}"
|
|
880
|
+
),
|
|
881
|
+
actor=events.UserActor.from_user(user),
|
|
882
|
+
targets=[events.Target.from_model(instance_model)],
|
|
883
|
+
)
|
|
884
|
+
fleet_model.instances.append(instance_model)
|
|
885
|
+
else:
|
|
886
|
+
for i in range(_get_fleet_nodes_to_provision(spec)):
|
|
887
|
+
instance_model = create_fleet_instance_model(
|
|
888
|
+
session=session,
|
|
889
|
+
project=project,
|
|
890
|
+
username=user.name,
|
|
891
|
+
spec=spec,
|
|
892
|
+
instance_num=i,
|
|
893
|
+
)
|
|
894
|
+
events.emit(
|
|
895
|
+
session,
|
|
896
|
+
(
|
|
897
|
+
"Instance created on fleet submission."
|
|
898
|
+
f" Status: {instance_model.status.upper()}"
|
|
899
|
+
),
|
|
900
|
+
# Set `SystemActor` for consistency with other places where cloud instances can be
|
|
901
|
+
# created (fleet spec consolidation, job provisioning, etc). Think of the fleet as being
|
|
902
|
+
# created by the user, while the cloud instance is created by the system to satisfy the
|
|
903
|
+
# fleet spec.
|
|
904
|
+
actor=events.SystemActor(),
|
|
905
|
+
targets=[events.Target.from_model(instance_model)],
|
|
906
|
+
)
|
|
907
|
+
fleet_model.instances.append(instance_model)
|
|
908
|
+
await session.commit()
|
|
909
|
+
return fleet_model_to_fleet(fleet_model)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
async def _update_fleet(
|
|
913
|
+
session: AsyncSession,
|
|
914
|
+
user: UserModel,
|
|
915
|
+
project: ProjectModel,
|
|
916
|
+
spec: FleetSpec,
|
|
917
|
+
current_resource: Optional[Fleet],
|
|
918
|
+
force: bool,
|
|
919
|
+
fleet_model: FleetModel,
|
|
920
|
+
) -> Fleet:
|
|
921
|
+
fleet = fleet_model_to_fleet(fleet_model)
|
|
922
|
+
_set_fleet_spec_defaults(fleet.spec)
|
|
923
|
+
fleet_sensitive = fleet_model_to_fleet(fleet_model, include_sensitive=True)
|
|
924
|
+
_set_fleet_spec_defaults(fleet_sensitive.spec)
|
|
925
|
+
|
|
926
|
+
if not force:
|
|
927
|
+
if current_resource is not None:
|
|
928
|
+
_set_fleet_spec_defaults(current_resource.spec)
|
|
929
|
+
if (
|
|
930
|
+
current_resource is None
|
|
931
|
+
or current_resource.id != fleet.id
|
|
932
|
+
or current_resource.spec != fleet.spec
|
|
933
|
+
):
|
|
934
|
+
raise ServerClientError(
|
|
935
|
+
"Failed to apply plan. Resource has been changed. Try again or use force apply."
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
_check_can_update_fleet_spec(fleet_sensitive.spec, spec)
|
|
939
|
+
|
|
940
|
+
spec_json = spec.json()
|
|
941
|
+
fleet_model.spec = spec_json
|
|
942
|
+
|
|
943
|
+
if (
|
|
944
|
+
fleet_sensitive.spec.configuration.ssh_config is not None
|
|
945
|
+
and spec.configuration.ssh_config is not None
|
|
946
|
+
):
|
|
947
|
+
added_hosts, removed_hosts, changed_hosts = _calculate_ssh_hosts_changes(
|
|
948
|
+
current=fleet_sensitive.spec.configuration.ssh_config.hosts,
|
|
949
|
+
new=spec.configuration.ssh_config.hosts,
|
|
950
|
+
)
|
|
951
|
+
# `_check_can_update_fleet_spec` ensures hosts are not changed
|
|
952
|
+
assert not changed_hosts, changed_hosts
|
|
953
|
+
active_instance_nums: set[int] = set()
|
|
954
|
+
removed_instance_nums: list[int] = []
|
|
955
|
+
if removed_hosts or added_hosts:
|
|
956
|
+
for instance_model in fleet_model.instances:
|
|
957
|
+
if instance_model.deleted:
|
|
958
|
+
continue
|
|
959
|
+
active_instance_nums.add(instance_model.instance_num)
|
|
960
|
+
rci = get_instance_remote_connection_info(instance_model)
|
|
961
|
+
if rci is None:
|
|
962
|
+
logger.error(
|
|
963
|
+
"Cloud instance %s in SSH fleet %s",
|
|
964
|
+
instance_model.id,
|
|
965
|
+
fleet_model.id,
|
|
966
|
+
)
|
|
967
|
+
continue
|
|
968
|
+
if rci.host in removed_hosts:
|
|
969
|
+
removed_instance_nums.append(instance_model.instance_num)
|
|
970
|
+
if added_hosts:
|
|
971
|
+
await _check_ssh_hosts_not_yet_added(session, spec, fleet.id)
|
|
972
|
+
for host in added_hosts.values():
|
|
973
|
+
instance_num = get_next_instance_num(active_instance_nums)
|
|
974
|
+
instance_model = await create_fleet_ssh_instance_model(
|
|
975
|
+
project=project,
|
|
976
|
+
spec=spec,
|
|
977
|
+
ssh_params=spec.configuration.ssh_config,
|
|
978
|
+
env=spec.configuration.env,
|
|
979
|
+
instance_num=instance_num,
|
|
980
|
+
host=host,
|
|
981
|
+
)
|
|
982
|
+
events.emit(
|
|
983
|
+
session,
|
|
984
|
+
f"Instance created on fleet update. Status: {instance_model.status.upper()}",
|
|
985
|
+
actor=events.UserActor.from_user(user),
|
|
986
|
+
targets=[events.Target.from_model(instance_model)],
|
|
987
|
+
)
|
|
988
|
+
fleet_model.instances.append(instance_model)
|
|
989
|
+
active_instance_nums.add(instance_num)
|
|
990
|
+
if removed_instance_nums:
|
|
991
|
+
_terminate_fleet_instances(session, fleet_model, removed_instance_nums, actor=user)
|
|
992
|
+
|
|
993
|
+
await session.commit()
|
|
994
|
+
return fleet_model_to_fleet(fleet_model)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _can_update_fleet_spec(current_fleet_spec: FleetSpec, new_fleet_spec: FleetSpec) -> bool:
|
|
998
|
+
try:
|
|
999
|
+
_check_can_update_fleet_spec(current_fleet_spec, new_fleet_spec)
|
|
1000
|
+
except ServerClientError as e:
|
|
1001
|
+
logger.debug("Run cannot be updated: %s", repr(e))
|
|
1002
|
+
return False
|
|
1003
|
+
return True
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
M = TypeVar("M", bound=CoreModel)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _check_can_update(*updatable_fields: str):
|
|
1010
|
+
def decorator(fn: Callable[[M, M, ModelDiff], None]) -> Callable[[M, M], None]:
|
|
1011
|
+
@wraps(fn)
|
|
1012
|
+
def inner(current: M, new: M):
|
|
1013
|
+
diff = _check_can_update_inner(current, new, updatable_fields)
|
|
1014
|
+
fn(current, new, diff)
|
|
1015
|
+
|
|
1016
|
+
return inner
|
|
1017
|
+
|
|
1018
|
+
return decorator
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _check_can_update_inner(current: M, new: M, updatable_fields: tuple[str, ...]) -> ModelDiff:
|
|
1022
|
+
diff = diff_models(current, new)
|
|
1023
|
+
changed_fields = diff.keys()
|
|
1024
|
+
if not (changed_fields <= set(updatable_fields)):
|
|
1025
|
+
raise ServerClientError(
|
|
1026
|
+
f"Failed to update fields {list(changed_fields)}."
|
|
1027
|
+
f" Can only update {list(updatable_fields)}."
|
|
1028
|
+
)
|
|
1029
|
+
return diff
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
@_check_can_update("configuration", "configuration_path")
|
|
1033
|
+
def _check_can_update_fleet_spec(current: FleetSpec, new: FleetSpec, diff: ModelDiff):
|
|
1034
|
+
if "configuration" in diff:
|
|
1035
|
+
_check_can_update_fleet_configuration(current.configuration, new.configuration)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
@_check_can_update("ssh_config")
|
|
1039
|
+
def _check_can_update_fleet_configuration(
|
|
1040
|
+
current: FleetConfiguration, new: FleetConfiguration, diff: ModelDiff
|
|
1041
|
+
):
|
|
1042
|
+
if "ssh_config" in diff:
|
|
1043
|
+
current_ssh_config = current.ssh_config
|
|
1044
|
+
new_ssh_config = new.ssh_config
|
|
1045
|
+
if current_ssh_config is None:
|
|
1046
|
+
if new_ssh_config is not None:
|
|
1047
|
+
raise ServerClientError("Fleet type changed from Cloud to SSH, cannot update")
|
|
1048
|
+
elif new_ssh_config is None:
|
|
1049
|
+
raise ServerClientError("Fleet type changed from SSH to Cloud, cannot update")
|
|
1050
|
+
else:
|
|
1051
|
+
_check_can_update_ssh_config(current_ssh_config, new_ssh_config)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
@_check_can_update("hosts")
|
|
1055
|
+
def _check_can_update_ssh_config(current: SSHParams, new: SSHParams, diff: ModelDiff):
|
|
1056
|
+
if "hosts" in diff:
|
|
1057
|
+
_, _, changed_hosts = _calculate_ssh_hosts_changes(current.hosts, new.hosts)
|
|
1058
|
+
if changed_hosts:
|
|
1059
|
+
raise ServerClientError(
|
|
1060
|
+
f"Hosts configuration changed, cannot update: {list(changed_hosts)}"
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _calculate_ssh_hosts_changes(
|
|
1065
|
+
current: list[Union[SSHHostParams, str]], new: list[Union[SSHHostParams, str]]
|
|
1066
|
+
) -> tuple[dict[str, Union[SSHHostParams, str]], set[str], set[str]]:
|
|
1067
|
+
current_hosts = {h if isinstance(h, str) else h.hostname: h for h in current}
|
|
1068
|
+
new_hosts = {h if isinstance(h, str) else h.hostname: h for h in new}
|
|
1069
|
+
added_hosts = {h: new_hosts[h] for h in new_hosts.keys() - current_hosts}
|
|
1070
|
+
removed_hosts = current_hosts.keys() - new_hosts
|
|
1071
|
+
changed_hosts: set[str] = set()
|
|
1072
|
+
for host in current_hosts.keys() & new_hosts:
|
|
1073
|
+
current_host = current_hosts[host]
|
|
1074
|
+
new_host = new_hosts[host]
|
|
1075
|
+
if isinstance(current_host, str) or isinstance(new_host, str):
|
|
1076
|
+
if current_host != new_host:
|
|
1077
|
+
changed_hosts.add(host)
|
|
1078
|
+
elif diff_models(
|
|
1079
|
+
current_host, new_host, reset={"identity_file": True, "proxy_jump": {"identity_file"}}
|
|
1080
|
+
):
|
|
1081
|
+
changed_hosts.add(host)
|
|
1082
|
+
return added_hosts, removed_hosts, changed_hosts
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _check_can_manage_ssh_fleets(user: UserModel, project: ProjectModel):
|
|
1086
|
+
if user.global_role == GlobalRole.ADMIN:
|
|
1087
|
+
return
|
|
1088
|
+
member = get_member(user=user, project=project)
|
|
1089
|
+
if member is None:
|
|
1090
|
+
raise ForbiddenError()
|
|
1091
|
+
permissions = get_member_permissions(member)
|
|
1092
|
+
if permissions.can_manage_ssh_fleets:
|
|
1093
|
+
return
|
|
1094
|
+
raise ForbiddenError()
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
async def _check_ssh_hosts_not_yet_added(
|
|
1098
|
+
session: AsyncSession, spec: FleetSpec, current_fleet_id: Optional[uuid.UUID] = None
|
|
1099
|
+
):
|
|
1100
|
+
if spec.configuration.ssh_config and spec.configuration.ssh_config.hosts:
|
|
1101
|
+
# there are manually listed hosts, need to check them for existence
|
|
1102
|
+
active_instances = await list_active_remote_instances(session=session)
|
|
1103
|
+
|
|
1104
|
+
existing_hosts = set()
|
|
1105
|
+
for instance in active_instances:
|
|
1106
|
+
# ignore instances belonging to the same fleet -- in-place update/recreate
|
|
1107
|
+
if current_fleet_id is not None and instance.fleet_id == current_fleet_id:
|
|
1108
|
+
continue
|
|
1109
|
+
instance_conn_info = RemoteConnectionInfo.parse_raw(
|
|
1110
|
+
cast(str, instance.remote_connection_info)
|
|
1111
|
+
)
|
|
1112
|
+
existing_hosts.add(instance_conn_info.host)
|
|
1113
|
+
|
|
1114
|
+
instances_already_in_fleet = []
|
|
1115
|
+
for new_instance in spec.configuration.ssh_config.hosts:
|
|
1116
|
+
hostname = new_instance if isinstance(new_instance, str) else new_instance.hostname
|
|
1117
|
+
if hostname in existing_hosts:
|
|
1118
|
+
instances_already_in_fleet.append(hostname)
|
|
1119
|
+
|
|
1120
|
+
if instances_already_in_fleet:
|
|
1121
|
+
raise ServerClientError(
|
|
1122
|
+
msg=f"Instances [{', '.join(instances_already_in_fleet)}] are already assigned to a fleet."
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _remove_fleet_spec_sensitive_info(spec: FleetSpec):
|
|
1127
|
+
if spec.configuration.ssh_config is not None:
|
|
1128
|
+
spec.configuration.ssh_config.ssh_key = None
|
|
1129
|
+
for host in spec.configuration.ssh_config.hosts:
|
|
1130
|
+
if not isinstance(host, str):
|
|
1131
|
+
host.ssh_key = None
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _validate_fleet_spec_and_set_defaults(spec: FleetSpec):
|
|
1135
|
+
if spec.configuration.name is not None:
|
|
1136
|
+
validate_dstack_resource_name(spec.configuration.name)
|
|
1137
|
+
if spec.configuration.ssh_config is None and spec.configuration.nodes is None:
|
|
1138
|
+
raise ServerClientError("No ssh_config or nodes specified")
|
|
1139
|
+
if spec.configuration.ssh_config is not None and spec.configuration.nodes is not None:
|
|
1140
|
+
raise ServerClientError("ssh_config and nodes are mutually exclusive")
|
|
1141
|
+
if spec.configuration.ssh_config is not None:
|
|
1142
|
+
_validate_all_ssh_params_specified(spec.configuration.ssh_config)
|
|
1143
|
+
if spec.configuration.ssh_config.ssh_key is not None:
|
|
1144
|
+
_validate_ssh_key(spec.configuration.ssh_config.ssh_key)
|
|
1145
|
+
for host in spec.configuration.ssh_config.hosts:
|
|
1146
|
+
if isinstance(host, SSHHostParams) and host.ssh_key is not None:
|
|
1147
|
+
_validate_ssh_key(host.ssh_key)
|
|
1148
|
+
_validate_internal_ips(spec.configuration.ssh_config)
|
|
1149
|
+
_set_fleet_spec_defaults(spec)
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _set_fleet_spec_defaults(spec: FleetSpec):
|
|
1153
|
+
if spec.configuration.resources is not None:
|
|
1154
|
+
set_resources_defaults(spec.configuration.resources)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _validate_all_ssh_params_specified(ssh_config: SSHParams):
|
|
1158
|
+
for host in ssh_config.hosts:
|
|
1159
|
+
if isinstance(host, str):
|
|
1160
|
+
if ssh_config.ssh_key is None:
|
|
1161
|
+
raise ServerClientError(f"No ssh key specified for host {host}")
|
|
1162
|
+
if ssh_config.user is None:
|
|
1163
|
+
raise ServerClientError(f"No ssh user specified for host {host}")
|
|
1164
|
+
else:
|
|
1165
|
+
if ssh_config.ssh_key is None and host.ssh_key is None:
|
|
1166
|
+
raise ServerClientError(f"No ssh key specified for host {host.hostname}")
|
|
1167
|
+
if ssh_config.user is None and host.user is None:
|
|
1168
|
+
raise ServerClientError(f"No ssh user specified for host {host.hostname}")
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def _validate_ssh_key(ssh_key: SSHKey):
|
|
1172
|
+
if ssh_key.private is None:
|
|
1173
|
+
raise ServerClientError("Private key not provided")
|
|
1174
|
+
try:
|
|
1175
|
+
pkey_from_str(ssh_key.private)
|
|
1176
|
+
except ValueError:
|
|
1177
|
+
raise ServerClientError(
|
|
1178
|
+
"Unsupported key type. "
|
|
1179
|
+
"The key type should be RSA, ECDSA, or Ed25519 and should not be encrypted with passphrase."
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _validate_internal_ips(ssh_config: SSHParams):
|
|
1184
|
+
internal_ips_num = 0
|
|
1185
|
+
for host in ssh_config.hosts:
|
|
1186
|
+
if not isinstance(host, str) and host.internal_ip is not None:
|
|
1187
|
+
internal_ips_num += 1
|
|
1188
|
+
if internal_ips_num != 0 and internal_ips_num != len(ssh_config.hosts):
|
|
1189
|
+
raise ServerClientError("internal_ip must be specified for all hosts")
|
|
1190
|
+
if internal_ips_num > 0 and ssh_config.network is not None:
|
|
1191
|
+
raise ServerClientError("internal_ip is mutually exclusive with network")
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _get_fleet_nodes_to_provision(spec: FleetSpec) -> int:
|
|
1195
|
+
if spec.configuration.nodes is None:
|
|
1196
|
+
return 0
|
|
1197
|
+
return spec.configuration.nodes.target
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _terminate_fleet_instances(
|
|
1201
|
+
session: AsyncSession,
|
|
1202
|
+
fleet_model: FleetModel,
|
|
1203
|
+
instance_nums: Optional[List[int]],
|
|
1204
|
+
actor: UserModel,
|
|
1205
|
+
):
|
|
1206
|
+
if is_fleet_in_use(fleet_model, instance_nums=instance_nums):
|
|
1207
|
+
if instance_nums is not None:
|
|
1208
|
+
raise ServerClientError(
|
|
1209
|
+
f"Failed to delete fleet {fleet_model.name} instances {instance_nums}. Fleet instances are in use."
|
|
1210
|
+
)
|
|
1211
|
+
raise ServerClientError(f"Failed to delete fleet {fleet_model.name}. Fleet is in use.")
|
|
1212
|
+
for instance in fleet_model.instances:
|
|
1213
|
+
if instance_nums is not None and instance.instance_num not in instance_nums:
|
|
1214
|
+
continue
|
|
1215
|
+
if instance.status == InstanceStatus.TERMINATED:
|
|
1216
|
+
instance.deleted = True
|
|
1217
|
+
else:
|
|
1218
|
+
instance.termination_reason = InstanceTerminationReason.TERMINATED_BY_USER
|
|
1219
|
+
switch_instance_status(
|
|
1220
|
+
session,
|
|
1221
|
+
instance,
|
|
1222
|
+
InstanceStatus.TERMINATING,
|
|
1223
|
+
actor=events.UserActor.from_user(actor),
|
|
1224
|
+
)
|