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) now = utils.utcnow() cutoff = now - datetime.timedelta( seconds=config.settings().bot_death_timeout_secs) 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() fetch_future = bot_management.BotInfo.query().order( order).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. 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() params = { 'bots': bots, 'current_version': version, 'cursor': cursor.urlsafe() if cursor and more else '', '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, '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))
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) now = utils.utcnow() cutoff = now - datetime.timedelta( seconds=config.settings().bot_death_timeout_secs) 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() fetch_future = bot_management.BotInfo.query().order(order).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. 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() params = { 'bots': bots, 'current_version': version, 'cursor': cursor.urlsafe() if cursor and more else '', '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, '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))
def get(self): params = { "host_url": self.request.host_url, "is_admin": acl.is_admin(), "is_bot": acl.is_bot(), "is_privileged_user": acl.is_privileged_user(), "is_user": acl.is_user(), "mapreduce_jobs": [], "user_type": acl.get_user_type(), } if acl.is_admin(): params["mapreduce_jobs"] = [ {"id": job_id, "name": job_def["name"]} for job_id, job_def in mapreduce_jobs.MAPREDUCE_JOBS.iteritems() ] params["xsrf_token"] = self.generate_xsrf_token() self.response.write(template.render("swarming/root.html", params))
def get(self): params = { 'host_url': self.request.host_url, 'is_admin': acl.is_admin(), 'is_bot': acl.is_bot(), 'is_privileged_user': acl.is_privileged_user(), 'is_user': acl.is_user(), 'mapreduce_jobs': [], 'user_type': acl.get_user_type(), } if acl.is_admin(): params['mapreduce_jobs'] = [ {'id': job_id, 'name': job_def['name']} for job_id, job_def in mapreduce_jobs.MAPREDUCE_JOBS.iteritems() ] params['xsrf_token'] = self.generate_xsrf_token() self.response.write(template.render('swarming/root.html', params))
def get(self): params = { 'host_url': self.request.host_url, 'is_admin': acl.is_admin(), 'is_bot': acl.is_bot(), 'is_privileged_user': acl.is_privileged_user(), 'is_user': acl.is_user(), 'mapreduce_jobs': [], 'user_type': acl.get_user_type(), } if acl.is_admin(): params['mapreduce_jobs'] = [ {'id': job_id, 'name': job_def['job_name']} for job_id, job_def in mapreduce_jobs.MAPREDUCE_JOBS.iteritems() ] params['xsrf_token'] = self.generate_xsrf_token() self.response.write(template.render('swarming/root.html', params))
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))
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 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)) # Do not let dangling futures linger around. ndb.Future.wait_all(futures)
def get(self, task_id): 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 (NotImplementedError, ValueError): self.abort(404, 'Invalid key format.') # 'result' can be either a TaskRunResult or TaskResultSummary. result_future = key.get_async() request_future = request_key.get_async() result = result_future.get_result() if not result: self.abort(404, 'Invalid key.') if not acl.is_privileged_user(): self.abort(403, 'Implement access control based on the user') request = request_future.get_result() 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] params = { 'bot': bot_future.get_result() if bot_future else None, 'children_tasks': children_tasks, '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, 'now': utils.utcnow(), 'parent_task': parent_task, 'previous_task': previous_task, 'request': request, 'task': result, 'xsrf_token': self.generate_xsrf_token(), } self.response.write(template.render('swarming/user_task.html', params))
def get(self): """Handles both ndb.Query searches and search.Index().search() queries. If |task_name| is set or not affects the meaning of |cursor|. When set, the cursor is for search.Index, otherwise the cursor is for a ndb.Query. """ 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]) task_name = self.request.get('task_name', '').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() counts_future = self._get_counts_future(now) # This call is synchronous. try: tasks, cursor_str, sort, state = task_result.get_tasks( limit, cursor_str, sort, state, task_tags, task_name) # 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 (search.QueryError, 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_total 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_total for t in tasks if t.deduped_from and t.duration_total) duration_sum = safe_sum(durations) total_saved_percent = ( (100. * total_saved.total_seconds() / duration_sum.total_seconds()) if duration_sum else 0.) 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_name': task_name, '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, '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)) # Do not let dangling futures linger around. ndb.Future.wait_all(futures)
def get(self, task_id): 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 (NotImplementedError, ValueError): self.abort(404, "Invalid key format.") # 'result' can be either a TaskRunResult or TaskResultSummary. result_future = key.get_async() request_future = request_key.get_async() result = result_future.get_result() if not result: self.abort(404, "Invalid key.") if not acl.is_privileged_user(): self.abort(403, "Implement access control based on the user") request = request_future.get_result() 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] params = { "bot": bot_future.get_result() if bot_future else None, "children_tasks": children_tasks, "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, "now": utils.utcnow(), "parent_task": parent_task, "previous_task": previous_task, "request": request, "task": result, "xsrf_token": self.generate_xsrf_token(), } self.response.write(template.render("swarming/user_task.html", params))
def get(self): """Handles both ndb.Query searches and search.Index().search() queries. If |task_name| is set or not affects the meaning of |cursor|. When set, the cursor is for search.Index, otherwise the cursor is for a ndb.Query. """ 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]) task_name = self.request.get('task_name', '').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() counts_future = self._get_counts_future(now) # This call is synchronous. try: tasks, cursor_str, sort, state = task_result.get_tasks( limit, cursor_str, sort, state, task_tags, task_name) # 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 (search.QueryError, 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_total 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_total for t in tasks if t.deduped_from and t.duration_total) duration_sum = safe_sum(durations) total_saved_percent = ((100. * total_saved.total_seconds() / duration_sum.total_seconds()) if duration_sum else 0.) 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_name': task_name, '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, '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)) # Do not let dangling futures linger around. ndb.Future.wait_all(futures)