格式化硬盘脚本
#!/bin/bash
# =============================================================================
# 脚本名称: disk.sh
# 脚本功能: 生产环境磁盘初始化(格式化为 XFS、挂载、配置 fstab、创建数据目录)
# 使用说明: disk.sh -d <设备> [-m <挂载点>] [-D <数据子目录>] [-y] [-v] [-h]
# =============================================================================
set -uo pipefail # 注意: 不加 -e,由各函数自行处理错误,避免 set -e 吞掉错误信息
# ---------- 常量 ----------
readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_DIR="/var/log"
readonly LOG_FILE="${LOG_DIR}/disk_init.log"
readonly FSTAB="/etc/fstab"
# ---------- 默认参数 ----------
DISK=""
MOUNT_POINT="/export"
DATA_SUBDIR="Data"
YES=false
VERBOSE=false
# ---------- 颜色 ----------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# ---------- 日志函数 ----------
_ts() { date '+%Y-%m-%d %H:%M:%S'; }
log_info() { local m="[INFO] $(_ts) $*"; echo -e "${BLUE}${m}${NC}"; _log_file "$m"; }
log_success() { local m="[SUCCESS] $(_ts) $*"; echo -e "${GREEN}${m}${NC}"; _log_file "$m"; }
log_warn() { local m="[WARN] $(_ts) $*"; echo -e "${YELLOW}${m}${NC}"; _log_file "$m"; }
log_error() { local m="[ERROR] $(_ts) $*"; echo -e "${RED}${m}${NC}" >&2; _log_file "$m"; }
log_debug() { [[ "$VERBOSE" == "true" ]] && { local m="[DEBUG] $(_ts) $*"; echo -e "${BLUE}${m}${NC}"; _log_file "$m"; }; true; }
_log_file() {
# 写日志文件;若目录不可写则静默跳过,不中断流程
if [[ -w "$LOG_DIR" ]] || mkdir -p "$LOG_DIR" 2>/dev/null; then
echo "$1" >> "$LOG_FILE" 2>/dev/null || true
fi
}
# ---------- 错误退出 ----------
die() {
log_error "$1"
exit "${2:-1}"
}
# ---------- 用法说明 ----------
usage() {
cat <<EOF
用法: $SCRIPT_NAME -d <设备> [-m <挂载点>] [-D <数据子目录>] [-y] [-v] [-h]
选项:
-d <device> 磁盘设备路径(必填),如 /dev/vdb
-m <path> 挂载点路径(默认: /export)
-D <subdir> 挂载点下的数据子目录名(默认: Data)
-y 跳过交互确认(非交互 / CI 环境使用)
-v 开启详细日志
-h 显示此帮助信息
安全说明:
1. 脚本会永久擦除指定磁盘的全部数据
2. 系统盘(根分区所在设备)会被自动识别并拒绝操作
3. 操作前会自动备份 /etc/fstab
示例:
$SCRIPT_NAME -d /dev/vdb
$SCRIPT_NAME -d /dev/vdb -m /data -D app_data -y
EOF
}
# ---------- 参数解析 ----------
parse_args() {
while getopts ":d:m:D:yvh" opt; do
case "$opt" in
d) DISK="$OPTARG" ;;
m) MOUNT_POINT="$OPTARG" ;;
D) DATA_SUBDIR="$OPTARG" ;;
y) YES=true ;;
v) VERBOSE=true ;;
h) usage; exit 0 ;;
:) die "选项 -$OPTARG 需要参数" 1 ;;
\?) die "未知选项: -$OPTARG" 1 ;;
esac
done
[[ -z "$DISK" ]] && { usage; die "缺少必填参数: -d <设备>" 1; }
}
# ---------- 检查依赖工具 ----------
check_dependencies() {
local missing=()
for cmd in blkid mkfs.xfs lsblk fuser; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if (( ${#missing[@]} > 0 )); then
die "缺少必要工具: ${missing[*]},请先安装后重试" 5
fi
log_debug "依赖检查通过"
}
# ---------- 检查 root 权限 ----------
check_root() {
if [[ $EUID -ne 0 ]]; then
die "此脚本需要 root 权限,请使用 sudo 运行" 1
fi
}
# ---------- 获取根分区所在物理设备 ----------
get_root_device() {
# 取 / 挂载所在设备,再去掉末尾数字得到物理盘
local root_dev
root_dev=$(df --output=source / 2>/dev/null | tail -1)
# 去掉分区号(如 /dev/sda1 -> /dev/sda,/dev/nvme0n1p1 -> /dev/nvme0n1)
root_dev=$(echo "$root_dev" | sed -E 's/p?[0-9]+$//')
echo "$root_dev"
}
# ---------- 参数校验 ----------
validate_parameters() {
# 块设备检查
if [[ ! -b "$DISK" ]]; then
die "设备 $DISK 不存在或不是块设备" 2
fi
# 禁止操作系统盘
local root_device
root_device=$(get_root_device)
log_debug "根分区所在设备: $root_device"
local disk_base
disk_base=$(echo "$DISK" | sed -E 's/p?[0-9]+$//')
if [[ "$disk_base" == "$root_device" ]]; then
die "安全拒绝: $DISK 属于系统盘 ($root_device),禁止操作!" 3
fi
# 挂载点不能是根目录
if [[ "$MOUNT_POINT" == "/" ]]; then
die "挂载点不能为根目录 /" 1
fi
log_debug "参数校验通过: disk=$DISK, mount=$MOUNT_POINT, data_subdir=$DATA_SUBDIR"
}
# ---------- 安全卸载 ----------
safe_umount() {
local target="$1" # 设备或挂载点均可
local mount_point
mount_point=$(findmnt -nr -o TARGET "$target" 2>/dev/null || true)
[[ -z "$mount_point" ]] && return 0
log_info "检测到已挂载: $target -> $mount_point,尝试卸载..."
# 终止占用进程
if fuser -s "$mount_point" 2>/dev/null; then
log_warn "有进程正在使用 $mount_point,尝试终止..."
fuser -km "$mount_point" 2>/dev/null || true
sleep 2
fi
if ! umount "$target" 2>/dev/null; then
# 尝试 lazy unmount 兜底
log_warn "umount 失败,尝试 lazy umount..."
umount -l "$target" 2>/dev/null || die "无法卸载 $target,请手动处理后重试" 4
fi
log_info "卸载成功: $target"
}
# ---------- 用户确认 ----------
confirm_operation() {
if [[ "$YES" == "true" ]]; then
log_info "非交互模式 (-y),跳过确认"
return 0
fi
local disk_info
disk_info=$(lsblk -dn -o SIZE,MODEL "$DISK" 2>/dev/null || echo "未知")
echo ""
echo "========================================"
echo " !!! 警告:以下操作不可逆 !!!"
echo "========================================"
echo " 设备: $DISK ($disk_info)"
echo " 操作: 格式化为 XFS 文件系统"
echo " 挂载点: $MOUNT_POINT"
echo " 数据目录: ${MOUNT_POINT}/${DATA_SUBDIR}"
echo " 影响: 设备上所有数据将被永久删除!"
echo "========================================"
echo ""
read -r -p "请输入 'YES' 确认继续(其他任意输入取消): " confirmation
if [[ "$confirmation" != "YES" ]]; then
log_info "用户取消操作"
exit 0
fi
}
# ---------- 格式化磁盘 ----------
format_disk() {
log_info "开始格式化 $DISK 为 XFS 文件系统..."
# 若已有文件系统,记录警告
local current_fs
current_fs=$(blkid -s TYPE -o value "$DISK" 2>/dev/null || true)
if [[ -n "$current_fs" ]]; then
log_warn "设备 $DISK 当前文件系统: $current_fs,即将覆盖"
fi
# 先卸载(如已挂载)
safe_umount "$DISK"
# 格式化:-f 强制;普通磁盘不使用 RAID 条带参数
if ! mkfs.xfs -f -i size=512 -l size=128m "$DISK"; then
die "磁盘格式化失败" 6
fi
# 确保内核元数据同步
udevadm settle 2>/dev/null || true
blockdev --rereadpt "$DISK" 2>/dev/null || true
log_success "磁盘格式化完成: $DISK"
}
# ---------- 准备挂载点 ----------
prepare_mountpoint() {
log_info "准备挂载点: $MOUNT_POINT"
# 若挂载点已被其他设备占用,先卸载
if findmnt -n "$MOUNT_POINT" &>/dev/null; then
local occupied_by
occupied_by=$(findmnt -nr -o SOURCE "$MOUNT_POINT" 2>/dev/null || true)
log_warn "挂载点 $MOUNT_POINT 已被 ${occupied_by:-未知设备} 使用,尝试卸载..."
safe_umount "$MOUNT_POINT"
fi
if ! mkdir -p "$MOUNT_POINT"; then
die "创建挂载点目录失败: $MOUNT_POINT" 8
fi
log_debug "挂载点就绪: $MOUNT_POINT"
}
# ---------- 挂载磁盘 ----------
mount_disk() {
log_info "挂载 $DISK 到 $MOUNT_POINT"
if ! mount -t xfs -o defaults,noatime,nodiratime "$DISK" "$MOUNT_POINT"; then
die "磁盘挂载失败" 9
fi
if ! mountpoint -q "$MOUNT_POINT"; then
die "挂载验证失败: $MOUNT_POINT 不是有效挂载点" 10
fi
log_success "挂载成功: $DISK -> $MOUNT_POINT"
}
# ---------- 配置 fstab 自动挂载 ----------
configure_fstab() {
log_info "配置 fstab 自动挂载..."
local uuid
uuid=$(blkid -s UUID -o value "$DISK" 2>/dev/null)
if [[ -z "$uuid" ]]; then
die "获取磁盘 UUID 失败: $DISK" 11
fi
log_debug "磁盘 UUID: $uuid"
# 备份 fstab(带时间戳,唯一文件名)
local backup="${FSTAB}.bak.$(date +%Y%m%d_%H%M%S)"
if ! cp "$FSTAB" "$backup"; then
die "备份 $FSTAB 失败" 11
fi
log_info "fstab 已备份至: $backup"
local fstab_entry="UUID=${uuid} ${MOUNT_POINT} xfs defaults,noatime,nodiratime 0 0"
# 先删除与本次 UUID 或挂载点相关的旧条目(避免重复)
local tmp_fstab
tmp_fstab=$(mktemp)
grep -Ev "^[[:space:]]*UUID=${uuid}[[:space:]]|[[:space:]]${MOUNT_POINT}[[:space:]]" \
"$FSTAB" > "$tmp_fstab" || true
echo "$fstab_entry" >> "$tmp_fstab"
# 用 mount -a 验证新 fstab(仅新增条目),失败则不写入
if ! mount --fake -a -T "$tmp_fstab" 2>/dev/null; then
log_warn "mount --fake 验证不可用,跳过预验证(内核版本较旧)"
fi
# 原子替换 fstab
if ! mv "$tmp_fstab" "$FSTAB"; then
rm -f "$tmp_fstab"
die "写入 $FSTAB 失败" 12
fi
log_success "fstab 配置完成 (UUID=$uuid)"
}
# ---------- 创建数据目录 ----------
create_data_directory() {
local data_dir="${MOUNT_POINT}/${DATA_SUBDIR}"
log_info "创建数据目录: $data_dir"
if ! mkdir -p "$data_dir"; then
die "创建数据目录失败: $data_dir" 13
fi
chmod 755 "$data_dir"
log_success "数据目录创建完成: $data_dir"
}
# ---------- 结果摘要 ----------
show_summary() {
local uuid
uuid=$(blkid -s UUID -o value "$DISK" 2>/dev/null || echo "未知")
local fs_type
fs_type=$(blkid -s TYPE -o value "$DISK" 2>/dev/null || echo "未知")
echo ""
echo "========================================"
echo " 操作完成摘要"
echo "========================================"
printf " %-12s %s\n" "设备:" "$DISK"
printf " %-12s %s\n" "文件系统:" "$fs_type"
printf " %-12s %s\n" "UUID:" "$uuid"
printf " %-12s %s\n" "挂载点:" "$MOUNT_POINT"
printf " %-12s %s\n" "数据目录:" "${MOUNT_POINT}/${DATA_SUBDIR}"
printf " %-12s %s\n" "挂载状态:" "$(mountpoint -q "$MOUNT_POINT" && echo '已挂载' || echo '未挂载')"
echo "========================================"
echo " 磁盘空间:"
df -h "$MOUNT_POINT" | awk 'NR==1{printf " %s\n",$0} NR>1{printf " %s\n",$0}'
echo "========================================"
echo ""
}
# ---------- 信号处理 ----------
trap 'log_error "脚本被中断 (信号)"; exit 130' INT TERM
# ---------- 主流程 ----------
main() {
log_info "=== $SCRIPT_NAME 开始执行 (PID=$$) ==="
log_debug "原始参数: $*"
parse_args "$@"
check_root
check_dependencies
validate_parameters
log_info "目标设备: $DISK | 挂载点: $MOUNT_POINT | 数据子目录: $DATA_SUBDIR"
confirm_operation
format_disk
prepare_mountpoint
mount_disk
configure_fstab
create_data_directory
show_summary
log_success "=== 磁盘初始化完成 ==="
}
main "$@"
启动单一脚本
#!/usr/bin/env bash
# ==============================================================================
# dmsdocs.sh - 生产环境 dms-doc 容器部署脚本
#
# 功能:
# - 自动检测容器运行时:优先 docker,无 docker 则使用 nerdctl
# - 拉取指定 tag 的 dmsdocs 镜像并以受控方式启动容器
# - 支持镜像拉取重试、滚动替换(先备份旧容器,新容器启动失败自动回滚)
# - 支持 dry-run、参数化配置、健康检查、日志限额等生产化能力
#
# 退出码:
# 0 成功
# 1 参数错误
# 2 环境/依赖检查失败
# 3 镜像拉取失败
# 4 容器启动失败 / 健康检查失败(已尝试回滚)
# ==============================================================================
set -Eeuo pipefail
# ------------------------------ 默认配置 --------------------------------------
# 允许通过环境变量覆盖,便于 CI/CD 与多环境复用
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_VERSION="2.1.0"
IMAGE_PREFIX="${DMSDOCS_IMAGE_PREFIX:-hub-vpc.jdcloud.com/tj-deliver/dmsdocs-openeuler22-amd64}"
CONTAINER_NAME="${DMSDOCS_CONTAINER_NAME:-dms-doc}"
NETWORK_MODE="${DMSDOCS_NETWORK:-host}"
RESTART_POLICY="${DMSDOCS_RESTART:-unless-stopped}"
LOG_MAX_SIZE="${DMSDOCS_LOG_MAX_SIZE:-100m}"
LOG_MAX_FILE="${DMSDOCS_LOG_MAX_FILE:-3}"
PULL_RETRY="${DMSDOCS_PULL_RETRY:-3}"
PULL_RETRY_INTERVAL="${DMSDOCS_PULL_RETRY_INTERVAL:-5}"
HEALTH_CHECK_TIMEOUT="${DMSDOCS_HEALTH_TIMEOUT:-30}"
STOP_TIMEOUT="${DMSDOCS_STOP_TIMEOUT:-30}"
# 容器运行时:auto|docker|nerdctl,默认 auto 自动探测
RUNTIME_PREF="${DMSDOCS_RUNTIME:-auto}"
# nerdctl 使用的 namespace(默认 k8s.io 与 containerd CRI 一致,可改为 default)
NERDCTL_NAMESPACE="${DMSDOCS_NERDCTL_NAMESPACE:-k8s.io}"
TAG=""
FORCE_PULL=false
DRY_RUN=false
SKIP_HEALTH=false
# 运行时相关全局变量(由 detect_runtime 设置)
RUNTIME_BIN="" # 实际使用的命令,如 docker / nerdctl
RUNTIME_NAME="" # 名称标识: docker / nerdctl
RUNTIME_CMD=() # 完整调用前缀(数组),含 nerdctl 的 -n namespace
# ------------------------------ 日志 ------------------------------------------
# 仅在 stderr 是 TTY 时启用颜色,避免日志文件出现转义码
if [[ -t 2 ]]; then
readonly C_RED='\033[0;31m'
readonly C_GRN='\033[0;32m'
readonly C_YEL='\033[1;33m'
readonly C_NC='\033[0m'
else
readonly C_RED='' C_GRN='' C_YEL='' C_NC=''
fi
_log() {
local level="$1"; shift
local color="$1"; shift
printf '%b[%s] [%s]%b %s\n' \
"${color}" "$(date '+%Y-%m-%d %H:%M:%S')" "${level}" "${C_NC}" "$*" >&2
}
log_info() { _log "INFO" "${C_GRN}" "$@"; }
log_warn() { _log "WARN" "${C_YEL}" "$@"; }
log_error() { _log "ERROR" "${C_RED}" "$@"; }
# ------------------------------ 错误处理 --------------------------------------
on_error() {
local exit_code=$?
local line_no=$1
log_error "脚本在第 ${line_no} 行异常退出 (exit=${exit_code})"
exit "${exit_code}"
}
trap 'on_error ${LINENO}' ERR
# ------------------------------ 帮助信息 --------------------------------------
usage() {
cat <<EOF
${SCRIPT_NAME} v${SCRIPT_VERSION} - dms-doc 生产部署脚本
使用方法:
${SCRIPT_NAME} -o <镜像tag> [选项]
必选参数:
-o <tag> 指定镜像 tag
可选参数:
-f 强制重新拉取镜像(即使本地已存在)
-n <name> 自定义容器名 (默认: ${CONTAINER_NAME})
-p <prefix> 自定义镜像前缀 (默认: ${IMAGE_PREFIX})
-r <runtime> 指定运行时: auto|docker|nerdctl (默认: ${RUNTIME_PREF})
-d dry-run 模式,仅打印将要执行的命令
-s 跳过容器健康检查(不推荐)
-h 显示本帮助
环境变量(可覆盖默认值):
DMSDOCS_IMAGE_PREFIX, DMSDOCS_CONTAINER_NAME, DMSDOCS_NETWORK,
DMSDOCS_RESTART, DMSDOCS_LOG_MAX_SIZE, DMSDOCS_LOG_MAX_FILE,
DMSDOCS_PULL_RETRY, DMSDOCS_PULL_RETRY_INTERVAL,
DMSDOCS_HEALTH_TIMEOUT, DMSDOCS_STOP_TIMEOUT,
DMSDOCS_RUNTIME, DMSDOCS_NERDCTL_NAMESPACE
运行时选择策略:
auto 优先使用 docker;若 docker 不可用则回退到 nerdctl
docker 强制使用 docker
nerdctl 强制使用 nerdctl(默认 namespace=${NERDCTL_NAMESPACE})
示例:
${SCRIPT_NAME} -o v1.0.0-aistack-v3-404ee75
${SCRIPT_NAME} -o v1.0.0-aistack-v3-404ee75 -f
${SCRIPT_NAME} -o v1.0.0 -r nerdctl
DMSDOCS_CONTAINER_NAME=dms-doc-staging ${SCRIPT_NAME} -o v1.0.0 -d
EOF
}
# ------------------------------ 工具函数 --------------------------------------
run_cmd() {
if ${DRY_RUN}; then
log_info "[dry-run] $*"
return 0
fi
"$@"
}
# 调用容器运行时(docker 或 nerdctl)
rt() {
"${RUNTIME_CMD[@]}" "$@"
}
# 在 run_cmd 包裹下调用运行时(支持 dry-run)
run_rt() {
run_cmd "${RUNTIME_CMD[@]}" "$@"
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少依赖命令: $1"
exit 2
fi
}
# 检查给定运行时是否可用(命令存在且 daemon 可达)
runtime_available() {
local bin="$1"
command -v "${bin}" >/dev/null 2>&1 || return 1
case "${bin}" in
docker)
docker info >/dev/null 2>&1
;;
nerdctl)
# nerdctl 需要能连上 containerd,使用指定 namespace 探测
nerdctl -n "${NERDCTL_NAMESPACE}" info >/dev/null 2>&1
;;
*)
return 1
;;
esac
}
# 自动探测/选择运行时
detect_runtime() {
case "${RUNTIME_PREF}" in
docker)
if ! runtime_available docker; then
log_error "已指定 docker,但 docker 不可用(命令缺失或 daemon 不可达)"
exit 2
fi
RUNTIME_BIN="docker"; RUNTIME_NAME="docker"
RUNTIME_CMD=(docker)
;;
nerdctl)
if ! runtime_available nerdctl; then
log_error "已指定 nerdctl,但 nerdctl 不可用(命令缺失或 containerd 不可达)"
exit 2
fi
RUNTIME_BIN="nerdctl"; RUNTIME_NAME="nerdctl"
RUNTIME_CMD=(nerdctl -n "${NERDCTL_NAMESPACE}")
;;
auto|"")
if runtime_available docker; then
RUNTIME_BIN="docker"; RUNTIME_NAME="docker"
RUNTIME_CMD=(docker)
elif runtime_available nerdctl; then
RUNTIME_BIN="nerdctl"; RUNTIME_NAME="nerdctl"
RUNTIME_CMD=(nerdctl -n "${NERDCTL_NAMESPACE}")
log_warn "未检测到可用的 docker,回退使用 nerdctl (namespace=${NERDCTL_NAMESPACE})"
else
log_error "未检测到可用的容器运行时(docker / nerdctl 均不可用)"
exit 2
fi
;;
*)
log_error "无效的运行时配置: ${RUNTIME_PREF} (可选: auto|docker|nerdctl)"
exit 1
;;
esac
local ver
ver="$(rt version --format '{{.Server.Version}}' 2>/dev/null \
|| rt --version 2>/dev/null \
|| echo 'unknown')"
log_info "使用运行时: ${RUNTIME_NAME} (${ver})"
}
validate_tag() {
# 镜像 tag 合法字符: [A-Za-z0-9_.-],长度 <=128
if [[ ! "$1" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
log_error "非法的镜像 tag: '$1'"
exit 1
fi
}
container_exists() {
rt ps -a --format '{{.Names}}' | grep -Fxq "$1"
}
container_running() {
rt ps --format '{{.Names}}' | grep -Fxq "$1"
}
# ------------------------------ 业务步骤 --------------------------------------
preflight_check() {
require_cmd grep
require_cmd date
detect_runtime
log_info "环境检查通过"
}
pull_image() {
local image="$1"
local attempt=1
if ! ${FORCE_PULL} && rt image inspect "${image}" >/dev/null 2>&1; then
log_info "本地已存在镜像 ${image},跳过拉取(使用 -f 强制拉取)"
return 0
fi
while (( attempt <= PULL_RETRY )); do
log_info "拉取镜像 ${image} (第 ${attempt}/${PULL_RETRY} 次)"
if run_rt pull "${image}"; then
log_info "镜像拉取成功"
return 0
fi
log_warn "拉取失败,${PULL_RETRY_INTERVAL}s 后重试..."
sleep "${PULL_RETRY_INTERVAL}"
(( attempt++ ))
done
log_error "镜像拉取失败,已重试 ${PULL_RETRY} 次: ${image}"
exit 3
}
backup_old_container() {
# 滚动替换:将旧容器重命名为备份,启动失败时可回滚
local backup="${CONTAINER_NAME}-backup-$(date +%s)"
if container_exists "${CONTAINER_NAME}"; then
log_warn "发现旧容器 ${CONTAINER_NAME},重命名为 ${backup} 以便回滚"
if container_running "${CONTAINER_NAME}"; then
run_rt stop --time "${STOP_TIMEOUT}" "${CONTAINER_NAME}" >/dev/null
fi
run_rt rename "${CONTAINER_NAME}" "${backup}" >/dev/null
echo "${backup}"
else
echo ""
fi
}
rollback() {
local backup="$1"
[[ -z "${backup}" ]] && return 0
log_warn "执行回滚: 恢复容器 ${backup} -> ${CONTAINER_NAME}"
# 清理可能残留的同名失败容器
if container_exists "${CONTAINER_NAME}"; then
run_rt rm -f "${CONTAINER_NAME}" >/dev/null || true
fi
run_rt rename "${backup}" "${CONTAINER_NAME}" >/dev/null
run_rt start "${CONTAINER_NAME}" >/dev/null || true
log_info "回滚完成"
}
cleanup_backup() {
local backup="$1"
[[ -z "${backup}" ]] && return 0
log_info "清理备份容器 ${backup}"
run_rt rm -f "${backup}" >/dev/null || true
}
start_container() {
local image="$1"
log_info "启动新容器 ${CONTAINER_NAME} (image=${image}, runtime=${RUNTIME_NAME})"
# docker 与 nerdctl 通用的 run 参数
local -a run_args=(
run -d
--name="${CONTAINER_NAME}"
--network="${NETWORK_MODE}"
--restart="${RESTART_POLICY}"
--label "app=dms-doc"
--label "deployed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
--label "deployed_tag=${TAG}"
)
# 日志选项:docker 原生支持;nerdctl 也支持 json-file 驱动及 max-size/max-file
run_args+=(
--log-driver=json-file
--log-opt "max-size=${LOG_MAX_SIZE}"
--log-opt "max-file=${LOG_MAX_FILE}"
)
run_args+=("${image}")
run_rt "${run_args[@]}" >/dev/null
}
health_check() {
if ${SKIP_HEALTH} || ${DRY_RUN}; then
log_warn "已跳过健康检查"
return 0
fi
log_info "等待容器进入 running 状态(超时 ${HEALTH_CHECK_TIMEOUT}s)..."
local elapsed=0
while (( elapsed < HEALTH_CHECK_TIMEOUT )); do
local status
status="$(rt inspect -f '{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo 'missing')"
case "${status}" in
running)
# 再观察 3s,防止瞬间退出
sleep 3
status="$(rt inspect -f '{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo 'missing')"
if [[ "${status}" == "running" ]]; then
log_info "容器健康检查通过"
return 0
fi
;;
exited|dead)
log_error "容器已退出 (status=${status})"
rt logs --tail 50 "${CONTAINER_NAME}" >&2 || true
return 1
;;
esac
sleep 2
(( elapsed += 2 ))
done
log_error "健康检查超时,容器未达到 running 状态"
rt logs --tail 50 "${CONTAINER_NAME}" >&2 || true
return 1
}
print_summary() {
log_info "部署完成"
echo "----------------------------------------" >&2
rt ps -a --filter "name=^${CONTAINER_NAME}$" \
--format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' >&2 || true
echo "----------------------------------------" >&2
local cli="${RUNTIME_CMD[*]}"
log_info "查看日志: ${cli} logs -f ${CONTAINER_NAME}"
log_info "进入容器: ${cli} exec -it ${CONTAINER_NAME} bash"
}
# ------------------------------ 主流程 ----------------------------------------
main() {
while getopts "o:n:p:r:fdsh" opt; do
case "${opt}" in
o) TAG="${OPTARG}" ;;
n) CONTAINER_NAME="${OPTARG}" ;;
p) IMAGE_PREFIX="${OPTARG}" ;;
r) RUNTIME_PREF="${OPTARG}" ;;
f) FORCE_PULL=true ;;
d) DRY_RUN=true ;;
s) SKIP_HEALTH=true ;;
h) usage; exit 0 ;;
\?) usage; exit 1 ;;
esac
done
if [[ -z "${TAG}" ]]; then
log_error "必须通过 -o 指定镜像 tag"
usage
exit 1
fi
validate_tag "${TAG}"
local full_image="${IMAGE_PREFIX}:${TAG}"
log_info "目标镜像: ${full_image}"
log_info "容器名: ${CONTAINER_NAME}"
${DRY_RUN} && log_warn "当前为 DRY-RUN 模式,不会真实变更系统"
preflight_check
pull_image "${full_image}"
local backup
backup="$(backup_old_container)"
# 关闭 ERR trap,自行处理启动失败以便回滚
trap - ERR
if start_container "${full_image}" && health_check; then
cleanup_backup "${backup}"
trap 'on_error ${LINENO}' ERR
print_summary
exit 0
else
log_error "新容器启动/健康检查失败"
run_rt rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
rollback "${backup}"
exit 4
fi
}
main "$@"