skypilot-nightly 1.0.0.dev20250826__py3-none-any.whl → 1.0.0.dev20250828__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skypilot-nightly might be problematic. Click here for more details.

Files changed (83) hide show
  1. sky/__init__.py +2 -2
  2. sky/admin_policy.py +11 -10
  3. sky/authentication.py +4 -10
  4. sky/backends/backend.py +3 -5
  5. sky/backends/backend_utils.py +41 -56
  6. sky/backends/cloud_vm_ray_backend.py +13 -24
  7. sky/backends/local_docker_backend.py +3 -8
  8. sky/client/cli/command.py +43 -10
  9. sky/client/common.py +41 -14
  10. sky/client/sdk.py +24 -9
  11. sky/client/sdk_async.py +6 -2
  12. sky/clouds/aws.py +1 -1
  13. sky/clouds/cloud.py +15 -0
  14. sky/clouds/kubernetes.py +27 -0
  15. sky/clouds/ssh.py +2 -3
  16. sky/core.py +1 -4
  17. sky/dashboard/out/404.html +1 -1
  18. sky/dashboard/out/_next/static/chunks/{webpack-6e76f636a048e145.js → webpack-6dae1cd599a34def.js} +1 -1
  19. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  20. sky/dashboard/out/clusters/[cluster].html +1 -1
  21. sky/dashboard/out/clusters.html +1 -1
  22. sky/dashboard/out/config.html +1 -1
  23. sky/dashboard/out/index.html +1 -1
  24. sky/dashboard/out/infra/[context].html +1 -1
  25. sky/dashboard/out/infra.html +1 -1
  26. sky/dashboard/out/jobs/[job].html +1 -1
  27. sky/dashboard/out/jobs/pools/[pool].html +1 -1
  28. sky/dashboard/out/jobs.html +1 -1
  29. sky/dashboard/out/users.html +1 -1
  30. sky/dashboard/out/volumes.html +1 -1
  31. sky/dashboard/out/workspace/new.html +1 -1
  32. sky/dashboard/out/workspaces/[name].html +1 -1
  33. sky/dashboard/out/workspaces.html +1 -1
  34. sky/global_user_state.py +127 -23
  35. sky/jobs/client/sdk.py +5 -2
  36. sky/jobs/recovery_strategy.py +9 -4
  37. sky/logs/agent.py +2 -2
  38. sky/logs/aws.py +6 -3
  39. sky/provision/do/utils.py +2 -1
  40. sky/provision/kubernetes/config.py +2 -8
  41. sky/provision/kubernetes/instance.py +58 -8
  42. sky/provision/kubernetes/network_utils.py +3 -4
  43. sky/provision/kubernetes/utils.py +8 -7
  44. sky/provision/nebius/utils.py +51 -9
  45. sky/provision/vsphere/vsphere_utils.py +2 -8
  46. sky/schemas/api/responses.py +7 -0
  47. sky/serve/client/impl.py +5 -4
  48. sky/serve/replica_managers.py +4 -3
  49. sky/serve/serve_utils.py +4 -4
  50. sky/serve/server/impl.py +3 -2
  51. sky/serve/service_spec.py +2 -8
  52. sky/server/auth/authn.py +4 -0
  53. sky/server/auth/oauth2_proxy.py +10 -4
  54. sky/server/common.py +10 -3
  55. sky/server/daemons.py +10 -5
  56. sky/server/requests/executor.py +6 -1
  57. sky/server/requests/requests.py +21 -0
  58. sky/server/server.py +34 -33
  59. sky/server/uvicorn.py +33 -0
  60. sky/setup_files/dependencies.py +1 -0
  61. sky/sky_logging.py +4 -1
  62. sky/skylet/events.py +4 -5
  63. sky/skypilot_config.py +14 -12
  64. sky/ssh_node_pools/core.py +3 -1
  65. sky/task.py +4 -10
  66. sky/templates/nebius-ray.yml.j2 +4 -8
  67. sky/usage/usage_lib.py +3 -2
  68. sky/users/server.py +6 -6
  69. sky/utils/common_utils.py +0 -71
  70. sky/utils/controller_utils.py +4 -3
  71. sky/utils/dag_utils.py +4 -4
  72. sky/utils/kubernetes/config_map_utils.py +3 -3
  73. sky/utils/schemas.py +3 -0
  74. sky/utils/yaml_utils.py +102 -0
  75. sky/volumes/volume.py +8 -3
  76. {skypilot_nightly-1.0.0.dev20250826.dist-info → skypilot_nightly-1.0.0.dev20250828.dist-info}/METADATA +2 -1
  77. {skypilot_nightly-1.0.0.dev20250826.dist-info → skypilot_nightly-1.0.0.dev20250828.dist-info}/RECORD +83 -82
  78. /sky/dashboard/out/_next/static/{TPMkEeuj85tHTmIW7Gu3S → 9DW6d9jaP2kZt0NcgIfFa}/_buildManifest.js +0 -0
  79. /sky/dashboard/out/_next/static/{TPMkEeuj85tHTmIW7Gu3S → 9DW6d9jaP2kZt0NcgIfFa}/_ssgManifest.js +0 -0
  80. {skypilot_nightly-1.0.0.dev20250826.dist-info → skypilot_nightly-1.0.0.dev20250828.dist-info}/WHEEL +0 -0
  81. {skypilot_nightly-1.0.0.dev20250826.dist-info → skypilot_nightly-1.0.0.dev20250828.dist-info}/entry_points.txt +0 -0
  82. {skypilot_nightly-1.0.0.dev20250826.dist-info → skypilot_nightly-1.0.0.dev20250828.dist-info}/licenses/LICENSE +0 -0
  83. {skypilot_nightly-1.0.0.dev20250826.dist-info → skypilot_nightly-1.0.0.dev20250828.dist-info}/top_level.txt +0 -0
