AWS管理脚本:一键部署 Wavelength 环境 & 全能 EC2 换 IP 神器 (Zsh版)

如果你经常使用 AWS EC2,尤其是涉及到 AWS Wavelength (5G 频段) 或者是需要频繁更换实例公网 IP 的业务,你一定遇到过以下痛点:

  1. Wavelength 配置极繁琐:要启用 Zone Group、创建 Carrier Gateway、配置路由表、反复重试寻找不冲突的子网 CIDR……手动搞一次至少 10 分钟。

  2. 换 IP 手速慢:想要换个 IP,得手动解绑 EIP、释放 EIP、申请新 EIP、绑定 EIP,一套流程下来手都酸了。

  3. 脚本兼容性差:网上的脚本在 Linux 上能跑,一到 macOS (Bash 3.2) 或者 AWS CloudShell 里就各种报错。

为了解决这些问题,我编写了一个 “AWS 瑞士军刀 (AWS Swiss Army Knife)” 脚本。它是基于原生 Zsh 编写的,无需安装任何依赖,在 MacBook 终端和 AWS CloudShell 上都能完美运行。


🛠️ 脚本核心功能

这个脚本将最常用的功能合二为一,并修复了所有已知的兼容性问题:

  • 🌐 全球区域支持:支持动态获取 AWS 全球所有区域(不仅限于 Wavelength 区域),一键切换。

  • 🚀 一键部署 Wavelength:全自动完成环境搭建。自动启用区域、自动复用/创建网关、智能避让网段冲突、自动配置路由表。

  • 🔄 全能刷 IP 工具

    • Wavelength 实例:自动循环更换 Carrier IP(运营商 IP)。

    • 普通实例:自动循环更换标准 Public IP。

    • 极速交互:按 Enter 键秒换 IP,支持查重(不使用重复 IP)。

  • 🛡️ 防崩溃设计:针对 AWS API 的延迟和偶发错误(如 CreateTags 失败、区域未就绪)做了完善的容错处理,极其稳定。


💻 如何使用

方法一:在 AWS CloudShell 中使用(推荐)

这是最简单的方法,不需要配置本地环境。

  1. 登录 AWS 控制台,点击右上角的终端图标打开 CloudShell

  2. 输入 nano aws_tool.sh 创建文件。

  3. 将下方的脚本代码粘贴进去,按 Ctrl+O 保存,Ctrl+X 退出。

  4. 赋予执行权限并运行:

    Bash
    chmod +x aws_tool.sh
    ./aws_tool.sh
    

方法二:在 macOS 本地终端使用

确保你已经安装了 AWS CLI 并配置了凭证 (aws configure)。

  1. 打开终端 (Terminal)。

  2. 创建并运行脚本(步骤同上)。由于脚本指定了 #!/bin/zsh,它会自动使用 Mac 默认的 Zsh 解释器,无需担心 Bash 版本过旧的问题。


📜 脚本代码

(请复制以下代码保存为 aws_tool.sh)

Bash
#!/bin/zsh
# ============================================================
# AWS 瑞士军刀 (Zsh v9 UI 完美版)
# 优化: 明确显示“等待刷新”状态,不再显示莫名其妙的圆点
# 功能: 包含 v1-v8 所有修复 (全区域、防假死、逻辑修正)
# ============================================================

setopt shwordsplit
setopt no_nomatch

# ==============================
# 全局配置 & 颜色
# ==============================
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
USED_IP_FILE="ip.txt"

# 初始化默认区域
export AWS_REGION="${AWS_REGION:-$(aws configure get region 2>/dev/null || echo 'us-east-1')}"
export AWS_DEFAULT_REGION="$AWS_REGION"

# ==============================
# 功能 0: 切换区域
# ==============================
function switch_region_menu() {
    while true; do
        clear
        echo -e "${BLUE}=== 切换 AWS 区域 (Select Region) ===${NC}"
        echo -e "当前: ${GREEN}$AWS_REGION${NC}"
        echo
        echo " --- 常用 Wavelength 区域 ---"
        echo "1. US East (N. Virginia)      [us-east-1]  (Verizon)"
        echo "2. US West (Oregon)           [us-west-2]  (Verizon)"
        echo "3. Asia Pacific (Tokyo)       [ap-northeast-1] (KDDI)"
        echo "4. Asia Pacific (Seoul)       [ap-northeast-2] (SKT)"
        echo "5. Canada (Central)           [ca-central-1]   (Bell)"
        echo "6. Europe (London)            [eu-west-2]      (Vodafone)"
        echo
        echo " --- 全球通用 ---"
        echo "8. 📋 列出 AWS 所有可用区域 (动态获取)"
        echo "9. ⌨️  手动输入区域代码"
        echo "0. 🔙 返回主菜单"
        echo
        echo -n "请选择 [0-9]: "
        read -r r_choice

        case $r_choice in
            1) NEW_R="us-east-1" ;;
            2) NEW_R="us-west-2" ;;
            3) NEW_R="ap-northeast-1" ;;
            4) NEW_R="ap-northeast-2" ;;
            5) NEW_R="ca-central-1" ;;
            6) NEW_R="eu-west-2" ;;
            8)
                echo
                echo -n "正在连接 AWS 获取全球区域列表..."
                ALL_REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text | tr '\t' '\n' | sort)
                echo -e "\r\033[K"
                echo -e "${YELLOW}=== AWS 全球区域列表 ===${NC}"
                echo "$ALL_REGIONS" | pr -3 -t -w 80
                echo
                echo -n "请输入上方的一个区域代码 (例如 ap-east-1): "
                read -r NEW_R
                if [[ -z "$NEW_R" ]]; then continue; fi
                ;;
            9) 
                echo -n "请输入区域代码 (如 us-east-2): "
                read -r NEW_R
                ;;
            0) return ;;
            *) echo "无效选项"; sleep 1; continue ;;
        esac

        if [[ -n "$NEW_R" ]]; then
            export AWS_REGION="$NEW_R"
            export AWS_DEFAULT_REGION="$NEW_R"
            echo -e "✅ 已切换至: ${GREEN}$NEW_R${NC}"
            sleep 1
            return
        fi
    done
}

