| # 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') |