| 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 611 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 622 self.send_response({}) | 622 self.send_response({}) |
| 623 | 623 |
| 624 | 624 |
| 625 class BotTaskUpdateHandler(_BotApiHandler): | 625 class BotTaskUpdateHandler(_BotApiHandler): |
| 626 """Receives updates from a Bot for a task. | 626 """Receives updates from a Bot for a task. |
| 627 | 627 |
| 628 The handler verifies packets are processed in order and will refuse | 628 The handler verifies packets are processed in order and will refuse |
| 629 out-of-order packets. | 629 out-of-order packets. |
| 630 """ | 630 """ |
| 631 ACCEPTED_KEYS = { | 631 ACCEPTED_KEYS = { |
| 632 u'bot_overhead', u'cipd_stats', u'cost_usd', u'duration', u'exit_code', | 632 u'bot_overhead', u'cipd_pins', u'cipd_stats', u'cost_usd', u'duration', |
| 633 u'hard_timeout', u'id', u'io_timeout', u'isolated_stats', u'output', | 633 u'exit_code', u'hard_timeout', u'id', u'io_timeout', u'isolated_stats', |
| 634 u'output_chunk_start', u'outputs_ref', u'task_id', | 634 u'output', u'output_chunk_start', u'outputs_ref', u'task_id', |
| 635 } | 635 } |
| 636 REQUIRED_KEYS = {u'id', u'task_id'} | 636 REQUIRED_KEYS = {u'id', u'task_id'} |
| 637 | 637 |
| 638 @auth.public # auth happens in bot_auth.validate_bot_id_and_fetch_config() | 638 @auth.public # auth happens in bot_auth.validate_bot_id_and_fetch_config() |
| 639 def post(self, task_id=None): | 639 def post(self, task_id=None): |
| 640 # Unlike handshake and poll, we do not accept invalid keys here. This code | 640 # Unlike handshake and poll, we do not accept invalid keys here. This code |
| 641 # path is much more strict. | 641 # path is much more strict. |
| 642 request = self.parse_body() | 642 request = self.parse_body() |
| 643 msg = log_unexpected_subset_keys( | 643 msg = log_unexpected_subset_keys( |
| 644 self.ACCEPTED_KEYS, self.REQUIRED_KEYS, request, self.request, 'bot', | 644 self.ACCEPTED_KEYS, self.REQUIRED_KEYS, request, self.request, 'bot', |
| 645 'keys') | 645 'keys') |
| 646 if msg: | 646 if msg: |
| 647 self.abort_with_error(400, error=msg) | 647 self.abort_with_error(400, error=msg) |
| 648 | 648 |
| 649 bot_id = request['id'] | 649 bot_id = request['id'] |
| 650 task_id = request['task_id'] | 650 task_id = request['task_id'] |
| 651 | 651 |
| 652 # Make sure bot self-reported ID matches the authentication token. Raises | 652 # Make sure bot self-reported ID matches the authentication token. Raises |
| 653 # auth.AuthorizationError if not. | 653 # auth.AuthorizationError if not. |
| 654 bot_auth.validate_bot_id_and_fetch_config(bot_id) | 654 bot_auth.validate_bot_id_and_fetch_config(bot_id) |
| 655 | 655 |
| 656 bot_overhead = request.get('bot_overhead') | 656 bot_overhead = request.get('bot_overhead') |
| 657 cipd_pins = request.get('cipd_pins') |
| 658 cipd_stats = request.get('cipd_stats') |
| 657 cost_usd = request.get('cost_usd', 0) | 659 cost_usd = request.get('cost_usd', 0) |
| 658 duration = request.get('duration') | 660 duration = request.get('duration') |
| 659 exit_code = request.get('exit_code') | 661 exit_code = request.get('exit_code') |
| 660 hard_timeout = request.get('hard_timeout') | 662 hard_timeout = request.get('hard_timeout') |
| 661 io_timeout = request.get('io_timeout') | 663 io_timeout = request.get('io_timeout') |
| 662 isolated_stats = request.get('isolated_stats') | 664 isolated_stats = request.get('isolated_stats') |
| 663 cipd_stats = request.get('cipd_stats') | |
| 664 output = request.get('output') | 665 output = request.get('output') |
| 665 output_chunk_start = request.get('output_chunk_start') | 666 output_chunk_start = request.get('output_chunk_start') |
| 666 outputs_ref = request.get('outputs_ref') | 667 outputs_ref = request.get('outputs_ref') |
| 667 | 668 |
| 668 if (isolated_stats or cipd_stats) and bot_overhead is None: | 669 if (isolated_stats or cipd_stats) and bot_overhead is None: |
| 669 ereporter2.log_request( | 670 ereporter2.log_request( |
| 670 request=self.request, | 671 request=self.request, |
| 671 source='server', | 672 source='server', |
| 672 category='task_failure', | 673 category='task_failure', |
| 673 message='Failed to update task: %s' % task_id) | 674 message='Failed to update task: %s' % task_id) |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 710 logging.error('Failed to decode output\n%s\n%r', e, output) | 711 logging.error('Failed to decode output\n%s\n%r', e, output) |
| 711 output = output.encode('ascii', 'replace') | 712 output = output.encode('ascii', 'replace') |
| 712 except TypeError as e: | 713 except TypeError as e: |
| 713 # Save the output as-is instead. The error will be logged in ereporter2 | 714 # Save the output as-is instead. The error will be logged in ereporter2 |
| 714 # and returning a HTTP 500 would only force the bot to stay in a retry | 715 # and returning a HTTP 500 would only force the bot to stay in a retry |
| 715 # loop. | 716 # loop. |
| 716 logging.error('Failed to decode output\n%s\n%r', e, output) | 717 logging.error('Failed to decode output\n%s\n%r', e, output) |
| 717 if outputs_ref: | 718 if outputs_ref: |
| 718 outputs_ref = task_request.FilesRef(**outputs_ref) | 719 outputs_ref = task_request.FilesRef(**outputs_ref) |
| 719 | 720 |
| 721 if cipd_pins: |
| 722 cipd_pins = [task_request.CipdPackage(**pin) for pin in cipd_pins] |
| 723 |
| 720 try: | 724 try: |
| 721 state = task_scheduler.bot_update_task( | 725 state = task_scheduler.bot_update_task( |
| 722 run_result_key=run_result_key, | 726 run_result_key=run_result_key, |
| 723 bot_id=bot_id, | 727 bot_id=bot_id, |
| 724 output=output, | 728 output=output, |
| 725 output_chunk_start=output_chunk_start, | 729 output_chunk_start=output_chunk_start, |
| 726 exit_code=exit_code, | 730 exit_code=exit_code, |
| 727 duration=duration, | 731 duration=duration, |
| 728 hard_timeout=hard_timeout, | 732 hard_timeout=hard_timeout, |
| 729 io_timeout=io_timeout, | 733 io_timeout=io_timeout, |
| 730 cost_usd=cost_usd, | 734 cost_usd=cost_usd, |
| 731 outputs_ref=outputs_ref, | 735 outputs_ref=outputs_ref, |
| 736 cipd_pins=cipd_pins, |
| 732 performance_stats=performance_stats) | 737 performance_stats=performance_stats) |
| 733 if not state: | 738 if not state: |
| 734 logging.info('Failed to update, please retry') | 739 logging.info('Failed to update, please retry') |
| 735 self.abort_with_error(500, error='Failed to update, please retry') | 740 self.abort_with_error(500, error='Failed to update, please retry') |
| 736 | 741 |
| 737 if state in (task_result.State.COMPLETED, task_result.State.TIMED_OUT): | 742 if state in (task_result.State.COMPLETED, task_result.State.TIMED_OUT): |
| 738 action = 'task_completed' | 743 action = 'task_completed' |
| 739 elif state == task_result.State.CANCELED: | 744 elif state == task_result.State.CANCELED: |
| 740 action = 'task_canceled' | 745 action = 'task_canceled' |
| 741 else: | 746 else: |
| (...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 837 ('/swarming/api/v1/bot/poll', BotPollHandler), | 842 ('/swarming/api/v1/bot/poll', BotPollHandler), |
| 838 ('/swarming/api/v1/bot/server_ping', ServerPingHandler), | 843 ('/swarming/api/v1/bot/server_ping', ServerPingHandler), |
| 839 ('/swarming/api/v1/bot/task_update', BotTaskUpdateHandler), | 844 ('/swarming/api/v1/bot/task_update', BotTaskUpdateHandler), |
| 840 ('/swarming/api/v1/bot/task_update/<task_id:[a-f0-9]+>', | 845 ('/swarming/api/v1/bot/task_update/<task_id:[a-f0-9]+>', |
| 841 BotTaskUpdateHandler), | 846 BotTaskUpdateHandler), |
| 842 ('/swarming/api/v1/bot/task_error', BotTaskErrorHandler), | 847 ('/swarming/api/v1/bot/task_error', BotTaskErrorHandler), |
| 843 ('/swarming/api/v1/bot/task_error/<task_id:[a-f0-9]+>', | 848 ('/swarming/api/v1/bot/task_error/<task_id:[a-f0-9]+>', |
| 844 BotTaskErrorHandler), | 849 BotTaskErrorHandler), |
| 845 ] | 850 ] |
| 846 return [webapp2.Route(*i) for i in routes] | 851 return [webapp2.Route(*i) for i in routes] |
| OLD | NEW |