Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /swf-monitor/src/monitor_app/viewdir/panda_node_guard.py was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 """The Node guard page (site-canary docs/NODE_GUARD.md): the node record
0002 (black holes, half open, pinned) from the canary store with a person's
0003 clear and pin; every node the guard's last cycle judged, with its
0004 state, reason and evidence; the queues' calibration and any storm; the
0005 switches. Reads the store and the cached product the cycle stored;
0006 computes nothing and reaches neither PanDA nor Rucio.
0007 """
0008 import logging
0009 from datetime import datetime, timezone
0010 
0011 from django.contrib import messages
0012 from django.shortcuts import redirect, render
0013 from django.urls import reverse
0014 
0015 from monitor_app.panda.node_guard import DEFAULTS, STATE_KEY
0016 
0017 logger = logging.getLogger(__name__)
0018 
0019 STATE_LABELS = {'would_exclude': 'would exclude', 'excluded': 'excluded', 'clear': 'clear'}
0020 RECORD_LABELS = {'black_hole': 'black hole', 'half_open': 'half open', 'pinned': 'pinned',
0021                  'clear': 'clear'}
0022 REASON_LABELS = {
0023     'black_hole': 'black hole',
0024     'fixed_time': 'fixed-time kill',
0025     'below_floor': 'under the job floor',
0026     'failed_fraction': 'failed share under the threshold',
0027     'not_fast': 'failures not fast',
0028     'no_calibration': 'no calibration for the queue',
0029     'tasks_fail_everywhere': 'its tasks finish nowhere on the queue',
0030     'no_other_node': 'no other node on the queue',
0031     'queue_event': "the queue's event",
0032 }
0033 
0034 
0035 def _evidence_view(e):
0036     """The evidence dict as the templates read it."""
0037     e = e or {}
0038     return {
0039         'jobs': e.get('jobs'), 'failed': e.get('failed'), 'finished': e.get('finished'),
0040         'failed_pct': round(100 * (e.get('failed_fraction') or 0)),
0041         'fast_failed': e.get('fast_failed'), 'failed_no_duration': e.get('failed_no_duration'),
0042         'fast_pct': round(100 * (e.get('fast_fraction') or 0)),
0043         'tasks_failed': e.get('tasks_failed') or [],
0044         'tasks_finished_elsewhere': e.get('tasks_finished_elsewhere') or [],
0045         'other_nodes_finishing': e.get('other_nodes_finishing'),
0046         'first_end': e.get('first_end'), 'last_end': e.get('last_end'),
0047         'site': e.get('site') or '', 'sites': e.get('sites') or [],
0048         'durations': {k: (round(v / 60.0, 1) if v is not None else None)
0049                       for k, v in (e.get('failed_duration_s') or {}).items()},
0050         'error_codes': e.get('error_codes') or [],
0051         'sample_jobs': e.get('sample_jobs') or [],
0052         'duration_spread': e.get('duration_spread'), 'fixed_time': bool(e.get('fixed_time')),
0053         'elsewhere_hosts': sorted((e.get('tasks_finished_elsewhere_hosts') or {}).items()),
0054         'median_finished_min': (round(e['median_finished_s'] / 60.0)
0055                                 if e.get('median_finished_s') else None),
0056         'fast_under_min': (round(e['fast_under_s'] / 60.0) if e.get('fast_under_s') else None),
0057     }
0058 
0059 
0060 def _record_rows():
0061     """The node record as the page shows it: every node not clear."""
0062     from canary.store.models import NodeState
0063     rows = []
0064     for node in (NodeState.objects.exclude(status='clear')
0065                  .select_related('queue').order_by('queue__name', 'host')):
0066         lv = node.last_verdict or {}
0067         row = {'queue': node.queue.name, 'host': node.host, 'site': node.site,
0068                'status': node.status, 'status_label': RECORD_LABELS.get(node.status, node.status),
0069                'reason': node.reason, 'opened_at': node.opened_at, 'expires_at': node.expires_at,
0070                'reopened': node.reopened, 'trips': node.trips,
0071                'last_seen_at': node.last_seen_at,
0072                'last_verdict_at': lv.get('at'), 'last_tripped': lv.get('tripped'),
0073                'last_finished': lv.get('finished'), 'last_fast_failed': lv.get('fast_failed'),
0074                'changes': node.changes.count()}
0075         row.update(_evidence_view(node.evidence))
0076         rows.append(row)
0077     return rows
0078 
0079 
0080 def panda_node_guard(request):
0081     from monitor_app.middleware import is_tunnel_request
0082     from monitor_app.authority import may_act
0083     from monitor_app.models import CachedProduct, SysConfig
0084 
0085     config = SysConfig.get_config()
0086     row = CachedProduct.objects.filter(key=STATE_KEY).first()
0087     state = (row.value if row else None) or {}
0088     settings = {k: config.get(f'node_guard.{k}', v) for k, v in DEFAULTS.items()}
0089     nodes = []
0090     for n in state.get('nodes') or []:
0091         row = {
0092             'queue': n.get('queue'), 'host': n.get('host'),
0093             'state': n.get('state') or 'clear',
0094             'state_label': STATE_LABELS.get(n.get('state'), n.get('state') or ''),
0095             'reason': n.get('reason') or '',
0096             'reason_label': REASON_LABELS.get(n.get('reason'), n.get('reason') or ''),
0097             'log_id': n.get('log_id'),
0098         }
0099         row.update(_evidence_view(n.get('evidence')))
0100         nodes.append(row)
0101     try:
0102         record = _record_rows()
0103     except Exception as exc:  # noqa: BLE001
0104         logger.exception('node guard: the record could not be read')
0105         record = []
0106         state = dict(state, errors=list(state.get('errors') or [])
0107                      + [f'the node record could not be read: {type(exc).__name__}: {exc}'])
0108     operable = (request.user.is_authenticated and may_act(request.user.username)
0109                 and not is_tunnel_request(request))
0110     # tripped first, then by queue and host
0111     nodes.sort(key=lambda n: (n['state'] == 'clear', n['queue'] or '', n['host'] or ''))
0112     queues = []
0113     for name, q in sorted((state.get('queues') or {}).items()):
0114         med = q.get('median_finished_s')
0115         fast = q.get('fast_under_s')
0116         queues.append({
0117             'queue': name, 'jobs': q.get('jobs'), 'hosts': q.get('hosts'),
0118             'judged': q.get('judged'), 'tripped': q.get('tripped'),
0119             'queue_event': bool(q.get('queue_event')), 'storm_hosts': q.get('storm_hosts'),
0120             'storm_nodes': q.get('storm_nodes') or [],
0121             # a burst: more than storm_nodes hosts failing inside one interval
0122             'bursts': [{'at': datetime.fromtimestamp(b['from_s'], timezone.utc), 'hosts': b['hosts']}
0123                        for b in (q.get('bursts') or []) if b.get('from_s') is not None],
0124             'median_finished_min': round(med / 60.0) if med else None,
0125             'fast_under_min': round(fast / 60.0) if fast else None,
0126             'not_nodes': sorted((q.get('not_nodes') or {}).items()),
0127         })
0128     context = {
0129         'active_nav': {'panda_node_guard': True, 'sites': True},
0130         'state': state, 'never_run': not state,
0131         'cycle_at': state.get('cycle_at'),
0132         'mode': state.get('mode') or settings['mode'],
0133         'enabled': bool(state.get('enabled', settings['enabled'])),
0134         'settings': settings, 'errors': state.get('errors') or [],
0135         'nodes': nodes, 'tripped': [n for n in nodes if n['state'] != 'clear'],
0136         'queues': queues, 'record': record, 'operable': operable,
0137     }
0138     return render(request, 'monitor_app/panda_node_guard.html', context)
0139 
0140 
0141 def panda_node_guard_json(request):
0142     """The node guard as JSON for scripts (prod-notify): the last cycle's
0143     tripped nodes (would_exclude in shadow mode, excluded live) and the
0144     node record's rows that are not clear. The same reads as the page,
0145     nothing computed."""
0146     from django.core.serializers.json import DjangoJSONEncoder
0147     from django.http import JsonResponse
0148     from monitor_app.models import CachedProduct, SysConfig
0149 
0150     config = SysConfig.get_config()
0151     row = CachedProduct.objects.filter(key=STATE_KEY).first()
0152     state = (row.value if row else None) or {}
0153     tripped = [
0154         {'queue': n.get('queue'), 'host': n.get('host'), 'state': n.get('state'),
0155          'reason': n.get('reason') or '', 'site': (n.get('evidence') or {}).get('site') or '',
0156          'log_id': n.get('log_id')}
0157         for n in state.get('nodes') or [] if (n.get('state') or 'clear') != 'clear']
0158     errors = list(state.get('errors') or [])
0159     try:
0160         record = [{k: r.get(k) for k in ('queue', 'host', 'site', 'status', 'reason',
0161                                           'opened_at', 'expires_at', 'trips', 'last_verdict_at')}
0162                   for r in _record_rows()]
0163     except Exception as exc:  # noqa: BLE001
0164         logger.exception('node guard json: the record could not be read')
0165         record = []
0166         errors.append(f'the node record could not be read: {type(exc).__name__}: {exc}')
0167     return JsonResponse({
0168         'cycle_at': state.get('cycle_at'),
0169         'mode': state.get('mode') or config.get('node_guard.mode', DEFAULTS['mode']),
0170         'enabled': bool(state.get('enabled', config.get('node_guard.enabled', DEFAULTS['enabled']))),
0171         'errors': errors, 'tripped': tripped, 'record': record,
0172     }, encoder=DjangoJSONEncoder)
0173 
0174 
0175 def panda_node_guard_set(request):
0176     """A person's decision on one node of the record: clear, pin, unpin
0177     (back to clear, the guard may trip it again) or open a black hole by
0178     hand. POST-only; a write, so login-gated, authority-gated, and
0179     refused on the tunnel face like every rare operator action."""
0180     from monitor_app.middleware import is_tunnel_request
0181     from monitor_app.authority import may_act
0182     from monitor_app.epicprod_logging import log_epicprod_action
0183     from canary.store import nodes as node_record
0184 
0185     url = reverse('monitor_app:panda_node_guard')
0186     if request.method != 'POST':
0187         messages.warning(request, 'Node guard actions only respond to POST submissions.')
0188         return redirect(url)
0189     queue = (request.POST.get('queue') or '').strip()
0190     host = (request.POST.get('host') or '').strip()
0191     action = (request.POST.get('action') or '').strip()
0192     if is_tunnel_request(request):
0193         logger.warning('node guard %s refused on the tunnel face (%s/%s)', action, queue, host)
0194         return redirect(url)
0195     if not request.user.is_authenticated:
0196         messages.error(request, 'Login required to act on a node.')
0197         return redirect(url)
0198     if not may_act(request.user.username):
0199         messages.error(request, 'Your account has no authority to act on the system.')
0200         return redirect(url)
0201     status = {'clear': 'clear', 'unpin': 'clear', 'pin': 'pinned', 'open': 'black_hole'}.get(action)
0202     if not (queue and host and status):
0203         messages.error(request, 'A node (queue and host) and one of clear, pin, unpin, open are needed.')
0204         return redirect(url)
0205     note = (request.POST.get('note') or '').strip()
0206     try:
0207         node = node_record.set_status(queue, host, status, username=request.user.username,
0208                                       reason=f'{action}: {note}' if note else action)
0209     except Exception as exc:  # noqa: BLE001
0210         logger.exception('node guard %s failed (%s/%s)', action, queue, host)
0211         messages.error(request, f'The action failed: {type(exc).__name__}: {exc}')
0212         return redirect(url)
0213     log_epicprod_action(
0214         'web', 'node_guard_set', subject_type='panda_node', subject_key=f'{queue}/{host}',
0215         username=request.user.username, outcome=node.status, sublevel='normal', live_default=True,
0216         message=f'node guard {queue} {host}: {action} by {request.user.username}'
0217                 + (f' ({note})' if note else '') + f'; status {node.status}',
0218         action_taken=action, note=note, status=node.status)
0219     messages.success(request, f'{host} on {queue}: {RECORD_LABELS.get(node.status, node.status)}.')
0220     return redirect(url)