| 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 = False |
| 46 _VIEW = True |
| 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: | 71 if not request or not result: |
| 66 raise endpoints.NotFoundException('%s not found.' % task_id) | 72 raise endpoints.NotFoundException('%s not found.' % task_id) |
| 67 if not acl.is_bot() and not request.has_access: | 73 if viewing == _VIEW: |
| 68 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) | 74 if not acl.can_id_task_view(request.authenticated): |
| 75 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) |
| 76 else: |
| 77 if not acl.can_id_task_edit(request.authenticated): |
| 78 raise endpoints.ForbiddenException('%s is not accessible.' % task_id) |
| 69 return request, result | 79 return request, result |
| 70 except ValueError: | 80 except ValueError: |
| 71 raise endpoints.BadRequestException('%s is an invalid key.' % task_id) | 81 raise endpoints.BadRequestException('%s is an invalid key.' % task_id) |
| 72 | 82 |
| 73 | 83 |
| 74 def get_or_raise(key): | 84 def get_or_raise(key): |
| 75 """Returns an entity or raises an endpoints exception if it does not exist.""" | 85 """Returns an entity or raises an endpoints exception if it does not exist.""" |
| 76 result = key.get() | 86 result = key.get() |
| 77 if not result: | 87 if not result: |
| 78 raise endpoints.NotFoundException('%s not found.' % key.id()) | 88 raise endpoints.NotFoundException('%s not found.' % key.id()) |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 121 message_types.VoidMessage, | 131 message_types.VoidMessage, |
| 122 version=messages.IntegerField(1)) | 132 version=messages.IntegerField(1)) |
| 123 | 133 |
| 124 | 134 |
| 125 @swarming_api.api_class(resource_name='server', path='server') | 135 @swarming_api.api_class(resource_name='server', path='server') |
| 126 class SwarmingServerService(remote.Service): | 136 class SwarmingServerService(remote.Service): |
| 127 @gae_ts_mon.instrument_endpoint() | 137 @gae_ts_mon.instrument_endpoint() |
| 128 @auth.endpoints_method( | 138 @auth.endpoints_method( |
| 129 message_types.VoidMessage, swarming_rpcs.ServerDetails, | 139 message_types.VoidMessage, swarming_rpcs.ServerDetails, |
| 130 http_method='GET') | 140 http_method='GET') |
| 131 @auth.require(acl.is_bot_or_user) | 141 @auth.require(acl.can_access) |
| 132 def details(self, _request): | 142 def details(self, _request): |
| 133 """Returns information about the server.""" | 143 """Returns information about the server.""" |
| 134 host = 'https://' + os.environ['HTTP_HOST'] | 144 host = 'https://' + os.environ['HTTP_HOST'] |
| 135 | 145 |
| 136 cfg = config.settings() | 146 cfg = config.settings() |
| 137 | 147 |
| 138 mpp = '' | 148 mpp = '' |
| 139 if cfg.mp and cfg.mp.server: | 149 if cfg.mp and cfg.mp.server: |
| 140 mpp = cfg.mp.server | 150 mpp = cfg.mp.server |
| 141 # as a fallback, try pulling from datastore | 151 # as a fallback, try pulling from datastore |
| 142 if not mpp: | 152 if not mpp: |
| 143 mpp = machine_provider.MachineProviderConfiguration.get_instance_url() | 153 mpp = machine_provider.MachineProviderConfiguration.get_instance_url() |
| 144 if mpp: | 154 if mpp: |
| 145 mpp = mpp + '/leases/%s' | 155 mpp = mpp + '/leases/%s' |
| 146 | 156 |
| 147 return swarming_rpcs.ServerDetails( | 157 return swarming_rpcs.ServerDetails( |
| 148 bot_version=bot_code.get_bot_version(host)[0], | 158 bot_version=bot_code.get_bot_version(host)[0], |
| 149 server_version=utils.get_app_version(), | 159 server_version=utils.get_app_version(), |
| 150 machine_provider_template=mpp, | 160 machine_provider_template=mpp, |
| 151 display_server_url_template=cfg.display_server_url_template, | 161 display_server_url_template=cfg.display_server_url_template, |
| 152 luci_config=config.config.config_service_hostname(), | 162 luci_config=config.config.config_service_hostname(), |
| 153 default_isolate_server=cfg.isolate.default_server, | 163 default_isolate_server=cfg.isolate.default_server, |
| 154 default_isolate_namespace=cfg.isolate.default_namespace) | 164 default_isolate_namespace=cfg.isolate.default_namespace) |
| 155 | 165 |
| 156 @gae_ts_mon.instrument_endpoint() | 166 @gae_ts_mon.instrument_endpoint() |
| 157 @auth.endpoints_method( | 167 @auth.endpoints_method( |
| 158 message_types.VoidMessage, swarming_rpcs.BootstrapToken) | 168 message_types.VoidMessage, swarming_rpcs.BootstrapToken) |
| 159 @auth.require(acl.is_bootstrapper) | 169 @auth.require(acl.can_bot_create) |
| 160 def token(self, _request): | 170 def token(self, _request): |
| 161 """Returns a token to bootstrap a new bot.""" | 171 """Returns a token to bootstrap a new bot.""" |
| 162 return swarming_rpcs.BootstrapToken( | 172 return swarming_rpcs.BootstrapToken( |
| 163 bootstrap_token = bot_code.generate_bootstrap_token(), | 173 bootstrap_token = bot_code.generate_bootstrap_token(), |
| 164 ) | 174 ) |
| 165 | 175 |
| 166 @gae_ts_mon.instrument_endpoint() | 176 @gae_ts_mon.instrument_endpoint() |
| 167 @auth.endpoints_method( | 177 @auth.endpoints_method( |
| 168 message_types.VoidMessage, swarming_rpcs.ClientPermissions, | 178 message_types.VoidMessage, swarming_rpcs.ClientPermissions, |
| 169 http_method='GET') | 179 http_method='GET') |
| 170 @auth.public | 180 @auth.public |
| 171 def permissions(self, _request): | 181 def permissions(self, _request): |
| 172 """Returns the caller's permissions.""" | 182 """Returns the caller's permissions.""" |
| 183 # TODO(maruel): Redo this. |
| 173 return swarming_rpcs.ClientPermissions( | 184 return swarming_rpcs.ClientPermissions( |
| 174 delete_bot = acl.is_admin(), | 185 delete_bot = acl._is_admin(), |
| 175 terminate_bot = acl.is_privileged_user(), | 186 terminate_bot = acl._is_privileged_user(), |
| 176 get_configs = acl.is_user(), | 187 get_configs = acl._is_user(), |
| 177 put_configs = acl.is_admin(), | 188 put_configs = acl._is_admin(), |
| 178 cancel_task = acl.is_user(), | 189 cancel_task = acl._is_user(), |
| 179 cancel_tasks = acl.is_admin(), | 190 cancel_tasks = acl._is_admin(), |
| 180 get_bootstrap_token = acl.is_bootstrapper(), | 191 get_bootstrap_token = acl._is_bootstrapper(), |
| 181 ) | 192 ) |
| 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_config_view) |
| 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_config_view) |
| 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_config_edit) |
| 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_config_edit) |
| 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_task_view) |
| 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 # This enforces read access. |
| 304 _, result = get_request_and_result(request.task_id, _VIEW) |
| 293 return message_conversion.task_result_to_rpc( | 305 return message_conversion.task_result_to_rpc( |
| 294 result, request.include_performance_stats) | 306 result, request.include_performance_stats) |
| 295 | 307 |
| 296 @gae_ts_mon.instrument_endpoint() | 308 @gae_ts_mon.instrument_endpoint() |
| 297 @auth.endpoints_method( | 309 @auth.endpoints_method( |
| 298 TaskId, swarming_rpcs.TaskRequest, | 310 TaskId, swarming_rpcs.TaskRequest, |
| 299 name='request', | 311 name='request', |
| 300 path='{task_id}/request', | 312 path='{task_id}/request', |
| 301 http_method='GET') | 313 http_method='GET') |
| 302 @auth.require(acl.is_bot_or_user) | 314 @auth.require(acl.can_task_view) |
| 303 def request(self, request): | 315 def request(self, request): |
| 304 """Returns the task request corresponding to a task ID.""" | 316 """Returns the task request corresponding to a task ID.""" |
| 305 logging.debug('%s', request) | 317 logging.debug('%s', request) |
| 306 request_obj, _ = get_request_and_result(request.task_id) | 318 request_obj, _ = get_request_and_result(request.task_id, _VIEW) |
| 307 return message_conversion.task_request_to_rpc(request_obj) | 319 return message_conversion.task_request_to_rpc(request_obj) |
| 308 | 320 |
| 309 @gae_ts_mon.instrument_endpoint() | 321 @gae_ts_mon.instrument_endpoint() |
| 310 @auth.endpoints_method( | 322 @auth.endpoints_method( |
| 311 TaskId, swarming_rpcs.CancelResponse, | 323 TaskId, swarming_rpcs.CancelResponse, |
| 312 name='cancel', | 324 name='cancel', |
| 313 path='{task_id}/cancel') | 325 path='{task_id}/cancel') |
| 314 @auth.require(acl.is_bot_or_user) | 326 @auth.require(acl.can_task_edit) |
| 315 def cancel(self, request): | 327 def cancel(self, request): |
| 316 """Cancels a task. | 328 """Cancels a task. |
| 317 | 329 |
| 318 If a bot was running the task, the bot will forcibly cancel the task. | 330 If a bot was running the task, the bot will forcibly cancel the task. |
| 319 """ | 331 """ |
| 320 logging.debug('%s', request) | 332 logging.debug('%s', request) |
| 321 request_obj, result = get_request_and_result(request.task_id) | 333 request_obj, result = get_request_and_result(request.task_id, _EDIT) |
| 322 ok, was_running = task_scheduler.cancel_task(request_obj, result.key) | 334 ok, was_running = task_scheduler.cancel_task(request_obj, result.key) |
| 323 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running) | 335 return swarming_rpcs.CancelResponse(ok=ok, was_running=was_running) |
| 324 | 336 |
| 325 @gae_ts_mon.instrument_endpoint() | 337 @gae_ts_mon.instrument_endpoint() |
| 326 @auth.endpoints_method( | 338 @auth.endpoints_method( |
| 327 TaskId, swarming_rpcs.TaskOutput, | 339 TaskId, swarming_rpcs.TaskOutput, |
| 328 name='stdout', | 340 name='stdout', |
| 329 path='{task_id}/stdout', | 341 path='{task_id}/stdout', |
| 330 http_method='GET') | 342 http_method='GET') |
| 331 @auth.require(acl.is_bot_or_user) | 343 @auth.require(acl.can_task_view) |
| 332 def stdout(self, request): | 344 def stdout(self, request): |
| 333 """Returns the output of the task corresponding to a task ID.""" | 345 """Returns the output of the task corresponding to a task ID.""" |
| 334 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine | 346 # TODO(maruel): Add streaming. Real streaming is not supported by AppEngine |
| 335 # v1. | 347 # v1. |
| 336 # TODO(maruel): Send as raw content instead of encoded. This is not | 348 # TODO(maruel): Send as raw content instead of encoded. This is not |
| 337 # supported by cloud endpoints. | 349 # supported by cloud endpoints. |
| 338 logging.debug('%s', request) | 350 logging.debug('%s', request) |
| 339 _, result = get_request_and_result(request.task_id) | 351 _, result = get_request_and_result(request.task_id, _VIEW) |
| 340 output = result.get_output() | 352 output = result.get_output() |
| 341 if output: | 353 if output: |
| 342 output = output.decode('utf-8', 'replace') | 354 output = output.decode('utf-8', 'replace') |
| 343 return swarming_rpcs.TaskOutput(output=output) | 355 return swarming_rpcs.TaskOutput(output=output) |
| 344 | 356 |
| 345 | 357 |
| 346 @swarming_api.api_class(resource_name='tasks', path='tasks') | 358 @swarming_api.api_class(resource_name='tasks', path='tasks') |
| 347 class SwarmingTasksService(remote.Service): | 359 class SwarmingTasksService(remote.Service): |
| 348 """Swarming's tasks-related API.""" | 360 """Swarming's tasks-related API.""" |
| 349 @gae_ts_mon.instrument_endpoint() | 361 @gae_ts_mon.instrument_endpoint() |
| 350 @auth.endpoints_method( | 362 @auth.endpoints_method( |
| 351 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata) | 363 swarming_rpcs.NewTaskRequest, swarming_rpcs.TaskRequestMetadata) |
| 352 @auth.require(acl.is_bot_or_user) | 364 @auth.require(acl.can_task_create) |
| 353 def new(self, request): | 365 def new(self, request): |
| 354 """Creates a new task. | 366 """Creates a new task. |
| 355 | 367 |
| 356 The task will be enqueued in the tasks list and will be executed at the | 368 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 | 369 earliest opportunity by a bot that has at least the dimensions as described |
| 358 in the task request. | 370 in the task request. |
| 359 """ | 371 """ |
| 360 sb = (request.properties.secret_bytes | 372 sb = (request.properties.secret_bytes |
| 361 if request.properties is not None else None) | 373 if request.properties is not None else None) |
| 362 if sb is not None: | 374 if sb is not None: |
| (...skipping 20 matching lines...) Expand all Loading... |
| 383 | 395 |
| 384 return swarming_rpcs.TaskRequestMetadata( | 396 return swarming_rpcs.TaskRequestMetadata( |
| 385 request=message_conversion.task_request_to_rpc(request), | 397 request=message_conversion.task_request_to_rpc(request), |
| 386 task_id=task_pack.pack_result_summary_key(result_summary.key), | 398 task_id=task_pack.pack_result_summary_key(result_summary.key), |
| 387 task_result=previous_result) | 399 task_result=previous_result) |
| 388 | 400 |
| 389 @gae_ts_mon.instrument_endpoint() | 401 @gae_ts_mon.instrument_endpoint() |
| 390 @auth.endpoints_method( | 402 @auth.endpoints_method( |
| 391 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList, | 403 swarming_rpcs.TasksRequest, swarming_rpcs.TaskList, |
| 392 http_method='GET') | 404 http_method='GET') |
| 393 @auth.require(acl.is_privileged_user) | 405 @auth.require(acl.can_task_view) |
| 394 def list(self, request): | 406 def list(self, request): |
| 395 """Returns tasks results based on the filters. | 407 """Returns tasks results based on the filters. |
| 396 | 408 |
| 397 This endpoint is significantly slower than 'count'. Use 'count' when | 409 This endpoint is significantly slower than 'count'. Use 'count' when |
| 398 possible. | 410 possible. |
| 399 """ | 411 """ |
| 400 # TODO(maruel): Rename 'list' to 'results'. | 412 # TODO(maruel): Rename 'list' to 'results'. |
| 401 # TODO(maruel): Rename 'TaskList' to 'TaskResults'. | 413 # TODO(maruel): Rename 'TaskList' to 'TaskResults'. |
| 402 logging.debug('%s', request) | 414 logging.debug('%s', request) |
| 403 now = utils.utcnow() | 415 now = utils.utcnow() |
| (...skipping 17 matching lines...) Expand all Loading... |
| 421 message_conversion.task_result_to_rpc( | 433 message_conversion.task_result_to_rpc( |
| 422 i, request.include_performance_stats) | 434 i, request.include_performance_stats) |
| 423 for i in items | 435 for i in items |
| 424 ], | 436 ], |
| 425 now=now) | 437 now=now) |
| 426 | 438 |
| 427 @gae_ts_mon.instrument_endpoint() | 439 @gae_ts_mon.instrument_endpoint() |
| 428 @auth.endpoints_method( | 440 @auth.endpoints_method( |
| 429 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests, | 441 swarming_rpcs.TasksRequest, swarming_rpcs.TaskRequests, |
| 430 http_method='GET') | 442 http_method='GET') |
| 431 @auth.require(acl.is_privileged_user) | 443 @auth.require(acl.can_task_view) |
| 432 def requests(self, request): | 444 def requests(self, request): |
| 433 """Returns tasks requests based on the filters. | 445 """Returns tasks requests based on the filters. |
| 434 | 446 |
| 435 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when | 447 This endpoint is slightly slower than 'list'. Use 'list' or 'count' when |
| 436 possible. | 448 possible. |
| 437 """ | 449 """ |
| 438 logging.debug('%s', request) | 450 logging.debug('%s', request) |
| 439 if request.include_performance_stats: | 451 if request.include_performance_stats: |
| 440 raise endpoints.BadRequestException( | 452 raise endpoints.BadRequestException( |
| 441 'Can\'t set include_performance_stats for tasks/list') | 453 'Can\'t set include_performance_stats for tasks/list') |
| (...skipping 19 matching lines...) Expand all Loading... |
| 461 'This combination is unsupported, sorry.') | 473 'This combination is unsupported, sorry.') |
| 462 return swarming_rpcs.TaskRequests( | 474 return swarming_rpcs.TaskRequests( |
| 463 cursor=cursor, | 475 cursor=cursor, |
| 464 items=[message_conversion.task_request_to_rpc(i) for i in items], | 476 items=[message_conversion.task_request_to_rpc(i) for i in items], |
| 465 now=now) | 477 now=now) |
| 466 | 478 |
| 467 @gae_ts_mon.instrument_endpoint() | 479 @gae_ts_mon.instrument_endpoint() |
| 468 @auth.endpoints_method( | 480 @auth.endpoints_method( |
| 469 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse, | 481 swarming_rpcs.TasksCancelRequest, swarming_rpcs.TasksCancelResponse, |
| 470 http_method='POST') | 482 http_method='POST') |
| 471 @auth.require(acl.is_admin) | 483 @auth.require(acl.can_tasks_edit) |
| 472 def cancel(self, request): | 484 def cancel(self, request): |
| 473 """Cancel a subset of pending tasks based on the tags. | 485 """Cancel a subset of pending tasks based on the tags. |
| 474 | 486 |
| 475 Cancellation happens asynchronously, so when this call returns, | 487 Cancellation happens asynchronously, so when this call returns, |
| 476 cancellations will not have completed yet. | 488 cancellations will not have completed yet. |
| 477 """ | 489 """ |
| 478 logging.debug('%s', request) | 490 logging.debug('%s', request) |
| 479 if not request.tags: | 491 if not request.tags: |
| 480 # Prevent accidental cancellation of everything. | 492 # Prevent accidental cancellation of everything. |
| 481 raise endpoints.BadRequestException( | 493 raise endpoints.BadRequestException( |
| (...skipping 20 matching lines...) Expand all Loading... |
| 502 | 514 |
| 503 return swarming_rpcs.TasksCancelResponse( | 515 return swarming_rpcs.TasksCancelResponse( |
| 504 cursor=cursor, | 516 cursor=cursor, |
| 505 matched=len(tasks), | 517 matched=len(tasks), |
| 506 now=now) | 518 now=now) |
| 507 | 519 |
| 508 @gae_ts_mon.instrument_endpoint() | 520 @gae_ts_mon.instrument_endpoint() |
| 509 @auth.endpoints_method( | 521 @auth.endpoints_method( |
| 510 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount, | 522 swarming_rpcs.TasksCountRequest, swarming_rpcs.TasksCount, |
| 511 http_method='GET') | 523 http_method='GET') |
| 512 @auth.require(acl.is_privileged_user) | 524 @auth.require(acl.can_task_view) |
| 513 def count(self, request): | 525 def count(self, request): |
| 514 """Counts number of tasks in a given state.""" | 526 """Counts number of tasks in a given state.""" |
| 515 logging.debug('%s', request) | 527 logging.debug('%s', request) |
| 516 if not request.start: | 528 if not request.start: |
| 517 raise endpoints.BadRequestException('start (as epoch) is required') | 529 raise endpoints.BadRequestException('start (as epoch) is required') |
| 518 now = utils.utcnow() | 530 now = utils.utcnow() |
| 519 mem_key = self._memcache_key(request, now) | 531 mem_key = self._memcache_key(request, now) |
| 520 count = memcache.get(mem_key, namespace='tasks_count') | 532 count = memcache.get(mem_key, namespace='tasks_count') |
| 521 if count is not None: | 533 if count is not None: |
| 522 return swarming_rpcs.TasksCount(count=count, now=now) | 534 return swarming_rpcs.TasksCount(count=count, now=now) |
| (...skipping 19 matching lines...) Expand all Loading... |
| 542 return task_result.get_result_summaries_query( | 554 return task_result.get_result_summaries_query( |
| 543 start, end, | 555 start, end, |
| 544 sort or request.sort.name.lower(), | 556 sort or request.sort.name.lower(), |
| 545 request.state.name.lower(), | 557 request.state.name.lower(), |
| 546 request.tags) | 558 request.tags) |
| 547 | 559 |
| 548 @gae_ts_mon.instrument_endpoint() | 560 @gae_ts_mon.instrument_endpoint() |
| 549 @auth.endpoints_method( | 561 @auth.endpoints_method( |
| 550 message_types.VoidMessage, swarming_rpcs.TasksTags, | 562 message_types.VoidMessage, swarming_rpcs.TasksTags, |
| 551 http_method='GET') | 563 http_method='GET') |
| 552 @auth.require(acl.is_privileged_user) | 564 @auth.require(acl.can_task_view) |
| 553 def tags(self, _request): | 565 def tags(self, _request): |
| 554 """Returns the cached set of tags currently seen in the fleet.""" | 566 """Returns the cached set of tags currently seen in the fleet.""" |
| 555 tags = task_result.TagAggregation.KEY.get() | 567 tags = task_result.TagAggregation.KEY.get() |
| 556 ft = [ | 568 ft = [ |
| 557 swarming_rpcs.StringListPair(key=t.tag, value=t.values) | 569 swarming_rpcs.StringListPair(key=t.tag, value=t.values) |
| 558 for t in tags.tags | 570 for t in tags.tags |
| 559 ] | 571 ] |
| 560 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts) | 572 return swarming_rpcs.TasksTags(tasks_tags=ft, ts=tags.ts) |
| 561 | 573 |
| 562 | 574 |
| (...skipping 14 matching lines...) Expand all Loading... |
| 577 | 589 |
| 578 @swarming_api.api_class(resource_name='bot', path='bot') | 590 @swarming_api.api_class(resource_name='bot', path='bot') |
| 579 class SwarmingBotService(remote.Service): | 591 class SwarmingBotService(remote.Service): |
| 580 """Bot-related API. Permits querying information about the bot's properties""" | 592 """Bot-related API. Permits querying information about the bot's properties""" |
| 581 @gae_ts_mon.instrument_endpoint() | 593 @gae_ts_mon.instrument_endpoint() |
| 582 @auth.endpoints_method( | 594 @auth.endpoints_method( |
| 583 BotId, swarming_rpcs.BotInfo, | 595 BotId, swarming_rpcs.BotInfo, |
| 584 name='get', | 596 name='get', |
| 585 path='{bot_id}/get', | 597 path='{bot_id}/get', |
| 586 http_method='GET') | 598 http_method='GET') |
| 587 @auth.require(acl.is_privileged_user) | 599 @auth.require(acl.can_bot_view) |
| 588 def get(self, request): | 600 def get(self, request): |
| 589 """Returns information about a known bot. | 601 """Returns information about a known bot. |
| 590 | 602 |
| 591 This includes its state and dimensions, and if it is currently running a | 603 This includes its state and dimensions, and if it is currently running a |
| 592 task. | 604 task. |
| 593 """ | 605 """ |
| 594 logging.debug('%s', request) | 606 logging.debug('%s', request) |
| 595 bot_id = request.bot_id | 607 bot_id = request.bot_id |
| 596 bot = bot_management.get_info_key(bot_id).get() | 608 bot = bot_management.get_info_key(bot_id).get() |
| 597 deleted = False | 609 deleted = False |
| (...skipping 22 matching lines...) Expand all Loading... |
| 620 deleted = True | 632 deleted = True |
| 621 | 633 |
| 622 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(), | 634 return message_conversion.bot_info_to_rpc(bot, utils.utcnow(), |
| 623 deleted=deleted) | 635 deleted=deleted) |
| 624 | 636 |
| 625 @gae_ts_mon.instrument_endpoint() | 637 @gae_ts_mon.instrument_endpoint() |
| 626 @auth.endpoints_method( | 638 @auth.endpoints_method( |
| 627 BotId, swarming_rpcs.DeletedResponse, | 639 BotId, swarming_rpcs.DeletedResponse, |
| 628 name='delete', | 640 name='delete', |
| 629 path='{bot_id}/delete') | 641 path='{bot_id}/delete') |
| 630 @auth.require(acl.is_admin) | 642 @auth.require(acl.can_bot_edit) |
| 631 def delete(self, request): | 643 def delete(self, request): |
| 632 """Deletes the bot corresponding to a provided bot_id. | 644 """Deletes the bot corresponding to a provided bot_id. |
| 633 | 645 |
| 634 At that point, the bot will not appears in the list of bots but it is still | 646 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 | 647 possible to get information about the bot with its bot id is known, as |
| 636 historical data is not deleted. | 648 historical data is not deleted. |
| 637 | 649 |
| 638 It is meant to remove from the DB the presence of a bot that was retired, | 650 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 | 651 e.g. the VM was shut down already. Use 'terminate' instead of the bot is |
| 640 still alive. | 652 still alive. |
| 641 """ | 653 """ |
| 642 logging.debug('%s', request) | 654 logging.debug('%s', request) |
| 643 bot_key = bot_management.get_info_key(request.bot_id) | 655 bot_key = bot_management.get_info_key(request.bot_id) |
| 644 get_or_raise(bot_key) # raises 404 if there is no such bot | 656 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()? | 657 # TODO(maruel): If the bot was a MP, call lease_management.cleanup_bot()? |
| 646 task_queues.cleanup_after_bot(request.bot_id) | 658 task_queues.cleanup_after_bot(request.bot_id) |
| 647 bot_key.delete() | 659 bot_key.delete() |
| 648 return swarming_rpcs.DeletedResponse(deleted=True) | 660 return swarming_rpcs.DeletedResponse(deleted=True) |
| 649 | 661 |
| 650 @gae_ts_mon.instrument_endpoint() | 662 @gae_ts_mon.instrument_endpoint() |
| 651 @auth.endpoints_method( | 663 @auth.endpoints_method( |
| 652 BotEventsRequest, swarming_rpcs.BotEvents, | 664 BotEventsRequest, swarming_rpcs.BotEvents, |
| 653 name='events', | 665 name='events', |
| 654 path='{bot_id}/events', | 666 path='{bot_id}/events', |
| 655 http_method='GET') | 667 http_method='GET') |
| 656 @auth.require(acl.is_privileged_user) | 668 @auth.require(acl.can_bot_view) |
| 657 def events(self, request): | 669 def events(self, request): |
| 658 """Returns events that happened on a bot.""" | 670 """Returns events that happened on a bot.""" |
| 659 logging.debug('%s', request) | 671 logging.debug('%s', request) |
| 660 try: | 672 try: |
| 661 now = utils.utcnow() | 673 now = utils.utcnow() |
| 662 start = message_conversion.epoch_to_datetime(request.start) | 674 start = message_conversion.epoch_to_datetime(request.start) |
| 663 end = message_conversion.epoch_to_datetime(request.end) | 675 end = message_conversion.epoch_to_datetime(request.end) |
| 664 order = not (start or end) | 676 order = not (start or end) |
| 665 query = bot_management.get_events_query(request.bot_id, order) | 677 query = bot_management.get_events_query(request.bot_id, order) |
| 666 if not order: | 678 if not order: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 678 return swarming_rpcs.BotEvents( | 690 return swarming_rpcs.BotEvents( |
| 679 cursor=cursor, | 691 cursor=cursor, |
| 680 items=[message_conversion.bot_event_to_rpc(r) for r in items], | 692 items=[message_conversion.bot_event_to_rpc(r) for r in items], |
| 681 now=now) | 693 now=now) |
| 682 | 694 |
| 683 @gae_ts_mon.instrument_endpoint() | 695 @gae_ts_mon.instrument_endpoint() |
| 684 @auth.endpoints_method( | 696 @auth.endpoints_method( |
| 685 BotId, swarming_rpcs.TerminateResponse, | 697 BotId, swarming_rpcs.TerminateResponse, |
| 686 name='terminate', | 698 name='terminate', |
| 687 path='{bot_id}/terminate') | 699 path='{bot_id}/terminate') |
| 688 @auth.require(acl.is_bot_or_privileged_user) | 700 @auth.require(acl.can_bot_edit) |
| 689 def terminate(self, request): | 701 def terminate(self, request): |
| 690 """Asks a bot to terminate itself gracefully. | 702 """Asks a bot to terminate itself gracefully. |
| 691 | 703 |
| 692 The bot will stay in the DB, use 'delete' to remove it from the DB | 704 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 | 705 afterward. This request returns a pseudo-taskid that can be waited for to |
| 694 wait for the bot to turn down. | 706 wait for the bot to turn down. |
| 695 | 707 |
| 696 This command is particularly useful when a privileged user needs to safely | 708 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 | 709 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 | 710 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) | 727 result_summary = task_scheduler.schedule_request(request, None) |
| 716 return swarming_rpcs.TerminateResponse( | 728 return swarming_rpcs.TerminateResponse( |
| 717 task_id=task_pack.pack_result_summary_key(result_summary.key)) | 729 task_id=task_pack.pack_result_summary_key(result_summary.key)) |
| 718 | 730 |
| 719 @gae_ts_mon.instrument_endpoint() | 731 @gae_ts_mon.instrument_endpoint() |
| 720 @auth.endpoints_method( | 732 @auth.endpoints_method( |
| 721 BotTasksRequest, swarming_rpcs.BotTasks, | 733 BotTasksRequest, swarming_rpcs.BotTasks, |
| 722 name='tasks', | 734 name='tasks', |
| 723 path='{bot_id}/tasks', | 735 path='{bot_id}/tasks', |
| 724 http_method='GET') | 736 http_method='GET') |
| 725 @auth.require(acl.is_privileged_user) | 737 @auth.require(acl.can_task_view) |
| 726 def tasks(self, request): | 738 def tasks(self, request): |
| 727 """Lists a given bot's tasks within the specified date range. | 739 """Lists a given bot's tasks within the specified date range. |
| 728 | 740 |
| 729 In this case, the tasks are effectively TaskRunResult since it's individual | 741 In this case, the tasks are effectively TaskRunResult since it's individual |
| 730 task tries sent to this specific bot. | 742 task tries sent to this specific bot. |
| 731 | 743 |
| 732 It is impossible to search by both tags and bot id. If there's a need, | 744 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). | 745 TaskRunResult.tags will be added (via a copy from TaskRequest.tags). |
| 734 """ | 746 """ |
| 735 logging.debug('%s', request) | 747 logging.debug('%s', request) |
| (...skipping 22 matching lines...) Expand all Loading... |
| 758 | 770 |
| 759 | 771 |
| 760 @swarming_api.api_class(resource_name='bots', path='bots') | 772 @swarming_api.api_class(resource_name='bots', path='bots') |
| 761 class SwarmingBotsService(remote.Service): | 773 class SwarmingBotsService(remote.Service): |
| 762 """Bots-related API.""" | 774 """Bots-related API.""" |
| 763 | 775 |
| 764 @gae_ts_mon.instrument_endpoint() | 776 @gae_ts_mon.instrument_endpoint() |
| 765 @auth.endpoints_method( | 777 @auth.endpoints_method( |
| 766 swarming_rpcs.BotsRequest, swarming_rpcs.BotList, | 778 swarming_rpcs.BotsRequest, swarming_rpcs.BotList, |
| 767 http_method='GET') | 779 http_method='GET') |
| 768 @auth.require(acl.is_privileged_user) | 780 @auth.require(acl.can_bot_view) |
| 769 def list(self, request): | 781 def list(self, request): |
| 770 """Provides list of known bots. | 782 """Provides list of known bots. |
| 771 | 783 |
| 772 Deleted bots will not be listed. | 784 Deleted bots will not be listed. |
| 773 """ | 785 """ |
| 774 logging.debug('%s', request) | 786 logging.debug('%s', request) |
| 775 now = utils.utcnow() | 787 now = utils.utcnow() |
| 776 q = bot_management.BotInfo.query() | 788 q = bot_management.BotInfo.query() |
| 777 try: | 789 try: |
| 778 q = bot_management.filter_dimensions(q, request.dimensions) | 790 q = bot_management.filter_dimensions(q, request.dimensions) |
| 779 q = bot_management.filter_availability( | 791 q = bot_management.filter_availability( |
| 780 q, swarming_rpcs.to_bool(request.quarantined), | 792 q, swarming_rpcs.to_bool(request.quarantined), |
| 781 swarming_rpcs.to_bool(request.is_dead), now, | 793 swarming_rpcs.to_bool(request.is_dead), now, |
| 782 swarming_rpcs.to_bool(request.is_busy), | 794 swarming_rpcs.to_bool(request.is_busy), |
| 783 swarming_rpcs.to_bool(request.is_mp)) | 795 swarming_rpcs.to_bool(request.is_mp)) |
| 784 except ValueError as e: | 796 except ValueError as e: |
| 785 raise endpoints.BadRequestException(str(e)) | 797 raise endpoints.BadRequestException(str(e)) |
| 786 | 798 |
| 787 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor) | 799 bots, cursor = datastore_utils.fetch_page(q, request.limit, request.cursor) |
| 788 return swarming_rpcs.BotList( | 800 return swarming_rpcs.BotList( |
| 789 cursor=cursor, | 801 cursor=cursor, |
| 790 death_timeout=config.settings().bot_death_timeout_secs, | 802 death_timeout=config.settings().bot_death_timeout_secs, |
| 791 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots], | 803 items=[message_conversion.bot_info_to_rpc(bot, now) for bot in bots], |
| 792 now=now) | 804 now=now) |
| 793 | 805 |
| 794 @gae_ts_mon.instrument_endpoint() | 806 @gae_ts_mon.instrument_endpoint() |
| 795 @auth.endpoints_method( | 807 @auth.endpoints_method( |
| 796 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount, | 808 swarming_rpcs.BotsRequest, swarming_rpcs.BotsCount, |
| 797 http_method='GET') | 809 http_method='GET') |
| 798 @auth.require(acl.is_privileged_user) | 810 @auth.require(acl.can_bot_view) |
| 799 def count(self, request): | 811 def count(self, request): |
| 800 """Counts number of bots with given dimensions.""" | 812 """Counts number of bots with given dimensions.""" |
| 801 logging.debug('%s', request) | 813 logging.debug('%s', request) |
| 802 now = utils.utcnow() | 814 now = utils.utcnow() |
| 803 q = bot_management.BotInfo.query() | 815 q = bot_management.BotInfo.query() |
| 804 try: | 816 try: |
| 805 q = bot_management.filter_dimensions(q, request.dimensions) | 817 q = bot_management.filter_dimensions(q, request.dimensions) |
| 806 except ValueError as e: | 818 except ValueError as e: |
| 807 raise endpoints.BadRequestException(str(e)) | 819 raise endpoints.BadRequestException(str(e)) |
| 808 | 820 |
| 809 f_count = q.count_async() | 821 f_count = q.count_async() |
| 810 f_dead = (bot_management.filter_availability(q, None, True, now, None, None) | 822 f_dead = (bot_management.filter_availability(q, None, True, now, None, None) |
| 811 .count_async()) | 823 .count_async()) |
| 812 f_quarantined = ( | 824 f_quarantined = ( |
| 813 bot_management.filter_availability(q, True, None, now, None, None) | 825 bot_management.filter_availability(q, True, None, now, None, None) |
| 814 .count_async()) | 826 .count_async()) |
| 815 f_busy = (bot_management.filter_availability(q, None, None, now, True, None) | 827 f_busy = (bot_management.filter_availability(q, None, None, now, True, None) |
| 816 .count_async()) | 828 .count_async()) |
| 817 return swarming_rpcs.BotsCount( | 829 return swarming_rpcs.BotsCount( |
| 818 count=f_count.get_result(), | 830 count=f_count.get_result(), |
| 819 quarantined=f_quarantined.get_result(), | 831 quarantined=f_quarantined.get_result(), |
| 820 dead=f_dead.get_result(), | 832 dead=f_dead.get_result(), |
| 821 busy=f_busy.get_result(), | 833 busy=f_busy.get_result(), |
| 822 now=now) | 834 now=now) |
| 823 | 835 |
| 824 | |
| 825 @gae_ts_mon.instrument_endpoint() | 836 @gae_ts_mon.instrument_endpoint() |
| 826 @auth.endpoints_method( | 837 @auth.endpoints_method( |
| 827 message_types.VoidMessage, swarming_rpcs.BotsDimensions, | 838 message_types.VoidMessage, swarming_rpcs.BotsDimensions, |
| 828 http_method='GET') | 839 http_method='GET') |
| 829 @auth.require(acl.is_privileged_user) | 840 @auth.require(acl.can_bot_view) |
| 830 def dimensions(self, _request): | 841 def dimensions(self, _request): |
| 831 """Returns the cached set of dimensions currently in use in the fleet.""" | 842 """Returns the cached set of dimensions currently in use in the fleet.""" |
| 832 dims = bot_management.DimensionAggregation.KEY.get() | 843 dims = bot_management.DimensionAggregation.KEY.get() |
| 833 fd = [ | 844 fd = [ |
| 834 swarming_rpcs.StringListPair(key=d.dimension, value=d.values) | 845 swarming_rpcs.StringListPair(key=d.dimension, value=d.values) |
| 835 for d in dims.dimensions | 846 for d in dims.dimensions |
| 836 ] | 847 ] |
| 837 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts) | 848 return swarming_rpcs.BotsDimensions(bots_dimensions=fd, ts=dims.ts) |
| 838 | 849 |
| 839 | 850 |
| 840 def get_routes(): | 851 def get_routes(): |
| 841 return ( | 852 return ( |
| 842 endpoints_webapp2.api_routes(SwarmingServerService) + | 853 endpoints_webapp2.api_routes(SwarmingServerService) + |
| 843 endpoints_webapp2.api_routes(SwarmingTaskService) + | 854 endpoints_webapp2.api_routes(SwarmingTaskService) + |
| 844 endpoints_webapp2.api_routes(SwarmingTasksService) + | 855 endpoints_webapp2.api_routes(SwarmingTasksService) + |
| 845 endpoints_webapp2.api_routes(SwarmingBotService) + | 856 endpoints_webapp2.api_routes(SwarmingBotService) + |
| 846 endpoints_webapp2.api_routes(SwarmingBotsService) + | 857 endpoints_webapp2.api_routes(SwarmingBotsService) + |
| 847 # components.config endpoints for validation and configuring of luci-config | 858 # components.config endpoints for validation and configuring of luci-config |
| 848 # service URL. | 859 # service URL. |
| 849 endpoints_webapp2.api_routes(config.ConfigApi)) | 860 endpoints_webapp2.api_routes(config.ConfigApi)) |
| OLD | NEW |