Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c7d447c05 |
189
README.md
Normal file
189
README.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# EC2 EIP Rotator - 自动区域EIP更换工具
|
||||||
|
|
||||||
|
一个基于FastAPI的AWS EC2 Elastic IP自动更换工具,支持自动区域检测和Web界面操作。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🔍 **自动区域检测**:只需输入当前EIP公网IP,自动定位所属区域和实例
|
||||||
|
- 🚀 **一键更换**:自动分配新EIP并绑定到实例
|
||||||
|
- 🗑️ **可选释放**:可选择是否释放旧的EIP
|
||||||
|
- 📊 **操作记录**:完整的操作日志记录和查看
|
||||||
|
- 🔄 **软重启**:支持服务器软重启功能
|
||||||
|
- 🎨 **现代UI**:响应式Web界面,支持深色主题
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- AWS账户和相应的API权限
|
||||||
|
- 网络连接(用于访问AWS服务)
|
||||||
|
|
||||||
|
## 安装步骤
|
||||||
|
|
||||||
|
### 1. 克隆项目
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url>
|
||||||
|
cd Ec2ElasticIpSwapper
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置环境变量
|
||||||
|
创建 `.env` 文件并配置AWS凭证:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# AWS配置(仅用作起始区域获取区域清单)
|
||||||
|
AWS_REGION=ap-northeast-1
|
||||||
|
AWS_ACCESS_KEY_ID=AKIA...
|
||||||
|
AWS_SECRET_ACCESS_KEY=xxxx...
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:`AWS_REGION` 仅用作获取区域清单的起始区域,真正的操作区域由自动检测结果决定。
|
||||||
|
|
||||||
|
## 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn app:app --host 0.0.0.0 --port 9099
|
||||||
|
```
|
||||||
|
|
||||||
|
服务启动后,访问 `http://localhost:9099` 即可使用Web界面。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### Web界面操作
|
||||||
|
|
||||||
|
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凭证具有以下权限:
|
||||||
|
|
||||||
|
- `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 # 环境变量配置(需要创建)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
- **后端框架**:FastAPI
|
||||||
|
- **AWS SDK**:boto3
|
||||||
|
- **前端**:原生HTML/CSS/JavaScript
|
||||||
|
- **模板引擎**:Jinja2
|
||||||
|
- **服务器**:Uvicorn
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
### 自动区域检测
|
||||||
|
系统会自动遍历所有AWS区域,查找指定EIP的归属区域,无需手动选择区域。
|
||||||
|
|
||||||
|
### 重试机制
|
||||||
|
对于偶发的状态错误和限流,系统会自动重试最多3次,确保操作成功率。
|
||||||
|
|
||||||
|
### 操作日志
|
||||||
|
所有操作都会记录到内存日志中,包括:
|
||||||
|
- 操作时间
|
||||||
|
- 区域信息
|
||||||
|
- 实例信息
|
||||||
|
- 旧IP和新IP
|
||||||
|
- 操作状态
|
||||||
|
- 错误信息
|
||||||
|
|
||||||
|
### 软重启功能
|
||||||
|
支持通过Web界面或API进行服务器软重启,使用SIGTERM信号优雅关闭。
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **EIP未找到**
|
||||||
|
- 确认EIP属于当前AWS账户
|
||||||
|
- 确认EIP是Elastic IP而非普通公网IP
|
||||||
|
|
||||||
|
2. **权限不足**
|
||||||
|
- 检查AWS凭证配置
|
||||||
|
- 确认具有必要的EC2权限
|
||||||
|
|
||||||
|
3. **网络连接问题**
|
||||||
|
- 确认网络连接正常
|
||||||
|
- 检查防火墙设置
|
||||||
|
|
||||||
|
### 日志查看
|
||||||
|
通过Web界面的"更换记录"表格或API接口 `/api/logs` 查看详细的操作日志。
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
```bash
|
||||||
|
# 安装开发依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
python -m uvicorn app:app --host 0.0.0.0 --port 9099 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产部署
|
||||||
|
建议使用进程管理器如systemd、supervisor或Docker进行部署。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目采用MIT许可证。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交Issue和Pull Request来改进这个项目。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
- **v1.0.0**:初始版本,支持自动区域检测和EIP更换
|
||||||
|
- **v1.1.0**:添加软重启功能和改进的UI界面
|
||||||
138
app.py
138
app.py
@ -15,6 +15,8 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Dict, Any, Optional
|
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"))
|
SEED_REGION = os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1"))
|
||||||
|
|
||||||
# 轻量重试参数(关联/释放时用)
|
# 轻量重试参数(关联/释放/重启时用)
|
||||||
RETRY_ON = {
|
RETRY_ON = {
|
||||||
"IncorrectInstanceState",
|
"IncorrectInstanceState",
|
||||||
"RequestLimitExceeded",
|
"RequestLimitExceeded",
|
||||||
"Throttling",
|
"Throttling",
|
||||||
"ThrottlingException",
|
"ThrottlingException",
|
||||||
"DependencyViolation",
|
"DependencyViolation",
|
||||||
|
"InvalidInstanceID.NotFound",
|
||||||
|
"InvalidInstanceState",
|
||||||
}
|
}
|
||||||
MAX_RETRY = 3
|
MAX_RETRY = 3
|
||||||
RETRY_BASE_SLEEP = 0.8 # s
|
RETRY_BASE_SLEEP = 0.8 # s
|
||||||
@ -118,6 +122,115 @@ def clear_logs():
|
|||||||
LOGS.clear()
|
LOGS.clear()
|
||||||
return {"ok": True}
|
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")
|
@app.get("/healthz")
|
||||||
def healthz():
|
def healthz():
|
||||||
return {"ok": True, "seed_region": SEED_REGION}
|
return {"ok": True, "seed_region": SEED_REGION}
|
||||||
@ -355,6 +468,7 @@ INDEX_HTML = Template(r"""
|
|||||||
<div>
|
<div>
|
||||||
<button id="btnReload">刷新记录</button>
|
<button id="btnReload">刷新记录</button>
|
||||||
<button id="btnClear" class="danger">清空记录</button>
|
<button id="btnClear" class="danger">清空记录</button>
|
||||||
|
<button id="btnRestart" class="danger">重启EC2实例</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table id="tblLogs">
|
<table id="tblLogs">
|
||||||
@ -384,6 +498,7 @@ INDEX_HTML = Template(r"""
|
|||||||
const btnRotate = $('#btnRotate');
|
const btnRotate = $('#btnRotate');
|
||||||
const btnReload = $('#btnReload');
|
const btnReload = $('#btnReload');
|
||||||
const btnClear = $('#btnClear');
|
const btnClear = $('#btnClear');
|
||||||
|
const btnRestart = $('#btnRestart');
|
||||||
const tbody = $('#tblLogs tbody');
|
const tbody = $('#tblLogs tbody');
|
||||||
const overlay = $('#overlay');
|
const overlay = $('#overlay');
|
||||||
const loaderTxt = $('#loaderText');
|
const loaderTxt = $('#loaderText');
|
||||||
@ -465,6 +580,27 @@ INDEX_HTML = Template(r"""
|
|||||||
await reloadLogs();
|
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();
|
reloadLogs();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user