File indexing completed on 2026-11-09 10:47:25
0001
0002 """storage-sweep.py — the storage pass: production output on the JLab
0003 RSEs from registration until it settles, kept in the storage store and
0004 published as the epicprod ``storage`` Snapper component.
0005
0006 The prod-ops agent's doer for the ``storage_sweep`` message, enqueued
0007 by cron every four hours. Logic in ``swf_epicprod/analytics/storage.py``,
0008 publication in ``monitor_app/snapper_storage.py``; design in
0009 ``swf-epicprod/docs/STORAGE.md``. Django-bootstrap standalone script —
0010 also usable by hand.
0011
0012 Usage::
0013
0014 cd /data/wenauseic/github/swf-monitor/src
0015 source ../../swf-testbed/.venv/bin/activate && source ~/.env
0016 python ../scripts/storage-sweep.py [--campaigns 26.07]
0017 [--limit-files N] [--limit-datasets N]
0018 [--resume PASS_ID] [--publish-only]
0019 [--no-publish] [--dump projection.json]
0020
0021 A run with a limit works on a copy of the store and never publishes.
0022 ``--publish-only`` skips the crawl and publishes the projection of the
0023 store's last completed pass, for a pass whose publish step failed.
0024 """
0025 import argparse
0026 import json
0027 import os
0028 import signal
0029 import sys
0030
0031 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0032 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0033 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0034
0035 import django
0036 django.setup()
0037
0038 from swf_epicprod.analytics.storage import (
0039 PassInProgress, log, mark_pass_interrupted, project_store, run_pass)
0040
0041
0042 def _on_signal(signum, frame):
0043 raise KeyboardInterrupt(signal.Signals(signum).name)
0044
0045
0046 def main():
0047
0048
0049
0050 signal.signal(signal.SIGTERM, _on_signal)
0051 parser = argparse.ArgumentParser()
0052 parser.add_argument('--publish-only', action='store_true',
0053 help='no catalog reads: publish the projection of '
0054 'the store\'s last completed pass (after a '
0055 'failed publish)')
0056 parser.add_argument('--campaigns', default='',
0057 help='comma-separated campaign families '
0058 '(default: the delivery record\'s targets)')
0059 parser.add_argument('--limit-files', type=int, default=0,
0060 help='cap the file tier for a validation run '
0061 '(works on a copy of the store, no publish)')
0062 parser.add_argument('--limit-datasets', type=int, default=0,
0063 help='cap the dataset tier for a validation run')
0064 parser.add_argument('--resume', type=int, default=0, metavar='PASS_ID',
0065 help='continue an interrupted pass by id, skipping '
0066 'the locations it already stamped')
0067 parser.add_argument('--no-publish', action='store_true',
0068 help='run the pass without publishing the component')
0069 parser.add_argument('--dump', default='',
0070 help='write the projection JSON to this path')
0071 parser.add_argument('--created-by', default='prodops_agent')
0072 args = parser.parse_args()
0073 mode_name = 'incremental'
0074 campaigns = tuple(c.strip() for c in args.campaigns.split(',')
0075 if c.strip()) or None
0076 validation = bool(args.limit_files or args.limit_datasets)
0077
0078 if args.publish_only:
0079 summary, data = project_store()
0080 else:
0081 try:
0082 summary, data = run_pass(campaigns=campaigns,
0083 limit_files=args.limit_files,
0084 limit_datasets=args.limit_datasets,
0085 resume_pass=args.resume or None)
0086 except PassInProgress as exc:
0087
0088
0089 print('SUMMARY ' + json.dumps({'mode': mode_name,
0090 'skipped': str(exc)}))
0091 return 4
0092 except KeyboardInterrupt as exc:
0093
0094
0095
0096 pass_id = mark_pass_interrupted(str(exc) or 'interrupt')
0097 log(f'pass {pass_id} interrupted: {exc}')
0098 print('SUMMARY ' + json.dumps({'mode': mode_name,
0099 'pass_id': pass_id,
0100 'interrupted': str(exc)}))
0101 return 143
0102 if args.dump:
0103 with open(args.dump, 'w') as handle:
0104 json.dump(data, handle, indent=1, sort_keys=True)
0105 print(f'projection written to {args.dump}')
0106
0107 if validation or args.no_publish:
0108 summary['published'] = False
0109 else:
0110 from monitor_app.snapper_storage import publish_storage
0111 publication = publish_storage(data, since=data['interval']['start'])
0112 summary['published'] = True
0113 summary['revision'] = publication.update.revision
0114 summary['content_changed'] = publication.update.content_changed
0115
0116
0117
0118
0119
0120
0121 try:
0122 from swf_epicprod.analytics.storage_listings import (
0123 refresh_ghost_product)
0124 summary['ghost_product'] = refresh_ghost_product()
0125 except Exception as exc:
0126 log(f'ERROR ghost product refresh: {exc}')
0127 summary['ghost_product'] = {'error': str(exc)}
0128 print('SUMMARY ' + json.dumps(summary))
0129 return 0 if not summary.get('errors') else 3
0130
0131
0132 if __name__ == '__main__':
0133 sys.exit(main())