| Index: appengine/swarming/handlers_frontend.py
|
| diff --git a/appengine/swarming/handlers_frontend.py b/appengine/swarming/handlers_frontend.py
|
| index 187695bedd1e85a46fd45a788ac07f3cee357c14..2106555e46851efbdf9216a04b7a59a67c0ba861 100644
|
| --- a/appengine/swarming/handlers_frontend.py
|
| +++ b/appengine/swarming/handlers_frontend.py
|
| @@ -183,588 +183,58 @@ class RestrictedLaunchMapReduceJob(auth.AuthenticatingHandler):
|
|
|
|
|
| class BotsListHandler(auth.AuthenticatingHandler):
|
| - """Presents the list of known bots."""
|
| - ACCEPTABLE_BOTS_SORTS = {
|
| - 'last_seen_ts': 'Last Seen',
|
| - '-quarantined': 'Quarantined',
|
| - '__key__': 'ID',
|
| - }
|
| - SORT_OPTIONS = [
|
| - SortOptions(k, v) for k, v in sorted(ACCEPTABLE_BOTS_SORTS.iteritems())
|
| - ]
|
| + """Redirects to a list of known bots."""
|
|
|
| - @auth.autologin
|
| - @auth.require(acl.is_privileged_user)
|
| + @auth.public
|
| def get(self):
|
| limit = int(self.request.get('limit', 100))
|
| - cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor'))
|
| - sort_by = self.request.get('sort_by', '__key__')
|
| - if sort_by not in self.ACCEPTABLE_BOTS_SORTS:
|
| - self.abort(400, 'Invalid sort_by query parameter')
|
| -
|
| - if sort_by[0] == '-':
|
| - order = datastore_query.PropertyOrder(
|
| - sort_by[1:], datastore_query.PropertyOrder.DESCENDING)
|
| - else:
|
| - order = datastore_query.PropertyOrder(
|
| - sort_by, datastore_query.PropertyOrder.ASCENDING)
|
|
|
| dimensions = (
|
| l.strip() for l in self.request.get('dimensions', '').splitlines()
|
| )
|
| dimensions = [i for i in dimensions if i]
|
|
|
| - now = utils.utcnow()
|
| - cutoff = now - datetime.timedelta(
|
| - seconds=config.settings().bot_death_timeout_secs)
|
| -
|
| - # TODO(maruel): Counting becomes an issue at the 10k range, at that point it
|
| - # should be prepopulated in an entity and updated via a cron job.
|
| - num_bots_busy_future = bot_management.BotInfo.query(
|
| - bot_management.BotInfo.is_busy == True).count_async()
|
| - num_bots_dead_future = bot_management.BotInfo.query(
|
| - bot_management.BotInfo.last_seen_ts < cutoff).count_async()
|
| - num_bots_quarantined_future = bot_management.BotInfo.query(
|
| - bot_management.BotInfo.quarantined == True).count_async()
|
| - num_bots_total_future = bot_management.BotInfo.query().count_async()
|
| - q = bot_management.BotInfo.query().order(order)
|
| - for d in dimensions:
|
| - q = q.filter(bot_management.BotInfo.dimensions_flat == d)
|
| - fetch_future = q.fetch_page_async(limit, start_cursor=cursor)
|
| -
|
| - # TODO(maruel): self.request.host_url should be the default AppEngine url
|
| - # version and not the current one. It is only an issue when
|
| - # version-dot-appid.appspot.com urls are used to access this page.
|
| - # TODO(aludwin): Display both gRPC and non-gRPC versions
|
| - version = bot_code.get_bot_version(self.request.host_url)
|
| - bots, cursor, more = fetch_future.get_result()
|
| - # Prefetch the tasks. We don't actually use the value here, it'll be
|
| - # implicitly used by ndb local's cache when refetched by the html template.
|
| - tasks = filter(None, (b.task for b in bots))
|
| - ndb.get_multi(tasks)
|
| - num_bots_busy = num_bots_busy_future.get_result()
|
| - num_bots_dead = num_bots_dead_future.get_result()
|
| - num_bots_quarantined = num_bots_quarantined_future.get_result()
|
| - num_bots_total = num_bots_total_future.get_result()
|
| - try_link = '/botlist?l=%d' % limit
|
| + new_ui_link = '/botlist?l=%d' % limit
|
| if dimensions:
|
| - try_link += '&f=' + '&f='.join(dimensions)
|
| - params = {
|
| - 'bots': bots,
|
| - 'current_version': version,
|
| - 'cursor': cursor.urlsafe() if cursor and more else '',
|
| - 'dimensions': '\n'.join(dimensions),
|
| - 'is_admin': acl.is_admin(),
|
| - 'is_privileged_user': acl.is_privileged_user(),
|
| - 'limit': limit,
|
| - 'now': now,
|
| - 'num_bots_alive': num_bots_total - num_bots_dead,
|
| - 'num_bots_busy': num_bots_busy,
|
| - 'num_bots_dead': num_bots_dead,
|
| - 'num_bots_quarantined': num_bots_quarantined,
|
| - 'try_link': try_link,
|
| - 'sort_by': sort_by,
|
| - 'sort_options': self.SORT_OPTIONS,
|
| - 'xsrf_token': self.generate_xsrf_token(),
|
| - }
|
| - self.response.write(
|
| - template.render('swarming/restricted_botslist.html', params))
|
| + new_ui_link += '&f=' + '&f='.join(dimensions)
|
| +
|
| + self.redirect(new_ui_link)
|
|
|
|
|
| class BotHandler(auth.AuthenticatingHandler):
|
| - """Returns data about the bot, including last tasks and events."""
|
| + """Redirects to a page about the bot, including last tasks and events."""
|
|
|
| - @auth.autologin
|
| - @auth.require(acl.is_privileged_user)
|
| + @auth.public
|
| def get(self, bot_id):
|
| - # pagination is currently for tasks, not events.
|
| - limit = int(self.request.get('limit', 100))
|
| - cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor'))
|
| - run_results_future = task_result.TaskRunResult.query(
|
| - task_result.TaskRunResult.bot_id == bot_id).order(
|
| - -task_result.TaskRunResult.started_ts).fetch_page_async(
|
| - limit, start_cursor=cursor)
|
| - bot_future = bot_management.get_info_key(bot_id).get_async()
|
| - events_future = bot_management.get_events_query(
|
| - bot_id, True).fetch_async(100)
|
| -
|
| - now = utils.utcnow()
|
| -
|
| - # Calculate the time this bot was idle.
|
| - idle_time = datetime.timedelta()
|
| - run_time = datetime.timedelta()
|
| - run_results, cursor, more = run_results_future.get_result()
|
| - if run_results:
|
| - run_time = run_results[0].duration_now(now) or datetime.timedelta()
|
| - if not cursor and run_results[0].state != task_result.State.RUNNING:
|
| - # Add idle time since last task completed. Do not do this when a cursor
|
| - # is used since it's not representative.
|
| - idle_time = now - run_results[0].ended_ts
|
| - for index in xrange(1, len(run_results)):
|
| - # .started_ts will always be set by definition but .ended_ts may be None
|
| - # if the task was abandoned. We can't count idle time since the bot may
|
| - # have been busy running *another task*.
|
| - # TODO(maruel): One option is to add a third value "broken_time".
|
| - # Looking at timestamps specifically could help too, e.g. comparing
|
| - # ended_ts of this task vs the next one to see if the bot was assigned
|
| - # two tasks simultaneously.
|
| - if run_results[index].ended_ts:
|
| - idle_time += (
|
| - run_results[index-1].started_ts - run_results[index].ended_ts)
|
| - # We are taking the whole time the bot was doing work, not just the
|
| - # duration associated with the task.
|
| - duration = run_results[index].duration_as_seen_by_server
|
| - if duration:
|
| - run_time += duration
|
| -
|
| - events = events_future.get_result()
|
| - bot = bot_future.get_result()
|
| - if not bot and events:
|
| - # If there is not BotInfo, look if there are BotEvent child of this
|
| - # entity. If this is the case, it means the bot was deleted but it's
|
| - # useful to show information about it to the user even if the bot was
|
| - # deleted. For example, it could be an auto-scaled bot.
|
| - bot = bot_management.BotInfo(
|
| - key=bot_management.get_info_key(bot_id),
|
| - dimensions_flat=bot_management.dimensions_to_flat(
|
| - events[0].dimensions),
|
| - state=events[0].state,
|
| - external_ip=events[0].external_ip,
|
| - authenticated_as=events[0].authenticated_as,
|
| - version=events[0].version,
|
| - quarantined=events[0].quarantined,
|
| - task_id=events[0].task_id,
|
| - last_seen_ts=events[0].ts)
|
| -
|
| - params = {
|
| - 'bot': bot,
|
| - 'bot_id': bot_id,
|
| - # TODO(aludwin): Use the bot's correct gRPC status to determine the
|
| - # version
|
| - 'current_version': bot_code.get_bot_version(self.request.host_url),
|
| - 'cursor': cursor.urlsafe() if cursor and more else None,
|
| - 'events': events,
|
| - 'idle_time': idle_time,
|
| - 'is_admin': acl.is_admin(),
|
| - 'limit': limit,
|
| - 'now': now,
|
| - 'run_results': run_results,
|
| - 'run_time': run_time,
|
| - 'try_link': '/bot?id=%s' % bot_id,
|
| - 'xsrf_token': self.generate_xsrf_token(),
|
| - }
|
| - self.response.write(
|
| - template.render('swarming/restricted_bot.html', params))
|
| -
|
| -
|
| -class BotDeleteHandler(auth.AuthenticatingHandler):
|
| - """Deletes a known bot.
|
| -
|
| - This only deletes the BotInfo, not BotRoot, BotEvent's nor BotSettings.
|
| -
|
| - This is sufficient so the bot doesn't show up on the Bots page while keeping
|
| - historical data.
|
| - """
|
| -
|
| - @auth.require(acl.is_admin)
|
| - def post(self, bot_id):
|
| - bot_key = bot_management.get_info_key(bot_id)
|
| - if bot_key.get():
|
| - bot_key.delete()
|
| - self.redirect('/restricted/bots')
|
| + self.redirect('/bot?id=%s' % bot_id)
|
|
|
|
|
| ### User accessible pages.
|
|
|
|
|
| class TasksHandler(auth.AuthenticatingHandler):
|
| - """Lists all requests and allows callers to manage them."""
|
| - # Each entry is an item in the Sort column.
|
| - # Each entry is (key, text, hover)
|
| - SORT_CHOICES = [
|
| - ('created_ts', 'Created', 'Most recently created tasks are shown first.'),
|
| - ('modified_ts', 'Active',
|
| - 'Shows the most recently active tasks first. Using this order resets '
|
| - 'state to \'All\'.'),
|
| - ('completed_ts', 'Completed',
|
| - 'Shows the most recently completed tasks first. Using this order resets '
|
| - 'state to \'All\'.'),
|
| - ('abandoned_ts', 'Abandoned',
|
| - 'Shows the most recently abandoned tasks first. Using this order resets '
|
| - 'state to \'All\'.'),
|
| - ]
|
| + """Redirects to a list of all task requests."""
|
|
|
| - # Each list is one column in the Task state filtering column.
|
| - # Each sublist is the checkbox item in this column.
|
| - # Each entry is (key, text, hover)
|
| - # TODO(maruel): Evaluate what the categories the users would like for
|
| - # diagnosis, then adapt the DB to enable efficient queries.
|
| - STATE_CHOICES = [
|
| - [
|
| - ('all', 'All', 'All tasks ever requested independent of their state.'),
|
| - ('pending', 'Pending',
|
| - 'Tasks that are still ready to be assigned to a bot. Using this order '
|
| - 'resets order to \'Created\'.'),
|
| - ('running', 'Running',
|
| - 'Tasks being currently executed by a bot. Using this order resets '
|
| - 'order to \'Created\'.'),
|
| - ('pending_running', 'Pending|running',
|
| - 'Tasks either \'pending\' or \'running\'. Using this order resets '
|
| - 'order to \'Created\'.'),
|
| - ],
|
| - [
|
| - ('completed', 'Completed',
|
| - 'All tasks that are completed, independent if the task itself '
|
| - 'succeeded or failed. This excludes tasks that had an infrastructure '
|
| - 'failure. Using this order resets order to \'Created\'.'),
|
| - ('completed_success', 'Successes',
|
| - 'Tasks that completed successfully. Using this order resets order to '
|
| - '\'Created\'.'),
|
| - ('completed_failure', 'Failures',
|
| - 'Tasks that were executed successfully but failed, e.g. exit code is '
|
| - 'non-zero. Using this order resets order to \'Created\'.'),
|
| - ('timed_out', 'Timed out',
|
| - 'The execution timed out, so it was forcibly killed.'),
|
| - ],
|
| - [
|
| - ('bot_died', 'Bot died',
|
| - 'The bot stopped sending updates while running the task, causing the '
|
| - 'task execution to time out. This is considered an infrastructure '
|
| - 'failure and the usual reason is that the bot BSOD\'ed or '
|
| - 'spontaneously rebooted. Using this order resets order to '
|
| - '\'Created\'.'),
|
| - ('expired', 'Expired',
|
| - 'The task was not assigned a bot until its expiration timeout, causing '
|
| - 'the task to never being assigned to a bot. This can happen when the '
|
| - 'dimension filter was not available or overloaded with a low priority. '
|
| - 'Either fix the priority or bring up more bots with these dimensions. '
|
| - 'Using this order resets order to \'Created\'.'),
|
| - ('canceled', 'Canceled',
|
| - 'The task was explictly canceled by a user before it started '
|
| - 'executing. Using this order resets order to \'Created\'.'),
|
| - ],
|
| - ]
|
| -
|
| - @auth.autologin
|
| - @auth.require(acl.is_user)
|
| + @auth.public
|
| def get(self):
|
| - cursor_str = self.request.get('cursor')
|
| limit = int(self.request.get('limit', 100))
|
| - sort = self.request.get('sort', self.SORT_CHOICES[0][0])
|
| - state = self.request.get('state', self.STATE_CHOICES[0][0][0])
|
| - counts = self.request.get('counts', '').strip()
|
| task_tags = [
|
| line for line in self.request.get('task_tag', '').splitlines() if line
|
| ]
|
|
|
| - if not any(sort == i[0] for i in self.SORT_CHOICES):
|
| - self.abort(400, 'Invalid sort')
|
| - if not any(any(state == i[0] for i in j) for j in self.STATE_CHOICES):
|
| - self.abort(400, 'Invalid state')
|
| -
|
| - if sort != 'created_ts':
|
| - # Zap all filters in this case to reduce the number of required indexes.
|
| - # Revisit according to the user requests.
|
| - state = 'all'
|
| -
|
| - now = utils.utcnow()
|
| - # "Temporarily" disable the count. This is too slow on the prod server
|
| - # (>10s). The fix is to have the web page do a XHR query to get the values
|
| - # asynchronously.
|
| - counts_future = None
|
| - if counts == 'true':
|
| - counts_future = self._get_counts_future(now)
|
| -
|
| - try:
|
| - if task_tags:
|
| - # Enforce created_ts when tags are used.
|
| - sort = 'created_ts'
|
| - query = task_result.get_result_summaries_query(
|
| - None, None, sort, state, task_tags)
|
| - tasks, cursor_str = datastore_utils.fetch_page(query, limit, cursor_str)
|
| -
|
| - # Prefetch the TaskRequest all at once, so that ndb's in-process cache has
|
| - # it instead of fetching them one at a time indirectly when using
|
| - # TaskResultSummary.request_key.get().
|
| - futures = ndb.get_multi_async(t.request_key for t in tasks)
|
| -
|
| - # Evaluate the counts to print the filtering columns with the associated
|
| - # numbers.
|
| - state_choices = self._get_state_choices(counts_future)
|
| - except ValueError as e:
|
| - self.abort(400, str(e))
|
| -
|
| - def safe_sum(items):
|
| - return sum(items, datetime.timedelta())
|
| -
|
| - def avg(items):
|
| - if not items:
|
| - return 0.
|
| - return safe_sum(items) / len(items)
|
| -
|
| - def median(items):
|
| - if not items:
|
| - return 0.
|
| - middle = len(items) / 2
|
| - if len(items) % 2:
|
| - return items[middle]
|
| - return (items[middle-1]+items[middle]) / 2
|
| -
|
| - gen = (t.duration_now(now) for t in tasks)
|
| - durations = sorted(t for t in gen if t is not None)
|
| - gen = (t.pending_now(now) for t in tasks)
|
| - pendings = sorted(t for t in gen if t is not None)
|
| - total_cost_usd = sum(t.cost_usd for t in tasks)
|
| - total_cost_saved_usd = sum(
|
| - t.cost_saved_usd for t in tasks if t.cost_saved_usd)
|
| - # Include the overhead in the total amount of time saved, since it's
|
| - # overhead saved.
|
| - # In theory, t.duration_as_seen_by_server should always be set when
|
| - # t.deduped_from is set but there has some broken entities in the datastore.
|
| - total_saved = safe_sum(
|
| - t.duration_as_seen_by_server for t in tasks
|
| - if t.deduped_from and t.duration_as_seen_by_server)
|
| - duration_sum = safe_sum(durations)
|
| - total_saved_percent = (
|
| - (100. * total_saved.total_seconds() / duration_sum.total_seconds())
|
| - if duration_sum else 0.)
|
| -
|
| - try_link = '/tasklist?l=%d' % limit
|
| + new_ui_link = '/tasklist?l=%d' % limit
|
| if task_tags:
|
| - try_link += '&f=' + '&f='.join(task_tags)
|
| - params = {
|
| - 'cursor': cursor_str,
|
| - 'duration_average': avg(durations),
|
| - 'duration_median': median(durations),
|
| - 'duration_sum': duration_sum,
|
| - 'has_pending': any(t.is_pending for t in tasks),
|
| - 'has_running': any(t.is_running for t in tasks),
|
| - 'is_admin': acl.is_admin(),
|
| - 'is_privileged_user': acl.is_privileged_user(),
|
| - 'limit': limit,
|
| - 'now': now,
|
| - 'pending_average': avg(pendings),
|
| - 'pending_median': median(pendings),
|
| - 'pending_sum': safe_sum(pendings),
|
| - 'show_footer': bool(pendings or durations),
|
| - 'sort': sort,
|
| - 'sort_choices': self.SORT_CHOICES,
|
| - 'state': state,
|
| - 'state_choices': state_choices,
|
| - 'task_tag': '\n'.join(task_tags),
|
| - 'tasks': tasks,
|
| - 'total_cost_usd': total_cost_usd,
|
| - 'total_cost_saved_usd': total_cost_saved_usd,
|
| - 'total_saved': total_saved,
|
| - 'total_saved_percent': total_saved_percent,
|
| - 'try_link': try_link,
|
| - 'xsrf_token': self.generate_xsrf_token(),
|
| - }
|
| - # TODO(maruel): If admin or if the user is task's .user, show the Cancel
|
| - # button. Do not show otherwise.
|
| - self.response.write(template.render('swarming/user_tasks.html', params))
|
| + new_ui_link += '&f=' + '&f='.join(task_tags)
|
|
|
| - # Do not let dangling futures linger around.
|
| - ndb.Future.wait_all(futures)
|
| + self.redirect(new_ui_link)
|
|
|
| - def _get_counts_future(self, now):
|
| - """Returns all the counting futures in parallel."""
|
| - counts_future = {}
|
| - last_24h = now - datetime.timedelta(days=1)
|
| - for state_key, _, _ in itertools.chain.from_iterable(self.STATE_CHOICES):
|
| - query = task_result.get_result_summaries_query(
|
| - last_24h, None, 'created_ts', state_key, None)
|
| - counts_future[state_key] = query.count_async()
|
| - return counts_future
|
|
|
| - def _get_state_choices(self, counts_future):
|
| - """Converts STATE_CHOICES with _get_counts_future() into nice text."""
|
| - # Appends the number of tasks for each filter. It gives a sense of how much
|
| - # things are going on.
|
| - if counts_future:
|
| - counts = {k: v.get_result() for k, v in counts_future.iteritems()}
|
| - state_choices = []
|
| - for choice_list in self.STATE_CHOICES:
|
| - state_choices.append([])
|
| - for state_key, name, title in choice_list:
|
| - if counts_future:
|
| - name += ' (%d)' % counts[state_key]
|
| - state_choices[-1].append((state_key, name, title))
|
| - return state_choices
|
| +class TaskHandler(auth.AuthenticatingHandler):
|
| + """Redirects to a page containing task request and result."""
|
|
|
| -
|
| -class BaseTaskHandler(auth.AuthenticatingHandler):
|
| - """Handler that acts on a single task.
|
| -
|
| - Ensures that the user has access to the task.
|
| - """
|
| - def get_request_and_result(self, task_id, with_secret_bytes=False):
|
| - """Retrieves the TaskRequest for 'task_id' and enforces the ACL.
|
| -
|
| - Supports both TaskResultSummary (ends with 0) or TaskRunResult (ends with 1
|
| - or 2).
|
| -
|
| - Returns:
|
| - tuple(TaskRequest, SecretBytes, result): result can be either for
|
| - a TaskRunResult or a TaskResultSummay.
|
| - """
|
| - try:
|
| - key = task_pack.unpack_result_summary_key(task_id)
|
| - request_key = task_pack.result_summary_key_to_request_key(key)
|
| - except ValueError:
|
| - try:
|
| - key = task_pack.unpack_run_result_key(task_id)
|
| - request_key = task_pack.result_summary_key_to_request_key(
|
| - task_pack.run_result_key_to_result_summary_key(key))
|
| - except ValueError:
|
| - self.abort(404, 'Invalid key format.')
|
| - if with_secret_bytes:
|
| - sb_key = task_pack.request_key_to_secret_bytes_key(request_key)
|
| - request, result, secret_bytes = ndb.get_multi((request_key, key, sb_key))
|
| - else:
|
| - request, result = ndb.get_multi((request_key, key))
|
| - secret_bytes = None
|
| - if not request or not result:
|
| - self.abort(404, '%s not found.' % key.id())
|
| - if not request.has_access:
|
| - self.abort(403, '%s is not accessible.' % key.id())
|
| - return request, secret_bytes, result
|
| -
|
| -
|
| -class TaskHandler(BaseTaskHandler):
|
| - """Show the full text of a task request and its result."""
|
| -
|
| - @staticmethod
|
| - def packages_grouped_by_path(flat_packages):
|
| - """Returns sorted [(path, [PinInfo, ...])].
|
| -
|
| - Used by user_task.html.
|
| - """
|
| - retval = collections.defaultdict(list)
|
| - for pkg in flat_packages:
|
| - retval[pkg.path].append(pkg)
|
| - return sorted(retval.iteritems())
|
| -
|
| - @auth.autologin
|
| - @auth.require(acl.is_user)
|
| + @auth.public
|
| def get(self, task_id):
|
| - request, _, result = self.get_request_and_result(task_id)
|
| - parent_task_future = None
|
| - if request.parent_task_id:
|
| - parent_key = task_pack.unpack_run_result_key(request.parent_task_id)
|
| - parent_task_future = parent_key.get_async()
|
| - children_tasks_futures = [
|
| - task_pack.unpack_result_summary_key(c).get_async()
|
| - for c in result.children_task_ids
|
| - ]
|
| -
|
| - bot_id = result.bot_id
|
| - following_task_future = None
|
| - previous_task_future = None
|
| - if result.started_ts:
|
| - # Use a shortcut name because it becomes unwieldy otherwise.
|
| - cls = task_result.TaskRunResult
|
| -
|
| - # Note that the links will be to the TaskRunResult, not to
|
| - # TaskResultSummary.
|
| - following_task_future = cls.query(
|
| - cls.bot_id == bot_id,
|
| - cls.started_ts > result.started_ts,
|
| - ).order(cls.started_ts).get_async()
|
| - previous_task_future = cls.query(
|
| - cls.bot_id == bot_id,
|
| - cls.started_ts < result.started_ts,
|
| - ).order(-cls.started_ts).get_async()
|
| -
|
| - bot_future = (
|
| - bot_management.get_info_key(bot_id).get_async() if bot_id else None)
|
| -
|
| - following_task = None
|
| - if following_task_future:
|
| - following_task = following_task_future.get_result()
|
| -
|
| - previous_task = None
|
| - if previous_task_future:
|
| - previous_task = previous_task_future.get_result()
|
| -
|
| - parent_task = None
|
| - if parent_task_future:
|
| - parent_task = parent_task_future.get_result()
|
| - children_tasks = [c.get_result() for c in children_tasks_futures]
|
| -
|
| - cipd = None
|
| - if request.properties.cipd_input:
|
| - cipd = {
|
| - 'server': request.properties.cipd_input.server,
|
| - 'client_package': request.properties.cipd_input.client_package,
|
| - 'packages': self.packages_grouped_by_path(
|
| - request.properties.cipd_input.packages),
|
| - }
|
| -
|
| - cipd_pins = None
|
| - if result.cipd_pins:
|
| - cipd_pins = {
|
| - 'client_package': result.cipd_pins.client_package,
|
| - 'packages': self.packages_grouped_by_path(result.cipd_pins.packages),
|
| - }
|
| -
|
| - params = {
|
| - 'bot': bot_future.get_result() if bot_future else None,
|
| - 'children_tasks': children_tasks,
|
| - 'cipd': cipd,
|
| - 'cipd_pins': cipd_pins,
|
| - 'is_admin': acl.is_admin(),
|
| - 'is_gae_admin': users.is_current_user_admin(),
|
| - 'is_privileged_user': acl.is_privileged_user(),
|
| - 'following_task': following_task,
|
| - 'full_appid': os.environ['APPLICATION_ID'],
|
| - 'host_url': self.request.host_url,
|
| - 'is_running': result.state == task_result.State.RUNNING,
|
| - 'parent_task': parent_task,
|
| - 'previous_task': previous_task,
|
| - 'request': request,
|
| - 'task': result,
|
| - 'try_link': '/task?id=%s' % task_id,
|
| - 'xsrf_token': self.generate_xsrf_token(),
|
| - }
|
| - self.response.write(template.render('swarming/user_task.html', params))
|
| -
|
| -
|
| -class TaskCancelHandler(BaseTaskHandler):
|
| - """Cancel a task."""
|
| -
|
| - @auth.require(acl.is_user)
|
| - def post(self, task_id):
|
| - request, _, result = self.get_request_and_result(task_id)
|
| - if not task_scheduler.cancel_task(request, result.key)[0]:
|
| - self.abort(400, 'Task cancelation error')
|
| - # The cancel button appears at both the /tasks and /task pages. Redirect to
|
| - # the right place.
|
| - if self.request.get('redirect_to', '') == 'listing':
|
| - self.redirect('/user/tasks')
|
| - else:
|
| - self.redirect('/user/task/%s' % task_id)
|
| -
|
| -
|
| -class TaskRetryHandler(BaseTaskHandler):
|
| - """Retries the same task but with new metadata.
|
| -
|
| - Retrying a task forcibly make it not idempotent so the task is unconditionally
|
| - scheduled.
|
| - """
|
| -
|
| - @auth.require(acl.is_user)
|
| - def post(self, task_id):
|
| - original_request, secret_bytes, _ = self.get_request_and_result(
|
| - task_id, with_secret_bytes=True)
|
| - # Retrying a task is essentially reusing the same task request as the
|
| - # original one, but with new parameters.
|
| - new_request = task_request.new_request_clone(
|
| - original_request, secret_bytes,
|
| - allow_high_priority=acl.can_schedule_high_priority_tasks())
|
| - result_summary = task_scheduler.schedule_request(
|
| - new_request, secret_bytes)
|
| - self.redirect('/user/task/%s' % result_summary.task_id)
|
| + self.redirect('/task?id=%s' % task_id)
|
|
|
|
|
| ### Public pages.
|
| @@ -840,13 +310,10 @@ def create_application(debug):
|
| # User pages.
|
| ('/user/tasks', TasksHandler),
|
| ('/user/task/<task_id:[0-9a-fA-F]+>', TaskHandler),
|
| - ('/user/task/<task_id:[0-9a-fA-F]+>/cancel', TaskCancelHandler),
|
| - ('/user/task/<task_id:[0-9a-fA-F]+>/retry', TaskRetryHandler),
|
|
|
| # Privileged user pages.
|
| ('/restricted/bots', BotsListHandler),
|
| ('/restricted/bot/<bot_id:[^/]+>', BotHandler),
|
| - ('/restricted/bot/<bot_id:[^/]+>/delete', BotDeleteHandler),
|
|
|
| # Admin pages.
|
| ('/restricted/config', RestrictedConfigHandler),
|
|
|