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.
- sky/__init__.py +2 -2
- sky/backends/backend_utils.py +18 -10
- sky/backends/cloud_vm_ray_backend.py +2 -2
- sky/check.py +0 -29
- sky/client/cli/command.py +48 -28
- sky/client/cli/table_utils.py +279 -1
- sky/client/sdk.py +7 -18
- sky/core.py +15 -16
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/{UDSEoDB67vwFMZyCJ4HWU → 16g0-hgEgk6Db72hpE8MY}/_buildManifest.js +1 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/pools/{[pool]-07349868f7905d37.js → [pool]-509b2977a6373bf6.js} +1 -1
- 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 -1
- sky/dashboard/out/index.html +1 -1
- sky/dashboard/out/infra/[context].html +1 -1
- sky/dashboard/out/infra.html +1 -1
- sky/dashboard/out/jobs/[job].html +1 -1
- sky/dashboard/out/jobs/pools/[pool].html +1 -1
- sky/dashboard/out/jobs.html +1 -1
- sky/dashboard/out/users.html +1 -1
- sky/dashboard/out/volumes.html +1 -1
- sky/dashboard/out/workspace/new.html +1 -1
- sky/dashboard/out/workspaces/[name].html +1 -1
- sky/dashboard/out/workspaces.html +1 -1
- sky/data/storage.py +11 -0
- sky/data/storage_utils.py +1 -45
- sky/jobs/client/sdk.py +3 -2
- sky/jobs/controller.py +15 -0
- sky/jobs/server/core.py +24 -2
- sky/jobs/server/server.py +1 -1
- sky/jobs/utils.py +2 -1
- sky/provision/kubernetes/instance.py +1 -1
- sky/provision/kubernetes/utils.py +50 -28
- sky/schemas/api/responses.py +76 -0
- sky/server/common.py +2 -1
- sky/server/requests/serializers/decoders.py +16 -4
- sky/server/requests/serializers/encoders.py +12 -5
- sky/task.py +4 -0
- sky/utils/cluster_utils.py +23 -5
- sky/utils/command_runner.py +21 -5
- sky/utils/command_runner.pyi +11 -0
- sky/utils/volume.py +5 -0
- sky/volumes/client/sdk.py +3 -2
- sky/volumes/server/core.py +3 -2
- {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/METADATA +33 -33
- {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/RECORD +53 -54
- sky/volumes/utils.py +0 -224
- /sky/dashboard/out/_next/static/{UDSEoDB67vwFMZyCJ4HWU → 16g0-hgEgk6Db72hpE8MY}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250927.dist-info → skypilot_nightly-1.0.0.dev20251002.dist-info}/licenses/LICENSE +0 -0
- {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 = '
|
|
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.
|
|
40
|
+
__version__ = '1.0.0.dev20251002'
|
|
41
41
|
__root_dir__ = directory_utils.get_sky_dir()
|
|
42
42
|
|
|
43
43
|
|
sky/backends/backend_utils.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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 =
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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 =
|
|
1517
|
-
|
|
1518
|
-
|
|
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 =
|
|
2967
|
-
|
|
2968
|
-
|
|
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 =
|
|
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='
|
|
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
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
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 =
|
|
4243
|
-
|
|
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
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
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,
|
sky/client/cli/table_utils.py
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
"""Utilities for formatting tables for CLI output."""
|
|
2
|
-
|
|
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[
|
|
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[
|
|
1629
|
-
|
|
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[
|
|
1916
|
-
List['kubernetes_utils.KubernetesSkyPilotClusterInfoPayload'],
|
|
1917
|
-
|
|
1918
|
-
|
|
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.
|