blob: d1e16345df66562c2420b57047dde61e4869020b [file] [log] [blame] [edit]
# Copyright 2015 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""This module defines Swarming Server endpoints handlers."""
import functools
import logging
from google.appengine.api import datastore_errors
import endpoints
import gae_ts_mon
from protorpc import messages
from protorpc import message_types
from protorpc import protojson
from protorpc import remote
from components import auth
from components import datastore_utils
from components import endpoints_webapp2
from components import utils
import api_common
import handlers_exceptions
import message_conversion
import swarming_rpcs
from server import acl
from server import bot_code
from server import config
from server import task_queues
### Helper Methods
# Add support for BooleanField in protorpc in endpoints GET requests.
_old_decode_field = protojson.ProtoJson.decode_field
def _decode_field(self, field, value):
if (isinstance(field, messages.BooleanField) and
isinstance(value, basestring)):
return value.lower() == 'true'
return _old_decode_field(self, field, value)
protojson.ProtoJson.decode_field = _decode_field
def _convert_to_endpoints_exception(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except handlers_exceptions.NotFoundException as e:
raise endpoints.NotFoundException(e.message)
except handlers_exceptions.BadRequestException as e:
raise endpoints.BadRequestException(e.message)
except handlers_exceptions.InternalException as e:
raise endpoints.InternalServerErrorException(e.message)
except handlers_exceptions.PermissionException as e:
raise auth.AuthorizationError(e.message)
return wrapper
def endpoint(*args, **kwargs):
def decorator(func):
instrument = gae_ts_mon.instrument_endpoint()
endpoints_method = auth.endpoints_method(*args, **kwargs)
return endpoints_method(instrument(_convert_to_endpoints_exception(func)))
return decorator
def get_or_raise(key):
"""Returns an entity or raises an endpoints exception if it does not exist."""
result = key.get()
if not result:
raise endpoints.NotFoundException('%s not found.' % key.id())
return result
### API
swarming_api = auth.endpoints_api(
name='swarming',
version='v1',
description=
'API to interact with the Swarming service. Permits to create, '
'view and cancel tasks, query tasks and bots')
PermissionsRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
bot_id=messages.StringField(1),
task_id=messages.StringField(2),
tags=messages.StringField(3, repeated=True),
)
@swarming_api.api_class(resource_name='server', path='server')
class SwarmingServerService(remote.Service):
@endpoint(message_types.VoidMessage,
swarming_rpcs.ServerDetails,
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def details(self, _request):
"""Returns information about the server."""
details = api_common.get_server_details()
return swarming_rpcs.ServerDetails(
bot_version=details.bot_version,
server_version=details.server_version,
display_server_url_template=details.display_server_url_template,
luci_config=details.luci_config,
cas_viewer_server=details.cas_viewer_server,
)
@endpoint(message_types.VoidMessage, swarming_rpcs.BootstrapToken)
@auth.require(acl.can_create_bot, log_identity=True)
def token(self, _request):
"""Returns a token to bootstrap a new bot.
This may seem strange to be a POST and not a GET, but it's very
important to make sure GET requests are idempotent and safe
to be pre-fetched; generating a token is neither of those things.
"""
return swarming_rpcs.BootstrapToken(
bootstrap_token = bot_code.generate_bootstrap_token(),
)
@endpoint(PermissionsRequest,
swarming_rpcs.ClientPermissions,
http_method='GET')
@auth.public
def permissions(self, request):
"""Returns the caller's permissions."""
perms = api_common.get_permissions(request.bot_id, request.task_id,
request.tags)
return swarming_rpcs.ClientPermissions(
delete_bot=perms.delete_bot,
delete_bots=perms.delete_bots,
terminate_bot=perms.terminate_bot,
get_configs=perms.get_configs,
put_configs=perms.put_configs,
cancel_task=perms.cancel_task,
cancel_tasks=perms.cancel_tasks,
get_bootstrap_token=perms.get_bootstrap_token,
list_bots=perms.list_bots,
list_tasks=perms.list_tasks)
@endpoint(message_types.VoidMessage,
swarming_rpcs.FileContent,
http_method='GET')
@auth.require(acl.can_view_config, log_identity=True)
def get_bootstrap(self, _request):
"""Retrieves the current version of bootstrap.py."""
obj = bot_code.get_bootstrap('', '')
return swarming_rpcs.FileContent(
content=obj.content.decode('utf-8'),
who=obj.who,
when=obj.when,
version=obj.version)
@endpoint(message_types.VoidMessage,
swarming_rpcs.FileContent,
http_method='GET')
@auth.require(acl.can_view_config, log_identity=True)
def get_bot_config(self, _request):
"""Retrieves the current version of bot_config.py."""
obj, _ = bot_code.get_bot_config()
return swarming_rpcs.FileContent(
content=obj.content.decode('utf-8'),
who=obj.who,
when=obj.when,
version=obj.version)
TaskId = endpoints.ResourceContainer(
message_types.VoidMessage,
task_id=messages.StringField(1, required=True))
TaskIdWithPerf = endpoints.ResourceContainer(
message_types.VoidMessage,
task_id=messages.StringField(1, required=True),
include_performance_stats=messages.BooleanField(2, default=False))
TaskCancel = endpoints.ResourceContainer(
swarming_rpcs.TaskCancelRequest,
task_id=messages.StringField(1, required=True))
TaskIdWithOffset = endpoints.ResourceContainer(
message_types.VoidMessage,
task_id=messages.StringField(1, required=True),
offset=messages.IntegerField(2, default=0),
length=messages.IntegerField(3, default=0))
@swarming_api.api_class(resource_name='task', path='task')
class SwarmingTaskService(remote.Service):
"""Swarming's task-related API."""
@endpoint(TaskIdWithPerf,
swarming_rpcs.TaskResult,
name='result',
path='{task_id}/result',
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def result(self, request):
"""Reports the result of the task corresponding to a task ID.
It can be a 'run' ID specifying a specific retry or a 'summary' ID hidding
the fact that a task may have been retried transparently, when a bot reports
BOT_DIED.
A summary ID ends with '0', a run ID ends with '1' or '2'.
"""
logging.debug('%s', request)
# Workaround a bug in ndb where if a memcache set fails, a stale copy can be
# kept in memcache indefinitely. In the case of task result, if this happens
# on the very last store where the task is saved to NDB to be marked as
# completed, and that the DB store succeeds *but* the memcache update fails,
# this API will *always* return the stale version.
try:
_, result = api_common.get_request_and_result(request.task_id,
api_common.VIEW)
except ValueError:
raise endpoints.BadRequestException('Invalid task ID')
return message_conversion.task_result_to_rpc(
result, request.include_performance_stats)
@endpoint(TaskId,
swarming_rpcs.TaskRequest,
name='request',
path='{task_id}/request',
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def request(self, request):
"""Returns the task request corresponding to a task ID."""
logging.debug('%s', request)
request_key, _ = api_common.to_keys(request.task_id)
request_obj = api_common.get_task_request_async(
request.task_id, request_key, api_common.VIEW).get_result()
return message_conversion.task_request_to_rpc(request_obj)
@endpoint(TaskCancel,
swarming_rpcs.CancelResponse,
name='cancel',
path='{task_id}/cancel')
@auth.require(acl.can_access, log_identity=True)
def cancel(self, request):
"""Cancels a task.
If a bot was running the task, the bot will forcibly cancel the task.
"""
logging.debug('request %s', request)
ok, was_running = api_common.cancel_task(request.task_id,
request.kill_running)
return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running)
@endpoint(TaskIdWithOffset,
swarming_rpcs.TaskOutput,
name='stdout',
path='{task_id}/stdout',
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def stdout(self, request):
"""Returns the output of the task corresponding to a task ID."""
logging.debug('%s', request)
output, state = api_common.get_output(request.task_id, request.offset,
request.length)
if output:
# The datastore entity (TaskOutputChunk) stores output as a string blob.
# In python2 `str` objects are basically c-strings with no character
# encoding required. However, protorpc `StringField` requires that
# a string must be either:
# 1. encoded in 7bit ascii
# 2. have the unicode type
# Since the output of tasks is allowed to be utf-8 encoded strings,
# we must decode the string to utf-8 to be a valid StringField.
output = output.decode('utf-8', 'replace')
return swarming_rpcs.TaskOutput(output=output,
state=swarming_rpcs.TaskState(state))
TasksRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
limit=messages.IntegerField(1, default=200),
cursor=messages.StringField(2),
# These should be DateTimeField but endpoints + protorpc have trouble
# encoding this message in a GET request, this is due to DateTimeField's
# special encoding in protorpc-1.0/protorpc/message_types.py that is
# bypassed when using endpoints-1.0/endpoints/protojson.py to add GET query
# parameter support.
end=messages.FloatField(3),
start=messages.FloatField(4),
state=messages.EnumField(swarming_rpcs.TaskStateQuery, 5, default='ALL'),
tags=messages.StringField(6, repeated=True),
sort=messages.EnumField(swarming_rpcs.TaskSort, 7, default='CREATED_TS'),
include_performance_stats=messages.BooleanField(8, default=False))
TaskStatesRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
task_id=messages.StringField(1, repeated=True))
TasksCountRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
end=messages.FloatField(3),
start=messages.FloatField(4),
state=messages.EnumField(swarming_rpcs.TaskStateQuery, 5, default='ALL'),
tags=messages.StringField(6, repeated=True))
@swarming_api.api_class(resource_name='tasks', path='tasks')
class SwarmingTasksService(remote.Service):
"""Swarming's tasks-related API."""
@endpoint(swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata)
@auth.require(
acl.can_create_task, 'User cannot create tasks.', log_identity=True)
def new(self, request):
"""Creates a new task.
The task will be enqueued in the tasks list and will be executed at the
earliest opportunity by a bot that has at least the dimensions as described
in the task request.
"""
sb = (request.properties.secret_bytes
if request.properties is not None else None)
if sb is not None:
request.properties.secret_bytes = "HIDDEN"
logging.debug('%s', request)
if sb is not None:
request.properties.secret_bytes = sb
try:
request_obj, secret_bytes, template_apply = (
message_conversion.new_task_request_from_rpc(request, utils.utcnow()))
except (datastore_errors.BadValueError, ValueError) as e:
raise endpoints.BadRequestException(e.message)
ntr = api_common.new_task(request_obj, secret_bytes, template_apply,
request.evaluate_only, request.request_uuid)
return swarming_rpcs.TaskRequestMetadata(
request=message_conversion.task_request_to_rpc(ntr.request)
if ntr.request else None,
task_id=ntr.task_id,
task_result=message_conversion.task_result_to_rpc(
ntr.task_result, False) if ntr.task_result else None)
@endpoint(TasksRequest, swarming_rpcs.TaskList, http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def list(self, request):
"""Returns full task results based on the filters.
This endpoint is significantly slower than 'count'. Use 'count' when
possible. If you just want the state of tasks, use 'get_states'.
"""
# TODO(maruel): Rename 'list' to 'results'.
# TODO(maruel): Rename 'TaskList' to 'TaskResults'.
logging.debug('%s', request)
now = utils.utcnow()
try:
rsf = self._query_from_request(request)
except ValueError as e:
raise endpoints.BadRequestException("invalid datetime values %s" % str(e))
items, cursor = api_common.list_task_results(rsf, request.limit,
request.cursor)
return swarming_rpcs.TaskList(
cursor=cursor,
items=[
message_conversion.task_result_to_rpc(
i, request.include_performance_stats)
for i in items
],
now=now)
@endpoint(TaskStatesRequest, swarming_rpcs.TaskStates, http_method='GET')
# TODO(martiniss): users should be able to view their state. This requires
# looking up each TaskRequest.
@auth.require(acl.can_view_all_tasks, log_identity=True)
def get_states(self, request):
"""Returns task state for a specific set of tasks."""
logging.debug('%s', request)
states = api_common.get_states(request.task_id)
return swarming_rpcs.TaskStates(
states=[swarming_rpcs.TaskState(state) for state in states])
@endpoint(TasksRequest, swarming_rpcs.TaskRequests, http_method='GET')
@auth.require(acl.can_view_all_tasks, log_identity=True)
def requests(self, request):
"""Returns tasks requests based on the filters.
This endpoint is slightly slower than 'list'. Use 'list' or 'count' when
possible.
"""
logging.debug('%s', request)
if request.include_performance_stats:
raise endpoints.BadRequestException(
'Can\'t set include_performance_stats for tasks/list')
now = utils.utcnow()
try:
rsf = self._query_from_request(request)
except ValueError as e:
raise endpoints.BadRequestException("invalid datetime values %s" % str(e))
items, cursor = api_common.list_task_requests_no_realm_check(
rsf, request.limit, request.cursor)
return swarming_rpcs.TaskRequests(
cursor=cursor,
items=[message_conversion.task_request_to_rpc(i) for i in items],
now=now)
@endpoint(swarming_rpcs.TasksCancelRequest,
swarming_rpcs.TasksCancelResponse,
http_method='POST')
@auth.require(acl.can_access, log_identity=True)
def cancel(self, request):
"""Cancel a subset of pending tasks based on the tags.
Cancellation happens asynchronously, so when this call returns,
cancellations will not have completed yet.
"""
logging.debug('request %s', request)
try:
start = message_conversion.epoch_to_datetime(request.start)
end = message_conversion.epoch_to_datetime(request.end)
except ValueError as e:
raise endpoints.BadRequestException('Invalid timestamp: %s' % e)
tcr = api_common.cancel_tasks(request.tags, start, end, request.limit,
request.cursor, request.kill_running)
return swarming_rpcs.TasksCancelResponse(cursor=tcr.cursor,
matched=tcr.matched,
now=tcr.now)
@endpoint(TasksCountRequest, swarming_rpcs.TasksCount, http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def count(self, request):
"""Counts number of tasks in a given state."""
logging.debug('%s', request)
now = utils.utcnow()
trf = self._query_from_request(request, 'created_ts')
count = api_common.count_tasks(trf, now)
return swarming_rpcs.TasksCount(count=count, now=now)
def _query_from_request(self, request, sort=None):
"""Returns api_common.TaskFilters object."""
start = message_conversion.epoch_to_datetime(request.start)
end = message_conversion.epoch_to_datetime(request.end)
return api_common.TaskFilters(
start=start,
end=end,
sort=sort or request.sort.name.lower(),
state=request.state.name.lower(),
tags=request.tags,
)
TaskQueuesRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
# Note that it's possible that the RPC returns a tad more or less items than
# requested limit.
limit=messages.IntegerField(1, default=200),
cursor=messages.StringField(2))
@swarming_api.api_class(resource_name='queues', path='queues')
class SwarmingQueuesService(remote.Service):
@endpoint(TaskQueuesRequest, swarming_rpcs.TaskQueueList, http_method='GET')
@auth.require(acl.can_view_all_tasks, log_identity=True)
def list(self, request):
logging.debug('%s', request)
now = utils.utcnow()
q = task_queues.TaskDimensionsInfo.query()
cursor = request.cursor
out = []
count = 0
# As there can be a lot of terminate tasks, try to loop a few times (max 5)
# to get more items.
try:
while len(out) < request.limit and (cursor or not count) and count < 5:
items, cursor = datastore_utils.fetch_page(q, request.limit - len(out),
cursor)
for i in items:
for (dims_flat, valid_until_ts) in i.expiry_map().items():
# Ignore the tasks that are only id specific, since they are
# termination tasks. There may be one per bot, and it is not really
# useful for the user, the user may just query the list of bots.
if len(dims_flat) == 1 and dims_flat[0].startswith('id:'):
continue
out.append(
swarming_rpcs.TaskQueue(dimensions=list(dims_flat),
valid_until_ts=valid_until_ts))
count += 1
except ValueError as e:
raise endpoints.BadRequestException(
'Inappropriate filter for tasks/list: %s' % e)
return swarming_rpcs.TaskQueueList(cursor=cursor, items=out, now=now)
BotId = endpoints.ResourceContainer(
message_types.VoidMessage,
bot_id=messages.StringField(1, required=True))
BotEventsRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
bot_id=messages.StringField(1, required=True),
limit=messages.IntegerField(2, default=200),
cursor=messages.StringField(3),
# end, start are seconds since epoch.
end=messages.FloatField(4),
start=messages.FloatField(5))
BotTasksRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
bot_id=messages.StringField(1, required=True),
limit=messages.IntegerField(2, default=200),
cursor=messages.StringField(3),
# end, start are seconds since epoch.
end=messages.FloatField(4),
start=messages.FloatField(5),
state=messages.EnumField(swarming_rpcs.TaskStateQuery, 6, default='ALL'),
sort=messages.EnumField(swarming_rpcs.TaskSort, 7, default='CREATED_TS'),
include_performance_stats=messages.BooleanField(8, default=False))
@swarming_api.api_class(resource_name='bot', path='bot')
class SwarmingBotService(remote.Service):
"""Bot-related API. Permits querying information about the bot's properties"""
@endpoint(BotId,
swarming_rpcs.BotInfo,
name='get',
path='{bot_id}/get',
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def get(self, request):
"""Returns information about a known bot.
This includes its state and dimensions, and if it is currently running a
task.
"""
logging.debug('%s', request)
bot_id = request.bot_id
bot, deleted = api_common.get_bot(bot_id)
return message_conversion.bot_info_to_rpc(bot, deleted=deleted)
@endpoint(BotId,
swarming_rpcs.DeletedResponse,
name='delete',
path='{bot_id}/delete')
@auth.require(acl.can_access, log_identity=True)
def delete(self, request):
"""Deletes the bot corresponding to a provided bot_id.
At that point, the bot will not appears in the list of bots but it is still
possible to get information about the bot with its bot id is known, as
historical data is not deleted.
It is meant to remove from the DB the presence of a bot that was retired,
e.g. the VM was shut down already. Use 'terminate' instead of the bot is
still alive.
"""
logging.debug('%s', request)
bot_id = request.bot_id
api_common.delete_bot(bot_id)
return swarming_rpcs.DeletedResponse(deleted=True)
@endpoint(BotEventsRequest,
swarming_rpcs.BotEvents,
name='events',
path='{bot_id}/events',
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def events(self, request):
"""Returns events that happened on a bot."""
logging.debug('%s', request)
bot_id = request.bot_id
try:
now = utils.utcnow()
start = message_conversion.epoch_to_datetime(request.start)
end = message_conversion.epoch_to_datetime(request.end)
limit = request.limit
cursor = request.cursor
items, cursor = api_common.get_bot_events(bot_id, start, end, limit,
cursor)
except ValueError as e:
raise endpoints.BadRequestException(
'Inappropriate filter for bot.events: %s' % e)
return swarming_rpcs.BotEvents(
cursor=cursor,
items=[message_conversion.bot_event_to_rpc(r) for r in items],
now=now)
@endpoint(BotId,
swarming_rpcs.TerminateResponse,
name='terminate',
path='{bot_id}/terminate')
@auth.require(acl.can_access, log_identity=True)
def terminate(self, request):
"""Asks a bot to terminate itself gracefully.
The bot will stay in the DB, use 'delete' to remove it from the DB
afterward. This request returns a pseudo-taskid that can be waited for to
wait for the bot to turn down.
This command is particularly useful when a privileged user needs to safely
debug a machine specific issue. The user can trigger a terminate for one of
the bot exhibiting the issue, wait for the pseudo-task to run then access
the machine with the guarantee that the bot is not running anymore.
"""
# TODO(maruel): Disallow a terminate task when there's one currently
# pending or if the bot is considered 'dead', e.g. no contact since 10
# minutes.
logging.debug('%s', request)
bot_id = unicode(request.bot_id)
task_id = api_common.terminate_bot(bot_id)
return swarming_rpcs.TerminateResponse(task_id=task_id)
@endpoint(BotTasksRequest,
swarming_rpcs.BotTasks,
name='tasks',
path='{bot_id}/tasks',
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def tasks(self, request):
"""Lists a given bot's tasks within the specified date range.
In this case, the tasks are effectively TaskRunResult since it's individual
task tries sent to this specific bot.
It is impossible to search by both tags and bot id. If there's a need,
TaskRunResult.tags will be added (via a copy from TaskRequest.tags).
"""
logging.debug('%s', request)
try:
start = message_conversion.epoch_to_datetime(request.start)
end = message_conversion.epoch_to_datetime(request.end)
except ValueError as e:
raise endpoints.BadRequestException('Invalid timestamp: %s' % e)
now = utils.utcnow()
filters = api_common.TaskFilters(
start=start,
end=end,
sort=request.sort.name.lower(),
state=request.state.name.lower(),
tags=[],
)
items, cursor = api_common.list_bot_tasks(
request.bot_id,
filters,
request.limit,
request.cursor,
)
return swarming_rpcs.BotTasks(
cursor=cursor,
items=[
message_conversion.task_result_to_rpc(
r, request.include_performance_stats)
for r in items
],
now=now)
BotsRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
limit=messages.IntegerField(1, default=200),
cursor=messages.StringField(2),
# Must be a list of 'key:value' strings to filter the returned list of bots
# on.
dimensions=messages.StringField(3, repeated=True),
quarantined=messages.EnumField(
swarming_rpcs.ThreeStateBool, 4, default='NONE'),
in_maintenance=messages.EnumField(
swarming_rpcs.ThreeStateBool, 8, default='NONE'),
is_dead=messages.EnumField(swarming_rpcs.ThreeStateBool, 5, default='NONE'),
is_busy=messages.EnumField(swarming_rpcs.ThreeStateBool, 6, default='NONE'))
BotsCountRequest = endpoints.ResourceContainer(
message_types.VoidMessage,
dimensions=messages.StringField(1, repeated=True))
BotsDimensionsRequest = endpoints.ResourceContainer(
message_types.VoidMessage, pool=messages.StringField(1))
@swarming_api.api_class(resource_name='bots', path='bots')
class SwarmingBotsService(remote.Service):
"""Bots-related API."""
@endpoint(BotsRequest, swarming_rpcs.BotList, http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def list(self, request):
"""Provides list of known bots.
Deleted bots will not be listed.
"""
logging.debug('%s', request)
now = utils.utcnow()
quarantined = swarming_rpcs.to_bool(request.quarantined)
in_maintenance = swarming_rpcs.to_bool(request.in_maintenance)
is_dead = swarming_rpcs.to_bool(request.is_dead)
is_busy = swarming_rpcs.to_bool(request.is_busy)
bf = api_common.BotFilters(
dimensions=request.dimensions,
quarantined=quarantined,
in_maintenance=in_maintenance,
is_dead=is_dead,
is_busy=is_busy,
)
bots, cursor = api_common.list_bots(bf, request.limit, request.cursor)
return swarming_rpcs.BotList(
cursor=cursor,
death_timeout=config.settings().bot_death_timeout_secs,
items=[message_conversion.bot_info_to_rpc(bot) for bot in bots],
now=now)
@endpoint(BotsCountRequest, swarming_rpcs.BotsCount, http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def count(self, request):
"""Counts number of bots with given dimensions."""
logging.debug('%s', request)
now = utils.utcnow()
bc = api_common.count_bots(request.dimensions)
return swarming_rpcs.BotsCount(count=bc.count,
quarantined=bc.quarantined,
maintenance=bc.maintenance,
dead=bc.dead,
busy=bc.busy,
now=now)
@endpoint(BotsDimensionsRequest,
swarming_rpcs.BotsDimensions,
http_method='GET')
@auth.require(acl.can_access, log_identity=True)
def dimensions(self, request):
"""Returns the cached set of dimensions currently in use in the fleet."""
# Implemented only in Go now.
raise NotImplementedError()
def get_routes():
return endpoints_webapp2.api_routes([
SwarmingServerService,
SwarmingTaskService,
SwarmingTasksService,
SwarmingQueuesService,
SwarmingBotService,
SwarmingBotsService,
# components.config endpoints for validation and configuring of luci-config
# service URL.
config.ConfigApi], base_path='/api')