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.
- sky/__init__.py +2 -2
- sky/adaptors/kubernetes.py +1 -6
- sky/backends/backend_utils.py +26 -11
- sky/backends/cloud_vm_ray_backend.py +16 -5
- sky/client/cli/command.py +232 -9
- sky/client/sdk.py +195 -91
- sky/clouds/aws.py +10 -7
- sky/clouds/azure.py +10 -7
- sky/clouds/cloud.py +2 -0
- sky/clouds/cudo.py +2 -0
- sky/clouds/do.py +10 -7
- sky/clouds/fluidstack.py +2 -0
- sky/clouds/gcp.py +10 -7
- sky/clouds/hyperbolic.py +10 -7
- sky/clouds/ibm.py +2 -0
- sky/clouds/kubernetes.py +26 -9
- sky/clouds/lambda_cloud.py +10 -7
- sky/clouds/nebius.py +10 -7
- sky/clouds/oci.py +10 -7
- sky/clouds/paperspace.py +10 -7
- sky/clouds/runpod.py +10 -7
- sky/clouds/scp.py +10 -7
- sky/clouds/ssh.py +36 -0
- sky/clouds/vast.py +10 -7
- sky/clouds/vsphere.py +2 -0
- sky/core.py +21 -0
- sky/dag.py +14 -0
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/bs6UB9V4Jq10TIZ5x-kBK/_buildManifest.js +1 -0
- sky/dashboard/out/_next/static/chunks/141-fa5a20cbf401b351.js +11 -0
- sky/dashboard/out/_next/static/chunks/230-d6e363362017ff3a.js +1 -0
- sky/dashboard/out/_next/static/chunks/25.76c246239df93d50.js +6 -0
- sky/dashboard/out/_next/static/chunks/43-36177d00f6956ab2.js +1 -0
- sky/dashboard/out/_next/static/chunks/430.ed51037d1a4a438b.js +1 -0
- sky/dashboard/out/_next/static/chunks/470-92dd1614396389be.js +1 -0
- sky/dashboard/out/_next/static/chunks/544.110e53813fb98e2e.js +1 -0
- sky/dashboard/out/_next/static/chunks/645.961f08e39b8ce447.js +1 -0
- sky/dashboard/out/_next/static/chunks/690.55f9eed3be903f56.js +16 -0
- sky/dashboard/out/_next/static/chunks/697.6460bf72e760addd.js +20 -0
- sky/dashboard/out/_next/static/chunks/785.dc2686c3c1235554.js +1 -0
- sky/dashboard/out/_next/static/chunks/871-3db673be3ee3750b.js +6 -0
- sky/dashboard/out/_next/static/chunks/875.52c962183328b3f2.js +25 -0
- sky/dashboard/out/_next/static/chunks/973-81b2d057178adb76.js +1 -0
- sky/dashboard/out/_next/static/chunks/982.1b61658204416b0f.js +1 -0
- sky/dashboard/out/_next/static/chunks/984.e8bac186a24e5178.js +1 -0
- sky/dashboard/out/_next/static/chunks/990-0ad5ea1699e03ee8.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/{_app-ce31493da9747ef4.js → _app-9a3ce3170d2edcec.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-aff040d7bc5d0086.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-8040f2483897ed0c.js +6 -0
- sky/dashboard/out/_next/static/chunks/pages/{clusters-7e9736af1c6345a6.js → clusters-f119a5630a1efd61.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/config-6b255eae088da6a3.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-b302aea4d65766bf.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/infra-ee8cc4d449945d19.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-e4b23128db0774cd.js +16 -0
- sky/dashboard/out/_next/static/chunks/pages/jobs-0a5695ff3075d94a.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/users-4978cbb093e141e7.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/volumes-476b670ef33d1ecd.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspace/{new-31aa8bdcb7592635.js → new-5b59bce9eb208d84.js} +1 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-cb7e720b739de53a.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/workspaces-50e230828730cfb3.js +1 -0
- sky/dashboard/out/_next/static/chunks/webpack-08fdb9e6070127fc.js +1 -0
- sky/dashboard/out/_next/static/css/52082cf558ec9705.css +3 -0
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
- sky/dashboard/out/clusters/[cluster].html +1 -1
- sky/dashboard/out/clusters.html +1 -1
- sky/dashboard/out/config.html +1 -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.html +1 -1
- sky/dashboard/out/users.html +1 -1
- sky/dashboard/out/volumes.html +1 -0
- 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_utils.py +2 -4
- sky/exceptions.py +15 -0
- sky/execution.py +5 -0
- sky/global_user_state.py +129 -0
- sky/jobs/client/sdk.py +13 -11
- sky/jobs/server/core.py +4 -0
- sky/models.py +16 -0
- sky/provision/__init__.py +26 -0
- sky/provision/kubernetes/__init__.py +3 -0
- sky/provision/kubernetes/instance.py +38 -77
- sky/provision/kubernetes/utils.py +70 -4
- sky/provision/kubernetes/volume.py +147 -0
- sky/resources.py +20 -76
- sky/serve/client/sdk.py +13 -13
- sky/serve/server/core.py +5 -1
- sky/server/common.py +40 -5
- sky/server/constants.py +5 -1
- sky/server/metrics.py +105 -0
- sky/server/requests/executor.py +30 -14
- sky/server/requests/payloads.py +16 -0
- sky/server/requests/requests.py +35 -1
- sky/server/rest.py +153 -0
- sky/server/server.py +70 -43
- sky/server/state.py +20 -0
- sky/server/stream_utils.py +8 -3
- sky/server/uvicorn.py +153 -13
- sky/setup_files/dependencies.py +2 -0
- sky/skylet/constants.py +19 -3
- sky/skypilot_config.py +3 -0
- sky/ssh_node_pools/__init__.py +1 -0
- sky/ssh_node_pools/core.py +133 -0
- sky/ssh_node_pools/server.py +232 -0
- sky/task.py +141 -18
- sky/templates/kubernetes-ray.yml.j2 +30 -1
- sky/users/permission.py +2 -0
- sky/utils/context.py +3 -1
- sky/utils/kubernetes/deploy_remote_cluster.py +12 -185
- sky/utils/kubernetes/ssh_utils.py +221 -0
- sky/utils/resources_utils.py +66 -0
- sky/utils/rich_utils.py +6 -0
- sky/utils/schemas.py +146 -3
- sky/utils/status_lib.py +10 -0
- sky/utils/validator.py +11 -1
- sky/volumes/__init__.py +0 -0
- sky/volumes/client/__init__.py +0 -0
- sky/volumes/client/sdk.py +64 -0
- sky/volumes/server/__init__.py +0 -0
- sky/volumes/server/core.py +199 -0
- sky/volumes/server/server.py +85 -0
- sky/volumes/utils.py +158 -0
- sky/volumes/volume.py +198 -0
- {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/METADATA +2 -1
- {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/RECORD +135 -115
- sky/dashboard/out/_next/static/chunks/211.692afc57e812ae1a.js +0 -1
- sky/dashboard/out/_next/static/chunks/350.9e123a4551f68b0d.js +0 -1
- sky/dashboard/out/_next/static/chunks/37-4650f214e2119168.js +0 -6
- sky/dashboard/out/_next/static/chunks/42.2273cc2415291ceb.js +0 -6
- sky/dashboard/out/_next/static/chunks/443.b2242d0efcdf5f47.js +0 -1
- sky/dashboard/out/_next/static/chunks/470-1494c899266cf5c9.js +0 -1
- sky/dashboard/out/_next/static/chunks/513.309df9e18a9ff005.js +0 -1
- sky/dashboard/out/_next/static/chunks/641.c8e452bc5070a630.js +0 -1
- sky/dashboard/out/_next/static/chunks/682.4dd5dc116f740b5f.js +0 -6
- sky/dashboard/out/_next/static/chunks/760-a89d354797ce7af5.js +0 -1
- sky/dashboard/out/_next/static/chunks/843-bde186946d353355.js +0 -11
- sky/dashboard/out/_next/static/chunks/856-bfddc18e16f3873c.js +0 -1
- sky/dashboard/out/_next/static/chunks/901-b424d293275e1fd7.js +0 -1
- sky/dashboard/out/_next/static/chunks/973-56412c7976b4655b.js +0 -1
- sky/dashboard/out/_next/static/chunks/984.ae8c08791d274ca0.js +0 -50
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-4e065c812a52460b.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-520ec1ab65e2f2a4.js +0 -6
- sky/dashboard/out/_next/static/chunks/pages/config-e4f473661889e7cd.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/infra/[context]-00fd23b9577492ca.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/infra-8a4bf7370d4d9bb7.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-171c27f4ca94861c.js +0 -16
- sky/dashboard/out/_next/static/chunks/pages/jobs-55e5bcb16d563231.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/users-c9f4d785cdaa52d8.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces/[name]-ecc5a7003776cfa7.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/workspaces-f00cba35691483b1.js +0 -1
- sky/dashboard/out/_next/static/chunks/webpack-c85998e6a5722f21.js +0 -1
- sky/dashboard/out/_next/static/css/6ab927686b492a4a.css +0 -3
- sky/dashboard/out/_next/static/zsALxITkbP8J8NVwSDwMo/_buildManifest.js +0 -1
- /sky/dashboard/out/_next/static/{zsALxITkbP8J8NVwSDwMo → bs6UB9V4Jq10TIZ5x-kBK}/_ssgManifest.js +0 -0
- /sky/dashboard/out/_next/static/chunks/{938-ce7991c156584b06.js → 938-068520cc11738deb.js} +0 -0
- {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250624.dist-info → skypilot_nightly-1.0.0.dev20250626.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
"""Volume management core."""
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
import os
|
5
|
+
from typing import Any, Dict, Generator, List, Optional
|
6
|
+
import uuid
|
7
|
+
|
8
|
+
import filelock
|
9
|
+
|
10
|
+
import sky
|
11
|
+
from sky import global_user_state
|
12
|
+
from sky import models
|
13
|
+
from sky import provision
|
14
|
+
from sky import sky_logging
|
15
|
+
from sky.utils import common_utils
|
16
|
+
from sky.utils import status_lib
|
17
|
+
|
18
|
+
logger = sky_logging.init_logger(__name__)
|
19
|
+
|
20
|
+
# Filelocks for the storage management.
|
21
|
+
VOLUME_LOCK_PATH = os.path.expanduser('~/.sky/.{volume_name}.lock')
|
22
|
+
VOLUME_LOCK_TIMEOUT_SECONDS = 20
|
23
|
+
|
24
|
+
|
25
|
+
def volume_refresh():
|
26
|
+
"""Refreshes the volume status."""
|
27
|
+
volumes = global_user_state.get_volumes()
|
28
|
+
for volume in volumes:
|
29
|
+
volume_name = volume.get('name')
|
30
|
+
config = volume.get('handle')
|
31
|
+
if config is None:
|
32
|
+
logger.warning(f'Volume {volume_name} has no handle.'
|
33
|
+
'Skipping status refresh...')
|
34
|
+
continue
|
35
|
+
cloud = config.cloud
|
36
|
+
usedby = provision.get_volume_usedby(cloud, config)
|
37
|
+
with _volume_lock(volume_name):
|
38
|
+
latest_volume = global_user_state.get_volume_by_name(volume_name)
|
39
|
+
if latest_volume is None:
|
40
|
+
logger.warning(f'Volume {volume_name} not found.')
|
41
|
+
continue
|
42
|
+
status = latest_volume.get('status')
|
43
|
+
if not usedby:
|
44
|
+
if status != status_lib.VolumeStatus.READY:
|
45
|
+
logger.info(f'Update volume {volume_name} '
|
46
|
+
f'status to READY')
|
47
|
+
global_user_state.update_volume_status(
|
48
|
+
volume_name, status=status_lib.VolumeStatus.READY)
|
49
|
+
else:
|
50
|
+
if status != status_lib.VolumeStatus.IN_USE:
|
51
|
+
logger.info(f'Update volume {volume_name} '
|
52
|
+
f'status to IN_USE, usedby: {usedby}')
|
53
|
+
global_user_state.update_volume_status(
|
54
|
+
volume_name, status=status_lib.VolumeStatus.IN_USE)
|
55
|
+
|
56
|
+
|
57
|
+
def volume_list() -> List[Dict[str, Any]]:
|
58
|
+
"""Gets the volumes.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
[
|
62
|
+
{
|
63
|
+
'name': str,
|
64
|
+
'type': str,
|
65
|
+
'launched_at': int timestamp of creation,
|
66
|
+
'cloud': str,
|
67
|
+
'region': str,
|
68
|
+
'zone': str,
|
69
|
+
'size': str,
|
70
|
+
'config': Dict[str, Any],
|
71
|
+
'name_on_cloud': str,
|
72
|
+
'user_hash': str,
|
73
|
+
'workspace': str,
|
74
|
+
'last_attached_at': int timestamp of last attachment,
|
75
|
+
'last_use': last command,
|
76
|
+
'status': sky.VolumeStatus,
|
77
|
+
}
|
78
|
+
]
|
79
|
+
"""
|
80
|
+
volumes = global_user_state.get_volumes()
|
81
|
+
all_users = global_user_state.get_all_users()
|
82
|
+
user_map = {user.id: user.name for user in all_users}
|
83
|
+
records = []
|
84
|
+
for volume in volumes:
|
85
|
+
volume_name = volume.get('name')
|
86
|
+
record = {
|
87
|
+
'name': volume_name,
|
88
|
+
'launched_at': volume.get('launched_at'),
|
89
|
+
'user_hash': volume.get('user_hash'),
|
90
|
+
'user_name': user_map.get(volume.get('user_hash'), ''),
|
91
|
+
'workspace': volume.get('workspace'),
|
92
|
+
'last_attached_at': volume.get('last_attached_at'),
|
93
|
+
'last_use': volume.get('last_use'),
|
94
|
+
}
|
95
|
+
status = volume.get('status')
|
96
|
+
if status is not None:
|
97
|
+
record['status'] = status.value
|
98
|
+
else:
|
99
|
+
record['status'] = ''
|
100
|
+
config = volume.get('handle')
|
101
|
+
if config is None:
|
102
|
+
logger.warning(f'Volume {volume_name} has no handle.')
|
103
|
+
continue
|
104
|
+
record['type'] = config.type
|
105
|
+
record['cloud'] = config.cloud
|
106
|
+
record['region'] = config.region
|
107
|
+
record['zone'] = config.zone
|
108
|
+
record['size'] = config.size
|
109
|
+
record['config'] = config.config
|
110
|
+
record['name_on_cloud'] = config.name_on_cloud
|
111
|
+
records.append(record)
|
112
|
+
return records
|
113
|
+
|
114
|
+
|
115
|
+
def volume_delete(names: List[str]) -> None:
|
116
|
+
"""Deletes volumes.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
names: List of volume names to delete.
|
120
|
+
|
121
|
+
Raises:
|
122
|
+
ValueError: If the volume does not exist
|
123
|
+
or is in use or has no handle.
|
124
|
+
"""
|
125
|
+
for name in names:
|
126
|
+
volume = global_user_state.get_volume_by_name(name)
|
127
|
+
if volume is None:
|
128
|
+
raise ValueError(f'Volume {name} not found.')
|
129
|
+
if volume.get('status') == status_lib.VolumeStatus.IN_USE:
|
130
|
+
raise ValueError(f'Volume {name} is in use.')
|
131
|
+
config = volume.get('handle')
|
132
|
+
if config is None:
|
133
|
+
raise ValueError(f'Volume {name} has no handle.')
|
134
|
+
logger.debug(f'Deleting volume {name} with config {config}')
|
135
|
+
cloud = config.cloud
|
136
|
+
with _volume_lock(name):
|
137
|
+
provision.delete_volume(cloud, config)
|
138
|
+
global_user_state.delete_volume(name)
|
139
|
+
|
140
|
+
|
141
|
+
def volume_apply(name: str, volume_type: str, cloud: str, region: Optional[str],
|
142
|
+
zone: Optional[str], size: Optional[str],
|
143
|
+
config: Dict[str, Any]) -> None:
|
144
|
+
"""Creates or registers a volume.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
name: The name of the volume.
|
148
|
+
volume_type: The type of the volume.
|
149
|
+
cloud: The cloud of the volume.
|
150
|
+
region: The region of the volume.
|
151
|
+
zone: The zone of the volume.
|
152
|
+
size: The size of the volume.
|
153
|
+
config: The configuration of the volume.
|
154
|
+
|
155
|
+
"""
|
156
|
+
# Reuse the method for cluster name on cloud to
|
157
|
+
# generate the storage name on cloud.
|
158
|
+
cloud_obj = sky.CLOUD_REGISTRY.from_str(cloud)
|
159
|
+
assert cloud_obj is not None
|
160
|
+
name_uuid = str(uuid.uuid4())[:6]
|
161
|
+
name_on_cloud = common_utils.make_cluster_name_on_cloud(
|
162
|
+
name, max_length=cloud_obj.max_cluster_name_length())
|
163
|
+
name_on_cloud += '-' + name_uuid
|
164
|
+
config = models.VolumeConfig(
|
165
|
+
name=name,
|
166
|
+
type=volume_type,
|
167
|
+
cloud=str(cloud_obj),
|
168
|
+
region=region,
|
169
|
+
zone=zone,
|
170
|
+
size=size,
|
171
|
+
config=config,
|
172
|
+
name_on_cloud=name_on_cloud,
|
173
|
+
)
|
174
|
+
logger.debug(
|
175
|
+
f'Creating volume {name} on cloud {cloud} with config {config}')
|
176
|
+
with _volume_lock(name):
|
177
|
+
current_volume = global_user_state.get_volume_by_name(name)
|
178
|
+
if current_volume is not None:
|
179
|
+
logger.info(f'Volume {name} already exists.')
|
180
|
+
return
|
181
|
+
config = provision.apply_volume(cloud, config)
|
182
|
+
global_user_state.add_volume(name, config,
|
183
|
+
status_lib.VolumeStatus.READY)
|
184
|
+
|
185
|
+
|
186
|
+
@contextlib.contextmanager
|
187
|
+
def _volume_lock(volume_name: str) -> Generator[None, None, None]:
|
188
|
+
"""Context manager for volume lock."""
|
189
|
+
try:
|
190
|
+
with filelock.FileLock(VOLUME_LOCK_PATH.format(volume_name=volume_name),
|
191
|
+
VOLUME_LOCK_TIMEOUT_SECONDS):
|
192
|
+
yield
|
193
|
+
except filelock.Timeout as e:
|
194
|
+
raise RuntimeError(
|
195
|
+
f'Failed to update user due to a timeout '
|
196
|
+
f'when trying to acquire the lock at '
|
197
|
+
f'{VOLUME_LOCK_PATH.format(volume_name=volume_name)}. '
|
198
|
+
'Please try again or manually remove the lock '
|
199
|
+
f'file if you believe it is stale.') from e
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""REST API for storage management."""
|
2
|
+
|
3
|
+
import fastapi
|
4
|
+
|
5
|
+
import sky
|
6
|
+
from sky import clouds
|
7
|
+
from sky import sky_logging
|
8
|
+
from sky.server.requests import executor
|
9
|
+
from sky.server.requests import payloads
|
10
|
+
from sky.server.requests import requests as requests_lib
|
11
|
+
from sky.volumes import volume
|
12
|
+
from sky.volumes.server import core
|
13
|
+
|
14
|
+
logger = sky_logging.init_logger(__name__)
|
15
|
+
|
16
|
+
router = fastapi.APIRouter()
|
17
|
+
|
18
|
+
|
19
|
+
@router.get('')
|
20
|
+
async def volume_list(request: fastapi.Request) -> None:
|
21
|
+
"""Gets the volumes."""
|
22
|
+
executor.schedule_request(
|
23
|
+
request_id=request.state.request_id,
|
24
|
+
request_name='volume_list',
|
25
|
+
request_body=payloads.RequestBody(),
|
26
|
+
func=core.volume_list,
|
27
|
+
schedule_type=requests_lib.ScheduleType.SHORT,
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
@router.post('/delete')
|
32
|
+
async def volume_delete(request: fastapi.Request,
|
33
|
+
volume_delete_body: payloads.VolumeDeleteBody) -> None:
|
34
|
+
"""Deletes a volume."""
|
35
|
+
executor.schedule_request(
|
36
|
+
request_id=request.state.request_id,
|
37
|
+
request_name='volume_delete',
|
38
|
+
request_body=volume_delete_body,
|
39
|
+
func=core.volume_delete,
|
40
|
+
schedule_type=requests_lib.ScheduleType.LONG,
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
@router.post('/apply')
|
45
|
+
async def volume_apply(request: fastapi.Request,
|
46
|
+
volume_apply_body: payloads.VolumeApplyBody) -> None:
|
47
|
+
"""Creates or registers a volume."""
|
48
|
+
volume_cloud = volume_apply_body.cloud
|
49
|
+
volume_type = volume_apply_body.volume_type
|
50
|
+
volume_config = volume_apply_body.config
|
51
|
+
|
52
|
+
supported_volume_types = [
|
53
|
+
volume_type.value for volume_type in volume.VolumeType
|
54
|
+
]
|
55
|
+
if volume_type not in supported_volume_types:
|
56
|
+
raise fastapi.HTTPException(
|
57
|
+
status_code=400, detail=f'Invalid volume type: {volume_type}')
|
58
|
+
cloud = sky.CLOUD_REGISTRY.from_str(volume_cloud)
|
59
|
+
if cloud is None:
|
60
|
+
raise fastapi.HTTPException(status_code=400,
|
61
|
+
detail=f'Invalid cloud: {volume_cloud}')
|
62
|
+
if volume_type == volume.VolumeType.PVC.value:
|
63
|
+
if not cloud.is_same_cloud(clouds.Kubernetes()):
|
64
|
+
raise fastapi.HTTPException(
|
65
|
+
status_code=400,
|
66
|
+
detail='PVC storage is only supported on Kubernetes')
|
67
|
+
supported_access_modes = [
|
68
|
+
access_mode.value for access_mode in volume.VolumeAccessMode
|
69
|
+
]
|
70
|
+
if volume_config is None:
|
71
|
+
volume_config = {}
|
72
|
+
access_mode = volume_config.get('access_mode')
|
73
|
+
if access_mode is None:
|
74
|
+
volume_config[
|
75
|
+
'access_mode'] = volume.VolumeAccessMode.READ_WRITE_ONCE.value
|
76
|
+
elif access_mode not in supported_access_modes:
|
77
|
+
raise fastapi.HTTPException(
|
78
|
+
status_code=400, detail=f'Invalid access mode: {access_mode}')
|
79
|
+
executor.schedule_request(
|
80
|
+
request_id=request.state.request_id,
|
81
|
+
request_name='volume_apply',
|
82
|
+
request_body=volume_apply_body,
|
83
|
+
func=core.volume_apply,
|
84
|
+
schedule_type=requests_lib.ScheduleType.LONG,
|
85
|
+
)
|
sky/volumes/utils.py
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
"""Volume utils."""
|
2
|
+
import abc
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
|
+
|
6
|
+
import prettytable
|
7
|
+
|
8
|
+
from sky import sky_logging
|
9
|
+
from sky.utils import log_utils
|
10
|
+
from sky.volumes import volume
|
11
|
+
|
12
|
+
logger = sky_logging.init_logger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
def _get_infra_str(cloud: Optional[str], region: Optional[str],
|
16
|
+
zone: Optional[str]) -> str:
|
17
|
+
"""Get the infrastructure string for the volume."""
|
18
|
+
infra = ''
|
19
|
+
if cloud:
|
20
|
+
infra += cloud
|
21
|
+
if region:
|
22
|
+
infra += f'/{region}'
|
23
|
+
if zone:
|
24
|
+
infra += f'/{zone}'
|
25
|
+
return infra
|
26
|
+
|
27
|
+
|
28
|
+
class VolumeTable(abc.ABC):
|
29
|
+
"""The volume table."""
|
30
|
+
|
31
|
+
@abc.abstractmethod
|
32
|
+
def format(self) -> str:
|
33
|
+
"""Format the volume table for display."""
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class PVCVolumeTable(VolumeTable):
|
38
|
+
"""The PVC volume table."""
|
39
|
+
|
40
|
+
def __init__(self, volumes: List[Dict[str, Any]], show_all: bool = False):
|
41
|
+
super().__init__()
|
42
|
+
self.table = self._create_table(show_all)
|
43
|
+
self._add_rows(volumes, show_all)
|
44
|
+
|
45
|
+
def _create_table(self, show_all: bool = False) -> prettytable.PrettyTable:
|
46
|
+
"""Create the PVC volume table."""
|
47
|
+
# If show_all is True, show the table with the columns:
|
48
|
+
# NAME, TYPE, INFRA, SIZE, USER, WORKSPACE,
|
49
|
+
# AGE, LAST_USE, STATUS
|
50
|
+
# If show_all is False, show the table with the columns:
|
51
|
+
# NAME, TYPE, INFRA, SIZE, USER, WORKSPACE,
|
52
|
+
# AGE, LAST_USE, STATUS, NAME_ON_CLOUD,
|
53
|
+
# STORAGE_CLASS, ACCESS_MODE
|
54
|
+
|
55
|
+
if show_all:
|
56
|
+
columns = [
|
57
|
+
'NAME',
|
58
|
+
'TYPE',
|
59
|
+
'INFRA',
|
60
|
+
'SIZE',
|
61
|
+
'USER',
|
62
|
+
'WORKSPACE',
|
63
|
+
'AGE',
|
64
|
+
'STATUS',
|
65
|
+
'LAST_USE',
|
66
|
+
'NAME_ON_CLOUD',
|
67
|
+
'STORAGE_CLASS',
|
68
|
+
'ACCESS_MODE',
|
69
|
+
]
|
70
|
+
else:
|
71
|
+
columns = [
|
72
|
+
'NAME',
|
73
|
+
'TYPE',
|
74
|
+
'INFRA',
|
75
|
+
'SIZE',
|
76
|
+
'USER',
|
77
|
+
'WORKSPACE',
|
78
|
+
'AGE',
|
79
|
+
'STATUS',
|
80
|
+
'LAST_USE',
|
81
|
+
]
|
82
|
+
|
83
|
+
table = log_utils.create_table(columns)
|
84
|
+
return table
|
85
|
+
|
86
|
+
def _add_rows(self,
|
87
|
+
volumes: List[Dict[str, Any]],
|
88
|
+
show_all: bool = False) -> None:
|
89
|
+
"""Add rows to the PVC volume table."""
|
90
|
+
for row in volumes:
|
91
|
+
# Convert last_attached_at timestamp to human readable string
|
92
|
+
last_attached_at = row.get('last_attached_at')
|
93
|
+
if last_attached_at is not None:
|
94
|
+
last_attached_at_str = datetime.fromtimestamp(
|
95
|
+
last_attached_at).strftime('%Y-%m-%d %H:%M:%S')
|
96
|
+
else:
|
97
|
+
last_attached_at_str = '-'
|
98
|
+
size = row.get('size', '')
|
99
|
+
if size:
|
100
|
+
size = f'{size}Gi'
|
101
|
+
infra = _get_infra_str(row.get('cloud'), row.get('region'),
|
102
|
+
row.get('zone'))
|
103
|
+
table_row = [
|
104
|
+
row.get('name', ''),
|
105
|
+
row.get('type', ''),
|
106
|
+
infra,
|
107
|
+
size,
|
108
|
+
row.get('user_name', '-'),
|
109
|
+
row.get('workspace', '-'),
|
110
|
+
log_utils.readable_time_duration(row.get('launched_at', 0)),
|
111
|
+
row.get('status', ''),
|
112
|
+
last_attached_at_str,
|
113
|
+
]
|
114
|
+
if show_all:
|
115
|
+
table_row.append(row.get('name_on_cloud', ''))
|
116
|
+
table_row.append(
|
117
|
+
row.get('config', {}).get('storage_class_name', '-'))
|
118
|
+
table_row.append(row.get('config', {}).get('access_mode', ''))
|
119
|
+
|
120
|
+
self.table.add_row(table_row)
|
121
|
+
|
122
|
+
def format(self) -> str:
|
123
|
+
"""Format the PVC volume table for display."""
|
124
|
+
return str(self.table)
|
125
|
+
|
126
|
+
|
127
|
+
def format_volume_table(volumes: List[Dict[str, Any]],
|
128
|
+
show_all: bool = False) -> str:
|
129
|
+
"""Format the volume table for display.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
volume_table (dict): The volume table.
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
str: The formatted volume table.
|
136
|
+
"""
|
137
|
+
volumes_per_type: Dict[str, List[Dict[str, Any]]] = {}
|
138
|
+
supported_volume_types = [
|
139
|
+
volume_type.value for volume_type in volume.VolumeType
|
140
|
+
]
|
141
|
+
for row in volumes:
|
142
|
+
volume_type = row.get('type', '')
|
143
|
+
if volume_type in supported_volume_types:
|
144
|
+
if volume_type not in volumes_per_type:
|
145
|
+
volumes_per_type[volume_type] = []
|
146
|
+
volumes_per_type[volume_type].append(row)
|
147
|
+
else:
|
148
|
+
logger.warning(f'Unknown volume type: {volume_type}')
|
149
|
+
continue
|
150
|
+
table_str = ''
|
151
|
+
for volume_type, volume_list in volumes_per_type.items():
|
152
|
+
if volume_type == volume.VolumeType.PVC.value:
|
153
|
+
table = PVCVolumeTable(volume_list, show_all)
|
154
|
+
table_str += table.format()
|
155
|
+
if table_str:
|
156
|
+
return table_str
|
157
|
+
else:
|
158
|
+
return 'No existing volumes.'
|
sky/volumes/volume.py
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
"""Volume types and access modes."""
|
2
|
+
import enum
|
3
|
+
import time
|
4
|
+
from typing import Any, Dict, Optional
|
5
|
+
|
6
|
+
from sky import exceptions
|
7
|
+
from sky import global_user_state
|
8
|
+
from sky import models
|
9
|
+
from sky.utils import common_utils
|
10
|
+
from sky.utils import infra_utils
|
11
|
+
from sky.utils import resources_utils
|
12
|
+
from sky.utils import schemas
|
13
|
+
from sky.utils import status_lib
|
14
|
+
|
15
|
+
|
16
|
+
class VolumeType(enum.Enum):
|
17
|
+
"""Volume type."""
|
18
|
+
PVC = 'k8s-pvc'
|
19
|
+
|
20
|
+
|
21
|
+
class VolumeAccessMode(enum.Enum):
|
22
|
+
"""Volume access mode."""
|
23
|
+
READ_WRITE_ONCE = 'ReadWriteOnce'
|
24
|
+
READ_WRITE_ONCE_POD = 'ReadWriteOncePod'
|
25
|
+
READ_WRITE_MANY = 'ReadWriteMany'
|
26
|
+
READ_ONLY_MANY = 'ReadOnlyMany'
|
27
|
+
|
28
|
+
|
29
|
+
class VolumeMount:
|
30
|
+
"""Volume mount specification."""
|
31
|
+
|
32
|
+
def __init__(self, path: str, volume_name: str,
|
33
|
+
volume_config: models.VolumeConfig):
|
34
|
+
self.path: str = path
|
35
|
+
self.volume_name: str = volume_name
|
36
|
+
self.volume_config: models.VolumeConfig = volume_config
|
37
|
+
|
38
|
+
def pre_mount(self) -> None:
|
39
|
+
"""Update the volume status before actual mounting."""
|
40
|
+
# TODO(aylei): for ReadWriteOnce volume, we also need to queue the
|
41
|
+
# mount request if the target volume is already mounted to another
|
42
|
+
# cluster. For now, we only support ReadWriteMany volume.
|
43
|
+
global_user_state.update_volume(self.volume_name,
|
44
|
+
last_attached_at=int(time.time()),
|
45
|
+
status=status_lib.VolumeStatus.IN_USE)
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def resolve(cls, path: str, volume_name: str) -> 'VolumeMount':
|
49
|
+
"""Resolve the volume mount by populating metadata of volume."""
|
50
|
+
record = global_user_state.get_volume_by_name(volume_name)
|
51
|
+
if record is None:
|
52
|
+
raise exceptions.VolumeNotFoundError(
|
53
|
+
f'Volume {volume_name} not found.')
|
54
|
+
assert 'handle' in record, 'Volume handle is None.'
|
55
|
+
volume_config: models.VolumeConfig = record['handle']
|
56
|
+
return cls(path, volume_name, volume_config)
|
57
|
+
|
58
|
+
@classmethod
|
59
|
+
def from_yaml_config(cls, config: Dict[str, Any]) -> 'VolumeMount':
|
60
|
+
common_utils.validate_schema(config, schemas.get_volume_mount_schema(),
|
61
|
+
'Invalid volume mount config: ')
|
62
|
+
|
63
|
+
path = config.pop('path', None)
|
64
|
+
volume_name = config.pop('volume_name', None)
|
65
|
+
volume_config: models.VolumeConfig = models.VolumeConfig.model_validate(
|
66
|
+
config.pop('volume_config', None))
|
67
|
+
return cls(path, volume_name, volume_config)
|
68
|
+
|
69
|
+
def to_yaml_config(self) -> Dict[str, Any]:
|
70
|
+
return {
|
71
|
+
'path': self.path,
|
72
|
+
'volume_name': self.volume_name,
|
73
|
+
'volume_config': self.volume_config.model_dump(),
|
74
|
+
}
|
75
|
+
|
76
|
+
def __repr__(self):
|
77
|
+
return (f'VolumeMount('
|
78
|
+
f'\n\tpath={self.path},'
|
79
|
+
f'\n\tvolume_name={self.volume_name},'
|
80
|
+
f'\n\tvolume_config={self.volume_config})')
|
81
|
+
|
82
|
+
|
83
|
+
class Volume:
|
84
|
+
"""Volume specification."""
|
85
|
+
|
86
|
+
def __init__(
|
87
|
+
self,
|
88
|
+
name: Optional[str] = None,
|
89
|
+
type: Optional[str] = None, # pylint: disable=redefined-builtin
|
90
|
+
infra: Optional[str] = None,
|
91
|
+
size: Optional[str] = None,
|
92
|
+
resource_name: Optional[str] = None,
|
93
|
+
config: Optional[Dict[str, Any]] = None):
|
94
|
+
"""Initialize a Volume instance.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
name: Volume name
|
98
|
+
type: Volume type (e.g., 'k8s-pvc')
|
99
|
+
infra: Infrastructure specification
|
100
|
+
size: Volume size
|
101
|
+
config: Additional configuration
|
102
|
+
"""
|
103
|
+
self.name = name
|
104
|
+
self.type = type
|
105
|
+
self.infra = infra
|
106
|
+
self.size = size
|
107
|
+
self.resource_name = resource_name
|
108
|
+
self.config = config or {}
|
109
|
+
|
110
|
+
self.cloud: Optional[str] = None
|
111
|
+
self.region: Optional[str] = None
|
112
|
+
self.zone: Optional[str] = None
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def from_dict(cls, config_dict: Dict[str, Any]) -> 'Volume':
|
116
|
+
"""Create a Volume instance from a dictionary."""
|
117
|
+
return cls(name=config_dict.get('name'),
|
118
|
+
type=config_dict.get('type'),
|
119
|
+
infra=config_dict.get('infra'),
|
120
|
+
size=config_dict.get('size'),
|
121
|
+
resource_name=config_dict.get('resource_name'),
|
122
|
+
config=config_dict.get('config', {}))
|
123
|
+
|
124
|
+
def to_dict(self) -> Dict[str, Any]:
|
125
|
+
"""Convert the Volume to a dictionary."""
|
126
|
+
return {
|
127
|
+
'name': self.name,
|
128
|
+
'type': self.type,
|
129
|
+
'infra': self.infra,
|
130
|
+
'size': self.size,
|
131
|
+
'resource_name': self.resource_name,
|
132
|
+
'config': self.config,
|
133
|
+
'cloud': self.cloud,
|
134
|
+
'region': self.region,
|
135
|
+
'zone': self.zone,
|
136
|
+
}
|
137
|
+
|
138
|
+
def normalize_config(
|
139
|
+
self,
|
140
|
+
name: Optional[str] = None,
|
141
|
+
infra: Optional[str] = None,
|
142
|
+
type: Optional[str] = None, # pylint: disable=redefined-builtin
|
143
|
+
size: Optional[str] = None) -> None:
|
144
|
+
"""Override the volume config with CLI options,
|
145
|
+
adjust and validate the config.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
name: Volume name to override
|
149
|
+
infra: Infrastructure to override
|
150
|
+
type: Volume type to override
|
151
|
+
size: Volume size to override
|
152
|
+
"""
|
153
|
+
if name is not None:
|
154
|
+
self.name = name
|
155
|
+
if infra is not None:
|
156
|
+
self.infra = infra
|
157
|
+
if type is not None:
|
158
|
+
self.type = type
|
159
|
+
if size is not None:
|
160
|
+
self.size = size
|
161
|
+
|
162
|
+
# Validate schema
|
163
|
+
common_utils.validate_schema(self.to_dict(),
|
164
|
+
schemas.get_volume_schema(),
|
165
|
+
'Invalid volumes config: ')
|
166
|
+
|
167
|
+
# Adjust the volume config (e.g., parse size)
|
168
|
+
self._adjust_config()
|
169
|
+
|
170
|
+
# Validate the volume config
|
171
|
+
self._validate_config()
|
172
|
+
|
173
|
+
# Resolve the infrastructure options to cloud, region, zone
|
174
|
+
infra_info = infra_utils.InfraInfo.from_str(self.infra)
|
175
|
+
self.cloud = infra_info.cloud
|
176
|
+
self.region = infra_info.region
|
177
|
+
self.zone = infra_info.zone
|
178
|
+
|
179
|
+
def _adjust_config(self) -> None:
|
180
|
+
"""Adjust the volume config (e.g., parse size)."""
|
181
|
+
if self.size is None:
|
182
|
+
return
|
183
|
+
try:
|
184
|
+
size = resources_utils.parse_memory_resource(self.size,
|
185
|
+
'size',
|
186
|
+
allow_rounding=True)
|
187
|
+
if size == '0':
|
188
|
+
raise ValueError('Size must be no less than 1Gi')
|
189
|
+
self.size = size
|
190
|
+
except ValueError as e:
|
191
|
+
raise ValueError(f'Invalid size {self.size}: {e}') from e
|
192
|
+
|
193
|
+
def _validate_config(self) -> None:
|
194
|
+
"""Validate the volume config."""
|
195
|
+
if not self.resource_name and not self.size:
|
196
|
+
raise ValueError('Size is required for new volumes. '
|
197
|
+
'Please specify the size in the YAML file or '
|
198
|
+
'use the --size flag.')
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: skypilot-nightly
|
3
|
-
Version: 1.0.0.
|
3
|
+
Version: 1.0.0.dev20250626
|
4
4
|
Summary: SkyPilot: Run AI on Any Infra — Unified, Faster, Cheaper.
|
5
5
|
Author: SkyPilot Team
|
6
6
|
License: Apache 2.0
|
@@ -51,6 +51,7 @@ Requires-Dist: sqlalchemy
|
|
51
51
|
Requires-Dist: psycopg2-binary
|
52
52
|
Requires-Dist: casbin
|
53
53
|
Requires-Dist: sqlalchemy_adapter
|
54
|
+
Requires-Dist: prometheus_client>=0.8.0
|
54
55
|
Requires-Dist: passlib
|
55
56
|
Provides-Extra: aws
|
56
57
|
Requires-Dist: awscli>=1.27.10; extra == "aws"
|