skypilot-nightly 1.0.0.dev20250202__py3-none-any.whl → 1.0.0.dev20250204__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 CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  import urllib.request
6
6
 
7
7
  # Replaced with the current commit when building the wheels.
8
- _SKYPILOT_COMMIT_SHA = '269dfb19286a79f4f3a233aa525c73f6562dae37'
8
+ _SKYPILOT_COMMIT_SHA = 'e4ad98c907a819d9a6b7f6872445ca497401b8c9'
9
9
 
10
10
 
11
11
  def _get_git_commit():
@@ -35,7 +35,7 @@ def _get_git_commit():
35
35
 
36
36
 
37
37
  __commit__ = _get_git_commit()
38
- __version__ = '1.0.0.dev20250202'
38
+ __version__ = '1.0.0.dev20250204'
39
39
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
40
40
 
41
41
 
sky/check.py CHANGED
@@ -245,10 +245,10 @@ def _format_enabled_cloud(cloud_name: str) -> str:
245
245
  # here we are using rich. We should migrate this file to
246
246
  # use colorama as we do in the rest of the codebase.
247
247
  symbol = ('└── ' if i == len(existing_contexts) - 1 else '├── ')
248
- contexts_formatted.append(f'\n {symbol}{context}')
248
+ contexts_formatted.append(f'\n {symbol}{context}')
249
249
  context_info = f'Allowed contexts:{"".join(contexts_formatted)}'
250
250
  else:
251
251
  context_info = f'Active context: {existing_contexts[0]}'
252
252
 
253
- return f'{cloud_name}[/green][dim]\n └── {context_info}[/dim][green]'
253
+ return f'{cloud_name}[/green][dim]\n {context_info}[/dim][green]'
254
254
  return cloud_name
@@ -6,13 +6,17 @@ https://github.com/ray-project/ray/tree/master/dashboard/client/src) and/or get
6
6
  rid of the SSH port-forwarding business (see cli.py's job_dashboard()
7
7
  comment).
