| 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 """Internal bot API handlers.""" | 5 """Internal bot API handlers.""" |
| 6 | 6 |
| 7 import base64 | 7 import base64 |
| 8 import json | 8 import json |
| 9 import logging | 9 import logging |
| 10 import re | 10 import re |
| (...skipping 486 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 497 self.send_response({}) | 497 self.send_response({}) |
| 498 | 498 |
| 499 | 499 |
| 500 class BotTaskUpdateHandler(_BotApiHandler): | 500 class BotTaskUpdateHandler(_BotApiHandler): |
| 501 """Receives updates from a Bot for a task. | 501 """Receives updates from a Bot for a task. |
| 502 | 502 |
| 503 The handler verifies packets are processed in order and will refuse | 503 The handler verifies packets are processed in order and will refuse |
| 504 out-of-order packets. | 504 out-of-order packets. |
| 505 """ | 505 """ |
| 506 ACCEPTED_KEYS = { | 506 ACCEPTED_KEYS = { |
| 507 u'bot_overhead', u'cost_usd', u'duration', u'exit_code', | 507 u'bot_overhead', u'cipd_stats', u'cost_usd', u'duration', u'exit_code', |
| 508 u'hard_timeout', u'id', u'io_timeout', u'isolated_stats', u'output', | 508 u'hard_timeout', u'id', u'io_timeout', u'isolated_stats', u'output', |
| 509 u'output_chunk_start', u'outputs_ref', u'task_id', | 509 u'output_chunk_start', u'outputs_ref', u'task_id', |
| 510 } | 510 } |
| 511 REQUIRED_KEYS = {u'id', u'task_id'} | 511 REQUIRED_KEYS = {u'id', u'task_id'} |
| 512 | 512 |
| 513 @auth.require(acl.is_bot) | 513 @auth.require(acl.is_bot) |
| 514 def post(self, task_id=None): | 514 def post(self, task_id=None): |
| 515 # Unlike handshake and poll, we do not accept invalid keys here. This code | 515 # Unlike handshake and poll, we do not accept invalid keys here. This code |
| 516 # path is much more strict. | 516 # path is much more strict. |
| 517 request = self.parse_body() | 517 request = self.parse_body() |
| 518 msg = log_unexpected_subset_keys( | 518 msg = log_unexpected_subset_keys( |
| 519 self.ACCEPTED_KEYS, self.REQUIRED_KEYS, request, self.request, 'bot', | 519 self.ACCEPTED_KEYS, self.REQUIRED_KEYS, request, self.request, 'bot', |
| 520 'keys') | 520 'keys') |
| 521 if msg: | 521 if msg: |
| 522 self.abort_with_error(400, error=msg) | 522 self.abort_with_error(400, error=msg) |
| 523 | 523 |
| 524 bot_id = request['id'] | 524 bot_id = request['id'] |
| 525 cost_usd = request['cost_usd'] | 525 cost_usd = request['cost_usd'] |
| 526 task_id = request['task_id'] | 526 task_id = request['task_id'] |
| 527 | 527 |
| 528 # Make sure bot self-reported ID matches the authentication token. | 528 # Make sure bot self-reported ID matches the authentication token. |
| 529 bot_auth.validate_bot_id(bot_id) | 529 bot_auth.validate_bot_id(bot_id) |
| 530 | 530 |
| 531 bot_overhead = request.get('bot_overhead') | 531 bot_overhead = request.get('bot_overhead') |
| 532 duration = request.get('duration') | 532 duration = request.get('duration') |
| 533 exit_code = request.get('exit_code') | 533 exit_code = request.get('exit_code') |
| 534 hard_timeout = request.get('hard_timeout') | 534 hard_timeout = request.get('hard_timeout') |
| 535 io_timeout = request.get('io_timeout') | 535 io_timeout = request.get('io_timeout') |
| 536 isolated_stats = request.get('isolated_stats') | 536 isolated_stats = request.get('isolated_stats') |
| 537 cipd_stats = request.get('cipd_stats') |
| 537 output = request.get('output') | 538 output = request.get('output') |
| 538 output_chunk_start = request.get('output_chunk_start') | 539 output_chunk_start = request.get('output_chunk_start') |
| 539 outputs_ref = request.get('outputs_ref') | 540 outputs_ref = request.get('outputs_ref') |
| 540 | 541 |
| 541 if isolated_stats and bot_overhead is None: | 542 if (isolated_stats or cipd_stats) and bot_overhead is None: |
| 542 ereporter2.log_request( | 543 ereporter2.log_request( |
| 543 request=self.request, | 544 request=self.request, |
| 544 source='server', | 545 source='server', |
| 545 category='task_failure', | 546 category='task_failure', |
| 546 message='Failed to update task: %s' % task_id) | 547 message='Failed to update task: %s' % task_id) |
| 547 self.abort_with_error( | 548 self.abort_with_error( |
| 548 400, | 549 400, |
| 549 error='isolated_stats requires bot_overhead to be set' | 550 error='isolated_stats and cipd_stats require bot_overhead to be set' |
| 550 '\nbot_overhead: %s\nisolated_stats: %s' % | 551 '\nbot_overhead: %s\nisolate_stats: %s' % |
| 551 (bot_overhead, isolated_stats)) | 552 (bot_overhead, isolated_stats)) |
| 552 | 553 |
| 553 run_result_key = task_pack.unpack_run_result_key(task_id) | 554 run_result_key = task_pack.unpack_run_result_key(task_id) |
| 554 performance_stats = None | 555 performance_stats = None |
| 555 if bot_overhead: | 556 if bot_overhead: |
| 556 performance_stats = task_result.PerformanceStats( | 557 performance_stats = task_result.PerformanceStats( |
| 557 bot_overhead=bot_overhead) | 558 bot_overhead=bot_overhead) |
| 558 if isolated_stats: | 559 if isolated_stats: |
| 559 download = isolated_stats.get('download') or {} | 560 download = isolated_stats.get('download') or {} |
| 560 upload = isolated_stats.get('upload') or {} | 561 upload = isolated_stats.get('upload') or {} |
| 561 def unpack_base64(d, k): | 562 def unpack_base64(d, k): |
| 562 x = d.get(k) | 563 x = d.get(k) |
| 563 if x: | 564 if x: |
| 564 return base64.b64decode(x) | 565 return base64.b64decode(x) |
| 565 performance_stats.isolated_download = task_result.OperationStats( | 566 performance_stats.isolated_download = task_result.OperationStats( |
| 566 duration=download.get('duration'), | 567 duration=download.get('duration'), |
| 567 initial_number_items=download.get('initial_number_items'), | 568 initial_number_items=download.get('initial_number_items'), |
| 568 initial_size=download.get('initial_size'), | 569 initial_size=download.get('initial_size'), |
| 569 items_cold=unpack_base64(download, 'items_cold'), | 570 items_cold=unpack_base64(download, 'items_cold'), |
| 570 items_hot=unpack_base64(download, 'items_hot')) | 571 items_hot=unpack_base64(download, 'items_hot')) |
| 571 performance_stats.isolated_upload = task_result.OperationStats( | 572 performance_stats.isolated_upload = task_result.OperationStats( |
| 572 duration=upload.get('duration'), | 573 duration=upload.get('duration'), |
| 573 items_cold=unpack_base64(upload, 'items_cold'), | 574 items_cold=unpack_base64(upload, 'items_cold'), |
| 574 items_hot=unpack_base64(upload, 'items_hot')) | 575 items_hot=unpack_base64(upload, 'items_hot')) |
| 576 if cipd_stats: |
| 577 performance_stats.package_installation = task_result.OperationStats( |
| 578 duration=cipd_stats.get('duration')) |
| 575 | 579 |
| 576 if output is not None: | 580 if output is not None: |
| 577 try: | 581 try: |
| 578 output = base64.b64decode(output) | 582 output = base64.b64decode(output) |
| 579 except UnicodeEncodeError as e: | 583 except UnicodeEncodeError as e: |
| 580 logging.error('Failed to decode output\n%s\n%r', e, output) | 584 logging.error('Failed to decode output\n%s\n%r', e, output) |
| 581 output = output.encode('ascii', 'replace') | 585 output = output.encode('ascii', 'replace') |
| 582 except TypeError as e: | 586 except TypeError as e: |
| 583 # Save the output as-is instead. The error will be logged in ereporter2 | 587 # Save the output as-is instead. The error will be logged in ereporter2 |
| 584 # and returning a HTTP 500 would only force the bot to stay in a retry | 588 # and returning a HTTP 500 would only force the bot to stay in a retry |
| (...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 705 ('/swarming/api/v1/bot/poll', BotPollHandler), | 709 ('/swarming/api/v1/bot/poll', BotPollHandler), |
| 706 ('/swarming/api/v1/bot/server_ping', ServerPingHandler), | 710 ('/swarming/api/v1/bot/server_ping', ServerPingHandler), |
| 707 ('/swarming/api/v1/bot/task_update', BotTaskUpdateHandler), | 711 ('/swarming/api/v1/bot/task_update', BotTaskUpdateHandler), |
| 708 ('/swarming/api/v1/bot/task_update/<task_id:[a-f0-9]+>', | 712 ('/swarming/api/v1/bot/task_update/<task_id:[a-f0-9]+>', |
| 709 BotTaskUpdateHandler), | 713 BotTaskUpdateHandler), |
| 710 ('/swarming/api/v1/bot/task_error', BotTaskErrorHandler), | 714 ('/swarming/api/v1/bot/task_error', BotTaskErrorHandler), |
| 711 ('/swarming/api/v1/bot/task_error/<task_id:[a-f0-9]+>', | 715 ('/swarming/api/v1/bot/task_error/<task_id:[a-f0-9]+>', |
| 712 BotTaskErrorHandler), | 716 BotTaskErrorHandler), |
| 713 ] | 717 ] |
| 714 return [webapp2.Route(*i) for i in routes] | 718 return [webapp2.Route(*i) for i in routes] |
| OLD | NEW |