Skip to main content

Commands

cli

cli öffnet die eigentliche Switch-CLI, also die FastPath-/Switching-Ebene.

Dort arbeitest du mit Dingen wie:

show vlan
show running-config
configure
interface 0/1
switchport mode access
vlan database
write memory

Das ist die Cisco-ähnliche Oberfläche für:

  • VLANs
  • Ports
  • Trunks
  • Layer-2/teilweise Layer-3
  • laufende Switch-Konfiguration

Das Problem: Diese CLI verwaltet hauptsächlich den laufenden FastPath-Zustand. write memory meldet zwar Erfolg, aber die UniFi-Firmware baut FastPath beim Start wieder aus /tmp/system.cfg beziehungsweise der von ubntconf erzeugten Startup-Konfiguration auf. Deshalb verschwinden die Änderungen nach einem Reboot.




mca-cli-op

mca-cli-op gehört zur UniFi-Management-Ebene.

Auf deinem Switch ist es nur ein Symlink:

/usr/bin/mca-cli-op -> mcad

Es startet also das Programm mcad in einem speziellen Betriebsmodus.

mcad kümmert sich um Dinge wie:

  • Adoption
  • Controller-Kommunikation
  • Inform-Nachrichten
  • Provisionierung
  • Managementstatus
  • Werksreset
  • Speichern und Laden von UniFi-Konfiguration
  • mgmt.is_default
  • mgmt.is_setup_completed

Mit Argumenten wird es normalerweise als interner Einzelbefehl verwendet, zum Beispiel:

mca-cli-op inform
mca-cli-op set-inform http://controller:8080/inform
mca-cli-op set-default

Der Firmware-Wrapper ruft solche Befehle selbst auf. set-inform wird beispielsweise direkt an mca-cli-op weitergereicht, während set-default einen Werksreset einleitet.

Ohne Argument:

mca-cli-op

öffnet es die interaktive Management-CLI:

UniFi#

Das ist keine Linux-Shell und auch nicht dieselbe CLI wie cli. Deshalb kennt sie kein echo, awk, find oder Shell-Pipes.

Vereinfacht dargestellt

SSH-Shell
│
├── cli
│   └── FastPath-Switch-CLI
│       ├── VLANs
│       ├── Ports
│       ├── Trunks
│       └── running-config
│
└── mca-cli-op / mcad
    └── UniFi-Management-Stack
        ├── Adoption
        ├── Provisionierung
        ├── Controller/Inform
        ├── mgmt-Konfiguration
        └── Default-/Setup-Status


