# mgmtvm-update-oldgen.sh — фазы old-gen ( vStack-control-pane, perl ) для
# mgmtvm-update.sh. Не запускается сам: подключается через «.» и пользуется
# его функциями ( log, die, run, logrun, pkgs, state_set, reinstall_delivery ).
#
# Old-gen: vStack-control-pane 0.33.x, nginx, PostgreSQL 14, RabbitMQ 3.13,
# без seaql_migrations и /var/db/vstack/secrets. База совпадает с init-миграцией
# диспетчера ( m00_0000_initial ): переносится дампом, миграция регистрируется
# руками, остальные 30+ диспетчер докатывает сам.
#
# app-save ( на старой ОС, пока стоят PG14 и rabbit 3.13 ):
#   черновик settings.yml + inventory/nodes.yml -> стоп до --settings-reviewed;
#   секреты из vstack-passwords.conf; TLS из nginx; дамп БД; остановка и
#   выключение старых сервисов; data14 и rabbitmq — в сторону; удаление пакетов.
# app ( на 15 ): поставка, проход 1 ( диспетчер и seed выключены ), pg_restore
#   под mgmt, регистрация m00, выравнивание последовательностей, проход 2.
#
# Стадии nodes и seed-nodes скрипт не запускает: они трогают ноды.
#
# Инструкция: README.md в комплекте поставки

OG_PASS="${OG_PASS:-${LOCAL_ETC_DIR}/vstack-passwords.conf}"
OG_DCONF="${OG_DCONF:-${LOCAL_ETC_DIR}/vstack-dispatcher.conf}"
OG_ENV="${OG_ENV:-${LOCAL_ETC_DIR}/vstack-env.conf}"
OG_NGINX_DIR="${OG_NGINX_DIR:-${LOCAL_ETC_DIR}/nginx}"
OG_DUMP="${OG_DUMP:-/var/db/postgres/vstack-pre-center.dump}"
OG_COUNTS="${STATE_DIR}/oldgen-rowcounts.txt"
VSTACK_ETC="${VSTACK_ETC:-${LOCAL_ETC_DIR}/vstack}"
SETTINGS="${VSTACK_ETC}/settings.yml"
INVENTORY="${VSTACK_ETC}/inventory/nodes.yml"
SECRETS_DIR="${SECRETS_DIR:-/var/db/vstack/secrets}"
ANGIE_SSL="${ANGIE_SSL:-${LOCAL_ETC_DIR}/angie/ssl}"
OG_SERVICES="vStackCPDispatcher vStackCPEndpoint vStackclusterep nginx grafana influxd rabbitmq postgresql chronyd"
OG_RCVARS="vStackCPDispatcher_enable vStackCPDispatcher_args vStackCPEndpoint_enable vStackCPEndpoint_args vStackclusterep_enable nginx_enable chronyd_enable postgresql_enable rabbitmq_enable influxd_enable grafana_enable"
OG_PKGS_RE='^(vStack-control-pane|vStack-web|nginx|nginx-devel|postgresql1[0-7]-(server|client|contrib)|rabbitmq|grafana[0-9]+|chrony|chrony-lite)$'
OG_CRON_RE='vStack-control-pane|vStackCP|cron\.replication|p5\.'
OG_RMQ_USERS="vi_user net_user storage_user api-endpoint eemitter mgmt_dispatcher mgmt2vi mgmt2net mgmt2storage vm_controller mgmt2k8s edge_ctrl cluster-ep mgmt2nodeside nodeside_ctrl mgmt2target target_ctrl"
OG_MIGRATION_INITIAL="m00_0000_initial"

