# WLC Monitor WLC Monitor polls a Cisco Catalyst 9800 wireless LAN controller over SNMPv3 and sends email when controller, access-point, or configuration state changes. It can run continuously in Docker or as a one-shot systemd timer. Polling makes silence observable: a failed poll is itself a signal, whereas traps and syslog stop when a controller becomes unreachable. ## Alerts | Severity | Condition | Detection | |---|---|---| | CRITICAL | Controller unreachable | Configured number of consecutive failed polls | | CRITICAL | Controller rebooted | `sysUpTime` reset | | CRITICAL | AP down | AP disappears from `cLApTable` | | WARNING | AP rebooted | AP uptime reset | | WARNING | AP rejoined | Association uptime reset while AP uptime did not | | NOTICE | Running configuration changed | `ccmHistoryRunningLastChanged` advanced | | NOTICE | Configuration remains unsaved | Running change is newer than the saved change beyond the threshold | | RECOVERY | Controller or AP returns | Transition back to the available state | TimeTicks wrap at about 497 days. WLC Monitor compares the apparent counter movement with elapsed wall time so a normal wrap is not reported as a reboot. ## Requirements and compatibility - Docker Engine with Compose v2; or Python 3.10 or newer. - A Cisco Catalyst 9800 reachable over SNMPv3 authPriv. - Access to the CISCO-LWAPP-AP-MIB and CISCO-CONFIG-MAN-MIB objects used by the controller. - A reachable SMTP server. The implementation was developed against a Catalyst 9800-CL running IOS XE 17.12. Other 9800 models and releases may expose different MIB behavior; compatibility reports are welcome. SMTP supports STARTTLS, implicit TLS, optional username/password authentication, and plaintext delivery to a trusted relay. Do not select `none` across an untrusted network: alert bodies contain infrastructure names and operational state. Authentication is rejected in plaintext mode so SMTP credentials cannot be sent over an unencrypted connection. ## Quick start with Docker ```bash cp .env.example .env chmod 600 .env # Edit .env and set the controller, SNMP credentials, mail server, # sender, and recipients. docker compose up -d --build ``` Verify the controller and mail paths: ```bash docker compose exec wlc-monitor python /app/wlc_monitor.py --show docker compose exec wlc-monitor python /app/wlc_monitor.py --test-email docker compose logs -f wlc-monitor docker inspect --format '{{.State.Health.Status}}' wlc-monitor ``` The container runs unprivileged with a read-only root filesystem. Its named `/data` volume holds comparison state, failure counts, and any email awaiting retry. Preserve this volume across upgrades. Losing it creates a blank baseline: the next successful poll is deliberately silent, and changes during the gap cannot be reconstructed. The health check reports whether the poll loop is progressing, not whether the controller is reachable. Controller failure is a condition the monitor is expected to observe, not a reason to restart it. ## Configuration Environment variables override values in `config.ini`. In containers, prefer environment variables or mounted secret files. For a host install, begin with `config.ini.example`. | Variable | Required/default | Meaning | |---|---|---| | `WLC_HOST` | required | Controller hostname or address | | `WLC_PORT` | `161` | SNMP port | | `WLC_SNMP_USER` | required | SNMPv3 username | | `WLC_SNMP_AUTH` | required | Authentication passphrase | | `WLC_SNMP_PRIV` | required | AES privacy passphrase | | `WLC_SNMP_AUTH_PROTOCOL` | `sha` | `sha`, `sha224`, `sha256`, `sha384`, or `sha512` | | `WLC_TIMEOUT` | `5` | Per-request timeout in seconds | | `WLC_RETRIES` | `1` | SNMP retries | | `MAIL_SMTP_HOST` | required | SMTP server | | `MAIL_SMTP_PORT` | `25` | SMTP port | | `MAIL_SMTP_SECURITY` | `none` | `none`, `starttls`, or `ssl` | | `MAIL_SMTP_USER` | optional | SMTP username; password must also be set | | `MAIL_SMTP_PASSWORD` | optional | SMTP password; username must also be set | | `MAIL_FROM` | required | Envelope/header sender | | `MAIL_TO` | required | Comma-separated recipients | | `MAIL_SUBJECT_PREFIX` | `[WLC]` | Subject prefix | | `MONITOR_INTERVAL` | `300` | Seconds between continuous polls | | `MONITOR_FAIL_THRESHOLD` | `2` | Consecutive failures before alerting | | `MONITOR_UNSAVED_MINUTES` | `60` | Time before unsaved configuration alerts | | `MONITOR_STATE_FILE` | `/data/state.json` | Persistent state path | | `TZ` | `UTC` in Compose | Time zone used in alert timestamps | For secrets, `WLC_SNMP_AUTH_FILE`, `WLC_SNMP_PRIV_FILE`, and `MAIL_SMTP_PASSWORD_FILE` may point to mounted files instead of placing values directly in the process environment. Set either the direct variable or its `_FILE` form, never both. The SNMP authentication protocol must match the controller user. SHA-2 is preferred where the IOS XE release supports it; `sha` remains available for older configurations. ## Install with systemd Create a dedicated account and install the files: ```bash sudo useradd --system --no-create-home --shell /usr/sbin/nologin wlcmon sudo install -d -o root -g wlcmon -m 0750 /opt/wlc-monitor sudo install -o root -g wlcmon -m 0755 wlc_monitor.py /opt/wlc-monitor/ sudo install -o root -g wlcmon -m 0644 requirements.txt README.md /opt/wlc-monitor/ sudo cp config.ini.example /opt/wlc-monitor/config.ini sudo chown root:wlcmon /opt/wlc-monitor/config.ini sudo chmod 0640 /opt/wlc-monitor/config.ini sudo python3 -m venv /opt/wlc-monitor/venv sudo /opt/wlc-monitor/venv/bin/pip install -r /opt/wlc-monitor/requirements.txt ``` Edit `/opt/wlc-monitor/config.ini`, keeping `state_file = /var/lib/wlc-monitor/state.json`, then install and start the timer: ```bash sudo install -m 0644 wlc-monitor.service wlc-monitor.timer /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now wlc-monitor.timer ``` Verify: ```bash sudo systemctl start wlc-monitor.service journalctl -u wlc-monitor.service -n 20 systemctl list-timers wlc-monitor.timer ``` The unit creates `/var/lib/wlc-monitor` privately and applies a restrictive umask. The configuration is readable only by root and the dedicated service group. ## Local command-line use ```bash python3 -m venv .venv . .venv/bin/activate python -m pip install -r requirements.txt cp config.ini.example config.ini chmod 600 config.ini # Edit config.ini and use a writable state path such as ./state.json. python wlc_monitor.py --show python wlc_monitor.py --dry-run --no-save python wlc_monitor.py --test-email python wlc_monitor.py --loop --interval 60 python wlc_monitor.py --healthcheck ``` `--dry-run` prints alerts instead of sending them. It still updates state unless combined with `--no-save`. `--show` returns a nonzero exit status when the poll fails, making it suitable for scripts. The first successful poll is deliberately silent because there is no previous state to compare. ## Controller configuration Use a read-only SNMPv3 authPriv user and restrict its ACL to the single monitoring host. Exact SHA-2 syntax varies by IOS XE release; consult the controller documentation and make `WLC_SNMP_AUTH_PROTOCOL` match. ```text ip access-list standard SNMP-MON permit host snmp-server group WLCMON v3 priv read v1default access SNMP-MON snmp-server user wlcmon WLCMON v3 auth \ priv aes 128 ``` Do not paste real controller configuration, credentials, addresses, or device output into issues. ## Delivery and state behavior State is written atomically with mode `0600`. It can still contain controller and AP names, software details, and queued alert bodies, so treat it as operationally sensitive and do not commit or publish it. Transition alerts that fail SMTP delivery are stored in a small durable FIFO outbox and retried on the next poll. Delivery stops at the first failure so a recovery cannot arrive before its outage. A continuously unsaved configuration alerts once when it first exceeds the threshold, then becomes eligible again after it is saved. A crash between the SMTP server accepting a message and the state write can still cause a duplicate; recipients should tolerate at-least-once delivery. ## Operational limitations - SNMP reveals that a configuration changed, not what changed. Inspect the controller's configuration history for the actual diff. - The poll interval is the detection resolution. An AP that disconnects and recovers entirely between polls is invisible. - AP renames look like a down and join pair because APs are keyed by name. - The monitoring host remains a single point of failure. Use an external dead-man/heartbeat service if silent monitor failure must be detected. - A missing or partial AP table is treated as a failed poll to avoid false reboot/rejoin alerts. ## Development Run the same checks used by CI: ```bash python -m py_compile wlc_monitor.py python -m unittest discover -v python -m pip check docker compose config --quiet docker build -t wlc-monitor:test . ``` ## Contributing and security Bug reports and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md), not in a public issue. ## License WLC Monitor is available under the [MIT License](LICENSE).