OPNsense Forum

English Forums => Development and Code Review => Topic started by: ffsb42 on June 13, 2024, 10:29:07 PM

Title: How to update client names in tp-link OMADA SDN controler
Post by: ffsb42 on June 13, 2024, 10:29:07 PM
- not sure if this is the right section of the forum to post it (I guess it really is not opnsense specific)
- not sure if this could be added to opnsense directly or as a plugin
- you might want to audit the script and the python library prior to use in a PRODUCTION setting.
- I am using it successfully on omada SDN controlling tplink siwtches and AcccessPoints from a ubuntu host runnning the OMADA SDN as a container and usign opnsense unbound as primary local DNS.
but anyway, it it helps others...

here is a very simple script which can be run once or in cron to automatically rename OMADA clients based on a reverse DNS lookup.



#!/usr/bin/bash
version=0
changelog="version=$version, 13-Jun-2024, initial build"

version=2
changelog="version=$version, 14-Jun-2024, swapped loop from by-subnet to by-omada-clients-mac, saving before and after files in /var/tmp"


HELP="
dns2omada.sh is the most simplistic script ever meant to paliate one of dumbest laziest lack of feature of the tp-link OMADA SDN software....
it simply populate the OMADA client's name based on DNS name...  everytime the script runs it will keep a copy of the omada clients output
before and after the script execution in /var/tmp/omadaclients.*.dns2omada

using a python omada api...
Assumptions:
- you already have a local DNS server able to perform reverse DNS resolution per IP.
- you already have omada SDN setup on the network
- you should create an OMADA admin account dedicated to this process using a very long random password string without any special character
- my subnet is simply from 192.168.0.1 to 192.168.0.256 so I have a simple loop, you might have to modify the script if your subnet is class B
with an outer loop etc...
usage:
    1) install python: sudo apt install python3-pip
    2) install the omada python api: pip install tplink-omada-client
    3) update the parameters CHANGE_ME values in the scipt below
    4) run the script manually or in cron...     
"

# these should be self-explanatory
omada_username="CHANGEME_mrpotatoe"
omada_password="CHANGEME_lfdaruiRWGFD335qw324z"
omada_site="CHANGEME_homesweethome"
omada_url="https://CHANGEME_omada.mylocaldomain"
dns_server=CHANGEME_192.168.0.1
omadabin=/CHANGEME/bin/omada   # the absolute path to the omada binarie whch gets installed with the client.




if [[ $# -gt 0 ]]
then
    echo $HELP
    exit 3
fi

if fgrep -v grep $0 | fgrep CHANGEME  >/dev/null
then
    echo "ERROR: you forgot to change these varialbes from $0:"
    fgrep -v grep $0 | fgrep CHANGEME
    echo "$HELP"
    exit 4
fi
$omadabin -t myomada target --url $omada_url --user $omada_username --password $omada_password --site $omada_site --set-default

omadaclients=/tmp/omadaclients.$$.txt

$omadabin clients  > $omadaclients

cat $omadaclients | while read mac ip name device port junk
do
    clientname=""
    clientmac=$mac
    #
    # debugging
    #echo "parsing mac=$mac ip=$ip name=$name device=$device port=$port junk=$junk"

    if [[ $ip = "" ]] || [[ $ip = "-" ]]
    then
        echo "skiping $mac because omada does not have an IP for it"
    else   
        clientname=$(nslookup $ip $dns_server 2>/dev/null | fgrep "=" |cut -f2 -d"=" |cut -f1 -d"."|tail -1)
        if [[ $clientname = ""  ]]
        then
            echo "skipping $ip because I cant resolve it"
        else
            echo "setting hostname for $ip to $clientname for mac $clientmac "       
            $omadabin set-client-name $clientmac $clientname           
        fi
    fi
    unset  mac ip name device port junk   
done
cat $omadaclients > /var/tmp/omadaclients.before.dns2omada
$omadabin clients  > /var/tmp/omadaclients.after.dns2omada



Title: Re: How to update client names in tp-link OMADA SDN controler
Post by: nitrosont on July 02, 2024, 06:28:24 PM
Hi,

some questions:
Title: Re: How to update client names in tp-link OMADA SDN controler
Post by: ffsb42 on July 02, 2024, 10:38:06 PM

1) Which host to run it from?
It can run from anywhere as long as you have connectivity to the omada url  AND can do a reverse dns lookup.

2) omada binary location?
That depends on your own python installation...  when you run the pip install it should tell you the destination. I think by default if you are not root it is something like  ~/.local/bin

3) Is it sufficient to provice the MACs with names in OPNsense in the DHCP-server ("Services: ISC DHCPv4: [LAN]")?
AFAIK, to get the reverse dns and local name resolution you need:
3.1) to use ISC DHCP with the option to (Kea DHCP self registration is not working yet)
3.2) to run unbound DNS on opensense and check these 2 options:
Register ISC DHCP4 Leases
Register DHCP Static Mappings
that basically allow you to retrieve the name of dhcp hosts by IP addresses over DNS.

Title: Re: How to update client names in tp-link OMADA SDN controler
Post by: nitrosont on July 03, 2024, 12:06:00 AM
All right, thank you for the reply. I'll give that a try!
Title: Re: How to update client names in tp-link OMADA SDN controler
Post by: cyruz on December 02, 2025, 05:59:31 PM
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:



#!/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 ====="