sky/server/server.py CHANGED
@@ -7,7 +7,6 @@ import contextlib
7
7
  import datetime
8
8
  import hashlib
9
9
  import json
10
- import logging
11
10
  import multiprocessing
12
11
  import os
13
12
  import pathlib
@@ -24,7 +23,6 @@ import zipfile
24
23
  import aiofiles
25
24
  import fastapi
26
25
  from fastapi.middleware import cors
27
- from passlib.hash import apr_md5_crypt
28
26
  import starlette.middleware.base
29
27
  import uvloop
30
28
 
@@ -69,7 +67,6 @@ from sky.utils import common_utils
69
67
  from sky.utils import context
70
68
  from sky.utils import context_utils
71
69
  from sky.utils import dag_utils
72
- from sky.utils import env_options
73
70
  from sky.utils import status_lib
74
71
  from sky.utils import subprocess_utils
75
72
  from sky.volumes.server import server as volumes_rest
@@ -85,31 +82,6 @@ P = ParamSpec('P')
85
82
 
86
83
  _SERVER_USER_HASH_KEY = 'server_user_hash'
87
84
 
88
-
89
- def _add_timestamp_prefix_for_server_logs() -> None:
90
- server_logger = sky_logging.init_logger('sky.server')
91
- # Clear existing handlers first to prevent duplicates
92
- server_logger.handlers.clear()
93
- # Disable propagation to avoid the root logger of SkyPilot being affected
94
- server_logger.propagate = False
95
- # Add date prefix to the log message printed by loggers under
96
- # server.
97
- stream_handler = logging.StreamHandler(sys.stdout)
98
- if env_options.Options.SHOW_DEBUG_INFO.get():
99
- stream_handler.setLevel(logging.DEBUG)
100
- else:
101
- stream_handler.setLevel(logging.INFO)
102
- stream_handler.flush = sys.stdout.flush # type: ignore
103
- stream_handler.setFormatter(sky_logging.FORMATTER)
104
- server_logger.addHandler(stream_handler)
105
- # Add date prefix to the log message printed by uvicorn.
106
- for name in ['uvicorn', 'uvicorn.access']:
107
- uvicorn_logger = logging.getLogger(name)
108
- uvicorn_logger.handlers.clear()
109
- uvicorn_logger.addHandler(stream_handler)
110
-
111
-
112
- _add_timestamp_prefix_for_server_logs()
113
85
  logger = sky_logging.init_logger(__name__)
114
86
 
115
87
  # TODO(zhwu): Streaming requests, such log tailing after sky launch or sky logs,
@@ -148,7 +120,7 @@ def _try_set_basic_auth_user(request: fastapi.Request):
148
120
  username_encoded = username.encode('utf8')
149
121
  db_username_encoded = user.name.encode('utf8')
150
122
  if (username_encoded == db_username_encoded and
151
- apr_md5_crypt.verify(password, user.password)):
123
+ common.crypt_ctx.verify(password, user.password)):
152
124
  request.state.auth_user = user
153
125
  break
154
126
 
@@ -248,7 +220,7 @@ class BasicAuthMiddleware(starlette.middleware.base.BaseHTTPMiddleware):
248
220
  username_encoded = username.encode('utf8')
249
221
  db_username_encoded = user.name.encode('utf8')
250
222
  if (username_encoded == db_username_encoded and
251
- apr_md5_crypt.verify(password, user.password)):
223
+ common.crypt_ctx.verify(password, user.password)):
252
224
  valid_user = True
253
225
  request.state.auth_user = user
254
226
  await authn.override_user_info_in_request_body(request, user)
