skypilot-nightly 1.0.0.dev20250927__py3-none-any.whl → 1.0.0.dev20251002__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 (54) hide show
  1. sky/__init__.py +2 -2
  2. sky/backends/backend_utils.py +18 -10
  3. sky/backends/cloud_vm_ray_backend.py +2 -2
  4. sky/check.py +0 -29
  5. sky/client/cli/command.py +48 -28
  6. sky/client/cli/table_utils.py +279 -1
  7. sky/client/sdk.py +7 -18
  8. sky/core.py +15 -16
  9. sky/dashboard/out/404.html +1 -1
  10. sky/dashboard/out/_next/static/{UDSEoDB67vwFMZyCJ4HWU → 16g0-hgEgk6Db72hpE8MY}/_buildManifest.js +1 -1
  11. sky/dashboard/out/_next/static/chunks/pages/jobs/pools/{[pool]-07349868f7905d37.js → [pool]-509b2977a6373bf6.js} +1 -1
  12. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  13. sky/dashboard/out/clusters/[cluster].html +1 -1
  14. sky/dashboard/out/clusters.html +1 -1
  15. sky/dashboard/out/config.html +1 -1
  16. sky/dashboard/out/index.html +1 -1
  17. sky/dashboard/out/infra/[context].html +1 -1
  18. sky/dashboard/out/infra.html +1 -1
  19. sky/dashboard/out/jobs/[job].html +1 -1
  20. sky/dashboard/out/jobs/pools/[pool].html +1 -1
  21. sky/dashboard/out/jobs.html +1 -1
  22. sky/dashboard/out/users.html +1 -1
  23. sky/dashboard/out/volumes.html +1 -1
  24. sky/dashboard/out/workspace/new.html +1 -1
  25. sky/dashboard/out/workspaces/[name].html +1 -1
  26. sky/dashboard/out/workspaces.html +1 -1
  27. sky/data/storage.py +11 -0
  28. sky/data/storage_utils.py +1 -45
  29. sky/jobs/client/sdk.py +3 -2
  30. sky/jobs/controller.py +15 -0
  31. sky/jobs/server/core.py +24 -2
  32. sky/jobs/server/server.py +1 -1
  33. sky/jobs/utils.py +2 -1
  34. sky/provision/kubernetes/instance.py +1 -1
  35. sky/provision/kubernetes/utils.py +50 -28
  36. sky/schemas/api/responses.py +76 -0
  37. sky/server/common.py +2 -1
  38. sky/server/requests/serializers/decoders.py +16 -4
  39. sky/server/requests/serializers/encoders.py +12 -5
  40. sky/task.py +4 -0
  41. sky/utils/cluster_utils.py +23 -5
  42. sky/utils/command_runner.py +21 -5
  43. sky/utils/command_runner.pyi +11 -0
  44. sky/utils/volume.py +5 -0
  45. sky/volumes/client/sdk.py +3 -2
  46. sky/volumes/server/core.py +3 -2
  47. {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/METADATA +33 -33
  48. {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/RECORD +53 -54
  49. sky/volumes/utils.py +0 -224
  50. /sky/dashboard/out/_next/static/{UDSEoDB67vwFMZyCJ4HWU → 16g0-hgEgk6Db72hpE8MY}/_ssgManifest.js +0 -0
  51. {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/WHEEL +0 -0
  52. {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/entry_points.txt +0 -0
  53. {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/licenses/LICENSE +0 -0
  54. {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/top_level.txt +0 -0
sky/__init__.py CHANGED
@@ -7,7 +7,7 @@ import urllib.request
7
7
  from sky.utils import directory_utils
8
8
 
9
9
  # Replaced with the current commit when building the wheels.
10
- _SKYPILOT_COMMIT_SHA = 'e42224b6d29bd960c0e0daa69add0fe2ad695142'
10
+ _SKYPILOT_COMMIT_SHA = 'e92268dca4331cab08759798deab9265e753cba8'
11
11
 
12
12
 
13
13
  def _get_git_commit():
@@ -37,7 +37,7 @@ def _get_git_commit():
37
37
 
38
38
 
39
39
  __commit__ = _get_git_commit()
40
- __version__ = '1.0.0.dev20250927'
40
+ __version__ = '1.0.0.dev20251002'
41
41
  __root_dir__ = directory_utils.get_sky_dir()
42
42
 
43
43
 
@@ -723,11 +723,15 @@ def write_cluster_config(
723
723
  'is not supported by this cloud. Remove the config or set: '
724
724
  '`remote_identity: LOCAL_CREDENTIALS`.')
725
725
  if isinstance(cloud, clouds.Kubernetes):
726
- if skypilot_config.get_effective_region_config(
726
+ allowed_contexts = skypilot_config.get_workspace_cloud(
727
+ 'kubernetes').get('allowed_contexts', None)
728
+ if allowed_contexts is None:
729
+ allowed_contexts = skypilot_config.get_effective_region_config(
727
730
  cloud='kubernetes',
728
731
  region=None,
729
732
  keys=('allowed_contexts',),
730
- default_value=None) is None:
733
+ default_value=None)
734
+ if allowed_contexts is None:
731
735
  excluded_clouds.add(cloud)
732
736
  else:
733
737
  excluded_clouds.add(cloud)
@@ -2613,7 +2617,7 @@ def refresh_cluster_record(
2613
2617
  cluster_name: str,
2614
2618
  *,
2615
2619
  force_refresh_statuses: Optional[Set[status_lib.ClusterStatus]] = None,
2616
- acquire_per_cluster_status_lock: bool = True,
2620
+ cluster_lock_already_held: bool = False,
2617
2621
  cluster_status_lock_timeout: int = CLUSTER_STATUS_LOCK_TIMEOUT_SECONDS,
2618
2622
  include_user_info: bool = True,
2619
2623
  summary_response: bool = False) -> Optional[Dict[str, Any]]:
@@ -2633,9 +2637,13 @@ def refresh_cluster_record(
2633
2637
  _CLUSTER_STATUS_CACHE_DURATION_SECONDS old, and one of:
2634
2638
  1. the cluster is a spot cluster, or
2635
2639
  2. cluster autostop is set and the cluster is not STOPPED.
2636
- acquire_per_cluster_status_lock: Whether to acquire the per-cluster lock
2637
- before updating the status. Even if this is True, the lock may not be
2638
- acquired if the status does not need to be refreshed.
2640
+ cluster_lock_already_held: Whether the caller is already holding the
2641
+ per-cluster lock. You MUST NOT set this to True if the caller does not
2642
+ already hold the lock. If True, we will not acquire the lock before
2643
+ updating the status. Failing to hold the lock while updating the
2644
+ status can lead to correctness issues - e.g. an launch in-progress may
2645
+ appear to be DOWN incorrectly. Even if this is set to False, the lock
2646
+ may not be acquired if the status does not need to be refreshed.
2639
2647
  cluster_status_lock_timeout: The timeout to acquire the per-cluster
2640
2648
  lock. If timeout, the function will use the cached status. If the
2641
2649
  value is <0, do not timeout (wait for the lock indefinitely). By
@@ -2686,7 +2694,7 @@ def refresh_cluster_record(
2686
2694
  if not _must_refresh_cluster_status(record, force_refresh_statuses):
2687
2695
  return record
2688
2696
 
2689
- if not acquire_per_cluster_status_lock:
2697
+ if cluster_lock_already_held:
2690
2698
  return _update_cluster_status(cluster_name, include_user_info,
2691
2699
  summary_response)
2692
2700
 
@@ -2740,7 +2748,7 @@ def refresh_cluster_status_handle(
2740
2748
  cluster_name: str,
2741
2749
  *,
2742
2750
  force_refresh_statuses: Optional[Set[status_lib.ClusterStatus]] = None,
2743
- acquire_per_cluster_status_lock: bool = True,
2751
+ cluster_lock_already_held: bool = False,
2744
2752
  cluster_status_lock_timeout: int = CLUSTER_STATUS_LOCK_TIMEOUT_SECONDS
2745
2753
  ) -> Tuple[Optional[status_lib.ClusterStatus],
2746
2754
  Optional[backends.ResourceHandle]]:
@@ -2753,7 +2761,7 @@ def refresh_cluster_status_handle(
2753
2761
  record = refresh_cluster_record(
2754
2762
  cluster_name,
2755
2763
  force_refresh_statuses=force_refresh_statuses,
2756
- acquire_per_cluster_status_lock=acquire_per_cluster_status_lock,
2764
+ cluster_lock_already_held=cluster_lock_already_held,
2757
2765
  cluster_status_lock_timeout=cluster_status_lock_timeout,
2758
2766
  include_user_info=False,
2759
2767
  summary_response=True)
@@ -3078,7 +3086,7 @@ def _refresh_cluster(
3078
3086
  record = refresh_cluster_record(
3079
3087
  cluster_name,
3080
3088
  force_refresh_statuses=force_refresh_statuses,
3081
- acquire_per_cluster_status_lock=True,
3089
+ cluster_lock_already_held=False,
3082
3090
  include_user_info=include_user_info,
3083
3091
  summary_response=summary_response)
3084
3092
  except (exceptions.ClusterStatusFetchingError,
@@ -5121,7 +5121,7 @@ class CloudVmRayBackend(backends.Backend['CloudVmRayResourceHandle']):
5121
5121
  # observed in AWS. See also
5122
5122
  # _LAUNCH_DOUBLE_CHECK_WINDOW in backend_utils.py.
5123
5123
  force_refresh_statuses={status_lib.ClusterStatus.INIT},
5124
- acquire_per_cluster_status_lock=False))
5124
+ cluster_lock_already_held=True))
5125
5125
  cluster_status_fetched = True
5126
5126
  except exceptions.ClusterStatusFetchingError:
5127
5127
  logger.warning(
@@ -5725,7 +5725,7 @@ class CloudVmRayBackend(backends.Backend['CloudVmRayResourceHandle']):
5725
5725
  record = backend_utils.refresh_cluster_record(
5726
5726
  cluster_name,
5727
5727
  force_refresh_statuses={status_lib.ClusterStatus.INIT},
5728
- acquire_per_cluster_status_lock=False,
5728
+ cluster_lock_already_held=True,
5729
5729
  include_user_info=False,
5730
5730
  summary_response=True,
5731
5731
  )
sky/check.py CHANGED
@@ -621,35 +621,6 @@ def _format_enabled_cloud(cloud_name: str,
621
621
  if cloud_name in [repr(sky_clouds.Kubernetes()), repr(sky_clouds.SSH())]:
622
622
  return (f'{title}' + _format_context_details(
623
623
  cloud_name, show_details=False, ctx2text=ctx2text))
624
-
625
- if cloud_name == repr(sky_clouds.Kubernetes()):
626
- # Get enabled contexts for Kubernetes
627
- existing_contexts = sky_clouds.Kubernetes.existing_allowed_contexts()
628
- if not existing_contexts:
629
- return _green_color(cloud_and_capabilities)
630
-
631
- # Check if allowed_contexts is explicitly set in config
632
- allowed_contexts = skypilot_config.get_effective_region_config(
633
- cloud='kubernetes',
634
- region=None,
635
- keys=('allowed_contexts',),
636
- default_value=None)
637
-
638
- # Format the context info with consistent styling
639
- if allowed_contexts is not None:
640
- contexts_formatted = []
641
- for i, context in enumerate(existing_contexts):
642
- symbol = (ux_utils.INDENT_LAST_SYMBOL
643
- if i == len(existing_contexts) -
644
- 1 else ux_utils.INDENT_SYMBOL)
645
- contexts_formatted.append(f'\n {symbol}{context}')
646
- context_info = f' Allowed contexts:{"".join(contexts_formatted)}'
647
- else:
648
- context_info = f' Active context: {existing_contexts[0]}'
649
-
650
- return (f'{_green_color(cloud_and_capabilities)}\n'
651
- f' {colorama.Style.DIM}{context_info}'
652
- f'{colorama.Style.RESET_ALL}')
653
624
  return _green_color(cloud_and_capabilities)
654
625
 
655
626
 
sky/client/cli/command.py CHANGED
@@ -60,7 +60,6 @@ from sky.adaptors import common as adaptors_common
60
60
  from sky.client import sdk
61
61
  from sky.client.cli import flags
62
62
  from sky.client.cli import table_utils
63
- from sky.data import storage_utils
64
63
  from sky.provision.kubernetes import constants as kubernetes_constants
65
64
  from sky.provision.kubernetes import utils as kubernetes_utils
66
65
  from sky.schemas.api import responses
@@ -88,9 +87,9 @@ from sky.utils import status_lib
88
87
  from sky.utils import subprocess_utils
89
88
  from sky.utils import timeline
90
89
  from sky.utils import ux_utils
90
+ from sky.utils import volume as volume_utils
91
91
  from sky.utils import yaml_utils
92
92
  from sky.utils.cli_utils import status_utils
93
- from sky.volumes import utils as volumes_utils
94
93
  from sky.volumes.client import sdk as volumes_sdk
95
94
 
96
95
  if typing.TYPE_CHECKING:
@@ -1322,7 +1321,7 @@ def exec(
1322
1321
 
1323
1322
 
1324
1323
  def _handle_jobs_queue_request(
1325
- request_id: server_common.RequestId[List[Dict[str, Any]]],
1324
+ request_id: server_common.RequestId[List[responses.ManagedJobRecord]],
1326
1325
  show_all: bool,
1327
1326
  show_user: bool,
1328
1327
  max_num_jobs_to_show: Optional[int],
@@ -1395,10 +1394,10 @@ def _handle_jobs_queue_request(
1395
1394
  msg += ('Failed to query managed jobs: '
1396
1395
  f'{common_utils.format_exception(e, use_bracket=True)}')
1397
1396
  else:
1398
- msg = managed_jobs.format_job_table(managed_jobs_,
1399
- show_all=show_all,
1400
- show_user=show_user,
1401
- max_jobs=max_num_jobs_to_show)
1397
+ msg = table_utils.format_job_table(managed_jobs_,
1398
+ show_all=show_all,
1399
+ show_user=show_user,
1400
+ max_jobs=max_num_jobs_to_show)
1402
1401
  return num_in_progress_jobs, msg
1403
1402
 
1404
1403
 
@@ -1513,9 +1512,9 @@ def _status_kubernetes(show_all: bool):
1513
1512
  click.echo(f'\n{colorama.Fore.CYAN}{colorama.Style.BRIGHT}'
1514
1513
  f'Managed jobs'
1515
1514
  f'{colorama.Style.RESET_ALL}')
1516
- msg = managed_jobs.format_job_table(all_jobs,
1517
- show_all=show_all,
1518
- show_user=False)
1515
+ msg = table_utils.format_job_table(all_jobs,
1516
+ show_all=show_all,
1517
+ show_user=False)
1519
1518
  click.echo(msg)
1520
1519
  if any(['sky-serve-controller' in c.cluster_name for c in all_clusters]):
1521
1520
  # TODO: Parse serve controllers and show services separately.
@@ -2963,9 +2962,9 @@ def _hint_or_raise_for_down_jobs_controller(controller_name: str,
2963
2962
  'jobs (output of `sky jobs queue`) will be lost.')
2964
2963
  click.echo(msg)
2965
2964
  if managed_jobs_:
2966
- job_table = managed_jobs.format_job_table(managed_jobs_,
2967
- show_all=False,
2968
- show_user=True)
2965
+ job_table = table_utils.format_job_table(managed_jobs_,
2966
+ show_all=False,
2967
+ show_user=True)
2969
2968
  msg = controller.value.decline_down_for_dirty_controller_hint
2970
2969
  # Add prefix to each line to align with the bullet point.
2971
2970
  msg += '\n'.join(
@@ -4027,8 +4026,7 @@ def storage_ls(verbose: bool):
4027
4026
  """List storage objects managed by SkyPilot."""
4028
4027
  request_id = sdk.storage_ls()
4029
4028
  storages = sdk.stream_and_get(request_id)
4030
- storage_table = storage_utils.format_storage_table(storages,
4031
- show_all=verbose)
4029
+ storage_table = table_utils.format_storage_table(storages, show_all=verbose)
4032
4030
  click.echo(storage_table)
4033
4031
 
4034
4032
 
@@ -4123,13 +4121,15 @@ def volumes():
4123
4121
  @click.option('--infra',
4124
4122
  required=False,
4125
4123
  type=str,
4126
- help='Infra. Format: k8s, k8s/context-name. '
4124
+ help='Infrastructure to use. '
4125
+ 'Format: cloud, cloud/region, cloud/region/zone, or '
4126
+ 'k8s/context-name.'
4127
+ 'Examples: k8s, k8s/my-context, runpod/US/US-CA-2. '
4127
4128
  'Override the infra defined in the YAML.')
4128
- @click.option(
4129
- '--type',
4130
- required=False,
4131
- type=str,
4132
- help='Volume type. Format: pvc. Override the type defined in the YAML.')
4129
+ @click.option('--type',
4130
+ required=False,
4131
+ type=click.Choice(volume_utils.VolumeType.supported_types()),
4132
+ help='Volume type. Override the type defined in the YAML.')
4133
4133
  @click.option('--size',
4134
4134
  required=False,
4135
4135
  type=str,
@@ -4160,7 +4160,7 @@ def volumes_apply(
4160
4160
  sky volumes apply volume.yaml
4161
4161
  \b
4162
4162
  # Apply a volume from a command.
4163
- sky volumes apply --name pvc1 --infra k8s --type pvc --size 100Gi
4163
+ sky volumes apply --name pvc1 --infra k8s --type k8s-pvc --size 100Gi
4164
4164
  """
4165
4165
  # pylint: disable=import-outside-toplevel
4166
4166
  from sky.volumes import volume as volume_lib
@@ -4239,8 +4239,8 @@ def volumes_ls(verbose: bool):
4239
4239
  """List volumes managed by SkyPilot."""
4240
4240
  request_id = volumes_sdk.ls()
4241
4241
  all_volumes = sdk.stream_and_get(request_id)
4242
- volume_table = volumes_utils.format_volume_table(all_volumes,
4243
- show_all=verbose)
4242
+ volume_table = table_utils.format_volume_table(all_volumes,
4243
+ show_all=verbose)
4244
4244
  click.echo(volume_table)
4245
4245
 
4246
4246
 
@@ -4497,10 +4497,30 @@ def jobs_launch(
4497
4497
  job_id_handle = _async_call_or_wait(request_id, async_call,
4498
4498
  'sky.jobs.launch')
4499
4499
 
4500
- if not async_call and not detach_run:
4501
- job_ids = job_id_handle[0]
4502
- if isinstance(job_ids, int) or len(job_ids) == 1:
4503
- job_id = job_ids if isinstance(job_ids, int) else job_ids[0]
4500
+ if async_call:
4501
+ return
4502
+
4503
+ job_ids = [job_id_handle[0]] if isinstance(job_id_handle[0],
4504
+ int) else job_id_handle[0]
4505
+ if pool:
4506
+ # Display the worker assignment for the jobs.
4507
+ logger.debug(f'Getting service records for pool: {pool}')
4508
+ records_request_id = managed_jobs.pool_status(pool_names=pool)
4509
+ service_records = _async_call_or_wait(records_request_id, async_call,
4510
+ 'sky.jobs.pool_status')
4511
+ logger.debug(f'Pool status: {service_records}')
4512
+ replica_infos = service_records[0]['replica_info']
4513
+ for replica_info in replica_infos:
4514
+ job_id = replica_info.get('used_by', None)
4515
+ if job_id in job_ids:
4516
+ worker_id = replica_info['replica_id']
4517
+ version = replica_info['version']
4518
+ logger.info(f'Job ID: {job_id} assigned to pool {pool} '
4519
+ f'(worker: {worker_id}, version: {version})')
4520
+
4521
+ if not detach_run:
4522
+ if len(job_ids) == 1:
4523
+ job_id = job_ids[0]
4504
4524
  returncode = managed_jobs.tail_logs(name=None,
4505
4525
  job_id=job_id,
4506
4526
  follow=True,
@@ -1,8 +1,19 @@
1
1
  """Utilities for formatting tables for CLI output."""
2
- from typing import List
2
+ import abc
3
+ from datetime import datetime
4
+ from typing import Dict, List, Optional
3
5
 
6
+ import prettytable
7
+
8
+ from sky import sky_logging
9
+ from sky.jobs import utils as managed_jobs
4
10
  from sky.schemas.api import responses
11
+ from sky.skylet import constants
12
+ from sky.utils import common_utils
5
13
  from sky.utils import log_utils
14
+ from sky.utils import volume
15
+
16
+ logger = sky_logging.init_logger(__name__)
6
17
 
7
18
 
8
19
  def format_job_queue(jobs: List[responses.ClusterJobRecord]):
@@ -32,3 +43,270 @@ def format_job_queue(jobs: List[responses.ClusterJobRecord]):
32
43
  job.metadata.get('git_commit', '-'),
33
44
  ])
34
45
  return job_table
46
+
47
+
48
+ def format_storage_table(storages: List[responses.StorageRecord],
49
+ show_all: bool = False) -> str:
50
+ """Format the storage table for display.
51
+
52
+ Args:
53
+ storage_table (dict): The storage table.
54
+
55
+ Returns:
56
+ str: The formatted storage table.
57
+ """
58
+ storage_table = log_utils.create_table([
59
+ 'NAME',
60
+ 'UPDATED',
61
+ 'STORE',
62
+ 'COMMAND',
63
+ 'STATUS',
64
+ ])
65
+
66
+ for row in storages:
67
+ launched_at = row.launched_at
68
+ if show_all:
69
+ command = row.last_use
70
+ else:
71
+ command = common_utils.truncate_long_string(
72
+ row.last_use, constants.LAST_USE_TRUNC_LENGTH)
73
+ storage_table.add_row([
74
+ # NAME
75
+ row.name,
76
+ # LAUNCHED
77
+ log_utils.readable_time_duration(launched_at),
78
+ # CLOUDS
79
+ ', '.join([s.value for s in row.store]),
80
+ # COMMAND,
81
+ command,
82
+ # STATUS
83
+ row.status.value,
84
+ ])
85
+ if storages:
86
+ return str(storage_table)
87
+ else:
88
+ return 'No existing storage.'
89
+
90
+
91
+ def format_job_table(jobs: List[responses.ManagedJobRecord],
92
+ show_all: bool,
93
+ show_user: bool,
94
+ max_jobs: Optional[int] = None):
95
+ jobs = [job.model_dump() for job in jobs]
96
+ return managed_jobs.format_job_table(jobs,
97
+ show_all=show_all,
98
+ show_user=show_user,
99
+ max_jobs=max_jobs)
100
+
101
+
102
+ _BASIC_COLUMNS = [
103
+ 'NAME',
104
+ 'TYPE',
105
+ 'INFRA',
106
+ 'SIZE',
107
+ 'USER',
108
+ 'WORKSPACE',
109
+ 'AGE',
110
+ 'STATUS',
111
+ 'LAST_USE',
112
+ 'USED_BY',
113
+ ]
114
+
115
+
116
+ def _get_infra_str(cloud: Optional[str], region: Optional[str],
117
+ zone: Optional[str]) -> str:
118
+ """Get the infrastructure string for the volume."""
119
+ infra = ''
120
+ if cloud:
121
+ infra += cloud
122
+ if region:
123
+ infra += f'/{region}'
124
+ if zone:
125
+ infra += f'/{zone}'
126
+ return infra
127
+
128
+
129
+ class VolumeTable(abc.ABC):
130
+ """The volume table."""
131
+
132
+ def __init__(self,
133
+ volumes: List[responses.VolumeRecord],
134
+ show_all: bool = False):
135
+ super().__init__()
136
+ self.table = self._create_table(show_all)
137
+ self._add_rows(volumes, show_all)
138
+
139
+ def _get_row_base_columns(self,
140
+ row: responses.VolumeRecord,
141
+ show_all: bool = False) -> List[str]:
142
+ """Get the base columns for a row."""
143
+ # Convert last_attached_at timestamp to human readable string
144
+ last_attached_at = row.get('last_attached_at')
145
+ if last_attached_at is not None:
146
+ last_attached_at_str = datetime.fromtimestamp(
147
+ last_attached_at).strftime('%Y-%m-%d %H:%M:%S')
148
+ else:
149
+ last_attached_at_str = '-'
150
+ size = row.get('size', '')
151
+ if size:
152
+ size = f'{size}Gi'
153
+ usedby_str = '-'
154
+ usedby_clusters = row.get('usedby_clusters')
155
+ usedby_pods = row.get('usedby_pods')
156
+ if usedby_clusters:
157
+ usedby_str = f'{", ".join(usedby_clusters)}'
158
+ elif usedby_pods:
159
+ usedby_str = f'{", ".join(usedby_pods)}'
160
+ if show_all:
161
+ usedby = usedby_str
162
+ else:
163
+ usedby = common_utils.truncate_long_string(
164
+ usedby_str, constants.USED_BY_TRUNC_LENGTH)
165
+ infra = _get_infra_str(row.get('cloud'), row.get('region'),
166
+ row.get('zone'))
167
+ return [
168
+ row.get('name', ''),
169
+ row.get('type', ''),
170
+ infra,
171
+ size,
172
+ row.get('user_name', '-'),
173
+ row.get('workspace', '-'),
174
+ log_utils.human_duration(row.get('launched_at', 0)),
175
+ row.get('status', ''),
176
+ last_attached_at_str,
177
+ usedby,
178
+ ]
179
+
180
+ def _create_table(self, show_all: bool = False) -> prettytable.PrettyTable:
181
+ """Create the volume table."""
182
+ raise NotImplementedError
183
+
184
+ def _add_rows(self,
185
+ volumes: List[responses.VolumeRecord],
186
+ show_all: bool = False) -> None:
187
+ """Add rows to the volume table."""
188
+ raise NotImplementedError
189
+
190
+ @abc.abstractmethod
191
+ def format(self) -> str:
192
+ """Format the volume table for display."""
193
+ raise NotImplementedError
194
+
195
+
196
+ class PVCVolumeTable(VolumeTable):
197
+ """The PVC volume table."""
198
+
199
+ def _create_table(self, show_all: bool = False) -> prettytable.PrettyTable:
200
+ """Create the PVC volume table."""
201
+ # If show_all is False, show the table with the columns:
202
+ # NAME, TYPE, INFRA, SIZE, USER, WORKSPACE,
203
+ # AGE, STATUS, LAST_USE, USED_BY
204
+ # If show_all is True, show the table with the columns:
205
+ # NAME, TYPE, INFRA, SIZE, USER, WORKSPACE,
206
+ # AGE, STATUS, LAST_USE, USED_BY, NAME_ON_CLOUD
207
+ # STORAGE_CLASS, ACCESS_MODE
208
+
209
+ if show_all:
210
+ columns = _BASIC_COLUMNS + [
211
+ 'NAME_ON_CLOUD',
212
+ 'STORAGE_CLASS',
213
+ 'ACCESS_MODE',
214
+ ]
215
+ else:
216
+ columns = _BASIC_COLUMNS
217
+
218
+ table = log_utils.create_table(columns)
219
+ return table
220
+
221
+ def _add_rows(self,
222
+ volumes: List[responses.VolumeRecord],
223
+ show_all: bool = False) -> None:
224
+ """Add rows to the PVC volume table."""
225
+ for row in volumes:
226
+ table_row = self._get_row_base_columns(row, show_all)
227
+ if show_all:
228
+ table_row.append(row.get('name_on_cloud', ''))
229
+ table_row.append(
230
+ row.get('config', {}).get('storage_class_name', '-'))
231
+ table_row.append(row.get('config', {}).get('access_mode', ''))
232
+
233
+ self.table.add_row(table_row)
234
+
235
+ def format(self) -> str:
236
+ """Format the PVC volume table for display."""
237
+ return 'Kubernetes PVCs:\n' + str(self.table)
238
+
239
+
240
+ class RunPodVolumeTable(VolumeTable):
241
+ """The RunPod volume table."""
242
+
243
+ def _create_table(self, show_all: bool = False) -> prettytable.PrettyTable:
244
+ """Create the RunPod volume table."""
245
+ # If show_all is False, show the table with the columns:
246
+ # NAME, TYPE, INFRA, SIZE, USER, WORKSPACE,
247
+ # AGE, STATUS, LAST_USE, USED_BY
248
+ # If show_all is True, show the table with the columns:
249
+ # NAME, TYPE, INFRA, SIZE, USER, WORKSPACE,
250
+ # AGE, STATUS, LAST_USE, USED_BY, NAME_ON_CLOUD
251
+
252
+ if show_all:
253
+ columns = _BASIC_COLUMNS + ['NAME_ON_CLOUD']
254
+ else:
255
+ columns = _BASIC_COLUMNS
256
+
257
+ table = log_utils.create_table(columns)
258
+ return table
259
+
260
+ def _add_rows(self,
261
+ volumes: List[responses.VolumeRecord],
262
+ show_all: bool = False) -> None:
263
+ """Add rows to the RunPod volume table."""
264
+ for row in volumes:
265
+ table_row = self._get_row_base_columns(row, show_all)
266
+ if show_all:
267
+ table_row.append(row.get('name_on_cloud', ''))
268
+
269
+ self.table.add_row(table_row)
270
+
271
+ def format(self) -> str:
272
+ """Format the RunPod volume table for display."""
273
+ return 'RunPod Network Volumes:\n' + str(self.table)
274
+
275
+
276
+ def format_volume_table(volumes: List[responses.VolumeRecord],
277
+ show_all: bool = False) -> str:
278
+ """Format the volume table for display.
279
+
280
+ Args:
281
+ volume_table (dict): The volume table.
282
+
283
+ Returns:
284
+ str: The formatted volume table.
285
+ """
286
+ volumes_per_type: Dict[str, List[responses.VolumeRecord]] = {}
287
+ supported_volume_types = [
288
+ volume_type.value for volume_type in volume.VolumeType
289
+ ]
290
+ for row in volumes:
291
+ volume_type = row.get('type', '')
292
+ if volume_type in supported_volume_types:
293
+ if volume_type not in volumes_per_type:
294
+ volumes_per_type[volume_type] = []
295
+ volumes_per_type[volume_type].append(row)
296
+ else:
297
+ logger.warning(f'Unknown volume type: {volume_type}')
298
+ continue
299
+ table_str = ''
300
+ for volume_type, volume_list in volumes_per_type.items():
301
+ if table_str:
302
+ table_str += '\n\n'
303
+ if volume_type == volume.VolumeType.PVC.value:
304
+ pvc_table = PVCVolumeTable(volume_list, show_all)
305
+ table_str += pvc_table.format()
306
+ elif volume_type == volume.VolumeType.RUNPOD_NETWORK_VOLUME.value:
307
+ runpod_table = RunPodVolumeTable(volume_list, show_all)
308
+ table_str += runpod_table.format()
309
+ if table_str:
310
+ return table_str
311
+ else:
312
+ return 'No existing volumes.'
sky/client/sdk.py CHANGED
@@ -1618,26 +1618,15 @@ def cost_report(
1618
1618
  @usage_lib.entrypoint
1619
1619
  @server_common.check_server_healthy_or_start
1620
1620
  @annotations.client_api
1621
- def storage_ls() -> server_common.RequestId[List[Dict[str, Any]]]:
1621
+ def storage_ls() -> server_common.RequestId[List[responses.StorageRecord]]:
1622
1622
  """Gets the storages.
1623
1623
 
1624
1624
  Returns:
1625
1625
  The request ID of the storage list request.
1626
1626
 
1627
1627
  Request Returns:
1628
- storage_records (List[Dict[str, Any]]): A list of dicts, with each dict
1629
- containing the information of a storage.
1630
-
1631
- .. code-block:: python
1632
-
1633
- {
1634
- 'name': (str) storage name,
1635
- 'launched_at': (int) timestamp of creation,
1636
- 'store': (List[sky.StoreType]) storage type,
1637
- 'last_use': (int) timestamp of last use,
1638
- 'status': (sky.StorageStatus) storage status,
1639
- }
1640
- ]
1628
+ storage_records (List[responses.StorageRecord]):
1629
+ A list of storage records.
1641
1630
  """
1642
1631
  response = server_common.make_authenticated_request('GET', '/storage/ls')
1643
1632
  return server_common.get_request_id(response)
@@ -1912,10 +1901,10 @@ def kubernetes_node_info(
1912
1901
  @usage_lib.entrypoint
1913
1902
  @server_common.check_server_healthy_or_start
1914
1903
  @annotations.client_api
1915
- def status_kubernetes() -> server_common.RequestId[Tuple[
1916
- List['kubernetes_utils.KubernetesSkyPilotClusterInfoPayload'],
1917
- List['kubernetes_utils.KubernetesSkyPilotClusterInfoPayload'], List[Dict[
1918
- str, Any]], Optional[str]]]:
1904
+ def status_kubernetes() -> server_common.RequestId[
1905
+ Tuple[List['kubernetes_utils.KubernetesSkyPilotClusterInfoPayload'],
1906
+ List['kubernetes_utils.KubernetesSkyPilotClusterInfoPayload'],
1907
+ List[responses.ManagedJobRecord], Optional[str]]]:
1919
1908
  """Gets all SkyPilot clusters and jobs in the Kubernetes cluster.
1920
1909
 
1921
1910
  Managed jobs and services are also included in the clusters returned.