|
2 | 2 | async SystemD Manager implementation. |
3 | 3 | """ |
4 | 4 |
|
5 | | -import enum |
6 | 5 | import logging |
7 | | -from typing import Literal, Optional, Protocol, runtime_checkable |
8 | 6 |
|
9 | | -from dbus_fast import BusType, DBusError |
10 | | -from dbus_fast.aio import MessageBus, ProxyObject |
| 7 | +import dbus |
| 8 | +from dbus import DBusException, SystemBus |
| 9 | +from dbus.proxies import Interface |
11 | 10 |
|
12 | 11 | logger = logging.getLogger(__name__) |
13 | 12 |
|
14 | 13 |
|
15 | | -class UnitFileState(str, enum.Enum): |
16 | | - """This StrEnum class represents the different possible states of a unit file.""" |
17 | | - |
18 | | - ENABLED = "enabled" |
19 | | - """Indicates that a unit file is permanently enabled.""" |
20 | | - |
21 | | - ENABLED_RUNTIME = "enabled-runtime" |
22 | | - """Indicates the unit file is only temporarily enabled and will no longer be enabled after a reboot |
23 | | - (that means, it is enabled via /run/ symlinks, rather than /etc/).""" |
24 | | - |
25 | | - LINKED = "linked" |
26 | | - """Indicates that a unit is linked into /etc/ permanently.""" |
27 | | - |
28 | | - LINKED_RUNTIME = "linked-runtime" |
29 | | - """Indicates that a unit is linked into /run/ temporarily (until the next reboot).""" |
30 | | - |
31 | | - MASKED = "masked" |
32 | | - """Indicates that the unit file is masked permanently.""" |
33 | | - |
34 | | - MASKED_RUNTIME = "masked-runtime" |
35 | | - """Indicates that it is masked in /run/ temporarily (until the next reboot).""" |
36 | | - |
37 | | - STATIC = "static" |
38 | | - """Indicates that the unit is statically enabled, i.e. always enabled and doesn't need to be enabled explicitly.""" |
39 | | - |
40 | | - DISABLED = "disabled" |
41 | | - """Indicates that the unit file is not enabled.""" |
42 | | - |
43 | | - INVALID = "invalid" |
44 | | - """Indicates that it could not be determined whether the unit file is enabled.""" |
45 | | - |
46 | | - |
47 | | -UnitFileStateLiteral = Literal[ |
48 | | - "enabled", |
49 | | - "enabled-runtime", |
50 | | - "linked", |
51 | | - "linked-runtime", |
52 | | - "masked", |
53 | | - "masked-runtime", |
54 | | - "static", |
55 | | - "disabled", |
56 | | - "invalid", |
57 | | -] |
58 | | - |
59 | | - |
60 | | -class Mode(str, enum.Enum): |
61 | | - REPLACE = "replace" |
62 | | - FAIL = "fail" |
63 | | - ISOLATE = "isolate" |
64 | | - IGNORE_DEPENDENCIES = "ignore-dependencies" |
65 | | - IGNORE_REQUIREMENTS = "ignore-requirements" |
66 | | - |
67 | | - |
68 | | -class ActiveState(str, enum.Enum): |
69 | | - """ |
70 | | - ActiveState contains a state value that reflects the unit's current status. |
71 | | - """ |
72 | | - |
73 | | - ACTIVE = "active" |
74 | | - """ |
75 | | - The unit is active. |
76 | | - """ |
77 | | - |
78 | | - RELOADING = "reloading" |
79 | | - """ |
80 | | - The unit is active and reloading its configuration. |
81 | | - """ |
82 | | - |
83 | | - INACTIVE = "inactive" |
84 | | - """ |
85 | | - The unit is inactive, previous run was successful or hasn't yet occurred. |
86 | | - """ |
87 | | - |
88 | | - FAILED = "failed" |
89 | | - """ |
90 | | - The unit is inactive, previous run was unsuccessful. |
91 | | - """ |
92 | | - |
93 | | - ACTIVATING = "activating" |
94 | | - """ |
95 | | - The unit is transitioning from inactive to active state. |
96 | | - """ |
97 | | - |
98 | | - DEACTIVATING = "deactivating" |
99 | | - """ |
100 | | - The unit is in the process of deactivation. |
101 | | - """ |
102 | | - |
103 | | - |
104 | | -ActiveStateLiteral = Literal["active", "reloading", "inactive", "failed", "activating", "deactivating"] |
105 | | - |
106 | | - |
107 | | -@runtime_checkable |
108 | | -class SystemdProxy(Protocol): |
109 | | - """ABC for typing. |
110 | | -
|
111 | | - for description of methodsp |
112 | | - see https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html#The%20Manager%20Object""" |
113 | | - |
114 | | - async def call_enable_unit_files(self, files: list[str], runtime: bool, force: bool): ... |
115 | | - |
116 | | - async def call_get_unit_file_state(self, service) -> UnitFileStateLiteral: ... |
117 | | - |
118 | | - async def call_start_unit(self, name, mode): |
119 | | - pass |
120 | | - |
121 | | - async def call_stop_unit(self, name, mode): ... |
122 | | - |
123 | | - async def call_restart_unit(self, name, mode): ... |
124 | | - |
125 | | - async def call_disable_unit_files(self, files: list[str], runtime: bool): ... |
126 | | - |
127 | | - async def call_get_unit(self, name: str) -> str: ... |
128 | | - |
129 | | - |
130 | | -@runtime_checkable |
131 | | -class UnitProxy(Protocol): |
132 | | - """for typing. |
133 | | -
|
134 | | - for description of methods see |
135 | | - https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html#Service%20Unit%20Objects""" |
136 | | - |
137 | | - async def get_active_state(self) -> ActiveStateLiteral: ... |
138 | | - |
139 | | - |
140 | 14 | class SystemDManager: |
141 | 15 | """SystemD Manager class. |
142 | 16 |
|
143 | 17 | Used to manage the systemd services on the host on Linux. |
144 | 18 | """ |
145 | 19 |
|
146 | | - bus: Optional[MessageBus] |
147 | | - manager: Optional[SystemdProxy] |
| 20 | + bus: SystemBus |
| 21 | + manager: Interface |
148 | 22 |
|
149 | 23 | def __init__(self): |
150 | | - pass |
151 | | - |
152 | | - async def connect(self): |
153 | | - self.bus = MessageBus(bus_type=BusType.SYSTEM) |
154 | | - await self.bus.connect() |
155 | | - path = "/org/freedesktop/systemd1" |
156 | | - bus_name = "org.freedesktop.systemd1" |
157 | | - introspect = await self.bus.introspect(bus_name, path) |
158 | | - systemd_proxy: ProxyObject = self.bus.get_proxy_object(bus_name, path, introspection=introspect) |
159 | | - interface = systemd_proxy.get_interface("org.freedesktop.systemd1.Manager") |
160 | | - # Check required method are implemented |
161 | | - assert isinstance(interface, SystemdProxy) |
162 | | - self.manager = interface |
163 | | - |
164 | | - async def enable(self, service: str) -> None: |
165 | | - assert self.manager, "connect() not called" |
166 | | - await self.manager.call_enable_unit_files([service], False, True) |
| 24 | + self.bus = dbus.SystemBus() |
| 25 | + systemd = self.bus.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") |
| 26 | + self.manager = dbus.Interface(systemd, "org.freedesktop.systemd1.Manager") |
| 27 | + |
| 28 | + def stop_and_disable(self, service: str) -> None: |
| 29 | + if self.is_service_active(service): |
| 30 | + self.stop(service) |
| 31 | + if self.is_service_enabled(service): |
| 32 | + self.disable(service) |
| 33 | + |
| 34 | + def enable(self, service: str) -> None: |
| 35 | + self.manager.EnableUnitFiles([service], False, True) |
167 | 36 | logger.debug(f"Enabled {service} service") |
168 | 37 |
|
169 | | - async def start(self, service: str) -> None: |
170 | | - assert self.manager, "connect() not called" |
171 | | - await self.manager.call_start_unit(service, Mode.REPLACE) |
| 38 | + def start(self, service: str) -> None: |
| 39 | + self.manager.StartUnit(service, "replace") |
172 | 40 | logger.debug(f"Started {service} service") |
173 | 41 |
|
174 | | - async def stop(self, service: str) -> None: |
175 | | - assert self.manager, "connect() not called" |
176 | | - await self.manager.call_stop_unit(service, Mode.REPLACE) |
| 42 | + def stop(self, service: str) -> None: |
| 43 | + self.manager.StopUnit(service, "replace") |
177 | 44 | logger.debug(f"Stopped {service} service") |
178 | 45 |
|
179 | | - async def restart(self, service: str) -> None: |
180 | | - assert self.manager, "connect() not called" |
181 | | - await self.manager.call_restart_unit(service, Mode.REPLACE) |
| 46 | + def restart(self, service: str) -> None: |
| 47 | + self.manager.RestartUnit(service, "replace") |
182 | 48 | logger.debug(f"Restarted {service} service") |
183 | 49 |
|
184 | | - async def disable(self, service: str) -> None: |
185 | | - assert self.manager, "connect() not called" |
186 | | - await self.manager.call_disable_unit_files([service], False) |
| 50 | + def disable(self, service: str) -> None: |
| 51 | + self.manager.DisableUnitFiles([service], False) |
187 | 52 | logger.debug(f"Disabled {service} service") |
188 | 53 |
|
189 | | - async def is_service_enabled(self, service: str) -> bool: |
190 | | - assert self.manager, "connect() not called" |
| 54 | + def is_service_enabled(self, service: str) -> bool: |
191 | 55 | try: |
192 | | - state = await self.manager.call_get_unit_file_state(service) |
193 | | - return state == UnitFileState.ENABLED |
194 | | - except DBusError as error: |
| 56 | + return self.manager.GetUnitFileState(service) == "enabled" |
| 57 | + except DBusException as error: |
195 | 58 | logger.error(error) |
196 | 59 | return False |
197 | 60 |
|
198 | | - async def is_service_active(self, service: str) -> bool: |
199 | | - assert self.manager, "connect() not called" |
200 | | - assert self.bus, "connect() not called" |
| 61 | + def is_service_active(self, service: str) -> bool: |
201 | 62 | try: |
202 | | - path = await self.manager.call_get_unit(service) |
203 | | - bus_name = "org.freedesktop.systemd1" |
204 | | - introspect = await self.bus.introspect(bus_name, path) |
205 | | - systemd_service = self.bus.get_proxy_object(bus_name, path, introspection=introspect) |
206 | | - unit = systemd_service.get_interface("org.freedesktop.systemd1.Unit") |
207 | | - # Check required method are implemented |
208 | | - assert isinstance(unit, UnitProxy) |
209 | | - active_state = await unit.get_active_state() |
210 | | - return active_state == ActiveState.ACTIVE |
211 | | - except DBusError as error: |
| 63 | + systemd_service = self.bus.get_object("org.freedesktop.systemd1", object_path=self.manager.GetUnit(service)) |
| 64 | + unit = dbus.Interface(systemd_service, "org.freedesktop.systemd1.Unit") |
| 65 | + unit_properties = dbus.Interface(unit, "org.freedesktop.DBus.Properties") |
| 66 | + active_state = unit_properties.Get("org.freedesktop.systemd1.Unit", "ActiveState") |
| 67 | + return active_state == "active" |
| 68 | + except DBusException as error: |
212 | 69 | logger.error(error) |
213 | 70 | return False |
214 | 71 |
|
215 | | - async def enable_and_start(self, service: str) -> None: |
216 | | - if not await self.is_service_enabled(service): |
217 | | - await self.enable(service) |
218 | | - if not await self.is_service_active(service): |
219 | | - await self.start(service) |
220 | | - |
221 | | - async def stop_and_disable(self, service: str) -> None: |
222 | | - if await self.is_service_active(service): |
223 | | - await self.stop(service) |
224 | | - if await self.is_service_enabled(service): |
225 | | - await self.disable(service) |
| 72 | + def enable_and_start(self, service: str) -> None: |
| 73 | + if not self.is_service_enabled(service): |
| 74 | + self.enable(service) |
| 75 | + if not self.is_service_active(service): |
| 76 | + self.start(service) |
0 commit comments