# SQL — через stdin: кавычки и $$ не проходят через su -c
og_psql() { printf '%s\n' "$1" | ( cd /tmp && su -m postgres -c "psql -X -q -At -v ON_ERROR_STOP=1 -d vstack" ); }
og_conf() { sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*//p" "$2" 2>/dev/null | head -1 | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'\$//"; }
og_ini() { awk -F= -v s="[$1]" -v k="$2" '{ l = $0; gsub(/^[ \t]+|[ \t]+$/, "", l) } l == s {in_s=1; next} l ~ /^\[/ {in_s=0} { k1 = $1; gsub(/[ \t]/, "", k1) } in_s && k1 == k {sub(/^[^=]*=[ \t]*/, ""); sub(/[ \t]+$/, ""); print; exit}' "$3" 2>/dev/null; }
og_net24() { printf '%s\n' "$1" | awk -F. 'NF == 4 {printf "%s.%s.%s.0/24\n", $1, $2, $3}'; }

og_next_phase() {
  if [ "${FROM_MAJOR}" -lt "${TARGET_MAJOR}" ] || [ "${FROM_MODE}" = classic ] || [ "${BASE_BEHIND:-0}" = 1 ]; then echo world
  else echo app; fi
}

# ── черновик settings.yml и инвентаря ───────────────────────────────────────

# Сети — из node.node ( шаблон settings.yml прямо ссылается на его колонки ),
# vstack-env.conf ( есть не везде: на q8 нет ) — запасной источник.
og_node() { og_psql "select $1 from node.node where $1 is not null order by id limit 1" 2>/dev/null || true; }
og_pick() { for _v in "$@"; do [ -n "${_v}" ] && { printf '%s' "${_v}"; return 0; }; done; return 0; }

og_draft_settings() {
  _url="$( og_ini Base URL "${OG_DCONF}" )" || true
  _host="${_url#*://}"; _host="${_host%%/*}"
  _dom="$( printf '%s\n' "${_host}" | sed -E 's/^(wss|mgmtvm)\.[^.]+\.//' )"
  _ip="$( sed -n "s/^ifconfig_vtnet0=['\"]inet \([0-9.]*\).*/\1/p" "${ETC_DIR}/rc.conf" | head -1 )" || true
  _lif="$( og_psql "select lif from storage.pool where lif is not null limit 1" 2>/dev/null )" || true
  _nvmf="$( og_psql "select nvmf_inet from storage.pool where nvmf_inet is not null limit 1" 2>/dev/null )" || true
  _ntp="$( cat "${LOCAL_ETC_DIR}/chrony.conf" "${ETC_DIR}/ntp.conf" 2>/dev/null | awk '$1 == "server" || $1 == "pool" {printf "%s\"%s\"", (n++ ? ", " : ""), $2}' )" || true
  _cm="$( og_ini Base cluster_management_ip "${OG_DCONF}" )" || true
  # cluster vlan: vstack-env.conf, cluster_management_ip диспетчера, адрес 172.16/12 в rc.conf
  _rcv="$( sed -nE "s/^ifconfig_[a-z0-9_]+=['\"]inet (172\.(1[6-9]|2[0-9]|3[01])\.[0-9]+\.[0-9]+).*/\1/p" "${ETC_DIR}/rc.conf" | head -1 )" || true
  _cv="$( og_pick "$( og_conf cluster_vlan_network "${OG_ENV}" )" "$( og_net24 "${_cm}" )" "$( og_net24 "${_rcv}" )" )"
  _pub_if="$( og_pick "$( og_node publicnet_if )" "$( og_conf publicep_vlan_port "${OG_ENV}" )" )"
  _pub_net="$( og_pick "$( og_net24 "$( og_node "host(publicnet_inet::inet)" )" )" "$( og_conf publicep_vlan_network "${OG_ENV}" )" )"
  _st_if="$( og_pick "$( og_node storagenet_if )" "$( og_conf iscsi_vlan_port "${OG_ENV}" )" )"
  _st_net="$( og_pick "$( og_net24 "$( og_node "host(storagenet_inet::inet)" )" )" "$( og_conf iscsi_vlan_network "${OG_ENV}" )" )"
  _vm_if="$( og_pick "$( og_node vmnet_if )" "$( og_conf vxlan_vlan_port "${OG_ENV}" )" )"
  _vm_net="$( og_pick "$( og_net24 "$( og_node "host(vmnet_inet::inet)" )" )" "$( og_conf vxlan_vlan_network "${OG_ENV}" )" )"
  _vx_id="$( og_conf vxlan_vlan_id "${OG_ENV}" )" || true
  _vx_if="$( og_pick "$( og_node vxlan_if )" "${_vx_id:+vlan${_vx_id}}" )"
  _vx_net="$( og_pick "$( og_net24 "$( og_node "host(vxlan_inet::inet)" )" )" "$( og_conf vxlan_vlan_network "${OG_ENV}" )" )"
  _vl_if="$( og_pick "$( og_node vlan_if )" "$( og_conf vxlan_vlan_port "${OG_ENV}" )" )"
  _kms="$( og_ini Base kms_host "${OG_DCONF}" | sed 's/:[0-9]*$//' )" || true
  mkdir -p "${VSTACK_ETC}/inventory" "${VSTACK_ETC}/ssh"
  chmod 0700 "${VSTACK_ETC}/ssh"
  cat > "${SETTINGS}" <<EOF
---
# ЧЕРНОВИК mgmtvm-update $( date -u '+%Y-%m-%d' ) из old-gen: node.node, storage.pool,
# ${OG_DCONF}, ${OG_ENV}, rc.conf. ПРОВЕРИТЬ КАЖДОЕ ЗНАЧЕНИЕ ( пустые — обязательно ),
# затем --settings-reviewed. dispatcher_disabled / seed_disabled скрипт
# переключает сам ( два прохода ).
pass_len: 24
dev_mode: false
cluster_id: $( og_ini Base cluster_id "${OG_DCONF}" )
cluster_node_quantity: $( og_ini Base node_quantity "${OG_DCONF}" )
cluster_node_base: $( og_ini Base node_base "${OG_DCONF}" )
base_url: "${_url}"
mgmtvm_external_ip: "${_ip}"                    # CHECK: адрес mgmtvm в сети publicep
cluster_vlan_network: "${_cv}"
publicep_net: "${_pub_net}"
publicep_if: "${_pub_if}"
storagenet: "${_st_net}"
storagenet_if: "${_st_if}"
vmnet_net: "${_vm_net}"
vmnet_if: "${_vm_if}"
vxlan_net: "${_vx_net}"
vxlan_if: "${_vx_if}"
vlan_if: "${_vl_if}"
pool_net: "$( og_net24 "${_lif}" )"                              # storage.pool.lif = ${_lif}
nvmf_net: "$( og_net24 "${_nvmf}" )"
vstack_node_install_nvmf: $( [ -n "${_nvmf}" ] && echo true || echo false )
root_domain: "${_dom}"
kms_host: "${_kms}"    # CHECK: сверить с kms в vstack-env.conf нод
api_fw: []
wss_fw: ["10.0.0.0/8", "100.64.0.0/10", "172.16.0.0/12", "192.168.0.0/16"]   # CHECK: allow в ${OG_NGINX_DIR}
grafana_fw: ["10.0.0.0/8", "100.64.0.0/10", "172.16.0.0/12", "192.168.0.0/16"]
dhparam_bootstrap: false
unified_storage_enabled: false
vmagent_enabled: false            # CHECK: включение — отдельное решение
logging_enabled: false
pf_enabled: false                 # CHECK: на old-gen свой /etc/pf.conf, роль pf его заменит
ntp_servers: [${_ntp}]
dispatcher_disabled: true
seed_disabled: true
EOF
  chmod 0600 "${SETTINGS}"
  _laggs="$( og_conf laggs "${OG_ENV}" )" || true
  {
    echo "# ЧЕРНОВИК mgmtvm-update $( date -u '+%Y-%m-%d' ): проверить, затем --settings-reviewed"
    echo "nodes:"
    echo "  vars:"
    echo "    ansible_user: root"
    echo "    ansible_python_interpreter: /usr/local/bin/python3.11"
    echo "    ansible_ssh_private_key_file: ${VSTACK_ETC}/ssh/id_ed25519"
    echo "    first_pool_disk: \"$( og_conf first_pool_disk "${OG_ENV}" )\""
    echo "    laggs: \"${_laggs}\""
    for _lg in lagg10 lagg20; do
      # vstack-env.conf: "lacp ice0 ice2 9216 IP /24" -> "lagg10 ice0 ice2"
      _v="$( og_conf "${_lg}" "${OG_ENV}" | awk -v l="${_lg}" 'NF {printf "%s", l; for (i = 2; i <= NF && $i !~ /^[0-9]/; i++) printf " %s", $i}' )"
      echo "    ${_lg}: \"${_v}\""
    done
    echo "  hosts:"
    og_psql "select hostname || ' ' || coalesce(host(publicnet_inet::inet), publicnet_inet::text) from node.node order by id" 2>/dev/null \
      | while read -r _h _a; do printf '    %s:\n      ansible_host: %s\n' "${_h%%.*}" "${_a}"; done
  } > "${INVENTORY}"
  chmod 0600 "${INVENTORY}"
  [ -f "${VSTACK_ETC}/ssh/id_ed25519" ] || ssh-keygen -q -t ed25519 -N '' -C "mgmtvm-center" -f "${VSTACK_ETC}/ssh/id_ed25519"
  log "NOTICE: drafts written: ${SETTINGS}, ${INVENTORY}; ssh key ${VSTACK_ETC}/ssh/id_ed25519.pub must go to authorized_keys of every node before the nodes stage"
}

# ── секреты: пароли old-gen, чтобы ноды и influx/grafana не разошлись ───────

og_put_secret() {
  _dst="${SECRETS_DIR}/$1"; _val="$2"
  [ -s "${_dst}" ] && return 0
  [ -n "${_val}" ] || { log "WARNING: no old-gen value for $1, the role will generate a new one"; return 0; }
  printf '%s\n' "${_val}" > "${_dst}.tmp" && chmod 0600 "${_dst}.tmp" && mv "${_dst}.tmp" "${_dst}"
}

og_seed_secrets() {
  mkdir -p "${SECRETS_DIR}"; chmod 0700 "${SECRETS_DIR}"
  [ -f "${OG_PASS}" ] || log "WARNING: ${OG_PASS} not found — passwords fall back to ${OG_DCONF}"
  _p() { _v="$( og_conf "$1" "${OG_PASS}" )"; [ -n "${_v}" ] || _v="$( og_ini AMQP "$1" "${OG_DCONF}" )"; printf '%s' "${_v}"; }
  og_put_secret postgresql_mgmt_password "$( _v="$( og_conf postgresql_mgmt "${OG_PASS}" )"; [ -n "${_v}" ] || _v="$( og_ini db pwd "${OG_DCONF}" )"; printf '%s' "${_v}" )"
  og_put_secret postgresql_rman_password "$( og_conf postgresql_rman "${OG_PASS}" )"
  og_put_secret postgresql_postgres_password "$( og_conf postgres "${OG_PASS}" )"
  og_put_secret influxdb_admin_password "$( og_conf influxdb_admin "${OG_PASS}" )"
  # q8: vstack-passwords.conf нет, пароль tg ( им пишут ноды ) — в vstack-node.conf
  _tg="$( og_conf influxdb_tg "${OG_PASS}" )"
  [ -n "${_tg}" ] || [ "$( og_ini influx user "${LOCAL_ETC_DIR}/vstack-node.conf" | tr -d ' ' )" != tg ] \
    || _tg="$( og_ini influx password "${LOCAL_ETC_DIR}/vstack-node.conf" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' )"
  og_put_secret influxdb_tg_password "${_tg}"
  og_put_secret influxdb_www_password "$( og_conf influxdb_www "${OG_PASS}" )"
  og_put_secret grafana_admin_password "$( og_conf grafana_admin "${OG_PASS}" )"
  og_put_secret rabbitmq_admin_password "$( og_conf rabbitmq_admin "${OG_PASS}" )"
  for _u in ${OG_RMQ_USERS}; do
    og_put_secret "rmq_pass_$( printf '%s' "${_u}" | tr - _ )" "$( _p "${_u}" )"
  done
  [ -s /var/db/rabbitmq/.erlang.cookie ] && og_put_secret rabbitmq_erlang_cookie "$( cat /var/db/rabbitmq/.erlang.cookie )"
  # dispatcher_hmac_key и endpoint_secret не переносятся: у perl-центра их нет,
  # пароли пользователей проверяются в БД ( pgcrypto ), password_hmac
  # диспетчер снимает миграцией v1_0_64. Роль сгенерирует новые.
  log "INFO: secrets seeded: $( ls "${SECRETS_DIR}" | wc -l | tr -d ' ' ) files in ${SECRETS_DIR}"
}

# Роль influxdb работает под admin с паролем из секретов. Если в influx уже
# есть данные с auth-enabled, а пароля admin в old-gen нет ( q8: заводили
# руками, в конфигах только tg ), он задаётся заново: старый influxd на минуту
# поднимается без авторизации и только на 127.0.0.1, admin получает новый
# пароль ( или заводится ), авторизация возвращается, вход проверяется.
og_influx_admin() {
  [ -s "${SECRETS_DIR}/influxdb_admin_password" ] && return 0
  grep -qE '^[[:space:]]*auth-enabled[[:space:]]*=[[:space:]]*true' "${LOCAL_ETC_DIR}/influxd.conf" 2>/dev/null || return 0
  # данные = базы кроме _internal ( meta и _internal influxd заводит сам на старте )
  [ -n "$( ls /var/db/influxdb/data 2>/dev/null | grep -vx _internal )" ] || return 0
  command -v influx >/dev/null && command -v influxd >/dev/null || die 8 "influx admin password unknown and no influx/influxd to reset it"
  log "INFO: influx admin password is unknown: resetting it with auth disabled on 127.0.0.1 for a moment"
  _pw="$( openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | cut -c1-24 )"
  [ "${#_pw}" -eq 24 ] || die 8 "cannot generate a password"
  _port=18086
  service influxd stop >/dev/null 2>&1 || true
  env INFLUXDB_HTTP_AUTH_ENABLED=false INFLUXDB_HTTP_BIND_ADDRESS="127.0.0.1:${_port}" \
    daemon -p /var/run/mgmtvm-update-influx.pid -u influxd /usr/local/bin/influxd -config "${LOCAL_ETC_DIR}/influxd.conf"
  _w=0; until influx -host 127.0.0.1 -port "${_port}" -execute 'SHOW DATABASES' >/dev/null 2>&1; do
    _w=$(( _w + 2 )); [ "${_w}" -le 120 ] || break; sleep 2; done
  _users="$( influx -host 127.0.0.1 -port "${_port}" -format csv -execute 'SHOW USERS' 2>/dev/null )" || true
  if printf '%s\n' "${_users}" | grep -q '^admin,'; then
    _q="SET PASSWORD FOR \"admin\" = '${_pw}'"
  else
    _q="CREATE USER \"admin\" WITH PASSWORD '${_pw}' WITH ALL PRIVILEGES"
  fi
  _r="$( influx -host 127.0.0.1 -port "${_port}" -execute "${_q}" 2>&1 )" || true
  # admin и раньше мог быть без ALL PRIVILEGES — дать
  influx -host 127.0.0.1 -port "${_port}" -execute 'GRANT ALL PRIVILEGES TO "admin"' >/dev/null 2>&1 || true
  kill "$( cat /var/run/mgmtvm-update-influx.pid 2>/dev/null )" 2>/dev/null || true
  _w=0; while pgrep -U influxd -x influxd >/dev/null; do _w=$(( _w + 1 )); [ "${_w}" -le 60 ] || break; sleep 1; done
  rm -f /var/run/mgmtvm-update-influx.pid
  service influxd start >/dev/null 2>&1 || die 8 "influxd does not start again after the admin reset"
  _w=0; until influx -host 127.0.0.1 -username admin -password "${_pw}" -execute 'SHOW USERS' >/dev/null 2>&1; do
    _w=$(( _w + 2 )); [ "${_w}" -le 120 ] || die 8 "influx admin reset failed ( ${_r} ): auth is back, the old admin password is unchanged or unknown"; sleep 2; done
  printf '%s\n' "${_pw}" > "${SECRETS_DIR}/influxdb_admin_password.tmp" && chmod 0600 "${SECRETS_DIR}/influxdb_admin_password.tmp" \
    && mv "${SECRETS_DIR}/influxdb_admin_password.tmp" "${SECRETS_DIR}/influxdb_admin_password"
  log "INFO: influx admin password reset, users: $( printf '%s' "${_users}" | sed -n '2,$p' | cut -d, -f1 | tr '\n' ' ' )"
}

og_copy_tls() {
  [ -s "${ANGIE_SSL}/server.crt" ] && return 0
  _crt=""; _key=""
  # из живых — тот, что покрывает хост base_url ( wss.qNN... ), иначе первый живой
  _url="$( og_ini Base URL "${OG_DCONF}" )"; _h="${_url#*://}"; _h="${_h%%/*}"
  for _c in $( grep -rhoE 'ssl_certificate[[:space:]]+[^;]+' "${OG_NGINX_DIR}" 2>/dev/null | awk '{print $2}' | sort -u ); do
    case "${_c}" in /*) ;; *) _c="${OG_NGINX_DIR}/${_c}" ;; esac
    [ -s "${_c}" ] || continue
    openssl x509 -checkend 0 -noout -in "${_c}" >/dev/null 2>&1 || { log "WARNING: ${_c} expired, skipped"; continue; }
    if [ -n "${_h}" ] && openssl x509 -noout -checkhost "${_h}" -in "${_c}" 2>/dev/null | grep -q 'does match'; then
      _crt="${_c}"; break
    fi
    [ -n "${_crt}" ] || _crt="${_c}"
  done
  [ -n "${_crt}" ] || { log "WARNING: no live nginx certificate, the role will issue a self-signed one"; return 0; }
  # ключ ищется не по соседству в конфиге ( он бывает в другом include ), а
  # по совпадению открытого ключа среди всех ssl_certificate_key nginx
  _pub="$( openssl x509 -noout -pubkey -in "${_crt}" 2>/dev/null )"
  for _k in $( grep -rhoE 'ssl_certificate_key[[:space:]]+[^;]+' "${OG_NGINX_DIR}" 2>/dev/null | awk '{print $2}' | sort -u ); do
    case "${_k}" in /*) ;; *) _k="${OG_NGINX_DIR}/${_k}" ;; esac
    [ -s "${_k}" ] || continue
    [ "$( openssl pkey -pubout -in "${_k}" 2>/dev/null )" = "${_pub}" ] && { _key="${_k}"; break; }
  done
  [ -n "${_key}" ] || { log "WARNING: no key matching ${_crt} under ${OG_NGINX_DIR}, TLS not carried over"; return 0; }
  mkdir -p "${ANGIE_SSL}"
  cp -p "${_crt}" "${ANGIE_SSL}/server.crt"; install -m 0600 "${_key}" "${ANGIE_SSL}/server.key"
  log "INFO: TLS carried over from nginx: ${_crt} ( $( openssl x509 -noout -enddate -in "${_crt}" ) )"
}

# Роль rabbitmq держит epmd только на 127.0.0.1 ( ERL_EPMD_ADDRESS ), а имя
# ноды rabbit@mgmtvm резолвится по /etc/hosts. На old-gen mgmtvm указывает на
# внешний адрес -> CLI и rc-скрипт rabbit не находят epmd ( timeout ), проход 1
# падает на старте брокера. На new-gen ( q999 ) имя — на 127.0.0.1, так и здесь.
og_fix_hosts() {
  _fq="$( hostname )"; _sh="${_fq%%.*}"
  [ -n "${_sh}" ] || return 0
  awk -v f="${_fq}" -v s="${_sh}" '$1 ~ /^127\./ { for (i = 2; i <= NF; i++) if ($i == s) ok = 1 } END { exit !ok }' "${ETC_DIR}/hosts" && return 0
  cp -p "${ETC_DIR}/hosts" "${STATE_DIR}/hosts.oldgen"
  awk -v f="${_fq}" -v s="${_sh}" '
    /^[[:space:]]*#/ || NF == 0 { print; next }
    $1 !~ /^127\./ && $1 != "::1" { o = $1; for (i = 2; i <= NF; i++) if ($i != f && $i != s) o = o "\t" $i; if (o != $1) print o; next }
    { print }
    END { printf "127.0.0.1\t%s\t%s\n", f, s }' "${STATE_DIR}/hosts.oldgen" > "${ETC_DIR}/hosts.tmp" && mv "${ETC_DIR}/hosts.tmp" "${ETC_DIR}/hosts"
  log "INFO: /etc/hosts: ${_sh} -> 127.0.0.1 ( as on new-gen; previous copy ${STATE_DIR}/hosts.oldgen )"
}

# Классический rabbitmq.config old-gen ( erlang-термы ) rabbit 4 читает вместе с
# rabbitmq.conf роли, и старые листенеры ( management на внешнем адресе и т. п. )
# перекрывают новые: q24, 30.09 — eaddrnotavail на старте. Весь каталог — в сторону.
og_legacy_rabbit_conf() {
  _rd="${LOCAL_ETC_DIR}/rabbitmq"
  if [ -d "${_rd}" ] && [ ! -d "${_rd}.pre-center" ] && ! pkg-static info -e rabbitmq; then
    mv "${_rd}" "${_rd}.pre-center"; log "INFO: old rabbitmq config moved to ${_rd}.pre-center"
  elif [ -f "${_rd}/rabbitmq.config" ]; then
    mv "${_rd}/rabbitmq.config" "${STATE_DIR}/rabbitmq.config.oldgen"; log "INFO: legacy ${_rd}/rabbitmq.config moved to ${STATE_DIR}"
  fi
}

og_row_counts() {
  og_psql "select n.nspname || '.' || c.relname from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind = 'r' and n.nspname not in ('pg_catalog','information_schema') and n.nspname !~ '^pg_' order by 1" \
    | while read -r _t; do printf '%s %s\n' "${_t}" "$( og_psql "select count(*) from ${_t}" )"; done
}

app_save_oldgen() {
  log "INFO: phase app-save ( old-gen ): drafts, secrets, TLS, dump, old services out"
  [ "$( id -u )" = 0 ] || die 2 "root required"
  if [ ! -f "${SETTINGS}" ]; then
    service postgresql status >/dev/null 2>&1 || die 8 "PostgreSQL 14 is not running: the drafts and the dump need it"
    run og_draft_settings
    if [ "${OPT_GO}" -eq 1 ]; then
      log "NOTICE: review ${SETTINGS} and ${INVENTORY} ( every CHECK ), then run the same command with --settings-reviewed"
      _EXPECTED_EXIT=1; exit 10
    fi
    return 0
  fi
  [ "${OPT_SETTINGS_OK}" -eq 1 ] || die 10 "${SETTINGS} exists but is not confirmed: review it, then rerun with --settings-reviewed"
  run og_seed_secrets
  [ "${OPT_GO}" -eq 1 ] && og_influx_admin
  run og_copy_tls
  run og_fix_hosts
  if [ ! -s "${OG_DUMP}" ] || [ ! -f "${STATE_DIR}/oldgen-dump.ok" ]; then
    service postgresql status >/dev/null 2>&1 || die 8 "PostgreSQL 14 is not running, cannot dump"
    stop_services "vStackCPDispatcher vStackCPEndpoint vStackclusterep"
    run "( cd /tmp && su -m postgres -c \"pg_dump -Fc --no-owner --no-acl -d vstack -f ${OG_DUMP}.tmp\" ) && mv ${OG_DUMP}.tmp ${OG_DUMP}" || die 8 "pg_dump failed"
    run "og_row_counts > '${OG_COUNTS}'" || die 8 "row counts failed"
    run "pg_restore -l '${OG_DUMP}' > /dev/null && touch '${STATE_DIR}/oldgen-dump.ok'" || die 8 "dump ${OG_DUMP} is not readable"
    [ "${OPT_GO}" -eq 1 ] && log "INFO: dump ${OG_DUMP} ( $( du -h "${OG_DUMP}" | cut -f1 ) ), $( wc -l < "${OG_COUNTS}" | tr -d ' ' ) tables counted"
  fi
  stop_services "${OG_SERVICES}"
  run "pkill -f /usr/local/vStack-control-pane/ || true"
  run "cp -p '${ETC_DIR}/rc.conf' '${STATE_DIR}/rc.conf.oldgen'; [ -f '${ETC_DIR}/rc.conf.local' ] && cp -p '${ETC_DIR}/rc.conf.local' '${STATE_DIR}/rc.conf.local.oldgen' || true"
  for _f in "${ETC_DIR}/rc.conf" "${ETC_DIR}/rc.conf.local"; do
    [ -f "${_f}" ] || continue
    for _v in ${OG_RCVARS}; do
      grep -q "^${_v}=" "${_f}" && run "sysrc -f '${_f}' -x '${_v}' >/dev/null"
    done
  done
  # на месте, для отката: PG18 и rabbit 4 поднимутся с чистыми каталогами
  [ -d /var/db/postgres/data14 ] && run "mv /var/db/postgres/data14 /var/db/postgres/data14.pre-center"
  [ -d /var/db/rabbitmq ] && [ ! -d /var/db/rabbitmq.pre-center ] && run "mv /var/db/rabbitmq /var/db/rabbitmq.pre-center"
  run og_legacy_rabbit_conf
  # задания cron perl-центра ( q24: p5.Stor.ReFeeder раз в 5 минут ) без него сыплют ошибками
  if grep -qE "${OG_CRON_RE}" "${ETC_DIR}/crontab" 2>/dev/null || crontab -l 2>/dev/null | grep -qE "${OG_CRON_RE}"; then
    run "cp -p '${ETC_DIR}/crontab' '${STATE_DIR}/crontab.oldgen'; crontab -l > '${STATE_DIR}/crontab.root.oldgen' 2>/dev/null || true"
    run "sed -i '' -E '/${OG_CRON_RE}/d' '${ETC_DIR}/crontab'"
    run "crontab -l 2>/dev/null | grep -vE '${OG_CRON_RE}' | crontab - 2>/dev/null || true"
    log "INFO: old-gen cron jobs removed ( copies in ${STATE_DIR}/crontab*.oldgen )"
  fi
  _del="$( pkg-static query '%n' | grep -E "${OG_PKGS_RE}" | tr '\n' ' ' )"
  if [ -n "${_del}" ]; then
    run "logrun pkg-static delete -n ${_del}" || true
    run "logrun pkg-static delete -y ${_del}" || die 3 "cannot delete old-gen packages: ${_del}"
  fi
  # отметка переживает следующие запуски ( они перезаписывают GENERATION ): по ней
  # --cleanup-oldgen работает и после обычного обновления
  run "date -u '+%Y-%m-%dT%H:%M:%SZ' > '${STATE_DIR}/oldgen.migrated'"
  NEXT_PHASE="$( og_next_phase )"
  state_set "${NEXT_PHASE}"
}

# ── на 15: два прохода роли, данные между ними ──────────────────────────────

og_settings_pass() {
  for _k in dispatcher_disabled seed_disabled; do
    if grep -q "^${_k}:" "${SETTINGS}"; then
      sed -i '' "s/^${_k}:.*/${_k}: $1/" "${SETTINGS}"
    else
      echo "${_k}: $1" >> "${SETTINGS}"
    fi
  done
}

og_restore() {
  _n="$( og_psql "select count(*) from pg_tables where schemaname not in ('pg_catalog','information_schema')" )"
  [ "${_n}" -eq 0 ] || die 8 "database vstack is not empty ( ${_n} tables ) — restore skipped; drop it or restore the snapshot"
  PGPASSWORD="$( head -1 "${SECRETS_DIR}/postgresql_mgmt_password" )" \
    pg_restore -h 127.0.0.1 -U mgmt -d vstack --no-owner --role=mgmt -x --exit-on-error "${OG_DUMP}" \
    || die 8 "pg_restore failed"
}

og_fix_db() {
  # владельцы: всё под mgmt ( --no-owner --role=mgmt так и делает; проверка )
  _bad="$( og_psql "select count(*) from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname not in ('pg_catalog','information_schema') and n.nspname !~ '^pg_' and c.relkind in ('r','p','S','v','m') and pg_get_userbyid(c.relowner) <> 'mgmt'" )"
  [ "${_bad}" -eq 0 ] || die 8 "${_bad} relations are not owned by mgmt after restore"
  og_psql "create table if not exists public.seaql_migrations ( version varchar primary key, applied_at bigint not null ); alter table public.seaql_migrations owner to mgmt; insert into public.seaql_migrations ( version, applied_at ) values ( '${OG_MIGRATION_INITIAL}', extract( epoch from now() )::bigint ) on conflict do nothing" >/dev/null \
    || die 8 "cannot register ${OG_MIGRATION_INITIAL}"
  # последовательности old-gen отстают от данных ( q24: vdc_id_seq 1003 при max(id) 1024 )
  og_psql "$( cat <<'SQL'
do $$
declare r record; m bigint; cur bigint;
begin
  for r in
    select s.oid::regclass as seq, t.oid::regclass as tbl, a.attname as col
      from pg_class s
      join pg_depend d on d.objid = s.oid and d.classid = 'pg_class'::regclass and d.deptype in ('a', 'i')
      join pg_class t on t.oid = d.refobjid
      join pg_attribute a on a.attrelid = t.oid and a.attnum = d.refobjsubid
     where s.relkind = 'S'
  loop
    execute format('select max(%I) from %s', r.col, r.tbl) into m;
    select coalesce(last_value, 0) into cur from pg_sequences
     where format('%I.%I', schemaname, sequencename)::regclass = r.seq;
    if m is not null and m >= cur then
      perform setval(r.seq, m);
      raise notice 'setval % -> %', r.seq, m;
    end if;
  end loop;
end $$;
SQL
)" \
    || die 8 "sequence alignment failed"
}

og_verify_counts() {
  [ -s "${OG_COUNTS}" ] || return 0
  _diff=0
  while read -r _t _c; do
    _now="$( og_psql "select count(*) from ${_t}" 2>/dev/null )" || continue
    [ "${_now}" = "${_c}" ] || { log "WARNING: ${_t}: ${_c} rows in the dump, ${_now} now"; _diff=$(( _diff + 1 )); }
  done < "${OG_COUNTS}"
  log "INFO: row counts vs dump: $( wc -l < "${OG_COUNTS}" | tr -d ' ' ) tables, ${_diff} differ"
}

app_oldgen() {
  [ -s "${OG_DUMP}" ] || die 8 "no dump ${OG_DUMP}: the app-save phase did not run"
  run og_fix_hosts
  run og_legacy_rabbit_conf
  stop_services "${CENTER_SERVICES}"
  reinstall_delivery
  if [ ! -f "${STATE_DIR}/oldgen-restore.ok" ]; then
    run "og_settings_pass true"
    run "service vstack_center onestart mgmtvm >> \"${TEE_LOG}\" 2>&1" || die 8 "pass 1 ( dispatcher and seed disabled ) failed, see /var/log/vstack_center.log"
    run og_restore
    run og_fix_db
    run "touch '${STATE_DIR}/oldgen-restore.ok'"
  fi
  run "og_settings_pass false"
  run "service vstack_center onestart mgmtvm >> \"${TEE_LOG}\" 2>&1" || die 8 "pass 2 failed, see /var/log/vstack_center.log"
  if [ "${OPT_GO}" -eq 1 ]; then
    check_services || die 5 "center services are not all running"
    log "INFO: migrations: $( og_psql "select count(*) || ', last ' || max(version) from seaql_migrations" )"
    og_verify_counts
    log "NOTICE: rabbit passwords of node-side users that old-gen did not keep on the mgmtvm were generated anew: the nodes stage is required before nodes reconnect"
    log "NOTICE: nodes are untouched: put ${VSTACK_ETC}/ssh/id_ed25519.pub on every node, then service vstack_center onestart nodes with --check --diff first, then seed-nodes"
  fi
}

# ── уборка после перехода ( --cleanup-oldgen ) ──────────────────────────────
# Отдельным шагом, а не в finish: data14 и брокер 3.13 — данные отката, их
# снимают, когда Center на 15 проверен. Снимает всё, что от perl-центра
# осталось и Center не нужно: данные и конфиги прежних сервисов, rc-скрипты и
# rc-переменные, cron, сироты-пакеты ( perl ), файлы паролей old-gen.

OG_LEFT_FILES="/var/db/postgres/data14.pre-center /var/db/rabbitmq.pre-center ${LOCAL_ETC_DIR}/rabbitmq.pre-center ${OG_DUMP}
  /usr/local/vStack-control-pane /usr/local/vstack-control-pane /usr/local/vStack-web /var/db/old.rabbitmq ${LOCAL_ETC_DIR}/vstack-dispatcher.conf ${LOCAL_ETC_DIR}/vstack-passwords.conf
  ${LOCAL_ETC_DIR}/vStack-cluster-ep.conf ${LOCAL_ETC_DIR}/vstack-node.conf ${LOCAL_ETC_DIR}/vstack-env.conf
  ${LOCAL_ETC_DIR}/chrony.conf ${LOCAL_ETC_DIR}/chrony.conf.sample /var/db/vStackCPDispatcher ${LOCAL_ETC_DIR}/vstack-autosupport-agent.conf
  /var/db/chrony ${STATE_DIR}/oldgen-dump.ok ${STATE_DIR}/oldgen-rowcounts.txt ${STATE_DIR}/rabbitmq.config.oldgen
  ${STATE_DIR}/oldgen-restore.ok ${STATE_DIR}/crontab.oldgen ${STATE_DIR}/crontab.root.oldgen ${STATE_DIR}/hosts.oldgen
  ${STATE_DIR}/rc.conf.oldgen ${STATE_DIR}/rc.conf.local.oldgen"
OG_LEFT_RC="vStackCPDispatcher vStackCPEndpoint vStackclusterep nginx chronyd"
OG_LEFT_USERS="dispatcher endpoint"
OG_LEFT_PKGS_RE='^(chrony.*|vStack-.*|nginx.*|p5-.*|perl5.*|postgresql1[0-7]-.*|grafana[0-9]+|erlang-runtime2[0-6]|vStack-.*)$'

# Кандидат на удаление, которым ( или чем-то внутри которого ) владеет пакет,
# не трогается: 01.10 на q24 так ушёл /usr/local/www с файлами angie.
og_pkg_owned() { pkg-static which -q "$1" >/dev/null 2>&1 || pkg-static query '%Fp' 2>/dev/null | grep -q "^$1/"; }

og_cleanup_list() {
  for _f in ${OG_LEFT_FILES}; do [ -e "${_f}" ] && ! og_pkg_owned "${_f}" && echo "file ${_f}"; done
  [ -d "${OG_NGINX_DIR}" ] && ! og_pkg_owned "${OG_NGINX_DIR}" && echo "file ${OG_NGINX_DIR}"
  for _s in ${OG_LEFT_RC}; do
    [ -f "${LOCAL_RC_DIR}/${_s}" ] && [ -z "$( pkg-static which -q "${LOCAL_RC_DIR}/${_s}" 2>/dev/null )" ] && echo "file ${LOCAL_RC_DIR}/${_s}"
  done
  for _f in "${ETC_DIR}/rc.conf" "${ETC_DIR}/rc.conf.local"; do
    [ -f "${_f}" ] || continue
    sed -nE 's/^(vStack[A-Za-z]*_[a-z_]+|chronyd_[a-z_]+|nginx_[a-z_]+)=.*/rcvar \1/p' "${_f}" | sed "s|\$| ${_f}|"
  done
  { cat "${ETC_DIR}/crontab" 2>/dev/null; crontab -l 2>/dev/null; } | grep -nE "${OG_CRON_RE}" | sed 's/^/cron /'
  # учётки perl-демонов ( заводились руками ): Center их не использует, его демоны под root
  _users="${OG_LEFT_USERS}"
  pkg-static query '%n' | grep -q '^chrony' || _users="${_users} chronyd"
  for _u in ${_users}; do
    pw usershow "${_u}" >/dev/null 2>&1 || continue
    pgrep -U "${_u}" >/dev/null 2>&1 && continue
    echo "user ${_u}"
  done
  # сироты-зависимости ( perl, erlang 26, llvm15 ... ): pkg autoremove, одной строкой
  _n="$( pkg-static autoremove -n 2>/dev/null | sed -n '/to be REMOVED/,/^$/p' | grep -c ':' )" || true
  [ "${_n:-0}" -eq 0 ] || echo "autoremove ${_n}"
  for _p in $( pkg-static query '%n' | grep -E "${OG_LEFT_PKGS_RE}" ); do
    [ -z "$( pkg-static query '%rn' "${_p}" )" ] && ! pkgs rquery -U -r "${PRODUCT}" '%n' "${_p}" >/dev/null 2>&1 && echo "pkg ${_p}"
  done | sort -u
}

oldgen_cleanup() {
  log "INFO: phase oldgen-cleanup"
  [ "$( state_get )" = "done" ] || die 10 "the update is not finished ( state $( state_get ) ): cleanup only after phase done"
  [ "${GENERATION:-}" = old-gen ] || [ -f "${STATE_DIR}/oldgen.migrated" ] || die 2 "this machine was not migrated from old-gen ( GENERATION=${GENERATION:-?}, no ${STATE_DIR}/oldgen.migrated )"
  check_services || die 5 "center services are not all running: leftovers are the rollback, not removing"
  _m="$( og_psql "select count(*) from seaql_migrations" 2>/dev/null )" || true
  [ "${_m:-0}" -gt 1 ] || die 8 "seaql_migrations has ${_m:-0} rows: the dispatcher did not migrate, keeping the rollback data"
  prepare_repo
  _list="$( og_cleanup_list )"
  if [ -z "${_list}" ]; then log "INFO: nothing left from old-gen"; NEXT_PHASE="done"; return 0; fi
  printf '%s\n' "${_list}" | while IFS= read -r _l; do log "INFO: remove ${_l}"; done
  [ "${OPT_GO}" -eq 1 ] || { NEXT_PHASE="done"; return 0; }
  printf '%s\n' "${_list}" | awk '$1 == "file" {print $2}' | while IFS= read -r _f; do
    chflags -R noschg "${_f}" 2>/dev/null || true; rm -rf "${_f}"
  done
  printf '%s\n' "${_list}" | awk '$1 == "rcvar" {print $2, $3}' | while read -r _v _f; do
    sysrc -f "${_f}" -x "${_v}" >/dev/null 2>&1 || true
  done
  if printf '%s\n' "${_list}" | grep -q '^cron '; then
    sed -i '' -E "/${OG_CRON_RE}/d" "${ETC_DIR}/crontab"
    crontab -l 2>/dev/null | grep -vE "${OG_CRON_RE}" | crontab - 2>/dev/null || true
  fi
  printf '%s\n' "${_list}" | awk '$1 == "user" {print $2}' | while read -r _u; do
    find /tmp /var/tmp -xdev -user "${_u}" -delete 2>/dev/null || true
    pw userdel -n "${_u}" 2>/dev/null || log "WARNING: pw userdel ${_u} failed"
    pw groupshow "${_u}" >/dev/null 2>&1 && pw groupdel -n "${_u}" 2>/dev/null || true
  done
  _pk="$( printf '%s\n' "${_list}" | awk '$1 == "pkg" {print $2}' | tr '\n' ' ' )"
  [ -z "${_pk}" ] || logrun pkg-static delete -y ${_pk} || log "WARNING: could not delete ${_pk}"
  logrun pkg-static autoremove -y || true
  _left="$( og_cleanup_list )"
  [ -z "${_left}" ] || log "WARNING: still left: $( printf '%s' "${_left}" | tr '\n' ';' )"
  _bad="$( pkg-static check -d -a 2>&1 | grep -v '^Checking' )" || true
  [ -z "${_bad}" ] || log "WARNING: pkg check -d: $( printf '%s' "${_bad}" | tr '\n' ';' )"
  check_services || die 5 "a center service went down after the cleanup"
  log "INFO: old-gen leftovers removed"
  NEXT_PHASE="done"
}