8
8
  """
9
+ import collections
9
10
  import datetime
11
+ import enum
12
+ import os
10
13
  import pathlib
11
14
 
12
15
  import flask
13
16
  import yaml
14
17
 
15
18
  from sky import jobs as managed_jobs
19
+ from sky.jobs import constants as managed_job_constants
16
20
  from sky.utils import common_utils
17
21
  from sky.utils import controller_utils
18
22
 
@@ -41,6 +45,92 @@ def _is_running_on_jobs_controller() -> bool:
41
45
  return False
42
46
 
43
47
 
48
+ # Column indices for job table
49
+ class JobTableColumns(enum.IntEnum):
50
+ """Column indices for the jobs table in the dashboard.
51
+
52
+ - DROPDOWN (0): Column for expandable dropdown arrow
53
+ - ID (1): Job ID column
54
+ - TASK (2): Task name/number column
55
+ - NAME (3): Job name column
56
+ - RESOURCES (4): Resources used by job
57
+ - SUBMITTED (5): Job submission timestamp
58
+ - TOTAL_DURATION (6): Total time since job submission
59
+ - JOB_DURATION (7): Actual job runtime
60
+ - RECOVERIES (8): Number of job recoveries
61
+ - STATUS (9): Current job status
62
+ - STARTED (10): Job start timestamp
63
+ - CLUSTER (11): Cluster name
64
+ - REGION (12): Cloud region
65
+ - FAILOVER (13): Job failover history
66
+ - DETAILS (14): Job details
67
+ - ACTIONS (15): Available actions column
68
+ """
69
+ DROPDOWN = 0
70
+ ID = 1
71
+ TASK = 2
72
+ NAME = 3
73
+ RESOURCES = 4
74
+ SUBMITTED = 5
75
+ TOTAL_DURATION = 6
76
+ JOB_DURATION = 7
77
+ RECOVERIES = 8
78
+ STATUS = 9
79
+ STARTED = 10
80
+ CLUSTER = 11
81
+ REGION = 12
82
+ DETAILS = 13
83
+ FAILOVER = 14
84
+ ACTIONS = 15
85
+
86
+
87
+ # Column headers matching the indices above
88
+ JOB_TABLE_COLUMNS = [
89
+ '', 'ID', 'Task', 'Name', 'Resources', 'Submitted', 'Total Duration',
90
+ 'Job Duration', 'Status', 'Started', 'Cluster', 'Region', 'Failover',
91
+ 'Recoveries', 'Details', 'Actions'
92
+ ]
93
+
94
+
95
+ def _extract_launch_history(log_content: str) -> str:
96
+ """Extract launch history from log content.
97
+
98
+ Args:
99
+ log_content: Content of the log file.
100
+ Returns:
101
+ A formatted string containing the launch history.
102
+ """
103
+ launches = []
104
+ current_launch = None
105
+
106
+ for line in log_content.splitlines():
107
+ if 'Launching on' in line:
108
+ try:
109
+ parts = line.split(']')
110
+ if len(parts) >= 2:
111
+ timestamp = parts[0].split()[1:3]
112
+ message = parts[1].replace('[0m⚙︎', '').strip()
113
+ formatted_line = f'{" ".join(timestamp)} {message}'
114
+ if current_launch:
115
+ prev_time, prev_target = current_launch.rsplit(
116
+ ' Launching on ', 1)
117
+ launches.append(
118
+ f'{prev_time} Tried to launch on {prev_target}')
119
+
120
+ # Store the current launch
121
+ current_launch = formatted_line
122
+ except IndexError:
123
+ launches.append(line.strip())
124
+
125
+ # Add the final (successful) launch at the beginning
126
+ if current_launch:
127
+ result = [current_launch]
128
+ result.extend(launches)
129
+ return '\n'.join(result)
130
+
131
+ return 'No launch history found'
132
+
133
+
44
134
  @app.route('/')
45
135
  def home():
46
136
  if not _is_running_on_jobs_controller():
@@ -54,38 +144,84 @@ def home():
54
144
  rows = managed_jobs.format_job_table(all_managed_jobs,
55
145
  show_all=True,
56
146
  return_rows=True)
57
- # Add an empty column for the dropdown button. This will be added in the
58
- # jobs/templates/index.html file.
59
- rows = [[''] + row for row in rows]
60
-
61
- # FIXME(zongheng): make the job table/queue funcs return structured info so
62
- # that we don't have to do things like row[-5] below.
63
- columns = [
64
- '', 'ID', 'Task', 'Name', 'Resources', 'Submitted', 'Total Duration',
65
- 'Job Duration', 'Recoveries', 'Status', 'Started', 'Cluster', 'Region',
66
- 'Failure'
67
- ]
68
- if rows and len(rows[0]) != len(columns):
147
+
148
+ status_counts = collections.defaultdict(int)
149
+ for task in all_managed_jobs:
150
+ if not task['status'].is_terminal():
151
+ status_counts[task['status'].value] += 1
152
+
153
+ # Add an empty column for the dropdown button and actions column
154
+ rows = [[''] + row + [''] + [''] for row in rows
155
+ ] # Add empty cell for failover and actions column
156
+
157
+ # Add log content as failover history for each job
158
+ for row in rows:
159
+ job_id = str(row[JobTableColumns.ID]).strip().replace(' ⤳', '')
160
+ if job_id and job_id != '-':
161
+ try:
162
+ log_path = os.path.join(
163
+ os.path.expanduser(
164
+ managed_job_constants.JOBS_CONTROLLER_LOGS_DIR),
165
+ f'{job_id}.log')
166
+ if os.path.exists(log_path):
167
+ with open(log_path, 'r', encoding='utf-8') as f:
168
+ log_content = f.read()
169
+ row[JobTableColumns.FAILOVER] = _extract_launch_history(
170
+ log_content)
171
+ else:
172
+ row[JobTableColumns.FAILOVER] = 'Log file not found'
173
+ except (IOError, OSError) as e:
174
+ row[JobTableColumns.FAILOVER] = f'Error reading log: {str(e)}'
175
+ app.logger.error('All managed jobs:')
176
+
177
+ # Validate column count
178
+ if rows and len(rows[0]) != len(JOB_TABLE_COLUMNS):
69
179
  raise RuntimeError(
70
- 'Dashboard code and managed job queue code are out of sync.')
180
+ f'Dashboard code and managed job queue code are out of sync. '
181
+ f'Expected {(JOB_TABLE_COLUMNS)} columns, got {(rows[0])}')
71
182
 
72
- # Fix STATUS color codes: '\x1b[33mCANCELLED\x1b[0m' -> 'CANCELLED'.
183
+ # Fix STATUS color codes: '\x1b[33mCANCELLED\x1b[0m' -> 'CANCELLED'
73
184
  for row in rows:
74
- row[-5] = common_utils.remove_color(row[-5])
75
- # Remove filler rows ([''], ..., ['-']).
76
- rows = [row for row in rows if ''.join(map(str, row)) != '']
185
+ row[JobTableColumns.STATUS] = common_utils.remove_color(
186
+ row[JobTableColumns.STATUS])
187
+
188
+ # Remove filler rows ([''], ..., ['-'])
189
+ rows = [
190
+ row for row in rows
191
+ if ''.join(map(str, row[:JobTableColumns.ACTIONS])) != ''
192
+ ]
193
+
194
+ # Get all unique status values
195
+ status_values = sorted(
196
+ list(set(row[JobTableColumns.STATUS] for row in rows)))
77
197
 
78
- # Get all unique status values.
79
- status_values = sorted(list(set(row[-5] for row in rows)))
80
198
  rendered_html = flask.render_template(
81
199
  'index.html',
82
- columns=columns,
200
+ columns=JOB_TABLE_COLUMNS,
83
201
  rows=rows,
84
202
  last_updated_timestamp=timestamp,
85
203
  status_values=status_values,
204
+ status_counts=status_counts,
86
205
  )
87
206
  return rendered_html
88
207
 
89
208
 
209
+ @app.route('/download_log/<job_id>')
210
+ def download_log(job_id):
211
+ try:
212
+ log_path = os.path.join(
213
+ os.path.expanduser(managed_job_constants.JOBS_CONTROLLER_LOGS_DIR),
214
+ f'{job_id}.log')
215
+ if not os.path.exists(log_path):
216
+ flask.abort(404)
217
+ return flask.send_file(log_path,
218
+ mimetype='text/plain',
219
+ as_attachment=True,
220
+ download_name=f'job_{job_id}.log')
221
+ except (IOError, OSError) as e:
222
+ app.logger.error(f'Error downloading log for job {job_id}: {str(e)}')
223
+ flask.abort(500)
224
+
225
+
90
226
  if __name__ == '__main__':
91
227
  app.run()
@@ -5,61 +5,314 @@
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
8
- <title>SkyPilot Dashboard</title>
8
+ <title>SkyPilot Managed Jobs</title>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
10
10
  integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
11
11
  <style>
12
- .table-no-stripes tbody tr:nth-of-type(even) {
13
- background-color: transparent;
12
+ :root {
13
+ --primary-color: #0d6efd;
14
+ --secondary-color: #6c757d;
15
+ --success-color: #198754;
16
+ --warning-color: #ffc107;
17
+ --info-color: #0dcaf0;
18
+ --light-color: #f8f9fa;
14
19
  }
15
20
 
16
- .table-hover-selected tbody tr:hover {
17
- background-color: #f5f5f5;
21
+ body {
22
+ margin-top: 0;
23
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
24
+ background-color: white;
18
25
  }
19
26
 
20
- .footer {
21
- font-size: 14px;
22
- color: #777;
23
- margin-top: 20px;
27
+ .container {
28
+ max-width: 100%;
29
+ width: 100%;
30
+ background-color: white;
31
+ border-radius: 0;
32
+ box-shadow: none;
33
+ padding: 2rem;
34
+ margin-bottom: 0;
24
35
  }
25
36
 
26
- body {
27
- margin-top: 20px;
37
+ header {
38
+ position: sticky;
39
+ top: 0;
40
+ background: white;
41
+ z-index: 1000;
42
+ padding: 1.5rem 2rem;
43
+ margin: -2rem -2rem 1.5rem -2rem; /* Negative margins to match container padding */
44
+ border-bottom: 1px solid #dee2e6;
28
45
  }
29
46
 
30
- .bg-light {
31
- color: #212529;
32
- /* for some reason not in bootstrap? */
47
+ /* Add shadow when header is sticky */
48
+ header.sticky {
49
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
33
50
  }
34
51
 
35
- .fixed-header-table thead {
36
- position: sticky;
37
- top: 0;
38
- background-color: #f8f9fa;
39
- /* Replace with your desired background color */
40
- color: #000000;
41
- /* Replace with your desired text color */
42
- z-index: 1;
52
+ h1 {
53
+ color: var(--primary-color);
54
+ font-weight: 600;
55
+ font-size: 2rem;
43
56
  }
44
- .clickable {
45
- cursor: pointer; /* This makes the cursor a pointer when hovering over the element */
57
+
58
+ .table {
59
+ width: 100%; /* Ensure table takes full container width */
60
+ border-radius: 8px;
61
+ overflow: hidden;
62
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.05);
63
+ table-layout: auto; /* Allow table to adjust column widths automatically */
64
+ }
65
+
66
+ .fixed-header-table thead th,
67
+ .fixed-header-table thead td { /* Added td selector */
68
+ background-color: var(--light-color);
69
+ padding: 1rem;
70
+ font-weight: 600;
71
+ border-bottom: 2px solid #dee2e6;
72
+ }
73
+
74
+ .table th:nth-child(2), /* ID column */
75
+ .table td:nth-child(2) {
76
+ min-width: 30px; /* Reduced from 100px */
77
+ max-width: 60px; /* Added max-width */
78
+ overflow: hidden; /* Handle overflow */
79
+ text-overflow: ellipsis; /* Show ellipsis for overflow */
80
+ white-space: nowrap; /* Keep text on one line */
81
+ }
82
+
83
+ .table th:nth-child(4), /* Name column */
84
+ .table td:nth-child(4) {
85
+ min-width: 150px;
86
+ }
87
+
88
+ .table th:nth-child(10), /* Status column */
89
+ .table td:nth-child(10) {
90
+ min-width: 120px;
91
+ }
92
+
93
+ .table th,
94
+ .table td {
95
+ padding: 0.8rem 1rem;
96
+ white-space: nowrap; /* Prevent text wrapping in cells */
97
+ }
98
+
99
+ /* Allow Details column to wrap */
100
+ .table th:nth-child(12), /* Details column */
101
+ .table td:nth-child(12) {
102
+ white-space: normal; /* Allow text wrapping */
103
+ max-width: 250px; /* Limit width to prevent excessive stretching */
104
+ word-wrap: break-word; /* Break long words if needed */
105
+ }
106
+
107
+ .badge {
108
+ padding: 0.5em 0.8em;
109
+ font-weight: 500;
110
+ border-radius: 6px;
111
+ }
112
+
113
+ .btn-outline-secondary {
114
+ transition: all 0.2s;
115
+ }
116
+
117
+ .btn-outline-secondary:hover {
118
+ background-color: var(--secondary-color);
119
+ color: white;
120
+ transform: translateY(-1px);
46
121
  }
47
122
 
48
123
  .filter-controls {
49
- display: flex;
50
- gap: 10px;
51
- align-items: center; /* This ensures vertical alignment */
52
- margin-top: 1rem;
124
+ background-color: var(--light-color);
125
+ padding: 1rem;
126
+ border-radius: 8px;
127
+ margin: 1rem 0;
128
+ }
129
+
130
+ .form-select {
131
+ border-radius: 6px;
132
+ border: 1px solid #dee2e6;
133
+ padding: 0.5rem 2rem 0.5rem 1rem;
134
+ transition: all 0.2s;
135
+ }
136
+
137
+ .form-select:hover {
138
+ border-color: var(--primary-color);
139
+ }
140
+
141
+ .form-check-input {
142
+ cursor: pointer;
143
+ }
144
+
145
+ .clickable {
146
+ transition: color 0.2s;
147
+ }
148
+
149
+ .clickable:hover {
150
+ color: var(--primary-color);
151
+ }
152
+
153
+ /* Status badge animations */
154
+ .badge {
155
+ animation: fadeIn 0.3s ease-in;
156
+ }
157
+
158
+ @keyframes fadeIn {
159
+ from { opacity: 0; transform: translateY(-2px); }
160
+ to { opacity: 1; transform: translateY(0); }
161
+ }
162
+
163
+ /* Loading indicator for auto-refresh */
164
+ .refresh-indicator {
165
+ display: inline-block;
166
+ margin-left: 8px;
167
+ font-size: 12px;
168
+ color: var(--secondary-color);
169
+ }
170
+
171
+ .refresh-spinner {
172
+ display: none;
173
+ width: 12px;
174
+ height: 12px;
175
+ border: 2px solid #f3f3f3;
176
+ border-top: 2px solid var(--primary-color);
177
+ border-radius: 50%;
178
+ animation: spin 1s linear infinite;
179
+ }
180
+
181
+ @keyframes spin {
182
+ 0% { transform: rotate(0deg); }
183
+ 100% { transform: rotate(360deg); }
184
+ }
185
+
186
+ .status-container {
187
+ cursor: help;
53
188
  position: relative;
54
- z-index: 2;
189
+ display: inline-block;
190
+ }
191
+
192
+ .status-container:hover::after {
193
+ content: attr(title);
194
+ position: absolute;
195
+ left: 0; /* Changed from 50% */
196
+ bottom: 100%;
197
+ transform: translateY(-8px); /* Removed translateX */
198
+ z-index: 1001;
199
+ background-color: rgba(33, 37, 41, 0.9);
200
+ color: white;
201
+ padding: 0.75rem 1rem;
202
+ border-radius: 6px;
203
+ font-size: 0.875rem;
204
+ white-space: pre;
205
+ min-width: 500px;
206
+ max-width: 800px;
207
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
208
+ pointer-events: none;
209
+ opacity: 0;
210
+ animation: tooltipFadeIn 0.2s ease-in-out forwards;
211
+ }
212
+
213
+ /* Update the arrow position */
214
+ .status-container:hover::before {
215
+ content: '';
216
+ position: absolute;
217
+ left: 20px; /* Changed from 50% */
218
+ bottom: 100%;
219
+ transform: none; /* Removed transform */
220
+ border: 8px solid transparent;
221
+ border-top-color: rgba(33, 37, 41, 0.9);
222
+ z-index: 1001;
223
+ pointer-events: none;
224
+ opacity: 0;
225
+ animation: tooltipFadeIn 0.2s ease-in-out forwards;
55
226
  }
56
227
 
57
- /* Customize the select focus/hover states */
58
- .form-select:focus {
59
- border-color: #dee2e6;
60
- box-shadow: 0 0 0 0.1rem rgba(0,0,0,0.1);
228
+ @keyframes tooltipFadeIn {
229
+ to {
230
+ opacity: 1;
231
+ transform: translateY(0); /* Simplified animation */
232
+ }
233
+ }
234
+
235
+ /* Ensure the table doesn't cut off tooltips */
236
+ .table {
237
+ overflow: visible !important;
238
+ }
239
+
240
+ .fixed-header-table {
241
+ overflow: visible !important;
61
242
  }
62
243
 
244
+ /* Add horizontal scroll for very small screens */
245
+ @media (max-width: 1200px) {
246
+ .table-responsive {
247
+ overflow-x: auto;
248
+ }
249
+ }
250
+
251
+ /* Update the timestamp hover styles */
252
+ #last-updated {
253
+ position: relative;
254
+ text-decoration: none;
255
+ border-bottom: 1px solid transparent;
256
+ transition: border-bottom-color 0.2s;
257
+ cursor: help;
258
+ }
259
+
260
+ #last-updated:hover {
261
+ border-bottom-color: var(--secondary-color);
262
+ }
263
+
264
+ #last-updated:hover::after {
265
+ content: attr(title);
266
+ position: absolute;
267
+ bottom: 100%;
268
+ left: 50%;
269
+ transform: translateX(-50%);
270
+ padding: 0.5rem 1rem;
271
+ background-color: rgba(33, 37, 41, 0.9);
272
+ color: white;
273
+ border-radius: 6px;
274
+ font-size: 0.875rem;
275
+ white-space: nowrap;
276
+ z-index: 1000;
277
+ margin-bottom: 8px;
278
+ }
279
+
280
+ #last-updated:hover::before {
281
+ content: '';
282
+ position: absolute;
283
+ bottom: 100%;
284
+ left: 50%;
285
+ transform: translateX(-50%);
286
+ border: 8px solid transparent;
287
+ border-top-color: rgba(33, 37, 41, 0.9);
288
+ margin-bottom: -8px;
289
+ z-index: 1000;
290
+ }
291
+
292
+ .clickable-badge {
293
+ cursor: pointer;
294
+ transition: transform 0.2s, opacity 0.2s;
295
+ opacity: 0.4;
296
+ }
297
+
298
+ .clickable-badge:hover {
299
+ transform: scale(1.1);
300
+ opacity: 1;
301
+ }
302
+
303
+ .clickable-badge.selected-filter {
304
+ transform: scale(1.1);
305
+ opacity: 1;
306
+ box-shadow: 0 0 0 2px #fff, 0 0 0 4px currentColor;
307
+ }
308
+
309
+ /* Ensure tooltips appear above the sticky header */
310
+ .status-container:hover::after,
311
+ .status-container:hover::before,
312
+ #last-updated:hover::after,
313
+ #last-updated:hover::before {
314
+ z-index: 1001;
315
+ }
63
316
  </style>
64
317
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
65
318
  <script
@@ -70,29 +323,84 @@
70
323
  <body>
71
324
  <div class="container">
72
325
  <header>
73
- <h1>SkyPilot managed jobs</h1>
74
- <p class="text-muted mt-4" id="last-updated"></p>
75
- <div class="form-check form-switch">
76
- <input class="form-check-input" type="checkbox" id="refresh-toggle" checked>
77
- <label class="form-check-label" for="refresh-toggle">Auto-refresh (every 30s)</label>
78
- </div>
79
- <div class="filter-controls">
80
- <span class="fw-medium fs-6">Filter by status:</span>
81
- <select class="form-select" id="status-filter" style="width: auto;">
82
- <option value="">All statuses</option>
83
- {% for status in status_values %}
84
- <option value="{{ status }}">{{ status }}</option>
85
- {% endfor %}
86
- </select>
326
+ <div class="d-flex justify-content-between align-items-center">
327
+ <h1>SkyPilot Managed Jobs</h1>
328
+ <div class="d-flex align-items-center">
329
+ <div class="form-check form-switch me-3">
330
+ <input class="form-check-input" type="checkbox" id="refresh-toggle" checked>
331
+ <label class="form-check-label" for="refresh-toggle">
332
+ Auto-refresh
333
+ <span class="refresh-indicator">
334
+ <span class="refresh-spinner" id="refresh-spinner"></span>
335
+ </span>
336
+ </label>
337
+ </div>
338
+ <p class="text-muted mb-0" id="last-updated"></p>
339
+ </div>
87
340
  </div>
88
341
  </header>
89
342
 
343
+ <!-- Hidden status filter -->
344
+ <select id="status-filter" style="display: none;">
345
+ <option value="">All</option>
346
+ {% for status in status_values %}
347
+ <option value="{{ status }}">{{ status }}</option>
348
+ {% endfor %}
349
+ </select>
350
+
351
+ {% if rows %}
352
+ <p>Filter By :
353
+ <span class="badge bg-secondary clickable-badge selected-filter me-2" data-status="ALL">All</span>
354
+ {% set status_dict = {} %}
355
+ {% for row in rows %}
356
+ {% set status = row[9].split()[0] %}
357
+ {% if status not in status_dict %}
358
+ {% set _ = status_dict.update({status: 1}) %}
359
+ {% else %}
360
+ {% set _ = status_dict.update({status: status_dict[status] + 1}) %}
361
+ {% endif %}
362
+ {% endfor %}
363
+ {% for status, count in status_dict|dictsort %}
364
+ <span class="me-2">
365
+ <span class="me-1">; {{ count }}</span>
366
+ {% if status.startswith('RUNNING') %}
367
+ <span class="badge bg-primary clickable-badge" data-status="{{ status }}">{{ status }}</span>
368
+ {% elif status.startswith('PENDING') or status.startswith('SUBMITTED') %}
369
+ <span class="badge bg-light clickable-badge" data-status="{{ status }}">{{ status }}</span>
370
+ {% elif status.startswith('RECOVERING') or status.startswith('CANCELLING') or status.startswith('STARTING') %}
371
+ <span class="badge bg-info clickable-badge" data-status="{{ status }}">{{ status }}</span>
372
+ {% elif status.startswith('SUCCEEDED') %}
373
+ <span class="badge bg-success clickable-badge" data-status="{{ status }}">{{ status }}</span>
374
+ {% elif status.startswith('CANCELLED') %}
375
+ <span class="badge bg-secondary clickable-badge" data-status="{{ status }}">{{ status }}</span>
376
+ {% elif status.startswith('FAILED') %}
377
+ <span class="badge bg-danger clickable-badge" data-status="{{ status }}">{{ status }}</span>
378
+ {% else %}
379
+ <span class="clickable-badge" data-status="{{ status }}">{{ status }}</span>
380
+ {% endif %}
381
+ </span>
382
+ {% endfor %}
383
+ </p>
384
+ {% else %}
385
+ <p>No jobs found.</p>
386
+ {% endif %}
387
+
90
388
  <table class="table table-hover table-hover-selected fixed-header-table" id="jobs-table">
91
389
  <thead>
92
390
  <tr>
93
- {% for column in columns %}
94
- <th>{{ column }}</th>
95
- {% endfor %}
391
+ <td></td>
392
+ <th>ID</th>
393
+ <th>Task</th>
394
+ <th>Name</th>
395
+ <th>Total Duration</th>
396
+ <th>Job Duration</th>
397
+ <th>Status</th>
398
+ <th>Resources</th>
399
+ <th>Cluster</th>
400
+ <th>Region</th>
401
+ <th>Recoveries</th>
402
+ <th>Details</th>
403
+ <th>Actions</th>
96
404
  </tr>
97
405
  </thead>
98
406
  <tbody>
@@ -102,33 +410,41 @@
102
410
  <td>{{ row[1]|string|replace(' \u21B3', '') }}</td>
103
411
  <td>{{ row[2] }}</td>
104
412
  <td>{{ row[3] }}</td>
105
- <td>{{ row[4] }}</td>
106
- <td>{{ row[5] }}</td>
107
413
  <td>{{ row[6] }}</td>
108
414
  <td>{{ row[7] }}</td>
109
- <td>{{ row[8] }}</td>
110
415
  <td>
111
- <!-- https://getbootstrap.com/docs/4.0/components/badge/ -->
112
- {% if row[9].startswith('RUNNING') %}
113
- <span class="badge bg-primary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
114
- {% elif row[9].startswith('PENDING') or row[9].startswith('SUBMITTED') %}
115
- <span class="badge bg-light">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
116
- {% elif row[9].startswith('RECOVERING') or row[9].startswith('CANCELLING') or row[9].startswith('STARTING') %}
117
- <span class="badge bg-info">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
118
- {% elif row[9].startswith('SUCCEEDED') %}
119
- <span class="badge bg-success">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
120
- {% elif row[9].startswith('CANCELLED') %}
121
- <span class="badge bg-secondary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
122
- {% elif row[9].startswith('FAILED') %}
123
- <span class="badge bg-warning">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
124
- {% else %}
125
- {{ row[9] }}
416
+ <!-- Status column with tooltip -->
417
+ <div class="status-container" style="position: relative;" title="{{ row[14] }}">
418
+ {% if row[9].startswith('RUNNING') %}
419
+ <span class="badge bg-primary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
420
+ {% elif row[9].startswith('PENDING') or row[9].startswith('SUBMITTED') %}
421
+ <span class="badge bg-warning">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
422
+ {% elif row[9].startswith('RECOVERING') or row[9].startswith('CANCELLING') or row[9].startswith('STARTING') %}
423
+ <span class="badge bg-info">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
424
+ {% elif row[9].startswith('SUCCEEDED') %}
425
+ <span class="badge bg-success">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
426
+ {% elif row[9].startswith('CANCELLED') %}
427
+ <span class="badge bg-secondary">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
428
+ {% elif row[9].startswith('FAILED') %}
429
+ <span class="badge bg-danger">{{ row[9].split()[0] }}</span>{{ row[9][row[9].split()[0]|length:] }}
430
+ {% else %}
431
+ {{ row[9] }}
432
+ {% endif %}
433
+ </div>
434
+ </td>
435
+ <td>{{ row[4] }}</td> {# Resources #}
436
+ <td>{{ row[11] }}</td> {# Cluster #}
437
+ <td>{{ row[12] }}</td> {# Region #}
438
+ <td>{{ row[8] }}</td> {# Recoveries #}
439
+ <td>{{ row[13] }}</td> {# Details #}
440
+ <td>
441
+ {% if row[1]|string|replace(' \u21B3', '') and row[1]|string|replace(' \u21B3', '') != '-' %}
442
+ <a href="{{ url_for('download_log', job_id=row[1]|string|replace(' \u21B3', '')) }}"
443
+ class="btn btn-sm btn-outline-secondary">
444
+ Controller Log
445
+ </a>
126
446
  {% endif %}
127
447
  </td>
128
- <td>{{ row[10] }}</td>
129
- <td>{{ row[11] }}</td>
130
- <td>{{ row[12] }}</td>
131
- <td>{{ row[13] }}</td>
132
448
  </tr>
133
449
  {% endfor %}
134
450
  </tbody>
@@ -197,7 +513,9 @@
197
513
  document.addEventListener("DOMContentLoaded", function () {
198
514
  var timestamp = "{{ last_updated_timestamp }}"; // Get the UTC timestamp from the template
199
515
  var localTimestamp = moment.utc(timestamp).tz(moment.tz.guess()).format('YYYY-MM-DD HH:mm:ss z');
516
+ var utcTimestamp = moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss UTC');
200
517
  document.getElementById("last-updated").textContent = "Last updated: " + localTimestamp;
518
+ document.getElementById("last-updated").title = utcTimestamp;
201
519
  });
202
520
  </script>
203
521
  <script>
@@ -210,6 +528,7 @@
210
528
  } else {
211
529
  refreshToggle.checked = false;
212
530
  }
531
+
213
532
  // Add event listener to the toggle switch
214
533
  function handleAutoRefresh() {
215
534
  localStorage.setItem("refreshState", refreshToggle.checked);
@@ -217,6 +536,9 @@
217
536
  if (refreshToggle.checked) {
218
537
  // Auto-refresh is enabled
219
538
  refreshInterval = setInterval(function () {
539
+ // Store current filter before reload
540
+ var currentFilter = document.getElementById("status-filter").value;
541
+ localStorage.setItem("statusFilter", currentFilter);
220
542
  location.reload();
221
543
  }, 30000); // 30 seconds in milliseconds
222
544
  } else {
@@ -224,6 +546,17 @@
224
546
  clearInterval(refreshInterval);
225
547
  }
226
548
  }
549
+
550
+ // Restore filter state after page load
551
+ document.addEventListener("DOMContentLoaded", function() {
552
+ var savedFilter = localStorage.getItem("statusFilter");
553
+ if (savedFilter) {
554
+ var statusFilter = document.getElementById("status-filter");
555
+ statusFilter.value = savedFilter;
556
+ filterStatus(savedFilter);
557
+ }
558
+ });
559
+
227
560
  refreshToggle.addEventListener("change", handleAutoRefresh);
228
561
  handleAutoRefresh();
229
562
  </script>
@@ -231,13 +564,15 @@
231
564
  function filterStatus(status) {
232
565
  var rows = document.querySelectorAll("#jobs-table tbody tr");
233
566
  rows.forEach(function(row) {
234
- var statusCell = row.querySelector("td:nth-child(10)"); // Status is in the 10th column
235
- var statusText = statusCell.textContent.trim().split(' ')[0]; // Get first word of status
236
-
237
- if (status === '' || statusText === status) {
238
- row.style.display = "";
239
- } else {
240
- row.style.display = "none";
567
+ var statusCell = row.querySelector("td:nth-child(7)"); // Status is now in the 7th column
568
+ if (statusCell) {
569
+ var statusText = statusCell.textContent.trim().split(' ')[0]; // Get first word of status
570
+
571
+ if (status === '' || statusText === status) {
572
+ row.style.display = "";
573
+ } else {
574
+ row.style.display = "none";
575
+ }
241
576
  }
242
577
  });
243
578
  }
@@ -249,6 +584,85 @@
249
584
  });
250
585
  });
251
586
  </script>
587
+ <script>
588
+ // Show loading spinner during refresh
589
+ window.addEventListener('beforeunload', function() {
590
+ document.getElementById('refresh-spinner').style.display = 'inline-block';
591
+ });
592
+
593
+ // Enhance table row hover effect
594
+ document.querySelectorAll('tbody tr').forEach(row => {
595
+ row.addEventListener('mouseenter', function() {
596
+ this.style.transform = 'scale(1.002)';
597
+ this.style.transition = 'transform 0.2s';
598
+ });
599
+
600
+ row.addEventListener('mouseleave', function() {
601
+ this.style.transform = 'scale(1)';
602
+ });
603
+ });
604
+ </script>
605
+ <script>
606
+ // Update column indices for job table
607
+ const JOB_TABLE_COLUMNS = [
608
+ '', 'ID', 'Task', 'Name', 'Total Duration',
609
+ 'Job Duration', 'Status', 'Resources', 'Cluster', 'Region', 'Recoveries', 'Details',
610
+ 'Actions'
611
+ ];
612
+ </script>
613
+ <script>
614
+ // Replace the existing click handler for status badges with this updated version
615
+ document.addEventListener("DOMContentLoaded", function() {
616
+ const statusFilter = document.getElementById('status-filter');
617
+ const badges = document.querySelectorAll('.clickable-badge');
618
+
619
+ // Set initial state
620
+ const savedFilter = localStorage.getItem("statusFilter") || '';
621
+ updateSelectedBadge(savedFilter);
622
+
623
+ badges.forEach(function(badge) {
624
+ badge.addEventListener('click', function() {
625
+ const status = this.dataset.status;
626
+ const currentFilter = statusFilter.value;
627
+
628
+ // If clicking the already selected filter, clear it (show all)
629
+ const newStatus = (status === currentFilter || (status === 'ALL' && currentFilter === '')) ? '' :
630
+ (status === 'ALL' ? '' : status);
631
+
632
+ // Update filter and UI
633
+ statusFilter.value = newStatus;
634
+ filterStatus(newStatus);
635
+ localStorage.setItem("statusFilter", newStatus);
636
+ updateSelectedBadge(newStatus);
637
+ });
638
+ });
639
+
640
+ function updateSelectedBadge(selectedStatus) {
641
+ badges.forEach(badge => {
642
+ badge.classList.remove('selected-filter');
643
+ if ((selectedStatus === '' && badge.dataset.status === 'ALL') ||
644
+ badge.dataset.status === selectedStatus) {
645
+ badge.classList.add('selected-filter');
646
+ }
647
+ });
648
+ }
649
+ });
650
+ </script>
651
+ <script>
652
+ // Add scroll event listener to handle sticky header shadow
653
+ document.addEventListener("DOMContentLoaded", function() {
654
+ const header = document.querySelector('header');
655
+ const container = document.querySelector('.container');
656
+
657
+ window.addEventListener('scroll', function() {
658
+ if (container.getBoundingClientRect().top < 0) {
659
+ header.classList.add('sticky');
660
+ } else {
661
+ header.classList.remove('sticky');
662
+ }
663
+ });
664
+ });
665
+ </script>
252
666
 
253
667
  </body>
254
668
 
@@ -64,6 +64,21 @@ def _get_ssh_key_name(prefix: str = '') -> str:
64
64
  return name
65
65
 
66
66
 
67
+ def _get_private_ip(instance_info: Dict[str, Any], single_node: bool) -> str:
68
+ private_ip = instance_info.get('private_ip')
69
+ if private_ip is None:
70
+ if single_node:
71
+ # The Lambda cloud API may return an instance info without
72
+ # private IP. It does not align with their docs, but we still
73
+ # allow single-node cluster to proceed with provisioning, by using
74
+ # 127.0.0.1, as private IP is not critical for single-node case.
75
+ return '127.0.0.1'
76
+ msg = f'Failed to retrieve private IP for instance {instance_info}.'
77
+ logger.error(msg)
78
+ raise RuntimeError(msg)
79
+ return private_ip
80
+
81
+
67
82
  def run_instances(region: str, cluster_name_on_cloud: str,
68
83
  config: common.ProvisionConfig) -> common.ProvisionRecord:
69
84
  """Runs instances for the given cluster"""
@@ -197,13 +212,14 @@ def get_cluster_info(
197
212
  ) -> common.ClusterInfo:
198
213
  del region # unused
199
214
  running_instances = _filter_instances(cluster_name_on_cloud, ['active'])
215
+ single_node = len(running_instances) == 1
200
216
  instances: Dict[str, List[common.InstanceInfo]] = {}
201
217
  head_instance_id = None
202
218
  for instance_id, instance_info in running_instances.items():
203
219
  instances[instance_id] = [
204
220
  common.InstanceInfo(
205
221
  instance_id=instance_id,
206
- internal_ip=instance_info['private_ip'],
222
+ internal_ip=_get_private_ip(instance_info, single_node),
207
223
  external_ip=instance_info['ip'],
208
224
  ssh_port=22,
209
225
  tags={},
sky/serve/serve_state.py CHANGED
@@ -55,33 +55,35 @@ def create_table(cursor: 'sqlite3.Cursor', conn: 'sqlite3.Connection') -> None:
55
55
  PRIMARY KEY (service_name, replica_id))""")
