feat(tooling): containerize dns-cli in tooling-cli/dns
Consolidate the Technitium DNS CLI into tooling-cli/dns/ with a
containerized bash CLI (src/dns_cli.sh), Dockerfile (alpine +
curl/python3/bind-tools/bash), README, AGENTS.md, validate.sh, and
.env.example. Env vars aligned to the centralized ~/.creds/technitium.env
store (TECHNITIUM_URL, TECHNITIUM_DNS_TOKEN, TECHNITIUM_DNS_ZONE).
Image built and pushed to the registry as
git.knownelement.com/reachableceo/dns-cli:latest. Verified live:
zones, list, and search all return data.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# dns-cli - CLI wrapper for Technitium DNS Server API
|
||||
#
|
||||
# Usage:
|
||||
# dns-cli zones List all zones
|
||||
# dns-cli list [zone] List records in a zone (default: $TECHNITIUM_DNS_ZONE)
|
||||
# dns-cli get <name> Resolve a record (A/AAAA)
|
||||
# dns-cli add <name> <ip> [ttl] Add A record (+ auto PTR)
|
||||
# dns-cli delete <name> <ip> Delete A record
|
||||
# dns-cli flush Flush DNS cache
|
||||
# dns-cli search <pattern> Search records by name pattern
|
||||
#
|
||||
# Env (provided via --env-file ~/.creds/technitium.env):
|
||||
# TECHNITIUM_URL Technitium base URL (required)
|
||||
# TECHNITIUM_DNS_TOKEN API token (required)
|
||||
# TECHNITIUM_DNS_ZONE Default zone (required)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
URL="${TECHNITIUM_URL:?TECHNITIUM_URL is required}"
|
||||
TOKEN="${TECHNITIUM_DNS_TOKEN:?TECHNITIUM_DNS_TOKEN is required}"
|
||||
ZONE="${TECHNITIUM_DNS_ZONE:?TECHNITIUM_DNS_ZONE is required}"
|
||||
|
||||
# API helper — adds Bearer auth, returns JSON
|
||||
api() {
|
||||
local method="$1" endpoint="$2"; shift 2
|
||||
local args=()
|
||||
for kv in "$@"; do
|
||||
args+=(-d "$kv")
|
||||
done
|
||||
curl -s -H "Authorization: Bearer $TOKEN" -X "$method" "${args[@]}" \
|
||||
"$URL/api/$endpoint" 2>/dev/null
|
||||
}
|
||||
|
||||
# Subcommands
|
||||
cmd_zones() {
|
||||
api GET "zones/list" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
for z in d.get('response',{}).get('zones',[]):
|
||||
print(z['name'])
|
||||
"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local zone="${1:-$ZONE}"
|
||||
api GET "zones/records/get?domain=$zone&zone=$zone&listZone=true" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
recs = d.get('response',{}).get('records',[])
|
||||
for r in sorted(recs, key=lambda x: (x['name'], x['type'])):
|
||||
rd = r.get('rData',{})
|
||||
ip = rd.get('ipAddress','')
|
||||
val = ip or rd.get('nameServer','') or rd.get('primaryNameServer','') or str(rd)
|
||||
print(f\"{r['name']:50s} {r['type']:6s} {r.get('ttl',''):6} {val}\")
|
||||
"
|
||||
}
|
||||
|
||||
cmd_get() {
|
||||
local name="$1"
|
||||
dig +short "$name" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cmd_add() {
|
||||
local name="$1" ip="$2" ttl="${3:-3600}"
|
||||
local fqdn="$name"
|
||||
[[ "$fqdn" != *.* ]] && fqdn="$name.$ZONE"
|
||||
api POST "zones/records/add" \
|
||||
"zone=$ZONE" "domain=$fqdn" "type=A" "ttl=$ttl" "ipAddress=$ip" "ptr=true" "overwrite=true" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(d.get('status','?'), d.get('errorMessage',''))
|
||||
"
|
||||
}
|
||||
|
||||
cmd_delete() {
|
||||
local name="$1" ip="$2"
|
||||
local fqdn="$name"
|
||||
[[ "$fqdn" != *.* ]] && fqdn="$name.$ZONE"
|
||||
api POST "zones/records/delete" \
|
||||
"zone=$ZONE" "domain=$fqdn" "type=A" "ipAddress=$ip" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(d.get('status','?'), d.get('errorMessage',''))
|
||||
"
|
||||
}
|
||||
|
||||
cmd_flush() {
|
||||
api POST "cache/flush" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(d.get('status','?'))
|
||||
"
|
||||
}
|
||||
|
||||
cmd_search() {
|
||||
local pattern="$1"
|
||||
api GET "zones/records/get?domain=$ZONE&zone=$ZONE&listZone=true" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
recs = d.get('response',{}).get('records',[])
|
||||
for r in sorted(recs, key=lambda x: x['name']):
|
||||
if '$pattern' in r['name'].lower():
|
||||
rd = r.get('rData',{})
|
||||
ip = rd.get('ipAddress','')
|
||||
val = ip or rd.get('nameServer','') or str(rd)
|
||||
print(f\"{r['name']:50s} {r['type']:6s} {val}\")
|
||||
"
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-}" in
|
||||
zones) cmd_zones ;;
|
||||
list) shift; cmd_list "${1:-}" ;;
|
||||
get) shift; cmd_get "$1" ;;
|
||||
add) shift; cmd_add "$@" ;;
|
||||
delete) shift; cmd_delete "$@" ;;
|
||||
flush) cmd_flush ;;
|
||||
search) shift; cmd_search "$1" ;;
|
||||
""|-h|--help|help)
|
||||
sed -n '2,18p' "$0" >&2
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown command: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user