USW-Pro-48-US.7.5.6# mca-cli-op help 2>&1
UniFi Command Line Interface - Ubiquiti Networks

   info                      display device information
   set-default               restore to factory default
   set-inform <inform_url>   attempt inform URL (e.g. set-inform http://192.168.0.8:8080/inform)
   upgrade <firmware_url>    upgrade firmware (e.g. upgrade http://192.168.0.8/unifi_fw.bin)
   fwupdate --url <firmware_url|firmware_name> [--dl-only] [--md5sum <sum_of_fw>]
            [--keep-firmware] [--keep-running] [--reboot-sys] 
                                   new firmware update command
   reboot                    reboot the device

USW-Pro-48-US.7.5.6# printf 'Rückgabecode: %s\n' "$?"
Rückgabecode: 0
USW-Pro-48-US.7.5.6# mca-cli-op info

Model:       USW-Pro-48
Version:     7.5.6.17090
MAC Address: f4:e2:c6:c0:ec:22
IP Address:  172.16.1.22
Hostname:    USW-Pro-48
Uptime:      3893 seconds
NTP:         Not synchronized

Status:      Unable to resolve (http://unifi:8080/inform)
USW-Pro-48-US.7.5.6# echo '=== Managementwerte ==='
=== Managementwerte ===
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# for datei in \
>     /tmp/system.cfg \
>     /tmp/default.cfg \
>     /etc/persistent/cfg/mgmt
> do
>     if [ -r "$datei" ]; then
>         echo
>         echo "--- $datei ---"
> 
>         awk -F= '
>             $1 == "mgmt.is_default" ||
>             $1 == "mgmt.is_setup_completed" ||
>             $1 == "mgmt.cfgversion" ||
>             $1 == "mgmt.selfrun" ||
>             $1 ~ /^mgmt\.servers\.[0-9]+\.url$/ ||
>             $1 ~ /^mgmt\.led_/ {
>                 printf "%d: %s\n", NR, $0
>             }
>         ' "$datei"
>     else
>         echo
>         echo "--- $datei fehlt oder ist nicht lesbar ---"
>     fi
> done

--- /tmp/system.cfg ---
2: mgmt.is_default=true

--- /tmp/default.cfg ---
2: mgmt.is_default=true

--- /etc/persistent/cfg/mgmt fehlt oder ist nicht lesbar ---
USW-Pro-48-US.7.5.6# 

USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# awk '
> {
>     text[NR] = $0
> }
> 
> /set_cfg_save/ {
>     for (nummer = NR - 15; nummer <= NR + 20; nummer++) {
>         if (nummer > 0) {
>             zeigen[nummer] = 1
>         }
>     }
> }
> 
> END {
>     for (nummer = 1; nummer <= NR; nummer++) {
>         if (zeigen[nummer]) {
>             printf "%5d: %s\n", nummer, text[nummer]
>         }
>     }
> }
> ' /usr/etc/syswrapper.sh
 1030:          export SSH_CLIENT=""
 1031:          export UI_SSH_SAFE_CONTEXT="$LOG $TAIL_PID"
 1032:          setsid "$SCRIPT" "$cmd" "$@" >>$LOG 2>&1 </dev/null 3<&- &
 1033:          wait $TAIL_PID
 1034:          cat <&3 # Dump anything that tail might have missed
 1035:          exit 0
 1036:  fi
 1037: 
 1038:  set -- $UI_SSH_SAFE_CONTEXT
 1039:  [ $# -eq 2 ] || return
 1040:  local LOG=$1
 1041:  local TAIL_PID=$2
 1042:  local CLEAN_UP="kill $TAIL_PID 2>/dev/null; rm -f $LOG"
 1043:  trap "$CLEAN_UP; exit 0" HUP INT QUIT TERM EXIT
 1044: }
 1045: set_cfg_save() {
 1046:  if [ ! -e "$NEED_CFG_SAVE_FILE" ]; then
 1047:          log "Creating $NEED_CFG_SAVE_FILE"
 1048:          touch "$NEED_CFG_SAVE_FILE"
 1049:  fi
 1050: 
 1051:  pidof $SYSMON || syswrapper.sh cfg_save_check
 1052: 
 1053:  state_reload
 1054: }
 1055: 
 1056: # mac2serial <mac>
 1057: mac2serial() {
 1058:  echo $1 | sed -e 's/://g' -e 'y/ABCDEF/abcdef/'
 1059: }
 1060: 
 1061: # pkill_generic <process_name> [signal] [args]
 1062: pkill_generic() {
 1063:  local process=$1
 1064:  local signal=$2
 1065:  shift; shift;
 2975:  if type 'is_support_stacking' 2> /dev/null | grep -q 'function' && is_support_stacking ; then
 2976:          role="$(swctrl mgmt stack-info | sed -n 's/role=\([a-z]*\),.*/\1/p')"
 2977: 
 2978:          if [ "$role" = "controller" ]; then
 2979:                  log "[Stack] $cmd remote unit $unit_number"
 2980:                  swctrl mgmt stack-run -b "$unit_number" "syswrapper.sh $cmd"
 2981:          fi
 2982:  fi
 2983:  ;;
 2984: set-etherlight-mode)
 2985:  exit_if_busy $cmd $*
 2986:  exit_if_state_lock_failed $cmd $*
 2987:  _set_etherlight_mode "$1"
 2988:  rc=$?
 2989:  if [ $rc -eq 1 ]; then
 2990:          set_cfg_save
 2991:  fi
 2992:  state_unlock
 2993:  ;;
 2994: set-stack-unit-etherlight-mode)
 2995:  exit_if_busy $cmd $*
 2996:  exit_if_state_lock_failed $cmd $*
 2997:  _set_etherlight_mode "$1"
 2998:  rc=$?
 2999:  if [ $rc -eq 1 ]; then
 3000:          set_cfg_save
 3001:  fi
 3002:  state_unlock
 3003:  ;;
 3004: set-etherlight-device-type)
 3005:  exit_if_busy $cmd $*
 3006:  exit_if_state_lock_failed $cmd $*
 3007:  pkill_generic led_locate.sh -f
 3008: 
 3009:  led_device_type_mode=3
 3010:  _set_etherlight_mode $led_device_type_mode
 3011:  _set_port_bitmap_color $led_device_type_mode $@
 3012: 
 3013:  state_unlock
 3014:  ;;
 3015: set-port-locate)
 3016:  exit_if_busy $cmd $*
 3017:  exit_if_state_lock_failed $cmd $*
 3018:  pkill_generic led_locate.sh -f
 3019: 
 3020:  led_port_locate_mode=4
 3136:  if [ "$2" = "nopersist" ]; then
 3137:          persist=0
 3138:          log "[apply-config] persist=false - config will not be saved to flash"
 3139:  fi
 3140:  state_lock
 3141:  before=$(sed "s/\..*//" /proc/uptime)
 3142:  before_mca_pid=$(cat_file /var/run/mcad.0.pid)
 3143:  full_provision=0
 3144:  if ! do_fast_apply; then
 3145:          lockfile_retry 60 "$NEED_CFG_SAVE_LOCK_FILE" || exit 1
 3146:          full_provision=1
 3147:          /usr/etc/rc.d/rc restart
 3148:          unlockfile "$NEED_CFG_SAVE_LOCK_FILE"
 3149:  fi
 3150:  if [ "$persist" = 1 ]; then
 3151:          pidof $SYSMON && set_cfg_save
 3152:  fi
 3153:  state_reload
 3154:  now=$(sed "s/\..*//" /proc/uptime)
 3155:  delta=$((now - before))
 3156:  log "Provision took $delta sec, full=$full_provision, persist=$persist"
 3157:  # Remove deltas that may be due to time syncs, etc.
 3158:  if [ "$delta" -ge 0 -a "$delta" -lt 300 ]; then
 3159:          if [ "$full_provision" = 1 ]; then
 3160:                  add_stat /var/run/provision_time.total $delta
 3161:                  add_stat /var/run/provision_time.count 1
 3162:          else
 3163:                  add_stat /var/run/fast_apply_time.total $delta
 3164:                  add_stat /var/run/fast_apply_time.count 1
 3165:          fi
 3166:  fi
 3167:  # Update fw auto-rollback flag based on controller config
 3168:  # Default is enabled — only disable when explicitly set to "disabled"
 3169:  if [ "$(get_config_value /tmp/system.cfg switch.fw_auto_rollback.status)" = "disabled" ]; then
 3170:          touch /var/run/fw_auto_rollback_disabled
 3171:  else
 3195:  if [ ! -f /tmp/previous.cfg ]; then
 3196:          log "[apply-previous-config] previous.cfg not found, skipping rollback"
 3197:          state_unlock
 3198:          exit 1
 3199:  fi
 3200:  log "[apply-previous-config] reverting to previous configuration"
 3201:  cp -f /tmp/previous.cfg /tmp/system.cfg
 3202:  before=$(sed "s/\..*//" /proc/uptime)
 3203:  full_provision=0
 3204:  if ! do_fast_apply; then
 3205:          lockfile_retry 60 "$NEED_CFG_SAVE_LOCK_FILE" || exit 1
 3206:          full_provision=1
 3207:          /usr/etc/rc.d/rc restart
 3208:          unlockfile "$NEED_CFG_SAVE_LOCK_FILE"
 3209:  fi
 3210:  pidof $SYSMON && set_cfg_save
 3211:  state_reload
 3212:  now=$(sed "s/\..*//" /proc/uptime)
 3213:  delta=$((now - before))
 3214:  log "[apply-previous-config] Rollback took $delta sec, full=$full_provision"
 3215:  state_unlock
 3216:  ;;
 3217: apply-stack-unit-config)
 3218:  run_ssh_safe "$@"
 3219:  if type 'is_support_stacking' 2> /dev/null | grep -q 'function' && is_support_stacking ; then
 3220:          unit="$1"
 3221:          systemcfg_path="$2"
 3222:          role="$(swctrl mgmt stack-info | sed -n 's/role=\([a-z]*\),.*/\1/p')"
 3223:          before=$(sed "s/\..*//" /proc/uptime)
 3224:          STACKING_PATH="/tmp/stacking"
 3225: 
 3226:          if [ ! -f "$systemcfg_path" ]; then
 3227:                  log "[error] Configuration file not found: $systemcfg_path."
 3228:                  exit 1
 3229:          fi
 3230: 
 3269:          /bin/sh /var/run/if_up_hook.$ifname
 3270:  fi
 3271:  ;;
 3272: soft-restart)
 3273:  exit_if_busy $cmd $*
 3274:  run_ssh_safe "$@"
 3275:  state_lock
 3276:  lockfile_retry 30 "$NEED_CFG_SAVE_LOCK_FILE" || exit 1
 3277:  /usr/etc/rc.d/rc restart
 3278:  unlockfile "$NEED_CFG_SAVE_LOCK_FILE"
 3279:  state_reload
 3280:  state_unlock
 3281:  ;;
 3282: save-config)
 3283:  state_lock
 3284:  set_cfg_save
 3285:  state_unlock
 3286:  ;;
 3287: reload)
 3288:  exit_if_busy $cmd $*
 3289:  exit_if_state_lock_failed $cmd $*
 3290:  state_reload
 3291:  state_unlock
 3292:  ;;
 3293: set-ready)
 3294:  # called by mcagent
 3295:  state_lock
 3296:  set_state_ready
 3297:  state_unlock
 3298:  # Mark boot as successful for auto-rollback (once per boot)
 3299:  if [ ! -f /var/run/boot_success_marked ] && [ -x /sbin/ubntbox ] && \
 3300:     grep -q "ubnt_active_slot=" /proc/cmdline 2>/dev/null; then
 3301:          ubntbox bootsel mark-success && touch /var/run/boot_success_marked
 3302:  fi
 3303:  # Update fw auto-rollback flag based on controller config
 3304:  # Default is enabled — only disable when explicitly set to "disabled"
 3656:  ble_stp $@
 3657:  ;;
 3658: ble-check)
 3659:  ble_check $@
 3660:  ;;
 3661: kick-sta)
 3662:  kick_sta $1
 3663:  ;;
 3664: kick-sta-on)
 3665:  kick_sta_on $2 $1 $3
 3666:  ;;
 3667: block-sta)
 3668:  driver_kick_block_sta $1
 3669:  add_mac /etc/persistent/cfg/blocked_sta "$1"
 3670:  state_lock
 3671:  set_cfg_save
 3672:  state_unlock
 3673:  ;;
 3674: unblock-sta)
 3675:  del_mac /etc/persistent/cfg/blocked_sta "$1"
 3676:  driver_unblock_sta $1
 3677:  state_lock
 3678:  set_cfg_save
 3679:  state_unlock
 3680:  ;;
 3681: apply-blocked-sta)
 3682:  driver_apply_blocklist
 3683:  ;;
 3684: apply-blocked-sta-ifup)
 3685:  driver_apply_blocklist_ifup $1
 3686:  ;;
 3687: redirector-init)
 3688:  guest_redirector_init_iptables "$@"
 3689:  ;;
 3690: redirector-deinit)
 3691:  guest_redirector_deinit_iptables "$@"
 3692:  ;;
 3693: authorize-guest)
 3694:  guest_authorize "$@"
 3695:  ;;
 3696: unauthorize-guest)
 3697:  guest_unauthorize "$@"
 3698:  kick_sta $1
USW-Pro-48-US.7.5.6#

Das ist der bisher wichtigste Fund. Die Firmware besitzt tatsächlich einen vorgesehenen internen Speicherweg:

syswrapper.sh save-config
        ↓
set_cfg_save
        ↓
/var/run/need_cfg_save
        ↓
sysmon
        ↓
syswrapper.sh cfg_save_check
        ↓
sanitize_cfg
        ↓
cfgmtd -w -p /etc /tmp/system.cfg

save-config ist also etwas anderes als FastPaths write memory. Es speichert die UniFi-Systemkonfiguration, aus der beim Boot anschließend /tmp/.startup-config für den Switch erzeugt wird. Genau diese Bootkette wurde in deinen bisherigen Tests sichtbar.

Der Haken bleibt: cfg_save_check schreibt nur, wenn nach sanitize_cfg eine nichtleere Datei /etc/persistent/cfg/mgmt existiert. Ohne sie versucht es lediglich, eine alte Managementdatei aus dem Flash wiederherzustellen.

USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# rm -rf /tmp/usw-flash-read
USW-Pro-48-US.7.5.6# mkdir -p /tmp/usw-flash-read
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# cfgmtd -r \
>     -p /tmp/usw-flash-read \
>     -f /tmp/usw-flash-read/system.cfg

USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# printf 'cfgmtd Rückgabecode: %s\n' "$?"
cfgmtd Rückgabecode: 0
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# echo

USW-Pro-48-US.7.5.6# echo '=== Gelesene Dateien ==='
=== Gelesene Dateien ===
USW-Pro-48-US.7.5.6# find /tmp/usw-flash-read \
>     -maxdepth 4 \
>     -type f \
>     -exec ls -l {} \;
-rw-------    1 ui       admin         1018 Jun 22 10:03 /tmp/usw-flash-read/system.cfg
-rw-------    1 ui       admin           83 Jan  1  1970 /tmp/usw-flash-read/persistent/dropbear_ed25519_host_key
-rw-------    1 ui       admin           89 Jan  1  1970 /tmp/usw-flash-read/persistent/dropbear_ed25519_host_key.pub
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# echo

USW-Pro-48-US.7.5.6# echo '=== Managementdatei ==='
=== Managementdatei ===
USW-Pro-48-US.7.5.6# if [ -r /tmp/usw-flash-read/persistent/cfg/mgmt ]; then
>     awk -F= '
>         $1 == "mgmt.is_default" ||
>         $1 == "mgmt.is_setup_completed" ||
>         $1 == "mgmt.cfgversion" ||
>         $1 == "mgmt.selfrun" ||
>         $1 ~ /^mgmt\.servers\.[0-9]+\.url$/ ||
>         $1 ~ /^mgmt\.led_/ {
>             printf "%d: %s\n", NR, $0
>         }
>     ' /tmp/usw-flash-read/persistent/cfg/mgmt
> else
>     echo 'Keine persistente Managementdatei im gelesenen Flash-Stand'
> fi
Keine persistente Managementdatei im gelesenen Flash-Stand
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# echo

