Initial commit: Tencent DNSPod DNS deployment skill

- Support for single record deployment
- Batch deployment from JSON config
- Service quick deployment (Web/API/CDN)
- .env file support for secure credentials
- Complete documentation
This commit is contained in:
OpenClaw
2026-03-01 11:44:05 +08:00
commit 7abea390ad
15 changed files with 2323 additions and 0 deletions

171
scripts/batch_deploy.py Executable file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
DNSPod批量部署脚本
从配置文件批量创建DNS记录
"""
import os
import sys
import json
import time
from pathlib import Path
from deploy_record import (
deploy_record,
check_domain_exists,
create_domain,
load_env
)
# 加载 .env
load_env()
def load_config(config_file):
"""加载配置文件"""
if not Path(config_file).exists():
print(f"错误: 配置文件不存在: {config_file}")
return None
try:
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError as e:
print(f"错误: 配置文件格式错误: {e}")
return None
def batch_deploy(domain, config, create_domain_flag=False, delay=0.5):
"""批量部署DNS记录"""
records = config.get('records', [])
if not records:
print("错误: 配置文件中没有记录")
return False
print(f"\n批量部署开始")
print(f"域名: {domain}")
print(f"记录数量: {len(records)}")
print(f"延迟: {delay}s\n")
# 检查域名是否存在
if not check_domain_exists(domain):
if create_domain_flag:
if not create_domain(domain):
return False
else:
print(f"✗ 域名不存在: {domain}")
print(f"提示: 使用 --create-domain 自动创建域名")
return False
# 统计
success_count = 0
fail_count = 0
skip_count = 0
# 部署每条记录
for idx, record_config in enumerate(records, 1):
print(f"\n[{idx}/{len(records)}] 正在部署...")
subdomain = record_config.get('subdomain', '@')
record_type = record_config.get('type')
value = record_config.get('value')
line = record_config.get('line', '默认')
ttl = record_config.get('ttl', 600)
remark = record_config.get('remark', '')
if not record_type or not value:
print(f"✗ 跳过: 缺少必要参数 (type或value)")
skip_count += 1
continue
# 部署记录
success = deploy_record(
domain=domain,
subdomain=subdomain,
record_type=record_type,
value=value,
line=line,
ttl=ttl,
force=True, # 批量模式自动更新
create_domain_flag=False,
remark=remark
)
if success:
success_count += 1
else:
fail_count += 1
# API限频延迟
if idx < len(records):
time.sleep(delay)
# 输出统计
print(f"\n{'='*50}")
print(f"批量部署完成")
print(f" 成功: {success_count}")
print(f" 失败: {fail_count}")
print(f" 跳过: {skip_count}")
print(f"{'='*50}\n")
return fail_count == 0
def main():
import argparse
parser = argparse.ArgumentParser(description='DNSPod批量部署', formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
配置文件格式 (JSON):
{
"records": [
{
"subdomain": "@",
"type": "A",
"value": "1.2.3.4",
"line": "默认",
"ttl": 600,
"remark": "主域名"
},
{
"subdomain": "www",
"type": "A",
"value": "1.2.3.4",
"line": "默认"
},
{
"subdomain": "api",
"type": "A",
"value": "1.2.3.5",
"line": "电信"
}
]
}
示例:
%(prog)s --domain example.com --config dns-config.json
%(prog)s --domain example.com --config dns-config.json --create-domain
%(prog)s --domain example.com --config dns-config.json --delay 1.0
''')
parser.add_argument('--domain', required=True, help='域名(如: example.com)')
parser.add_argument('--config', required=True, help='配置文件路径(JSON格式)')
parser.add_argument('--create-domain', action='store_true', help='域名不存在时自动创建')
parser.add_argument('--delay', type=float, default=0.5, help='API调用间隔(秒, 默认: 0.5, 避免限频)')
args = parser.parse_args()
# 加载配置
config = load_config(args.config)
if not config:
sys.exit(1)
# 批量部署
success = batch_deploy(
domain=args.domain,
config=config,
create_domain_flag=args.create_domain,
delay=args.delay
)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

68
scripts/delete_record.py Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
DNS记录删除脚本
"""
import os
import sys
from deploy_record import (
call_api,
find_record,
load_env
)
# 加载 .env
load_env()
def delete_record(domain, subdomain, record_type):
"""删除DNS记录"""
print(f"\n删除DNS记录: {subdomain or '@'}.{domain} ({record_type})")
# 查找记录
record = find_record(domain, subdomain, record_type)
if not record:
print(f"✗ 记录不存在")
return False
record_id = record.get("RecordId")
current_value = record.get("Value")
print(f" 记录ID: {record_id}")
print(f" 当前值: {current_value}")
# 确认删除
response = input("\n确认删除? (y/N): ")
if response.lower() != 'y':
print(" 已取消")
return False
# 调用删除API
try:
params = {
"Domain": domain,
"RecordId": record_id
}
result = call_api("DeleteRecord", params)
print(f"✓ 记录删除成功")
return True
except Exception as e:
print(f"✗ 记录删除失败: {e}")
return False
def main():
import argparse
parser = argparse.ArgumentParser(description='删除DNS记录')
parser.add_argument('--domain', required=True, help='域名(如: example.com)')
parser.add_argument('--subdomain', default='@', help='子域名(默认: @)')
parser.add_argument('--type', required=True, help='记录类型(A/CNAME/MX等)')
args = parser.parse_args()
success = delete_record(args.domain, args.subdomain, args.type)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

