Hello guys,
I didn't want to open a new post because what I want to add feels like a continuation to this very same topic.
The problem that op solved with his solution, can be tackled also in a different way, with a script in OPNsense, using Omada Open API.
I created this script (disclosing the help of AI here), to read dnsmasq leases, create a local cache and update Omada Client List through its API. The API calls are executed only if there is a change in the cache, so traffic is minimized. There is also a small logging feature to track last syncs.
Omada requirements:
I didn't want to open a new post because what I want to add feels like a continuation to this very same topic.
The problem that op solved with his solution, can be tackled also in a different way, with a script in OPNsense, using Omada Open API.
I created this script (disclosing the help of AI here), to read dnsmasq leases, create a local cache and update Omada Client List through its API. The API calls are executed only if there is a change in the cache, so traffic is minimized. There is also a small logging feature to track last syncs.
Omada requirements:
- In Global View - Settings - Platform Integration create a New App with Client mode and Admin role. You will be rewarded with a Client ID and a Client Secret.
- Get the Controller ID in the browser, opening the link https://omada_ip:8043/api/info. The field we are looking for is omadacId.
- To find the Site ID, open Omada, open the Developer Tools of your browser, go to the Network section and open a page of your Omada Site. Search (CTRL+F) for the prefix /sites/ among the requests. You will see a series of request with the format <controller_id>/api/v2/sites/<site_id> (xxxxxxxxxx/api/v2/sites/xxxxxxxxxx). Get the Site ID from there.
Code Select
#!/bin/sh
# dnsmasq_omada_sync.sh
# Sync dnsmasq hostnames -> Omada Controller.
# Skips Omada login if cache shows no changes.
# To create a cron job for this file in OPNsense:
# 1. Copy this file in /root/scripts
# 2. Create the file /usr/local/opnsense/service/conf/actions.d/actions_omada.conf
# 3. Append the following lines to the file:
# [start]
# command:/root/scripts/dnsmasq_omada_sync.sh
# parameters:
# type: script
# message: Synchronize dnsmasq hostnames with Omada
# description: Synchronize dnsmasq hostnames with Omada
# 4. Restart configd: service configd restart
# 5. Create the cron job in "System - Settings - Cron"
# --------------------------------
# cyruz - https://ciroprincipe.net
set -e
### ==============================
### CONFIGURATION
### ==============================
OMADA_URL="https://omada_ip:8043"
OMADAC_ID="" # Omada Controller ID
SITE_ID="" # Omada Site ID
CLIENT_ID="" # Open API Client ID
CLIENT_SECRET="" # Open API Client Secret
LEASE_FILE="/var/db/dnsmasq.leases"
LEASE_CACHE="/var/db/dnsmasq_omada.cache"
LOG_FILE="/var/log/dnsmasq_omada_sync.log"
MAX_LOG_SIZE=$((1024 * 1024))
#######################################
# REQUIREMENTS CHECK
#######################################
require_cmd() {
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "Error: required command missing: $cmd" >&2
exit 1
}
done
}
require_cmd curl jq awk date sort mktemp tr sed wc stat mkdir
#######################################
# LOG ROTATION
#######################################
rotate_logs() {
log_dir=$(dirname "$LOG_FILE")
[ -d "$log_dir" ] || mkdir -p "$log_dir"
if [ -f "$LOG_FILE" ]; then
size=$(stat -f %z "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$size" -ge "$MAX_LOG_SIZE" ]; then
[ -f "${LOG_FILE}.1" ] && rm -f "${LOG_FILE}.1"
mv "$LOG_FILE" "${LOG_FILE}.1"
fi
fi
}
rotate_logs
exec >>"$LOG_FILE" 2>&1
if [ ! -r "$LEASE_FILE" ]; then
echo "$(date -Iseconds) Error: dnsmasq lease file not readable: $LEASE_FILE"
exit 1
fi
echo "===== $(date -Iseconds) - dnsmasq_omada_sync start ====="
#######################################
# STEP 1: Parse dnsmasq leases
#######################################
echo "[*] Reading dnsmasq leases..."
NOW_EPOCH=$(date +%s)
TMP_LEASES=$(mktemp -t dnsmasq.XXXXXX)
TMP_NEW_HOST=$(mktemp -t newhost.XXXXXX)
cleanup() {
rm -f "$TMP_LEASES" "$TMP_NEW_HOST"
}
trap cleanup EXIT
# Keep only valid (non-expired, hostname present) leases, last entry per MAC.
awk -v now="$NOW_EPOCH" '
now <= $1 && $4 != "" && $4 != "*" {
line[tolower($2)] = $0
}
END {
for (m in line) print line[m]
}
' "$LEASE_FILE" > "$TMP_LEASES"
# Build NEW_HOST list: "mac hostname".
# dnsmasq format: expiry mac ip hostname clientid.
while read -r expiry mac ip host cid; do
[ -z "$mac" ] && continue
mac_lc=$(printf "%s\n" "$mac" | tr "A-Z" "a-z")
printf "%s %s\n" "$mac_lc" "$host"
done < "$TMP_LEASES" > "$TMP_NEW_HOST"
#######################################
# STEP 2: Compare with cache BEFORE API
#######################################
echo "[*] Loading previous cache (if any) and comparing..."
CHANGED=0
if [ -f "$LEASE_CACHE" ]; then
while read -r mac new_host; do
[ -z "$mac" ] && continue
old_host=$(awk -v m="$mac" 'tolower($1)==m {print $2; exit}' "$LEASE_CACHE" 2>/dev/null || true)
if [ -z "$old_host" ] || [ "$old_host" != "$new_host" ]; then
CHANGED=$((CHANGED + 1))
fi
done < "$TMP_NEW_HOST"
else
# No cache yet: everything is considered "changed".
CHANGED=$(wc -l < "$TMP_NEW_HOST" | awk '{print $1}')
fi
if [ "$CHANGED" -eq 0 ]; then
echo "[*] No hostname updates detected — exiting without Omada API calls."
echo "===== $(date -Iseconds) - dnsmasq_omada_sync end (no changes) ====="
exit 0
fi
echo "[*] Detected $CHANGED hostname changes — requesting access token..."
#######################################
# STEP 3: Get access token (client_credentials)
#######################################
TOKEN=$(
curl -sk -X POST \
"$OMADA_URL/openapi/authorize/token?grant_type=client_credentials" \
-H "content-type:application/json" \
-d '{
"omadacId": "'"$OMADAC_ID"'",
"client_id": "'"$CLIENT_ID"'",
"client_secret": "'"$CLIENT_SECRET"'"
}' \
| jq -r '.result.accessToken'
)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "Error: failed to obtain access token from Omada OpenAPI"
echo "===== $(date -Iseconds) - dnsmasq_omada_sync end (token error) ====="
exit 1
fi
#######################################
# STEP 4: Apply hostname updates
#######################################
echo "[*] Applying hostname updates via OpenAPI..."
UPDATED=0
MISSING=0
FAILED=0
while read -r mac new_host; do
[ -z "$mac" ] && continue
# Load old host from cache (if any).
if [ -f "$LEASE_CACHE" ]; then
old_host=$(awk -v m="$mac" 'tolower($1)==m {print $2; exit}' "$LEASE_CACHE" 2>/dev/null || true)
else
old_host=""
fi
# Only process entries that changed vs cache.
if [ -n "$old_host" ] && [ "$old_host" = "$new_host" ]; then
continue
fi
# Build clientMac as required by OpenAPI path: upper-case with dashes.
mac_id=$(printf "%s\n" "$mac" | tr 'a-f' 'A-F' | tr ':' '-')
# Sanitize hostname for JSON (escape double quotes).
safe_host=$(printf "%s" "$new_host" | sed 's/"/\\"/g')
echo "[*] Setting name for $mac_id -> $safe_host"
RESP=$(curl -sk -X PATCH \
"$OMADA_URL/openapi/v1/$OMADAC_ID/sites/$SITE_ID/clients/$mac_id/name" \
-H "content-type:application/json" \
-H "Authorization:AccessToken=$TOKEN" \
-d "{\"name\":\"$safe_host\"}" )
ERR=$(printf "%s" "$RESP" | jq -r '.errorCode' 2>/dev/null || echo "unknown")
if [ "$ERR" = "0" ]; then
UPDATED=$((UPDATED + 1))
elif [ "$ERR" = "-41011" ]; then
echo "[!] Client $mac_id does not exist in this site (errorCode -41011)."
MISSING=$((MISSING + 1))
else
echo "[!] Failed to update $mac_id (errorCode $ERR)."
FAILED=$((FAILED + 1))
fi
done < "$TMP_NEW_HOST"
echo "[*] Updated clients: $UPDATED"
echo "[*] Missing in Omada: $MISSING"
echo "[*] Failed updates: $FAILED"
#######################################
# STEP 5: Rewrite cache
#######################################
echo "[*] Writing updated cache..."
sort "$TMP_NEW_HOST" > "$LEASE_CACHE.tmp"
mv "$LEASE_CACHE.tmp" "$LEASE_CACHE"
echo "===== $(date -Iseconds) - dnsmasq_omada_sync end ====="
"