@@ -852,6 +824,15 @@ async def upload_zip_file(request: fastapi.Request, user_hash: str,
852
824
  chunk_index: The chunk index, starting from 0.
853
825
  total_chunks: The total number of chunks.
854
826
  """
827
+ # Field _body would be set if the request body has been received, fail fast
828
+ # to surface potential memory issues, i.e. catch the issue in our smoke
829
+ # test.
830
+ # pylint: disable=protected-access
831
+ if hasattr(request, '_body'):
832
+ raise fastapi.HTTPException(
833
+ status_code=500,
834
+ detail='Upload request body should not be received before streaming'
835
+ )
855
836
  # Add the upload id to the cleanup list.
856
837
  upload_ids_to_cleanup[(upload_id,
857
838
  user_hash)] = (datetime.datetime.now() +
@@ -919,8 +900,9 @@ async def upload_zip_file(request: fastapi.Request, user_hash: str,
919
900
  zip_file_path.rename(zip_file_path.with_suffix(''))
920
901
  missing_chunks = get_missing_chunks(total_chunks)
921
902
  if missing_chunks:
922
- return payloads.UploadZipFileResponse(status='uploading',
923
- missing_chunks=missing_chunks)
903
+ return payloads.UploadZipFileResponse(
904
+ status=responses.UploadStatus.UPLOADING.value,
905
+ missing_chunks=missing_chunks)
924
906
  zip_file_path = client_file_mounts_dir / f'{upload_id}.zip'
925
907
  async with aiofiles.open(zip_file_path, 'wb') as zip_file:
926
908
  for chunk in range(total_chunks):
@@ -937,7 +919,8 @@ async def upload_zip_file(request: fastapi.Request, user_hash: str,
937
919
  unzip_file(zip_file_path, client_file_mounts_dir)
938
920
  if total_chunks > 1:
939
921
  shutil.rmtree(chunk_dir)
940
- return payloads.UploadZipFileResponse(status='completed')
922
+ return payloads.UploadZipFileResponse(
923
+ status=responses.UploadStatus.COMPLETED.value)
941
924
 
942
925
 
943
926
  def _is_relative_to(path: pathlib.Path, parent: pathlib.Path) -> bool:
@@ -1420,6 +1403,9 @@ async def api_get(request_id: str) -> payloads.RequestPayload:
1420
1403
  raise fastapi.HTTPException(
1421
1404
  status_code=500, detail=request_task.encode().model_dump())
1422
1405
  return request_task.encode()
1406
+ elif (request_task.status == requests_lib.RequestStatus.RUNNING and
1407
+ daemons.is_daemon_request_id(request_id)):
1408
+ return request_task.encode()
1423
1409
  # yield control to allow other coroutines to run, sleep shortly
1424
1410
  # to avoid storming the DB and CPU in the meantime
1425
1411
  await asyncio.sleep(0.1)
@@ -1508,6 +1494,14 @@ async def stream(
1508
1494
  if log_path == constants.API_SERVER_LOGS:
1509
1495
  resolved_log_path = pathlib.Path(
1510
1496
  constants.API_SERVER_LOGS).expanduser()
1497
+ if not resolved_log_path.exists():
1498
+ raise fastapi.HTTPException(
1499
+ status_code=404,
1500
+ detail='Server log file does not exist. The API server may '
1501
+ 'have been started with `--foreground` - check the '
1502
+ 'stdout of API server process, such as: '
1503
+ '`kubectl logs -n api-server-namespace '
1504
+ 'api-server-pod-name`')
1511
1505
  else:
1512
1506
  # This should be a log path under ~/sky_logs.
1513
1507
  resolved_logs_directory = pathlib.Path(
@@ -1786,6 +1780,11 @@ async def complete_volume_name(incomplete: str,) -> List[str]:
1786
1780
  return global_user_state.get_volume_names_start_with(incomplete)
1787
1781
 
1788
1782
 
1783
+ @app.get('/api/completion/api_request')
1784
+ async def complete_api_request(incomplete: str,) -> List[str]:
1785
+ return requests_lib.get_api_request_ids_start_with(incomplete)
1786
+
1787
+
1789
1788
  @app.get('/dashboard/{full_path:path}')
1790
1789
  async def serve_dashboard(full_path: str):
1791
1790
  """Serves the Next.js dashboard application.
@@ -1859,6 +1858,8 @@ if __name__ == '__main__':
1859
1858
 
1860
1859
  from sky.server import uvicorn as skyuvicorn
1861
1860
 
1861
+ skyuvicorn.add_timestamp_prefix_for_server_logs()
1862
+
1862
1863
  # Initialize global user state db
1863
1864
  global_user_state.initialize_and_get_db()
1864
1865
  # Initialize request db
sky/server/uvicorn.py CHANGED
@@ -4,8 +4,10 @@ This module is a wrapper around uvicorn to customize the behavior of the
4
4
  server.
5
5
  """
6
6
  import asyncio
7
+ import logging
7
8
  import os
8
9
  import signal
10
+ import sys
9
11
  import threading
10
12
  import time
11
13
  from types import FrameType
@@ -21,6 +23,7 @@ from sky.server import state
21
23
  from sky.server.requests import requests as requests_lib
22
24
  from sky.skylet import constants
23
25
  from sky.utils import context_utils
26
+ from sky.utils import env_options
24
27
  from sky.utils import subprocess_utils
25
28
 
26
29
  logger = sky_logging.init_logger(__name__)
@@ -47,6 +50,35 @@ _RETRIABLE_REQUEST_NAMES = [
47
50
  ]
48
51
 
49
52
 
53
+ def add_timestamp_prefix_for_server_logs() -> None:
54
+ """Configure logging for API server.
55
+
56
+ Note: we only do this in the main API server process and uvicorn processes,
57
+ to avoid affecting executor logs (including in modules like
58
+ sky.server.requests) that may get sent to the client.
59
+ """
60
+ server_logger = sky_logging.init_logger('sky.server')
61
+ # Clear existing handlers first to prevent duplicates
62
+ server_logger.handlers.clear()
63
+ # Disable propagation to avoid the root logger of SkyPilot being affected
64
+ server_logger.propagate = False
65
+ # Add date prefix to the log message printed by loggers under
66
+ # server.
67
+ stream_handler = logging.StreamHandler(sys.stdout)
68
+ if env_options.Options.SHOW_DEBUG_INFO.get():
69
+ stream_handler.setLevel(logging.DEBUG)
70
+ else:
71
+ stream_handler.setLevel(logging.INFO)
72
+ stream_handler.flush = sys.stdout.flush # type: ignore
73
+ stream_handler.setFormatter(sky_logging.FORMATTER)
74
+ server_logger.addHandler(stream_handler)
75
+ # Add date prefix to the log message printed by uvicorn.
76
+ for name in ['uvicorn', 'uvicorn.access']:
77
+ uvicorn_logger = logging.getLogger(name)
78
+ uvicorn_logger.handlers.clear()
79
+ uvicorn_logger.addHandler(stream_handler)
80
+
81
+
50
82
  class Server(uvicorn.Server):
51
83
  """Server wrapper for uvicorn.
52
84
 
@@ -162,6 +194,7 @@ class Server(uvicorn.Server):
162
194
 
163
195
  def run(self, *args, **kwargs):
164
196
  """Run the server process."""
197
+ add_timestamp_prefix_for_server_logs()
165
198
  context_utils.hijack_sys_attrs()
166
199
  # Use default loop policy of uvicorn (use uvloop if available).
167
200
  self.config.setup_event_loop()
@@ -65,6 +65,7 @@ install_requires = [
65
65
  # Required for API server metrics
66
66
  'prometheus_client>=0.8.0',
67
67
  'passlib',
68
+ 'bcrypt',
68
69
  'pyjwt',
69
70
  'gitpython',
70
71
  'types-paramiko',
sky/sky_logging.py CHANGED
@@ -202,7 +202,10 @@ def set_sky_logging_levels(level: int):
202
202
 
203
203
 
204
204
  def logging_enabled(logger: logging.Logger, level: int) -> bool:
205
- return logger.level <= level
205
+ # Note(cooperc): This may return true in a lot of cases where we won't
206
+ # actually log anything, since the log level is set on the handler in
207
+ # _setup_logger.
208
+ return logger.getEffectiveLevel() <= level
206
209
 
207
210
 
208
211
  @contextlib.contextmanager
sky/skylet/events.py CHANGED
@@ -7,7 +7,6 @@ import time
7
7
  import traceback
8
8
 
9
9
  import psutil
10
- import yaml
11
10
 
12
11
  from sky import clouds
13
12
  from sky import sky_logging
@@ -21,9 +20,9 @@ from sky.skylet import constants
21
20
  from sky.skylet import job_lib
22
21
  from sky.usage import usage_lib
23
22
  from sky.utils import cluster_utils
24
- from sky.utils import common_utils
25
23
  from sky.utils import registry
26
24
  from sky.utils import ux_utils
25
+ from sky.utils import yaml_utils
27
26
 
28
27
  # Seconds of sleep between the processing of skylet events.
29
28
  EVENT_CHECKING_INTERVAL_SECONDS = 20
@@ -181,7 +180,7 @@ class AutostopEvent(SkyletEvent):
181
180
 
182
181
  config_path = os.path.abspath(
183
182
  os.path.expanduser(cluster_utils.SKY_CLUSTER_YAML_REMOTE_PATH))
184
- config = common_utils.read_yaml(config_path)
183
+ config = yaml_utils.read_yaml(config_path)
185
184
  provider_name = cluster_utils.get_provider_name(config)
186
185
  cloud = registry.CLOUD_REGISTRY.from_str(provider_name)
187
186
  assert cloud is not None, f'Unknown cloud: {provider_name}'
@@ -309,7 +308,7 @@ class AutostopEvent(SkyletEvent):
309
308
  else:
310
309
  yaml_str = self._CATCH_NODES.sub(r'cache_stopped_nodes: true',
311
310
  yaml_str)
312
- config = yaml.safe_load(yaml_str)
311
+ config = yaml_utils.safe_load(yaml_str)
313
312
  # Set the private key with the existed key on the remote instance.
314
313
  config['auth']['ssh_private_key'] = '~/ray_bootstrap_key.pem'
315
314
  # NOTE: We must do this, otherwise with ssh_proxy_command still under
@@ -326,5 +325,5 @@ class AutostopEvent(SkyletEvent):
326
325
  config['auth'].pop('ssh_proxy_command', None)
327
326
  # Empty the file_mounts.
328
327
  config['file_mounts'] = {}
329
- common_utils.dump_yaml(yaml_path, config)
328
+ yaml_utils.dump_yaml(yaml_path, config)
330
329
  logger.debug('Replaced upscaling speed to 0.')
sky/skypilot_config.py CHANGED
@@ -75,6 +75,7 @@ from sky.utils import config_utils
75
75
  from sky.utils import context
76
76
  from sky.utils import schemas
77
77
  from sky.utils import ux_utils
78
+ from sky.utils import yaml_utils
78
79
  from sky.utils.db import db_utils
79
80
  from sky.utils.kubernetes import config_map_utils
80
81
 
@@ -493,7 +494,7 @@ def reload_config() -> None:
493
494
  def parse_and_validate_config_file(config_path: str) -> config_utils.Config:
494
495
  config = config_utils.Config()
495
496
  try:
496
- config_dict = common_utils.read_yaml(config_path)
497
+ config_dict = yaml_utils.read_yaml(config_path)
497
498
  config = config_utils.Config.from_dict(config_dict)
498
499
  # pop the db url from the config, and set it to the env var.
499
500
  # this is to avoid db url (considered a sensitive value)
@@ -503,7 +504,7 @@ def parse_and_validate_config_file(config_path: str) -> config_utils.Config:
503
504
  os.environ[constants.ENV_VAR_DB_CONNECTION_URI] = db_url
504
505
  if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
505
506
  logger.debug(f'Config loaded from {config_path}:\n'
506
- f'{common_utils.dump_yaml_str(dict(config))}')
507
+ f'{yaml_utils.dump_yaml_str(dict(config))}')
507
508
  except yaml.YAMLError as e:
508
509
  logger.error(f'Error in loading config file ({config_path}):', e)
509
510
  if config:
@@ -532,7 +533,7 @@ def _parse_dotlist(dotlist: List[str]) -> config_utils.Config:
532
533
  if len(key) == 0 or len(value) == 0:
533
534
  raise ValueError(f'Invalid config override: {arg}. '
534
535
  'Please use the format: key=value')
535
- value = yaml.safe_load(value)
536
+ value = yaml_utils.safe_load(value)
536
537
  nested_keys = tuple(key.split('.'))
537
538
  config.set_nested(nested_keys, value)
538
539
  return config
@@ -585,7 +586,8 @@ def _reload_config_as_server() -> None:
585
586
  row = session.query(config_yaml_table).filter_by(
586
587
  key=key).first()
587
588
  if row:
588
- db_config = config_utils.Config(yaml.safe_load(row.value))
589
+ db_config = config_utils.Config(
590
+ yaml_utils.safe_load(row.value))
589
591
  db_config.pop_nested(('db',), None)
590
592
  return db_config
591
593
  return None
@@ -598,7 +600,7 @@ def _reload_config_as_server() -> None:
598
600
  sqlalchemy_engine.dispose()
599
601
  if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
600
602
  logger.debug(f'server config: \n'
601
- f'{common_utils.dump_yaml_str(dict(server_config))}')
603
+ f'{yaml_utils.dump_yaml_str(dict(server_config))}')
602
604
  _set_loaded_config(server_config)
