Files
tools/oci/create_instance.sh
Wang Defa b34e1ef872 feat: 为所有脚本添加远程库加载支持
## 功能概述

实现了智能远程库加载机制,允许脚本从远程 Gitea 仓库动态下载公共库,
同时保留本地库优先的策略,确保脚本可以独立运行而不依赖本地项目结构。

## 变更内容

### 新增文件

- **common/remote_loader.sh**: 核心远程加载器
  - 支持 curl/wget 双重下载方式
  - 临时文件自动清理机制
  - 环境变量可配置仓库 URL

- **examples/remote_example.sh**: 纯远程模式示例
  - 演示完全独立的远程加载脚本
  - 适合单文件分发场景

- **examples/hybrid_loader_template.sh**: 混合模式模板
  - 本地优先 + 远程回退策略
  - 适合仓库内脚本

- **examples/REMOTE_LOADER_README.md**: 完整使用文档
  - 三种使用模式详解
  - 故障排除指南
  - 最佳实践建议

### 更新脚本(版本 2.1.0)

所有主要脚本均升级到 2.1.0 版本,支持智能库加载:

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

## 加载策略

1. 检查 FORCE_REMOTE=1 环境变量 → 强制远程
2. 检查本地库存在性 → 优先使用本地
3. 本地不存在 → 自动回退远程
4. 远程失败 → 友好错误提示

## 使用示例

### 本地模式(默认)
```bash
./linux/create_raid0_array.sh
```

### 强制远程模式
```bash
FORCE_REMOTE=1 ./linux/create_raid0_array.sh
```

### 自定义仓库 URL
```bash
REMOTE_LIB_URL=https://example.com/repo ./script.sh
```

## 技术特性

-  向后兼容:默认使用本地库,行为不变
-  离线可用:本地库存在时无需网络
-  独立分发:支持单文件脚本分发
-  错误处理:详细的错误信息和诊断建议
-  环境可配:REMOTE_LIB_URL 和 FORCE_REMOTE
-  自动清理:临时文件通过 trap 自动删除
2025-12-26 15:00:06 +08:00

