fix: get_config reads the backup as text, not JSON
CI / test (3.10) (push) Successful in 36s
CI / test (3.11) (push) Successful in 34s
CI / test (3.12) (push) Successful in 39s
CI / test (3.10) (pull_request) Successful in 32s
CI / test (3.11) (pull_request) Successful in 31s
CI / test (3.12) (pull_request) Successful in 33s

/api/core/backup/download/this answers with config.xml itself, but
get_config went through _get(), which calls .json(). That raised on
every real device, the except swallowed it, and "running" came back
empty. The unit tests replaced _get with a lambda, so they never saw it.

_get_text() returns the body as text. A failed read now raises instead
of returning an empty string, so the caller sees why it failed.

Closes #11
This commit is contained in:
Christian Manivong committed 2026-10-11 18:35:44 +02:00
1 parent fa88850535
commit 852c1d59df
2 files changed
+79 -15

No files matched your search

+25 -12
View File
@@ -172,6 +172,22 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
response.raise_for_status()
return response.json()
def _get_text(self, path: str) -> str:
"""Perform a GET request and return the body as text.
For the few endpoints that answer with a file rather than JSON, such
as the configuration backup.
:raises ConnectionClosedException: if called before :meth:`open`.
:raises RequestException: on HTTP-level errors.
"""
if self.session is None:
raise ConnectionClosedException("Not connected – call open() first.")
url = self.base_url.rstrip("/") + path
response = self.session.get(url, timeout=self.timeout)
response.raise_for_status()
return response.text
def _post(self, path: str, data: dict[str, Any] | None = None) -> dict[str, Any]:
"""Perform a POST request against the OPNsense REST API.
@@ -835,22 +851,19 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
the JSON-serialised staged routes when a candidate has been loaded,
or an empty string otherwise.
Calls ``GET /api/core/backup/download/this``.
Calls ``GET /api/core/backup/download/this``, which answers with
``config.xml`` itself rather than JSON. A failed read raises: an empty
``running`` would claim the device has no configuration and hide why
the read failed, e.g. an API key without the backup privilege.
"""
configs: dict[str, str] = {"running": "", "startup": "", "candidate": ""}
if retrieve in ("all", "running", "startup"):
try:
response = self._get("/api/core/backup/download/this")
xml_text: str = (
response if isinstance(response, str) else str(response)
)
if retrieve in ("all", "running"):
configs["running"] = xml_text
if retrieve in ("all", "startup"):
configs["startup"] = xml_text
except Exception as exc:
logger.warning("Failed to fetch running/startup config: %s", exc)
xml_text = self._get_text("/api/core/backup/download/this")
if retrieve in ("all", "running"):
configs["running"] = xml_text
if retrieve in ("all", "startup"):
configs["startup"] = xml_text
if retrieve in ("all", "candidate") and self._candidate_config is not None:
configs["candidate"] = json.dumps(self._candidate_config, indent=2)