Files
tools/gcp/create_ai_projects.sh
Wang Defa 89f24a7fef fix: 修复所有脚本的 process substitution 兼容性问题
## 问题描述

在使用 `set -u` 严格模式时,`source <(curl ...)` 或 `source <(wget ...)`
的 process substitution 方式会在脚本退出时产生错误:

```
/dev/fd/63: line 1: fifo: unbound variable
```

## 根本原因

Process substitution 创建的临时文件描述符(如 /dev/fd/63)在退出时
与 Bash 的 `set -u` 严格模式存在兼容性问题,导致错误消息。

## 修复方案

将 process substitution 替换为临时文件方案:

**旧方案(有问题):**
```bash
source <(curl -fsSL "$url")
```

**新方案(兼容性好):**
```bash
temp_loader=$(mktemp)
curl -fsSL "$url" -o "$temp_loader"
source "$temp_loader"
rm -f "$temp_loader"
```

## 修改的文件

批量修复了所有 7 个脚本的远程加载逻辑:

- oci/create_instance.sh
- linux/create_raid0_array.sh
- linux/install_oh_my_zsh.sh
- linux/repartition_disks.sh
- gcp/create_ai_projects.sh
- gcp/delete_all_projects.sh
- common/demo_usage.sh

## 优势

-  避免 process substitution 的兼容性问题
-  与 `set -u` 严格模式完全兼容
-  显式的临时文件管理,更易理解
-  确保所有分支都正确清理临时文件
-  保持 curl/wget 双重支持不变
2025-12-26 15:20:51 +08:00

