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

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

Issue 2984843002: swarming: switch to a 'capability focused' ACL system (Closed)
Patch Set: Tuned permissions, added tests 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
« no previous file with comments | « appengine/swarming/handlers_bot.py ('k') | appengine/swarming/handlers_endpoints_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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:
66 raise endpoints.NotFoundException('%s not found.' % task_id)
67 if not acl.is_bot() and not request.has_access:
68 raise endpoints.ForbiddenException('%s is not accessible.' % task_id)
69 return request, result
70 except ValueError: 71 except ValueError:
71 raise endpoints.BadRequestException('%s is an invalid key.' % task_id) 72 raise endpoints.BadRequestException('%s is an invalid key.' % task_id)
73 if not request or not result:
74 raise endpoints.NotFoundException('%s not found.' % task_id)
75 if viewing == _VIEW:
76 if not acl.can_view_task(request):
77 raise endpoints.ForbiddenException('%s is not accessible.' % task_id)
78 elif viewing == _EDIT:
79 if not acl.can_edit_task(request):
80 raise endpoints.ForbiddenException('%s is not accessible.' % task_id)
81 else:
82 raise endpoints.InternalServerErrorException('get_request_and_result()')
83 return request, result
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())
79 return result 91 return result
80 92
81 93
(...skipping 39 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."""
173 return swarming_rpcs.ClientPermissions( 185 return swarming_rpcs.ClientPermissions(
174 delete_bot = acl.is_admin(), 186 delete_bot=acl.can_delete_bot(),
175 terminate_bot = acl.is_privileged_user(), 187 terminate_bot=acl.can_edit_bot(),
176 get_configs = acl.is_user(), 188 get_configs=acl.can_view_config(),
177 put_configs = acl.is_admin(), 189 put_configs=acl.can_edit_config(),
178 cancel_task = acl.is_user(), 190 cancel_task=acl._is_user() or acl.is_ip_whitelisted_machine(),
179 cancel_tasks = acl.is_admin(), 191 cancel_tasks=acl.can_edit_all_tasks(),
180 get_bootstrap_token = acl.is_bootstrapper(), 192 get_bootstrap_token=acl.can_create_bot())
181 )
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_view_config)
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_view_config)
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_edit_config)
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_edit_config)
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_access)
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 _, result = get_request_and_result(request.task_id, _VIEW)
293 return message_conversion.task_result_to_rpc( 304 return message_conversion.task_result_to_rpc(
294 result, request.include_performance_stats) 305 result, request.include_performance_stats)
295 306
296 @gae_ts_mon.instrument_endpoint() 307 @gae_ts_mon.instrument_endpoint()
297 @auth.endpoints_method( 308 @auth.endpoints_method(
298 TaskId, swarming_rpcs.TaskRequest, 309 TaskId, swarming_rpcs.TaskRequest,
299 name='request', 310 name='request',
300 path='{task_id}/request', 311 path='{task_id}/request',
301 http_method='GET') 312 http_method='GET')
302 @auth.require(acl.is_bot_or_user) 313 @auth.require(acl.can_access)
303 def request(self, request): 314 def request(self, request):
304 """Returns the task request corresponding to a task ID.""" 315 """Returns the task request corresponding to a task ID."""
305 logging.debug('%s', request) 316 logging.debug('%s', request)
306 request_obj, _ = get_request_and_result(request.task_id) 317 request_obj, _ = get_request_and_result(request.task_id, _VIEW)
307 return message_conversion.task_request_to_rpc(request_obj) 318 return message_conversion.task_request_to_rpc(request_obj)
308 319
309 @gae_ts_mon.instrument_endpoint() 320 @gae_ts_mon.instrument_endpoint()
310 @auth.endpoints_method( 321 @auth.endpoints_method(
311 TaskId, swarming_rpcs.CancelResponse, 322 TaskId, swarming_rpcs.CancelResponse,
312 name='cancel', 323 name='cancel',
313 path='{task_id}/cancel') 324 path='{task_id}/cancel')
314 @auth.require(acl.is_bot_or_user) 325 @auth.require(acl.can_access)
315 def cancel(self, request): 326 def cancel(self, request):
316 """Cancels a task. 327 """Cancels a task.
317 328
318 If a bot was running the task, the bot will forcibly cancel the task. 329 If a bot was running the task, the bot will forcibly cancel the task.
319 """ 330 """
320 logging.debug('%s', request) 331 logging.debug('%s', request)
321 request_obj, result = get_request_and_result(request.task_id) 332 request_obj, result = get_request_and_result(request.task_id, _EDIT)
322 ok, was_running = task_scheduler.cancel_task(request_obj, result.key) 333 ok, was_running = task_scheduler.cancel_task(request_obj, result.key)
323 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running) 334 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running)
324 335
325 @gae_ts_mon.instrument_endpoint() 336 @gae_ts_mon.instrument_endpoint()
326 @auth.endpoints_method( 337 @auth.endpoints_method(
327 TaskId, swarming_rpcs.TaskOutput, 338 TaskId, swarming_rpcs.TaskOutput,
328 name='stdout', 339 name='stdout',
329 path='{task_id}/stdout', 340 path='{task_id}/stdout',
330 http_method='GET') 341 http_method='GET')
331 @auth.require(acl.is_bot_or_user) 342 @auth.require(acl.can_access)
332 def stdout(self, request): 343 def stdout(self, request):
333 """Returns the output of the task corresponding to a task ID.""" 344 """Returns the output of the task corresponding to a task ID."""
334 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine 345 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine
335 # v1. 346 # v1.
336 # TODO(maruel): Send as raw content instead of encoded. This is not 347 # TODO(maruel): Send as raw content instead of encoded. This is not
337 # supported by cloud endpoints. 348 # supported by cloud endpoints.
338 logging.debug('%s', request) 349 logging.debug('%s', request)
339 _, result = get_request_and_result(request.task_id) 350 _, result = get_request_and_result(request.task_id, _VIEW)
340 output = result.get_output() 351 output = result.get_output()
341 if output: 352 if output:
342 output = output.decode('utf-8', 'replace') 353 output = output.decode('utf-8', 'replace')
343 return swarming_rpcs.TaskOutput(output=output) 354 return swarming_rpcs.TaskOutput(output=output)
344 355
345 356
346 @swarming_api.api_class(resource_name='tasks', path='tasks') 357 @swarming_api.api_class(resource_name='tasks', path='tasks')
347 class SwarmingTasksService(remote.Service): 358 class SwarmingTasksService(remote.Service):
348 """Swarming's tasks-related API.""" 359 """Swarming's tasks-related API."""
349 @gae_ts_mon.instrument_endpoint() 360 @gae_ts_mon.instrument_endpoint()
350 @auth.endpoints_method( 361 @auth.endpoints_method(
351 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata) 362 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata)
352 @auth.require(acl.is_bot_or_user) 363 @auth.require(acl.can_create_task)
353 def new(self, request): 364 def new(self, request):
354 """Creates a new task. 365 """Creates a new task.
355 366
356 The task will be enqueued in the tasks list and will be executed at the 367 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 368 earliest opportunity by a bot that has at least the dimensions as described
358 in the task request. 369 in the task request.
359 """ 370 """
360 sb = (request.properties.secret_bytes 371 sb = (request.properties.secret_bytes
361 if request.properties is not None else None) 372 if request.properties is not None else None)
362 if sb is not None: 373 if sb is not None:
(...skipping 20 matching lines...) Expand all
383 394
384 return swarming_rpcs.TaskRequestMetadata( 395 return swarming_rpcs.TaskRequestMetadata(
385 request=message_conversion.task_request_to_rpc(request), 396 request=message_conversion.task_request_to_rpc(request),
386 task_id=task_pack.pack_result_summary_key(result_summary.key), 397 task_id=task_pack.pack_result_summary_key(result_summary.key),
387 task_result=previous_result) 398 task_result=previous_result)
388 399
389 @gae_ts_mon.instrument_endpoint() 400 @gae_ts_mon.instrument_endpoint()
390 @auth.endpoints_method( 401 @auth.endpoints_method(
391 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList, 402 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList,
392 http_method='GET') 403 http_method='GET')
393 @auth.require(acl.is_privileged_user) 404 @auth.require(acl.can_view_all_tasks)
394 def list(self, request): 405 def list(self, request):
395 """Returns tasks results based on the filters. 406 """Returns tasks results based on the filters.
396 407
397 This endpoint is significantly slower than 'count'. Use 'count' when 408 This endpoint is significantly slower than 'count'. Use 'count' when
398 possible. 409 possible.
399 """ 410 """
400 # TODO(maruel): Rename 'list' to 'results'. 411 # TODO(maruel): Rename 'list' to 'results'.
401 # TODO(maruel): Rename 'TaskList' to 'TaskResults'. 412 # TODO(maruel): Rename 'TaskList' to 'TaskResults'.
402 logging.debug('%s', request) 413 logging.debug('%s', request)
403 now = utils.utcnow() 414 now = utils.utcnow()
(...skipping 17 matching lines...) Expand all
421 message_conversion.task_result_to_rpc( 432 message_conversion.task_result_to_rpc(
422 i, request.include_performance_stats) 433 i, request.include_performance_stats)
423 for i in items 434 for i in items
424 ], 435 ],
425 now=now) 436 now=now)
426 437
427 @gae_ts_mon.instrument_endpoint() 438 @gae_ts_mon.instrument_endpoint()
428 @auth.endpoints_method( 439 @auth.endpoints_method(
429 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests, 440 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests,
430 http_method='GET') 441 http_method='GET')
431 @auth.require(acl.is_privileged_user) 442 @auth.require(acl.can_view_all_tasks)
432 def requests(self, request): 443 def requests(self, request):
433 """Returns tasks requests based on the filters. 444 """Returns tasks requests based on the filters.
434 445
435 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when 446 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when
436 possible. 447 possible.
437 """ 448 """
438 logging.debug('%s', request) 449 logging.debug('%s', request)
439 if request.include_performance_stats: 450 if request.include_performance_stats:
440 raise endpoints.BadRequestException( 451 raise endpoints.BadRequestException(
441 'Can\'t set include_performance_stats for tasks/list') 452 'Can\'t set include_performance_stats for tasks/list')
(...skipping 19 matching lines...) Expand all
461 'This combination is unsupported, sorry.') 472 'This combination is unsupported, sorry.')
462 return swarming_rpcs.TaskRequests( 473 return swarming_rpcs.TaskRequests(
463 cursor=cursor, 474 cursor=cursor,
464 items=[message_conversion.task_request_to_rpc(i) for i in items], 475 items=[message_conversion.task_request_to_rpc(i) for i in items],
465 now=now) 476 now=now)
466 477
467 @gae_ts_mon.instrument_endpoint() 478 @gae_ts_mon.instrument_endpoint()
468 @auth.endpoints_method( 479 @auth.endpoints_method(
469 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse, 480 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse,
470 http_method='POST') 481 http_method='POST')
471 @auth.require(acl.is_admin) 482 @auth.require(acl.can_edit_all_tasks)
472 def cancel(self, request): 483 def cancel(self, request):
473 """Cancel a subset of pending tasks based on the tags. 484 """Cancel a subset of pending tasks based on the tags.
474 485
475 Cancellation happens asynchronously, so when this call returns, 486 Cancellation happens asynchronously, so when this call returns,
476 cancellations will not have completed yet. 487 cancellations will not have completed yet.
477 """ 488 """
478 logging.debug('%s', request) 489 logging.debug('%s', request)
479 if not request.tags: 490 if not request.tags:
480 # Prevent accidental cancellation of everything. 491 # Prevent accidental cancellation of everything.
481 raise endpoints.BadRequestException( 492 raise endpoints.BadRequestException(
(...skipping 20 matching lines...) Expand all
502 513
503 return swarming_rpcs.TasksCancelResponse( 514 return swarming_rpcs.TasksCancelResponse(
504 cursor=cursor, 515 cursor=cursor,
505 matched=len(tasks), 516 matched=len(tasks),
506 now=now) 517 now=now)
507 518
508 @gae_ts_mon.instrument_endpoint() 519 @gae_ts_mon.instrument_endpoint()
509 @auth.endpoints_method( 520 @auth.endpoints_method(
510 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount, 521 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount,
511 http_method='GET') 522 http_method='GET')
512 @auth.require(acl.is_privileged_user) 523 @auth.require(acl.can_view_all_tasks)
513 def count(self, request): 524 def count(self, request):
514 """Counts number of tasks in a given state.""" 525 """Counts number of tasks in a given state."""
515 logging.debug('%s', request) 526 logging.debug('%s', request)
516 if not request.start: 527 if not request.start:
517 raise endpoints.BadRequestException('start (as epoch) is required') 528 raise endpoints.BadRequestException('start (as epoch) is required')
518 now = utils.utcnow() 529 now = utils.utcnow()
519 mem_key = self._memcache_key(request, now) 530 mem_key = self._memcache_key(request, now)
520 count = memcache.get(mem_key, namespace='tasks_count') 531 count = memcache.get(mem_key, namespace='tasks_count')
521 if count is not None: 532 if count is not None:
522 return swarming_rpcs.TasksCount(count=count, now=now) 533 return swarming_rpcs.TasksCount(count=count, now=now)
(...skipping 19 matching lines...) Expand all
542 return task_result.get_result_summaries_query( 553 return task_result.get_result_summaries_query(
543 start, end, 554 start, end,
544 sort or request.sort.name.lower(), 555 sort or request.sort.name.lower(),
545 request.state.name.lower(), 556 request.state.name.lower(),
546 request.tags) 557 request.tags)
547 558
548 @gae_ts_mon.instrument_endpoint() 559 @gae_ts_mon.instrument_endpoint()
549 @auth.endpoints_method( 560 @auth.endpoints_method(
550 message_types.VoidMessage, swarming_rpcs.TasksTags, 561 message_types.VoidMessage, swarming_rpcs.TasksTags,
551 http_method='GET') 562 http_method='GET')
552 @auth.require(acl.is_privileged_user) 563 @auth.require(acl.can_view_all_tasks)
553 def tags(self, _request): 564 def tags(self, _request):
554 """Returns the cached set of tags currently seen in the fleet.""" 565 """Returns the cached set of tags currently seen in the fleet."""
555 tags = task_result.TagAggregation.KEY.get() 566 tags = task_result.TagAggregation.KEY.get()
556 ft = [ 567 ft = [
557 swarming_rpcs.StringListPair(key=t.tag, value=t.values) 568 swarming_rpcs.StringListPair(key=t.tag, value=t.values)
558 for t in tags.tags 569 for t in tags.tags
559 ] 570 ]
560 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts) 571 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts)
561 572
562 573
(...skipping 14 matching lines...) Expand all
577 588
578 @swarming_api.api_class(resource_name='bot', path='bot') 589 @swarming_api.api_class(resource_name='bot', path='bot')
579 class SwarmingBotService(remote.Service): 590 class SwarmingBotService(remote.Service):
580 """Bot-related API. Permits querying information about the bot's properties""" 591 """Bot-related API. Permits querying information about the bot's properties"""
581 @gae_ts_mon.instrument_endpoint() 592 @gae_ts_mon.instrument_endpoint()
582 @auth.endpoints_method( 593 @auth.endpoints_method(
583 BotId, swarming_rpcs.BotInfo, 594 BotId, swarming_rpcs.BotInfo,
584 name='get', 595 name='get',
585 path='{bot_id}/get', 596 path='{bot_id}/get',
586 http_method='GET') 597 http_method='GET')
587 @auth.require(acl.is_privileged_user) 598 @auth.require(acl.can_view_bot)
588 def get(self, request): 599 def get(self, request):
589 """Returns information about a known bot. 600 """Returns information about a known bot.
590 601
591 This includes its state and dimensions, and if it is currently running a 602 This includes its state and dimensions, and if it is currently running a
592 task. 603 task.
593 """ 604 """
594 logging.debug('%s', request) 605 logging.debug('%s', request)
595 bot_id = request.bot_id 606 bot_id = request.bot_id
596 bot = bot_management.get_info_key(bot_id).get() 607 bot = bot_management.get_info_key(bot_id).get()
597 deleted = False 608 deleted = False
(...skipping 22 matching lines...) Expand all
620 deleted = True 631 deleted = True
621 632
622 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(), 633 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(),
623 deleted=deleted) 634 deleted=deleted)
624 635
625 @gae_ts_mon.instrument_endpoint() 636 @gae_ts_mon.instrument_endpoint()
626 @auth.endpoints_method( 637 @auth.endpoints_method(
627 BotId, swarming_rpcs.DeletedResponse, 638 BotId, swarming_rpcs.DeletedResponse,
628 name='delete', 639 name='delete',
629 path='{bot_id}/delete') 640 path='{bot_id}/delete')
630 @auth.require(acl.is_admin) 641 @auth.require(acl.can_delete_bot)
631 def delete(self, request): 642 def delete(self, request):
632 """Deletes the bot corresponding to a provided bot_id. 643 """Deletes the bot corresponding to a provided bot_id.
633 644
634 At that point, the bot will not appears in the list of bots but it is still 645 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 646 possible to get information about the bot with its bot id is known, as
636 historical data is not deleted. 647 historical data is not deleted.
637 648
638 It is meant to remove from the DB the presence of a bot that was retired, 649 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 650 e.g. the VM was shut down already. Use 'terminate' instead of the bot is
640 still alive. 651 still alive.
641 """ 652 """
642 logging.debug('%s', request) 653 logging.debug('%s', request)
643 bot_key = bot_management.get_info_key(request.bot_id) 654 bot_key = bot_management.get_info_key(request.bot_id)
644 get_or_raise(bot_key) # raises 404 if there is no such bot 655 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()? 656 # TODO(maruel): If the bot was a MP, call lease_management.cleanup_bot()?
646 task_queues.cleanup_after_bot(request.bot_id) 657 task_queues.cleanup_after_bot(request.bot_id)
647 bot_key.delete() 658 bot_key.delete()
648 return swarming_rpcs.DeletedResponse(deleted=True) 659 return swarming_rpcs.DeletedResponse(deleted=True)
649 660
650 @gae_ts_mon.instrument_endpoint() 661 @gae_ts_mon.instrument_endpoint()
651 @auth.endpoints_method( 662 @auth.endpoints_method(
652 BotEventsRequest, swarming_rpcs.BotEvents, 663 BotEventsRequest, swarming_rpcs.BotEvents,
653 name='events', 664 name='events',
654 path='{bot_id}/events', 665 path='{bot_id}/events',
655 http_method='GET') 666 http_method='GET')
656 @auth.require(acl.is_privileged_user) 667 @auth.require(acl.can_view_bot)
657 def events(self, request): 668 def events(self, request):
658 """Returns events that happened on a bot.""" 669 """Returns events that happened on a bot."""
659 logging.debug('%s', request) 670 logging.debug('%s', request)
660 try: 671 try:
661 now = utils.utcnow() 672 now = utils.utcnow()
662 start = message_conversion.epoch_to_datetime(request.start) 673 start = message_conversion.epoch_to_datetime(request.start)
663 end = message_conversion.epoch_to_datetime(request.end) 674 end = message_conversion.epoch_to_datetime(request.end)
664 order = not (start or end) 675 order = not (start or end)
665 query = bot_management.get_events_query(request.bot_id, order) 676 query = bot_management.get_events_query(request.bot_id, order)
666 if not order: 677 if not order:
(...skipping 11 matching lines...) Expand all
678 return swarming_rpcs.BotEvents( 689 return swarming_rpcs.BotEvents(
679 cursor=cursor, 690 cursor=cursor,
680 items=[message_conversion.bot_event_to_rpc(r) for r in items], 691 items=[message_conversion.bot_event_to_rpc(r) for r in items],
681 now=now) 692 now=now)
682 693
683 @gae_ts_mon.instrument_endpoint() 694 @gae_ts_mon.instrument_endpoint()
684 @auth.endpoints_method( 695 @auth.endpoints_method(
685 BotId, swarming_rpcs.TerminateResponse, 696 BotId, swarming_rpcs.TerminateResponse,
686 name='terminate', 697 name='terminate',
687 path='{bot_id}/terminate') 698 path='{bot_id}/terminate')
688 @auth.require(acl.is_bot_or_privileged_user) 699 @auth.require(acl.can_edit_bot)
689 def terminate(self, request): 700 def terminate(self, request):
690 """Asks a bot to terminate itself gracefully. 701 """Asks a bot to terminate itself gracefully.
691 702
692 The bot will stay in the DB, use 'delete' to remove it from the DB 703 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 704 afterward. This request returns a pseudo-taskid that can be waited for to
694 wait for the bot to turn down. 705 wait for the bot to turn down.
695 706
696 This command is particularly useful when a privileged user needs to safely 707 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 708 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 709 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) 726 result_summary = task_scheduler.schedule_request(request, None)
716 return swarming_rpcs.TerminateResponse( 727 return swarming_rpcs.TerminateResponse(
717 task_id=task_pack.pack_result_summary_key(result_summary.key)) 728 task_id=task_pack.pack_result_summary_key(result_summary.key))
718 729
719 @gae_ts_mon.instrument_endpoint() 730 @gae_ts_mon.instrument_endpoint()
720 @auth.endpoints_method( 731 @auth.endpoints_method(
721 BotTasksRequest, swarming_rpcs.BotTasks, 732 BotTasksRequest, swarming_rpcs.BotTasks,
722 name='tasks', 733 name='tasks',
723 path='{bot_id}/tasks', 734 path='{bot_id}/tasks',
724 http_method='GET') 735 http_method='GET')
725 @auth.require(acl.is_privileged_user) 736 @auth.require(acl.can_view_all_tasks)
726 def tasks(self, request): 737 def tasks(self, request):
727 """Lists a given bot's tasks within the specified date range. 738 """Lists a given bot's tasks within the specified date range.
728 739
729 In this case, the tasks are effectively TaskRunResult since it's individual 740 In this case, the tasks are effectively TaskRunResult since it's individual
730 task tries sent to this specific bot. 741 task tries sent to this specific bot.
731 742
732 It is impossible to search by both tags and bot id. If there's a need, 743 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). 744 TaskRunResult.tags will be added (via a copy from TaskRequest.tags).
734 """ 745 """
735 logging.debug('%s', request) 746 logging.debug('%s', request)
(...skipping 22 matching lines...) Expand all
758 769
759 770
760 @swarming_api.api_class(resource_name='bots', path='bots') 771 @swarming_api.api_class(resource_name='bots', path='bots')
761 class SwarmingBotsService(remote.Service): 772 class SwarmingBotsService(remote.Service):
762 """Bots-related API.""" 773 """Bots-related API."""
763 774
764 @gae_ts_mon.instrument_endpoint() 775 @gae_ts_mon.instrument_endpoint()
765 @auth.endpoints_method( 776 @auth.endpoints_method(
766 swarming_rpcs.BotsRequest, swarming_rpcs.BotList, 777 swarming_rpcs.BotsRequest, swarming_rpcs.BotList,
767 http_method='GET') 778 http_method='GET')
768 @auth.require(acl.is_privileged_user) 779 @auth.require(acl.can_view_bot)
769 def list(self, request): 780 def list(self, request):
770 """Provides list of known bots. 781 """Provides list of known bots.
771 782
772 Deleted bots will not be listed. 783 Deleted bots will not be listed.
773 """ 784 """
774 logging.debug('%s', request) 785 logging.debug('%s', request)
775 now = utils.utcnow() 786 now = utils.utcnow()
776 q = bot_management.BotInfo.query() 787 q = bot_management.BotInfo.query()
777 try: 788 try:
778 q = bot_management.filter_dimensions(q, request.dimensions) 789 q = bot_management.filter_dimensions(q, request.dimensions)
779 q = bot_management.filter_availability( 790 q = bot_management.filter_availability(
780 q, swarming_rpcs.to_bool(request.quarantined), 791 q, swarming_rpcs.to_bool(request.quarantined),
781 swarming_rpcs.to_bool(request.is_dead), now, 792 swarming_rpcs.to_bool(request.is_dead), now,
782 swarming_rpcs.to_bool(request.is_busy), 793 swarming_rpcs.to_bool(request.is_busy),
783 swarming_rpcs.to_bool(request.is_mp)) 794 swarming_rpcs.to_bool(request.is_mp))
784 except ValueError as e: 795 except ValueError as e:
785 raise endpoints.BadRequestException(str(e)) 796 raise endpoints.BadRequestException(str(e))
786 797
787 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor) 798 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor)
788 return swarming_rpcs.BotList( 799 return swarming_rpcs.BotList(
789 cursor=cursor, 800 cursor=cursor,
790 death_timeout=config.settings().bot_death_timeout_secs, 801 death_timeout=config.settings().bot_death_timeout_secs,
791 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots], 802 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots],
792 now=now) 803 now=now)
793 804
794 @gae_ts_mon.instrument_endpoint() 805 @gae_ts_mon.instrument_endpoint()
795 @auth.endpoints_method( 806 @auth.endpoints_method(
796 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount, 807 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount,
797 http_method='GET') 808 http_method='GET')
798 @auth.require(acl.is_privileged_user) 809 @auth.require(acl.can_view_bot)
799 def count(self, request): 810 def count(self, request):
800 """Counts number of bots with given dimensions.""" 811 """Counts number of bots with given dimensions."""
801 logging.debug('%s', request) 812 logging.debug('%s', request)
802 now = utils.utcnow() 813 now = utils.utcnow()
803 q = bot_management.BotInfo.query() 814 q = bot_management.BotInfo.query()
804 try: 815 try:
805 q = bot_management.filter_dimensions(q, request.dimensions) 816 q = bot_management.filter_dimensions(q, request.dimensions)
806 except ValueError as e: 817 except ValueError as e:
807 raise endpoints.BadRequestException(str(e)) 818 raise endpoints.BadRequestException(str(e))
808 819
809 f_count = q.count_async() 820 f_count = q.count_async()
810 f_dead = (bot_management.filter_availability(q, None, True, now, None, None) 821 f_dead = (bot_management.filter_availability(q, None, True, now, None, None)
811 .count_async()) 822 .count_async())
812 f_quarantined = ( 823 f_quarantined = (
813 bot_management.filter_availability(q, True, None, now, None, None) 824 bot_management.filter_availability(q, True, None, now, None, None)
814 .count_async()) 825 .count_async())
815 f_busy = (bot_management.filter_availability(q, None, None, now, True, None) 826 f_busy = (bot_management.filter_availability(q, None, None, now, True, None)
816 .count_async()) 827 .count_async())
817 return swarming_rpcs.BotsCount( 828 return swarming_rpcs.BotsCount(
818 count=f_count.get_result(), 829 count=f_count.get_result(),
819 quarantined=f_quarantined.get_result(), 830 quarantined=f_quarantined.get_result(),
820 dead=f_dead.get_result(), 831 dead=f_dead.get_result(),
821 busy=f_busy.get_result(), 832 busy=f_busy.get_result(),
822 now=now) 833 now=now)
823 834
824
825 @gae_ts_mon.instrument_endpoint() 835 @gae_ts_mon.instrument_endpoint()
826 @auth.endpoints_method( 836 @auth.endpoints_method(
827 message_types.VoidMessage, swarming_rpcs.BotsDimensions, 837 message_types.VoidMessage, swarming_rpcs.BotsDimensions,
828 http_method='GET') 838 http_method='GET')
829 @auth.require(acl.is_privileged_user) 839 @auth.require(acl.can_view_bot)
830 def dimensions(self, _request): 840 def dimensions(self, _request):
831 """Returns the cached set of dimensions currently in use in the fleet.""" 841 """Returns the cached set of dimensions currently in use in the fleet."""
832 dims = bot_management.DimensionAggregation.KEY.get() 842 dims = bot_management.DimensionAggregation.KEY.get()
833 fd = [ 843 fd = [
834 swarming_rpcs.StringListPair(key=d.dimension, value=d.values) 844 swarming_rpcs.StringListPair(key=d.dimension, value=d.values)
835 for d in dims.dimensions 845 for d in dims.dimensions
836 ] 846 ]
837 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts) 847 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts)
838 848
839 849
840 def get_routes(): 850 def get_routes():
841 return ( 851 return (
842 endpoints_webapp2.api_routes(SwarmingServerService) + 852 endpoints_webapp2.api_routes(SwarmingServerService) +
843 endpoints_webapp2.api_routes(SwarmingTaskService) + 853 endpoints_webapp2.api_routes(SwarmingTaskService) +
844 endpoints_webapp2.api_routes(SwarmingTasksService) + 854 endpoints_webapp2.api_routes(SwarmingTasksService) +
845 endpoints_webapp2.api_routes(SwarmingBotService) + 855 endpoints_webapp2.api_routes(SwarmingBotService) +
846 endpoints_webapp2.api_routes(SwarmingBotsService) + 856 endpoints_webapp2.api_routes(SwarmingBotsService) +
847 # components.config endpoints for validation and configuring of luci-config 857 # components.config endpoints for validation and configuring of luci-config
848 # service URL. 858 # service URL.
849 endpoints_webapp2.api_routes(config.ConfigApi)) 859 endpoints_webapp2.api_routes(config.ConfigApi))
OLDNEW
« no previous file with comments | « appengine/swarming/handlers_bot.py ('k') | appengine/swarming/handlers_endpoints_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698