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

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

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

Powered by Google App Engine
This is Rietveld 408576698