Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2013 The LUCI Authors. All rights reserved. | 1 # Copyright 2013 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 """Main entry point for Swarming service. | 5 """Main entry point for Swarming service. |
| 6 | 6 |
| 7 This file contains the URL handlers for all the Swarming service URLs, | 7 This file contains the URL handlers for all the Swarming service URLs, |
| 8 implemented using the webapp2 framework. | 8 implemented using the webapp2 framework. |
| 9 """ | 9 """ |
| 10 | 10 |
| (...skipping 165 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 176 if success: | 176 if success: |
| 177 self.redirect('/restricted/mapreduce/status') | 177 self.redirect('/restricted/mapreduce/status') |
| 178 else: | 178 else: |
| 179 self.abort(500, 'Failed to launch the job') | 179 self.abort(500, 'Failed to launch the job') |
| 180 | 180 |
| 181 | 181 |
| 182 ### acl.is_privileged_user pages. | 182 ### acl.is_privileged_user pages. |
| 183 | 183 |
| 184 | 184 |
| 185 class BotsListHandler(auth.AuthenticatingHandler): | 185 class BotsListHandler(auth.AuthenticatingHandler): |
| 186 """Presents the list of known bots.""" | 186 """Redirects to a list of known bots.""" |
| 187 ACCEPTABLE_BOTS_SORTS = { | |
| 188 'last_seen_ts': 'Last Seen', | |
| 189 '-quarantined': 'Quarantined', | |
| 190 '__key__': 'ID', | |
| 191 } | |
| 192 SORT_OPTIONS = [ | |
| 193 SortOptions(k, v) for k, v in sorted(ACCEPTABLE_BOTS_SORTS.iteritems()) | |
| 194 ] | |
| 195 | 187 |
| 196 @auth.autologin | 188 @auth.public |
| 197 @auth.require(acl.is_privileged_user) | |
| 198 def get(self): | 189 def get(self): |
| 199 limit = int(self.request.get('limit', 100)) | 190 limit = int(self.request.get('limit', 100)) |
| 200 cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor')) | |
| 201 sort_by = self.request.get('sort_by', '__key__') | |
| 202 if sort_by not in self.ACCEPTABLE_BOTS_SORTS: | |
| 203 self.abort(400, 'Invalid sort_by query parameter') | |
| 204 | |
| 205 if sort_by[0] == '-': | |
| 206 order = datastore_query.PropertyOrder( | |
| 207 sort_by[1:], datastore_query.PropertyOrder.DESCENDING) | |
| 208 else: | |
| 209 order = datastore_query.PropertyOrder( | |
| 210 sort_by, datastore_query.PropertyOrder.ASCENDING) | |
| 211 | 191 |
| 212 dimensions = ( | 192 dimensions = ( |
| 213 l.strip() for l in self.request.get('dimensions', '').splitlines() | 193 l.strip() for l in self.request.get('dimensions', '').splitlines() |
| 214 ) | 194 ) |
| 215 dimensions = [i for i in dimensions if i] | 195 dimensions = [i for i in dimensions if i] |
| 216 | 196 |
| 217 now = utils.utcnow() | 197 new_ui_link = '/botlist?l=%d' % limit |
| 218 cutoff = now - datetime.timedelta( | 198 if dimensions: |
| 219 seconds=config.settings().bot_death_timeout_secs) | 199 new_ui_link += '&f=' + '&f='.join(dimensions) |
| 220 | 200 |
| 221 # TODO(maruel): Counting becomes an issue at the 10k range, at that point it | 201 self.redirect(new_ui_link) |
| 222 # should be prepopulated in an entity and updated via a cron job. | |
| 223 num_bots_busy_future = bot_management.BotInfo.query( | |
| 224 bot_management.BotInfo.is_busy == True).count_async() | |
| 225 num_bots_dead_future = bot_management.BotInfo.query( | |
| 226 bot_management.BotInfo.last_seen_ts < cutoff).count_async() | |
| 227 num_bots_quarantined_future = bot_management.BotInfo.query( | |
| 228 bot_management.BotInfo.quarantined == True).count_async() | |
| 229 num_bots_total_future = bot_management.BotInfo.query().count_async() | |
| 230 q = bot_management.BotInfo.query().order(order) | |
| 231 for d in dimensions: | |
| 232 q = q.filter(bot_management.BotInfo.dimensions_flat == d) | |
| 233 fetch_future = q.fetch_page_async(limit, start_cursor=cursor) | |
| 234 | |
| 235 # TODO(maruel): self.request.host_url should be the default AppEngine url | |
| 236 # version and not the current one. It is only an issue when | |
| 237 # version-dot-appid.appspot.com urls are used to access this page. | |
| 238 # TODO(aludwin): Display both gRPC and non-gRPC versions | |
| 239 version = bot_code.get_bot_version(self.request.host_url) | |
| 240 bots, cursor, more = fetch_future.get_result() | |
| 241 # Prefetch the tasks. We don't actually use the value here, it'll be | |
| 242 # implicitly used by ndb local's cache when refetched by the html template. | |
| 243 tasks = filter(None, (b.task for b in bots)) | |
| 244 ndb.get_multi(tasks) | |
| 245 num_bots_busy = num_bots_busy_future.get_result() | |
| 246 num_bots_dead = num_bots_dead_future.get_result() | |
| 247 num_bots_quarantined = num_bots_quarantined_future.get_result() | |
| 248 num_bots_total = num_bots_total_future.get_result() | |
| 249 try_link = '/botlist?l=%d' % limit | |
| 250 if dimensions: | |
| 251 try_link += '&f=' + '&f='.join(dimensions) | |
| 252 params = { | |
| 253 'bots': bots, | |
| 254 'current_version': version, | |
| 255 'cursor': cursor.urlsafe() if cursor and more else '', | |
| 256 'dimensions': '\n'.join(dimensions), | |
| 257 'is_admin': acl.is_admin(), | |
| 258 'is_privileged_user': acl.is_privileged_user(), | |
| 259 'limit': limit, | |
| 260 'now': now, | |
| 261 'num_bots_alive': num_bots_total - num_bots_dead, | |
| 262 'num_bots_busy': num_bots_busy, | |
| 263 'num_bots_dead': num_bots_dead, | |
| 264 'num_bots_quarantined': num_bots_quarantined, | |
| 265 'try_link': try_link, | |
| 266 'sort_by': sort_by, | |
| 267 'sort_options': self.SORT_OPTIONS, | |
| 268 'xsrf_token': self.generate_xsrf_token(), | |
| 269 } | |
| 270 self.response.write( | |
| 271 template.render('swarming/restricted_botslist.html', params)) | |
| 272 | 202 |
| 273 | 203 |
| 274 class BotHandler(auth.AuthenticatingHandler): | 204 class BotHandler(auth.AuthenticatingHandler): |
| 275 """Returns data about the bot, including last tasks and events.""" | 205 """Redirects to a page about the bot, including last tasks and events.""" |
| 276 | 206 |
| 277 @auth.autologin | 207 @auth.public |
| 278 @auth.require(acl.is_privileged_user) | |
| 279 def get(self, bot_id): | 208 def get(self, bot_id): |
| 280 # pagination is currently for tasks, not events. | 209 self.redirect('/bot?id=%s' % bot_id) |
| 281 limit = int(self.request.get('limit', 100)) | |
| 282 cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor')) | |
| 283 run_results_future = task_result.TaskRunResult.query( | |
| 284 task_result.TaskRunResult.bot_id == bot_id).order( | |
| 285 -task_result.TaskRunResult.started_ts).fetch_page_async( | |
| 286 limit, start_cursor=cursor) | |
| 287 bot_future = bot_management.get_info_key(bot_id).get_async() | |
| 288 events_future = bot_management.get_events_query( | |
| 289 bot_id, True).fetch_async(100) | |
| 290 | |
| 291 now = utils.utcnow() | |
| 292 | |
| 293 # Calculate the time this bot was idle. | |
| 294 idle_time = datetime.timedelta() | |
| 295 run_time = datetime.timedelta() | |
| 296 run_results, cursor, more = run_results_future.get_result() | |
| 297 if run_results: | |
| 298 run_time = run_results[0].duration_now(now) or datetime.timedelta() | |
| 299 if not cursor and run_results[0].state != task_result.State.RUNNING: | |
| 300 # Add idle time since last task completed. Do not do this when a cursor | |
| 301 # is used since it's not representative. | |
| 302 idle_time = now - run_results[0].ended_ts | |
| 303 for index in xrange(1, len(run_results)): | |
| 304 # .started_ts will always be set by definition but .ended_ts may be None | |
| 305 # if the task was abandoned. We can't count idle time since the bot may | |
| 306 # have been busy running *another task*. | |
| 307 # TODO(maruel): One option is to add a third value "broken_time". | |
| 308 # Looking at timestamps specifically could help too, e.g. comparing | |
| 309 # ended_ts of this task vs the next one to see if the bot was assigned | |
| 310 # two tasks simultaneously. | |
| 311 if run_results[index].ended_ts: | |
| 312 idle_time += ( | |
| 313 run_results[index-1].started_ts - run_results[index].ended_ts) | |
| 314 # We are taking the whole time the bot was doing work, not just the | |
| 315 # duration associated with the task. | |
| 316 duration = run_results[index].duration_as_seen_by_server | |
| 317 if duration: | |
| 318 run_time += duration | |
| 319 | |
| 320 events = events_future.get_result() | |
| 321 bot = bot_future.get_result() | |
| 322 if not bot and events: | |
| 323 # If there is not BotInfo, look if there are BotEvent child of this | |
| 324 # entity. If this is the case, it means the bot was deleted but it's | |
| 325 # useful to show information about it to the user even if the bot was | |
| 326 # deleted. For example, it could be an auto-scaled bot. | |
| 327 bot = bot_management.BotInfo( | |
| 328 key=bot_management.get_info_key(bot_id), | |
| 329 dimensions_flat=bot_management.dimensions_to_flat( | |
| 330 events[0].dimensions), | |
| 331 state=events[0].state, | |
| 332 external_ip=events[0].external_ip, | |
| 333 authenticated_as=events[0].authenticated_as, | |
| 334 version=events[0].version, | |
| 335 quarantined=events[0].quarantined, | |
| 336 task_id=events[0].task_id, | |
| 337 last_seen_ts=events[0].ts) | |
| 338 | |
| 339 params = { | |
| 340 'bot': bot, | |
| 341 'bot_id': bot_id, | |
| 342 # TODO(aludwin): Use the bot's correct gRPC status to determine the | |
| 343 # version | |
| 344 'current_version': bot_code.get_bot_version(self.request.host_url), | |
| 345 'cursor': cursor.urlsafe() if cursor and more else None, | |
| 346 'events': events, | |
| 347 'idle_time': idle_time, | |
| 348 'is_admin': acl.is_admin(), | |
| 349 'limit': limit, | |
| 350 'now': now, | |
| 351 'run_results': run_results, | |
| 352 'run_time': run_time, | |
| 353 'try_link': '/bot?id=%s' % bot_id, | |
| 354 'xsrf_token': self.generate_xsrf_token(), | |
| 355 } | |
| 356 self.response.write( | |
| 357 template.render('swarming/restricted_bot.html', params)) | |
| 358 | 210 |
| 359 | 211 |
| 360 class BotDeleteHandler(auth.AuthenticatingHandler): | 212 class BotDeleteHandler(auth.AuthenticatingHandler): |
|
M-A Ruel
2016/11/11 16:19:18
All POST handlers are not needed anymore, remove.
| |
| 361 """Deletes a known bot. | 213 """Redirects to a page about the bot, where the delete can be called.""" |
| 362 | 214 |
| 363 This only deletes the BotInfo, not BotRoot, BotEvent's nor BotSettings. | 215 @auth.public |
| 364 | |
| 365 This is sufficient so the bot doesn't show up on the Bots page while keeping | |
| 366 historical data. | |
| 367 """ | |
| 368 | |
| 369 @auth.require(acl.is_admin) | |
| 370 def post(self, bot_id): | 216 def post(self, bot_id): |
| 371 bot_key = bot_management.get_info_key(bot_id) | 217 self.redirect('/bot?id=%s' % bot_id) |
| 372 if bot_key.get(): | |
| 373 bot_key.delete() | |
| 374 self.redirect('/restricted/bots') | |
| 375 | 218 |
| 376 | 219 |
| 377 ### User accessible pages. | 220 ### User accessible pages. |
| 378 | 221 |
| 379 | 222 |
| 380 class TasksHandler(auth.AuthenticatingHandler): | 223 class TasksHandler(auth.AuthenticatingHandler): |
| 381 """Lists all requests and allows callers to manage them.""" | 224 """Redirects to a list of all task requests.""" |
| 382 # Each entry is an item in the Sort column. | |
| 383 # Each entry is (key, text, hover) | |
| 384 SORT_CHOICES = [ | |
| 385 ('created_ts', 'Created', 'Most recently created tasks are shown first.'), | |
| 386 ('modified_ts', 'Active', | |
| 387 'Shows the most recently active tasks first. Using this order resets ' | |
| 388 'state to \'All\'.'), | |
| 389 ('completed_ts', 'Completed', | |
| 390 'Shows the most recently completed tasks first. Using this order resets ' | |
| 391 'state to \'All\'.'), | |
| 392 ('abandoned_ts', 'Abandoned', | |
| 393 'Shows the most recently abandoned tasks first. Using this order resets ' | |
| 394 'state to \'All\'.'), | |
| 395 ] | |
| 396 | 225 |
| 397 # Each list is one column in the Task state filtering column. | 226 @auth.public |
| 398 # Each sublist is the checkbox item in this column. | |
| 399 # Each entry is (key, text, hover) | |
| 400 # TODO(maruel): Evaluate what the categories the users would like for | |
| 401 # diagnosis, then adapt the DB to enable efficient queries. | |
| 402 STATE_CHOICES = [ | |
| 403 [ | |
| 404 ('all', 'All', 'All tasks ever requested independent of their state.'), | |
| 405 ('pending', 'Pending', | |
| 406 'Tasks that are still ready to be assigned to a bot. Using this order ' | |
| 407 'resets order to \'Created\'.'), | |
| 408 ('running', 'Running', | |
| 409 'Tasks being currently executed by a bot. Using this order resets ' | |
| 410 'order to \'Created\'.'), | |
| 411 ('pending_running', 'Pending|running', | |
| 412 'Tasks either \'pending\' or \'running\'. Using this order resets ' | |
| 413 'order to \'Created\'.'), | |
| 414 ], | |
| 415 [ | |
| 416 ('completed', 'Completed', | |
| 417 'All tasks that are completed, independent if the task itself ' | |
| 418 'succeeded or failed. This excludes tasks that had an infrastructure ' | |
| 419 'failure. Using this order resets order to \'Created\'.'), | |
| 420 ('completed_success', 'Successes', | |
| 421 'Tasks that completed successfully. Using this order resets order to ' | |
| 422 '\'Created\'.'), | |
| 423 ('completed_failure', 'Failures', | |
| 424 'Tasks that were executed successfully but failed, e.g. exit code is ' | |
| 425 'non-zero. Using this order resets order to \'Created\'.'), | |
| 426 ('timed_out', 'Timed out', | |
| 427 'The execution timed out, so it was forcibly killed.'), | |
| 428 ], | |
| 429 [ | |
| 430 ('bot_died', 'Bot died', | |
| 431 'The bot stopped sending updates while running the task, causing the ' | |
| 432 'task execution to time out. This is considered an infrastructure ' | |
| 433 'failure and the usual reason is that the bot BSOD\'ed or ' | |
| 434 'spontaneously rebooted. Using this order resets order to ' | |
| 435 '\'Created\'.'), | |
| 436 ('expired', 'Expired', | |
| 437 'The task was not assigned a bot until its expiration timeout, causing ' | |
| 438 'the task to never being assigned to a bot. This can happen when the ' | |
| 439 'dimension filter was not available or overloaded with a low priority. ' | |
| 440 'Either fix the priority or bring up more bots with these dimensions. ' | |
| 441 'Using this order resets order to \'Created\'.'), | |
| 442 ('canceled', 'Canceled', | |
| 443 'The task was explictly canceled by a user before it started ' | |
| 444 'executing. Using this order resets order to \'Created\'.'), | |
| 445 ], | |
| 446 ] | |
| 447 | |
| 448 @auth.autologin | |
| 449 @auth.require(acl.is_user) | |
| 450 def get(self): | 227 def get(self): |
| 451 cursor_str = self.request.get('cursor') | |
| 452 limit = int(self.request.get('limit', 100)) | 228 limit = int(self.request.get('limit', 100)) |
| 453 sort = self.request.get('sort', self.SORT_CHOICES[0][0]) | |
| 454 state = self.request.get('state', self.STATE_CHOICES[0][0][0]) | |
| 455 counts = self.request.get('counts', '').strip() | |
| 456 task_tags = [ | 229 task_tags = [ |
| 457 line for line in self.request.get('task_tag', '').splitlines() if line | 230 line for line in self.request.get('task_tag', '').splitlines() if line |
| 458 ] | 231 ] |
| 459 | 232 |
| 460 if not any(sort == i[0] for i in self.SORT_CHOICES): | 233 new_ui_link = '/tasklist?l=%d' % limit |
| 461 self.abort(400, 'Invalid sort') | 234 if task_tags: |
| 462 if not any(any(state == i[0] for i in j) for j in self.STATE_CHOICES): | 235 new_ui_link += '&f=' + '&f='.join(task_tags) |
| 463 self.abort(400, 'Invalid state') | |
| 464 | 236 |
| 465 if sort != 'created_ts': | 237 self.redirect(new_ui_link) |
| 466 # Zap all filters in this case to reduce the number of required indexes. | |
| 467 # Revisit according to the user requests. | |
| 468 state = 'all' | |
| 469 | |
| 470 now = utils.utcnow() | |
| 471 # "Temporarily" disable the count. This is too slow on the prod server | |
| 472 # (>10s). The fix is to have the web page do a XHR query to get the values | |
| 473 # asynchronously. | |
| 474 counts_future = None | |
| 475 if counts == 'true': | |
| 476 counts_future = self._get_counts_future(now) | |
| 477 | |
| 478 try: | |
| 479 if task_tags: | |
| 480 # Enforce created_ts when tags are used. | |
| 481 sort = 'created_ts' | |
| 482 query = task_result.get_result_summaries_query( | |
| 483 None, None, sort, state, task_tags) | |
| 484 tasks, cursor_str = datastore_utils.fetch_page(query, limit, cursor_str) | |
| 485 | |
| 486 # Prefetch the TaskRequest all at once, so that ndb's in-process cache has | |
| 487 # it instead of fetching them one at a time indirectly when using | |
| 488 # TaskResultSummary.request_key.get(). | |
| 489 futures = ndb.get_multi_async(t.request_key for t in tasks) | |
| 490 | |
| 491 # Evaluate the counts to print the filtering columns with the associated | |
| 492 # numbers. | |
| 493 state_choices = self._get_state_choices(counts_future) | |
| 494 except ValueError as e: | |
| 495 self.abort(400, str(e)) | |
| 496 | |
| 497 def safe_sum(items): | |
| 498 return sum(items, datetime.timedelta()) | |
| 499 | |
| 500 def avg(items): | |
| 501 if not items: | |
| 502 return 0. | |
| 503 return safe_sum(items) / len(items) | |
| 504 | |
| 505 def median(items): | |
| 506 if not items: | |
| 507 return 0. | |
| 508 middle = len(items) / 2 | |
| 509 if len(items) % 2: | |
| 510 return items[middle] | |
| 511 return (items[middle-1]+items[middle]) / 2 | |
| 512 | |
| 513 gen = (t.duration_now(now) for t in tasks) | |
| 514 durations = sorted(t for t in gen if t is not None) | |
| 515 gen = (t.pending_now(now) for t in tasks) | |
| 516 pendings = sorted(t for t in gen if t is not None) | |
| 517 total_cost_usd = sum(t.cost_usd for t in tasks) | |
| 518 total_cost_saved_usd = sum( | |
| 519 t.cost_saved_usd for t in tasks if t.cost_saved_usd) | |
| 520 # Include the overhead in the total amount of time saved, since it's | |
| 521 # overhead saved. | |
| 522 # In theory, t.duration_as_seen_by_server should always be set when | |
| 523 # t.deduped_from is set but there has some broken entities in the datastore. | |
| 524 total_saved = safe_sum( | |
| 525 t.duration_as_seen_by_server for t in tasks | |
| 526 if t.deduped_from and t.duration_as_seen_by_server) | |
| 527 duration_sum = safe_sum(durations) | |
| 528 total_saved_percent = ( | |
| 529 (100. * total_saved.total_seconds() / duration_sum.total_seconds()) | |
| 530 if duration_sum else 0.) | |
| 531 | |
| 532 try_link = '/tasklist?l=%d' % limit | |
| 533 if task_tags: | |
| 534 try_link += '&f=' + '&f='.join(task_tags) | |
| 535 params = { | |
| 536 'cursor': cursor_str, | |
| 537 'duration_average': avg(durations), | |
| 538 'duration_median': median(durations), | |
| 539 'duration_sum': duration_sum, | |
| 540 'has_pending': any(t.is_pending for t in tasks), | |
| 541 'has_running': any(t.is_running for t in tasks), | |
| 542 'is_admin': acl.is_admin(), | |
| 543 'is_privileged_user': acl.is_privileged_user(), | |
| 544 'limit': limit, | |
| 545 'now': now, | |
| 546 'pending_average': avg(pendings), | |
| 547 'pending_median': median(pendings), | |
| 548 'pending_sum': safe_sum(pendings), | |
| 549 'show_footer': bool(pendings or durations), | |
| 550 'sort': sort, | |
| 551 'sort_choices': self.SORT_CHOICES, | |
| 552 'state': state, | |
| 553 'state_choices': state_choices, | |
| 554 'task_tag': '\n'.join(task_tags), | |
| 555 'tasks': tasks, | |
| 556 'total_cost_usd': total_cost_usd, | |
| 557 'total_cost_saved_usd': total_cost_saved_usd, | |
| 558 'total_saved': total_saved, | |
| 559 'total_saved_percent': total_saved_percent, | |
| 560 'try_link': try_link, | |
| 561 'xsrf_token': self.generate_xsrf_token(), | |
| 562 } | |
| 563 # TODO(maruel): If admin or if the user is task's .user, show the Cancel | |
| 564 # button. Do not show otherwise. | |
| 565 self.response.write(template.render('swarming/user_tasks.html', params)) | |
| 566 | |
| 567 # Do not let dangling futures linger around. | |
| 568 ndb.Future.wait_all(futures) | |
| 569 | |
| 570 def _get_counts_future(self, now): | |
| 571 """Returns all the counting futures in parallel.""" | |
| 572 counts_future = {} | |
| 573 last_24h = now - datetime.timedelta(days=1) | |
| 574 for state_key, _, _ in itertools.chain.from_iterable(self.STATE_CHOICES): | |
| 575 query = task_result.get_result_summaries_query( | |
| 576 last_24h, None, 'created_ts', state_key, None) | |
| 577 counts_future[state_key] = query.count_async() | |
| 578 return counts_future | |
| 579 | |
| 580 def _get_state_choices(self, counts_future): | |
| 581 """Converts STATE_CHOICES with _get_counts_future() into nice text.""" | |
| 582 # Appends the number of tasks for each filter. It gives a sense of how much | |
| 583 # things are going on. | |
| 584 if counts_future: | |
| 585 counts = {k: v.get_result() for k, v in counts_future.iteritems()} | |
| 586 state_choices = [] | |
| 587 for choice_list in self.STATE_CHOICES: | |
| 588 state_choices.append([]) | |
| 589 for state_key, name, title in choice_list: | |
| 590 if counts_future: | |
| 591 name += ' (%d)' % counts[state_key] | |
| 592 state_choices[-1].append((state_key, name, title)) | |
| 593 return state_choices | |
| 594 | 238 |
| 595 | 239 |
| 596 class BaseTaskHandler(auth.AuthenticatingHandler): | 240 class TaskHandler(auth.AuthenticatingHandler): |
| 597 """Handler that acts on a single task. | 241 """Redirects to a page containing task request and result.""" |
| 598 | 242 |
| 599 Ensures that the user has access to the task. | 243 @auth.public |
| 600 """ | 244 def get(self, task_id): |
| 601 def get_request_and_result(self, task_id, with_secret_bytes=False): | 245 self.redirect('/task?id=%s' % task_id) |
| 602 """Retrieves the TaskRequest for 'task_id' and enforces the ACL. | |
| 603 | |
| 604 Supports both TaskResultSummary (ends with 0) or TaskRunResult (ends with 1 | |
| 605 or 2). | |
| 606 | |
| 607 Returns: | |
| 608 tuple(TaskRequest, SecretBytes, result): result can be either for | |
| 609 a TaskRunResult or a TaskResultSummay. | |
| 610 """ | |
| 611 try: | |
| 612 key = task_pack.unpack_result_summary_key(task_id) | |
| 613 request_key = task_pack.result_summary_key_to_request_key(key) | |
| 614 except ValueError: | |
| 615 try: | |
| 616 key = task_pack.unpack_run_result_key(task_id) | |
| 617 request_key = task_pack.result_summary_key_to_request_key( | |
| 618 task_pack.run_result_key_to_result_summary_key(key)) | |
| 619 except ValueError: | |
| 620 self.abort(404, 'Invalid key format.') | |
| 621 if with_secret_bytes: | |
| 622 sb_key = task_pack.request_key_to_secret_bytes_key(request_key) | |
| 623 request, result, secret_bytes = ndb.get_multi((request_key, key, sb_key)) | |
| 624 else: | |
| 625 request, result = ndb.get_multi((request_key, key)) | |
| 626 secret_bytes = None | |
| 627 if not request or not result: | |
| 628 self.abort(404, '%s not found.' % key.id()) | |
| 629 if not request.has_access: | |
| 630 self.abort(403, '%s is not accessible.' % key.id()) | |
| 631 return request, secret_bytes, result | |
| 632 | 246 |
| 633 | 247 |
| 634 class TaskHandler(BaseTaskHandler): | 248 class TaskCancelHandler(auth.AuthenticatingHandler): |
|
M-A Ruel
2016/11/11 16:19:18
Remove
| |
| 635 """Show the full text of a task request and its result.""" | 249 """Redirects to a page about the task, where the cancel can be called.""" |
| 636 | 250 |
| 637 @staticmethod | 251 @auth.public |
| 638 def packages_grouped_by_path(flat_packages): | 252 def post(self, task_id): |
| 639 """Returns sorted [(path, [PinInfo, ...])]. | 253 self.redirect('/task?id=%s' % task_id) |
| 640 | |
| 641 Used by user_task.html. | |
| 642 """ | |
| 643 retval = collections.defaultdict(list) | |
| 644 for pkg in flat_packages: | |
| 645 retval[pkg.path].append(pkg) | |
| 646 return sorted(retval.iteritems()) | |
| 647 | |
| 648 @auth.autologin | |
| 649 @auth.require(acl.is_user) | |
| 650 def get(self, task_id): | |
| 651 request, _, result = self.get_request_and_result(task_id) | |
| 652 parent_task_future = None | |
| 653 if request.parent_task_id: | |
| 654 parent_key = task_pack.unpack_run_result_key(request.parent_task_id) | |
| 655 parent_task_future = parent_key.get_async() | |
| 656 children_tasks_futures = [ | |
| 657 task_pack.unpack_result_summary_key(c).get_async() | |
| 658 for c in result.children_task_ids | |
| 659 ] | |
| 660 | |
| 661 bot_id = result.bot_id | |
| 662 following_task_future = None | |
| 663 previous_task_future = None | |
| 664 if result.started_ts: | |
| 665 # Use a shortcut name because it becomes unwieldy otherwise. | |
| 666 cls = task_result.TaskRunResult | |
| 667 | |
| 668 # Note that the links will be to the TaskRunResult, not to | |
| 669 # TaskResultSummary. | |
| 670 following_task_future = cls.query( | |
| 671 cls.bot_id == bot_id, | |
| 672 cls.started_ts > result.started_ts, | |
| 673 ).order(cls.started_ts).get_async() | |
| 674 previous_task_future = cls.query( | |
| 675 cls.bot_id == bot_id, | |
| 676 cls.started_ts < result.started_ts, | |
| 677 ).order(-cls.started_ts).get_async() | |
| 678 | |
| 679 bot_future = ( | |
| 680 bot_management.get_info_key(bot_id).get_async() if bot_id else None) | |
| 681 | |
| 682 following_task = None | |
| 683 if following_task_future: | |
| 684 following_task = following_task_future.get_result() | |
| 685 | |
| 686 previous_task = None | |
| 687 if previous_task_future: | |
| 688 previous_task = previous_task_future.get_result() | |
| 689 | |
| 690 parent_task = None | |
| 691 if parent_task_future: | |
| 692 parent_task = parent_task_future.get_result() | |
| 693 children_tasks = [c.get_result() for c in children_tasks_futures] | |
| 694 | |
| 695 cipd = None | |
| 696 if request.properties.cipd_input: | |
| 697 cipd = { | |
| 698 'server': request.properties.cipd_input.server, | |
| 699 'client_package': request.properties.cipd_input.client_package, | |
| 700 'packages': self.packages_grouped_by_path( | |
| 701 request.properties.cipd_input.packages), | |
| 702 } | |
| 703 | |
| 704 cipd_pins = None | |
| 705 if result.cipd_pins: | |
| 706 cipd_pins = { | |
| 707 'client_package': result.cipd_pins.client_package, | |
| 708 'packages': self.packages_grouped_by_path(result.cipd_pins.packages), | |
| 709 } | |
| 710 | |
| 711 params = { | |
| 712 'bot': bot_future.get_result() if bot_future else None, | |
| 713 'children_tasks': children_tasks, | |
| 714 'cipd': cipd, | |
| 715 'cipd_pins': cipd_pins, | |
| 716 'is_admin': acl.is_admin(), | |
| 717 'is_gae_admin': users.is_current_user_admin(), | |
| 718 'is_privileged_user': acl.is_privileged_user(), | |
| 719 'following_task': following_task, | |
| 720 'full_appid': os.environ['APPLICATION_ID'], | |
| 721 'host_url': self.request.host_url, | |
| 722 'is_running': result.state == task_result.State.RUNNING, | |
| 723 'parent_task': parent_task, | |
| 724 'previous_task': previous_task, | |
| 725 'request': request, | |
| 726 'task': result, | |
| 727 'try_link': '/task?id=%s' % task_id, | |
| 728 'xsrf_token': self.generate_xsrf_token(), | |
| 729 } | |
| 730 self.response.write(template.render('swarming/user_task.html', params)) | |
| 731 | 254 |
| 732 | 255 |
| 733 class TaskCancelHandler(BaseTaskHandler): | 256 class TaskRetryHandler(auth.AuthenticatingHandler): |
|
M-A Ruel
2016/11/11 16:19:18
Remove
| |
| 734 """Cancel a task.""" | 257 """Redirects to a page about the task, where the cancel can be called.""" |
| 735 | 258 |
| 736 @auth.require(acl.is_user) | 259 @auth.public |
| 737 def post(self, task_id): | 260 def post(self, task_id): |
| 738 request, _, result = self.get_request_and_result(task_id) | 261 self.redirect('/task?id=%s' % task_id) |
| 739 if not task_scheduler.cancel_task(request, result.key)[0]: | |
| 740 self.abort(400, 'Task cancelation error') | |
| 741 # The cancel button appears at both the /tasks and /task pages. Redirect to | |
| 742 # the right place. | |
| 743 if self.request.get('redirect_to', '') == 'listing': | |
| 744 self.redirect('/user/tasks') | |
| 745 else: | |
| 746 self.redirect('/user/task/%s' % task_id) | |
| 747 | |
| 748 | |
| 749 class TaskRetryHandler(BaseTaskHandler): | |
| 750 """Retries the same task but with new metadata. | |
| 751 | |
| 752 Retrying a task forcibly make it not idempotent so the task is unconditionally | |
| 753 scheduled. | |
| 754 """ | |
| 755 | |
| 756 @auth.require(acl.is_user) | |
| 757 def post(self, task_id): | |
| 758 original_request, secret_bytes, _ = self.get_request_and_result( | |
| 759 task_id, with_secret_bytes=True) | |
| 760 # Retrying a task is essentially reusing the same task request as the | |
| 761 # original one, but with new parameters. | |
| 762 new_request = task_request.new_request_clone( | |
| 763 original_request, secret_bytes, | |
| 764 allow_high_priority=acl.can_schedule_high_priority_tasks()) | |
| 765 result_summary = task_scheduler.schedule_request( | |
| 766 new_request, secret_bytes) | |
| 767 self.redirect('/user/task/%s' % result_summary.task_id) | |
| 768 | 262 |
| 769 | 263 |
| 770 ### Public pages. | 264 ### Public pages. |
| 771 | 265 |
| 772 | 266 |
| 773 class OldUIHandler(auth.AuthenticatingHandler): | 267 class OldUIHandler(auth.AuthenticatingHandler): |
| 774 @auth.public | 268 @auth.public |
| 775 def get(self): | 269 def get(self): |
| 776 params = { | 270 params = { |
| 777 'host_url': self.request.host_url, | 271 'host_url': self.request.host_url, |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 870 | 364 |
| 871 # If running on a local dev server, allow bots to connect without prior | 365 # If running on a local dev server, allow bots to connect without prior |
| 872 # groups configuration. Useful when running smoke test. | 366 # groups configuration. Useful when running smoke test. |
| 873 if utils.is_local_dev_server(): | 367 if utils.is_local_dev_server(): |
| 874 acl.bootstrap_dev_server_acls() | 368 acl.bootstrap_dev_server_acls() |
| 875 | 369 |
| 876 routes.extend(handlers_backend.get_routes()) | 370 routes.extend(handlers_backend.get_routes()) |
| 877 routes.extend(handlers_bot.get_routes()) | 371 routes.extend(handlers_bot.get_routes()) |
| 878 routes.extend(handlers_endpoints.get_routes()) | 372 routes.extend(handlers_endpoints.get_routes()) |
| 879 return webapp2.WSGIApplication(routes, debug=debug) | 373 return webapp2.WSGIApplication(routes, debug=debug) |
| OLD | NEW |