603
605
  _set_loaded_config_path(server_config_path)
604
606
 
@@ -626,7 +628,7 @@ def _reload_config_as_client() -> None:
626
628
  if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
627
629
  logger.debug(
628
630
  f'client config (before task and CLI overrides): \n'
629
- f'{common_utils.dump_yaml_str(dict(overlaid_client_config))}')
631
+ f'{yaml_utils.dump_yaml_str(dict(overlaid_client_config))}')
630
632
  _set_loaded_config(overlaid_client_config)
631
633
  _set_loaded_config_path([user_config_path, project_config_path])
632
634
 
@@ -736,9 +738,9 @@ def override_skypilot_config(
736
738
  'Failed to override the SkyPilot config on API '
737
739
  'server with your local SkyPilot config:\n'
738
740
  '=== SkyPilot config on API server ===\n'
739
- f'{common_utils.dump_yaml_str(dict(original_config))}\n'
741
+ f'{yaml_utils.dump_yaml_str(dict(original_config))}\n'
740
742
  '=== Your local SkyPilot config ===\n'
741
- f'{common_utils.dump_yaml_str(dict(override_configs))}\n'
743
+ f'{yaml_utils.dump_yaml_str(dict(override_configs))}\n'
742
744
  f'Details: {e}') from e
743
745
  finally:
