| 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 164 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 175 # New tasks should show up on the status page. | 175 # New tasks should show up on the status page. |
| 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 OldBotsListHandler(auth.AuthenticatingHandler): |
| 186 """Redirects to a list of known bots.""" | 186 """Presents the list of known bots.""" |
| 187 | 187 ACCEPTABLE_BOTS_SORTS = { |
| 188 @auth.public | 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 |
| 196 @auth.autologin |
| 197 @auth.require(acl.is_privileged_user) |
| 189 def get(self): | 198 def get(self): |
| 190 limit = int(self.request.get('limit', 100)) | 199 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) |
| 191 | 211 |
| 192 dimensions = ( | 212 dimensions = ( |
| 193 l.strip() for l in self.request.get('dimensions', '').splitlines() | 213 l.strip() for l in self.request.get('dimensions', '').splitlines() |
| 194 ) | 214 ) |
| 195 dimensions = [i for i in dimensions if i] | 215 dimensions = [i for i in dimensions if i] |
| 196 | 216 |
| 197 new_ui_link = '/botlist?l=%d' % limit | 217 now = utils.utcnow() |
| 218 cutoff = now - datetime.timedelta( |
| 219 seconds=config.settings().bot_death_timeout_secs) |
| 220 |
| 221 # TODO(maruel): Counting becomes an issue at the 10k range, at that point it |
| 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 |
| 198 if dimensions: | 250 if dimensions: |
| 199 new_ui_link += '&f=' + '&f='.join(dimensions) | 251 try_link += '&f=' + '&f='.join(dimensions) |
| 200 | 252 params = { |
| 201 self.redirect(new_ui_link) | 253 'bots': bots, |
| 202 | 254 'current_version': version, |
| 203 | 255 'cursor': cursor.urlsafe() if cursor and more else '', |
| 204 class BotHandler(auth.AuthenticatingHandler): | 256 'dimensions': '\n'.join(dimensions), |
| 205 """Redirects to a page about the bot, including last tasks and events.""" | 257 'is_admin': acl.is_admin(), |
| 206 | 258 'is_privileged_user': acl.is_privileged_user(), |
| 207 @auth.public | 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 |
| 273 |
| 274 class OldBotHandler(auth.AuthenticatingHandler): |
| 275 """Returns data about the bot, including last tasks and events.""" |
| 276 |
| 277 @auth.autologin |
| 278 @auth.require(acl.is_privileged_user) |
| 208 def get(self, bot_id): | 279 def get(self, bot_id): |
| 209 self.redirect('/bot?id=%s' % bot_id) | 280 # pagination is currently for tasks, not events. |
| 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 |
| 359 |
| 360 class OldBotDeleteHandler(auth.AuthenticatingHandler): |
| 361 """Deletes a known bot. |
| 362 |
| 363 This only deletes the BotInfo, not BotRoot, BotEvent's nor BotSettings. |
| 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): |
| 371 bot_key = bot_management.get_info_key(bot_id) |
| 372 if bot_key.get(): |
| 373 bot_key.delete() |
| 374 self.redirect('/restricted/bots') |
| 210 | 375 |
| 211 | 376 |
| 212 ### User accessible pages. | 377 ### User accessible pages. |
| 213 | 378 |
| 214 | 379 |
| 215 class TasksHandler(auth.AuthenticatingHandler): | 380 class OldTasksHandler(auth.AuthenticatingHandler): |
| 216 """Redirects to a list of all task requests.""" | 381 """Lists all requests and allows callers to manage them.""" |
| 217 | 382 # Each entry is an item in the Sort column. |
| 218 @auth.public | 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 |
| 397 # Each list is one column in the Task state filtering column. |
| 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) |
| 219 def get(self): | 450 def get(self): |
| 451 cursor_str = self.request.get('cursor') |
| 220 limit = int(self.request.get('limit', 100)) | 452 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() |
| 221 task_tags = [ | 456 task_tags = [ |
| 222 line for line in self.request.get('task_tag', '').splitlines() if line | 457 line for line in self.request.get('task_tag', '').splitlines() if line |
| 223 ] | 458 ] |
| 224 | 459 |
| 225 new_ui_link = '/tasklist?l=%d' % limit | 460 if not any(sort == i[0] for i in self.SORT_CHOICES): |
| 461 self.abort(400, 'Invalid sort') |
| 462 if not any(any(state == i[0] for i in j) for j in self.STATE_CHOICES): |
| 463 self.abort(400, 'Invalid state') |
| 464 |
| 465 if sort != 'created_ts': |
| 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 |
| 226 if task_tags: | 533 if task_tags: |
| 227 new_ui_link += '&f=' + '&f='.join(task_tags) | 534 try_link += '&f=' + '&f='.join(task_tags) |
| 228 | 535 params = { |
| 229 self.redirect(new_ui_link) | 536 'cursor': cursor_str, |
| 230 | 537 'duration_average': avg(durations), |
| 231 | 538 'duration_median': median(durations), |
| 232 class TaskHandler(auth.AuthenticatingHandler): | 539 'duration_sum': duration_sum, |
| 233 """Redirects to a page containing task request and result.""" | 540 'has_pending': any(t.is_pending for t in tasks), |
| 234 | 541 'has_running': any(t.is_running for t in tasks), |
| 235 @auth.public | 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 |
| 595 |
| 596 class BaseOldTaskHandler(auth.AuthenticatingHandler): |
| 597 """Handler that acts on a single task. |
| 598 |
| 599 Ensures that the user has access to the task. |
| 600 """ |
| 601 def get_request_and_result(self, task_id, with_secret_bytes=False): |
| 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 |
| 633 |
| 634 class OldTaskHandler(BaseOldTaskHandler): |
| 635 """Show the full text of a task request and its result.""" |
| 636 |
| 637 @staticmethod |
| 638 def packages_grouped_by_path(flat_packages): |
| 639 """Returns sorted [(path, [PinInfo, ...])]. |
| 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) |
| 236 def get(self, task_id): | 650 def get(self, task_id): |
| 237 self.redirect('/task?id=%s' % 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 |
| 732 |
| 733 class OldTaskCancelHandler(BaseOldTaskHandler): |
| 734 """Cancel a task.""" |
| 735 |
| 736 @auth.require(acl.is_user) |
| 737 def post(self, task_id): |
| 738 request, _, result = self.get_request_and_result(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 OldTaskRetryHandler(BaseOldTaskHandler): |
| 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) |
| 238 | 768 |
| 239 | 769 |
| 240 ### Public pages. | 770 ### Public pages. |
| 241 | 771 |
| 242 | 772 |
| 243 class OldUIHandler(auth.AuthenticatingHandler): | 773 class OldUIHandler(auth.AuthenticatingHandler): |
| 244 @auth.public | 774 @auth.public |
| 245 def get(self): | 775 def get(self): |
| 246 params = { | 776 params = { |
| 247 'host_url': self.request.host_url, | 777 'host_url': self.request.host_url, |
| (...skipping 10 matching lines...) Expand all Loading... |
| 258 params['mapreduce_jobs'] = [ | 788 params['mapreduce_jobs'] = [ |
| 259 {'id': job_id, 'name': job_def['job_name']} | 789 {'id': job_id, 'name': job_def['job_name']} |
| 260 for job_id, job_def in mapreduce_jobs.MAPREDUCE_JOBS.iteritems() | 790 for job_id, job_def in mapreduce_jobs.MAPREDUCE_JOBS.iteritems() |
| 261 ] | 791 ] |
| 262 params['xsrf_token'] = self.generate_xsrf_token() | 792 params['xsrf_token'] = self.generate_xsrf_token() |
| 263 if acl.is_bootstrapper(): | 793 if acl.is_bootstrapper(): |
| 264 params['bootstrap_token'] = bot_code.generate_bootstrap_token() | 794 params['bootstrap_token'] = bot_code.generate_bootstrap_token() |
| 265 self.response.write(template.render('swarming/root.html', params)) | 795 self.response.write(template.render('swarming/root.html', params)) |
| 266 | 796 |
| 267 | 797 |
| 798 class BotsListHandler(auth.AuthenticatingHandler): |
| 799 """Redirects to a list of known bots.""" |
| 800 |
| 801 @auth.public |
| 802 def get(self): |
| 803 limit = int(self.request.get('limit', 100)) |
| 804 |
| 805 dimensions = ( |
| 806 l.strip() for l in self.request.get('dimensions', '').splitlines() |
| 807 ) |
| 808 dimensions = [i for i in dimensions if i] |
| 809 |
| 810 new_ui_link = '/botlist?l=%d' % limit |
| 811 if dimensions: |
| 812 new_ui_link += '&f=' + '&f='.join(dimensions) |
| 813 |
| 814 self.redirect(new_ui_link) |
| 815 |
| 816 |
| 817 class BotHandler(auth.AuthenticatingHandler): |
| 818 """Redirects to a page about the bot, including last tasks and events.""" |
| 819 |
| 820 @auth.public |
| 821 def get(self, bot_id): |
| 822 self.redirect('/bot?id=%s' % bot_id) |
| 823 |
| 824 |
| 825 ### User accessible pages. |
| 826 |
| 827 |
| 828 class TasksHandler(auth.AuthenticatingHandler): |
| 829 """Redirects to a list of all task requests.""" |
| 830 |
| 831 @auth.public |
| 832 def get(self): |
| 833 limit = int(self.request.get('limit', 100)) |
| 834 task_tags = [ |
| 835 line for line in self.request.get('task_tag', '').splitlines() if line |
| 836 ] |
| 837 |
| 838 new_ui_link = '/tasklist?l=%d' % limit |
| 839 if task_tags: |
| 840 new_ui_link += '&f=' + '&f='.join(task_tags) |
| 841 |
| 842 self.redirect(new_ui_link) |
| 843 |
| 844 |
| 845 class TaskHandler(auth.AuthenticatingHandler): |
| 846 """Redirects to a page containing task request and result.""" |
| 847 |
| 848 @auth.public |
| 849 def get(self, task_id): |
| 850 self.redirect('/task?id=%s' % task_id) |
| 851 |
| 852 |
| 268 class UIHandler(auth.AuthenticatingHandler): | 853 class UIHandler(auth.AuthenticatingHandler): |
| 854 """Serves the landing page for the new UI of the requested page. |
| 855 |
| 856 This landing page is stamped with the OAuth 2.0 client id from the |
| 857 configuration.""" |
| 269 @auth.public | 858 @auth.public |
| 270 def get(self, page): | 859 def get(self, page): |
| 271 if not page: | 860 if not page: |
| 272 page = 'swarming' | 861 page = 'swarming' |
| 273 | 862 |
| 274 params = { | 863 params = { |
| 275 'client_id': config.settings().ui_client_id, | 864 'client_id': config.settings().ui_client_id, |
| 276 } | 865 } |
| 277 try: | 866 try: |
| 278 self.response.write(template.render( | 867 self.response.write(template.render( |
| (...skipping 21 matching lines...) Expand all Loading... |
| 300 template.bootstrap() | 889 template.bootstrap() |
| 301 utils.set_task_queue_module('default') | 890 utils.set_task_queue_module('default') |
| 302 | 891 |
| 303 routes = [ | 892 routes = [ |
| 304 # Frontend pages. They return HTML. | 893 # Frontend pages. They return HTML. |
| 305 # Public pages. | 894 # Public pages. |
| 306 ('/oldui', OldUIHandler), | 895 ('/oldui', OldUIHandler), |
| 307 ('/stats', stats_gviz.StatsSummaryHandler), | 896 ('/stats', stats_gviz.StatsSummaryHandler), |
| 308 ('/<page:(bot|botlist|task|tasklist|)>', UIHandler), | 897 ('/<page:(bot|botlist|task|tasklist|)>', UIHandler), |
| 309 | 898 |
| 310 # User pages. | 899 # Task pages. Redirects to Polymer UI |
| 311 ('/user/tasks', TasksHandler), | 900 ('/user/tasks', TasksHandler), |
| 312 ('/user/task/<task_id:[0-9a-fA-F]+>', TaskHandler), | 901 ('/user/task/<task_id:[0-9a-fA-F]+>', TaskHandler), |
| 313 | 902 |
| 314 # Privileged user pages. | 903 # Bot pages. Redirects to Polymer UI |
| 315 ('/restricted/bots', BotsListHandler), | 904 ('/restricted/bots', BotsListHandler), |
| 316 ('/restricted/bot/<bot_id:[^/]+>', BotHandler), | 905 ('/restricted/bot/<bot_id:[^/]+>', BotHandler), |
| 317 | 906 |
| 907 # User pages. TODO(kjlubick): Remove on January 1, 2017 |
| 908 ('/oldui/user/tasks', OldTasksHandler), |
| 909 ('/oldui/user/task/<task_id:[0-9a-fA-F]+>', OldTaskHandler), |
| 910 ('/oldui/user/task/<task_id:[0-9a-fA-F]+>/cancel', OldTaskCancelHandler), |
| 911 ('/oldui/user/task/<task_id:[0-9a-fA-F]+>/retry', OldTaskRetryHandler), |
| 912 |
| 913 # Privileged user pages. TODO(kjlubick): Remove on January 1, 2017 |
| 914 ('/oldui/restricted/bots', OldBotsListHandler), |
| 915 ('/oldui/restricted/bot/<bot_id:[^/]+>', OldBotHandler), |
| 916 ('/oldui/restricted/bot/<bot_id:[^/]+>/delete', OldBotDeleteHandler), |
| 917 |
| 318 # Admin pages. | 918 # Admin pages. |
| 319 ('/restricted/config', RestrictedConfigHandler), | 919 ('/restricted/config', RestrictedConfigHandler), |
| 320 ('/restricted/cancel_pending', RestrictedCancelPendingHandler), | 920 ('/restricted/cancel_pending', RestrictedCancelPendingHandler), |
| 321 ('/restricted/upload/bot_config', UploadBotConfigHandler), | 921 ('/restricted/upload/bot_config', UploadBotConfigHandler), |
| 322 ('/restricted/upload/bootstrap', UploadBootstrapHandler), | 922 ('/restricted/upload/bootstrap', UploadBootstrapHandler), |
| 323 | 923 |
| 324 # Mapreduce related urls. | 924 # Mapreduce related urls. |
| 325 (r'/restricted/launch_mapreduce', RestrictedLaunchMapReduceJob), | 925 (r'/restricted/launch_mapreduce', RestrictedLaunchMapReduceJob), |
| 326 | 926 |
| 327 # The new APIs: | 927 # The new APIs: |
| 328 ('/swarming/api/v1/stats/summary/<resolution:[a-z]+>', | 928 ('/swarming/api/v1/stats/summary/<resolution:[a-z]+>', |
| 329 stats_gviz.StatsGvizSummaryHandler), | 929 stats_gviz.StatsGvizSummaryHandler), |
| 330 ('/swarming/api/v1/stats/dimensions/<dimensions:.+>/<resolution:[a-z]+>', | 930 ('/swarming/api/v1/stats/dimensions/<dimensions:.+>/<resolution:[a-z]+>', |
| 331 stats_gviz.StatsGvizDimensionsHandler), | 931 stats_gviz.StatsGvizDimensionsHandler), |
| 332 | 932 |
| 333 ('/_ah/mail/<to:.+>', EmailHandler), | 933 ('/_ah/mail/<to:.+>', EmailHandler), |
| 334 ('/_ah/warmup', WarmupHandler), | 934 ('/_ah/warmup', WarmupHandler), |
| 335 ] | 935 ] |
| 336 routes = [webapp2.Route(*i) for i in routes] | 936 routes = [webapp2.Route(*i) for i in routes] |
| 337 | 937 |
| 338 # If running on a local dev server, allow bots to connect without prior | 938 # If running on a local dev server, allow bots to connect without prior |
| 339 # groups configuration. Useful when running smoke test. | 939 # groups configuration. Useful when running smoke test. |
| 340 if utils.is_local_dev_server(): | 940 if utils.is_local_dev_server(): |
| 341 acl.bootstrap_dev_server_acls() | 941 acl.bootstrap_dev_server_acls() |
| 342 | 942 |
| 343 routes.extend(handlers_backend.get_routes()) | 943 routes.extend(handlers_backend.get_routes()) |
| 344 routes.extend(handlers_bot.get_routes()) | 944 routes.extend(handlers_bot.get_routes()) |
| 345 routes.extend(handlers_endpoints.get_routes()) | 945 routes.extend(handlers_endpoints.get_routes()) |
| 346 return webapp2.WSGIApplication(routes, debug=debug) | 946 return webapp2.WSGIApplication(routes, debug=debug) |
| OLD | NEW |