#!/bin/bash # ============================================================================ # MEGA DEPLOY v1.0.0 # Script de deploy guiado para stacks Docker Swarm # Hospedado em: setup.omniforge.com.br (Cloudflare R2) # Uso: bash <(curl -sSL setup.omniforge.com.br) # ============================================================================ VERSION="1.0.0" GITHUB_RAW="https://raw.githubusercontent.com/jessefreitas/setup_omniforge/main/stacks" CREDENTIALS_FILE="/root/mega-deploy-info.txt" TEMP_DIR="/tmp" # ============================================================================ # DADOS DE CONTATO E SUPORTE (editar aqui) # ============================================================================ OMNIFORGE_SITE="https://omnichat.omniforge.com.br" OMNIFORGE_DOCS="https://omnichat.omniforge.com.br/setup" OMNIFORGE_SUPORTE="+55 53 99903-4520" OMNIFORGE_WHATSAPP="https://wa.me/5553999034520?text=Preciso+de+ajuda+com+a+instalacao" OMNIFORGE_COMUNIDADE="https://clubmega.circle.so/join?invitation_token=ba73c646aff2004422cc5830bcb7eb4aa44502e9-ab7a729e-98e3-4097-b505-455dd5081eff" OMNIFORGE_COMUNIDADE_NOME="Comunidade Circle Mega" # Quando executado via "bash <(curl ...)", o stdin fica preso no pipe do curl. # ============================================================================ # CORES E FORMATACAO # ============================================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' # ============================================================================ # VARIAVEIS GLOBAIS DE ESTADO # ============================================================================ WIZARD_MODE=false TRAEFIK_OK=false PORTAINER_OK=false POSTGRES_OK=false REDIS_OK=false MEGA_OK=false WAHA_OK=false FZAP_OK=false EVOLUTION_OK=false DATABASUS_OK=false N8N_OK=false # Variaveis compartilhadas entre funcoes REDE_INTERNA="" PG_HOST="" PG_PORT="" PG_USER="" PG_PASS="" PG_DATABASES="" PGBOUNCER_PORT="" REDIS_HOST="" REDIS_PORT="" # ============================================================================ # BANNER # ============================================================================ show_banner() { clear echo -e "${MAGENTA}" cat << 'BANNER' ╔═══════════════════════════════════════════════════════════════════╗ ║ ║ ║ ███╗ ███╗███████╗ ██████╗ █████╗ ║ ║ ████╗ ████║██╔════╝██╔════╝ ██╔══██╗ ║ ║ ██╔████╔██║█████╗ ██║ ███╗███████║ ║ ║ ██║╚██╔╝██║██╔══╝ ██║ ██║██╔══██║ ║ ║ ██║ ╚═╝ ██║███████╗╚██████╔╝██║ ██║ ║ ║ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ DEPLOY v1.0.0 ║ ║ ║ ╚═══════════════════════════════════════════════════════════════════╝ BANNER echo -e "${RESET}" echo -e "${DIM} Deploy automatizado de stacks Docker Swarm${RESET}" echo -e "${DIM} https://omnichat.omniforge.com.br/setup${RESET}" echo "" } # ============================================================================ # TELA DE BOAS-VINDAS (apresentacao + comunidade) # ============================================================================ show_welcome() { clear echo -e "${MAGENTA}" cat << 'WELCOME' ╔═══════════════════════════════════════════════════════════════════╗ ║ ║ ║ ███╗ ███╗███████╗ ██████╗ █████╗ ║ ║ ████╗ ████║██╔════╝██╔════╝ ██╔══██╗ ║ ║ ██╔████╔██║█████╗ ██║ ███╗███████║ ║ ║ ██║╚██╔╝██║██╔══╝ ██║ ██║██╔══██║ ║ ║ ██║ ╚═╝ ██║███████╗╚██████╔╝██║ ██║ ║ ║ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ DEPLOY v1.0.0 ║ ║ ║ ╚═══════════════════════════════════════════════════════════════════╝ WELCOME echo -e "${RESET}" echo -e "${BOLD}${WHITE} Bem-vindo ao Mega Deploy!${RESET}" echo "" echo -e " Este instalador e um projeto ${GREEN}open source${RESET} mantido pela" echo -e " ${BOLD}${CYAN}OmniForge${RESET} para a comunidade Mega." echo "" echo -e " Nosso objetivo e simplificar a instalacao e configuracao das" echo -e " stacks Docker Swarm que compoem o ecossistema Mega," echo -e " automatizando o que antes levava horas de trabalho manual." echo "" echo -e " ${GREEN}Obrigado por fazer parte da comunidade!${RESET}" echo -e " Cada usuario que utiliza e reporta melhorias ajuda o projeto" echo -e " a crescer e ficar cada vez mais robusto para todos." echo "" separator echo "" echo -e " ${BOLD}${WHITE}CONTATO E SUPORTE${RESET}" echo "" echo -e " ${CYAN}Site:${RESET} ${OMNIFORGE_SITE}" echo -e " ${CYAN}Pagina do projeto:${RESET}${OMNIFORGE_DOCS}" echo -e " ${CYAN}WhatsApp:${RESET} ${OMNIFORGE_SUPORTE}" echo -e " ${CYAN}Comunidade:${RESET} ${OMNIFORGE_COMUNIDADE_NOME}" echo -e " ${DIM}${OMNIFORGE_COMUNIDADE}${RESET}" echo "" separator echo "" echo -e " ${BOLD}${YELLOW}CONSULTORIA E ASSESSORIA${RESET}" echo "" echo -e " Servicos de consultoria, assessoria em tecnologia, instalacao," echo -e " manutencao e onboarding do Mega podem ser contratados diretamente" echo -e " com a equipe OmniForge:" echo "" echo -e " ${GREEN}WhatsApp:${RESET} ${OMNIFORGE_SUPORTE}" echo -e " ${GREEN}Site:${RESET} https://omnichat.omniforge.com.br/consultoria" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para continuar...${RESET}" read } # ============================================================================ # CHECKLIST PRE-INSTALACAO (o que o usuario precisa ter em maos) # ============================================================================ show_checklist() { clear show_banner echo -e "${BOLD}${YELLOW} ANTES DE COMECAR — CHECKLIST${RESET}" echo "" echo -e " Tenha os itens abaixo em maos antes de prosseguir." echo -e " ${DIM}Itens marcados como (opcional) podem ser configurados depois.${RESET}" echo "" separator echo "" echo -e " ${BOLD}${WHITE}1. DOMINIOS${RESET} ${RED}(obrigatorio)${RESET}" echo -e " Todos os dominios abaixo devem estar com DNS tipo A" echo -e " apontando para o IP desta VPS ${BOLD}ANTES${RESET} de iniciar." echo "" echo -e " ${CYAN}*${RESET} portainer.seudominio.com" echo -e " ${CYAN}*${RESET} app.seudominio.com ${DIM}(Mega)${RESET}" echo -e " ${CYAN}*${RESET} waha.seudominio.com ${DIM}(ou fzap/evolution)${RESET}" echo -e " ${CYAN}*${RESET} backup.seudominio.com ${DIM}(Databasus)${RESET}" echo -e " ${CYAN}*${RESET} n8n.seudominio.com ${DIM}(N8N)${RESET}" echo "" echo -e " ${BOLD}${WHITE}2. E-MAIL PARA SSL${RESET} ${RED}(obrigatorio)${RESET}" echo -e " Um e-mail valido para registro dos certificados Let's Encrypt." echo "" echo -e " ${BOLD}${WHITE}3. CREDENCIAIS SMTP${RESET} ${RED}(obrigatorio para o Mega)${RESET}" echo -e " O Mega precisa de SMTP para enviar notificacoes." echo "" echo -e " ${CYAN}*${RESET} Host SMTP ${DIM}(ex: smtp.gmail.com)${RESET}" echo -e " ${CYAN}*${RESET} Porta ${DIM}(587 ou 465)${RESET}" echo -e " ${CYAN}*${RESET} Usuario e senha" echo -e " ${CYAN}*${RESET} E-mail remetente" echo "" echo -e " ${BOLD}${WHITE}4. STORAGE S3${RESET} ${YELLOW}(opcional)${RESET}" echo -e " Se quiser armazenar arquivos em cloud (AWS S3, Cloudflare R2," echo -e " DigitalOcean Spaces). Se nao configurar, usa storage local." echo "" echo -e " ${CYAN}*${RESET} Bucket name" echo -e " ${CYAN}*${RESET} Access Key ID e Secret Access Key" echo -e " ${CYAN}*${RESET} Region / Endpoint URL" echo "" echo -e " ${BOLD}${WHITE}5. LICENCE KEYS${RESET} ${RED}(obrigatorio se usar WAHA ou FZAP)${RESET}" echo -e " As licencas sao ${BOLD}obrigatorias${RESET} para usar WAHA e FZAP." echo -e " Solicite ao seu desenvolvedor ou fornecedor antes de instalar." echo "" echo -e " ${CYAN}*${RESET} WAHA Licence Key" echo -e " ${CYAN}*${RESET} FZAP Licence Key" echo "" separator echo "" echo -e " ${GREEN}Senhas do PostgreSQL, Redis, Portainer, API Keys e tokens${RESET}" echo -e " ${GREEN}sao gerados automaticamente — voce NAO precisa preparar nada.${RESET}" echo "" separator echo "" echo -ne " Tudo pronto? ${GREEN}[S]${RESET}im / ${RED}[n]${RESET}ao (sair): " local resposta read resposta resposta="${resposta:-S}" case "$resposta" in [nN]) echo "" msg_info "Sem problemas! Prepare os itens acima e execute novamente." echo -e " ${DIM}bash <(curl -sSL https://setup.omniforge.com.br)${RESET}" echo "" exit 0 ;; esac } # ============================================================================ # FUNCOES UTILITARIAS # ============================================================================ separator() { echo -e "${DIM}────────────────────────────────────────────────────────────${RESET}" } section_header() { echo "" separator echo -e "${BOLD}${WHITE} $1${RESET}" separator echo "" } msg_info() { echo -e "${CYAN}[INFO]${RESET} $1" } msg_ok() { echo -e "${GREEN}[ OK ]${RESET} $1" } msg_warn() { echo -e "${YELLOW}[WARN]${RESET} $1" } msg_fail() { echo -e "${RED}[FAIL]${RESET} $1" } step_ok() { local step="$1" local total="$2" local message="$3" echo -e " ${step}/${total} - ${GREEN}[ OK ]${RESET} - ${message}" } step_fail() { local step="$1" local total="$2" local message="$3" echo -e " ${step}/${total} - ${RED}[ FAIL ]${RESET} - ${message}" } step_skip() { local step="$1" local total="$2" local message="$3" echo -e " ${step}/${total} - ${YELLOW}[ SKIP ]${RESET} - ${message}" } # ============================================================================ # GERADORES # ============================================================================ generate_password() { # 256 bits de entropia (32 bytes), base64url sem caracteres especiais openssl rand -base64 32 | tr -d '+/=\n' | head -c 43 } generate_secret_key() { openssl rand -hex 64 } generate_api_key() { openssl rand -hex 16 } # ============================================================================ # VALIDACAO DE DNS # ============================================================================ get_vps_ip() { local ip="" ip=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null) || \ ip=$(curl -4 -s --max-time 5 api.ipify.org 2>/dev/null) || \ ip=$(curl -4 -s --max-time 5 icanhazip.com 2>/dev/null) || \ ip=$(hostname -I 2>/dev/null | awk '{print $1}') echo "$ip" } validate_domain() { local domain="$1" local expected_ip="$2" local resolved="" if command -v dig &>/dev/null; then resolved=$(dig +short "$domain" A 2>/dev/null | grep -E '^[0-9]+\.' | tail -1) fi if [ -z "$resolved" ] && command -v host &>/dev/null; then resolved=$(host "$domain" 2>/dev/null | grep 'has address' | head -1 | awk '{print $NF}') fi if [ -z "$resolved" ]; then resolved=$(getent ahosts "$domain" 2>/dev/null | grep STREAM | head -1 | awk '{print $1}') fi if [ -z "$resolved" ]; then echo "NOT_RESOLVED" elif [ "$resolved" = "$expected_ip" ]; then echo "OK" else echo "WRONG_IP:$resolved" fi } # ============================================================================ # INPUT # ============================================================================ ask_input() { local prompt="$1" local default="$2" local var_name="$3" local is_secret="${4:-false}" # Se o valor ja esta pre-definido (wizard mode), pular a pergunta local current_val eval "current_val=\"\${$var_name}\"" if [ -n "$current_val" ]; then return 0 fi if [ -n "$default" ]; then echo -ne " ${CYAN}${prompt}${RESET} [${default}]: " else echo -ne " ${CYAN}${prompt}${RESET}: " fi if [ "$is_secret" = "true" ]; then read -s input_val echo else read input_val fi if [ -z "$input_val" ] && [ -n "$default" ]; then input_val="$default" fi eval "$var_name='$input_val'" } resolve_pg_credentials() { # 1. Ja disponivel na sessao atual if [ -n "$PG_PASS" ]; then return 0; fi # 2. Tentar carregar do arquivo de credenciais salvo if [ -f "$CREDENTIALS_FILE" ]; then local saved_pass saved_user saved_pass=$(grep -A10 '^\[PostgreSQL\]' "$CREDENTIALS_FILE" 2>/dev/null | grep 'Senha' | head -1 | sed 's/.*: //' | tr -d ' \r') saved_user=$(grep -A10 '^\[PostgreSQL\]' "$CREDENTIALS_FILE" 2>/dev/null | grep 'Usuario' | head -1 | sed 's/.*: //' | tr -d ' \r') if [ -n "$saved_pass" ]; then PG_PASS="$saved_pass" PG_USER="${saved_user:-postgres}" PG_HOST="postgres" PG_PORT="5432" msg_info "Credenciais PostgreSQL carregadas do arquivo salvo" return 0 fi fi # 3. Nao encontrou — instalar agora automaticamente msg_warn "PostgreSQL nao encontrado. Instalando automaticamente..." install_postgres if [ -z "$PG_PASS" ]; then return 1; fi return 0 } resolve_redis_credentials() { # 1. Ja disponivel na sessao atual if [ -n "$REDIS_PASS" ]; then return 0; fi # 2. Tentar carregar do arquivo de credenciais salvo if [ -f "$CREDENTIALS_FILE" ]; then local saved_pass saved_pass=$(grep -A10 '^\[Redis\]' "$CREDENTIALS_FILE" 2>/dev/null | grep 'Senha' | head -1 | sed 's/.*: //' | tr -d ' \r') if [ -n "$saved_pass" ]; then REDIS_PASS="$saved_pass" REDIS_HOST="redis" REDIS_PORT="6379" msg_info "Credenciais Redis carregadas do arquivo salvo" return 0 fi fi # 3. Nao encontrou — instalar agora automaticamente msg_warn "Redis nao encontrado. Instalando automaticamente..." install_redis if [ -z "$REDIS_PASS" ]; then return 1; fi return 0 } # Cria um database no PostgreSQL e habilita pgvector (idempotente) create_pg_database() { local db_name="$1" [ -z "$db_name" ] && return 0 local pg_container pg_container=$(docker ps -q -f name=postgres_postgres 2>/dev/null | head -1) if [ -z "$pg_container" ]; then msg_warn "Container PostgreSQL nao encontrado — database '${db_name}' sera criado quando subir" return 0 fi # Criar se nao existir docker exec "$pg_container" psql -U "$PG_USER" -tc \ "SELECT 1 FROM pg_database WHERE datname='${db_name}';" 2>/dev/null | grep -q 1 if [ $? -ne 0 ]; then docker exec "$pg_container" psql -U "$PG_USER" -c \ "CREATE DATABASE ${db_name};" 2>/dev/null \ && msg_ok "Database '${db_name}' criado" \ || msg_warn "Falha ao criar database '${db_name}'" else msg_info "Database '${db_name}' ja existe" fi # Habilitar pgvector docker exec "$pg_container" psql -U "$PG_USER" -d "$db_name" -c \ "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true } # Deploy de stack via API do Portainer (permite editar pelo UI) # Fallback automatico para docker stack deploy se Portainer nao disponivel deploy_via_portainer() { local stack_name="$1" local yaml_file="$2" # Ler credenciais salvas do Portainer local portainer_url portainer_pass portainer_url=$(grep -A5 '^\[Portainer\]' "$CREDENTIALS_FILE" 2>/dev/null | grep 'URL' | head -1 | sed 's/.*: //' | tr -d ' \r') portainer_pass=$(grep -A5 '^\[Portainer\]' "$CREDENTIALS_FILE" 2>/dev/null | grep 'Senha' | head -1 | sed 's/.*: //' | tr -d ' \r') if [ -z "$portainer_url" ] || [ -z "$portainer_pass" ]; then msg_info "Portainer ainda nao configurado — usando docker stack deploy" docker stack deploy --prune --resolve-image always -c "$yaml_file" "$stack_name" 2>&1 return $? fi msg_info "Implantando '${stack_name}' via API do Portainer..." local py_result py_result=$(PORTAINER_URL="$portainer_url" PORTAINER_PASS="$portainer_pass" \ STACK_NAME="$stack_name" YAML_FILE="$yaml_file" \ python3 << 'PYEOF' import os, json, ssl, sys import urllib.request, urllib.error url = os.environ["PORTAINER_URL"].rstrip("/") pw = os.environ["PORTAINER_PASS"] name = os.environ["STACK_NAME"] yfile = os.environ["YAML_FILE"] # Normalizar URL base do Portainer if not url.startswith("http"): url = "https://" + url api = url + "/api" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE def req(method, path, data=None, headers={}): h = {"Content-Type": "application/json"} h.update(headers) r = urllib.request.Request(api + path, data=data, headers=h, method=method) with urllib.request.urlopen(r, context=ctx, timeout=15) as resp: return json.load(resp) # 1. Login try: resp = req("POST", "/auth", json.dumps({"username": "admin", "password": pw}).encode()) jwt = resp["jwt"] except Exception as e: print(f"PORTAINER_LOGIN_FAIL: {e}", file=sys.stderr) sys.exit(1) auth = {"Authorization": f"Bearer {jwt}"} # 2. Endpoint endpoints = req("GET", "/endpoints", headers=auth) endpoint_id = endpoints[0]["Id"] # 3. Swarm ID swarm = req("GET", f"/endpoints/{endpoint_id}/docker/swarm", headers=auth) swarm_id = swarm["ID"] # 4. YAML with open(yfile) as f: yaml_content = f.read() # 5. Verificar stack existente stacks = req("GET", "/stacks", headers=auth) existing = next((s for s in stacks if s["Name"] == name), None) if existing: sid = existing["Id"] body = json.dumps({"stackFileContent": yaml_content, "pullImage": True, "prune": True}).encode() req("PUT", f"/stacks/{sid}?endpointId={endpoint_id}", data=body, headers=auth) print(f"[PORTAINER] Stack '{name}' atualizada (id={sid})") else: body = json.dumps({"name": name, "swarmID": swarm_id, "stackFileContent": yaml_content}).encode() result = req("POST", f"/stacks/create/swarm/string?endpointId={endpoint_id}", data=body, headers=auth) print(f"[PORTAINER] Stack '{name}' criada (id={result.get('Id','?')})") PYEOF ) local py_exit=$? if [ $py_exit -eq 0 ]; then echo "$py_result" else msg_warn "API do Portainer falhou — usando docker stack deploy como fallback" docker stack deploy --prune --resolve-image always -c "$yaml_file" "$stack_name" 2>&1 fi } ask_confirm() { local prompt="$1" local default="${2:-s}" local result if [ "$default" = "s" ]; then echo -ne " ${CYAN}${prompt}${RESET} [S/n]: " else echo -ne " ${CYAN}${prompt}${RESET} [s/N]: " fi read result result="${result:-$default}" result=$(echo "$result" | tr '[:upper:]' '[:lower:]') if [ "$result" = "s" ] || [ "$result" = "y" ]; then return 0 else return 1 fi } # ============================================================================ # WAIT SERVICE # ============================================================================ wait_service() { local service_name="$1" local max_wait="${2:-120}" local interval=5 local elapsed=0 echo -ne " ${DIM}Aguardando servico ${service_name}...${RESET}" while [ $elapsed -lt $max_wait ]; do local replicas replicas=$(docker service ls --format '{{.Name}} {{.Replicas}}' 2>/dev/null | grep "^${service_name} " | awk '{print $2}') if [ -n "$replicas" ]; then local current desired current=$(echo "$replicas" | cut -d'/' -f1) desired=$(echo "$replicas" | cut -d'/' -f2) if [ "$current" = "$desired" ] && [ "$current" != "0" ]; then echo -e "\r ${GREEN}Servico ${service_name} rodando (${replicas})${RESET} " return 0 fi fi sleep $interval elapsed=$((elapsed + interval)) echo -ne "\r ${DIM}Aguardando servico ${service_name}... (${elapsed}s/${max_wait}s)${RESET}" done echo -e "\r ${YELLOW}Timeout aguardando ${service_name} (${max_wait}s)${RESET} " return 1 } # ============================================================================ # CREDENCIAIS # ============================================================================ init_credentials_file() { local datahora datahora=$(date '+%d/%m/%Y %H:%M:%S') cat > "$CREDENTIALS_FILE" << EOF ══════════════════════════════════════════════════════════════ MEGA DEPLOY v${VERSION} — CREDENCIAIS E URLS ══════════════════════════════════════════════════════════════ Gerado em: ${datahora} Servidor: $(hostname -f 2>/dev/null || hostname) IP: $(curl -s -4 ifconfig.me 2>/dev/null || echo "N/A") EOF chmod 600 "$CREDENTIALS_FILE" } save_credential() { local section="$1" local key="$2" local value="$3" # Criar arquivo se nao existir if [ ! -f "$CREDENTIALS_FILE" ]; then init_credentials_file fi # Verificar se a secao ja existe if ! grep -q "^\[${section}\]" "$CREDENTIALS_FILE" 2>/dev/null; then echo "" >> "$CREDENTIALS_FILE" echo "[${section}]" >> "$CREDENTIALS_FILE" fi # Adicionar a credencial abaixo da secao sed -i "/^\[${section}\]/a\\ ${key} : ${value}" "$CREDENTIALS_FILE" 2>/dev/null || \ echo " ${key} : ${value}" >> "$CREDENTIALS_FILE" } save_credential_block() { local section="$1" shift if [ ! -f "$CREDENTIALS_FILE" ]; then init_credentials_file fi # Remover bloco anterior da mesma secao (se existir) para evitar duplicatas if grep -q "^\[${section}\]" "$CREDENTIALS_FILE" 2>/dev/null; then # Apaga da linha [section] ate a proxima linha em branco ou proximo [ sed -i "/^\[${section}\]/,/^$/{ /^\[${section}\]/d; /^$/d; /^ /d; }" "$CREDENTIALS_FILE" 2>/dev/null fi echo "" >> "$CREDENTIALS_FILE" echo "[${section}]" >> "$CREDENTIALS_FILE" while [ $# -gt 0 ]; do echo " $1" >> "$CREDENTIALS_FILE" shift done } finalize_credentials_file() { if [ -f "$CREDENTIALS_FILE" ]; then echo "" >> "$CREDENTIALS_FILE" echo "══════════════════════════════════════════════════════════════" >> "$CREDENTIALS_FILE" echo " MANTENHA ESTE ARQUIVO EM LOCAL SEGURO!" >> "$CREDENTIALS_FILE" echo "══════════════════════════════════════════════════════════════" >> "$CREDENTIALS_FILE" fi } show_credentials_summary() { if [ -f "$CREDENTIALS_FILE" ]; then echo "" separator echo -e "${BOLD}${WHITE} CREDENCIAIS DE ACESSO${RESET}" separator cat "$CREDENTIALS_FILE" separator echo -e " ${YELLOW}Arquivo salvo em: ${CREDENTIALS_FILE}${RESET}" separator fi } # ============================================================================ # DOWNLOAD YAML # ============================================================================ download_yaml() { local url="$1" local output="$2" local description="$3" msg_info "Baixando ${description}..." curl -sSL --connect-timeout 15 --max-time 60 -o "$output" "$url" 2>/dev/null if [ ! -f "$output" ]; then msg_fail "Falha ao baixar ${description}" return 1 fi local filesize filesize=$(wc -c < "$output" 2>/dev/null || echo "0") if [ "$filesize" -lt 100 ]; then msg_fail "${description} baixado mas arquivo muito pequeno (${filesize} bytes). Verifique a URL." rm -f "$output" return 1 fi msg_ok "${description} baixado com sucesso (${filesize} bytes)" return 0 } # ============================================================================ # CHECK REQUIREMENTS # ============================================================================ check_requirements() { section_header "VERIFICANDO REQUISITOS" local errors=0 local total=6 local step=0 # 1. Root step=$((step + 1)) if [ "$(id -u)" -eq 0 ]; then step_ok "$step" "$total" "Executando como root" else step_fail "$step" "$total" "Este script precisa ser executado como root" errors=$((errors + 1)) fi # 2. Docker step=$((step + 1)) if command -v docker &>/dev/null; then local docker_version docker_version=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',') step_ok "$step" "$total" "Docker instalado (${docker_version})" # Garantir compatibilidade API if [ ! -f /etc/systemd/system/docker.service.d/override.conf ]; then mkdir -p /etc/systemd/system/docker.service.d printf '[Service]\nEnvironment=DOCKER_MIN_API_VERSION=1.24\n' > /etc/systemd/system/docker.service.d/override.conf systemctl daemon-reload && systemctl restart docker > /dev/null 2>&1 sleep 5 fi else msg_warn "Docker nao encontrado. Instalando automaticamente..." curl -fsSL https://get.docker.com | bash > /dev/null 2>&1 if command -v docker &>/dev/null; then local docker_version docker_version=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',') step_ok "$step" "$total" "Docker instalado automaticamente (${docker_version})" # Adicionar compatibilidade Docker API para Traefik mkdir -p /etc/systemd/system/docker.service.d cat > /etc/systemd/system/docker.service.d/override.conf << 'DOCKEREOF' [Service] Environment=DOCKER_MIN_API_VERSION=1.24 DOCKEREOF systemctl daemon-reload && systemctl restart docker > /dev/null 2>&1 sleep 3 else step_fail "$step" "$total" "Falha ao instalar Docker. Verifique sua conexao e tente novamente." errors=$((errors + 1)) fi fi # 3. Docker Swarm step=$((step + 1)) local swarm_status swarm_status=$(docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null) if [ "$swarm_status" = "active" ]; then step_ok "$step" "$total" "Docker Swarm ativo" else msg_warn "Docker Swarm nao esta ativo. Inicializando..." docker swarm init 2>/dev/null || docker swarm init --advertise-addr $(hostname -I | awk '{print $1}') 2>/dev/null swarm_status=$(docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null) if [ "$swarm_status" = "active" ]; then step_ok "$step" "$total" "Docker Swarm inicializado com sucesso" else step_fail "$step" "$total" "Falha ao inicializar Docker Swarm" errors=$((errors + 1)) fi fi # 4. curl step=$((step + 1)) if command -v curl &>/dev/null; then step_ok "$step" "$total" "curl disponivel" else msg_warn "curl nao encontrado. Instalando..." apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1 if command -v curl &>/dev/null; then step_ok "$step" "$total" "curl instalado com sucesso" else step_fail "$step" "$total" "Falha ao instalar curl" errors=$((errors + 1)) fi fi # 5. jq step=$((step + 1)) if command -v jq &>/dev/null; then step_ok "$step" "$total" "jq disponivel" else msg_warn "jq nao encontrado. Instalando..." apt-get update -qq && apt-get install -y -qq jq > /dev/null 2>&1 if command -v jq &>/dev/null; then step_ok "$step" "$total" "jq instalado com sucesso" else step_fail "$step" "$total" "Falha ao instalar jq" errors=$((errors + 1)) fi fi # 6. openssl step=$((step + 1)) if command -v openssl &>/dev/null; then step_ok "$step" "$total" "openssl disponivel" else step_fail "$step" "$total" "openssl nao encontrado" errors=$((errors + 1)) fi echo "" if [ $errors -gt 0 ]; then msg_fail "Encontrados ${errors} problema(s). Corrija antes de continuar." exit 1 fi msg_ok "Todos os requisitos atendidos!" echo "" } # ============================================================================ # DETECT INSTALLED # ============================================================================ detect_installed() { local services_output containers_output services_output=$(docker service ls 2>/dev/null) containers_output=$(docker ps --format '{{.Names}}' 2>/dev/null) # Traefik: stack ou container standalone if echo "$services_output" | grep -q "traefik_traefik" || \ echo "$containers_output" | grep -qi "traefik"; then TRAEFIK_OK=true; else TRAEFIK_OK=false; fi # Portainer: stack, container ou porta 9000 respondendo if echo "$services_output" | grep -q "portainer_portainer" || \ echo "$containers_output" | grep -qi "portainer" || \ curl -sk --max-time 2 http://localhost:9000/api/status > /dev/null 2>&1; then PORTAINER_OK=true; else PORTAINER_OK=false; fi # PostgreSQL if echo "$services_output" | grep -q "postgres_postgres" || \ echo "$containers_output" | grep -qi "postgres"; then POSTGRES_OK=true; else POSTGRES_OK=false; fi # Redis if echo "$services_output" | grep -q "redis_redis" || \ echo "$containers_output" | grep -qi "^redis"; then REDIS_OK=true; else REDIS_OK=false; fi # Mega if echo "$services_output" | grep -qE "chatwoot_mega_app|mega_mega_app|mega_app" || \ echo "$containers_output" | grep -qi "chatwoot\|mega_app"; then MEGA_OK=true; else MEGA_OK=false; fi # WAHA if echo "$services_output" | grep -q "waha_waha" || \ echo "$containers_output" | grep -qi "waha"; then WAHA_OK=true; else WAHA_OK=false; fi # FZAP if echo "$services_output" | grep -q "fzap_fzap" || \ echo "$containers_output" | grep -qi "fzap"; then FZAP_OK=true; else FZAP_OK=false; fi # Evolution if echo "$services_output" | grep -qE "evolution_evolution" || \ echo "$containers_output" | grep -qi "evolution"; then EVOLUTION_OK=true; else EVOLUTION_OK=false; fi # Databasus if echo "$services_output" | grep -q "databasus_databasus" || \ echo "$containers_output" | grep -qi "databasus"; then DATABASUS_OK=true; else DATABASUS_OK=false; fi # N8N if echo "$services_output" | grep -q "n8n_n8n" || \ echo "$containers_output" | grep -qi "^n8n"; then N8N_OK=true; else N8N_OK=false; fi } show_status() { echo -e " ${BOLD}Status das Stacks:${RESET}" echo "" local status_icon if $TRAEFIK_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} Traefik $($TRAEFIK_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $PORTAINER_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} Portainer $($PORTAINER_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $POSTGRES_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} PostgreSQL $($POSTGRES_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $REDIS_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} Redis $($REDIS_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $MEGA_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} Mega$($MEGA_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $WAHA_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} WAHA $($WAHA_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $FZAP_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} FZAP $($FZAP_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $EVOLUTION_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} Evolution v2 $($EVOLUTION_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $DATABASUS_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} Databasus $($DATABASUS_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" if $N8N_OK; then status_icon="${GREEN}●${RESET}"; else status_icon="${RED}○${RESET}"; fi echo -e " ${status_icon} N8N $($N8N_OK && echo -e "${GREEN}Instalado${RESET}" || echo -e "${DIM}Nao instalado${RESET}")" echo "" } # ============================================================================ # INSTALL TRAEFIK + PORTAINER # ============================================================================ install_traefik_portainer() { detect_installed if $TRAEFIK_OK || $PORTAINER_OK; then echo "" msg_warn "Traefik ou Portainer ja esta instalado nesta VPS. Abortando para evitar conflito." $TRAEFIK_OK && echo -e " ${GREEN}●${RESET} Traefik — ${GREEN}detectado${RESET}" $PORTAINER_OK && echo -e " ${GREEN}●${RESET} Portainer — ${GREEN}detectado${RESET}" echo "" echo -e " Para reinstalar, remova as stacks manualmente:" echo -e " ${BOLD}docker stack rm traefik portainer${RESET}" echo "" read -p "Pressione ENTER para voltar ao menu..." return 0 fi section_header "INSTALANDO TRAEFIK + PORTAINER" local total=6 local step=0 # --- Coleta de dados --- echo -e " ${BOLD}Configure a infraestrutura base:${RESET}" echo "" ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" ask_input "E-mail para certificado SSL (Let's Encrypt)" "" "EMAIL_SSL" if [ -z "$EMAIL_SSL" ]; then msg_fail "E-mail SSL e obrigatorio!" return 1 fi ask_input "Dominio do Portainer (ex: portainer.seudominio.com)" "" "URL_PORTAINER" if [ -z "$URL_PORTAINER" ]; then msg_fail "Dominio do Portainer e obrigatorio!" return 1 fi local PORTAINER_PASS="${PORTAINER_PASS}" ask_input "Senha do Portainer (vazio = gerar automaticamente)" "" "PORTAINER_PASS" "true" if [ -z "$PORTAINER_PASS" ]; then PORTAINER_PASS=$(generate_password) msg_info "Senha gerada: ${PORTAINER_PASS}" fi echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Criar rede overlay --- step=$((step + 1)) docker network create --driver=overlay --attachable "$REDE_INTERNA" 2>/dev/null || true if docker network ls | grep -q "$REDE_INTERNA"; then step_ok "$step" "$total" "Rede overlay '${REDE_INTERNA}' criada/existente" else step_fail "$step" "$total" "Falha ao criar rede overlay" return 1 fi # --- Passo 2: Criar volumes --- step=$((step + 1)) docker volume create volume_swarm_shared 2>/dev/null || true docker volume create volume_swarm_certificates 2>/dev/null || true if docker volume ls | grep -q "volume_swarm_shared" && docker volume ls | grep -q "volume_swarm_certificates"; then step_ok "$step" "$total" "Volumes criados (volume_swarm_shared, volume_swarm_certificates)" else step_fail "$step" "$total" "Falha ao criar volumes" return 1 fi # --- Passo 3: Baixar e processar traefik.yaml --- step=$((step + 1)) local traefik_raw="${TEMP_DIR}/traefik_raw.yaml" local traefik_deploy="${TEMP_DIR}/traefik_deploy.yaml" if ! download_yaml "${GITHUB_RAW}/traefik.yaml" "$traefik_raw" "traefik.yaml"; then step_fail "$step" "$total" "Falha ao baixar traefik.yaml" return 1 fi sed \ -e 's|\$nome_rede_interna|'"${REDE_INTERNA}"'|g' \ -e 's|\${nome_rede_interna}|'"${REDE_INTERNA}"'|g' \ -e 's|\$email_ssl|'"${EMAIL_SSL}"'|g' \ -e 's|\${email_ssl}|'"${EMAIL_SSL}"'|g' \ -e 's|\$url_portainer|'"${URL_PORTAINER}"'|g' \ -e 's|\${url_portainer}|'"${URL_PORTAINER}"'|g' \ -e 's|nome_rede_interna|'"${REDE_INTERNA}"'|g' \ -e 's|email_ssl_aqui|'"${EMAIL_SSL}"'|g' \ -e 's|email_ssl|'"${EMAIL_SSL}"'|g' \ -e 's|seu_email@exemplo.com|'"${EMAIL_SSL}"'|g' \ -e 's|email@example.com|'"${EMAIL_SSL}"'|g' \ "$traefik_raw" > "$traefik_deploy" step_ok "$step" "$total" "traefik.yaml processado" # --- Passo 4: Deploy traefik --- step=$((step + 1)) docker stack deploy --prune --resolve-image always -c "$traefik_deploy" traefik 2>&1 if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack traefik implantada" else step_fail "$step" "$total" "Falha ao implantar stack traefik" return 1 fi # --- Passo 5: Baixar, processar e deploy portainer --- step=$((step + 1)) local portainer_raw="${TEMP_DIR}/portainer_raw.yaml" local portainer_deploy="${TEMP_DIR}/portainer_deploy.yaml" if ! download_yaml "${GITHUB_RAW}/portainer.yaml" "$portainer_raw" "portainer.yaml"; then step_fail "$step" "$total" "Falha ao baixar portainer.yaml" return 1 fi sed \ -e 's|\$nome_rede_interna|'"${REDE_INTERNA}"'|g' \ -e 's|\${nome_rede_interna}|'"${REDE_INTERNA}"'|g' \ -e 's|\$url_portainer|'"${URL_PORTAINER}"'|g' \ -e 's|\${url_portainer}|'"${URL_PORTAINER}"'|g' \ -e 's|nome_rede_interna|'"${REDE_INTERNA}"'|g' \ -e 's|portainer\.example\.com|'"${URL_PORTAINER}"'|g' \ -e 's|portainer\.seudominio\.com|'"${URL_PORTAINER}"'|g' \ "$portainer_raw" > "$portainer_deploy" docker stack deploy --prune --resolve-image always -c "$portainer_deploy" portainer 2>&1 if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack portainer implantada" else step_fail "$step" "$total" "Falha ao implantar stack portainer" return 1 fi # --- Passo 6: Inicializar admin do Portainer --- step=$((step + 1)) msg_info "Aguardando Portainer inicializar..." local portainer_ready=false for i in $(seq 1 12); do local status status=$(curl -k -s -o /dev/null -w "%{http_code}" "https://${URL_PORTAINER}/api/status" 2>/dev/null) if [ "$status" = "200" ]; then portainer_ready=true break fi sleep 5 done if $portainer_ready; then # Criar admin local admin_result admin_http admin_http=$(curl -k -s -o /dev/null -w "%{http_code}" -X POST "https://${URL_PORTAINER}/api/users/admin/init" \ -H "Content-Type: application/json" \ -d "{\"username\":\"admin\",\"password\":\"${PORTAINER_PASS}\"}" 2>/dev/null) admin_result=$(curl -k -s -X POST "https://${URL_PORTAINER}/api/users/admin/init" \ -H "Content-Type: application/json" \ -d "{\"username\":\"admin\",\"password\":\"${PORTAINER_PASS}\"}" 2>/dev/null) if [ "$admin_http" = "200" ] || echo "$admin_result" | grep -q "Username" 2>/dev/null; then step_ok "$step" "$total" "Admin do Portainer criado" elif [ "$admin_http" = "422" ] || echo "$admin_result" | grep -q "administrator user already exists" 2>/dev/null; then # Volume persistido de instalacao anterior — senha desconhecida, fazer reset automatico msg_warn "Admin ja existe (volume portainer_data persistido). Resetando senha..." local reset_result reset_result=$(docker run --rm -v portainer_data:/data portainer/helper-reset-password 2>&1) local new_pass new_pass=$(echo "$reset_result" | grep -oP '(?<=new password: ).*' | tr -d '\r\n') if [ -n "$new_pass" ]; then PORTAINER_PASS="$new_pass" step_ok "$step" "$total" "Senha do Portainer resetada automaticamente" else msg_warn "Reset automatico falhou. Para resetar manualmente:" msg_warn " docker service scale portainer_portainer=0" msg_warn " docker run --rm -v portainer_data:/data portainer/helper-reset-password" msg_warn " docker service scale portainer_portainer=1" PORTAINER_PASS="(ver reset manual acima)" step_skip "$step" "$total" "Reset manual necessario" fi else msg_warn "Resposta inesperada do Portainer (HTTP ${admin_http})" step_skip "$step" "$total" "Portainer acessivel mas admin nao confirmado" fi else msg_warn "Portainer demorou para responder. Configure o admin manualmente em https://${URL_PORTAINER}" step_skip "$step" "$total" "Inicializacao do admin adiada (Portainer ainda subindo)" fi # --- Cleanup --- rm -f "$traefik_raw" "$portainer_raw" # --- Salvar credenciais --- save_credential_block "Traefik" \ "Rede Interna : ${REDE_INTERNA}" \ "Email SSL : ${EMAIL_SSL}" save_credential_block "Portainer" \ "URL : https://${URL_PORTAINER}" \ "Login : admin" \ "Senha : ${PORTAINER_PASS}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} TRAEFIK + PORTAINER INSTALADOS COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_PORTAINER}${RESET}" echo -e " Login : ${BOLD}admin${RESET}" echo -e " Senha : ${BOLD}${PORTAINER_PASS}${RESET}" echo -e " Rede : ${BOLD}${REDE_INTERNA}${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" echo "" echo -e "${YELLOW}╔══════════════════════════════════════════════════════════╗${RESET}" echo -e "${YELLOW}║ ⚠ ACAO NECESSARIA — ACESSE O PORTAINER AGORA! ║${RESET}" echo -e "${YELLOW}╠══════════════════════════════════════════════════════════╣${RESET}" echo -e "${YELLOW}║ ║${RESET}" echo -e "${YELLOW}║ O Portainer expira em 5 minutos sem acesso! ║${RESET}" echo -e "${YELLOW}║ ║${RESET}" echo -e "${YELLOW}║ URL : https://${URL_PORTAINER}${RESET}" echo -e "${YELLOW}║ Login : admin ║${RESET}" echo -e "${YELLOW}║ Senha : ${PORTAINER_PASS}${RESET}" echo -e "${YELLOW}║ ║${RESET}" echo -e "${YELLOW}║ Apos logar, volte aqui para continuar a instalacao. ║${RESET}" echo -e "${YELLOW}║ ║${RESET}" echo -e "${YELLOW}║ Se expirou, rode: ║${RESET}" echo -e "${YELLOW}║ docker service update --force portainer_portainer ║${RESET}" echo -e "${YELLOW}╚══════════════════════════════════════════════════════════╝${RESET}" echo "" read -p " Pressione ENTER depois de logar no Portainer para continuar..." echo "" # Atualizar estado detect_installed } # ============================================================================ # INSTALL POSTGRES # ============================================================================ install_postgres() { detect_installed if $POSTGRES_OK; then msg_warn "PostgreSQL ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm postgres${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner if [ "${SKIP_PGBOUNCER:-false}" = "true" ]; then echo -e " ${BOLD}${CYAN}━━━ POSTGRESQL 16 ━━━${RESET}" echo "" echo -e " Banco de dados relacional robusto e open source." echo -e " Armazena todos os dados do Mega, Evolution e demais stacks." echo "" echo -e " ${DIM}• PostgreSQL 16 com extensao pgvector${RESET}" echo -e " ${DIM}• Configuracao automatica de bancos por servico${RESET}" echo -e " ${DIM}• Senha gerada automaticamente${RESET}" else echo -e " ${BOLD}${CYAN}━━━ POSTGRESQL 16 + PGBOUNCER ━━━${RESET}" echo "" echo -e " Banco de dados relacional com pool de conexoes." echo -e " Armazena todos os dados do Mega, Evolution e demais stacks." echo "" echo -e " ${DIM}• PostgreSQL 16 com extensao pgvector${RESET}" echo -e " ${DIM}• PgBouncer: pool de conexoes para melhor performance${RESET}" echo -e " ${DIM}• Configuracao automatica de bancos por servico${RESET}" echo -e " ${DIM}• Recomendado para producao com mais de 1 servico${RESET}" fi echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO POSTGRESQL 16 + PGVECTOR + PGBOUNCER" local total=5 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then # Tentar detectar rede existente local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi # --- Coleta de dados --- echo -e " ${BOLD}Configure o PostgreSQL:${RESET}" echo "" ask_input "Usuario PostgreSQL" "postgres" "PG_USER" if [ -z "$PG_PASS" ]; then PG_PASS=$(generate_password) fi msg_info "Senha PostgreSQL gerada com 256 bits de segurança" ask_input "Porta PostgreSQL" "5432" "PG_PORT" if ! $WIZARD_MODE; then ask_input "Databases extras a criar agora (opcional, vazio = pular)" "" "PG_DATABASES" fi ask_input "Porta PgBouncer" "6432" "PGBOUNCER_PORT" local PGBOUNCER_MAX_CONN="${PGBOUNCER_MAX_CONN}" ask_input "Max conexoes PgBouncer" "100" "PGBOUNCER_MAX_CONN" # Salvar host para uso posterior PG_HOST="postgres" echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Criar volume --- step=$((step + 1)) docker volume create postgres_data 2>/dev/null || true step_ok "$step" "$total" "Volume postgres_data criado/existente" # --- Passo 2: Deploy PostgreSQL + PgBouncer --- step=$((step + 1)) cat > "${TEMP_DIR}/postgres_deploy.yaml" << PGEOF version: "3.8" services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: ${PG_USER} POSTGRES_PASSWORD: ${PG_PASS} POSTGRES_DB: postgres ports: - "${PG_PORT}:5432" volumes: - postgres_data:/var/lib/postgresql/data networks: - ${REDE_INTERNA} deploy: mode: replicated replicas: 1 placement: constraints: [node.role == manager] resources: limits: memory: 2G reservations: memory: 512M pgbouncer: image: edoburu/pgbouncer:latest environment: DATABASE_URL: "postgres://${PG_USER}:${PG_PASS}@postgres:5432/postgres" POOL_MODE: transaction MAX_CLIENT_CONN: "${PGBOUNCER_MAX_CONN}" DEFAULT_POOL_SIZE: "20" MIN_POOL_SIZE: "5" LISTEN_PORT: "${PGBOUNCER_PORT}" ports: - "${PGBOUNCER_PORT}:${PGBOUNCER_PORT}" networks: - ${REDE_INTERNA} deploy: mode: replicated replicas: 1 placement: constraints: [node.role == manager] volumes: postgres_data: external: true networks: ${REDE_INTERNA}: external: true name: ${REDE_INTERNA} PGEOF deploy_via_portainer postgres "${TEMP_DIR}/postgres_deploy.yaml" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack postgres implantada" else step_fail "$step" "$total" "Falha ao implantar stack postgres" return 1 fi # --- Passo 3: Aguardar PostgreSQL inicializar --- step=$((step + 1)) msg_info "Aguardando PostgreSQL inicializar (pode levar ate 60s)..." sleep 15 local pg_ready=false for i in $(seq 1 12); do local pg_container pg_container=$(docker ps -q -f name=postgres_postgres 2>/dev/null | head -1) if [ -n "$pg_container" ]; then if docker exec "$pg_container" pg_isready -U "$PG_USER" 2>/dev/null | grep -q "accepting connections"; then pg_ready=true break fi fi sleep 5 done if $pg_ready; then step_ok "$step" "$total" "PostgreSQL aceitando conexoes" else msg_warn "PostgreSQL pode ainda estar inicializando. Databases serao criados na proxima tentativa." step_skip "$step" "$total" "Health check pendente" fi # --- Passo 4: Criar databases --- step=$((step + 1)) local pg_container pg_container=$(docker ps -q -f name=postgres_postgres 2>/dev/null | head -1) if [ -n "$pg_container" ]; then local IFS=',' local db_errors=0 for db_name in $PG_DATABASES; do db_name=$(echo "$db_name" | tr -d ' ') if [ -n "$db_name" ]; then docker exec "$pg_container" psql -U "$PG_USER" -tc \ "SELECT 1 FROM pg_database WHERE datname='${db_name}';" 2>/dev/null | grep -q 1 if [ $? -ne 0 ]; then docker exec "$pg_container" psql -U "$PG_USER" -c \ "CREATE DATABASE ${db_name};" 2>/dev/null if [ $? -eq 0 ]; then msg_ok "Database '${db_name}' criado" else msg_warn "Falha ao criar database '${db_name}'" db_errors=$((db_errors + 1)) fi else msg_info "Database '${db_name}' ja existe" fi fi done unset IFS if [ $db_errors -eq 0 ]; then step_ok "$step" "$total" "Databases criados com sucesso" else step_skip "$step" "$total" "Alguns databases tiveram problemas" fi # Habilitar extensao pgvector em todos os databases IFS=',' for db_name in $PG_DATABASES; do db_name=$(echo "$db_name" | tr -d ' ') if [ -n "$db_name" ]; then docker exec "$pg_container" psql -U "$PG_USER" -d "$db_name" -c \ "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true fi done unset IFS else step_skip "$step" "$total" "Container PostgreSQL nao encontrado (databases serao criados quando subir)" fi # --- Passo 5: Verificacao final --- step=$((step + 1)) wait_service "postgres_postgres" 60 wait_service "postgres_pgbouncer" 60 step_ok "$step" "$total" "Verificacao final concluida" # --- Cleanup --- rm -f "${TEMP_DIR}/postgres_deploy.yaml" # --- Salvar credenciais --- save_credential_block "PostgreSQL" \ "Host : postgres" \ "Porta : ${PG_PORT}" \ "PgBouncer : ${PGBOUNCER_PORT}" \ "Usuario : ${PG_USER}" \ "Senha : ${PG_PASS}" \ "Databases : ${PG_DATABASES}" \ "Max Conexoes : ${PGBOUNCER_MAX_CONN}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} POSTGRESQL + PGBOUNCER INSTALADOS COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " Host : ${BOLD}postgres:${PG_PORT}${RESET}" echo -e " PgBouncer : ${BOLD}postgres:${PGBOUNCER_PORT}${RESET}" echo -e " Usuario : ${BOLD}${PG_USER}${RESET}" echo -e " Senha : ${BOLD}${PG_PASS}${RESET}" echo -e " Databases : ${BOLD}${PG_DATABASES}${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL REDIS # ============================================================================ install_redis() { detect_installed if $REDIS_OK; then msg_warn "Redis ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm redis${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ REDIS 7.4 ━━━${RESET}" echo "" echo -e " Banco de dados em memoria para cache e filas." echo -e " Usado pelo Mega para sessoes, jobs e pub/sub." echo "" echo -e " ${DIM}• Ultra-rapido: opera inteiramente em RAM${RESET}" echo -e " ${DIM}• Fila de jobs do Sidekiq${RESET}" echo -e " ${DIM}• Cache de sessoes e dados temporarios${RESET}" echo -e " ${DIM}• Necessario antes de instalar o Mega${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO REDIS 7.4" local total=3 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi REDIS_HOST="redis" REDIS_PORT="6379" REDIS_PASS=$(generate_password) msg_info "Senha Redis gerada com 256 bits de segurança" echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Criar volume --- step=$((step + 1)) docker volume create redis_data 2>/dev/null || true step_ok "$step" "$total" "Volume redis_data criado/existente" # --- Passo 2: Deploy Redis --- step=$((step + 1)) cat > "${TEMP_DIR}/redis_deploy.yaml" << REDISEOF version: "3.8" services: redis: image: redis:7.4-alpine command: > redis-server --requirepass ${REDIS_PASS} --appendonly yes --maxmemory 1gb --maxmemory-policy allkeys-lru --save 60 1000 --save 300 100 --loglevel warning volumes: - redis_data:/data networks: - ${REDE_INTERNA} deploy: mode: replicated replicas: 1 placement: constraints: [node.role == manager] resources: limits: memory: 1200M reservations: memory: 256M volumes: redis_data: external: true networks: ${REDE_INTERNA}: external: true name: ${REDE_INTERNA} REDISEOF deploy_via_portainer redis "${TEMP_DIR}/redis_deploy.yaml" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack redis implantada" else step_fail "$step" "$total" "Falha ao implantar stack redis" return 1 fi # --- Passo 3: Health check --- step=$((step + 1)) wait_service "redis_redis" 60 local redis_container redis_container=$(docker ps -q -f name=redis_redis 2>/dev/null | head -1) if [ -n "$redis_container" ]; then local ping_result ping_result=$(docker exec "$redis_container" redis-cli ping 2>/dev/null) if [ "$ping_result" = "PONG" ]; then step_ok "$step" "$total" "Redis respondendo (PONG)" else step_skip "$step" "$total" "Redis implantado, health check pendente" fi else step_skip "$step" "$total" "Redis implantado, container ainda subindo" fi # --- Cleanup --- rm -f "${TEMP_DIR}/redis_deploy.yaml" # --- Salvar credenciais --- save_credential_block "Redis" \ "Host : redis" \ "Porta : 6379" \ "Senha : ${REDIS_PASS}" \ "URI : redis://:${REDIS_PASS}@redis:6379" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} REDIS INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " Host : ${BOLD}redis:6379${RESET}" echo -e " Senha : ${BOLD}${REDIS_PASS}${RESET}" echo -e " URI : ${BOLD}redis://:${REDIS_PASS}@redis:6379${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL MEGA # ============================================================================ install_mega() { detect_installed if $MEGA_OK; then msg_warn "Mega ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm mega${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ MEGA ━━━${RESET}" echo "" echo -e " Plataforma open source de atendimento ao cliente." echo -e " Centraliza conversas de ${BOLD}WhatsApp, e-mail, chat, Telegram${RESET} e mais" echo -e " em uma unica caixa de entrada compartilhada pelo time." echo "" echo -e " ${DIM}• Atribuicao de conversas entre agentes${RESET}" echo -e " ${DIM}• Respostas rapidas e automacoes nativas${RESET}" echo -e " ${DIM}• Relatorios e metricas de atendimento${RESET}" echo -e " ${DIM}• Integra com WAHA, Evolution e FZAP via este installer${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO MEGA" local total=5 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi # --- Coleta de dados --- echo -e " ${BOLD}Configure o Mega:${RESET}" echo "" echo -e " ${YELLOW}--- Dados Gerais ---${RESET}" ask_input "Dominio do Mega (ex: app.seudominio.com)" "" "URL_MEGA" if [ -z "$URL_MEGA" ]; then msg_fail "Dominio e obrigatorio!" return 1 fi ask_input "Nome da aplicacao" "MegaChat" "NOME_MEGA" local SECRET_KEY_MEGA SECRET_KEY_MEGA=$(generate_secret_key) msg_info "Secret key gerada automaticamente" echo "" echo -e " ${YELLOW}--- PostgreSQL ---${RESET}" local MEGA_PG_HOST MEGA_PG_PORT MEGA_PG_USER MEGA_PG_PASS MEGA_PG_DB resolve_pg_credentials if [ -z "$PG_PASS" ]; then msg_fail "Falha ao instalar PostgreSQL automaticamente. Verifique os logs." return 1 fi MEGA_PG_HOST="$PG_HOST" MEGA_PG_PORT="${PG_PORT:-5432}" MEGA_PG_USER="$PG_USER" MEGA_PG_PASS="$PG_PASS" msg_info "Usando PostgreSQL: ${MEGA_PG_HOST}:${MEGA_PG_PORT} (usuario: ${MEGA_PG_USER})" ask_input "Database do Mega" "chatwoot_db" "MEGA_PG_DB" create_pg_database "$MEGA_PG_DB" echo "" echo -e " ${YELLOW}--- SMTP ---${RESET}" local SMTP_HOST="${SMTP_HOST}" SMTP_PORT="${SMTP_PORT}" SMTP_USER="${SMTP_USER}" SMTP_PASS="${SMTP_PASS}" SMTP_EMAIL="${SMTP_EMAIL}" SMTP_DOMAIN="${SMTP_DOMAIN}" ask_input "Host SMTP (ex: smtp.gmail.com)" "" "SMTP_HOST" ask_input "Porta SMTP" "587" "SMTP_PORT" ask_input "Usuario SMTP" "" "SMTP_USER" ask_input "Senha SMTP" "" "SMTP_PASS" "true" ask_input "E-mail remetente (ex: noreply@seudominio.com)" "" "SMTP_EMAIL" ask_input "Dominio do e-mail (ex: seudominio.com)" "" "SMTP_DOMAIN" echo "" if ! $WIZARD_MODE; then echo -e " ${YELLOW}--- Integracoes (opcional) ---${RESET}" fi local MEGA_EVO_URL="${MEGA_EVO_URL}" MEGA_EVO_TOKEN="${MEGA_EVO_TOKEN}" MEGA_WAHA_URL="${MEGA_WAHA_URL}" MEGA_WAHA_TOKEN="${MEGA_WAHA_TOKEN}" ask_input "URL Evolution API (sem https://, vazio para pular)" "" "MEGA_EVO_URL" if [ -n "$MEGA_EVO_URL" ]; then ask_input "Token Admin Evolution" "" "MEGA_EVO_TOKEN" fi ask_input "URL WAHA (sem https://, vazio para pular)" "" "MEGA_WAHA_URL" if [ -n "$MEGA_WAHA_URL" ]; then ask_input "Token Admin WAHA" "" "MEGA_WAHA_TOKEN" fi # --- Storage de midia --- local MEGA_STORAGE_CHOICE="${MEGA_STORAGE_CHOICE}" local MEGA_STORAGE_TYPE="local" local MEGA_S3_BUCKET="${MEGA_S3_BUCKET}" MEGA_S3_ACCESS_KEY="${MEGA_S3_ACCESS_KEY}" MEGA_S3_SECRET_KEY="${MEGA_S3_SECRET_KEY}" MEGA_S3_REGION="${MEGA_S3_REGION}" MEGA_S3_ENDPOINT="${MEGA_S3_ENDPOINT}" if [ -z "$MEGA_STORAGE_CHOICE" ]; then echo "" echo -e " ${YELLOW}--- Storage de midia ---${RESET}" echo -e " ${WHITE}O Mega armazena arquivos de conversa (imagens, audios, documentos).${RESET}" echo -e " ${WHITE}Por padrao usa storage local — a pasta precisa ter permissao de escrita.${RESET}" echo "" echo -e " ${GREEN}1)${RESET} Local (padrao — pasta /app/storage dentro do container)" echo -e " ${GREEN}2)${RESET} S3 — Amazon AWS" echo -e " ${GREEN}3)${RESET} S3-compatible — Cloudflare R2, DigitalOcean Spaces, etc." echo "" echo -ne " ${CYAN}Escolha o tipo de storage${RESET} [1]: " read MEGA_STORAGE_CHOICE MEGA_STORAGE_CHOICE="${MEGA_STORAGE_CHOICE:-1}" if [ "$MEGA_STORAGE_CHOICE" = "2" ]; then echo "" echo -e " ${BOLD}Configuracao S3 (AWS):${RESET}" ask_input "Bucket name" "" "MEGA_S3_BUCKET" ask_input "Access Key ID" "" "MEGA_S3_ACCESS_KEY" ask_input "Secret Access Key" "" "MEGA_S3_SECRET_KEY" "true" ask_input "Region" "us-east-1" "MEGA_S3_REGION" elif [ "$MEGA_STORAGE_CHOICE" = "3" ]; then echo "" echo -e " ${BOLD}Configuracao S3-compatible (R2, Spaces, etc):${RESET}" ask_input "Bucket name" "" "MEGA_S3_BUCKET" ask_input "Access Key ID" "" "MEGA_S3_ACCESS_KEY" ask_input "Secret Access Key" "" "MEGA_S3_SECRET_KEY" "true" ask_input "Region" "auto" "MEGA_S3_REGION" ask_input "Endpoint URL (ex: https://ACCOUNT.r2.cloudflarestorage.com)" "" "MEGA_S3_ENDPOINT" fi fi if [ "$MEGA_STORAGE_CHOICE" = "2" ]; then MEGA_STORAGE_TYPE="s3" elif [ "$MEGA_STORAGE_CHOICE" = "3" ]; then MEGA_STORAGE_TYPE="s3_compatible" fi echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Baixar mega-yml --- step=$((step + 1)) local mega_raw="${TEMP_DIR}/mega_raw.yaml" local mega_deploy="${TEMP_DIR}/mega_deploy.yaml" if ! download_yaml "${GITHUB_RAW}/mega-yml.txt" "$mega_raw" "mega-yml.txt"; then step_fail "$step" "$total" "Falha ao baixar mega-yml" return 1 fi step_ok "$step" "$total" "mega-yml baixado" # --- Passo 2: Substituir variaveis --- step=$((step + 1)) # Copiar para arquivo de trabalho cp "$mega_raw" "$mega_deploy" # Substituicoes principais sed -i "s|app\.example\.com|${URL_MEGA}|g" "$mega_deploy" sed -i "s|ExampleChat|${NOME_MEGA}|g" "$mega_deploy" sed -i "s|changemeexamplekey|${SECRET_KEY_MEGA}|g" "$mega_deploy" # PostgreSQL sed -i "s|123\.123\.123\.123|${MEGA_PG_HOST}|g" "$mega_deploy" sed -i "s|examplepassword|${MEGA_PG_PASS}|g" "$mega_deploy" # Database name nas connection strings (cuidado com contexto) sed -i "s|examplechat_dat|mega_data|g" "$mega_deploy" sed -i "s|POSTGRES_DATABASE: examplechat|POSTGRES_DATABASE: ${MEGA_PG_DB}|g" "$mega_deploy" sed -i "s|POSTGRES_DATABASE=examplechat|POSTGRES_DATABASE=${MEGA_PG_DB}|g" "$mega_deploy" 2>/dev/null || true # Routers do Traefik (example_app → mega_app) sed -i "s|example_app|mega_app|g" "$mega_deploy" # SMTP if [ -n "$SMTP_HOST" ]; then sed -i "s|smtp\.example\.com|${SMTP_HOST}|g" "$mega_deploy" fi if [ -n "$SMTP_EMAIL" ]; then sed -i "s|noreply@example\.com|${SMTP_EMAIL}|g" "$mega_deploy" fi if [ -n "$SMTP_PASS" ]; then sed -i "s|smtpexamplepassword|${SMTP_PASS}|g" "$mega_deploy" fi if [ -n "$SMTP_USER" ]; then sed -i "s|SMTP_USERNAME=.*|SMTP_USERNAME=${SMTP_USER}|g" "$mega_deploy" 2>/dev/null || true fi if [ -n "$SMTP_PORT" ]; then sed -i "s|SMTP_PORT=587|SMTP_PORT=${SMTP_PORT}|g" "$mega_deploy" 2>/dev/null || true fi if [ -n "$SMTP_DOMAIN" ]; then # Substituir dominio generico em contexto de email, com cuidado sed -i "s|MAILER_SENDER_EMAIL=.*|MAILER_SENDER_EMAIL=${SMTP_EMAIL}|g" "$mega_deploy" 2>/dev/null || true sed -i "s|SMTP_DOMAIN=example\.com|SMTP_DOMAIN=${SMTP_DOMAIN}|g" "$mega_deploy" 2>/dev/null || true fi # Integracoes - Evolution if [ -n "$MEGA_EVO_URL" ]; then sed -i "s|api\.evolution\.example\.com|${MEGA_EVO_URL}|g" "$mega_deploy" fi if [ -n "$MEGA_EVO_TOKEN" ]; then # Token evolution com contexto (a primeira ocorrencia e geralmente evolution) # Usar abordagem mais segura com marcadores sed -i "/[Ee]volution/,/token/{s|exampletoken1234567890|${MEGA_EVO_TOKEN}|}" "$mega_deploy" 2>/dev/null || \ sed -i "0,/exampletoken1234567890/s|exampletoken1234567890|${MEGA_EVO_TOKEN}|" "$mega_deploy" 2>/dev/null || true fi # Integracoes - WAHA if [ -n "$MEGA_WAHA_URL" ]; then sed -i "s|waha\.example\.com|${MEGA_WAHA_URL}|g" "$mega_deploy" fi if [ -n "$MEGA_WAHA_TOKEN" ]; then sed -i "s|exampletoken1234567890|${MEGA_WAHA_TOKEN}|g" "$mega_deploy" 2>/dev/null || true fi # Redis interno do Mega — renomear para mega_redis evitar colisao de DNS # com o Redis global (ambos estariam na mega_net com o nome "redis") sed -i "s|redis://example_redis:6379|redis://mega_redis:6379|g" "$mega_deploy" sed -i "s|REDIS_URL: redis://redis:6379|REDIS_URL: redis://mega_redis:6379|g" "$mega_deploy" sed -i "s| redis:$| mega_redis:|g" "$mega_deploy" sed -i "s|mired|${REDE_INTERNA}|g" "$mega_deploy" # Rede sed -i "s|examplenet|${REDE_INTERNA}|g" "$mega_deploy" # Storage de midia if [ "$MEGA_STORAGE_TYPE" = "s3" ]; then sed -i "s|ACTIVE_STORAGE_SERVICE=local|ACTIVE_STORAGE_SERVICE=amazon|g" "$mega_deploy" 2>/dev/null || true sed -i "/ACTIVE_STORAGE_SERVICE/a\\ - STORAGE_BUCKET_NAME=${MEGA_S3_BUCKET}\\ - STORAGE_ACCESS_KEY_ID=${MEGA_S3_ACCESS_KEY}\\ - STORAGE_SECRET_ACCESS_KEY=${MEGA_S3_SECRET_KEY}\\ - STORAGE_REGION=${MEGA_S3_REGION}" "$mega_deploy" 2>/dev/null || true elif [ "$MEGA_STORAGE_TYPE" = "s3_compatible" ]; then sed -i "s|ACTIVE_STORAGE_SERVICE=local|ACTIVE_STORAGE_SERVICE=s3_compatible|g" "$mega_deploy" 2>/dev/null || true sed -i "/ACTIVE_STORAGE_SERVICE/a\\ - STORAGE_BUCKET_NAME=${MEGA_S3_BUCKET}\\ - STORAGE_ACCESS_KEY_ID=${MEGA_S3_ACCESS_KEY}\\ - STORAGE_SECRET_ACCESS_KEY=${MEGA_S3_SECRET_KEY}\\ - STORAGE_REGION=${MEGA_S3_REGION}\\ - STORAGE_ENDPOINT=${MEGA_S3_ENDPOINT}\\ - STORAGE_FORCE_PATH_STYLE=true" "$mega_deploy" 2>/dev/null || true else msg_info "Storage: local (/app/storage)" msg_warn "Certifique-se de que o volume 'mega_data' tem permissao de escrita!" fi step_ok "$step" "$total" "Variaveis substituidas no mega-yml" # --- Passo 3: Criar volumes --- step=$((step + 1)) docker volume create mega_data 2>/dev/null || true docker volume create mega_redis_data 2>/dev/null || true step_ok "$step" "$total" "Volumes mega_data e mega_redis_data criados/existentes" # --- Passo 4: Deploy --- step=$((step + 1)) deploy_via_portainer mega "$mega_deploy" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack mega implantada" else step_fail "$step" "$total" "Falha ao implantar stack mega" return 1 fi # --- Passo 5: Aguardar e rodar migration --- step=$((step + 1)) msg_info "Aguardando Mega inicializar... (pode levar 2-3 minutos)" wait_service "mega_mega_app" 180 # Aguardar um pouco mais para a app estar pronta sleep 20 local mega_container mega_container=$(docker ps -q -f name=mega_mega_app 2>/dev/null | head -1) if [ -n "$mega_container" ]; then msg_info "Executando migrations..." # Remover constraints duplicadas conhecidas que causam falha no db:chatwoot_prepare local pg_container pg_container=$(docker ps -q -f name=postgres_postgres 2>/dev/null | head -1) if [ -n "$pg_container" ]; then docker exec "$pg_container" psql -U "${PG_USER:-postgres}" -d "${MEGA_PG_DB:-chatwoot_db}" \ -c "ALTER TABLE active_storage_attachments DROP CONSTRAINT IF EXISTS fk_rails_c3b3935057;" \ 2>/dev/null || true fi # Rodar migrations — tenta chatwoot_prepare, fallback para db:migrate local migrate_out migrate_exit migrate_out=$(docker exec "$mega_container" bundle exec rails db:chatwoot_prepare 2>&1) migrate_exit=$? if [ $migrate_exit -eq 0 ]; then step_ok "$step" "$total" "Migrations executadas com sucesso" else msg_warn "db:chatwoot_prepare falhou, tentando db:migrate..." migrate_out=$(docker exec "$mega_container" bundle exec rails db:migrate 2>&1) migrate_exit=$? if [ $migrate_exit -eq 0 ]; then step_ok "$step" "$total" "Migrations executadas via db:migrate" else msg_warn "Migration com erros. Forcando restart do servico..." docker service update --force mega_mega_app 2>/dev/null || true step_skip "$step" "$total" "Migration parcial — servico reiniciado" fi fi else msg_warn "Container mega_app nao encontrado. Execute manualmente:" echo -e " ${DIM}docker exec \$(docker ps -q -f name=mega_mega_app) bundle exec rails db:chatwoot_prepare${RESET}" step_skip "$step" "$total" "Migration adiada" fi # --- Cleanup --- rm -f "$mega_raw" # --- Salvar credenciais --- local mega_storage_info="local (/app/storage)" if [ "$MEGA_STORAGE_TYPE" = "s3" ]; then mega_storage_info="S3 AWS — bucket: ${MEGA_S3_BUCKET} (${MEGA_S3_REGION})" elif [ "$MEGA_STORAGE_TYPE" = "s3_compatible" ]; then mega_storage_info="S3-compatible — bucket: ${MEGA_S3_BUCKET} (${MEGA_S3_ENDPOINT})" fi save_credential_block "Mega" \ "URL : https://${URL_MEGA}" \ "Nome : ${NOME_MEGA}" \ "Secret Key : ${SECRET_KEY_MEGA}" \ "DB Host : ${MEGA_PG_HOST}:${MEGA_PG_PORT}" \ "DB Name : ${MEGA_PG_DB}" \ "DB User : ${MEGA_PG_USER}" \ "SMTP : ${SMTP_HOST}:${SMTP_PORT}" \ "SMTP Email : ${SMTP_EMAIL}" \ "Storage : ${mega_storage_info}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} MEGA INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_MEGA}${RESET}" echo -e " Nome : ${BOLD}${NOME_MEGA}${RESET}" echo -e " Acesso: ${BOLD}configurar no primeiro acesso${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL WAHA # ============================================================================ install_waha() { detect_installed if $WAHA_OK; then msg_warn "WAHA ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm waha${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ WAHA — WhatsApp HTTP API ━━━${RESET}" echo "" echo -e " Gateway de WhatsApp baseado no WhatsApp Web." echo -e " Conecta o Mega ao WhatsApp via API REST." echo "" echo -e " ${DIM}• Suporte a multiplos numeros (multi-session)${RESET}" echo -e " ${DIM}• Envio e recebimento de mensagens, midias e documentos${RESET}" echo -e " ${DIM}• Webhooks em tempo real para o Mega${RESET}" echo -e " ${DIM}• Versao Plus: suporte a grupos, status e mais${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO WAHA" local total=4 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi # --- Coleta de dados --- echo -e " ${BOLD}Configure o WAHA:${RESET}" echo "" local URL_WAHA="${URL_WAHA}" WAHA_API_KEY="${WAHA_API_KEY}" WAHA_DASH_USER="${WAHA_DASH_USER}" WAHA_DASH_PASS="${WAHA_DASH_PASS}" local WAHA_PG_HOST WAHA_PG_PORT WAHA_PG_USER WAHA_PG_PASS WAHA_DB="${WAHA_DB}" ask_input "Dominio do WAHA (ex: waha.seudominio.com)" "" "URL_WAHA" if [ -z "$URL_WAHA" ]; then msg_fail "Dominio e obrigatorio!" return 1 fi ask_input "API Key do WAHA (vazio = gerar)" "" "WAHA_API_KEY" if [ -z "$WAHA_API_KEY" ]; then WAHA_API_KEY=$(generate_api_key) echo -e " ${DIM}API Key gerada: ${WAHA_API_KEY}${RESET}" fi ask_input "Usuario do Dashboard WAHA" "wahaadmin" "WAHA_DASH_USER" ask_input "Senha do Dashboard WAHA (vazio = gerar)" "" "WAHA_DASH_PASS" "true" if [ -z "$WAHA_DASH_PASS" ]; then WAHA_DASH_PASS=$(generate_password) echo -e " ${DIM}Senha Dashboard gerada: ${WAHA_DASH_PASS}${RESET}" fi echo "" echo -e " ${YELLOW}--- PostgreSQL para WAHA ---${RESET}" resolve_pg_credentials if [ -z "$PG_PASS" ]; then msg_fail "Falha ao instalar PostgreSQL automaticamente. Verifique os logs." return 1 fi WAHA_PG_HOST="$PG_HOST" WAHA_PG_PORT="${PG_PORT:-5432}" WAHA_PG_USER="$PG_USER" WAHA_PG_PASS="$PG_PASS" msg_info "Usando PostgreSQL: ${WAHA_PG_HOST}:${WAHA_PG_PORT} (usuario: ${WAHA_PG_USER})" ask_input "Database WAHA" "waha_db" "WAHA_DB" create_pg_database "$WAHA_DB" # --- Storage de midia S3 (opcional) --- echo "" echo -e " ${YELLOW}--- Storage de midia (S3 opcional) ---${RESET}" echo -e " ${WHITE}WAHA pode armazenar midias em S3 em vez de localmente.${RESET}" echo "" local WAHA_STORAGE_TYPE="local" local WAHA_S3_BUCKET="${WAHA_S3_BUCKET}" WAHA_S3_ACCESS_KEY="${WAHA_S3_ACCESS_KEY}" WAHA_S3_SECRET_KEY="${WAHA_S3_SECRET_KEY}" WAHA_S3_REGION="${WAHA_S3_REGION}" WAHA_S3_ENDPOINT="${WAHA_S3_ENDPOINT}" # Se S3 ja pre-configurado pelo wizard, usar direto if [ -n "$WAHA_S3_BUCKET" ]; then if [ -n "$WAHA_S3_ENDPOINT" ]; then WAHA_STORAGE_TYPE="s3_compatible" else WAHA_STORAGE_TYPE="s3" fi elif ask_confirm "Configurar S3 para midias do WAHA?" "n"; then echo "" echo -e " ${GREEN}1)${RESET} S3 — Amazon AWS" echo -e " ${GREEN}2)${RESET} S3-compatible — Cloudflare R2, DigitalOcean Spaces, etc." echo "" echo -ne " ${CYAN}Tipo${RESET} [1]: " local waha_s3_type read waha_s3_type waha_s3_type="${waha_s3_type:-1}" ask_input "Bucket name" "" "WAHA_S3_BUCKET" ask_input "Access Key ID" "" "WAHA_S3_ACCESS_KEY" ask_input "Secret Access Key" "" "WAHA_S3_SECRET_KEY" "true" ask_input "Region" "us-east-1" "WAHA_S3_REGION" if [ "$waha_s3_type" = "2" ]; then WAHA_STORAGE_TYPE="s3_compatible" ask_input "Endpoint URL (ex: https://ACCOUNT.r2.cloudflarestorage.com)" "" "WAHA_S3_ENDPOINT" else WAHA_STORAGE_TYPE="s3" fi fi # Gerar hash SHA512 da API key local WAHA_API_KEY_HASH WAHA_API_KEY_HASH="sha512:$(echo -n "${WAHA_API_KEY}" | sha512sum | cut -d' ' -f1)" # Construir URI PostgreSQL para WAHA local WAHA_PG_URI WAHA_PG_URI="${WAHA_PG_USER}:${WAHA_PG_PASS}@${WAHA_PG_HOST}:${WAHA_PG_PORT}/${WAHA_DB}" echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Baixar waha.yml --- step=$((step + 1)) local waha_raw="${TEMP_DIR}/waha_raw.yaml" local waha_deploy="${TEMP_DIR}/waha_deploy.yaml" if ! download_yaml "${GITHUB_RAW}/waha.yml" "$waha_raw" "waha.yml"; then step_fail "$step" "$total" "Falha ao baixar waha.yml" return 1 fi step_ok "$step" "$total" "waha.yml baixado" # --- Passo 2: Substituir variaveis --- step=$((step + 1)) cp "$waha_raw" "$waha_deploy" # Dominio sed -i "s|waha\.example\.com|${URL_WAHA}|g" "$waha_deploy" # API Key plain sed -i "s|72095ca99cdd085c57c896dfcebbdffb|${WAHA_API_KEY}|g" "$waha_deploy" # API Key hash (a hash SHA512 longa) # Pegar a hash antiga do arquivo e substituir pela nova local old_hash old_hash=$(grep -oP 'sha512:[a-f0-9]{128}' "$waha_deploy" 2>/dev/null | head -1) if [ -n "$old_hash" ]; then sed -i "s|${old_hash}|${WAHA_API_KEY_HASH}|g" "$waha_deploy" fi # PostgreSQL URI sed -i "s|postgres:Mfcd62!!Mfcd62!!@postgreswaha:5432/wahadb|${WAHA_PG_URI}|g" "$waha_deploy" # Tambem tentar formatos alternativos da URI sed -i "s|postgresql://[^\"]*wahadb|postgresql://${WAHA_PG_URI}|g" "$waha_deploy" 2>/dev/null || true # Dashboard sed -i "s|wahaadmin|${WAHA_DASH_USER}|g" "$waha_deploy" 2>/dev/null || true sed -i "s|96a3dce1633e59b4b0b|${WAHA_DASH_PASS}|g" "$waha_deploy" # Rede — tudo na mega_net sed -i "s|examplenet|${REDE_INTERNA}|g" "$waha_deploy" 2>/dev/null || true # Substitui referencia de rede em qualquer nivel de indentacao (waha service: 6 esp, redis service: 8 esp) sed -i "s|- waha$|- ${REDE_INTERNA}|g" "$waha_deploy" 2>/dev/null || true # Substitui declaracao top-level da rede (4 espacos na chave, 6 no name) sed -i "s| waha:| ${REDE_INTERNA}:|g" "$waha_deploy" 2>/dev/null || true sed -i "s| name: waha$| name: ${REDE_INTERNA}|g" "$waha_deploy" 2>/dev/null || true # Renomear Redis interno para waha_redis (evitar colisao de DNS na mega_net) sed -i "s| redis:$| waha_redis:|g" "$waha_deploy" sed -i "s|redis://redis:6379|redis://waha_redis:6379|g" "$waha_deploy" 2>/dev/null || true # Storage S3 do WAHA if [ "$WAHA_STORAGE_TYPE" != "local" ]; then # Descomentar e preencher linhas S3 sed -i "s|#- WAHA_MEDIA_STORAGE=S3| - WAHA_MEDIA_STORAGE=S3|g" "$waha_deploy" 2>/dev/null || true sed -i "s|#- WAHA_S3_REGION=| - WAHA_S3_REGION=${WAHA_S3_REGION}|g" "$waha_deploy" 2>/dev/null || true sed -i "s|#- WAHA_S3_BUCKET=| - WAHA_S3_BUCKET=${WAHA_S3_BUCKET}|g" "$waha_deploy" 2>/dev/null || true sed -i "s|#- WAHA_S3_ACCESS_KEY_ID=| - WAHA_S3_ACCESS_KEY_ID=${WAHA_S3_ACCESS_KEY}|g" "$waha_deploy" 2>/dev/null || true sed -i "s|#- WAHA_S3_SECRET_ACCESS_KEY=| - WAHA_S3_SECRET_ACCESS_KEY=${WAHA_S3_SECRET_KEY}|g" "$waha_deploy" 2>/dev/null || true if [ "$WAHA_STORAGE_TYPE" = "s3_compatible" ]; then sed -i "s|#- WAHA_S3_ENDPOINT=| - WAHA_S3_ENDPOINT=${WAHA_S3_ENDPOINT}|g" "$waha_deploy" 2>/dev/null || true sed -i "s|#- WAHA_S3_FORCE_PATH_STYLE=True| - WAHA_S3_FORCE_PATH_STYLE=True|g" "$waha_deploy" 2>/dev/null || true fi sed -i "s|#- WAHA_S3_PROXY_FILES=False| - WAHA_S3_PROXY_FILES=False|g" "$waha_deploy" 2>/dev/null || true msg_info "Storage WAHA: S3 — bucket ${WAHA_S3_BUCKET}" fi step_ok "$step" "$total" "Variaveis substituidas no waha.yml" # --- Passo 3: Garantir rede interna --- step=$((step + 1)) docker network create --driver=overlay --attachable "${REDE_INTERNA}" 2>/dev/null || true step_ok "$step" "$total" "Rede ${REDE_INTERNA} verificada" # --- Passo 4: Deploy --- step=$((step + 1)) deploy_via_portainer waha "$waha_deploy" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack waha implantada" else step_fail "$step" "$total" "Falha ao implantar stack waha" return 1 fi wait_service "waha_waha" 120 # --- Cleanup --- rm -f "$waha_raw" # --- Salvar credenciais --- local waha_storage_info="local" if [ "$WAHA_STORAGE_TYPE" = "s3" ]; then waha_storage_info="S3 AWS — bucket: ${WAHA_S3_BUCKET} (${WAHA_S3_REGION})" elif [ "$WAHA_STORAGE_TYPE" = "s3_compatible" ]; then waha_storage_info="S3-compatible — bucket: ${WAHA_S3_BUCKET} (${WAHA_S3_ENDPOINT})" fi save_credential_block "WAHA" \ "URL : https://${URL_WAHA}" \ "API Key : ${WAHA_API_KEY}" \ "API Key Hash : ${WAHA_API_KEY_HASH}" \ "Dashboard User : ${WAHA_DASH_USER}" \ "Dashboard Pass : ${WAHA_DASH_PASS}" \ "DB URI : postgresql://${WAHA_PG_URI}" \ "Storage : ${waha_storage_info}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} WAHA INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_WAHA}${RESET}" echo -e " API Key : ${BOLD}${WAHA_API_KEY}${RESET}" echo -e " Dashboard : ${BOLD}${WAHA_DASH_USER} / ${WAHA_DASH_PASS}${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL FZAP # ============================================================================ install_fzap() { detect_installed if $FZAP_OK; then msg_warn "FZAP ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm fzap${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ FZAP ━━━${RESET}" echo "" echo -e " Conector WhatsApp brasileiro, alternativa ao WAHA." echo -e " Alta compatibilidade com o Mega e suporte em PT-BR." echo "" echo -e " ${DIM}• Multi-sessao com painel de gerenciamento${RESET}" echo -e " ${DIM}• Versao Free e versao com licence key${RESET}" echo -e " ${DIM}• Integra nativamente com este installer${RESET}" echo -e " ${DIM}• Suporte via comunidade Mega${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO FZAP" local total=4 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi # --- Coleta de dados --- echo -e " ${BOLD}Configure o FZAP:${RESET}" echo "" local URL_FZAP="${URL_FZAP}" FZAP_ADMIN_TOKEN="${FZAP_ADMIN_TOKEN}" FZAP_LICENCE_KEY="${FZAP_LICENCE_KEY}" local FZAP_PG_HOST FZAP_PG_PORT FZAP_PG_USER FZAP_PG_PASS FZAP_DB="${FZAP_DB}" ask_input "Dominio do FZAP (ex: fzap.seudominio.com)" "" "URL_FZAP" if [ -z "$URL_FZAP" ]; then msg_fail "Dominio e obrigatorio!" return 1 fi ask_input "Admin Token do FZAP (vazio = gerar)" "" "FZAP_ADMIN_TOKEN" if [ -z "$FZAP_ADMIN_TOKEN" ]; then FZAP_ADMIN_TOKEN=$(generate_api_key) echo -e " ${DIM}Admin Token gerado: ${FZAP_ADMIN_TOKEN}${RESET}" fi ask_input "Licence Key FZAP (vazio = versao free)" "" "FZAP_LICENCE_KEY" echo "" echo -e " ${YELLOW}--- PostgreSQL para FZAP ---${RESET}" resolve_pg_credentials if [ -z "$PG_PASS" ]; then msg_fail "Falha ao instalar PostgreSQL automaticamente. Verifique os logs." return 1 fi FZAP_PG_HOST="$PG_HOST" FZAP_PG_PORT="${PG_PORT:-5432}" FZAP_PG_USER="$PG_USER" FZAP_PG_PASS="$PG_PASS" msg_info "Usando PostgreSQL: ${FZAP_PG_HOST}:${FZAP_PG_PORT} (usuario: ${FZAP_PG_USER})" ask_input "Database FZAP" "fzap_db" "FZAP_DB" create_pg_database "$FZAP_DB" echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Criar volumes --- step=$((step + 1)) docker volume create fzap_dbdata 2>/dev/null || true docker volume create fzap_files 2>/dev/null || true step_ok "$step" "$total" "Volumes fzap_dbdata e fzap_files criados" # --- Passo 2: Gerar YAML --- step=$((step + 1)) local fzap_deploy="${TEMP_DIR}/fzap_deploy.yaml" cat > "$fzap_deploy" << FZAPEOF version: "3.7" services: fzap: image: dncarbonell/fzap:latest volumes: - fzap_dbdata:/app/dbdata - fzap_files:/app/files networks: - ${REDE_INTERNA} environment: - ADMIN_TOKEN=${FZAP_ADMIN_TOKEN} - FLOUDS_LICENCE_KEY=${FZAP_LICENCE_KEY} - PUBLIC_BASE_URL=https://${URL_FZAP} - FZAP_LANGUAGE=pt-BR - DB_HOST=${FZAP_PG_HOST} - DB_USER=${FZAP_PG_USER} - DB_PASSWORD=${FZAP_PG_PASS} - DB_NAME=${FZAP_DB} - DB_PORT=${FZAP_PG_PORT} - DB_DRIVER=postgres - TZ=America/Sao_Paulo - WEBHOOK_FORMAT=json - SESSION_DEVICE_NAME=Fzap deploy: mode: replicated replicas: 1 placement: constraints: - node.role == manager labels: - traefik.enable=true - traefik.http.routers.fzap.rule=Host(\`${URL_FZAP}\`) - traefik.http.services.fzap.loadbalancer.server.port=8080 - traefik.http.routers.fzap.service=fzap - traefik.http.routers.fzap.tls.certresolver=letsencryptresolver - traefik.http.routers.fzap.entrypoints=websecure volumes: fzap_dbdata: external: true name: fzap_dbdata fzap_files: external: true name: fzap_files networks: ${REDE_INTERNA}: external: true name: ${REDE_INTERNA} FZAPEOF step_ok "$step" "$total" "YAML gerado" # --- Passo 3: Deploy --- step=$((step + 1)) deploy_via_portainer fzap "$fzap_deploy" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack fzap implantada" else step_fail "$step" "$total" "Falha ao implantar stack fzap" return 1 fi # --- Passo 4: Health check --- step=$((step + 1)) wait_service "fzap_fzap" 120 step_ok "$step" "$total" "FZAP online" # --- Cleanup --- rm -f "$fzap_deploy" # --- Salvar credenciais --- save_credential_block "FZAP" \ "URL : https://${URL_FZAP}" \ "Admin Token : ${FZAP_ADMIN_TOKEN}" \ "Licence Key : ${FZAP_LICENCE_KEY:-free}" \ "Database : ${FZAP_DB}" \ "PG Host : ${FZAP_PG_HOST}:${FZAP_PG_PORT}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} FZAP INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_FZAP}${RESET}" echo -e " Admin Token : ${BOLD}${FZAP_ADMIN_TOKEN}${RESET}" echo -e " Licenca : ${BOLD}${FZAP_LICENCE_KEY:-free}${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL EVOLUTION v2 # ============================================================================ install_evolution() { detect_installed if $EVOLUTION_OK; then msg_warn "Evolution ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm evolution${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ EVOLUTION API v2 ━━━${RESET}" echo "" echo -e " API de WhatsApp mais completa do ecossistema open source." echo -e " Suporte a WhatsApp Web e WhatsApp Business API (oficial)." echo "" echo -e " ${DIM}• Multi-instancia com gerenciamento via API REST${RESET}" echo -e " ${DIM}• Suporte a Typebot, N8N, Mega e outros${RESET}" echo -e " ${DIM}• Funcionalidades avancadas: grupos, lista, botoes${RESET}" echo -e " ${DIM}• Websocket em tempo real${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO EVOLUTION API v2" local total=4 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi # --- Coleta de dados --- echo -e " ${BOLD}Configure o Evolution API v2:${RESET}" echo "" local URL_EVOLUTION="${URL_EVOLUTION}" EVOLUTION_API_KEY="${EVOLUTION_API_KEY}" local EVO_PG_HOST EVO_PG_PORT EVO_PG_USER EVO_PG_PASS EVO_PG_DB="${EVO_PG_DB}" local EVO_CW_PG_DB="${EVO_CW_PG_DB}" local EVOLUTION_PG_URI EVOLUTION_CHATWOOT_PG_URI EVOLUTION_REDIS_URI ask_input "Dominio do Evolution (ex: api.evolution.seudominio.com)" "" "URL_EVOLUTION" if [ -z "$URL_EVOLUTION" ]; then msg_fail "Dominio e obrigatorio!" return 1 fi ask_input "API Key do Evolution (vazio = gerar)" "" "EVOLUTION_API_KEY" if [ -z "$EVOLUTION_API_KEY" ]; then EVOLUTION_API_KEY=$(generate_api_key) echo -e " ${DIM}API Key gerada: ${EVOLUTION_API_KEY}${RESET}" fi echo "" echo -e " ${YELLOW}--- PostgreSQL para Evolution ---${RESET}" resolve_pg_credentials if [ -z "$PG_PASS" ]; then msg_fail "Falha ao instalar PostgreSQL automaticamente. Verifique os logs." return 1 fi EVO_PG_HOST="$PG_HOST" EVO_PG_PORT="${PG_PORT:-5432}" EVO_PG_USER="$PG_USER" EVO_PG_PASS="$PG_PASS" msg_info "Usando PostgreSQL: ${EVO_PG_HOST}:${EVO_PG_PORT} (usuario: ${EVO_PG_USER})" ask_input "Database Evolution" "evolution_db" "EVO_PG_DB" create_pg_database "$EVO_PG_DB" ask_input "Database Mega (para import)" "chatwoot_db" "EVO_CW_PG_DB" create_pg_database "$EVO_CW_PG_DB" EVOLUTION_PG_URI="postgresql://${EVO_PG_USER}:${EVO_PG_PASS}@${EVO_PG_HOST}:${EVO_PG_PORT}/${EVO_PG_DB}" EVOLUTION_CHATWOOT_PG_URI="postgresql://${EVO_PG_USER}:${EVO_PG_PASS}@${EVO_PG_HOST}:${EVO_PG_PORT}/${EVO_CW_PG_DB}" # Redis proprio da Evolution (embutido no stack, renomeado para evolution_redis) EVOLUTION_REDIS_URI="redis://evolution_redis:6379/1" echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Baixar evolution.yml --- step=$((step + 1)) local evo_raw="${TEMP_DIR}/evolution_raw.yaml" local evo_deploy="${TEMP_DIR}/evolution_deploy.yaml" if ! download_yaml "${GITHUB_RAW}/evolution.yml" "$evo_raw" "evolution.yml"; then step_fail "$step" "$total" "Falha ao baixar evolution.yml" return 1 fi step_ok "$step" "$total" "evolution.yml baixado" # --- Passo 2: Substituir variaveis --- step=$((step + 1)) cp "$evo_raw" "$evo_deploy" # Dominio — substituir no contexto de Host/URL do evolution sed -i "s|evolution\.example\.com|${URL_EVOLUTION}|g" "$evo_deploy" sed -i "s|api\.evolution\.example\.com|${URL_EVOLUTION}|g" "$evo_deploy" 2>/dev/null || true # Tambem tentar formato generico (cuidado para nao quebrar outros dominios) sed -i "s|SERVER_URL=https://example\.com|SERVER_URL=https://${URL_EVOLUTION}|g" "$evo_deploy" 2>/dev/null || true # API Key sed -i "s|your_api_key|${EVOLUTION_API_KEY}|g" "$evo_deploy" sed -i "s|YOUR_API_KEY|${EVOLUTION_API_KEY}|g" "$evo_deploy" 2>/dev/null || true sed -i "s|your-api-key|${EVOLUTION_API_KEY}|g" "$evo_deploy" 2>/dev/null || true # PostgreSQL URI evolution sed -i "s|postgresql://user:password@db\.example\.com:5432/evolution_db|${EVOLUTION_PG_URI}|g" "$evo_deploy" sed -i "s|postgresql://user:password@localhost:5432/evolution|${EVOLUTION_PG_URI}|g" "$evo_deploy" 2>/dev/null || true # PostgreSQL URI chatwoot (import) sed -i "s|postgresql://user:password@db\.example\.com:5432/chatwoot_db|${EVOLUTION_CHATWOOT_PG_URI}|g" "$evo_deploy" sed -i "s|postgresql://user:password@localhost:5432/chatwoot|${EVOLUTION_CHATWOOT_PG_URI}|g" "$evo_deploy" 2>/dev/null || true # Renomear Redis interno para evolution_redis (evitar confusao no docker service ls) sed -i "s| redis:$| evolution_redis:|g" "$evo_deploy" # Redis URI — atualizar para apontar ao nome renomeado sed -i "s|redis://localhost:6379/1|redis://evolution_redis:6379/1|g" "$evo_deploy" 2>/dev/null || true sed -i "s|redis://redis:6379/1|redis://evolution_redis:6379/1|g" "$evo_deploy" 2>/dev/null || true sed -i "s|redis://redis:6379|redis://evolution_redis:6379|g" "$evo_deploy" 2>/dev/null || true # Rede — tudo na mega_net sed -i "s|examplenet|${REDE_INTERNA}|g" "$evo_deploy" 2>/dev/null || true # Referencia nos services (6 espacos em ambos os services) sed -i "s|- evolution$|- ${REDE_INTERNA}|g" "$evo_deploy" 2>/dev/null || true # Declaracao top-level (2 espacos na chave, 4 no name) sed -i "s| evolution:| ${REDE_INTERNA}:|g" "$evo_deploy" 2>/dev/null || true sed -i "s| name: evolution$| name: ${REDE_INTERNA}|g" "$evo_deploy" 2>/dev/null || true step_ok "$step" "$total" "Variaveis substituidas no evolution.yml" # --- Passo 3: Garantir rede interna --- step=$((step + 1)) docker network create --driver=overlay --attachable "${REDE_INTERNA}" 2>/dev/null || true step_ok "$step" "$total" "Rede ${REDE_INTERNA} verificada" # --- Passo 4: Deploy --- step=$((step + 1)) deploy_via_portainer evolution "$evo_deploy" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack evolution implantada" else step_fail "$step" "$total" "Falha ao implantar stack evolution" return 1 fi wait_service "evolution_evolution" 120 || wait_service "evolution_evolution_v2" 120 || true # --- Cleanup --- rm -f "$evo_raw" # --- Salvar credenciais --- save_credential_block "Evolution API v2" \ "URL : https://${URL_EVOLUTION}" \ "API Key : ${EVOLUTION_API_KEY}" \ "PG URI : ${EVOLUTION_PG_URI}" \ "Mega PG URI : ${EVOLUTION_CHATWOOT_PG_URI}" \ "Redis URI : ${EVOLUTION_REDIS_URI}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} EVOLUTION API v2 INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_EVOLUTION}${RESET}" echo -e " API Key : ${BOLD}${EVOLUTION_API_KEY}${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL DATABASUS # ============================================================================ install_databasus() { detect_installed if $DATABASUS_OK; then msg_warn "Databasus ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm databasus${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ DATABASUS ━━━${RESET}" echo "" echo -e " Sistema de backup automatico para bancos PostgreSQL." echo -e " Agenda dumps diarios e envia para S3 ou armazena localmente." echo "" echo -e " ${DIM}• Backup agendado via cron${RESET}" echo -e " ${DIM}• Suporte a S3, Cloudflare R2 e MinIO${RESET}" echo -e " ${DIM}• Retencao configuravel (7, 14, 30 dias)${RESET}" echo -e " ${DIM}• Restauracao simples com um comando${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "INSTALANDO DATABASUS" local total=4 local step=0 # --- Detectar rede --- if [ -z "$REDE_INTERNA" ]; then local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna" | head -1) if [ -n "$detected_net" ]; then REDE_INTERNA="$detected_net" msg_info "Rede detectada: ${REDE_INTERNA}" else ask_input "Nome da rede interna" "mega_net" "REDE_INTERNA" fi fi # --- Coleta de dados --- echo -e " ${BOLD}Configure o Databasus:${RESET}" echo "" local URL_DATABASUS="${URL_DATABASUS}" ask_input "Dominio do Databasus (ex: backup.seudominio.com)" "" "URL_DATABASUS" if [ -z "$URL_DATABASUS" ]; then msg_fail "Dominio e obrigatorio!" return 1 fi # --- Perguntar sobre auto-config --- echo "" echo -e " ${YELLOW}--- Configuracao de backup ---${RESET}" echo -e " ${WHITE}Apos a instalacao, o Databasus precisa ser configurado com${RESET}" echo -e " ${WHITE}a conexao ao PostgreSQL e a politica de backup.${RESET}" echo "" echo -e " ${GREEN}1)${RESET} Configurar automaticamente via API (recomendado)" echo -e " ${DIM}Cria conexao ao PostgreSQL + policy de backup diario${RESET}" echo -e " ${GREEN}2)${RESET} Configurar manualmente pela interface web" echo -e " ${DIM}Acesse https://${URL_DATABASUS} apos a instalacao${RESET}" echo "" local DB_CONFIG_MODE="${DB_CONFIG_MODE}" local DB_BACKUP_DEST="${DB_BACKUP_DEST:-local}" local DB_S3_BUCKET="${DB_S3_BUCKET}" DB_S3_ACCESS_KEY="${DB_S3_ACCESS_KEY}" DB_S3_SECRET_KEY="${DB_S3_SECRET_KEY}" DB_S3_REGION="${DB_S3_REGION}" DB_S3_ENDPOINT="${DB_S3_ENDPOINT}" if [ -z "$DB_CONFIG_MODE" ]; then echo -ne " ${CYAN}Escolha${RESET} [1]: " read DB_CONFIG_MODE DB_CONFIG_MODE="${DB_CONFIG_MODE:-1}" fi if [ "$DB_CONFIG_MODE" = "1" ]; then # Garantir que temos credenciais do PostgreSQL resolve_pg_credentials if [ -z "$PG_PASS" ]; then msg_warn "PostgreSQL nao encontrado — auto-config nao disponivel, use a UI." DB_CONFIG_MODE="2" else # Se backup dest ja pre-configurado pelo wizard, pular if [ "$DB_BACKUP_DEST" = "local" ] && ! $WIZARD_MODE; then echo "" echo -e " ${YELLOW}Destino dos backups:${RESET}" echo -e " ${GREEN}1)${RESET} Local (/backups dentro do container)" echo -e " ${GREEN}2)${RESET} S3 — Amazon AWS" echo -e " ${GREEN}3)${RESET} S3-compatible — Cloudflare R2, DigitalOcean Spaces, etc." echo "" echo -ne " ${CYAN}Destino${RESET} [1]: " local db_dest_choice read db_dest_choice db_dest_choice="${db_dest_choice:-1}" if [ "$db_dest_choice" = "2" ]; then DB_BACKUP_DEST="s3" ask_input "Bucket name" "" "DB_S3_BUCKET" ask_input "Access Key ID" "" "DB_S3_ACCESS_KEY" ask_input "Secret Access Key" "" "DB_S3_SECRET_KEY" "true" ask_input "Region" "us-east-1" "DB_S3_REGION" elif [ "$db_dest_choice" = "3" ]; then DB_BACKUP_DEST="s3_compatible" ask_input "Bucket name" "" "DB_S3_BUCKET" ask_input "Access Key ID" "" "DB_S3_ACCESS_KEY" ask_input "Secret Access Key" "" "DB_S3_SECRET_KEY" "true" ask_input "Region" "auto" "DB_S3_REGION" ask_input "Endpoint URL (ex: https://ACCOUNT.r2.cloudflarestorage.com)" "" "DB_S3_ENDPOINT" fi fi fi fi echo "" echo -e " ${BOLD}Iniciando instalacao...${RESET}" echo "" # --- Passo 1: Criar volume --- step=$((step + 1)) docker volume create databasus_data 2>/dev/null || true step_ok "$step" "$total" "Volume databasus_data criado/existente" # --- Passo 2: Deploy --- step=$((step + 1)) cat > "${TEMP_DIR}/databasus_deploy.yaml" << DBEOF version: "3.8" services: databasus: image: ghcr.io/databasus/databasus:latest volumes: - databasus_data:/app/data networks: - ${REDE_INTERNA} deploy: mode: replicated replicas: 1 placement: constraints: [node.role == manager] labels: - traefik.enable=true - traefik.http.routers.databasus.rule=Host(\`${URL_DATABASUS}\`) - traefik.http.routers.databasus.entrypoints=websecure - traefik.http.routers.databasus.tls.certresolver=letsencryptresolver - traefik.http.services.databasus.loadbalancer.server.port=4005 - traefik.http.routers.databasus.service=databasus volumes: databasus_data: external: true networks: ${REDE_INTERNA}: external: true name: ${REDE_INTERNA} DBEOF deploy_via_portainer databasus "${TEMP_DIR}/databasus_deploy.yaml" if [ $? -eq 0 ]; then step_ok "$step" "$total" "Stack databasus implantada" else step_fail "$step" "$total" "Falha ao implantar stack databasus" return 1 fi # --- Passo 3: Health check --- step=$((step + 1)) wait_service "databasus_databasus" 120 step_ok "$step" "$total" "Databasus online" # --- Passo 4: Auto-config ou instrucoes UI --- step=$((step + 1)) if [ "$DB_CONFIG_MODE" = "1" ]; then msg_info "Aguardando API do Databasus ficar pronta..." local databasus_ready=false for i in $(seq 1 24); do local api_status api_status=$(curl -k -s -o /dev/null -w "%{http_code}" "https://${URL_DATABASUS}/api/v1/health" 2>/dev/null) # Tambem tentar sem path especifico [ "$api_status" = "000" ] && api_status=$(curl -k -s -o /dev/null -w "%{http_code}" "https://${URL_DATABASUS}" 2>/dev/null) if [ "$api_status" = "200" ] || [ "$api_status" = "301" ] || [ "$api_status" = "302" ]; then databasus_ready=true break fi sleep 5 done if $databasus_ready; then msg_info "Tentando auto-configuracao via API..." # Tentar descobrir API e criar policy via Swagger local api_result api_result=$(python3 << PYEOF import urllib.request, urllib.error, json, ssl, sys ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE base = "https://${URL_DATABASUS}" # 1. Tentar buscar Swagger para descobrir endpoints swagger_paths = ["/swagger/doc.json", "/swagger/index.html", "/api/v1/swagger.json", "/api/docs"] api_found = False for path in swagger_paths: try: req = urllib.request.Request(base + path, headers={"User-Agent": "mega-deploy/1.0"}) with urllib.request.urlopen(req, context=ctx, timeout=10) as r: if r.status == 200: print(f"SWAGGER_FOUND:{path}") api_found = True # Tentar parsear como JSON para extrair endpoints try: doc = json.load(r) paths = list(doc.get("paths", {}).keys()) print(f"ENDPOINTS:{json.dumps(paths[:20])}") except: pass break except: continue if not api_found: print("SWAGGER_NOT_FOUND") # 2. Tentar criar database connection via endpoints comuns db_endpoints = ["/api/v1/databases", "/api/v1/connections", "/api/v1/database"] pg_payload = json.dumps({ "name": "PostgreSQL Local", "type": "postgresql", "host": "${PG_HOST}", "port": int("${PG_PORT}"), "username": "${PG_USER}", "password": "${PG_PASS}", "database": "postgres" }).encode() for ep in db_endpoints: try: req = urllib.request.Request(base + ep, data=pg_payload, method="POST", headers={"Content-Type": "application/json", "User-Agent": "mega-deploy/1.0"}) with urllib.request.urlopen(req, context=ctx, timeout=15) as r: result = json.load(r) print(f"DB_CREATED:{ep}:{json.dumps(result)}") break except urllib.error.HTTPError as e: body = e.read().decode('utf-8', errors='replace')[:200] print(f"DB_ENDPOINT_FAIL:{ep}:{e.code}:{body}") except Exception as e: print(f"DB_ENDPOINT_ERROR:{ep}:{e}") # 3. Tentar criar backup policy policy_endpoints = ["/api/v1/policies", "/api/v1/backups/policies", "/api/v1/backup-policies"] policy_payload = json.dumps({ "name": "Backup diario PostgreSQL", "schedule": "0 2 * * *", "retention": {"days": 30, "count": 30}, "database_id": 1 }).encode() for ep in policy_endpoints: try: req = urllib.request.Request(base + ep, data=policy_payload, method="POST", headers={"Content-Type": "application/json", "User-Agent": "mega-deploy/1.0"}) with urllib.request.urlopen(req, context=ctx, timeout=15) as r: result = json.load(r) print(f"POLICY_CREATED:{ep}:{json.dumps(result)}") break except urllib.error.HTTPError as e: body = e.read().decode('utf-8', errors='replace')[:200] print(f"POLICY_FAIL:{ep}:{e.code}:{body}") except Exception as e: print(f"POLICY_ERROR:{ep}:{e}") PYEOF ) # Analisar resultado if echo "$api_result" | grep -q "DB_CREATED"; then msg_ok "Conexao PostgreSQL criada automaticamente no Databasus" if echo "$api_result" | grep -q "POLICY_CREATED"; then msg_ok "Policy de backup diario criada (2h da manha, retencao 30 dias)" else msg_warn "Policy de backup nao criada automaticamente" msg_info "Configure a policy de backup pela UI: https://${URL_DATABASUS}" fi step_ok "$step" "$total" "Auto-config concluida (parcial ou total)" elif echo "$api_result" | grep -q "SWAGGER_FOUND"; then local swagger_path swagger_path=$(echo "$api_result" | grep "SWAGGER_FOUND" | cut -d: -f2-) msg_warn "API encontrada (${swagger_path}) mas auto-config precisa de autenticacao" msg_info "Acesse https://${URL_DATABASUS} e configure:" echo -e " ${DIM}1. Adicione o PostgreSQL: ${PG_HOST}:${PG_PORT} (usuario: ${PG_USER})${RESET}" echo -e " ${DIM}2. Crie uma policy de backup (ex: diario as 2h, retencao 30 dias)${RESET}" if [ "$DB_BACKUP_DEST" != "local" ]; then echo -e " ${DIM}3. Configure destino S3: bucket ${DB_S3_BUCKET}${RESET}" fi step_ok "$step" "$total" "Databasus online — configuracao via UI necessaria" else msg_warn "API do Databasus nao respondeu como esperado" msg_info "Configure manualmente pela UI: https://${URL_DATABASUS}" echo -e " ${DIM}PostgreSQL: ${PG_HOST}:${PG_PORT} (usuario: ${PG_USER})${RESET}" step_skip "$step" "$total" "Auto-config nao disponivel — use a UI" fi else msg_warn "Databasus demorou para responder" msg_info "Configure manualmente: https://${URL_DATABASUS}" step_skip "$step" "$total" "API nao respondeu a tempo" fi else # Modo manual — instrucoes claras echo "" echo -e " ${BOLD}${YELLOW}CONFIGURACAO MANUAL NECESSARIA${RESET}" echo "" echo -e " Acesse: ${BOLD}https://${URL_DATABASUS}${RESET}" echo "" echo -e " ${WHITE}Passo 1: Adicionar conexao ao PostgreSQL${RESET}" if [ -n "$PG_HOST" ]; then echo -e " ${DIM}Host : ${PG_HOST}${RESET}" echo -e " ${DIM}Porta : ${PG_PORT:-5432}${RESET}" echo -e " ${DIM}Usuario : ${PG_USER}${RESET}" echo -e " ${DIM}Senha : (mesma do PostgreSQL salva nas credenciais)${RESET}" fi echo "" echo -e " ${WHITE}Passo 2: Criar policy de backup${RESET}" echo -e " ${DIM}Schedule : diario as 2h da manha (0 2 * * *)${RESET}" echo -e " ${DIM}Retencao : 30 dias${RESET}" echo "" echo -e " ${WHITE}Passo 3: Configurar destino${RESET}" if [ "$DB_BACKUP_DEST" = "s3" ]; then echo -e " ${DIM}Tipo : S3 (AWS)${RESET}" echo -e " ${DIM}Bucket : ${DB_S3_BUCKET}${RESET}" echo -e " ${DIM}Region : ${DB_S3_REGION}${RESET}" elif [ "$DB_BACKUP_DEST" = "s3_compatible" ]; then echo -e " ${DIM}Tipo : S3-compatible${RESET}" echo -e " ${DIM}Bucket : ${DB_S3_BUCKET}${RESET}" echo -e " ${DIM}Endpoint : ${DB_S3_ENDPOINT}${RESET}" else echo -e " ${DIM}Tipo : Local (/app/data/backups)${RESET}" fi step_ok "$step" "$total" "Instrucoes exibidas" fi # --- Cleanup --- rm -f "${TEMP_DIR}/databasus_deploy.yaml" # --- Salvar credenciais --- local db_dest_info="local" if [ "$DB_BACKUP_DEST" = "s3" ]; then db_dest_info="S3 AWS — bucket: ${DB_S3_BUCKET}" elif [ "$DB_BACKUP_DEST" = "s3_compatible" ]; then db_dest_info="S3-compatible — bucket: ${DB_S3_BUCKET} (${DB_S3_ENDPOINT})" fi save_credential_block "Databasus" \ "URL : https://${URL_DATABASUS}" \ "Config : ${DB_CONFIG_MODE:-manual} (1=auto, 2=manual)" \ "Backup destino : ${db_dest_info}" \ "PG conectado : ${PG_HOST:-nao configurado}:${PG_PORT:-5432}" # --- Resumo --- echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} DATABASUS INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_DATABASUS}${RESET}" echo -e " Backup : ${BOLD}${db_dest_info}${RESET}" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # INSTALL N8N # ============================================================================ install_n8n() { detect_installed if $N8N_OK; then msg_warn "N8N ja esta instalado nesta VPS. Abortando." echo -e " Para reinstalar: ${BOLD}docker stack rm n8n${RESET}" echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi clear show_banner echo -e " ${BOLD}${CYAN}━━━ N8N — AUTOMACAO DE WORKFLOWS ━━━${RESET}" echo "" echo -e " Plataforma no-code/low-code para automacao de processos." echo -e " Conecta o Mega com centenas de servicos e APIs externas." echo "" echo -e " ${DIM}• +400 integracoes nativas (Google, Slack, HTTP, SQL...)${RESET}" echo -e " ${DIM}• Criacao visual de fluxos sem codigo${RESET}" echo -e " ${DIM}• Automacao de atendimento, notificacoes e dados${RESET}" echo -e " ${DIM}• Self-hosted: seus dados ficam na sua VPS${RESET}" echo "" separator echo "" echo -ne " ${GREEN}Pressione ENTER para configurar...${RESET}" read section_header "CONFIGURACAO — N8N" local detected_net detected_net=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "mega_net|rede_interna|mega$" | head -1) ask_input "Dominio do n8n" "n8n.$(hostname -d 2>/dev/null || echo 'seudominio.com')" URL_N8N ask_input "Rede interna Docker" "${detected_net:-mega_net}" N8N_NET local N8N_ENCRYPTION_KEY N8N_ENCRYPTION_KEY=$(generate_secret_key | head -c 32) # PostgreSQL echo "" resolve_pg_credentials if [ -z "$PG_PASS" ]; then msg_fail "Falha ao instalar PostgreSQL automaticamente. Verifique os logs." return 1 fi N8N_PG_HOST="$PG_HOST" N8N_PG_PORT="${PG_PORT:-5432}" N8N_PG_USER="$PG_USER" N8N_PG_PASS="$PG_PASS" msg_info "Usando PostgreSQL: ${N8N_PG_HOST}:${N8N_PG_PORT} (usuario: ${N8N_PG_USER})" ask_input "Database n8n" "n8n_db" N8N_PG_DB create_pg_database "$N8N_PG_DB" local TOTAL=4 local STEP=0 section_header "INSTALANDO N8N" # Criar rede se nao existir docker network create --driver=overlay --attachable "${N8N_NET}" 2>/dev/null || true # Criar volumes STEP=$((STEP+1)) docker volume create n8n_data > /dev/null 2>&1 docker volume create n8n_redis_data > /dev/null 2>&1 step_ok "$STEP" "$TOTAL" "Volumes n8n_data + n8n_redis_data criados" # Gerar YAML inline (queue mode completo) local n8n_yaml n8n_yaml="${TEMP_DIR}/n8n_deploy.yaml" cat > "$n8n_yaml" << YAMLEOF version: "3.7" x-n8n-env: &n8n_env N8N_HOST: ${URL_N8N} N8N_PORT: "5678" N8N_PROTOCOL: https N8N_EDITOR_BASE_URL: https://${URL_N8N}/ WEBHOOK_URL: https://${URL_N8N}/ N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} GENERIC_TIMEZONE: America/Sao_Paulo TZ: America/Sao_Paulo DB_TYPE: postgresdb DB_POSTGRESDB_HOST: ${N8N_PG_HOST} DB_POSTGRESDB_PORT: "${N8N_PG_PORT}" DB_POSTGRESDB_DATABASE: ${N8N_PG_DB} DB_POSTGRESDB_USER: ${N8N_PG_USER} DB_POSTGRESDB_PASSWORD: ${N8N_PG_PASS} EXECUTIONS_MODE: queue EXECUTIONS_TIMEOUT: "3600" EXECUTIONS_TIMEOUT_MAX: "7200" N8N_CONCURRENCY_PRODUCTION_LIMIT: "10" EXECUTIONS_DATA_PRUNE: "true" EXECUTIONS_DATA_MAX_AGE: "336" EXECUTIONS_DATA_PRUNE_MAX_COUNT: "10000" QUEUE_BULL_REDIS_HOST: n8n_redis QUEUE_BULL_REDIS_PORT: "6379" QUEUE_BULL_REDIS_DB: "0" QUEUE_BULL_PREFIX: bull QUEUE_HEALTH_CHECK_ACTIVE: "true" QUEUE_HEALTH_CHECK_PORT: "5679" QUEUE_WORKER_LOCK_DURATION: "60000" QUEUE_WORKER_LOCK_RENEW_TIME: "10000" QUEUE_WORKER_STALLED_INTERVAL: "30000" N8N_EXECUTIONS_QUEUE_RECOVERY_INTERVAL: "180" N8N_EXECUTIONS_QUEUE_RECOVERY_BATCH: "100" N8N_METRICS: "false" N8N_DIAGNOSTICS_ENABLED: "false" N8N_HIRING_BANNER_ENABLED: "false" N8N_PERSONALIZATION_ENABLED: "false" N8N_TEMPLATES_ENABLED: "true" services: n8n: image: n8nio/n8n:latest environment: <<: *n8n_env volumes: - n8n_data:/home/node/.n8n networks: - ${N8N_NET} deploy: mode: replicated replicas: 1 placement: constraints: - node.role == manager resources: limits: cpus: "2" memory: 2048M reservations: cpus: "0.5" memory: 512M labels: - traefik.enable=true - traefik.http.routers.n8n.rule=Host(\`${URL_N8N}\`) - traefik.http.routers.n8n.entrypoints=websecure - traefik.http.routers.n8n.tls.certresolver=letsencryptresolver - traefik.http.services.n8n.loadbalancer.server.port=5678 - traefik.http.routers.n8n.service=n8n - traefik.docker.network=${N8N_NET} n8n_worker: image: n8nio/n8n:latest command: worker environment: <<: *n8n_env volumes: - n8n_data:/home/node/.n8n networks: - ${N8N_NET} deploy: mode: replicated replicas: 2 placement: constraints: - node.role == manager resources: limits: cpus: "2" memory: 2048M reservations: cpus: "0.5" memory: 512M restart_policy: condition: on-failure delay: 10s max_attempts: 5 n8n_webhook: image: n8nio/n8n:latest command: webhook environment: <<: *n8n_env volumes: - n8n_data:/home/node/.n8n networks: - ${N8N_NET} deploy: mode: replicated replicas: 1 placement: constraints: - node.role == manager resources: limits: cpus: "1" memory: 1024M reservations: cpus: "0.25" memory: 256M restart_policy: condition: on-failure delay: 10s max_attempts: 5 n8n_redis: image: redis:7.4.2 command: [ "redis-server", "--appendonly", "yes", "--appendfsync", "everysec", "--port", "6379", "--maxmemory", "512mb", "--maxmemory-policy", "noeviction", "--save", "900", "1", "--save", "300", "10", "--save", "60", "10000", "--hz", "50", "--tcp-keepalive", "300" ] volumes: - n8n_redis_data:/data networks: - ${N8N_NET} deploy: mode: replicated replicas: 1 placement: constraints: - node.role == manager resources: limits: cpus: "1" memory: 768M reservations: cpus: "0.25" memory: 256M restart_policy: condition: on-failure delay: 5s max_attempts: 5 volumes: n8n_data: external: true n8n_redis_data: networks: ${N8N_NET}: external: true name: ${N8N_NET} YAMLEOF STEP=$((STEP+1)) step_ok "$STEP" "$TOTAL" "YAML gerado" # Deploy STEP=$((STEP+1)) deploy_via_portainer n8n "$n8n_yaml" if [ $? -eq 0 ]; then step_ok "$STEP" "$TOTAL" "Stack n8n deployada" else step_fail "$STEP" "$TOTAL" "Falha ao deployar stack n8n" return 1 fi # Health check STEP=$((STEP+1)) wait_service "n8n_n8n" 60 wait_service "n8n_n8n_worker" 60 || true wait_service "n8n_n8n_redis" 60 || true step_ok "$STEP" "$TOTAL" "n8n + workers + redis iniciados" # Salvar credenciais save_credential_block "N8N" \ "URL : https://${URL_N8N}" \ "Encryption Key : ${N8N_ENCRYPTION_KEY}" \ "Database : ${N8N_PG_DB}" \ "Modo : queue (workers separados)" \ "Workers : 2 replicas" \ "Webhook : 1 replica" \ "Redis : n8n_redis:6379 (Bull queue)" # Exibir resumo echo "" echo -e "${GREEN}===============================================${RESET}" echo -e "${GREEN} N8N INSTALADO COM SUCESSO!${RESET}" echo -e "${GREEN}===============================================${RESET}" echo -e " URL : ${BOLD}https://${URL_N8N}${RESET}" echo -e " Acesso : configurar no primeiro acesso" echo -e "${GREEN}===============================================${RESET}" echo "" echo -e " Credenciais salvas em: ${CREDENTIALS_FILE}" detect_installed } # ============================================================================ # WIZARD — INSTALAR TUDO (jornada guiada de 5 passos) # ============================================================================ wizard_install_all() { WIZARD_MODE=true local VPS_IP VPS_IP=$(get_vps_ip) # Detectar o que já está instalado detect_installed # ══════════════════════════════════════════════════ # PASSO 1 DE 5 — ESCOLHA DOS SERVICOS # ══════════════════════════════════════════════════ clear show_banner echo -e " ${BOLD}${WHITE}PASSO 1 DE 5 — ESCOLHA DOS SERVICOS${RESET}" echo "" echo -e " ${DIM}Marque quais servicos deseja instalar. Servicos ja${RESET}" echo -e " ${DIM}instalados sao detectados automaticamente e pulados.${RESET}" echo "" # Mega local install_mega_flag=false if $MEGA_OK; then echo -e " ${GREEN}[ja instalado]${RESET} Mega" else if ask_confirm " Instalar Mega (CRM/chat)?" "s"; then install_mega_flag=true fi fi # WhatsApp local install_waha_flag=false local install_fzap_flag=false local install_evo_flag=false echo "" echo -e " ${BOLD}Provedor WhatsApp:${RESET}" echo "" if ask_confirm " Instalar WAHA?" "s"; then install_waha_flag=true fi if ask_confirm " Instalar Evolution v2?" "n"; then install_evo_flag=true fi if ask_confirm " Instalar FZAP?" "n"; then install_fzap_flag=true fi # N8N local install_n8n_flag=false echo "" if $N8N_OK; then echo -e " ${GREEN}[ja instalado]${RESET} N8N" install_n8n_flag=false else if ask_confirm " Instalar N8N (automacoes)?" "s"; then install_n8n_flag=true fi fi # Databasus local install_databasus_flag=false echo "" if $DATABASUS_OK; then echo -e " ${GREEN}[ja instalado]${RESET} Databasus" else if ask_confirm " Instalar Databasus (backup BD)?" "s"; then install_databasus_flag=true fi fi # ══════════════════════════════════════════════════ # PASSO 2 DE 5 — DOMINIOS # ══════════════════════════════════════════════════ clear show_banner echo -e " ${BOLD}${WHITE}PASSO 2 DE 5 — SEUS DOMINIOS${RESET}" echo "" echo -e " ${WHITE}Informe os dominios que voce ja configurou no DNS.${RESET}" if [ -n "$VPS_IP" ]; then echo -e " ${DIM}Todos devem ter registro A apontando para: ${BOLD}${VPS_IP}${RESET}" fi echo "" # Portainer — pre-preencher se já instalado local portainer_default="" if [ -f "$CREDENTIALS_FILE" ]; then portainer_default=$(grep -A5 "\[Portainer\]" "$CREDENTIALS_FILE" 2>/dev/null | grep "URL" | sed 's/.*: https:\/\///' | tr -d ' \r' | head -1) fi if $TRAEFIK_OK && $PORTAINER_OK && [ -n "$portainer_default" ]; then echo -e " ${GREEN}Portainer ja instalado:${RESET} ${BOLD}https://${portainer_default}${RESET}" URL_PORTAINER="$portainer_default" else ask_input "Portainer (ex: painel.seudominio.com)" "$portainer_default" "URL_PORTAINER" fi if $install_mega_flag; then ask_input "Mega (ex: app.seudominio.com)" "" "URL_MEGA" fi if $install_waha_flag; then ask_input "WAHA (ex: waha.seudominio.com)" "" "URL_WAHA" fi if $install_fzap_flag; then ask_input "FZAP (ex: fzap.seudominio.com)" "" "URL_FZAP" fi if $install_evo_flag; then ask_input "Evolution (ex: evolution.seudominio.com)" "" "URL_EVOLUTION" fi if $install_databasus_flag; then ask_input "Databasus / Backup (ex: backup.seudominio.com)" "" "URL_DATABASUS" fi if $install_n8n_flag; then ask_input "N8N (ex: n8n.seudominio.com)" "" "URL_N8N" fi # --- Validar DNS --- echo "" msg_info "Validando DNS dos dominios..." echo "" local all_domains_ok=true local domains_to_check=() local domain_labels=() [ -n "$URL_PORTAINER" ] && domains_to_check+=("$URL_PORTAINER") && domain_labels+=("Portainer") $install_mega_flag && [ -n "$URL_MEGA" ] && domains_to_check+=("$URL_MEGA") && domain_labels+=("Mega") $install_waha_flag && [ -n "$URL_WAHA" ] && domains_to_check+=("$URL_WAHA") && domain_labels+=("WAHA") $install_fzap_flag && [ -n "$URL_FZAP" ] && domains_to_check+=("$URL_FZAP") && domain_labels+=("FZAP") $install_evo_flag && [ -n "$URL_EVOLUTION" ] && domains_to_check+=("$URL_EVOLUTION") && domain_labels+=("Evolution") $install_databasus_flag && [ -n "$URL_DATABASUS" ] && domains_to_check+=("$URL_DATABASUS") && domain_labels+=("Databasus") $install_n8n_flag && [ -n "$URL_N8N" ] && domains_to_check+=("$URL_N8N") && domain_labels+=("N8N") for i in "${!domains_to_check[@]}"; do local domain="${domains_to_check[$i]}" local label="${domain_labels[$i]}" if [ -z "$domain" ]; then continue fi local result result=$(validate_domain "$domain" "$VPS_IP") case "$result" in OK) echo -e " ${GREEN}✓${RESET} ${label}: ${domain} ${GREEN}→ ${VPS_IP}${RESET}" ;; NOT_RESOLVED) echo -e " ${RED}✗${RESET} ${label}: ${domain} ${RED}→ nao resolve!${RESET}" all_domains_ok=false ;; WRONG_IP:*) local wrong_ip="${result#WRONG_IP:}" echo -e " ${YELLOW}!${RESET} ${label}: ${domain} → ${wrong_ip} ${YELLOW}(esperado: ${VPS_IP})${RESET}" all_domains_ok=false ;; esac done if ! $all_domains_ok; then echo "" echo -e " ${YELLOW}Alguns dominios nao apontam para este servidor.${RESET}" echo -e " ${DIM}O certificado SSL pode falhar se o DNS nao estiver correto.${RESET}" echo "" if ! ask_confirm "Continuar mesmo assim?" "n"; then WIZARD_MODE=false return 1 fi else echo "" msg_ok "Todos os dominios apontam corretamente!" fi echo "" echo -ne " ${GREEN}Pressione ENTER para continuar...${RESET}" read # ══════════════════════════════════════════════════ # PASSO 3 DE 5 — E-MAIL E NOTIFICACOES # ══════════════════════════════════════════════════ clear show_banner echo -e " ${BOLD}${WHITE}PASSO 3 DE 5 — E-MAIL E NOTIFICACOES${RESET}" echo "" echo -e " ${YELLOW}Certificado SSL (Let's Encrypt)${RESET}" echo -e " ${DIM}Um e-mail valido para registrar os certificados HTTPS.${RESET}" echo "" ask_input "E-mail para SSL" "" "EMAIL_SSL" echo "" echo -e " ${YELLOW}SMTP — Envio de e-mails pelo Mega${RESET}" echo -e " ${DIM}O Mega envia notificacoes, convites e alertas por e-mail.${RESET}" echo -e " ${DIM}Se usar Gmail: ative 'Senhas de app' em myaccount.google.com${RESET}" echo "" ask_input "Host SMTP (ex: smtp.gmail.com)" "" "SMTP_HOST" ask_input "Porta SMTP" "587" "SMTP_PORT" ask_input "Usuario SMTP (geralmente o e-mail)" "" "SMTP_USER" ask_input "Senha SMTP" "" "SMTP_PASS" "true" ask_input "E-mail remetente (ex: noreply@seudominio.com)" "" "SMTP_EMAIL" # Auto-derivar dominio do e-mail do remetente if [ -n "$SMTP_EMAIL" ]; then SMTP_DOMAIN=$(echo "$SMTP_EMAIL" | sed 's/.*@//') fi echo "" echo -ne " ${GREEN}Pressione ENTER para continuar...${RESET}" read # ══════════════════════════════════════════════════ # PASSO 4 DE 5 — ARMAZENAMENTO DE ARQUIVOS # ══════════════════════════════════════════════════ clear show_banner echo -e " ${BOLD}${WHITE}PASSO 4 DE 5 — ARMAZENAMENTO DE ARQUIVOS${RESET}" echo "" echo -e " ${WHITE}O Mega e os provedores WhatsApp armazenam imagens, audios${RESET}" echo -e " ${WHITE}e documentos das conversas. Escolha onde guardar:${RESET}" echo "" echo -e " ${GREEN}1)${RESET} Local ${DIM}(dentro do servidor — mais simples, sem custo extra)${RESET}" echo -e " ${GREEN}2)${RESET} Amazon S3" echo -e " ${GREEN}3)${RESET} S3-compatible ${DIM}(Cloudflare R2, DigitalOcean Spaces, etc.)${RESET}" echo "" echo -ne " ${CYAN}Escolha${RESET} [1]: " local storage_choice read storage_choice storage_choice="${storage_choice:-1}" MEGA_STORAGE_CHOICE="$storage_choice" if [ "$storage_choice" = "2" ] || [ "$storage_choice" = "3" ]; then echo "" echo -e " ${BOLD}Credenciais S3${RESET}" echo -e " ${DIM}Estas credenciais serao usadas por todos os servicos.${RESET}" echo "" local S3_BUCKET S3_ACCESS_KEY S3_SECRET_KEY S3_REGION S3_ENDPOINT ask_input "Bucket name" "" "S3_BUCKET" ask_input "Access Key ID" "" "S3_ACCESS_KEY" ask_input "Secret Access Key" "" "S3_SECRET_KEY" "true" if [ "$storage_choice" = "2" ]; then ask_input "Region" "us-east-1" "S3_REGION" else ask_input "Region" "auto" "S3_REGION" ask_input "Endpoint URL (ex: https://ACCOUNT.r2.cloudflarestorage.com)" "" "S3_ENDPOINT" fi # Propagar para todos os servicos MEGA_S3_BUCKET="$S3_BUCKET" MEGA_S3_ACCESS_KEY="$S3_ACCESS_KEY" MEGA_S3_SECRET_KEY="$S3_SECRET_KEY" MEGA_S3_REGION="$S3_REGION" MEGA_S3_ENDPOINT="${S3_ENDPOINT:-}" WAHA_S3_BUCKET="$S3_BUCKET" WAHA_S3_ACCESS_KEY="$S3_ACCESS_KEY" WAHA_S3_SECRET_KEY="$S3_SECRET_KEY" WAHA_S3_REGION="$S3_REGION" WAHA_S3_ENDPOINT="${S3_ENDPOINT:-}" DB_S3_BUCKET="$S3_BUCKET" DB_S3_ACCESS_KEY="$S3_ACCESS_KEY" DB_S3_SECRET_KEY="$S3_SECRET_KEY" DB_S3_REGION="$S3_REGION" DB_S3_ENDPOINT="${S3_ENDPOINT:-}" fi # ══════════════════════════════════════════════════ # PASSO 5 DE 5 — REVISAR E INSTALAR # ══════════════════════════════════════════════════ clear show_banner echo -e " ${BOLD}${WHITE}PASSO 5 DE 5 — REVISAR E INSTALAR${RESET}" echo "" separator echo "" echo -e " ${BOLD}${WHITE}SERVICOS:${RESET}" echo "" echo -e " ${GREEN}●${RESET} Traefik + Portainer ${DIM}${URL_PORTAINER}${RESET}" echo -e " ${GREEN}●${RESET} PostgreSQL + PgBouncer" echo -e " ${GREEN}●${RESET} Redis" $install_mega_flag && echo -e " ${GREEN}●${RESET} Mega ${DIM}${URL_MEGA}${RESET}" $install_waha_flag && echo -e " ${GREEN}●${RESET} WAHA ${DIM}${URL_WAHA}${RESET}" $install_fzap_flag && echo -e " ${GREEN}●${RESET} FZAP ${DIM}${URL_FZAP}${RESET}" $install_evo_flag && echo -e " ${GREEN}●${RESET} Evolution v2 ${DIM}${URL_EVOLUTION}${RESET}" $install_databasus_flag && echo -e " ${GREEN}●${RESET} Databasus ${DIM}${URL_DATABASUS}${RESET}" $install_n8n_flag && echo -e " ${GREEN}●${RESET} N8N ${DIM}${URL_N8N}${RESET}" echo "" echo -e " ${BOLD}${WHITE}CONFIGURACOES:${RESET}" echo "" echo -e " ${CYAN}SMTP:${RESET} ${SMTP_HOST}:${SMTP_PORT} (${SMTP_EMAIL})" local storage_label="Local (servidor)" [ "$storage_choice" = "2" ] && storage_label="Amazon S3 (${S3_BUCKET})" [ "$storage_choice" = "3" ] && storage_label="S3-compatible (${S3_BUCKET})" echo -e " ${CYAN}Storage:${RESET} ${storage_label}" echo -e " ${CYAN}SSL:${RESET} ${EMAIL_SSL}" echo "" separator echo "" echo -e " ${DIM}Senhas, API keys e tokens serao gerados automaticamente${RESET}" echo -e " ${DIM}e salvos em: ${CREDENTIALS_FILE}${RESET}" echo "" if ! ask_confirm "Iniciar instalacao?" "s"; then WIZARD_MODE=false return 1 fi # ══════════════════════════════════════════════════ # PRE-SETAR DEFAULTS (o usuario nao precisa saber) # ══════════════════════════════════════════════════ REDE_INTERNA="${REDE_INTERNA:-mega_net}" NOME_MEGA="${NOME_MEGA:-MegaChat}" PG_USER="${PG_USER:-postgres}" PG_PORT="${PG_PORT:-5432}" PGBOUNCER_PORT="${PGBOUNCER_PORT:-6432}" PGBOUNCER_MAX_CONN="${PGBOUNCER_MAX_CONN:-100}" MEGA_PG_DB="${MEGA_PG_DB:-chatwoot_db}" WAHA_DB="${WAHA_DB:-waha_db}" FZAP_DB="${FZAP_DB:-fzap_db}" EVO_PG_DB="${EVO_PG_DB:-evolution_db}" EVO_CW_PG_DB="${EVO_CW_PG_DB:-chatwoot_db}" N8N_PG_DB="${N8N_PG_DB:-n8n_db}" N8N_NET="${REDE_INTERNA}" DB_CONFIG_MODE="1" if [ "$storage_choice" = "2" ]; then DB_BACKUP_DEST="s3" elif [ "$storage_choice" = "3" ]; then DB_BACKUP_DEST="s3_compatible" else DB_BACKUP_DEST="local" fi # Pre-gerar API keys para integracao cruzada Mega↔WhatsApp if $install_waha_flag; then WAHA_API_KEY="${WAHA_API_KEY:-$(generate_api_key)}" MEGA_WAHA_URL="${URL_WAHA}" MEGA_WAHA_TOKEN="${WAHA_API_KEY}" fi if $install_evo_flag; then EVOLUTION_API_KEY="${EVOLUTION_API_KEY:-$(generate_api_key)}" MEGA_EVO_URL="${URL_EVOLUTION}" MEGA_EVO_TOKEN="${EVOLUTION_API_KEY}" fi # Storage S3 para WAHA (mesmas credenciais) if $install_waha_flag && [ "$storage_choice" != "1" ]; then WAHA_STORAGE_CONFIGURED="true" fi # ══════════════════════════════════════════════════ # EXECUTAR INSTALACAO # ══════════════════════════════════════════════════ echo "" echo -e " ${BOLD}${MAGENTA}═══ INICIANDO INSTALACAO COMPLETA ═══${RESET}" echo "" local wizard_error=false # 1. Traefik + Portainer if ! $TRAEFIK_OK || ! $PORTAINER_OK; then install_traefik_portainer || { msg_fail "Traefik/Portainer falhou — abortando."; wizard_error=true; } detect_installed else msg_info "Traefik + Portainer ja instalados, pulando..." fi if $wizard_error; then WIZARD_MODE=false return 1 fi # 2. PostgreSQL if ! $POSTGRES_OK; then install_postgres || { msg_fail "PostgreSQL falhou — abortando. Sem banco, nenhum servico funcionara."; WIZARD_MODE=false; return 1; } else msg_info "PostgreSQL ja instalado, pulando..." resolve_pg_credentials fi # 3. Redis if ! $REDIS_OK; then install_redis || { msg_fail "Redis falhou — abortando."; WIZARD_MODE=false; return 1; } else msg_info "Redis ja instalado, pulando..." resolve_redis_credentials fi # 4. Mega if $install_mega_flag && ! $MEGA_OK; then install_mega || msg_warn "Mega falhou — continuando com os demais servicos..." elif $MEGA_OK; then msg_info "Mega ja instalado, pulando..." fi # 5. WhatsApp providers if $install_waha_flag && ! $WAHA_OK; then install_waha || msg_warn "WAHA falhou — continuando..." fi if $install_fzap_flag && ! $FZAP_OK; then install_fzap || msg_warn "FZAP falhou — continuando..." fi if $install_evo_flag && ! $EVOLUTION_OK; then install_evolution || msg_warn "Evolution falhou — continuando..." fi # 6. Databasus if $install_databasus_flag && ! $DATABASUS_OK; then install_databasus || msg_warn "Databasus falhou — continuando..." elif $DATABASUS_OK; then msg_info "Databasus ja instalado, pulando..." fi # 7. N8N if $install_n8n_flag && ! $N8N_OK; then install_n8n || msg_warn "N8N falhou — continuando..." elif $N8N_OK; then msg_info "N8N ja instalado, pulando..." fi # ══════════════════════════════════════════════════ # RESULTADO FINAL # ══════════════════════════════════════════════════ echo "" echo -e "${GREEN}════════════════════════════════════════════════════════${RESET}" echo -e "${GREEN} INSTALACAO COMPLETA FINALIZADA!${RESET}" echo -e "${GREEN}════════════════════════════════════════════════════════${RESET}" echo "" show_credentials_summary echo "" echo -e "${GREEN}════════════════════════════════════════════════════════${RESET}" echo -e " ${BOLD}MANTENHA ESTE ARQUIVO EM LOCAL SEGURO!${RESET}" echo -e " Salvo em: ${CREDENTIALS_FILE}" echo -e "${GREEN}════════════════════════════════════════════════════════${RESET}" WIZARD_MODE=false } # ============================================================================ # MENU INFRA (Caminho A - infra nao instalada) # ============================================================================ show_menu_infra() { while true; do show_banner detect_installed echo -e " ${YELLOW}${BOLD}ATENCAO: Infraestrutura base nao detectada!${RESET}" echo "" echo -e " ${WHITE}Traefik e Portainer sao necessarios antes de instalar${RESET}" echo -e " ${WHITE}qualquer outra stack.${RESET}" echo "" separator echo "" echo -e " ${BOLD}${WHITE}O que deseja fazer?${RESET}" echo "" echo -e " ${GREEN}1)${RESET} Instalar Traefik + Portainer (infra base)" echo -e " ${RED}0)${RESET} Sair" echo "" echo -ne " ${CYAN}Escolha uma opcao${RESET}: " local choice read choice case "$choice" in 1) install_traefik_portainer detect_installed if $TRAEFIK_OK && $PORTAINER_OK; then msg_ok "Infraestrutura base instalada! Voltando ao menu principal..." sleep 2 return 0 fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 0) echo "" msg_info "Saindo do Mega Deploy. Ate a proxima!" echo "" finalize_credentials_file exit 0 ;; *) msg_warn "Opcao invalida!" sleep 1 ;; esac done } # ============================================================================ # VERIFICAR ATUALIZACOES DAS STACKS (upstream vs local) # ============================================================================ check_stack_updates() { section_header "VERIFICAR ATUALIZACOES DAS STACKS" local UPSTREAM_REPO="megaapp977/stack" local OMNIFORGE_REPO="jessefreitas/setup_omniforge" local UPSTREAM_RAW="https://raw.githubusercontent.com/${UPSTREAM_REPO}/main" local OMNIFORGE_RAW="https://raw.githubusercontent.com/${OMNIFORGE_REPO}/main" # Mapeamento dos arquivos local stacks=("traefik" "portainer" "evolution" "waha" "mega" "mega_sidekiq") local up_paths=("traefik/traefik.yaml" "portainer/portainer.yaml" "evolution/evolution.yml" "waha/waha.yml" "mega-docker/mega-orion.yml" "mega-docker/mega-sidekiq-split.yml") local of_paths=("traefik.yaml" "portainer.yaml" "evolution.yml" "waha.yml" "mega-orion.yml" "mega-sidekiq-split.yml") msg_info "Comparando repos upstream e OmniForge..." echo -e " ${DIM}Upstream : ${UPSTREAM_REPO}${RESET}" echo -e " ${DIM}OmniForge: ${OMNIFORGE_REPO}${RESET}" echo "" local synced=0 divergent=0 errors=0 for i in "${!stacks[@]}"; do local stack="${stacks[$i]}" local up_url="${UPSTREAM_RAW}/${up_paths[$i]}" local of_url="${OMNIFORGE_RAW}/${of_paths[$i]}" # Buscar SHA256 de ambos via Python local result result=$(python3 << PYEOF import urllib.request, ssl, hashlib, re, sys ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE def fetch_info(url): try: req = urllib.request.Request(url, headers={"User-Agent": "sync/1.0"}) with urllib.request.urlopen(req, context=ctx, timeout=10) as r: content = r.read() sha = hashlib.sha256(content).hexdigest() imgs = sorted(set(re.findall(r'image:\s*(\S+)', content.decode('utf-8', errors='replace')))) return sha, imgs, len(content) except: return None, [], 0 up_sha, up_imgs, up_size = fetch_info("${up_url}") of_sha, of_imgs, of_size = fetch_info("${of_url}") if up_sha is None and of_sha is None: print("ERROR||ambos falharam") elif up_sha is None: print("ERROR||upstream nao encontrado") elif of_sha is None: print("ERROR||omniforge nao encontrado") elif up_sha == of_sha: print(f"SYNCED||{up_sha[:12]}") else: # Comparar imagens up_img_str = ", ".join(up_imgs) if up_imgs else "-" of_img_str = ", ".join(of_imgs) if of_imgs else "-" if up_imgs != of_imgs: print(f"VERSION||UP: {up_img_str}||OF: {of_img_str}") else: print(f"CONFIG||{abs(up_size - of_size)} bytes diff") PYEOF ) local status=$(echo "$result" | cut -d'|' -f1) local detail=$(echo "$result" | sed 's/^[^|]*||//') case "$status" in SYNCED) echo -e " ${GREEN}[=]${RESET} ${stack}: sincronizado (${detail})" synced=$((synced + 1)) ;; VERSION) echo -e " ${YELLOW}[!]${RESET} ${stack}: ${BOLD}versao diferente${RESET}" local up_ver=$(echo "$detail" | cut -d'|' -f1) local of_ver=$(echo "$detail" | sed 's/^.*||//') echo -e " ${DIM}${up_ver}${RESET}" echo -e " ${DIM}${of_ver}${RESET}" divergent=$((divergent + 1)) ;; CONFIG) echo -e " ${YELLOW}[~]${RESET} ${stack}: config diferente (${detail})" divergent=$((divergent + 1)) ;; ERROR) echo -e " ${RED}[x]${RESET} ${stack}: ${detail}" errors=$((errors + 1)) ;; esac done echo "" separator echo -e " Resultado: ${GREEN}${synced} sync${RESET} | ${YELLOW}${divergent} divergentes${RESET} | ${RED}${errors} erros${RESET}" if [ $divergent -gt 0 ]; then echo "" echo -e " ${YELLOW}Existem atualizacoes disponiveis nos repos!${RESET}" echo -e " ${DIM}Verifique: https://github.com/${UPSTREAM_REPO}${RESET}" echo -e " ${DIM}Fork: https://github.com/${OMNIFORGE_REPO}${RESET}" fi echo "" echo -e " ${YELLOW}Pressione ENTER para voltar...${RESET}" read } # ============================================================================ # MENU PRINCIPAL (Caminho B - infra instalada) # ============================================================================ show_menu_main() { while true; do show_banner detect_installed show_status echo -e " ${BOLD}${WHITE}MENU PRINCIPAL${RESET}" echo "" # Instalar TUDO echo -e " ${BOLD}${GREEN}1)${RESET} ${BOLD}Instalar TUDO${RESET} ${DIM}(wizard guiado)${RESET}" echo "" # Mega if $MEGA_OK; then echo -e " ${GREEN}2)${RESET} Mega ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}2)${RESET} Mega ${DIM}[Nao instalado]${RESET}" fi # WAHA if $WAHA_OK; then echo -e " ${GREEN}3)${RESET} WAHA ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}3)${RESET} WAHA ${DIM}[Nao instalado]${RESET}" fi # FZAP if $FZAP_OK; then echo -e " ${GREEN}4)${RESET} FZAP ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}4)${RESET} FZAP ${DIM}[Nao instalado]${RESET}" fi # Evolution if $EVOLUTION_OK; then echo -e " ${GREEN}5)${RESET} Evolution API v2 ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}5)${RESET} Evolution API v2 ${DIM}[Nao instalado]${RESET}" fi # N8N if $N8N_OK; then echo -e " ${GREEN}6)${RESET} N8N ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}6)${RESET} N8N ${DIM}[Nao instalado]${RESET}" fi echo "" # PostgreSQL if $POSTGRES_OK; then echo -e " ${GREEN}7)${RESET} PostgreSQL ${GREEN}[Instalado]${RESET}" echo -e " ${GREEN}8)${RESET} PostgreSQL + PgBouncer ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}7)${RESET} PostgreSQL ${DIM}[Nao instalado]${RESET}" echo -e " ${GREEN}8)${RESET} PostgreSQL + PgBouncer ${DIM}[Nao instalado]${RESET}" fi # Redis if $REDIS_OK; then echo -e " ${GREEN}9)${RESET} Redis ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}9)${RESET} Redis ${DIM}[Nao instalado]${RESET}" fi # Databasus if $DATABASUS_OK; then echo -e " ${GREEN}10)${RESET} Databasus ${GREEN}[Instalado]${RESET}" else echo -e " ${GREEN}10)${RESET} Databasus ${DIM}[Nao instalado]${RESET}" fi echo "" separator echo -e " ${GREEN}11)${RESET} Reinstalar Traefik + Portainer" echo -e " ${GREEN}12)${RESET} Ver credenciais salvas" echo -e " ${YELLOW}13)${RESET} Atualizar stack" echo -e " ${RED}14)${RESET} Deletar stack" echo -e " ${CYAN}15)${RESET} Verificar atualizacoes" echo -e " ${YELLOW}16)${RESET} Rollback stack" echo -e " ${RED}0)${RESET} Sair" echo "" echo -ne " ${CYAN}Escolha uma opcao${RESET}: " local choice read choice case "$choice" in 1) wizard_install_all echo "" echo -e " ${YELLOW}Pressione ENTER para voltar ao menu...${RESET}" read ;; 2) if $MEGA_OK; then echo "" msg_warn "Mega ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_mega fi else install_mega fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 3) if $WAHA_OK; then echo "" msg_warn "WAHA ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_waha fi else install_waha fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 4) if $FZAP_OK; then echo "" msg_warn "FZAP ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_fzap fi else install_fzap fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 5) if $EVOLUTION_OK; then echo "" msg_warn "Evolution API v2 ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_evolution fi else install_evolution fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 6) if $N8N_OK; then echo "" msg_warn "N8N ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_n8n fi else install_n8n fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 7) if $POSTGRES_OK; then echo "" msg_warn "PostgreSQL ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then SKIP_PGBOUNCER=true install_postgres fi else SKIP_PGBOUNCER=true install_postgres fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 8) if $POSTGRES_OK; then echo "" msg_warn "PostgreSQL ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_postgres fi else install_postgres fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 9) if $REDIS_OK; then echo "" msg_warn "Redis ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_redis fi else install_redis fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 10) if $DATABASUS_OK; then echo "" msg_warn "Databasus ja esta instalado!" if ask_confirm "Deseja reinstalar?" "n"; then install_databasus fi else install_databasus fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 11) if ask_confirm "Reinstalar Traefik + Portainer?" "n"; then install_traefik_portainer fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 12) echo "" if [ -f "$CREDENTIALS_FILE" ]; then separator cat "$CREDENTIALS_FILE" separator else msg_warn "Nenhuma credencial salva ainda." fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read ;; 13) update_stack ;; 14) delete_stack ;; 15) check_stack_updates ;; 16) rollback_stack ;; 0) echo "" finalize_credentials_file show_credentials_summary echo -e " ${GREEN}Obrigado por usar o Mega Deploy!${RESET}" echo "" exit 0 ;; *) msg_warn "Opcao invalida!" sleep 1 ;; esac done } # ============================================================================ # DELETE STACK # ============================================================================ delete_stack() { section_header "DELETAR STACK" echo -e " ${RED}${BOLD}ATENCAO: Esta acao remove permanentemente a stack e todos os dados!${RESET}" echo "" # Pedir senha root para confirmar local root_pass echo -ne " ${CYAN}Senha do root para confirmar:${RESET} " read -s root_pass echo "" # Verificar senha if ! echo "$root_pass" | su -c "true" root 2>/dev/null; then msg_fail "Senha incorreta. Operacao cancelada." echo "" echo -e " ${YELLOW}Pressione ENTER para voltar...${RESET}" read return 1 fi msg_ok "Autenticado." echo "" echo -e " ${BOLD}Qual stack deseja deletar?${RESET}" echo "" echo -e " ${RED}1)${RESET} Traefik + Portainer (infra base)" echo -e " ${RED}2)${RESET} PostgreSQL + PgBouncer" echo -e " ${RED}3)${RESET} Redis" echo -e " ${RED}4)${RESET} Mega" echo -e " ${RED}5)${RESET} WAHA" echo -e " ${RED}6)${RESET} Evolution API v2" echo -e " ${RED}7)${RESET} Databasus" echo -e " ${RED}8)${RESET} N8N" echo -e " ${RED}9)${RESET} TUDO (limpar VPS inteira)" echo -e " ${GREEN}0)${RESET} Cancelar" echo "" echo -ne " ${CYAN}Escolha uma opcao${RESET}: " local dchoice read dchoice local stacks_to_rm="" local label="" case "$dchoice" in 1) stacks_to_rm="traefik portainer"; label="Traefik + Portainer" ;; 2) stacks_to_rm="postgres"; label="PostgreSQL + PgBouncer" ;; 3) stacks_to_rm="redis"; label="Redis" ;; 4) stacks_to_rm="mega chatwoot"; label="Mega" ;; 5) stacks_to_rm="waha"; label="WAHA" ;; 6) stacks_to_rm="evolution"; label="Evolution API v2" ;; 7) stacks_to_rm="databasus"; label="Databasus" ;; 8) stacks_to_rm="n8n"; label="N8N" ;; 9) stacks_to_rm="traefik portainer postgres redis mega chatwoot waha evolution databasus n8n fzap"; label="TODAS AS STACKS" ;; 0) return 0 ;; *) msg_warn "Opcao invalida."; return 1 ;; esac echo "" echo -e " ${RED}${BOLD}Confirme: deletar ${label}? Esta acao nao pode ser desfeita!${RESET}" if ! ask_confirm "Confirmar delecao de ${label}?" "n"; then msg_info "Operacao cancelada." echo "" echo -e " ${YELLOW}Pressione ENTER para voltar...${RESET}" read return 0 fi echo "" for stack in $stacks_to_rm; do if docker stack ls 2>/dev/null | grep -q "^${stack} "; then docker stack rm "$stack" 2>/dev/null && msg_ok "Stack '${stack}' removida." || msg_warn "Falha ao remover '${stack}'." else msg_info "Stack '${stack}' nao encontrada, pulando." fi done # Se remocao total, limpar volumes e redes if [ "$dchoice" = "9" ]; then echo "" msg_info "Aguardando containers encerrarem..." sleep 10 # Remover volumes conhecidos (incluindo portainer_data que persiste senha) for vol in volume_swarm_shared volume_swarm_certificates portainer_data mega_data mega_redis_data postgres_data n8n_data; do docker volume rm "$vol" 2>/dev/null && msg_ok "Volume '${vol}' removido." || true done # Remover redes overlay customizadas for net in mega_net eazychat_net; do docker network rm "$net" 2>/dev/null && msg_ok "Rede '${net}' removida." || true done # Remover arquivo de credenciais rm -f "$CREDENTIALS_FILE" && msg_ok "Arquivo de credenciais removido." || true msg_ok "Limpeza total concluida. VPS pronta para nova instalacao." fi echo "" msg_ok "Delecao concluida." echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read } # ============================================================================ # UPDATE STACK # ============================================================================ warn_non_persistent_volumes() { local stack_name="$1" local non_persistent="" # Verificar cada servico da stack for svc in $(docker service ls --format '{{.Name}}' 2>/dev/null | grep "^${stack_name}_"); do local mounts mounts=$(docker service inspect "$svc" --format '{{range .Spec.TaskTemplate.ContainerSpec.Mounts}}{{.Type}}:{{.Source}}:{{.Target}} {{end}}' 2>/dev/null) for mount in $mounts; do local mtype msrc mtype=$(echo "$mount" | cut -d: -f1) msrc=$(echo "$mount" | cut -d: -f2) # Bind sem path absoluto ou tmpfs = nao persistente if [ "$mtype" = "tmpfs" ]; then non_persistent="${non_persistent}\n ${RED}●${RESET} ${svc}: tmpfs (dados perdidos no restart)" elif [ "$mtype" = "bind" ] && [ -z "$msrc" ]; then non_persistent="${non_persistent}\n ${RED}●${RESET} ${svc}: bind sem origem (dados nao persistidos)" fi done done if [ -n "$non_persistent" ]; then echo "" echo -e " ${YELLOW}${BOLD}⚠ AVISO: Volumes nao-persistentes detectados!${RESET}" echo -e " ${YELLOW}Os dados abaixo serao PERDIDOS apos o update:${RESET}" echo -e "$non_persistent" echo "" if ! ask_confirm "Continuar mesmo assim?" "n"; then msg_info "Update cancelado." return 1 fi fi return 0 } # ============================================================================ # ROLLBACK STACK # ============================================================================ rollback_stack() { section_header "ROLLBACK DE STACK" echo -e " ${BOLD}Qual stack deseja fazer rollback?${RESET}" echo -e " ${DIM}(volta para a versao anterior da imagem Docker)${RESET}" echo "" # Listar stacks ativas com imagem atual local stacks_found=0 local -a stack_names=() local -a stack_services=() for stack_name in traefik portainer postgres redis mega waha evolution databasus n8n; do local services services=$(docker service ls --format '{{.Name}}' 2>/dev/null | grep "^${stack_name}_" | head -5) if [ -n "$services" ]; then stacks_found=$((stacks_found + 1)) stack_names+=("$stack_name") echo -e " ${GREEN}${stacks_found})${RESET} ${stack_name}" # Mostrar imagem atual local first_svc first_svc=$(echo "$services" | head -1) local cur_img cur_img=$(docker service inspect "$first_svc" --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) echo -e " ${DIM}Imagem atual: ${cur_img}${RESET}" # Verificar se tem versao anterior local prev_img prev_img=$(docker service inspect "$first_svc" --format '{{.PreviousSpec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) if [ -n "$prev_img" ] && [ "$prev_img" != "$cur_img" ]; then echo -e " ${DIM}Versao anterior: ${prev_img}${RESET}" else echo -e " ${DIM}Sem versao anterior disponivel${RESET}" fi fi done if [ $stacks_found -eq 0 ]; then msg_warn "Nenhuma stack encontrada para rollback." echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi echo "" echo -e " ${RED}0)${RESET} Cancelar" echo "" echo -ne " ${CYAN}Escolha uma opcao${RESET}: " local rchoice read rchoice if [ "$rchoice" = "0" ] || [ -z "$rchoice" ]; then return 0; fi local idx=$((rchoice - 1)) if [ $idx -lt 0 ] || [ $idx -ge $stacks_found ]; then msg_warn "Opcao invalida." return 0 fi local target="${stack_names[$idx]}" local target_services target_services=$(docker service ls --format '{{.Name}}' 2>/dev/null | grep "^${target}_") echo "" echo -e " ${YELLOW}${BOLD}ROLLBACK: ${target}${RESET}" echo -e " ${DIM}Servicos afetados:${RESET}" echo "$target_services" | while read svc; do local cur prev cur=$(docker service inspect "$svc" --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) prev=$(docker service inspect "$svc" --format '{{.PreviousSpec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) echo -e " ${svc}: ${cur} → ${prev:-sem anterior}" done echo "" if ! ask_confirm "Confirmar rollback de ${target}?" "n"; then msg_info "Rollback cancelado." return 0 fi # Executar rollback via docker service rollback local errors=0 echo "$target_services" | while read svc; do msg_info "Rollback: ${svc}..." if docker service rollback "$svc" 2>/dev/null; then msg_ok "${svc}: rollback concluido" else msg_warn "${svc}: rollback falhou (pode nao ter versao anterior)" errors=$((errors + 1)) fi done if [ $errors -eq 0 ]; then echo "" msg_ok "Rollback de '${target}' concluido com sucesso!" else echo "" msg_warn "Rollback parcial — alguns servicos nao tinham versao anterior." fi echo "" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read } # ============================================================================ # UPDATE STACK # ============================================================================ update_stack() { section_header "ATUALIZAR STACK" local BACKUP_DIR="/root/backup_stacks" mkdir -p "$BACKUP_DIR" # --- Passo 1: Qual stack --- echo -e " ${BOLD}Qual stack deseja atualizar?${RESET}" echo "" local -a available_stacks=() local idx=0 for stack_name in traefik portainer postgres redis mega waha fzap evolution databasus n8n; do local services services=$(docker service ls --format '{{.Name}}' 2>/dev/null | grep "^${stack_name}_" | head -1) if [ -n "$services" ]; then idx=$((idx + 1)) available_stacks+=("$stack_name") local cur_img cur_img=$(docker service inspect "$services" --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) echo -e " ${GREEN}${idx})${RESET} ${stack_name} ${DIM}(${cur_img})${RESET}" fi done if [ $idx -eq 0 ]; then msg_warn "Nenhuma stack encontrada." echo ""; read -p "Pressione ENTER para voltar..."; return 0 fi echo "" echo -e " ${RED}0)${RESET} Cancelar" echo "" echo -ne " ${CYAN}Escolha a stack${RESET}: " local uchoice read uchoice if [ "$uchoice" = "0" ] || [ -z "$uchoice" ]; then return 0; fi local uidx=$((uchoice - 1)) if [ $uidx -lt 0 ] || [ $uidx -ge $idx ]; then msg_warn "Opcao invalida." return 0 fi local target="${available_stacks[$uidx]}" local target_services target_services=$(docker service ls --format '{{.Name}}' 2>/dev/null | grep "^${target}_") # --- Passo 2: O que atualizar --- echo "" echo -e " ${BOLD}O que deseja atualizar em '${target}'?${RESET}" echo "" echo -e " ${GREEN}1)${RESET} Atualizar imagem (pull nova versao)" echo -e " ${GREEN}2)${RESET} Alterar numero de replicas" echo -e " ${GREEN}3)${RESET} Alterar recursos (CPU / memoria)" echo -e " ${GREEN}4)${RESET} Force restart (mesma config, reinicia containers)" echo -e " ${GREEN}5)${RESET} Alterar imagem para versao especifica" echo -e " ${GREEN}0)${RESET} Cancelar" echo "" echo -ne " ${CYAN}Escolha${RESET}: " local action read action if [ "$action" = "0" ] || [ -z "$action" ]; then return 0; fi # --- Passo 3: Backup antes de qualquer mudanca --- warn_non_persistent_volumes "$target" || return 0 local backup_ts backup_ts=$(date +%Y%m%d_%H%M%S) local backup_file="${BACKUP_DIR}/${target}_${backup_ts}.yaml" msg_info "Salvando backup da stack '${target}' em ${backup_file}..." # Exportar estado atual de todos os servicos da stack { echo "# Backup da stack '${target}' — ${backup_ts}" echo "# Gerado automaticamente pelo Mega Deploy antes de update" echo "" echo "$target_services" | while read svc; do echo "---" echo "# Service: ${svc}" docker service inspect "$svc" --pretty 2>/dev/null echo "" done } > "$backup_file" msg_ok "Backup salvo: ${backup_file}" # --- Passo 4: Aplicar acao --- case "$action" in 1) # Atualizar imagem (pull latest) echo "" msg_info "Atualizando imagem de todos os servicos de '${target}'..." echo "$target_services" | while read svc; do local cur_img cur_img=$(docker service inspect "$svc" --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) msg_info "${svc}: pulling ${cur_img}..." docker service update --image "$cur_img" --force "$svc" 2>/dev/null \ && msg_ok "${svc}: imagem atualizada" \ || msg_warn "${svc}: falha ao atualizar" done ;; 2) # Alterar replicas echo "" echo "$target_services" | while read svc; do local cur_rep cur_rep=$(docker service inspect "$svc" --format '{{.Spec.Mode.Replicated.Replicas}}' 2>/dev/null) echo -e " ${DIM}${svc}: ${cur_rep} replicas${RESET}" done echo "" echo -ne " ${CYAN}Servico a alterar (nome completo)${RESET}: " local svc_name read svc_name if [ -z "$svc_name" ]; then return 0; fi echo -ne " ${CYAN}Novo numero de replicas${RESET}: " local new_rep read new_rep if [ -z "$new_rep" ]; then return 0; fi docker service scale "${svc_name}=${new_rep}" 2>&1 msg_ok "Replicas de ${svc_name} alteradas para ${new_rep}" ;; 3) # Alterar recursos echo "" echo "$target_services" | while read svc; do local cpu mem cpu=$(docker service inspect "$svc" --format '{{.Spec.TaskTemplate.Resources.Limits.NanoCPUs}}' 2>/dev/null) mem=$(docker service inspect "$svc" --format '{{.Spec.TaskTemplate.Resources.Limits.MemoryBytes}}' 2>/dev/null) local cpu_val="sem limite" local mem_val="sem limite" [ -n "$cpu" ] && [ "$cpu" != "0" ] && cpu_val="$(echo "scale=1; $cpu / 1000000000" | bc 2>/dev/null || echo "${cpu}ns") CPUs" [ -n "$mem" ] && [ "$mem" != "0" ] && mem_val="$(echo "scale=0; $mem / 1048576" | bc 2>/dev/null || echo "${mem}B") MB" echo -e " ${DIM}${svc}: ${cpu_val} / ${mem_val}${RESET}" done echo "" echo -ne " ${CYAN}Servico a alterar (nome completo)${RESET}: " local svc_name read svc_name if [ -z "$svc_name" ]; then return 0; fi echo -ne " ${CYAN}Limite de CPUs (ex: 2)${RESET}: " local new_cpu read new_cpu echo -ne " ${CYAN}Limite de memoria (ex: 2G, 512M)${RESET}: " local new_mem read new_mem local update_args="" [ -n "$new_cpu" ] && update_args="$update_args --limit-cpu $new_cpu" [ -n "$new_mem" ] && update_args="$update_args --limit-memory $new_mem" if [ -n "$update_args" ]; then docker service update $update_args "$svc_name" 2>&1 msg_ok "Recursos de ${svc_name} atualizados" else msg_warn "Nenhuma alteracao informada" fi ;; 4) # Force restart echo "" msg_info "Reiniciando todos os servicos de '${target}'..." echo "$target_services" | while read svc; do docker service update --force "$svc" 2>/dev/null \ && msg_ok "${svc}: reiniciado" \ || msg_warn "${svc}: falha" done ;; 5) # Alterar para versao especifica echo "" echo "$target_services" | while read svc; do local cur_img cur_img=$(docker service inspect "$svc" --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null | cut -d@ -f1) echo -e " ${DIM}${svc}: ${cur_img}${RESET}" done echo "" echo -ne " ${CYAN}Servico a alterar (nome completo)${RESET}: " local svc_name read svc_name if [ -z "$svc_name" ]; then return 0; fi echo -ne " ${CYAN}Nova imagem (ex: n8nio/n8n:2.9.2)${RESET}: " local new_img read new_img if [ -z "$new_img" ]; then return 0; fi docker service update --image "$new_img" "$svc_name" 2>&1 msg_ok "${svc_name}: imagem alterada para ${new_img}" ;; *) msg_warn "Opcao invalida." ;; esac echo "" echo -e " ${DIM}Backups salvos em: ${BACKUP_DIR}/${RESET}" echo -e " ${YELLOW}Pressione ENTER para continuar...${RESET}" read } # ============================================================================ # FUNCAO PRINCIPAL # ============================================================================ main() { # Redirecionar stdin de volta pro terminal se estiver em pipe (curl) if [ ! -t 0 ]; then exec < /dev/tty 2>/dev/null || true fi # Tela 1 — Boas-vindas (apresentacao + comunidade + contatos) show_welcome # Tela 2 — Checklist pre-instalacao (o que o usuario precisa ter) show_checklist # Verificar requisitos (Docker, Swarm, curl, jq, openssl) show_banner check_requirements # Inicializar arquivo de credenciais (se nao existir) if [ ! -f "$CREDENTIALS_FILE" ]; then init_credentials_file fi # Detectar stacks instaladas detect_installed # Decidir qual menu mostrar if ! $TRAEFIK_OK || ! $PORTAINER_OK; then show_menu_infra fi # Se chegou aqui, infra esta instalada show_menu_main } # ============================================================================ # TRATAMENTO DE SINAIS # ============================================================================ cleanup() { echo "" msg_info "Interrupcao detectada. Limpando arquivos temporarios..." rm -f "${TEMP_DIR}/traefik_raw.yaml" "${TEMP_DIR}/traefik_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/portainer_raw.yaml" "${TEMP_DIR}/portainer_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/postgres_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/redis_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/mega_raw.yaml" "${TEMP_DIR}/mega_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/waha_raw.yaml" "${TEMP_DIR}/waha_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/evolution_raw.yaml" "${TEMP_DIR}/evolution_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/databasus_deploy.yaml" 2>/dev/null rm -f "${TEMP_DIR}/n8n_deploy.yaml" 2>/dev/null finalize_credentials_file echo -e " ${GREEN}Feito.${RESET}" exit 130 } trap cleanup SIGINT SIGTERM # ============================================================================ # EXECUTAR # ============================================================================ main "$@"