#!/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 --mas-container \ # --synapse-config-path --log-dir \ # [--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 \ --mas-container \ --synapse-config-path \ --log-dir \ [--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 mas-cli syn2mas --synapse-config # donde 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