Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c7d447c05 |
2
.env
2
.env
@ -1,3 +1,3 @@
|
||||
AWS_REGION=us-west-2
|
||||
AWS_REGION=ap-east-1
|
||||
AWS_ACCESS_KEY_ID=AKIA6JQ45ADS6JTBQ3L3
|
||||
AWS_SECRET_ACCESS_KEY=QB9nTtc12tDF0qT9StdEL11yx9wlt138tlsLJKDm
|
||||
269
README.md
269
README.md
@ -1,220 +1,189 @@
|
||||
# Lightsail 静态IP 更换工具
|
||||
# EC2 EIP Rotator - 自动区域EIP更换工具
|
||||
|
||||
一个用于自动更换 AWS Lightsail 实例静态IP的Web工具,支持自动区域检测和实例定位。
|
||||
一个基于FastAPI的AWS EC2 Elastic IP自动更换工具,支持自动区域检测和Web界面操作。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔍 **自动区域检测** - 输入静态IP地址后自动定位所属AWS区域
|
||||
- 🎯 **自动实例定位** - 自动找到绑定的Lightsail实例
|
||||
- 🆕 **新IP分配** - 自动分配新的静态IP地址
|
||||
- 🔗 **自动绑定** - 将新静态IP绑定到实例
|
||||
- 🗑️ **可选释放** - 可选择是否释放旧静态IP
|
||||
- 📊 **操作日志** - 记录所有操作历史和状态
|
||||
- 🔄 **重试机制** - 处理临时错误和API限流
|
||||
- 🌐 **Web界面** - 简洁易用的Web操作界面
|
||||
- 🔍 **自动区域检测**:只需输入当前EIP公网IP,自动定位所属区域和实例
|
||||
- 🚀 **一键更换**:自动分配新EIP并绑定到实例
|
||||
- 🗑️ **可选释放**:可选择是否释放旧的EIP
|
||||
- 📊 **操作记录**:完整的操作日志记录和查看
|
||||
- 🔄 **软重启**:支持服务器软重启功能
|
||||
- 🎨 **现代UI**:响应式Web界面,支持深色主题
|
||||
|
||||
## 系统要求
|
||||
|
||||
- Python 3.7+
|
||||
- Python 3.8+
|
||||
- AWS账户和相应的API权限
|
||||
- 网络访问AWS Lightsail服务
|
||||
- 网络连接(用于访问AWS服务)
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd Ec2ElasticIpSwapper
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 配置AWS凭证
|
||||
### 3. 配置环境变量
|
||||
创建 `.env` 文件并配置AWS凭证:
|
||||
|
||||
#### 方法一:环境变量(推荐)
|
||||
|
||||
创建 `.env` 文件:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
```env
|
||||
# AWS配置(仅用作起始区域获取区域清单)
|
||||
AWS_REGION=ap-northeast-1
|
||||
AWS_ACCESS_KEY_ID=AKIA...
|
||||
AWS_SECRET_ACCESS_KEY=xxxx...
|
||||
```
|
||||
|
||||
#### 方法二:AWS CLI配置
|
||||
**注意**:`AWS_REGION` 仅用作获取区域清单的起始区域,真正的操作区域由自动检测结果决定。
|
||||
|
||||
```bash
|
||||
aws configure
|
||||
```
|
||||
|
||||
#### 方法三:IAM角色(EC2实例)
|
||||
|
||||
如果运行在EC2实例上,可以配置IAM角色。
|
||||
|
||||
### 4. 启动服务
|
||||
## 启动服务
|
||||
|
||||
```bash
|
||||
python -m uvicorn app:app --host 0.0.0.0 --port 9099
|
||||
```
|
||||
|
||||
### 5. 访问Web界面
|
||||
|
||||
打开浏览器访问:`http://localhost:9099`
|
||||
服务启动后,访问 `http://localhost:9099` 即可使用Web界面。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. **输入当前静态IP** - 在输入框中输入当前绑定在Lightsail实例上的静态IP地址
|
||||
2. **点击更换按钮** - 系统会自动:
|
||||
- 检测IP所属区域
|
||||
- 找到绑定的实例
|
||||
- 分配新的静态IP
|
||||
- 绑定到实例
|
||||
- 释放旧静态IP(可选)
|
||||
3. **查看操作日志** - 在下方表格中查看所有操作记录
|
||||
### Web界面操作
|
||||
|
||||
## 所需AWS权限
|
||||
1. **打开浏览器**访问 `http://localhost:9099`
|
||||
2. **输入当前EIP**:在"当前 EIP 公网IP"字段输入要更换的EIP地址
|
||||
3. **点击"换新EIP"**:系统会自动:
|
||||
- 检测EIP所属区域和实例
|
||||
- 分配新的EIP
|
||||
- 绑定到实例
|
||||
- 可选释放旧EIP
|
||||
4. **查看记录**:在"更换记录"表格中查看所有操作历史
|
||||
|
||||
### API接口
|
||||
|
||||
#### 更换EIP
|
||||
```bash
|
||||
curl -X POST http://localhost:9099/api/rotate_by_ip \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"current_ip": "1.2.3.4", "release_old": true}'
|
||||
```
|
||||
|
||||
#### 查看日志
|
||||
```bash
|
||||
curl http://localhost:9099/api/logs
|
||||
```
|
||||
|
||||
#### 清空日志
|
||||
```bash
|
||||
curl -X POST http://localhost:9099/api/logs/clear
|
||||
```
|
||||
|
||||
#### 重启服务器
|
||||
```bash
|
||||
curl -X POST http://localhost:9099/api/restart
|
||||
```
|
||||
|
||||
#### 健康检查
|
||||
```bash
|
||||
curl http://localhost:9099/healthz
|
||||
```
|
||||
|
||||
## 权限要求
|
||||
|
||||
确保您的AWS凭证具有以下权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"lightsail:GetStaticIps",
|
||||
"lightsail:AllocateStaticIp",
|
||||
"lightsail:AttachStaticIp",
|
||||
"lightsail:DetachStaticIp",
|
||||
"lightsail:ReleaseStaticIp",
|
||||
"lightsail:GetInstance",
|
||||
"lightsail:GetRegions",
|
||||
"sts:GetCallerIdentity"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
- `ec2:DescribeAddresses` - 查询EIP信息
|
||||
- `ec2:DescribeRegions` - 获取区域列表
|
||||
- `ec2:DescribeInstances` - 查询实例信息
|
||||
- `ec2:AllocateAddress` - 分配新EIP
|
||||
- `ec2:AssociateAddress` - 绑定EIP到实例
|
||||
- `ec2:ReleaseAddress` - 释放EIP
|
||||
- `ec2:CreateTags` - 为新EIP添加标签
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
Ec2ElasticIpSwapper/
|
||||
├── app.py # 主应用文件
|
||||
├── requirements.txt # Python依赖
|
||||
├── README.md # 项目说明
|
||||
└── .env # 环境变量配置(需要创建)
|
||||
```
|
||||
|
||||
## API接口
|
||||
## 技术架构
|
||||
|
||||
### 更换静态IP
|
||||
- **后端框架**:FastAPI
|
||||
- **AWS SDK**:boto3
|
||||
- **前端**:原生HTML/CSS/JavaScript
|
||||
- **模板引擎**:Jinja2
|
||||
- **服务器**:Uvicorn
|
||||
|
||||
```http
|
||||
POST /api/rotate_by_ip
|
||||
Content-Type: application/json
|
||||
## 功能说明
|
||||
|
||||
{
|
||||
"current_ip": "1.2.3.4",
|
||||
"release_old": true
|
||||
}
|
||||
```
|
||||
### 自动区域检测
|
||||
系统会自动遍历所有AWS区域,查找指定EIP的归属区域,无需手动选择区域。
|
||||
|
||||
**响应示例:**
|
||||
### 重试机制
|
||||
对于偶发的状态错误和限流,系统会自动重试最多3次,确保操作成功率。
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"region": "ap-northeast-1",
|
||||
"instance_id": "my-instance",
|
||||
"old_ip": "1.2.3.4",
|
||||
"new_ip": "5.6.7.8",
|
||||
"old_released": true
|
||||
}
|
||||
```
|
||||
### 操作日志
|
||||
所有操作都会记录到内存日志中,包括:
|
||||
- 操作时间
|
||||
- 区域信息
|
||||
- 实例信息
|
||||
- 旧IP和新IP
|
||||
- 操作状态
|
||||
- 错误信息
|
||||
|
||||
### 获取操作日志
|
||||
|
||||
```http
|
||||
GET /api/logs
|
||||
```
|
||||
|
||||
### 清空操作日志
|
||||
|
||||
```http
|
||||
POST /api/logs/clear
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
```http
|
||||
GET /healthz
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `AWS_REGION` | 起始区域(用于获取区域列表) | `us-east-1` |
|
||||
| `AWS_ACCESS_KEY_ID` | AWS访问密钥ID | - |
|
||||
| `AWS_SECRET_ACCESS_KEY` | AWS秘密访问密钥 | - |
|
||||
|
||||
### 重试配置
|
||||
|
||||
代码中内置了重试机制,用于处理临时错误:
|
||||
|
||||
- **最大重试次数**: 3次
|
||||
- **重试间隔**: 0.8秒(指数退避)
|
||||
- **重试条件**: 实例状态错误、请求限制、限流异常等
|
||||
### 软重启功能
|
||||
支持通过Web界面或API进行服务器软重启,使用SIGTERM信号优雅关闭。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见错误
|
||||
### 常见问题
|
||||
|
||||
1. **"静态IP not found in any region"**
|
||||
- 检查IP地址是否正确
|
||||
- 确认IP属于当前AWS账户
|
||||
- 验证AWS凭证权限
|
||||
1. **EIP未找到**
|
||||
- 确认EIP属于当前AWS账户
|
||||
- 确认EIP是Elastic IP而非普通公网IP
|
||||
|
||||
2. **"静态IP is not attached to any instance"**
|
||||
- 确认静态IP已绑定到Lightsail实例
|
||||
- 检查实例状态是否正常
|
||||
2. **权限不足**
|
||||
- 检查AWS凭证配置
|
||||
- 确认具有必要的EC2权限
|
||||
|
||||
3. **权限错误**
|
||||
- 检查AWS凭证是否正确配置
|
||||
- 确认具有所需的Lightsail权限
|
||||
3. **网络连接问题**
|
||||
- 确认网络连接正常
|
||||
- 检查防火墙设置
|
||||
|
||||
### 日志查看
|
||||
通过Web界面的"更换记录"表格或API接口 `/api/logs` 查看详细的操作日志。
|
||||
|
||||
- 在Web界面下方查看操作日志
|
||||
- 每个操作都会记录时间、区域、实例、IP地址、状态等信息
|
||||
## 开发说明
|
||||
|
||||
## 注意事项
|
||||
### 本地开发
|
||||
```bash
|
||||
# 安装开发依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
- ⚠️ **备份重要数据** - 更换IP可能影响服务连接
|
||||
- ⚠️ **DNS更新** - 更换IP后需要更新相关DNS记录
|
||||
- ⚠️ **防火墙规则** - 检查安全组和防火墙规则
|
||||
- ⚠️ **费用考虑** - 未使用的静态IP会产生费用,建议及时释放
|
||||
# 启动开发服务器
|
||||
python -m uvicorn app:app --host 0.0.0.0 --port 9099 --reload
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: FastAPI + Python
|
||||
- **AWS SDK**: Boto3
|
||||
- **前端**: 原生HTML/CSS/JavaScript
|
||||
- **服务器**: Uvicorn ASGI
|
||||
### 生产部署
|
||||
建议使用进程管理器如systemd、supervisor或Docker进行部署。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
本项目采用MIT许可证。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
欢迎提交Issue和Pull Request来改进这个项目。
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0
|
||||
- 初始版本
|
||||
- 支持Lightsail静态IP自动更换
|
||||
- Web界面和API接口
|
||||
- 自动区域检测和实例定位
|
||||
- **v1.0.0**:初始版本,支持自动区域检测和EIP更换
|
||||
- **v1.1.0**:添加软重启功能和改进的UI界面
|
||||
|
||||
267
app.py
267
app.py
@ -1,6 +1,6 @@
|
||||
# Lightsail Static IP Rotator — Auto-Region + Loading Overlay (Optimized)
|
||||
# EC2 EIP Rotator — Auto-Region + Loading Overlay (Optimized)
|
||||
# ----------------------------------------------------------------------------
|
||||
# 只输入“当前静态IP公网IP”→ 自动定位其所在区域与实例 → 分配新静态IP → 绑定 → 可选释放旧静态IP
|
||||
# 只输入“当前 EIP 公网IP”→ 自动定位其所在区域与实例 → 分配新EIP → 绑定 → 可选释放旧EIP
|
||||
# 启动:
|
||||
# pip install -r requirements.txt
|
||||
# python -m uvicorn app:app --host 0.0.0.0 --port 9099
|
||||
@ -15,6 +15,8 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
@ -31,13 +33,15 @@ load_dotenv()
|
||||
# 仅用作“起始区域”去拿区域清单;真正的操作区域由自动探测结果决定
|
||||
SEED_REGION = os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1"))
|
||||
|
||||
# 轻量重试参数(关联/释放时用)
|
||||
# 轻量重试参数(关联/释放/重启时用)
|
||||
RETRY_ON = {
|
||||
"IncorrectInstanceState",
|
||||
"RequestLimitExceeded",
|
||||
"Throttling",
|
||||
"ThrottlingException",
|
||||
"DependencyViolation",
|
||||
"InvalidInstanceID.NotFound",
|
||||
"InvalidInstanceState",
|
||||
}
|
||||
MAX_RETRY = 3
|
||||
RETRY_BASE_SLEEP = 0.8 # s
|
||||
@ -50,7 +54,7 @@ IPv4_RE = re.compile(
|
||||
r"(25[0-5]|2[0-4]\d|[01]?\d?\d)$"
|
||||
)
|
||||
|
||||
app = FastAPI(title="Lightsail Static IP Rotator (Auto-Region, Optimized)")
|
||||
app = FastAPI(title="EC2 EIP Rotator (Auto-Region, Optimized)")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@ -60,8 +64,8 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
# ---------------- helpers ----------------
|
||||
def lightsail_in(region: str):
|
||||
return boto3.client("lightsail", region_name=region)
|
||||
def ec2_in(region: str):
|
||||
return boto3.client("ec2", region_name=region)
|
||||
|
||||
def sts_in(region: str):
|
||||
return boto3.client("sts", region_name=region)
|
||||
@ -78,10 +82,10 @@ def account_id(seed_region: str) -> str:
|
||||
def list_regions(seed_region: str) -> List[str]:
|
||||
"""从 seed 区域取区域清单;失败则回退 us-east-1。"""
|
||||
try:
|
||||
regions = lightsail_in(seed_region).get_regions()["regions"]
|
||||
regions = ec2_in(seed_region).describe_regions(AllRegions=False)["Regions"]
|
||||
except Exception:
|
||||
regions = lightsail_in("us-east-1").get_regions()["regions"]
|
||||
return [r["name"] for r in regions]
|
||||
regions = ec2_in("us-east-1").describe_regions(AllRegions=False)["Regions"]
|
||||
return [r["RegionName"] for r in regions]
|
||||
|
||||
def json_error(message: str, status: int = 400):
|
||||
raise HTTPException(status_code=status, detail=message)
|
||||
@ -118,6 +122,115 @@ def clear_logs():
|
||||
LOGS.clear()
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/api/restart")
|
||||
def restart_ec2_instance(body: Dict[str, Any] = Body(...)):
|
||||
"""
|
||||
重启AWS EC2实例功能
|
||||
根据IP地址找到对应的EC2实例并重启
|
||||
"""
|
||||
current_ip = (body.get("current_ip") or "").strip()
|
||||
|
||||
if not current_ip:
|
||||
json_error("current_ip required", 422)
|
||||
if not is_ipv4(current_ip):
|
||||
json_error(f"invalid IPv4: {current_ip}", 422)
|
||||
|
||||
acct = account_id(SEED_REGION)
|
||||
regions = list_regions(SEED_REGION)
|
||||
|
||||
home_region: Optional[str] = None
|
||||
addr: Optional[Dict[str, Any]] = None
|
||||
|
||||
# 1) 自动查找 IP 归属区域
|
||||
for region in regions:
|
||||
try:
|
||||
out = ec2_in(region).describe_addresses(PublicIps=[current_ip])
|
||||
arr = out.get("Addresses", [])
|
||||
if arr:
|
||||
home_region = region
|
||||
addr = arr[0]
|
||||
break
|
||||
except ClientError as e:
|
||||
code = e.response.get("Error", {}).get("Code")
|
||||
# 不是该区会抛 InvalidAddress.NotFound / InvalidParameterValue,忽略
|
||||
if code in ("InvalidAddress.NotFound", "InvalidParameterValue"):
|
||||
continue
|
||||
json_error(str(e), 400)
|
||||
|
||||
if not home_region or not addr:
|
||||
remark = "EIP 不属于当前账号的任一区域,或不是 Elastic IP"
|
||||
append_log({
|
||||
"time": now_ts(), "region": "-", "instance_name": "-",
|
||||
"instance_id": "-", "arn": "-", "old_ip": current_ip,
|
||||
"new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark,
|
||||
})
|
||||
json_error(f"EIP {current_ip} not found in any region of this account", 404)
|
||||
|
||||
# 2) 获取实例信息
|
||||
ec2 = ec2_in(home_region)
|
||||
inst_id = addr.get("InstanceId")
|
||||
|
||||
if not inst_id:
|
||||
remark = "该 EIP 未绑定到任何实例"
|
||||
append_log({
|
||||
"time": now_ts(), "region": home_region, "instance_name": "-",
|
||||
"instance_id": "-", "arn": "-", "old_ip": current_ip,
|
||||
"new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark,
|
||||
})
|
||||
json_error(f"EIP {current_ip} is not attached to any instance", 400)
|
||||
|
||||
# 实例名(日志显示)
|
||||
inst_name = inst_id
|
||||
try:
|
||||
di = ec2.describe_instances(InstanceIds=[inst_id])
|
||||
for r in di.get("Reservations", []):
|
||||
for ins in r.get("Instances", []):
|
||||
for t in ins.get("Tags", []):
|
||||
if t.get("Key") == "Name":
|
||||
inst_name = t.get("Value")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# 重启EC2实例
|
||||
with_retry(ec2.reboot_instances, InstanceIds=[inst_id])
|
||||
|
||||
append_log({
|
||||
"time": now_ts(),
|
||||
"region": home_region,
|
||||
"instance_name": inst_name,
|
||||
"instance_id": inst_id,
|
||||
"arn": f"arn:aws:ec2:{home_region}:{acct}:instance/{inst_id}",
|
||||
"old_ip": current_ip,
|
||||
"new_ip": current_ip, # IP不变
|
||||
"retry": 0,
|
||||
"status": "OK",
|
||||
"remark": "EC2实例重启成功",
|
||||
})
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"region": home_region,
|
||||
"instance_id": inst_id,
|
||||
"instance_name": inst_name,
|
||||
"message": "EC2实例重启成功"
|
||||
}
|
||||
|
||||
except ClientError as e:
|
||||
append_log({
|
||||
"time": now_ts(),
|
||||
"region": home_region,
|
||||
"instance_name": inst_name,
|
||||
"instance_id": inst_id,
|
||||
"arn": f"arn:aws:ec2:{home_region}:{acct}:instance/{inst_id}",
|
||||
"old_ip": current_ip,
|
||||
"new_ip": "-",
|
||||
"retry": 0,
|
||||
"status": "FAIL",
|
||||
"remark": f"重启失败: {str(e)}",
|
||||
})
|
||||
json_error(f"重启EC2实例失败: {str(e)}", 400)
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz():
|
||||
return {"ok": True, "seed_region": SEED_REGION}
|
||||
@ -136,8 +249,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
|
||||
"release_old": true
|
||||
}
|
||||
步骤:
|
||||
1) 枚举区域, 用 get_static_ips() 找到归属区
|
||||
2) 取 InstanceName -> allocate_static_ip() -> attach_static_ip() -> (可选) release 旧静态IP
|
||||
1) 枚举区域, 用 describe_addresses(PublicIps=[ip]) 找到归属区
|
||||
2) 取 InstanceId -> allocate_address(Domain='vpc') -> associate -> (可选) release 旧EIP
|
||||
"""
|
||||
current_ip = (body.get("current_ip") or "").strip()
|
||||
release_old = bool(body.get("release_old", True))
|
||||
@ -156,77 +269,88 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
|
||||
# 1) 自动查找 IP 归属区域
|
||||
for region in regions:
|
||||
try:
|
||||
out = lightsail_in(region).get_static_ips()
|
||||
arr = out.get("staticIps", [])
|
||||
for static_ip in arr:
|
||||
if static_ip.get("ipAddress") == current_ip:
|
||||
out = ec2_in(region).describe_addresses(PublicIps=[current_ip])
|
||||
arr = out.get("Addresses", [])
|
||||
if arr:
|
||||
home_region = region
|
||||
addr = static_ip
|
||||
break
|
||||
if home_region:
|
||||
addr = arr[0]
|
||||
break
|
||||
except ClientError as e:
|
||||
code = e.response.get("Error", {}).get("Code")
|
||||
# 不是该区会抛 NotFoundException,忽略
|
||||
if code in ("NotFoundException", "InvalidParameterValue"):
|
||||
# 不是该区会抛 InvalidAddress.NotFound / InvalidParameterValue,忽略
|
||||
if code in ("InvalidAddress.NotFound", "InvalidParameterValue"):
|
||||
continue
|
||||
json_error(str(e), 400)
|
||||
|
||||
if not home_region or not addr:
|
||||
remark = "静态IP 不属于当前账号的任一区域,或不是 Lightsail 静态IP"
|
||||
remark = "EIP 不属于当前账号的任一区域,或不是 Elastic IP"
|
||||
append_log({
|
||||
"time": now_ts(), "region": "-", "instance_name": "-",
|
||||
"instance_id": "-", "arn": "-", "old_ip": current_ip,
|
||||
"new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark,
|
||||
})
|
||||
json_error(f"静态IP {current_ip} not found in any region of this account", 404)
|
||||
json_error(f"EIP {current_ip} not found in any region of this account", 404)
|
||||
|
||||
# 2) 在归属区执行更换
|
||||
lightsail = lightsail_in(home_region)
|
||||
inst_name = addr.get("attachedTo")
|
||||
old_static_ip_name = addr.get("name")
|
||||
ec2 = ec2_in(home_region)
|
||||
inst_id = addr.get("InstanceId")
|
||||
old_alloc = addr.get("AllocationId")
|
||||
|
||||
if not inst_name:
|
||||
remark = "该静态IP 未绑定到任何实例"
|
||||
if not inst_id:
|
||||
remark = "该 EIP 未绑定到任何实例"
|
||||
append_log({
|
||||
"time": now_ts(), "region": home_region, "instance_name": "-",
|
||||
"instance_id": "-", "arn": "-", "old_ip": current_ip,
|
||||
"new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark,
|
||||
})
|
||||
json_error(f"静态IP {current_ip} is not attached to any instance", 400)
|
||||
json_error(f"EIP {current_ip} is not attached to any instance", 400)
|
||||
|
||||
# 实例名(日志显示)
|
||||
inst_name = inst_id
|
||||
try:
|
||||
di = lightsail.get_instance(instanceName=inst_name)
|
||||
inst_name = di.get("instance", {}).get("name", inst_name)
|
||||
di = ec2.describe_instances(InstanceIds=[inst_id])
|
||||
for r in di.get("Reservations", []):
|
||||
for ins in r.get("Instances", []):
|
||||
for t in ins.get("Tags", []):
|
||||
if t.get("Key") == "Name":
|
||||
inst_name = t.get("Value")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# allocate new static IP
|
||||
new_static_ip_name = f"rotated-{int(time.time())}"
|
||||
with_retry(lightsail.allocate_static_ip, staticIpName=new_static_ip_name)
|
||||
# allocate new
|
||||
new_addr = with_retry(ec2.allocate_address, Domain="vpc")
|
||||
new_alloc = new_addr["AllocationId"]
|
||||
new_public_ip = new_addr["PublicIp"]
|
||||
|
||||
# 获取新静态IP的详细信息
|
||||
new_static_ip_info = lightsail.get_static_ip(staticIpName=new_static_ip_name)
|
||||
new_public_ip = new_static_ip_info["staticIp"]["ipAddress"]
|
||||
# 给新 EIP 打标签(便于审计/回溯)
|
||||
try:
|
||||
ec2.create_tags(
|
||||
Resources=[new_alloc],
|
||||
Tags=[
|
||||
{"Key": "RotatedFrom", "Value": current_ip},
|
||||
{"Key": "RotatedAt", "Value": now_ts()},
|
||||
{"Key": "Rotator", "Value": "auto-region-ui"},
|
||||
],
|
||||
)
|
||||
except ClientError:
|
||||
# 标签失败不影响主流程
|
||||
pass
|
||||
|
||||
# attach to instance
|
||||
# associate to instance (force)
|
||||
with_retry(
|
||||
lightsail.attach_static_ip,
|
||||
staticIpName=new_static_ip_name,
|
||||
instanceName=inst_name,
|
||||
ec2.associate_address,
|
||||
AllocationId=new_alloc,
|
||||
InstanceId=inst_id,
|
||||
AllowReassociation=True,
|
||||
)
|
||||
|
||||
# optional release old
|
||||
remark = "success"
|
||||
released = False
|
||||
if release_old and old_static_ip_name and old_static_ip_name != new_static_ip_name:
|
||||
if release_old and old_alloc and old_alloc != new_alloc:
|
||||
try:
|
||||
# 先分离旧静态IP
|
||||
with_retry(lightsail.detach_static_ip, staticIpName=old_static_ip_name)
|
||||
# 再释放旧静态IP
|
||||
with_retry(lightsail.release_static_ip, staticIpName=old_static_ip_name)
|
||||
with_retry(ec2.release_address, AllocationId=old_alloc)
|
||||
released = True
|
||||
except ClientError as re:
|
||||
remark = f"old_release_error: {re}"
|
||||
@ -235,8 +359,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
|
||||
"time": now_ts(),
|
||||
"region": home_region,
|
||||
"instance_name": inst_name,
|
||||
"instance_id": inst_name, # Lightsail使用实例名作为ID
|
||||
"arn": f"arn:aws:lightsail:{home_region}:{acct}:instance/{inst_name}",
|
||||
"instance_id": inst_id,
|
||||
"arn": f"arn:aws:ec2:{home_region}:{acct}:instance/{inst_id}",
|
||||
"old_ip": current_ip,
|
||||
"new_ip": new_public_ip,
|
||||
"retry": 0,
|
||||
@ -247,7 +371,7 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
|
||||
return {
|
||||
"ok": True,
|
||||
"region": home_region,
|
||||
"instance_id": inst_name,
|
||||
"instance_id": inst_id,
|
||||
"old_ip": current_ip,
|
||||
"new_ip": new_public_ip,
|
||||
"old_released": released
|
||||
@ -258,8 +382,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
|
||||
"time": now_ts(),
|
||||
"region": home_region,
|
||||
"instance_name": inst_name,
|
||||
"instance_id": inst_name,
|
||||
"arn": f"arn:aws:lightsail:{home_region}:{acct}:instance/{inst_name}",
|
||||
"instance_id": inst_id,
|
||||
"arn": f"arn:aws:ec2:{home_region}:{acct}:instance/{inst_id}",
|
||||
"old_ip": current_ip,
|
||||
"new_ip": "-",
|
||||
"retry": 0,
|
||||
@ -276,7 +400,7 @@ INDEX_HTML = Template(r"""
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Lightsail 静态IP 更换面板(自动区域)</title>
|
||||
<title>Lightsail IP 更换面板(EC2 自动区域)</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body { margin:0; background:#0b1020; color:#e2e8f0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft YaHei",sans-serif; }
|
||||
@ -317,22 +441,22 @@ INDEX_HTML = Template(r"""
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Lightsail 静态IP 更换面板(自动区域)</h1>
|
||||
<h1>Lightsail IP 更换面板(EC2 自动区域)</h1>
|
||||
|
||||
<div class="card hint" style="margin-bottom:12px">
|
||||
• 请输入<b>当前绑定在实例上的静态IP公网IP</b>,系统会<b>自动定位所属区域与实例</b>,分配<b>新</b>静态IP并绑定,然后(可选)释放旧静态IP。<br/>
|
||||
• 无需选择区域/实例;需权限:GetStaticIps / AllocateStaticIp / AttachStaticIp / DetachStaticIp / ReleaseStaticIp。<br/>
|
||||
• 请输入<b>当前绑定在实例上的 EIP 公网IP</b>,系统会<b>自动定位所属区域与实例</b>,分配<b>新</b>EIP并绑定,然后(可选)释放旧EIP。<br/>
|
||||
• 无需选择区域/实例;需权限:DescribeAddresses / AllocateAddress / AssociateAddress / ReleaseAddress。<br/>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<span>当前静态IP 公网IP</span>
|
||||
<span>当前 EIP 公网IP</span>
|
||||
<input id="inpIp" type="text" placeholder="例如 18.183.203.98" inputmode="numeric"
|
||||
pattern="^(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.(25[0-5]|2[0-4]\\d|[01]?\\d?\\d)$" />
|
||||
<label class="muted" style="display:none;align-items:center;gap:6px">
|
||||
<input id="chkRelease" type="checkbox" checked/> 更换后释放旧静态IP
|
||||
<input id="chkRelease" type="checkbox" checked/> 更换后释放旧EIP
|
||||
</label>
|
||||
<button id="btnRotate" class="primary">换新静态IP(自动区域)</button>
|
||||
<button id="btnRotate" class="primary">换新EIP(自动区域)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -344,6 +468,7 @@ INDEX_HTML = Template(r"""
|
||||
<div>
|
||||
<button id="btnReload">刷新记录</button>
|
||||
<button id="btnClear" class="danger">清空记录</button>
|
||||
<button id="btnRestart" class="danger">重启EC2实例</button>
|
||||
</div>
|
||||
</div>
|
||||
<table id="tblLogs">
|
||||
@ -373,6 +498,7 @@ INDEX_HTML = Template(r"""
|
||||
const btnRotate = $('#btnRotate');
|
||||
const btnReload = $('#btnReload');
|
||||
const btnClear = $('#btnClear');
|
||||
const btnRestart = $('#btnRestart');
|
||||
const tbody = $('#tblLogs tbody');
|
||||
const overlay = $('#overlay');
|
||||
const loaderTxt = $('#loaderText');
|
||||
@ -428,17 +554,17 @@ INDEX_HTML = Template(r"""
|
||||
|
||||
btnRotate.onclick = async ()=>{
|
||||
const ip = (inpIp.value||'').trim();
|
||||
if(!ip) return alert('请输入当前静态IP 公网IP');
|
||||
if(!ip) return alert('请输入当前 EIP 公网IP');
|
||||
if(!IPv4RE.test(ip)) return alert('请输入有效的 IPv4 地址');
|
||||
|
||||
showLoading(`正在为 ${ip} 更换静态IP …`);
|
||||
showLoading(`正在为 ${ip} 更换 EIP …`);
|
||||
try{
|
||||
await api('/api/rotate_by_ip', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ current_ip: ip, release_old: true })
|
||||
});
|
||||
hideLoading();
|
||||
alert('换新静态IP成功');
|
||||
alert('换新EIP成功');
|
||||
await reloadLogs();
|
||||
}catch(e){
|
||||
hideLoading();
|
||||
@ -454,6 +580,27 @@ INDEX_HTML = Template(r"""
|
||||
await reloadLogs();
|
||||
};
|
||||
|
||||
btnRestart.onclick = async ()=>{
|
||||
const ip = (inpIp.value||'').trim();
|
||||
if(!ip) return alert('请先输入当前 EIP 公网IP');
|
||||
if(!IPv4RE.test(ip)) return alert('请输入有效的 IPv4 地址');
|
||||
|
||||
if(!confirm(`确定要重启绑定到 ${ip} 的EC2实例吗?`)) return;
|
||||
showLoading(`正在重启绑定到 ${ip} 的EC2实例...`);
|
||||
try{
|
||||
await api('/api/restart', {
|
||||
method:'POST',
|
||||
body: JSON.stringify({ current_ip: ip })
|
||||
});
|
||||
hideLoading();
|
||||
alert('EC2实例重启成功');
|
||||
await reloadLogs();
|
||||
}catch(e){
|
||||
hideLoading();
|
||||
alert('重启失败:' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
reloadLogs();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user