diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa6d6bc --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# Lightsail 静态IP 更换工具 + +一个用于自动更换 AWS Lightsail 实例静态IP的Web工具,支持自动区域检测和实例定位。 + +## 功能特性 + +- 🔍 **自动区域检测** - 输入静态IP地址后自动定位所属AWS区域 +- 🎯 **自动实例定位** - 自动找到绑定的Lightsail实例 +- 🆕 **新IP分配** - 自动分配新的静态IP地址 +- 🔗 **自动绑定** - 将新静态IP绑定到实例 +- 🗑️ **可选释放** - 可选择是否释放旧静态IP +- 📊 **操作日志** - 记录所有操作历史和状态 +- 🔄 **重试机制** - 处理临时错误和API限流 +- 🌐 **Web界面** - 简洁易用的Web操作界面 + +## 系统要求 + +- Python 3.7+ +- AWS账户和相应的API权限 +- 网络访问AWS Lightsail服务 + +## 安装步骤 + +### 1. 克隆项目 + +```bash +git clone +cd Ec2ElasticIpSwapper +``` + +### 2. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 3. 配置AWS凭证 + +#### 方法一:环境变量(推荐) + +创建 `.env` 文件: + +```bash +# .env +AWS_REGION=ap-northeast-1 +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=xxxx... +``` + +#### 方法二:AWS CLI配置 + +```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` + +## 使用方法 + +1. **输入当前静态IP** - 在输入框中输入当前绑定在Lightsail实例上的静态IP地址 +2. **点击更换按钮** - 系统会自动: + - 检测IP所属区域 + - 找到绑定的实例 + - 分配新的静态IP + - 绑定到实例 + - 释放旧静态IP(可选) +3. **查看操作日志** - 在下方表格中查看所有操作记录 + +## 所需AWS权限 + +确保您的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": "*" + } + ] +} +``` + +## API接口 + +### 更换静态IP + +```http +POST /api/rotate_by_ip +Content-Type: application/json + +{ + "current_ip": "1.2.3.4", + "release_old": true +} +``` + +**响应示例:** + +```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 +} +``` + +### 获取操作日志 + +```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. **"静态IP not found in any region"** + - 检查IP地址是否正确 + - 确认IP属于当前AWS账户 + - 验证AWS凭证权限 + +2. **"静态IP is not attached to any instance"** + - 确认静态IP已绑定到Lightsail实例 + - 检查实例状态是否正常 + +3. **权限错误** + - 检查AWS凭证是否正确配置 + - 确认具有所需的Lightsail权限 + +### 日志查看 + +- 在Web界面下方查看操作日志 +- 每个操作都会记录时间、区域、实例、IP地址、状态等信息 + +## 注意事项 + +- ⚠️ **备份重要数据** - 更换IP可能影响服务连接 +- ⚠️ **DNS更新** - 更换IP后需要更新相关DNS记录 +- ⚠️ **防火墙规则** - 检查安全组和防火墙规则 +- ⚠️ **费用考虑** - 未使用的静态IP会产生费用,建议及时释放 + +## 技术栈 + +- **后端**: FastAPI + Python +- **AWS SDK**: Boto3 +- **前端**: 原生HTML/CSS/JavaScript +- **服务器**: Uvicorn ASGI + +## 许可证 + +MIT License + +## 贡献 + +欢迎提交Issue和Pull Request! + +## 更新日志 + +### v1.0.0 +- 初始版本 +- 支持Lightsail静态IP自动更换 +- Web界面和API接口 +- 自动区域检测和实例定位 diff --git a/app.py b/app.py index 22076d2..71ef413 100644 --- a/app.py +++ b/app.py @@ -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 # python -m uvicorn app:app --host 0.0.0.0 --port 9099 @@ -50,7 +50,7 @@ IPv4_RE = re.compile( 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( CORSMiddleware, allow_origins=["*"], @@ -60,8 +60,8 @@ app.add_middleware( ) # ---------------- helpers ---------------- -def ec2_in(region: str): - return boto3.client("ec2", region_name=region) +def lightsail_in(region: str): + return boto3.client("lightsail", region_name=region) def sts_in(region: str): return boto3.client("sts", region_name=region) @@ -78,10 +78,10 @@ def account_id(seed_region: str) -> str: def list_regions(seed_region: str) -> List[str]: """从 seed 区域取区域清单;失败则回退 us-east-1。""" try: - regions = ec2_in(seed_region).describe_regions(AllRegions=False)["Regions"] + regions = lightsail_in(seed_region).get_regions()["regions"] except Exception: - regions = ec2_in("us-east-1").describe_regions(AllRegions=False)["Regions"] - return [r["RegionName"] for r in regions] + regions = lightsail_in("us-east-1").get_regions()["regions"] + return [r["name"] for r in regions] def json_error(message: str, status: int = 400): raise HTTPException(status_code=status, detail=message) @@ -136,8 +136,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)): "release_old": true } 步骤: - 1) 枚举区域, 用 describe_addresses(PublicIps=[ip]) 找到归属区 - 2) 取 InstanceId -> allocate_address(Domain='vpc') -> associate -> (可选) release 旧EIP + 1) 枚举区域, 用 get_static_ips() 找到归属区 + 2) 取 InstanceName -> allocate_static_ip() -> attach_static_ip() -> (可选) release 旧静态IP """ current_ip = (body.get("current_ip") or "").strip() release_old = bool(body.get("release_old", True)) @@ -156,88 +156,77 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)): # 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] + out = lightsail_in(region).get_static_ips() + arr = out.get("staticIps", []) + for static_ip in arr: + if static_ip.get("ipAddress") == current_ip: + home_region = region + addr = static_ip + break + if home_region: break except ClientError as e: code = e.response.get("Error", {}).get("Code") - # 不是该区会抛 InvalidAddress.NotFound / InvalidParameterValue,忽略 - if code in ("InvalidAddress.NotFound", "InvalidParameterValue"): + # 不是该区会抛 NotFoundException,忽略 + if code in ("NotFoundException", "InvalidParameterValue"): continue json_error(str(e), 400) if not home_region or not addr: - remark = "EIP 不属于当前账号的任一区域,或不是 Elastic IP" + remark = "静态IP 不属于当前账号的任一区域,或不是 Lightsail 静态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) + json_error(f"静态IP {current_ip} not found in any region of this account", 404) # 2) 在归属区执行更换 - ec2 = ec2_in(home_region) - inst_id = addr.get("InstanceId") - old_alloc = addr.get("AllocationId") + lightsail = lightsail_in(home_region) + inst_name = addr.get("attachedTo") + old_static_ip_name = addr.get("name") - if not inst_id: - remark = "该 EIP 未绑定到任何实例" + if not inst_name: + remark = "该静态IP 未绑定到任何实例" 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) + json_error(f"静态IP {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") + di = lightsail.get_instance(instanceName=inst_name) + inst_name = di.get("instance", {}).get("name", inst_name) except Exception: pass try: - # allocate new - new_addr = with_retry(ec2.allocate_address, Domain="vpc") - new_alloc = new_addr["AllocationId"] - new_public_ip = new_addr["PublicIp"] + # allocate new static IP + new_static_ip_name = f"rotated-{int(time.time())}" + with_retry(lightsail.allocate_static_ip, staticIpName=new_static_ip_name) + + # 获取新静态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 - - # associate to instance (force) + # attach to instance with_retry( - ec2.associate_address, - AllocationId=new_alloc, - InstanceId=inst_id, - AllowReassociation=True, + lightsail.attach_static_ip, + staticIpName=new_static_ip_name, + instanceName=inst_name, ) # optional release old remark = "success" 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: - 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 except ClientError as re: remark = f"old_release_error: {re}" @@ -246,8 +235,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)): "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}", + "instance_id": inst_name, # Lightsail使用实例名作为ID + "arn": f"arn:aws:lightsail:{home_region}:{acct}:instance/{inst_name}", "old_ip": current_ip, "new_ip": new_public_ip, "retry": 0, @@ -258,7 +247,7 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)): return { "ok": True, "region": home_region, - "instance_id": inst_id, + "instance_id": inst_name, "old_ip": current_ip, "new_ip": new_public_ip, "old_released": released @@ -269,8 +258,8 @@ def rotate_by_ip(body: Dict[str, Any] = Body(...)): "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}", + "instance_id": inst_name, + "arn": f"arn:aws:lightsail:{home_region}:{acct}:instance/{inst_name}", "old_ip": current_ip, "new_ip": "-", "retry": 0, @@ -287,7 +276,7 @@ INDEX_HTML = Template(r""" -Lightsail IP 更换面板(EC2 自动区域) +Lightsail 静态IP 更换面板(自动区域)