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