File indexing completed on 2026-11-09 10:47:24
0001
0002 """register-evgen-rucio.py — register one EVGEN input directory in JLab Rucio.
0003
0004 The production-ops agent's doer for the EVGEN inputs page's "Register in
0005 Rucio" action (swf-epicprod docs/EPICPROD_EVGEN_INPUTS.md § Registration).
0006 Given an EVGEN path (``/EVGEN/<class>/...``) it lists the files under it on
0007 the JLab production door, takes each file's size and adler32 from the door
0008 (``xrdfs query checksum`` — the server computes it; no bytes are read), and
0009 registers them as ``epic:/EVGEN/...`` datasets at RSE ``EIC-XRD`` under the
0010 ``eicprod`` account: one dataset per directory holding files, one replica
0011 per file with its PFN on the door, files attached to their dataset. The
0012 registration contract is the production team's reference
0013 (eic/simulation_campaign_hepmc3 PR #100: ``scripts/calculate_checksum_xrd.sh``
0014 and ``scripts/register_from_checksum_listing.py``). Re-running is safe:
0015 existing datasets, replicas, and attachments are kept and counted.
0016
0017 Credential: tokens from the PanDA server (swf_epicprod.jlab_auth): a storage
0018 token scoped to the directory being registered for the door, and a rucio
0019 token for the catalog. The JLab ``eicprod`` x509 proxy is the fallback for
0020 both while it lasts (it cannot be renewed past 2026-10-28).
0021 ``EVGEN_X509_PROXY`` names the agent's private copy (mode 0600; the same
0022 file the submission doer ships in the sandbox). ``EVGEN_X509_PROXY_SOURCE``,
0023 when set, names the renewal source — the production team's proxy drop — and
0024 a source proxy that outlives the private copy is copied over it before use.
0025
0026 No silent failures: a file whose checksum the door does not return makes the
0027 run incomplete and nothing is registered (exit 4); a dataset whose Rucio file
0028 list disagrees with the door listing after registration exits 7. The last
0029 stdout line is a JSON summary the agent relays to the page and the action
0030 stream; progress and errors go to stderr.
0031
0032 Usage::
0033
0034 register-evgen-rucio.py --path /EVGEN/BACKGROUNDS/BEAMGAS/proton/pythia8.306-1.0/100GeV
0035 register-evgen-rucio.py --path ... --dry-run # list + checksum only, no Rucio write
0036 register-evgen-rucio.py --path ... --events-only # count events, record on the DIDs
0037
0038 Exit codes: 0 ok · 2 bad path · 3 listing failed or empty · 4 checksum
0039 incomplete · 5 proxy unusable · 6 Rucio error · 7 verification mismatch.
0040 """
0041 import argparse
0042 import calendar
0043 import json
0044 import os
0045 import re
0046 import subprocess
0047 import sys
0048 import time
0049 from collections import defaultdict
0050 from concurrent.futures import ThreadPoolExecutor
0051
0052
0053
0054
0055 XRD_DOOR = os.environ.get('EVGEN_XRD_DOOR', 'root://dtn-rucio.jlab.org:1094')
0056 XRD_BASE = '/volatile/eic/EPIC'
0057 RUCIO_HOST = os.environ.get('JLAB_RUCIO_URL', 'https://rucio-server.jlab.org:443')
0058 RUCIO_ACCOUNT = os.environ.get('EVGEN_RUCIO_ACCOUNT', 'eicprod')
0059 RUCIO_SCOPE = 'epic'
0060 RUCIO_RSE = os.environ.get('EVGEN_RUCIO_RSE', 'EIC-XRD')
0061 CHECKSUM_WORKERS = int(os.environ.get('EVGEN_CHECKSUM_WORKERS', '4'))
0062 LISTING_TIMEOUT = 600
0063 CHECKSUM_TIMEOUT = 120
0064 BATCH_SIZE = 500
0065 PATH_OK = re.compile(r'^/EVGEN/[A-Za-z0-9._=+\-/]+$')
0066
0067
0068 EVENTS_WORKERS = int(os.environ.get('EVGEN_EVENTS_WORKERS', '4'))
0069 EVENTS_TIMEOUT = int(os.environ.get('EVGEN_EVENTS_TIMEOUT', '120'))
0070 EVENTS_TREE = os.environ.get('EVGEN_EVENTS_TREE', 'hepmc3_tree')
0071
0072 EXIT_BAD_PATH, EXIT_LISTING, EXIT_CHECKSUM, EXIT_PROXY, EXIT_RUCIO, EXIT_VERIFY = 2, 3, 4, 5, 6, 7
0073 EXIT_EVENTS = 8
0074
0075
0076 def _log(msg):
0077 print(msg, file=sys.stderr, flush=True)
0078
0079
0080 class DoerError(Exception):
0081 def __init__(self, code, msg):
0082 super().__init__(msg)
0083 self.code = code
0084
0085
0086
0087
0088 def _x509_not_after(path):
0089 """Expiry epoch of a proxy file via openssl (the credential checker's
0090 method)."""
0091 out = subprocess.run(['openssl', 'x509', '-enddate', '-noout', '-in', path],
0092 capture_output=True, text=True, timeout=15)
0093 if out.returncode != 0:
0094 raise RuntimeError((out.stderr or out.stdout).strip())
0095 not_after = out.stdout.strip().split('=', 1)[1]
0096 return float(calendar.timegm(time.strptime(not_after, '%b %d %H:%M:%S %Y %Z')))
0097
0098
0099 def resolve_proxy():
0100 """The proxy to use: the private copy, refreshed from the renewal source
0101 when the source outlives it. Returns (path, summary)."""
0102 home = os.environ.get('EVGEN_X509_PROXY', '')
0103 source = os.environ.get('EVGEN_X509_PROXY_SOURCE', '')
0104 if not home:
0105 raise DoerError(EXIT_PROXY, 'EVGEN_X509_PROXY is not set')
0106 home_exp = None
0107 if os.path.exists(home):
0108 try:
0109 home_exp = _x509_not_after(home)
0110 except Exception as e:
0111 _log(f'WARNING: private proxy {home} unreadable: {e}')
0112 refreshed = False
0113 if source:
0114 try:
0115 src_exp = _x509_not_after(source)
0116 except Exception as e:
0117
0118
0119 _log(f'WARNING: proxy source {source} unusable: {e}')
0120 else:
0121 if src_exp > time.time() and (home_exp is None or src_exp > home_exp + 60):
0122 data = open(source, 'rb').read()
0123
0124
0125 fd = os.open(home, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
0126 try:
0127 os.write(fd, data)
0128 finally:
0129 os.close(fd)
0130 os.chmod(home, 0o600)
0131 _log(f'proxy refreshed from {source} '
0132 f'(expires {time.strftime("%Y-%m-%d", time.gmtime(src_exp))})')
0133 home_exp = src_exp
0134 refreshed = True
0135 if home_exp is None:
0136 raise DoerError(EXIT_PROXY, f'no usable proxy at {home}')
0137 days_left = (home_exp - time.time()) / 86400.0
0138 if days_left <= 0:
0139 raise DoerError(EXIT_PROXY, f'proxy {home} expired '
0140 f'{time.strftime("%Y-%m-%d", time.gmtime(home_exp))}')
0141 return home, {'path': home, 'days_left': round(days_left, 1),
0142 'refreshed_from_source': refreshed}
0143
0144
0145
0146
0147 def door_credential(scope, proxy):
0148 """(door URL, environment, how) for xrootd work under the door path
0149 ``scope``: a storage token for that path, else the proxy
0150 (swf_epicprod.jlab_auth.door_access). The token is cached for the run."""
0151 from swf_epicprod.jlab_auth import door_access
0152 return door_access(XRD_DOOR, scope, proxy=proxy)
0153
0154
0155 def _xrdfs(args, scope, proxy, timeout):
0156 door, env, _how = door_credential(scope, proxy)
0157 return subprocess.run(['xrdfs', door] + args, capture_output=True,
0158 text=True, timeout=timeout, env=env)
0159
0160
0161
0162
0163
0164 EVGEN_SUFFIX = '.hepmc3.tree.root'
0165
0166
0167 OBSOLETE_DIR = 'obsolete'
0168
0169
0170 def list_files(evgen_path, proxy):
0171 """Every EVGEN file under the path on the door: [(door_path, bytes)],
0172 and the files left out: NFS silly-rename remnants (``.nfs*``), files
0173 that are not ``.hepmc3.tree.root``, and files under a directory named
0174 ``obsolete``, each counted."""
0175 door_path = XRD_BASE + evgen_path
0176 try:
0177 p = _xrdfs(['ls', '-l', '-R', door_path], door_path, proxy, LISTING_TIMEOUT)
0178 except subprocess.TimeoutExpired:
0179 raise DoerError(EXIT_LISTING, f'listing timed out after {LISTING_TIMEOUT}s')
0180 if p.returncode != 0:
0181 reason = (p.stderr or p.stdout).strip().splitlines()
0182 raise DoerError(EXIT_LISTING, 'listing failed: '
0183 + (reason[-1] if reason else f'rc={p.returncode}'))
0184 files, skipped, other, obsolete = [], [], [], []
0185 for line in (p.stdout or '').splitlines():
0186 parts = line.split(maxsplit=6)
0187 if len(parts) < 7:
0188 continue
0189 flags, _owner, _group, size, _date, _time, path = parts
0190 if flags.startswith('d'):
0191 continue
0192 if OBSOLETE_DIR in path[len(door_path):].split('/')[:-1]:
0193 obsolete.append(path)
0194 continue
0195 if os.path.basename(path).startswith('.nfs'):
0196 skipped.append(path)
0197 continue
0198 if not path.endswith(EVGEN_SUFFIX):
0199 other.append(path)
0200 continue
0201 try:
0202 files.append((path, int(size)))
0203 except ValueError:
0204 raise DoerError(EXIT_LISTING, f'unparseable listing line: {line!r}')
0205 if not files:
0206 raise DoerError(EXIT_LISTING, f'no {EVGEN_SUFFIX} files under {door_path}'
0207 + (f' ({len(other)} other files)' if other else '')
0208 + (f' ({len(obsolete)} under {OBSOLETE_DIR}/)' if obsolete else ''))
0209 return files, skipped, other, obsolete
0210
0211
0212 def checksums(files, proxy, scope):
0213 """adler32 per file from the door, under the door path ``scope``;
0214 returns ({path: adler32}, [(path, why)])."""
0215 def one(path):
0216 try:
0217 p = _xrdfs(['query', 'checksum', path], scope, proxy, CHECKSUM_TIMEOUT)
0218 except subprocess.TimeoutExpired:
0219 return path, None, f'timed out after {CHECKSUM_TIMEOUT}s'
0220 parts = (p.stdout or '').split()
0221 if p.returncode == 0 and len(parts) == 2 and parts[0] == 'adler32':
0222 return path, parts[1], ''
0223 why = (p.stderr or p.stdout).strip().splitlines()
0224 return path, None, why[-1] if why else f'rc={p.returncode}'
0225
0226 ok, failed = {}, []
0227 with ThreadPoolExecutor(max_workers=CHECKSUM_WORKERS) as pool:
0228 for path, adler, why in pool.map(one, [f[0] for f in files]):
0229 if adler:
0230 ok[path] = adler
0231 else:
0232 failed.append((path, why))
0233 return ok, failed
0234
0235
0236
0237
0238 def plan_datasets(files, adlers):
0239 """Group files into datasets: one per directory holding files, named by
0240 the DID convention (door path minus XRD_BASE).
0241
0242 The PFN is the RSE prefix followed by the DID name, which carries its
0243 own leading slash: the deterministic mapping the RSE expects yields
0244 ``/volatile/eic/EPIC//EVGEN/...``, and add_replicas refuses a PFN that
0245 differs from it by so much as that slash."""
0246 datasets = defaultdict(list)
0247 for door_path, size in files:
0248 name = door_path[len(XRD_BASE):]
0249 datasets[os.path.dirname(name)].append({
0250 'scope': RUCIO_SCOPE, 'name': name, 'bytes': size,
0251 'adler32': adlers[door_path] if door_path in adlers else '',
0252 'pfn': f'{XRD_DOOR}/{XRD_BASE}/{name}'})
0253 return dict(sorted(datasets.items()))
0254
0255
0256 def rucio_client(proxy):
0257 """The Rucio client as the production account: by a token from the
0258 PanDA server, else by the proxy (swf_epicprod.jlab_auth); refuses a
0259 credential that maps to any other account. ``proxy`` may be None once
0260 the proxy is gone; the token is then the only credential."""
0261 from rucio.common.exception import RucioException
0262 from swf_epicprod.jlab_auth import rucio_client as jlab_rucio_client
0263 try:
0264 client = jlab_rucio_client(account=RUCIO_ACCOUNT, host=RUCIO_HOST, proxy=proxy)
0265 who = client.whoami()
0266 except (RucioException, RuntimeError) as e:
0267 raise DoerError(EXIT_RUCIO, f'Rucio auth failed: {e}')
0268 if who.get('account') != RUCIO_ACCOUNT:
0269 raise DoerError(EXIT_RUCIO, f'{client.jlab_auth} maps to account '
0270 f'{who.get("account")!r}, not {RUCIO_ACCOUNT!r}')
0271 _log(f'JLab Rucio as {RUCIO_ACCOUNT} by {client.jlab_auth}')
0272 return client
0273
0274
0275 def resolve_proxy_or_none():
0276 """resolve_proxy's (path, summary), or (None, {'error': reason}) when
0277 there is no usable proxy: for passes whose catalog work a token does
0278 alone, the proxy is only the fallback."""
0279 try:
0280 return resolve_proxy()
0281 except DoerError as e:
0282 return None, {'error': str(e)}
0283
0284
0285 def count_events(datasets, proxy, scope, write=True):
0286 """Event count per file, read through the door as the entry count of
0287 the HepMC3 tree (the tree header carries it; no event bytes move), then
0288 recorded as Rucio's ``events`` attribute on each file DID and, once
0289 every file of a dataset is counted, on the dataset. Returns one record
0290 per dataset; a file that cannot be counted is reported by name and
0291 leaves its dataset's total unset — never a partial total presented as
0292 the whole. ``write=False`` counts and reports without touching Rucio.
0293 uproot's xrootd client reads the door's credential for ``scope`` from
0294 this process's environment, which takes exactly the door's: the token
0295 over the root:// PFN as it is (uproot knows no roots://; the client
0296 turns on TLS for the token), else the proxy. A proxy left beside the
0297 token breaks the client's TLS (the robot's copy is group-readable), so
0298 the robot proxy stays the PanDA sign-in under its own name."""
0299 import uproot
0300 from concurrent.futures import TimeoutError as FutureTimeout
0301 from rucio.common.exception import RucioException
0302
0303 client = rucio_client(proxy) if write else None
0304 _door, env, how = door_credential(scope, proxy)
0305 if os.environ.get('X509_USER_PROXY'):
0306 os.environ.setdefault('EPICPROD_PANDA_LOGIN_PROXY', os.environ['X509_USER_PROXY'])
0307 for key in ('XrdSecPROTOCOL', 'BEARER_TOKEN', 'X509_USER_PROXY', 'X509_CERT_DIR'):
0308 if env.get(key):
0309 os.environ[key] = env[key]
0310 else:
0311 os.environ.pop(key, None)
0312 _log(f'door credential: {how}')
0313
0314 class _NoWrite:
0315 def set_metadata(self, *_args, **_kwargs):
0316 return True
0317
0318 def _one(entry):
0319 if not entry['name'].endswith('.root'):
0320 return entry['name'], None, 'not a ROOT file'
0321 try:
0322 with uproot.open(entry['pfn']) as fh:
0323 if EVENTS_TREE not in fh:
0324 return entry['name'], None, f'no {EVENTS_TREE} in file'
0325 return entry['name'], int(fh[EVENTS_TREE].num_entries), ''
0326 except Exception as e:
0327 return entry['name'], None, f'{type(e).__name__}: {e}'
0328
0329 client = client or _NoWrite()
0330 result = []
0331 for ds_name, entries in datasets.items():
0332 rec = {'did': f'{RUCIO_SCOPE}:{ds_name}', 'files': len(entries),
0333 'counted': 0, 'events': None, 'failed': []}
0334 counts = {}
0335 with ThreadPoolExecutor(max_workers=EVENTS_WORKERS) as pool:
0336 futures = {pool.submit(_one, e): e for e in entries}
0337 for fut, entry in futures.items():
0338 try:
0339 name, n, why = fut.result(timeout=EVENTS_TIMEOUT)
0340 except FutureTimeout:
0341 name, n, why = entry['name'], None, f'timed out after {EVENTS_TIMEOUT}s'
0342 if n is None:
0343 rec['failed'].append({'file': name, 'reason': why})
0344 else:
0345 counts[name] = n
0346 for name, n in counts.items():
0347 try:
0348 client.set_metadata(RUCIO_SCOPE, name, 'events', n)
0349 rec['counted'] += 1
0350 except RucioException as e:
0351 rec['failed'].append({'file': name, 'reason': f'set_metadata: {e}'})
0352 if rec['counted'] == len(entries):
0353
0354
0355
0356 total = sum(counts.values())
0357 if not write:
0358 rec['events'] = total
0359 else:
0360 try:
0361 derived = client.get_metadata(RUCIO_SCOPE, ds_name).get('events')
0362 except RucioException as e:
0363 derived = None
0364 rec['failed'].append({'file': ds_name,
0365 'reason': f'get_metadata: {e}'})
0366 if derived == total:
0367 rec['events'] = total
0368 elif derived is not None or not rec['failed']:
0369 rec['failed'].append({
0370 'file': ds_name,
0371 'reason': f'dataset events {derived} differs from the '
0372 f'sum of its files {total}'})
0373 rec['failed'] = rec['failed'][:20]
0374 _log(f' {rec["did"]}: {rec["counted"]}/{rec["files"]} files counted, '
0375 f'events {rec["events"] if rec["events"] is not None else "unset"}'
0376 + (f', {len(rec["failed"])} failed' if rec['failed'] else ''))
0377 result.append(rec)
0378 return result
0379
0380
0381 def register(datasets, proxy):
0382 from rucio.common.exception import (DataIdentifierAlreadyExists,
0383 DuplicateContent, FileAlreadyExists,
0384 RucioException)
0385 client = rucio_client(proxy)
0386
0387 result = []
0388 for ds_name, entries in datasets.items():
0389 rec = {'did': f'{RUCIO_SCOPE}:{ds_name}', 'files': len(entries),
0390 'bytes': sum(e['bytes'] for e in entries),
0391 'created': False, 'replicas_new': 0, 'attached_new': 0}
0392 try:
0393 client.add_dataset(scope=RUCIO_SCOPE, name=ds_name)
0394 rec['created'] = True
0395 except DataIdentifierAlreadyExists:
0396 pass
0397 except RucioException as e:
0398 raise DoerError(EXIT_RUCIO, f'add_dataset {ds_name}: {e}')
0399
0400
0401 for start in range(0, len(entries), BATCH_SIZE):
0402 batch = entries[start:start + BATCH_SIZE]
0403 try:
0404 client.add_replicas(rse=RUCIO_RSE, files=batch, ignore_availability=True)
0405 rec['replicas_new'] += len(batch)
0406 except FileAlreadyExists:
0407 for entry in batch:
0408 try:
0409 client.add_replicas(rse=RUCIO_RSE, files=[entry],
0410 ignore_availability=True)
0411 rec['replicas_new'] += 1
0412 except FileAlreadyExists:
0413 pass
0414 except RucioException as e:
0415 raise DoerError(EXIT_RUCIO, f'add_replicas {entry["name"]}: {e}')
0416 except RucioException as e:
0417 raise DoerError(EXIT_RUCIO, f'add_replicas batch in {ds_name}: {e}')
0418 dids = [{'scope': e['scope'], 'name': e['name']} for e in entries]
0419 for start in range(0, len(dids), BATCH_SIZE):
0420 batch = dids[start:start + BATCH_SIZE]
0421 try:
0422 client.attach_dids(scope=RUCIO_SCOPE, name=ds_name, dids=batch)
0423 rec['attached_new'] += len(batch)
0424 except (DuplicateContent, FileAlreadyExists):
0425 for did in batch:
0426 try:
0427 client.attach_dids(scope=RUCIO_SCOPE, name=ds_name, dids=[did])
0428 rec['attached_new'] += 1
0429 except (DuplicateContent, FileAlreadyExists):
0430 pass
0431 except RucioException as e:
0432 raise DoerError(EXIT_RUCIO, f'attach {did["name"]}: {e}')
0433 except RucioException as e:
0434 raise DoerError(EXIT_RUCIO, f'attach_dids batch to {ds_name}: {e}')
0435
0436
0437 try:
0438 in_rucio = {f['name'] for f in client.list_files(RUCIO_SCOPE, ds_name)}
0439 except RucioException as e:
0440 raise DoerError(EXIT_RUCIO, f'list_files {ds_name}: {e}')
0441 missing = [e['name'] for e in entries if e['name'] not in in_rucio]
0442 if missing:
0443 raise DoerError(EXIT_VERIFY, f'{len(missing)} of {len(entries)} files not '
0444 f'in {rec["did"]} after registration, e.g. {missing[0]}')
0445 rec['rucio_files'] = len(in_rucio)
0446 _log(f' {rec["did"]}: {rec["files"]} files, {rec["bytes"]} bytes, '
0447 f'{"created" if rec["created"] else "existed"}, '
0448 f'{rec["replicas_new"]} new replicas, {rec["attached_new"]} newly attached, '
0449 f'{rec["rucio_files"]} files in Rucio')
0450 result.append(rec)
0451 return result
0452
0453
0454
0455
0456 def main(argv):
0457 ap = argparse.ArgumentParser(description='Register one EVGEN directory in JLab Rucio.')
0458 ap.add_argument('--path', required=True, help='EVGEN path, e.g. /EVGEN/DIS/...')
0459 ap.add_argument('--dry-run', action='store_true',
0460 help='list and checksum only; write nothing to Rucio')
0461 ap.add_argument('--events-only', action='store_true',
0462 help='count events per file through the door and record '
0463 'them on the registered DIDs; no registration')
0464 args = ap.parse_args(argv[1:])
0465
0466 evgen_path = args.path.strip().rstrip('/')
0467 summary = {'ok': False, 'path': evgen_path, 'dry_run': bool(args.dry_run)}
0468 t0 = time.monotonic()
0469 try:
0470 if not PATH_OK.match(evgen_path) or '..' in evgen_path or '//' in evgen_path \
0471 or len([s for s in evgen_path.split('/') if s]) < 3:
0472 raise DoerError(EXIT_BAD_PATH, f'not an EVGEN path: {evgen_path!r}')
0473 proxy, summary['proxy'] = resolve_proxy_or_none()
0474 _log(f'proxy {proxy}: {summary["proxy"]["days_left"]} days left' if proxy
0475 else f'no proxy ({summary["proxy"]["error"]}): tokens only')
0476 scope = XRD_BASE + evgen_path
0477 summary['door_credential'] = door_credential(scope, proxy)[2]
0478 _log(f'door credential: {summary["door_credential"]}')
0479
0480 files, skipped, other, obsolete = list_files(evgen_path, proxy)
0481 summary.update(files=len(files), bytes=sum(s for _, s in files),
0482 skipped=len(skipped), not_evgen=len(other), obsolete=len(obsolete))
0483 _log(f'{len(files)} files, {summary["bytes"]} bytes under {XRD_BASE}{evgen_path}'
0484 + (f' ({len(skipped)} .nfs remnants skipped)' if skipped else '')
0485 + (f' ({len(other)} files not {EVGEN_SUFFIX} left out)' if other else '')
0486 + (f' ({len(obsolete)} files under {OBSOLETE_DIR}/ left out)' if obsolete else ''))
0487
0488 if args.events_only:
0489
0490
0491 datasets = plan_datasets(files, defaultdict(str))
0492 summary['datasets'] = count_events(datasets, proxy, scope,
0493 write=not args.dry_run)
0494 failed_files = sum(len(d['failed']) for d in summary['datasets'])
0495 summary['events'] = (sum(d['events'] for d in summary['datasets'])
0496 if all(d['events'] is not None
0497 for d in summary['datasets']) else None)
0498 if failed_files:
0499 raise DoerError(EXIT_EVENTS, f'{failed_files} file(s) not counted; '
0500 f'dataset totals left unset where a file is missing')
0501 summary['ok'] = True
0502 summary['seconds'] = round(time.monotonic() - t0, 1)
0503 print(json.dumps(summary), flush=True)
0504 return 0
0505
0506 adlers, failed = checksums(files, proxy, scope)
0507 if failed:
0508 summary['checksum_failed'] = [{'file': f, 'reason': why} for f, why in failed[:20]]
0509 for f, why in failed[:20]:
0510 _log(f' CHECKSUM FAILED {f}: {why}')
0511 raise DoerError(EXIT_CHECKSUM, f'{len(failed)} of {len(files)} checksums '
0512 f'not returned by the door; nothing registered')
0513 _log(f'{len(adlers)} checksums from the door in {time.monotonic() - t0:.0f}s')
0514
0515 datasets = plan_datasets(files, adlers)
0516 summary['datasets'] = [{'did': f'{RUCIO_SCOPE}:{n}', 'files': len(e),
0517 'bytes': sum(x['bytes'] for x in e)}
0518 for n, e in datasets.items()]
0519 if args.dry_run:
0520 for d in summary['datasets']:
0521 _log(f' would register {d["did"]}: {d["files"]} files, {d["bytes"]} bytes')
0522 summary['ok'] = True
0523 else:
0524 summary['datasets'] = register(datasets, proxy)
0525 summary['ok'] = True
0526 except DoerError as e:
0527 summary['error'] = str(e)
0528 summary['exit_code'] = e.code
0529 _log(f'ERROR: {e}')
0530 except Exception as e:
0531 summary['error'] = f'{type(e).__name__}: {e}'
0532 summary['exit_code'] = 1
0533 _log(f'ERROR: {summary["error"]}')
0534 summary['seconds'] = round(time.monotonic() - t0, 1)
0535 print(json.dumps(summary), flush=True)
0536 return 0 if summary['ok'] else summary['exit_code']
0537
0538
0539 if __name__ == '__main__':
0540 sys.exit(main(sys.argv))