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

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

Issue 2500503002: Redirecting old ui to new ui (Closed)
Patch Set: Remove post handlers Created 4 years, 1 month 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 | « no previous file | appengine/swarming/handlers_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 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
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
359
360 class BotDeleteHandler(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')
375 210
376 211
377 ### User accessible pages. 212 ### User accessible pages.
378 213
379 214
380 class TasksHandler(auth.AuthenticatingHandler): 215 class TasksHandler(auth.AuthenticatingHandler):
381 """Lists all requests and allows callers to manage them.""" 216 """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 217
397 # Each list is one column in the Task state filtering column. 218 @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): 219 def get(self):
451 cursor_str = self.request.get('cursor')
452 limit = int(self.request.get('limit', 100)) 220 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 = [ 221 task_tags = [
457 line for line in self.request.get('task_tag', '').splitlines() if line 222 line for line in self.request.get('task_tag', '').splitlines() if line
458 ] 223 ]
459 224
460 if not any(sort == i[0] for i in self.SORT_CHOICES): 225 new_ui_link = '/tasklist?l=%d' % limit
461 self.abort(400, 'Invalid sort') 226 if task_tags:
462 if not any(any(state == i[0] for i in j) for j in self.STATE_CHOICES): 227 new_ui_link += '&f=' + '&f='.join(task_tags)
463 self.abort(400, 'Invalid state')
464 228
465 if sort != 'created_ts': 229 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 230
595 231
596 class BaseTaskHandler(auth.AuthenticatingHandler): 232 class TaskHandler(auth.AuthenticatingHandler):
597 """Handler that acts on a single task. 233 """Redirects to a page containing task request and result."""
598 234
599 Ensures that the user has access to the task. 235 @auth.public
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 TaskHandler(BaseTaskHandler):
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)
650 def get(self, task_id): 236 def get(self, task_id):
651 request, _, result = self.get_request_and_result(task_id) 237 self.redirect('/task?id=%s' % 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 TaskCancelHandler(BaseTaskHandler):
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 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 238
769 239
770 ### Public pages. 240 ### Public pages.
771 241
772 242
773 class OldUIHandler(auth.AuthenticatingHandler): 243 class OldUIHandler(auth.AuthenticatingHandler):
774 @auth.public 244 @auth.public
775 def get(self): 245 def get(self):
776 params = { 246 params = {
777 'host_url': self.request.host_url, 247 'host_url': self.request.host_url,
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
833 routes = [ 303 routes = [
834 # Frontend pages. They return HTML. 304 # Frontend pages. They return HTML.
835 # Public pages. 305 # Public pages.
836 ('/oldui', OldUIHandler), 306 ('/oldui', OldUIHandler),
837 ('/stats', stats_gviz.StatsSummaryHandler), 307 ('/stats', stats_gviz.StatsSummaryHandler),
838 ('/<page:(bot|botlist|task|tasklist|)>', UIHandler), 308 ('/<page:(bot|botlist|task|tasklist|)>', UIHandler),
839 309
840 # User pages. 310 # User pages.
841 ('/user/tasks', TasksHandler), 311 ('/user/tasks', TasksHandler),
842 ('/user/task/<task_id:[0-9a-fA-F]+>', TaskHandler), 312 ('/user/task/<task_id:[0-9a-fA-F]+>', TaskHandler),
843 ('/user/task/<task_id:[0-9a-fA-F]+>/cancel', TaskCancelHandler),
844 ('/user/task/<task_id:[0-9a-fA-F]+>/retry', TaskRetryHandler),
845 313
846 # Privileged user pages. 314 # Privileged user pages.
847 ('/restricted/bots', BotsListHandler), 315 ('/restricted/bots', BotsListHandler),
848 ('/restricted/bot/<bot_id:[^/]+>', BotHandler), 316 ('/restricted/bot/<bot_id:[^/]+>', BotHandler),
849 ('/restricted/bot/<bot_id:[^/]+>/delete', BotDeleteHandler),
850 317
851 # Admin pages. 318 # Admin pages.
852 ('/restricted/config', RestrictedConfigHandler), 319 ('/restricted/config', RestrictedConfigHandler),
853 ('/restricted/cancel_pending', RestrictedCancelPendingHandler), 320 ('/restricted/cancel_pending', RestrictedCancelPendingHandler),
854 ('/restricted/upload/bot_config', UploadBotConfigHandler), 321 ('/restricted/upload/bot_config', UploadBotConfigHandler),
855 ('/restricted/upload/bootstrap', UploadBootstrapHandler), 322 ('/restricted/upload/bootstrap', UploadBootstrapHandler),
856 323
857 # Mapreduce related urls. 324 # Mapreduce related urls.
858 (r'/restricted/launch_mapreduce', RestrictedLaunchMapReduceJob), 325 (r'/restricted/launch_mapreduce', RestrictedLaunchMapReduceJob),
859 326
(...skipping 10 matching lines...) Expand all
870 337
871 # If running on a local dev server, allow bots to connect without prior 338 # If running on a local dev server, allow bots to connect without prior
872 # groups configuration. Useful when running smoke test. 339 # groups configuration. Useful when running smoke test.
873 if utils.is_local_dev_server(): 340 if utils.is_local_dev_server():
874 acl.bootstrap_dev_server_acls() 341 acl.bootstrap_dev_server_acls()
875 342
876 routes.extend(handlers_backend.get_routes()) 343 routes.extend(handlers_backend.get_routes())
877 routes.extend(handlers_bot.get_routes()) 344 routes.extend(handlers_bot.get_routes())
878 routes.extend(handlers_endpoints.get_routes()) 345 routes.extend(handlers_endpoints.get_routes())
879 return webapp2.WSGIApplication(routes, debug=debug) 346 return webapp2.WSGIApplication(routes, debug=debug)
OLDNEW
« no previous file with comments | « no previous file | appengine/swarming/handlers_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698