""" IP轮换服务模块 提供IP地址轮换功能,包括: - 从可用设备中选择未使用的IP - 配置网关路由规则 - 执行IP轮换操作 - 查询当前状态和统计信息 使用Redis存储使用记录,支持按天统计IP使用情况。 """ import random from typing import Any, Dict, List, Optional, Tuple from .config import settings, city_dict, client_infos, port_mapping from .eip_client import client_singleton as eip from .redis_store import store_singleton as kv from .line_status_manager import status_manager def _extract_device_ip_and_id(device: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]: """ 从设备信息中提取IP地址和边缘设备ID 由于后端返回结构可能不一致,使用多种字段名进行容错提取: - IP字段:ip, public_ip, eip, addr, address - ID字段:id, edge, mac, device_id Args: device: 设备信息字典 Returns: Tuple[Optional[str], Optional[str]]: (IP地址, 边缘设备ID) """ # 尝试多种可能的IP字段名 ip = ( device.get("public") ) macaddr = device.get("macaddr") return (str(ip), str(macaddr)) def select_unused_ip(devices: List[Dict[str, Any]]) -> Tuple[Optional[str], Optional[str]]: """ 从设备列表中选择今天未使用的IP地址 遍历设备列表,找到第一个今天未使用的IP地址和对应的边缘设备ID Args: devices: 设备信息列表 Returns: Tuple[Optional[str], Optional[str]]: (IP地址, 边缘设备ID),如果没找到则返回(None, None) """ for d in devices.get('edges'): # print(settings.max_online) # print(d) if d['online'] < settings.max_online: ip, edge_id = _extract_device_ip_and_id(d) if not ip: # 没有可识别的IP,跳过此设备 continue if not kv.is_ip_used_today(ip): return ip, edge_id return None, None def apply_gateway_route(edge_id: Optional[str], ip: str, geo: str, client_id:str) -> Dict[str, Any]: """ 将选中的边缘设备配置到网关路由规则中 创建路由规则并应用到指定的网关设备,实现IP轮换 Args: edge_id: 边缘设备ID ip: IP地址(用于日志记录,实际路由基于edge_id) Returns: Dict[str, Any]: 网关配置设置结果 """ lines = status_manager.get_all_lines_status() rule = [] for line in lines: if line['id'] == client_id: rule.append({ "table": client_id, # 路由表ID "enable": True, # 启用规则 "edge": [edge_id] if edge_id else [], # 边缘设备列表 "network": [client_infos[str(client_id)]], # 网络配置(当前为空) "cityhash": geo or "", # 城市哈希值 }) else: rule.append({ "table": line['id'], # 路由表ID "enable": True, # 启用规则 "edge": [line['edge_device']], # 边缘设备列表 "network": [client_infos[str(line['id'])]], # 网络配置(当前为空) "cityhash": line['geo_location'], # 城市哈希值 }) config = {"id": 1, "rules": rule} # 配置ID和规则列表 return eip.gateway_config_set(settings.eip_gateway_mac, config) def rotate(client_id,cities) -> Dict[str, Any]: """ 执行IP轮换操作 从指定地理位置获取设备列表,选择未使用的IP进行轮换 Args: cityhash: 城市哈希值,用于指定地理位置 num: 获取设备数量,默认为配置值或10 Returns: Dict[str, Any]: 轮换结果 - changed: bool - 是否成功轮换 - reason: str - 失败原因(当changed=False时) - ip: str - 新的IP地址(当changed=True时) - edge: str - 边缘设备ID(当changed=True时) - status: dict - 网关状态信息(当changed=True时) Raises: ValueError: 当cityhash为空时 """ # 确定地理位置参数 geo = get_random_cityhash(cities) # 确定获取设备数量 n = 1000 # 获取设备列表 devices = eip.list_devices(geo=geo, offset=0, num=n) # 选择未使用的IP ip, edge_id = select_unused_ip(devices) if not ip: return {"changed": False, "reason": "没有可用且今天未使用的 IP"} # 应用网关路由配置 _ = apply_gateway_route(edge_id=edge_id, ip=ip, geo= geo,client_id=client_id) # 记录使用情况 kv.add_used_ip_today(ip) # 标记IP为今天已使用 kv.set_current(ip=ip, edge_id=edge_id) # 设置当前使用的IP和设备 # 获取网关状态 status = eip.gateway_status(settings.eip_gateway_mac) return {"changed": True, "ip": ip, "edge": edge_id, "status": status, "geo": geo} def citie_list(): return {"data":list(city_dict.keys())} def status() -> Dict[str, Any]: """ 获取当前轮换服务状态 返回当前使用的IP、今日使用统计和网关状态信息 Returns: Dict[str, Any]: 状态信息 - current: dict - 当前使用的IP和设备信息 - used_today: int - 今日已使用的IP数量 - gateway: dict - 网关状态信息(如果获取失败则为空字典) """ # 获取当前使用的IP和设备信息 cur = kv.get_current() # 获取今日使用统计 used_count = kv.get_used_count_today() # 获取网关状态(容错处理) gw = {} try: gw = eip.gateway_status(settings.eip_gateway_mac) except Exception: # 网关状态获取失败时使用空字典 pass return {"current": cur, "used_today": used_count, "gateway": gw} def get_random_city(city_list) -> Dict[str, str]: """ 随机获取一个城市编码 从所有可用的城市中随机选择一个,返回城市名称和对应的编码 Returns: Dict[str, str]: 包含城市信息的字典 - province: 省份名称 - city: 城市名称 - cityhash: 城市编码 """ # 收集所有城市信息 all_cities = [] for province, cities in city_dict.items(): if province in city_list: for city, cityhash in cities.items(): all_cities.append({ "province": province, "city": city, "cityhash": cityhash }) # 随机选择一个城市 if not all_cities: raise ValueError("没有可用的城市数据") selected_city = random.choice(all_cities) return selected_city def get_random_cityhash(cities) -> str: """ 随机获取一个城市编码(简化版本) Returns: str: 随机城市编码 """ city_info = get_random_city(cities) return city_info["cityhash"] def get_port(line_id): return port_mapping[line_id]