def parseIp(ipPair, version): """解析本地网卡中的IP信息 Args: ipPair (str): IP信息文本 version (int): 4,6 Returns: tuple: (ip, score) """ ipLine, tlft = ipPair if version == 4: ipMatch = re.findall(r'inet ([0-9\.]+)/(\d+)\s', ipLine) elif version == 6: ipMatch = re.findall(r'inet6 ([0-9a-f:]+)/(\d+)\s', ipLine) APP.debug(ipMatch) ip, prefix = ipMatch[0] prefix = int(prefix) tlftMatch = re.findall( r'valid_lft (\d+sec|forever) preferred_lft (\d+sec|forever)', tlft) APP.debug(tlftMatch) valid, prefer = tlftMatch[0] if prefer == 'forever': score = sys.maxsize else: prefer = int(prefer[:-3]) # Remove 'sec' valid = int(valid[:-3]) # Remove 'sec' score = prefer + valid if 'mngtmpaddr' in ipLine: score = prefer * 2 return (ip, score)
def getIp(version): methodConfig = CONFIG[f'get_ipv{version}'] ipRegex = None ipApi = None if 'regex' in methodConfig: ipRegex = getIpByRegex(version) if 'api' in methodConfig: ipData = getIpByApi(version) ipApi = ipData['ip'] ipUrl = ipData['url'] ip = None if ipRegex and ipApi: ip = ipApi if ipRegex != ipApi: APP.warn(f'IPv{version}不一致', f'从网卡获取的IP为{ipRegex},从{ipUrl}获取的IP为{ipApi}') elif not ipRegex and not ipApi: raise RuntimeError(f'未获取到IPv{version}地址') elif ipApi: ip = ipApi elif ipRegex: ip = ipRegex return ip
def getIpByRegex(version): """获取本机IP地址 """ if version == 4: command = 'ip -4 addr show scope global {} up | ' \ 'grep -v " deprecated" | ' \ 'grep -v " 0sec" | ' \ 'grep -A1 "inet [1-9]"'.format(CONFIG['interface']) elif version == 6: command = 'ip -6 addr show scope global {} up | ' \ 'grep -v " deprecated" | ' \ 'grep -v " 0sec" | ' \ 'grep -A1 "inet6 [^f:]"'.format(CONFIG['interface']) APP.debug(command) ipStr = subprocess.check_output(command, shell=True).decode('utf-8') APP.debug(ipStr) ipParts = [x.strip() for x in ipStr.split('\n')] APP.debug(ipParts) ipPairList = list(zip(ipParts[::2], ipParts[1::2])) APP.debug(f'获取到本地IPv{version} >>> {ipPairList}') if len(ipPairList) == 1: ips = parseIp(ipPairList[0], version) return ips[0] elif len(ipParts) > 1: ipList = [] for ipPair in ipPairList: ips = parseIp(ipPair, version) ipList.append(ips) ipList.sort(key=lambda ip: ip[-1], reverse=True) APP.debug(ipList) return ipList[0][0] else: raise RuntimeError(f'无法找到IPv{version}地址')
def notify_by_server_chan(config, data): prefix = 'error_' if 'error' in data else '' url = 'https://sc.ftqq.com/{sckey}.send'.format(**config) title = config[prefix + 'title'].format(**data) message = config[prefix + 'message'].format(**data) APP.debug(f'Server酱 数据===> {title} ; {message}') res = requests.get(url, params={'text': title, 'desp': message}) APP.debug(f'Server酱推送结果:{res.json()}')
def getIpByApi(version): apiUrls = ['ip.sb', 'myip.com', 'dyndns.com'] for apiUrl in apiUrls: iper = Ip.create(apiUrl, version) ip = iper.getIp() APP.debug(ip) if ip['ip']: return ip return {'ip': None, 'url': None}
def notifyByServerChan(config, data): prefix = 'error_' if 'error' in data else '' url = 'https://sc.ftqq.com/{sckey}.send'.format(**config) title = config[prefix + 'title'].format(**data) message = config[prefix + 'message'].format(**data) APP.debug(f'Server酱 数据===> {title} ; {message}') #p('Server酱===>', title, '; ', message) if not CONFIG['dry']: res = requests.get(url, params={'text': title, 'desp': message}) #p('Server酱推送结果:', res.json()) APP.debug(f'Server酱推送结果:{res.json()}')
def saveIP(version, ip, domains): """保存新IP Args: ip (str): 新IP version (int): 4,6 """ ipFilePath = IP_FILE.format(version) for domain in domains: DOMAIN_IP[domain] = ip dump_json(ipFilePath, DOMAIN_IP) APP.debug(f'新IP地址已经保存到{ipFilePath}')
def notify_by_ding_talk(config, data): """发消息给钉钉机器人 """ dt_data = data.copy() dt_data['comment_li'] = '\n'.join((f'- {c}' for c in data['comments'])) dt_data['command_li'] = '\n'.join((f'- {c}' for c in data['commands'])) dt_data['stdout_li'] = '\n'.join((f'- {c}' for c in data['stdout_list'])) dt_data['stderr_li'] = '\n'.join((f'- {c}' for c in data['stderr_list'])) dt_msg = { "msgtype": 'markdown', "markdown": { 'title': DINGTAIL_SUBJECT.format(**dt_data), 'text': DINGTAIL_BODY.format(**dt_data) } } res = do_notify_by_ding_talk(config, dt_msg) APP.debug(f'钉钉推送结果:{res.json()}')
def notify_by_email(data): mail_data = data.copy() #APP.debug(f'邮件 数据===> {subject} ; {body}') subject = MAIL_SUBJECT.format(**data) mail_data['comment_li'] = ''.join( (f'<li>{c}</li>' for c in data['comments'])) mail_data['command_li'] = ''.join( (f'<li>{c}</li>' for c in data['commands'])) mail_data['stdout_li'] = ''.join( (f'<li>{c}</li>' for c in data['stdout_list'])) mail_data['stderr_li'] = ''.join( (f'<li>{c}</li>' for c in data['stderr_list'])) body = MAIL_BODY.format(**mail_data) res = APP.send_email(subject, html_body=body) if res: APP.error(f'邮件推送失败:{res}') else: APP.debug(f'邮件发送成功,数据===> {subject}')
def notifyByEmail(config, data): prefix = 'error_' if 'error' in data else '' subject = config[prefix + 'subject'].format(**data) body = config[prefix + 'body'].format(**data) APP.debug(f'邮件 数据===> {subject} ; {body}') #p('邮件===>', subject, '; ', body) if not CONFIG['dry']: res = APP.send_email(subject, body) if res: APP.error(f'邮件推送失败:{res}') #p('邮件推送失败:', res, force=True) else: APP.debug('邮件发送成功。')
def notifyByDingTail(config, data): """发消息给钉钉机器人 """ token = config['token'] if not token: APP.error('没有钉钉token') #p('APP.error: 没有钉钉token') return prefix = 'error_' if 'error' in data else '' data = { "msgtype": "text", "text": { "content": config['keyword'] + config[prefix + 'message'].format(**data) }, "at": config['at'] } APP.debug(f'钉钉机器人 数据===> {data}') #p('钉钉机器人===>', data) if not CONFIG['dry']: res = requests.post(url="https://oapi.dingtalk.com/robot/send?access_token={}".format(token), \ headers = {'Content-Type': 'application/json'}, data=json.dumps(data)) #p('钉钉推送结果:', res.json()) APP.debug(f'钉钉推送结果:{res.json()}')
def set_ip(): domains = request_get('domain') token = request_get('token') #print(domains, token) if token != TOKEN: message = {'error': 'Unauthorized'} return jsonify(message), 404 if not domains or type(domains) not in (str, list): message = { 'error': 'Invalid request data', 'example1': { 'domain': 'sub.domain.com' }, 'example2': { 'domain': ['sub1.domain.com', 'sub2.domain.com'] } } return jsonify(message), 422 ipv6 = get_real_ip() ipv4 = ipv6to4(ipv6) newIP = ipv4 if ipv4 else ipv6 version = 4 if ipv4 else 6 dnsType = 'AAAA' if version == 6 else 'A' if type(domains) == str: domains = [domains] results = [] changedDomains = [] for domain in domains: oldIp = getOldIP(version, domain) if newIP != oldIp or CONFIG['force']: APP.info(f'域名{domain}的IPv{version}已发生改变,上次地址为{oldIp}') result = refreshRecord(domain, newIP, version) results.append(result) changedDomains.append(domain) else: APP.debug(f'域名{domain}的{dnsType}纪录未发生改变') results.append({ "status": { "code": "1", "message": "Action completed successful", "created_at": now() }, 'record': { "name": domain, "value": newIP, "status": "unchanged" } }) if not CONFIG['dry']: saveIP(version, newIP, domains) if changedDomains: notify({ 'version': version, 'dnsType': 'AAAA' if version == 6 else 'A', 'ip': newIP, 'domains': ','.join(changedDomains) }) return jsonify(results)
def refreshRecord(subDomainName, newIP, version): """更新记录IP,或添加新记录 Args: subDomainName (str): 记录名(包含域名) newIP (str): IP version (int): 4,6 Raises: RuntimeError: 错误 """ getDomains() recordType = 'AAAA' if version == 6 else 'A' example = { "status": { "code": "1", "message": "Action completed successful", "created_at": now() }, } # TODO: 统一对dry判断 if CONFIG['dry']: example["NOTICE"] = '目前在dry模式下运行' for domainId in DOMAIN_RECORD.keys(): domain = DOMAIN_RECORD[domainId] if domain['name'] in subDomainName: getRecords(domainId) key = f'{subDomainName}:{recordType}' if key in domain['records']: record = domain['records'][key] if record['value'] == newIP: APP.debug(f'{subDomainName}的IPv{version}地址与线上一致:{newIP}') continue data = { 'domain_id': domainId, 'record_id': record['id'], 'sub_domain': record['name'], 'value': newIP, 'record_type': record['type'], 'record_line': '默认' } action = 'Record.Modify' example["record"] = { "id": 16894439, "name": domain['name'], "value": newIP, "status": "enable" } else: data = { 'domain_id': domainId, 'sub_domain': '@' if subDomainName == domain['name'] else subDomainName.replace('.' + domain['name'], ''), 'value': newIP, 'record_type': recordType, 'record_line': '默认' } action = 'Record.Create' example["record"] = { "id": "16894439", "name": domain['name'], "status": "enable" } APP.debug(data) if not CONFIG['dry']: result = requestDnsApi(action, data) APP.debug(result) return result APP.debug(example) # Clear cache DOMAIN_RECORD.clear() return example
def run(version): assert (version == 4 or version == 6) dnsType = 'AAAA' if version == 6 else 'A' domains = CONFIG[f'ipv{version}'] if not domains: APP.debug(f'未配置IPv{version},不需要更新。') return try: APP.debug('*' * 40 + f'IPv{version}' + '*' * 40) newIP = getIp(version) APP.debug(f'解析IPv{version}结果为:{newIP}') changedDomains = [] for subDomain in domains: oldIp = getOldIP(version, subDomain) if newIP != oldIp or CONFIG['force']: APP.info(f'域名{subDomain}的IPv{version}已发生改变,上次地址为{oldIp}') result = refreshRecord(subDomain, newIP, version) status = result['status'] if status['code'] != '1': raise RuntimeError('{}-{}'.format(status['code'], status['message'])) APP.info(f'域名{subDomain}的{dnsType}纪录已经更新为{newIP}') changedDomains.append(subDomain) else: APP.debug(f'域名{subDomain}的{dnsType}纪录未发生改变') if not CONFIG['dry']: saveIP(version, newIP, domains) if changedDomains: notify({ 'version': version, 'dnsType': dnsType, 'ip': newIP, 'domains': ','.join(changedDomains) }) except Exception as ex: APP.error(f'!!!运行失败,原因:{ex}') notify({ 'version': version, 'dnsType': dnsType, 'domains': ','.join(domains), 'error': ex }) if CONFIG['dry']: print('\n\n' + '!' * 20 + f'-这是在dry模式下运行,实际IPv{version}域名未作更新-' + '!' * 20)