479 lines
19 KiB
Bash
Executable File
479 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# 00-init-tools.sh — 安装并配置服务器基础工具环境
|
||
#
|
||
# 功能:
|
||
# 1. 安装 Go(默认 1.22.5,可通过参数指定版本)
|
||
# 2. 配置 GOPROXY(自动测速选最快节点)
|
||
# 3. 安装 Node.js / npm(前端依赖)
|
||
# 4. 安装 Docker(基础设施容器)
|
||
# 5. 安装 etcdctl(查看 Etcd 服务注册)
|
||
# 6. 安装 Nginx 并写入 PC/CMS/Build-CMS/Build-Down/OpenIM 反代(本机 HTTP :80;外部 HTTPS 由 LB/CDN 终止)
|
||
# 7. 写入 /etc/profile.d/deploy-env.sh(永久生效)
|
||
#
|
||
# 用法:
|
||
# ./deploy-test/00-init-tools.sh # 安装全部(含 Nginx 反代)
|
||
# ./deploy-test/00-init-tools.sh go # 只安装/配置 Go
|
||
# ./deploy-test/00-init-tools.sh node # 只安装 Node.js
|
||
# ./deploy-test/00-init-tools.sh docker # 只安装 Docker
|
||
# ./deploy-test/00-init-tools.sh goproxy # 只配置 GOPROXY
|
||
# sudo ./deploy-test/00-init-tools.sh etcdctl # 只安装 etcdctl(需 root)
|
||
# sudo ./deploy-test/00-init-tools.sh nginx # 只安装 Nginx 反代(需 root)
|
||
#
|
||
# 前置条件: root 或 sudo 权限,Ubuntu/Debian 系统
|
||
#
|
||
# 下一步: ./deploy-test/01-init-env.sh
|
||
# =============================================================================
|
||
set -euo pipefail
|
||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
||
init_dirs
|
||
init_script_log
|
||
|
||
# ── 可调参数 ───────────────────────────────────────────────────────────────────
|
||
GO_VERSION="${GO_VERSION:-1.22.5}"
|
||
GO_ARCH="${GO_ARCH:-amd64}" # amd64 / arm64
|
||
NODE_VERSION="${NODE_VERSION:-20}" # Node.js LTS 大版本
|
||
ETCDCTL_VERSION="${ETCDCTL_VERSION:-3.5.17}"
|
||
PROFILE_FILE="/etc/profile.d/deploy-env.sh"
|
||
|
||
TARGET="${1:-all}"
|
||
|
||
header "步骤 0 — 初始化工具环境"
|
||
echo " 目标: ${TARGET} (Go=${GO_VERSION}, Node=${NODE_VERSION})"
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 辅助函数
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
# 检查命令是否存在(不触发 set -e)
|
||
_has() { command -v "$1" &>/dev/null; }
|
||
|
||
# 追加到全局 profile(幂等:同一行不重复写)
|
||
_append_profile() {
|
||
local line="$1"
|
||
grep -qxF "$line" "$PROFILE_FILE" 2>/dev/null || echo "$line" >> "$PROFILE_FILE"
|
||
}
|
||
|
||
# 测试 GOPROXY 节点延迟,返回 ms(超时返回 9999)
|
||
_probe_proxy() {
|
||
local url="${1%/}/github.com/gin-gonic/gin/@v/list"
|
||
local ms
|
||
ms=$(curl -s -o /dev/null -w "%{time_total}" --max-time 5 "$url" 2>/dev/null || echo "9.999")
|
||
echo "${ms/./}" | sed 's/^0*//' | awk '{printf "%d\n", $1 * 1000 / 1000 + 0}'
|
||
# 简单地把秒转毫秒并取整
|
||
python3 -c "print(int(float('${ms}') * 1000))" 2>/dev/null || echo 9999
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 1. Go
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
_install_go() {
|
||
step "安装 Go ${GO_VERSION} (linux/${GO_ARCH})"
|
||
|
||
local tarball="go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
|
||
local url="https://go.dev/dl/${tarball}"
|
||
local tmp="/tmp/${tarball}"
|
||
|
||
if _has go; then
|
||
local cur
|
||
cur=$(go version | awk '{print $3}' | sed 's/go//')
|
||
if [[ "$cur" == "$GO_VERSION" ]]; then
|
||
success " Go ${GO_VERSION} 已安装,跳过"
|
||
return 0
|
||
fi
|
||
warn " 当前 Go 版本 ${cur},将升级到 ${GO_VERSION}"
|
||
fi
|
||
|
||
info " 下载 ${url}"
|
||
curl -fL --progress-bar -o "$tmp" "$url" || {
|
||
# 备用镜像
|
||
warn " go.dev 下载失败,尝试备用镜像..."
|
||
curl -fL --progress-bar -o "$tmp" "https://golang.google.cn/dl/${tarball}" || {
|
||
error " 下载失败,请手动安装: https://go.dev/dl/"
|
||
return 1
|
||
}
|
||
}
|
||
|
||
info " 解压到 /usr/local/go"
|
||
rm -rf /usr/local/go
|
||
tar -C /usr/local -xzf "$tmp"
|
||
rm -f "$tmp"
|
||
|
||
# 写入 profile
|
||
[[ -f "$PROFILE_FILE" ]] || touch "$PROFILE_FILE"
|
||
_append_profile 'export PATH=$PATH:/usr/local/go/bin'
|
||
_append_profile 'export GOPATH=/root/go'
|
||
_append_profile 'export GOMODCACHE=/root/go/pkg/mod'
|
||
|
||
# 当前会话也生效
|
||
export PATH=$PATH:/usr/local/go/bin
|
||
export GOPATH=/root/go
|
||
export GOMODCACHE=/root/go/pkg/mod
|
||
|
||
success " Go $(go version | awk '{print $3}') 安装完成 → /usr/local/go/bin/go"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 2. GOPROXY — 自动选最快节点
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
_config_goproxy() {
|
||
step "配置 GOPROXY(自动测速)"
|
||
|
||
if ! _has go; then
|
||
warn " go 未安装,跳过 GOPROXY 配置"
|
||
return 0
|
||
fi
|
||
|
||
declare -A PROXIES=(
|
||
["proxy.golang.org"]="https://proxy.golang.org"
|
||
["goproxy.cn"]="https://goproxy.cn"
|
||
["goproxy.io"]="https://goproxy.io"
|
||
)
|
||
|
||
local best_name="goproxy.io"
|
||
local best_ms=9999
|
||
|
||
info " 测速中..."
|
||
for name in "${!PROXIES[@]}"; do
|
||
local url="${PROXIES[$name]}"
|
||
local ms
|
||
ms=$(python3 -c "
|
||
import urllib.request, time, sys
|
||
try:
|
||
t = time.time()
|
||
urllib.request.urlopen('${url}/github.com/gin-gonic/gin/@v/list', timeout=5)
|
||
print(int((time.time()-t)*1000))
|
||
except:
|
||
print(9999)
|
||
" 2>/dev/null)
|
||
echo " ${name}: ${ms}ms"
|
||
if (( ms < best_ms )); then
|
||
best_ms=$ms
|
||
best_name=$name
|
||
best_url="${url}"
|
||
fi
|
||
done
|
||
|
||
# 构建代理列表:最快的放第一
|
||
local proxy_list="${best_url},https://proxy.golang.org,https://goproxy.cn,direct"
|
||
# 去重
|
||
proxy_list=$(echo "$proxy_list" | tr ',' '\n' | awk '!seen[$0]++' | tr '\n' ',' | sed 's/,$//')
|
||
|
||
go env -w GOPROXY="${proxy_list}"
|
||
go env -w GONOSUMDB="*"
|
||
go env -w GOFLAGS=""
|
||
|
||
# 同步写入 profile(go env -w 已持久化到 GOENV,这里写 profile 作为双保险)
|
||
_append_profile "export GOPROXY=${proxy_list}"
|
||
_append_profile 'export GONOSUMDB=*'
|
||
|
||
success " GOPROXY=${proxy_list}"
|
||
success " 最快节点: ${best_name} (${best_ms}ms)"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 3. Node.js
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
_install_node() {
|
||
step "安装 Node.js ${NODE_VERSION}.x LTS"
|
||
|
||
if _has node; then
|
||
local cur
|
||
cur=$(node --version)
|
||
info " 当前 Node.js 版本: ${cur}"
|
||
# 检查大版本是否匹配
|
||
local major
|
||
major=$(echo "$cur" | sed 's/v//' | cut -d. -f1)
|
||
if (( major >= NODE_VERSION )); then
|
||
success " Node.js >= ${NODE_VERSION},跳过"
|
||
return 0
|
||
fi
|
||
warn " 版本过旧,将升级"
|
||
fi
|
||
|
||
if _has apt-get; then
|
||
info " 使用 NodeSource 安装 Node.js ${NODE_VERSION}.x"
|
||
curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash -
|
||
apt-get install -y nodejs
|
||
elif _has yum; then
|
||
curl -fsSL "https://rpm.nodesource.com/setup_${NODE_VERSION}.x" | bash -
|
||
yum install -y nodejs
|
||
else
|
||
warn " 未找到 apt-get/yum,请手动安装 Node.js: https://nodejs.org/"
|
||
return 1
|
||
fi
|
||
|
||
# 安装常用全局包管理器
|
||
npm install -g pnpm yarn 2>/dev/null || true
|
||
|
||
_append_profile 'export PATH=$PATH:/usr/local/bin'
|
||
|
||
# 将 GitHub SSH 地址重写为 HTTPS(服务器通常没有 GitHub SSH 密钥)
|
||
git config --global url."https://github.com/".insteadOf "git+ssh://git@github.com/" 2>/dev/null || true
|
||
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" 2>/dev/null || true
|
||
git config --global url."https://github.com/".insteadOf "git@github.com:" 2>/dev/null || true
|
||
|
||
success " Node.js $(node --version),npm $(npm --version) 安装完成"
|
||
_has pnpm && success " pnpm $(pnpm --version)"
|
||
_has yarn && success " yarn $(yarn --version)"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 4. Docker
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
_install_docker() {
|
||
step "安装 Docker"
|
||
|
||
if _has docker; then
|
||
success " Docker $(docker --version | awk '{print $3}' | tr -d ',') 已安装,跳过"
|
||
return 0
|
||
fi
|
||
|
||
if _has apt-get; then
|
||
info " 使用官方脚本安装 Docker..."
|
||
curl -fsSL https://get.docker.com | sh
|
||
elif _has yum; then
|
||
yum install -y docker
|
||
systemctl enable docker
|
||
else
|
||
warn " 未找到 apt-get/yum,请手动安装 Docker: https://docs.docker.com/engine/install/"
|
||
return 1
|
||
fi
|
||
|
||
# 启动服务
|
||
systemctl start docker 2>/dev/null || service docker start 2>/dev/null || true
|
||
|
||
success " Docker $(docker --version | awk '{print $3}' | tr -d ',') 安装完成"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 5. etcdctl
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
_install_etcdctl() {
|
||
step "安装 etcdctl ${ETCDCTL_VERSION}"
|
||
|
||
if _has etcdctl; then
|
||
success " etcdctl 已安装: $(etcdctl version | head -1)"
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$(id -u)" -ne 0 ]]; then
|
||
warn " etcdctl 安装需 root,请执行: sudo $0 etcdctl"
|
||
return 0
|
||
fi
|
||
|
||
if _has apt-get; then
|
||
apt-get update -y
|
||
if apt-get install -y etcd-client; then
|
||
success " etcdctl 安装完成: $(etcdctl version | head -1)"
|
||
return 0
|
||
fi
|
||
warn " apt 安装 etcd-client 失败,尝试下载官方二进制"
|
||
elif _has dnf; then
|
||
if dnf install -y etcd; then
|
||
success " etcdctl 安装完成: $(etcdctl version | head -1)"
|
||
return 0
|
||
fi
|
||
warn " dnf 安装 etcd 失败,尝试下载官方二进制"
|
||
elif _has yum; then
|
||
if yum install -y etcd; then
|
||
success " etcdctl 安装完成: $(etcdctl version | head -1)"
|
||
return 0
|
||
fi
|
||
warn " yum 安装 etcd 失败,尝试下载官方二进制"
|
||
fi
|
||
|
||
local arch
|
||
case "$(uname -m)" in
|
||
x86_64|amd64) arch="amd64" ;;
|
||
aarch64|arm64) arch="arm64" ;;
|
||
*)
|
||
error " 不支持的架构: $(uname -m),请手动安装 etcdctl"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
local tarball="etcd-v${ETCDCTL_VERSION}-linux-${arch}.tar.gz"
|
||
local url="https://github.com/etcd-io/etcd/releases/download/v${ETCDCTL_VERSION}/${tarball}"
|
||
local tmp="/tmp/${tarball}"
|
||
local out_dir="/tmp/etcd-v${ETCDCTL_VERSION}-linux-${arch}"
|
||
|
||
info " 下载 ${url}"
|
||
curl -fL --progress-bar -o "$tmp" "$url"
|
||
rm -rf "$out_dir"
|
||
tar -C /tmp -xzf "$tmp"
|
||
install -m 0755 "${out_dir}/etcdctl" /usr/local/bin/etcdctl
|
||
rm -rf "$tmp" "$out_dir"
|
||
|
||
success " etcdctl 安装完成: $(etcdctl version | head -1)"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 6. Nginx — PC / CMS / Build-CMS / Build-Down / OpenIM 统一入口(本机 HTTP :80)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
_install_pc_nginx_proxy() {
|
||
step "安装 Nginx 并配置 OpenIM/PC/CMS/Build-CMS/Build-Down 反代"
|
||
|
||
if [[ "$(id -u)" -ne 0 ]]; then
|
||
error " Nginx 安装需 root,请执行: sudo $0 nginx"
|
||
return 1
|
||
fi
|
||
|
||
local script_dir
|
||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
local conf_src="${script_dir}/nginx/openim-pc-proxy.conf"
|
||
local conf_name="openim-pc-proxy.conf"
|
||
local proxy_domain="${PC_PROXY_DOMAIN:-pc-jack.imharry.work}"
|
||
|
||
if [[ -f "$ENV_FILE" ]]; then
|
||
# shellcheck source=/dev/null
|
||
source "$ENV_FILE"
|
||
proxy_domain="${PC_PROXY_DOMAIN:-$proxy_domain}"
|
||
fi
|
||
|
||
if [[ ! -f "$conf_src" ]]; then
|
||
error " 找不到配置: $conf_src"
|
||
return 1
|
||
fi
|
||
|
||
if ! _has nginx; then
|
||
if _has apt-get; then
|
||
apt-get update -y
|
||
apt-get install -y nginx
|
||
elif _has dnf; then
|
||
dnf install -y nginx
|
||
elif _has yum; then
|
||
yum install -y nginx
|
||
else
|
||
error " 未检测到 apt/dnf/yum,请先手动安装 nginx"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
if [[ -d /etc/nginx/sites-available ]]; then
|
||
install -m 0644 "$conf_src" "/etc/nginx/sites-available/${conf_name}"
|
||
mkdir -p /etc/nginx/sites-enabled
|
||
ln -sf "/etc/nginx/sites-available/${conf_name}" "/etc/nginx/sites-enabled/${conf_name}"
|
||
if [[ -f /etc/nginx/sites-enabled/default ]]; then
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
info " 已移除 sites-enabled/default,避免与 openim-pc-proxy 冲突"
|
||
fi
|
||
# 保留 sites-available/default 文件时,去掉 default_server,防止日后误启用再次抢占 :80
|
||
if [[ -f /etc/nginx/sites-available/default ]]; then
|
||
sed -i.bak-openim \
|
||
-e 's/listen \[::\]:80 default_server;/listen [::]:80;/g' \
|
||
-e 's/listen 80 default_server;/listen 80;/g' \
|
||
/etc/nginx/sites-available/default 2>/dev/null || true
|
||
info " 已去除 sites-available/default 中的 default_server(若存在)"
|
||
fi
|
||
else
|
||
install -m 0644 "$conf_src" "/etc/nginx/conf.d/${conf_name}"
|
||
fi
|
||
|
||
nginx -t
|
||
systemctl enable nginx 2>/dev/null || true
|
||
systemctl restart nginx
|
||
|
||
local health_failed=0
|
||
local host
|
||
for host in \
|
||
"$proxy_domain" \
|
||
"cms-jack.imharry.work" \
|
||
"build-jack.imharry.work" \
|
||
"down-jack.imharry.work"
|
||
do
|
||
if curl -fsS --max-time 3 -H "Host: ${host}" http://127.0.0.1/nginx-health >/dev/null 2>&1; then
|
||
success " Host 路由已生效: ${host}"
|
||
else
|
||
error " Host 路由检查失败: ${host}"
|
||
health_failed=1
|
||
fi
|
||
done
|
||
|
||
success " Nginx 反代已启用(配置: $conf_src)"
|
||
info " 本机 Nginx 仅监听 TCP 80;curl -sS -H 'Host: ${proxy_domain}' http://127.0.0.1/nginx-health 应返回 ok"
|
||
info " 外部 HTTPS 可由 LB/CDN/其它网关终止后转发到本机 :80"
|
||
info " PC 入口: https://${proxy_domain}/"
|
||
info " CMS 入口: http://cms-jack.imharry.work/"
|
||
info " Build CMS 入口: http://build-jack.imharry.work/"
|
||
info " Build Down 入口: http://down-jack.imharry.work/"
|
||
|
||
if [[ "$health_failed" -ne 0 ]]; then
|
||
error " Nginx 已重启,但部分 Host 路由校验失败,请检查 server_name / 默认站点 / 其它占用 :80 的配置"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# all 时非 root 则跳过(不中断 Go/Node/Docker/etcdctl)
|
||
_run_nginx_if_root() {
|
||
if [[ "$(id -u)" -eq 0 ]]; then
|
||
_install_pc_nginx_proxy
|
||
else
|
||
warn " 当前非 root,已跳过 Nginx。需要时在服务器上执行: sudo $0 nginx"
|
||
fi
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 执行
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
case "$TARGET" in
|
||
go)
|
||
_install_go
|
||
_config_goproxy
|
||
;;
|
||
goproxy)
|
||
_config_goproxy
|
||
;;
|
||
node)
|
||
_install_node
|
||
;;
|
||
docker)
|
||
_install_docker
|
||
;;
|
||
etcdctl)
|
||
_install_etcdctl
|
||
;;
|
||
nginx)
|
||
_install_pc_nginx_proxy
|
||
;;
|
||
all)
|
||
_install_go
|
||
_config_goproxy
|
||
_install_node
|
||
_install_docker
|
||
_install_etcdctl
|
||
_run_nginx_if_root
|
||
;;
|
||
*)
|
||
error "未知目标: $TARGET"
|
||
echo "用法: $0 [all|go|goproxy|node|docker|etcdctl|nginx]"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# 汇总
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
echo ""
|
||
success "环境初始化完成!"
|
||
echo ""
|
||
echo -e "${BOLD}当前工具版本:${NC}"
|
||
_has go && echo " Go: $(go version | awk '{print $3, $4}')" || echo " Go: 未安装"
|
||
_has node && echo " Node: $(node --version)" || echo " Node: 未安装"
|
||
_has npm && echo " npm: $(npm --version)" || echo " npm: 未安装"
|
||
_has pnpm && echo " pnpm: $(pnpm --version)" || echo " pnpm: 未安装"
|
||
_has yarn && echo " yarn: $(yarn --version)" || echo " yarn: 未安装"
|
||
_has docker && echo " Docker: $(docker --version | awk '{print $3}' | tr -d ',')" || echo " Docker: 未安装"
|
||
_has etcdctl && echo " etcdctl: $(etcdctl version | head -1 | sed 's/^etcdctl version: //')" || echo " etcdctl: 未安装"
|
||
_has nginx && echo " Nginx: $(nginx -v 2>&1 | sed 's/^nginx version: //')" || echo " Nginx: 未安装"
|
||
echo ""
|
||
echo -e "${BOLD}GOPROXY 配置:${NC}"
|
||
_has go && go env GOPROXY || echo " (go 未安装)"
|
||
echo ""
|
||
echo -e "${YELLOW}注意: 新终端需执行以下命令使环境变量生效:${NC}"
|
||
echo -e " ${CYAN}source /etc/profile.d/deploy-env.sh${NC}"
|
||
echo -e " 或重新登录 SSH"
|
||
echo ""
|
||
echo -e "${BOLD}下一步:${NC}"
|
||
echo -e " ${CYAN}./deploy-test/01-init-env.sh${NC}"
|