Files
napalm-opnsense/README.md
T
Christian Manivong e3ed0f1bdd
CI / test (3.10) (push) Successful in 30s
CI / test (3.11) (push) Successful in 29s
CI / test (3.12) (push) Successful in 30s
CI / test (3.10) (pull_request) Successful in 30s
CI / test (3.11) (pull_request) Successful in 28s
CI / test (3.12) (pull_request) Successful in 29s
fix: name and version from system_information; a CARP address on its MASTER's interface
get_facts read hostname and version from /api/core/system/status, which on
OPNsense 26.7 carries neither: every firewall read as nameless and "unknown",
and netOrk kept naming one by its address. /api/diagnostics/system/system_information
has both (name is the FQDN, versions[0] "OPNsense 26.7.7-amd64"). The status
answer still counts where the new one gives nothing.

get_interfaces_ip listed each interface's primary address only, so a CARP address
never showed as the holder's. It now lists an address bound to a VHID on the node
that is MASTER for it, and leaves it off a BACKUP -- as keepalived puts the address
on the master alone. netOrk's VIP badge sits on that interface (netOrk #1121).
2026-10-16 15:37:43 +02:00

222 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/diagnostics/system/system_information` (name, version) + `GET /api/core/system/status` (fallback on older versions) |
| `get_interfaces` | ✅ | `GET /api/interfaces/overview/export` |
| `get_interfaces_ip` | ✅ | `GET /api/interfaces/overview/export`; a CARP address only on the node that is MASTER for it |
| `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` |
| `get_listening_sockets` | ✅ | `GET /api/diagnostics/interface/get_socket_statistics` + `/api/core/service/search` + `/api/wireguard/service/show` (WireGuard's kernel sockets by listen port) |
| `get_virtual_ips` | ✅ | `GET /api/interfaces/overview/export`: every address bound to a CARP VHID (CARP address and IP aliases on it), with this node's CARP state. `interfaces/vip_settings` is not read: it carries the CARP passwords |
| `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).