feat(dyndns): add get_ddns_status() for os-ddclient enabled/running state
Verified against a live OPNsense 24.7 instance: the service id is "ddclient" but the REST module is "dyndns" (/api/ddclient/* all 404). Scoped to enabled/running only -- no ddclient/dyndns account was configured on the test device to verify a per-account "registered IP" shape against, so that comparison is deliberately left out rather than guessed.
This commit is contained in:
@@ -1091,6 +1091,33 @@ class OPNsenseDriver(FirewallDriver):
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_ddns_status(self) -> dict[str, Any] | None:
|
||||||
|
"""Return Dynamic DNS (os-ddclient) enabled/running state.
|
||||||
|
|
||||||
|
Calls ``GET /api/dyndns/service/status`` and
|
||||||
|
``GET /api/dyndns/settings/get`` -- note the API module is
|
||||||
|
"dyndns", not "ddclient" (the service id reported by
|
||||||
|
get_services() is "ddclient", but its REST controller lives under
|
||||||
|
a different, historical module name).
|
||||||
|
|
||||||
|
Returns ``{"enabled": bool, "running": bool}``, or ``None`` if the
|
||||||
|
plugin isn't installed/reachable. Scoped to enabled/running only:
|
||||||
|
no per-account "registered IP" comparison, since that would need a
|
||||||
|
configured account to verify the response shape against.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
status = self._get("/api/dyndns/service/status")
|
||||||
|
settings = self._get("/api/dyndns/settings/get")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("get_ddns_status() failed: %s", exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
general = settings.get("ddclient", {}).get("general", {})
|
||||||
|
return {
|
||||||
|
"enabled": general.get("enabled") == "1",
|
||||||
|
"running": status.get("status") == "running",
|
||||||
|
}
|
||||||
|
|
||||||
def get_dhcp_leases(self) -> list[dict[str, Any]]:
|
def get_dhcp_leases(self) -> list[dict[str, Any]]:
|
||||||
"""Return active DHCP leases from OPNsense.
|
"""Return active DHCP leases from OPNsense.
|
||||||
|
|
||||||
|
|||||||
@@ -1060,6 +1060,66 @@ class TestGetCertificates:
|
|||||||
driver._post = lambda path, data=None: (_ for _ in ()).throw(Exception("404"))
|
driver._post = lambda path, data=None: (_ for _ in ()).throw(Exception("404"))
|
||||||
assert driver.get_certificates() == []
|
assert driver.get_certificates() == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# get_ddns_status()
|
||||||
|
#
|
||||||
|
# Endpoints verified 2026-07-15 against a live OPNsense 24.7 instance: the
|
||||||
|
# service id is "ddclient" (get_services()) but the API module is "dyndns",
|
||||||
|
# not "ddclient" -- /api/ddclient/* all 404, /api/dyndns/service/status and
|
||||||
|
# /api/dyndns/settings/get are the real paths. Scoped to enabled/running
|
||||||
|
# only: no live ddclient/dyndns account was configured on the test device
|
||||||
|
# to verify a per-account "registered IP" shape against, so that comparison
|
||||||
|
# is deliberately not implemented here (would be guesswork).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
DYNDNS_STATUS_RESPONSE = {"status": "running"}
|
||||||
|
DYNDNS_SETTINGS_ENABLED = {"ddclient": {"general": {"enabled": "1"}}}
|
||||||
|
DYNDNS_SETTINGS_DISABLED = {"ddclient": {"general": {"enabled": "0"}}}
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDdnsStatus:
|
||||||
|
def _fake_get(self, status_response, settings_response):
|
||||||
|
def fake_get(path):
|
||||||
|
if "service/status" in path:
|
||||||
|
return status_response
|
||||||
|
return settings_response
|
||||||
|
|
||||||
|
return fake_get
|
||||||
|
|
||||||
|
def test_calls_expected_endpoints(self, driver):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_get(path):
|
||||||
|
calls.append(path)
|
||||||
|
if "service/status" in path:
|
||||||
|
return DYNDNS_STATUS_RESPONSE
|
||||||
|
return DYNDNS_SETTINGS_ENABLED
|
||||||
|
|
||||||
|
driver._get = fake_get
|
||||||
|
driver.get_ddns_status()
|
||||||
|
assert calls == ["/api/dyndns/service/status", "/api/dyndns/settings/get"]
|
||||||
|
|
||||||
|
def test_enabled_and_running(self, driver):
|
||||||
|
driver._get = self._fake_get(DYNDNS_STATUS_RESPONSE, DYNDNS_SETTINGS_ENABLED)
|
||||||
|
result = driver.get_ddns_status()
|
||||||
|
assert result == {"enabled": True, "running": True}
|
||||||
|
|
||||||
|
def test_enabled_but_not_running(self, driver):
|
||||||
|
driver._get = self._fake_get({"status": "stopped"}, DYNDNS_SETTINGS_ENABLED)
|
||||||
|
result = driver.get_ddns_status()
|
||||||
|
assert result == {"enabled": True, "running": False}
|
||||||
|
|
||||||
|
def test_disabled(self, driver):
|
||||||
|
driver._get = self._fake_get(DYNDNS_STATUS_RESPONSE, DYNDNS_SETTINGS_DISABLED)
|
||||||
|
result = driver.get_ddns_status()
|
||||||
|
assert result == {"enabled": False, "running": True}
|
||||||
|
|
||||||
|
def test_api_error_returns_none(self, driver):
|
||||||
|
driver._get = lambda path: (_ for _ in ()).throw(Exception("404"))
|
||||||
|
assert driver.get_ddns_status() is None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _post()
|
# _post()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user