anyscale 0.26.16__py3-none-any.whl → 0.26.18__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.
- anyscale/anyscale-cloud-setup-gcp.yaml +2 -0
- anyscale/anyscale-cloud-setup.yaml +0 -4
- anyscale/client/README.md +7 -37
- anyscale/client/openapi_client/__init__.py +5 -20
- anyscale/client/openapi_client/api/default_api.py +410 -2163
- anyscale/client/openapi_client/models/__init__.py +5 -20
- anyscale/client/openapi_client/models/{create_session_response.py → i_know_response.py} +51 -51
- anyscale/client/openapi_client/models/{session_details.py → i_know_time_series_event.py} +35 -35
- anyscale/client/openapi_client/models/job_report.py +199 -0
- anyscale/client/openapi_client/models/job_with_report.py +254 -0
- anyscale/client/openapi_client/models/{webterminal_list_response.py → jobwithreport_list_response.py} +15 -15
- anyscale/commands/cloud_commands.py +71 -0
- anyscale/connect_utils/prepare_cluster.py +19 -14
- anyscale/controllers/cloud_controller.py +164 -1
- anyscale/job/_private/job_sdk.py +22 -24
- anyscale/version.py +1 -1
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/METADATA +1 -1
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/RECORD +23 -38
- anyscale/client/openapi_client/models/archived_logs_info.py +0 -164
- anyscale/client/openapi_client/models/archivedlogsinfo_response.py +0 -121
- anyscale/client/openapi_client/models/create_experimental_workspace_from_job.py +0 -123
- anyscale/client/openapi_client/models/create_session_from_snapshot_options.py +0 -538
- anyscale/client/openapi_client/models/create_session_in_db.py +0 -434
- anyscale/client/openapi_client/models/createsessionresponse_response.py +0 -121
- anyscale/client/openapi_client/models/external_service_status.py +0 -147
- anyscale/client/openapi_client/models/external_service_status_response.py +0 -250
- anyscale/client/openapi_client/models/externalservicestatusresponse_response.py +0 -121
- anyscale/client/openapi_client/models/monitor_logs_extension.py +0 -100
- anyscale/client/openapi_client/models/session_describe.py +0 -175
- anyscale/client/openapi_client/models/session_history_item.py +0 -146
- anyscale/client/openapi_client/models/sessiondescribe_response.py +0 -121
- anyscale/client/openapi_client/models/sessiondetails_response.py +0 -121
- anyscale/client/openapi_client/models/sessionhistoryitem_list_response.py +0 -147
- anyscale/client/openapi_client/models/update_compute_template.py +0 -146
- anyscale/client/openapi_client/models/update_compute_template_config.py +0 -464
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/LICENSE +0 -0
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/NOTICE +0 -0
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/WHEEL +0 -0
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.16.dist-info → anyscale-0.26.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,254 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
"""
|
4
|
+
Managed Ray API
|
5
|
+
|
6
|
+
No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501
|
7
|
+
|
8
|
+
The version of the OpenAPI document: 0.1.0
|
9
|
+
Generated by: https://openapi-generator.tech
|
10
|
+
"""
|
11
|
+
|
12
|
+
|
13
|
+
import pprint
|
14
|
+
import re # noqa: F401
|
15
|
+
|
16
|
+
import six
|
17
|
+
|
18
|
+
from openapi_client.configuration import Configuration
|
19
|
+
|
20
|
+
|
21
|
+
class JobWithReport(object):
|
22
|
+
"""NOTE: This class is auto generated by OpenAPI Generator.
|
23
|
+
Ref: https://openapi-generator.tech
|
24
|
+
|
25
|
+
Do not edit the class manually.
|
26
|
+
"""
|
27
|
+
|
28
|
+
"""
|
29
|
+
Attributes:
|
30
|
+
openapi_types (dict): The key is attribute name
|
31
|
+
and the value is attribute type.
|
32
|
+
attribute_map (dict): The key is attribute name
|
33
|
+
and the value is json key in definition.
|
34
|
+
"""
|
35
|
+
openapi_types = {
|
36
|
+
'job_id': 'str',
|
37
|
+
'job_name': 'str',
|
38
|
+
'job_state': 'HaJobStates',
|
39
|
+
'job_report': 'JobReport',
|
40
|
+
'created_at': 'datetime',
|
41
|
+
'finished_at': 'datetime'
|
42
|
+
}
|
43
|
+
|
44
|
+
attribute_map = {
|
45
|
+
'job_id': 'job_id',
|
46
|
+
'job_name': 'job_name',
|
47
|
+
'job_state': 'job_state',
|
48
|
+
'job_report': 'job_report',
|
49
|
+
'created_at': 'created_at',
|
50
|
+
'finished_at': 'finished_at'
|
51
|
+
}
|
52
|
+
|
53
|
+
def __init__(self, job_id=None, job_name=None, job_state=None, job_report=None, created_at=None, finished_at=None, local_vars_configuration=None): # noqa: E501
|
54
|
+
"""JobWithReport - a model defined in OpenAPI""" # noqa: E501
|
55
|
+
if local_vars_configuration is None:
|
56
|
+
local_vars_configuration = Configuration()
|
57
|
+
self.local_vars_configuration = local_vars_configuration
|
58
|
+
|
59
|
+
self._job_id = None
|
60
|
+
self._job_name = None
|
61
|
+
self._job_state = None
|
62
|
+
self._job_report = None
|
63
|
+
self._created_at = None
|
64
|
+
self._finished_at = None
|
65
|
+
self.discriminator = None
|
66
|
+
|
67
|
+
self.job_id = job_id
|
68
|
+
self.job_name = job_name
|
69
|
+
self.job_state = job_state
|
70
|
+
if job_report is not None:
|
71
|
+
self.job_report = job_report
|
72
|
+
self.created_at = created_at
|
73
|
+
if finished_at is not None:
|
74
|
+
self.finished_at = finished_at
|
75
|
+
|
76
|
+
@property
|
77
|
+
def job_id(self):
|
78
|
+
"""Gets the job_id of this JobWithReport. # noqa: E501
|
79
|
+
|
80
|
+
|
81
|
+
:return: The job_id of this JobWithReport. # noqa: E501
|
82
|
+
:rtype: str
|
83
|
+
"""
|
84
|
+
return self._job_id
|
85
|
+
|
86
|
+
@job_id.setter
|
87
|
+
def job_id(self, job_id):
|
88
|
+
"""Sets the job_id of this JobWithReport.
|
89
|
+
|
90
|
+
|
91
|
+
:param job_id: The job_id of this JobWithReport. # noqa: E501
|
92
|
+
:type: str
|
93
|
+
"""
|
94
|
+
if self.local_vars_configuration.client_side_validation and job_id is None: # noqa: E501
|
95
|
+
raise ValueError("Invalid value for `job_id`, must not be `None`") # noqa: E501
|
96
|
+
|
97
|
+
self._job_id = job_id
|
98
|
+
|
99
|
+
@property
|
100
|
+
def job_name(self):
|
101
|
+
"""Gets the job_name of this JobWithReport. # noqa: E501
|
102
|
+
|
103
|
+
|
104
|
+
:return: The job_name of this JobWithReport. # noqa: E501
|
105
|
+
:rtype: str
|
106
|
+
"""
|
107
|
+
return self._job_name
|
108
|
+
|
109
|
+
@job_name.setter
|
110
|
+
def job_name(self, job_name):
|
111
|
+
"""Sets the job_name of this JobWithReport.
|
112
|
+
|
113
|
+
|
114
|
+
:param job_name: The job_name of this JobWithReport. # noqa: E501
|
115
|
+
:type: str
|
116
|
+
"""
|
117
|
+
if self.local_vars_configuration.client_side_validation and job_name is None: # noqa: E501
|
118
|
+
raise ValueError("Invalid value for `job_name`, must not be `None`") # noqa: E501
|
119
|
+
|
120
|
+
self._job_name = job_name
|
121
|
+
|
122
|
+
@property
|
123
|
+
def job_state(self):
|
124
|
+
"""Gets the job_state of this JobWithReport. # noqa: E501
|
125
|
+
|
126
|
+
|
127
|
+
:return: The job_state of this JobWithReport. # noqa: E501
|
128
|
+
:rtype: HaJobStates
|
129
|
+
"""
|
130
|
+
return self._job_state
|
131
|
+
|
132
|
+
@job_state.setter
|
133
|
+
def job_state(self, job_state):
|
134
|
+
"""Sets the job_state of this JobWithReport.
|
135
|
+
|
136
|
+
|
137
|
+
:param job_state: The job_state of this JobWithReport. # noqa: E501
|
138
|
+
:type: HaJobStates
|
139
|
+
"""
|
140
|
+
if self.local_vars_configuration.client_side_validation and job_state is None: # noqa: E501
|
141
|
+
raise ValueError("Invalid value for `job_state`, must not be `None`") # noqa: E501
|
142
|
+
|
143
|
+
self._job_state = job_state
|
144
|
+
|
145
|
+
@property
|
146
|
+
def job_report(self):
|
147
|
+
"""Gets the job_report of this JobWithReport. # noqa: E501
|
148
|
+
|
149
|
+
|
150
|
+
:return: The job_report of this JobWithReport. # noqa: E501
|
151
|
+
:rtype: JobReport
|
152
|
+
"""
|
153
|
+
return self._job_report
|
154
|
+
|
155
|
+
@job_report.setter
|
156
|
+
def job_report(self, job_report):
|
157
|
+
"""Sets the job_report of this JobWithReport.
|
158
|
+
|
159
|
+
|
160
|
+
:param job_report: The job_report of this JobWithReport. # noqa: E501
|
161
|
+
:type: JobReport
|
162
|
+
"""
|
163
|
+
|
164
|
+
self._job_report = job_report
|
165
|
+
|
166
|
+
@property
|
167
|
+
def created_at(self):
|
168
|
+
"""Gets the created_at of this JobWithReport. # noqa: E501
|
169
|
+
|
170
|
+
|
171
|
+
:return: The created_at of this JobWithReport. # noqa: E501
|
172
|
+
:rtype: datetime
|
173
|
+
"""
|
174
|
+
return self._created_at
|
175
|
+
|
176
|
+
@created_at.setter
|
177
|
+
def created_at(self, created_at):
|
178
|
+
"""Sets the created_at of this JobWithReport.
|
179
|
+
|
180
|
+
|
181
|
+
:param created_at: The created_at of this JobWithReport. # noqa: E501
|
182
|
+
:type: datetime
|
183
|
+
"""
|
184
|
+
if self.local_vars_configuration.client_side_validation and created_at is None: # noqa: E501
|
185
|
+
raise ValueError("Invalid value for `created_at`, must not be `None`") # noqa: E501
|
186
|
+
|
187
|
+
self._created_at = created_at
|
188
|
+
|
189
|
+
@property
|
190
|
+
def finished_at(self):
|
191
|
+
"""Gets the finished_at of this JobWithReport. # noqa: E501
|
192
|
+
|
193
|
+
|
194
|
+
:return: The finished_at of this JobWithReport. # noqa: E501
|
195
|
+
:rtype: datetime
|
196
|
+
"""
|
197
|
+
return self._finished_at
|
198
|
+
|
199
|
+
@finished_at.setter
|
200
|
+
def finished_at(self, finished_at):
|
201
|
+
"""Sets the finished_at of this JobWithReport.
|
202
|
+
|
203
|
+
|
204
|
+
:param finished_at: The finished_at of this JobWithReport. # noqa: E501
|
205
|
+
:type: datetime
|
206
|
+
"""
|
207
|
+
|
208
|
+
self._finished_at = finished_at
|
209
|
+
|
210
|
+
def to_dict(self):
|
211
|
+
"""Returns the model properties as a dict"""
|
212
|
+
result = {}
|
213
|
+
|
214
|
+
for attr, _ in six.iteritems(self.openapi_types):
|
215
|
+
value = getattr(self, attr)
|
216
|
+
if isinstance(value, list):
|
217
|
+
result[attr] = list(map(
|
218
|
+
lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
|
219
|
+
value
|
220
|
+
))
|
221
|
+
elif hasattr(value, "to_dict"):
|
222
|
+
result[attr] = value.to_dict()
|
223
|
+
elif isinstance(value, dict):
|
224
|
+
result[attr] = dict(map(
|
225
|
+
lambda item: (item[0], item[1].to_dict())
|
226
|
+
if hasattr(item[1], "to_dict") else item,
|
227
|
+
value.items()
|
228
|
+
))
|
229
|
+
else:
|
230
|
+
result[attr] = value
|
231
|
+
|
232
|
+
return result
|
233
|
+
|
234
|
+
def to_str(self):
|
235
|
+
"""Returns the string representation of the model"""
|
236
|
+
return pprint.pformat(self.to_dict())
|
237
|
+
|
238
|
+
def __repr__(self):
|
239
|
+
"""For `print` and `pprint`"""
|
240
|
+
return self.to_str()
|
241
|
+
|
242
|
+
def __eq__(self, other):
|
243
|
+
"""Returns true if both objects are equal"""
|
244
|
+
if not isinstance(other, JobWithReport):
|
245
|
+
return False
|
246
|
+
|
247
|
+
return self.to_dict() == other.to_dict()
|
248
|
+
|
249
|
+
def __ne__(self, other):
|
250
|
+
"""Returns true if both objects are not equal"""
|
251
|
+
if not isinstance(other, JobWithReport):
|
252
|
+
return True
|
253
|
+
|
254
|
+
return self.to_dict() != other.to_dict()
|
@@ -18,7 +18,7 @@ import six
|
|
18
18
|
from openapi_client.configuration import Configuration
|
19
19
|
|
20
20
|
|
21
|
-
class
|
21
|
+
class JobwithreportListResponse(object):
|
22
22
|
"""NOTE: This class is auto generated by OpenAPI Generator.
|
23
23
|
Ref: https://openapi-generator.tech
|
24
24
|
|
@@ -33,7 +33,7 @@ class WebterminalListResponse(object):
|
|
33
33
|
and the value is json key in definition.
|
34
34
|
"""
|
35
35
|
openapi_types = {
|
36
|
-
'results': 'list[
|
36
|
+
'results': 'list[JobWithReport]',
|
37
37
|
'metadata': 'ListResponseMetadata'
|
38
38
|
}
|
39
39
|
|
@@ -43,7 +43,7 @@ class WebterminalListResponse(object):
|
|
43
43
|
}
|
44
44
|
|
45
45
|
def __init__(self, results=None, metadata=None, local_vars_configuration=None): # noqa: E501
|
46
|
-
"""
|
46
|
+
"""JobwithreportListResponse - a model defined in OpenAPI""" # noqa: E501
|
47
47
|
if local_vars_configuration is None:
|
48
48
|
local_vars_configuration = Configuration()
|
49
49
|
self.local_vars_configuration = local_vars_configuration
|
@@ -58,21 +58,21 @@ class WebterminalListResponse(object):
|
|
58
58
|
|
59
59
|
@property
|
60
60
|
def results(self):
|
61
|
-
"""Gets the results of this
|
61
|
+
"""Gets the results of this JobwithreportListResponse. # noqa: E501
|
62
62
|
|
63
63
|
|
64
|
-
:return: The results of this
|
65
|
-
:rtype: list[
|
64
|
+
:return: The results of this JobwithreportListResponse. # noqa: E501
|
65
|
+
:rtype: list[JobWithReport]
|
66
66
|
"""
|
67
67
|
return self._results
|
68
68
|
|
69
69
|
@results.setter
|
70
70
|
def results(self, results):
|
71
|
-
"""Sets the results of this
|
71
|
+
"""Sets the results of this JobwithreportListResponse.
|
72
72
|
|
73
73
|
|
74
|
-
:param results: The results of this
|
75
|
-
:type: list[
|
74
|
+
:param results: The results of this JobwithreportListResponse. # noqa: E501
|
75
|
+
:type: list[JobWithReport]
|
76
76
|
"""
|
77
77
|
if self.local_vars_configuration.client_side_validation and results is None: # noqa: E501
|
78
78
|
raise ValueError("Invalid value for `results`, must not be `None`") # noqa: E501
|
@@ -81,20 +81,20 @@ class WebterminalListResponse(object):
|
|
81
81
|
|
82
82
|
@property
|
83
83
|
def metadata(self):
|
84
|
-
"""Gets the metadata of this
|
84
|
+
"""Gets the metadata of this JobwithreportListResponse. # noqa: E501
|
85
85
|
|
86
86
|
|
87
|
-
:return: The metadata of this
|
87
|
+
:return: The metadata of this JobwithreportListResponse. # noqa: E501
|
88
88
|
:rtype: ListResponseMetadata
|
89
89
|
"""
|
90
90
|
return self._metadata
|
91
91
|
|
92
92
|
@metadata.setter
|
93
93
|
def metadata(self, metadata):
|
94
|
-
"""Sets the metadata of this
|
94
|
+
"""Sets the metadata of this JobwithreportListResponse.
|
95
95
|
|
96
96
|
|
97
|
-
:param metadata: The metadata of this
|
97
|
+
:param metadata: The metadata of this JobwithreportListResponse. # noqa: E501
|
98
98
|
:type: ListResponseMetadata
|
99
99
|
"""
|
100
100
|
|
@@ -134,14 +134,14 @@ class WebterminalListResponse(object):
|
|
134
134
|
|
135
135
|
def __eq__(self, other):
|
136
136
|
"""Returns true if both objects are equal"""
|
137
|
-
if not isinstance(other,
|
137
|
+
if not isinstance(other, JobwithreportListResponse):
|
138
138
|
return False
|
139
139
|
|
140
140
|
return self.to_dict() == other.to_dict()
|
141
141
|
|
142
142
|
def __ne__(self, other):
|
143
143
|
"""Returns true if both objects are not equal"""
|
144
|
-
if not isinstance(other,
|
144
|
+
if not isinstance(other, JobwithreportListResponse):
|
145
145
|
return True
|
146
146
|
|
147
147
|
return self.to_dict() != other.to_dict()
|
@@ -1166,3 +1166,74 @@ def get_default_cloud() -> None:
|
|
1166
1166
|
|
1167
1167
|
except ValueError as e:
|
1168
1168
|
log.error(f"Error retrieving default cloud: {e}")
|
1169
|
+
|
1170
|
+
|
1171
|
+
@cloud_cli.command(
|
1172
|
+
name="jobs-report",
|
1173
|
+
help=(
|
1174
|
+
"Generate a report of the jobs created in the last 7 days in HTML format. "
|
1175
|
+
"Shows unused CPU-hours, unused GPU-hours, and other data."
|
1176
|
+
),
|
1177
|
+
cls=AnyscaleCommand,
|
1178
|
+
hidden=True,
|
1179
|
+
)
|
1180
|
+
@click.option(
|
1181
|
+
"--cloud-id",
|
1182
|
+
help="ID of the cloud to generate a report on.",
|
1183
|
+
type=str,
|
1184
|
+
required=True,
|
1185
|
+
)
|
1186
|
+
@click.option(
|
1187
|
+
"--csv",
|
1188
|
+
help="Outputs the report in CSV format.",
|
1189
|
+
type=bool,
|
1190
|
+
required=False,
|
1191
|
+
default=False,
|
1192
|
+
is_flag=True,
|
1193
|
+
)
|
1194
|
+
@click.option(
|
1195
|
+
"--out",
|
1196
|
+
help="Output file name for the report. (Default jobs_report.html)",
|
1197
|
+
type=str,
|
1198
|
+
required=False,
|
1199
|
+
default=None,
|
1200
|
+
)
|
1201
|
+
@click.option(
|
1202
|
+
"--sort-by",
|
1203
|
+
help=(
|
1204
|
+
"Column to sort by. (Default created_at). "
|
1205
|
+
"created_at: Job creation time. "
|
1206
|
+
"gpu: Unused GPU hours. "
|
1207
|
+
"cpu: Unused CPU hours. "
|
1208
|
+
"instances: Number of instances."
|
1209
|
+
),
|
1210
|
+
type=click.Choice(["created_at", "gpu", "cpu", "instances"], case_sensitive=False),
|
1211
|
+
required=False,
|
1212
|
+
default="created_at",
|
1213
|
+
)
|
1214
|
+
@click.option(
|
1215
|
+
"--sort-order",
|
1216
|
+
help="Sort order. (Default desc)",
|
1217
|
+
type=click.Choice(["asc", "desc"], case_sensitive=False),
|
1218
|
+
required=False,
|
1219
|
+
default="desc",
|
1220
|
+
)
|
1221
|
+
def generate_jobs_report(
|
1222
|
+
cloud_id: str, csv: bool, out: Optional[str], sort_by: str, sort_order: str
|
1223
|
+
) -> None:
|
1224
|
+
"""
|
1225
|
+
Generate a report of the jobs created in the last 7 days in HTML format.
|
1226
|
+
Shows unused CPU-hours, unused GPU-hours, and other data.
|
1227
|
+
:param cloud_id: The ID of the cloud to generate a report on.
|
1228
|
+
:param csv: Outputs the report in CSV format.
|
1229
|
+
:param out: Output file name for the report.
|
1230
|
+
"""
|
1231
|
+
if out is None:
|
1232
|
+
out = "jobs_report.html" if not csv else "jobs_report.csv"
|
1233
|
+
|
1234
|
+
try:
|
1235
|
+
CloudController().generate_jobs_report(
|
1236
|
+
cloud_id, csv, out, sort_by, sort_order == "asc"
|
1237
|
+
)
|
1238
|
+
except ValueError as e:
|
1239
|
+
log.error(f"Error generating jobs report: {e}")
|
@@ -56,6 +56,7 @@ BUILD_STEPS = [
|
|
56
56
|
# Default minutes for autosuspend.
|
57
57
|
DEFAULT_AUTOSUSPEND_TIMEOUT = 120
|
58
58
|
|
59
|
+
|
59
60
|
# Default docker images to use for connect clusters.
|
60
61
|
def _get_base_image(image: str, ray_version: str, cpu_or_gpu: str) -> str:
|
61
62
|
py_version = "".join(str(x) for x in sys.version_info[0:2])
|
@@ -557,34 +558,36 @@ class PrepareClusterBlock:
|
|
557
558
|
"""
|
558
559
|
Get the default cluster env build based on the local python and ray versions.
|
559
560
|
"""
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
561
|
+
major, minor = sys.version_info[:2]
|
562
|
+
MIN_PY_VER = (3, 8)
|
563
|
+
MAX_PY_VER = (3, 12)
|
564
|
+
|
565
|
+
if not (MIN_PY_VER <= (major, minor) <= MAX_PY_VER):
|
564
566
|
raise ValueError(
|
565
|
-
"No default
|
566
|
-
|
567
|
-
|
567
|
+
f"No default container image for python version {major}.{minor}."
|
568
|
+
f"Please use a Python version between {MIN_PY_VER[0]}.{MIN_PY_VER[1]} "
|
569
|
+
f"and {MAX_PY_VER[0]}.{MAX_PY_VER[1]}."
|
568
570
|
)
|
571
|
+
|
569
572
|
ray_version = self._ray.__version__
|
570
573
|
if version.parse(ray_version) < version.parse(MINIMUM_RAY_VERSION):
|
571
574
|
raise ValueError(
|
572
|
-
f"No default
|
575
|
+
f"No default container image for Ray version {ray_version}. Please upgrade "
|
573
576
|
f"to a version >= {MINIMUM_RAY_VERSION}."
|
574
577
|
)
|
575
578
|
if "dev0" in ray_version:
|
576
579
|
raise ValueError(
|
577
580
|
f"Your locally installed Ray version is {ray_version}. "
|
578
|
-
"There is no default
|
581
|
+
"There is no default container image for nightly versions of Ray."
|
579
582
|
)
|
580
583
|
try:
|
581
584
|
build = self.api_client.get_default_cluster_env_build_api_v2_builds_default_py_version_ray_version_get(
|
582
|
-
f"py{
|
585
|
+
f"py{major}{minor}", ray_version
|
583
586
|
).result
|
584
587
|
return build
|
585
588
|
except Exception: # noqa: BLE001
|
586
589
|
raise RuntimeError(
|
587
|
-
f"Failed to get default
|
590
|
+
f"Failed to get default container image for Ray: {ray_version} on Python: py{major}{minor}"
|
588
591
|
)
|
589
592
|
|
590
593
|
def _get_cluster_build(
|
@@ -815,14 +818,16 @@ class PrepareClusterBlock:
|
|
815
818
|
return compute_template_id
|
816
819
|
|
817
820
|
def _register_compute_template(
|
818
|
-
self, project_id: str, config_object: ComputeTemplateConfig
|
821
|
+
self, project_id: str, config_object: ComputeTemplateConfig # noqa: ARG002
|
819
822
|
) -> str:
|
820
823
|
"""
|
821
|
-
Register compute template with a default name and return the compute template id.
|
824
|
+
Register compute template with a default name and return the compute template id.
|
825
|
+
"""
|
822
826
|
created_template = self.api_client.create_compute_template_api_v2_compute_templates_post(
|
823
827
|
create_compute_template=CreateComputeTemplate(
|
824
828
|
name=gen_valid_name("autogenerated-config"),
|
825
|
-
|
829
|
+
# project ID is deprecated in compute config
|
830
|
+
project_id=None,
|
826
831
|
config=config_object,
|
827
832
|
anonymous=True,
|
828
833
|
)
|
@@ -3,6 +3,7 @@ Fetches data required and formats output for `anyscale cloud` commands.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import copy
|
6
|
+
from datetime import datetime, timedelta
|
6
7
|
import json
|
7
8
|
from os import getenv
|
8
9
|
import pathlib
|
@@ -16,6 +17,7 @@ import boto3
|
|
16
17
|
from botocore.exceptions import ClientError, NoCredentialsError
|
17
18
|
import click
|
18
19
|
from click import Abort, ClickException
|
20
|
+
from rich.progress import Progress, track
|
19
21
|
import yaml
|
20
22
|
|
21
23
|
from anyscale import __version__ as anyscale_version
|
@@ -72,8 +74,12 @@ from anyscale.controllers.cloud_functional_verification_controller import (
|
|
72
74
|
CloudFunctionalVerificationType,
|
73
75
|
)
|
74
76
|
from anyscale.formatters import clouds_formatter
|
77
|
+
from anyscale.job._private.job_sdk import (
|
78
|
+
HA_JOB_STATE_TO_JOB_STATE,
|
79
|
+
TERMINAL_HA_JOB_STATES,
|
80
|
+
)
|
75
81
|
from anyscale.shared_anyscale_utils.aws import AwsRoleArn
|
76
|
-
from anyscale.shared_anyscale_utils.conf import ANYSCALE_ENV
|
82
|
+
from anyscale.shared_anyscale_utils.conf import ANYSCALE_ENV, ANYSCALE_HOST
|
77
83
|
from anyscale.util import ( # pylint:disable=private-import
|
78
84
|
_client,
|
79
85
|
_get_aws_efs_mount_target_ip,
|
@@ -3643,3 +3649,160 @@ class CloudController(BaseController):
|
|
3643
3649
|
)
|
3644
3650
|
|
3645
3651
|
### End of edit cloud ###
|
3652
|
+
|
3653
|
+
def generate_jobs_report(
|
3654
|
+
self, cloud_id: str, csv: bool, out_path: str, sort: str, sort_order_asc: bool
|
3655
|
+
) -> None:
|
3656
|
+
end_time = datetime.now()
|
3657
|
+
start_time = end_time - timedelta(days=7)
|
3658
|
+
|
3659
|
+
full_results = []
|
3660
|
+
paging_token: Optional[str] = None
|
3661
|
+
count_per_page = 20
|
3662
|
+
curr_page_results = None
|
3663
|
+
total_jobs: Optional[int] = None
|
3664
|
+
|
3665
|
+
with Progress() as progress:
|
3666
|
+
download_task = progress.add_task("Downloading jobs...", total=None)
|
3667
|
+
|
3668
|
+
while curr_page_results is None or len(curr_page_results) == count_per_page:
|
3669
|
+
response = self.api_client.list_job_reports_api_v2_job_reports_get(
|
3670
|
+
cloud_id,
|
3671
|
+
start_time=start_time,
|
3672
|
+
end_time=end_time,
|
3673
|
+
paging_token=paging_token,
|
3674
|
+
count=count_per_page,
|
3675
|
+
)
|
3676
|
+
curr_page_results = response.results
|
3677
|
+
full_results.extend(curr_page_results)
|
3678
|
+
paging_token = response.metadata.next_paging_token
|
3679
|
+
total_jobs = response.metadata.total
|
3680
|
+
progress.update(
|
3681
|
+
download_task, total=total_jobs, advance=len(curr_page_results)
|
3682
|
+
)
|
3683
|
+
|
3684
|
+
progress.update(download_task, completed=total_jobs)
|
3685
|
+
|
3686
|
+
if not full_results:
|
3687
|
+
self.log.info("No jobs found in the last 7 days.")
|
3688
|
+
return
|
3689
|
+
|
3690
|
+
filtered_results = [
|
3691
|
+
job
|
3692
|
+
for job in full_results
|
3693
|
+
if job.job_state in TERMINAL_HA_JOB_STATES and job.job_report is not None
|
3694
|
+
]
|
3695
|
+
if sort == "created_at":
|
3696
|
+
filtered_results.sort(
|
3697
|
+
key=lambda x: x.created_at, reverse=not sort_order_asc
|
3698
|
+
)
|
3699
|
+
elif sort == "gpu":
|
3700
|
+
filtered_results.sort(
|
3701
|
+
key=lambda x: x.job_report.unused_gpu_hours or 0,
|
3702
|
+
reverse=not sort_order_asc,
|
3703
|
+
)
|
3704
|
+
elif sort == "cpu":
|
3705
|
+
filtered_results.sort(
|
3706
|
+
key=lambda x: x.job_report.unused_cpu_hours or 0,
|
3707
|
+
reverse=not sort_order_asc,
|
3708
|
+
)
|
3709
|
+
elif sort == "instances":
|
3710
|
+
filtered_results.sort(
|
3711
|
+
key=lambda x: x.job_report.max_instances_launched or 0,
|
3712
|
+
reverse=not sort_order_asc,
|
3713
|
+
)
|
3714
|
+
|
3715
|
+
with open(out_path, "w") as out_file:
|
3716
|
+
if csv:
|
3717
|
+
out_file.write(
|
3718
|
+
"Job ID,Job name,Job state,Created at,Finished at,Duration,Unused CPU hours,Unused GPU hours,Max concurrent instances\n"
|
3719
|
+
)
|
3720
|
+
for job in track(filtered_results, description="Generating report..."):
|
3721
|
+
job_state = HA_JOB_STATE_TO_JOB_STATE[job.job_state]
|
3722
|
+
if job.finished_at is not None:
|
3723
|
+
duration = str(job.finished_at - job.created_at)
|
3724
|
+
finished_at = str(job.finished_at)
|
3725
|
+
else:
|
3726
|
+
duration = ""
|
3727
|
+
finished_at = ""
|
3728
|
+
unused_cpu_hours = job.job_report.unused_cpu_hours or ""
|
3729
|
+
unused_gpu_hours = job.job_report.unused_gpu_hours or ""
|
3730
|
+
max_instances_launched = job.job_report.max_instances_launched or ""
|
3731
|
+
|
3732
|
+
out_file.write(
|
3733
|
+
f'"{job.job_id}","{job.job_name}","{job_state}","{str(job.created_at)}","{finished_at}","{duration}","{unused_cpu_hours}","{unused_gpu_hours}","{max_instances_launched}"\n'
|
3734
|
+
)
|
3735
|
+
else:
|
3736
|
+
out_file.write(
|
3737
|
+
f"""
|
3738
|
+
<html>
|
3739
|
+
<head>
|
3740
|
+
<title>Jobs Report - {str(end_time)}</title>
|
3741
|
+
<style>
|
3742
|
+
table {{
|
3743
|
+
border: 1px solid black;
|
3744
|
+
border-collapse: collapse;
|
3745
|
+
}}
|
3746
|
+
th, td {{
|
3747
|
+
border: 1px solid black;
|
3748
|
+
border-collapse: collapse;
|
3749
|
+
padding: 8px;
|
3750
|
+
}}
|
3751
|
+
</style>
|
3752
|
+
</head>
|
3753
|
+
<body>
|
3754
|
+
<h1>Job Report - {str(end_time)}</h1>
|
3755
|
+
<p>Total jobs reported (finished jobs): {len(filtered_results)}</p>
|
3756
|
+
<p>Total jobs in the last 7 days: {total_jobs}</p>
|
3757
|
+
<table>
|
3758
|
+
<thead>
|
3759
|
+
<tr>
|
3760
|
+
<th>Job ID</th>
|
3761
|
+
<th>Job name</th>
|
3762
|
+
<th>Job state</th>
|
3763
|
+
<th>Created at</th>
|
3764
|
+
<th>Finished at</th>
|
3765
|
+
<th>Duration</th>
|
3766
|
+
<th>Unused CPU hours</th>
|
3767
|
+
<th>Unused GPU hours</th>
|
3768
|
+
<th>Max concurrent instances</th>
|
3769
|
+
</tr>
|
3770
|
+
</thead>
|
3771
|
+
<tbody>
|
3772
|
+
"""
|
3773
|
+
)
|
3774
|
+
|
3775
|
+
for job in track(filtered_results, description="Generating report..."):
|
3776
|
+
job_state = HA_JOB_STATE_TO_JOB_STATE[job.job_state]
|
3777
|
+
if job.finished_at is not None:
|
3778
|
+
duration = str(job.finished_at - job.created_at)
|
3779
|
+
finished_at = str(job.finished_at)
|
3780
|
+
else:
|
3781
|
+
duration = ""
|
3782
|
+
finished_at = ""
|
3783
|
+
unused_cpu_hours = job.job_report.unused_cpu_hours or ""
|
3784
|
+
unused_gpu_hours = job.job_report.unused_gpu_hours or ""
|
3785
|
+
max_instances_launched = job.job_report.max_instances_launched or ""
|
3786
|
+
|
3787
|
+
out_file.write(
|
3788
|
+
f"""
|
3789
|
+
<tr>
|
3790
|
+
<td><a target="_blank" rel="noreferrer" href="{ANYSCALE_HOST}/jobs/{job.job_id}">{job.job_id}</a></td>
|
3791
|
+
<td>{job.job_name}</td>
|
3792
|
+
<td>{job_state}</td>
|
3793
|
+
<td>{str(job.created_at)}</td>
|
3794
|
+
<td>{finished_at}</td>
|
3795
|
+
<td>{duration}</td>
|
3796
|
+
<td>{unused_cpu_hours}</td>
|
3797
|
+
<td>{unused_gpu_hours}</td>
|
3798
|
+
<td>{max_instances_launched}</td>
|
3799
|
+
</tr>
|
3800
|
+
"""
|
3801
|
+
)
|
3802
|
+
|
3803
|
+
out_file.write(
|
3804
|
+
"""
|
3805
|
+
</tbody>
|
3806
|
+
</table>
|
3807
|
+
"""
|
3808
|
+
)
|