feat: restart the host (reboot_host) #13
@@ -120,6 +120,11 @@ _REFRESH = {
|
|||||||
}
|
}
|
||||||
_YUM_SECURITY_COMMAND = "LC_ALL=C yum updateinfo list security -q 2>/dev/null"
|
_YUM_SECURITY_COMMAND = "LC_ALL=C yum updateinfo list security -q 2>/dev/null"
|
||||||
|
|
||||||
|
#: Restart the host two seconds later, detached from this session: the launcher's
|
||||||
|
#: exit status comes back before the host goes down, and closing the session
|
||||||
|
#: cannot take the restart with it.
|
||||||
|
_REBOOT_DETACHED = "sh -c '(trap \"\" HUP; sleep 2; /sbin/reboot) </dev/null >/dev/null 2>&1 &'"
|
||||||
|
|
||||||
#: What to do when sudo wants a password netOrk does not have.
|
#: What to do when sudo wants a password netOrk does not have.
|
||||||
_SUDO_PASSWORD_HINT = (
|
_SUDO_PASSWORD_HINT = (
|
||||||
"sudo requires a password on this device but none is configured in netOrk. "
|
"sudo requires a password on this device but none is configured in netOrk. "
|
||||||
@@ -389,6 +394,21 @@ class LinuxDriver(KernelFactsMixin, SystemdServicesMixin, HostStatusMixin, OSDri
|
|||||||
return self._sudo(command, read_timeout=timeout)
|
return self._sudo(command, read_timeout=timeout)
|
||||||
return self._send(f"sudo -n {command}", read_timeout=timeout)
|
return self._send(f"sudo -n {command}", read_timeout=timeout)
|
||||||
|
|
||||||
|
def reboot_host(self) -> None:
|
||||||
|
"""Restart the host (``HostRebootMixin``); returns once the restart is under way.
|
||||||
|
|
||||||
|
:raises RuntimeError: when the host refuses -- sudo without a password,
|
||||||
|
no ``reboot`` -- or its answer carried no exit status.
|
||||||
|
"""
|
||||||
|
output, status = _split_status(
|
||||||
|
self._run_privileged(f"{_REBOOT_DETACHED}; echo {_RC_MARKER}$?", 30)
|
||||||
|
)
|
||||||
|
if status != 0:
|
||||||
|
reason = output or f"the reboot command exited with status {status}"
|
||||||
|
if "password is required" in output:
|
||||||
|
reason = f"{reason}\n{_SUDO_PASSWORD_HINT}"
|
||||||
|
raise RuntimeError(reason)
|
||||||
|
|
||||||
def _run_host_status_command(self, command: str) -> str:
|
def _run_host_status_command(self, command: str) -> str:
|
||||||
"""The transport for ``HostStatusMixin.get_host_status``: read-only, no sudo."""
|
"""The transport for ``HostStatusMixin.get_host_status``: read-only, no sudo."""
|
||||||
return self._send(command, read_timeout=60)
|
return self._send(command, read_timeout=60)
|
||||||
|
|||||||
@@ -1517,3 +1517,41 @@ class TestTerminalCodes:
|
|||||||
driver._device.send_command.side_effect = ["0", "Hit:1 noble InRelease\n\x1b>__NETORK_RC=0"]
|
driver._device.send_command.side_effect = ["0", "Hit:1 noble InRelease\n\x1b>__NETORK_RC=0"]
|
||||||
|
|
||||||
assert driver.refresh_available_updates()["success"] is True
|
assert driver.refresh_available_updates()["success"] is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestRebootHost:
|
||||||
|
"""``reboot_host`` (napalm-device-types' ``HostRebootMixin``) restarts the host.
|
||||||
|
|
||||||
|
Without it netOrk could not restart a Linux host at all: its capability check
|
||||||
|
looks for ``reboot_host`` and found nothing (netOrk #637). The restart is
|
||||||
|
detached and a moment late, so the launcher's exit status comes back before
|
||||||
|
the host goes down, and closing the session cannot take it along.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_the_driver_can_restart_its_host(self):
|
||||||
|
assert callable(getattr(LinuxDriver, "reboot_host", None))
|
||||||
|
|
||||||
|
def test_the_restart_is_detached_and_privileged(self, driver):
|
||||||
|
driver._root = False
|
||||||
|
driver._device.send_command.return_value = "\n__NETORK_RC=0"
|
||||||
|
|
||||||
|
driver.reboot_host()
|
||||||
|
|
||||||
|
sent = driver._device.send_command.call_args[0][0]
|
||||||
|
assert sent.startswith("sudo -n sh -c ")
|
||||||
|
assert "/sbin/reboot" in sent and "trap" in sent and "&" in sent
|
||||||
|
assert sent.endswith("echo __NETORK_RC=$?")
|
||||||
|
|
||||||
|
def test_a_refusal_is_raised_with_what_the_host_said(self, driver):
|
||||||
|
driver._root = False
|
||||||
|
driver._device.send_command.return_value = "sudo: a password is required\n__NETORK_RC=1"
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="password is required"):
|
||||||
|
driver.reboot_host()
|
||||||
|
|
||||||
|
def test_no_exit_status_is_no_success(self, driver):
|
||||||
|
driver._root = True
|
||||||
|
driver._device.send_command.return_value = "something else"
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
driver.reboot_host()
|
||||||
|
|||||||
Reference in New Issue
Block a user