# ==============================
# 功能 1: 部署环境
# ==============================
function deploy_wavelength() {
    echo -e "${BLUE}>>> 进入 Wavelength 部署模式...${NC}"
    
    (
        set -euo pipefail
        
        REGION="$AWS_REGION"
        if [[ -z "$REGION" ]]; then echo -e "${RED}未检测到 Region。${NC}"; exit 1; fi
        echo -e "当前区域: ${GREEN}$REGION${NC}"

        echo; echo -e "${YELLOW}[1/5] 扫描并启用 Wavelength 区域...${NC}"
        WL_AZS=$(aws ec2 describe-availability-zones --region "$REGION" --all-availability-zones --filters "Name=zone-type,Values=wavelength-zone" --query "AvailabilityZones[].ZoneName" --output text | tr '\t' '\n' | sort)
        
        if [[ -z "$WL_AZS" ]]; then 
            echo -e "${RED}提示: 该区域 ($REGION) 没有检测到 Wavelength 资源。${NC}"
            echo -e "此功能仅适用于 Wavelength 区域。"
            echo -e "但您仍然可以使用 ${GREEN}[3. 刷换 IP]${NC} 功能来管理普通 EC2 实例。"
            exit 0
        fi

        WL_GROUPS=$(aws ec2 describe-availability-zones --region "$REGION" --all-availability-zones --filters "Name=zone-type,Values=wavelength-zone" --query "AvailabilityZones[].GroupName" --output text | tr '\t' '\n' | sort -u)
        for group in $WL_GROUPS; do
            echo -n "启用组 $group ... "
            aws ec2 modify-availability-zone-group --group-name "$group" --opt-in-status opted-in --region "$REGION" >/dev/null 2>&1 || true
            echo -e "${GREEN}OK${NC}"
        done

        echo -n "⏳ 等待区域生效 (15秒)... "
        sleep 15
        echo -e "${GREEN}继续${NC}"

        echo; echo -e "${YELLOW}[2/5] 获取 VPC 信息...${NC}"
        VPC_INFO=$(aws ec2 describe-vpcs --region "$REGION" --query "Vpcs[].[VpcId, IsDefault, CidrBlock]" --output text)
        if [[ -z "$VPC_INFO" ]]; then echo -e "${RED}无 VPC。${NC}"; exit 1; fi

        TARGET_VPC=$(echo "$VPC_INFO" | grep "True" | head -n 1 || true)
        if [[ -z "$TARGET_VPC" ]]; then TARGET_VPC=$(echo "$VPC_INFO" | head -n 1); fi

        VPC_ID=$(echo "$TARGET_VPC" | awk '{print $1}')
        VPC_CIDR=$(echo "$TARGET_VPC" | awk '{print $3}')
        echo -e "选中 VPC: ${GREEN}$VPC_ID${NC} (CIDR: $VPC_CIDR)"
        CIDR_PREFIX=$(echo "$VPC_CIDR" | cut -d'.' -f1,2)

        echo; echo -e "${YELLOW}[3/5] 配置 Carrier Gateway${NC}"
        EXISTING_CAGW=$(aws ec2 describe-carrier-gateways --filters "Name=vpc-id,Values=$VPC_ID" "Name=state,Values=available" --region "$REGION" --query "CarrierGateways[0].CarrierGatewayId" --output text)

        if [[ "$EXISTING_CAGW" != "None" && -n "$EXISTING_CAGW" ]]; then
            CAGW_ID="$EXISTING_CAGW"
            echo -e "✅ 复用网关: ${GREEN}$CAGW_ID${NC}"
        else
            CAGW_ID=$(aws ec2 create-carrier-gateway --vpc-id "$VPC_ID" --region "$REGION" --query "CarrierGateway.CarrierGatewayId" --output text)
            aws ec2 create-tags --resources "$CAGW_ID" --tags Key=Name,Value="Auto-Wavelength-CAGW" --region "$REGION"
            echo -e "✅ 创建网关: ${GREEN}$CAGW_ID${NC}"
        fi

        echo -n "⏳ 等待网关就绪..."
        while true; do
            STATE=$(aws ec2 describe-carrier-gateways --carrier-gateway-ids "$CAGW_ID" --region "$REGION" --query "CarrierGateways[0].State" --output text)
            if [[ "$STATE" == "available" ]]; then echo -e " ${GREEN}OK!${NC}"; break; else echo -n "."; sleep 2; fi
        done

        echo; echo -e "${YELLOW}[4/5] 配置路由表${NC}"
        RT_ID=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Wavelength-RouteTable-Auto" --region "$REGION" --query "RouteTables[0].RouteTableId" --output text)

        if [[ "$RT_ID" != "None" && -n "$RT_ID" ]]; then
            echo -e "复用路由表: ${GREEN}$RT_ID${NC}"
        else
            RT_ID=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --region "$REGION" --query "RouteTable.RouteTableId" --output text)
            aws ec2 create-tags --resources "$RT_ID" --tags Key=Name,Value="Wavelength-RouteTable-Auto" --region "$REGION"
            echo -e "✅ 创建路由表: ${GREEN}$RT_ID${NC}"
        fi

        echo; echo -e "${YELLOW}[5/5] 创建子网并关联路由...${NC}"
        CURRENT_OCTET=200

        for az in $WL_AZS; do
            echo -e "   ----------------------------------------"
            echo -e "   正在处理区域: ${BLUE}$az${NC}"
            TARGET_SUBNET=""
            EXISTING_SUBNET=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=availability-zone,Values=$az" --region "$REGION" --query "Subnets[0].SubnetId" --output text)

            if [[ "$EXISTING_SUBNET" != "None" && -n "$EXISTING_SUBNET" ]]; then
                echo -e "   ✅ 已存在子网: $EXISTING_SUBNET (复用)"
                TARGET_SUBNET="$EXISTING_SUBNET"
            else
                ATTEMPTS=0; MAX_ATTEMPTS=20; SUCCESS=false
                while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
                    NEW_CIDR="${CIDR_PREFIX}.${CURRENT_OCTET}.0/24"
                    if [ $CURRENT_OCTET -gt 254 ]; then CURRENT_OCTET=100; NEW_CIDR="${CIDR_PREFIX}.${CURRENT_OCTET}.0/24"; fi
                    echo -n "   🛠  尝试创建网段 $NEW_CIDR ... "
                    CREATE_OUT=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "$NEW_CIDR" --availability-zone "$az" --region "$REGION" --output json 2>/dev/null || echo "ERROR")
                    
                    if [[ "$CREATE_OUT" == "ERROR" ]]; then
                        echo -e "${YELLOW}失败 -> 重试...${NC}"
                        CURRENT_OCTET=$((CURRENT_OCTET+1)); ATTEMPTS=$((ATTEMPTS+1)); sleep 1
                    else
                        TARGET_SUBNET=$(echo "$CREATE_OUT" | grep '"SubnetId":' | cut -d'"' -f4)
                        echo -e "${GREEN}成功! ($TARGET_SUBNET)${NC}"
                        sleep 2
                        SHORT_AZ=${az##*-}
                        aws ec2 create-tags --resources "$TARGET_SUBNET" --tags Key=Name,Value="Auto-Subnet-$SHORT_AZ" --region "$REGION" 2>/dev/null || true
                        CURRENT_OCTET=$((CURRENT_OCTET+1)); SUCCESS=true; break
                    fi
                done
                if [ "$SUCCESS" = false ]; then echo -e "   ${RED}❌ 严重错误: 无法创建子网。${NC}"; continue; fi
            fi

            if [[ -n "$TARGET_SUBNET" ]]; then
                echo -n "   🔗 关联路由表... "
                aws ec2 associate-route-table --route-table-id "$RT_ID" --subnet-id "$TARGET_SUBNET" --region "$REGION" >/dev/null 2>&1 || true
                echo -e "${GREEN}完成${NC}"
            fi
        done

        echo; echo -e "${YELLOW}检查并添加最终路由...${NC}"
        CURRENT_ROUTE_GW=$(aws ec2 describe-route-tables --route-table-id "$RT_ID" --region "$REGION" --query "RouteTables[0].Routes[?DestinationCidrBlock=='0.0.0.0/0'].GatewayId" --output text)
        if [[ "$CURRENT_ROUTE_GW" != "None" && -n "$CURRENT_ROUTE_GW" && "$CURRENT_ROUTE_GW" != "$CAGW_ID" ]]; then
            echo -e "${YELLOW}清理旧路由...${NC}"
            aws ec2 delete-route --route-table-id "$RT_ID" --destination-cidr-block 0.0.0.0/0 --region "$REGION" 2>/dev/null || true
        fi

        OUTPUT=$(aws ec2 create-route --route-table-id "$RT_ID" --destination-cidr-block 0.0.0.0/0 --carrier-gateway-id "$CAGW_ID" --region "$REGION" 2>&1)
        EXIT_CODE=$?
        if [ $EXIT_CODE -eq 0 ] || echo "$OUTPUT" | grep -q "RouteAlreadyExists"; then
            echo -e "✅ 路由添加成功 (0.0.0.0/0 -> $CAGW_ID)"
        else
            echo -e "${RED}❌ 路由添加失败: $OUTPUT ${NC}"
        fi
        
        echo; echo -e "${GREEN}🎉 部署完成!${NC}"
    )
    echo -e "${BLUE}按回车键返回主菜单...${NC}"
    read
}

