#!/bin/bash # 移除 set -e,改为手动控制错误,防止意外退出 # set -e set -o pipefail # ================= 颜色与日志 ================= RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "${BLUE}[STEP]${NC} $1"; } log_success() { echo -e "${CYAN}[OK]${NC} $1"; } log_header() { echo "" echo "========================================" echo -e "${BLUE}$1${NC}" echo "========================================" } # ================= 配置与初始化 ================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HOST_FILE="${SCRIPT_DIR}/minio.txt" # 依赖的子脚本名称列表 SUB_SCRIPTS=( "1.prepare_file.sh" "2.deploy_keys.sh" "3.prepare_disk.sh" "4.deploy_cluster.sh" ) # 定义脚本名称与下载 URL 的映射 (修正了原代码中多余的 .sh 后缀) declare -A SCRIPT_URLS SCRIPT_URLS["1.prepare_file.sh"]="https://zhengyu1992.cn/file/software/minio/1.prepare_file.sh" SCRIPT_URLS["2.deploy_keys.sh"]="https://zhengyu1992.cn/file/software/minio/2.deploy_keys.sh" SCRIPT_URLS["3.prepare_disk.sh"]="https://zhengyu1992.cn/file/software/minio/3.prepare_disk.sh" SCRIPT_URLS["4.deploy_cluster.sh"]="https://zhengyu1992.cn/file/software/minio/4.deploy_cluster.sh" # 核心二进制文件和服务文件列表 CORE_FILES=("minio" "mc" "minio.service") log_info "已导出环境变量: MINIO_ROOT_USER=$MINIO_ROOT_USER" log_info "已导出环境变量: MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD" # ================= 前置检查 ================= log_header " MinIO 集群全自动部署启动" # 1. 检查 minio.txt if [ ! -f "$HOST_FILE" ]; then log_error "找不到主机列表文件: $HOST_FILE" exit 1 fi log_info "主机列表: $HOST_FILE" log_info "工作目录: $SCRIPT_DIR" # --------------------------------------------------------- # 2. 检查并自动下载/修复 4 个子脚本 # --------------------------------------------------------- log_step "检查子脚本完整性 (${#SUB_SCRIPTS[@]} 个)..." for script_name in "${SUB_SCRIPTS[@]}"; do script_path="${SCRIPT_DIR}/${script_name}" # 检查是否存在 if [ ! -f "$script_path" ]; then log_warn "缺少脚本: ${script_name},正在下载..." download_url="${SCRIPT_URLS[$script_name]}" if [ -z "$download_url" ]; then log_error "错误:脚本 ${script_name} 没有配置下载 URL!" exit 1 fi # 下载 if ! curl -fsSL "$download_url" -o "$script_path"; then # 尝试 wget 备用 if command -v wget &> /dev/null; then wget -q "$download_url" -O "$script_path" else log_error "下载失败:${script_name} (curl 和 wget 均不可用或网络错误)" exit 1 fi fi # 赋予执行权限 chmod +x "$script_path" if [ -s "$script_path" ]; then log_success "脚本 ${script_name} 下载并授权成功。" else log_error "下载的文件为空:${script_name}" exit 1 fi elif [ ! -x "$script_path" ]; then # 存在但无执行权限 log_warn "脚本 ${script_name} 缺少执行权限,正在修复..." chmod +x "$script_path" log_success "权限修复成功。" else log_info "脚本 ${script_name} 已就绪。" fi done # 依赖的子脚本定义 SCRIPT_PREPARE_FILE="${SCRIPT_DIR}/1.prepare_file.sh" SCRIPT_INIT_NODE="${SCRIPT_DIR}/2.deploy_keys.sh" SCRIPT_DISK="${SCRIPT_DIR}/3.prepare_disk.sh" SCRIPT_MINIO="${SCRIPT_DIR}/4.deploy_cluster.sh" # --------------------------------------------------------- # 3. 检查核心文件 (minio, mc, minio.service) # --------------------------------------------------------- log_step "检查必要核心文件 (minio, mc, minio.service)..." NEED_RUN_PREPARE=false MISSING_FILES=() for file in "${CORE_FILES[@]}"; do file_path="${SCRIPT_DIR}/${file}" if [ ! -f "$file_path" ]; then log_warn "缺少核心文件: ${file}" MISSING_FILES+=("$file") NEED_RUN_PREPARE=true elif [ ! -s "$file_path" ]; then log_warn "核心文件为空: ${file}" MISSING_FILES+=("$file") NEED_RUN_PREPARE=true fi done if [ "$NEED_RUN_PREPARE" = true ]; then log_info "检测到以下文件缺失或损坏: ${MISSING_FILES[*]}" log_info "正在自动执行 1.prepare_file.sh 进行下载和修复..." SCRIPT_PREPARE_FILE="${SCRIPT_DIR}/1.prepare_file.sh" # 确保准备脚本有权限 if [ ! -x "$SCRIPT_PREPARE_FILE" ]; then chmod +x "$SCRIPT_PREPARE_FILE" fi # 执行下载脚本 "$SCRIPT_PREPARE_FILE" EXEC_RESULT=$? if [ $EXEC_RESULT -ne 0 ]; then log_error "自动下载脚本 (1.prepare_file.sh) 执行失败 (返回值: $EXEC_RESULT)" log_error "请手动运行 './1.prepare_file.sh' 查看具体错误信息。" exit 1 fi # 二次检查:确认下载是否成功 STILL_MISSING=false for file in "${CORE_FILES[@]}"; do file_path="${SCRIPT_DIR}/${file}" if [ ! -f "$file_path" ] || [ ! -s "$file_path" ]; then log_error "下载完成后仍未找到有效文件: ${file}" STILL_MISSING=true fi done if [ "$STILL_MISSING" = true ]; then log_error "必要文件下载失败。可能是网络问题或下载源失效。" log_error "建议:手动检查 1.prepare_file.sh 中的下载链接,或手动上传 minio 和 mc 二进制文件。" exit 1 else log_success "所有核心文件自动下载并校验成功。" # 额外步骤:确保 mc 有执行权限 (防止下载下来没权限导致后续报错) chmod +x "${SCRIPT_DIR}/mc" "${SCRIPT_DIR}/minio" fi else log_success "所有核心二进制文件及服务文件已就绪。" fi # 脚本存在但无执行权限 -> 添加权限 if [ ! -x "$script_path" ]; then log_warn "脚本无执行权限: $script_name,正在添加权限..." chmod +x "$script_path" if [ $? -ne 0 ]; then log_error "无法为 $script_name 添加执行权限。" exit 1 fi log_info "$script_name 权限已修复。" fi log_success "所有子脚本已就绪 (已检查/下载/授权)。" # ================= 解析主机列表 ================= log_step "解析主机列表..." declare -a NODE_NAMES declare -a NODE_IPS declare -a NODE_USERS declare -a NODE_PASSES declare -a NODE_DEVICES COUNT=0 while IFS= read -r line || [[ -n "$line" ]]; do # 跳过空行和注释 [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue # 使用 awk 提取,避免 shell 词法解析特殊字符的问题 # 注意:如果密码中包含空格,awk '{print $4}' 会截断。 # 假设密码不含空格(通常密码策略不建议含空格),如果有空格需调整 minio.txt 格式或用引号包裹后特殊处理 name=$(echo "$line" | awk '{print $1}') ip=$(echo "$line" | awk '{print $2}') user=$(echo "$line" | awk '{print $3}') pass=$(echo "$line" | awk '{print $4}') devices=$(echo "$line" | awk '{print $5}') if [ -z "$name" ] || [ -z "$ip" ] || [ -z "$user" ] || [ -z "$pass" ] || [ -z "$devices" ]; then log_warn "跳过格式错误的行 (缺少列): $line" continue fi # 存入数组 NODE_NAMES+=("$name") NODE_IPS+=("$ip") NODE_USERS+=("$user") NODE_PASSES+=("$pass") NODE_DEVICES+=("$devices") ((COUNT++)) done < <(tail -n +2 "$HOST_FILE") if [ $COUNT -eq 0 ]; then log_error "未找到有效的节点配置,请检查 $HOST_FILE 格式" exit 1 fi log_success "共解析到 $COUNT 个节点" TOTAL_NODES=$COUNT FAILED_COUNT=0 # ================= 阶段 1: 基础环境 (SSH) ================= log_header "阶段 1/4: 初始化基础环境 (SSH/Hosts/Sudo)" for i in "${!NODE_NAMES[@]}"; do name="${NODE_NAMES[$i]}" ip="${NODE_IPS[$i]}" user="${NODE_USERS[$i]}" pass="${NODE_PASSES[$i]}" log_step "正在初始化: $name ($ip)" # 调用脚本,注意密码变量加双引号 if "$SCRIPT_INIT_NODE" "$name" "$ip" "$user" "$pass"; then log_success "$name 基础环境就绪" else log_error "$name 基础环境初始化失败" ((FAILED_COUNT++)) # 标记该节点后续跳过 (简单策略:继续循环,但记录失败数,后续步骤仍尝试但可能失败) # 更严谨的做法是将该索引加入跳过列表,这里为了简化,若 SSH 失败,后续 SSH 操作必败,自然会计入失败 fi done sleep 2 # 等待 SSH 配置生效 # ================= 阶段 2: 磁盘准备 ================= log_header "阶段 2/4: 磁盘格式化与挂载" for i in "${!NODE_NAMES[@]}"; do name="${NODE_NAMES[$i]}" user="${NODE_USERS[$i]}" devices="${NODE_DEVICES[$i]}" log_step "正在准备磁盘: $name (设备: $devices)" TARGET="${user}@${name}" if "$SCRIPT_DISK" "$TARGET" "$devices"; then log_success "$name 磁盘准备完成" else log_error "$name 磁盘准备失败" ((FAILED_COUNT++)) fi done # ================= 阶段 3: MinIO 部署 ================= log_header "阶段 3/4: 部署 MinIO 服务" for i in "${!NODE_NAMES[@]}"; do name="${NODE_NAMES[$i]}" user="${NODE_USERS[$i]}" log_step "正在部署 MinIO: $name" TARGET="${user}@${name}" # 传递环境变量给子脚本(如果子脚本需要) # 这里主要依赖子脚本内部逻辑,如果需要动态密码,可修改 4.deploy_cluster.sh 接收参数 if "$SCRIPT_MINIO" "$TARGET" "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; then log_success "$name MinIO 服务启动成功" else log_error "$name MinIO 部署失败" ((FAILED_COUNT++)) fi done # ================= 阶段 4: 集群验收 ================= log_header "阶段 4/4: 集群状态验收" FIRST_NAME="${NODE_NAMES[0]}" FIRST_USER="${NODE_USERS[0]}" ENTRY_NODE="${FIRST_USER}@${FIRST_NAME}" log_info "使用入口节点: $ENTRY_NODE" log_info "预期账号: $MINIO_ROOT_USER" # 1. 配置 mc alias log_step "配置 mc 别名..." # 构造命令,确保密码正确传递 CMD_ALIAS="mc alias set mycluster http://${FIRST_NAME}:9000 '${MINIO_ROOT_USER}' '${MINIO_ROOT_PASSWORD}'" if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 "$ENTRY_NODE" "$CMD_ALIAS"; then log_success "mc 别名配置成功" else log_error "配置 mc 别名失败 (请检查服务是否启动或密码是否正确)" ((FAILED_COUNT++)) # 即使失败也尝试继续看能否获取信息,或者直接退出 # 这里选择继续尝试 info,因为有时候 alias 失败是网络波动 fi # 2. 检查集群状态 (带重试机制) log_step "检查集群健康状态 (最多等待 90 秒)..." MAX_RETRIES=18 # 最大重试次数 SLEEP_INTERVAL=5 # 每次间隔秒数 (总等待时间 = 18 * 5 = 90 秒) RETRY_COUNT=0 CLUSTER_READY=false while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do # 获取 JSON 数据 (屏蔽 SSH 警告,确保纯 JSON) # 注意:使用 -q 静默模式,并将 stderr 重定向到 /dev/null 防止污染 JSON OUTPUT=$(ssh -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 "$ENTRY_NODE" "mc admin info mycluster --json" 2>/dev/null) if [ -z "$OUTPUT" ]; then log_warn "第 $((RETRY_COUNT + 1)) 次检查:无法获取数据 (SSH 无返回),等待中..." sleep $SLEEP_INTERVAL ((RETRY_COUNT++)) continue fi # 验证 JSON 合法性 if ! echo "$OUTPUT" | jq -e '.status' > /dev/null 2>&1; then log_warn "第 $((RETRY_COUNT + 1)) 次检查:返回数据非 JSON 格式,等待服务稳定..." # 这里可以选择打印一点调试信息,但不退出 # echo "$OUTPUT" | head -n 3 sleep $SLEEP_INTERVAL ((RETRY_COUNT++)) continue fi # 解析状态 ONLINE_COUNT=$(echo "$OUTPUT" | jq '[.info.servers[] | select(.state == "online")] | length' 2>/dev/null) TOTAL_COUNT=$(echo "$OUTPUT" | jq '.info.servers | length' 2>/dev/null) # 容错处理 ONLINE_COUNT=${ONLINE_COUNT:-0} TOTAL_COUNT=${TOTAL_COUNT:-0} if [ "$TOTAL_COUNT" -eq 0 ]; then log_warn "第 $((RETRY_COUNT + 1)) 次检查:未检测到服务器节点信息,等待中..." sleep $SLEEP_INTERVAL ((RETRY_COUNT++)) continue fi log_info "第 $((RETRY_COUNT + 1)) 次检查: $ONLINE_COUNT / $TOTAL_COUNT 节点在线" # 打印当前详细状态 (可选,只在非全在线时打印,避免刷屏) if [ "$ONLINE_COUNT" -ne "$TOTAL_COUNT" ]; then echo "$OUTPUT" | jq -r '.info.servers[] | " - \(.endpoint): \(.state)"' | head -n 5 fi if [ "$ONLINE_COUNT" -eq "$TOTAL_COUNT" ]; then CLUSTER_READY=true break fi # 如果还没好,继续等待 sleep $SLEEP_INTERVAL ((RETRY_COUNT++)) done if [ "$CLUSTER_READY" = true ]; then log_success "✅ 集群健康检查通过! 所有 $TOTAL_COUNT 个节点已在线并就绪。" # 打印最终详细状态表 echo -e "${CYAN}--- 节点详情 ---${NC}" echo "$OUTPUT" | jq -r '.info.servers[] | " \(.endpoint): \(.state) | 硬盘: \(.drives | length) 盘"' echo -e "${CYAN}------------------${NC}" else log_error "❌ 集群未在预期时间内完全启动 (当前: $ONLINE_COUNT / $TOTAL_COUNT)!" log_warn "部分节点可能仍处于 'initializing' 状态,这通常是因为磁盘格式化或网络同步较慢。" echo "" echo -e "${YELLOW}========================================${NC}" echo -e "${YELLOW} 手动排查建议 (请在管理节点执行):${NC}" echo -e "${YELLOW}========================================${NC}" echo "" echo "1. 实时查看 MinIO 服务日志 (观察是否有报错或正在格式化):" echo " ssh $ENTRY_NODE 'journalctl -u minio -f --no-pager | tail -n 50'" echo "" echo "2. 再次手动检查集群状态:" echo " ssh $ENTRY_NODE 'mc admin info mycluster'" echo "" echo "3. 检查各节点间网络连通性 (从 minio1 ping 其他节点):" echo " ssh $ENTRY_NODE 'for h in minio2 minio3 minio4; do ping -c 1 \$h; done'" echo "" echo "4. 如果日志显示 'Formatting', 请耐心等待,大磁盘可能需要几分钟。" echo "" ((FAILED_COUNT++)) fi # ================= 总结 ================= echo "" echo "========================================" if [ $FAILED_COUNT -eq 0 ]; then log_success " 恭喜!MinIO 集群部署全部成功!" log_info " 访问控制台: http://${FIRST_NAME}:9001" log_info " 账号: ${MINIO_ROOT_USER}" log_info " 密码: ${MINIO_ROOT_PASSWORD}" log_info " 快速开始:" echo " export MC_HOST_mycluster='http://${MINIO_ROOT_USER}:${MINIO_ROOT_PASSWORD}@${FIRST_NAME}:9000'" echo " mc mb mycluster/mybucket" else log_error " 部署完成,但有 $FAILED_COUNT 处异常/失败,请检查上方日志。" exit 1 fi echo "========================================"