56
56
  cursor.execute("""\
57
57
  CREATE TABLE IF NOT EXISTS version_specs (
58
- version INTEGER,
58
+ version INTEGER,
59
59
  service_name TEXT,
60
60
  spec BLOB,
61
61
  PRIMARY KEY (service_name, version))""")
62
62
  conn.commit()
63
63
 
64
+ # Backward compatibility.
65
+ db_utils.add_column_to_table(cursor, conn, 'services',
66
+ 'requested_resources_str', 'TEXT')
67
+ # Deprecated: switched to `active_versions` below for the version
68
+ # considered active by the load balancer. The
69
+ # authscaler/replica_manager version can be found in the
70
+ # version_specs table.
71
+ db_utils.add_column_to_table(
72
+ cursor, conn, 'services', 'current_version',
73
+ f'INTEGER DEFAULT {constants.INITIAL_VERSION}')
74
+ # The versions that is activated for the service. This is a list
75
+ # of integers in json format.
76
+ db_utils.add_column_to_table(cursor, conn, 'services', 'active_versions',
77
+ f'TEXT DEFAULT {json.dumps([])!r}')
78
+ db_utils.add_column_to_table(cursor, conn, 'services',
79
+ 'load_balancing_policy', 'TEXT DEFAULT NULL')
80
+ # Whether the service's load balancer is encrypted with TLS.
81
+ db_utils.add_column_to_table(cursor, conn, 'services', 'tls_encrypted',
82
+ 'INTEGER DEFAULT 0')
83
+ conn.commit()
84
+
64
85
 
