diff --git a/docs/api.clients.servers.rst b/docs/api.clients.servers.rst index 58362561..9cb9e53d 100644 --- a/docs/api.clients.servers.rst +++ b/docs/api.clients.servers.rst @@ -2,6 +2,38 @@ ServersClient ================== +Waiting for a graceful shutdown +------------------------------- + +The shutdown action sends an ACPI request to the guest operating system. +:meth:`~hcloud.actions.client.BoundAction.wait_until_finished` waits for that +request to be sent, rather than for the guest to stop. Before changing the +server type or performing another operation that requires a stopped server, +reload its status until it is :attr:`~hcloud.servers.domain.Server.STATUS_OFF`. + +For an existing :class:`~hcloud.servers.client.BoundServer` named ``server``, +the following example checks the status at most 60 times. It raises a timeout +instead of continuing if the guest does not shut down:: + + from time import sleep + + from hcloud.servers.domain import Server + + server.shutdown().wait_until_finished() + for attempt in range(60): + server.reload() + if server.status == Server.STATUS_OFF: + break + if attempt < 59: + sleep(1) + else: + raise TimeoutError("Server did not shut down after 60 status checks") + +A guest may ignore the request, for example if it does not handle ACPI. +Choose the polling interval and limit to suit the workload. A forced +:meth:`~hcloud.servers.client.BoundServer.power_off` cuts power without a clean +guest shutdown; this example does not fall back to it automatically. + .. autoclass:: hcloud.servers.client.ServersClient :members: diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index ce5a72c0..bcf32a3f 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -279,6 +279,10 @@ def power_on(self) -> BoundAction: def reboot(self) -> BoundAction: """Reboots a server gracefully by sending an ACPI request. + The returned action finishes when the request has been sent. Waiting + for it does not guarantee that the guest operating system has rebooted + or that its services are ready. The guest may ignore ACPI requests. + :return: :class:`BoundAction ` """ return self._client.reboot(self) @@ -293,6 +297,12 @@ def reset(self) -> BoundAction: def shutdown(self) -> BoundAction: """Shuts down a server gracefully by sending an ACPI shutdown request. + The returned action finishes when the request has been sent, not when + the guest operating system has stopped. After waiting for the action, + reload the server and poll its status until it is ``off`` before an + operation that requires a stopped server. The guest may ignore ACPI, + so polling should have a timeout. + :return: :class:`BoundAction ` """ return self._client.shutdown(self) @@ -861,6 +871,10 @@ def power_on(self, server: Server | BoundServer) -> BoundAction: def reboot(self, server: Server | BoundServer) -> BoundAction: """Reboots a server gracefully by sending an ACPI request. + The returned action finishes when the request has been sent. Waiting + for it does not guarantee that the guest operating system has rebooted + or that its services are ready. The guest may ignore ACPI requests. + :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ @@ -885,6 +899,12 @@ def reset(self, server: Server | BoundServer) -> BoundAction: def shutdown(self, server: Server | BoundServer) -> BoundAction: """Shuts down a server gracefully by sending an ACPI shutdown request. + The returned action finishes when the request has been sent, not when + the guest operating system has stopped. After waiting for the action, + reload the server and poll its status until it is ``off`` before an + operation that requires a stopped server. The guest may ignore ACPI, + so polling should have a timeout. + :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """