Browse Source

retain msgs on systemctl/hostname/preparing-for-shutdown

https://github.com/fphammerle/systemctl-mqtt/pull/6/files
Fabian Peter Hammerle 3 weeks ago
parent
commit
f348d613ce
4 changed files with 93 additions and 9 deletions
  1. 6 0
      docker-apparmor-profile
  2. 31 3
      systemctl_mqtt/__init__.py
  3. 12 5
      tests/test_mqtt.py
  4. 44 1
      tests/test_state_dbus.py

+ 6 - 0
docker-apparmor-profile

@@ -58,4 +58,10 @@ profile systemctl-mqtt flags=(attach_disconnected) {
        interface=org.freedesktop.login1.Manager
        member=PrepareForShutdown
        peer=(label=unconfined),
+  dbus (send)
+       bus=system
+       path=/org/freedesktop/login1
+       interface=org.freedesktop.DBus.Properties
+       member=Get
+       peer=(label=unconfined),
 }

+ 31 - 3
systemctl_mqtt/__init__.py

@@ -143,15 +143,17 @@ class _State:
                 self._shutdown_lock = None
 
     def _publish_preparing_for_shutdown(
-        self, mqtt_client: paho.mqtt.client.Client, active: bool
+        self, mqtt_client: paho.mqtt.client.Client, active: bool, block: bool,
     ) -> None:
         # https://github.com/eclipse/paho.mqtt.python/blob/v1.5.0/src/paho/mqtt/client.py#L1199
         topic = self.mqtt_topic_prefix + "/preparing-for-shutdown"
         payload = json.dumps(active)
         _LOGGER.info("publishing %r on %s", payload, topic)
         msg_info = mqtt_client.publish(
-            topic=topic, payload=payload,
+            topic=topic, payload=payload, retain=True,
         )  # type: paho.mqtt.client.MQTTMessageInfo
+        if not block:
+            return
         msg_info.wait_for_publish()
         if msg_info.rc != paho.mqtt.client.MQTT_ERR_SUCCESS:
             _LOGGER.error(
@@ -163,7 +165,9 @@ class _State:
     ) -> None:
         assert isinstance(active, dbus.Boolean)
         active = bool(active)
-        self._publish_preparing_for_shutdown(mqtt_client=mqtt_client, active=active)
+        self._publish_preparing_for_shutdown(
+            mqtt_client=mqtt_client, active=active, block=True,
+        )
         if active:
             self.release_shutdown_lock()
         else:
@@ -179,6 +183,29 @@ class _State:
             ),
         )
 
+    def publish_preparing_for_shutdown(
+        self, mqtt_client: paho.mqtt.client.Client,
+    ) -> None:
+        try:
+            active = self._login_manager.Get(
+                "org.freedesktop.login1.Manager",
+                "PreparingForShutdown",
+                dbus_interface="org.freedesktop.DBus.Properties",
+            )
+        except dbus.DBusException as exc:
+            _LOGGER.error(
+                "failed to read logind's PreparingForShutdown property: %s",
+                exc.get_dbus_message(),
+            )
+            return
+        assert isinstance(active, dbus.Boolean), active
+        self._publish_preparing_for_shutdown(
+            mqtt_client=mqtt_client,
+            active=bool(active),
+            # https://github.com/eclipse/paho.mqtt.python/issues/439#issuecomment-565514393
+            block=False,
+        )
+
 
 class _MQTTAction:
 
@@ -226,6 +253,7 @@ def _mqtt_on_connect(
     _LOGGER.debug("connected to MQTT broker %s:%d", mqtt_broker_host, mqtt_broker_port)
     state.acquire_shutdown_lock()
     state.register_prepare_for_shutdown_handler(mqtt_client=mqtt_client)
+    state.publish_preparing_for_shutdown(mqtt_client=mqtt_client)
     for topic_suffix, action in _MQTT_TOPIC_SUFFIX_ACTION_MAPPING.items():
         topic = state.mqtt_topic_prefix + "/" + topic_suffix
         _LOGGER.info("subscribing to %s", topic)

+ 12 - 5
tests/test_mqtt.py

@@ -20,6 +20,7 @@ import threading
 import time
 import unittest.mock
 
+import dbus
 import paho.mqtt.client
 import pytest
 from paho.mqtt.client import MQTTMessage
@@ -44,8 +45,9 @@ def test__run(caplog, mqtt_host, mqtt_port, mqtt_topic_prefix):
         "gi.repository.GLib.MainLoop.run"
     ) as glib_loop_mock, unittest.mock.patch(
         "systemctl_mqtt._get_login_manager"
-    ):
+    ) as get_login_manager_mock:
         ssl_wrap_socket_mock.return_value.send = len