65
- _DB = db_utils.SQLiteConn(_DB_PATH, create_table)
66
- # Backward compatibility.
67
- db_utils.add_column_to_table(_DB.cursor, _DB.conn, 'services',
68
- 'requested_resources_str', 'TEXT')
69
- # Deprecated: switched to `active_versions` below for the version considered
70
- # active by the load balancer. The authscaler/replica_manager version can be
71
- # found in the version_specs table.
72
- db_utils.add_column_to_table(_DB.cursor, _DB.conn, 'services',
73
- 'current_version',
74
- f'INTEGER DEFAULT {constants.INITIAL_VERSION}')
75
- # The versions that is activated for the service. This is a list of integers in
76
- # json format.
77
- db_utils.add_column_to_table(_DB.cursor, _DB.conn, 'services',
78
- 'active_versions',
79
- f'TEXT DEFAULT {json.dumps([])!r}')
80
- db_utils.add_column_to_table(_DB.cursor, _DB.conn, 'services',
81
- 'load_balancing_policy', 'TEXT DEFAULT NULL')
82
- # Whether the service's load balancer is encrypted with TLS.
83
- db_utils.add_column_to_table(_DB.cursor, _DB.conn, 'services', 'tls_encrypted',
84
- 'INTEGER DEFAULT 0')
86
+ db_utils.SQLiteConn(_DB_PATH, create_table)
85
87
  _UNIQUE_CONSTRAINT_FAILED_ERROR_MSG = 'UNIQUE constraint failed: services.name'
