Skip to content

Instantly share code, notes, and snippets.

@jult
Last active May 10, 2026 16:15
Show Gist options
  • Select an option

  • Save jult/0b8832e4cfe0c11874439e4efb526030 to your computer and use it in GitHub Desktop.

Select an option

Save jult/0b8832e4cfe0c11874439e4efb526030 to your computer and use it in GitHub Desktop.
Reserved Leases for Technitium DNS/DHCP server importer/converter from dnsmasq
#!/bin/bash
#
# Reserved Leases for Technitium DNS/DHCP server importer/converter from /etc/dnsmasq.conf
#
# Converts dnsmasq dhcp-host entries (static/fixed IPv4-addresses with MAC-address/hostname) to Technitium
# via the Technitium HTTP API
# Handles format: dhcp-host=MAC,IP,hostname
#
# Do a dry run to verify that all entries parse correctly:
# # chmod +x dnsmasq-to-technitium.sh
# ./dnsmasq-to-technitium.sh -p youradminwebUIpassword -s DHCPscopeName -n
# Then do the actual import using
# ./dnsmasq-to-technitium.sh -p youradminwebUIpassword -s DHCPscopeName
#
# For DHCPscopeName USE THE EXACT Name visible in Technitium web-UI under DHCP > Scopes > Name
# make sure the scope includes the IPv4 addresses that are in the dnsmasq list!
set -eo pipefail
log() {
echo "[$(date --rfc-3339=seconds)] $*"
}
die() {
echo "ERROR: $*" >&2
exit 1
}
usage() {
cat << EOF
Usage: $0 [options]
Options:
-u URL Technitium base URL (default: http://127.0.0.1:5380)
-U user Technitium username (default: admin)
-p pass Technitium password (required)
-s scope DHCP scope name in Technitium (required)
-f file dnsmasq config file (default: /etc/dnsmasq.conf)
-n Dry run - parse and show entries without importing
-h Show this help
Example:
$0 -p secretpass -s stro.men
$0 -p secretpass -s stro.men -n
EOF
exit 0
}
# ── Defaults ──────────────────────────────────────────────────────────────────
TECHNITIUM_URL="http://127.0.0.1:5380"
TECHNITIUM_USER="admin"
TECHNITIUM_PASS=""
SCOPE_NAME=""
DNSMASQ_CONF="/etc/dnsmasq.conf"
DRY_RUN=false
# ── Parse arguments ───────────────────────────────────────────────────────────
while getopts "u:U:p:s:f:nh" opt; do
case $opt in
u) TECHNITIUM_URL="$OPTARG" ;;
U) TECHNITIUM_USER="$OPTARG" ;;
p) TECHNITIUM_PASS="$OPTARG" ;;
s) SCOPE_NAME="$OPTARG" ;;
f) DNSMASQ_CONF="$OPTARG" ;;
n) DRY_RUN=true ;;
h) usage ;;
*) usage ;;
esac
done
# ── Validate ──────────────────────────────────────────────────────────────────
[[ -f "$DNSMASQ_CONF" ]] || die "dnsmasq config not found: $DNSMASQ_CONF"
[[ -n "$TECHNITIUM_PASS" ]] || die "Technitium password required (-p)"
[[ -n "$SCOPE_NAME" ]] || die "Technitium scope name required (-s)"
# URL-encode the scope name (pure bash, no python dependency)
urlencode() {
local string="$1"
local encoded=""
local i c
for (( i=0; i<${#string}; i++ )); do
c="${string:$i:1}"
case "$c" in
[a-zA-Z0-9._~-]) encoded+="$c" ;;
*) printf -v encoded '%s%%%02X' "$encoded" "'$c" ;;
esac
done
echo "$encoded"
}
SCOPE_ENCODED=$(urlencode "$SCOPE_NAME")
# ── Step 1: Authenticate ──────────────────────────────────────────────────────
log "Authenticating with Technitium at $TECHNITIUM_URL..."
RESPONSE=$(curl -sf "$TECHNITIUM_URL/api/user/login?user=$TECHNITIUM_USER&pass=$TECHNITIUM_PASS") \
|| die "Failed to connect to Technitium at $TECHNITIUM_URL"
TOKEN=$(echo "$RESPONSE" | grep -o '"token":"[^"]*"' | sed 's/"token":"//;s/"//') \
|| die "Could not extract token from response"
[[ -n "$TOKEN" ]] || die "Authentication failed - check credentials"
log "Authentication successful"
# ── Step 2: Verify scope exists ───────────────────────────────────────────────
log "Verifying DHCP scope '$SCOPE_NAME' exists..."
SCOPE_RESPONSE=$(curl -sf "$TECHNITIUM_URL/api/dhcp/scopes/get?token=$TOKEN&name=$SCOPE_ENCODED") || true
SCOPE_STATUS=$(echo "$SCOPE_RESPONSE" | grep -o '"status":"[^"]*"' | sed 's/"status":"//;s/"//') || true
[[ "$SCOPE_STATUS" == "ok" ]] || die "DHCP scope '$SCOPE_NAME' not found. Create it first in the Technitium web UI."
log "Scope '$SCOPE_NAME' confirmed"
# ── Step 3: Parse and import ──────────────────────────────────────────────────
log "Parsing $DNSMASQ_CONF..."
echo ""
IMPORTED=0
SKIPPED=0
ERRORS=0
while IFS= read -r line || [[ -n "$line" ]]; do
# Only process dhcp-host lines, skip comments and blanks
[[ "$line" =~ ^dhcp-host= ]] || continue
# Strip prefix and any trailing comment
value="${line#dhcp-host=}"
value="${value%%#*}"
value="${value%% }"
# Split into exactly 3 fields by comma
MAC=$(echo "$value" | cut -d',' -f1 | tr -d ' ')
IP=$(echo "$value" | cut -d',' -f2 | tr -d ' ')
HOSTNAME=$(echo "$value" | cut -d',' -f3 | tr -d ' \r\n')
# Trim whitespace from each field
MAC="${MAC// /}"
IP="${IP// /}"
HOSTNAME="${HOSTNAME// /}"
# Validate MAC and IP
if ! [[ "$MAC" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; then
log "SKIP: Invalid MAC in: $line"
SKIPPED=$((SKIPPED + 1))
continue
fi
if ! [[ "$IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log "SKIP: Invalid IP in: $line"
SKIPPED=$((SKIPPED + 1))
continue
fi
if $DRY_RUN; then
printf "DRY RUN: %-20s %-16s %s\n" "$MAC" "$IP" "${HOSTNAME:-<no hostname>}"
IMPORTED=$((IMPORTED + 1))
continue
fi
# Build API call
API_PARAMS="token=$TOKEN&name=$SCOPE_ENCODED&hardwareAddress=$MAC&ipAddress=$IP"
[[ -n "$HOSTNAME" ]] && API_PARAMS="$API_PARAMS&hostName=$HOSTNAME"
RESULT=$(curl -sf "$TECHNITIUM_URL/api/dhcp/scopes/addReservedLease?$API_PARAMS") || true
STATUS=$(echo "$RESULT" | grep -o '"status":"[^"]*"' | sed 's/"status":"//;s/"//') || true
if [[ "$STATUS" == "ok" ]]; then
printf "OK: %-20s %-16s %s\n" "$MAC" "$IP" "${HOSTNAME:-<no hostname>}"
IMPORTED=$((IMPORTED + 1))
else
ERROR_MSG=$(echo "$RESULT" | grep -o '"errorMessage":"[^"]*"' | sed 's/"errorMessage":"//;s/"//') || true
printf "FAIL: %-20s %-16s %s [%s]\n" "$MAC" "$IP" "${HOSTNAME:-<no hostname>}" "${ERROR_MSG:-unknown error}"
ERRORS=$((ERRORS + 1))
fi
done < "$DNSMASQ_CONF"
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
log "─────────────────────────────────────────────────────"
if $DRY_RUN; then
log "DRY RUN complete - no changes made to Technitium"
log "Would import: $IMPORTED entries"
log "Would skip: $SKIPPED invalid entries"
else
log "Import complete"
log "Imported: $IMPORTED entries"
log "Skipped: $SKIPPED invalid entries"
log "Errors: $ERRORS entries"
fi
log "─────────────────────────────────────────────────────"
# ── Logout ────────────────────────────────────────────────────────────────────
curl -sf "$TECHNITIUM_URL/api/user/logout?token=$TOKEN" > /dev/null || true
log "Logged out"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment