Browse Source

instruct sessions to lock on systemctl/hostname/lock-all-sessions

Fabian Peter Hammerle 3 years ago
parent
commit
c790a93981
10 changed files with 95 additions and 38 deletions
  1. 1 0
      .gitignore
  2. 3 0
      CHANGELOG.md
  3. 6 0
      README.md
  4. 1 1
      docker-apparmor-profile
  5. 1 0
      setup.py
  6. 21 9
      systemctl_mqtt/__init__.py
  7. 9 1
      systemctl_mqtt/_dbus.py
  8. 34 0
      tests/test_action.py
  9. 8 20
      tests/test_dbus.py
  10. 11 7
      tests/test_mqtt.py

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 .coverage
+.git/index
 .mypy_cache/
 build/
 dist/

+ 3 - 0
CHANGELOG.md

@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### Added
+- MQTT message on topic `systemctl/hostname/lock-all-sessions`
+  instructs all sessions to activate screen locks
+  (functionally equivalent to command `loginctl lock-sessions`)
 - command line option `--poweroff-delay-seconds` (default: 4 seconds)
 
 ### Changed

+ 6 - 0
README.md

@@ -27,6 +27,12 @@ Schedule poweroff by sending a MQTT message to topic `systemctl/hostname/powerof
 $ mosquitto_pub -h MQTT_BROKER -t systemctl/hostname/poweroff -n
 ```
 
