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
@@ -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.dev20250624
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"