File indexing completed on 2026-04-27 07:41:43
0001 """
0002 PanDA Production Monitor views.
0003
0004 Web views for ePIC PanDA production monitoring — jobs, tasks, errors,
0005 activity overview, and detail pages with rich cross-linking.
0006 """
0007
0008 from django.shortcuts import render
0009 from django.contrib.auth.decorators import login_required
0010 from django.http import JsonResponse, HttpResponse
0011 from django.urls import reverse
0012
0013 from datetime import datetime
0014 from zoneinfo import ZoneInfo
0015
0016 from ..utils import DataTablesProcessor
0017 from ..panda import (
0018 get_activity, study_job, list_jobs,
0019 list_jobs_dt, list_tasks_dt,
0020 job_filter_counts, task_filter_counts,
0021 get_task, error_summary, diagnose_jobs,
0022 list_queues, get_queue,
0023 )
0024 from ..panda.constants import LIST_FIELDS, TASK_LIST_FIELDS
0025
0026
0027
0028
0029 JOB_COLUMNS = [
0030 {'name': 'pandaid', 'title': 'PanDA ID', 'orderable': True},
0031 {'name': 'jeditaskid', 'title': 'Task ID', 'orderable': True},
0032 {'name': 'produsername', 'title': 'User', 'orderable': True},
0033 {'name': 'jobstatus', 'title': 'Status', 'orderable': True},
0034 {'name': 'computingsite', 'title': 'Site', 'orderable': True},
0035 {'name': 'transformation', 'title': 'Transformation', 'orderable': True},
0036 {'name': 'creationtime', 'title': 'Created', 'orderable': True},
0037 {'name': 'endtime', 'title': 'Ended', 'orderable': True},
0038 {'name': 'corecount', 'title': 'Cores', 'orderable': True},
0039 ]
0040
0041 JOB_FIELD_NAMES = [c['name'] for c in JOB_COLUMNS]
0042
0043
0044 JOB_ORDER_MAP = {
0045 0: '"pandaid"', 1: '"jeditaskid"', 2: '"produsername"',
0046 3: '"jobstatus"', 4: '"computingsite"', 5: '"transformation"',
0047 6: '"creationtime"', 7: '"endtime"', 8: '"corecount"',
0048 }
0049
0050 TASK_COLUMNS = [
0051 {'name': 'jeditaskid', 'title': 'Task ID', 'orderable': True},
0052 {'name': 'taskname', 'title': 'Task Name', 'orderable': True},
0053 {'name': 'status', 'title': 'Status', 'orderable': True},
0054 {'name': 'username', 'title': 'User', 'orderable': True},
0055 {'name': 'workinggroup', 'title': 'Working Group', 'orderable': True},
0056 {'name': 'creationdate', 'title': 'Created', 'orderable': True},
0057 {'name': 'modificationtime', 'title': 'Modified', 'orderable': True},
0058 {'name': 'progress', 'title': 'Progress', 'orderable': True},
0059 {'name': 'failurerate', 'title': 'Failure Rate', 'orderable': True},
0060 ]
0061
0062 TASK_FIELD_NAMES = [c['name'] for c in TASK_COLUMNS]
0063
0064 TASK_ORDER_MAP = {
0065 0: '"jeditaskid"', 1: '"taskname"', 2: '"status"',
0066 3: '"username"', 4: '"workinggroup"', 5: '"creationdate"',
0067 6: '"modificationtime"', 7: '"progress"', 8: '"failurerate"',
0068 }
0069
0070 ERROR_COLUMNS = [
0071 {'name': 'error_source', 'title': 'Component', 'orderable': False},
0072 {'name': 'error_code', 'title': 'Code', 'orderable': False},
0073 {'name': 'error_diag', 'title': 'Diagnostic', 'orderable': False},
0074 {'name': 'count', 'title': 'Count', 'orderable': False},
0075 {'name': 'task_count', 'title': 'Tasks', 'orderable': False},
0076 {'name': 'users', 'title': 'Users', 'orderable': False},
0077 {'name': 'sites', 'title': 'Sites', 'orderable': False},
0078 ]
0079
0080 DIAG_COLUMNS = [
0081 {'name': 'pandaid', 'title': 'PanDA ID', 'orderable': False},
0082 {'name': 'jeditaskid', 'title': 'Task ID', 'orderable': False},
0083 {'name': 'produsername', 'title': 'User', 'orderable': False},
0084 {'name': 'jobstatus', 'title': 'Status', 'orderable': False},
0085 {'name': 'computingsite', 'title': 'Site', 'orderable': False},
0086 {'name': 'errors', 'title': 'Errors', 'orderable': False},
0087 {'name': 'endtime', 'title': 'Ended', 'orderable': False},
0088 ]
0089
0090
0091
0092
0093 _EASTERN = ZoneInfo('America/New_York')
0094
0095
0096 def _linkify(text):
0097 """Wrap text in an <a> tag if it looks like a URL.
0098
0099 TRF links (pandaserver-doma.cern.ch/trf/) are routed through our
0100 view-text endpoint which extracts readable content from self-extracting zips.
0101 """
0102 if text and text.startswith(('http://', 'https://')):
0103 href = text
0104 if 'pandaserver-doma.cern.ch/trf/' in text:
0105 href = reverse('monitor_app:panda_view_text') + '?url=' + text
0106 return f'<a href="{href}" target="_blank" rel="noopener">{text}</a>'
0107 return text
0108
0109
0110 def _fmt_dt(val):
0111 """Format an ISO datetime string or datetime object for display."""
0112 if not val:
0113 return ''
0114 if isinstance(val, str):
0115 try:
0116 val = datetime.fromisoformat(val)
0117 except (ValueError, TypeError):
0118 return val
0119 return val.astimezone(_EASTERN).strftime('%Y%m%d %H:%M:%S')
0120
0121
0122 STATUS_COLORS = {
0123 'finished': '#28a745', 'done': '#28a745',
0124 'failed': '#dc3545', 'broken': '#dc3545',
0125 'running': '#007bff',
0126 'activated': '#17a2b8', 'ready': '#17a2b8',
0127 'cancelled': '#6c757d', 'aborted': '#6c757d', 'closed': '#6c757d',
0128 'exhausted': '#fd7e14',
0129 }
0130
0131
0132 def _status_badge(status, url=None):
0133 """Render a status as a colored badge, optionally linked."""
0134 color = STATUS_COLORS.get(status, '#6c757d')
0135 badge = (f'<span style="background:{color};color:#fff;padding:2px 8px;'
0136 f'border-radius:3px;font-size:0.85em;">{status}</span>')
0137 if url:
0138 return f'<a href="{url}" style="text-decoration:none;">{badge}</a>'
0139 return badge
0140
0141
0142 DAYS_OPTIONS = [1, 3, 7, 14, 30]
0143
0144
0145 def _get_days(request):
0146 """Extract days parameter from request, default 7."""
0147 try:
0148 return int(request.GET.get('days', 7))
0149 except (ValueError, TypeError):
0150 return 7
0151
0152
0153 def _days_context(days):
0154 """Build days selector context for templates."""
0155 return {
0156 'days': days,
0157 'days_options': [{'value': d, 'active': d == days} for d in DAYS_OPTIONS],
0158 }
0159
0160
0161
0162
0163 @login_required
0164 def panda_activity(request):
0165 days = _get_days(request)
0166 data = get_activity(days=days)
0167 if 'error' in data:
0168 ctx = {'error': data['error']}
0169 ctx.update(_days_context(days))
0170 return render(request, 'monitor_app/panda_activity.html', ctx)
0171 data.update(_days_context(days))
0172 return render(request, 'monitor_app/panda_activity.html', data)
0173
0174
0175
0176
0177 @login_required
0178 def panda_jobs_list(request):
0179 days = _get_days(request)
0180 selected_site = request.GET.get('site', '')
0181 description = f'Production jobs from the last {days} days.'
0182 if selected_site:
0183 site_url = reverse('monitor_app:epic_queue_detail', args=[selected_site])
0184 description += f'<br><a href="{site_url}">Site info for <strong>{selected_site}</strong></a>'
0185
0186 context = {
0187 'table_title': 'PanDA Jobs',
0188 'table_description': description,
0189 'ajax_url': reverse('monitor_app:panda_jobs_datatable_ajax'),
0190 'filter_counts_url': reverse('monitor_app:panda_jobs_filter_counts'),
0191 'columns': JOB_COLUMNS,
0192 'filter_fields': [
0193 {'name': 'status', 'label': 'Status', 'type': 'select'},
0194 {'name': 'username', 'label': 'User', 'type': 'select'},
0195 {'name': 'site', 'label': 'Site', 'type': 'select'},
0196 ],
0197 'selected_status': request.GET.get('status', ''),
0198 'selected_username': request.GET.get('username', ''),
0199 'selected_site': request.GET.get('site', ''),
0200 'selected_taskid': request.GET.get('taskid', ''),
0201 }
0202 context.update(_days_context(days))
0203 return render(request, 'monitor_app/panda_jobs_list.html', context)
0204
0205
0206 @login_required
0207 def panda_jobs_datatable_ajax(request):
0208 dt = DataTablesProcessor(request, JOB_FIELD_NAMES,
0209 default_order_column=0, default_order_direction='desc')
0210 days = _get_days(request)
0211 status = request.GET.get('status', '') or None
0212 username = request.GET.get('username', '') or None
0213 site = request.GET.get('site', '') or None
0214 taskid = request.GET.get('taskid', '') or None
0215 reqid = request.GET.get('reqid', '') or None
0216
0217 order_col = JOB_ORDER_MAP.get(dt.order_column_idx, '"pandaid"')
0218 order_dir = 'ASC' if dt.order_direction == 'asc' else 'DESC'
0219 order_by = f'{order_col} {order_dir}'
0220
0221 rows, total, filtered = list_jobs_dt(
0222 days=days, status=status, username=username, site=site,
0223 taskid=taskid, reqid=reqid,
0224 order_by=order_by, limit=dt.length, offset=dt.start,
0225 search=dt.search_value or None,
0226 )
0227
0228 data = []
0229 for job in rows:
0230 job_url = reverse('monitor_app:panda_job_detail', args=[job['pandaid']])
0231 task_url = reverse('monitor_app:panda_task_detail', args=[job['jeditaskid']]) if job.get('jeditaskid') else None
0232 jobs_by_user_url = reverse('monitor_app:panda_jobs_list') + f'?days={days}&username={job["produsername"]}' if job.get('produsername') else None
0233 jobs_by_site_url = reverse('monitor_app:panda_jobs_list') + f'?days={days}&site={job["computingsite"]}' if job.get('computingsite') else None
0234 jobs_by_status_url = reverse('monitor_app:panda_jobs_list') + f'?days={days}&status={job["jobstatus"]}' if job.get('jobstatus') else None
0235
0236 data.append([
0237 f'<a href="{job_url}">{job["pandaid"]}</a>',
0238 f'<a href="{task_url}">{job["jeditaskid"]}</a>' if task_url else str(job.get('jeditaskid', '')),
0239 f'<a href="{jobs_by_user_url}">{job["produsername"]}</a>' if jobs_by_user_url else '',
0240 _status_badge(job['jobstatus'], jobs_by_status_url) if job.get('jobstatus') else '',
0241 f'<a href="{jobs_by_site_url}">{job["computingsite"]}</a>' if jobs_by_site_url else '',
0242 _linkify(job.get('transformation', '') or ''),
0243 _fmt_dt(job.get('creationtime')),
0244 _fmt_dt(job.get('endtime')),
0245 str(job.get('corecount', '') or ''),
0246 ])
0247
0248 return dt.create_response(data, total, filtered)
0249
0250
0251 @login_required
0252 def panda_jobs_filter_counts(request):
0253 days = _get_days(request)
0254 status = request.GET.get('status', '') or None
0255 username = request.GET.get('username', '') or None
0256 site = request.GET.get('site', '') or None
0257 taskid = request.GET.get('taskid', '') or None
0258 reqid = request.GET.get('reqid', '') or None
0259
0260 counts = job_filter_counts(days=days, status=status, username=username,
0261 site=site, taskid=taskid, reqid=reqid)
0262 return JsonResponse({'filter_counts': counts})
0263
0264
0265
0266
0267 @login_required
0268 def panda_tasks_list(request):
0269 days = _get_days(request)
0270 context = {
0271 'table_title': 'PanDA Tasks',
0272 'table_description': f'JEDI tasks from the last {days} days.',
0273 'ajax_url': reverse('monitor_app:panda_tasks_datatable_ajax'),
0274 'filter_counts_url': reverse('monitor_app:panda_tasks_filter_counts'),
0275 'columns': TASK_COLUMNS,
0276 'filter_fields': [
0277 {'name': 'status', 'label': 'Status', 'type': 'select'},
0278 {'name': 'username', 'label': 'User', 'type': 'select'},
0279 {'name': 'workinggroup', 'label': 'Working Group', 'type': 'select'},
0280 ],
0281 'selected_status': request.GET.get('status', ''),
0282 'selected_username': request.GET.get('username', ''),
0283 'selected_workinggroup': request.GET.get('workinggroup', ''),
0284 }
0285 context.update(_days_context(days))
0286 return render(request, 'monitor_app/panda_tasks_list.html', context)
0287
0288
0289 @login_required
0290 def panda_tasks_datatable_ajax(request):
0291 dt = DataTablesProcessor(request, TASK_FIELD_NAMES,
0292 default_order_column=0, default_order_direction='desc')
0293 days = _get_days(request)
0294 status = request.GET.get('status', '') or None
0295 username = request.GET.get('username', '') or None
0296 taskname = request.GET.get('taskname', '') or None
0297 workinggroup = request.GET.get('workinggroup', '') or None
0298
0299 order_col = TASK_ORDER_MAP.get(dt.order_column_idx, '"jeditaskid"')
0300 order_dir = 'ASC' if dt.order_direction == 'asc' else 'DESC'
0301 order_by = f'{order_col} {order_dir}'
0302
0303 rows, total, filtered = list_tasks_dt(
0304 days=days, status=status, username=username, taskname=taskname,
0305 workinggroup=workinggroup,
0306 order_by=order_by, limit=dt.length, offset=dt.start,
0307 search=dt.search_value or None,
0308 )
0309
0310 data = []
0311 for task in rows:
0312 task_url = reverse('monitor_app:panda_task_detail', args=[task['jeditaskid']])
0313 tasks_by_user_url = reverse('monitor_app:panda_tasks_list') + f'?days={days}&username={task["username"]}' if task.get('username') else None
0314 tasks_by_status_url = reverse('monitor_app:panda_tasks_list') + f'?days={days}&status={task["status"]}' if task.get('status') else None
0315 tasks_by_wg_url = reverse('monitor_app:panda_tasks_list') + f'?days={days}&workinggroup={task["workinggroup"]}' if task.get('workinggroup') else None
0316
0317
0318 taskname_display = task.get('taskname', '') or ''
0319 if len(taskname_display) > 80:
0320 taskname_display = taskname_display[:77] + '...'
0321
0322 progress = task.get('progress')
0323 progress_str = f'{progress}%' if progress is not None else ''
0324
0325 failurerate = task.get('failurerate')
0326 failurerate_str = f'{failurerate}%' if failurerate is not None else ''
0327
0328 data.append([
0329 f'<a href="{task_url}">{task["jeditaskid"]}</a>',
0330 f'<a href="{task_url}" title="{task.get("taskname", "")}">{taskname_display}</a>',
0331 _status_badge(task['status'], tasks_by_status_url) if task.get('status') else '',
0332 f'<a href="{tasks_by_user_url}">{task["username"]}</a>' if tasks_by_user_url else '',
0333 f'<a href="{tasks_by_wg_url}">{task["workinggroup"]}</a>' if tasks_by_wg_url else str(task.get('workinggroup', '') or ''),
0334 _fmt_dt(task.get('creationdate')),
0335 _fmt_dt(task.get('modificationtime')),
0336 progress_str,
0337 failurerate_str,
0338 ])
0339
0340 return dt.create_response(data, total, filtered)
0341
0342
0343 @login_required
0344 def panda_tasks_filter_counts(request):
0345 days = _get_days(request)
0346 status = request.GET.get('status', '') or None
0347 username = request.GET.get('username', '') or None
0348 workinggroup = request.GET.get('workinggroup', '') or None
0349
0350 counts = task_filter_counts(days=days, status=status,
0351 username=username, workinggroup=workinggroup)
0352 return JsonResponse({'filter_counts': counts})
0353
0354
0355
0356
0357 @login_required
0358 def panda_job_detail(request, pandaid):
0359 data = study_job(int(pandaid))
0360 if 'error' in data:
0361 return render(request, 'monitor_app/panda_job_detail.html',
0362 {'error': data['error'], 'pandaid': pandaid})
0363 data['pandaid'] = pandaid
0364 job = data.get('job') or {}
0365 job['transformation_is_url'] = (job.get('transformation') or '').startswith(('http://', 'https://'))
0366 trf = job.get('transformation') or ''
0367 if 'pandaserver-doma.cern.ch/trf/' in trf:
0368 job['transformation_view_url'] = reverse('monitor_app:panda_view_text') + '?url=' + trf
0369 return render(request, 'monitor_app/panda_job_detail.html', data)
0370
0371
0372
0373
0374 def panda_view_text(request):
0375 """Fetch a PanDA transformation URL — self-extracting zip with embedded scripts.
0376
0377 Extracts the bash header and all text files from the zip, presents them
0378 as readable plain text.
0379 """
0380 import httpx
0381 import io
0382 import zipfile
0383 url = request.GET.get('url', '')
0384 if not url or not url.startswith('https://'):
0385 return HttpResponse('Missing or invalid url parameter', status=400,
0386 content_type='text/plain')
0387 try:
0388 resp = httpx.get(url, timeout=15, follow_redirects=True)
0389 except Exception as e:
0390 return HttpResponse(f'Failed to fetch: {e}', status=502,
0391 content_type='text/plain')
0392 data = resp.content
0393 parts = []
0394
0395 try:
0396 lines = []
0397 for line in data.split(b'\n'):
0398 try:
0399 lines.append(line.decode('utf-8'))
0400 except UnicodeDecodeError:
0401 break
0402 if lines:
0403 parts.append(f'=== Shell header ({len(lines)} lines) ===\n')
0404 parts.append('\n'.join(lines))
0405 except Exception:
0406 pass
0407
0408 try:
0409 buf = io.BytesIO(data)
0410 with zipfile.ZipFile(buf) as zf:
0411 for name in zf.namelist():
0412 try:
0413 content = zf.read(name).decode('utf-8')
0414 parts.append(f'\n\n=== {name} ===\n')
0415 parts.append(content)
0416 except (UnicodeDecodeError, KeyError):
0417 parts.append(f'\n\n=== {name} (binary, skipped) ===\n')
0418 except zipfile.BadZipFile:
0419 if not parts:
0420
0421 parts.append(data.decode('utf-8', errors='replace'))
0422 return HttpResponse(''.join(parts), content_type='text/plain; charset=utf-8')
0423
0424
0425
0426
0427 @login_required
0428 def panda_task_detail(request, jeditaskid):
0429 task = get_task(int(jeditaskid))
0430 if isinstance(task, dict) and 'error' in task:
0431 return render(request, 'monitor_app/panda_task_detail.html',
0432 {'error': task['error'], 'jeditaskid': jeditaskid})
0433
0434
0435 jobs_data = list_jobs(taskid=int(jeditaskid), days=90, limit=200)
0436 jobs = jobs_data.get('jobs', []) if not jobs_data.get('error') else []
0437 summary = jobs_data.get('summary', {}) if not jobs_data.get('error') else {}
0438
0439 return render(request, 'monitor_app/panda_task_detail.html', {
0440 'task': task,
0441 'jeditaskid': jeditaskid,
0442 'jobs': jobs,
0443 'job_summary': summary,
0444 'job_count': len(jobs),
0445 })
0446
0447
0448
0449
0450 @login_required
0451 def panda_errors_list(request):
0452 days = _get_days(request)
0453 context = {
0454 'table_title': 'PanDA Error Summary',
0455 'table_description': f'Top error patterns across failed jobs in the last {days} days.',
0456 'ajax_url': reverse('monitor_app:panda_errors_datatable_ajax'),
0457 'columns': ERROR_COLUMNS,
0458 }
0459 context.update(_days_context(days))
0460 return render(request, 'monitor_app/panda_errors.html', context)
0461
0462
0463 @login_required
0464 def panda_errors_datatable_ajax(request):
0465 dt = DataTablesProcessor(request, [c['name'] for c in ERROR_COLUMNS],
0466 default_order_column=3, default_order_direction='desc')
0467 days = _get_days(request)
0468 username = request.GET.get('username', '') or None
0469 site = request.GET.get('site', '') or None
0470 error_source = request.GET.get('error_source', '') or None
0471
0472 result = error_summary(days=days, username=username, site=site,
0473 error_source=error_source, limit=200)
0474
0475 if 'error' in result:
0476 return dt.create_response([], 0, 0)
0477
0478 errors = result.get('errors', [])
0479 total = len(errors)
0480
0481 data = []
0482 for err in errors:
0483 diag_url = reverse('monitor_app:panda_diagnostics_list') + f'?days={days}&error_source={err["error_source"]}'
0484 users_str = ', '.join(err.get('users', [])[:5])
0485 if len(err.get('users', [])) > 5:
0486 users_str += f' (+{len(err["users"]) - 5})'
0487 sites_str = ', '.join(err.get('sites', [])[:3])
0488 if len(err.get('sites', [])) > 3:
0489 sites_str += f' (+{len(err["sites"]) - 3})'
0490
0491 diag_text = err.get('error_diag', '') or ''
0492 if len(diag_text) > 120:
0493 diag_text = diag_text[:117] + '...'
0494
0495 data.append([
0496 f'<a href="{diag_url}">{err["error_source"]}</a>',
0497 str(err.get('error_code', '')),
0498 f'<span title="{err.get("error_diag", "")}">{diag_text}</span>',
0499 str(err.get('count', 0)),
0500 str(err.get('task_count', 0)),
0501 users_str,
0502 sites_str,
0503 ])
0504
0505 return dt.create_response(data, total, total)
0506
0507
0508
0509
0510 @login_required
0511 def panda_diagnostics_list(request):
0512 days = _get_days(request)
0513 context = {
0514 'table_title': 'PanDA Job Diagnostics',
0515 'table_description': f'Failed jobs with error details from the last {days} days.',
0516 'ajax_url': reverse('monitor_app:panda_diagnostics_datatable_ajax'),
0517 'columns': DIAG_COLUMNS,
0518 }
0519 context.update(_days_context(days))
0520 return render(request, 'monitor_app/panda_diagnostics.html', context)
0521
0522
0523 @login_required
0524 def panda_diagnostics_datatable_ajax(request):
0525 dt = DataTablesProcessor(request, [c['name'] for c in DIAG_COLUMNS],
0526 default_order_column=0, default_order_direction='desc')
0527 days = _get_days(request)
0528 username = request.GET.get('username', '') or None
0529 site = request.GET.get('site', '') or None
0530 taskid = request.GET.get('taskid', '') or None
0531 error_source = request.GET.get('error_source', '') or None
0532
0533 result = diagnose_jobs(days=days, username=username, site=site,
0534 taskid=taskid, error_component=error_source,
0535 limit=500)
0536
0537 if 'error' in result:
0538 return dt.create_response([], 0, 0)
0539
0540 jobs = result.get('jobs', [])
0541 total = len(jobs)
0542
0543
0544 page_jobs = jobs[dt.start:dt.start + dt.length]
0545
0546 data = []
0547 for job in page_jobs:
0548 job_url = reverse('monitor_app:panda_job_detail', args=[job['pandaid']])
0549 task_url = reverse('monitor_app:panda_task_detail', args=[job['jeditaskid']]) if job.get('jeditaskid') else None
0550
0551 errors_html = []
0552 for err in job.get('errors', []):
0553 diag = err.get('diag', '')
0554 if len(diag) > 80:
0555 diag = diag[:77] + '...'
0556 errors_html.append(f'<strong>{err["component"]}</strong>:{err["code"]} {diag}')
0557
0558 data.append([
0559 f'<a href="{job_url}">{job["pandaid"]}</a>',
0560 f'<a href="{task_url}">{job["jeditaskid"]}</a>' if task_url else str(job.get('jeditaskid', '')),
0561 job.get('produsername', ''),
0562 _status_badge(job['jobstatus']) if job.get('jobstatus') else '',
0563 job.get('computingsite', ''),
0564 '<br>'.join(errors_html) if errors_html else '',
0565 _fmt_dt(job.get('endtime')),
0566 ])
0567
0568 return dt.create_response(data, total, total)
0569
0570
0571
0572
0573 @login_required
0574 def epic_queues_list(request):
0575 """ePIC compute queues from live PanDA schedconfig."""
0576 result = list_queues(vo='eic')
0577 queues = result.get('queues', [])
0578 return render(request, 'monitor_app/epic_queues_list.html', {
0579 'queues': queues,
0580 })
0581
0582
0583 @login_required
0584 def epic_queue_detail(request, queue_name):
0585 """Full schedconfig for a single ePIC queue."""
0586 import json as json_mod
0587 result = get_queue(queue_name)
0588 if 'error' in result:
0589 return render(request, 'monitor_app/epic_queue_detail.html', {
0590 'error': result['error'],
0591 'queue_name': queue_name,
0592 })
0593 config = result['queue']
0594
0595
0596 identity_keys = [
0597 'panda_queue', 'name', 'nickname', 'siteid', 'site', 'panda_site',
0598 'atlas_site', 'gocname', 'id',
0599 ]
0600 status_keys = [
0601 'status', 'state', 'rc_site_state', 'state_comment', 'state_update',
0602 'last_modified', 'last_update',
0603 ]
0604 resource_keys = [
0605 'resource_type', 'type', 'capability', 'corecount', 'corepower',
0606 'maxrss', 'meanrss', 'minrss', 'maxtime', 'mintime', 'maxwdir',
0607 'maxinputsize', 'timefloor', 'vo_name',
0608 ]
0609 location_keys = [
0610 'region', 'country', 'cloud', 'tier', 'tier_level', 'rc', 'rc_site',
0611 'rc_country',
0612 ]
0613 container_keys = [
0614 'container_type', 'container_options', 'is_cvmfs',
0615 ]
0616 pilot_keys = [
0617 'pilot_version', 'pilot_manager', 'python_version', 'jobseed',
0618 ]
0619
0620 def _section(keys):
0621 return {k: config[k] for k in keys if k in config}
0622
0623 sections = [
0624 ('Identity', _section(identity_keys)),
0625 ('Status', _section(status_keys)),
0626 ('Resources', _section(resource_keys)),
0627 ('Location', _section(location_keys)),
0628 ('Container', _section(container_keys)),
0629 ('Pilot', _section(pilot_keys)),
0630 ]
0631
0632
0633 shown = set()
0634 for _, s in sections:
0635 shown.update(s.keys())
0636 other = {k: v for k, v in config.items() if k not in shown}
0637
0638 return render(request, 'monitor_app/epic_queue_detail.html', {
0639 'queue_name': queue_name,
0640 'sections': sections,
0641 'other': other,
0642 'config_json': json_mod.dumps(config, indent=2, default=str),
0643 })