+        get_login_manager_mock.return_value.Get.return_value = dbus.Boolean(False)
         systemctl_mqtt._run(
             mqtt_host=mqtt_host,
             mqtt_port=mqtt_port,
@@ -102,11 +104,15 @@ def test__run(caplog, mqtt_host, mqtt_port, mqtt_topic_prefix):
     assert caplog.records[1].levelno == logging.DEBUG
     assert caplog.records[1].message == "acquired shutdown inhibitor lock"
     assert caplog.records[2].levelno == logging.INFO
-    assert caplog.records[2].message == "subscribing to {}".format(
+    assert caplog.records[2].message == "publishing 'false' on {}".format(
+        mqtt_topic_prefix + "/preparing-for-shutdown"
+    )
+    assert caplog.records[3].levelno == logging.INFO
+    assert caplog.records[3].message == "subscribing to {}".format(
         mqtt_topic_prefix + "/poweroff"
     )
-    assert caplog.records[3].levelno == logging.DEBUG
-    assert caplog.records[3].message == "registered MQTT callback for topic {}".format(
+    assert caplog.records[4].levelno == logging.DEBUG
+    assert caplog.records[4].message == "registered MQTT callback for topic {}".format(
         mqtt_topic_prefix + "/poweroff"
     ) + " triggering {}".format(
         systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING["poweroff"].action
@@ -163,8 +169,9 @@ def _initialize_mqtt_client(
         "gi.repository.GLib.MainLoop.run"
     ), unittest.mock.patch(
         "systemctl_mqtt._get_login_manager"
-    ):
+    ) as get_login_manager_mock:
         ssl_wrap_socket_mock.return_value.send = len
+        get_login_manager_mock.return_value.Get.return_value = dbus.Boolean(False)
         systemctl_mqtt._run(
             mqtt_host=mqtt_host,
             mqtt_port=mqtt_port,

+ 44 - 1
tests/test_state_dbus.py

@@ -67,10 +67,53 @@ def test_prepare_for_shutdown_handler(caplog, active):
         acquire_lock_mock.assert_called_once_with()
         release_lock_mock.assert_not_called()
     mqtt_client_mock.publish.assert_called_once_with(
-        topic="any/preparing-for-shutdown", payload="true" if active else "false",
+        topic="any/preparing-for-shutdown",
+        payload="true" if active else "false",
+        retain=True,
     )
     assert len(caplog.records) == 1
     assert caplog.records[0].levelno == logging.ERROR
     assert caplog.records[0].message.startswith(
         "failed to publish on any/preparing-for-shutdown"
     )
+
+
+@pytest.mark.parametrize("active", [True, False])
+def test_publish_preparing_for_shutdown(active):
+    login_manager_mock = unittest.mock.MagicMock()
+    login_manager_mock.Get.return_value = dbus.Boolean(active)
+    with unittest.mock.patch(
+        "systemctl_mqtt._get_login_manager", return_value=login_manager_mock
+    ):
+        state = systemctl_mqtt._State(mqtt_topic_prefix="any")
+    assert state._login_manager == login_manager_mock
+    mqtt_client_mock = unittest.mock.MagicMock()
+    state.publish_preparing_for_shutdown(mqtt_client=mqtt_client_mock)
+    login_manager_mock.Get.assert_called_once_with(
+        "org.freedesktop.login1.Manager",
+        "PreparingForShutdown",
+        dbus_interface="org.freedesktop.DBus.Properties",
+    )
+    mqtt_client_mock.publish.assert_called_once_with(
+        topic="any/preparing-for-shutdown",
+        payload="true" if active else "false",
+        retain=True,
+    )
+
+
+def test_publish_preparing_for_shutdown_get_fail(caplog):
+    login_manager_mock = unittest.mock.MagicMock()
+    login_manager_mock.Get.side_effect = dbus.DBusException("mocked")
+    with unittest.mock.patch(
+        "systemctl_mqtt._get_login_manager", return_value=login_manager_mock
+    ):
+        state = systemctl_mqtt._State(mqtt_topic_prefix="any")
+    mqtt_client_mock = unittest.mock.MagicMock()
+    state.publish_preparing_for_shutdown(mqtt_client=None)
+    mqtt_client_mock.publish.assert_not_called()
+    assert len(caplog.records) == 1
+    assert caplog.records[0].levelno == logging.ERROR
+    assert (
+        caplog.records[0].message
+        == "failed to read logind's PreparingForShutdown property: mocked"
+    )