#!/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.07.23-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/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 < 创建实例数量 (默认: $DEFAULT_NUMBER) --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 启动卷大小 (默认: 自动) --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 "$@"