cf-cli-tools/cf-dns
feibisi 1be3d8ef8d 初始提交:Cloudflare CLI 工具集
debian 开发,modiqi 修复 cf_load_profile 缺失问题。
9 个命令:cf-api/cf-zone/cf-dns/cf-cache/cf-ssl/cf-fw/cf-pagerule/cf-analytics/cf-hostname
支持多账号、域名自动解析 zone_id。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 02:39:31 +08:00

156 lines
6.2 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
# cf-dns — Cloudflare DNS 记录管理
# 用法: cf-dns list <domain> [-t type] [-n name] [--export]
# cf-dns add <domain> <type> <name> <content> [-l ttl] [--proxied]
# cf-dns update <domain> <record_id> <type> <name> <content> [-l ttl] [--proxied]
# cf-dns delete <domain> <record_id>
# cf-dns find <domain> <name> 按名称查找记录
# cf-dns import <domain> <file> 导入 BIND 格式 zone 文件
# cf-dns export <domain> 导出为 BIND 格式
# 选项: -p <profile> 指定 profile
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/cf-lib.sh"
cf_parse_profile "$@"
set -- "${CF_REMAINING_ARGS[@]}"
cf_load_profile
usage() {
cat >&2 <<'EOF'
用法: cf-dns <command> <domain> [args...]
命令:
list <domain> [-t type] [-n name] 列出 DNS 记录
add <domain> <type> <name> <content> 添加记录
[-l ttl] [--proxied] 可选: TTL(默认auto), 代理(默认关)
update <domain> <id> <type> <name> <content> 更新记录
[-l ttl] [--proxied|--no-proxied]
delete <domain> <record_id> 删除记录
find <domain> <name> 按名称查找
export <domain> 导出 BIND 格式
import <domain> <file> 导入 BIND zone 文件
EOF
exit 1
}
[[ $# -lt 1 ]] && usage
CMD="$1"; shift
case "$CMD" in
list)
[[ $# -lt 1 ]] && { echo "用法: cf-dns list <domain> [-t type] [-n name]" >&2; exit 1; }
DOMAIN="$1"; shift
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
TYPE="" NAME="" PAGE=1
while [[ $# -gt 0 ]]; do
case "$1" in
-t) TYPE="$2"; shift 2 ;;
-n) NAME="$2"; shift 2 ;;
*) shift ;;
esac
done
QUERY="/zones/$ZONE_ID/dns_records?per_page=100&page=$PAGE"
[[ -n "$TYPE" ]] && QUERY+="&type=$TYPE"
[[ -n "$NAME" ]] && QUERY+="&name=$NAME"
resp=$(cf_call GET "$QUERY")
cf_check_error "$resp"
echo "$resp" | jq -r '
["类型", "名称", "内容", "代理", "TTL", "ID"],
(.result[] | [.type, .name, (.content[:40]), (if .proxied then "✓" else "✗" end), (.ttl|tostring), .id]) |
@tsv' | column -t -s $'\t'
total=$(echo "$resp" | jq '.result_info.total_count')
echo -e "\n共 $total 条记录"
;;
add)
[[ $# -lt 4 ]] && { echo "用法: cf-dns add <domain> <type> <name> <content> [-l ttl] [--proxied]" >&2; exit 1; }
DOMAIN="$1" RTYPE="$2" RNAME="$3" RCONTENT="$4"; shift 4
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
TTL=1 PROXIED=false
while [[ $# -gt 0 ]]; do
case "$1" in
-l) TTL="$2"; shift 2 ;;
--proxied) PROXIED=true; shift ;;
*) shift ;;
esac
done
DATA=$(jq -n --arg t "$RTYPE" --arg n "$RNAME" --arg c "$RCONTENT" \
--argjson ttl "$TTL" --argjson p "$PROXIED" \
'{type:$t, name:$n, content:$c, ttl:$ttl, proxied:$p}')
resp=$(cf_call POST "/zones/$ZONE_ID/dns_records" -d "$DATA")
cf_check_error "$resp"
echo "$resp" | jq '.result | "已添加: \(.type) \(.name) -> \(.content) (ID: \(.id))"' -r
;;
update)
[[ $# -lt 5 ]] && { echo "用法: cf-dns update <domain> <id> <type> <name> <content>" >&2; exit 1; }
DOMAIN="$1" RID="$2" RTYPE="$3" RNAME="$4" RCONTENT="$5"; shift 5
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
TTL=1 PROXIED=""
while [[ $# -gt 0 ]]; do
case "$1" in
-l) TTL="$2"; shift 2 ;;
--proxied) PROXIED=true; shift ;;
--no-proxied) PROXIED=false; shift ;;
*) shift ;;
esac
done
DATA=$(jq -n --arg t "$RTYPE" --arg n "$RNAME" --arg c "$RCONTENT" --argjson ttl "$TTL" \
'{type:$t, name:$n, content:$c, ttl:$ttl}')
[[ -n "$PROXIED" ]] && DATA=$(echo "$DATA" | jq --argjson p "$PROXIED" '. + {proxied:$p}')
resp=$(cf_call PUT "/zones/$ZONE_ID/dns_records/$RID" -d "$DATA")
cf_check_error "$resp"
echo "$resp" | jq '.result | "已更新: \(.type) \(.name) -> \(.content)"' -r
;;
delete)
[[ $# -lt 2 ]] && { echo "用法: cf-dns delete <domain> <record_id>" >&2; exit 1; }
DOMAIN="$1" RID="$2"
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
resp=$(cf_call DELETE "/zones/$ZONE_ID/dns_records/$RID")
cf_check_error "$resp"
echo "已删除记录: $RID"
;;
find)
[[ $# -lt 2 ]] && { echo "用法: cf-dns find <domain> <name>" >&2; exit 1; }
DOMAIN="$1" SEARCH="$2"
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
# 如果搜索词不含域名后缀,自动补上
[[ "$SEARCH" != *"$DOMAIN"* ]] && SEARCH="${SEARCH}.${DOMAIN}"
resp=$(cf_call GET "/zones/$ZONE_ID/dns_records?name=$SEARCH")
cf_check_error "$resp"
count=$(echo "$resp" | jq '.result | length')
if [[ "$count" == "0" ]]; then
echo "未找到匹配 $SEARCH 的记录"
else
echo "$resp" | jq -r '
["类型", "名称", "内容", "代理", "TTL", "ID"],
(.result[] | [.type, .name, .content, (if .proxied then "✓" else "✗" end), (.ttl|tostring), .id]) |
@tsv' | column -t -s $'\t'
fi
;;
export)
[[ $# -lt 1 ]] && { echo "用法: cf-dns export <domain>" >&2; exit 1; }
DOMAIN="$1"
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
cf_call GET "/zones/$ZONE_ID/dns_records/export"
;;
import)
[[ $# -lt 2 ]] && { echo "用法: cf-dns import <domain> <file>" >&2; exit 1; }
DOMAIN="$1" FILE="$2"
[[ ! -f "$FILE" ]] && { echo "错误: 文件不存在 $FILE" >&2; exit 1; }
ZONE_ID=$(cf_resolve_zone "$DOMAIN")
resp=$(cf_call POST "/zones/$ZONE_ID/dns_records/import" \
-H "Content-Type: multipart/form-data" \
-F "file=@$FILE")
cf_check_error "$resp"
echo "$resp" | jq '.result | "导入完成: 添加 \(.recs_added) 条, 总计 \(.total_records_parsed) 条"' -r
;;
*) usage ;;
esac