#!/bin/bash # bj-quikexport-admin — привилегированные операции модуля «Экспорт биржевой # информации» (ARQA quikexport). Вызывается root-воркером bj-keymedia-worker # по запросам bj-server. Печатает JSON {"ok":true,...} или {"ok":false,"error":...}. set -u ACTION="${1:-}"; STAGING="${2:-}"; EXTRA="${BJ_EXTRA:-}" Q=/opt/arqasrv/quik/utils/quikexport; SVC=quikexport; OWNER=arqasrv json(){ python3 -c 'import json,sys;print(json.dumps(json.loads(sys.argv[1]),ensure_ascii=False))' "$1"; } err(){ python3 -c 'import json,sys;print(json.dumps({"ok":False,"error":sys.argv[1]},ensure_ascii=False))' "$1"; exit 0; } b64(){ base64 -w0 "$1" 2>/dev/null || true; } case "$ACTION" in dump) [ -d "$Q" ] || err "каталог экспортёра $Q не найден" python3 - "$Q" <<'PY' import json,sys,base64,os q=sys.argv[1]; out={"ok":True,"dir":q} for n in ("export.ini","info.ini"): p=os.path.join(q,n) out[n]=base64.b64encode(open(p,'rb').read()).decode() if os.path.exists(p) else "" out["keys"]=[n for n in ("pubring.txk","secring.txk") if os.path.exists(os.path.join(q,n))] out["keyfiles"]={n:base64.b64encode(open(os.path.join(q,n),'rb').read()).decode() for n in out["keys"]} print(json.dumps(out,ensure_ascii=False)) PY ;; apply) case "$STAGING" in /var/lib/bj/quik-export-apply/*) [ -d "$STAGING" ] || err "нет папки staging" ;; *) err "staging вне зоны" ;; esac [ -d "$Q" ] || err "каталог экспортёра $Q не найден — сначала установите модуль" TS=$(date +%Y%m%d-%H%M%S); BK="$Q/bj-backup-$TS"; mkdir -p "$BK" for f in export.ini info.ini pubring.txk secring.txk; do [ -f "$Q/$f" ] && cp -p "$Q/$f" "$BK/"; done applied=() for f in export.ini info.ini pubring.txk secring.txk; do if [ -f "$STAGING/$f" ]; then install -o $OWNER -g $OWNER -m 640 "$STAGING/$f" "$Q/$f" && applied+=("$f"); fi done if [ -f "$STAGING/odbc.ini" ]; then python3 - "$STAGING/odbc.ini" <<'PY' import sys,re new=open(sys.argv[1]).read().strip()+"\n" p="/etc/odbc.ini" try: cur=open(p).read() except FileNotFoundError: cur="" # заменяем/добавляем секцию [quik_export] parts=re.split(r'(?m)^(?=\[)',cur); keep=[s for s in parts if s.strip() and not s.startswith('[quik_export]')] open(p,'w').write("".join(keep).rstrip()+("\n\n" if keep else "")+new) PY applied+=("odbc.ini") fi # ротация логов экспортёра (quikapi.log растёт ~1.8 ГБ/сутки) cat > /etc/logrotate.d/quikexport </dev/null | tail -n +11 | xargs -r rm -rf svc="none" case "$EXTRA" in restart) systemctl daemon-reload 2>/dev/null; systemctl restart $SVC >/dev/null 2>&1 && svc="restarted" || svc="restart-failed" ;; stop) systemctl stop $SVC >/dev/null 2>&1 && svc="stopped" ;; esac python3 -c 'import json,sys;print(json.dumps({"ok":True,"applied":sys.argv[1].split(),"backup":sys.argv[2],"service":sys.argv[3]},ensure_ascii=False))' "${applied[*]:-}" "$BK" "$svc" ;; service) case "$EXTRA" in start|stop|restart) systemctl daemon-reload 2>/dev/null; out=$(systemctl "$EXTRA" $SVC 2>&1 | grep -v '^Warning:') ; systemctl is-active --quiet $SVC || [ "$EXTRA" = stop ] && json "{\"ok\":true,\"op\":\"$EXTRA\"}" || err "systemctl $EXTRA: $out" ;; enable) systemctl enable $SVC >/dev/null 2>&1; json '{"ok":true,"op":"enable"}' ;; *) err "неизвестная операция службы" ;; esac ;; db-init) case "$STAGING" in /var/lib/bj/quik-export-apply/*) [ -f "$STAGING/db.json" ] || err "нет db.json" ;; *) err "staging вне зоны" ;; esac python3 - "$STAGING" <<'PY' import json,sys,subprocess,os st=sys.argv[1]; d=json.load(open(os.path.join(st,"db.json"))) db,u,p,ru,rp=d["db"],d["user"],d["password"],d["ro_user"],d["ro_password"] def psql(sql,dbname="postgres"): r=subprocess.run(["sudo","-u","postgres","psql","-v","ON_ERROR_STOP=1","-Atq","-d",dbname,"-c",sql],capture_output=True,text=True) if r.returncode: raise SystemExit(json.dumps({"ok":False,"error":r.stderr.strip()[-400:]},ensure_ascii=False)) return r.stdout.strip() q=lambda s: "'"+s.replace("'","''")+"'" steps=[] if psql(f"select 1 from pg_roles where rolname={q(u)}")!="1": psql(f"create role {u} login password {q(p)}"); steps.append("роль "+u) else: psql(f"alter role {u} with password {q(p)}") if psql(f"select 1 from pg_roles where rolname={q(ru)}")!="1": psql(f"create role {ru} login password {q(rp)}"); steps.append("роль "+ru) else: psql(f"alter role {ru} with password {q(rp)}") psql(f"alter role {u} set log_min_messages = fatal") created=False if psql(f"select 1 from pg_database where datname={q(db)}")!="1": psql(f"create database {db} owner {u} encoding 'UTF8' template template0 lc_collate 'C.UTF-8' lc_ctype 'C.UTF-8'"); steps.append("база "+db); created=True if created or psql("select count(*) from pg_tables where schemaname='public' and tablename='Portfolio'",db)!="1": env=dict(os.environ,PGPASSWORD=p) for f in ("exporter_pg.sql","npr_migration.sql"): r=subprocess.run(["psql","-v","ON_ERROR_STOP=1","-q","-h","localhost","-U",u,"-d",db,"-f",os.path.join(st,f)],capture_output=True,text=True,env=env) if r.returncode: raise SystemExit(json.dumps({"ok":False,"error":f+": "+r.stderr.strip()[-400:]},ensure_ascii=False)) steps.append("схема "+f) psql(f"grant connect on database {db} to {ru}",) for s in ("public","npr"): psql(f"grant usage on schema {s} to {ru}; grant select on all tables in schema {s} to {ru}; alter default privileges for role {u} in schema {s} grant select on tables to {ru}",db) print(json.dumps({"ok":True,"steps":steps or ["база уже существует, права обновлены"]},ensure_ascii=False)) PY ;; install) case "$STAGING" in /var/lib/bj/quik-export-apply/*) : ;; *) err "staging вне зоны" ;; esac [ -d "$Q" ] && err "экспортёр уже установлен в $Q" RUN=$(ls "$STAGING"/install_quikexport-*.run 2>/dev/null | head -1); [ -n "$RUN" ] || err "в дистрибутиве нет install_quikexport-*.run" chmod +x "$RUN"; out=$(cd "$STAGING" && "$RUN" -i 2>&1) || err "установщик: ${out: -300}" apt-get install -y -q libjemalloc2 unixodbc odbc-postgresql libpq5 >/dev/null 2>&1 || true mkdir -p /etc/systemd/system/$SVC.service.d printf '[Unit]\nAfter=network-online.target postgresql.service\nWants=network-online.target\n[Service]\nRestart=on-failure\nRestartSec=15\nMemoryMax=3G\n[Install]\nWantedBy=multi-user.target\n' > /etc/systemd/system/$SVC.service.d/override.conf systemctl daemon-reload; systemctl enable $SVC >/dev/null 2>&1 json '{"ok":true,"installed":true}' ;; *) err "неизвестное действие $ACTION" ;; esac