USW-Pro-48-US.7.5.6# echo '=== Managementwerte aus system.cfg ==='
=== Managementwerte aus system.cfg ===
USW-Pro-48-US.7.5.6# if [ -r /tmp/usw-flash-read/system.cfg ]; then
>     awk -F= '
>         $1 == "mgmt.is_default" ||
>         $1 == "mgmt.is_setup_completed" ||
>         $1 == "mgmt.cfgversion" ||
>         $1 == "mgmt.selfrun" ||
>         $1 ~ /^mgmt\.servers\.[0-9]+\.url$/ ||
>         $1 ~ /^mgmt\.led_/ {
>             printf "%d: %s\n", NR, $0
>         }
>     ' /tmp/usw-flash-read/system.cfg
> else
>     echo 'Keine system.cfg gelesen'
> fi
2: mgmt.is_default=true
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# rm -rf /tmp/usw-mgmt-runtime-test
USW-Pro-48-US.7.5.6# mkdir -p /tmp/usw-mgmt-runtime-test
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# if [ -d /etc/persistent/cfg ]; then
>     cp -a /etc/persistent/cfg /tmp/usw-mgmt-runtime-test/cfg.before
> else
>     echo 'cfg war nicht vorhanden' > /tmp/usw-mgmt-runtime-test/cfg-was-missing
> fi
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# mkdir -p /etc/persistent/cfg
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# cat > /etc/persistent/cfg/mgmt <<'EOF'
> mgmt.is_default=false
> mgmt.is_setup_completed=true
> EOF
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# chmod 600 /etc/persistent/cfg/mgmt
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# echo '=== Angelegte Laufzeitdatei ==='
=== Angelegte Laufzeitdatei ===
USW-Pro-48-US.7.5.6# cat /etc/persistent/cfg/mgmt
mgmt.is_default=false
mgmt.is_setup_completed=true
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# echo

USW-Pro-48-US.7.5.6# echo '=== Interner Adoptionstest ==='
=== Interner Adoptionstest ===
USW-Pro-48-US.7.5.6# /sbin/syswrapper.sh check-is-adopted
sh: false: unknown operand
USW-Pro-48-US.7.5.6# printf 'Rückgabecode: %s\n' "$?"
Rückgabecode: 2
USW-Pro-48-US.7.5.6# 
USW-Pro-48-US.7.5.6# echo

USW-Pro-48-US.7.5.6# echo '=== Laufzeitstatus ==='
=== Laufzeitstatus ===
USW-Pro-48-US.7.5.6# printf 'IsDefault='
IsDefault=USW-Pro-48-US.7.5.6# cat /proc/ubnthal/status/IsDefault
true
USW-Pro-48-US.7.5.6# printf 'LED-Pattern='
LED-Pattern=USW-Pro-48-US.7.5.6# cat /proc/gpio/led_pattern
2
USW-Pro-48-US.7.5.6#

Der Fehler kommt nicht von deiner mgmt-Datei. Der Firmware-Code ist schlicht schlampig geschrieben:

[ $(get_fw_env is_default) = false -o $(get_mgmt_cfg mgmt.is_default) = false ]

Da get_fw_env is_default auf diesem Switch leer zurückkommt, entsteht sinngemäß:

[ = false -o false = false ]

BusyBox antwortet darauf mit:

sh: false: unknown operand

Die Funktion check-is-adopted ist auf diesem Modell also kaputt, weil Ubiquiti Variablen nicht in Anführungszeichen gesetzt hat. Ein Klassiker: Milliardenunternehmen, Shellcode wie aus einem Freitagnachmittag.

IsDefault=true und LED-Muster 2 bleiben zunächst unverändert, weil das Anlegen der Datei den laufenden mcad-Status nicht automatisch neu lädt.