skypilot-nightly 1.0.0.dev20250520__py3-none-any.whl → 1.0.0.dev20250521__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 (62) hide show
  1. sky/__init__.py +2 -2
  2. sky/backends/backend_utils.py +4 -1
  3. sky/backends/cloud_vm_ray_backend.py +56 -37
  4. sky/check.py +3 -3
  5. sky/cli.py +89 -16
  6. sky/client/cli.py +89 -16
  7. sky/client/sdk.py +20 -3
  8. sky/dashboard/out/404.html +1 -1
  9. sky/dashboard/out/_next/static/chunks/236-1a3a9440417720eb.js +6 -0
  10. sky/dashboard/out/_next/static/chunks/37-d584022b0da4ac3b.js +6 -0
  11. sky/dashboard/out/_next/static/chunks/393-e1eaa440481337ec.js +1 -0
  12. sky/dashboard/out/_next/static/chunks/480-f28cd152a98997de.js +1 -0
  13. sky/dashboard/out/_next/static/chunks/{678-206dddca808e6d16.js → 582-683f4f27b81996dc.js} +2 -2
  14. sky/dashboard/out/_next/static/chunks/pages/_app-8cfab319f9fb3ae8.js +1 -0
  15. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-33bc2bec322249b1.js +1 -0
  16. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-e2fc2dd1955e6c36.js +1 -0
  17. sky/dashboard/out/_next/static/chunks/pages/clusters-3a748bd76e5c2984.js +1 -0
  18. sky/dashboard/out/_next/static/chunks/pages/infra-9180cd91cee64b96.js +1 -0
  19. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-70756c2dad850a7e.js +1 -0
  20. sky/dashboard/out/_next/static/chunks/pages/jobs-ecd804b9272f4a7c.js +1 -0
  21. sky/dashboard/out/_next/static/css/7e7ce4ff31d3977b.css +3 -0
  22. sky/dashboard/out/_next/static/hvWzC5E6Q4CcKzXcWbgig/_buildManifest.js +1 -0
  23. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  24. sky/dashboard/out/clusters/[cluster].html +1 -1
  25. sky/dashboard/out/clusters.html +1 -1
  26. sky/dashboard/out/index.html +1 -1
  27. sky/dashboard/out/infra.html +1 -0
  28. sky/dashboard/out/jobs/[job].html +1 -1
  29. sky/dashboard/out/jobs.html +1 -1
  30. sky/execution.py +1 -1
  31. sky/jobs/server/core.py +1 -1
  32. sky/jobs/utils.py +38 -7
  33. sky/optimizer.py +36 -29
  34. sky/provision/provisioner.py +16 -7
  35. sky/resources.py +60 -15
  36. sky/serve/serve_utils.py +5 -13
  37. sky/server/common.py +14 -5
  38. sky/server/requests/payloads.py +3 -3
  39. sky/utils/cli_utils/status_utils.py +95 -56
  40. sky/utils/common_utils.py +35 -2
  41. sky/utils/infra_utils.py +175 -0
  42. sky/utils/resources_utils.py +41 -21
  43. sky/utils/schemas.py +65 -5
  44. {skypilot_nightly-1.0.0.dev20250520.dist-info → skypilot_nightly-1.0.0.dev20250521.dist-info}/METADATA +1 -1
  45. {skypilot_nightly-1.0.0.dev20250520.dist-info → skypilot_nightly-1.0.0.dev20250521.dist-info}/RECORD +50 -47
  46. {skypilot_nightly-1.0.0.dev20250520.dist-info → skypilot_nightly-1.0.0.dev20250521.dist-info}/WHEEL +1 -1
  47. sky/dashboard/out/_next/static/8hlc2dkbIDDBOkxtEW7X6/_buildManifest.js +0 -1
  48. sky/dashboard/out/_next/static/chunks/236-f49500b82ad5392d.js +0 -6
  49. sky/dashboard/out/_next/static/chunks/37-0a572fe0dbb89c4d.js +0 -6
  50. sky/dashboard/out/_next/static/chunks/845-0ca6f2c1ba667c3b.js +0 -1
  51. sky/dashboard/out/_next/static/chunks/979-7bf73a4c7cea0f5c.js +0 -1
  52. sky/dashboard/out/_next/static/chunks/pages/_app-e6b013bc3f77ad60.js +0 -1
  53. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]/[job]-e15db85d0ea1fbe1.js +0 -1
  54. sky/dashboard/out/_next/static/chunks/pages/clusters/[cluster]-f383db7389368ea7.js +0 -1
  55. sky/dashboard/out/_next/static/chunks/pages/clusters-a93b93e10b8b074e.js +0 -1
  56. sky/dashboard/out/_next/static/chunks/pages/jobs/[job]-03f279c6741fb48b.js +0 -1
  57. sky/dashboard/out/_next/static/chunks/pages/jobs-a75029b67aab6a2e.js +0 -1
  58. sky/dashboard/out/_next/static/css/c6933bbb2ce7f4dd.css +0 -3
  59. /sky/dashboard/out/_next/static/{8hlc2dkbIDDBOkxtEW7X6 → hvWzC5E6Q4CcKzXcWbgig}/_ssgManifest.js +0 -0
  60. {skypilot_nightly-1.0.0.dev20250520.dist-info → skypilot_nightly-1.0.0.dev20250521.dist-info}/entry_points.txt +0 -0
  61. {skypilot_nightly-1.0.0.dev20250520.dist-info → skypilot_nightly-1.0.0.dev20250521.dist-info}/licenses/LICENSE +0 -0
  62. {skypilot_nightly-1.0.0.dev20250520.dist-info → skypilot_nightly-1.0.0.dev20250521.dist-info}/top_level.txt +0 -0
