94 lines
3.4 KiB
Markdown
94 lines
3.4 KiB
Markdown
# 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": "<image-name-or-version>" }`.
|
||
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.
|