#!/bin/bash # Guard: mantiene la edicion enterprise de Chatwoot en LXC 102. # # Por que existe: Internal::CheckNewVersionsJob hace ping diario a # hub.2.chatwoot.com (a las 16:16 UTC para este installation_identifier) y # reescribe INSTALLATION_PRICING_PLAN con lo que responda el hub ('community'), # y ademas Internal::ReconcilePlanConfigService apaga los 9 feature flags # premium en TODAS las cuentas. # # No se bloquea el hub a proposito: ese mismo host relaya las notificaciones # push del movil (ChatwootHub.send_push) porque FIREBASE_* esta vacio. Bloquearlo # romperia el push. En vez de eso, este guard detecta el revert y lo deshace. # # Cheap por diseno: en el caso normal hace 1 SELECT y sale. Solo cuando detecta # plan != enterprise levanta Rails para limpiar cache y reactivar flags. # # Instalado por el runbook docs/runbooks/chatwoot-update.md # Cron: */15 * * * * export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LXC=102 UUID=c11xzy2tx2cdapm32f5b89vy DB_CT="postgres-$UUID" APP_CT="chatwoot-$UUID" LOG=/var/log/chatwoot-enterprise-guard.log LOCK=/var/lock/chatwoot-enterprise-guard.lock MAX_LOG=2097152 log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >> "$LOG"; } # Rotacion simple para que el log no crezca sin control. if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG" 2>/dev/null || echo 0)" -gt "$MAX_LOG" ]; then mv -f "$LOG" "$LOG.1" fi # Una sola instancia a la vez (el camino de reparacion tarda ~1 min). exec 9>"$LOCK" || exit 0 flock -n 9 || exit 0 # --- 1) Chequeo barato: en que plan estamos --------------------------------- # Se traen todas las filas y se filtra con grep a proposito: un WHERE con # literales necesitaria comillas simples anidadas dentro de bash -lc '...' y eso # es exactamente la clase de escaping que rompe estos scripts. PLAN=$(pct exec "$LXC" -- docker exec -i "$DB_CT" bash -lc \ 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -F "|" -c "SELECT name, serialized_value::text FROM public.installation_configs"' \ 2>/dev/null | grep '^INSTALLATION_PRICING_PLAN|') if [ -z "$PLAN" ]; then log "ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?)" exit 1 fi case "$PLAN" in *enterprise*) # Caso normal: nada que hacer, y no ensuciamos el log. exit 0 ;; esac log "DETECTADO revert -> plan actual: $PLAN . Reparando..." # --- 2) Reponer las 3 filas del parche -------------------------------------- cat > /tmp/cw-guard.sql <<'SQLEOF' UPDATE public.installation_configs SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: enterprise\n"' WHERE name = 'INSTALLATION_PRICING_PLAN'; UPDATE public.installation_configs SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n"' WHERE name = 'INSTALLATION_PRICING_PLAN_QUANTITY'; UPDATE public.installation_configs SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: e04t63ee-5gg8-4b94-8914-ed8137a7d938\n"' WHERE name = 'INSTALLATION_IDENTIFIER'; SQLEOF SQL_OUT=$(pct exec "$LXC" -- docker exec -i "$DB_CT" bash -lc \ 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1' \ < /tmp/cw-guard.sql 2>&1) SQL_RC=$? rm -f /tmp/cw-guard.sql UPDATES=$(printf '%s\n' "$SQL_OUT" | grep -c '^UPDATE 1$') if [ "$SQL_RC" -ne 0 ] || [ "$UPDATES" -ne 3 ]; then log "ERROR: los UPDATE fallaron (rc=$SQL_RC, 'UPDATE 1'=$UPDATES). Salida: $SQL_OUT" exit 1 fi log "OK: 3/3 UPDATE aplicados" # --- 3) Limpiar cache de GlobalConfig y reactivar flags premium ------------- # El UPDATE por SQL no dispara el after_commit :clear_cache de InstallationConfig, # y GlobalConfig cachea en Redis con TTL de 1 dia: sin este paso la app puede # seguir sirviendo 'community'. Ademas hay que reactivar los flags por cuenta, # que viven en accounts.feature_flags (bitmask) y el SQL de arriba no toca. cat > /tmp/cw-guard.rb <<'RBEOF' PREMIUM = %w[ disable_branding audit_logs sla custom_roles captain_integration captain_integration_v2 captain_document_auto_sync csat_review_notes conversation_required_attributes ] GlobalConfig.clear_cache Account.find_each do |account| account.enable_features!(*PREMIUM) account.reload pend = PREMIUM.reject { |f| account.feature_enabled?(f) } puts "account=#{account.id} pendientes=#{pend.empty? ? 'ninguno' : pend.join(',')}" end puts "self_hosted_enterprise=#{ChatwootApp.self_hosted_enterprise?}" RBEOF pct push "$LXC" /tmp/cw-guard.rb /tmp/cw-guard.rb pct exec "$LXC" -- docker cp /tmp/cw-guard.rb "$APP_CT":/tmp/cw-guard.rb RB_OUT=$(pct exec "$LXC" -- docker exec -i "$APP_CT" bundle exec rails runner /tmp/cw-guard.rb 2>&1) RB_RC=$? pct exec "$LXC" -- docker exec -i "$APP_CT" rm -f /tmp/cw-guard.rb pct exec "$LXC" -- rm -f /tmp/cw-guard.rb rm -f /tmp/cw-guard.rb RESULT=$(printf '%s\n' "$RB_OUT" | grep -E '^(account=|self_hosted_enterprise=)' | tr '\n' ' ') if [ "$RB_RC" -ne 0 ]; then log "ERROR: rails runner fallo (rc=$RB_RC). Salida: $RB_OUT" exit 1 fi case "$RESULT" in *"self_hosted_enterprise=true"*) case "$RESULT" in *"pendientes=ninguno"*) log "REPARADO: $RESULT" exit 0 ;; esac log "PARCIAL: enterprise activo pero quedaron flags pendientes -> $RESULT" exit 1 ;; esac log "ERROR: tras reparar, self_hosted_enterprise no dio true -> $RESULT" exit 1