diff --git a/napalm_opnsense/opnsense.py b/napalm_opnsense/opnsense.py index c3276cc..9213903 100644 --- a/napalm_opnsense/opnsense.py +++ b/napalm_opnsense/opnsense.py @@ -2050,6 +2050,16 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver): "output": f"The update check did not finish within {_REFRESH_POLLS * _REFRESH_INTERVAL} s", } + def reboot_host(self) -> None: + """Restart the firewall through its firmware API (``HostRebootMixin``). + + :raises RuntimeError: when the API refuses the request. + """ + try: + self._post("/api/core/firmware/reboot") + except Exception as exc: + raise RuntimeError(f"The firewall refused to reboot: {exc}") from exc + def get_host_status(self) -> dict[str, Any]: """Whether the firewall needs a reboot to finish an update; it does not patch itself as far as netOrk can tell.""" diff --git a/tests/unit/test_updates.py b/tests/unit/test_updates.py index 9d82732..4f04b5a 100644 --- a/tests/unit/test_updates.py +++ b/tests/unit/test_updates.py @@ -123,3 +123,18 @@ class TestHostStatus: def test_no_pending_reboot(self, driver): with patch.object(driver, "_get", return_value=_status()): assert driver.get_host_status()["reboot_required"] is False + + +class TestRebootHost: + """``reboot_host`` restarts the firewall through its firmware API (netOrk #637).""" + + def test_the_firmware_api_is_asked(self, driver): + with patch.object(driver, "_post", return_value={"status": "ok"}) as post: + driver.reboot_host() + + post.assert_called_once_with("/api/core/firmware/reboot") + + def test_a_refusal_is_raised(self, driver): + with patch.object(driver, "_post", side_effect=ConnectionError("403 Forbidden")): + with pytest.raises(RuntimeError, match="403"): + driver.reboot_host()