Skip to content

Commit af28f4b

Browse files
committed
fix: Handle systemctl commands when dbus not ready (canonical#4681)
fix: Handle systemctl commands when dbus not ready During `cloud-init status`, we check systemctl to ensure the status we're reporting is accurate. However, we can get an error from systemctl if dbus isn't ready yet. This commit will either ignore the error if we can assume that cloud-init is still running, or retry until we get a proper response from systemctl. Fixes canonicalGH-4676
1 parent 6e04a2c commit af28f4b

2 files changed

Lines changed: 148 additions & 17 deletions

File tree

cloudinit/cmd/status.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def handle_status_args(name, args) -> int:
137137
"""Handle calls to 'cloud-init status' as a subcommand."""
138138
# Read configured paths
139139
paths = read_cfg_paths()
140-
details = get_status_details(paths)
140+
details = get_status_details(paths, args.wait)
141141
if args.wait:
142142
while details.status in (
143143
UXAppStatus.NOT_RUN,
@@ -147,7 +147,7 @@ def handle_status_args(name, args) -> int:
147147
if args.format == "tabular":
148148
sys.stdout.write(".")
149149
sys.stdout.flush()
150-
details = get_status_details(paths)
150+
details = get_status_details(paths, args.wait)
151151
sleep(0.25)
152152
details_dict: Dict[str, Union[None, str, List[str], Dict[str, Any]]] = {
153153
"datasource": details.datasource,
@@ -272,12 +272,15 @@ def get_bootstatus(disable_file, paths) -> Tuple[UXAppBootStatusCode, str]:
272272
return (bootstatus_code, reason)
273273

274274

275-
def _get_systemd_status() -> Optional[UXAppStatus]:
276-
"""Get status from systemd.
275+
def _get_error_or_running_from_systemd() -> Optional[UXAppStatus]:
276+
"""Get if systemd is in error or running state.
277277
278278
Using systemd, we can get more fine-grained status of the
279279
individual unit. Determine if we're still
280-
running or if there's an error we haven't otherwise detected
280+
running or if there's an error we haven't otherwise detected.
281+
282+
If we don't detect error or running, return None as we don't want to
283+
report any other particular status based on systemd.
281284
"""
282285
for service in [
283286
"cloud-final.service",
@@ -324,7 +327,42 @@ def _get_systemd_status() -> Optional[UXAppStatus]:
324327
return None
325328

326329

327-
def get_status_details(paths: Optional[Paths] = None) -> StatusDetails:
330+
def _get_error_or_running_from_systemd_with_retry(
331+
existing_status: UXAppStatus, *, wait: bool
332+
) -> Optional[UXAppStatus]:
333+
"""Get systemd status and retry if dbus isn't ready.
334+
335+
If cloud-init has determined that we're still running, then we can
336+
ignore the status from systemd. However, if cloud-init has detected error,
337+
then we should retry on systemd status so we don't incorrectly report
338+
error state while cloud-init is still running.
339+
"""
340+
while True:
341+
try:
342+
return _get_error_or_running_from_systemd()
343+
except subp.ProcessExecutionError as e:
344+
last_exception = e
345+
if existing_status in (
346+
UXAppStatus.DEGRADED_RUNNING,
347+
UXAppStatus.RUNNING,
348+
):
349+
return None
350+
if wait:
351+
sleep(0.25)
352+
else:
353+
break
354+
print(
355+
"Failed to get status from systemd. "
356+
"Cloud-init status may be inaccurate. ",
357+
f"Error from systemctl: {last_exception.stderr}",
358+
file=sys.stderr,
359+
)
360+
return None
361+
362+
363+
def get_status_details(
364+
paths: Optional[Paths] = None, wait: bool = False
365+
) -> StatusDetails:
328366
"""Return a dict with status, details and errors.
329367
330368
@param paths: An initialized cloudinit.helpers.paths object.
@@ -395,7 +433,9 @@ def get_status_details(paths: Optional[Paths] = None) -> StatusDetails:
395433
UXAppStatus.NOT_RUN,
396434
UXAppStatus.DISABLED,
397435
):
398-
systemd_status = _get_systemd_status()
436+
systemd_status = _get_error_or_running_from_systemd_with_retry(
437+
status, wait=wait
438+
)
399439
if systemd_status:
400440
status = systemd_status
401441

tests/unittests/cmd/test_status.py

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
import pytest
1111

12+
from cloudinit import subp
1213
from cloudinit.atomic_helper import write_json
1314
from cloudinit.cmd import status
14-
from cloudinit.cmd.status import UXAppStatus, _get_systemd_status
15+
from cloudinit.cmd.status import (
16+
UXAppStatus,
17+
_get_error_or_running_from_systemd,
18+
_get_error_or_running_from_systemd_with_retry,
19+
)
1520
from cloudinit.subp import SubpResult
1621
from cloudinit.util import ensure_file
1722
from tests.unittests.helpers import wrap_and_call
@@ -59,7 +64,10 @@ class TestStatus:
5964
"Cloud-init enabled by systemd cloud-init-generator",
6065
),
6166
)
62-
@mock.patch(f"{M_PATH}_get_systemd_status", return_value=None)
67+
@mock.patch(
68+
f"{M_PATH}_get_error_or_running_from_systemd_with_retry",
69+
return_value=None,
70+
)
6371
def test_get_status_details_ds_none(
6472
self,
6573
m_get_systemd_status,
@@ -704,7 +712,10 @@ def fakeexists(filepath):
704712
],
705713
)
706714
@mock.patch(M_PATH + "read_cfg_paths")
707-
@mock.patch(f"{M_PATH}_get_systemd_status", return_value=None)
715+
@mock.patch(
716+
f"{M_PATH}_get_error_or_running_from_systemd_with_retry",
717+
return_value=None,
718+
)
708719
def test_status_output(
709720
self,
710721
m_get_systemd_status,
@@ -745,7 +756,10 @@ def test_status_output(
745756
assert out == expected_status
746757

747758
@mock.patch(M_PATH + "read_cfg_paths")
748-
@mock.patch(f"{M_PATH}_get_systemd_status", return_value=None)
759+
@mock.patch(
760+
f"{M_PATH}_get_error_or_running_from_systemd_with_retry",
761+
return_value=None,
762+
)
749763
def test_status_wait_blocks_until_done(
750764
self, m_get_systemd_status, m_read_cfg_paths, config: Config, capsys
751765
):
@@ -796,7 +810,10 @@ def fake_sleep(interval):
796810
assert out == "....\nstatus: done\n"
797811

798812
@mock.patch(M_PATH + "read_cfg_paths")
799-
@mock.patch(f"{M_PATH}_get_systemd_status", return_value=None)
813+
@mock.patch(
814+
f"{M_PATH}_get_error_or_running_from_systemd_with_retry",
815+
return_value=None,
816+
)
800817
def test_status_wait_blocks_until_error(
801818
self, m_get_systemd_status, m_read_cfg_paths, config: Config, capsys
802819
):
@@ -849,7 +866,10 @@ def fake_sleep(interval):
849866
assert out == "....\nstatus: error\n"
850867

851868
@mock.patch(M_PATH + "read_cfg_paths")
852-
@mock.patch(f"{M_PATH}_get_systemd_status", return_value=None)
869+
@mock.patch(
870+
f"{M_PATH}_get_error_or_running_from_systemd_with_retry",
871+
return_value=None,
872+
)
853873
def test_status_main(
854874
self, m_get_systemd_status, m_read_cfg_paths, config: Config, capsys
855875
):
@@ -873,15 +893,20 @@ def test_status_main(
873893
assert out == "status: running\n"
874894

875895

876-
class TestSystemdStatusDetails:
896+
class TestGetErrorOrRunningFromSystemd:
897+
@pytest.fixture(autouse=True)
898+
def common_mocks(self, mocker):
899+
mocker.patch("cloudinit.cmd.status.sleep")
900+
yield
901+
877902
@pytest.mark.parametrize(
878903
["active_state", "unit_file_state", "sub_state", "main_pid", "status"],
879904
[
880905
# To cut down on the combination of states, I'm grouping
881906
# enabled, enabled-runtime, and static into an "enabled" state
882907
# and everything else functionally disabled.
883908
# Additionally, SubStates are undocumented and may mean something
884-
# different depending on the ActiveState they are mapped too.
909+
# different depending on the ActiveState they are mapped to.
885910
# Because of this I'm only testing SubState combinations seen
886911
# in real-world testing (or using "any" string if we dont care).
887912
("activating", "enabled", "start", "123", UXAppStatus.RUNNING),
@@ -910,7 +935,7 @@ class TestSystemdStatusDetails:
910935
("failed", "invalid", "failed", "0", UXAppStatus.ERROR),
911936
],
912937
)
913-
def test_get_systemd_status(
938+
def test_get_error_or_running_from_systemd(
914939
self, active_state, unit_file_state, sub_state, main_pid, status
915940
):
916941
with mock.patch(
@@ -923,4 +948,70 @@ def test_get_systemd_status(
923948
stderr=None,
924949
),
925950
):
926-
assert _get_systemd_status() == status
951+
assert _get_error_or_running_from_systemd() == status
952+
953+
def test_exception_while_running(self, mocker, capsys):
954+
m_subp = mocker.patch(
955+
f"{M_PATH}subp.subp",
956+
side_effect=subp.ProcessExecutionError(
957+
"Message recipient disconnected from message bus without"
958+
" replying"
959+
),
960+
)
961+
assert (
962+
_get_error_or_running_from_systemd_with_retry(
963+
UXAppStatus.RUNNING, wait=True
964+
)
965+
is None
966+
)
967+
assert 1 == m_subp.call_count
968+
assert "Failed to get status" not in capsys.readouterr().err
969+
970+
def test_retry(self, mocker, capsys):
971+
m_subp = mocker.patch(
972+
f"{M_PATH}subp.subp",
973+
side_effect=[
974+
subp.ProcessExecutionError(
975+
"Message recipient disconnected from message bus without"
976+
" replying"
977+
),
978+
subp.ProcessExecutionError(
979+
"Message recipient disconnected from message bus without"
980+
" replying"
981+
),
982+
SubpResult(
983+
"ActiveState=activating\nUnitFileState=enabled\n"
984+
"SubState=start\nMainPID=123\n",
985+
stderr=None,
986+
),
987+
],
988+
)
989+
assert (
990+
_get_error_or_running_from_systemd_with_retry(
991+
UXAppStatus.ERROR, wait=True
992+
)
993+
is UXAppStatus.RUNNING
994+
)
995+
assert 3 == m_subp.call_count
996+
assert "Failed to get status" not in capsys.readouterr().err
997+
998+
def test_retry_no_wait(self, mocker, capsys):
999+
m_subp = mocker.patch(
1000+
f"{M_PATH}subp.subp",
1001+
side_effect=subp.ProcessExecutionError(
1002+
"Message recipient disconnected from message bus without"
1003+
" replying"
1004+
),
1005+
)
1006+
mocker.patch("time.time", side_effect=[1, 2, 50])
1007+
assert (
1008+
_get_error_or_running_from_systemd_with_retry(
1009+
UXAppStatus.ERROR, wait=False
1010+
)
1011+
is None
1012+
)
1013+
assert 1 == m_subp.call_count
1014+
assert (
1015+
"Failed to get status from systemd. "
1016+
"Cloud-init status may be inaccurate."
1017+
) in capsys.readouterr().err

0 commit comments

Comments
 (0)