File indexing completed on 2026-11-09 10:47:22
0001
0002 """cric-declared-state.py — the declared record from CRIC.
0003 The doer of the ops agent's ``cric_declared_state`` (swf-epicprod
0004 docs/CONTINUOUS_PRODUCTION.md, Declared downtime): reads the PanDA queue
0005 status rules, the DDM endpoint status rules and the downtime objects of
0006 the CRIC ePIC's PanDA reads (datalake-cric.cern.ch) with the production
0007 proxy, keeps them as the declared record in the entry store
0008 (monitor_app/declared.py), and logs one ``declared_state_sync`` action
0009 naming what appeared, changed, expired and cleared. The scope is the
0010 EIC queues (vo eic), the endpoints those queues name, and the resource
0011 centres behind them.
0012 Credentials: ``X509_USER_PROXY`` (the ops agent's Rucio proxy, accepted
0013 by CRIC) and the grid CA directory (``X509_CERT_DIR``, default
0014 /etc/grid-security/certificates).
0015 Django-bootstrap standalone script, by hand::
0016 cd /data/wenauseic/github/swf-monitor/src
0017 ../../swf-testbed/.venv/bin/python ../scripts/cric-declared-state.py [--dry-run] [--created-by NAME]
0018 Prints one JSON line: the counts read and the changes. Exit status 1 when
0019 a CRIC read failed; nothing is written then.
0020 """
0021 import argparse
0022 import json
0023 import logging
0024 import os
0025 import sys
0026 import time
0027
0028 import requests
0029
0030 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0031 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0032 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0033 import django
0034 django.setup()
0035 from django.utils import timezone
0036 from monitor_app.epicprod_logging import log_epicprod_action
0037 from monitor_app import declared
0038
0039 log = logging.getLogger('cric-declared-state')
0040 TIMEOUT = 60
0041
0042
0043 def cric_get(url, proxy, ca_dir):
0044 """One CRIC read with the proxy as client certificate; raises on
0045 anything but a 200 with JSON."""
0046 r = requests.get(url, cert=(proxy, proxy), verify=ca_dir, timeout=TIMEOUT)
0047 if r.status_code != 200:
0048 raise RuntimeError(f'{url}: HTTP {r.status_code}: {r.text[:200]}')
0049 return r.json()
0050
0051
0052 def scope(queues_doc):
0053 """The EIC queues, the endpoints they name (astorages) and the
0054 resource centres behind them (each with its queues, so a site's
0055 window reaches the queue readers), from the pandaqueue document."""
0056 queues = set(queues_doc or {})
0057 endpoints, rcsites = set(), {}
0058 for name, q in (queues_doc or {}).items():
0059 for names in ((q or {}).get('astorages') or {}).values():
0060 endpoints.update(names or [])
0061 if (q or {}).get('rc_site'):
0062 rcsites.setdefault(q['rc_site'], set()).add(name)
0063 return queues, endpoints, rcsites
0064
0065
0066 def main():
0067 ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
0068 ap.add_argument('--dry-run', action='store_true',
0069 help='read CRIC and print the records, write nothing')
0070 ap.add_argument('--created-by', default='cric_declared_state')
0071 args = ap.parse_args()
0072 logging.basicConfig(level=logging.INFO, stream=sys.stderr,
0073 format='%(asctime)s %(name)s %(levelname)s %(message)s')
0074 t0 = time.monotonic()
0075 proxy = os.environ.get('X509_USER_PROXY') or ''
0076 ca_dir = os.environ.get('X509_CERT_DIR') or '/etc/grid-security/certificates'
0077 if not proxy or not os.path.exists(proxy):
0078 msg = f'no proxy at X509_USER_PROXY={proxy!r}; CRIC refuses anonymous reads'
0079 log.error(msg)
0080 log_epicprod_action('prodops-agent', 'declared_state_sync', outcome='error',
0081 username=args.created_by, sublevel='normal', live_default=True,
0082 reason=msg,
0083 level=logging.ERROR, message=f'declared state sync failed: {msg}')
0084 print(json.dumps({'error': msg}))
0085 return 1
0086 try:
0087 queues_doc = cric_get(declared.CRIC_QUEUES, proxy, ca_dir)
0088 queues, endpoints, rcsites = scope(queues_doc)
0089 found = []
0090 found += declared.rules_from_pandaqueuestatus(
0091 cric_get(declared.CRIC_QUEUE_STATUS, proxy, ca_dir), queues)
0092 found += declared.rules_from_ddmendpointstatus(
0093 cric_get(declared.CRIC_ENDPOINT_STATUS, proxy, ca_dir), endpoints)
0094 found += declared.windows_from_downtime(
0095 cric_get(declared.CRIC_DOWNTIME, proxy, ca_dir), rcsites)
0096 except Exception as e:
0097 log.exception('CRIC read failed')
0098 log_epicprod_action('prodops-agent', 'declared_state_sync', outcome='error',
0099 username=args.created_by, sublevel='normal', live_default=True,
0100 reason=str(e)[:300],
0101 level=logging.ERROR,
0102 duration_ms=int((time.monotonic() - t0) * 1000),
0103 message=f'declared state sync failed: {str(e)[:300]}')
0104 print(json.dumps({'error': str(e)[:300]}))
0105 return 1
0106 now = timezone.now()
0107 counts = {'queues': len(queues), 'endpoints': len(endpoints), 'rcsites': len(rcsites),
0108 'records': len(found),
0109 'in_force': sum(1 for r in found if declared.standing_of(r, now) == 'active'),
0110 'future': sum(1 for r in found if declared.standing_of(r, now) == 'future')}
0111 if args.dry_run:
0112 print(json.dumps({'dry_run': True, **counts, 'records': found}, default=str))
0113 return 0
0114 changes = declared.sync(found, now, changed_by=args.created_by)
0115 moved = {k: v for k, v in changes.items() if k != 'unchanged' and v}
0116
0117
0118
0119
0120 by_name = {r['name']: r for r in found}
0121 stored = {r['name']: r for r in declared.records()}
0122 for kind_of_change in ('appeared', 'changed', 'expired', 'cleared'):
0123 for name in changes.get(kind_of_change) or []:
0124 r = by_name.get(name) or stored.get(name) or {}
0125 target = r.get('target') or name
0126 kind = r.get('kind') or ''
0127 url = (f'/panda/epic-queues/{target}/#declared' if kind == 'queue'
0128 else f'/rucio-endpoints/{target}/' if kind == 'endpoint'
0129 else '/panda/epic-queues/')
0130 log_epicprod_action(
0131 'prodops-agent', 'declared_state_changed',
0132 subject_type={'queue': 'panda_queue', 'endpoint': 'ddm_endpoint'}.get(kind, 'rc_site'),
0133 subject_key=target, username=args.created_by,
0134 outcome=kind_of_change, sublevel='normal', live_default=True,
0135 message=f"declared state {kind_of_change}: {target}: {declared.summary_line(r)}"[:300],
0136 summary=declared.summary_line(r), url=url,
0137 severity='warning' if kind_of_change in ('appeared', 'changed') else 'info',
0138 kind=kind, record=name, declared_by=r.get('declared_by', ''),
0139 end=r.get('end'), start=r.get('start'))
0140 log_epicprod_action(
0141 'prodops-agent', 'declared_state_sync',
0142 outcome='ok', username=args.created_by,
0143 sublevel='normal' if moved else 'low', live_default=bool(moved),
0144 duration_ms=int((time.monotonic() - t0) * 1000),
0145 message=('declared state from CRIC: '
0146 + f"{counts['in_force']} in force, {counts['future']} future"
0147 + ''.join(f"; {k} {', '.join(v)}" for k, v in moved.items())),
0148 **counts, **{k: len(v) for k, v in changes.items() if k != 'unchanged'},
0149 unchanged=changes['unchanged'])
0150 print(json.dumps({**counts, 'changes': changes}, default=str))
0151 return 0
0152
0153
0154 if __name__ == '__main__':
0155 sys.exit(main())