Christian Manivong 5d193ba7e8
CI / test (3.11) (push) Failing after 7s
CI / test (3.10) (push) Failing after 12s
CI / test (3.9) (push) Failing after 6s
CI / test (3.12) (push) Failing after 18s
fix(ping): post the settings where the API expects them, not one node above
Every ping against a live OPNsense failed:

    ping job creation failed for 10.30.0.1: {'result': 'failed',
    'validations': {'ping.settings.hostname': 'A value is required.'}}

_ping_model_node read GET /api/diagnostics/ping/get and took the single
dict-valued key as the node to post under. The real model nests two levels:

    {"ping": {"settings": {"hostname": "", "fam": {"ip": {...}, "ip6": {...}},
                           "source_address": "", "packetsize": "", ...}}}

so the helper answered "ping" and the job was created with the fields sitting
where the settings node belongs. The hostname never arrived, and the firewall
said so on every single call.

_ping_model_path walks the whole chain of single-dict wrappers and stops at the
first level holding more than one key — the field level, where fam is a dict
too and one more step would land inside a form field. _wrap_in_model nests the
settings accordingly, so a one-level model keeps working and the default, for
when /get cannot be read, is what current firmware ships.

The tests missed this because FakePingAPI answered /get with a one-level model
and read the posted payload back through the same assumption: the fake agreed
with the code about a shape neither of them shares with a device. It now speaks
what an OPNsense speaks, reads the payload through the model path, and a second
test keeps the one-level case covered.

Verified against a live firewall: the job is accepted ({"result": "ok"}) and
10.30.0.1 answers 3 of 3 at 0.116 ms.

Closes #3
2026-08-22 15:44:56 +07:00
2026-05-29 09:22:10 +02:00
2026-05-29 09:22:10 +02:00
2026-05-29 09:22:10 +02:00
2026-05-29 09:22:10 +02:00

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

pip install napalm-opnsense

Or from source:

git clone https://github.com/napalm-automation-community/napalm-opnsense
cd napalm-opnsense
pip install -e .

Quick start

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:

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.

[
  {
    "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

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

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.

License

Apache 2.0 — see LICENSE.

S
Description
No description provided
Readme
203 KiB
Languages
Python 100%