# cnMaestro Cleanup + Bulk Firmware Upgrade Tool **Date:** 2026-05-09 **Status:** Approved, implementation starting ## Goal Build a Python CLI for cnMaestro Cloud (cloud.cambiumnetworks.com) that: 1. Removes devices offline for > 24 hours (configurable threshold). 2. Submits bulk firmware upgrade jobs to bring online devices to the latest firmware available for their model. ## Non-Goals - NetBox sync (separate concern). - Daemon/scheduling (use cron + `--yes`). - Firmware rollback (cnMaestro handles). - Custom firmware uploads (use cnMaestro UI). ## Architecture ``` cnmaestro/ ├── README.md # Setup, API client creation, usage ├── .env.example # CNMAESTRO_CLIENT_ID / _SECRET / _BASE_URL ├── pyproject.toml # uv project ├── cnmaestro/ │ ├── __init__.py │ ├── client.py # OAuth2 + paginated httpx wrapper │ ├── devices.py # list, partition, delete │ ├── firmware.py # latest-per-model, job submission │ └── cli.py # click CLI └── tests/ ├── test_client.py ├── test_devices.py └── test_firmware.py ``` ## API Reference (cnMaestro Cloud v2) - **Auth:** `POST /api/v2/access/token` with `client_credentials` grant → bearer token. Cache in-memory, re-fetch on 401. - **List devices:** `GET /api/v2/devices?limit=100&offset=N`. Key fields: `mac`, `name`, `network`, `tower`, `status` ("online"/"offline"), `last_sync` (epoch ms or ISO 8601), `product`, `software_version`. - **Delete device:** `DELETE /api/v2/devices/{mac}`. - **Firmware images:** `GET /api/v2/firmware`. Returns image metadata with `product` and `version` fields per image. - **Submit job:** `POST /api/v2/devices/jobs/firmware` with `{ "devices": ["MAC", ...], "package": "" }`. Limit 100 MACs per job. ## Behavior ### `cnmaestro cleanup [--dry-run] [--yes] [--threshold-hours 24] [--force]` 1. Fetch all devices (paginated). 2. Partition: online / offline-recent / offline >24h / never-synced. 3. Removal candidates = offline >24h ∪ never-synced. 4. Render `rich` table: name, MAC, product, last_sync, tower. 5. Show counts. If candidates > 50% of fleet, refuse unless `--force`. 6. Prompt; on confirm, DELETE each with progress bar; collect failures. ### `cnmaestro upgrade [--dry-run] [--yes] [--product P1,P2,...]` 1. Fetch all devices, keep `status=online`. 2. Fetch firmware images list. 3. For each product, pick highest semver as target. 4. Build groups `(product, target_version) → [mac, ...]` for devices where `software_version != target`. 5. Render summary table. 6. Prompt; submit one POST per group, chunked to 100 MACs. Print job IDs. ## Safety Rails - Default to dry-run summary + interactive confirm. - 50%-of-fleet sanity check on cleanup. - Token never persisted. - All destructive ops appended to `./cnmaestro.log` (timestamped). - Tests use `respx` to mock httpx; CI never hits live API. ## Testing Strategy - Unit tests for partition logic (no HTTP). - Unit tests for "latest version per model" with fixture firmware list. - HTTP-mocked tests for client pagination, 401 retry, delete. - No live API tests in CI. ## Setup (User) 1. Log into cloud.cambiumnetworks.com. 2. Manage Services → API Clients → Add Client. 3. Copy client ID + secret into `.env`. 4. `uv sync && uv run cnmaestro cleanup --dry-run` to validate.