# ==============================
# 功能 2: 刷 IP 工具 (通用)
# ==============================
function run_ip_tool() {
    touch "$USED_IP_FILE"
    
    local REGION="$AWS_REGION"
    if [[ -z "$REGION" ]]; then echo -e "${RED}未检测到 Region。${NC}"; return; fi
    
    # 启动保活进程
    (
      while true; do
        sleep 300 
        echo -e "\n\033[90m[防超时守护] $(date +%H:%M:%S) 终端保持活跃中...\033[0m" >&2
        echo -n "按 [Enter] 刷IP | [q] 上级 | [m] 主菜单... " >&2
      done
    ) &
    local KEEP_ALIVE_PID=$!
    
    function cleanup_ip_tool() {
        kill $KEEP_ALIVE_PID 2>/dev/null || true
    }
    trap cleanup_ip_tool EXIT

    # 定义辅助函数
    region_from_az() { echo "$1" | cut -d- -f1-3; }
    is_wavelength_az() { [[ "$1" == *wlz* ]]; }
    normalize_nbg() { [[ "$1" == *a ]] && echo "${1%a}" || echo "$1"; }

    # 普通实例
    simple_swap_interactive() {
        local INSTANCE_ID="$1"; local REGION="$2"
        {
            echo
            echo "普通实例模式 ($INSTANCE_ID)"
            echo "-------------------------------------"
            echo " [Enter] 立即更换 IP"
            echo " [q]     返回实例列表"
            echo " [m]     返回主菜单"
            echo "-------------------------------------"
        } >&2
        
        while true; do
            echo -n "请操作 > " >&2
            if ! read -r input; then break; fi
            
            if [[ "$input" == "q" ]]; then return 0; fi
            if [[ "$input" == "m" ]]; then return 10; fi 
            
            echo -e " [正在执行...]" >&2

            while true; do
                # 获取旧 IP 以便比对
                local OLD_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[0].Instances[0].PublicIpAddress" --output text --region "$REGION")

                local ALLOC_ID=$(aws ec2 describe-addresses --filters "Name=instance-id,Values=$INSTANCE_ID" --query "Addresses[0].AllocationId" --output text --region "$REGION")
                if [[ "$ALLOC_ID" != "None" ]]; then
                    local ASSOC=$(aws ec2 describe-addresses --filters "Name=instance-id,Values=$INSTANCE_ID" --query "Addresses[0].AssociationId" --output text --region "$REGION")
                    aws ec2 disassociate-address --association-id "$ASSOC" --region "$REGION"
                    aws ec2 release-address --allocation-id "$ALLOC_ID" --region "$REGION"
                fi
                local NEW_ALLOC=$(aws ec2 allocate-address --domain vpc --query "AllocationId" --output text --region "$REGION")
                aws ec2 associate-address --instance-id "$INSTANCE_ID" --allocation-id "$NEW_ALLOC" --region "$REGION" >/dev/null
                local NEW_ASSOC=$(aws ec2 describe-addresses --allocation-ids "$NEW_ALLOC" --query "Addresses[0].AssociationId" --output text --region "$REGION")
                aws ec2 disassociate-address --association-id "$NEW_ASSOC" --region "$REGION"
                aws ec2 release-address --allocation-id "$NEW_ALLOC" --region "$REGION"
                
                local NEW_IP="None"
                # === 优化 UI: 明确显示等待 ===
                echo -n "等待刷新" >&2
                while [[ "$NEW_IP" == "None" || "$NEW_IP" == "$OLD_IP" ]]; do
                    sleep 1
                    NEW_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[0].Instances[0].PublicIpAddress" --output text --region "$REGION")
                    echo -n "." >&2
                done
                echo "" >&2
                
                echo "获取 IP: $NEW_IP" >&2
                if grep -Fq "$NEW_IP" "$USED_IP_FILE"; then echo "重复,重试..." >&2; continue; fi
                echo "$(date +%F_%T) $NEW_IP" >> "$USED_IP_FILE"
                break
            done
        done
    }

    # Wavelength 实例
    wavelength_swap_interactive() {
        local INSTANCE_ID="$1"; local NBG_RAW="$2"
        local NBG="$(normalize_nbg "$NBG_RAW")"; local REGION="$(region_from_az "$NBG")"
        {
            echo
            echo "Wavelength 模式 ($INSTANCE_ID)"
            echo "-------------------------------------"
            echo " [Enter] 立即更换 IP"
            echo " [q]     返回实例列表"
            echo " [m]     返回主菜单"
            echo "-------------------------------------"
        } >&2
        
        while true; do
            echo -n "请操作 > " >&2
            if ! read -r input; then break; fi
            
            if [[ "$input" == "q" ]]; then return 0; fi
            if [[ "$input" == "m" ]]; then return 10; fi
            
            echo -e " [正在执行...]" >&2

            while true; do
                local OLD_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[0].Instances[0].PublicIpAddress" --output text --region "$REGION")
                if [[ "$OLD_IP" == "None" ]]; then
                     local DNS=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[0].Instances[0].PublicDnsName" --output text --region "$REGION")
                     if [[ -n "$DNS" && "$DNS" != "None" ]]; then
                        local IP_PART="${DNS%%.*}"; IP_PART="${IP_PART#ec2-}"
                        OLD_IP="${IP_PART//-/.}"
                     fi
                fi

                local ALLOC_ID=$(aws ec2 describe-addresses --filters "Name=instance-id,Values=$INSTANCE_ID" --query "Addresses[0].AllocationId" --output text --region "$REGION")
                if [[ "$ALLOC_ID" != "None" ]]; then
                    local ASSOC=$(aws ec2 describe-addresses --filters "Name=instance-id,Values=$INSTANCE_ID" --query "Addresses[0].AssociationId" --output text --region "$REGION")
                    echo "释放旧 IP..." >&2
                    aws ec2 disassociate-address --association-id "$ASSOC" --region "$REGION"
                    aws ec2 release-address --allocation-id "$ALLOC_ID" --network-border-group "$NBG" --region "$REGION"
                fi
                echo "分配新 IP..." >&2
                local NEW_ALLOC=$(aws ec2 allocate-address --domain vpc --network-border-group "$NBG" --query "AllocationId" --output text --region "$REGION")
                aws ec2 associate-address --instance-id "$INSTANCE_ID" --allocation-id "$NEW_ALLOC" --region "$REGION" >/dev/null
                local NEW_ASSOC=$(aws ec2 describe-addresses --allocation-ids "$NEW_ALLOC" --query "Addresses[0].AssociationId" --output text --region "$REGION")
                echo "解绑并释放..." >&2
                aws ec2 disassociate-address --association-id "$NEW_ASSOC" --region "$REGION"
                aws ec2 release-address --allocation-id "$NEW_ALLOC" --network-border-group "$NBG" --region "$REGION"
                
                local NEW_IP="None"
                # === 优化 UI: 明确显示等待 ===
                echo -n "等待刷新" >&2
                local retry=0
                while [[ "$NEW_IP" == "None" || "$NEW_IP" == "$OLD_IP" ]]; do
                    sleep 1
                    local DNS=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[0].Instances[0].PublicDnsName" --output text --region "$REGION")
                    if [[ -n "$DNS" && "$DNS" != "None" ]]; then
                        local IP_PART="${DNS%%.*}"; IP_PART="${IP_PART#ec2-}"
                        NEW_IP="${IP_PART//-/.}"
                    else
                        NEW_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query "Reservations[0].Instances[0].PublicIpAddress" --output text --region "$REGION")
                    fi
                    
                    echo -n "." >&2
                    retry=$((retry+1))
                    if [[ $retry -gt 15 ]]; then break; fi 
                done
                echo "" >&2
                
                echo "IP -> $NEW_IP" >&2
                if grep -Fq "$NEW_IP" "$USED_IP_FILE"; then echo "重复,重试..." >&2; continue; fi
                echo "$(date +%F_%T) $NEW_IP" >> "$USED_IP_FILE"
                break
            done
        done
    }

    select_instance() {
        local REGION="$1"; local STATE_FILTER="${2:-running}"
        local FILTER_ARGS=(); if [[ "$STATE_FILTER" != "all" ]]; then FILTER_ARGS+=(--filters "Name=instance-state-name,Values=$STATE_FILTER"); fi
        local TMP_LIST="/tmp/aws_ec2_list_$$"
        
        echo -n "正在连接 AWS 获取实例列表..." >&2
        
        aws ec2 describe-instances "${FILTER_ARGS[@]}" --region "$REGION" --query "Reservations[].Instances[].[InstanceId,State.Name,Placement.AvailabilityZone,PublicIpAddress,PrivateIpAddress,Tags[?Key=='Name'].Value|[0]]" --output text | sed '/^$/d' > "$TMP_LIST"
        
        echo -e "\r\033[K" >&2
        
        if [[ ! -s "$TMP_LIST" ]]; then echo "❌ 未找到实例" >&2; rm -f "$TMP_LIST"; return 1; fi
        local max_idx=$(wc -l < "$TMP_LIST" | tr -d ' ')
        
        {
            echo
            echo "=== 实例列表 ($REGION) ==="
            printf "%-4s %-20s %-10s %-20s %s\n" "No." "InstanceId" "State" "AZ" "PublicIP"
            local idx=0; local line
            while read -r line; do
                idx=$((idx+1))
                read -r IID STATE AZ PUB PRI NAME <<< "$line"
                PUB="${PUB:-None}"
                printf "%-4s %-20s %-10s %-20s %s\n" "$idx" "$IID" "$STATE" "$AZ" "$PUB"
            done < "$TMP_LIST"
            echo
        } >&2
        
        local choice
        while true; do
            echo -n "选择实例编号 (1-$max_idx): " >&2
            if ! read -r choice; then rm -f "$TMP_LIST"; return 1; fi
            if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= max_idx )); then
                local picked=$(sed -n "${choice}p" "$TMP_LIST")
                read -r IID STATE AZ PUB PRI NAME <<< "$picked"
                echo "$IID|$AZ"
                rm -f "$TMP_LIST"; return 0
            fi
        done
    }

    echo -n "实例状态过滤 (running/all,默认 running): "
    read -r STATE_FILTER
    STATE_FILTER="${STATE_FILTER:-running}"

    while true; do
        local RESULT
        if ! RESULT=$(select_instance "$REGION" "$STATE_FILTER"); then
            echo "返回主菜单..."
            cleanup_ip_tool
            return
        fi
        
        local INSTANCE_ID="${RESULT%|*}"
        local AZ="${RESULT#*|}"
        local RET_CODE=0

        if is_wavelength_az "$AZ"; then
            local NBG="$(normalize_nbg "$AZ")"
            wavelength_swap_interactive "$INSTANCE_ID" "$NBG"
            RET_CODE=$?
        else
            simple_swap_interactive "$INSTANCE_ID" "$REGION"
            RET_CODE=$?
        fi

        if [[ $RET_CODE -eq 10 ]]; then
            cleanup_ip_tool
            return
        fi
        
        echo "准备重新选择实例..."
        sleep 1
    done
}

