├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── setup.py ├── sysdmanager ├── __init__.py └── systemd_manager.py └── tests ├── __init__.py ├── test_files ├── test_service.py └── test_service.service └── test_sysdmanager.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | build 4 | dist 5 | sysdmanager.egg-info -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Emlid Limited 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Makefile 2 | LICENSE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | python setup.py install 3 | 4 | package: 5 | python setup.py sdist 6 | 7 | clean: 8 | rm -rf build 9 | rm -rf dist 10 | rm -rf sysdmanager.egg-info -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Simple python D-Bus API to control systemd services 2 | 3 | ##### Prerequisities 4 | 5 | This packages uses D-Bus to control systemd. You need to have D-Bus itself and its python bindings in order to use this library. For example, on Ubuntu you need to install the following packages: 6 | 7 | `sudo apt install dbus libdbus-glib-1-dev libdbus-1-dev python-dbus` 8 | 9 | The setup.py **does not contain** the `python-dbus` dependency and you have to install it manually. 10 | 11 | ##### Example 12 | 13 | ``` 14 | from sysdmanager import SystemdManager 15 | 16 | manager = SystemdManager() 17 | if not manager.is_active("bluetooth.service"): 18 | manager.start_unit("bluetooth.service") 19 | 20 | ``` 21 | 22 | ##### API 23 | 24 | * `SystemdManager().start_unit(unit_name, mode="replace")` 25 | * `unit_name` - a string in form of `'*.service'` 26 | * `mode` - start mode. One of `replace`, `fail`, `isolate`, `ignore-dependencies`, `ignore-requirements`. Details [here](https://www.freedesktop.org/wiki/Software/systemd/dbus/) 27 | * returns bool representing operation success 28 | * `SystemdManager().stop_unit(unit_name, mode="replace")` 29 | * `unit_name` - a string in form of `'*.service'` 30 | * `mode` - start mode. One of `replace`, `fail`, `isolate`, `ignore-dependencies`, `ignore-requirements`. Details [here](https://www.freedesktop.org/wiki/Software/systemd/dbus/) 31 | * returns bool representing operation success 32 | * `SystemdManager().enable_unit(unit_name)` 33 | * `unit_name` - a string in form of `'*.service'` 34 | * returns bool representing operation success 35 | * `SystemdManager().disable_unit(unit_name)` 36 | * `unit_name` - a string in form of `'*.service'` 37 | * returns bool representing operation success 38 | * `SystemdManager().is_active(unit_name)` 39 | * `unit_name` - a string in form of `'*.service'` 40 | * returns bool representing unit state 41 | 42 | ##### Credits 43 | 44 | This package was written by [Aleksandr Aleksandrov](https://github.com/AD-Aleksandrov) and is used in [Emlid](https://emlid.com)'s products, such as Reach and Reach RS. 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="sysdmanager", 5 | version="0.1.1", 6 | license="BSD-3", 7 | author="Aleksandr Aleksandrov", 8 | author_email="aleksandr.aleksandrov@emlid.com", 9 | url="https://github.com/emlid/systemd-manager", 10 | packages=find_packages(exclude=["tests"]) 11 | ) 12 | -------------------------------------------------------------------------------- /sysdmanager/__init__.py: -------------------------------------------------------------------------------- 1 | from .systemd_manager import SystemdManager 2 | -------------------------------------------------------------------------------- /sysdmanager/systemd_manager.py: -------------------------------------------------------------------------------- 1 | # Written by Aleksandr Aleksandrov 2 | # 3 | # Copyright (c) 2016, Emlid Limited 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, 7 | # with or without modification, 8 | # are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, 11 | # this list of conditions and the following disclaimer. 12 | # 13 | # 2. Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # 3. Neither the name of the copyright holder nor the names of its contributors 18 | # may be used to endorse or promote products derived from this software 19 | # without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 23 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 24 | # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 26 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 27 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 30 | # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 31 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 32 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 33 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | 36 | import dbus 37 | 38 | 39 | class SystemdManager(object): 40 | 41 | UNIT_INTERFACE = "org.freedesktop.systemd1.Unit" 42 | SERVICE_UNIT_INTERFACE = "org.freedesktop.systemd1.Service" 43 | 44 | def __init__(self): 45 | self.__bus = dbus.SystemBus() 46 | 47 | def start_unit(self, unit_name, mode="replace"): 48 | interface = self._get_interface() 49 | 50 | if interface is None: 51 | return False 52 | 53 | try: 54 | interface.StartUnit(unit_name, mode) 55 | return True 56 | except dbus.exceptions.DBusException as error: 57 | print(error) 58 | return False 59 | 60 | def stop_unit(self, unit_name, mode="replace"): 61 | interface = self._get_interface() 62 | 63 | if interface is None: 64 | return False 65 | 66 | try: 67 | interface.StopUnit(unit_name, mode) 68 | return True 69 | except dbus.exceptions.DBusException as error: 70 | print(error) 71 | return False 72 | 73 | def restart_unit(self, unit_name, mode="replace"): 74 | interface = self._get_interface() 75 | 76 | if interface is None: 77 | return False 78 | 79 | try: 80 | interface.RestartUnit(unit_name, mode) 81 | return True 82 | except dbus.exceptions.DBusException as error: 83 | print(error) 84 | return False 85 | 86 | def enable_unit(self, unit_name): 87 | interface = self._get_interface() 88 | 89 | if interface is None: 90 | return False 91 | 92 | try: 93 | interface.EnableUnitFiles([unit_name], 94 | dbus.Boolean(False), 95 | dbus.Boolean(True)) 96 | return True 97 | except dbus.exceptions.DBusException as error: 98 | print(error) 99 | return False 100 | 101 | def disable_unit(self, unit_name): 102 | interface = self._get_interface() 103 | 104 | if interface is None: 105 | return False 106 | 107 | try: 108 | interface.DisableUnitFiles([unit_name], dbus.Boolean(False)) 109 | return True 110 | except dbus.exceptions.DBusException as error: 111 | print(error) 112 | return False 113 | 114 | def _get_unit_file_state(self, unit_name): 115 | interface = self._get_interface() 116 | 117 | if interface is None: 118 | return None 119 | 120 | try: 121 | state = interface.GetUnitFileState(unit_name) 122 | return state 123 | except dbus.exceptions.DBusException as error: 124 | print(error) 125 | return False 126 | 127 | def _get_interface(self): 128 | try: 129 | obj = self.__bus.get_object("org.freedesktop.systemd1", 130 | "/org/freedesktop/systemd1") 131 | return dbus.Interface(obj, "org.freedesktop.systemd1.Manager") 132 | except dbus.exceptions.DBusException as error: 133 | print(error) 134 | return None 135 | 136 | def get_active_state(self, unit_name): 137 | properties = self._get_unit_properties(unit_name, self.UNIT_INTERFACE) 138 | 139 | if properties is None: 140 | return False 141 | 142 | try: 143 | state = properties["ActiveState"].encode("utf-8") 144 | return state 145 | except KeyError: 146 | return False 147 | 148 | def is_active(self, unit_name): 149 | unit_state = self.get_active_state(unit_name) 150 | return unit_state == b"active" 151 | 152 | def is_failed(self, unit_name): 153 | unit_state = self.get_active_state(unit_name) 154 | return unit_state == b"failed" 155 | 156 | def get_error_code(self, unit_name): 157 | service_properties = self._get_unit_properties(unit_name, self.SERVICE_UNIT_INTERFACE) 158 | 159 | if service_properties is None: 160 | return None 161 | 162 | return self._get_exec_status(service_properties) 163 | 164 | def _get_exec_status(self, properties): 165 | try: 166 | exec_status = int(properties["ExecMainStatus"]) 167 | return exec_status 168 | except KeyError: 169 | return None 170 | 171 | def _get_result(self, properties): 172 | try: 173 | result = properties["Result"].encode("utf-8") 174 | return result 175 | except KeyError: 176 | return False 177 | 178 | def _get_unit_properties(self, unit_name, unit_interface): 179 | interface = self._get_interface() 180 | 181 | if interface is None: 182 | return None 183 | 184 | try: 185 | unit_path = interface.LoadUnit(unit_name) 186 | 187 | obj = self.__bus.get_object( 188 | "org.freedesktop.systemd1", unit_path) 189 | 190 | properties_interface = dbus.Interface( 191 | obj, "org.freedesktop.DBus.Properties") 192 | 193 | return properties_interface.GetAll(unit_interface) 194 | 195 | except dbus.exceptions.DBusException as error: 196 | print(error) 197 | return None 198 | 199 | 200 | if __name__ == "__main__": 201 | s = SystemdManager() 202 | print(s.get_error_code("wpa_supplicant.service")) 203 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emlid/systemd-manager/4cae1498227d6bfae375fa56d11de6ca01833aaa/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_files/test_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import time 3 | 4 | def main(): 5 | while True: 6 | time.sleep(1) 7 | 8 | 9 | if __name__ == '__main__': 10 | try: 11 | main() 12 | except KeyboardInterrupt: 13 | pass -------------------------------------------------------------------------------- /tests/test_files/test_service.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Systemd Manager test service 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/usr/bin/test_service.py 7 | Restart=always 8 | RestartSec=1 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /tests/test_sysdmanager.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | import pytest 3 | 4 | from sysdmanager import SystemdManager 5 | 6 | 7 | @pytest.fixture() 8 | def exec_status_properties(): 9 | props = { 10 | 'ExecMainStatus': 255, 11 | 'Result': 'failed' 12 | } 13 | return props 14 | 15 | 16 | class TestSystemdManager: 17 | def setup_method(self): 18 | self.sysdmanager = SystemdManager() 19 | self.unit_name = "test_service.service" 20 | 21 | def test_start_unit(self): 22 | start_status = self.sysdmanager.start_unit(self.unit_name) 23 | assert start_status 24 | 25 | active = self.sysdmanager.is_active(self.unit_name) 26 | assert active 27 | 28 | def test_stop_unit(self): 29 | stop_status = self.sysdmanager.stop_unit(self.unit_name) 30 | assert stop_status 31 | 32 | active = self.sysdmanager.is_active(self.unit_name) 33 | assert not active 34 | 35 | def test_restart_unit(self): 36 | # Confirm that an active unit, when restarted, stays active 37 | start_status = self.sysdmanager.start_unit(self.unit_name) 38 | assert start_status 39 | 40 | restart_status = self.sysdmanager.restart_unit(self.unit_name) 41 | assert restart_status 42 | 43 | active = self.sysdmanager.is_active(self.unit_name) 44 | assert active 45 | 46 | # Confirm that an inactive unit, when restarted, becomes active 47 | stop_status = self.sysdmanager.stop_unit(self.unit_name) 48 | assert stop_status 49 | 50 | restart_status = self.sysdmanager.restart_unit(self.unit_name) 51 | assert restart_status 52 | 53 | active = self.sysdmanager.is_active(self.unit_name) 54 | assert active 55 | 56 | def test_enable_unit(self): 57 | enable_status = self.sysdmanager.enable_unit(self.unit_name) 58 | assert enable_status 59 | 60 | unit_file_state = self.sysdmanager._get_unit_file_state(self.unit_name) 61 | assert unit_file_state == 'enabled' 62 | 63 | def test_disable_unit(self): 64 | disable_status = self.sysdmanager.disable_unit(self.unit_name) 65 | assert disable_status 66 | 67 | unit_file_state = self.sysdmanager._get_unit_file_state(self.unit_name) 68 | assert unit_file_state == 'disabled' 69 | 70 | def test_get_interface(self): 71 | iface = self.sysdmanager._get_interface() 72 | assert isinstance(iface, dbus.Interface) 73 | 74 | def test_get_active_state(self): 75 | self.sysdmanager.start_unit(self.unit_name) 76 | state = self.sysdmanager._get_active_state(self.unit_name) 77 | assert state == 'active' 78 | 79 | def test_is_active(self): 80 | active_state = self.sysdmanager.is_active(self.unit_name) 81 | assert active_state 82 | 83 | def test_is_failed(self): 84 | failed_state = self.sysdmanager.is_failed(self.unit_name) 85 | assert failed_state == False 86 | 87 | def test_get_exec_status(self, exec_status_properties): 88 | exec_status = self.sysdmanager._get_exec_status(exec_status_properties) 89 | assert exec_status == {'failed': 255} 90 | 91 | def test_get_unit_properties(self): 92 | props = self.sysdmanager._get_unit_properties(self.unit_name, 93 | self.sysdmanager.SERVICE_UNIT_INTERFACE) 94 | assert isinstance(props, dbus.Dictionary) 95 | --------------------------------------------------------------------------------