feat:自动更换ip+流量监控
This commit is contained in:
132
services/lightsail_static_ip.py
Normal file
132
services/lightsail_static_ip.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""AWS Lightsail Static IP 操作"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from .aws_region import normalize_aws_region
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LightsailStaticIp:
|
||||
name: str
|
||||
ip_address: str | None
|
||||
attached_to: str | None
|
||||
is_attached: bool
|
||||
|
||||
|
||||
_INSTANCE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
|
||||
|
||||
|
||||
def is_valid_lightsail_instance_name(value: str | None) -> bool:
|
||||
if not value:
|
||||
return False
|
||||
return bool(_INSTANCE_NAME_RE.match(value.strip()))
|
||||
|
||||
|
||||
def create_lightsail_client(
|
||||
*,
|
||||
region: str,
|
||||
aws_access_key: str | None,
|
||||
aws_secret_key: str | None,
|
||||
):
|
||||
kwargs: dict[str, Any] = {"region_name": normalize_aws_region(region)}
|
||||
if aws_access_key and aws_secret_key:
|
||||
kwargs["aws_access_key_id"] = aws_access_key
|
||||
kwargs["aws_secret_access_key"] = aws_secret_key
|
||||
return boto3.client("lightsail", **kwargs)
|
||||
|
||||
|
||||
def _list_static_ips(client) -> list[LightsailStaticIp]:
|
||||
resp = client.get_static_ips()
|
||||
items = resp.get("staticIps") or []
|
||||
result: list[LightsailStaticIp] = []
|
||||
for item in items:
|
||||
result.append(
|
||||
LightsailStaticIp(
|
||||
name=item.get("name") or "",
|
||||
ip_address=item.get("ipAddress"),
|
||||
attached_to=item.get("attachedTo"),
|
||||
is_attached=bool(item.get("isAttached")),
|
||||
)
|
||||
)
|
||||
return [ip for ip in result if ip.name]
|
||||
|
||||
|
||||
def _get_attached_static_ip(client, *, instance_name: str) -> LightsailStaticIp | None:
|
||||
for ip in _list_static_ips(client):
|
||||
if ip.is_attached and ip.attached_to == instance_name:
|
||||
return ip
|
||||
return None
|
||||
|
||||
|
||||
def _generate_static_ip_name(instance_name: str) -> str:
|
||||
safe = re.sub(r"[^A-Za-z0-9-]+", "-", instance_name).strip("-").lower() or "instance"
|
||||
safe = safe[:24]
|
||||
ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
||||
return f"proxyauto-{safe}-{ts}"
|
||||
|
||||
|
||||
def rotate_lightsail_static_ip(
|
||||
client,
|
||||
*,
|
||||
instance_name: str,
|
||||
release_old: bool,
|
||||
) -> dict[str, str]:
|
||||
current = _get_attached_static_ip(client, instance_name=instance_name)
|
||||
|
||||
if current:
|
||||
client.detach_static_ip(staticIpName=current.name)
|
||||
|
||||
if release_old:
|
||||
for attempt in range(8):
|
||||
try:
|
||||
client.release_static_ip(staticIpName=current.name)
|
||||
break
|
||||
except ClientError as exc:
|
||||
if attempt == 7:
|
||||
raise
|
||||
code = exc.response.get("Error", {}).get("Code")
|
||||
if code in {"OperationFailureException", "InvalidInputException"}:
|
||||
time.sleep(1)
|
||||
continue
|
||||
raise
|
||||
|
||||
new_name = _generate_static_ip_name(instance_name)
|
||||
for attempt in range(5):
|
||||
try:
|
||||
client.allocate_static_ip(staticIpName=new_name)
|
||||
break
|
||||
except ClientError as exc:
|
||||
if attempt == 4:
|
||||
raise
|
||||
message = (exc.response.get("Error", {}).get("Message") or "").lower()
|
||||
if "already exists" in message or "alreadyexist" in message:
|
||||
new_name = f"{new_name}-{attempt + 1}"
|
||||
continue
|
||||
raise
|
||||
|
||||
client.attach_static_ip(staticIpName=new_name, instanceName=instance_name)
|
||||
|
||||
public_ip: str | None = None
|
||||
for _ in range(20):
|
||||
try:
|
||||
resp = client.get_static_ip(staticIpName=new_name)
|
||||
static_ip = resp.get("staticIp") or {}
|
||||
public_ip = static_ip.get("ipAddress")
|
||||
if public_ip:
|
||||
break
|
||||
except ClientError:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
if not public_ip:
|
||||
raise RuntimeError("Lightsail 未返回新的 Static IP 地址,请稍后重试")
|
||||
|
||||
return {"public_ip": public_ip, "static_ip_name": new_name}
|
||||
Reference in New Issue
Block a user