86
88
 
87
89
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20250202
3
+ Version: 1.0.0.dev20250204
4
4
  Summary: SkyPilot: An intercloud broker for the clouds
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0
@@ -1,7 +1,7 @@
1
- sky/__init__.py,sha256=WL8LMURbd_cRtu0cPtp8XVTr9t2awrOYnS-0LqeuKBI,5529
1
+ sky/__init__.py,sha256=wD_DitkbMqx2kv8G3wQz7yRYrWtCMbhBQoeM6oaT1KQ,5529
2
2
  sky/admin_policy.py,sha256=hPo02f_A32gCqhUueF0QYy1fMSSKqRwYEg_9FxScN_s,3248
3
3
  sky/authentication.py,sha256=LXUDABKP1FJCS256xTTDJa40WXwHKF5x49S-4hZbD1M,21501
4
- sky/check.py,sha256=qTpm3N1zUZi2inEZPsrbt278B3h8nsk2gnepzIgLybE,10899
4
+ sky/check.py,sha256=xzLlxUkBCrzpOho8lw65EvKLPl_b9lA2nteF5MSYbDQ,10885
5
5
  sky/cli.py,sha256=B-YWYiKnfSGdSOXtAY8SRGOGhneUeNPBjXFZ0FuLZ8w,214131