# ==============================
# 主菜单
# ==============================
while true; do
    clear
    echo -e "${BLUE}========================================${NC}"
    echo -e "${BLUE}    AWS 瑞士军刀 (Zsh v9 UI 完美版)    ${NC}"
    echo -e "${BLUE}========================================${NC}"
    echo -e "当前区域: ${GREEN}${AWS_REGION:-未设置}${NC}"
    echo
    echo "1. 切换区域 (Switch Region)"
    echo "2. 部署环境 (Wavelength Only)"
    echo "3. 刷换 IP  (Universal IP Tool)"
    echo "4. 退出     (Exit)"
    echo
    echo -n "请输入选项 [1-4]: "
    read -r choice

    case $choice in
        1) switch_region_menu ;;
        2) deploy_wavelength ;;
        3) run_ip_tool ;;
        4|q|exit) echo "再见!"; exit 0 ;;
        *) echo "无效选项"; sleep 1 ;;
    esac
done

📸 功能演示

1. 主菜单

脚本启动后,界面清晰明了:

Plaintext
========================================
    AWS 瑞士军刀 (Zsh v7 交互增强版)   
========================================
当前区域: us-east-1

1. 切换区域 (Switch Region)
2. 部署环境 (Wavelength Only)
3. 刷换 IP  (Universal IP Tool)
4. 退出     (Exit)