744
746
  _set_loaded_config(original_config)
@@ -765,7 +767,7 @@ def replace_skypilot_config(new_configs: config_utils.Config) -> Iterator[None]:
765
767
  mode='w',
766
768
  prefix='mutated-skypilot-config-',
767
769
  suffix='.yaml') as temp_file:
768
- common_utils.dump_yaml(temp_file.name, dict(**new_configs))
770
+ yaml_utils.dump_yaml(temp_file.name, dict(**new_configs))
769
771
  # Modify the env var of current process or context so that the
770
772
  # new config will be used by spawned sub-processes.
771
773
  # Note that this code modifies os.environ directly because it
@@ -829,7 +831,7 @@ def apply_cli_config(cli_config: Optional[List[str]]) -> Dict[str, Any]:
829
831
  parsed_config = _compose_cli_config(cli_config)
830
832
  if sky_logging.logging_enabled(logger, sky_logging.DEBUG):
831
833
  logger.debug(f'applying following CLI overrides: \n'
832
- f'{common_utils.dump_yaml_str(dict(parsed_config))}')
834
+ f'{yaml_utils.dump_yaml_str(dict(parsed_config))}')
833
835
  _set_loaded_config(
834
836
  overlay_skypilot_config(original_config=_get_loaded_config(),
835
837
  override_configs=parsed_config))
