Compare commits

..

2 Commits

Author SHA1 Message Date
0751ce1e6d 1111 2025-10-22 11:42:12 +08:00
b60284f8dc lightail-init 2025-10-22 11:38:04 +08:00
3 changed files with 212 additions and 328 deletions

2
.env
View File

@ -1,3 +1,3 @@
AWS_REGION=ap-east-1 AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=AKIA6JQ45ADS6JTBQ3L3 AWS_ACCESS_KEY_ID=AKIA6JQ45ADS6JTBQ3L3
AWS_SECRET_ACCESS_KEY=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 AWS_SECRET_ACCESS_KEY=QB9nTtc12tDF0qT9StdEL11yx9wlt138tlsLJKDm

267
README.md
View File

@ -1,189 +1,220 @@
# EC2 EIP Rotator - 自动区域EIP更换工具 # Lightsail 静态IP 更换工具
一个基于FastAPI的AWS EC2 Elastic IP自动更换工具支持自动区域检测和Web界面操作 一个用于自动更换 AWS Lightsail 实例静态IP的Web工具支持自动区域检测和实例定位
## 功能特性 ## 功能特性
- 🔍 **自动区域检测**只需输入当前EIP公网IP自动定位所属区域和实例 - 🔍 **自动区域检测** - 输入静态IP地址后自动定位所属AWS区域
- 🚀 **一键更换**自动分配新EIP并绑定到实例 - 🎯 **自动实例定位** - 自动找到绑定的Lightsail实例
- 🗑️ **可选释放**可选择是否释放旧的EIP - 🆕 **新IP分配** - 自动分配新的静态IP地址
- 📊 **操作记录**:完整的操作日志记录和查看 - 🔗 **自动绑定** - 将新静态IP绑定到实例
- 🔄 **软重启**:支持服务器软重启功能 - 🗑️ **可选释放** - 可选择是否释放旧静态IP
- 🎨 **现代UI**响应式Web界面支持深色主题 - 📊 **操作日志** - 记录所有操作历史和状态
- 🔄 **重试机制** - 处理临时错误和API限流
- 🌐 **Web界面** - 简洁易用的Web操作界面
## 系统要求 ## 系统要求
- Python 3.8+ - Python 3.7+
- AWS账户和相应的API权限 - AWS账户和相应的API权限
- 网络连接(用于访问AWS服务 - 网络访问AWS Lightsail服务
## 安装步骤 ## 安装步骤
### 1. 克隆项目 ### 1. 克隆项目
```bash ```bash
git clone <your-repo-url> git clone <your-repo-url>
cd Ec2ElasticIpSwapper cd Ec2ElasticIpSwapper
``` ```
### 2. 安装依赖 ### 2. 安装依赖
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
### 3. 配置环境变量 ### 3. 配置AWS凭证
创建 `.env` 文件并配置AWS凭证
```env #### 方法一:环境变量(推荐)
# AWS配置仅用作起始区域获取区域清单
创建 `.env` 文件:
```bash
# .env
AWS_REGION=ap-northeast-1 AWS_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=AKIA... AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=xxxx... AWS_SECRET_ACCESS_KEY=xxxx...
``` ```
**注意**`AWS_REGION` 仅用作获取区域清单的起始区域,真正的操作区域由自动检测结果决定。 #### 方法二AWS CLI配置
## 启动服务 ```bash
aws configure
```
#### 方法三IAM角色EC2实例
如果运行在EC2实例上可以配置IAM角色。
### 4. 启动服务
```bash ```bash
python -m uvicorn app:app --host 0.0.0.0 --port 9099 python -m uvicorn app:app --host 0.0.0.0 --port 9099
``` ```
服务启动后,访问 `http://localhost:9099` 即可使用Web界面。 ### 5. 访问Web界面
打开浏览器访问:`http://localhost:9099`
## 使用方法 ## 使用方法
### Web界面操作 1. **输入当前静态IP** - 在输入框中输入当前绑定在Lightsail实例上的静态IP地址
2. **点击更换按钮** - 系统会自动:
1. **打开浏览器**访问 `http://localhost:9099` - 检测IP所属区域
2. **输入当前EIP**:在"当前 EIP 公网IP"字段输入要更换的EIP地址 - 找到绑定的实例
3. **点击"换新EIP"**:系统会自动: - 分配新的静态IP
- 检测EIP所属区域和实例
- 分配新的EIP
- 绑定到实例 - 绑定到实例
- 可选释放旧EIP - 释放旧静态IP可选
4. **查看记录**:在"更换记录"表格中查看所有操作历史 3. **查看操作日志** - 在下方表格中查看所有操作记录
### API接口 ## 所需AWS权限
#### 更换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凭证具有以下权限 确保您的AWS凭证具有以下权限
- `ec2:DescribeAddresses` - 查询EIP信息 ```json
- `ec2:DescribeRegions` - 获取区域列表 {
- `ec2:DescribeInstances` - 查询实例信息 "Version": "2012-10-17",
- `ec2:AllocateAddress` - 分配新EIP "Statement": [
- `ec2:AssociateAddress` - 绑定EIP到实例 {
- `ec2:ReleaseAddress` - 释放EIP "Effect": "Allow",
- `ec2:CreateTags` - 为新EIP添加标签 "Action": [
"lightsail:GetStaticIps",
## 项目结构 "lightsail:AllocateStaticIp",
"lightsail:AttachStaticIp",
``` "lightsail:DetachStaticIp",
Ec2ElasticIpSwapper/ "lightsail:ReleaseStaticIp",
├── app.py # 主应用文件 "lightsail:GetInstance",
├── requirements.txt # Python依赖 "lightsail:GetRegions",
├── README.md # 项目说明 "sts:GetCallerIdentity"
└── .env # 环境变量配置(需要创建) ],
"Resource": "*"
}
]
}
``` ```
## 技术架构 ## API接口
- **后端框架**FastAPI ### 更换静态IP
- **AWS SDK**boto3
- **前端**原生HTML/CSS/JavaScript
- **模板引擎**Jinja2
- **服务器**Uvicorn
## 功能说明 ```http
POST /api/rotate_by_ip
Content-Type: application/json
### 自动区域检测 {
系统会自动遍历所有AWS区域查找指定EIP的归属区域无需手动选择区域。 "current_ip": "1.2.3.4",
"release_old": true
}
```
### 重试机制 **响应示例:**
对于偶发的状态错误和限流系统会自动重试最多3次确保操作成功率。
### 操作日志 ```json
所有操作都会记录到内存日志中,包括: {
- 操作时间 "ok": true,
- 区域信息 "region": "ap-northeast-1",
- 实例信息 "instance_id": "my-instance",
- 旧IP和新IP "old_ip": "1.2.3.4",
- 操作状态 "new_ip": "5.6.7.8",
- 错误信息 "old_released": true
}
```
### 软重启功能 ### 获取操作日志
支持通过Web界面或API进行服务器软重启使用SIGTERM信号优雅关闭。
```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秒(指数退避)
- **重试条件**: 实例状态错误、请求限制、限流异常等
## 故障排除 ## 故障排除
### 常见问题 ### 常见错误
1. **EIP未找到** 1. **"静态IP not found in any region"**
- 确认EIP属于当前AWS账户 - 检查IP地址是否正确
- 确认EIP是Elastic IP而非普通公网IP - 确认IP属于当前AWS账户
- 验证AWS凭证权限
2. **权限不足** 2. **"静态IP is not attached to any instance"**
- 检查AWS凭证配置 - 确认静态IP已绑定到Lightsail实例
- 确认具有必要的EC2权限 - 检查实例状态是否正常
3. **网络连接问题** 3. **权限错误**
- 确认网络连接正常 - 检查AWS凭证是否正确配置
- 检查防火墙设置 - 确认具有所需的Lightsail权限
### 日志查看 ### 日志查看
通过Web界面的"更换记录"表格或API接口 `/api/logs` 查看详细的操作日志。
## 开发说明 - 在Web界面下方查看操作日志
- 每个操作都会记录时间、区域、实例、IP地址、状态等信息
### 本地开发 ## 注意事项
```bash
# 安装开发依赖
pip install -r requirements.txt
# 启动开发服务器 - ⚠️ **备份重要数据** - 更换IP可能影响服务连接
python -m uvicorn app:app --host 0.0.0.0 --port 9099 --reload - ⚠️ **DNS更新** - 更换IP后需要更新相关DNS记录
``` - ⚠️ **防火墙规则** - 检查安全组和防火墙规则
- ⚠️ **费用考虑** - 未使用的静态IP会产生费用建议及时释放
### 生产部署 ## 技术栈
建议使用进程管理器如systemd、supervisor或Docker进行部署。
- **后端**: FastAPI + Python
- **AWS SDK**: Boto3
- **前端**: 原生HTML/CSS/JavaScript
- **服务器**: Uvicorn ASGI
## 许可证 ## 许可证
本项目采用MIT许可证。 MIT License
## 贡献 ## 贡献
欢迎提交Issue和Pull Request来改进这个项目。 欢迎提交Issue和Pull Request
## 更新日志 ## 更新日志
- **v1.0.0**初始版本支持自动区域检测和EIP更换 ### v1.0.0
- **v1.1.0**添加软重启功能和改进的UI界面 - 初始版本
- 支持Lightsail静态IP自动更换
- Web界面和API接口
- 自动区域检测和实例定位

