144 lines
5.5 KiB
Python
Executable File
144 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""apply-dshackle-overrides.py - weave HOST-LOCAL dshackle upstream overrides into
|
|
the generated main_configs/*.yaml so they survive every configure-drpc regeneration.
|
|
|
|
Override file (NOT in the repo - host-local, like rpc-local/compose.d):
|
|
/root/rpc-local/dshackle-overrides.yaml
|
|
|
|
Schema (match by upstream id OR by chain; id wins):
|
|
upstreams:
|
|
uk-4-base-mainnet-op-reth-pruned: # or e.g. "chain:base"
|
|
method_groups_disabled: [trace, debug] # removed from enabled, added to disabled
|
|
methods_disabled: [eth_getFilterLogs] # per-method disables (methods.disabled)
|
|
options:
|
|
disable-validation: true # merged into upstream options
|
|
|
|
Rationale: heavy methods at flat per-call pay poison the chain's latency scoring
|
|
(operator method-eviction rule 2026-07-10; see method-economics.py). The disable
|
|
decision is local to the gateway's market, so the config lives on the gateway.
|
|
|
|
Called by reload_dshackle.sh before signaling dshackle - i.e. re-applied on every
|
|
ansible config push. NOTE: dshackle only re-reads upstream method lists on RESTART;
|
|
this keeps the on-disk config permanently correct, the next restart activates it.
|
|
Idempotent. Silent no-op when the override file is absent.
|
|
"""
|
|
import glob
|
|
import os
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
OVERRIDES = '/root/rpc-local/dshackle-overrides.yaml'
|
|
CONFIG_DIR = '/root/rpc/main_configs'
|
|
|
|
|
|
def apply(upstream, ov):
|
|
changed = False
|
|
mg = upstream.setdefault('method-groups', {}) or {}
|
|
for grp in ov.get('method_groups_disabled') or []:
|
|
en = mg.get('enabled') or []
|
|
if grp in en:
|
|
en.remove(grp)
|
|
mg['enabled'] = en
|
|
changed = True
|
|
dis = mg.get('disabled') or []
|
|
if grp not in dis:
|
|
dis.append(grp)
|
|
mg['disabled'] = dis
|
|
changed = True
|
|
if mg:
|
|
upstream['method-groups'] = mg
|
|
for meth in ov.get('methods_disabled') or []:
|
|
methods = upstream.setdefault('methods', {}) or {}
|
|
dis = methods.get('disabled') or []
|
|
if not any((d.get('name') if isinstance(d, dict) else d) == meth for d in dis):
|
|
dis.append({'name': meth})
|
|
methods['disabled'] = dis
|
|
upstream['methods'] = methods
|
|
changed = True
|
|
for k, v in (ov.get('options') or {}).items():
|
|
opts = upstream.setdefault('options', {}) or {}
|
|
if opts.get(k) != v:
|
|
opts[k] = v
|
|
upstream['options'] = opts
|
|
changed = True
|
|
return changed
|
|
|
|
|
|
ENVFILE = '/root/rpc/.env'
|
|
|
|
|
|
def env_overrides():
|
|
"""Chain-level overrides from .env (rendered from host_vars env_overrides):
|
|
DSHACKLE_<CHAIN>_METHOD_GROUPS_DISABLED=trace,debug
|
|
DSHACKLE_<CHAIN>_METHODS_DISABLED=eth_getFilterLogs
|
|
<CHAIN> = dshackle chain name, upper, '-' -> '_' (e.g. BASE, ETH_BEACON_CHAIN)."""
|
|
spec = {}
|
|
if not os.path.exists(ENVFILE):
|
|
return spec
|
|
import re as _re
|
|
for line in open(ENVFILE):
|
|
m = _re.match(r'DSHACKLE_([A-Z0-9_]+)_(METHOD_GROUPS|METHODS)_DISABLED=(.*)', line.strip())
|
|
if not m:
|
|
continue
|
|
chain = m.group(1).lower().replace('_', '-')
|
|
key = 'method_groups_disabled' if m.group(2) == 'METHOD_GROUPS' else 'methods_disabled'
|
|
vals = [v.strip() for v in m.group(3).split(',') if v.strip()]
|
|
if vals:
|
|
spec.setdefault(f'chain:{chain}', {}).setdefault(key, []).extend(vals)
|
|
return spec
|
|
|
|
|
|
def main():
|
|
spec = env_overrides()
|
|
if os.path.exists(OVERRIDES):
|
|
for k, v in ((yaml.safe_load(open(OVERRIDES)) or {}).get('upstreams') or {}).items():
|
|
cur = spec.setdefault(k, {})
|
|
for kk, vv in (v or {}).items():
|
|
if isinstance(vv, list):
|
|
cur[kk] = sorted(set((cur.get(kk) or []) + vv))
|
|
else:
|
|
cur.setdefault(kk, vv)
|
|
if not spec:
|
|
return
|
|
total = 0
|
|
for path in glob.glob(os.path.join(CONFIG_DIR, '*.yaml')):
|
|
try:
|
|
doc = yaml.safe_load(open(path))
|
|
except yaml.YAMLError:
|
|
continue
|
|
if not isinstance(doc, dict) or 'upstreams' not in doc:
|
|
continue
|
|
changed = False
|
|
for up in doc['upstreams'] or []:
|
|
if not isinstance(up, dict):
|
|
continue
|
|
ov = spec.get(up.get('id')) or spec.get(f"chain:{up.get('chain')}")
|
|
if ov and apply(up, ov):
|
|
changed = True
|
|
if changed:
|
|
text = yaml.safe_dump(doc, default_flow_style=False, sort_keys=False)
|
|
# dshackle silently refuses to hot-reload configs containing TABS
|
|
# (operator gotcha 2026-07-10) - fail loudly, never write a tabbed file.
|
|
if '\t' in text:
|
|
print(f'dshackle-overrides: REFUSING to write {path} - tab in output')
|
|
continue
|
|
with open(path, 'w') as f:
|
|
f.write(text)
|
|
total += 1
|
|
print(f'dshackle-overrides: rewove {path}')
|
|
# tab audit on ALL configs (even unchanged): a tabbed file silently fails
|
|
# dshackle's hot-reload, so disk and running state diverge invisibly.
|
|
for path in glob.glob(os.path.join(CONFIG_DIR, '*.yaml')):
|
|
try:
|
|
if '\t' in open(path, errors='replace').read():
|
|
print(f'dshackle-overrides: WARNING {path} contains TABS - dshackle will SILENTLY skip reloading it')
|
|
except OSError:
|
|
pass
|
|
if total:
|
|
print(f'dshackle-overrides: {total} file(s) rewoven (dshackle hot-reloads on the HUP that follows)')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|