## 新增功能 - 创建统一的公共函数库 (common/logging.sh, common/error_handler.sh) - 添加功能演示脚本 (common/demo_usage.sh) - 完善的使用文档 (common/README.md) ## 代码重构 - 重构所有脚本使用统一的公共库 - 为所有函数添加完整的文档注释 - 统一代码格式(4空格缩进、严格模式) - 标准化错误处理和日志输出 ## 文件重命名 - gcp/create_ai_project.sh → gcp/create_ai_projects.sh (单复数统一) - gcp/delete_all_project.sh → gcp/delete_all_projects.sh (单复数统一) - linux/install_ohmyzsh.sh → linux/install_oh_my_zsh.sh (专有名词规范) - linux/create_raid0_with_ext4.sh → linux/create_raid0_array.sh (简化命名) - common/example.sh → common/demo_usage.sh (更具描述性) ## 技术改进 - 使用 readonly 声明常量 - 启用 set -euo pipefail 严格模式 - 统一的 ANSI 颜色日志输出 - 完善的命令重试机制 - 栈追踪支持
389 lines
12 KiB
Bash
389 lines
12 KiB
Bash
#!/bin/bash
|
||
# ============================================================================
|
||
# 文件名: create_instance.sh
|
||
# 描述: Oracle Cloud Infrastructure 虚拟机批量部署工具
|
||
# 作者: Cloud Tools Project
|
||
# 版本: 2.0.0
|
||
# ============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
# 获取脚本目录
|
||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||
|
||
# 加载公共库
|
||
source "${PROJECT_ROOT}/common/logging.sh"
|
||
source "${PROJECT_ROOT}/common/error_handler.sh"
|
||
|
||
# 默认配置参数
|
||
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 "$@" |