File indexing completed on 2026-04-27 07:41:45
0001 """
0002 REST client for swf-monitor via SSH tunnel.
0003
0004 Two modes:
0005 - proxy(): forwards a Django request to swf-monitor and returns raw bytes/content-type.
0006 Used for DataTables AJAX and filter-counts (browser views).
0007 - _get(): fetches clean JSON dicts. Used by MCP tools (future).
0008 """
0009
0010 import logging
0011 import re
0012 import httpx
0013 from django.conf import settings
0014 from django.http import HttpResponse
0015 from django.template.loader import render_to_string
0016
0017 logger = logging.getLogger(__name__)
0018
0019 TIMEOUT = 30
0020 UPSTREAM_HEADERS = {'Host': 'pandaserver02.sdcc.bnl.gov'}
0021
0022
0023
0024
0025 NAV_AUTH_RE = re.compile(rb'<div class="nav-auth">.*?</div>', re.DOTALL)
0026
0027
0028
0029
0030
0031
0032 NAV_ALARMS_LINK = (
0033 b'<a href="/prod/alarms/" style="margin-left:1em;">Alarms</a>'
0034 )
0035 NAV_PROD_END_RE = re.compile(
0036 rb'(<span class="nav-mode nav-production">[\s\S]*?)(</span>)',
0037 re.DOTALL,
0038 )
0039
0040
0041 def _base():
0042 return settings.SWF_MONITOR_URL.rstrip('/')
0043
0044
0045 def proxy(request, path, service_user=None):
0046 """Proxy a request to swf-monitor, return an HttpResponse.
0047
0048 Forwards HTTP method, query parameters, request body, and authenticated
0049 user identity (via X-Remote-User header). Returns the upstream response
0050 as-is (content-type, status code, body) with URL rewriting.
0051
0052 service_user: fallback identity injected as X-Remote-User when no Django
0053 user is authenticated. Use for service-to-service endpoints that the
0054 upstream requires IsAuthenticated on (e.g. /api/panda/* viewsets).
0055 """
0056 url = f"{_base()}{path}"
0057 params = request.GET.dict()
0058 headers = dict(UPSTREAM_HEADERS)
0059
0060
0061 if hasattr(request, 'user') and request.user.is_authenticated:
0062 headers['X-Remote-User'] = request.user.username
0063 elif service_user:
0064 headers['X-Remote-User'] = service_user
0065
0066 method = request.method.upper()
0067 try:
0068 if method == 'GET':
0069 resp = httpx.get(url, params=params, timeout=TIMEOUT,
0070 verify=False, headers=headers)
0071 elif method in ('POST', 'PATCH', 'PUT'):
0072 ct = request.content_type or 'application/octet-stream'
0073 headers['Content-Type'] = ct
0074 resp = httpx.request(method, url, params=params, content=request.body,
0075 timeout=TIMEOUT, verify=False, headers=headers)
0076 elif method == 'DELETE':
0077 resp = httpx.delete(url, params=params, timeout=TIMEOUT,
0078 verify=False, headers=headers)
0079 else:
0080 return HttpResponse(
0081 f'{{"error": "Method {method} not supported"}}',
0082 status=405, content_type='application/json',
0083 )
0084
0085 body = resp.content
0086 ct = resp.headers.get('content-type', 'application/json')
0087
0088
0089
0090 prefix = (settings.FORCE_SCRIPT_NAME or '').encode()
0091 if b'/swf-monitor/' in body:
0092 body = body.replace(b'.gov/swf-monitor/', b'.gov/\x00SWF_PRESERVE\x00/')
0093 body = body.replace(b'/swf-monitor/', prefix + b'/')
0094 body = body.replace(b'.gov/\x00SWF_PRESERVE\x00/', b'.gov/swf-monitor/')
0095
0096 if b'navMode' in body:
0097 body = body.replace(
0098 b"localStorage.getItem('navMode')",
0099 b"'production'",
0100 )
0101
0102
0103
0104 if b'<div class="nav-auth">' in body:
0105 local_auth = render_to_string(
0106 'monitor_app/_nav_auth.html', request=request,
0107 ).encode('utf-8')
0108 body = NAV_AUTH_RE.sub(lambda m: local_auth, body, count=1)
0109
0110 if b'nav-mode nav-production' in body and b'/prod/alarms/' not in body:
0111 body = NAV_PROD_END_RE.sub(
0112 lambda m: m.group(1) + NAV_ALARMS_LINK + m.group(2),
0113 body, count=1,
0114 )
0115
0116 if b'pandaserver-doma.cern.ch/trf/' in body:
0117 body = body.replace(b'href="https://pandaserver-doma.cern.ch/trf/', b'href="' + prefix + b'/panda/view-text/?url=https://pandaserver-doma.cern.ch/trf/')
0118 body = body.replace(b'href=\\"https://pandaserver-doma.cern.ch/trf/', b'href=\\"' + prefix + b'/panda/view-text/?url=https://pandaserver-doma.cern.ch/trf/')
0119 return HttpResponse(body, status=resp.status_code, content_type=ct)
0120 except httpx.ConnectError as e:
0121 logger.error(f"Cannot reach swf-monitor at {url}: {e}")
0122 return HttpResponse(
0123 '{"error": "Cannot reach swf-monitor (tunnel down?)"}',
0124 status=502, content_type='application/json',
0125 )
0126 except Exception as e:
0127 logger.error(f"Proxy to {url} failed: {e}")
0128 return HttpResponse(
0129 f'{{"error": "{e}"}}',
0130 status=502, content_type='application/json',
0131 )
0132
0133
0134 def _get(path, params=None, as_user=None):
0135 """GET request to swf-monitor, return parsed JSON dict.
0136
0137 `as_user` sets X-Remote-User for TunnelAuthentication on endpoints that
0138 require auth (e.g. /api/users/). Pass a service username like
0139 'swf-remote-sync' when running from management commands without a
0140 Django request context.
0141 """
0142 url = f"{_base()}{path}"
0143 headers = dict(UPSTREAM_HEADERS)
0144 if as_user:
0145 headers['X-Remote-User'] = as_user
0146 try:
0147 resp = httpx.get(url, params=params, timeout=TIMEOUT, verify=False, headers=headers)
0148 resp.raise_for_status()
0149 return resp.json()
0150 except httpx.ConnectError as e:
0151 logger.error(f"Cannot reach swf-monitor at {url}: {e}")
0152 return {'error': 'Cannot reach swf-monitor (tunnel down?)'}
0153 except httpx.HTTPStatusError as e:
0154 logger.error(f"swf-monitor {e.response.status_code} for {url}")
0155 return {'error': f'Upstream error: {e.response.status_code}'}
0156 except Exception as e:
0157 logger.error(f"Request to {url} failed: {e}")
0158 return {'error': str(e)}
0159
0160
0161
0162
0163 def get_activity(**kwargs):
0164 return _get('/api/panda/activity/', kwargs)
0165
0166 def list_jobs(**kwargs):
0167 return _get('/api/panda/jobs/', kwargs)
0168
0169 def study_job(pandaid):
0170 return _get(f'/api/panda/jobs/{pandaid}/')
0171
0172 def diagnose_jobs(**kwargs):
0173 return _get('/api/panda/jobs/diagnose/', kwargs)
0174
0175 def error_summary(**kwargs):
0176 return _get('/api/panda/jobs/errors/', kwargs)
0177
0178 def list_tasks(**kwargs):
0179 return _get('/api/panda/tasks/', kwargs)
0180
0181 def get_task(jeditaskid):
0182 return _get(f'/api/panda/tasks/{jeditaskid}/')
0183
0184
0185
0186
0187 TAG_TYPE_MAP = {'p': 'physics-tags', 'e': 'evgen-tags', 's': 'simu-tags', 'r': 'reco-tags'}
0188
0189
0190 def list_tags(tag_type, **kwargs):
0191 endpoint = TAG_TYPE_MAP.get(tag_type, f'{tag_type}-tags')
0192 return _get(f'/pcs/api/{endpoint}/', kwargs)
0193
0194
0195 def get_tag(tag_type, tag_number):
0196 endpoint = TAG_TYPE_MAP.get(tag_type, f'{tag_type}-tags')
0197 return _get(f'/pcs/api/{endpoint}/{tag_number}/')
0198
0199
0200 def list_datasets(**kwargs):
0201 return _get('/pcs/api/datasets/', kwargs)
0202
0203
0204 def get_dataset(pk):
0205 return _get(f'/pcs/api/datasets/{pk}/')
0206
0207
0208 def list_prod_configs(**kwargs):
0209 return _get('/pcs/api/prod-configs/', kwargs)
0210
0211
0212 def get_prod_config(pk):
0213 return _get(f'/pcs/api/prod-configs/{pk}/')