"""IP 更换核心逻辑""" from __future__ import annotations import logging from datetime import datetime, timezone from database import Config, ProxyMachine, get_session, ensure_singleton_config from services.aws_eip import create_ec2_client, is_valid_instance_id, rotate_elastic_ip from services.lightsail_static_ip import ( create_lightsail_client, is_valid_lightsail_instance_name, rotate_lightsail_static_ip, ) from services.cloudflare_dns import CloudflareAuth, upsert_a_record logger = logging.getLogger(__name__) def _get_cf_auth(config: Config) -> CloudflareAuth: auth_type = (config.cloudflare_auth_type or "").strip() or "api_token" if auth_type == "api_token": return CloudflareAuth(auth_type="api_token", api_token=config.cf_api_token) return CloudflareAuth( auth_type="global_key", email=config.cf_email, api_key=config.cf_api_key, ) def change_ip_for_machine(machine: ProxyMachine, config: Config) -> dict[str, str]: """为指定机器更换 IP 并更新 DNS""" if not machine.enabled: raise ValueError(f"机器 {machine.name} 已被禁用") aws_service = (machine.aws_service or "ec2").strip().lower() # 执行 IP 轮换 if aws_service == "lightsail": if not is_valid_lightsail_instance_name(machine.aws_instance_id): raise ValueError( f"Lightsail 实例名格式不正确:{machine.aws_instance_id}" ) lightsail = create_lightsail_client( region=machine.aws_region, aws_access_key=config.aws_access_key, aws_secret_key=config.aws_secret_key, ) logger.info( "Rotating Lightsail Static IP for instance %s (%s)", machine.aws_instance_id, machine.name, ) aws_result = rotate_lightsail_static_ip( lightsail, instance_name=machine.aws_instance_id, release_old=bool(config.release_old_eip), ) else: if not is_valid_instance_id(machine.aws_instance_id): raise ValueError( f"EC2 Instance ID 格式不正确:{machine.aws_instance_id}(应类似 i-xxxxxxxxxxxxxxxxx)" ) ec2 = create_ec2_client( region=machine.aws_region, aws_access_key=config.aws_access_key, aws_secret_key=config.aws_secret_key, ) logger.info( "Rotating Elastic IP for instance %s (%s)", machine.aws_instance_id, machine.name, ) aws_result = rotate_elastic_ip( ec2, instance_id=machine.aws_instance_id, release_old=bool(config.release_old_eip), ) public_ip = aws_result.get("public_ip") if not public_ip: raise RuntimeError("AWS 未返回新的 Public IP") message = f"IP 已更换为 {public_ip}" # 如果机器配置了域名,更新 DNS if machine.cf_zone_id and machine.cf_record_name: logger.info("Updating Cloudflare A record %s -> %s", machine.cf_record_name, public_ip) record = upsert_a_record( zone_id=machine.cf_zone_id, record_name=machine.cf_record_name, ip=public_ip, proxied=bool(machine.cf_proxied), record_id=machine.cf_record_id, auth=_get_cf_auth(config), ) record_id = record.get("id") if record_id: machine.cf_record_id = record_id message = f"已更新 {machine.cf_record_name} -> {public_ip}" return { "public_ip": public_ip, "message": message, } def run_ip_change_for_machine(machine_id: int) -> dict: """为指定机器执行一次 IP 更换""" session = get_session() try: machine = session.query(ProxyMachine).filter_by(id=machine_id).first() if not machine: return {"ok": False, "message": "机器不存在"} config = ensure_singleton_config(session) started_at = datetime.now(timezone.utc) try: result = change_ip_for_machine(machine, config) machine.last_run_at = started_at machine.last_success = True machine.current_ip = result.get("public_ip") machine.last_message = result.get("message") or "OK" session.add(machine) session.commit() logger.info("IP change success for %s: %s", machine.name, machine.current_ip) return {"ok": True, "machine_name": machine.name, **result} except Exception as exc: logger.exception("IP change failed for %s", machine.name) machine.last_run_at = started_at machine.last_success = False machine.last_message = str(exc) session.add(machine) session.commit() return {"ok": False, "machine_name": machine.name, "message": str(exc)} finally: session.close() # 兼容旧接口 def run_ip_change() -> dict: """执行一次 IP 更换(兼容旧接口)""" session = get_session() try: # 获取第一台启用的机器 machine = session.query(ProxyMachine).filter_by(enabled=True).first() if not machine: return {"ok": False, "message": "没有可用的机器"} return run_ip_change_for_machine(machine.id) finally: session.close()