@@ -33,17 +33,15 @@ class StatusColumn:
33
33
  def __init__(self,
34
34
  name: str,
35
35
  calc_func: Callable,
36
- trunc_length: int = 0,
36
+ truncate: bool = True,
37
37
  show_by_default: bool = True):
38
38
  self.name = name
39
39
  self.calc_func = calc_func
40
- self.trunc_length = trunc_length
40
+ self.truncate: bool = truncate
41
41
  self.show_by_default = show_by_default
42
42
 
43
43
  def calc(self, record):
44
- val = self.calc_func(record)
45
- if self.trunc_length != 0:
46
- val = common_utils.truncate_long_string(str(val), self.trunc_length)
44
+ val = self.calc_func(record, self.truncate)
47
45
  return val
48
46
 
49
47
 
@@ -68,19 +66,20 @@ def show_status_table(cluster_records: List[_ClusterRecord],
68
66
  StatusColumn('USER_ID', _get_user_hash, show_by_default=False))
69
67
 
70
68
  status_columns += [
71
- StatusColumn('LAUNCHED', _get_launched),
72
- StatusColumn('RESOURCES',
73
- _get_resources,
74
- trunc_length=70 if not show_all else 0),
75
- StatusColumn('REGION', _get_region, show_by_default=False),
76
- StatusColumn('ZONE', _get_zone, show_by_default=False),
69
+ StatusColumn('INFRA', _get_infra, truncate=not show_all),
70
+ StatusColumn('RESOURCES', _get_resources, truncate=not show_all),
77
71
  StatusColumn('STATUS', _get_status_colored),
78
72
  StatusColumn('AUTOSTOP', _get_autostop),
79
- StatusColumn('HEAD_IP', _get_head_ip, show_by_default=False),
80
- StatusColumn('COMMAND',
81
- _get_command,
82
- trunc_length=COMMAND_TRUNC_LENGTH if not show_all else 0),
73
+ StatusColumn('LAUNCHED', _get_launched),
83
74
  ]
75
+ if show_all:
76
+ status_columns += [
77
+ StatusColumn('HEAD_IP', _get_head_ip, show_by_default=False),
78
+ StatusColumn('COMMAND',
79
+ _get_command,
80
+ truncate=not show_all,
81
+ show_by_default=False),
82
+ ]
84
83
 
85
84
  columns = []
86
85
  for status_column in status_columns:
