File indexing completed on 2026-06-13 08:42:16
0001 """corun job notification callback endpoint for PanDA bot Mattermost notices."""
0002
0003 import json
0004 import logging
0005
0006 from django.views.decorators.csrf import csrf_exempt
0007 from django.views.decorators.http import require_POST
0008 from django.http import JsonResponse
0009 from decouple import config
0010 from mattermostdriver import Driver
0011
0012 logger = logging.getLogger('panda_bot')
0013
0014
0015 def _short_text(value, limit=200):
0016 if value is None:
0017 return ''
0018 text = str(value).strip()
0019 if len(text) <= limit:
0020 return text
0021 return text[:limit - 3].rstrip() + '...'
0022
0023
0024 def _format_duration(timing):
0025 if timing in (None, ''):
0026 return ''
0027 if isinstance(timing, dict):
0028 for key in ('duration_s', 'elapsed_s', 'seconds', 'total_seconds'):
0029 if key in timing:
0030 timing = timing[key]
0031 break
0032 else:
0033 return ''
0034 try:
0035 total_seconds = int(round(float(timing)))
0036 except (TypeError, ValueError):
0037 return ''
0038 if total_seconds < 0:
0039 return ''
0040
0041 hours, remainder = divmod(total_seconds, 3600)
0042 minutes, seconds = divmod(remainder, 60)
0043 if hours:
0044 return f"{hours}h{minutes}m{seconds}s"
0045 if minutes:
0046 return f"{minutes}m{seconds}s"
0047 return f"{seconds}s"
0048
0049
0050 def _mattermost_driver():
0051 return Driver({
0052 'url': config('MATTERMOST_URL', default='chat.epic-eic.org'),
0053 'token': config('MATTERMOST_TOKEN'),
0054 'scheme': 'https',
0055 'port': 443,
0056 })
0057
0058
0059 def _message_from_payload(payload):
0060 status = payload.get('status', 'unknown')
0061 display_name = (
0062 payload.get('result_page_title')
0063 or payload.get('definition_name')
0064 or payload.get('definition_id')
0065 or 'corun job'
0066 )
0067 result_url = payload.get('result_page_url')
0068 submitted_by = _short_text(payload.get('submitted_by'), limit=80)
0069 duration = _format_duration(payload.get('timing'))
0070 error = payload.get('error')
0071
0072 title = f"corun job {status}: {_short_text(display_name)}"
0073 lines = [f"**{title}**"]
0074 if result_url:
0075 lines.append(f"Result: {result_url}")
0076 if submitted_by:
0077 lines.append(f"Submitted by: {submitted_by}")
0078 if duration:
0079 lines.append(f"Duration: {duration}")
0080 if error:
0081 lines.append(f"Error: `{str(error)[:1000]}`")
0082 return "\n".join(lines)
0083
0084
0085 @csrf_exempt
0086 @require_POST
0087 def corun_callback(request):
0088 """Receive corun terminal-job callbacks and post them to the pandabot channel."""
0089 try:
0090 if int(request.META.get('CONTENT_LENGTH') or 0) > 8192:
0091 return JsonResponse({'error': 'payload too large'}, status=413)
0092 except (TypeError, ValueError):
0093 pass
0094
0095 try:
0096 payload = json.loads(request.body.decode('utf-8'))
0097 except (UnicodeDecodeError, json.JSONDecodeError) as exc:
0098 return JsonResponse({'error': f'invalid JSON: {exc}'}, status=400)
0099
0100 if not isinstance(payload, dict):
0101 return JsonResponse({'error': 'payload must be a JSON object'}, status=400)
0102
0103 status = payload.get('status')
0104 if status not in {'completed', 'failed', 'cancelled'}:
0105 return JsonResponse({'error': 'ignored non-terminal status'}, status=400)
0106
0107 try:
0108 driver = _mattermost_driver()
0109 driver.login()
0110 team = driver.teams.get_team_by_name(config('MATTERMOST_TEAM', default='main'))
0111 channel = driver.channels.get_channel_by_name(
0112 team['id'], config('MATTERMOST_CHANNEL', default='pandabot')
0113 )
0114 driver.posts.create_post(options={
0115 'channel_id': channel['id'],
0116 'message': _message_from_payload(payload),
0117 })
0118 except Exception as exc:
0119 logger.exception("Failed to post corun callback to Mattermost")
0120 return JsonResponse({'error': str(exc)}, status=502)
0121
0122 logger.info(
0123 "Posted corun callback notice for job %s status=%s",
0124 payload.get('job_id'), status,
0125 )
0126 return JsonResponse({'ok': True})