582 lines
18 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================================
# 文件名: create_ai_projects.sh
# 描述: 批量创建 Google Cloud Platform AI 项目并配置相关服务
# 作者: Cloud Tools Project
# 版本: 2.1.0(支持远程库加载)
# ============================================================================
set -euo pipefail # 启用严格模式
# ============================================================================
# 远程库加载配置
# ============================================================================
# 远程仓库 URL可通过环境变量覆盖
readonly REMOTE_BASE_URL="${REMOTE_LIB_URL:-https://gitea.bcde.io/wangdefa/tools/raw/branch/main}"
# 获取脚本目录(用于本地加载)
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
#
# 智能加载公共库
#
# 加载策略:
# 1. 如果 FORCE_REMOTE=1强制使用远程库
# 2. 否则尝试使用本地库
# 3. 本地库不存在时自动回退到远程库
#
load_common_libs() {
local use_remote=false
# 检查是否强制远程
if [[ "${FORCE_REMOTE:-0}" == "1" ]]; then
echo "[INFO] 强制使用远程库 (FORCE_REMOTE=1)" >&2
use_remote=true
# 检查本地库是否存在
elif [[ -f "${PROJECT_ROOT}/common/logging.sh" ]] && [[ -f "${PROJECT_ROOT}/common/error_handler.sh" ]]; then
# shellcheck disable=SC1091
source "${PROJECT_ROOT}/common/logging.sh"
# shellcheck disable=SC1091
source "${PROJECT_ROOT}/common/error_handler.sh"
return 0
else
echo "[WARN] 本地库不存在,使用远程库" >&2
use_remote=true
fi
# 使用远程库
if [[ "$use_remote" == "true" ]]; then
# 下载到临时文件(避免 process substitution 与 set -u 的交互问题)
local temp_loader
temp_loader=$(mktemp)
if command -v curl &>/dev/null; then
echo "[INFO] 使用 curl 下载远程库..." >&2
if curl -fsSL "${REMOTE_BASE_URL}/common/remote_loader.sh" -o "$temp_loader" 2>/dev/null; then
# shellcheck disable=SC1090
if source "$temp_loader"; then
rm -f "$temp_loader"
return 0
fi
fi
elif command -v wget &>/dev/null; then
echo "[INFO] 使用 wget 下载远程库..." >&2
if wget -qO "$temp_loader" "${REMOTE_BASE_URL}/common/remote_loader.sh" 2>/dev/null; then
# shellcheck disable=SC1090
if source "$temp_loader"; then
rm -f "$temp_loader"
return 0
fi
fi
fi
rm -f "$temp_loader"
echo "[ERROR] 无法加载公共库" >&2
echo "[ERROR] - 本地库不存在" >&2
echo "[ERROR] - 远程下载失败(需要 curl 或 wget" >&2
echo "[ERROR] - 仓库 URL: ${REMOTE_BASE_URL}" >&2
exit 1
fi
}
# 加载公共库
load_common_libs
# 默认配置
readonly DEFAULT_PROJECT_ID_PREFIX="project"
readonly DEFAULT_START_NUM=1
readonly DEFAULT_REPEAT_NUM=5
readonly DEFAULT_MAX_RETRIES=3
readonly DEFAULT_RETRY_DELAY=5
# 创建日志文件
readonly LOG_FILE="created_projects_$(date +%Y%m%d_%H%M%S).log"
log_set_file "$LOG_FILE"
#
# 显示脚本使用方法
#
usage() {
log_info "用法: $0 [选项]"
log_info "选项:"
log_info " -p, --prefix PREFIX 项目ID前缀 (默认: $DEFAULT_PROJECT_ID_PREFIX)"
log_info " -s, --start START 项目编号起始值 (默认: $DEFAULT_START_NUM)"
log_info " -n, --number NUMBER 创建项目的数量 (默认: $DEFAULT_REPEAT_NUM)"
log_info " --max-retries RETRIES 单个命令失败时的最大重试次数 (默认: $DEFAULT_MAX_RETRIES)"
log_info " --retry-delay DELAY 每次重试之间的延迟秒数 (默认: $DEFAULT_RETRY_DELAY)"
log_info " --debug 启用调试模式"
log_info " -h, --help 显示此帮助信息"
}
#
# 检查必要的命令是否存在
#
check_command() {
if ! command -v "$1" &> /dev/null; then
log_error "命令 $1 未找到。请确保它已安装并在PATH中。"
exit 1
fi
}
#
# 重试执行命令
#
retry_command() {
local max_attempts=$1
local delay=$2
shift 2
local cmd_to_run=("$@")
local attempt=1
local exit_code
local temp_output
local temp_error
while [ $attempt -le "$max_attempts" ]; do
log_info "尝试执行 (第 $attempt/$max_attempts 次): ${cmd_to_run[*]}"
# 创建临时文件捕获输出和错误
temp_output=$(mktemp)
temp_error=$(mktemp)
if "${cmd_to_run[@]}" > "$temp_output" 2> "$temp_error"; then
log_success "命令成功执行: ${cmd_to_run[*]}"
# 如果有输出,显示它
if [ -s "$temp_output" ]; then
cat "$temp_output"
fi
rm -f "$temp_output" "$temp_error" 2>/dev/null || true
return 0
else
exit_code=$?
log_warning "命令执行失败 (第 $attempt/$max_attempts 次),退出码: $exit_code"
# 显示错误信息用于调试
if [ -s "$temp_error" ]; then
log_error "错误输出: $(cat "$temp_error")"
fi
if [ $attempt -lt "$max_attempts" ]; then
log_info "将在 $delay 秒后重试..."
sleep "$delay"
fi
fi
rm -f "$temp_output" "$temp_error" 2>/dev/null || true
((attempt++))
done
log_error "命令在 $max_attempts 次尝试后仍然失败: ${cmd_to_run[*]}"
return 1
}
#
# 重试并获取命令输出
#
retry_command_with_output() {
local max_attempts=$1
local delay=$2
local output_var=$3
shift 3
local cmd_to_run=("$@")
local attempt=1
local exit_code
local temp_output
local temp_error
while [ $attempt -le "$max_attempts" ]; do
log_info "尝试执行 (第 $attempt/$max_attempts 次): ${cmd_to_run[*]}"
temp_output=$(mktemp)
temp_error=$(mktemp)
if "${cmd_to_run[@]}" > "$temp_output" 2> "$temp_error"; then
log_success "命令成功执行: ${cmd_to_run[*]}"
# 将输出赋值给指定变量
eval "$output_var=\$(cat '$temp_output')"
rm -f "$temp_output" "$temp_error" 2>/dev/null || true
return 0
else
exit_code=$?
log_warning "命令执行失败 (第 $attempt/$max_attempts 次),退出码: $exit_code"
if [ -s "$temp_error" ]; then
log_error "错误输出: $(cat "$temp_error")"
fi
if [ $attempt -lt "$max_attempts" ]; then
log_info "将在 $delay 秒后重试..."
sleep "$delay"
fi
fi
rm -f "$temp_output" "$temp_error" 2>/dev/null || true
((attempt++))
done
log_error "命令在 $max_attempts 次尝试后仍然失败: ${cmd_to_run[*]}"
return 1
}
#
# 创建 GCP 项目
#
create_project() {
local project_id=$1
local max_retries=$2
local retry_delay=$3
log_info "创建项目: $project_id"
# 检查项目是否已存在
if gcloud projects describe "$project_id" &>/dev/null; then
log_warning "项目 $project_id 已存在,跳过创建"
return 0
fi
if retry_command "$max_retries" "$retry_delay" gcloud projects create "$project_id" --name="$project_id"; then
log_success "项目 $project_id 创建成功"
return 0
else
log_error "创建项目 $project_id 失败 (已重试)"
return 1
fi
}
#
# 链接结算账号
#
link_billing() {
local project_id=$1
local billing_id=$2
local max_retries=$3
local retry_delay=$4
log_info "链接结算账号到项目: $project_id"
if retry_command "$max_retries" "$retry_delay" gcloud beta billing projects link "$project_id" --billing-account="$billing_id"; then
log_success "结算账号链接成功"
return 0
else
log_error "链接结算账号到项目 $project_id 失败 (已重试)"
return 1
fi
}
#
# 启用服务
#
enable_services() {
local project_id=$1
local max_retries=$2
local retry_delay=$3
log_info "启用 aiplatform.googleapis.com 服务"
if retry_command "$max_retries" "$retry_delay" gcloud services enable aiplatform.googleapis.com --project="$project_id"; then
log_success "服务启用成功"
return 0
else
log_error "启用 aiplatform.googleapis.com 服务失败 (已重试)"
return 1
fi
}
#
# 创建服务账号
#
create_service_account() {
local project_id=$1
local max_retries=$2
local retry_delay=$3
log_info "创建服务账号 service-account@$project_id.iam.gserviceaccount.com"
# 检查服务账号是否已存在
if gcloud iam service-accounts describe "service-account@$project_id.iam.gserviceaccount.com" --project="$project_id" &>/dev/null; then
log_warning "服务账号已存在,跳过创建"
return 0
fi
if retry_command "$max_retries" "$retry_delay" gcloud iam service-accounts create service-account \
--display-name="AI Platform Service Account" \
--project="$project_id"; then
log_success "服务账号创建成功"
return 0
else
log_error "创建服务账号失败 (已重试)"
return 1
fi
}
#
# 添加 IAM 策略
#
add_iam_policy() {
local project_id=$1
local max_retries=$2
local retry_delay=$3
log_info "授予 aiplatform.serviceAgent 角色"
if retry_command "$max_retries" "$retry_delay" gcloud projects add-iam-policy-binding "$project_id" \
--member="serviceAccount:service-account@$project_id.iam.gserviceaccount.com" \
--role="roles/aiplatform.serviceAgent"; then
log_success "IAM 策略授予成功"
return 0
else
log_error "授予 IAM 策略失败 (已重试)"
return 1
fi
}
#
# 创建服务账号密钥
#
create_service_account_key() {
local project_id=$1
local max_retries=$2
local retry_delay=$3
log_info "创建服务账号密钥"
# 检查密钥文件是否已存在
if [ -f "pass-$project_id.json" ]; then
log_warning "密钥文件 pass-$project_id.json 已存在,跳过创建"
return 0
fi
if retry_command "$max_retries" "$retry_delay" gcloud iam service-accounts keys create "pass-$project_id.json" \
--iam-account="service-account@$project_id.iam.gserviceaccount.com" \
--project="$project_id"; then
log_success "服务账号密钥创建成功: pass-$project_id.json"
return 0
else
log_error "创建服务账号密钥失败 (已重试)"
return 1
fi
}
# 参数解析和验证
PROJECT_ID_PREFIX="$DEFAULT_PROJECT_ID_PREFIX"
START_NUM="$DEFAULT_START_NUM"
REPEAT_NUM="$DEFAULT_REPEAT_NUM"
MAX_RETRIES="$DEFAULT_MAX_RETRIES"
RETRY_DELAY="$DEFAULT_RETRY_DELAY"
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case $1 in
-p|--prefix)
PROJECT_ID_PREFIX="$2"
shift 2
;;
-s|--start)
START_NUM="$2"
shift 2
;;
-n|--number)
REPEAT_NUM="$2"
shift 2
;;
--max-retries)
MAX_RETRIES="$2"
shift 2
;;
--retry-delay)
RETRY_DELAY="$2"
shift 2
;;
--debug)
DEBUG_MODE=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
log_error "未知选项: $1"
usage
exit 1
;;
esac
done
# 检查必要的命令
check_command gcloud
# 验证参数
if ! [[ "$START_NUM" =~ ^[0-9]+$ ]]; then
log_error "起始编号必须是一个正整数"
exit 1
fi
if ! [[ "$REPEAT_NUM" =~ ^[0-9]+$ ]] || [ "$REPEAT_NUM" -lt 1 ]; then
log_error "项目数量必须是一个正整数"
exit 1
fi
if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -lt 0 ]; then
log_error "最大重试次数必须是一个非负整数"
exit 1
fi
if ! [[ "$RETRY_DELAY" =~ ^[0-9]+$ ]] || [ "$RETRY_DELAY" -lt 0 ]; then
log_error "重试延迟必须是一个非负整数"
exit 1
fi
log_info "################################################################################"
log_info "开始创建 GCP 项目批量任务"
log_info "################################################################################"
# 调试模式提示
if [ "$DEBUG_MODE" = true ]; then
log_debug "调试模式已启用"
fi
# 获取结算账号
log_info "获取结算账号..."
billing_id=""
if retry_command_with_output "$MAX_RETRIES" "$RETRY_DELAY" billing_id_output gcloud beta billing accounts list --format="value(name.basename())" --filter="open=true"; then
# 提取第一个结算账号ID
billing_id=$(echo "$billing_id_output" | head -n 1 | tr -d '[:space:]')
if [ -z "$billing_id" ]; then
log_error "未找到有效的活动结算账号。请检查您的结算账户。"
exit 1
fi
log_success "结算账号: $billing_id"
else
log_error "获取结算账号失败 (已重试)。请检查您的结算账户。"
exit 1
fi
# 计算结束编号
END_NUM=$((START_NUM + REPEAT_NUM - 1))
log_info "配置信息:"
log_info " 项目前缀: $PROJECT_ID_PREFIX"
log_info " 起始编号: $START_NUM"
log_info " 结束编号: $END_NUM"
log_info " 项目总数: $REPEAT_NUM"
log_info " 最大重试: $MAX_RETRIES"
log_info " 重试延迟: $RETRY_DELAY"
# 显示将要创建的项目列表
log_info "将创建以下项目:"
for i in $(seq "$START_NUM" "$END_NUM"); do
PROJECT_ID="${PROJECT_ID_PREFIX}$(printf "%02d" "$i")"
log_info " - $PROJECT_ID"
done
# 确认开始创建
log_warning "准备开始创建项目..."
sleep 1
# 统计变量
successful_projects=0
failed_projects=0
# 创建项目和相关资源 - 主循环
log_debug "开始主循环,从 $START_NUM$END_NUM"
for i in $(seq "$START_NUM" "$END_NUM"); do
PROJECT_ID="${PROJECT_ID_PREFIX}$(printf "%02d" "$i")"
current_num=$((i - START_NUM + 1))
log_info "-----------------------------------------------------"
log_info "开始处理项目 ($current_num/$REPEAT_NUM): $PROJECT_ID"
log_debug "当前循环变量 i=$i, 项目ID=$PROJECT_ID"
log_info "-----------------------------------------------------"
# 执行所有步骤
project_success=true
log_debug "步骤1: 创建项目"
if ! create_project "$PROJECT_ID" "$MAX_RETRIES" "$RETRY_DELAY"; then
log_debug "创建项目失败"
project_success=false
fi
if [ "$project_success" = true ]; then
log_debug "步骤2: 链接结算账号"
if ! link_billing "$PROJECT_ID" "$billing_id" "$MAX_RETRIES" "$RETRY_DELAY"; then
log_debug "链接结算账号失败"
project_success=false
fi
fi
if [ "$project_success" = true ]; then
log_debug "步骤3: 启用服务"
if ! enable_services "$PROJECT_ID" "$MAX_RETRIES" "$RETRY_DELAY"; then
log_debug "启用服务失败"
project_success=false
fi
fi
if [ "$project_success" = true ]; then
log_debug "步骤4: 创建服务账号"
if ! create_service_account "$PROJECT_ID" "$MAX_RETRIES" "$RETRY_DELAY"; then
log_debug "创建服务账号失败"
project_success=false
fi
fi
if [ "$project_success" = true ]; then
log_debug "步骤5: 添加IAM策略"
if ! add_iam_policy "$PROJECT_ID" "$MAX_RETRIES" "$RETRY_DELAY"; then
log_debug "添加IAM策略失败"
project_success=false
fi
fi
if [ "$project_success" = true ]; then
log_debug "步骤6: 创建服务账号密钥"
if ! create_service_account_key "$PROJECT_ID" "$MAX_RETRIES" "$RETRY_DELAY"; then
log_debug "创建服务账号密钥失败"
project_success=false
fi
fi
# 统计结果
if [ "$project_success" = true ]; then
log_success "项目 $PROJECT_ID 所有操作处理完成"
((successful_projects++))
else
log_warning "项目 $PROJECT_ID 处理过程中出现错误,部分操作可能未完成。"
((failed_projects++))
fi
# 添加延迟,避免 API 限制
log_debug "等待2秒后继续下一个项目..."
sleep 2
log_debug "完成项目 $PROJECT_ID 的处理,准备处理下一个项目"
done
log_debug "主循环结束"
# 显示结果
log_info "================== 执行完成 =================="
log_success "成功创建: $successful_projects 个项目"
log_error "创建失败: $failed_projects 个项目"
log_info "详细日志: $LOG_FILE"
echo "================== 创建完成于: $(date) ==================" >> "$LOG_FILE"
# 显示成功创建的项目列表
if [ "$successful_projects" -gt 0 ]; then
log_success "成功创建的项目:"
for i in $(seq "$START_NUM" "$END_NUM"); do
PROJECT_ID="${PROJECT_ID_PREFIX}$(printf "%02d" "$i")"
if [ -f "pass-$PROJECT_ID.json" ]; then
log_info "$PROJECT_ID (密钥文件: pass-$PROJECT_ID.json)"
fi
done
fi
if [ "$failed_projects" -gt 0 ]; then
log_error "$failed_projects 个项目在处理过程中遇到无法恢复的错误。"
log_info "请检查上面的日志以获取详细信息。"
exit 1
fi
log_success "所有项目创建/配置完成。"