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