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

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

Issue 2500503002: Redirecting old ui to new ui (Closed)
Patch Set: 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 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
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)
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