File indexing completed on 2026-04-27 07:41:45
0001 """
0002 PanDA monitoring views for swf-remote (epic-devcloud.org).
0003
0004 Most pages proxy full rendered HTML from swf-monitor through the SSH tunnel.
0005 The hub page is rendered locally (devcloud-specific content).
0006 """
0007
0008 from django.contrib.auth import logout as auth_logout
0009 from django.contrib.auth.decorators import login_required
0010 from django.http import HttpResponse, JsonResponse
0011 from django.shortcuts import redirect, render
0012 from django.views.decorators.csrf import csrf_exempt
0013
0014 from . import monitor_client
0015
0016
0017
0018
0019 @csrf_exempt
0020 def logout_view(request):
0021 """Log out and redirect to home.
0022
0023 csrf_exempt because all pages are proxied from swf-monitor — the CSRF
0024 token in the logout form is swf-monitor's, which we can never validate.
0025 Logout is state-destroying so CSRF risk is negligible (worst case: attacker
0026 logs the user out).
0027 """
0028 auth_logout(request)
0029 return redirect('/prod/')
0030
0031
0032 @login_required
0033 def account(request):
0034 return render(request, 'monitor_app/account.html')
0035
0036
0037 def about(request):
0038 """About page — proxied from swf-monitor for content consistency."""
0039 return monitor_client.proxy(request, '/about/')
0040
0041
0042
0043
0044 def home(request):
0045 """Root — always production on devcloud."""
0046 from django.shortcuts import redirect
0047 from django.urls import reverse
0048 return redirect(reverse('monitor_app:prod_home'))
0049
0050
0051 def prod_home(request):
0052 return monitor_client.proxy(request, '/prod/')
0053
0054
0055 def testbed_home(request):
0056 return monitor_client.proxy(request, '/testbed/')
0057
0058
0059
0060
0061 def panda_activity(request):
0062 return monitor_client.proxy(request, '/panda/activity/')
0063
0064
0065 def panda_jobs_list(request):
0066 return monitor_client.proxy(request, '/panda/jobs/')
0067
0068
0069 def panda_jobs_datatable_ajax(request):
0070 return monitor_client.proxy(request, '/panda/jobs/datatable/')
0071
0072
0073 def panda_jobs_filter_counts(request):
0074 return monitor_client.proxy(request, '/panda/jobs/filter-counts/')
0075
0076
0077 def panda_job_detail(request, pandaid):
0078 return monitor_client.proxy(request, f'/panda/jobs/{pandaid}/')
0079
0080
0081 def panda_tasks_list(request):
0082 return monitor_client.proxy(request, '/panda/tasks/')
0083
0084
0085 def panda_tasks_datatable_ajax(request):
0086 return monitor_client.proxy(request, '/panda/tasks/datatable/')
0087
0088
0089 def panda_tasks_filter_counts(request):
0090 return monitor_client.proxy(request, '/panda/tasks/filter-counts/')
0091
0092
0093 def panda_task_detail(request, jeditaskid):
0094 return monitor_client.proxy(request, f'/panda/tasks/{jeditaskid}/')
0095
0096
0097 def panda_errors_list(request):
0098 return monitor_client.proxy(request, '/panda/errors/')
0099
0100
0101 def panda_errors_datatable_ajax(request):
0102 return monitor_client.proxy(request, '/panda/errors/datatable/')
0103
0104
0105 def panda_diagnostics_list(request):
0106 return monitor_client.proxy(request, '/panda/diagnostics/')
0107
0108
0109 def panda_diagnostics_datatable_ajax(request):
0110 return monitor_client.proxy(request, '/panda/diagnostics/datatable/')
0111
0112
0113
0114
0115
0116
0117 def pcs_proxy(request, **kwargs):
0118 """Proxy any PCS page to swf-monitor based on request path."""
0119 path = request.path_info
0120 return monitor_client.proxy(request, path)
0121
0122
0123 @csrf_exempt
0124 def pcs_api_proxy(request, path):
0125 """Proxy PCS REST API requests.
0126
0127 GET is public. Write methods (POST/PATCH/DELETE) require login —
0128 the user's identity is forwarded to swf-monitor via X-Remote-User.
0129 CSRF is exempted here because swf-monitor's API uses token auth,
0130 not session+CSRF.
0131 """
0132 if request.method != 'GET' and not request.user.is_authenticated:
0133 return JsonResponse({'error': 'Login required'}, status=401)
0134 return monitor_client.proxy(request, f'/pcs/api/{path}')
0135
0136
0137 def panda_api_proxy(request, path):
0138 """Proxy PanDA REST API requests to swf-monitor /api/panda/<path>.
0139
0140 Read-only: GET only. Upstream requires IsAuthenticated, so we inject
0141 a service identity when no Django user is logged in — the alarm engine
0142 and other service consumers hit this anonymously from localhost and
0143 appear to upstream as 'swf-remote-proxy'.
0144 """
0145 if request.method != 'GET':
0146 return JsonResponse({'error': 'GET only'}, status=405)
0147 return monitor_client.proxy(
0148 request, f'/api/panda/{path}', service_user='swf-remote-proxy'
0149 )
0150
0151
0152
0153
0154
0155 def epic_queues_list(request):
0156 return monitor_client.proxy(request, '/panda/epic-queues/')
0157
0158
0159 def epic_queue_detail(request, queue_name):
0160 return monitor_client.proxy(request, f'/panda/epic-queues/{queue_name}/')
0161
0162
0163 def static_proxy(request, path):
0164 """Proxy static assets from swf-monitor — CSS, JS always in sync."""
0165 return monitor_client.proxy(request, f'/static/{path}')
0166
0167
0168 from .alarm_views import (
0169 alarms_dashboard,
0170 alarm_event_detail,
0171 alarm_config_edit,
0172 alarm_config_save,
0173 alarm_config_version,
0174 alarm_test,
0175 alarm_run_report,
0176 alarm_task_history,
0177 team_create,
0178 team_new,
0179 team_edit,
0180 team_save,
0181 team_version,
0182 )
0183
0184
0185 def panda_view_text(request):
0186 """Fetch a PanDA transformation URL — self-extracting zip with embedded scripts.
0187
0188 Extracts the bash header and all text files from the zip, presents them
0189 as readable plain text.
0190 """
0191 import httpx
0192 import io
0193 import zipfile
0194 url = request.GET.get('url', '')
0195 if not url or not url.startswith('https://'):
0196 return HttpResponse('Missing or invalid url parameter', status=400, content_type='text/plain')
0197 try:
0198 resp = httpx.get(url, timeout=15, follow_redirects=True)
0199 except Exception as e:
0200 return HttpResponse(f'Failed to fetch: {e}', status=502, content_type='text/plain')
0201 data = resp.content
0202 parts = []
0203
0204 try:
0205 lines = []
0206 for line in data.split(b'\n'):
0207 try:
0208 lines.append(line.decode('utf-8'))
0209 except UnicodeDecodeError:
0210 break
0211 if lines:
0212 parts.append(f'=== Shell header ({len(lines)} lines) ===\n')
0213 parts.append('\n'.join(lines))
0214 except Exception:
0215 pass
0216
0217 try:
0218 buf = io.BytesIO(data)
0219 with zipfile.ZipFile(buf) as zf:
0220 for name in zf.namelist():
0221 try:
0222 content = zf.read(name).decode('utf-8')
0223 parts.append(f'\n\n=== {name} ===\n')
0224 parts.append(content)
0225 except (UnicodeDecodeError, KeyError):
0226 parts.append(f'\n\n=== {name} (binary, skipped) ===\n')
0227 except zipfile.BadZipFile:
0228 if not parts:
0229
0230 parts.append(data.decode('utf-8', errors='replace'))
0231 return HttpResponse(''.join(parts), content_type='text/plain; charset=utf-8')