@@ -873,7 +875,7 @@ def update_api_server_config_no_lock(config: config_utils.Config) -> None:
873
875
  def _set_config_yaml_to_db(key: str,
874
876
  config: config_utils.Config):
875
877
  assert sqlalchemy_engine is not None
876
- config_str = common_utils.dump_yaml_str(dict(config))
878
+ config_str = yaml_utils.dump_yaml_str(dict(config))
877
879
  with orm.Session(sqlalchemy_engine) as session:
878
880
  if (sqlalchemy_engine.dialect.name ==
879
881
  db_utils.SQLAlchemyDialect.SQLITE.value):
@@ -899,7 +901,7 @@ def update_api_server_config_no_lock(config: config_utils.Config) -> None:
899
901
 
900
902
  if not db_updated:
901
903
  # save to the local file (PVC in Kubernetes, local file otherwise)
902
- common_utils.dump_yaml(global_config_path, dict(config))
904
+ yaml_utils.dump_yaml(global_config_path, dict(config))
903
905
 
904
906
  if config_map_utils.is_running_in_kubernetes():
905
907
  # In Kubernetes, sync the PVC config to ConfigMap for user
@@ -5,6 +5,8 @@ from typing import Any, Dict, List
5
5
 
6
6
  import yaml
7
7
 
8
+ from sky.utils import yaml_utils
9
+
8
10
 
9
11
  class SSHNodePoolManager:
10
12
  """Manager for SSH Node Pool configurations."""
@@ -21,7 +23,7 @@ class SSHNodePoolManager:
21
23
 
22
24
  try:
23
25
  with open(self.config_path, 'r', encoding='utf-8') as f:
24
- return yaml.safe_load(f) or {}
26
+ return yaml_utils.safe_load(f) or {}
25
27
  except Exception as e:
26
28
  raise RuntimeError(
27
29
  f'Failed to read SSH Node Pool config: {e}') from e
sky/task.py CHANGED
@@ -4,7 +4,6 @@ import inspect
4
4
  import json
5
5
  import os
6
6
  import re
7
- import typing
8
7
  from typing import (Any, Callable, Dict, Iterable, List, Optional, Set, Tuple,
9
8
  Union)
10
9
 
@@ -15,7 +14,6 @@ from sky import dag as dag_lib
15
14
  from sky import exceptions
16
15
  from sky import resources as resources_lib
17
16
  from sky import sky_logging
18
- from sky.adaptors import common as adaptors_common
19
17
  from sky.data import data_utils
20
18
  from sky.data import storage as storage_lib
21
19
  from sky.provision import docker_utils
@@ -26,11 +24,7 @@ from sky.utils import registry
26
24
  from sky.utils import schemas
27
25
  from sky.utils import ux_utils
28
26
  from sky.utils import volume as volume_lib
29
-
30
- if typing.TYPE_CHECKING:
31
- import yaml
32
- else:
33
- yaml = adaptors_common.LazyImport('yaml')
27
+ from sky.utils import yaml_utils
34
28
 
35
29
  logger = sky_logging.init_logger(__name__)
36
30
 
@@ -570,7 +564,7 @@ class Task:
570
564
  secrets_overrides: Optional[List[Tuple[str, str]]] = None,
571
565
  ) -> 'Task':
572
566
  user_specified_yaml = config.pop('_user_specified_yaml',
573
- common_utils.dump_yaml_str(config))
567
+ yaml_utils.dump_yaml_str(config))
574
568
  # More robust handling for 'envs': explicitly convert keys and values to
