OPNsense has no synchronous ping endpoint. /api/diagnostics/ping is a job API — create, start, read statistics, stop, remove — so a single ping costs five requests and roughly a second of waiting, which makes the generic per-host sweep from napalm-device-types unusable for a whole subnet. The override exploits what the job API does offer instead: jobs are independent and run on the firewall in parallel, and search_jobs reports all of them in one response. A batch (32 by default) is created and started, waited for once, harvested with a single request and then cleaned up, so waiting time per batch is constant rather than linear in hosts. PING_SWEEP_MAX_TARGETS bounds the sweep as a whole — this runs on production firewalls. Two details the API forces: results are polled, because search_jobs signals the running ping with SIGINFO and then parses whatever it has written so far, so the first read of a healthy host can still show zero probes; and the model's root node is read from /get rather than hardcoded, so a rename in a future OPNsense release cannot silently break job creation. Tested against mocked API responses only — no live device is reachable at the moment, so the endpoint shapes come from the OPNsense sources (PingController, scripts/interfaces/ping.py).
220 lines
7.2 KiB
Markdown
220 lines
7.2 KiB
Markdown
# napalm-opnsense
|
||
|
||
NAPALM community driver for **OPNsense** firewalls (read-only, via REST API).
|
||
|
||
## Tested devices
|
||
|
||
| Model | OPNsense Version | Tested |
|
||
|---|---|---|
|
||
| OPNsense (virtual/bare metal) | 24.7 | ✅ |
|
||
|
||
> Additional OPNsense versions should work — contributions welcome.
|
||
|
||
## Requirements
|
||
|
||
| Dependency | Minimum version |
|
||
|---|---|
|
||
| Python | 3.9 |
|
||
| NAPALM | 4.0 |
|
||
| requests | 2.28 |
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
pip install napalm-opnsense
|
||
```
|
||
|
||
Or from source:
|
||
|
||
```bash
|
||
git clone https://github.com/napalm-automation-community/napalm-opnsense
|
||
cd napalm-opnsense
|
||
pip install -e .
|
||
```
|
||
|
||
## Quick start
|
||
|
||
```python
|
||
from napalm import get_network_driver
|
||
|
||
driver = get_network_driver("opnsense")
|
||
with driver(
|
||
"192.168.1.1",
|
||
"my_api_key",
|
||
"my_api_secret",
|
||
optional_args={"verify": False},
|
||
) as device:
|
||
facts = device.get_facts()
|
||
print(facts)
|
||
```
|
||
|
||
## Authentication
|
||
|
||
OPNsense uses API key/secret pairs instead of username/password. Generate
|
||
a key pair in the OPNsense GUI under **System → Access → Users → edit user →
|
||
API keys**.
|
||
|
||
Pass the credentials via `optional_args`:
|
||
|
||
```python
|
||
optional_args={
|
||
"api_key": "your-api-key",
|
||
"api_secret": "your-api-secret",
|
||
"verify": False, # set to a CA bundle path or True in production
|
||
}
|
||
```
|
||
|
||
Alternatively, pass the key/secret as the positional `username`/`password`
|
||
arguments.
|
||
|
||
## Implemented getters
|
||
|
||
| Getter | Status | OPNsense API Endpoint |
|
||
|---|---|---|
|
||
| `get_facts` | ✅ | `GET /api/core/system/status` |
|
||
| `get_interfaces` | ✅ | `GET /api/interfaces/overview/export` |
|
||
| `get_interfaces_ip` | ✅ | `GET /api/interfaces/addresses/export` |
|
||
| `get_interfaces_counters` | ✅ | `GET /api/diagnostics/interface/get_interface_statistics` |
|
||
| `get_arp_table` | ✅ | `GET /api/diagnostics/interface/get_arp` |
|
||
| `get_ipv6_neighbors_table` | ✅ | `GET /api/diagnostics/interface/get_ndp` |
|
||
| `get_route_to` | ✅ | `GET /api/diagnostics/interface/get_routes` |
|
||
| `get_environment` | ✅ | `GET /api/diagnostics/system/system_resources` + `system_temperature` |
|
||
| `get_lldp_neighbors` | ✅ ¹ | `GET /api/lldpd/service/neighbor` |
|
||
| `get_lldp_neighbors_detail` | ✅ ¹ | `GET /api/lldpd/service/neighbor` |
|
||
| `get_ntp_servers` | ✅ | `GET /api/ntpd/service/status` |
|
||
| `get_config` | ✅ | `GET /api/core/backup/download/this` (XML) |
|
||
| `is_alive` | ✅ | TCP socket check |
|
||
| `get_bgp_neighbors` | ✅ ² | `GET /api/quagga/bgp/get` + `GET /api/quagga/diagnostics/bgpneighbors` |
|
||
| `get_vlans` | ✅ | `GET /api/interfaces/vlan_settings/search_item` |
|
||
| `get_mac_address_table` | ❌ | Not applicable (firewall, no L2 switching) |
|
||
| `ping` | ✅ | `POST /api/diagnostics/ping/set` + `start` + `search_jobs` + `stop`/`remove` |
|
||
| `ping_sweep` | ✅ ³ | same endpoints, one batch of parallel jobs at a time |
|
||
|
||
> ¹ Requires the `os-lldpd` plugin. Returns empty dict if the plugin is not installed.
|
||
> ² Requires the `os-frr` (FRR/Quagga) plugin. Returns empty dict if the plugin is not installed or FRR is not running.
|
||
> ³ Overrides the generic per-host loop from `napalm-device-types`.
|
||
|
||
### Ping
|
||
|
||
OPNsense has no synchronous ping endpoint: `/api/diagnostics/ping` is a *job*
|
||
API — create, start, read statistics, stop, remove. A single ping therefore
|
||
costs five requests and about a second of waiting, which makes the generic
|
||
sequential sweep from `napalm-device-types` unusable for a whole subnet.
|
||
|
||
`ping_sweep` exploits what the job API does offer instead: jobs are
|
||
independent and run on the firewall in parallel, and `search_jobs` reports all
|
||
of them in one response. It creates and starts a batch
|
||
(`PING_SWEEP_BATCH_SIZE`, default 32), waits once, reads every result with a
|
||
single request, then cleans the batch up — waiting time per batch is constant
|
||
rather than linear in hosts. `PING_SWEEP_MAX_TARGETS` (default 512) bounds the
|
||
sweep as a whole; both are deliberately conservative, since this runs on
|
||
production firewalls.
|
||
|
||
Results are polled rather than read once: `search_jobs` signals the running
|
||
ping with `SIGINFO` and parses whatever it has written so far, so the
|
||
statistics line lands in the log slightly after the request that triggered it.
|
||
|
||
`ttl` and `vrf` are accepted for NAPALM compatibility and ignored — the API
|
||
has no equivalent.
|
||
|
||
## Config management
|
||
|
||
OPNsense does not expose a single generic "push config" endpoint. Config is
|
||
managed per-module via separate API controllers. This driver implements config
|
||
management for **static routes** via `/api/routes/routes/`.
|
||
|
||
> **Why routes?** Routes are the most common network-automation target on a
|
||
> firewall, and the OPNsense routes API provides full CRUD operations.
|
||
|
||
### Supported methods
|
||
|
||
| Method | OPNsense API |
|
||
|---|---|
|
||
| `load_merge_candidate(config=...)` | stages routes in memory |
|
||
| `compare_config()` | diffs against `GET /api/routes/routes/searchroute` |
|
||
| `commit_config()` | snapshots backup → `POST /api/routes/routes/addroute` × n → `reconfigure` |
|
||
| `discard_config()` | clears staged candidate |
|
||
| `rollback()` | `POST /api/core/backup/revert_backup/{id}` — restores the config.xml snapshot taken before the last commit |
|
||
|
||
### Config format
|
||
|
||
The `load_merge_candidate` `config` parameter must be a **JSON array** of route
|
||
objects, each with `network` and `gateway` keys. `gateway` must be the
|
||
**name** of an existing OPNsense gateway (as configured under
|
||
*System → Gateways → Configuration*), not an IP address.
|
||
|
||
```json
|
||
[
|
||
{
|
||
"network": "10.0.0.0/8",
|
||
"gateway": "WAN_GW",
|
||
"descr": "Corporate internal",
|
||
"disabled": "0"
|
||
},
|
||
{
|
||
"network": "0.0.0.0/0",
|
||
"gateway": "WAN_GW",
|
||
"descr": "Default route"
|
||
}
|
||
]
|
||
```
|
||
|
||
### Example
|
||
|
||
```python
|
||
import json
|
||
|
||
driver = get_network_driver("opnsense")
|
||
d = driver("192.168.1.1", "user", "pass", optional_args={"api_key": "k", "api_secret": "s"})
|
||
d.open()
|
||
|
||
routes = json.dumps([{"network": "10.0.0.0/8", "gateway": "WAN_GW"}])
|
||
d.load_merge_candidate(config=routes)
|
||
|
||
print(d.compare_config()) # unified diff
|
||
d.commit_config() # applies routes and calls reconfigure
|
||
d.rollback() # removes the routes just added
|
||
d.close()
|
||
```
|
||
|
||
### Limitations
|
||
|
||
- `load_replace_candidate` is not supported (no XML upload endpoint in the JSON API).
|
||
- Non-route config (firewall rules, DHCP, DNS, interfaces, …) must be managed
|
||
via OPNsense module-specific controllers — outside the scope of this driver.
|
||
- `rollback()` reverts the entire `config.xml` to the pre-commit state (not just
|
||
the routes). OPNsense's backup API has no partial-restore capability.
|
||
- Rollback uses the backup snapshot taken at `commit_config()` time. If no
|
||
commit was made in the current session, the most recent available backup is
|
||
used as a fallback.
|
||
|
||
## Optional arguments
|
||
|
||
| Argument | Default | Description |
|
||
|---|---|---|
|
||
| `api_key` | `username` | OPNsense API key |
|
||
| `api_secret` | `password` | OPNsense API secret |
|
||
| `base_url` | `https://<hostname>` | Override the base URL |
|
||
| `verify` | `True` | TLS certificate verification (path or bool) |
|
||
|
||
## Development
|
||
|
||
```bash
|
||
pip install -e ".[dev]"
|
||
pytest tests/
|
||
```
|
||
|
||
## CI
|
||
|
||
This project includes a GitHub Actions workflow that:
|
||
- Runs unit tests across Python 3.9–3.12
|
||
- Builds sdist and wheel
|
||
- Uploads build artifacts
|
||
|
||
See [.github/workflows/ci.yml](.github/workflows/ci.yml).
|
||
|
||
## License
|
||
|
||
Apache 2.0 — see [LICENSE](LICENSE).
|
||
|