271
app.py
View File

@ -1,6 +1,6 @@
# EC2 EIP Rotator — Auto-Region + Loading Overlay (Optimized) # Lightsail Static IP Rotator — Auto-Region + Loading Overlay (Optimized)
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# 只输入“当前 EIP 公网IP”→ 自动定位其所在区域与实例 → 分配新EIP → 绑定 → 可选释放旧EIP # 只输入“当前静态IP公网IP”→ 自动定位其所在区域与实例 → 分配新静态IP → 绑定 → 可选释放旧静态IP
# 启动: # 启动:
# pip install -r requirements.txt # pip install -r requirements.txt
# python -m uvicorn app:app --host 0.0.0.0 --port 9099 # python -m uvicorn app:app --host 0.0.0.0 --port 9099
@ -15,8 +15,6 @@ 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
@ -33,15 +31,13 @@ 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
@ -54,7 +50,7 @@ IPv4_RE = re.compile(
r"(25[0-5]|2[0-4]\d|[01]?\d?\d)$" r"(25[0-5]|2[0-4]\d|[01]?\d?\d)$"
) )
app = FastAPI(title="EC2 EIP Rotator (Auto-Region, Optimized)") app = FastAPI(title="Lightsail Static IP Rotator (Auto-Region, Optimized)")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@ -64,8 +60,8 @@ app.add_middleware(
) )
# ---------------- helpers ---------------- # ---------------- helpers ----------------
def ec2_in(region: str): def lightsail_in(region: str):
return boto3.client("ec2", region_name=region) return boto3.client("lightsail", region_name=region)
def sts_in(region: str): def sts_in(region: str):
return boto3.client("sts", region_name=region) return boto3.client("sts", region_name=region)
@ -82,10 +78,10 @@ def account_id(seed_region: str) -> str:
def list_regions(seed_region: str) -> List[str]: def list_regions(seed_region: str) -> List[str]:
"""从 seed 区域取区域清单;失败则回退 us-east-1。""" """从 seed 区域取区域清单;失败则回退 us-east-1。"""
try: try:
regions = ec2_in(seed_region).describe_regions(AllRegions=False)["Regions"] regions = lightsail_in(seed_region).get_regions()["regions"]
except Exception: except Exception:
regions = ec2_in("us-east-1").describe_regions(AllRegions=False)["Regions"] regions = lightsail_in("us-east-1").get_regions()["regions"]
return [r["RegionName"] for r in regions] return [r["name"] for r in regions]
def json_error(message: str, status: int = 400): def json_error(message: str, status: int = 400):
raise HTTPException(status_code=status, detail=message) raise HTTPException(status_code=status, detail=message)
@ -122,115 +118,6 @@ 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}
@ -249,8 +136,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
"release_old": true "release_old": true
} }
步骤: 步骤:
1) 枚举区域, describe_addresses(PublicIps=[ip]) 找到归属区 1) 枚举区域, get_static_ips() 找到归属区
2) InstanceId -> allocate_address(Domain='vpc') -> associate -> (可选) release 旧EIP 2) InstanceName -> allocate_static_ip() -> attach_static_ip() -> (可选) release 旧静态IP
""" """
current_ip = (body.get("current_ip") or "").strip() current_ip = (body.get("current_ip") or "").strip()
release_old = bool(body.get("release_old", True)) release_old = bool(body.get("release_old", True))
@ -269,88 +156,77 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
# 1) 自动查找 IP 归属区域 # 1) 自动查找 IP 归属区域
for region in regions: for region in regions:
try: try:
out = ec2_in(region).describe_addresses(PublicIps=[current_ip]) out = lightsail_in(region).get_static_ips()
arr = out.get("Addresses", []) arr = out.get("staticIps", [])
if arr: for static_ip in arr:
home_region = region if static_ip.get("ipAddress") == current_ip:
addr = arr[0] home_region = region
addr = static_ip
break
if home_region:
break break
except ClientError as e: except ClientError as e:
code = e.response.get("Error", {}).get("Code") code = e.response.get("Error", {}).get("Code")
# 不是该区会抛 InvalidAddress.NotFound / InvalidParameterValue,忽略 # 不是该区会抛 NotFoundException,忽略
if code in ("InvalidAddress.NotFound", "InvalidParameterValue"): if code in ("NotFoundException", "InvalidParameterValue"):
continue continue
json_error(str(e), 400) json_error(str(e), 400)
if not home_region or not addr: if not home_region or not addr:
remark = "EIP 不属于当前账号的任一区域,或不是 Elastic IP" remark = "静态IP 不属于当前账号的任一区域,或不是 Lightsail 静态IP"
append_log({ append_log({
"time": now_ts(), "region": "-", "instance_name": "-", "time": now_ts(), "region": "-", "instance_name": "-",
"instance_id": "-", "arn": "-", "old_ip": current_ip, "instance_id": "-", "arn": "-", "old_ip": current_ip,
"new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark, "new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark,
}) })
json_error(f"EIP {current_ip} not found in any region of this account", 404) json_error(f"静态IP {current_ip} not found in any region of this account", 404)
# 2) 在归属区执行更换 # 2) 在归属区执行更换
ec2 = ec2_in(home_region) lightsail = lightsail_in(home_region)
inst_id = addr.get("InstanceId") inst_name = addr.get("attachedTo")
old_alloc = addr.get("AllocationId") old_static_ip_name = addr.get("name")
if not inst_id: if not inst_name:
remark = " EIP 未绑定到任何实例" remark = "静态IP 未绑定到任何实例"
append_log({ append_log({
"time": now_ts(), "region": home_region, "instance_name": "-", "time": now_ts(), "region": home_region, "instance_name": "-",
"instance_id": "-", "arn": "-", "old_ip": current_ip, "instance_id": "-", "arn": "-", "old_ip": current_ip,
"new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark, "new_ip": "-", "retry": 0, "status": "FAIL", "remark": remark,
}) })
json_error(f"EIP {current_ip} is not attached to any instance", 400) json_error(f"静态IP {current_ip} is not attached to any instance", 400)
# 实例名(日志显示) # 实例名(日志显示)
inst_name = inst_id
try: try:
di = ec2.describe_instances(InstanceIds=[inst_id]) di = lightsail.get_instance(instanceName=inst_name)
for r in di.get("Reservations", []): inst_name = di.get("instance", {}).get("name", inst_name)
for ins in r.get("Instances", []):
for t in ins.get("Tags", []):
if t.get("Key") == "Name":
inst_name = t.get("Value")
except Exception: except Exception:
pass pass
try: try:
# allocate new # allocate new static IP
new_addr = with_retry(ec2.allocate_address, Domain="vpc") new_static_ip_name = f"rotated-{int(time.time())}"
new_alloc = new_addr["AllocationId"] with_retry(lightsail.allocate_static_ip, staticIpName=new_static_ip_name)
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 打标签(便于审计/回溯) # attach to instance
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
# associate to instance (force)
with_retry( with_retry(
ec2.associate_address, lightsail.attach_static_ip,
AllocationId=new_alloc, staticIpName=new_static_ip_name,
InstanceId=inst_id, instanceName=inst_name,
AllowReassociation=True,
) )
# optional release old # optional release old
remark = "success" remark = "success"
released = False released = False
if release_old and old_alloc and old_alloc != new_alloc: if release_old and old_static_ip_name and old_static_ip_name != new_static_ip_name:
try: try:
with_retry(ec2.release_address, AllocationId=old_alloc) # 先分离旧静态IP
with_retry(lightsail.detach_static_ip, staticIpName=old_static_ip_name)
# 再释放旧静态IP
with_retry(lightsail.release_static_ip, staticIpName=old_static_ip_name)
released = True released = True
except ClientError as re: except ClientError as re:
remark = f"old_release_error: {re}" remark = f"old_release_error: {re}"
@ -359,8 +235,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
"time": now_ts(), "time": now_ts(),
"region": home_region, "region": home_region,
"instance_name": inst_name, "instance_name": inst_name,
"instance_id": inst_id, "instance_id": inst_name, # Lightsail使用实例名作为ID
"arn": f"arn:aws:ec2:{home_region}:{acct}:instance/{inst_id}", "arn": f"arn:aws:lightsail:{home_region}:{acct}:instance/{inst_name}",
"old_ip": current_ip, "old_ip": current_ip,
"new_ip": new_public_ip, "new_ip": new_public_ip,
"retry": 0, "retry": 0,
@ -371,7 +247,7 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
return { return {
"ok": True, "ok": True,
"region": home_region, "region": home_region,
"instance_id": inst_id, "instance_id": inst_name,
"old_ip": current_ip, "old_ip": current_ip,
"new_ip": new_public_ip, "new_ip": new_public_ip,
"old_released": released "old_released": released
@ -382,8 +258,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)):
"time": now_ts(), "time": now_ts(),
"region": home_region, "region": home_region,
"instance_name": inst_name, "instance_name": inst_name,
"instance_id": inst_id, "instance_id": inst_name,
"arn": f"arn:aws:ec2:{home_region}:{acct}:instance/{inst_id}", "arn": f"arn:aws:lightsail:{home_region}:{acct}:instance/{inst_name}",
"old_ip": current_ip, "old_ip": current_ip,
"new_ip": "-", "new_ip": "-",
"retry": 0, "retry": 0,
@ -400,7 +276,7 @@ INDEX_HTML = Template(r"""
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Lightsail IP 更换面板EC2 自动区域</title> <title>Lightsail 静态IP 更换面板自动区域</title>
<style> <style>
:root { color-scheme: dark; } :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; } body { margin:0; background:#0b1020; color:#e2e8f0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft YaHei",sans-serif; }
@ -441,22 +317,22 @@ INDEX_HTML = Template(r"""
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
<h1>Lightsail IP 更换面板EC2 自动区域</h1> <h1>Lightsail 静态IP 更换面板自动区域</h1>
<div class="card hint" style="margin-bottom:12px"> <div class="card hint" style="margin-bottom:12px">
请输入<b>当前绑定在实例上的 EIP 公网IP</b>系统会<b>自动定位所属区域与实例</b>分配<b></b>EIP并绑定然后可选释放旧EIP<br/> 请输入<b>当前绑定在实例上的静态IP公网IP</b>系统会<b>自动定位所属区域与实例</b>分配<b></b>静态IP并绑定然后可选释放旧静态IP<br/>
无需选择区域/实例需权限DescribeAddresses / AllocateAddress / AssociateAddress / ReleaseAddress<br/> 无需选择区域/实例需权限GetStaticIps / AllocateStaticIp / AttachStaticIp / DetachStaticIp / ReleaseStaticIp<br/>
</div> </div>
<div class="card"> <div class="card">
<div class="row"> <div class="row">
<span>当前 EIP 公网IP</span> <span>当前静态IP 公网IP</span>
<input id="inpIp" type="text" placeholder="例如 18.183.203.98" inputmode="numeric" <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)$" /> 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"> <label class="muted" style="display:none;align-items:center;gap:6px">
<input id="chkRelease" type="checkbox" checked/> 更换后释放旧EIP <input id="chkRelease" type="checkbox" checked/> 更换后释放旧静态IP
</label> </label>
<button id="btnRotate" class="primary">换新EIP自动区域</button> <button id="btnRotate" class="primary">换新静态IP自动区域</button>
</div> </div>
</div> </div>
@ -468,7 +344,6 @@ 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">
@ -498,7 +373,6 @@ 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');
@ -554,17 +428,17 @@ INDEX_HTML = Template(r"""
btnRotate.onclick = async ()=>{ btnRotate.onclick = async ()=>{
const ip = (inpIp.value||'').trim(); const ip = (inpIp.value||'').trim();
if(!ip) return alert('请输入当前 EIP 公网IP'); if(!ip) return alert('请输入当前静态IP 公网IP');
if(!IPv4RE.test(ip)) return alert('请输入有效的 IPv4 地址'); if(!IPv4RE.test(ip)) return alert('请输入有效的 IPv4 地址');
showLoading(`正在为 ${ip} 更换 EIP `); showLoading(`正在为 ${ip} 更换静态IP `);
try{ try{
await api('/api/rotate_by_ip', { await api('/api/rotate_by_ip', {
method: 'POST', method: 'POST',
body: JSON.stringify({ current_ip: ip, release_old: true }) body: JSON.stringify({ current_ip: ip, release_old: true })
}); });
hideLoading(); hideLoading();
alert('换新EIP成功'); alert('换新静态IP成功');
await reloadLogs(); await reloadLogs();
}catch(e){ }catch(e){
hideLoading(); hideLoading();
@ -580,27 +454,6 @@ 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>