daef7ea190
Helper functions (matrix-mas capability group): - mas_client_register_bash_infra: register/sync OAuth clients via mas-cli - mas_syn2mas_migration_bash_infra: dry-run + apply user migration to MAS - synapse_msc3861_enable_go_infra: edit homeserver.yaml MSC3861 block (with diff) - wellknown_oidc_patch_go_infra: patch well-known JSON with msc2965.authentication - synapse_login_flows_check_go_infra: health-check post-migration login flows Flows + issues for custom Matrix clients (PC + Android): - 0010 matrix-client-pc: Wails + React+Mantine (issues 0147-0153) - 0011 matrix-client-android: Kotlin + Compose (issues 0154-0161) - 0162 enable MAS as auth provider (Synapse delegate) — EXECUTED on VPS - 0163 custom admin panel propio (sustituye synapse-admin) Production state (organic-machine.com): - Synapse migrated SQLite -> Postgres - MSC3861 active, password_config disabled - 21 users + 41 access_tokens migrated via syn2mas - 4 MAS clients registered (element, matrix_pc, matrix_android, admin_panel) - synapse-admin container removed + Coolify route deleted - well-known patched with org.matrix.msc2965.authentication Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
326 lines
14 KiB
Bash
326 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|
# mas_syn2mas_migration — Migra usuarios Synapse a MAS via mas-cli syn2mas.
|
|
# Fuerza dry-run primero, archiva el log, aborta si conflicts > threshold,
|
|
# y solo ejecuta la migracion real cuando se pasa --apply.
|
|
#
|
|
# Usage:
|
|
# mas_syn2mas_migration --ssh-host <host> --mas-container <name> \
|
|
# --synapse-config-path <path-on-host> --log-dir <local-path> \
|
|
# [--max-conflicts N] [--apply]
|
|
#
|
|
# Output: JSON en stdout con status, dry_run_log, apply_log, conflicts, users_migrated, duration_s
|
|
|
|
set -euo pipefail
|
|
|
|
mas_syn2mas_migration() {
|
|
local ssh_host=""
|
|
local mas_container=""
|
|
local synapse_config_path=""
|
|
local log_dir=""
|
|
local max_conflicts=0
|
|
local do_apply=false
|
|
|
|
# ---- Parse args ----
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--ssh-host)
|
|
ssh_host="$2"
|
|
shift 2
|
|
;;
|
|
--mas-container)
|
|
mas_container="$2"
|
|
shift 2
|
|
;;
|
|
--synapse-config-path)
|
|
synapse_config_path="$2"
|
|
shift 2
|
|
;;
|
|
--log-dir)
|
|
log_dir="$2"
|
|
shift 2
|
|
;;
|
|
--max-conflicts)
|
|
max_conflicts="$2"
|
|
shift 2
|
|
;;
|
|
--apply)
|
|
do_apply=true
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
cat >&2 <<'USAGE'
|
|
mas_syn2mas_migration - Migra usuarios Synapse a Matrix Authentication Service (MAS)
|
|
|
|
Usage:
|
|
mas_syn2mas_migration \
|
|
--ssh-host <host> \
|
|
--mas-container <name> \
|
|
--synapse-config-path <path-on-host> \
|
|
--log-dir <local-path> \
|
|
[--max-conflicts N] \
|
|
[--apply]
|
|
|
|
Opciones:
|
|
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
|
|
--mas-container Nombre del container MAS (ej. element_matrix_chat-mas-1)
|
|
--synapse-config-path Ruta en el VPS al homeserver.yaml
|
|
(ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml)
|
|
--log-dir Directorio local donde archivar logs dry-run y apply
|
|
--max-conflicts N Tope de conflictos en dry-run antes de abortar (default 0)
|
|
--apply Ejecutar migracion real. Sin esta flag: solo dry-run.
|
|
|
|
Comportamiento:
|
|
1. Siempre ejecuta dry-run primero y archiva el log.
|
|
2. Si conflicts > max-conflicts -> status=aborted, exit 2.
|
|
3. Sin --apply -> status=ok (dry-run completado), exit 0.
|
|
4. Con --apply -> ejecuta migracion real, archiva log, verifica postcondicion.
|
|
|
|
Output JSON: {"status":"ok|aborted|error","dry_run_log":"path","apply_log":"path|null","conflicts":N,"users_migrated":N,"duration_s":N}
|
|
USAGE
|
|
echo '{"status":"help","dry_run_log":"","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}'
|
|
return 0
|
|
;;
|
|
*)
|
|
echo "mas_syn2mas_migration: argumento desconocido: $1" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ---- Validar argumentos obligatorios ----
|
|
local errors=()
|
|
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
|
|
[[ -z "$mas_container" ]] && errors+=("--mas-container es obligatorio")
|
|
[[ -z "$synapse_config_path" ]] && errors+=("--synapse-config-path es obligatorio")
|
|
[[ -z "$log_dir" ]] && errors+=("--log-dir es obligatorio")
|
|
|
|
if [[ ${#errors[@]} -gt 0 ]]; then
|
|
for err in "${errors[@]}"; do
|
|
echo "ERROR: $err" >&2
|
|
done
|
|
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
|
|
return 1
|
|
fi
|
|
|
|
# Validar que max_conflicts es un entero no negativo
|
|
if ! [[ "$max_conflicts" =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: --max-conflicts debe ser un entero >= 0, recibido: $max_conflicts" >&2
|
|
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
|
|
return 1
|
|
fi
|
|
|
|
# ---- Dependencias locales ----
|
|
if ! command -v jq &>/dev/null; then
|
|
echo "ERROR: jq no encontrado. Instalar: apt install jq / brew install jq" >&2
|
|
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
|
|
return 1
|
|
fi
|
|
|
|
# ---- Crear log-dir con permisos restringidos ----
|
|
mkdir -p "$log_dir"
|
|
chmod 0700 "$log_dir"
|
|
|
|
local ts
|
|
ts=$(date +%s)
|
|
|
|
local dry_run_log="${log_dir}/syn2mas_dryrun_${ts}.log"
|
|
local apply_log_path="null"
|
|
local apply_log_file="${log_dir}/syn2mas_apply_${ts}.log"
|
|
|
|
# La ruta del homeserver.yaml dentro del container MAS se pasa como --synapse-config
|
|
# MAS monta el directorio del synapse bajo /data/ por convencion, pero la ruta real
|
|
# puede variar — usamos la ruta tal como existe en el host (montada via volume).
|
|
# El comando real esperado: docker exec <container> mas-cli syn2mas --synapse-config <path>
|
|
# donde <path> es la ruta tal como el container la ve (via volume mount).
|
|
# Asumimos que el VPS tiene el config accesible en la misma ruta dentro del container.
|
|
local container_config="/data/homeserver.yaml"
|
|
|
|
echo "mas_syn2mas_migration: ssh-host=${ssh_host} container=${mas_container} max-conflicts=${max_conflicts} apply=${do_apply}" >&2
|
|
|
|
# =========================================================================
|
|
# PASO 1: DRY-RUN obligatorio
|
|
# =========================================================================
|
|
echo "mas_syn2mas_migration: ejecutando dry-run..." >&2
|
|
|
|
local dry_exit=0
|
|
# Capturar stdout+stderr del dry-run en el log y tambien en variable para parsing
|
|
local dry_output
|
|
dry_output=$(ssh "$ssh_host" \
|
|
"docker exec '${mas_container}' mas-cli syn2mas \
|
|
--synapse-config '${container_config}' \
|
|
--dry-run" \
|
|
2>&1) || dry_exit=$?
|
|
|
|
# Archivar log con timestamp + header informativo
|
|
{
|
|
echo "# mas_syn2mas_migration dry-run"
|
|
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
|
|
echo "# synapse-config-path=${synapse_config_path}"
|
|
echo "# exit=${dry_exit}"
|
|
echo "# ---"
|
|
printf '%s\n' "$dry_output"
|
|
} > "$dry_run_log"
|
|
chmod 0600 "$dry_run_log"
|
|
|
|
echo "mas_syn2mas_migration: dry-run exit=${dry_exit}, log=${dry_run_log}" >&2
|
|
|
|
if [[ $dry_exit -ne 0 ]]; then
|
|
# Si el comando SSH falla completamente (no es fallo de syn2mas sino de conectividad)
|
|
echo "mas_syn2mas_migration: ERROR — dry-run falló con exit ${dry_exit}" >&2
|
|
local escaped_out
|
|
escaped_out=$(printf '%s' "${dry_output}" | jq -Rs '.')
|
|
local dry_run_log_json
|
|
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
|
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
|
|
return 1
|
|
fi
|
|
|
|
# =========================================================================
|
|
# PASO 2: Parsear conflicts del dry-run
|
|
# =========================================================================
|
|
# Regex sobre lineas tipo:
|
|
# "Conflict:", "Skipping:", "Error processing user", "conflict"
|
|
# También contamos líneas que indiquen usuarios problemáticos.
|
|
local conflicts=0
|
|
local conflict_lines
|
|
conflict_lines=$(printf '%s\n' "$dry_output" | \
|
|
grep -ciE '(conflict|skipping|error processing user|cannot migrate|already exists)' 2>/dev/null || true)
|
|
|
|
# grep -c devuelve string; convertir a int defensivamente
|
|
if [[ "$conflict_lines" =~ ^[0-9]+$ ]]; then
|
|
conflicts=$conflict_lines
|
|
else
|
|
# Parser falló de forma inesperada — abortar defensivamente
|
|
echo "mas_syn2mas_migration: ERROR — no se pudo parsear el conteo de conflicts del dry-run (parser defensivo)" >&2
|
|
local dry_run_log_json
|
|
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
|
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
|
|
return 1
|
|
fi
|
|
|
|
echo "mas_syn2mas_migration: conflicts detectados en dry-run: ${conflicts} (max permitido: ${max_conflicts})" >&2
|
|
|
|
# =========================================================================
|
|
# PASO 3: Verificar threshold de conflicts
|
|
# =========================================================================
|
|
if [[ $conflicts -gt $max_conflicts ]]; then
|
|
echo "mas_syn2mas_migration: ABORTADO — conflicts (${conflicts}) > max-conflicts (${max_conflicts})" >&2
|
|
echo "Revisar: ${dry_run_log}" >&2
|
|
local dry_run_log_json
|
|
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
|
echo "{\"status\":\"aborted\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
|
|
return 2
|
|
fi
|
|
|
|
# =========================================================================
|
|
# PASO 4: Si no --apply, terminar aqui con status=ok (dry-run completado)
|
|
# =========================================================================
|
|
if [[ "$do_apply" == "false" ]]; then
|
|
echo "mas_syn2mas_migration: dry-run completado (${conflicts} conflicts). Revisar log y re-ejecutar con --apply." >&2
|
|
local dry_run_log_json
|
|
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
|
echo "{\"status\":\"ok\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
|
|
return 0
|
|
fi
|
|
|
|
# =========================================================================
|
|
# PASO 5: Migracion REAL (--apply)
|
|
# =========================================================================
|
|
echo "mas_syn2mas_migration: ejecutando migracion REAL..." >&2
|
|
local apply_start
|
|
apply_start=$(date +%s)
|
|
|
|
local apply_exit=0
|
|
local apply_output
|
|
apply_output=$(ssh "$ssh_host" \
|
|
"docker exec '${mas_container}' mas-cli syn2mas \
|
|
--synapse-config '${container_config}'" \
|
|
2>&1) || apply_exit=$?
|
|
|
|
local apply_end
|
|
apply_end=$(date +%s)
|
|
local duration_s=$(( apply_end - apply_start ))
|
|
|
|
# Archivar log de apply
|
|
{
|
|
echo "# mas_syn2mas_migration apply"
|
|
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
|
|
echo "# synapse-config-path=${synapse_config_path}"
|
|
echo "# exit=${apply_exit} duration_s=${duration_s}"
|
|
echo "# ---"
|
|
printf '%s\n' "$apply_output"
|
|
} > "$apply_log_file"
|
|
chmod 0600 "$apply_log_file"
|
|
|
|
apply_log_path="$apply_log_file"
|
|
echo "mas_syn2mas_migration: apply exit=${apply_exit}, duration=${duration_s}s, log=${apply_log_file}" >&2
|
|
|
|
if [[ $apply_exit -ne 0 ]]; then
|
|
echo "mas_syn2mas_migration: ERROR — migracion real falló con exit ${apply_exit}" >&2
|
|
local dry_run_log_json apply_log_json
|
|
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
|
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
|
|
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":${duration_s}}"
|
|
return 1
|
|
fi
|
|
|
|
# =========================================================================
|
|
# PASO 6: Postcondicion — comparar usuarios en MAS vs Synapse
|
|
# =========================================================================
|
|
echo "mas_syn2mas_migration: verificando postcondicion (usuarios MAS vs Synapse)..." >&2
|
|
|
|
local mas_user_count=0
|
|
local synapse_user_count=0
|
|
local users_migrated=0
|
|
local post_status="ok"
|
|
|
|
# Contar usuarios en MAS via mas-cli admin user list
|
|
local mas_count_raw
|
|
mas_count_raw=$(ssh "$ssh_host" \
|
|
"docker exec '${mas_container}' mas-cli manage list-users --json 2>/dev/null | jq length" \
|
|
2>/dev/null || echo "0")
|
|
|
|
if [[ "$mas_count_raw" =~ ^[0-9]+$ ]]; then
|
|
mas_user_count=$mas_count_raw
|
|
else
|
|
echo "mas_syn2mas_migration: ADVERTENCIA — no se pudo obtener conteo de usuarios MAS (output: ${mas_count_raw})" >&2
|
|
post_status="ok" # No abortar, solo advertir
|
|
fi
|
|
|
|
# Contar usuarios locales en Synapse via psql (excluyendo bots/AS)
|
|
# Intentamos obtener el count; si falla, continuamos sin abortar
|
|
local synapse_count_raw
|
|
synapse_count_raw=$(ssh "$ssh_host" \
|
|
"docker exec '${mas_container}' mas-cli syn2mas --synapse-config '${container_config}' --dry-run 2>&1 | grep -oE 'Found [0-9]+ users' | grep -oE '[0-9]+' | head -1" \
|
|
2>/dev/null || echo "0")
|
|
|
|
if [[ "$synapse_count_raw" =~ ^[0-9]+$ ]]; then
|
|
synapse_user_count=$synapse_count_raw
|
|
fi
|
|
|
|
users_migrated=$mas_user_count
|
|
|
|
# Si tenemos ambos counts y difieren significativamente, marcar como warning en log
|
|
if [[ $synapse_user_count -gt 0 && $mas_user_count -eq 0 ]]; then
|
|
echo "mas_syn2mas_migration: ADVERTENCIA — MAS reporta 0 usuarios pero Synapse tenia ${synapse_user_count}" >&2
|
|
post_status="error"
|
|
fi
|
|
|
|
echo "mas_syn2mas_migration: postcondicion: mas_users=${mas_user_count} synapse_users=${synapse_user_count} status=${post_status}" >&2
|
|
|
|
# =========================================================================
|
|
# PASO 7: Emitir JSON final
|
|
# =========================================================================
|
|
local dry_run_log_json apply_log_json
|
|
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
|
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
|
|
|
|
echo "{\"status\":\"${post_status}\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":${users_migrated},\"duration_s\":${duration_s}}"
|
|
return 0
|
|
}
|
|
|
|
# Ejecutar si se llama directamente (no sourced)
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
mas_syn2mas_migration "$@"
|
|
fi
|