-
Notifications
You must be signed in to change notification settings - Fork 782
Expand file tree
/
Copy pathsystemd.py
More file actions
236 lines (196 loc) · 7.87 KB
/
systemd.py
File metadata and controls
236 lines (196 loc) · 7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""Interface to Systemd over D-Bus."""
from functools import wraps
import logging
from typing import NamedTuple
from dbus_fast import Variant
from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import (
DBusError,
DBusFatalError,
DBusInterfaceError,
DBusServiceUnkownError,
DBusSystemdNoSuchUnit,
)
from ..utils.dbus import DBusSignalWrapper
from .const import (
DBUS_ATTR_ACTIVE_STATE,
DBUS_ATTR_FINISH_TIMESTAMP,
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC,
DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC,
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC,
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC,
DBUS_ATTR_VIRTUALIZATION,
DBUS_ERR_SYSTEMD_NO_SUCH_UNIT,
DBUS_IFACE_SYSTEMD_MANAGER,
DBUS_IFACE_SYSTEMD_UNIT,
DBUS_NAME_SYSTEMD,
DBUS_OBJECT_SYSTEMD,
DBUS_SIGNAL_PROPERTIES_CHANGED,
StartUnitMode,
StopUnitMode,
SystemState,
UnitActiveState,
)
from .interface import DBusInterface, DBusInterfaceProxy, dbus_property
from .utils import dbus_connected
_LOGGER: logging.Logger = logging.getLogger(__name__)
class ExecStartEntry(NamedTuple):
"""Systemd ExecStart entry for transient units (D-Bus type signature 'sasb')."""
binary: str
argv: list[str]
ignore_failure: bool
def systemd_errors(func):
"""Wrap systemd dbus methods to handle its specific error types."""
@wraps(func)
async def wrapper(*args, **kwds):
try:
return await func(*args, **kwds)
except DBusFatalError as err:
if err.type == DBUS_ERR_SYSTEMD_NO_SUCH_UNIT:
raise DBusSystemdNoSuchUnit(str(err)) from None
raise err
return wrapper
class SystemdUnit(DBusInterface):
"""Systemd service unit."""
name: str = DBUS_NAME_SYSTEMD
bus_name: str = DBUS_NAME_SYSTEMD
def __init__(self, object_path: str) -> None:
"""Initialize object."""
super().__init__()
self._object_path = object_path
@property
def object_path(self) -> str:
"""Object path for dbus object."""
return self._object_path
@dbus_connected
async def get_active_state(self) -> UnitActiveState:
"""Get active state of the unit."""
return UnitActiveState(await self.connected_dbus.Unit.get("active_state"))
@dbus_connected
def properties_changed(self) -> DBusSignalWrapper:
"""Return signal wrapper for properties changed."""
return self.connected_dbus.signal(DBUS_SIGNAL_PROPERTIES_CHANGED)
@dbus_connected
async def wait_for_active_state(
self, target_states: set[UnitActiveState]
) -> UnitActiveState:
"""Wait for unit to reach one of the target active states.
Caller must handle TimeoutError if a timeout is desired.
"""
async with self.properties_changed() as signal:
state = await self.get_active_state()
while state not in target_states:
interface, changed, _ = await signal.wait_for_signal()
if (
interface == DBUS_IFACE_SYSTEMD_UNIT
and DBUS_ATTR_ACTIVE_STATE in changed
):
state = UnitActiveState(changed[DBUS_ATTR_ACTIVE_STATE].value)
return state
class Systemd(DBusInterfaceProxy):
"""Systemd function handler.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html
"""
name: str = DBUS_NAME_SYSTEMD
bus_name: str = DBUS_NAME_SYSTEMD
object_path: str = DBUS_OBJECT_SYSTEMD
# NFailedUnits is the only property that emits a change signal and we don't use it
sync_properties: bool = False
properties_interface: str = DBUS_IFACE_SYSTEMD_MANAGER
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd")
except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning(
"No systemd support on the host. Host control has been disabled."
)
if self.is_connected:
try:
await self.connected_dbus.Manager.call("subscribe")
except DBusError:
_LOGGER.warning("Could not subscribe to systemd signals")
@property
@dbus_property
def startup_time(self) -> float:
"""Return startup time in seconds."""
return (
float(self.properties[DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC])
+ float(self.properties[DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC])
+ float(self.properties[DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC])
+ float(self.properties[DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC])
) / 1e6
@property
@dbus_property
def boot_timestamp(self) -> int:
"""Return the boot timestamp."""
return self.properties[DBUS_ATTR_FINISH_TIMESTAMP]
@property
@dbus_property
def virtualization(self) -> str:
"""Return virtualization hypervisor being used."""
return self.properties[DBUS_ATTR_VIRTUALIZATION]
@dbus_connected
async def reboot(self) -> None:
"""Reboot host computer."""
await self.connected_dbus.Manager.call("reboot")
@dbus_connected
async def power_off(self) -> None:
"""Power off host computer."""
await self.connected_dbus.Manager.call("power_off")
@dbus_connected
@systemd_errors
async def start_unit(self, unit: str, mode: StartUnitMode) -> str:
"""Start a systemd service unit. Returns object path of job."""
return await self.connected_dbus.Manager.call("start_unit", unit, mode)
@dbus_connected
@systemd_errors
async def stop_unit(self, unit: str, mode: StopUnitMode) -> str:
"""Stop a systemd service unit. Returns object path of job."""
return await self.connected_dbus.Manager.call("stop_unit", unit, mode)
@dbus_connected
@systemd_errors
async def reload_unit(self, unit: str, mode: StartUnitMode) -> str:
"""Reload a systemd service unit. Returns object path of job."""
return await self.connected_dbus.Manager.call(
"reload_or_restart_unit", unit, mode
)
@dbus_connected
@systemd_errors
async def restart_unit(self, unit: str, mode: StartUnitMode) -> str:
"""Restart a systemd service unit. Returns object path of job."""
return await self.connected_dbus.Manager.call("restart_unit", unit, mode)
@dbus_connected
async def list_units(
self,
) -> list[tuple[str, str, str, str, str, str, str, int, str, str]]:
"""Return a list of available systemd services."""
return await self.connected_dbus.Manager.call("list_units")
@dbus_connected
async def get_system_state(self) -> SystemState:
"""Return the systemd manager state."""
return SystemState(await self.connected_dbus.Manager.get("system_state"))
@dbus_connected
async def start_transient_unit(
self, unit: str, mode: StartUnitMode, properties: list[tuple[str, Variant]]
) -> str:
"""Start a transient unit which is released when stopped or on reboot. Returns object path of job."""
return await self.connected_dbus.Manager.call(
"start_transient_unit", unit, mode, properties, []
)
@dbus_connected
@systemd_errors
async def reset_failed_unit(self, unit: str) -> None:
"""Reset the failed state of a unit."""
await self.connected_dbus.Manager.call("reset_failed_unit", unit)
@dbus_connected
@systemd_errors
async def get_unit(self, unit: str) -> SystemdUnit:
"""Return systemd unit for unit name."""
obj_path = await self.connected_dbus.Manager.call("get_unit", unit)
systemd_unit = SystemdUnit(obj_path)
await systemd_unit.connect(self.connected_dbus.bus)
return systemd_unit