+Lock screen by sending a MQTT message to topic `systemctl/hostname/lock-all-sessions`.
+
+```
+$ mosquitto_pub -h MQTT_BROKER -t systemctl/hostname/lock-all-sessions -n
+```
+
 ### Shutdown Report
 
 `systemctl-mqtt` subscribes to [logind](https://freedesktop.org/wiki/Software/systemd/logind/)'s `PrepareForShutdown` signal.

+ 1 - 1
docker-apparmor-profile

@@ -50,7 +50,7 @@ profile systemctl-mqtt flags=(attach_disconnected) {
        bus=system
        path=/org/freedesktop/login1
        interface=org.freedesktop.login1.Manager
-       member={Inhibit,ListInhibitors,ScheduleShutdown}
+       member={Inhibit,ListInhibitors,ScheduleShutdown,LockSessions}
        peer=(label=unconfined),
   dbus (receive)
        bus=system

+ 1 - 0
setup.py

@@ -42,6 +42,7 @@ setuptools.setup(
         "automation",
         "home-assistant",
         "home-automation",
+        "lock",
         "mqtt",
         "shutdown",
         "systemd",

+ 21 - 9
systemctl_mqtt/__init__.py

@@ -76,7 +76,7 @@ class _State:
             assert self._shutdown_lock is None
             # https://www.freedesktop.org/wiki/Software/systemd/inhibit/
             self._shutdown_lock = self._login_manager.Inhibit(
-                "shutdown", "systemctl-mqtt", "Report shutdown via MQTT", "delay",
+                "shutdown", "systemctl-mqtt", "Report shutdown via MQTT", "delay"
             )
             _LOGGER.debug("acquired shutdown inhibitor lock")
 
@@ -93,7 +93,7 @@ class _State:
         return self.mqtt_topic_prefix + "/preparing-for-shutdown"
 
     def _publish_preparing_for_shutdown(
-        self, mqtt_client: paho.mqtt.client.Client, active: bool, block: 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._preparing_for_shutdown_topic
@@ -101,7 +101,7 @@ class _State:
         payload = systemctl_mqtt._mqtt.encode_bool(active)
         _LOGGER.info("publishing %r on %s", payload, topic)
         msg_info = mqtt_client.publish(
-            topic=topic, payload=payload, retain=True,
+            topic=topic, payload=payload, retain=True
         )  # type: paho.mqtt.client.MQTTMessageInfo
         if not block:
             return
@@ -117,7 +117,7 @@ class _State:
         assert isinstance(active, dbus.Boolean)
         active = bool(active)
         self._publish_preparing_for_shutdown(
-            mqtt_client=mqtt_client, active=active, block=True,
+            mqtt_client=mqtt_client, active=active, block=True
         )
         if active:
             self.release_shutdown_lock()
@@ -135,7 +135,7 @@ class _State:
         )
 
     def publish_preparing_for_shutdown(
-        self, mqtt_client: paho.mqtt.client.Client,
+        self, mqtt_client: paho.mqtt.client.Client
     ) -> None:
         try:
             active = self._login_manager.Get(
@@ -191,7 +191,7 @@ class _State:
         }
         _LOGGER.debug("publishing home assistant config on %s", discovery_topic)
         mqtt_client.publish(
-            topic=discovery_topic, payload=json.dumps(config), retain=True,
+            topic=discovery_topic, payload=json.dumps(config), retain=True
         )
 
 
@@ -229,7 +229,19 @@ class _MQTTActionSchedulePoweroff(_MQTTAction):
         return type(self).__name__
 
 
-_MQTT_TOPIC_SUFFIX_ACTION_MAPPING = {"poweroff": _MQTTActionSchedulePoweroff()}
+class _MQTTActionLockAllSessions(_MQTTAction):
+    def trigger(self, state: _State) -> None:
+        # pylint: disable=protected-access
+        systemctl_mqtt._dbus.lock_all_sessions()
+
+    def __str__(self) -> str:
+        return type(self).__name__
+
+
+_MQTT_TOPIC_SUFFIX_ACTION_MAPPING = {
+    "poweroff": _MQTTActionSchedulePoweroff(),
+    "lock-all-sessions": _MQTTActionLockAllSessions(),
+}
 
 
 def _mqtt_on_connect(
@@ -349,7 +361,7 @@ def _main() -> None:
     )
     # https://www.home-assistant.io/docs/mqtt/discovery/#discovery_prefix
     argparser.add_argument(
-        "--homeassistant-discovery-prefix", type=str, default="homeassistant", help=" ",
+        "--homeassistant-discovery-prefix", type=str, default="homeassistant", help=" "
     )
     argparser.add_argument(
         "--homeassistant-node-id",
@@ -359,7 +371,7 @@ def _main() -> None:
         help=" ",
     )
     argparser.add_argument(
-        "--poweroff-delay-seconds", type=float, default=4.0, help=" ",
+        "--poweroff-delay-seconds", type=float, default=4.0, help=" "
     )
     args = argparser.parse_args()
     if args.mqtt_port:

+ 9 - 1
systemctl_mqtt/_dbus.py

@@ -66,7 +66,7 @@ def schedule_shutdown(action: str, delay: datetime.timedelta) -> None:
     # datetime.datetime.isoformat(timespec=) not available in python3.5
     # https://github.com/python/cpython/blob/v3.5.9/Lib/datetime.py#L1552
     _LOGGER.info(
-        "scheduling %s for %s", action, shutdown_datetime.strftime("%Y-%m-%d %H:%M:%S"),
+        "scheduling %s for %s", action, shutdown_datetime.strftime("%Y-%m-%d %H:%M:%S")
     )
     # https://dbus.freedesktop.org/doc/dbus-python/tutorial.html?highlight=signature#basic-types
     shutdown_epoch_usec = dbus.UInt64(shutdown_datetime.timestamp() * 10 ** 6)
@@ -95,3 +95,11 @@ def schedule_shutdown(action: str, delay: datetime.timedelta) -> None:
         else:
             _LOGGER.error("failed to schedule %s: %s", action, exc_msg)
     _log_shutdown_inhibitors(login_manager)
+
+
+def lock_all_sessions() -> None:
+    """
+    $ loginctl lock-sessions
+    """
+    _LOGGER.info("instruct all sessions to activate screen locks")
+    get_login_manager().LockSessions()

+ 34 - 0
tests/test_action.py

@@ -25,3 +25,37 @@ def test_poweroff_trigger(delay):
             )
         )
     schedule_shutdown_mock.assert_called_once_with(action="poweroff", delay=delay)
+
+
+@pytest.mark.parametrize(
+    ("topic_suffix", "expected_action_arg"), [("poweroff", "poweroff")]
+)
+def test_mqtt_topic_suffix_action_mapping_poweroff(topic_suffix, expected_action_arg):
+    mqtt_action = systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING[topic_suffix]
+    login_manager_mock = unittest.mock.MagicMock()
+    with unittest.mock.patch(
+        "systemctl_mqtt._dbus.get_login_manager", return_value=login_manager_mock
+    ):
+        mqtt_action.trigger(
+            state=systemctl_mqtt._State(
+                mqtt_topic_prefix="systemctl/hostname",
+                homeassistant_discovery_prefix="homeassistant",
+                homeassistant_node_id="node",
+                poweroff_delay=datetime.timedelta(),
+            )
+        )
+    assert login_manager_mock.ScheduleShutdown.call_count == 1
+    schedule_args, schedule_kwargs = login_manager_mock.ScheduleShutdown.call_args
+    assert len(schedule_args) == 2
+    assert schedule_args[0] == expected_action_arg
+    assert not schedule_kwargs
+
+
+def test_mqtt_topic_suffix_action_mapping_lock():
+    mqtt_action = systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING["lock-all-sessions"]
+    login_manager_mock = unittest.mock.MagicMock()
+    with unittest.mock.patch(
+        "systemctl_mqtt._dbus.get_login_manager", return_value=login_manager_mock
+    ):
+        mqtt_action.trigger(state="dummy")
+    login_manager_mock.LockSessions.assert_called_once_with()

+ 8 - 20
tests/test_dbus.py

@@ -151,25 +151,13 @@ def test__schedule_shutdown_fail(caplog, action, exception_message, log_message)
     assert "inhibitor" in caplog.records[2].message
 
 
-@pytest.mark.parametrize(
-    ("topic_suffix", "expected_action_arg"), [("poweroff", "poweroff")]
-)
-def test_mqtt_topic_suffix_action_mapping(topic_suffix, expected_action_arg):
-    mqtt_action = systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING[topic_suffix]
+def test_lock_all_sessions(caplog):
     login_manager_mock = unittest.mock.MagicMock()
     with unittest.mock.patch(
-        "systemctl_mqtt._dbus.get_login_manager", return_value=login_manager_mock,
-    ):
-        mqtt_action.trigger(
-            state=systemctl_mqtt._State(
-                mqtt_topic_prefix="systemctl/hostname",
-                homeassistant_discovery_prefix="homeassistant",
-                homeassistant_node_id="node",
-                poweroff_delay=datetime.timedelta(),
-            )
-        )
-    assert login_manager_mock.ScheduleShutdown.call_count == 1
-    schedule_args, schedule_kwargs = login_manager_mock.ScheduleShutdown.call_args
-    assert len(schedule_args) == 2
-    assert schedule_args[0] == expected_action_arg
-    assert not schedule_kwargs
+        "systemctl_mqtt._dbus.get_login_manager", return_value=login_manager_mock
+    ), caplog.at_level(logging.INFO):
+        systemctl_mqtt._dbus.lock_all_sessions()
+    login_manager_mock.LockSessions.assert_called_once_with()
+    assert len(caplog.records) == 1
+    assert caplog.records[0].levelno == logging.INFO
+    assert caplog.records[0].message == "instruct all sessions to activate screen locks"