337
scripts/deploy_record.py Executable file
View File

@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""
DNSPod单条记录部署脚本
用于快速添加或更新DNS记录
支持 .env 文件配置敏感信息
"""
import os
import sys
import json
import hmac
import hashlib
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import urlencode, quote
try:
import requests
except ImportError:
print("错误: 缺少 requests 库")
print("请运行: pip install -r requirements.txt")
sys.exit(1)
# 加载 .env 文件
def load_env():
"""加载.env文件"""
env_file = Path(__file__).parent.parent / '.env'
if env_file.exists():
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip()
# 加载 .env
load_env()
# API配置
API_ENDPOINT = "dnspod.tencentcloudapi.com"
API_VERSION = "2021-03-23"
REGION = "ap-guangzhou"
def get_credentials():
"""从环境变量或.env文件获取腾讯云凭证"""
secret_id = os.getenv('TENCENT_SECRET_ID')
secret_key = os.getenv('TENCENT_SECRET_KEY')
if not secret_id or not secret_key:
print("错误: 未找到腾讯云API密钥")
print("\n请设置环境变量或创建 .env 文件:")
print(" 方式1: 设置环境变量")
print(" export TENCENT_SECRET_ID=\"你的SecretId\"")
print(" export TENCENT_SECRET_KEY=\"你的SecretKey\"")
print("\n 方式2: 创建 .env 文件")
print(" cp .env.example .env")
print(" 然后编辑 .env 文件,填入你的密钥")
print("\n获取密钥地址: https://console.cloud.tencent.com/cam/capi")
sys.exit(1)
return secret_id, secret_key
def sign_request(secret_id, secret_key, action, params):
"""
生成腾讯云API签名
文档: https://cloud.tencent.com/document/product/1427/56152
"""
# 1. 构造请求体
body = json.dumps(params)
# 2. 构造规范请求串
# 请求方法
http_request_method = "POST"
# 请求URI
canonical_uri = "/"
# 请求查询字符串(空)
canonical_querystring = ""
# 请求头
canonical_headers = f"content-type:application/json\nhost:{API_ENDPOINT}\n"
signed_headers = "content-type;host"
# 请求哈希值
hashed_request_payload = hashlib.sha256(body.encode('utf-8')).hexdigest()
canonical_request = (
http_request_method + "\n" +
canonical_uri + "\n" +
canonical_querystring + "\n" +
canonical_headers + "\n" +
signed_headers + "\n" +
hashed_request_payload
)
# 3. 构造待签名字符串
algorithm = "TC3-HMAC-SHA256"
timestamp = int(time.time())
date = datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d')
credential_scope = f"{date}/{API_VERSION}/tc3_request"
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
string_to_sign = (
algorithm + "\n" +
str(timestamp) + "\n" +
credential_scope + "\n" +
hashed_canonical_request
)
# 4. 计算签名
def _hmac_sha256(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
secret_date = _hmac_sha256(f"TC3{secret_key}".encode('utf-8'), date)
secret_service = _hmac_sha256(secret_date, API_VERSION)
secret_signing = _hmac_sha256(secret_service, "tc3_request")
signature = hmac.new(secret_signing, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
# 5. 构造Authorization头
authorization = (
algorithm + " " +
"Credential=" + secret_id + "/" + credential_scope + ", " +
"SignedHeaders=" + signed_headers + ", " +
"Signature=" + signature
)
return {
"authorization": authorization,
"body": body,
"timestamp": timestamp
}
def call_api(action, params):
"""调用腾讯云API"""
secret_id, secret_key = get_credentials()
# 生成签名
sig = sign_request(secret_id, secret_key, action, params)
# 构造请求头
headers = {
"Authorization": sig["authorization"],
"Content-Type": "application/json",
"Host": API_ENDPOINT,
"X-TC-Action": action,
"X-TC-Timestamp": str(sig["timestamp"]),
"X-TC-Version": API_VERSION,
"X-TC-Region": REGION
}
# 发送请求
url = f"https://{API_ENDPOINT}/"
response = requests.post(url, headers=headers, data=sig["body"])
return response.json()
def check_domain_exists(domain):
"""检查域名是否存在"""
try:
result = call_api("DescribeDomainList", {})
domains = [d["Name"] for d in result.get("Response", {}).get("DomainList", [])]
return domain in domains
except Exception as e:
print(f"检查域名失败: {e}")
return False
def create_domain(domain):
"""创建域名"""
print(f"正在创建域名: {domain}")
try:
result = call_api("CreateDomain", {"Domain": domain})
print(f"✓ 域名创建成功: {domain}")
return True
except Exception as e:
print(f"✗ 域名创建失败: {e}")
return False
def find_record(domain, subdomain, record_type):
"""查找现有记录"""
try:
params = {
"Domain": domain,
"Subdomain": subdomain,
"RecordType": record_type
}
result = call_api("DescribeRecordList", params)
records = result.get("Response", {}).get("RecordList", [])
for record in records:
if record.get("Name") == subdomain and record.get("Type") == record_type:
return record
return None
except Exception as e:
print(f"查找记录失败: {e}")
return None
def create_record(domain, subdomain, record_type, value, line="默认", ttl=600, remark=""):
"""创建DNS记录"""
params = {
"Domain": domain,
"RecordType": record_type,
"RecordLine": line,
"Value": value,
"TTL": ttl
}
# 添加子域名
if subdomain and subdomain != "@":
params["SubDomain"] = subdomain
# 添加备注
if remark:
params["Remark"] = remark
try:
result = call_api("CreateRecord", params)
record_id = result.get("Response", {}).get("RecordId")
print(f"✓ 记录创建成功: {subdomain or '@'}.{domain} ({record_type}) → {value}")
return record_id
except Exception as e:
print(f"✗ 记录创建失败: {e}")
return None
def modify_record(domain, record_id, subdomain, record_type, value, line="默认", ttl=600, remark=""):
"""修改DNS记录"""
params = {
"Domain": domain,
"RecordId": record_id,
"RecordType": record_type,
"RecordLine": line,
"Value": value,
"TTL": ttl
}
if subdomain and subdomain != "@":
params["SubDomain"] = subdomain
if remark:
params["Remark"] = remark
try:
result = call_api("ModifyRecord", params)
print(f"✓ 记录更新成功: {subdomain or '@'}.{domain} ({record_type}) → {value}")
return True
except Exception as e:
print(f"✗ 记录更新失败: {e}")
return False
def deploy_record(domain, subdomain, record_type, value, line="默认", ttl=600, force=False, create_domain_flag=False, remark=""):
"""部署DNS记录(创建或更新)"""
print(f"\n部署DNS记录: {subdomain or '@'}.{domain}")
print(f" 类型: {record_type}")
print(f" 值: {value}")
print(f" 线路: {line}")
print(f" TTL: {ttl}s")
# 检查域名是否存在
if not check_domain_exists(domain):
if create_domain_flag:
if not create_domain(domain):
return False
else:
print(f"✗ 域名不存在: {domain}")
print(f" 提示: 使用 --create-domain 自动创建域名")
return False
# 查找现有记录
existing = find_record(domain, subdomain, record_type)
if existing:
if not force:
old_value = existing.get("Value", "")
print(f"\n记录已存在:")
print(f" 当前值: {old_value}")
print(f" 新值: {value}")
# 检查是否需要更新
if old_value == value:
print(" 值未变化,无需更新")
return True
response = input("\n是否更新? (y/N): ")
if response.lower() != 'y':
print(" 已取消")
return False
record_id = existing.get("RecordId")
return modify_record(domain, record_id, subdomain, record_type, value, line, ttl, remark)
else:
return create_record(domain, subdomain, record_type, value, line, ttl, remark)
def main():
import argparse
parser = argparse.ArgumentParser(description='DNSPod记录快速部署', formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 添加www的A记录
%(prog)s --domain example.com --subdomain www --type A --value 1.2.3.4
# 添加主域名记录
%(prog)s --domain example.com --subdomain @ --type A --value 1.2.3.4
# 添加CNAME记录
%(prog)s --domain example.com --subdomain cdn --type CNAME --value cdn.example.com
# 强制更新现有记录
%(prog)s --domain example.com --subdomain www --type A --value 1.2.3.5 --force
# 域名不存在时自动创建
%(prog)s --domain example.com --subdomain www --type A --value 1.2.3.4 --create-domain
''')
parser.add_argument('--domain', required=True, help='域名(如: example.com)')
parser.add_argument('--subdomain', default='@', help='子域名(默认: @ 表示主域名)')
parser.add_argument('--type', required=True, choices=['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV'], help='记录类型')
parser.add_argument('--value', required=True, help='记录值')
parser.add_argument('--line', default='默认', help='线路(默认: 默认)')
parser.add_argument('--ttl', type=int, default=600, help='TTL(秒, 默认: 600)')
parser.add_argument('--force', action='store_true', help='强制更新现有记录(不询问)')
parser.add_argument('--create-domain', action='store_true', help='域名不存在时自动创建')
parser.add_argument('--remark', default='', help='记录备注')
args = parser.parse_args()
# 部署记录
success = deploy_record(
domain=args.domain,
subdomain=args.subdomain,
record_type=args.type,
value=args.value,
line=args.line,
ttl=args.ttl,
force=args.force,
create_domain_flag=args.create_domain,
remark=args.remark
)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

163
scripts/deploy_service.py Executable file
View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
快速服务部署脚本
一键部署常见服务的DNS配置(Web服务、API服务、CDN等)
"""
import os
import sys
from deploy_record import deploy_record, load_env
# 加载 .env
load_env()
def deploy_web_service(domain, ip, include_wildcard=False, line="默认", ttl=600):
"""部署Web服务(主域名 + www + 可选泛域名)"""
print(f"\n{'='*50}")
print(f"部署Web服务: {domain}")
print(f"目标IP: {ip}")
print(f"{'='*50}\n")
success_count = 0
total_count = 2 + (1 if include_wildcard else 0)
# 1. 主域名 @
if deploy_record(domain, '@', 'A', ip, line, ttl, force=True, remark='主域名'):
success_count += 1
# 2. www子域名
if deploy_record(domain, 'www', 'A', ip, line, ttl, force=True, remark='Web服务'):
success_count += 1
# 3. 泛域名 *(可选)
if include_wildcard:
if deploy_record(domain, '*', 'A', ip, line, ttl, force=True, remark='泛域名'):
success_count += 1
print(f"\n{'='*50}")
print(f"Web服务部署完成: {success_count}/{total_count} 成功")
print(f"{'='*50}\n")
return success_count == total_count
def deploy_api_service(domain, ip, subdomain="api", line="默认", ttl=600):
"""部署API服务"""
print(f"\n{'='*50}")
print(f"部署API服务: {subdomain}.{domain}")
print(f"目标IP: {ip}")
print(f"{'='*50}\n")
success = deploy_record(domain, subdomain, 'A', ip, line, ttl, force=True, remark='API服务')
print(f"\n{'='*50}")
print(f"API服务部署{'成功' if success else '失败'}")
print(f"{'='*50}\n")
return success
def deploy_cdn_service(domain, cdn_cname, subdomain="cdn", line="默认", ttl=600):
"""部署CDN加速"""
print(f"\n{'='*50}")
print(f"部署CDN加速: {subdomain}.{domain}")
print(f"CNAME: {cdn_cname}")
print(f"{'='*50}\n")
success = deploy_record(domain, subdomain, 'CNAME', cdn_cname, line, ttl, force=True, remark='CDN加速')
print(f"\n{'='*50}")
print(f"CDN部署{'成功' if success else '失败'}")
print(f"{'='*50}\n")
return success
def deploy_mx_service(domain, mx_server, priority=10, line="默认"):
"""部署邮件服务(MX记录)"""
print(f"\n{'='*50}")
print(f"部署邮件服务: {domain}")
print(f"MX服务器: {mx_server}")
print(f"优先级: {priority}")
print(f"{'='*50}\n")
# 注意: MX记录需要特殊处理,这里简化处理
# 实际需要调用MX专用的创建接口
print(f"提示: MX记录部署需要手动配置")
print(f" 记录类型: MX")
print(f" 主机记录: @")
print(f" 记录值: {mx_server}")
print(f" 优先级: {priority}")
print(f" 线路: {line}\n")
return True
def main():
import argparse
parser = argparse.ArgumentParser(description='快速服务部署', formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
服务类型:
web - Web服务(@ + www)
api - API服务(api子域名)
cdn - CDN加速(CNAME记录)
mx - 邮件服务(MX记录)
示例:
# 部署Web服务
%(prog)s --domain example.com --service web --ip 1.2.3.4
# 部署Web服务(含泛域名)
%(prog)s --domain example.com --service web --ip 1.2.3.4 --wildcard
# 部署API服务
%(prog)s --domain example.com --service api --ip 1.2.3.5 --subdomain api
# 部署CDN
%(prog)s --domain example.com --service cdn --cname cdn.example.com.cdn.dnsv1.com
# 部署邮件服务
%(prog)s --domain example.com --service mx --mx-server mx.example.com --priority 10
''')
parser.add_argument('--domain', required=True, help='域名(如: example.com)')
parser.add_argument('--service', required=True, choices=['web', 'api', 'cdn', 'mx'], help='服务类型')
parser.add_argument('--ip', help='目标IP地址(A记录)')
parser.add_argument('--cname', help='CNAME目标域名')
parser.add_argument('--mx-server', help='MX服务器地址')
parser.add_argument('--priority', type=int, default=10, help='MX优先级(默认: 10)')
parser.add_argument('--subdomain', default='api', help='API子域名(默认: api)')
parser.add_argument('--cdn-subdomain', default='cdn', help='CDN子域名(默认: cdn)')
parser.add_argument('--line', default='默认', help='线路(默认: 默认)')
parser.add_argument('--ttl', type=int, default=600, help='TTL(秒, 默认: 600)')
parser.add_argument('--wildcard', action='store_true', help='Web服务是否添加泛域名解析(*.domain)')
args = parser.parse_args()
success = False
if args.service == 'web':
if not args.ip:
print("错误: Web服务需要指定 --ip 参数")
sys.exit(1)
success = deploy_web_service(args.domain, args.ip, args.wildcard, args.line, args.ttl)
elif args.service == 'api':
if not args.ip:
print("错误: API服务需要指定 --ip 参数")
sys.exit(1)
success = deploy_api_service(args.domain, args.ip, args.subdomain, args.line, args.ttl)
elif args.service == 'cdn':
if not args.cname:
print("错误: CDN服务需要指定 --cname 参数")
sys.exit(1)
success = deploy_cdn_service(args.domain, args.cname, args.cdn_subdomain, args.line, args.ttl)
elif args.service == 'mx':
if not args.mx_server:
print("错误: 邮件服务需要指定 --mx-server 参数")
sys.exit(1)
success = deploy_mx_service(args.domain, args.mx_server, args.priority, args.line)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

77
scripts/list_records.py Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
DNS记录列表查询脚本
"""
import os
import sys
import json
from deploy_record import call_api, load_env
# 加载 .env
load_env()
def list_records(domain, record_type=None, subdomain=None):
"""查询DNS记录列表"""
params = {"Domain": domain}
if record_type:
params["RecordType"] = record_type
if subdomain:
params["Subdomain"] = subdomain
try:
result = call_api("DescribeRecordList", params)
records = result.get("Response", {}).get("RecordList", [])
if not records:
print(f"\n没有找到记录")
return []
print(f"\n{'='*80}")
print(f"DNS记录列表: {domain}")
print(f"{'='*80}\n")
# 表头
print(f"{'主机记录':<20} {'类型':<10} {'记录值':<30} {'线路':<10} {'TTL':<8} {'状态':<6}")
print(f"{'-'*80}")
# 记录列表
for record in records:
name = record.get("Name", "")
record_type = record.get("Type", "")
value = record.get("Value", "")
line = record.get("Line", "")
ttl = record.get("TTL", 0)
status = "启用" if record.get("Enabled", 1) == 1 else "禁用"
# 截断过长的值
if len(value) > 28:
value = value[:28] + ".."
print(f"{name:<20} {record_type:<10} {value:<30} {line:<10} {ttl:<8} {status:<6}")
print(f"{'-'*80}")
print(f"总计: {len(records)} 条记录\n")
return records
except Exception as e:
print(f"查询失败: {e}")
return []
def main():
import argparse
parser = argparse.ArgumentParser(description='查询DNS记录列表')
parser.add_argument('--domain', required=True, help='域名(如: example.com)')
parser.add_argument('--type', help='筛选记录类型')
parser.add_argument('--subdomain', help='筛选子域名')
args = parser.parse_args()
list_records(args.domain, args.type, args.subdomain)
if __name__ == '__main__':
main()