6
6
  sky/cloud_stores.py,sha256=PcLT57_8SZy7o6paAluElfBynaLkbaOq3l-8dNg1AVM,23672
7
7
  sky/core.py,sha256=fE1rn4Ku94S0XmWTO5-6t6eT6aaJImNczRqEnTe8v7Q,38742
@@ -104,9 +104,9 @@ sky/jobs/recovery_strategy.py,sha256=m-EA-MWXPFrgx2CYFPr6MmgeUoDTEBmY2xruD2PRSGY
104
104
  sky/jobs/scheduler.py,sha256=WAvNb8-vBk8q1zFordFdpH7gxqWDjPHDGZZay6aodOk,12028
105
105
  sky/jobs/state.py,sha256=bvBNZMg3DzPfS4eHNzMqYaMui2cqnWoWGDIaiOpaXSk,40770
106
106
  sky/jobs/utils.py,sha256=9tCKeY2x1lOgFQdaxqx6tZd2zd2e3pdUOQGvgvbf1Rk,52682
107
- sky/jobs/dashboard/dashboard.py,sha256=KMSarpVcfnc-ELPFvy1M9_I1k4kSeXubTk3ibQC67Tg,3219
107
+ sky/jobs/dashboard/dashboard.py,sha256=lLXAt755bkOh0XNjl5eQbu5vys7zYpvEoJgM97DkpeM,7706
108
108
  sky/jobs/dashboard/static/favicon.ico,sha256=uYlvgxSM7gjBmXpZ8wydvZUPAbJiiix-rc2Xe5mma9s,15086