+ 11 - 7
tests/test_mqtt.py

@@ -103,14 +103,18 @@ def test__run(
         state._login_manager.connect_to_signal.call_args[1]["signal_name"]
         == "PrepareForShutdown"
     )
-    mqtt_subscribe_mock.assert_called_once_with(mqtt_topic_prefix + "/poweroff")
+    assert mqtt_subscribe_mock.call_args_list == [
+        unittest.mock.call(mqtt_topic_prefix + "/poweroff"),
+        unittest.mock.call(mqtt_topic_prefix + "/lock-all-sessions"),
+    ]
     assert mqtt_client.on_message is None
-    assert (  # pylint: disable=comparison-with-callable
-        mqtt_client._on_message_filtered[mqtt_topic_prefix + "/poweroff"]
-        == systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING[
-            "poweroff"
-        ].mqtt_message_callback
-    )
+    for suffix in ("poweroff", "lock-all-sessions"):
+        assert (  # pylint: disable=comparison-with-callable
+            mqtt_client._on_message_filtered[mqtt_topic_prefix + "/" + suffix]
+            == systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING[
+                suffix
+            ].mqtt_message_callback
+        )
     assert caplog.records[0].levelno == logging.DEBUG
     assert caplog.records[0].message == "connected to MQTT broker {}:{}".format(
         mqtt_host, mqtt_port