Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1110)

Side by Side Diff: appengine/swarming/handlers_endpoints.py

Issue 2984843002: swarming: switch to a 'capability focused' ACL system (Closed)
Patch Set: Address comments Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2015 The LUCI Authors. All rights reserved. 1 # Copyright 2015 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 """This module defines Swarming Server endpoints handlers.""" 5 """This module defines Swarming Server endpoints handlers."""
6 6
7 import logging 7 import logging
8 import os 8 import os
9 9
10 from google.appengine.api import datastore_errors 10 from google.appengine.api import datastore_errors
(...skipping 22 matching lines...) Expand all
33 from server import task_pack 33 from server import task_pack
34 from server import task_queues 34 from server import task_queues
35 from server import task_request 35 from server import task_request
36 from server import task_result 36 from server import task_result
37 from server import task_scheduler 37 from server import task_scheduler
38 38
39 39
40 ### Helper Methods 40 ### Helper Methods
41 41
42 42
43 # Used by get_request_and_result(), clearer than using True/False and important
44 # as this is part of the security boundary.
45 _EDIT = object()
46 _VIEW = object()
47
48
43 # Add support for BooleanField in protorpc in endpoints GET requests. 49 # Add support for BooleanField in protorpc in endpoints GET requests.
44 _old_decode_field = protojson.ProtoJson.decode_field 50 _old_decode_field = protojson.ProtoJson.decode_field
45 def _decode_field(self, field, value): 51 def _decode_field(self, field, value):
46 if (isinstance(field, messages.BooleanField) and 52 if (isinstance(field, messages.BooleanField) and
47 isinstance(value, basestring)): 53 isinstance(value, basestring)):
48 return value.lower() == 'true' 54 return value.lower() == 'true'
49 return _old_decode_field(self, field, value) 55 return _old_decode_field(self, field, value)
50 protojson.ProtoJson.decode_field = _decode_field 56 protojson.ProtoJson.decode_field = _decode_field
51 57
52 58
53 def get_request_and_result(task_id): 59 def get_request_and_result(task_id, viewing):
54 """Provides the key and TaskRequest corresponding to a task ID. 60 """Provides the key and TaskRequest corresponding to a task ID.
55 61
56 Enforces the ACL for users. Allows bots all access for the moment. 62 Enforces the ACL for users. Allows bots all access for the moment.
57 63
58 Returns: 64 Returns:
59 tuple(TaskRequest, result): result can be either for a TaskRunResult or a 65 tuple(TaskRequest, result): result can be either for a TaskRunResult or a
60 TaskResultSummay. 66 TaskResultSummay.
61 """ 67 """
62 try: 68 try:
63 request_key, result_key = task_pack.get_request_and_result_keys(task_id) 69 request_key, result_key = task_pack.get_request_and_result_keys(task_id)
64 request, result = ndb.get_multi((request_key, result_key)) 70 request, result = ndb.get_multi((request_key, result_key))
65 if not request or not result: 71 if not request or not result:
66 raise endpoints.NotFoundException('%s not found.' % task_id) 72 raise endpoints.NotFoundException('%s not found.' % task_id)
Vadim Sh. 2017/07/24 23:07:25 strictly speaking we are leaking presence/absence
M-A Ruel 2017/07/25 13:46:38 I do not mind leaking the presence of objects, as
67 if not acl.is_bot() and not request.has_access: 73 if viewing == _VIEW:
Vadim Sh. 2017/07/24 23:07:25 can we move these checks outside of 'try/except' b
M-A Ruel 2017/07/25 13:46:38 True, fixed.
68 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) 74 if not acl.can_view_task(request):
75 raise endpoints.ForbiddenException('%s is not accessible.' % task_id)
76 elif viewing == _EDIT:
77 if not acl.can_edit_task(request):
78 raise endpoints.ForbiddenException('%s is not accessible.' % task_id)
79 else:
80 raise endpoints.InternalServerErrorException('get_request_and_result()')
69 return request, result 81 return request, result
70 except ValueError: 82 except ValueError:
71 raise endpoints.BadRequestException('%s is an invalid key.' % task_id) 83 raise endpoints.BadRequestException('%s is an invalid key.' % task_id)
72 84
73 85
74 def get_or_raise(key): 86 def get_or_raise(key):
75 """Returns an entity or raises an endpoints exception if it does not exist.""" 87 """Returns an entity or raises an endpoints exception if it does not exist."""
76 result = key.get() 88 result = key.get()
77 if not result: 89 if not result:
78 raise endpoints.NotFoundException('%s not found.' % key.id()) 90 raise endpoints.NotFoundException('%s not found.' % key.id())
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
121 message_types.VoidMessage, 133 message_types.VoidMessage,
122 version=messages.IntegerField(1)) 134 version=messages.IntegerField(1))
123 135
124 136
125 @swarming_api.api_class(resource_name='server', path='server') 137 @swarming_api.api_class(resource_name='server', path='server')
126 class SwarmingServerService(remote.Service): 138 class SwarmingServerService(remote.Service):
127 @gae_ts_mon.instrument_endpoint() 139 @gae_ts_mon.instrument_endpoint()
128 @auth.endpoints_method( 140 @auth.endpoints_method(
129 message_types.VoidMessage, swarming_rpcs.ServerDetails, 141 message_types.VoidMessage, swarming_rpcs.ServerDetails,
130 http_method='GET') 142 http_method='GET')
131 @auth.require(acl.is_bot_or_user) 143 @auth.require(acl.can_access)
132 def details(self, _request): 144 def details(self, _request):
133 """Returns information about the server.""" 145 """Returns information about the server."""
134 host = 'https://' + os.environ['HTTP_HOST'] 146 host = 'https://' + os.environ['HTTP_HOST']
135 147
136 cfg = config.settings() 148 cfg = config.settings()
137 149
138 mpp = '' 150 mpp = ''
139 if cfg.mp and cfg.mp.server: 151 if cfg.mp and cfg.mp.server:
140 mpp = cfg.mp.server 152 mpp = cfg.mp.server
141 # as a fallback, try pulling from datastore 153 # as a fallback, try pulling from datastore
142 if not mpp: 154 if not mpp:
143 mpp = machine_provider.MachineProviderConfiguration.get_instance_url() 155 mpp = machine_provider.MachineProviderConfiguration.get_instance_url()
144 if mpp: 156 if mpp:
145 mpp = mpp + '/leases/%s' 157 mpp = mpp + '/leases/%s'
146 158
147 return swarming_rpcs.ServerDetails( 159 return swarming_rpcs.ServerDetails(
148 bot_version=bot_code.get_bot_version(host)[0], 160 bot_version=bot_code.get_bot_version(host)[0],
149 server_version=utils.get_app_version(), 161 server_version=utils.get_app_version(),
150 machine_provider_template=mpp, 162 machine_provider_template=mpp,
151 display_server_url_template=cfg.display_server_url_template, 163 display_server_url_template=cfg.display_server_url_template,
152 luci_config=config.config.config_service_hostname(), 164 luci_config=config.config.config_service_hostname(),
153 default_isolate_server=cfg.isolate.default_server, 165 default_isolate_server=cfg.isolate.default_server,
154 default_isolate_namespace=cfg.isolate.default_namespace) 166 default_isolate_namespace=cfg.isolate.default_namespace)
155 167
156 @gae_ts_mon.instrument_endpoint() 168 @gae_ts_mon.instrument_endpoint()
157 @auth.endpoints_method( 169 @auth.endpoints_method(
158 message_types.VoidMessage, swarming_rpcs.BootstrapToken) 170 message_types.VoidMessage, swarming_rpcs.BootstrapToken)
159 @auth.require(acl.is_bootstrapper) 171 @auth.require(acl.can_create_bot)
160 def token(self, _request): 172 def token(self, _request):
161 """Returns a token to bootstrap a new bot.""" 173 """Returns a token to bootstrap a new bot."""
162 return swarming_rpcs.BootstrapToken( 174 return swarming_rpcs.BootstrapToken(
163 bootstrap_token = bot_code.generate_bootstrap_token(), 175 bootstrap_token = bot_code.generate_bootstrap_token(),
164 ) 176 )
165 177
166 @gae_ts_mon.instrument_endpoint() 178 @gae_ts_mon.instrument_endpoint()
167 @auth.endpoints_method( 179 @auth.endpoints_method(
168 message_types.VoidMessage, swarming_rpcs.ClientPermissions, 180 message_types.VoidMessage, swarming_rpcs.ClientPermissions,
169 http_method='GET') 181 http_method='GET')
170 @auth.public 182 @auth.public
171 def permissions(self, _request): 183 def permissions(self, _request):
172 """Returns the caller's permissions.""" 184 """Returns the caller's permissions."""
185 # TODO(maruel): Redo this.
173 return swarming_rpcs.ClientPermissions( 186 return swarming_rpcs.ClientPermissions(
174 delete_bot = acl.is_admin(), 187 delete_bot = acl._is_admin(),
175 terminate_bot = acl.is_privileged_user(), 188 terminate_bot = acl._is_privileged_user(),
176 get_configs = acl.is_user(), 189 get_configs = acl._is_user(),
177 put_configs = acl.is_admin(), 190 put_configs = acl._is_admin(),
178 cancel_task = acl.is_user(), 191 cancel_task = acl._is_user(),
179 cancel_tasks = acl.is_admin(), 192 cancel_tasks = acl._is_admin(),
180 get_bootstrap_token = acl.is_bootstrapper(), 193 get_bootstrap_token = acl._is_bootstrapper(),
181 ) 194 )
182 195
183 @gae_ts_mon.instrument_endpoint() 196 @gae_ts_mon.instrument_endpoint()
184 @auth.endpoints_method( 197 @auth.endpoints_method(
185 VersionRequest, swarming_rpcs.FileContent, 198 VersionRequest, swarming_rpcs.FileContent,
186 http_method='GET') 199 http_method='GET')
187 @auth.require(acl.is_bot_or_user) 200 @auth.require(acl.can_view_config)
188 def get_bootstrap(self, request): 201 def get_bootstrap(self, request):
189 """Retrieves the current or a previous version of bootstrap.py. 202 """Retrieves the current or a previous version of bootstrap.py.
190 203
191 When the file is sourced via luci-config, the version parameter is ignored. 204 When the file is sourced via luci-config, the version parameter is ignored.
192 Eventually the support for 'version' will be removed completely. 205 Eventually the support for 'version' will be removed completely.
193 """ 206 """
194 obj = bot_code.get_bootstrap('', '', request.version) 207 obj = bot_code.get_bootstrap('', '', request.version)
195 if not obj: 208 if not obj:
196 return swarming_rpcs.FileContent() 209 return swarming_rpcs.FileContent()
197 return swarming_rpcs.FileContent( 210 return swarming_rpcs.FileContent(
198 content=obj.content.decode('utf-8'), 211 content=obj.content.decode('utf-8'),
199 who=obj.who, 212 who=obj.who,
200 when=obj.when, 213 when=obj.when,
201 version=obj.version) 214 version=obj.version)
202 215
203 @gae_ts_mon.instrument_endpoint() 216 @gae_ts_mon.instrument_endpoint()
204 @auth.endpoints_method( 217 @auth.endpoints_method(
205 VersionRequest, swarming_rpcs.FileContent, 218 VersionRequest, swarming_rpcs.FileContent,
206 http_method='GET') 219 http_method='GET')
207 @auth.require(acl.is_bot_or_user) 220 @auth.require(acl.can_view_config)
208 def get_bot_config(self, request): 221 def get_bot_config(self, request):
209 """Retrieves the current or a previous version of bot_config.py. 222 """Retrieves the current or a previous version of bot_config.py.
210 223
211 When the file is sourced via luci-config, the version parameter is ignored. 224 When the file is sourced via luci-config, the version parameter is ignored.
212 Eventually the support for 'version' will be removed completely. 225 Eventually the support for 'version' will be removed completely.
213 """ 226 """
214 obj = bot_code.get_bot_config(request.version) 227 obj = bot_code.get_bot_config(request.version)
215 if not obj: 228 if not obj:
216 return swarming_rpcs.FileContent() 229 return swarming_rpcs.FileContent()
217 return swarming_rpcs.FileContent( 230 return swarming_rpcs.FileContent(
218 content=obj.content.decode('utf-8'), 231 content=obj.content.decode('utf-8'),
219 who=obj.who, 232 who=obj.who,
220 when=obj.when, 233 when=obj.when,
221 version=obj.version) 234 version=obj.version)
222 235
223 @gae_ts_mon.instrument_endpoint() 236 @gae_ts_mon.instrument_endpoint()
224 @auth.endpoints_method( 237 @auth.endpoints_method(
225 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent) 238 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent)
226 @auth.require(acl.is_admin) 239 @auth.require(acl.can_edit_config)
227 def put_bootstrap(self, request): 240 def put_bootstrap(self, request):
228 """Stores a new version of bootstrap.py. 241 """Stores a new version of bootstrap.py.
229 242
230 Warning: if a file exists in luci-config, the file stored by this function 243 Warning: if a file exists in luci-config, the file stored by this function
231 is ignored. Uploads are not blocked in case the file is later deleted from 244 is ignored. Uploads are not blocked in case the file is later deleted from
232 luci-config. 245 luci-config.
233 """ 246 """
234 key = bot_code.store_bootstrap(request.content.encode('utf-8')) 247 key = bot_code.store_bootstrap(request.content.encode('utf-8'))
235 obj = key.get() 248 obj = key.get()
236 return swarming_rpcs.FileContent( 249 return swarming_rpcs.FileContent(
237 who=obj.who.to_bytes() if obj.who else None, 250 who=obj.who.to_bytes() if obj.who else None,
238 when=obj.created_ts, 251 when=obj.created_ts,
239 version=str(obj.version)) 252 version=str(obj.version))
240 253
241 @gae_ts_mon.instrument_endpoint() 254 @gae_ts_mon.instrument_endpoint()
242 @auth.endpoints_method( 255 @auth.endpoints_method(
243 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent) 256 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent)
244 @auth.require(acl.is_admin) 257 @auth.require(acl.can_edit_config)
245 def put_bot_config(self, request): 258 def put_bot_config(self, request):
246 """Stores a new version of bot_config.py. 259 """Stores a new version of bot_config.py.
247 260
248 Warning: if a file exists in luci-config, the file stored by this function 261 Warning: if a file exists in luci-config, the file stored by this function
249 is ignored. Uploads are not blocked in case the file is later deleted from 262 is ignored. Uploads are not blocked in case the file is later deleted from
250 luci-config. 263 luci-config.
251 """ 264 """
252 host = 'https://' + os.environ['HTTP_HOST'] 265 host = 'https://' + os.environ['HTTP_HOST']
253 key = bot_code.store_bot_config(host, request.content.encode('utf-8')) 266 key = bot_code.store_bot_config(host, request.content.encode('utf-8'))
254 obj = key.get() 267 obj = key.get()
(...skipping 16 matching lines...) Expand all
271 284
272 @swarming_api.api_class(resource_name='task', path='task') 285 @swarming_api.api_class(resource_name='task', path='task')
273 class SwarmingTaskService(remote.Service): 286 class SwarmingTaskService(remote.Service):
274 """Swarming's task-related API.""" 287 """Swarming's task-related API."""
275 @gae_ts_mon.instrument_endpoint() 288 @gae_ts_mon.instrument_endpoint()
276 @auth.endpoints_method( 289 @auth.endpoints_method(
277 TaskIdWithPerf, swarming_rpcs.TaskResult, 290 TaskIdWithPerf, swarming_rpcs.TaskResult,
278 name='result', 291 name='result',
279 path='{task_id}/result', 292 path='{task_id}/result',
280 http_method='GET') 293 http_method='GET')
281 @auth.require(acl.is_bot_or_user) 294 @auth.require(acl.can_access)
282 def result(self, request): 295 def result(self, request):
283 """Reports the result of the task corresponding to a task ID. 296 """Reports the result of the task corresponding to a task ID.
284 297
285 It can be a 'run' ID specifying a specific retry or a 'summary' ID hidding 298 It can be a 'run' ID specifying a specific retry or a 'summary' ID hidding
286 the fact that a task may have been retried transparently, when a bot reports 299 the fact that a task may have been retried transparently, when a bot reports
287 BOT_DIED. 300 BOT_DIED.
288 301
289 A summary ID ends with '0', a run ID ends with '1' or '2'. 302 A summary ID ends with '0', a run ID ends with '1' or '2'.
290 """ 303 """
291 logging.debug('%s', request) 304 logging.debug('%s', request)
292 _, result = get_request_and_result(request.task_id) 305 _, result = get_request_and_result(request.task_id, _VIEW)
293 return message_conversion.task_result_to_rpc( 306 return message_conversion.task_result_to_rpc(
294 result, request.include_performance_stats) 307 result, request.include_performance_stats)
295 308
296 @gae_ts_mon.instrument_endpoint() 309 @gae_ts_mon.instrument_endpoint()
297 @auth.endpoints_method( 310 @auth.endpoints_method(
298 TaskId, swarming_rpcs.TaskRequest, 311 TaskId, swarming_rpcs.TaskRequest,
299 name='request', 312 name='request',
300 path='{task_id}/request', 313 path='{task_id}/request',
301 http_method='GET') 314 http_method='GET')
302 @auth.require(acl.is_bot_or_user) 315 @auth.require(acl.can_access)
303 def request(self, request): 316 def request(self, request):
304 """Returns the task request corresponding to a task ID.""" 317 """Returns the task request corresponding to a task ID."""
305 logging.debug('%s', request) 318 logging.debug('%s', request)
306 request_obj, _ = get_request_and_result(request.task_id) 319 request_obj, _ = get_request_and_result(request.task_id, _VIEW)
307 return message_conversion.task_request_to_rpc(request_obj) 320 return message_conversion.task_request_to_rpc(request_obj)
308 321
309 @gae_ts_mon.instrument_endpoint() 322 @gae_ts_mon.instrument_endpoint()
310 @auth.endpoints_method( 323 @auth.endpoints_method(
311 TaskId, swarming_rpcs.CancelResponse, 324 TaskId, swarming_rpcs.CancelResponse,
312 name='cancel', 325 name='cancel',
313 path='{task_id}/cancel') 326 path='{task_id}/cancel')
314 @auth.require(acl.is_bot_or_user) 327 @auth.require(acl.can_access)
315 def cancel(self, request): 328 def cancel(self, request):
316 """Cancels a task. 329 """Cancels a task.
317 330
318 If a bot was running the task, the bot will forcibly cancel the task. 331 If a bot was running the task, the bot will forcibly cancel the task.
319 """ 332 """
320 logging.debug('%s', request) 333 logging.debug('%s', request)
321 request_obj, result = get_request_and_result(request.task_id) 334 request_obj, result = get_request_and_result(request.task_id, _EDIT)
322 ok, was_running = task_scheduler.cancel_task(request_obj, result.key) 335 ok, was_running = task_scheduler.cancel_task(request_obj, result.key)
323 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running) 336 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running)
324 337
325 @gae_ts_mon.instrument_endpoint() 338 @gae_ts_mon.instrument_endpoint()
326 @auth.endpoints_method( 339 @auth.endpoints_method(
327 TaskId, swarming_rpcs.TaskOutput, 340 TaskId, swarming_rpcs.TaskOutput,
328 name='stdout', 341 name='stdout',
329 path='{task_id}/stdout', 342 path='{task_id}/stdout',
330 http_method='GET') 343 http_method='GET')
331 @auth.require(acl.is_bot_or_user) 344 @auth.require(acl.can_access)
332 def stdout(self, request): 345 def stdout(self, request):
333 """Returns the output of the task corresponding to a task ID.""" 346 """Returns the output of the task corresponding to a task ID."""
334 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine 347 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine
335 # v1. 348 # v1.
336 # TODO(maruel): Send as raw content instead of encoded. This is not 349 # TODO(maruel): Send as raw content instead of encoded. This is not
337 # supported by cloud endpoints. 350 # supported by cloud endpoints.
338 logging.debug('%s', request) 351 logging.debug('%s', request)
339 _, result = get_request_and_result(request.task_id) 352 _, result = get_request_and_result(request.task_id, _VIEW)
340 output = result.get_output() 353 output = result.get_output()
341 if output: 354 if output:
342 output = output.decode('utf-8', 'replace') 355 output = output.decode('utf-8', 'replace')
343 return swarming_rpcs.TaskOutput(output=output) 356 return swarming_rpcs.TaskOutput(output=output)
344 357
345 358
346 @swarming_api.api_class(resource_name='tasks', path='tasks') 359 @swarming_api.api_class(resource_name='tasks', path='tasks')
347 class SwarmingTasksService(remote.Service): 360 class SwarmingTasksService(remote.Service):
348 """Swarming's tasks-related API.""" 361 """Swarming's tasks-related API."""
349 @gae_ts_mon.instrument_endpoint() 362 @gae_ts_mon.instrument_endpoint()
350 @auth.endpoints_method( 363 @auth.endpoints_method(
351 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata) 364 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata)
352 @auth.require(acl.is_bot_or_user) 365 @auth.require(acl.can_create_task)
353 def new(self, request): 366 def new(self, request):
354 """Creates a new task. 367 """Creates a new task.
355 368
356 The task will be enqueued in the tasks list and will be executed at the 369 The task will be enqueued in the tasks list and will be executed at the
357 earliest opportunity by a bot that has at least the dimensions as described 370 earliest opportunity by a bot that has at least the dimensions as described
358 in the task request. 371 in the task request.
359 """ 372 """
360 sb = (request.properties.secret_bytes 373 sb = (request.properties.secret_bytes
361 if request.properties is not None else None) 374 if request.properties is not None else None)
362 if sb is not None: 375 if sb is not None:
(...skipping 20 matching lines...) Expand all
383 396
384 return swarming_rpcs.TaskRequestMetadata( 397 return swarming_rpcs.TaskRequestMetadata(
385 request=message_conversion.task_request_to_rpc(request), 398 request=message_conversion.task_request_to_rpc(request),
386 task_id=task_pack.pack_result_summary_key(result_summary.key), 399 task_id=task_pack.pack_result_summary_key(result_summary.key),
387 task_result=previous_result) 400 task_result=previous_result)
388 401
389 @gae_ts_mon.instrument_endpoint() 402 @gae_ts_mon.instrument_endpoint()
390 @auth.endpoints_method( 403 @auth.endpoints_method(
391 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList, 404 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList,
392 http_method='GET') 405 http_method='GET')
393 @auth.require(acl.is_privileged_user) 406 @auth.require(acl.can_view_all_tasks)
394 def list(self, request): 407 def list(self, request):
395 """Returns tasks results based on the filters. 408 """Returns tasks results based on the filters.
396 409
397 This endpoint is significantly slower than 'count'. Use 'count' when 410 This endpoint is significantly slower than 'count'. Use 'count' when
398 possible. 411 possible.
399 """ 412 """
400 # TODO(maruel): Rename 'list' to 'results'. 413 # TODO(maruel): Rename 'list' to 'results'.
401 # TODO(maruel): Rename 'TaskList' to 'TaskResults'. 414 # TODO(maruel): Rename 'TaskList' to 'TaskResults'.
402 logging.debug('%s', request) 415 logging.debug('%s', request)
403 now = utils.utcnow() 416 now = utils.utcnow()
(...skipping 17 matching lines...) Expand all
421 message_conversion.task_result_to_rpc( 434 message_conversion.task_result_to_rpc(
422 i, request.include_performance_stats) 435 i, request.include_performance_stats)
423 for i in items 436 for i in items
424 ], 437 ],
425 now=now) 438 now=now)
426 439
427 @gae_ts_mon.instrument_endpoint() 440 @gae_ts_mon.instrument_endpoint()
428 @auth.endpoints_method( 441 @auth.endpoints_method(
429 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests, 442 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests,
430 http_method='GET') 443 http_method='GET')
431 @auth.require(acl.is_privileged_user) 444 @auth.require(acl.can_view_all_tasks)
432 def requests(self, request): 445 def requests(self, request):
433 """Returns tasks requests based on the filters. 446 """Returns tasks requests based on the filters.
434 447
435 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when 448 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when
436 possible. 449 possible.
437 """ 450 """
438 logging.debug('%s', request) 451 logging.debug('%s', request)
439 if request.include_performance_stats: 452 if request.include_performance_stats:
440 raise endpoints.BadRequestException( 453 raise endpoints.BadRequestException(
441 'Can\'t set include_performance_stats for tasks/list') 454 'Can\'t set include_performance_stats for tasks/list')
(...skipping 19 matching lines...) Expand all
461 'This combination is unsupported, sorry.') 474 'This combination is unsupported, sorry.')
462 return swarming_rpcs.TaskRequests( 475 return swarming_rpcs.TaskRequests(
463 cursor=cursor, 476 cursor=cursor,
464 items=[message_conversion.task_request_to_rpc(i) for i in items], 477 items=[message_conversion.task_request_to_rpc(i) for i in items],
465 now=now) 478 now=now)
466 479
467 @gae_ts_mon.instrument_endpoint() 480 @gae_ts_mon.instrument_endpoint()
468 @auth.endpoints_method( 481 @auth.endpoints_method(
469 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse, 482 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse,
470 http_method='POST') 483 http_method='POST')
471 @auth.require(acl.is_admin) 484 @auth.require(acl.can_edit_all_tasks)
472 def cancel(self, request): 485 def cancel(self, request):
473 """Cancel a subset of pending tasks based on the tags. 486 """Cancel a subset of pending tasks based on the tags.
474 487
475 Cancellation happens asynchronously, so when this call returns, 488 Cancellation happens asynchronously, so when this call returns,
476 cancellations will not have completed yet. 489 cancellations will not have completed yet.
477 """ 490 """
478 logging.debug('%s', request) 491 logging.debug('%s', request)
479 if not request.tags: 492 if not request.tags:
480 # Prevent accidental cancellation of everything. 493 # Prevent accidental cancellation of everything.
481 raise endpoints.BadRequestException( 494 raise endpoints.BadRequestException(
(...skipping 20 matching lines...) Expand all
502 515
503 return swarming_rpcs.TasksCancelResponse( 516 return swarming_rpcs.TasksCancelResponse(
504 cursor=cursor, 517 cursor=cursor,
505 matched=len(tasks), 518 matched=len(tasks),
506 now=now) 519 now=now)
507 520
508 @gae_ts_mon.instrument_endpoint() 521 @gae_ts_mon.instrument_endpoint()
509 @auth.endpoints_method( 522 @auth.endpoints_method(
510 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount, 523 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount,
511 http_method='GET') 524 http_method='GET')
512 @auth.require(acl.is_privileged_user) 525 @auth.require(acl.can_view_all_tasks)
513 def count(self, request): 526 def count(self, request):
514 """Counts number of tasks in a given state.""" 527 """Counts number of tasks in a given state."""
515 logging.debug('%s', request) 528 logging.debug('%s', request)
516 if not request.start: 529 if not request.start:
517 raise endpoints.BadRequestException('start (as epoch) is required') 530 raise endpoints.BadRequestException('start (as epoch) is required')
518 now = utils.utcnow() 531 now = utils.utcnow()
519 mem_key = self._memcache_key(request, now) 532 mem_key = self._memcache_key(request, now)
520 count = memcache.get(mem_key, namespace='tasks_count') 533 count = memcache.get(mem_key, namespace='tasks_count')
521 if count is not None: 534 if count is not None:
522 return swarming_rpcs.TasksCount(count=count, now=now) 535 return swarming_rpcs.TasksCount(count=count, now=now)
(...skipping 19 matching lines...) Expand all
542 return task_result.get_result_summaries_query( 555 return task_result.get_result_summaries_query(
543 start, end, 556 start, end,
544 sort or request.sort.name.lower(), 557 sort or request.sort.name.lower(),
545 request.state.name.lower(), 558 request.state.name.lower(),
546 request.tags) 559 request.tags)
547 560
548 @gae_ts_mon.instrument_endpoint() 561 @gae_ts_mon.instrument_endpoint()
549 @auth.endpoints_method( 562 @auth.endpoints_method(
550 message_types.VoidMessage, swarming_rpcs.TasksTags, 563 message_types.VoidMessage, swarming_rpcs.TasksTags,
551 http_method='GET') 564 http_method='GET')
552 @auth.require(acl.is_privileged_user) 565 @auth.require(acl.can_view_all_tasks)
553 def tags(self, _request): 566 def tags(self, _request):
554 """Returns the cached set of tags currently seen in the fleet.""" 567 """Returns the cached set of tags currently seen in the fleet."""
555 tags = task_result.TagAggregation.KEY.get() 568 tags = task_result.TagAggregation.KEY.get()
556 ft = [ 569 ft = [
557 swarming_rpcs.StringListPair(key=t.tag, value=t.values) 570 swarming_rpcs.StringListPair(key=t.tag, value=t.values)
558 for t in tags.tags 571 for t in tags.tags
559 ] 572 ]
560 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts) 573 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts)
561 574
562 575
(...skipping 14 matching lines...) Expand all
577 590
578 @swarming_api.api_class(resource_name='bot', path='bot') 591 @swarming_api.api_class(resource_name='bot', path='bot')
579 class SwarmingBotService(remote.Service): 592 class SwarmingBotService(remote.Service):
580 """Bot-related API. Permits querying information about the bot's properties""" 593 """Bot-related API. Permits querying information about the bot's properties"""
581 @gae_ts_mon.instrument_endpoint() 594 @gae_ts_mon.instrument_endpoint()
582 @auth.endpoints_method( 595 @auth.endpoints_method(
583 BotId, swarming_rpcs.BotInfo, 596 BotId, swarming_rpcs.BotInfo,
584 name='get', 597 name='get',
585 path='{bot_id}/get', 598 path='{bot_id}/get',
586 http_method='GET') 599 http_method='GET')
587 @auth.require(acl.is_privileged_user) 600 @auth.require(acl.can_view_bot)
588 def get(self, request): 601 def get(self, request):
589 """Returns information about a known bot. 602 """Returns information about a known bot.
590 603
591 This includes its state and dimensions, and if it is currently running a 604 This includes its state and dimensions, and if it is currently running a
592 task. 605 task.
593 """ 606 """
594 logging.debug('%s', request) 607 logging.debug('%s', request)
595 bot_id = request.bot_id 608 bot_id = request.bot_id
596 bot = bot_management.get_info_key(bot_id).get() 609 bot = bot_management.get_info_key(bot_id).get()
597 deleted = False 610 deleted = False
(...skipping 22 matching lines...) Expand all
620 deleted = True 633 deleted = True
621 634
622 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(), 635 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(),
623 deleted=deleted) 636 deleted=deleted)
624 637
625 @gae_ts_mon.instrument_endpoint() 638 @gae_ts_mon.instrument_endpoint()
626 @auth.endpoints_method( 639 @auth.endpoints_method(
627 BotId, swarming_rpcs.DeletedResponse, 640 BotId, swarming_rpcs.DeletedResponse,
628 name='delete', 641 name='delete',
629 path='{bot_id}/delete') 642 path='{bot_id}/delete')
630 @auth.require(acl.is_admin) 643 @auth.require(acl.can_edit_bot)
631 def delete(self, request): 644 def delete(self, request):
632 """Deletes the bot corresponding to a provided bot_id. 645 """Deletes the bot corresponding to a provided bot_id.
633 646
634 At that point, the bot will not appears in the list of bots but it is still 647 At that point, the bot will not appears in the list of bots but it is still
635 possible to get information about the bot with its bot id is known, as 648 possible to get information about the bot with its bot id is known, as
636 historical data is not deleted. 649 historical data is not deleted.
637 650
638 It is meant to remove from the DB the presence of a bot that was retired, 651 It is meant to remove from the DB the presence of a bot that was retired,
639 e.g. the VM was shut down already. Use 'terminate' instead of the bot is 652 e.g. the VM was shut down already. Use 'terminate' instead of the bot is
640 still alive. 653 still alive.
641 """ 654 """
642 logging.debug('%s', request) 655 logging.debug('%s', request)
643 bot_key = bot_management.get_info_key(request.bot_id) 656 bot_key = bot_management.get_info_key(request.bot_id)
644 get_or_raise(bot_key) # raises 404 if there is no such bot 657 get_or_raise(bot_key) # raises 404 if there is no such bot
645 # TODO(maruel): If the bot was a MP, call lease_management.cleanup_bot()? 658 # TODO(maruel): If the bot was a MP, call lease_management.cleanup_bot()?
646 task_queues.cleanup_after_bot(request.bot_id) 659 task_queues.cleanup_after_bot(request.bot_id)
647 bot_key.delete() 660 bot_key.delete()
648 return swarming_rpcs.DeletedResponse(deleted=True) 661 return swarming_rpcs.DeletedResponse(deleted=True)
649 662
650 @gae_ts_mon.instrument_endpoint() 663 @gae_ts_mon.instrument_endpoint()
651 @auth.endpoints_method( 664 @auth.endpoints_method(
652 BotEventsRequest, swarming_rpcs.BotEvents, 665 BotEventsRequest, swarming_rpcs.BotEvents,
653 name='events', 666 name='events',
654 path='{bot_id}/events', 667 path='{bot_id}/events',
655 http_method='GET') 668 http_method='GET')
656 @auth.require(acl.is_privileged_user) 669 @auth.require(acl.can_view_bot)
657 def events(self, request): 670 def events(self, request):
658 """Returns events that happened on a bot.""" 671 """Returns events that happened on a bot."""
659 logging.debug('%s', request) 672 logging.debug('%s', request)
660 try: 673 try:
661 now = utils.utcnow() 674 now = utils.utcnow()
662 start = message_conversion.epoch_to_datetime(request.start) 675 start = message_conversion.epoch_to_datetime(request.start)
663 end = message_conversion.epoch_to_datetime(request.end) 676 end = message_conversion.epoch_to_datetime(request.end)
664 order = not (start or end) 677 order = not (start or end)
665 query = bot_management.get_events_query(request.bot_id, order) 678 query = bot_management.get_events_query(request.bot_id, order)
666 if not order: 679 if not order:
(...skipping 11 matching lines...) Expand all
678 return swarming_rpcs.BotEvents( 691 return swarming_rpcs.BotEvents(
679 cursor=cursor, 692 cursor=cursor,
680 items=[message_conversion.bot_event_to_rpc(r) for r in items], 693 items=[message_conversion.bot_event_to_rpc(r) for r in items],
681 now=now) 694 now=now)
682 695
683 @gae_ts_mon.instrument_endpoint() 696 @gae_ts_mon.instrument_endpoint()
684 @auth.endpoints_method( 697 @auth.endpoints_method(
685 BotId, swarming_rpcs.TerminateResponse, 698 BotId, swarming_rpcs.TerminateResponse,
686 name='terminate', 699 name='terminate',
687 path='{bot_id}/terminate') 700 path='{bot_id}/terminate')
688 @auth.require(acl.is_bot_or_privileged_user) 701 @auth.require(acl.can_edit_bot)
689 def terminate(self, request): 702 def terminate(self, request):
690 """Asks a bot to terminate itself gracefully. 703 """Asks a bot to terminate itself gracefully.
691 704
692 The bot will stay in the DB, use 'delete' to remove it from the DB 705 The bot will stay in the DB, use 'delete' to remove it from the DB
693 afterward. This request returns a pseudo-taskid that can be waited for to 706 afterward. This request returns a pseudo-taskid that can be waited for to
694 wait for the bot to turn down. 707 wait for the bot to turn down.
695 708
696 This command is particularly useful when a privileged user needs to safely 709 This command is particularly useful when a privileged user needs to safely
697 debug a machine specific issue. The user can trigger a terminate for one of 710 debug a machine specific issue. The user can trigger a terminate for one of
698 the bot exhibiting the issue, wait for the pseudo-task to run then access 711 the bot exhibiting the issue, wait for the pseudo-task to run then access
(...skipping 16 matching lines...) Expand all
715 result_summary = task_scheduler.schedule_request(request, None) 728 result_summary = task_scheduler.schedule_request(request, None)
716 return swarming_rpcs.TerminateResponse( 729 return swarming_rpcs.TerminateResponse(
717 task_id=task_pack.pack_result_summary_key(result_summary.key)) 730 task_id=task_pack.pack_result_summary_key(result_summary.key))
718 731
719 @gae_ts_mon.instrument_endpoint() 732 @gae_ts_mon.instrument_endpoint()
720 @auth.endpoints_method( 733 @auth.endpoints_method(
721 BotTasksRequest, swarming_rpcs.BotTasks, 734 BotTasksRequest, swarming_rpcs.BotTasks,
722 name='tasks', 735 name='tasks',
723 path='{bot_id}/tasks', 736 path='{bot_id}/tasks',
724 http_method='GET') 737 http_method='GET')
725 @auth.require(acl.is_privileged_user) 738 @auth.require(acl.can_view_all_tasks)
726 def tasks(self, request): 739 def tasks(self, request):
727 """Lists a given bot's tasks within the specified date range. 740 """Lists a given bot's tasks within the specified date range.
728 741
729 In this case, the tasks are effectively TaskRunResult since it's individual 742 In this case, the tasks are effectively TaskRunResult since it's individual
730 task tries sent to this specific bot. 743 task tries sent to this specific bot.
731 744
732 It is impossible to search by both tags and bot id. If there's a need, 745 It is impossible to search by both tags and bot id. If there's a need,
733 TaskRunResult.tags will be added (via a copy from TaskRequest.tags). 746 TaskRunResult.tags will be added (via a copy from TaskRequest.tags).
734 """ 747 """
735 logging.debug('%s', request) 748 logging.debug('%s', request)
(...skipping 22 matching lines...) Expand all
758 771
759 772
760 @swarming_api.api_class(resource_name='bots', path='bots') 773 @swarming_api.api_class(resource_name='bots', path='bots')
761 class SwarmingBotsService(remote.Service): 774 class SwarmingBotsService(remote.Service):
762 """Bots-related API.""" 775 """Bots-related API."""
763 776
764 @gae_ts_mon.instrument_endpoint() 777 @gae_ts_mon.instrument_endpoint()
765 @auth.endpoints_method( 778 @auth.endpoints_method(
766 swarming_rpcs.BotsRequest, swarming_rpcs.BotList, 779 swarming_rpcs.BotsRequest, swarming_rpcs.BotList,
767 http_method='GET') 780 http_method='GET')
768 @auth.require(acl.is_privileged_user) 781 @auth.require(acl.can_view_bot)
769 def list(self, request): 782 def list(self, request):
770 """Provides list of known bots. 783 """Provides list of known bots.
771 784
772 Deleted bots will not be listed. 785 Deleted bots will not be listed.
773 """ 786 """
774 logging.debug('%s', request) 787 logging.debug('%s', request)
775 now = utils.utcnow() 788 now = utils.utcnow()
776 q = bot_management.BotInfo.query() 789 q = bot_management.BotInfo.query()
777 try: 790 try:
778 q = bot_management.filter_dimensions(q, request.dimensions) 791 q = bot_management.filter_dimensions(q, request.dimensions)
779 q = bot_management.filter_availability( 792 q = bot_management.filter_availability(
780 q, swarming_rpcs.to_bool(request.quarantined), 793 q, swarming_rpcs.to_bool(request.quarantined),
781 swarming_rpcs.to_bool(request.is_dead), now, 794 swarming_rpcs.to_bool(request.is_dead), now,
782 swarming_rpcs.to_bool(request.is_busy), 795 swarming_rpcs.to_bool(request.is_busy),
783 swarming_rpcs.to_bool(request.is_mp)) 796 swarming_rpcs.to_bool(request.is_mp))
784 except ValueError as e: 797 except ValueError as e:
785 raise endpoints.BadRequestException(str(e)) 798 raise endpoints.BadRequestException(str(e))
786 799
787 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor) 800 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor)
788 return swarming_rpcs.BotList( 801 return swarming_rpcs.BotList(
789 cursor=cursor, 802 cursor=cursor,
790 death_timeout=config.settings().bot_death_timeout_secs, 803 death_timeout=config.settings().bot_death_timeout_secs,
791 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots], 804 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots],
792 now=now) 805 now=now)
793 806
794 @gae_ts_mon.instrument_endpoint() 807 @gae_ts_mon.instrument_endpoint()
795 @auth.endpoints_method( 808 @auth.endpoints_method(
796 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount, 809 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount,
797 http_method='GET') 810 http_method='GET')
798 @auth.require(acl.is_privileged_user) 811 @auth.require(acl.can_view_bot)
799 def count(self, request): 812 def count(self, request):
800 """Counts number of bots with given dimensions.""" 813 """Counts number of bots with given dimensions."""
801 logging.debug('%s', request) 814 logging.debug('%s', request)
802 now = utils.utcnow() 815 now = utils.utcnow()
803 q = bot_management.BotInfo.query() 816 q = bot_management.BotInfo.query()
804 try: 817 try:
805 q = bot_management.filter_dimensions(q, request.dimensions) 818 q = bot_management.filter_dimensions(q, request.dimensions)
806 except ValueError as e: 819 except ValueError as e:
807 raise endpoints.BadRequestException(str(e)) 820 raise endpoints.BadRequestException(str(e))
808 821
809 f_count = q.count_async() 822 f_count = q.count_async()
810 f_dead = (bot_management.filter_availability(q, None, True, now, None, None) 823 f_dead = (bot_management.filter_availability(q, None, True, now, None, None)
811 .count_async()) 824 .count_async())
812 f_quarantined = ( 825 f_quarantined = (
813 bot_management.filter_availability(q, True, None, now, None, None) 826 bot_management.filter_availability(q, True, None, now, None, None)
814 .count_async()) 827 .count_async())
815 f_busy = (bot_management.filter_availability(q, None, None, now, True, None) 828 f_busy = (bot_management.filter_availability(q, None, None, now, True, None)
816 .count_async()) 829 .count_async())
817 return swarming_rpcs.BotsCount( 830 return swarming_rpcs.BotsCount(
818 count=f_count.get_result(), 831 count=f_count.get_result(),
819 quarantined=f_quarantined.get_result(), 832 quarantined=f_quarantined.get_result(),
820 dead=f_dead.get_result(), 833 dead=f_dead.get_result(),
821 busy=f_busy.get_result(), 834 busy=f_busy.get_result(),
822 now=now) 835 now=now)
823 836
824
825 @gae_ts_mon.instrument_endpoint() 837 @gae_ts_mon.instrument_endpoint()
826 @auth.endpoints_method( 838 @auth.endpoints_method(
827 message_types.VoidMessage, swarming_rpcs.BotsDimensions, 839 message_types.VoidMessage, swarming_rpcs.BotsDimensions,
828 http_method='GET') 840 http_method='GET')
829 @auth.require(acl.is_privileged_user) 841 @auth.require(acl.can_view_bot)
830 def dimensions(self, _request): 842 def dimensions(self, _request):
831 """Returns the cached set of dimensions currently in use in the fleet.""" 843 """Returns the cached set of dimensions currently in use in the fleet."""
832 dims = bot_management.DimensionAggregation.KEY.get() 844 dims = bot_management.DimensionAggregation.KEY.get()
833 fd = [ 845 fd = [
834 swarming_rpcs.StringListPair(key=d.dimension, value=d.values) 846 swarming_rpcs.StringListPair(key=d.dimension, value=d.values)
835 for d in dims.dimensions 847 for d in dims.dimensions
836 ] 848 ]
837 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts) 849 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts)
838 850
839 851
840 def get_routes(): 852 def get_routes():
841 return ( 853 return (
842 endpoints_webapp2.api_routes(SwarmingServerService) + 854 endpoints_webapp2.api_routes(SwarmingServerService) +
843 endpoints_webapp2.api_routes(SwarmingTaskService) + 855 endpoints_webapp2.api_routes(SwarmingTaskService) +
844 endpoints_webapp2.api_routes(SwarmingTasksService) + 856 endpoints_webapp2.api_routes(SwarmingTasksService) +
845 endpoints_webapp2.api_routes(SwarmingBotService) + 857 endpoints_webapp2.api_routes(SwarmingBotService) +
846 endpoints_webapp2.api_routes(SwarmingBotsService) + 858 endpoints_webapp2.api_routes(SwarmingBotsService) +
847 # components.config endpoints for validation and configuring of luci-config 859 # components.config endpoints for validation and configuring of luci-config
848 # service URL. 860 # service URL.
849 endpoints_webapp2.api_routes(config.ConfigApi)) 861 endpoints_webapp2.api_routes(config.ConfigApi))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698