109
- sky/jobs/dashboard/templates/index.html,sha256=su1tqgcsXNl1lGl9hfIR6ig1f531OO57x1Tc2mNDK7U,11139
109
+ sky/jobs/dashboard/templates/index.html,sha256=nBiIjYDgZdRyO4pNt_2qEj201QvCiiO2n2kuVuSgKeI,26289
110
110
  sky/provision/__init__.py,sha256=rnuwL9x3qQGhX3EYW6KyZWfw_yBqGSiFBDz86T5xus4,6339
111
111
  sky/provision/common.py,sha256=E8AlSUFcn0FYQq1erNmoVfMAdsF9tP2yxfyk-9PLvQU,10286
112
112
  sky/provision/constants.py,sha256=oc_XDUkcoLQ_lwDy5yMeMSWviKS0j0s1c0pjlvpNeWY,800
@@ -154,7 +154,7 @@ sky/provision/kubernetes/manifests/smarter-device-manager-configmap.yaml,sha256=
154
154
  sky/provision/kubernetes/manifests/smarter-device-manager-daemonset.yaml,sha256=RtTq4F1QUmR2Uunb6zuuRaPhV7hpesz4saHjn3Ncsb4,2010
155
155
  sky/provision/lambda_cloud/__init__.py,sha256=6EEvSgtUeEiup9ivIFevHmgv0GqleroO2X0K7TRa2nE,612