575
569
  # str, since users may pass '123' as keys/values which will get parsed
576
570
  # as int causing validate_schema() to fail.
@@ -836,7 +830,7 @@ class Task:
836
830
  # https://github.com/yaml/pyyaml/issues/165#issuecomment-430074049
837
831
  # to raise errors on duplicate keys.
838
832
  user_specified_yaml = f.read()
839
- config = yaml.safe_load(user_specified_yaml)
833
+ config = yaml_utils.safe_load(user_specified_yaml)
840
834
 
841
835
  if isinstance(config, str):
842
836
  with ux_utils.print_exception_no_traceback():
@@ -1611,7 +1605,7 @@ class Task:
1611
1605
  if use_user_specified_yaml:
1612
1606
  if self._user_specified_yaml is None:
1613
1607
  return self._to_yaml_config(redact_secrets=True)
1614
- config = yaml.safe_load(self._user_specified_yaml)
1608
+ config = yaml_utils.safe_load(self._user_specified_yaml)
1615
1609
  if config.get('secrets') is not None:
1616
1610
  config['secrets'] = {k: '<redacted>' for k in config['secrets']}
1617
1611
  return config
@@ -56,15 +56,11 @@ available_node_types:
56
56
  filesystem_mount_path: {{ fs.filesystem_mount_path }}
57
57
  {%- endfor %}
58
58
  UserData: |
