blob: 6d19666d82618506ea5b9120a59f86968d9992ef [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#!/usr/bin/env python3
2"""systemctl: subset of systemctl used for image construction
Patrick Williamsc124f4f2015-09-15 14:41:29 -05003
Brad Bishopc342db32019-05-15 21:57:59 -04004Mask/preset systemd units
5"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006
Brad Bishopc342db32019-05-15 21:57:59 -04007import argparse
8import fnmatch
9import os
10import re
11import sys
Patrick Williamsc124f4f2015-09-15 14:41:29 -050012
Brad Bishopc342db32019-05-15 21:57:59 -040013from collections import namedtuple
Andrew Geissler615f2f12022-07-15 14:00:58 -050014from itertools import chain
Brad Bishopc342db32019-05-15 21:57:59 -040015from pathlib import Path
Patrick Williamsc124f4f2015-09-15 14:41:29 -050016
Brad Bishopc342db32019-05-15 21:57:59 -040017version = 1.0
Patrick Williamsc124f4f2015-09-15 14:41:29 -050018
Brad Bishopc342db32019-05-15 21:57:59 -040019ROOT = Path("/")
20SYSCONFDIR = Path("etc")
21BASE_LIBDIR = Path("lib")
22LIBDIR = Path("usr", "lib")
Patrick Williamsc124f4f2015-09-15 14:41:29 -050023
Brad Bishopc342db32019-05-15 21:57:59 -040024locations = list()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050025
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026
Brad Bishopc342db32019-05-15 21:57:59 -040027class SystemdFile():
28 """Class representing a single systemd configuration file"""
Andrew Geissler615f2f12022-07-15 14:00:58 -050029 def __init__(self, root, path, instance_unit_name):
Brad Bishopc342db32019-05-15 21:57:59 -040030 self.sections = dict()
31 self._parse(root, path)
Brad Bishop08902b02019-08-20 09:16:51 -040032 dirname = os.path.basename(path.name) + ".d"
33 for location in locations:
Andrew Geissler615f2f12022-07-15 14:00:58 -050034 files = (root / location / "system" / dirname).glob("*.conf")
35 if instance_unit_name:
36 inst_dirname = instance_unit_name + ".d"
37 files = chain(files, (root / location / "system" / inst_dirname).glob("*.conf"))
38 for path2 in sorted(files):
Brad Bishop08902b02019-08-20 09:16:51 -040039 self._parse(root, path2)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050040
Brad Bishopc342db32019-05-15 21:57:59 -040041 def _parse(self, root, path):
42 """Parse a systemd syntax configuration file
Patrick Williamsc124f4f2015-09-15 14:41:29 -050043
Brad Bishopc342db32019-05-15 21:57:59 -040044 Args:
45 path: A pathlib.Path object pointing to the file
Patrick Williamsc124f4f2015-09-15 14:41:29 -050046
Brad Bishopc342db32019-05-15 21:57:59 -040047 """
48 skip_re = re.compile(r"^\s*([#;]|$)")
49 section_re = re.compile(r"^\s*\[(?P<section>.*)\]")
50 kv_re = re.compile(r"^\s*(?P<key>[^\s]+)\s*=\s*(?P<value>.*)")
51 section = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -050052
Brad Bishopc342db32019-05-15 21:57:59 -040053 if path.is_symlink():
54 try:
55 path.resolve()
56 except FileNotFoundError:
57 # broken symlink, try relative to root
58 path = root / Path(os.readlink(str(path))).relative_to(ROOT)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050059
Brad Bishopc342db32019-05-15 21:57:59 -040060 with path.open() as f:
61 for line in f:
62 if skip_re.match(line):
63 continue
Brad Bishopec8c5652018-11-05 19:23:07 -050064
Brad Bishopa34c0302019-09-23 22:34:48 -040065 line = line.strip()
Brad Bishopc342db32019-05-15 21:57:59 -040066 m = section_re.match(line)
67 if m:
Brad Bishop08902b02019-08-20 09:16:51 -040068 if m.group('section') not in self.sections:
69 section = dict()
70 self.sections[m.group('section')] = section
71 else:
72 section = self.sections[m.group('section')]
Brad Bishopc342db32019-05-15 21:57:59 -040073 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -050074
Brad Bishopc342db32019-05-15 21:57:59 -040075 while line.endswith("\\"):
76 line += f.readline().rstrip("\n")
Patrick Williamsc124f4f2015-09-15 14:41:29 -050077
Brad Bishopc342db32019-05-15 21:57:59 -040078 m = kv_re.match(line)
79 k = m.group('key')
80 v = m.group('value')
81 if k not in section:
82 section[k] = list()
83 section[k].extend(v.split())
Patrick Williamsc124f4f2015-09-15 14:41:29 -050084
Brad Bishopc342db32019-05-15 21:57:59 -040085 def get(self, section, prop):
86 """Get a property from section
87
88 Args:
89 section: Section to retrieve property from
90 prop: Property to retrieve
91
92 Returns:
93 List representing all properties of type prop in section.
94
95 Raises:
96 KeyError: if ``section`` or ``prop`` not found
97 """
98 return self.sections[section][prop]
99
100
101class Presets():
102 """Class representing all systemd presets"""
103 def __init__(self, scope, root):
104 self.directives = list()
105 self._collect_presets(scope, root)
106
107 def _parse_presets(self, presets):
108 """Parse presets out of a set of preset files"""
109 skip_re = re.compile(r"^\s*([#;]|$)")
110 directive_re = re.compile(r"^\s*(?P<action>enable|disable)\s+(?P<unit_name>(.+))")
111
112 Directive = namedtuple("Directive", "action unit_name")
113 for preset in presets:
114 with preset.open() as f:
115 for line in f:
116 m = directive_re.match(line)
117 if m:
118 directive = Directive(action=m.group('action'),
119 unit_name=m.group('unit_name'))
120 self.directives.append(directive)
121 elif skip_re.match(line):
122 pass
123 else:
124 sys.exit("Unparsed preset line in {}".format(preset))
125
126 def _collect_presets(self, scope, root):
127 """Collect list of preset files"""
128 presets = dict()
129 for location in locations:
130 paths = (root / location / scope).glob("*.preset")
131 for path in paths:
132 # earlier names override later ones
133 if path.name not in presets:
134 presets[path.name] = path
135
136 self._parse_presets([v for k, v in sorted(presets.items())])
137
138 def state(self, unit_name):
139 """Return state of preset for unit_name
140
141 Args:
142 presets: set of presets
143 unit_name: name of the unit
144
145 Returns:
146 None: no matching preset
147 `enable`: unit_name is enabled
148 `disable`: unit_name is disabled
149 """
150 for directive in self.directives:
151 if fnmatch.fnmatch(unit_name, directive.unit_name):
152 return directive.action
153
154 return None
155
156
157def add_link(path, target):
158 try:
159 path.parent.mkdir(parents=True)
160 except FileExistsError:
161 pass
162 if not path.is_symlink():
163 print("ln -s {} {}".format(target, path))
164 path.symlink_to(target)
165
166
167class SystemdUnitNotFoundError(Exception):
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700168 def __init__(self, path, unit):
169 self.path = path
170 self.unit = unit
Brad Bishopc342db32019-05-15 21:57:59 -0400171
172
173class SystemdUnit():
174 def __init__(self, root, unit):
175 self.root = root
176 self.unit = unit
177 self.config = None
178
179 def _path_for_unit(self, unit):
180 for location in locations:
181 path = self.root / location / "system" / unit
Andrew Geissler82c905d2020-04-13 13:39:40 -0500182 if path.exists() or path.is_symlink():
Brad Bishopc342db32019-05-15 21:57:59 -0400183 return path
184
185 raise SystemdUnitNotFoundError(self.root, unit)
186
187 def _process_deps(self, config, service, location, prop, dirstem):
188 systemdir = self.root / SYSCONFDIR / "systemd" / "system"
189
190 target = ROOT / location.relative_to(self.root)
191 try:
192 for dependent in config.get('Install', prop):
193 wants = systemdir / "{}.{}".format(dependent, dirstem) / service
194 add_link(wants, target)
195
196 except KeyError:
197 pass
198
Andrew Geissler5199d832021-09-24 16:47:35 -0500199 def enable(self, caller_unit=None):
Brad Bishopc342db32019-05-15 21:57:59 -0400200 # if we're enabling an instance, first extract the actual instance
201 # then figure out what the template unit is
202 template = re.match(r"[^@]+@(?P<instance>[^\.]*)\.", self.unit)
Andrew Geissler615f2f12022-07-15 14:00:58 -0500203 instance_unit_name = None
Brad Bishopc342db32019-05-15 21:57:59 -0400204 if template:
205 instance = template.group('instance')
Andrew Geissler615f2f12022-07-15 14:00:58 -0500206 if instance != "":
207 instance_unit_name = self.unit
Brad Bishopc342db32019-05-15 21:57:59 -0400208 unit = re.sub(r"@[^\.]*\.", "@.", self.unit, 1)
209 else:
210 instance = None
211 unit = self.unit
212
213 path = self._path_for_unit(unit)
214
215 if path.is_symlink():
216 # ignore aliases
217 return
218
Andrew Geissler615f2f12022-07-15 14:00:58 -0500219 config = SystemdFile(self.root, path, instance_unit_name)
Brad Bishopc342db32019-05-15 21:57:59 -0400220 if instance == "":
221 try:
222 default_instance = config.get('Install', 'DefaultInstance')[0]
223 except KeyError:
224 # no default instance, so nothing to enable
225 return
226
227 service = self.unit.replace("@.",
228 "@{}.".format(default_instance))
229 else:
230 service = self.unit
231
232 self._process_deps(config, service, path, 'WantedBy', 'wants')
233 self._process_deps(config, service, path, 'RequiredBy', 'requires')
234
235 try:
236 for also in config.get('Install', 'Also'):
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700237 try:
Andrew Geissler5199d832021-09-24 16:47:35 -0500238 if caller_unit != also:
239 SystemdUnit(self.root, also).enable(unit)
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700240 except SystemdUnitNotFoundError as e:
241 sys.exit("Error: Systemctl also enable issue with %s (%s)" % (service, e.unit))
Brad Bishopc342db32019-05-15 21:57:59 -0400242
243 except KeyError:
244 pass
245
246 systemdir = self.root / SYSCONFDIR / "systemd" / "system"
247 target = ROOT / path.relative_to(self.root)
248 try:
249 for dest in config.get('Install', 'Alias'):
250 alias = systemdir / dest
251 add_link(alias, target)
252
253 except KeyError:
254 pass
255
256 def mask(self):
257 systemdir = self.root / SYSCONFDIR / "systemd" / "system"
258 add_link(systemdir / self.unit, "/dev/null")
259
260
261def collect_services(root):
262 """Collect list of service files"""
263 services = set()
264 for location in locations:
265 paths = (root / location / "system").glob("*")
266 for path in paths:
267 if path.is_dir():
268 continue
269 services.add(path.name)
270
271 return services
272
273
274def preset_all(root):
275 presets = Presets('system-preset', root)
276 services = collect_services(root)
277
278 for service in services:
279 state = presets.state(service)
280
281 if state == "enable" or state is None:
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700282 try:
283 SystemdUnit(root, service).enable()
284 except SystemdUnitNotFoundError:
285 sys.exit("Error: Systemctl preset_all issue in %s" % service)
Brad Bishopc342db32019-05-15 21:57:59 -0400286
287 # If we populate the systemd links we also create /etc/machine-id, which
288 # allows systemd to boot with the filesystem read-only before generating
289 # a real value and then committing it back.
290 #
291 # For the stateless configuration, where /etc is generated at runtime
292 # (for example on a tmpfs), this script shouldn't run at all and we
293 # allow systemd to completely populate /etc.
294 (root / SYSCONFDIR / "machine-id").touch()
295
296
297def main():
298 if sys.version_info < (3, 4, 0):
299 sys.exit("Python 3.4 or greater is required")
300
301 parser = argparse.ArgumentParser()
Andrew Geissler09209ee2020-12-13 08:44:15 -0600302 parser.add_argument('command', nargs='?', choices=['enable', 'mask',
Brad Bishopc342db32019-05-15 21:57:59 -0400303 'preset-all'])
304 parser.add_argument('service', nargs=argparse.REMAINDER)
305 parser.add_argument('--root')
306 parser.add_argument('--preset-mode',
307 choices=['full', 'enable-only', 'disable-only'],
308 default='full')
309
310 args = parser.parse_args()
311
312 root = Path(args.root) if args.root else ROOT
313
314 locations.append(SYSCONFDIR / "systemd")
315 # Handle the usrmerge case by ignoring /lib when it's a symlink
316 if not (root / BASE_LIBDIR).is_symlink():
317 locations.append(BASE_LIBDIR / "systemd")
318 locations.append(LIBDIR / "systemd")
319
Andrew Geissler09209ee2020-12-13 08:44:15 -0600320 command = args.command
321 if not command:
322 parser.print_help()
323 return 0
324
Brad Bishopc342db32019-05-15 21:57:59 -0400325 if command == "mask":
326 for service in args.service:
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700327 try:
328 SystemdUnit(root, service).mask()
329 except SystemdUnitNotFoundError as e:
330 sys.exit("Error: Systemctl main mask issue in %s (%s)" % (service, e.unit))
Brad Bishopc342db32019-05-15 21:57:59 -0400331 elif command == "enable":
332 for service in args.service:
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700333 try:
334 SystemdUnit(root, service).enable()
335 except SystemdUnitNotFoundError as e:
336 sys.exit("Error: Systemctl main enable issue in %s (%s)" % (service, e.unit))
Brad Bishopc342db32019-05-15 21:57:59 -0400337 elif command == "preset-all":
338 if len(args.service) != 0:
339 sys.exit("Too many arguments.")
340 if args.preset_mode != "enable-only":
341 sys.exit("Only enable-only is supported as preset-mode.")
342 preset_all(root)
343 else:
344 raise RuntimeError()
345
346
347if __name__ == '__main__':
348 main()