@@ -160,10 +159,10 @@ def show_cost_report_table(cluster_records: List[_ClusterCostReportRecord],
160
159
  status_columns = [
161
160
  StatusColumn('NAME', _get_name),
162
161
  StatusColumn('LAUNCHED', _get_launched),
163
- StatusColumn('DURATION', _get_duration, trunc_length=20),
162
+ StatusColumn('DURATION', _get_duration, truncate=False),
164
163
  StatusColumn('RESOURCES',
165
164
  _get_resources_for_cost_report,
166
- trunc_length=70 if not show_all else 0),
165
+ truncate=False),
167
166
  StatusColumn('STATUS',
168
167
  _get_status_for_cost_report,
169
168
  show_by_default=True),
@@ -221,47 +220,68 @@ def show_cost_report_table(cluster_records: List[_ClusterCostReportRecord],
221
220
  # Some of these lambdas are invoked on both _ClusterRecord and
222
221
  # _ClusterCostReportRecord, which is okay as we guarantee the queried fields
223
222
  # exist in those cases.
224
- _get_name = (lambda cluster_record: cluster_record['name'])
225
- _get_user_hash = (lambda cluster_record: cluster_record['user_hash'])
226
- _get_user_name = (lambda cluster_record: cluster_record.get('user_name', '-'))
227
- _get_launched = (lambda cluster_record: log_utils.readable_time_duration(
223
+ _get_name = (lambda cluster_record, _: cluster_record['name'])
224
+ _get_user_hash = (lambda cluster_record, _: cluster_record['user_hash'])
225
+ _get_user_name = (
226
+ lambda cluster_record, _: cluster_record.get('user_name', '-'))
227
+ _get_launched = (lambda cluster_record, _: log_utils.readable_time_duration(
228
228
  cluster_record['launched_at']))
229
- _get_region = (
230
- lambda clusters_status: clusters_status['handle'].launched_resources.region)
231
- _get_command = (lambda cluster_record: cluster_record['last_use'])
232
- _get_duration = (lambda cluster_record: log_utils.readable_time_duration(
229
+ _get_duration = (lambda cluster_record, _: log_utils.readable_time_duration(
233
230
  0, cluster_record['duration'], absolute=True))
234
231
 
235
232
 
236
- def _get_status(cluster_record: _ClusterRecord) -> status_lib.ClusterStatus:
237
- return cluster_record['status']
238
-
233
+ def _get_command(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
234
+ command = cluster_record.get('last_use', '-')
235
+ if truncate:
236
+ return common_utils.truncate_long_string(command, COMMAND_TRUNC_LENGTH)
237
+ return command
239
238
 
240
- def _get_status_colored(cluster_record: _ClusterRecord) -> str:
241
- return _get_status(cluster_record).colored_str()
242
239
 
240
+ def _get_status(cluster_record: _ClusterRecord,
241
+ truncate: bool = True) -> status_lib.ClusterStatus:
242
+ del truncate
243
+ return cluster_record['status']
243
244
 
244
- def _get_resources(cluster_record: _ClusterRecord) -> str:
245
- if 'resources_str' in cluster_record:
246
- return cluster_record['resources_str']
247
- handle = cluster_record['handle']
248
- if isinstance(handle, backends.LocalDockerResourceHandle):
249
- resources_str = 'docker'
250
- elif isinstance(handle, backends.CloudVmRayResourceHandle):
251
- resources_str = resources_utils.get_readable_resources_repr(handle)
252
- else:
253
- raise ValueError(f'Unknown handle type {type(handle)} encountered.')
254
- return resources_str
255
245
 
246
+ def _get_status_colored(cluster_record: _ClusterRecord,
247
+ truncate: bool = True) -> str:
248
+ del truncate
249
+ return _get_status(cluster_record).colored_str()
256
250
 
257
- def _get_zone(cluster_record: _ClusterRecord) -> str:
258
- zone_str = cluster_record['handle'].launched_resources.zone
259
- if zone_str is None:
260
- zone_str = '-'
261
- return zone_str
262
251
 
252
+ def _get_resources(cluster_record: _ClusterRecord,
253
+ truncate: bool = True) -> str:
254
+ """Get the resources information for a cluster.
263
255
 
264
- def _get_autostop(cluster_record: _ClusterRecord) -> str:
256
+ Returns:
257
+ A string in one of the following formats:
258
+ - For cloud VMs: "Nx instance_type" (e.g., "1x m6i.2xlarge")
259
+ - For K8S/SSH: "Nx (...)"
260
+ - "-" if no resource information is available
261
+ """
262
+ handle = cluster_record['handle']
263
+ if isinstance(handle, backends.CloudVmRayResourceHandle):
264
+ launched_resources = handle.launched_resources
265
+ if launched_resources is None:
266
+ return '-'
267
+
268
+ # For cloud VMs, show instance type directly
269
+ # For K8S/SSH, show (...) as the resource type
270
+ resources_str = cluster_record.get('resources_str', None)
271
+ if not truncate:
272
+ resources_str_full = cluster_record.get('resources_str_full', None)
273
+ if resources_str_full is not None:
274
+ resources_str = resources_str_full
275
+ if resources_str is None:
276
+ resources_str = resources_utils.get_readable_resources_repr(
277
+ handle, simplify=truncate)
278
+
279
+ return resources_str
280
+ return '-'
281
+
282
+
283
+ def _get_autostop(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
284
+ del truncate
265
285
  autostop_str = ''
266
286
  separation = ''
267
287
  if cluster_record['autostop'] >= 0:
@@ -276,7 +296,8 @@ def _get_autostop(cluster_record: _ClusterRecord) -> str:
276
296
  return autostop_str
277
297
 
278
298
 
279
- def _get_head_ip(cluster_record: _ClusterRecord) -> str:
299
+ def _get_head_ip(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
300
+ del truncate # Unused
280
301
  handle = cluster_record['handle']
281
302
  if not isinstance(handle, backends.CloudVmRayResourceHandle):
282
303
  return '-'
@@ -291,6 +312,25 @@ def _is_pending_autostop(cluster_record: _ClusterRecord) -> bool:
291
312
  cluster_record) != status_lib.ClusterStatus.STOPPED
292
313
 
293
314
 
315
+ def _get_infra(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
316
+ """Get the infrastructure information for a cluster.
317
+
318
+ Returns:
319
+ A string in one of the following formats:
320
+ - AWS/region (e.g., "AWS/us-east-1")
321
+ - K8S/context (e.g., "K8S/my-ctx")
322
+ - SSH/hostname (e.g., "SSH/my-tobi-box")
323
+ - "-" if no infrastructure information is available
324
+ """
325
+ handle = cluster_record['handle']
326
+ if isinstance(handle, backends.CloudVmRayResourceHandle):
327
+ if handle.launched_resources is None:
328
+ # If launched_resources is None, try to get infra from the record
329
+ return cluster_record.get('infra', '-')
330
+ return handle.launched_resources.infra.formatted_str(truncate)
331
+ return '-'
332
+
333
+
294
334
  # ---- 'sky cost-report' helper functions below ----
295
335
 
296
336
 
@@ -347,14 +387,13 @@ def show_kubernetes_cluster_status_table(
347
387
  show_all: bool) -> None:
348
388
  """Compute cluster table values and display for Kubernetes clusters."""
349
389
  status_columns = [
350
- StatusColumn('USER', lambda c: c.user),
351
- StatusColumn('NAME', lambda c: c.cluster_name),
352
- StatusColumn('LAUNCHED',
353
- lambda c: log_utils.readable_time_duration(c.launched_at)),
354
- StatusColumn('RESOURCES',
355
- lambda c: c.resources_str,
356
- trunc_length=70 if not show_all else 0),
357
- StatusColumn('STATUS', lambda c: c.status.colored_str()),
390
+ StatusColumn('USER', lambda c, _: c.user),
391
+ StatusColumn('NAME', lambda c, _: c.cluster_name),
392
+ StatusColumn('RESOURCES', lambda c, _: c.resources_str, truncate=False),
393
+ StatusColumn('STATUS', lambda c, _: c.status.colored_str()),
394
+ StatusColumn(
395
+ 'LAUNCHED',
396
+ lambda c, _: log_utils.readable_time_duration(c.launched_at)),
358
397
  # TODO(romilb): We should consider adding POD_NAME field here when --all
359
398
  # is passed to help users fetch pod name programmatically.
360
399
  ]
sky/utils/common_utils.py CHANGED
@@ -723,10 +723,43 @@ def deprecated_function(
723
723
  return new_func
724
724
 
725
725
 
726
- def truncate_long_string(s: str, max_length: int = 35) -> str:
727
- """Truncate a string to a maximum length, preserving whole words."""
726
+ def truncate_long_string(s: str,
727
+ max_length: int = 35,
728
+ truncate_middle: bool = False) -> str:
729
+ """Truncate a string to a maximum length.
730
+
731
+ Args:
732
+ s: String to truncate.
733
+ max_length: Maximum length of the truncated string.
734
+ truncate_middle: Whether to truncate in the middle of the string.
735
+ If True, the middle part of the string is replaced with '...'.
736
+ If False, truncation happens at the end preserving whole words.
737
+
738
+ Returns:
739
+ Truncated string.
740
+ """
728
741
  if len(s) <= max_length:
729
742
  return s
743
+
744
+ if truncate_middle:
745
+ # Reserve 3 characters for '...'
746
+ if max_length <= 3:
747
+ return '...'
748
+
749
+ # Calculate how many characters to keep from beginning and end
750
+ half_length = (max_length - 3) // 2
751
+ remainder = (max_length - 3) % 2
752
+
753
+ # Keep one more character at the beginning if max_length - 3 is odd
754
+ start_length = half_length + remainder
755
+ end_length = half_length
756
+
757
+ # When end_length is 0, just show the start part and '...'
758
+ if end_length == 0:
759
+ return s[:start_length] + '...'
760
+ return s[:start_length] + '...' + s[-end_length:]
761
+
762
+ # Original end-truncation logic
730
763
  splits = s.split(' ')
731
764
  if len(splits[0]) > max_length:
732
765
  return splits[0][:max_length] + '...' # Use '…'?
@@ -0,0 +1,175 @@
1
+ """Utility functions for handling infrastructure specifications."""
2
+ import dataclasses
3
+ from typing import Optional
4
+
5
+ from sky.utils import common_utils
6
+ from sky.utils import ux_utils
7
+
8
+ _REGION_OR_ZONE_TRUNCATION_LENGTH = 25
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class InfraInfo:
13
+ """Infrastructure information parsed from infra string.
14
+
15
+ When a field is None, it means the field is not specified.
16
+ """
17
+ cloud: Optional[str] = None
18
+ region: Optional[str] = None
19
+ zone: Optional[str] = None
20
+
21
+ def __init__(self,
22
+ cloud: Optional[str] = None,
23
+ region: Optional[str] = None,
24
+ zone: Optional[str] = None):
25
+ assert cloud not in ['none', 'None', 'NONE'], 'cloud must be specified'
26
+ if not cloud or cloud == '*':
27
+ cloud = None
28
+ if not region or region == '*':
29
+ region = None
30
+ if not zone or zone == '*':
31
+ zone = None
32
+
33
+ self.cloud = cloud
34
+ self.region = region
35
+ self.zone = zone
36
+
37
+ @staticmethod
38
+ def from_str(infra: Optional[str]) -> 'InfraInfo':
39
+ """Parse the infra string into cloud, region, and zone components.
40
+
41
+ The format of the infra string is `cloud`, `cloud/region`, or
42
+ `cloud/region/zone`. Examples: `aws`, `aws/us-east-1`,
43
+ `aws/us-east-1/us-east-1a`. For any field, you can use `*` to indicate
44
+ that any value is acceptable.
45
+
46
+ If `*` is used for any field, the InfraInfo will have None for that
47
+ field.
48
+
49
+ Args:
50
+ infra: A string in the format of `cloud`, `cloud/region`, or
51
+ `cloud/region/zone`. Examples: `aws`, `aws/us-east-1`,
52
+ `aws/us-east-1/us-east-1a`.
53
+
54
+ Returns:
55
+ An InfraInfo object containing cloud, region, and zone information.
56
+
57
+ Raises:
58
+ ValueError: If the infra string is malformed.
59
+ """
60
+ if infra is None or not infra.strip():
61
+ return InfraInfo()
62
+
63
+ infra = infra.strip().strip('/')
64
+
65
+ # Split on / to get cloud, region, zone
66
+ parts = [p.strip() for p in infra.strip().split('/')]
67
+
68
+ if '' in parts:
69
+ with ux_utils.print_exception_no_traceback():
70
+ raise ValueError(
71
+ f'Invalid infra format: {infra}. Format should not contain '
72
+ 'empty parts (e.g., double slashes "//").')
73
+
74
+ if not parts or not parts[0]:
75
+ with ux_utils.print_exception_no_traceback():
76
+ raise ValueError(
77
+ f'Invalid infra format: {infra}. Expected format is '
78
+ '"cloud", "cloud/region", or "cloud/region/zone".')
79
+
80
+ cloud_name: Optional[str] = parts[0].lower()
81
+
82
+ # Handle Kubernetes contexts specially, as they can contain slashes
83
+ if cloud_name in ['k8s', 'kubernetes']:
84
+ # For Kubernetes, the entire string after "k8s/" is the
85
+ # context name (region)
86
+ cloud_name = 'kubernetes' # Normalize k8s to kubernetes
87
+ region = '/'.join(parts[1:]) if len(parts) >= 2 else None
88
+ zone = None
89
+ else:
90
+ # For non-Kubernetes clouds, continue with regular parsing
91
+ # but be careful to only split into max 3 parts
92
+ region_zone_parts = parts[1:]
93
+ region = None
94
+ zone = None
95
+ if region_zone_parts:
96
+ region = region_zone_parts[0]
97
+ if len(region_zone_parts) > 1:
98
+ zone = region_zone_parts[1]
99
+ if len(region_zone_parts) > 2:
100
+ with ux_utils.print_exception_no_traceback():
101
+ raise ValueError(
102
+ f'Invalid infra format: {infra}. Expected format '
103
+ 'is "cloud", "cloud/region", or '
104
+ '"cloud/region/zone".')
105
+
106
+ if cloud_name == '*':
107
+ cloud_name = None
108
+ if region == '*':
109
+ region = None
110
+ if zone == '*':
111
+ zone = None
112
+ return InfraInfo(cloud=cloud_name, region=region, zone=zone)
113
+
114
+ def to_str(self) -> Optional[str]:
115
+ """Formats cloud, region, and zone into an infra string.
116
+
117
+ Args:
118
+ cloud: The cloud object
119
+ region: The region name
120
+ zone: The zone name
121
+
122
+ Returns:
123
+ A formatted infra string, or None if cloud is None or '*'
124
+ """
125
+ cloud = self.cloud
126
+ region = self.region
127
+ zone = self.zone
128
+
129
+ if cloud is None:
130
+ cloud = '*'
131
+ if region is None:
132
+ region = '*'
133
+ if zone is None:
134
+ zone = '*'
135
+
136
+ # Build the parts list and filter out trailing wildcards
137
+ parts = [cloud.lower(), region, zone]
138
+ while parts and parts[-1] == '*':
139
+ parts.pop()
140
+
141
+ if not parts:
142
+ return None
143
+
144
+ # Join the parts with '/'
145
+ return '/'.join(parts)
146
+
147
+ def formatted_str(self, truncate: bool = True) -> str:
148
+ """Formats cloud, region, and zone into an infra string.
149
+
150
+ Args:
151
+ truncate: Whether to truncate the region or zone
152
+
153
+ Returns:
154
+ A formatted infra string, or None if cloud is None or '*'
155
+ """
156
+ if self.cloud is None or self.cloud == '*':
157
+ return '-'
158
+
159
+ region_or_zone = None
160
+ if self.zone is not None and self.zone != '*':
161
+ region_or_zone = self.zone
162
+ elif self.region is not None and self.region != '*':
163
+ region_or_zone = self.region
164
+
165
+ if region_or_zone is not None and truncate:
166
+ region_or_zone = common_utils.truncate_long_string(
167
+ region_or_zone,
168
+ _REGION_OR_ZONE_TRUNCATION_LENGTH,
169
+ truncate_middle=True)
170
+
171
+ formatted_str = f'{self.cloud}'
172
+ if region_or_zone is not None:
173
+ formatted_str += f' ({region_or_zone})'
174
+
175
+ return formatted_str
@@ -4,11 +4,11 @@ import enum
4
4
  import itertools
5
5
  import json
6
6
  import math
7
- import re
8
7
  import typing
9
8
  from typing import Dict, List, Optional, Set, Union
10
9
 
11
10
  from sky import skypilot_config
11
+ from sky.utils import common_utils
12
12
  from sky.utils import registry
13
13
  from sky.utils import ux_utils
14
14
 
@@ -139,34 +139,54 @@ def simplify_ports(ports: List[str]) -> List[str]:
139
139
 
140
140
  def format_resource(resource: 'resources_lib.Resources',
141
141
  simplify: bool = False) -> str:
142
+ resource = resource.assert_launchable()
143
+ vcpu, mem = resource.cloud.get_vcpus_mem_from_instance_type(
144
+ resource.instance_type)
145
+
146
+ components = []
147
+
148
+ if resource.accelerators is not None:
149
+ acc, count = list(resource.accelerators.items())[0]
150
+ components.append(f'gpus={acc}:{count}')
151
+
152
+ is_k8s = str(resource.cloud).lower() == 'kubernetes'
153
+ if (resource.accelerators is None or is_k8s or not simplify):
154
+ if vcpu is not None:
155
+ components.append(f'cpus={int(vcpu)}')
156
+ if mem is not None:
157
+ components.append(f'mem={int(mem)}')
158
+
159
+ instance_type = resource.instance_type
142
160
  if simplify:
143
- resource = resource.assert_launchable()
144
- cloud = resource.cloud
145
- if resource.accelerators is None:
146
- vcpu, _ = cloud.get_vcpus_mem_from_instance_type(
147
- resource.instance_type)
148
- assert vcpu is not None, 'vCPU must be specified'
149
- hardware = f'vCPU={int(vcpu)}'
150
- else:
151
- hardware = f'{resource.accelerators}'
152
- spot = '[Spot]' if resource.use_spot else ''
153
- return f'{cloud}({spot}{hardware})'
161
+ instance_type = common_utils.truncate_long_string(instance_type, 15)
162
+ if not is_k8s:
163
+ components.append(instance_type)
164
+ if simplify:
165
+ components.append('...')
154
166
  else:
155
- # accelerator_args is way too long.
156
- # Convert from:
157
- # GCP(n1-highmem-8, {'tpu-v2-8': 1}, accelerator_args={'runtime_version': '2.12.0'} # pylint: disable=line-too-long
158
- # to:
159
- # GCP(n1-highmem-8, {'tpu-v2-8': 1}...)
160
- pattern = ', accelerator_args={.*}'
161
- launched_resource_str = re.sub(pattern, '...', str(resource))
162
- return launched_resource_str
167
+ image_id = resource.image_id
168
+ if image_id is not None:
169
+ if None in image_id:
170
+ components.append(f'image_id={image_id[None]}')
171
+ else:
172
+ components.append(f'image_id={image_id}')
173
+ components.append(f'disk={resource.disk_size}')
174
+ disk_tier = resource.disk_tier
175
+ if disk_tier is not None:
176
+ components.append(f'disk_tier={disk_tier.value}')
177
+ ports = resource.ports
178
+ if ports is not None:
179
+ components.append(f'ports={ports}')
180
+
181
+ spot = '[spot]' if resource.use_spot else ''
182
+ return f'{spot}({"" if not components else ", ".join(components)})'
163
183
 
164
184
 
165
185
  def get_readable_resources_repr(handle: 'backends.CloudVmRayResourceHandle',
166
186
  simplify: bool = False) -> str:
167
187
  if (handle.launched_nodes is not None and
168
188
  handle.launched_resources is not None):
169
- return (f'{handle.launched_nodes}x '
189
+ return (f'{handle.launched_nodes}x'
170
190
  f'{format_resource(handle.launched_resources, simplify)}')
171
191
  return _DEFAULT_MESSAGE_HANDLE_INITIALIZING
172
192
 
sky/utils/schemas.py CHANGED
@@ -69,6 +69,39 @@ def _get_single_resources_schema():
69
69
  # To avoid circular imports, only import when needed.
70
70
  # pylint: disable=import-outside-toplevel
71
71
  from sky.clouds import service_catalog
72
+
73
+ # Building the regex pattern for the infra field
74
+ # Format: cloud[/region[/zone]] or wildcards or kubernetes context
75
+ # Match any cloud name (case insensitive)
76
+ all_clouds = list(service_catalog.ALL_CLOUDS)
77
+ all_clouds.remove('kubernetes')
78
+ cloud_pattern = f'(?i:({"|".join(all_clouds)}))'
79
+
80
+ # Optional /region followed by optional /zone
81
+ # /[^/]+ matches a slash followed by any characters except slash (region or
82
+ # zone name)
83
+ # The outer (?:...)? makes the entire region/zone part optional
84
+ region_zone_pattern = '(?:/[^/]+(?:/[^/]+)?)?'
85
+
86
+ # Wildcard patterns:
87
+ # 1. * - any cloud
88
+ # 2. */region - any cloud with specific region
89
+ # 3. */*/zone - any cloud, any region, specific zone
90
+ wildcard_cloud = '\\*' # Wildcard for cloud
91
+ wildcard_with_region = '(?:/[^/]+(?:/[^/]+)?)?'
92
+
93
+ # Kubernetes specific pattern - matches:
94
+ # 1. Just the word "kubernetes" or "k8s" by itself
95
+ # 2. "k8s/" or "kubernetes/" followed by any context name (which may contain
96
+ # slashes)
97
+ kubernetes_pattern = '(?i:kubernetes|k8s)(?:/.+)?'
98
+
99
+ # Combine all patterns with alternation (|)
100
+ # ^ marks start of string, $ marks end of string
101
+ infra_pattern = (f'^(?:{cloud_pattern}{region_zone_pattern}|'
102
+ f'{wildcard_cloud}{wildcard_with_region}|'
103
+ f'{kubernetes_pattern})$')
104
+
72
105
  return {
73
106
  '$schema': 'https://json-schema.org/draft/2020-12/schema',
74
107
  'type': 'object',
@@ -85,6 +118,21 @@ def _get_single_resources_schema():
85
118
  'zone': {
86
119
  'type': 'string',
87
120
  },
121
+ 'infra': {
122
+ 'type': 'string',
123
+ 'description':
124
+ ('Infrastructure specification in format: '
125
+ 'cloud[/region[/zone]]. Use "*" as a wildcard.'),
126
+ # Pattern validates:
127
+ # 1. cloud[/region[/zone]] - e.g. "aws", "aws/us-east-1",
128
+ # "aws/us-east-1/us-east-1a"
129
+ # 2. Wildcard patterns - e.g. "*", "*/us-east-1",
130
+ # "*/*/us-east-1a", "aws/*/us-east-1a"
131
+ # 3. Kubernetes patterns - e.g. "kubernetes/my-context",
132
+ # "k8s/context-name",
133
+ # "k8s/aws:eks:us-east-1:123456789012:cluster/my-cluster"
134
+ 'pattern': infra_pattern,
135
+ },
88
136
  'cpus': {
89
137
  'anyOf': [{
90
138
  'type': 'string',
@@ -676,7 +724,7 @@ _LABELS_SCHEMA = {
676
724
  }
677
725
  }
678
726
 
679
- _PRORPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY = {
727
+ _PROPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY = {
680
728
  'oneOf': [
681
729
  {
682
730
  'type': 'string'
@@ -800,7 +848,7 @@ def get_config_schema():
800
848
  'type': 'boolean',
801
849
  },
802
850
  'security_group_name':
803
- (_PRORPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY),
851
+ (_PROPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY),
804
852
  'vpc_name': {
805
853
  'oneOf': [{
806
854
  'type': 'string',
@@ -1058,11 +1106,22 @@ def get_config_schema():
1058
1106
  }
1059
1107
  }
1060
1108
 
1109
+ provision_configs = {
1110
+ 'type': 'object',
1111
+ 'required': [],
1112
+ 'additionalProperties': False,
1113
+ 'properties': {
1114
+ 'ssh_timeout': {
1115
+ 'type': 'integer',
1116
+ 'minimum': 1,
1117
+ },
1118
+ }
1119
+ }
1120
+
1061
1121
  for cloud, config in cloud_configs.items():
1062
1122
  if cloud == 'aws':
1063
- config['properties'].update({
1064
- 'remote_identity': _PRORPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY
1065
- })
1123
+ config['properties'].update(
1124
+ {'remote_identity': _PROPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY})
1066
1125
  elif cloud == 'kubernetes':
1067
1126
  config['properties'].update(_REMOTE_IDENTITY_SCHEMA_KUBERNETES)
1068
1127
  else:
@@ -1080,6 +1139,7 @@ def get_config_schema():
1080
1139
  'docker': docker_configs,
1081
1140
  'nvidia_gpus': gpu_configs,
1082
1141
  'api_server': api_server,
1142
+ 'provision': provision_configs,
1083
1143
  **cloud_configs,
1084
1144
  },
1085
1145
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20250520
3
+ Version: 1.0.0.dev20250521
4
4
  Summary: SkyPilot: Run AI on Any Infra — Unified, Faster, Cheaper.
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0