skypilot-nightly 1.0.0.dev20250502__py3-none-any.whl → 1.0.0.dev20251203__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.
- sky/__init__.py +22 -6
- sky/adaptors/aws.py +81 -16
- sky/adaptors/common.py +25 -2
- sky/adaptors/coreweave.py +278 -0
- sky/adaptors/do.py +8 -2
- sky/adaptors/gcp.py +11 -0
- sky/adaptors/hyperbolic.py +8 -0
- sky/adaptors/ibm.py +5 -2
- sky/adaptors/kubernetes.py +149 -18
- sky/adaptors/nebius.py +173 -30
- sky/adaptors/primeintellect.py +1 -0
- sky/adaptors/runpod.py +68 -0
- sky/adaptors/seeweb.py +183 -0
- sky/adaptors/shadeform.py +89 -0
- sky/admin_policy.py +187 -4
- sky/authentication.py +179 -225
- sky/backends/__init__.py +4 -2
- sky/backends/backend.py +22 -9
- sky/backends/backend_utils.py +1323 -397
- sky/backends/cloud_vm_ray_backend.py +1749 -1029
- sky/backends/docker_utils.py +1 -1
- sky/backends/local_docker_backend.py +11 -6
- sky/backends/task_codegen.py +633 -0
- sky/backends/wheel_utils.py +55 -9
- sky/{clouds/service_catalog → catalog}/__init__.py +21 -19
- sky/{clouds/service_catalog → catalog}/aws_catalog.py +27 -8
- sky/{clouds/service_catalog → catalog}/azure_catalog.py +10 -7
- sky/{clouds/service_catalog → catalog}/common.py +90 -49
- sky/{clouds/service_catalog → catalog}/cudo_catalog.py +8 -5
- sky/{clouds/service_catalog → catalog}/data_fetchers/analyze.py +1 -1
- sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_aws.py +116 -80
- sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_cudo.py +38 -38
- sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_gcp.py +70 -16
- sky/catalog/data_fetchers/fetch_hyperbolic.py +136 -0
- sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_lambda_cloud.py +1 -0
- sky/catalog/data_fetchers/fetch_nebius.py +338 -0
- sky/catalog/data_fetchers/fetch_runpod.py +698 -0
- sky/catalog/data_fetchers/fetch_seeweb.py +329 -0
- sky/catalog/data_fetchers/fetch_shadeform.py +142 -0
- sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_vast.py +1 -1
- sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_vsphere.py +1 -1
- sky/{clouds/service_catalog → catalog}/do_catalog.py +5 -2
- sky/{clouds/service_catalog → catalog}/fluidstack_catalog.py +6 -3
- sky/{clouds/service_catalog → catalog}/gcp_catalog.py +41 -15
- sky/catalog/hyperbolic_catalog.py +136 -0
- sky/{clouds/service_catalog → catalog}/ibm_catalog.py +9 -6
- sky/{clouds/service_catalog → catalog}/kubernetes_catalog.py +36 -24
- sky/{clouds/service_catalog → catalog}/lambda_catalog.py +9 -6
- sky/{clouds/service_catalog → catalog}/nebius_catalog.py +9 -7
- sky/{clouds/service_catalog → catalog}/oci_catalog.py +9 -6
- sky/{clouds/service_catalog → catalog}/paperspace_catalog.py +5 -2
- sky/catalog/primeintellect_catalog.py +95 -0
- sky/{clouds/service_catalog → catalog}/runpod_catalog.py +11 -4
- sky/{clouds/service_catalog → catalog}/scp_catalog.py +9 -6
- sky/catalog/seeweb_catalog.py +184 -0
- sky/catalog/shadeform_catalog.py +165 -0
- sky/catalog/ssh_catalog.py +167 -0
- sky/{clouds/service_catalog → catalog}/vast_catalog.py +6 -3
- sky/{clouds/service_catalog → catalog}/vsphere_catalog.py +5 -2
- sky/check.py +533 -185
- sky/cli.py +5 -5975
- sky/client/{cli.py → cli/command.py} +2591 -1956
- sky/client/cli/deprecation_utils.py +99 -0
- sky/client/cli/flags.py +359 -0
- sky/client/cli/table_utils.py +322 -0
- sky/client/cli/utils.py +79 -0
- sky/client/common.py +78 -32
- sky/client/oauth.py +82 -0
- sky/client/sdk.py +1219 -319
- sky/client/sdk_async.py +827 -0
- sky/client/service_account_auth.py +47 -0
- sky/cloud_stores.py +82 -3
- sky/clouds/__init__.py +13 -0
- sky/clouds/aws.py +564 -164
- sky/clouds/azure.py +105 -83
- sky/clouds/cloud.py +140 -40
- sky/clouds/cudo.py +68 -50
- sky/clouds/do.py +66 -48
- sky/clouds/fluidstack.py +63 -44
- sky/clouds/gcp.py +339 -110
- sky/clouds/hyperbolic.py +293 -0
- sky/clouds/ibm.py +70 -49
- sky/clouds/kubernetes.py +570 -162
- sky/clouds/lambda_cloud.py +74 -54
- sky/clouds/nebius.py +210 -81
- sky/clouds/oci.py +88 -66
- sky/clouds/paperspace.py +61 -44
- sky/clouds/primeintellect.py +317 -0
- sky/clouds/runpod.py +164 -74
- sky/clouds/scp.py +89 -86
- sky/clouds/seeweb.py +477 -0
- sky/clouds/shadeform.py +400 -0
- sky/clouds/ssh.py +263 -0
- sky/clouds/utils/aws_utils.py +10 -4
- sky/clouds/utils/gcp_utils.py +87 -11
- sky/clouds/utils/oci_utils.py +38 -14
- sky/clouds/utils/scp_utils.py +231 -167
- sky/clouds/vast.py +99 -77
- sky/clouds/vsphere.py +51 -40
- sky/core.py +375 -173
- sky/dag.py +15 -0
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/96_E2yl3QAiIJGOYCkSpB/_buildManifest.js +1 -0
- sky/dashboard/out/_next/static/chunks/1141-e6aa9ab418717c59.js +11 -0
- sky/dashboard/out/_next/static/chunks/1272-1ef0bf0237faccdb.js +1 -0
- sky/dashboard/out/_next/static/chunks/1871-7e202677c42f43fe.js +6 -0
- sky/dashboard/out/_next/static/chunks/2260-7703229c33c5ebd5.js +1 -0
- sky/dashboard/out/_next/static/chunks/2350.fab69e61bac57b23.js +1 -0
- sky/dashboard/out/_next/static/chunks/2369.fc20f0c2c8ed9fe7.js +15 -0
- sky/dashboard/out/_next/static/chunks/2755.edd818326d489a1d.js +26 -0
- sky/dashboard/out/_next/static/chunks/3294.20a8540fe697d5ee.js +1 -0
- sky/dashboard/out/_next/static/chunks/3785.7e245f318f9d1121.js +1 -0
- sky/dashboard/out/_next/static/chunks/3800-7b45f9fbb6308557.js +1 -0
- sky/dashboard/out/_next/static/chunks/3850-ff4a9a69d978632b.js +1 -0
- sky/dashboard/out/_next/static/chunks/3937.210053269f121201.js +1 -0
- sky/dashboard/out/_next/static/chunks/4725.172ede95d1b21022.js +1 -0
- sky/dashboard/out/_next/static/chunks/4937.a2baa2df5572a276.js +15 -0
- sky/dashboard/out/_next/static/chunks/5739-d67458fcb1386c92.js +8 -0
- sky/dashboard/out/_next/static/chunks/6130-2be46d70a38f1e82.js +1 -0
- sky/dashboard/out/_next/static/chunks/616-3d59f75e2ccf9321.js +39 -0
- sky/dashboard/out/_next/static/chunks/6212-7bd06f60ba693125.js +13 -0
- sky/dashboard/out/_next/static/chunks/6856-8f27d1c10c98def8.js +1 -0
- sky/dashboard/out/_next/static/chunks/6989-01359c57e018caa4.js +1 -0
- sky/dashboard/out/_next/static/chunks/6990-9146207c4567fdfd.js +1 -0
- sky/dashboard/out/_next/static/chunks/7359-c8d04e06886000b3.js +30 -0
- sky/dashboard/out/_next/static/chunks/7411-b15471acd2cba716.js +41 -0
- sky/dashboard/out/_next/static/chunks/7615-019513abc55b3b47.js +1 -0
- sky/dashboard/out/_next/static/chunks/8640.5b9475a2d18c5416.js +16 -0
- sky/dashboard/out/_next/static/chunks/8969-452f9d5cbdd2dc73.js +1 -0
- sky/dashboard/out/_next/static/chunks/9025.fa408f3242e9028d.js +6 -0
- sky/dashboard/out/_next/static/chunks/9353-cff34f7e773b2e2b.js +1 -0
- sky/dashboard/out/_next/static/chunks/9360.a536cf6b1fa42355.js +31 -0
- sky/dashboard/out/_next/static/chunks/9847.3aaca6bb33455140.js +30 -0
- sky/dashboard/out/_next/static/chunks/fd9d1056-86323a29a8f7e46a.js +1 -0
- sky/dashboard/out/_next/static/chunks/framework-cf60a09ccd051a10.js +33 -0
- sky/dashboard/out/_next/static/chunks/main-app-587214043926b3cc.js +1 -0
- sky/dashboard/out/_next/static/chunks/main-f15ccb73239a3bf1.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/_app-bde01e4a2beec258.js +34 -0
- sky/dashboard/out/_next/static/chunks/pages/_error-c66a4e8afc46f17b.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-792db96d918c98c9.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-abfcac9c137aa543.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters-ee39056f9851a3ff.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/config-dfb9bf07b13045f4.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/index-444f1804401f04ea.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-c0b5935149902e6f.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra-aed0ea19df7cf961.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-d66997e2bfc837cf.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs/pools/[pool]-9faf940b253e3e06.js +21 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs-2072b48b617989c9.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/users-f42674164aa73423.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/volumes-b84b948ff357c43e.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspace/new-3f88a1c7e86a3f86.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-84a40f8c7c627fe4.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces-531b2f8c4bf89f82.js +1 -0
- sky/dashboard/out/_next/static/chunks/webpack-64e05f17bf2cf8ce.js +1 -0
- sky/dashboard/out/_next/static/css/0748ce22df867032.css +3 -0
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
- sky/dashboard/out/clusters/[cluster].html +1 -1
- sky/dashboard/out/clusters.html +1 -1
- sky/dashboard/out/config.html +1 -0
- sky/dashboard/out/index.html +1 -1
- sky/dashboard/out/infra/[context].html +1 -0
- sky/dashboard/out/infra.html +1 -0
- sky/dashboard/out/jobs/[job].html +1 -1
- sky/dashboard/out/jobs/pools/[pool].html +1 -0
- sky/dashboard/out/jobs.html +1 -1
- sky/dashboard/out/users.html +1 -0
- sky/dashboard/out/volumes.html +1 -0
- sky/dashboard/out/workspace/new.html +1 -0
- sky/dashboard/out/workspaces/[name].html +1 -0
- sky/dashboard/out/workspaces.html +1 -0
- sky/data/data_utils.py +137 -1
- sky/data/mounting_utils.py +269 -84
- sky/data/storage.py +1460 -1807
- sky/data/storage_utils.py +43 -57
- sky/exceptions.py +126 -2
- sky/execution.py +216 -63
- sky/global_user_state.py +2390 -586
- sky/jobs/__init__.py +7 -0
- sky/jobs/client/sdk.py +300 -58
- sky/jobs/client/sdk_async.py +161 -0
- sky/jobs/constants.py +15 -8
- sky/jobs/controller.py +848 -275
- sky/jobs/file_content_utils.py +128 -0
- sky/jobs/log_gc.py +193 -0
- sky/jobs/recovery_strategy.py +402 -152
- sky/jobs/scheduler.py +314 -189
- sky/jobs/server/core.py +836 -255
- sky/jobs/server/server.py +156 -115
- sky/jobs/server/utils.py +136 -0
- sky/jobs/state.py +2109 -706
- sky/jobs/utils.py +1306 -215
- sky/logs/__init__.py +21 -0
- sky/logs/agent.py +108 -0
- sky/logs/aws.py +243 -0
- sky/logs/gcp.py +91 -0
- sky/metrics/__init__.py +0 -0
- sky/metrics/utils.py +453 -0
- sky/models.py +78 -1
- sky/optimizer.py +164 -70
- sky/provision/__init__.py +90 -4
- sky/provision/aws/config.py +147 -26
- sky/provision/aws/instance.py +136 -50
- sky/provision/azure/instance.py +11 -6
- sky/provision/common.py +13 -1
- sky/provision/cudo/cudo_machine_type.py +1 -1
- sky/provision/cudo/cudo_utils.py +14 -8
- sky/provision/cudo/cudo_wrapper.py +72 -71
- sky/provision/cudo/instance.py +10 -6
- sky/provision/do/instance.py +10 -6
- sky/provision/do/utils.py +4 -3
- sky/provision/docker_utils.py +140 -33
- sky/provision/fluidstack/instance.py +13 -8
- sky/provision/gcp/__init__.py +1 -0
- sky/provision/gcp/config.py +301 -19
- sky/provision/gcp/constants.py +218 -0
- sky/provision/gcp/instance.py +36 -8
- sky/provision/gcp/instance_utils.py +18 -4
- sky/provision/gcp/volume_utils.py +247 -0
- sky/provision/hyperbolic/__init__.py +12 -0
- sky/provision/hyperbolic/config.py +10 -0
- sky/provision/hyperbolic/instance.py +437 -0
- sky/provision/hyperbolic/utils.py +373 -0
- sky/provision/instance_setup.py +101 -20
- sky/provision/kubernetes/__init__.py +5 -0
- sky/provision/kubernetes/config.py +9 -52
- sky/provision/kubernetes/constants.py +17 -0
- sky/provision/kubernetes/instance.py +919 -280
- sky/provision/kubernetes/manifests/fusermount-server-daemonset.yaml +1 -2
- sky/provision/kubernetes/network.py +27 -17
- sky/provision/kubernetes/network_utils.py +44 -43
- sky/provision/kubernetes/utils.py +1221 -534
- sky/provision/kubernetes/volume.py +343 -0
- sky/provision/lambda_cloud/instance.py +22 -16
- sky/provision/nebius/constants.py +50 -0
- sky/provision/nebius/instance.py +19 -6
- sky/provision/nebius/utils.py +237 -137
- sky/provision/oci/instance.py +10 -5
- sky/provision/paperspace/instance.py +10 -7
- sky/provision/paperspace/utils.py +1 -1
- sky/provision/primeintellect/__init__.py +10 -0
- sky/provision/primeintellect/config.py +11 -0
- sky/provision/primeintellect/instance.py +454 -0
- sky/provision/primeintellect/utils.py +398 -0
- sky/provision/provisioner.py +117 -36
- sky/provision/runpod/__init__.py +5 -0
- sky/provision/runpod/instance.py +27 -6
- sky/provision/runpod/utils.py +51 -18
- sky/provision/runpod/volume.py +214 -0
- sky/provision/scp/__init__.py +15 -0
- sky/provision/scp/config.py +93 -0
- sky/provision/scp/instance.py +707 -0
- sky/provision/seeweb/__init__.py +11 -0
- sky/provision/seeweb/config.py +13 -0
- sky/provision/seeweb/instance.py +812 -0
- sky/provision/shadeform/__init__.py +11 -0
- sky/provision/shadeform/config.py +12 -0
- sky/provision/shadeform/instance.py +351 -0
- sky/provision/shadeform/shadeform_utils.py +83 -0
- sky/provision/ssh/__init__.py +18 -0
- sky/provision/vast/instance.py +13 -8
- sky/provision/vast/utils.py +10 -7
- sky/provision/volume.py +164 -0
- sky/provision/vsphere/common/ssl_helper.py +1 -1
- sky/provision/vsphere/common/vapiconnect.py +2 -1
- sky/provision/vsphere/common/vim_utils.py +4 -4
- sky/provision/vsphere/instance.py +15 -10
- sky/provision/vsphere/vsphere_utils.py +17 -20
- sky/py.typed +0 -0
- sky/resources.py +845 -119
- sky/schemas/__init__.py +0 -0
- sky/schemas/api/__init__.py +0 -0
- sky/schemas/api/responses.py +227 -0
- sky/schemas/db/README +4 -0
- sky/schemas/db/env.py +90 -0
- sky/schemas/db/global_user_state/001_initial_schema.py +124 -0
- sky/schemas/db/global_user_state/002_add_workspace_to_cluster_history.py +35 -0
- sky/schemas/db/global_user_state/003_fix_initial_revision.py +61 -0
- sky/schemas/db/global_user_state/004_is_managed.py +34 -0
- sky/schemas/db/global_user_state/005_cluster_event.py +32 -0
- sky/schemas/db/global_user_state/006_provision_log.py +41 -0
- sky/schemas/db/global_user_state/007_cluster_event_request_id.py +34 -0
- sky/schemas/db/global_user_state/008_skylet_ssh_tunnel_metadata.py +34 -0
- sky/schemas/db/global_user_state/009_last_activity_and_launched_at.py +89 -0
- sky/schemas/db/global_user_state/010_save_ssh_key.py +66 -0
- sky/schemas/db/global_user_state/011_is_ephemeral.py +34 -0
- sky/schemas/db/kv_cache/001_initial_schema.py +29 -0
- sky/schemas/db/script.py.mako +28 -0
- sky/schemas/db/serve_state/001_initial_schema.py +67 -0
- sky/schemas/db/serve_state/002_yaml_content.py +34 -0
- sky/schemas/db/skypilot_config/001_initial_schema.py +30 -0
- sky/schemas/db/spot_jobs/001_initial_schema.py +97 -0
- sky/schemas/db/spot_jobs/002_cluster_pool.py +42 -0
- sky/schemas/db/spot_jobs/003_pool_hash.py +34 -0
- sky/schemas/db/spot_jobs/004_job_file_contents.py +42 -0
- sky/schemas/db/spot_jobs/005_logs_gc.py +38 -0
- sky/schemas/db/spot_jobs/006_controller_pid_started_at.py +34 -0
- sky/schemas/db/spot_jobs/007_config_file_content.py +34 -0
- sky/schemas/generated/__init__.py +0 -0
- sky/schemas/generated/autostopv1_pb2.py +36 -0
- sky/schemas/generated/autostopv1_pb2.pyi +43 -0
- sky/schemas/generated/autostopv1_pb2_grpc.py +146 -0
- sky/schemas/generated/jobsv1_pb2.py +86 -0
- sky/schemas/generated/jobsv1_pb2.pyi +254 -0
- sky/schemas/generated/jobsv1_pb2_grpc.py +542 -0
- sky/schemas/generated/managed_jobsv1_pb2.py +76 -0
- sky/schemas/generated/managed_jobsv1_pb2.pyi +278 -0
- sky/schemas/generated/managed_jobsv1_pb2_grpc.py +278 -0
- sky/schemas/generated/servev1_pb2.py +58 -0
- sky/schemas/generated/servev1_pb2.pyi +115 -0
- sky/schemas/generated/servev1_pb2_grpc.py +322 -0
- sky/serve/autoscalers.py +357 -5
- sky/serve/client/impl.py +310 -0
- sky/serve/client/sdk.py +47 -139
- sky/serve/client/sdk_async.py +130 -0
- sky/serve/constants.py +12 -9
- sky/serve/controller.py +68 -17
- sky/serve/load_balancer.py +106 -60
- sky/serve/load_balancing_policies.py +116 -2
- sky/serve/replica_managers.py +434 -249
- sky/serve/serve_rpc_utils.py +179 -0
- sky/serve/serve_state.py +569 -257
- sky/serve/serve_utils.py +775 -265
- sky/serve/server/core.py +66 -711
- sky/serve/server/impl.py +1093 -0
- sky/serve/server/server.py +21 -18
- sky/serve/service.py +192 -89
- sky/serve/service_spec.py +144 -20
- sky/serve/spot_placer.py +3 -0
- sky/server/auth/__init__.py +0 -0
- sky/server/auth/authn.py +50 -0
- sky/server/auth/loopback.py +38 -0
- sky/server/auth/oauth2_proxy.py +202 -0
- sky/server/common.py +478 -182
- sky/server/config.py +85 -23
- sky/server/constants.py +44 -6
- sky/server/daemons.py +295 -0
- sky/server/html/token_page.html +185 -0
- sky/server/metrics.py +160 -0
- sky/server/middleware_utils.py +166 -0
- sky/server/requests/executor.py +558 -138
- sky/server/requests/payloads.py +364 -24
- sky/server/requests/preconditions.py +21 -17
- sky/server/requests/process.py +112 -29
- sky/server/requests/request_names.py +121 -0
- sky/server/requests/requests.py +822 -226
- sky/server/requests/serializers/decoders.py +82 -31
- sky/server/requests/serializers/encoders.py +140 -22
- sky/server/requests/threads.py +117 -0
- sky/server/rest.py +455 -0
- sky/server/server.py +1309 -285
- sky/server/state.py +20 -0
- sky/server/stream_utils.py +327 -61
- sky/server/uvicorn.py +217 -3
- sky/server/versions.py +270 -0
- sky/setup_files/MANIFEST.in +11 -1
- sky/setup_files/alembic.ini +160 -0
- sky/setup_files/dependencies.py +139 -31
- sky/setup_files/setup.py +44 -42
- sky/sky_logging.py +114 -7
- sky/skylet/attempt_skylet.py +106 -24
- sky/skylet/autostop_lib.py +129 -8
- sky/skylet/configs.py +29 -20
- sky/skylet/constants.py +216 -25
- sky/skylet/events.py +101 -21
- sky/skylet/job_lib.py +345 -164
- sky/skylet/log_lib.py +297 -18
- sky/skylet/log_lib.pyi +44 -1
- sky/skylet/providers/ibm/node_provider.py +12 -8
- sky/skylet/providers/ibm/vpc_provider.py +13 -12
- sky/skylet/ray_patches/__init__.py +17 -3
- sky/skylet/ray_patches/autoscaler.py.diff +18 -0
- sky/skylet/ray_patches/cli.py.diff +19 -0
- sky/skylet/ray_patches/command_runner.py.diff +17 -0
- sky/skylet/ray_patches/log_monitor.py.diff +20 -0
- sky/skylet/ray_patches/resource_demand_scheduler.py.diff +32 -0
- sky/skylet/ray_patches/updater.py.diff +18 -0
- sky/skylet/ray_patches/worker.py.diff +41 -0
- sky/skylet/runtime_utils.py +21 -0
- sky/skylet/services.py +568 -0
- sky/skylet/skylet.py +72 -4
- sky/skylet/subprocess_daemon.py +104 -29
- sky/skypilot_config.py +506 -99
- sky/ssh_node_pools/__init__.py +1 -0
- sky/ssh_node_pools/core.py +135 -0
- sky/ssh_node_pools/server.py +233 -0
- sky/task.py +685 -163
- sky/templates/aws-ray.yml.j2 +11 -3
- sky/templates/azure-ray.yml.j2 +2 -1
- sky/templates/cudo-ray.yml.j2 +1 -0
- sky/templates/do-ray.yml.j2 +2 -1
- sky/templates/fluidstack-ray.yml.j2 +1 -0
- sky/templates/gcp-ray.yml.j2 +62 -1
- sky/templates/hyperbolic-ray.yml.j2 +68 -0
- sky/templates/ibm-ray.yml.j2 +2 -1
- sky/templates/jobs-controller.yaml.j2 +27 -24
- sky/templates/kubernetes-loadbalancer.yml.j2 +2 -0
- sky/templates/kubernetes-ray.yml.j2 +611 -50
- sky/templates/lambda-ray.yml.j2 +2 -1
- sky/templates/nebius-ray.yml.j2 +34 -12
- sky/templates/oci-ray.yml.j2 +1 -0
- sky/templates/paperspace-ray.yml.j2 +2 -1
- sky/templates/primeintellect-ray.yml.j2 +72 -0
- sky/templates/runpod-ray.yml.j2 +10 -1
- sky/templates/scp-ray.yml.j2 +4 -50
- sky/templates/seeweb-ray.yml.j2 +171 -0
- sky/templates/shadeform-ray.yml.j2 +73 -0
- sky/templates/sky-serve-controller.yaml.j2 +22 -2
- sky/templates/vast-ray.yml.j2 +1 -0
- sky/templates/vsphere-ray.yml.j2 +1 -0
- sky/templates/websocket_proxy.py +212 -37
- sky/usage/usage_lib.py +31 -15
- sky/users/__init__.py +0 -0
- sky/users/model.conf +15 -0
- sky/users/permission.py +397 -0
- sky/users/rbac.py +121 -0
- sky/users/server.py +720 -0
- sky/users/token_service.py +218 -0
- sky/utils/accelerator_registry.py +35 -5
- sky/utils/admin_policy_utils.py +84 -38
- sky/utils/annotations.py +38 -5
- sky/utils/asyncio_utils.py +78 -0
- sky/utils/atomic.py +1 -1
- sky/utils/auth_utils.py +153 -0
- sky/utils/benchmark_utils.py +60 -0
- sky/utils/cli_utils/status_utils.py +159 -86
- sky/utils/cluster_utils.py +31 -9
- sky/utils/command_runner.py +354 -68
- sky/utils/command_runner.pyi +93 -3
- sky/utils/common.py +35 -8
- sky/utils/common_utils.py +314 -91
- sky/utils/config_utils.py +74 -5
- sky/utils/context.py +403 -0
- sky/utils/context_utils.py +242 -0
- sky/utils/controller_utils.py +383 -89
- sky/utils/dag_utils.py +31 -12
- sky/utils/db/__init__.py +0 -0
- sky/utils/db/db_utils.py +485 -0
- sky/utils/db/kv_cache.py +149 -0
- sky/utils/db/migration_utils.py +137 -0
- sky/utils/directory_utils.py +12 -0
- sky/utils/env_options.py +13 -0
- sky/utils/git.py +567 -0
- sky/utils/git_clone.sh +460 -0
- sky/utils/infra_utils.py +195 -0
- sky/utils/kubernetes/cleanup-tunnel.sh +62 -0
- sky/utils/kubernetes/config_map_utils.py +133 -0
- sky/utils/kubernetes/create_cluster.sh +15 -29
- sky/utils/kubernetes/delete_cluster.sh +10 -7
- sky/utils/kubernetes/deploy_ssh_node_pools.py +1177 -0
- sky/utils/kubernetes/exec_kubeconfig_converter.py +22 -31
- sky/utils/kubernetes/generate_kind_config.py +6 -66
- sky/utils/kubernetes/generate_kubeconfig.sh +4 -1
- sky/utils/kubernetes/gpu_labeler.py +18 -8
- sky/utils/kubernetes/k8s_gpu_labeler_job.yaml +2 -1
- sky/utils/kubernetes/k8s_gpu_labeler_setup.yaml +16 -16
- sky/utils/kubernetes/kubernetes_deploy_utils.py +284 -114
- sky/utils/kubernetes/rsync_helper.sh +11 -3
- sky/utils/kubernetes/ssh-tunnel.sh +379 -0
- sky/utils/kubernetes/ssh_utils.py +221 -0
- sky/utils/kubernetes_enums.py +8 -15
- sky/utils/lock_events.py +94 -0
- sky/utils/locks.py +416 -0
- sky/utils/log_utils.py +82 -107
- sky/utils/perf_utils.py +22 -0
- sky/utils/resource_checker.py +298 -0
- sky/utils/resources_utils.py +249 -32
- sky/utils/rich_utils.py +217 -39
- sky/utils/schemas.py +955 -160
- sky/utils/serialize_utils.py +16 -0
- sky/utils/status_lib.py +10 -0
- sky/utils/subprocess_utils.py +29 -15
- sky/utils/tempstore.py +70 -0
- sky/utils/thread_utils.py +91 -0
- sky/utils/timeline.py +26 -53
- sky/utils/ux_utils.py +84 -15
- sky/utils/validator.py +11 -1
- sky/utils/volume.py +165 -0
- sky/utils/yaml_utils.py +111 -0
- sky/volumes/__init__.py +13 -0
- sky/volumes/client/__init__.py +0 -0
- sky/volumes/client/sdk.py +150 -0
- sky/volumes/server/__init__.py +0 -0
- sky/volumes/server/core.py +270 -0
- sky/volumes/server/server.py +124 -0
- sky/volumes/volume.py +215 -0
- sky/workspaces/__init__.py +0 -0
- sky/workspaces/core.py +655 -0
- sky/workspaces/server.py +101 -0
- sky/workspaces/utils.py +56 -0
- sky_templates/README.md +3 -0
- sky_templates/__init__.py +3 -0
- sky_templates/ray/__init__.py +0 -0
- sky_templates/ray/start_cluster +183 -0
- sky_templates/ray/stop_cluster +75 -0
- skypilot_nightly-1.0.0.dev20251203.dist-info/METADATA +676 -0
- skypilot_nightly-1.0.0.dev20251203.dist-info/RECORD +611 -0
- {skypilot_nightly-1.0.0.dev20250502.dist-info → skypilot_nightly-1.0.0.dev20251203.dist-info}/WHEEL +1 -1
- skypilot_nightly-1.0.0.dev20251203.dist-info/top_level.txt +2 -0
- sky/benchmark/benchmark_state.py +0 -256
- sky/benchmark/benchmark_utils.py +0 -641
- sky/clouds/service_catalog/constants.py +0 -7
- sky/dashboard/out/_next/static/GWvVBSCS7FmUiVmjaL1a7/_buildManifest.js +0 -1
- sky/dashboard/out/_next/static/chunks/236-2db3ee3fba33dd9e.js +0 -6
- sky/dashboard/out/_next/static/chunks/312-c3c8845990db8ffc.js +0 -15
- sky/dashboard/out/_next/static/chunks/37-0a572fe0dbb89c4d.js +0 -6
- sky/dashboard/out/_next/static/chunks/678-206dddca808e6d16.js +0 -59
- sky/dashboard/out/_next/static/chunks/845-9e60713e0c441abc.js +0 -1
- sky/dashboard/out/_next/static/chunks/979-7bf73a4c7cea0f5c.js +0 -1
- sky/dashboard/out/_next/static/chunks/fd9d1056-2821b0f0cabcd8bd.js +0 -1
- sky/dashboard/out/_next/static/chunks/framework-87d061ee6ed71b28.js +0 -33
- sky/dashboard/out/_next/static/chunks/main-app-241eb28595532291.js +0 -1
- sky/dashboard/out/_next/static/chunks/main-e0e2335212e72357.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/_app-e6b013bc3f77ad60.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/_error-1be831200e60c5c0.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-6ac338bc2239cb45.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-f383db7389368ea7.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters-a93b93e10b8b074e.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/index-f9f039532ca8cbc4.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-1c519e1afc523dc9.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs-a75029b67aab6a2e.js +0 -1
- sky/dashboard/out/_next/static/chunks/webpack-830f59b8404e96b8.js +0 -1
- sky/dashboard/out/_next/static/css/c6933bbb2ce7f4dd.css +0 -3
- sky/jobs/dashboard/dashboard.py +0 -223
- sky/jobs/dashboard/static/favicon.ico +0 -0
- sky/jobs/dashboard/templates/index.html +0 -831
- sky/jobs/server/dashboard_utils.py +0 -69
- sky/skylet/providers/scp/__init__.py +0 -2
- sky/skylet/providers/scp/config.py +0 -149
- sky/skylet/providers/scp/node_provider.py +0 -578
- sky/templates/kubernetes-ssh-jump.yml.j2 +0 -94
- sky/utils/db_utils.py +0 -100
- sky/utils/kubernetes/deploy_remote_cluster.sh +0 -308
- sky/utils/kubernetes/ssh_jump_lifecycle_manager.py +0 -191
- skypilot_nightly-1.0.0.dev20250502.dist-info/METADATA +0 -361
- skypilot_nightly-1.0.0.dev20250502.dist-info/RECORD +0 -396
- skypilot_nightly-1.0.0.dev20250502.dist-info/top_level.txt +0 -1
- /sky/{clouds/service_catalog → catalog}/config.py +0 -0
- /sky/{benchmark → catalog/data_fetchers}/__init__.py +0 -0
- /sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_azure.py +0 -0
- /sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_fluidstack.py +0 -0
- /sky/{clouds/service_catalog → catalog}/data_fetchers/fetch_ibm.py +0 -0
- /sky/{clouds/service_catalog/data_fetchers → client/cli}/__init__.py +0 -0
- /sky/dashboard/out/_next/static/{GWvVBSCS7FmUiVmjaL1a7 → 96_E2yl3QAiIJGOYCkSpB}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250502.dist-info → skypilot_nightly-1.0.0.dev20251203.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250502.dist-info → skypilot_nightly-1.0.0.dev20251203.dist-info}/licenses/LICENSE +0 -0
sky/authentication.py
CHANGED
|
@@ -19,37 +19,34 @@ controller. (Lambda cloud is an exception, due to the limitation of the cloud
|
|
|
19
19
|
provider. See the comments in setup_lambda_authentication)
|
|
20
20
|
"""
|
|
21
21
|
import copy
|
|
22
|
-
import functools
|
|
23
22
|
import os
|
|
24
23
|
import re
|
|
25
24
|
import socket
|
|
26
25
|
import subprocess
|
|
27
26
|
import sys
|
|
28
|
-
import
|
|
29
|
-
from typing import Any, Dict, Tuple
|
|
27
|
+
from typing import Any, Dict
|
|
30
28
|
import uuid
|
|
31
29
|
|
|
32
30
|
import colorama
|
|
33
|
-
import filelock
|
|
34
31
|
|
|
35
32
|
from sky import clouds
|
|
36
33
|
from sky import exceptions
|
|
37
34
|
from sky import sky_logging
|
|
38
|
-
from sky import skypilot_config
|
|
39
|
-
from sky.adaptors import common as adaptors_common
|
|
40
35
|
from sky.adaptors import gcp
|
|
41
36
|
from sky.adaptors import ibm
|
|
42
|
-
from sky.adaptors import kubernetes
|
|
43
37
|
from sky.adaptors import runpod
|
|
38
|
+
from sky.adaptors import seeweb as seeweb_adaptor
|
|
39
|
+
from sky.adaptors import shadeform as shadeform_adaptor
|
|
44
40
|
from sky.adaptors import vast
|
|
45
41
|
from sky.provision.fluidstack import fluidstack_utils
|
|
46
42
|
from sky.provision.kubernetes import utils as kubernetes_utils
|
|
47
43
|
from sky.provision.lambda_cloud import lambda_utils
|
|
44
|
+
from sky.provision.primeintellect import utils as primeintellect_utils
|
|
45
|
+
from sky.utils import auth_utils
|
|
48
46
|
from sky.utils import common_utils
|
|
49
|
-
from sky.utils import config_utils
|
|
50
|
-
from sky.utils import kubernetes_enums
|
|
51
47
|
from sky.utils import subprocess_utils
|
|
52
48
|
from sky.utils import ux_utils
|
|
49
|
+
from sky.utils import yaml_utils
|
|
53
50
|
|
|
54
51
|
logger = sky_logging.init_logger(__name__)
|
|
55
52
|
|
|
@@ -58,114 +55,38 @@ logger = sky_logging.init_logger(__name__)
|
|
|
58
55
|
# using Cloud Client Libraries for Python, where possible, for new code
|
|
59
56
|
# development.
|
|
60
57
|
|
|
61
|
-
MAX_TRIALS = 64
|
|
62
|
-
# TODO(zhwu): Support user specified key pair.
|
|
63
|
-
# We intentionally not have the ssh key pair to be stored in
|
|
64
|
-
# ~/.sky/api_server/clients, i.e. sky.server.common.API_SERVER_CLIENT_DIR,
|
|
65
|
-
# because ssh key pair need to persist across API server restarts, while
|
|
66
|
-
# the former dir is empheral.
|
|
67
|
-
_SSH_KEY_PATH_PREFIX = '~/.sky/clients/{user_hash}/ssh'
|
|
68
|
-
|
|
69
|
-
if typing.TYPE_CHECKING:
|
|
70
|
-
import yaml
|
|
71
|
-
else:
|
|
72
|
-
yaml = adaptors_common.LazyImport('yaml')
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_ssh_key_and_lock_path() -> Tuple[str, str, str]:
|
|
76
|
-
user_hash = common_utils.get_user_hash()
|
|
77
|
-
user_ssh_key_prefix = _SSH_KEY_PATH_PREFIX.format(user_hash=user_hash)
|
|
78
|
-
|
|
79
|
-
os.makedirs(os.path.expanduser(user_ssh_key_prefix),
|
|
80
|
-
exist_ok=True,
|
|
81
|
-
mode=0o700)
|
|
82
|
-
private_key_path = os.path.join(user_ssh_key_prefix, 'sky-key')
|
|
83
|
-
public_key_path = os.path.join(user_ssh_key_prefix, 'sky-key.pub')
|
|
84
|
-
lock_path = os.path.join(user_ssh_key_prefix, '.__internal-sky-key.lock')
|
|
85
|
-
return private_key_path, public_key_path, lock_path
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _generate_rsa_key_pair() -> Tuple[str, str]:
|
|
89
|
-
# Keep the import of the cryptography local to avoid expensive
|
|
90
|
-
# third-party imports when not needed.
|
|
91
|
-
# pylint: disable=import-outside-toplevel
|
|
92
|
-
from cryptography.hazmat.backends import default_backend
|
|
93
|
-
from cryptography.hazmat.primitives import serialization
|
|
94
|
-
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
95
|
-
|
|
96
|
-
key = rsa.generate_private_key(backend=default_backend(),
|
|
97
|
-
public_exponent=65537,
|
|
98
|
-
key_size=2048)
|
|
99
|
-
|
|
100
|
-
private_key = key.private_bytes(
|
|
101
|
-
encoding=serialization.Encoding.PEM,
|
|
102
|
-
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
103
|
-
encryption_algorithm=serialization.NoEncryption()).decode(
|
|
104
|
-
'utf-8').strip()
|
|
105
|
-
|
|
106
|
-
public_key = key.public_key().public_bytes(
|
|
107
|
-
serialization.Encoding.OpenSSH,
|
|
108
|
-
serialization.PublicFormat.OpenSSH).decode('utf-8').strip()
|
|
109
|
-
|
|
110
|
-
return public_key, private_key
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _save_key_pair(private_key_path: str, public_key_path: str,
|
|
114
|
-
private_key: str, public_key: str) -> None:
|
|
115
|
-
key_dir = os.path.dirname(private_key_path)
|
|
116
|
-
os.makedirs(key_dir, exist_ok=True, mode=0o700)
|
|
117
|
-
|
|
118
|
-
with open(
|
|
119
|
-
private_key_path,
|
|
120
|
-
'w',
|
|
121
|
-
encoding='utf-8',
|
|
122
|
-
opener=functools.partial(os.open, mode=0o600),
|
|
123
|
-
) as f:
|
|
124
|
-
f.write(private_key)
|
|
125
|
-
|
|
126
|
-
with open(public_key_path,
|
|
127
|
-
'w',
|
|
128
|
-
encoding='utf-8',
|
|
129
|
-
opener=functools.partial(os.open, mode=0o644)) as f:
|
|
130
|
-
f.write(public_key)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def get_or_generate_keys() -> Tuple[str, str]:
|
|
134
|
-
"""Returns the aboslute private and public key paths."""
|
|
135
|
-
private_key_path, public_key_path, lock_path = get_ssh_key_and_lock_path()
|
|
136
|
-
private_key_path = os.path.expanduser(private_key_path)
|
|
137
|
-
public_key_path = os.path.expanduser(public_key_path)
|
|
138
|
-
lock_path = os.path.expanduser(lock_path)
|
|
139
|
-
|
|
140
|
-
lock_dir = os.path.dirname(lock_path)
|
|
141
|
-
# We should have the folder ~/.sky/generated/ssh to have 0o700 permission,
|
|
142
|
-
# as the ssh configs will be written to this folder as well in
|
|
143
|
-
# backend_utils.SSHConfigHelper
|
|
144
|
-
os.makedirs(lock_dir, exist_ok=True, mode=0o700)
|
|
145
|
-
with filelock.FileLock(lock_path, timeout=10):
|
|
146
|
-
if not os.path.exists(private_key_path):
|
|
147
|
-
public_key, private_key = _generate_rsa_key_pair()
|
|
148
|
-
_save_key_pair(private_key_path, public_key_path, private_key,
|
|
149
|
-
public_key)
|
|
150
|
-
assert os.path.exists(public_key_path), (
|
|
151
|
-
'Private key found, but associated public key '
|
|
152
|
-
f'{public_key_path} does not exist.')
|
|
153
|
-
return private_key_path, public_key_path
|
|
154
|
-
|
|
155
58
|
|
|
156
59
|
def configure_ssh_info(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
157
|
-
_, public_key_path = get_or_generate_keys()
|
|
60
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
158
61
|
with open(public_key_path, 'r', encoding='utf-8') as f:
|
|
159
62
|
public_key = f.read().strip()
|
|
160
|
-
config_str =
|
|
63
|
+
config_str = yaml_utils.dump_yaml_str(config)
|
|
161
64
|
config_str = config_str.replace('skypilot:ssh_user',
|
|
162
65
|
config['auth']['ssh_user'])
|
|
163
66
|
config_str = config_str.replace('skypilot:ssh_public_key_content',
|
|
164
67
|
public_key)
|
|
165
|
-
config =
|
|
68
|
+
config = yaml_utils.safe_load(config_str)
|
|
166
69
|
return config
|
|
167
70
|
|
|
168
71
|
|
|
72
|
+
def parse_gcp_project_oslogin(project):
|
|
73
|
+
"""Helper function to parse GCP project metadata."""
|
|
74
|
+
common_metadata = project.get('commonInstanceMetadata', {})
|
|
75
|
+
if not isinstance(common_metadata, dict):
|
|
76
|
+
common_metadata = {}
|
|
77
|
+
|
|
78
|
+
metadata_items = common_metadata.get('items', [])
|
|
79
|
+
if not isinstance(metadata_items, list):
|
|
80
|
+
metadata_items = []
|
|
81
|
+
|
|
82
|
+
project_oslogin = next(
|
|
83
|
+
(item for item in metadata_items
|
|
84
|
+
if isinstance(item, dict) and item.get('key') == 'enable-oslogin'),
|
|
85
|
+
{}).get('value', 'False')
|
|
86
|
+
|
|
87
|
+
return project_oslogin
|
|
88
|
+
|
|
89
|
+
|
|
169
90
|
# Snippets of code inspired from
|
|
170
91
|
# https://github.com/ray-project/ray/blob/master/python/ray/autoscaler/_private/gcp/config.py
|
|
171
92
|
# Takes in config, a yaml dict and outputs a postprocessed dict
|
|
@@ -174,7 +95,7 @@ def configure_ssh_info(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
174
95
|
# Retry for the GCP as sometimes there will be connection reset by peer error.
|
|
175
96
|
@common_utils.retry
|
|
176
97
|
def setup_gcp_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
177
|
-
_, public_key_path = get_or_generate_keys()
|
|
98
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
178
99
|
config = copy.deepcopy(config)
|
|
179
100
|
|
|
180
101
|
project_id = config['provider']['project_id']
|
|
@@ -223,10 +144,7 @@ def setup_gcp_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
223
144
|
'Please check your network connection.')
|
|
224
145
|
raise
|
|
225
146
|
|
|
226
|
-
project_oslogin
|
|
227
|
-
(item for item in project['commonInstanceMetadata'].get('items', [])
|
|
228
|
-
if item['key'] == 'enable-oslogin'), {}).get('value', 'False')
|
|
229
|
-
|
|
147
|
+
project_oslogin = parse_gcp_project_oslogin(project)
|
|
230
148
|
if project_oslogin.lower() == 'true':
|
|
231
149
|
logger.info(
|
|
232
150
|
f'OS Login is enabled for GCP project {project_id}. Running '
|
|
@@ -242,7 +160,7 @@ def setup_gcp_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
242
160
|
os_login_username = None
|
|
243
161
|
if proc.returncode == 0:
|
|
244
162
|
try:
|
|
245
|
-
profile =
|
|
163
|
+
profile = yaml_utils.safe_load(proc.stdout)
|
|
246
164
|
username = profile['posixAccounts'][0]['username']
|
|
247
165
|
if username:
|
|
248
166
|
os_login_username = username
|
|
@@ -302,11 +220,11 @@ def setup_gcp_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
302
220
|
|
|
303
221
|
def setup_lambda_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
304
222
|
|
|
305
|
-
get_or_generate_keys()
|
|
223
|
+
auth_utils.get_or_generate_keys()
|
|
306
224
|
|
|
307
225
|
# Ensure ssh key is registered with Lambda Cloud
|
|
308
226
|
lambda_client = lambda_utils.LambdaCloudClient()
|
|
309
|
-
_, public_key_path = get_or_generate_keys()
|
|
227
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
310
228
|
with open(public_key_path, 'r', encoding='utf-8') as f:
|
|
311
229
|
public_key = f.read().strip()
|
|
312
230
|
prefix = f'sky-key-{common_utils.get_user_hash()}'
|
|
@@ -323,7 +241,7 @@ def setup_ibm_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
323
241
|
and updates config file.
|
|
324
242
|
keys default location: '~/.ssh/sky-key' and '~/.ssh/sky-key.pub'
|
|
325
243
|
"""
|
|
326
|
-
private_key_path, _ = get_or_generate_keys()
|
|
244
|
+
private_key_path, _ = auth_utils.get_or_generate_keys()
|
|
327
245
|
|
|
328
246
|
def _get_unique_key_name():
|
|
329
247
|
suffix_len = 10
|
|
@@ -332,7 +250,7 @@ def setup_ibm_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
332
250
|
client = ibm.client(region=config['provider']['region'])
|
|
333
251
|
resource_group_id = config['provider']['resource_group_id']
|
|
334
252
|
|
|
335
|
-
_, public_key_path = get_or_generate_keys()
|
|
253
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
336
254
|
with open(os.path.abspath(os.path.expanduser(public_key_path)),
|
|
337
255
|
'r',
|
|
338
256
|
encoding='utf-8') as file:
|
|
@@ -372,116 +290,31 @@ def setup_ibm_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
372
290
|
|
|
373
291
|
|
|
374
292
|
def setup_kubernetes_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
375
|
-
|
|
376
|
-
# ClusterIP service.
|
|
377
|
-
nodeport_mode = kubernetes_enums.KubernetesNetworkingMode.NODEPORT
|
|
378
|
-
port_forward_mode = kubernetes_enums.KubernetesNetworkingMode.PORTFORWARD
|
|
379
|
-
network_mode_str = skypilot_config.get_nested(('kubernetes', 'networking'),
|
|
380
|
-
port_forward_mode.value)
|
|
381
|
-
try:
|
|
382
|
-
network_mode = kubernetes_enums.KubernetesNetworkingMode.from_str(
|
|
383
|
-
network_mode_str)
|
|
384
|
-
except ValueError as e:
|
|
385
|
-
# Add message saying "Please check: ~/.sky/config.yaml" to the error
|
|
386
|
-
# message.
|
|
387
|
-
with ux_utils.print_exception_no_traceback():
|
|
388
|
-
raise ValueError(str(e) + ' Please check: ~/.sky/config.yaml.') \
|
|
389
|
-
from None
|
|
390
|
-
_, public_key_path = get_or_generate_keys()
|
|
391
|
-
|
|
392
|
-
# Add the user's public key to the SkyPilot cluster.
|
|
393
|
-
secret_name = clouds.Kubernetes.SKY_SSH_KEY_SECRET_NAME
|
|
394
|
-
secret_field_name = clouds.Kubernetes().ssh_key_secret_field_name
|
|
395
|
-
context = config['provider'].get(
|
|
396
|
-
'context', kubernetes_utils.get_current_kube_config_context_name())
|
|
397
|
-
if context == kubernetes.in_cluster_context_name():
|
|
398
|
-
# If the context is an in-cluster context name, we are running in a pod
|
|
399
|
-
# with in-cluster configuration. We need to set the context to None
|
|
400
|
-
# to use the mounted service account.
|
|
401
|
-
context = None
|
|
293
|
+
context = kubernetes_utils.get_context_from_config(config['provider'])
|
|
402
294
|
namespace = kubernetes_utils.get_namespace_from_config(config['provider'])
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
metadata=k8s.client.V1ObjectMeta(**secret_metadata),
|
|
422
|
-
string_data={secret_field_name: public_key})
|
|
423
|
-
try:
|
|
424
|
-
if kubernetes_utils.check_secret_exists(secret_name, namespace,
|
|
425
|
-
context):
|
|
426
|
-
logger.debug(f'Key {secret_name} exists in the cluster, '
|
|
427
|
-
'patching it...')
|
|
428
|
-
kubernetes.core_api(context).patch_namespaced_secret(
|
|
429
|
-
secret_name, namespace, secret)
|
|
430
|
-
else:
|
|
431
|
-
logger.debug(f'Key {secret_name} does not exist in the cluster, '
|
|
432
|
-
'creating it...')
|
|
433
|
-
kubernetes.core_api(context).create_namespaced_secret(
|
|
434
|
-
namespace, secret)
|
|
435
|
-
except kubernetes.api_exception() as e:
|
|
436
|
-
if e.status == 409 and e.reason == 'AlreadyExists':
|
|
437
|
-
logger.debug(f'Key {secret_name} was created concurrently, '
|
|
438
|
-
'patching it...')
|
|
439
|
-
kubernetes.core_api(context).patch_namespaced_secret(
|
|
440
|
-
secret_name, namespace, secret)
|
|
441
|
-
else:
|
|
442
|
-
raise e
|
|
443
|
-
|
|
444
|
-
private_key_path, _ = get_or_generate_keys()
|
|
445
|
-
if network_mode == nodeport_mode:
|
|
446
|
-
ssh_jump_name = clouds.Kubernetes.SKY_SSH_JUMP_NAME
|
|
447
|
-
service_type = kubernetes_enums.KubernetesServiceType.NODEPORT
|
|
448
|
-
# Setup service for SSH jump pod. We create the SSH jump service here
|
|
449
|
-
# because we need to know the service IP address and port to set the
|
|
450
|
-
# ssh_proxy_command in the autoscaler config.
|
|
451
|
-
kubernetes_utils.setup_ssh_jump_svc(ssh_jump_name, namespace, context,
|
|
452
|
-
service_type)
|
|
453
|
-
ssh_proxy_cmd = kubernetes_utils.get_ssh_proxy_command(
|
|
454
|
-
ssh_jump_name,
|
|
455
|
-
nodeport_mode,
|
|
456
|
-
private_key_path=private_key_path,
|
|
457
|
-
context=context,
|
|
458
|
-
namespace=namespace)
|
|
459
|
-
elif network_mode == port_forward_mode:
|
|
460
|
-
# Using `kubectl port-forward` creates a direct tunnel to the pod and
|
|
461
|
-
# does not require a ssh jump pod.
|
|
462
|
-
kubernetes_utils.check_port_forward_mode_dependencies()
|
|
463
|
-
# TODO(romilb): This can be further optimized. Instead of using the
|
|
464
|
-
# head node as a jump pod for worker nodes, we can also directly
|
|
465
|
-
# set the ssh_target to the worker node. However, that requires
|
|
466
|
-
# changes in the downstream code to return a mapping of node IPs to
|
|
467
|
-
# pod names (to be used as ssh_target) and updating the upstream
|
|
468
|
-
# SSHConfigHelper to use a different ProxyCommand for each pod.
|
|
469
|
-
# This optimization can reduce SSH time from ~0.35s to ~0.25s, tested
|
|
470
|
-
# on GKE.
|
|
471
|
-
ssh_target = config['cluster_name'] + '-head'
|
|
472
|
-
ssh_proxy_cmd = kubernetes_utils.get_ssh_proxy_command(
|
|
473
|
-
ssh_target,
|
|
474
|
-
port_forward_mode,
|
|
475
|
-
private_key_path=private_key_path,
|
|
476
|
-
context=context,
|
|
477
|
-
namespace=namespace)
|
|
478
|
-
else:
|
|
479
|
-
# This should never happen because we check for this in from_str above.
|
|
480
|
-
raise ValueError(f'Unsupported networking mode: {network_mode_str}')
|
|
295
|
+
private_key_path, _ = auth_utils.get_or_generate_keys()
|
|
296
|
+
# Using `kubectl port-forward` creates a direct tunnel to the pod and
|
|
297
|
+
# does not require a ssh jump pod.
|
|
298
|
+
kubernetes_utils.check_port_forward_mode_dependencies()
|
|
299
|
+
# TODO(romilb): This can be further optimized. Instead of using the
|
|
300
|
+
# head node as a jump pod for worker nodes, we can also directly
|
|
301
|
+
# set the ssh_target to the worker node. However, that requires
|
|
302
|
+
# changes in the downstream code to return a mapping of node IPs to
|
|
303
|
+
# pod names (to be used as ssh_target) and updating the upstream
|
|
304
|
+
# SSHConfigHelper to use a different ProxyCommand for each pod.
|
|
305
|
+
# This optimization can reduce SSH time from ~0.35s to ~0.25s, tested
|
|
306
|
+
# on GKE.
|
|
307
|
+
pod_name = config['cluster_name'] + '-head'
|
|
308
|
+
ssh_proxy_cmd = kubernetes_utils.get_ssh_proxy_command(
|
|
309
|
+
pod_name,
|
|
310
|
+
private_key_path=private_key_path,
|
|
311
|
+
context=context,
|
|
312
|
+
namespace=namespace)
|
|
481
313
|
config['auth']['ssh_proxy_command'] = ssh_proxy_cmd
|
|
482
314
|
config['auth']['ssh_private_key'] = private_key_path
|
|
483
315
|
|
|
484
|
-
|
|
316
|
+
# Add the user's public key to the SkyPilot cluster.
|
|
317
|
+
return configure_ssh_info(config)
|
|
485
318
|
|
|
486
319
|
|
|
487
320
|
# ---------------------------------- RunPod ---------------------------------- #
|
|
@@ -490,7 +323,7 @@ def setup_runpod_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
490
323
|
- Generates a new SSH key pair if one does not exist.
|
|
491
324
|
- Adds the public SSH key to the user's RunPod account.
|
|
492
325
|
"""
|
|
493
|
-
_, public_key_path = get_or_generate_keys()
|
|
326
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
494
327
|
with open(public_key_path, 'r', encoding='UTF-8') as pub_key_file:
|
|
495
328
|
public_key = pub_key_file.read().strip()
|
|
496
329
|
runpod.runpod.cli.groups.ssh.functions.add_ssh_key(public_key)
|
|
@@ -503,7 +336,7 @@ def setup_vast_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
503
336
|
- Generates a new SSH key pair if one does not exist.
|
|
504
337
|
- Adds the public SSH key to the user's Vast account.
|
|
505
338
|
"""
|
|
506
|
-
_, public_key_path = get_or_generate_keys()
|
|
339
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
507
340
|
with open(public_key_path, 'r', encoding='UTF-8') as pub_key_file:
|
|
508
341
|
public_key = pub_key_file.read().strip()
|
|
509
342
|
current_key_list = vast.vast().show_ssh_keys() # pylint: disable=assignment-from-no-return
|
|
@@ -517,7 +350,7 @@ def setup_vast_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
517
350
|
|
|
518
351
|
def setup_fluidstack_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
519
352
|
|
|
520
|
-
_, public_key_path = get_or_generate_keys()
|
|
353
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
521
354
|
|
|
522
355
|
client = fluidstack_utils.FluidstackClient()
|
|
523
356
|
public_key = None
|
|
@@ -526,3 +359,124 @@ def setup_fluidstack_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
526
359
|
client.get_or_add_ssh_key(public_key)
|
|
527
360
|
config['auth']['ssh_public_key'] = public_key_path
|
|
528
361
|
return configure_ssh_info(config)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def setup_hyperbolic_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
365
|
+
"""Sets up SSH authentication for Hyperbolic."""
|
|
366
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
367
|
+
with open(public_key_path, 'r', encoding='utf-8') as f:
|
|
368
|
+
public_key = f.read().strip()
|
|
369
|
+
|
|
370
|
+
# TODO: adjust below to use public_keys instead of
|
|
371
|
+
# public_key once backwards-compatibility is no longer required
|
|
372
|
+
config['publicKey'] = public_key
|
|
373
|
+
|
|
374
|
+
# Set up auth section for Ray template
|
|
375
|
+
config.setdefault('auth', {})
|
|
376
|
+
config['auth']['ssh_user'] = 'ubuntu'
|
|
377
|
+
config['auth']['ssh_public_key'] = public_key_path
|
|
378
|
+
|
|
379
|
+
return configure_ssh_info(config)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def setup_shadeform_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
383
|
+
"""Sets up SSH authentication for Shadeform.
|
|
384
|
+
- Generates a new SSH key pair if one does not exist.
|
|
385
|
+
- Adds the public SSH key to the user's Shadeform account.
|
|
386
|
+
|
|
387
|
+
Note: This assumes there is a Shadeform Python SDK available.
|
|
388
|
+
If no official SDK exists, this function would need to use direct API calls.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
392
|
+
ssh_key_id = None
|
|
393
|
+
|
|
394
|
+
with open(public_key_path, 'r', encoding='utf-8') as f:
|
|
395
|
+
public_key = f.read().strip()
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
# Add SSH key to Shadeform using our utility functions
|
|
399
|
+
ssh_key_id = shadeform_adaptor.add_ssh_key_to_shadeform(public_key)
|
|
400
|
+
|
|
401
|
+
except ImportError as e:
|
|
402
|
+
# If required dependencies are missing
|
|
403
|
+
logger.warning(
|
|
404
|
+
f'Failed to add Shadeform SSH key due to missing dependencies: '
|
|
405
|
+
f'{e}. Manually configure SSH keys in your Shadeform account.')
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.warning(f'Failed to set up Shadeform authentication: {e}')
|
|
409
|
+
raise exceptions.CloudUserIdentityError(
|
|
410
|
+
'Failed to set up SSH authentication for Shadeform. '
|
|
411
|
+
f'Please ensure your Shadeform credentials are configured: {e}'
|
|
412
|
+
) from e
|
|
413
|
+
|
|
414
|
+
if ssh_key_id is None:
|
|
415
|
+
raise Exception('Failed to add SSH key to Shadeform')
|
|
416
|
+
|
|
417
|
+
# Configure SSH info in the config
|
|
418
|
+
config['auth']['ssh_public_key'] = public_key_path
|
|
419
|
+
config['auth']['ssh_key_id'] = ssh_key_id
|
|
420
|
+
|
|
421
|
+
return configure_ssh_info(config)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def setup_primeintellect_authentication(
|
|
425
|
+
config: Dict[str, Any]) -> Dict[str, Any]:
|
|
426
|
+
"""Sets up SSH authentication for Prime Intellect.
|
|
427
|
+
- Generates a new SSH key pair if one does not exist.
|
|
428
|
+
- Adds the public SSH key to the user's Prime Intellect account.
|
|
429
|
+
"""
|
|
430
|
+
# Ensure local SSH keypair exists and fetch public key content
|
|
431
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
432
|
+
with open(public_key_path, 'r', encoding='utf-8') as f:
|
|
433
|
+
public_key = f.read().strip()
|
|
434
|
+
|
|
435
|
+
# Register the public key with Prime Intellect (no-op if already exists)
|
|
436
|
+
client = primeintellect_utils.PrimeIntellectAPIClient()
|
|
437
|
+
client.get_or_add_ssh_key(public_key)
|
|
438
|
+
|
|
439
|
+
# Set up auth section for Ray template
|
|
440
|
+
config.setdefault('auth', {})
|
|
441
|
+
# Default username for Prime Intellect images
|
|
442
|
+
config['auth']['ssh_user'] = 'ubuntu'
|
|
443
|
+
config['auth']['ssh_public_key'] = public_key_path
|
|
444
|
+
|
|
445
|
+
return configure_ssh_info(config)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def setup_seeweb_authentication(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
449
|
+
"""Registers the public key with Seeweb and notes the remote name."""
|
|
450
|
+
# 1. local key pair
|
|
451
|
+
auth_utils.get_or_generate_keys()
|
|
452
|
+
|
|
453
|
+
# 2. public key
|
|
454
|
+
_, public_key_path = auth_utils.get_or_generate_keys()
|
|
455
|
+
with open(public_key_path, 'r', encoding='utf-8') as f:
|
|
456
|
+
public_key = f.read().strip()
|
|
457
|
+
|
|
458
|
+
# 3. Seeweb API client
|
|
459
|
+
client = seeweb_adaptor.client()
|
|
460
|
+
|
|
461
|
+
# 4. Check if key is already registered
|
|
462
|
+
prefix = f'sky-key-{common_utils.get_user_hash()}'
|
|
463
|
+
remote_name = None
|
|
464
|
+
for k in client.fetch_ssh_keys():
|
|
465
|
+
if k.key.strip() == public_key:
|
|
466
|
+
remote_name = k.label # already present
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
# 5. doesn't exist, choose a unique name and create it
|
|
470
|
+
if remote_name is None:
|
|
471
|
+
suffix = 1
|
|
472
|
+
remote_name = prefix
|
|
473
|
+
existing_names = {k.label for k in client.fetch_ssh_keys()}
|
|
474
|
+
while remote_name in existing_names:
|
|
475
|
+
suffix += 1
|
|
476
|
+
remote_name = f'{prefix}-{suffix}'
|
|
477
|
+
client.create_ssh_key(label=remote_name, key=public_key)
|
|
478
|
+
|
|
479
|
+
# 6. Put the remote name in cluster-config (like for Lambda)
|
|
480
|
+
config['auth']['remote_key_name'] = remote_name
|
|
481
|
+
|
|
482
|
+
return config
|
sky/backends/__init__.py
CHANGED
|
@@ -3,11 +3,13 @@ from sky.backends.backend import Backend
|
|
|
3
3
|
from sky.backends.backend import ResourceHandle
|
|
4
4
|
from sky.backends.cloud_vm_ray_backend import CloudVmRayBackend
|
|
5
5
|
from sky.backends.cloud_vm_ray_backend import CloudVmRayResourceHandle
|
|
6
|
+
from sky.backends.cloud_vm_ray_backend import LocalResourcesHandle
|
|
7
|
+
from sky.backends.cloud_vm_ray_backend import SkyletClient
|
|
6
8
|
from sky.backends.local_docker_backend import LocalDockerBackend
|
|
7
9
|
from sky.backends.local_docker_backend import LocalDockerResourceHandle
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
10
12
|
'Backend', 'ResourceHandle', 'CloudVmRayBackend',
|
|
11
|
-
'CloudVmRayResourceHandle', '
|
|
12
|
-
'LocalDockerResourceHandle'
|
|
13
|
+
'CloudVmRayResourceHandle', 'SkyletClient', 'LocalResourcesHandle',
|
|
14
|
+
'LocalDockerBackend', 'LocalDockerResourceHandle'
|
|
13
15
|
]
|
sky/backends/backend.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Sky backend interface."""
|
|
2
2
|
import typing
|
|
3
|
-
from typing import Dict, Generic, Optional, Tuple
|
|
3
|
+
from typing import Any, Dict, Generic, Optional, Tuple, Union
|
|
4
4
|
|
|
5
5
|
from sky.usage import usage_lib
|
|
6
6
|
from sky.utils import cluster_utils
|
|
@@ -37,8 +37,9 @@ class Backend(Generic[_ResourceHandleType]):
|
|
|
37
37
|
ResourceHandle = ResourceHandle # pylint: disable=invalid-name
|
|
38
38
|
|
|
39
39
|
# --- APIs ---
|
|
40
|
-
def check_resources_fit_cluster(
|
|
41
|
-
|
|
40
|
+
def check_resources_fit_cluster(
|
|
41
|
+
self, handle: _ResourceHandleType,
|
|
42
|
+
task: 'task_lib.Task') -> Optional['resources.Resources']:
|
|
42
43
|
"""Check whether resources of the task are satisfied by cluster."""
|
|
43
44
|
raise NotImplementedError
|
|
44
45
|
|
|
@@ -89,8 +90,16 @@ class Backend(Generic[_ResourceHandleType]):
|
|
|
89
90
|
|
|
90
91
|
@timeline.event
|
|
91
92
|
@usage_lib.messages.usage.update_runtime('sync_workdir')
|
|
92
|
-
def sync_workdir(self, handle: _ResourceHandleType,
|
|
93
|
-
|
|
93
|
+
def sync_workdir(self, handle: _ResourceHandleType,
|
|
94
|
+
workdir: Union[Path, Dict[str, Any]],
|
|
95
|
+
envs_and_secrets: Dict[str, str]) -> None:
|
|
96
|
+
return self._sync_workdir(handle, workdir, envs_and_secrets)
|
|
97
|
+
|
|
98
|
+
@timeline.event
|
|
99
|
+
@usage_lib.messages.usage.update_runtime('download_file')
|
|
100
|
+
def download_file(self, handle: _ResourceHandleType, local_file_path: str,
|
|
101
|
+
remote_file_path: str) -> None:
|
|
102
|
+
return self._download_file(handle, local_file_path, remote_file_path)
|
|
94
103
|
|
|
95
104
|
@timeline.event
|
|
96
105
|
@usage_lib.messages.usage.update_runtime('sync_file_mounts')
|
|
@@ -117,7 +126,6 @@ class Backend(Generic[_ResourceHandleType]):
|
|
|
117
126
|
def execute(self,
|
|
118
127
|
handle: _ResourceHandleType,
|
|
119
128
|
task: 'task_lib.Task',
|
|
120
|
-
detach_run: bool,
|
|
121
129
|
dryrun: bool = False) -> Optional[int]:
|
|
122
130
|
"""Execute the task on the cluster.
|
|
123
131
|
|
|
@@ -128,7 +136,7 @@ class Backend(Generic[_ResourceHandleType]):
|
|
|
128
136
|
handle.get_cluster_name())
|
|
129
137
|
usage_lib.messages.usage.update_actual_task(task)
|
|
130
138
|
with rich_utils.safe_status(ux_utils.spinner_message('Submitting job')):
|
|
131
|
-
return self._execute(handle, task,
|
|
139
|
+
return self._execute(handle, task, dryrun)
|
|
132
140
|
|
|
133
141
|
@timeline.event
|
|
134
142
|
def post_execute(self, handle: _ResourceHandleType, down: bool) -> None:
|
|
@@ -164,7 +172,13 @@ class Backend(Generic[_ResourceHandleType]):
|
|
|
164
172
|
) -> Tuple[Optional[_ResourceHandleType], bool]:
|
|
165
173
|
raise NotImplementedError
|
|
166
174
|
|
|
167
|
-
def _sync_workdir(self, handle: _ResourceHandleType,
|
|
175
|
+
def _sync_workdir(self, handle: _ResourceHandleType,
|
|
176
|
+
workdir: Union[Path, Dict[str, Any]],
|
|
177
|
+
envs_and_secrets: Dict[str, str]) -> None:
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
def _download_file(self, handle: _ResourceHandleType, local_file_path: str,
|
|
181
|
+
remote_file_path: str) -> None:
|
|
168
182
|
raise NotImplementedError
|
|
169
183
|
|
|
170
184
|
def _sync_file_mounts(
|
|
@@ -182,7 +196,6 @@ class Backend(Generic[_ResourceHandleType]):
|
|
|
182
196
|
def _execute(self,
|
|
183
197
|
handle: _ResourceHandleType,
|
|
184
198
|
task: 'task_lib.Task',
|
|
185
|
-
detach_run: bool,
|
|
186
199
|
dryrun: bool = False) -> Optional[int]:
|
|
187
200
|
raise NotImplementedError
|
|
188
201
|
|