1"""
2Contains systemd related help files
3"""
4
5import logging
6import os
7import re
8import subprocess
9
10import salt.loader.context
11import salt.utils.path
12import salt.utils.stringutils
13from salt.exceptions import SaltInvocationError
14
15try:
16    import dbus
17except ImportError:
18    dbus = None
19
20
21log = logging.getLogger(__name__)
22
23
24def booted(context=None):
25    """
26    Return True if the system was booted with systemd, False otherwise.  If the
27    loader context dict ``__context__`` is passed, this function will set the
28    ``salt.utils.systemd.booted`` key to represent if systemd is running and
29    keep the logic below from needing to be run again during the same salt run.
30    """
31    contextkey = "salt.utils.systemd.booted"
32    if isinstance(context, (dict, salt.loader.context.NamedLoaderContext)):
33        # Can't put this if block on the same line as the above if block,
34        # because it willl break the elif below.
35        if contextkey in context:
36            return context[contextkey]
37    elif context is not None:
38        raise SaltInvocationError("context must be a dictionary if passed")
39
40    try:
41        # This check does the same as sd_booted() from libsystemd-daemon:
42        # http://www.freedesktop.org/software/systemd/man/sd_booted.html
43        ret = bool(os.stat("/run/systemd/system"))
44    except OSError:
45        ret = False
46
47    try:
48        context[contextkey] = ret
49    except TypeError:
50        pass
51
52    return ret
53
54
55def offline(context=None):
56    """Return True if systemd is in offline mode
57
58    .. versionadded:: 3004
59    """
60    contextkey = "salt.utils.systemd.offline"
61    if isinstance(context, (dict, salt.loader.context.NamedLoaderContext)):
62        if contextkey in context:
63            return context[contextkey]
64    elif context is not None:
65        raise SaltInvocationError("context must be a dictionary if passed")
66
67    # Note that there is a difference from SYSTEMD_OFFLINE=1.  Here we
68    # assume that there is no PID 1 to talk with.
69    ret = not booted(context) and salt.utils.path.which("systemctl")
70
71    try:
72        context[contextkey] = ret
73    except TypeError:
74        pass
75
76    return ret
77
78
79def version(context=None):
80    """
81    Attempts to run systemctl --version. Returns None if unable to determine
82    version.
83    """
84    contextkey = "salt.utils.systemd.version"
85    if isinstance(context, (dict, salt.loader.context.NamedLoaderContext)):
86        # Can't put this if block on the same line as the above if block,
87        # because it will break the elif below.
88        if contextkey in context:
89            return context[contextkey]
90    elif context is not None:
91        raise SaltInvocationError("context must be a dictionary if passed")
92    stdout = subprocess.Popen(
93        ["systemctl", "--version"],
94        close_fds=True,
95        stdout=subprocess.PIPE,
96        stderr=subprocess.STDOUT,
97    ).communicate()[0]
98    outstr = salt.utils.stringutils.to_str(stdout)
99    try:
100        ret = int(re.search(r"\w+ ([0-9]+)", outstr.splitlines()[0]).group(1))
101    except (AttributeError, IndexError, ValueError):
102        log.error(
103            "Unable to determine systemd version from systemctl "
104            "--version, output follows:\n%s",
105            outstr,
106        )
107        return None
108    else:
109        try:
110            context[contextkey] = ret
111        except TypeError:
112            pass
113        return ret
114
115
116def has_scope(context=None):
117    """
118    Scopes were introduced in systemd 205, this function returns a boolean
119    which is true when the minion is systemd-booted and running systemd>=205.
120    """
121    if not booted(context):
122        return False
123    _sd_version = version(context)
124    if _sd_version is None:
125        return False
126    return _sd_version >= 205
127
128
129def pid_to_service(pid):
130    """
131    Check if a PID belongs to a systemd service and return its name.
132    Return None if the PID does not belong to a service.
133
134    Uses DBUS if available.
135    """
136    if dbus:
137        return _pid_to_service_dbus(pid)
138    else:
139        return _pid_to_service_systemctl(pid)
140
141
142def _pid_to_service_systemctl(pid):
143    systemd_cmd = ["systemctl", "--output", "json", "status", str(pid)]
144    try:
145        systemd_output = subprocess.run(
146            systemd_cmd, check=True, text=True, capture_output=True
147        )
148        status_json = salt.utils.json.find_json(systemd_output.stdout)
149    except (ValueError, subprocess.CalledProcessError):
150        return None
151
152    name = status_json.get("_SYSTEMD_UNIT")
153    if name and name.endswith(".service"):
154        return _strip_suffix(name)
155    else:
156        return None
157
158
159def _pid_to_service_dbus(pid):
160    """
161    Use DBUS to check if a PID belongs to a running systemd service and return the service name if it does.
162    """
163    bus = dbus.SystemBus()
164    systemd_object = bus.get_object(
165        "org.freedesktop.systemd1", "/org/freedesktop/systemd1"
166    )
167    systemd = dbus.Interface(systemd_object, "org.freedesktop.systemd1.Manager")
168    try:
169        service_path = systemd.GetUnitByPID(pid)
170        service_object = bus.get_object("org.freedesktop.systemd1", service_path)
171        service_props = dbus.Interface(
172            service_object, "org.freedesktop.DBus.Properties"
173        )
174        service_name = service_props.Get("org.freedesktop.systemd1.Unit", "Id")
175        name = str(service_name)
176
177        if name and name.endswith(".service"):
178            return _strip_suffix(name)
179        else:
180            return None
181    except dbus.DBusException:
182        return None
183
184
185def _strip_suffix(service_name):
186    """
187    Strip ".service" suffix from a given service name.
188    """
189    return service_name[:-8]
190