初始化1
This commit is contained in:
126
dns-api/app.py
Normal file
126
dns-api/app.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
DNS 解析管理 API — 独立版本
|
||||
部署在 101.132.183.138:32210 上,仅提供阿里云 DNS 解析管理
|
||||
从 10.1.1.160 迁移而来,去掉了 Nginx/SSL 相关依赖
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from flask import Flask, request
|
||||
from dns_service import DnsService
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['JSON_AS_ASCII'] = False # 中文不转 Unicode
|
||||
|
||||
# 初始化 DNS 服务(阿里云 AccessKey 从环境变量读取)
|
||||
dns_service = DnsService()
|
||||
|
||||
# 默认域名 zone,通过环境变量配置,默认 zgitm.com
|
||||
DEFAULT_ZONE = os.environ.get('DNS_DEFAULT_ZONE', 'zgitm.com')
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 健康检查
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@app.route('/api/nginx/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""健康检查"""
|
||||
ali_key = os.environ.get('Ali_Key', '')
|
||||
return {
|
||||
'code': 0,
|
||||
'message': 'DNS Manager API is running',
|
||||
'data': {
|
||||
'service': 'dns-api',
|
||||
'version': '2.0',
|
||||
'default_zone': DEFAULT_ZONE,
|
||||
'ali_key_configured': bool(ali_key),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# DNS 解析管理接口
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@app.route('/api/nginx/dns/records', methods=['GET'])
|
||||
def list_dns_records():
|
||||
"""获取 DNS 解析记录列表"""
|
||||
try:
|
||||
zone = request.args.get('zone', DEFAULT_ZONE)
|
||||
rr = request.args.get('rr', None)
|
||||
rtype = request.args.get('type', None)
|
||||
records = dns_service.list_records(zone, rr_keyword=rr, record_type=rtype)
|
||||
return {'code': 0, 'message': 'success', 'data': records}
|
||||
except Exception as e:
|
||||
logger.error(f"获取DNS记录失败: {e}")
|
||||
return {'code': -1, 'message': str(e), 'data': []}
|
||||
|
||||
|
||||
@app.route('/api/nginx/dns/records', methods=['POST'])
|
||||
def add_dns_record():
|
||||
"""添加 DNS 解析记录"""
|
||||
try:
|
||||
body = request.get_json(force=True)
|
||||
zone = body.get('zone', DEFAULT_ZONE)
|
||||
rr = body.get('rr', '').strip()
|
||||
record_type = body.get('type', 'A').upper()
|
||||
value = body.get('value', '').strip()
|
||||
ttl = body.get('ttl', 600)
|
||||
|
||||
if not rr:
|
||||
return {'code': -1, 'message': '主机记录(rr)不能为空'}, 400
|
||||
if not value:
|
||||
return {'code': -1, 'message': '记录值(value)不能为空'}, 400
|
||||
|
||||
result = dns_service.add_record(zone, rr, record_type, value, ttl)
|
||||
return {'code': 0, 'message': f'{result["full_domain"]} 记录已添加', 'data': result}
|
||||
except Exception as e:
|
||||
logger.error(f"添加DNS记录失败: {e}")
|
||||
return {'code': -1, 'message': str(e)}, 500
|
||||
|
||||
|
||||
@app.route('/api/nginx/dns/records/<record_id>', methods=['DELETE'])
|
||||
def delete_dns_record(record_id):
|
||||
"""删除 DNS 解析记录"""
|
||||
try:
|
||||
dns_service.delete_record(record_id)
|
||||
return {'code': 0, 'message': 'DNS记录已删除'}
|
||||
except Exception as e:
|
||||
logger.error(f"删除DNS记录失败: {e}")
|
||||
return {'code': -1, 'message': str(e)}, 500
|
||||
|
||||
|
||||
@app.route('/api/nginx/dns/records/<record_id>', methods=['PUT'])
|
||||
def update_dns_record(record_id):
|
||||
"""修改 DNS 解析记录"""
|
||||
try:
|
||||
body = request.get_json(force=True)
|
||||
rr = body.get('rr', '@')
|
||||
record_type = body.get('type', 'A').upper()
|
||||
value = body.get('value', '').strip()
|
||||
ttl = body.get('ttl', 600)
|
||||
|
||||
if not value:
|
||||
return {'code': -1, 'message': '记录值(value)不能为空'}, 400
|
||||
|
||||
dns_service.update_record(record_id, rr, record_type, value, ttl)
|
||||
return {'code': 0, 'message': 'DNS记录已更新'}
|
||||
except Exception as e:
|
||||
logger.error(f"修改DNS记录失败: {e}")
|
||||
return {'code': -1, 'message': str(e)}, 500
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 启动入口
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info(f"DNS Manager API 启动在端口 32210, 默认 zone: {DEFAULT_ZONE}")
|
||||
app.run(host='0.0.0.0', port=32210, debug=False)
|
||||
228
dns-api/dns_service.py
Normal file
228
dns-api/dns_service.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
阿里云 DNS 解析管理服务
|
||||
通过阿里云 OpenAPI 管理 zgitm.com 的 DNS 解析记录
|
||||
使用 HMAC-SHA1 签名,兼容 Python 3.6+
|
||||
"""
|
||||
import os
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import logging
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 阿里云 API 配置
|
||||
ALIDNS_ENDPOINT = 'https://alidns.aliyuncs.com'
|
||||
API_VERSION = '2015-01-09'
|
||||
SIGNATURE_METHOD = 'HMAC-SHA1'
|
||||
SIGNATURE_VERSION = '1.0'
|
||||
|
||||
|
||||
class DnsService:
|
||||
"""阿里云 DNS 解析管理"""
|
||||
|
||||
def __init__(self):
|
||||
self.access_key_id = os.environ.get('Ali_Key', '')
|
||||
self.access_key_secret = os.environ.get('Ali_Secret', '')
|
||||
|
||||
def _sign(self, method, params):
|
||||
"""
|
||||
HMAC-SHA1 签名(阿里云 API 签名 v1.0)
|
||||
"""
|
||||
# 1. 按参数名排序
|
||||
sorted_keys = sorted(params.keys())
|
||||
sorted_params = [(k, params[k]) for k in sorted_keys]
|
||||
|
||||
# 2. 构造规范化查询字符串
|
||||
canonicalized = urllib.parse.urlencode(sorted_params)
|
||||
|
||||
# 3. 构造签名源字符串
|
||||
string_to_sign = method + '&' + urllib.parse.quote('/', safe='') + '&' + urllib.parse.quote(canonicalized, safe='')
|
||||
|
||||
# 4. HMAC-SHA1 签名
|
||||
key = (self.access_key_secret + '&').encode('utf-8')
|
||||
h = hmac.new(key, string_to_sign.encode('utf-8'), hashlib.sha1)
|
||||
signature = base64.b64encode(h.digest()).decode('utf-8')
|
||||
|
||||
return signature
|
||||
|
||||
def _call(self, action, extra_params=None):
|
||||
"""
|
||||
调用阿里云 OpenAPI
|
||||
|
||||
Args:
|
||||
action: API 名称 (如 DescribeDomainRecords)
|
||||
extra_params: 额外参数
|
||||
|
||||
Returns:
|
||||
dict: API 响应
|
||||
"""
|
||||
if not self.access_key_id or not self.access_key_secret:
|
||||
raise RuntimeError('阿里云 AccessKey 未配置')
|
||||
|
||||
# 公共参数
|
||||
params = {
|
||||
'AccessKeyId': self.access_key_id,
|
||||
'Action': action,
|
||||
'Format': 'JSON',
|
||||
'Version': API_VERSION,
|
||||
'SignatureMethod': SIGNATURE_METHOD,
|
||||
'SignatureVersion': SIGNATURE_VERSION,
|
||||
'SignatureNonce': str(uuid.uuid4()),
|
||||
'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
||||
}
|
||||
if extra_params:
|
||||
params.update(extra_params)
|
||||
|
||||
# 签名
|
||||
params['Signature'] = self._sign('GET', params)
|
||||
|
||||
# 构造 URL
|
||||
url = ALIDNS_ENDPOINT + '/?' + urllib.parse.urlencode(params)
|
||||
|
||||
# 发送请求
|
||||
try:
|
||||
req = urllib.request.Request(url, method='GET')
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
result = json.loads(resp.read().decode('utf-8'))
|
||||
return result
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8') if e.fp else ''
|
||||
logger.error(f'DNS API HTTP {e.code}: {body}')
|
||||
raise RuntimeError(f'DNS API 请求失败: {e.code} {body}')
|
||||
except Exception as e:
|
||||
logger.error(f'DNS API 异常: {e}')
|
||||
raise RuntimeError(f'DNS API 异常: {e}')
|
||||
|
||||
def list_records(self, domain_zone, rr_keyword=None, record_type=None):
|
||||
"""
|
||||
获取域名解析记录列表
|
||||
|
||||
Args:
|
||||
domain_zone: 主域名 (如 zgitm.com)
|
||||
rr_keyword: 主机记录筛选 (如 www)
|
||||
record_type: 记录类型筛选 (如 A, CNAME)
|
||||
|
||||
Returns:
|
||||
list[dict]: 解析记录列表
|
||||
"""
|
||||
params = {'DomainName': domain_zone}
|
||||
if rr_keyword:
|
||||
params['RRKeyWord'] = rr_keyword
|
||||
if record_type:
|
||||
params['Type'] = record_type
|
||||
|
||||
resp = self._call('DescribeDomainRecords', params)
|
||||
records = resp.get('DomainRecords', {}).get('Record', [])
|
||||
# 统一转列表
|
||||
if isinstance(records, dict):
|
||||
records = [records]
|
||||
|
||||
result = []
|
||||
for r in records:
|
||||
result.append({
|
||||
'record_id': r.get('RecordId', ''),
|
||||
'rr': r.get('RR', ''),
|
||||
'type': r.get('Type', 'A'),
|
||||
'value': r.get('Value', ''),
|
||||
'ttl': r.get('TTL', 600),
|
||||
'line': r.get('Line', 'default'),
|
||||
'status': r.get('Status', 'ENABLE'),
|
||||
'full_domain': (r.get('RR', '') + '.' + domain_zone).strip('.'),
|
||||
})
|
||||
return result
|
||||
|
||||
def add_record(self, domain_zone, rr, record_type, value, ttl=600):
|
||||
"""
|
||||
添加解析记录
|
||||
|
||||
Args:
|
||||
domain_zone: 主域名
|
||||
rr: 主机记录 (如 webssd, @, www)
|
||||
record_type: 记录类型 (A, CNAME, TXT 等)
|
||||
value: 记录值 (IP地址或域名)
|
||||
ttl: TTL 秒数
|
||||
|
||||
Returns:
|
||||
dict: {'record_id': ..., 'full_domain': ...}
|
||||
"""
|
||||
params = {
|
||||
'DomainName': domain_zone,
|
||||
'RR': rr,
|
||||
'Type': record_type.upper(),
|
||||
'Value': value,
|
||||
'TTL': str(ttl),
|
||||
}
|
||||
resp = self._call('AddDomainRecord', params)
|
||||
record_id = resp.get('RecordId', '')
|
||||
full_domain = (rr + '.' + domain_zone).strip('.').replace('@.', '')
|
||||
logger.info(f'DNS 记录已添加: {full_domain} {record_type} {value} (ID: {record_id})')
|
||||
return {'record_id': record_id, 'full_domain': full_domain}
|
||||
|
||||
def delete_record(self, record_id):
|
||||
"""
|
||||
删除解析记录
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
resp = self._call('DeleteDomainRecord', {'RecordId': str(record_id)})
|
||||
logger.info(f'DNS 记录已删除: {record_id}')
|
||||
return True
|
||||
|
||||
def update_record(self, record_id, rr, record_type, value, ttl=600):
|
||||
"""
|
||||
修改解析记录
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
rr: 主机记录
|
||||
record_type: 记录类型
|
||||
value: 记录值
|
||||
ttl: TTL
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
params = {
|
||||
'RecordId': str(record_id),
|
||||
'RR': rr,
|
||||
'Type': record_type.upper(),
|
||||
'Value': value,
|
||||
'TTL': str(ttl),
|
||||
}
|
||||
try:
|
||||
self._call('UpdateDomainRecord', params)
|
||||
logger.info(f'DNS 记录已修改: {record_id} → {rr} {record_type} {value}')
|
||||
except RuntimeError as e:
|
||||
msg = str(e)
|
||||
# 未做任何改动直接保存,阿里云返回 DomainRecordDuplicate,视为成功
|
||||
if 'DomainRecordDuplicate' in msg:
|
||||
logger.info(f'DNS 记录未变更,跳过: {record_id} {rr} {record_type} {value}')
|
||||
else:
|
||||
raise
|
||||
return True
|
||||
|
||||
def set_record_status(self, record_id, status):
|
||||
"""
|
||||
启用/停用解析记录
|
||||
|
||||
Args:
|
||||
record_id: 记录ID
|
||||
status: Enable / Disable
|
||||
"""
|
||||
self._call('SetDomainRecordStatus', {
|
||||
'RecordId': str(record_id),
|
||||
'Status': status.upper(),
|
||||
})
|
||||
logger.info(f'DNS 记录状态已更新: {record_id} → {status}')
|
||||
return True
|
||||
29
dns-api/gunicorn_config.py
Normal file
29
dns-api/gunicorn_config.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Gunicorn 生产环境配置文件 — DNS API
|
||||
用法: gunicorn -c gunicorn_config.py app:app
|
||||
"""
|
||||
import os
|
||||
|
||||
# 监听地址和端口
|
||||
bind = "0.0.0.0:32210"
|
||||
|
||||
# Worker 进程数
|
||||
workers = int(os.environ.get('GUNICORN_WORKERS', 2))
|
||||
|
||||
# Worker 类型
|
||||
worker_class = "sync"
|
||||
|
||||
# 超时
|
||||
timeout = 60
|
||||
graceful_timeout = 10
|
||||
|
||||
# 日志
|
||||
accesslog = "-" # 标准输出
|
||||
errorlog = "-" # 标准错误
|
||||
loglevel = "info"
|
||||
|
||||
# 进程名称
|
||||
proc_name = "dns-manager-api"
|
||||
|
||||
# 后台运行
|
||||
daemon = False
|
||||
2
dns-api/requirements.txt
Normal file
2
dns-api/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
flask==3.1.0
|
||||
gunicorn==23.0.0
|
||||
Reference in New Issue
Block a user