2. 部署 Wavelength 环境

选择 2,脚本会自动扫描并配置所有网络设施。你只需要看着屏幕上的绿色 OK 即可。针对 InternalError 或网段冲突,脚本会自动重试和避让。

3. 刷 IP 模式

选择 3,脚本会列出当前区域的所有实例。选择一个实例后进入交互模式:

  • Enter:立即更换一个新的 IP。

  • q:返回上级列表选择其他机器。

  • m:直接返回主菜单切换区域。

Plaintext
Wavelength 模式 (i-0xxxxxx)
-------------------------------------
 [Enter] 立即更换 IP
 [q]     返回实例列表
 [m]     返回主菜单
-------------------------------------
请操作 > [正在执行...]
释放旧 IP...
分配新 IP...
解绑并释放...
IP -> 155.146.xx.xx
✅ 已记录:2025-12-23_10:00:00 155.146.xx.xx

💡 常见问题 (FAQ)

Q: 为什么要在 CloudShell 里用?

A: CloudShell 自带了 AWS CLI 和认证信息,网络直通 AWS 内网,延迟极低,刷 IP 速度最快。

Q: 普通 EC2 能用吗?

A: 可以!脚本会自动识别实例所在的子网类型。如果是普通区域,它会使用标准的 Allocate/Release Address 流程更换公网 IP。

Q: 脚本报错 trap: undefined signal: RETURN 怎么办?

A: 请确保使用的是最新版脚本(v3以上),我已经移除了不兼容的信号捕获,改用了标准的 EXIT 信号,完美兼容 macOS。


Next Post Previous Post
No Comment
Add Comment
comment url