Metadata-Version: 2.4
Name: napalm-opnsense
Version: 0.5.0
Summary: NAPALM driver for OPNsense (read-only via REST API).
Author: Christian Manivong
License-Expression: Apache-2.0
Project-URL: Repository, https://git.netork.io/NAPALM/napalm-opnsense
Project-URL: Issues, https://git.netork.io/NAPALM/napalm-opnsense/issues
Project-URL: Changelog, https://git.netork.io/NAPALM/napalm-opnsense/src/branch/main/CHANGELOG.md
Classifier: Topic :: Utilities
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: napalm>=4.0.0
Requires-Dist: napalm-device-types>=4.21.1
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

# 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.10 |
| NAPALM | 4.0 |
| napalm-device-types | 4.21.1 |
| requests | 2.28 |

## Installation

```bash
pip install napalm-opnsense
```

Or from source:

```bash
git clone https://git.netork.io/NAPALM/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

The rules are in [STANDARDS.md](https://git.netork.io/NAPALM/driver-standard/src/branch/main/STANDARDS.md).

```bash
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest --cov
```

## CI

[.gitea/workflows/ci.yml](.gitea/workflows/ci.yml) calls the shared workflow of the
driver standard: ruff, mypy, the tests on Python 3.10–3.13 and at the lowest declared
versions, bandit, pip-audit and a build check. A release tag publishes to PyPI.

## License

Apache 2.0 — see [LICENSE](LICENSE).

