| OLD | NEW |
| 1 # Copyright 2014 The Swarming Authors. All rights reserved. | 1 # Copyright 2014 The Swarming Authors. All rights reserved. |
| 2 # Use of this source code is governed by the Apache v2.0 license that can be | 2 # Use of this source code is governed by the Apache v2.0 license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Swarming client REST APIs handlers.""" | 5 """Swarming client REST APIs handlers.""" |
| 6 | 6 |
| 7 import base64 | 7 import base64 |
| 8 import datetime | 8 import datetime |
| 9 import json | 9 import json |
| 10 import logging | 10 import logging |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 81 | 81 |
| 82 | 82 |
| 83 ### New Client APIs. | 83 ### New Client APIs. |
| 84 | 84 |
| 85 | 85 |
| 86 class ClientApiListHandler(auth.ApiHandler): | 86 class ClientApiListHandler(auth.ApiHandler): |
| 87 """All query handlers""" | 87 """All query handlers""" |
| 88 | 88 |
| 89 @auth.public | 89 @auth.public |
| 90 def get(self): | 90 def get(self): |
| 91 logging.error('Unexpected old client') |
| 91 # Hard to make it any simpler. | 92 # Hard to make it any simpler. |
| 92 prefix = '/swarming/api/v1/client/' | 93 prefix = '/swarming/api/v1/client/' |
| 93 data = { | 94 data = { |
| 94 r.template[len(prefix):]: process_doc(r.handler) for r in get_routes() | 95 r.template[len(prefix):]: process_doc(r.handler) for r in get_routes() |
| 95 if r.template.startswith(prefix) and hasattr(r.handler, 'get') | 96 if r.template.startswith(prefix) and hasattr(r.handler, 'get') |
| 96 } | 97 } |
| 97 self.send_response(data) | 98 self.send_response(data) |
| 98 | 99 |
| 99 | 100 |
| 100 class ClientHandshakeHandler(auth.ApiHandler): | 101 class ClientHandshakeHandler(auth.ApiHandler): |
| (...skipping 12 matching lines...) Expand all Loading... |
| 113 """ | 114 """ |
| 114 | 115 |
| 115 # This handler is called to get XSRF token, there's nothing to enforce yet. | 116 # This handler is called to get XSRF token, there's nothing to enforce yet. |
| 116 xsrf_token_enforce_on = () | 117 xsrf_token_enforce_on = () |
| 117 | 118 |
| 118 EXPECTED_KEYS = frozenset() | 119 EXPECTED_KEYS = frozenset() |
| 119 | 120 |
| 120 @auth.require_xsrf_token_request | 121 @auth.require_xsrf_token_request |
| 121 @auth.require(acl.is_bot_or_user) | 122 @auth.require(acl.is_bot_or_user) |
| 122 def post(self): | 123 def post(self): |
| 124 logging.error('Unexpected old client') |
| 123 request = self.parse_body() | 125 request = self.parse_body() |
| 124 log_unexpected_keys( | 126 log_unexpected_keys( |
| 125 self.EXPECTED_KEYS, request, self.request, 'client', 'keys') | 127 self.EXPECTED_KEYS, request, self.request, 'client', 'keys') |
| 126 data = { | 128 data = { |
| 127 # This access token will be used to validate each subsequent request. | 129 # This access token will be used to validate each subsequent request. |
| 128 'server_version': utils.get_app_version(), | 130 'server_version': utils.get_app_version(), |
| 129 'xsrf_token': self.generate_xsrf_token(), | 131 'xsrf_token': self.generate_xsrf_token(), |
| 130 } | 132 } |
| 131 self.send_response(data) | 133 self.send_response(data) |
| 132 | 134 |
| 133 | 135 |
| 134 class ClientTaskResultBase(auth.ApiHandler): | 136 class ClientTaskResultBase(auth.ApiHandler): |
| 135 """Implements the common base code for task related query APIs.""" | 137 """Implements the common base code for task related query APIs.""" |
| 136 | 138 |
| 137 def get_result_key(self, task_id): | 139 def get_result_key(self, task_id): |
| 140 logging.error('Unexpected old client') |
| 138 # TODO(maruel): Users can only request their own task. Privileged users can | 141 # TODO(maruel): Users can only request their own task. Privileged users can |
| 139 # request any task. | 142 # request any task. |
| 140 key = None | 143 key = None |
| 141 summary_key = None | 144 summary_key = None |
| 142 try: | 145 try: |
| 143 key = task_pack.unpack_result_summary_key(task_id) | 146 key = task_pack.unpack_result_summary_key(task_id) |
| 144 summary_key = key | 147 summary_key = key |
| 145 except ValueError: | 148 except ValueError: |
| 146 try: | 149 try: |
| 147 key = task_pack.unpack_run_result_key(task_id) | 150 key = task_pack.unpack_run_result_key(task_id) |
| 148 summary_key = task_pack.run_result_key_to_result_summary_key(key) | 151 summary_key = task_pack.run_result_key_to_result_summary_key(key) |
| 149 except ValueError: | 152 except ValueError: |
| 150 self.abort_with_error(400, error='Invalid key') | 153 self.abort_with_error(400, error='Invalid key') |
| 151 return key, summary_key | 154 return key, summary_key |
| 152 | 155 |
| 153 def get_result_entity(self, task_id): | 156 def get_result_entity(self, task_id): |
| 154 key, _ = self.get_result_key(task_id) | 157 key, _ = self.get_result_key(task_id) |
| 155 result = key.get() | 158 result = key.get() |
| 156 if not result: | 159 if not result: |
| 157 self.abort_with_error(404, error='Task not found') | 160 self.abort_with_error(404, error='Task not found') |
| 158 return result | 161 return result |
| 159 | 162 |
| 160 | 163 |
| 161 class ClientTaskResultHandler(ClientTaskResultBase): | 164 class ClientTaskResultHandler(ClientTaskResultBase): |
| 162 """Task's result meta data""" | 165 """Task's result meta data""" |
| 163 | 166 |
| 164 @auth.require(acl.is_bot_or_user) | 167 @auth.require(acl.is_bot_or_user) |
| 165 def get(self, task_id): | 168 def get(self, task_id): |
| 169 logging.error('Unexpected old client') |
| 166 result = self.get_result_entity(task_id) | 170 result = self.get_result_entity(task_id) |
| 167 self.send_response(utils.to_json_encodable(result)) | 171 self.send_response(utils.to_json_encodable(result)) |
| 168 | 172 |
| 169 | 173 |
| 170 class ClientTaskResultRequestHandler(ClientTaskResultBase): | 174 class ClientTaskResultRequestHandler(ClientTaskResultBase): |
| 171 """Task's request details""" | 175 """Task's request details""" |
| 172 | 176 |
| 173 @auth.require(acl.is_bot_or_user) | 177 @auth.require(acl.is_bot_or_user) |
| 174 def get(self, task_id): | 178 def get(self, task_id): |
| 179 logging.error('Unexpected old client') |
| 175 _, summary_key = self.get_result_key(task_id) | 180 _, summary_key = self.get_result_key(task_id) |
| 176 request_key = task_pack.result_summary_key_to_request_key(summary_key) | 181 request_key = task_pack.result_summary_key_to_request_key(summary_key) |
| 177 self.send_response(utils.to_json_encodable(request_key.get())) | 182 self.send_response(utils.to_json_encodable(request_key.get())) |
| 178 | 183 |
| 179 | 184 |
| 180 class ClientTaskResultOutputHandler(ClientTaskResultBase): | 185 class ClientTaskResultOutputHandler(ClientTaskResultBase): |
| 181 """Task's output for a single command""" | 186 """Task's output for a single command""" |
| 182 | 187 |
| 183 @auth.require(acl.is_bot_or_user) | 188 @auth.require(acl.is_bot_or_user) |
| 184 def get(self, task_id, command_index): | 189 def get(self, task_id, command_index): |
| 190 logging.error('Unexpected old client') |
| 185 result = self.get_result_entity(task_id) | 191 result = self.get_result_entity(task_id) |
| 186 output = result.get_command_output_async(int(command_index)).get_result() | 192 output = result.get_command_output_async(int(command_index)).get_result() |
| 187 if output: | 193 if output: |
| 188 output = output.decode('utf-8', 'replace') | 194 output = output.decode('utf-8', 'replace') |
| 189 # JSON then reencodes to ascii compatible encoded strings, which explodes | 195 # JSON then reencodes to ascii compatible encoded strings, which explodes |
| 190 # the size. | 196 # the size. |
| 191 data = { | 197 data = { |
| 192 'output': output, | 198 'output': output, |
| 193 } | 199 } |
| 194 self.send_response(utils.to_json_encodable(data)) | 200 self.send_response(utils.to_json_encodable(data)) |
| 195 | 201 |
| 196 | 202 |
| 197 class ClientTaskResultOutputAllHandler(ClientTaskResultBase): | 203 class ClientTaskResultOutputAllHandler(ClientTaskResultBase): |
| 198 """All output from all commands in a task""" | 204 """All output from all commands in a task""" |
| 199 | 205 |
| 200 @auth.require(acl.is_bot_or_user) | 206 @auth.require(acl.is_bot_or_user) |
| 201 def get(self, task_id): | 207 def get(self, task_id): |
| 208 logging.error('Unexpected old client') |
| 202 result = self.get_result_entity(task_id) | 209 result = self.get_result_entity(task_id) |
| 203 # JSON then reencodes to ascii compatible encoded strings, which explodes | 210 # JSON then reencodes to ascii compatible encoded strings, which explodes |
| 204 # the size. | 211 # the size. |
| 205 data = { | 212 data = { |
| 206 'outputs': [ | 213 'outputs': [ |
| 207 i.decode('utf-8', 'replace') if i else i | 214 i.decode('utf-8', 'replace') if i else i |
| 208 for i in result.get_outputs() | 215 for i in result.get_outputs() |
| 209 ], | 216 ], |
| 210 } | 217 } |
| 211 self.send_response(utils.to_json_encodable(data)) | 218 self.send_response(utils.to_json_encodable(data)) |
| (...skipping 16 matching lines...) Expand all Loading... |
| 228 'completed', 'completed_success', 'completed_failure', 'bot_died', | 235 'completed', 'completed_success', 'completed_failure', 'bot_died', |
| 229 'expired', 'canceled'. Defaults to 'all'. | 236 'expired', 'canceled'. Defaults to 'all'. |
| 230 | 237 |
| 231 In particular, one of `name`, `tag` or `state` can be used | 238 In particular, one of `name`, `tag` or `state` can be used |
| 232 exclusively. | 239 exclusively. |
| 233 """ | 240 """ |
| 234 EXPECTED = {'cursor', 'limit', 'name', 'sort', 'state', 'tag'} | 241 EXPECTED = {'cursor', 'limit', 'name', 'sort', 'state', 'tag'} |
| 235 | 242 |
| 236 @auth.require(acl.is_privileged_user) | 243 @auth.require(acl.is_privileged_user) |
| 237 def get(self): | 244 def get(self): |
| 245 logging.error('Unexpected old client') |
| 238 extra = frozenset(self.request.GET) - self.EXPECTED | 246 extra = frozenset(self.request.GET) - self.EXPECTED |
| 239 if extra: | 247 if extra: |
| 240 self.abort_with_error( | 248 self.abort_with_error( |
| 241 400, | 249 400, |
| 242 error='Extraneous query parameters. Did you make a typo? %s' % | 250 error='Extraneous query parameters. Did you make a typo? %s' % |
| 243 ','.join(sorted(extra))) | 251 ','.join(sorted(extra))) |
| 244 | 252 |
| 245 # Use a similar query to /user/tasks. | 253 # Use a similar query to /user/tasks. |
| 246 name = self.request.get('name') | 254 name = self.request.get('name') |
| 247 tags = self.request.get_all('tag') | 255 tags = self.request.get_all('tag') |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 290 'completed_success', | 298 'completed_success', |
| 291 'expired', | 299 'expired', |
| 292 'pending', | 300 'pending', |
| 293 'pending_running', | 301 'pending_running', |
| 294 'running', | 302 'running', |
| 295 'timed_out', | 303 'timed_out', |
| 296 } | 304 } |
| 297 | 305 |
| 298 @auth.require(acl.is_privileged_user) | 306 @auth.require(acl.is_privileged_user) |
| 299 def get(self): | 307 def get(self): |
| 308 logging.error('Unexpected old client') |
| 300 extra = frozenset(self.request.GET) - self.EXPECTED | 309 extra = frozenset(self.request.GET) - self.EXPECTED |
| 301 if extra: | 310 if extra: |
| 302 self.abort_with_error( | 311 self.abort_with_error( |
| 303 400, | 312 400, |
| 304 error='Extraneous query parameters. Did you make a typo? %s' % | 313 error='Extraneous query parameters. Did you make a typo? %s' % |
| 305 ','.join(sorted(extra))) | 314 ','.join(sorted(extra))) |
| 306 | 315 |
| 307 interval = self.request.get('interval', 24 * 3600) | 316 interval = self.request.get('interval', 24 * 3600) |
| 308 state = self.request.get('state', 'all') | 317 state = self.request.get('state', 'all') |
| 309 tags = self.request.get_all('tag') | 318 tags = self.request.get_all('tag') |
| (...skipping 25 matching lines...) Expand all Loading... |
| 335 class ClientApiBots(auth.ApiHandler): | 344 class ClientApiBots(auth.ApiHandler): |
| 336 """Bots known to the server""" | 345 """Bots known to the server""" |
| 337 | 346 |
| 338 ACCEPTABLE_FILTERS = ( | 347 ACCEPTABLE_FILTERS = ( |
| 339 'quarantined', | 348 'quarantined', |
| 340 'is_dead', | 349 'is_dead', |
| 341 ) | 350 ) |
| 342 | 351 |
| 343 @auth.require(acl.is_privileged_user) | 352 @auth.require(acl.is_privileged_user) |
| 344 def get(self): | 353 def get(self): |
| 354 logging.error('Unexpected old client') |
| 345 now = utils.utcnow() | 355 now = utils.utcnow() |
| 346 limit = int(self.request.get('limit', 1000)) | 356 limit = int(self.request.get('limit', 1000)) |
| 347 filter_by = self.request.get('filter') | 357 filter_by = self.request.get('filter') |
| 348 if filter_by and filter_by not in self.ACCEPTABLE_FILTERS: | 358 if filter_by and filter_by not in self.ACCEPTABLE_FILTERS: |
| 349 self.abort_with_error(400, error='Invalid filter query parameter') | 359 self.abort_with_error(400, error='Invalid filter query parameter') |
| 350 | 360 |
| 351 q = bot_management.BotInfo.query() | 361 q = bot_management.BotInfo.query() |
| 352 | 362 |
| 353 if not filter_by: | 363 if not filter_by: |
| 354 q = q.order(bot_management.BotInfo.key) | 364 q = q.order(bot_management.BotInfo.key) |
| (...skipping 23 matching lines...) Expand all Loading... |
| 378 'now': now, | 388 'now': now, |
| 379 } | 389 } |
| 380 self.send_response(utils.to_json_encodable(data)) | 390 self.send_response(utils.to_json_encodable(data)) |
| 381 | 391 |
| 382 | 392 |
| 383 class ClientApiBot(auth.ApiHandler): | 393 class ClientApiBot(auth.ApiHandler): |
| 384 """Bot's meta data""" | 394 """Bot's meta data""" |
| 385 | 395 |
| 386 @auth.require(acl.is_privileged_user) | 396 @auth.require(acl.is_privileged_user) |
| 387 def get(self, bot_id): | 397 def get(self, bot_id): |
| 398 logging.error('Unexpected old client') |
| 388 bot = bot_management.get_info_key(bot_id).get() | 399 bot = bot_management.get_info_key(bot_id).get() |
| 389 if not bot: | 400 if not bot: |
| 390 self.abort_with_error(404, error='Bot not found') | 401 self.abort_with_error(404, error='Bot not found') |
| 391 now = utils.utcnow() | 402 now = utils.utcnow() |
| 392 self.send_response(utils.to_json_encodable(bot.to_dict_with_now(now))) | 403 self.send_response(utils.to_json_encodable(bot.to_dict_with_now(now))) |
| 393 | 404 |
| 394 @auth.require(acl.is_admin) | 405 @auth.require(acl.is_admin) |
| 395 def delete(self, bot_id): | 406 def delete(self, bot_id): |
| 396 # Only delete BotInfo, not BotRoot, BotEvent nor BotSettings. | 407 # Only delete BotInfo, not BotRoot, BotEvent nor BotSettings. |
| 397 bot_key = bot_management.get_info_key(bot_id) | 408 bot_key = bot_management.get_info_key(bot_id) |
| 398 found = False | 409 found = False |
| 399 if bot_key.get(): | 410 if bot_key.get(): |
| 400 bot_key.delete() | 411 bot_key.delete() |
| 401 found = True | 412 found = True |
| 402 self.send_response({'deleted': bool(found)}) | 413 self.send_response({'deleted': bool(found)}) |
| 403 | 414 |
| 404 | 415 |
| 405 class ClientApiBotTask(auth.ApiHandler): | 416 class ClientApiBotTask(auth.ApiHandler): |
| 406 """Tasks executed on a specific bot""" | 417 """Tasks executed on a specific bot""" |
| 407 | 418 |
| 408 @auth.require(acl.is_privileged_user) | 419 @auth.require(acl.is_privileged_user) |
| 409 def get(self, bot_id): | 420 def get(self, bot_id): |
| 421 logging.error('Unexpected old client') |
| 410 limit = int(self.request.get('limit', 100)) | 422 limit = int(self.request.get('limit', 100)) |
| 411 cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor')) | 423 cursor = datastore_query.Cursor(urlsafe=self.request.get('cursor')) |
| 412 run_results, cursor, more = task_result.TaskRunResult.query( | 424 run_results, cursor, more = task_result.TaskRunResult.query( |
| 413 task_result.TaskRunResult.bot_id == bot_id).order( | 425 task_result.TaskRunResult.bot_id == bot_id).order( |
| 414 -task_result.TaskRunResult.started_ts).fetch_page( | 426 -task_result.TaskRunResult.started_ts).fetch_page( |
| 415 limit, start_cursor=cursor) | 427 limit, start_cursor=cursor) |
| 416 now = utils.utcnow() | 428 now = utils.utcnow() |
| 417 data = { | 429 data = { |
| 418 'cursor': cursor.urlsafe() if cursor and more else None, | 430 'cursor': cursor.urlsafe() if cursor and more else None, |
| 419 'items': run_results, | 431 'items': run_results, |
| 420 'limit': limit, | 432 'limit': limit, |
| 421 'now': now, | 433 'now': now, |
| 422 } | 434 } |
| 423 self.send_response(utils.to_json_encodable(data)) | 435 self.send_response(utils.to_json_encodable(data)) |
| 424 | 436 |
| 425 | 437 |
| 426 class ClientApiServer(auth.ApiHandler): | 438 class ClientApiServer(auth.ApiHandler): |
| 427 """Server details""" | 439 """Server details""" |
| 428 | 440 |
| 429 @auth.require(acl.is_privileged_user) | 441 @auth.require(acl.is_privileged_user) |
| 430 def get(self): | 442 def get(self): |
| 443 logging.error('Unexpected old client') |
| 431 data = { | 444 data = { |
| 432 'bot_version': bot_code.get_bot_version(self.request.host_url), | 445 'bot_version': bot_code.get_bot_version(self.request.host_url), |
| 433 } | 446 } |
| 434 self.send_response(utils.to_json_encodable(data)) | 447 self.send_response(utils.to_json_encodable(data)) |
| 435 | 448 |
| 436 | 449 |
| 437 class ClientRequestHandler(auth.ApiHandler): | 450 class ClientRequestHandler(auth.ApiHandler): |
| 438 """Creates a new request, returns the task id. | 451 """Creates a new request, returns the task id. |
| 439 | 452 |
| 440 Argument: | 453 Argument: |
| (...skipping 29 matching lines...) Expand all Loading... |
| 470 # task itself, e.g. what to run. | 483 # task itself, e.g. what to run. |
| 471 _REQUIRED_PROPERTIES_KEYS= frozenset( | 484 _REQUIRED_PROPERTIES_KEYS= frozenset( |
| 472 ['commands', 'data', 'dimensions', 'env', 'execution_timeout_secs', | 485 ['commands', 'data', 'dimensions', 'env', 'execution_timeout_secs', |
| 473 'io_timeout_secs']) | 486 'io_timeout_secs']) |
| 474 _EXPECTED_PROPERTIES_KEYS = frozenset( | 487 _EXPECTED_PROPERTIES_KEYS = frozenset( |
| 475 ['commands', 'data', 'dimensions', 'env', 'execution_timeout_secs', | 488 ['commands', 'data', 'dimensions', 'env', 'execution_timeout_secs', |
| 476 'grace_period_secs', 'idempotent', 'io_timeout_secs']) | 489 'grace_period_secs', 'idempotent', 'io_timeout_secs']) |
| 477 | 490 |
| 478 @auth.require(acl.is_bot_or_user) | 491 @auth.require(acl.is_bot_or_user) |
| 479 def post(self): | 492 def post(self): |
| 493 logging.error('Unexpected old client') |
| 480 data = self.parse_body() | 494 data = self.parse_body() |
| 481 msg = log_unexpected_subset_keys( | 495 msg = log_unexpected_subset_keys( |
| 482 self._EXPECTED_DATA_KEYS, self._REQUIRED_DATA_KEYS, data, self.request, | 496 self._EXPECTED_DATA_KEYS, self._REQUIRED_DATA_KEYS, data, self.request, |
| 483 'client', 'request keys') | 497 'client', 'request keys') |
| 484 if msg: | 498 if msg: |
| 485 self.abort_with_error(400, error=msg) | 499 self.abort_with_error(400, error=msg) |
| 486 data_properties = data['properties'] | 500 data_properties = data['properties'] |
| 487 msg = log_unexpected_subset_keys( | 501 msg = log_unexpected_subset_keys( |
| 488 self._EXPECTED_PROPERTIES_KEYS, self._REQUIRED_PROPERTIES_KEYS, | 502 self._EXPECTED_PROPERTIES_KEYS, self._REQUIRED_PROPERTIES_KEYS, |
| 489 data_properties, self.request, 'client', 'request properties keys') | 503 data_properties, self.request, 'client', 'request properties keys') |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 529 self.send_response(utils.to_json_encodable(data)) | 543 self.send_response(utils.to_json_encodable(data)) |
| 530 | 544 |
| 531 | 545 |
| 532 class ClientCancelHandler(auth.ApiHandler): | 546 class ClientCancelHandler(auth.ApiHandler): |
| 533 """Cancels a task.""" | 547 """Cancels a task.""" |
| 534 | 548 |
| 535 # TODO(maruel): Allow privileged users to cancel, and users to cancel their | 549 # TODO(maruel): Allow privileged users to cancel, and users to cancel their |
| 536 # own task. | 550 # own task. |
| 537 @auth.require(acl.is_admin) | 551 @auth.require(acl.is_admin) |
| 538 def post(self): | 552 def post(self): |
| 553 logging.error('Unexpected old client') |
| 539 request = self.parse_body() | 554 request = self.parse_body() |
| 540 task_id = request.get('task_id') | 555 task_id = request.get('task_id') |
| 541 summary_key = task_pack.unpack_result_summary_key(task_id) | 556 summary_key = task_pack.unpack_result_summary_key(task_id) |
| 542 | 557 |
| 543 ok, was_running = task_scheduler.cancel_task(summary_key) | 558 ok, was_running = task_scheduler.cancel_task(summary_key) |
| 544 out = { | 559 out = { |
| 545 'ok': ok, | 560 'ok': ok, |
| 546 'was_running': was_running, | 561 'was_running': was_running, |
| 547 } | 562 } |
| 548 self.send_response(out) | 563 self.send_response(out) |
| (...skipping 15 matching lines...) Expand all Loading... |
| 564 ClientTaskResultRequestHandler), | 579 ClientTaskResultRequestHandler), |
| 565 ('/swarming/api/v1/client/task/<task_id:[0-9a-f]+>/output/' | 580 ('/swarming/api/v1/client/task/<task_id:[0-9a-f]+>/output/' |
| 566 '<command_index:[0-9]+>', | 581 '<command_index:[0-9]+>', |
| 567 ClientTaskResultOutputHandler), | 582 ClientTaskResultOutputHandler), |
| 568 ('/swarming/api/v1/client/task/<task_id:[0-9a-f]+>/output/all', | 583 ('/swarming/api/v1/client/task/<task_id:[0-9a-f]+>/output/all', |
| 569 ClientTaskResultOutputAllHandler), | 584 ClientTaskResultOutputAllHandler), |
| 570 ('/swarming/api/v1/client/tasks', ClientApiTasksHandler), | 585 ('/swarming/api/v1/client/tasks', ClientApiTasksHandler), |
| 571 ('/swarming/api/v1/client/tasks/count', ClientApiTasksCountHandler), | 586 ('/swarming/api/v1/client/tasks/count', ClientApiTasksCountHandler), |
| 572 ] | 587 ] |
| 573 return [webapp2.Route(*i) for i in routes] | 588 return [webapp2.Route(*i) for i in routes] |
| OLD | NEW |