skypilot-nightly 1.0.0.dev20250624__py3-none-any.whl → 1.0.0.dev20250626__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.
Files changed (163) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/kubernetes.py +1 -6
  3. sky/backends/backend_utils.py +26 -11
  4. sky/backends/cloud_vm_ray_backend.py +16 -5
  5. sky/client/cli/command.py +232 -9
  6. sky/client/sdk.py +195 -91
  7. sky/clouds/aws.py +10 -7
  8. sky/clouds/azure.py +10 -7
  9. sky/clouds/cloud.py +2 -0
  10. sky/clouds/cudo.py +2 -0
  11. sky/clouds/do.py +10 -7
  12. sky/clouds/fluidstack.py +2 -0
  13. sky/clouds/gcp.py +10 -7
  14. sky/clouds/hyperbolic.py +10 -7
  15. sky/clouds/ibm.py +2 -0
  16. sky/clouds/kubernetes.py +26 -9
  17. sky/clouds/lambda_cloud.py +10 -7
  18. sky/clouds/nebius.py +10 -7
  19. sky/clouds/oci.py +10 -7
  20. sky/clouds/paperspace.py +10 -7
  21. sky/clouds/runpod.py +10 -7
  22. sky/clouds/scp.py +10 -7
  23. sky/clouds/ssh.py +36 -0
  24. sky/clouds/vast.py +10 -7
  25. sky/clouds/vsphere.py +2 -0
  26. sky/core.py +21 -0
  27. sky/dag.py +14 -0
  28. sky/dashboard/out/404.html +1 -1
  29. sky/dashboard/out/_next/static/bs6UB9V4Jq10TIZ5x-kBK/_buildManifest.js +1 -0
  30. sky/dashboard/out/_next/static/chunks/141-fa5a20cbf401b351.js +11 -0
  31. sky/dashboard/out/_next/static/chunks/230-d6e363362017ff3a.js +1 -0
  32. sky/dashboard/out/_next/static/chunks/25.76c246239df93d50.js +6 -0
  33. sky/dashboard/out/_next/static/chunks/43-36177d00f6956ab2.js +1 -0
  34. sky/dashboard/out/_next/static/chunks/430.ed51037d1a4a438b.js +1 -0
  35. sky/dashboard/out/_next/static/chunks/470-92dd1614396389be.js +1 -0
  36. sky/dashboard/out/_next/static/chunks/544.110e53813fb98e2e.js +1 -0
  37. sky/dashboard/out/_next/static/chunks/645.961f08e39b8ce447.js +1 -0
  38. sky/dashboard/out/_next/static/chunks/690.55f9eed3be903f56.js +16 -0
  39. sky/dashboard/out/_next/static/chunks/697.6460bf72e760addd.js +20 -0
  40. sky/dashboard/out/_next/static/chunks/785.dc2686c3c1235554.js +1 -0
  41. sky/dashboard/out/_next/static/chunks/871-3db673be3ee3750b.js +6 -0
  42. sky/dashboard/out/_next/static/chunks/875.52c962183328b3f2.js +25 -0
  43. sky/dashboard/out/_next/static/chunks/973-81b2d057178adb76.js +1 -0
  44. sky/dashboard/out/_next/static/chunks/982.1b61658204416b0f.js +1 -0
  45. sky/dashboard/out/_next/static/chunks/984.e8bac186a24e5178.js +1 -0
  46. sky/dashboard/out/_next/static/chunks/990-0ad5ea1699e03ee8.js +1 -0
  47. sky/dashboard/out/_next/static/chunks/pages/{_app-ce31493da9747ef4.js → _app-9a3ce3170d2edcec.js} +1 -1
  48. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-aff040d7bc5d0086.js +6 -0
  49. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-8040f2483897ed0c.js +6 -0
  50. sky/dashboard/out/_next/static/chunks/pages/{clusters-7e9736af1c6345a6.js → clusters-f119a5630a1efd61.js} +1 -1
  51. sky/dashboard/out/_next/static/chunks/pages/config-6b255eae088da6a3.js +1 -0
  52. sky/dashboard/out/_next/static/chunks/pages/infra/[context]-b302aea4d65766bf.js +1 -0
  53. sky/dashboard/out/_next/static/chunks/pages/infra-ee8cc4d449945d19.js +1 -0
  54. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-e4b23128db0774cd.js +16 -0
  55. sky/dashboard/out/_next/static/chunks/pages/jobs-0a5695ff3075d94a.js +1 -0
  56. sky/dashboard/out/_next/static/chunks/pages/users-4978cbb093e141e7.js +1 -0
  57. sky/dashboard/out/_next/static/chunks/pages/volumes-476b670ef33d1ecd.js +1 -0
  58. sky/dashboard/out/_next/static/chunks/pages/workspace/{new-31aa8bdcb7592635.js → new-5b59bce9eb208d84.js} +1 -1
  59. sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-cb7e720b739de53a.js +1 -0
  60. sky/dashboard/out/_next/static/chunks/pages/workspaces-50e230828730cfb3.js +1 -0
  61. sky/dashboard/out/_next/static/chunks/webpack-08fdb9e6070127fc.js +1 -0
  62. sky/dashboard/out/_next/static/css/52082cf558ec9705.css +3 -0
  63. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  64. sky/dashboard/out/clusters/[cluster].html +1 -1
  65. sky/dashboard/out/clusters.html +1 -1
  66. sky/dashboard/out/config.html +1 -1
  67. sky/dashboard/out/index.html +1 -1
  68. sky/dashboard/out/infra/[context].html +1 -1
  69. sky/dashboard/out/infra.html +1 -1
  70. sky/dashboard/out/jobs/[job].html +1 -1
  71. sky/dashboard/out/jobs.html +1 -1
  72. sky/dashboard/out/users.html +1 -1
  73. sky/dashboard/out/volumes.html +1 -0
  74. sky/dashboard/out/workspace/new.html +1 -1
  75. sky/dashboard/out/workspaces/[name].html +1 -1
  76. sky/dashboard/out/workspaces.html +1 -1
  77. sky/data/storage_utils.py +2 -4
  78. sky/exceptions.py +15 -0
  79. sky/execution.py +5 -0
  80. sky/global_user_state.py +129 -0
  81. sky/jobs/client/sdk.py +13 -11
  82. sky/jobs/server/core.py +4 -0
  83. sky/models.py +16 -0
  84. sky/provision/__init__.py +26 -0
  85. sky/provision/kubernetes/__init__.py +3 -0
  86. sky/provision/kubernetes/instance.py +38 -77
  87. sky/provision/kubernetes/utils.py +70 -4
  88. sky/provision/kubernetes/volume.py +147 -0
  89. sky/resources.py +20 -76
  90. sky/serve/client/sdk.py +13 -13
  91. sky/serve/server/core.py +5 -1
  92. sky/server/common.py +40 -5
  93. sky/server/constants.py +5 -1
  94. sky/server/metrics.py +105 -0
  95. sky/server/requests/executor.py +30 -14
  96. sky/server/requests/payloads.py +16 -0
  97. sky/server/requests/requests.py +35 -1
  98. sky/server/rest.py +153 -0
  99. sky/server/server.py +70 -43
  100. sky/server/state.py +20 -0
  101. sky/server/stream_utils.py +8 -3
  102. sky/server/uvicorn.py +153 -13
  103. sky/setup_files/dependencies.py +2 -0
  104. sky/skylet/constants.py +19 -3
  105. sky/skypilot_config.py +3 -0
  106. sky/ssh_node_pools/__init__.py +1 -0
  107. sky/ssh_node_pools/core.py +133 -0
  108. sky/ssh_node_pools/server.py +232 -0
  109. sky/task.py +141 -18
  110. sky/templates/kubernetes-ray.yml.j2 +30 -1
  111. sky/users/permission.py +2 -0
  112. sky/utils/context.py +3 -1
  113. sky/utils/kubernetes/deploy_remote_cluster.py +12 -185
  114. sky/utils/kubernetes/ssh_utils.py +221 -0
  115. sky/utils/resources_utils.py +66 -0
  116. sky/utils/rich_utils.py +6 -0
  117. sky/utils/schemas.py +146 -3
  118. sky/utils/status_lib.py +10 -0
  119. sky/utils/validator.py +11 -1
  120. sky/volumes/__init__.py +0 -0
  121. sky/volumes/client/__init__.py +0 -0
  122. sky/volumes/client/sdk.py +64 -0
  123. sky/volumes/server/__init__.py +0 -0
  124. sky/volumes/server/core.py +199 -0
  125. sky/volumes/server/server.py +85 -0
  126. sky/volumes/utils.py +158 -0
  127. sky/volumes/volume.py +198 -0
  128. {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/METADATA +2 -1
  129. {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/RECORD +135 -115
  130. sky/dashboard/out/_next/static/chunks/211.692afc57e812ae1a.js +0 -1
  131. sky/dashboard/out/_next/static/chunks/350.9e123a4551f68b0d.js +0 -1
  132. sky/dashboard/out/_next/static/chunks/37-4650f214e2119168.js +0 -6
  133. sky/dashboard/out/_next/static/chunks/42.2273cc2415291ceb.js +0 -6
  134. sky/dashboard/out/_next/static/chunks/443.b2242d0efcdf5f47.js +0 -1
  135. sky/dashboard/out/_next/static/chunks/470-1494c899266cf5c9.js +0 -1
  136. sky/dashboard/out/_next/static/chunks/513.309df9e18a9ff005.js +0 -1
  137. sky/dashboard/out/_next/static/chunks/641.c8e452bc5070a630.js +0 -1
  138. sky/dashboard/out/_next/static/chunks/682.4dd5dc116f740b5f.js +0 -6
  139. sky/dashboard/out/_next/static/chunks/760-a89d354797ce7af5.js +0 -1
  140. sky/dashboard/out/_next/static/chunks/843-bde186946d353355.js +0 -11
  141. sky/dashboard/out/_next/static/chunks/856-bfddc18e16f3873c.js +0 -1
  142. sky/dashboard/out/_next/static/chunks/901-b424d293275e1fd7.js +0 -1
  143. sky/dashboard/out/_next/static/chunks/973-56412c7976b4655b.js +0 -1
  144. sky/dashboard/out/_next/static/chunks/984.ae8c08791d274ca0.js +0 -50
  145. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-4e065c812a52460b.js +0 -6
  146. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-520ec1ab65e2f2a4.js +0 -6
  147. sky/dashboard/out/_next/static/chunks/pages/config-e4f473661889e7cd.js +0 -1
  148. sky/dashboard/out/_next/static/chunks/pages/infra/[context]-00fd23b9577492ca.js +0 -1
  149. sky/dashboard/out/_next/static/chunks/pages/infra-8a4bf7370d4d9bb7.js +0 -1
  150. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-171c27f4ca94861c.js +0 -16
  151. sky/dashboard/out/_next/static/chunks/pages/jobs-55e5bcb16d563231.js +0 -1
  152. sky/dashboard/out/_next/static/chunks/pages/users-c9f4d785cdaa52d8.js +0 -1
  153. sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-ecc5a7003776cfa7.js +0 -1
  154. sky/dashboard/out/_next/static/chunks/pages/workspaces-f00cba35691483b1.js +0 -1
  155. sky/dashboard/out/_next/static/chunks/webpack-c85998e6a5722f21.js +0 -1
  156. sky/dashboard/out/_next/static/css/6ab927686b492a4a.css +0 -3
  157. sky/dashboard/out/_next/static/zsALxITkbP8J8NVwSDwMo/_buildManifest.js +0 -1
  158. /sky/dashboard/out/_next/static/{zsALxITkbP8J8NVwSDwMo → bs6UB9V4Jq10TIZ5x-kBK}/_ssgManifest.js +0 -0
  159. /sky/dashboard/out/_next/static/chunks/{938-ce7991c156584b06.js → 938-068520cc11738deb.js} +0 -0
  160. {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/WHEEL +0 -0
  161. {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/entry_points.txt +0 -0
  162. {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/licenses/LICENSE +0 -0
  163. {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/top_level.txt +0 -0
sky/__init__.py CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  import urllib.request
6
6
 
7
7
  # Replaced with the current commit when building the wheels.
8
- _SKYPILOT_COMMIT_SHA = '0b29731d6bfe91381de57805e1fc1dd723e4b3a0'
8
+ _SKYPILOT_COMMIT_SHA = '512c53be5771c003cd9d94b5b89c23638a5e735e'
9
9
 
10
10
 
11
11
  def _get_git_commit():
@@ -35,7 +35,7 @@ def _get_git_commit():
35
35
 
36
36
 
37
37
  __commit__ = _get_git_commit()
38
- __version__ = '1.0.0.dev20250624'
38
+ __version__ = '1.0.0.dev20250626'
39
39
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
40
40
 
41
41
 
@@ -133,12 +133,7 @@ def _load_config(context: Optional[str] = None):
133
133
  '\nHint: Kubernetes attempted to query the current-context '
134
134
  'set in kubeconfig. Check if the current-context is valid.')
135
135
  with ux_utils.print_exception_no_traceback():
136
- if is_ssh_node_pool:
137
- # For SSH Node Pool, we don't want to surface k8s errors
138
- # (e.g., missing context) unless debug flag is set.
139
- logging.debug(f'Kubernetes error: {suffix}')
140
- else:
141
- raise ValueError(err_str) from None
136
+ raise ValueError(err_str) from None
142
137
 
143
138
  if context == in_cluster_context_name() or context is None:
144
139
  try:
@@ -66,6 +66,7 @@ if typing.TYPE_CHECKING:
66
66
  from sky import task as task_lib
67
67
  from sky.backends import cloud_vm_ray_backend
68
68
  from sky.backends import local_docker_backend
69
+ from sky.volumes import volume as volume_lib
69
70
  else:
70
71
  yaml = adaptors_common.LazyImport('yaml')
71
72
  requests = adaptors_common.LazyImport('requests')
@@ -541,16 +542,18 @@ def get_expirable_clouds(
541
542
  # TODO: too many things happening here - leaky abstraction. Refactor.
542
543
  @timeline.event
543
544
  def write_cluster_config(
544
- to_provision: 'resources_lib.Resources',
545
- num_nodes: int,
546
- cluster_config_template: str,
547
- cluster_name: str,
548
- local_wheel_path: pathlib.Path,
549
- wheel_hash: str,
550
- region: clouds.Region,
551
- zones: Optional[List[clouds.Zone]] = None,
552
- dryrun: bool = False,
553
- keep_launch_fields_in_existing_config: bool = True) -> Dict[str, str]:
545
+ to_provision: 'resources_lib.Resources',
546
+ num_nodes: int,
547
+ cluster_config_template: str,
548
+ cluster_name: str,
549
+ local_wheel_path: pathlib.Path,
550
+ wheel_hash: str,
551
+ region: clouds.Region,
552
+ zones: Optional[List[clouds.Zone]] = None,
553
+ dryrun: bool = False,
554
+ keep_launch_fields_in_existing_config: bool = True,
555
+ volume_mounts: Optional[List['volume_lib.VolumeMount']] = None,
556
+ ) -> Dict[str, str]:
554
557
  """Fills in cluster configuration templates and writes them out.
555
558
 
556
559
  Returns:
@@ -598,7 +601,7 @@ def write_cluster_config(
598
601
  resources_utils.ClusterName(
599
602
  cluster_name,
600
603
  cluster_name_on_cloud,
601
- ), region, zones, num_nodes, dryrun)
604
+ ), region, zones, num_nodes, dryrun, volume_mounts)
602
605
  config_dict = {}
603
606
 
604
607
  specific_reservations = set(
@@ -731,6 +734,15 @@ def write_cluster_config(
731
734
  high_availability_specified = controller_utils.high_availability_specified(
732
735
  cluster_name)
733
736
 
737
+ volume_mount_vars = []
738
+ if volume_mounts is not None:
739
+ for vol in volume_mounts:
740
+ volume_mount_vars.append({
741
+ 'name': vol.volume_name,
742
+ 'path': vol.path,
743
+ 'volume_name_on_cloud': vol.volume_config.name_on_cloud,
744
+ })
745
+
734
746
  # Use a tmp file path to avoid incomplete YAML file being re-used in the
735
747
  # future.
736
748
  tmp_yaml_path = yaml_path + '.tmp'
@@ -821,6 +833,9 @@ def write_cluster_config(
821
833
 
822
834
  # High availability
823
835
  'high_availability': high_availability_specified,
836
+
837
+ # Volume mounts
838
+ 'volume_mounts': volume_mount_vars,
824
839
  }),
825
840
  output_path=tmp_yaml_path)
826
841
  config_dict['cluster_name'] = cluster_name
@@ -73,6 +73,7 @@ from sky.utils import status_lib
73
73
  from sky.utils import subprocess_utils
74
74
  from sky.utils import timeline
75
75
  from sky.utils import ux_utils
76
+ from sky.volumes import volume as volume_lib
76
77
 
77
78
  if typing.TYPE_CHECKING:
78
79
  from sky import dag
@@ -1327,6 +1328,7 @@ class RetryingVmProvisioner(object):
1327
1328
  prev_handle: Optional['CloudVmRayResourceHandle'],
1328
1329
  prev_cluster_ever_up: bool,
1329
1330
  skip_if_config_hash_matches: Optional[str],
1331
+ volume_mounts: Optional[List[volume_lib.VolumeMount]],
1330
1332
  ) -> Dict[str, Any]:
1331
1333
  """The provision retry loop.
1332
1334
 
@@ -1432,7 +1434,9 @@ class RetryingVmProvisioner(object):
1432
1434
  region=region,
1433
1435
  zones=zones,
1434
1436
  dryrun=dryrun,
1435
- keep_launch_fields_in_existing_config=cluster_exists)
1437
+ keep_launch_fields_in_existing_config=cluster_exists,
1438
+ volume_mounts=volume_mounts,
1439
+ )
1436
1440
  except exceptions.ResourcesUnavailableError as e:
1437
1441
  # Failed due to catalog issue, e.g. image not found, or
1438
1442
  # GPUs are requested in a Kubernetes cluster but the cluster
@@ -2081,7 +2085,9 @@ class RetryingVmProvisioner(object):
2081
2085
  prev_cluster_status=prev_cluster_status,
2082
2086
  prev_handle=prev_handle,
2083
2087
  prev_cluster_ever_up=prev_cluster_ever_up,
2084
- skip_if_config_hash_matches=skip_if_config_hash_matches)
2088
+ skip_if_config_hash_matches=skip_if_config_hash_matches,
2089
+ volume_mounts=task.volume_mounts,
2090
+ )
2085
2091
  if dryrun:
2086
2092
  return config_dict
2087
2093
  except (exceptions.InvalidClusterNameError,
@@ -2435,9 +2441,14 @@ class CloudVmRayResourceHandle(backends.backend.ResourceHandle):
2435
2441
  zip(cluster_internal_ips, cluster_feasible_ips))
2436
2442
 
2437
2443
  # Ensure head node is the first element, then sort based on the
2438
- # external IPs for stableness
2439
- stable_internal_external_ips = [internal_external_ips[0]] + sorted(
2440
- internal_external_ips[1:], key=lambda x: x[1])
2444
+ # external IPs for stableness. Skip for k8s nodes since pods
2445
+ # worker ids are already mapped.
2446
+ if (cluster_info is not None and
2447
+ cluster_info.provider_name == 'kubernetes'):
2448
+ stable_internal_external_ips = internal_external_ips
2449
+ else:
2450
+ stable_internal_external_ips = [internal_external_ips[0]] + sorted(
2451
+ internal_external_ips[1:], key=lambda x: x[1])
2441
2452
  self.stable_internal_external_ips = stable_internal_external_ips
2442
2453
 
2443
2454
  @context_utils.cancellation_guard
sky/client/cli/command.py CHANGED
@@ -81,6 +81,8 @@ from sky.utils import subprocess_utils
81
81
  from sky.utils import timeline
82
82
  from sky.utils import ux_utils
83
83
  from sky.utils.cli_utils import status_utils
84
+ from sky.volumes import utils as volumes_utils
85
+ from sky.volumes.client import sdk as volumes_sdk
84
86
 
85
87
  if typing.TYPE_CHECKING:
86
88
  import types
@@ -202,13 +204,14 @@ def _get_cluster_records_and_set_ssh_config(
202
204
 
203
205
 
204
206
  def _get_glob_matches(candidate_names: List[str],
205
- glob_patterns: List[str]) -> List[str]:
207
+ glob_patterns: List[str],
208
+ resource_type: str = 'Storage') -> List[str]:
206
209
  """Returns a list of names that match the glob pattern."""
207
210
  glob_storages = []
208
211
  for glob_pattern in glob_patterns:
209
212
  glob_storage = fnmatch.filter(candidate_names, glob_pattern)
210
213
  if not glob_storage:
211
- click.echo(f'Storage {glob_pattern} not found.')
214
+ click.echo(f'{resource_type} {glob_pattern} not found.')
212
215
  glob_storages.extend(glob_storage)
213
216
  return list(set(glob_storages))
214
217
 
@@ -290,6 +293,19 @@ def _complete_storage_name(ctx: click.Context, param: click.Parameter,
290
293
  return response.json()
291
294
 
292
295
 
296
+ def _complete_volume_name(ctx: click.Context, param: click.Parameter,
297
+ incomplete: str) -> List[str]:
298
+ """Handle shell completion for volume names."""
299
+ del ctx, param # Unused.
300
+ response = requests_lib.get(
301
+ f'{server_common.get_server_url()}'
302
+ f'/api/completion/volume_name?incomplete={incomplete}',
303
+ timeout=2.0,
304
+ )
305
+ response.raise_for_status()
306
+ return response.json()
307
+
308
+
293
309
  def _complete_file_name(ctx: click.Context, param: click.Parameter,
294
310
  incomplete: str) -> List[str]:
295
311
  """Handle shell completion for file names.
@@ -531,8 +547,9 @@ def _parse_override_params(
531
547
  return override_params
532
548
 
533
549
 
534
- def _check_yaml(entrypoint: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
535
- """Checks if entrypoint is a readable YAML file.
550
+ def _check_yaml_only(
551
+ entrypoint: str) -> Tuple[bool, Optional[Dict[str, Any]], bool, str]:
552
+ """Checks if entrypoint is a readable YAML file without confirmation.
536
553
 
537
554
  Args:
538
555
  entrypoint: Path to a YAML file.
@@ -580,6 +597,17 @@ def _check_yaml(entrypoint: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
580
597
  invalid_reason = ('yaml.safe_load() failed. Please check if the'
581
598
  ' path is correct.')
582
599
  is_yaml = False
600
+ return is_yaml, result, yaml_file_provided, invalid_reason
601
+
602
+
603
+ def _check_yaml(entrypoint: str) -> Tuple[bool, Optional[Dict[str, Any]]]:
604
+ """Checks if entrypoint is a readable YAML file.
605
+
606
+ Args:
607
+ entrypoint: Path to a YAML file.
608
+ """
609
+ is_yaml, result, yaml_file_provided, invalid_reason = _check_yaml_only(
610
+ entrypoint)
583
611
  if not is_yaml:
584
612
  if yaml_file_provided:
585
613
  click.confirm(
@@ -3767,6 +3795,196 @@ def storage_delete(names: List[str], all: bool, yes: bool, async_call: bool): #
3767
3795
  f'{colorama.Style.RESET_ALL}')
3768
3796
 
3769
3797
 
3798
+ @cli.group(cls=_NaturalOrderGroup)
3799
+ def volumes():
3800
+ """SkyPilot Volumes CLI."""
3801
+ pass
3802
+
3803
+
3804
+ @volumes.command('apply', cls=_DocumentedCodeCommand)
3805
+ @flags.config_option(expose_value=False)
3806
+ @click.argument('entrypoint',
3807
+ required=False,
3808
+ type=str,
3809
+ nargs=-1,
3810
+ **_get_shell_complete_args(_complete_file_name))
3811
+ @click.option('--name',
3812
+ '-n',
3813
+ required=False,
3814
+ type=str,
3815
+ help='Volume name. Override the name defined in the YAML.')
3816
+ @click.option('--infra',
3817
+ required=False,
3818
+ type=str,
3819
+ help='Infra. Format: k8s, k8s/context-name. '
3820
+ 'Override the infra defined in the YAML.')
3821
+ @click.option(
3822
+ '--type',
3823
+ required=False,
3824
+ type=str,
3825
+ help='Volume type. Format: pvc. Override the type defined in the YAML.')
3826
+ @click.option('--size',
3827
+ required=False,
3828
+ type=str,
3829
+ help='Volume size. Override the size defined in the YAML.')
3830
+ @click.option('--yes',
3831
+ '-y',
3832
+ is_flag=True,
3833
+ default=False,
3834
+ required=False,
3835
+ help='Skip confirmation prompt.')
3836
+ @_add_click_options(flags.COMMON_OPTIONS)
3837
+ @usage_lib.entrypoint
3838
+ def volumes_apply(
3839
+ entrypoint: Optional[Tuple[str, ...]],
3840
+ name: Optional[str],
3841
+ infra: Optional[str],
3842
+ type: Optional[str], # pylint: disable=redefined-builtin
3843
+ size: Optional[str],
3844
+ yes: bool,
3845
+ async_call: bool):
3846
+ """Apply a volume.
3847
+
3848
+ Examples:
3849
+
3850
+ .. code-block:: bash
3851
+
3852
+ # Apply a volume from a YAML file.
3853
+ sky volumes apply volume.yaml
3854
+ \b
3855
+ # Apply a volume from a command.
3856
+ sky volumes apply --name pvc1 --infra k8s --type pvc --size 100Gi
3857
+ """
3858
+ # pylint: disable=import-outside-toplevel
3859
+ from sky.volumes import volume as volume_lib
3860
+
3861
+ volume_config_dict: Dict[str, Any] = {}
3862
+ if entrypoint is not None and len(entrypoint) > 0:
3863
+ entrypoint_str = ' '.join(entrypoint)
3864
+ is_yaml, yaml_config, yaml_file_provided, invalid_reason = (
3865
+ _check_yaml_only(entrypoint_str))
3866
+ if not is_yaml:
3867
+ if yaml_file_provided:
3868
+ raise click.BadParameter(f'{entrypoint_str!r} looks like a '
3869
+ f'yaml path but {invalid_reason}')
3870
+ else:
3871
+ raise click.BadParameter(
3872
+ f'{entrypoint_str!r} needs to be a YAML file')
3873
+ if yaml_config is not None:
3874
+ volume_config_dict = yaml_config.copy()
3875
+
3876
+ # Create Volume instance
3877
+ volume = volume_lib.Volume.from_dict(volume_config_dict)
3878
+
3879
+ # Normalize the volume config with CLI options
3880
+ volume.normalize_config(name=name, infra=infra, type=type, size=size)
3881
+
3882
+ logger.debug(f'Volume config: {volume.to_dict()}')
3883
+
3884
+ if not yes:
3885
+ click.confirm(f'Proceed to create volume {volume.name!r}?',
3886
+ default=True,
3887
+ abort=True,
3888
+ show_default=True)
3889
+
3890
+ # Call SDK to create volume
3891
+ try:
3892
+ request_id = volumes_sdk.apply(volume)
3893
+ _async_call_or_wait(request_id, async_call, 'sky.volumes.apply')
3894
+ except RuntimeError as e:
3895
+ logger.error(f'{colorama.Fore.RED}Error applying volume: '
3896
+ f'{common_utils.format_exception(e, use_bracket=True)}'
3897
+ f'{colorama.Style.RESET_ALL}')
3898
+
3899
+
3900
+ @volumes.command('ls', cls=_DocumentedCodeCommand)
3901
+ @flags.config_option(expose_value=False)
3902
+ @click.option('--verbose',
3903
+ '-v',
3904
+ default=False,
3905
+ is_flag=True,
3906
+ required=False,
3907
+ help='Show all information in full.')
3908
+ @usage_lib.entrypoint
3909
+ def volumes_ls(verbose: bool):
3910
+ """List volumes managed by SkyPilot."""
3911
+ request_id = volumes_sdk.ls()
3912
+ all_volumes = sdk.stream_and_get(request_id)
3913
+ volume_table = volumes_utils.format_volume_table(all_volumes,
3914
+ show_all=verbose)
3915
+ click.echo(volume_table)
3916
+
3917
+
3918
+ @volumes.command('delete', cls=_DocumentedCodeCommand)
3919
+ @flags.config_option(expose_value=False)
3920
+ @click.argument('names',
3921
+ required=False,
3922
+ type=str,
3923
+ nargs=-1,
3924
+ **_get_shell_complete_args(_complete_volume_name))
3925
+ @click.option('--all',
3926
+ '-a',
3927
+ default=False,
3928
+ is_flag=True,
3929
+ required=False,
3930
+ help='Delete all volumes.')
3931
+ @click.option('--yes',
3932
+ '-y',
3933
+ default=False,
3934
+ is_flag=True,
3935
+ required=False,
3936
+ help='Skip confirmation prompt.')
3937
+ @_add_click_options(flags.COMMON_OPTIONS)
3938
+ @usage_lib.entrypoint
3939
+ def volumes_delete(names: List[str], all: bool, yes: bool, async_call: bool): # pylint: disable=redefined-builtin
3940
+ """Delete volumes.
3941
+
3942
+ Examples:
3943
+
3944
+ .. code-block:: bash
3945
+
3946
+ # Delete two volumes.
3947
+ sky volumes delete pvc1 pvc2
3948
+ \b
3949
+ # Delete all volumes matching glob pattern 'pvc*'.
3950
+ sky volumes delete "pvc*"
3951
+ \b
3952
+ # Delete all volumes.
3953
+ sky volumes delete -a
3954
+ """
3955
+ if sum([bool(names), all]) != 1:
3956
+ raise click.UsageError('Either --all or a name must be specified.')
3957
+ all_volumes = sdk.get(volumes_sdk.ls())
3958
+ if all:
3959
+ if not all_volumes:
3960
+ click.echo('No volumes to delete.')
3961
+ return
3962
+ names = [volume['name'] for volume in all_volumes]
3963
+ else:
3964
+ existing_volume_names = [volume['name'] for volume in all_volumes]
3965
+ names = _get_glob_matches(existing_volume_names,
3966
+ names,
3967
+ resource_type='Volume')
3968
+ if names:
3969
+ if not yes:
3970
+ volume_names = ', '.join(names)
3971
+ volume_str = 'volumes' if len(names) > 1 else 'volume'
3972
+ click.confirm(
3973
+ f'Deleting {len(names)} {volume_str}: '
3974
+ f'{volume_names}. Proceed?',
3975
+ default=True,
3976
+ abort=True,
3977
+ show_default=True)
3978
+
3979
+ try:
3980
+ _async_call_or_wait(volumes_sdk.delete(names), async_call,
3981
+ 'sky.volumes.delete')
3982
+ except Exception as e: # pylint: disable=broad-except
3983
+ logger.error(f'{colorama.Fore.RED}Error deleting volumes {names}: '
3984
+ f'{common_utils.format_exception(e, use_bracket=True)}'
3985
+ f'{colorama.Style.RESET_ALL}')
3986
+
3987
+
3770
3988
  @cli.group(cls=_NaturalOrderGroup)
3771
3989
  def jobs():
3772
3990
  """Managed Jobs CLI (jobs with auto-recovery)."""
@@ -5186,13 +5404,18 @@ def ssh():
5186
5404
  is_flag=True,
5187
5405
  hidden=True,
5188
5406
  help='Run the command asynchronously.')
5189
- def ssh_up(infra: Optional[str], async_call: bool):
5190
- """Set up a cluster using SSH targets from ~/.sky/ssh_node_pools.yaml.
5407
+ @click.option('--file',
5408
+ '-f',
5409
+ required=False,
5410
+ help='The file containing the SSH targets.')
5411
+ def ssh_up(infra: Optional[str], async_call: bool, file: Optional[str]):
5412
+ """Set up a cluster using SSH targets from a file. If not specified,
5413
+ ~/.sky/ssh_node_pools.yaml will be used.
5191
5414
 
5192
- This command sets up a Kubernetes cluster on the machines specified in
5193
- ~/.sky/ssh_node_pools.yaml and configures SkyPilot to use it.
5415
+ This command sets up a Kubernetes cluster on the machines specified in the
5416
+ config file and configures SkyPilot to use it.
5194
5417
  """
5195
- request_id = sdk.ssh_up(infra=infra)
5418
+ request_id = sdk.ssh_up(infra=infra, file=file)
5196
5419
  if async_call:
5197
5420
  print(f'Request submitted with ID: {request_id}')
5198
5421
  else: