Back to home page

EIC code displayed by LXR

 
 

    


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 # Replace upstream's <div class="nav-auth">...</div> block with a locally-
0023 # rendered fragment so account/login/logout actions resolve to swf-remote
0024 # (devcloud) URLs, not upstream BNL URLs. Devcloud has its own user table.
0025 NAV_AUTH_RE = re.compile(rb'<div class="nav-auth">.*?</div>', re.DOTALL)
0026 
0027 # Inject an "Alarms" link at the end of the production-mode nav section
0028 # (swf-monitor's base.html wraps the production links in a
0029 # <span class="nav-mode nav-production">…</span>). swf-monitor itself
0030 # doesn't know about devcloud alarms, so we surface them here on the
0031 # proxied pages the same way we replace nav-auth.
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     # Pass authenticated user identity for attribution on swf-monitor
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         # Rewrite upstream paths to match our mount point.
0088         # /swf-monitor/X → {SCRIPT_NAME}/X (e.g. /prod/X)
0089         # Preserve absolute URLs to external hosts (e.g. pandaserver02).
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         # Force production mode — devcloud has no testbed toggle
0096         if b'navMode' in body:
0097             body = body.replace(
0098                 b"localStorage.getItem('navMode')",
0099                 b"'production'",
0100             )
0101         # Replace upstream's nav-auth section with a locally-rendered fragment.
0102         # Account management is autonomous on devcloud — login/logout/account
0103         # all resolve to local URLs against the local user table.
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         # Inject Alarms link at end of the production-mode nav section.
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         # Rewrite pandaserver-doma.cern.ch trf links through our text proxy
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 # ── Clean data accessors (for MCP, future) ──────────────────────────────────
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 # ── PCS data accessors ────────────────────────────────────────────────────
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}/')