156
156
  sky/provision/lambda_cloud/config.py,sha256=jq1iLzp4Up61r4JGxvtpVbJlgXnea3LHYQhCQyyl7ik,272
157
- sky/provision/lambda_cloud/instance.py,sha256=PEVwEPYOzZ3T_vcq6MtWbg4Au01FIUTlkpbIHEtySz0,8950
157
+ sky/provision/lambda_cloud/instance.py,sha256=_gVFNfaXdqYEmCtCwgaCkvU8BwxZarpyetSGGA_Dj64,9681
158
158
  sky/provision/lambda_cloud/lambda_utils.py,sha256=H2Qx4xdJyyEu2IXaj5AyppuPJW385nF5_KXFOk8j9RI,9858
159
159
  sky/provision/oci/__init__.py,sha256=5E6EUtTK3mqGVREw5TuVl5DxteBYTZigIii7c8gHExU,612
160
160
  sky/provision/oci/config.py,sha256=diSDTyHLokcuXGB2XgZCHFvsXa8bah1PP2XuMouW_UU,1650
@@ -195,7 +195,7 @@ sky/serve/core.py,sha256=ANjALyYiQUmcpWjQ1YJor2rqHJypQpzuQxuIPnDyEk0,35788
195
195
  sky/serve/load_balancer.py,sha256=2nkMPRvy-h7hJL4Qq__tkT8nIAVC_nmjyXf8mMGYEFk,13658
196
196
  sky/serve/load_balancing_policies.py,sha256=XVj76qBgqh7h6wfx53RKQFzBefDWTE4TCdCEtFLLtI4,5398
197
197
  sky/serve/replica_managers.py,sha256=SW7k2iivUZ6dw_YMgGYOHOGD9_yyV4byfKa8e5t8_HE,57587
198
- sky/serve/serve_state.py,sha256=2V9WVD9OZ26vgyIJ-kZcrlouazwFTrpjlZAK9DRv3uA,20084
198
+ sky/serve/serve_state.py,sha256=Do-MITtijw1z8dDUyJZMAGnjZCgdUFtJ2nwhhcjpOGI,20056
199
199
  sky/serve/serve_utils.py,sha256=m1Zcjslnzcr5AAppzV48WDOwMWjRaXotTUd_iN-dHgc,40654
200
200
  sky/serve/service.py,sha256=DPU1PJGuHa1WaNqxYqgpmqd4LA9jBbQM-KlLrA6C1M0,12156
201
201
  sky/serve/service_spec.py,sha256=Q0qnFRjNnfGIpksubH5VqPKIlvpWs5had_Ma_PSHyo8,16940
@@ -289,9 +289,9 @@ sky/utils/kubernetes/k8s_gpu_labeler_job.yaml,sha256=k0TBoQ4zgf79-sVkixKSGYFHQ7Z
289
289
  sky/utils/kubernetes/k8s_gpu_labeler_setup.yaml,sha256=VLKT2KKimZu1GDg_4AIlIt488oMQvhRZWwsj9vBbPUg,3812
290
290
  sky/utils/kubernetes/rsync_helper.sh,sha256=h4YwrPFf9727CACnMJvF3EyK_0OeOYKKt4su_daKekw,1256
291
291
  sky/utils/kubernetes/ssh_jump_lifecycle_manager.py,sha256=Kq1MDygF2IxFmu9FXpCxqucXLmeUrvs6OtRij6XTQbo,6554
292
- skypilot_nightly-1.0.0.dev20250202.dist-info/LICENSE,sha256=emRJAvE7ngL6x0RhQvlns5wJzGI3NEQ_WMjNmd9TZc4,12170
293
- skypilot_nightly-1.0.0.dev20250202.dist-info/METADATA,sha256=a3GhFswZbub9mm90ULVc-rXF0engagKI4_gOLGqwVac,21251
294
- skypilot_nightly-1.0.0.dev20250202.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
295
- skypilot_nightly-1.0.0.dev20250202.dist-info/entry_points.txt,sha256=StA6HYpuHj-Y61L2Ze-hK2IcLWgLZcML5gJu8cs6nU4,36
296
- skypilot_nightly-1.0.0.dev20250202.dist-info/top_level.txt,sha256=qA8QuiNNb6Y1OF-pCUtPEr6sLEwy2xJX06Bd_CrtrHY,4
297
- skypilot_nightly-1.0.0.dev20250202.dist-info/RECORD,,
292
+ skypilot_nightly-1.0.0.dev20250204.dist-info/LICENSE,sha256=emRJAvE7ngL6x0RhQvlns5wJzGI3NEQ_WMjNmd9TZc4,12170
293
+ skypilot_nightly-1.0.0.dev20250204.dist-info/METADATA,sha256=8lPz1CRU_biL8tZh6AWIZKdor-F20sON44UpXTOyiSw,21251
294
+ skypilot_nightly-1.0.0.dev20250204.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
295
+ skypilot_nightly-1.0.0.dev20250204.dist-info/entry_points.txt,sha256=StA6HYpuHj-Y61L2Ze-hK2IcLWgLZcML5gJu8cs6nU4,36
296
+ skypilot_nightly-1.0.0.dev20250204.dist-info/top_level.txt,sha256=qA8QuiNNb6Y1OF-pCUtPEr6sLEwy2xJX06Bd_CrtrHY,4
297
+ skypilot_nightly-1.0.0.dev20250204.dist-info/RECORD,,