59
- runcmd:
60
- - sudo sed -i 's/^#\?AllowTcpForwarding.*/AllowTcpForwarding yes/' /etc/ssh/sshd_config
61
- - systemctl restart sshd
62
-
63
59
  {# Two available OS images:
64
- 1. ubuntu22.04-driverless - requires Docker installation
65
- 2. ubuntu22.04-cuda12 - comes with Docker pre-installed
66
- To optimize deployment speed, Docker is only installed when using ubuntu22.04-driverless #}
67
- {%- if docker_image is not none and image_id == 'ubuntu22.04-driverless' %}
60
+ 1. ubuntu24.04-driverless - requires Docker installation
61
+ 2. ubuntu24.04-cuda12 - comes with Docker pre-installed
62
+ To optimize deployment speed, Docker is only installed when using ubuntu24.04-driverless #}
63
+ {%- if docker_image is not none and image_id.endswith('-driverless') %}
68
64
  apt:
69
65
  sources:
70
66
  docker.list:
sky/usage/usage_lib.py CHANGED
@@ -19,6 +19,7 @@ from sky.usage import constants
19
19
  from sky.utils import common_utils
20
20
  from sky.utils import env_options
21
21
  from sky.utils import ux_utils
22
+ from sky.utils import yaml_utils
22
23
 
23
24
  if typing.TYPE_CHECKING:
24
25
  import inspect
@@ -402,7 +403,7 @@ def _clean_yaml(yaml_info: Dict[str, Optional[str]]):
402
403
  contents = inspect.getsource(contents)
403
404
 
404
405
  if type(contents) in constants.USAGE_MESSAGE_REDACT_TYPES:
405
- lines = common_utils.dump_yaml_str({
406
+ lines = yaml_utils.dump_yaml_str({
406
407
  redact_type: contents
407
408
  }).strip().split('\n')
408
409
  message = (f'{len(lines)} lines {redact_type.upper()}'
@@ -431,7 +432,7 @@ def prepare_json_from_yaml_config(
431
432
  with open(yaml_config_or_path, 'r', encoding='utf-8') as f:
432
433
  lines = f.readlines()
433
434
  comment_lines = [line for line in lines if line.startswith('#')]
434
- yaml_info = common_utils.read_yaml_all(yaml_config_or_path)
435
+ yaml_info = yaml_utils.read_yaml_all(yaml_config_or_path)
435
436
 
436
437
  for i in range(len(yaml_info)):
437
438
  if yaml_info[i] is None:
sky/users/server.py CHANGED
@@ -10,11 +10,11 @@ from typing import Any, Dict, Generator, List
10
10
 
11
11
  import fastapi
12
12
  import filelock
13
- from passlib.hash import apr_md5_crypt
14
13
 
15
14
  from sky import global_user_state
16
15
  from sky import models
17
16
  from sky import sky_logging
17
+ from sky.server import common as server_common
18
18
  from sky.server.requests import payloads
19
19
  from sky.skylet import constants
20
20
  from sky.users import permission
@@ -86,7 +86,7 @@ async def user_create(user_create_body: payloads.UserCreateBody) -> None:
86
86
  role = rbac.get_default_role()
87
87
 
88
88
  # Create user
89
- password_hash = apr_md5_crypt.hash(password)
89
+ password_hash = server_common.crypt_ctx.hash(password)
90
90
  user_hash = hashlib.md5(
91
91
  username.encode()).hexdigest()[:common_utils.USER_HASH_LENGTH]
92
92
  with _user_lock(user_hash):
@@ -146,7 +146,7 @@ async def user_update(request: fastapi.Request,
146
146
 
147
147
  with _user_lock(user_info.id):
148
148
  if password:
149
- password_hash = apr_md5_crypt.hash(password)
149
+ password_hash = server_common.crypt_ctx.hash(password)
150
150
  global_user_state.add_or_update_user(
151
151
  models.User(id=user_info.id,
152
152
  name=user_info.name,
@@ -271,13 +271,13 @@ async def user_import(
271
271
  creation_errors.append(f'{username}: User already exists')
272
272
  continue
273
273
 
274
- # Check if password is already hashed (APR1 hash)
275
- if password.startswith('$apr1$'):
274
+ # Check if password is already hashed
275
+ if server_common.crypt_ctx.identify(password) is not None:
276
276
  # Password is already hashed, use it directly
277
277
  password_hash = password
278
278
  else:
279
279
  # Password is plain text, hash it
280
- password_hash = apr_md5_crypt.hash(password)
280
+ password_hash = server_common.crypt_ctx.hash(password)
281
281
 
282
282
  user_hash = hashlib.md5(
283
283
  username.encode()).hexdigest()[:common_utils.USER_HASH_LENGTH]
sky/utils/common_utils.py CHANGED
@@ -6,7 +6,6 @@ import functools
6
6
  import getpass
7
7
  import hashlib
8
8
  import inspect
9
- import io
10
9
  import os
11
10
  import platform
12
11
  import random
@@ -34,11 +33,9 @@ from sky.utils import validator
34
33
  if typing.TYPE_CHECKING:
35
34
  import jinja2
36
35
  import psutil
37
- import yaml
38
36
  else:
39
37
  jinja2 = adaptors_common.LazyImport('jinja2')
40
38
  psutil = adaptors_common.LazyImport('psutil')
41
- yaml = adaptors_common.LazyImport('yaml')
42
39
 
43
40
  USER_HASH_FILE = os.path.expanduser('~/.sky/user_hash')
44
41
  USER_HASH_LENGTH = 8
@@ -573,74 +570,6 @@ def user_and_hostname_hash() -> str:
573
570
  return f'{getpass.getuser()}-{hostname_hash}'
574
571
 
575
572
 
576
- def read_yaml(path: Optional[str]) -> Dict[str, Any]:
577
- if path is None:
578
- raise ValueError('Attempted to read a None YAML.')
579
- with open(path, 'r', encoding='utf-8') as f:
580
- config = yaml.safe_load(f)
581
- return config
582
-
583
-
584
- def read_yaml_all_str(yaml_str: str) -> List[Dict[str, Any]]:
585
- stream = io.StringIO(yaml_str)
586
- config = yaml.safe_load_all(stream)
587
- configs = list(config)
588
- if not configs:
589
- # Empty YAML file.
590
- return [{}]
591
- return configs
592
-
593
-
594
- def read_yaml_all(path: str) -> List[Dict[str, Any]]:
595
- with open(path, 'r', encoding='utf-8') as f:
596
- return read_yaml_all_str(f.read())
597
-
598
-
599
- def dump_yaml(path: str,
600
- config: Union[List[Dict[str, Any]], Dict[str, Any]],
601
- blank: bool = False) -> None:
602
- """Dumps a YAML file.
603
-
604
- Args:
605
- path: the path to the YAML file.
606
- config: the configuration to dump.
607
- """
608
- with open(path, 'w', encoding='utf-8') as f:
609
- contents = dump_yaml_str(config)
610
- if blank and isinstance(config, dict) and len(config) == 0:
611
- # when dumping to yaml, an empty dict will go in as {}.
612
- contents = ''
613
- f.write(contents)
614
-
615
-
616
- def dump_yaml_str(config: Union[List[Dict[str, Any]], Dict[str, Any]]) -> str:
617
- """Dumps a YAML string.
618
-
619
- Args:
620
- config: the configuration to dump.
621
-
622
- Returns:
623
- The YAML string.
624
- """
625
-
626
- # https://github.com/yaml/pyyaml/issues/127
627
- class LineBreakDumper(yaml.SafeDumper):
628
-
629
- def write_line_break(self, data=None):
630
- super().write_line_break(data)
631
- if len(self.indents) == 1:
632
- super().write_line_break()
633
-
634
- if isinstance(config, list):
635
- dump_func = yaml.dump_all # type: ignore
636
- else:
637
- dump_func = yaml.dump # type: ignore
638
- return dump_func(config,
639
- Dumper=LineBreakDumper,
640
- sort_keys=False,
641
- default_flow_style=False)
642
-
643
-
644
573
  def make_decorator(cls, name_or_fn: Union[str, Callable],
645
574
  **ctx_kwargs) -> Callable:
646
575
  """Make the cls a decorator.