Last active
May 10, 2026 16:15
-
-
Save jult/0b8832e4cfe0c11874439e4efb526030 to your computer and use it in GitHub Desktop.
Reserved Leases for Technitium DNS/DHCP server importer/converter from dnsmasq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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