| OLD | NEW |
| 1 # Copyright 2015 The LUCI Authors. All rights reserved. | 1 # Copyright 2015 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """This module defines Swarming Server endpoints handlers.""" | 5 """This module defines Swarming Server endpoints handlers.""" |
| 6 | 6 |
| 7 import logging | 7 import logging |
| 8 import os | 8 import os |
| 9 | 9 |
| 10 from google.appengine.api import datastore_errors | 10 from google.appengine.api import datastore_errors |
| (...skipping 22 matching lines...) Expand all Loading... |
| 33 from server import task_pack | 33 from server import task_pack |
| 34 from server import task_queues | 34 from server import task_queues |
| 35 from server import task_request | 35 from server import task_request |
| 36 from server import task_result | 36 from server import task_result |
| 37 from server import task_scheduler | 37 from server import task_scheduler |
| 38 | 38 |
| 39 | 39 |
| 40 ### Helper Methods | 40 ### Helper Methods |
| 41 | 41 |
| 42 | 42 |
| 43 # Used by get_request_and_result(), clearer than using True/False and important |
| 44 # as this is part of the security boundary. |
| 45 _EDIT = object() |
| 46 _VIEW = object() |
| 47 |
| 48 |
| 43 # Add support for BooleanField in protorpc in endpoints GET requests. | 49 # Add support for BooleanField in protorpc in endpoints GET requests. |
| 44 _old_decode_field = protojson.ProtoJson.decode_field | 50 _old_decode_field = protojson.ProtoJson.decode_field |
| 45 def _decode_field(self, field, value): | 51 def _decode_field(self, field, value): |
| 46 if (isinstance(field, messages.BooleanField) and | 52 if (isinstance(field, messages.BooleanField) and |
| 47 isinstance(value, basestring)): | 53 isinstance(value, basestring)): |
| 48 return value.lower() == 'true' | 54 return value.lower() == 'true' |
| 49 return _old_decode_field(self, field, value) | 55 return _old_decode_field(self, field, value) |
| 50 protojson.ProtoJson.decode_field = _decode_field | 56 protojson.ProtoJson.decode_field = _decode_field |
| 51 | 57 |
| 52 | 58 |
| 53 def get_request_and_result(task_id): | 59 def get_request_and_result(task_id, viewing): |
| 54 """Provides the key and TaskRequest corresponding to a task ID. | 60 """Provides the key and TaskRequest corresponding to a task ID. |
| 55 | 61 |
| 56 Enforces the ACL for users. Allows bots all access for the moment. | 62 Enforces the ACL for users. Allows bots all access for the moment. |
| 57 | 63 |
| 58 Returns: | 64 Returns: |
| 59 tuple(TaskRequest, result): result can be either for a TaskRunResult or a | 65 tuple(TaskRequest, result): result can be either for a TaskRunResult or a |
| 60 TaskResultSummay. | 66 TaskResultSummay. |
| 61 """ | 67 """ |
| 62 try: | 68 try: |
| 63 request_key, result_key = task_pack.get_request_and_result_keys(task_id) | 69 request_key, result_key = task_pack.get_request_and_result_keys(task_id) |
| 64 request, result = ndb.get_multi((request_key, result_key)) | 70 request, result = ndb.get_multi((request_key, result_key)) |
| 65 if not request or not result: | |
| 66 raise endpoints.NotFoundException('%s not found.' % task_id) | |
| 67 if not acl.is_bot() and not request.has_access: | |
| 68 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) | |
| 69 return request, result | |
| 70 except ValueError: | 71 except ValueError: |
| 71 raise endpoints.BadRequestException('%s is an invalid key.' % task_id) | 72 raise endpoints.BadRequestException('%s is an invalid key.' % task_id) |
| 73 if not request or not result: |
| 74 raise endpoints.NotFoundException('%s not found.' % task_id) |
| 75 if viewing == _VIEW: |
| 76 if not acl.can_view_task(request): |
| 77 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) |
| 78 elif viewing == _EDIT: |
| 79 if not acl.can_edit_task(request): |
| 80 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) |
| 81 else: |
| 82 raise endpoints.InternalServerErrorException('get_request_and_result()') |
| 83 return request, result |
| 72 | 84 |
| 73 | 85 |
| 74 def get_or_raise(key): | 86 def get_or_raise(key): |
| 75 """Returns an entity or raises an endpoints exception if it does not exist.""" | 87 """Returns an entity or raises an endpoints exception if it does not exist.""" |
| 76 result = key.get() | 88 result = key.get() |
| 77 if not result: | 89 if not result: |
| 78 raise endpoints.NotFoundException('%s not found.' % key.id()) | 90 raise endpoints.NotFoundException('%s not found.' % key.id()) |
| 79 return result | 91 return result |
| 80 | 92 |
| 81 | 93 |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 121 message_types.VoidMessage, | 133 message_types.VoidMessage, |
| 122 version=messages.IntegerField(1)) | 134 version=messages.IntegerField(1)) |
| 123 | 135 |
| 124 | 136 |
| 125 @swarming_api.api_class(resource_name='server', path='server') | 137 @swarming_api.api_class(resource_name='server', path='server') |
| 126 class SwarmingServerService(remote.Service): | 138 class SwarmingServerService(remote.Service): |
| 127 @gae_ts_mon.instrument_endpoint() | 139 @gae_ts_mon.instrument_endpoint() |
| 128 @auth.endpoints_method( | 140 @auth.endpoints_method( |
| 129 message_types.VoidMessage, swarming_rpcs.ServerDetails, | 141 message_types.VoidMessage, swarming_rpcs.ServerDetails, |
| 130 http_method='GET') | 142 http_method='GET') |
| 131 @auth.require(acl.is_bot_or_user) | 143 @auth.require(acl.can_access) |
| 132 def details(self, _request): | 144 def details(self, _request): |
| 133 """Returns information about the server.""" | 145 """Returns information about the server.""" |
| 134 host = 'https://' + os.environ['HTTP_HOST'] | 146 host = 'https://' + os.environ['HTTP_HOST'] |
| 135 | 147 |
| 136 cfg = config.settings() | 148 cfg = config.settings() |
| 137 | 149 |
| 138 mpp = '' | 150 mpp = '' |
| 139 if cfg.mp and cfg.mp.server: | 151 if cfg.mp and cfg.mp.server: |
| 140 mpp = cfg.mp.server | 152 mpp = cfg.mp.server |
| 141 # as a fallback, try pulling from datastore | 153 # as a fallback, try pulling from datastore |
| 142 if not mpp: | 154 if not mpp: |
| 143 mpp = machine_provider.MachineProviderConfiguration.get_instance_url() | 155 mpp = machine_provider.MachineProviderConfiguration.get_instance_url() |
| 144 if mpp: | 156 if mpp: |
| 145 mpp = mpp + '/leases/%s' | 157 mpp = mpp + '/leases/%s' |
| 146 | 158 |
| 147 return swarming_rpcs.ServerDetails( | 159 return swarming_rpcs.ServerDetails( |
| 148 bot_version=bot_code.get_bot_version(host)[0], | 160 bot_version=bot_code.get_bot_version(host)[0], |
| 149 server_version=utils.get_app_version(), | 161 server_version=utils.get_app_version(), |
| 150 machine_provider_template=mpp, | 162 machine_provider_template=mpp, |
| 151 display_server_url_template=cfg.display_server_url_template, | 163 display_server_url_template=cfg.display_server_url_template, |
| 152 luci_config=config.config.config_service_hostname(), | 164 luci_config=config.config.config_service_hostname(), |
| 153 default_isolate_server=cfg.isolate.default_server, | 165 default_isolate_server=cfg.isolate.default_server, |
| 154 default_isolate_namespace=cfg.isolate.default_namespace) | 166 default_isolate_namespace=cfg.isolate.default_namespace) |
| 155 | 167 |
| 156 @gae_ts_mon.instrument_endpoint() | 168 @gae_ts_mon.instrument_endpoint() |
| 157 @auth.endpoints_method( | 169 @auth.endpoints_method( |
| 158 message_types.VoidMessage, swarming_rpcs.BootstrapToken) | 170 message_types.VoidMessage, swarming_rpcs.BootstrapToken) |
| 159 @auth.require(acl.is_bootstrapper) | 171 @auth.require(acl.can_create_bot) |
| 160 def token(self, _request): | 172 def token(self, _request): |
| 161 """Returns a token to bootstrap a new bot.""" | 173 """Returns a token to bootstrap a new bot.""" |
| 162 return swarming_rpcs.BootstrapToken( | 174 return swarming_rpcs.BootstrapToken( |
| 163 bootstrap_token = bot_code.generate_bootstrap_token(), | 175 bootstrap_token = bot_code.generate_bootstrap_token(), |
| 164 ) | 176 ) |
| 165 | 177 |
| 166 @gae_ts_mon.instrument_endpoint() | 178 @gae_ts_mon.instrument_endpoint() |
| 167 @auth.endpoints_method( | 179 @auth.endpoints_method( |
| 168 message_types.VoidMessage, swarming_rpcs.ClientPermissions, | 180 message_types.VoidMessage, swarming_rpcs.ClientPermissions, |
| 169 http_method='GET') | 181 http_method='GET') |
| 170 @auth.public | 182 @auth.public |
| 171 def permissions(self, _request): | 183 def permissions(self, _request): |
| 172 """Returns the caller's permissions.""" | 184 """Returns the caller's permissions.""" |
| 173 return swarming_rpcs.ClientPermissions( | 185 return swarming_rpcs.ClientPermissions( |
| 174 delete_bot = acl.is_admin(), | 186 delete_bot=acl.can_delete_bot(), |
| 175 terminate_bot = acl.is_privileged_user(), | 187 terminate_bot=acl.can_edit_bot(), |
| 176 get_configs = acl.is_user(), | 188 get_configs=acl.can_view_config(), |
| 177 put_configs = acl.is_admin(), | 189 put_configs=acl.can_edit_config(), |
| 178 cancel_task = acl.is_user(), | 190 cancel_task=acl._is_user() or acl.is_ip_whitelisted_machine(), |
| 179 cancel_tasks = acl.is_admin(), | 191 cancel_tasks=acl.can_edit_all_tasks(), |
| 180 get_bootstrap_token = acl.is_bootstrapper(), | 192 get_bootstrap_token=acl.can_create_bot()) |
| 181 ) | |
| 182 | 193 |
| 183 @gae_ts_mon.instrument_endpoint() | 194 @gae_ts_mon.instrument_endpoint() |
| 184 @auth.endpoints_method( | 195 @auth.endpoints_method( |
| 185 VersionRequest, swarming_rpcs.FileContent, | 196 VersionRequest, swarming_rpcs.FileContent, |
| 186 http_method='GET') | 197 http_method='GET') |
| 187 @auth.require(acl.is_bot_or_user) | 198 @auth.require(acl.can_view_config) |
| 188 def get_bootstrap(self, request): | 199 def get_bootstrap(self, request): |
| 189 """Retrieves the current or a previous version of bootstrap.py. | 200 """Retrieves the current or a previous version of bootstrap.py. |
| 190 | 201 |
| 191 When the file is sourced via luci-config, the version parameter is ignored. | 202 When the file is sourced via luci-config, the version parameter is ignored. |
| 192 Eventually the support for 'version' will be removed completely. | 203 Eventually the support for 'version' will be removed completely. |
| 193 """ | 204 """ |
| 194 obj = bot_code.get_bootstrap('', '', request.version) | 205 obj = bot_code.get_bootstrap('', '', request.version) |
| 195 if not obj: | 206 if not obj: |
| 196 return swarming_rpcs.FileContent() | 207 return swarming_rpcs.FileContent() |
| 197 return swarming_rpcs.FileContent( | 208 return swarming_rpcs.FileContent( |
| 198 content=obj.content.decode('utf-8'), | 209 content=obj.content.decode('utf-8'), |
| 199 who=obj.who, | 210 who=obj.who, |
| 200 when=obj.when, | 211 when=obj.when, |
| 201 version=obj.version) | 212 version=obj.version) |
| 202 | 213 |
| 203 @gae_ts_mon.instrument_endpoint() | 214 @gae_ts_mon.instrument_endpoint() |
| 204 @auth.endpoints_method( | 215 @auth.endpoints_method( |
| 205 VersionRequest, swarming_rpcs.FileContent, | 216 VersionRequest, swarming_rpcs.FileContent, |
| 206 http_method='GET') | 217 http_method='GET') |
| 207 @auth.require(acl.is_bot_or_user) | 218 @auth.require(acl.can_view_config) |
| 208 def get_bot_config(self, request): | 219 def get_bot_config(self, request): |
| 209 """Retrieves the current or a previous version of bot_config.py. | 220 """Retrieves the current or a previous version of bot_config.py. |
| 210 | 221 |
| 211 When the file is sourced via luci-config, the version parameter is ignored. | 222 When the file is sourced via luci-config, the version parameter is ignored. |
| 212 Eventually the support for 'version' will be removed completely. | 223 Eventually the support for 'version' will be removed completely. |
| 213 """ | 224 """ |
| 214 obj = bot_code.get_bot_config(request.version) | 225 obj = bot_code.get_bot_config(request.version) |
| 215 if not obj: | 226 if not obj: |
| 216 return swarming_rpcs.FileContent() | 227 return swarming_rpcs.FileContent() |
| 217 return swarming_rpcs.FileContent( | 228 return swarming_rpcs.FileContent( |
| 218 content=obj.content.decode('utf-8'), | 229 content=obj.content.decode('utf-8'), |
| 219 who=obj.who, | 230 who=obj.who, |
| 220 when=obj.when, | 231 when=obj.when, |
| 221 version=obj.version) | 232 version=obj.version) |
| 222 | 233 |
| 223 @gae_ts_mon.instrument_endpoint() | 234 @gae_ts_mon.instrument_endpoint() |
| 224 @auth.endpoints_method( | 235 @auth.endpoints_method( |
| 225 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent) | 236 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent) |
| 226 @auth.require(acl.is_admin) | 237 @auth.require(acl.can_edit_config) |
| 227 def put_bootstrap(self, request): | 238 def put_bootstrap(self, request): |
| 228 """Stores a new version of bootstrap.py. | 239 """Stores a new version of bootstrap.py. |
| 229 | 240 |
| 230 Warning: if a file exists in luci-config, the file stored by this function | 241 Warning: if a file exists in luci-config, the file stored by this function |
| 231 is ignored. Uploads are not blocked in case the file is later deleted from | 242 is ignored. Uploads are not blocked in case the file is later deleted from |
| 232 luci-config. | 243 luci-config. |
| 233 """ | 244 """ |
| 234 key = bot_code.store_bootstrap(request.content.encode('utf-8')) | 245 key = bot_code.store_bootstrap(request.content.encode('utf-8')) |
| 235 obj = key.get() | 246 obj = key.get() |
| 236 return swarming_rpcs.FileContent( | 247 return swarming_rpcs.FileContent( |
| 237 who=obj.who.to_bytes() if obj.who else None, | 248 who=obj.who.to_bytes() if obj.who else None, |
| 238 when=obj.created_ts, | 249 when=obj.created_ts, |
| 239 version=str(obj.version)) | 250 version=str(obj.version)) |
| 240 | 251 |
| 241 @gae_ts_mon.instrument_endpoint() | 252 @gae_ts_mon.instrument_endpoint() |
| 242 @auth.endpoints_method( | 253 @auth.endpoints_method( |
| 243 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent) | 254 swarming_rpcs.FileContentRequest, swarming_rpcs.FileContent) |
| 244 @auth.require(acl.is_admin) | 255 @auth.require(acl.can_edit_config) |
| 245 def put_bot_config(self, request): | 256 def put_bot_config(self, request): |
| 246 """Stores a new version of bot_config.py. | 257 """Stores a new version of bot_config.py. |
| 247 | 258 |
| 248 Warning: if a file exists in luci-config, the file stored by this function | 259 Warning: if a file exists in luci-config, the file stored by this function |
| 249 is ignored. Uploads are not blocked in case the file is later deleted from | 260 is ignored. Uploads are not blocked in case the file is later deleted from |
| 250 luci-config. | 261 luci-config. |
| 251 """ | 262 """ |
| 252 host = 'https://' + os.environ['HTTP_HOST'] | 263 host = 'https://' + os.environ['HTTP_HOST'] |
| 253 key = bot_code.store_bot_config(host, request.content.encode('utf-8')) | 264 key = bot_code.store_bot_config(host, request.content.encode('utf-8')) |
| 254 obj = key.get() | 265 obj = key.get() |
| (...skipping 16 matching lines...) Expand all Loading... |
| 271 | 282 |
| 272 @swarming_api.api_class(resource_name='task', path='task') | 283 @swarming_api.api_class(resource_name='task', path='task') |
| 273 class SwarmingTaskService(remote.Service): | 284 class SwarmingTaskService(remote.Service): |
| 274 """Swarming's task-related API.""" | 285 """Swarming's task-related API.""" |
| 275 @gae_ts_mon.instrument_endpoint() | 286 @gae_ts_mon.instrument_endpoint() |
| 276 @auth.endpoints_method( | 287 @auth.endpoints_method( |
| 277 TaskIdWithPerf, swarming_rpcs.TaskResult, | 288 TaskIdWithPerf, swarming_rpcs.TaskResult, |
| 278 name='result', | 289 name='result', |
| 279 path='{task_id}/result', | 290 path='{task_id}/result', |
| 280 http_method='GET') | 291 http_method='GET') |
| 281 @auth.require(acl.is_bot_or_user) | 292 @auth.require(acl.can_access) |
| 282 def result(self, request): | 293 def result(self, request): |
| 283 """Reports the result of the task corresponding to a task ID. | 294 """Reports the result of the task corresponding to a task ID. |
| 284 | 295 |
| 285 It can be a 'run' ID specifying a specific retry or a 'summary' ID hidding | 296 It can be a 'run' ID specifying a specific retry or a 'summary' ID hidding |
| 286 the fact that a task may have been retried transparently, when a bot reports | 297 the fact that a task may have been retried transparently, when a bot reports |
| 287 BOT_DIED. | 298 BOT_DIED. |
| 288 | 299 |
| 289 A summary ID ends with '0', a run ID ends with '1' or '2'. | 300 A summary ID ends with '0', a run ID ends with '1' or '2'. |
| 290 """ | 301 """ |
| 291 logging.debug('%s', request) | 302 logging.debug('%s', request) |
| 292 _, result = get_request_and_result(request.task_id) | 303 _, result = get_request_and_result(request.task_id, _VIEW) |
| 293 return message_conversion.task_result_to_rpc( | 304 return message_conversion.task_result_to_rpc( |
| 294 result, request.include_performance_stats) | 305 result, request.include_performance_stats) |
| 295 | 306 |
| 296 @gae_ts_mon.instrument_endpoint() | 307 @gae_ts_mon.instrument_endpoint() |
| 297 @auth.endpoints_method( | 308 @auth.endpoints_method( |
| 298 TaskId, swarming_rpcs.TaskRequest, | 309 TaskId, swarming_rpcs.TaskRequest, |
| 299 name='request', | 310 name='request', |
| 300 path='{task_id}/request', | 311 path='{task_id}/request', |
| 301 http_method='GET') | 312 http_method='GET') |
| 302 @auth.require(acl.is_bot_or_user) | 313 @auth.require(acl.can_access) |
| 303 def request(self, request): | 314 def request(self, request): |
| 304 """Returns the task request corresponding to a task ID.""" | 315 """Returns the task request corresponding to a task ID.""" |
| 305 logging.debug('%s', request) | 316 logging.debug('%s', request) |
| 306 request_obj, _ = get_request_and_result(request.task_id) | 317 request_obj, _ = get_request_and_result(request.task_id, _VIEW) |
| 307 return message_conversion.task_request_to_rpc(request_obj) | 318 return message_conversion.task_request_to_rpc(request_obj) |
| 308 | 319 |
| 309 @gae_ts_mon.instrument_endpoint() | 320 @gae_ts_mon.instrument_endpoint() |
| 310 @auth.endpoints_method( | 321 @auth.endpoints_method( |
| 311 TaskId, swarming_rpcs.CancelResponse, | 322 TaskId, swarming_rpcs.CancelResponse, |
| 312 name='cancel', | 323 name='cancel', |
| 313 path='{task_id}/cancel') | 324 path='{task_id}/cancel') |
| 314 @auth.require(acl.is_bot_or_user) | 325 @auth.require(acl.can_access) |
| 315 def cancel(self, request): | 326 def cancel(self, request): |
| 316 """Cancels a task. | 327 """Cancels a task. |
| 317 | 328 |
| 318 If a bot was running the task, the bot will forcibly cancel the task. | 329 If a bot was running the task, the bot will forcibly cancel the task. |
| 319 """ | 330 """ |
| 320 logging.debug('%s', request) | 331 logging.debug('%s', request) |
| 321 request_obj, result = get_request_and_result(request.task_id) | 332 request_obj, result = get_request_and_result(request.task_id, _EDIT) |
| 322 ok, was_running = task_scheduler.cancel_task(request_obj, result.key) | 333 ok, was_running = task_scheduler.cancel_task(request_obj, result.key) |
| 323 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running) | 334 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running) |
| 324 | 335 |
| 325 @gae_ts_mon.instrument_endpoint() | 336 @gae_ts_mon.instrument_endpoint() |
| 326 @auth.endpoints_method( | 337 @auth.endpoints_method( |
| 327 TaskId, swarming_rpcs.TaskOutput, | 338 TaskId, swarming_rpcs.TaskOutput, |
| 328 name='stdout', | 339 name='stdout', |
| 329 path='{task_id}/stdout', | 340 path='{task_id}/stdout', |
| 330 http_method='GET') | 341 http_method='GET') |
| 331 @auth.require(acl.is_bot_or_user) | 342 @auth.require(acl.can_access) |
| 332 def stdout(self, request): | 343 def stdout(self, request): |
| 333 """Returns the output of the task corresponding to a task ID.""" | 344 """Returns the output of the task corresponding to a task ID.""" |
| 334 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine | 345 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine |
| 335 # v1. | 346 # v1. |
| 336 # TODO(maruel): Send as raw content instead of encoded. This is not | 347 # TODO(maruel): Send as raw content instead of encoded. This is not |
| 337 # supported by cloud endpoints. | 348 # supported by cloud endpoints. |
| 338 logging.debug('%s', request) | 349 logging.debug('%s', request) |
| 339 _, result = get_request_and_result(request.task_id) | 350 _, result = get_request_and_result(request.task_id, _VIEW) |
| 340 output = result.get_output() | 351 output = result.get_output() |
| 341 if output: | 352 if output: |
| 342 output = output.decode('utf-8', 'replace') | 353 output = output.decode('utf-8', 'replace') |
| 343 return swarming_rpcs.TaskOutput(output=output) | 354 return swarming_rpcs.TaskOutput(output=output) |
| 344 | 355 |
| 345 | 356 |
| 346 @swarming_api.api_class(resource_name='tasks', path='tasks') | 357 @swarming_api.api_class(resource_name='tasks', path='tasks') |
| 347 class SwarmingTasksService(remote.Service): | 358 class SwarmingTasksService(remote.Service): |
| 348 """Swarming's tasks-related API.""" | 359 """Swarming's tasks-related API.""" |
| 349 @gae_ts_mon.instrument_endpoint() | 360 @gae_ts_mon.instrument_endpoint() |
| 350 @auth.endpoints_method( | 361 @auth.endpoints_method( |
| 351 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata) | 362 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata) |
| 352 @auth.require(acl.is_bot_or_user) | 363 @auth.require(acl.can_create_task) |
| 353 def new(self, request): | 364 def new(self, request): |
| 354 """Creates a new task. | 365 """Creates a new task. |
| 355 | 366 |
| 356 The task will be enqueued in the tasks list and will be executed at the | 367 The task will be enqueued in the tasks list and will be executed at the |
| 357 earliest opportunity by a bot that has at least the dimensions as described | 368 earliest opportunity by a bot that has at least the dimensions as described |
| 358 in the task request. | 369 in the task request. |
| 359 """ | 370 """ |
| 360 sb = (request.properties.secret_bytes | 371 sb = (request.properties.secret_bytes |
| 361 if request.properties is not None else None) | 372 if request.properties is not None else None) |
| 362 if sb is not None: | 373 if sb is not None: |
| (...skipping 20 matching lines...) Expand all Loading... |
| 383 | 394 |
| 384 return swarming_rpcs.TaskRequestMetadata( | 395 return swarming_rpcs.TaskRequestMetadata( |
| 385 request=message_conversion.task_request_to_rpc(request), | 396 request=message_conversion.task_request_to_rpc(request), |
| 386 task_id=task_pack.pack_result_summary_key(result_summary.key), | 397 task_id=task_pack.pack_result_summary_key(result_summary.key), |
| 387 task_result=previous_result) | 398 task_result=previous_result) |
| 388 | 399 |
| 389 @gae_ts_mon.instrument_endpoint() | 400 @gae_ts_mon.instrument_endpoint() |
| 390 @auth.endpoints_method( | 401 @auth.endpoints_method( |
| 391 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList, | 402 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList, |
| 392 http_method='GET') | 403 http_method='GET') |
| 393 @auth.require(acl.is_privileged_user) | 404 @auth.require(acl.can_view_all_tasks) |
| 394 def list(self, request): | 405 def list(self, request): |
| 395 """Returns tasks results based on the filters. | 406 """Returns tasks results based on the filters. |
| 396 | 407 |
| 397 This endpoint is significantly slower than 'count'. Use 'count' when | 408 This endpoint is significantly slower than 'count'. Use 'count' when |
| 398 possible. | 409 possible. |
| 399 """ | 410 """ |
| 400 # TODO(maruel): Rename 'list' to 'results'. | 411 # TODO(maruel): Rename 'list' to 'results'. |
| 401 # TODO(maruel): Rename 'TaskList' to 'TaskResults'. | 412 # TODO(maruel): Rename 'TaskList' to 'TaskResults'. |
| 402 logging.debug('%s', request) | 413 logging.debug('%s', request) |
| 403 now = utils.utcnow() | 414 now = utils.utcnow() |
| (...skipping 17 matching lines...) Expand all Loading... |
| 421 message_conversion.task_result_to_rpc( | 432 message_conversion.task_result_to_rpc( |
| 422 i, request.include_performance_stats) | 433 i, request.include_performance_stats) |
| 423 for i in items | 434 for i in items |
| 424 ], | 435 ], |
| 425 now=now) | 436 now=now) |
| 426 | 437 |
| 427 @gae_ts_mon.instrument_endpoint() | 438 @gae_ts_mon.instrument_endpoint() |
| 428 @auth.endpoints_method( | 439 @auth.endpoints_method( |
| 429 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests, | 440 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests, |
| 430 http_method='GET') | 441 http_method='GET') |
| 431 @auth.require(acl.is_privileged_user) | 442 @auth.require(acl.can_view_all_tasks) |
| 432 def requests(self, request): | 443 def requests(self, request): |
| 433 """Returns tasks requests based on the filters. | 444 """Returns tasks requests based on the filters. |
| 434 | 445 |
| 435 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when | 446 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when |
| 436 possible. | 447 possible. |
| 437 """ | 448 """ |
| 438 logging.debug('%s', request) | 449 logging.debug('%s', request) |
| 439 if request.include_performance_stats: | 450 if request.include_performance_stats: |
| 440 raise endpoints.BadRequestException( | 451 raise endpoints.BadRequestException( |
| 441 'Can\'t set include_performance_stats for tasks/list') | 452 'Can\'t set include_performance_stats for tasks/list') |
| (...skipping 19 matching lines...) Expand all Loading... |
| 461 'This combination is unsupported, sorry.') | 472 'This combination is unsupported, sorry.') |
| 462 return swarming_rpcs.TaskRequests( | 473 return swarming_rpcs.TaskRequests( |
| 463 cursor=cursor, | 474 cursor=cursor, |
| 464 items=[message_conversion.task_request_to_rpc(i) for i in items], | 475 items=[message_conversion.task_request_to_rpc(i) for i in items], |
| 465 now=now) | 476 now=now) |
| 466 | 477 |
| 467 @gae_ts_mon.instrument_endpoint() | 478 @gae_ts_mon.instrument_endpoint() |
| 468 @auth.endpoints_method( | 479 @auth.endpoints_method( |
| 469 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse, | 480 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse, |
| 470 http_method='POST') | 481 http_method='POST') |
| 471 @auth.require(acl.is_admin) | 482 @auth.require(acl.can_edit_all_tasks) |
| 472 def cancel(self, request): | 483 def cancel(self, request): |
| 473 """Cancel a subset of pending tasks based on the tags. | 484 """Cancel a subset of pending tasks based on the tags. |
| 474 | 485 |
| 475 Cancellation happens asynchronously, so when this call returns, | 486 Cancellation happens asynchronously, so when this call returns, |
| 476 cancellations will not have completed yet. | 487 cancellations will not have completed yet. |
| 477 """ | 488 """ |
| 478 logging.debug('%s', request) | 489 logging.debug('%s', request) |
| 479 if not request.tags: | 490 if not request.tags: |
| 480 # Prevent accidental cancellation of everything. | 491 # Prevent accidental cancellation of everything. |
| 481 raise endpoints.BadRequestException( | 492 raise endpoints.BadRequestException( |
| (...skipping 20 matching lines...) Expand all Loading... |
| 502 | 513 |
| 503 return swarming_rpcs.TasksCancelResponse( | 514 return swarming_rpcs.TasksCancelResponse( |
| 504 cursor=cursor, | 515 cursor=cursor, |
| 505 matched=len(tasks), | 516 matched=len(tasks), |
| 506 now=now) | 517 now=now) |
| 507 | 518 |
| 508 @gae_ts_mon.instrument_endpoint() | 519 @gae_ts_mon.instrument_endpoint() |
| 509 @auth.endpoints_method( | 520 @auth.endpoints_method( |
| 510 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount, | 521 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount, |
| 511 http_method='GET') | 522 http_method='GET') |
| 512 @auth.require(acl.is_privileged_user) | 523 @auth.require(acl.can_view_all_tasks) |
| 513 def count(self, request): | 524 def count(self, request): |
| 514 """Counts number of tasks in a given state.""" | 525 """Counts number of tasks in a given state.""" |
| 515 logging.debug('%s', request) | 526 logging.debug('%s', request) |
| 516 if not request.start: | 527 if not request.start: |
| 517 raise endpoints.BadRequestException('start (as epoch) is required') | 528 raise endpoints.BadRequestException('start (as epoch) is required') |
| 518 now = utils.utcnow() | 529 now = utils.utcnow() |
| 519 mem_key = self._memcache_key(request, now) | 530 mem_key = self._memcache_key(request, now) |
| 520 count = memcache.get(mem_key, namespace='tasks_count') | 531 count = memcache.get(mem_key, namespace='tasks_count') |
| 521 if count is not None: | 532 if count is not None: |
| 522 return swarming_rpcs.TasksCount(count=count, now=now) | 533 return swarming_rpcs.TasksCount(count=count, now=now) |
| (...skipping 19 matching lines...) Expand all Loading... |
| 542 return task_result.get_result_summaries_query( | 553 return task_result.get_result_summaries_query( |
| 543 start, end, | 554 start, end, |
| 544 sort or request.sort.name.lower(), | 555 sort or request.sort.name.lower(), |
| 545 request.state.name.lower(), | 556 request.state.name.lower(), |
| 546 request.tags) | 557 request.tags) |
| 547 | 558 |
| 548 @gae_ts_mon.instrument_endpoint() | 559 @gae_ts_mon.instrument_endpoint() |
| 549 @auth.endpoints_method( | 560 @auth.endpoints_method( |
| 550 message_types.VoidMessage, swarming_rpcs.TasksTags, | 561 message_types.VoidMessage, swarming_rpcs.TasksTags, |
| 551 http_method='GET') | 562 http_method='GET') |
| 552 @auth.require(acl.is_privileged_user) | 563 @auth.require(acl.can_view_all_tasks) |
| 553 def tags(self, _request): | 564 def tags(self, _request): |
| 554 """Returns the cached set of tags currently seen in the fleet.""" | 565 """Returns the cached set of tags currently seen in the fleet.""" |
| 555 tags = task_result.TagAggregation.KEY.get() | 566 tags = task_result.TagAggregation.KEY.get() |
| 556 ft = [ | 567 ft = [ |
| 557 swarming_rpcs.StringListPair(key=t.tag, value=t.values) | 568 swarming_rpcs.StringListPair(key=t.tag, value=t.values) |
| 558 for t in tags.tags | 569 for t in tags.tags |
| 559 ] | 570 ] |
| 560 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts) | 571 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts) |
| 561 | 572 |
| 562 | 573 |
| (...skipping 14 matching lines...) Expand all Loading... |
| 577 | 588 |
| 578 @swarming_api.api_class(resource_name='bot', path='bot') | 589 @swarming_api.api_class(resource_name='bot', path='bot') |
| 579 class SwarmingBotService(remote.Service): | 590 class SwarmingBotService(remote.Service): |
| 580 """Bot-related API. Permits querying information about the bot's properties""" | 591 """Bot-related API. Permits querying information about the bot's properties""" |
| 581 @gae_ts_mon.instrument_endpoint() | 592 @gae_ts_mon.instrument_endpoint() |
| 582 @auth.endpoints_method( | 593 @auth.endpoints_method( |
| 583 BotId, swarming_rpcs.BotInfo, | 594 BotId, swarming_rpcs.BotInfo, |
| 584 name='get', | 595 name='get', |
| 585 path='{bot_id}/get', | 596 path='{bot_id}/get', |
| 586 http_method='GET') | 597 http_method='GET') |
| 587 @auth.require(acl.is_privileged_user) | 598 @auth.require(acl.can_view_bot) |
| 588 def get(self, request): | 599 def get(self, request): |
| 589 """Returns information about a known bot. | 600 """Returns information about a known bot. |
| 590 | 601 |
| 591 This includes its state and dimensions, and if it is currently running a | 602 This includes its state and dimensions, and if it is currently running a |
| 592 task. | 603 task. |
| 593 """ | 604 """ |
| 594 logging.debug('%s', request) | 605 logging.debug('%s', request) |
| 595 bot_id = request.bot_id | 606 bot_id = request.bot_id |
| 596 bot = bot_management.get_info_key(bot_id).get() | 607 bot = bot_management.get_info_key(bot_id).get() |
| 597 deleted = False | 608 deleted = False |
| (...skipping 22 matching lines...) Expand all Loading... |
| 620 deleted = True | 631 deleted = True |
| 621 | 632 |
| 622 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(), | 633 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(), |
| 623 deleted=deleted) | 634 deleted=deleted) |
| 624 | 635 |
| 625 @gae_ts_mon.instrument_endpoint() | 636 @gae_ts_mon.instrument_endpoint() |
| 626 @auth.endpoints_method( | 637 @auth.endpoints_method( |
| 627 BotId, swarming_rpcs.DeletedResponse, | 638 BotId, swarming_rpcs.DeletedResponse, |
| 628 name='delete', | 639 name='delete', |
| 629 path='{bot_id}/delete') | 640 path='{bot_id}/delete') |
| 630 @auth.require(acl.is_admin) | 641 @auth.require(acl.can_delete_bot) |
| 631 def delete(self, request): | 642 def delete(self, request): |
| 632 """Deletes the bot corresponding to a provided bot_id. | 643 """Deletes the bot corresponding to a provided bot_id. |
| 633 | 644 |
| 634 At that point, the bot will not appears in the list of bots but it is still | 645 At that point, the bot will not appears in the list of bots but it is still |
| 635 possible to get information about the bot with its bot id is known, as | 646 possible to get information about the bot with its bot id is known, as |
| 636 historical data is not deleted. | 647 historical data is not deleted. |
| 637 | 648 |
| 638 It is meant to remove from the DB the presence of a bot that was retired, | 649 It is meant to remove from the DB the presence of a bot that was retired, |
| 639 e.g. the VM was shut down already. Use 'terminate' instead of the bot is | 650 e.g. the VM was shut down already. Use 'terminate' instead of the bot is |
| 640 still alive. | 651 still alive. |
| 641 """ | 652 """ |
| 642 logging.debug('%s', request) | 653 logging.debug('%s', request) |
| 643 bot_key = bot_management.get_info_key(request.bot_id) | 654 bot_key = bot_management.get_info_key(request.bot_id) |
| 644 get_or_raise(bot_key) # raises 404 if there is no such bot | 655 get_or_raise(bot_key) # raises 404 if there is no such bot |
| 645 # TODO(maruel): If the bot was a MP, call lease_management.cleanup_bot()? | 656 # TODO(maruel): If the bot was a MP, call lease_management.cleanup_bot()? |
| 646 task_queues.cleanup_after_bot(request.bot_id) | 657 task_queues.cleanup_after_bot(request.bot_id) |
| 647 bot_key.delete() | 658 bot_key.delete() |
| 648 return swarming_rpcs.DeletedResponse(deleted=True) | 659 return swarming_rpcs.DeletedResponse(deleted=True) |
| 649 | 660 |
| 650 @gae_ts_mon.instrument_endpoint() | 661 @gae_ts_mon.instrument_endpoint() |
| 651 @auth.endpoints_method( | 662 @auth.endpoints_method( |
| 652 BotEventsRequest, swarming_rpcs.BotEvents, | 663 BotEventsRequest, swarming_rpcs.BotEvents, |
| 653 name='events', | 664 name='events', |
| 654 path='{bot_id}/events', | 665 path='{bot_id}/events', |
| 655 http_method='GET') | 666 http_method='GET') |
| 656 @auth.require(acl.is_privileged_user) | 667 @auth.require(acl.can_view_bot) |
| 657 def events(self, request): | 668 def events(self, request): |
| 658 """Returns events that happened on a bot.""" | 669 """Returns events that happened on a bot.""" |
| 659 logging.debug('%s', request) | 670 logging.debug('%s', request) |
| 660 try: | 671 try: |
| 661 now = utils.utcnow() | 672 now = utils.utcnow() |
| 662 start = message_conversion.epoch_to_datetime(request.start) | 673 start = message_conversion.epoch_to_datetime(request.start) |
| 663 end = message_conversion.epoch_to_datetime(request.end) | 674 end = message_conversion.epoch_to_datetime(request.end) |
| 664 order = not (start or end) | 675 order = not (start or end) |
| 665 query = bot_management.get_events_query(request.bot_id, order) | 676 query = bot_management.get_events_query(request.bot_id, order) |
| 666 if not order: | 677 if not order: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 678 return swarming_rpcs.BotEvents( | 689 return swarming_rpcs.BotEvents( |
| 679 cursor=cursor, | 690 cursor=cursor, |
| 680 items=[message_conversion.bot_event_to_rpc(r) for r in items], | 691 items=[message_conversion.bot_event_to_rpc(r) for r in items], |
| 681 now=now) | 692 now=now) |
| 682 | 693 |
| 683 @gae_ts_mon.instrument_endpoint() | 694 @gae_ts_mon.instrument_endpoint() |
| 684 @auth.endpoints_method( | 695 @auth.endpoints_method( |
| 685 BotId, swarming_rpcs.TerminateResponse, | 696 BotId, swarming_rpcs.TerminateResponse, |
| 686 name='terminate', | 697 name='terminate', |
| 687 path='{bot_id}/terminate') | 698 path='{bot_id}/terminate') |
| 688 @auth.require(acl.is_bot_or_privileged_user) | 699 @auth.require(acl.can_edit_bot) |
| 689 def terminate(self, request): | 700 def terminate(self, request): |
| 690 """Asks a bot to terminate itself gracefully. | 701 """Asks a bot to terminate itself gracefully. |
| 691 | 702 |
| 692 The bot will stay in the DB, use 'delete' to remove it from the DB | 703 The bot will stay in the DB, use 'delete' to remove it from the DB |
| 693 afterward. This request returns a pseudo-taskid that can be waited for to | 704 afterward. This request returns a pseudo-taskid that can be waited for to |
| 694 wait for the bot to turn down. | 705 wait for the bot to turn down. |
| 695 | 706 |
| 696 This command is particularly useful when a privileged user needs to safely | 707 This command is particularly useful when a privileged user needs to safely |
| 697 debug a machine specific issue. The user can trigger a terminate for one of | 708 debug a machine specific issue. The user can trigger a terminate for one of |
| 698 the bot exhibiting the issue, wait for the pseudo-task to run then access | 709 the bot exhibiting the issue, wait for the pseudo-task to run then access |
| (...skipping 16 matching lines...) Expand all Loading... |
| 715 result_summary = task_scheduler.schedule_request(request, None) | 726 result_summary = task_scheduler.schedule_request(request, None) |
| 716 return swarming_rpcs.TerminateResponse( | 727 return swarming_rpcs.TerminateResponse( |
| 717 task_id=task_pack.pack_result_summary_key(result_summary.key)) | 728 task_id=task_pack.pack_result_summary_key(result_summary.key)) |
| 718 | 729 |
| 719 @gae_ts_mon.instrument_endpoint() | 730 @gae_ts_mon.instrument_endpoint() |
| 720 @auth.endpoints_method( | 731 @auth.endpoints_method( |
| 721 BotTasksRequest, swarming_rpcs.BotTasks, | 732 BotTasksRequest, swarming_rpcs.BotTasks, |
| 722 name='tasks', | 733 name='tasks', |
| 723 path='{bot_id}/tasks', | 734 path='{bot_id}/tasks', |
| 724 http_method='GET') | 735 http_method='GET') |
| 725 @auth.require(acl.is_privileged_user) | 736 @auth.require(acl.can_view_all_tasks) |
| 726 def tasks(self, request): | 737 def tasks(self, request): |
| 727 """Lists a given bot's tasks within the specified date range. | 738 """Lists a given bot's tasks within the specified date range. |
| 728 | 739 |
| 729 In this case, the tasks are effectively TaskRunResult since it's individual | 740 In this case, the tasks are effectively TaskRunResult since it's individual |
| 730 task tries sent to this specific bot. | 741 task tries sent to this specific bot. |
| 731 | 742 |
| 732 It is impossible to search by both tags and bot id. If there's a need, | 743 It is impossible to search by both tags and bot id. If there's a need, |
| 733 TaskRunResult.tags will be added (via a copy from TaskRequest.tags). | 744 TaskRunResult.tags will be added (via a copy from TaskRequest.tags). |
| 734 """ | 745 """ |
| 735 logging.debug('%s', request) | 746 logging.debug('%s', request) |
| (...skipping 22 matching lines...) Expand all Loading... |
| 758 | 769 |
| 759 | 770 |
| 760 @swarming_api.api_class(resource_name='bots', path='bots') | 771 @swarming_api.api_class(resource_name='bots', path='bots') |
| 761 class SwarmingBotsService(remote.Service): | 772 class SwarmingBotsService(remote.Service): |
| 762 """Bots-related API.""" | 773 """Bots-related API.""" |
| 763 | 774 |
| 764 @gae_ts_mon.instrument_endpoint() | 775 @gae_ts_mon.instrument_endpoint() |
| 765 @auth.endpoints_method( | 776 @auth.endpoints_method( |
| 766 swarming_rpcs.BotsRequest, swarming_rpcs.BotList, | 777 swarming_rpcs.BotsRequest, swarming_rpcs.BotList, |
| 767 http_method='GET') | 778 http_method='GET') |
| 768 @auth.require(acl.is_privileged_user) | 779 @auth.require(acl.can_view_bot) |
| 769 def list(self, request): | 780 def list(self, request): |
| 770 """Provides list of known bots. | 781 """Provides list of known bots. |
| 771 | 782 |
| 772 Deleted bots will not be listed. | 783 Deleted bots will not be listed. |
| 773 """ | 784 """ |
| 774 logging.debug('%s', request) | 785 logging.debug('%s', request) |
| 775 now = utils.utcnow() | 786 now = utils.utcnow() |
| 776 q = bot_management.BotInfo.query() | 787 q = bot_management.BotInfo.query() |
| 777 try: | 788 try: |
| 778 q = bot_management.filter_dimensions(q, request.dimensions) | 789 q = bot_management.filter_dimensions(q, request.dimensions) |
| 779 q = bot_management.filter_availability( | 790 q = bot_management.filter_availability( |
| 780 q, swarming_rpcs.to_bool(request.quarantined), | 791 q, swarming_rpcs.to_bool(request.quarantined), |
| 781 swarming_rpcs.to_bool(request.is_dead), now, | 792 swarming_rpcs.to_bool(request.is_dead), now, |
| 782 swarming_rpcs.to_bool(request.is_busy), | 793 swarming_rpcs.to_bool(request.is_busy), |
| 783 swarming_rpcs.to_bool(request.is_mp)) | 794 swarming_rpcs.to_bool(request.is_mp)) |
| 784 except ValueError as e: | 795 except ValueError as e: |
| 785 raise endpoints.BadRequestException(str(e)) | 796 raise endpoints.BadRequestException(str(e)) |
| 786 | 797 |
| 787 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor) | 798 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor) |
| 788 return swarming_rpcs.BotList( | 799 return swarming_rpcs.BotList( |
| 789 cursor=cursor, | 800 cursor=cursor, |
| 790 death_timeout=config.settings().bot_death_timeout_secs, | 801 death_timeout=config.settings().bot_death_timeout_secs, |
| 791 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots], | 802 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots], |
| 792 now=now) | 803 now=now) |
| 793 | 804 |
| 794 @gae_ts_mon.instrument_endpoint() | 805 @gae_ts_mon.instrument_endpoint() |
| 795 @auth.endpoints_method( | 806 @auth.endpoints_method( |
| 796 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount, | 807 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount, |
| 797 http_method='GET') | 808 http_method='GET') |
| 798 @auth.require(acl.is_privileged_user) | 809 @auth.require(acl.can_view_bot) |
| 799 def count(self, request): | 810 def count(self, request): |
| 800 """Counts number of bots with given dimensions.""" | 811 """Counts number of bots with given dimensions.""" |
| 801 logging.debug('%s', request) | 812 logging.debug('%s', request) |
| 802 now = utils.utcnow() | 813 now = utils.utcnow() |
| 803 q = bot_management.BotInfo.query() | 814 q = bot_management.BotInfo.query() |
| 804 try: | 815 try: |
| 805 q = bot_management.filter_dimensions(q, request.dimensions) | 816 q = bot_management.filter_dimensions(q, request.dimensions) |
| 806 except ValueError as e: | 817 except ValueError as e: |
| 807 raise endpoints.BadRequestException(str(e)) | 818 raise endpoints.BadRequestException(str(e)) |
| 808 | 819 |
| 809 f_count = q.count_async() | 820 f_count = q.count_async() |
| 810 f_dead = (bot_management.filter_availability(q, None, True, now, None, None) | 821 f_dead = (bot_management.filter_availability(q, None, True, now, None, None) |
| 811 .count_async()) | 822 .count_async()) |
| 812 f_quarantined = ( | 823 f_quarantined = ( |
| 813 bot_management.filter_availability(q, True, None, now, None, None) | 824 bot_management.filter_availability(q, True, None, now, None, None) |
| 814 .count_async()) | 825 .count_async()) |
| 815 f_busy = (bot_management.filter_availability(q, None, None, now, True, None) | 826 f_busy = (bot_management.filter_availability(q, None, None, now, True, None) |
| 816 .count_async()) | 827 .count_async()) |
| 817 return swarming_rpcs.BotsCount( | 828 return swarming_rpcs.BotsCount( |
| 818 count=f_count.get_result(), | 829 count=f_count.get_result(), |
| 819 quarantined=f_quarantined.get_result(), | 830 quarantined=f_quarantined.get_result(), |
| 820 dead=f_dead.get_result(), | 831 dead=f_dead.get_result(), |
| 821 busy=f_busy.get_result(), | 832 busy=f_busy.get_result(), |
| 822 now=now) | 833 now=now) |
| 823 | 834 |
| 824 | |
| 825 @gae_ts_mon.instrument_endpoint() | 835 @gae_ts_mon.instrument_endpoint() |
| 826 @auth.endpoints_method( | 836 @auth.endpoints_method( |
| 827 message_types.VoidMessage, swarming_rpcs.BotsDimensions, | 837 message_types.VoidMessage, swarming_rpcs.BotsDimensions, |
| 828 http_method='GET') | 838 http_method='GET') |
| 829 @auth.require(acl.is_privileged_user) | 839 @auth.require(acl.can_view_bot) |
| 830 def dimensions(self, _request): | 840 def dimensions(self, _request): |
| 831 """Returns the cached set of dimensions currently in use in the fleet.""" | 841 """Returns the cached set of dimensions currently in use in the fleet.""" |
| 832 dims = bot_management.DimensionAggregation.KEY.get() | 842 dims = bot_management.DimensionAggregation.KEY.get() |
| 833 fd = [ | 843 fd = [ |
| 834 swarming_rpcs.StringListPair(key=d.dimension, value=d.values) | 844 swarming_rpcs.StringListPair(key=d.dimension, value=d.values) |
| 835 for d in dims.dimensions | 845 for d in dims.dimensions |
| 836 ] | 846 ] |
| 837 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts) | 847 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts) |
| 838 | 848 |
| 839 | 849 |
| 840 def get_routes(): | 850 def get_routes(): |
| 841 return ( | 851 return ( |
| 842 endpoints_webapp2.api_routes(SwarmingServerService) + | 852 endpoints_webapp2.api_routes(SwarmingServerService) + |
| 843 endpoints_webapp2.api_routes(SwarmingTaskService) + | 853 endpoints_webapp2.api_routes(SwarmingTaskService) + |
| 844 endpoints_webapp2.api_routes(SwarmingTasksService) + | 854 endpoints_webapp2.api_routes(SwarmingTasksService) + |
| 845 endpoints_webapp2.api_routes(SwarmingBotService) + | 855 endpoints_webapp2.api_routes(SwarmingBotService) + |
| 846 endpoints_webapp2.api_routes(SwarmingBotsService) + | 856 endpoints_webapp2.api_routes(SwarmingBotsService) + |
| 847 # components.config endpoints for validation and configuring of luci-config | 857 # components.config endpoints for validation and configuring of luci-config |
| 848 # service URL. | 858 # service URL. |
| 849 endpoints_webapp2.api_routes(config.ConfigApi)) | 859 endpoints_webapp2.api_routes(config.ConfigApi)) |
| OLD | NEW |