446 lines
14 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_instance.sh
# 描述: Oracle Cloud Infrastructure 虚拟机批量部署工具
# 作者: 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
if command -v curl &>/dev/null; then
echo "[INFO] 使用 curl 下载远程库..." >&2
# shellcheck disable=SC1090
if source <(curl -fsSL "${REMOTE_BASE_URL}/common/remote_loader.sh" 2>/dev/null); then
return 0
fi
elif command -v wget &>/dev/null; then
echo "[INFO] 使用 wget 下载远程库..." >&2
# shellcheck disable=SC1090
if source <(wget -qO- "${REMOTE_BASE_URL}/common/remote_loader.sh" 2>/dev/null); then
return 0
fi
fi
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_NUMBER=1
readonly DEFAULT_SHAPE="VM.Standard.A1.Flex"
readonly DEFAULT_SHAPE_CONFIG="1+6"
readonly DEFAULT_IMAGE_NAME="Canonical-Ubuntu-20.04-aarch64-2025.05.20-0"
readonly DEFAULT_BOOT_VOLUME_SIZE=0
readonly DEFAULT_VPU=120
readonly DEFAULT_DOMAIN=0
#
# 检查必要的依赖工具是否安装
#
check_dependencies() {
local deps=("oci" "jq" "base64" "mkfifo")
local missing=()
for cmd in "${deps[@]}"; do
if ! check_command "$cmd"; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_error "缺少必要依赖: ${missing[*]}"
exit 1
fi
log_success "运行环境检查通过"
}
#
# 生成随机安全密码
#
# 返回:
# 16位随机密码
#
generate_password() {
LC_ALL=C tr -dc 'A-Za-z0-9!#%&()*+,-.:;<=>?@[]^_~' </dev/urandom | head -c 16
}
#
# 生成实例初始化的 cloud-init 用户数据
#
# 参数:
# $1 - root 密码
#
# 返回:
# Base64 编码的 user_data
#
generate_user_data() {
local password="$1"
cat <<EOF | base64 -w 0
#!/bin/bash
echo root:"$password" | chpasswd
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/g' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/g' /etc/ssh/sshd_config
rm -f /etc/ssh/sshd_config.d/* /etc/ssh/ssh_config.d/*
systemctl restart sshd
EOF
}
#
# 生成实例规格配置
#
# 参数:
# $1 - 规格配置字符串 (格式: CPU+内存 或 CPU+内存+基线)
#
# 返回:
# JSON 格式的规格配置
#
generate_shape_config() {
local config_str="$1"
IFS='+' read -ra parts <<< "$config_str"
case "${#parts[@]}" in
2)
echo "{\"ocpus\":${parts[0]},\"memoryInGBs\":${parts[1]}}"
;;
3)
case "${parts[2]}" in
0.125)
echo "{\"ocpus\":${parts[0]},\"memoryInGBs\":${parts[1]},\"baselineOcpuUtilization\":\"BASELINE_1_8\"}"
;;
0.5)
echo "{\"ocpus\":${parts[0]},\"memoryInGBs\":${parts[1]},\"baselineOcpuUtilization\":\"BASELINE_1_2\"}"
;;
*)
log_error "无效的 baseline 配置: ${parts[2]}"
exit 1
;;
esac
;;
*)
log_error "无效的 shape 配置格式: $config_str"
exit 1
;;
esac
}
#
# 配置 OCI 网络资源
#
# 参数:
# $1 - Compartment OCID
# $2 - FIFO 文件路径用于返回子网ID
#
configure_network() {
local ocid="$1"
local fifo="$2"
local vcn_dns="vcn$(shuf -i 100000-999999 -n 1)"
local subnet_dns="subnet$(shuf -i 100000-999999 -n 1)"
# 创建 VCN
log_info "正在创建虚拟网络 (DNS: $vcn_dns)"
local vcn_id
vcn_id=$(oci network vcn create \
--compartment-id "$ocid" \
--cidr-blocks '["10.0.0.0/16"]' \
--is-ipv6-enabled true \
--dns-label "$vcn_dns" \
--query 'data.id' \
--raw-output 2>/dev/null)
check_not_empty "$vcn_id" "vcn_id"
log_success "VCN 创建成功 (ID: $vcn_id)"
# 生成 IPv6 CIDR
log_info "正在生成 IPv6 CIDR..."
local ipv6_cidr
ipv6_cidr=$(oci network vcn get --vcn-id "$vcn_id" --query 'data."ipv6-cidr-blocks"[0]' --raw-output)
ipv6_cidr="${ipv6_cidr%00::*}$((RANDOM%90+10))::/64"
log_success "IPv6 CIDR 生成成功: $ipv6_cidr"
# 创建子网
log_info "正在创建子网 (DNS: $subnet_dns)"
local subnet_id
subnet_id=$(OCI_CLI_SUPPRESS_JSON_OUTPUT=true oci network subnet create \
--compartment-id "$ocid" \
--vcn-id "$vcn_id" \
--cidr-block "10.0.0.0/16" \
--ipv6-cidr-block "$ipv6_cidr" \
--dns-label "$subnet_dns" \
--query 'data.id' \
--raw-output 2>/dev/null)
check_not_empty "$subnet_id" "subnet_id"
log_success "子网创建成功 (ID: $subnet_id)"
# 配置网关
log_info "正在配置网络网关..."
local gateway_id
gateway_id=$(oci network internet-gateway create \
--compartment-id "$ocid" \
--vcn-id "$vcn_id" \
--is-enabled true \
--query 'data.id' \
--raw-output 2>/dev/null)
check_return $? "网关创建失败"
log_success "网关创建成功 (ID: $gateway_id)"
# 配置路由表
log_info "正在配置路由表..."
local rt_id
rt_id=$(oci network route-table list \
--compartment-id "$ocid" \
--vcn-id "$vcn_id" \
--query 'data[0].id' \
--raw-output 2>/dev/null)
check_not_empty "$rt_id" "rt_id"
oci network route-table update --rt-id "$rt_id" \
--route-rules "[
{\"cidrBlock\":\"0.0.0.0/0\",\"networkEntityId\":\"$gateway_id\"},
{\"cidrBlock\":\"0:0:0:0:0:0:0:0/0\",\"networkEntityId\":\"$gateway_id\"}
]" --force >/dev/null
check_return $? "路由配置失败"
log_success "路由表配置成功"
# 配置安全组
log_info "正在配置安全组规则..."
local sg_id
sg_id=$(oci network security-list list \
--compartment-id "$ocid" \
--vcn-id "$vcn_id" \
--query 'data[0].id' \
--raw-output 2>/dev/null)
oci network security-list update --security-list-id "$sg_id" \
--ingress-security-rules '[]' --force >/dev/null
check_return $? "安全组清空失败"
oci network security-list update --security-list-id "$sg_id" \
--ingress-security-rules "[
{\"protocol\":\"all\",\"source\":\"0.0.0.0/0\",\"sourceType\":\"CIDR_BLOCK\"},
{\"protocol\":\"all\",\"source\":\"::/0\",\"sourceType\":\"CIDR_BLOCK\"}
]" --force >/dev/null
check_return $? "安全组规则更新失败"
log_success "安全组规则配置成功"
# 返回子网ID通过 FIFO
echo "$subnet_id" > "$fifo"
}
#
# 显示帮助信息
#
show_help() {
cat <<EOF
用法: ${0##*/} [选项]
选项:
-n, --number <数量> 创建实例数量 (默认: $DEFAULT_NUMBER)
--ocid <OCID> 区域ID (自动获取)
--shape <形状> 实例规格 (默认: $DEFAULT_SHAPE)
--shape_config <配置> 资源配置格式: CPU+内存[+基线] (默认: $DEFAULT_SHAPE_CONFIG)
--subnet_id <子网ID> 指定现有子网ID (自动创建)
--image_name <镜像名> 系统镜像名称 (默认: $DEFAULT_IMAGE_NAME)
--vpu <数值> 启动卷性能单位 (默认: $DEFAULT_VPU)
--boot_volume_size <GB> 启动卷大小 (默认: 自动)
--domain <索引> 可用域序号 (默认: $DEFAULT_DOMAIN)
--password <密码> 指定 root 密码 (默认自动生成)
-h, --help 显示本帮助信息
示例:
$0 -n 3 --shape VM.Standard.E4.Flex --shape_config 2+16
EOF
}
#
# 主函数 - 批量创建 OCI 实例
#
main() {
log_info "============ OCI 实例批量部署工具 ============"
# 参数初始化
local number=$DEFAULT_NUMBER
local shape=$DEFAULT_SHAPE
local shape_config=$DEFAULT_SHAPE_CONFIG
local image_name=$DEFAULT_IMAGE_NAME
local boot_size=$DEFAULT_BOOT_VOLUME_SIZE
local vpu=$DEFAULT_VPU
local domain=$DEFAULT_DOMAIN
local ocid password subnet_id
# 参数解析
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--number) number="$2"; shift 2 ;;
--ocid) ocid="$2"; shift 2 ;;
--shape) shape="$2"; shift 2 ;;
--shape_config) shape_config="$2"; shift 2 ;;
--subnet_id) subnet_id="$2"; shift 2 ;;
--image_name) image_name="$2"; shift 2 ;;
--vpu) vpu="$2"; shift 2 ;;
--boot_volume_size) boot_size="$2"; shift 2 ;;
--domain) domain="$2"; shift 2 ;;
--password) password="$2"; shift 2 ;;
-h|--help) show_help; exit 0 ;;
*) log_error "无效参数: $1"; exit 1 ;;
esac
done
# 环境检查
check_dependencies
# 自动生成密码
if [[ -z "${password:-}" ]]; then
password=$(generate_password)
fi
log_warning "使用密码: $password"
# OCID 自动获取
if [[ -z "${ocid:-}" ]]; then
log_info "正在获取 OCID..."
ocid=$(oci iam availability-domain list --query 'data[0]."compartment-id"' --raw-output 2>/dev/null)
check_not_empty "$ocid" "ocid"
fi
log_success "OCID 获取成功: $ocid"
# 网络配置
if [[ -z "${subnet_id:-}" ]]; then
log_info "正在配置网络..."
local fifo
fifo=$(mktemp -u)
mkfifo "$fifo"
trap 'rm -f "$fifo"' EXIT
configure_network "$ocid" "$fifo" &
read subnet_id < "$fifo"
log_success "网络配置完成 (子网ID: $subnet_id)"
else
log_info "使用指定的子网ID: $subnet_id"
fi
# 生成配置
local shape_cfg
shape_cfg=$(generate_shape_config "$shape_config")
local image_id
image_id=$(oci compute image list \
--compartment-id "$ocid" \
--display-name "$image_name" \
--query 'data[0].id' \
--raw-output 2>/dev/null)
check_return $? "镜像查询失败"
local source_cfg
if [[ $boot_size -eq 0 ]] || [[ $boot_size -lt 50 ]]; then
source_cfg="{\"sourceType\":\"image\",\"imageId\":\"$image_id\",\"bootVolumeVpusPerGB\":$vpu}"
else
source_cfg="{\"sourceType\":\"image\",\"imageId\":\"$image_id\",\"bootVolumeVpusPerGB\":$vpu,\"bootVolumeSizeInGBs\":$boot_size}"
fi
local metadata="{\"user_data\":\"$(generate_user_data "$password")\"}"
local tags="{\"RootPassword\":\"$password\"}"
# 获取可用域
log_info "正在获取可用域列表..."
local domains
domains=$(oci iam availability-domain list \
--compartment-id "$ocid" \
--query 'data[*].name' \
--raw-output 2>/dev/null)
check_return $? "无法获取可用域"
domains=$(echo "$domains" | jq -c | tr -d '[]"' | tr ',' ' ')
read -ra domains <<< "$domains"
if [[ ${#domains[@]} -eq 0 ]]; then
log_error "未找到可用域"
exit 1
fi
local target_domain="${domains[$domain]}"
log_success "选择可用域: $target_domain"
# 批量创建实例
log_info "开始批量创建实例 (数量: $number)..."
local instance_id
for ((i=1; i<=number; i++)); do
log_info "正在创建第 $i/$number 个实例..."
instance_id=$(oci compute instance launch \
--compartment-id "$ocid" \
--display-name "instance-$(date +%Y%m%d-%H%M%S)" \
--shape "$shape" \
--shape-config "$shape_cfg" \
--subnet-id "$subnet_id" \
--assign-ipv6-ip true \
--source-details "$source_cfg" \
--availability-domain "$target_domain" \
--metadata "$metadata" \
--freeform-tags "$tags" \
--query 'data."id"' \
--raw-output)
check_return $? "实例创建失败"
log_success "实例 $i/$number 创建成功 (ID: $instance_id)"
# 随机延迟避免 API 限制
sleep $((RANDOM % 5 + 1))
done
log_success "所有实例部署完成!"
}
# 执行主函数
main "$@"