summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--meta/recipes-core/systemd/systemd-systemctl-native.bb23
-rwxr-xr-xmeta/recipes-core/systemd/systemd-systemctl/systemctl376
2 files changed, 11 insertions, 388 deletions
diff --git a/meta/recipes-core/systemd/systemd-systemctl-native.bb b/meta/recipes-core/systemd/systemd-systemctl-native.bb
index ffa024caef..73862b4e23 100644
--- a/meta/recipes-core/systemd/systemd-systemctl-native.bb
+++ b/meta/recipes-core/systemd/systemd-systemctl-native.bb
@@ -1,17 +1,16 @@
1SUMMARY = "Wrapper for enabling systemd services" 1SUMMARY = "Systemctl executable from systemd"
2 2
3LICENSE = "MIT" 3require systemd.inc
4LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420"
5 4
5DEPENDS = "gperf-native libcap-native util-linux-native python3-jinja2-native"
6 6
7inherit native 7inherit pkgconfig meson native
8 8
9SRC_URI = "file://systemctl" 9MESON_TARGET = "systemctl:executable"
10MESON_INSTALL_TAGS = "systemctl"
11EXTRA_OEMESON:append = " -Dlink-systemctl-shared=false"
10 12
11S = "${WORKDIR}/sources" 13# Systemctl is supposed to operate on target, but the target sysroot is not
12UNPACKDIR = "${S}" 14# determined at run-time, but rather set during configure
13 15# More details are here https://github.com/systemd/systemd/issues/35897#issuecomment-2665405887
14do_install() { 16EXTRA_OEMESON:append = " --sysconfdir ${sysconfdir_native}"
15 install -d ${D}${bindir}
16 install -m 0755 ${S}/systemctl ${D}${bindir}
17}
diff --git a/meta/recipes-core/systemd/systemd-systemctl/systemctl b/meta/recipes-core/systemd/systemd-systemctl/systemctl
deleted file mode 100755
index 65e3157859..0000000000
--- a/meta/recipes-core/systemd/systemd-systemctl/systemctl
+++ /dev/null
@@ -1,376 +0,0 @@
1#!/usr/bin/env python3
2"""systemctl: subset of systemctl used for image construction
3
4Mask/preset systemd units
5"""
6
7import argparse
8import fnmatch
9import os
10import re
11import sys
12
13from collections import namedtuple
14from itertools import chain
15from pathlib import Path
16
17version = 1.0
18
19ROOT = Path("/")
20SYSCONFDIR = Path("etc")
21BASE_LIBDIR = Path("lib")
22LIBDIR = Path("usr", "lib")
23
24locations = list()
25
26
27class SystemdFile():
28 """Class representing a single systemd configuration file"""
29
30 _clearable_keys = ['WantedBy']
31
32 def __init__(self, root, path, instance_unit_name, unit_type):
33 self.sections = dict()
34 self._parse(root, path)
35 dirname = os.path.basename(path.name) + ".d"
36 for location in locations:
37 files = (root / location / unit_type / dirname).glob("*.conf")
38 if instance_unit_name:
39 inst_dirname = instance_unit_name + ".d"
40 files = chain(files, (root / location / unit_type / inst_dirname).glob("*.conf"))
41 for path2 in sorted(files):
42 self._parse(root, path2)
43
44 def _parse(self, root, path):
45 """Parse a systemd syntax configuration file
46
47 Args:
48 path: A pathlib.Path object pointing to the file
49
50 """
51 skip_re = re.compile(r"^\s*([#;]|$)")
52 section_re = re.compile(r"^\s*\[(?P<section>.*)\]")
53 kv_re = re.compile(r"^\s*(?P<key>[^\s]+)\s*=\s*(?P<value>.*)")
54 section = None
55
56 if path.is_symlink():
57 try:
58 path.resolve()
59 except FileNotFoundError:
60 # broken symlink, try relative to root
61 path = root / Path(os.readlink(str(path))).relative_to(ROOT)
62
63 with path.open() as f:
64 for line in f:
65 if skip_re.match(line):
66 continue
67
68 line = line.strip()
69 m = section_re.match(line)
70 if m:
71 if m.group('section') not in self.sections:
72 section = dict()
73 self.sections[m.group('section')] = section
74 else:
75 section = self.sections[m.group('section')]
76 continue
77
78 while line.endswith("\\"):
79 line += f.readline().rstrip("\n")
80
81 m = kv_re.match(line)
82 k = m.group('key')
83 v = m.group('value')
84 if k not in section:
85 section[k] = list()
86
87 # If we come across a "key=" line for a "clearable key", then
88 # forget all preceding assignments. This works because we are
89 # processing files in correct parse order.
90 if k in self._clearable_keys and not v:
91 del section[k]
92 continue
93
94 section[k].extend(v.split())
95
96 def get(self, section, prop):
97 """Get a property from section
98
99 Args:
100 section: Section to retrieve property from
101 prop: Property to retrieve
102
103 Returns:
104 List representing all properties of type prop in section.
105
106 Raises:
107 KeyError: if ``section`` or ``prop`` not found
108 """
109 return self.sections[section][prop]
110
111
112class Presets():
113 """Class representing all systemd presets"""
114 def __init__(self, scope, root):
115 self.directives = list()
116 self._collect_presets(scope, root)
117
118 def _parse_presets(self, presets):
119 """Parse presets out of a set of preset files"""
120 skip_re = re.compile(r"^\s*([#;]|$)")
121 directive_re = re.compile(r"^\s*(?P<action>enable|disable)\s+(?P<unit_name>(.+))")
122
123 Directive = namedtuple("Directive", "action unit_name")
124 for preset in presets:
125 with preset.open() as f:
126 for line in f:
127 m = directive_re.match(line)
128 if m:
129 directive = Directive(action=m.group('action'),
130 unit_name=m.group('unit_name'))
131 self.directives.append(directive)
132 elif skip_re.match(line):
133 pass
134 else:
135 sys.exit("Unparsed preset line in {}".format(preset))
136
137 def _collect_presets(self, scope, root):
138 """Collect list of preset files"""
139 presets = dict()
140 for location in locations:
141 paths = (root / location / scope).glob("*.preset")
142 for path in paths:
143 # earlier names override later ones
144 if path.name not in presets:
145 presets[path.name] = path
146
147 self._parse_presets([v for k, v in sorted(presets.items())])
148
149 def state(self, unit_name):
150 """Return state of preset for unit_name
151
152 Args:
153 presets: set of presets
154 unit_name: name of the unit
155
156 Returns:
157 None: no matching preset
158 `enable`: unit_name is enabled
159 `disable`: unit_name is disabled
160 """
161 for directive in self.directives:
162 if fnmatch.fnmatch(unit_name, directive.unit_name):
163 return directive.action
164
165 return None
166
167
168def add_link(path, target):
169 try:
170 path.parent.mkdir(parents=True)
171 except FileExistsError:
172 pass
173 if not path.is_symlink():
174 print("ln -s {} {}".format(target, path))
175 path.symlink_to(target)
176
177
178class SystemdUnitNotFoundError(Exception):
179 def __init__(self, path, unit):
180 self.path = path
181 self.unit = unit
182
183
184class SystemdUnit():
185 def __init__(self, root, unit, unit_type):
186 self.root = root
187 self.unit = unit
188 self.unit_type = unit_type
189 self.config = None
190
191 def _path_for_unit(self, unit):
192 for location in locations:
193 path = self.root / location / self.unit_type / unit
194 if path.exists() or path.is_symlink():
195 return path
196
197 raise SystemdUnitNotFoundError(self.root, unit)
198
199 def _process_deps(self, config, service, location, prop, dirstem, instance):
200 systemdir = self.root / SYSCONFDIR / "systemd" / self.unit_type
201
202 target = ROOT / location.relative_to(self.root)
203 try:
204 for dependent in config.get('Install', prop):
205 # expand any %i to instance (ignoring escape sequence %%)
206 dependent = re.sub("([^%](%%)*)%i", "\\g<1>{}".format(instance), dependent)
207 wants = systemdir / "{}.{}".format(dependent, dirstem) / service
208 add_link(wants, target)
209
210 except KeyError:
211 pass
212
213 def enable(self, units_enabled=[]):
214 # if we're enabling an instance, first extract the actual instance
215 # then figure out what the template unit is
216 template = re.match(r"[^@]+@(?P<instance>[^\.]*)\.", self.unit)
217 instance_unit_name = None
218 if template:
219 instance = template.group('instance')
220 if instance != "":
221 instance_unit_name = self.unit
222 unit = re.sub(r"@[^\.]*\.", "@.", self.unit, 1)
223 else:
224 instance = None
225 unit = self.unit
226
227 if instance_unit_name is not None:
228 try:
229 # Try first with instance unit name. Systemd allows to create instance unit files
230 # e.g. `gnome-shell@wayland.service` which cause template unit file to be ignored
231 # for the instance for which instance unit file is present. In that case, there may
232 # not be any template file at all.
233 path = self._path_for_unit(instance_unit_name)
234 except SystemdUnitNotFoundError:
235 path = self._path_for_unit(unit)
236 else:
237 path = self._path_for_unit(unit)
238
239 if path.is_symlink():
240 # ignore aliases
241 return
242
243 config = SystemdFile(self.root, path, instance_unit_name, self.unit_type)
244 if instance == "":
245 try:
246 default_instance = config.get('Install', 'DefaultInstance')[0]
247 except KeyError:
248 # no default instance, so nothing to enable
249 return
250
251 service = self.unit.replace("@.",
252 "@{}.".format(default_instance))
253 else:
254 service = self.unit
255
256 self._process_deps(config, service, path, 'WantedBy', 'wants', instance)
257 self._process_deps(config, service, path, 'RequiredBy', 'requires', instance)
258
259 try:
260 for also in config.get('Install', 'Also'):
261 try:
262 units_enabled.append(unit)
263 if also not in units_enabled:
264 SystemdUnit(self.root, also, self.unit_type).enable(units_enabled)
265 except SystemdUnitNotFoundError as e:
266 sys.exit("Error: Systemctl also enable issue with %s (%s)" % (service, e.unit))
267
268 except KeyError:
269 pass
270
271 systemdir = self.root / SYSCONFDIR / "systemd" / self.unit_type
272 target = ROOT / path.relative_to(self.root)
273 try:
274 for dest in config.get('Install', 'Alias'):
275 alias = systemdir / dest
276 add_link(alias, target)
277
278 except KeyError:
279 pass
280
281 def mask(self):
282 systemdir = self.root / SYSCONFDIR / "systemd" / self.unit_type
283 add_link(systemdir / self.unit, "/dev/null")
284
285
286def collect_services(root, unit_type):
287 """Collect list of service files"""
288 services = set()
289 for location in locations:
290 paths = (root / location / unit_type).glob("*")
291 for path in paths:
292 if path.is_dir():
293 continue
294 services.add(path.name)
295
296 return services
297
298
299def preset_all(root, unit_type):
300 presets = Presets('{}-preset'.format(unit_type), root)
301 services = collect_services(root, unit_type)
302
303 for service in services:
304 state = presets.state(service)
305
306 if state == "enable" or state is None:
307 try:
308 SystemdUnit(root, service, unit_type).enable()
309 except SystemdUnitNotFoundError:
310 sys.exit("Error: Systemctl preset_all issue in %s" % service)
311
312 # If we populate the systemd links we also create /etc/machine-id, which
313 # allows systemd to boot with the filesystem read-only before generating
314 # a real value and then committing it back.
315 #
316 # For the stateless configuration, where /etc is generated at runtime
317 # (for example on a tmpfs), this script shouldn't run at all and we
318 # allow systemd to completely populate /etc.
319 (root / SYSCONFDIR / "machine-id").touch()
320
321
322def main():
323 if sys.version_info < (3, 4, 0):
324 sys.exit("Python 3.4 or greater is required")
325
326 parser = argparse.ArgumentParser()
327 parser.add_argument('command', nargs='?', choices=['enable', 'mask',
328 'preset-all'])
329 parser.add_argument('service', nargs=argparse.REMAINDER)
330 parser.add_argument('--root')
331 parser.add_argument('--preset-mode',
332 choices=['full', 'enable-only', 'disable-only'],
333 default='full')
334 parser.add_argument('--global', dest="opt_global", action="store_true", default=False)
335
336 args = parser.parse_args()
337
338 root = Path(args.root) if args.root else ROOT
339
340 locations.append(SYSCONFDIR / "systemd")
341 # Handle the usrmerge case by ignoring /lib when it's a symlink
342 if not (root / BASE_LIBDIR).is_symlink():
343 locations.append(BASE_LIBDIR / "systemd")
344 locations.append(LIBDIR / "systemd")
345
346 command = args.command
347 if not command:
348 parser.print_help()
349 return 0
350
351 unit_type = "user" if args.opt_global else "system"
352
353 if command == "mask":
354 for service in args.service:
355 try:
356 SystemdUnit(root, service, unit_type).mask()
357 except SystemdUnitNotFoundError as e:
358 sys.exit("Error: Systemctl main mask issue in %s (%s)" % (service, e.unit))
359 elif command == "enable":
360 for service in args.service:
361 try:
362 SystemdUnit(root, service, unit_type).enable()
363 except SystemdUnitNotFoundError as e:
364 sys.exit("Error: Systemctl main enable issue in %s (%s)" % (service, e.unit))
365 elif command == "preset-all":
366 if len(args.service) != 0:
367 sys.exit("Too many arguments.")
368 if args.preset_mode != "enable-only":
369 sys.exit("Only enable-only is supported as preset-mode.")
370 preset_all(root, unit_type)
371 else:
372 raise RuntimeError()
373
374
375if __name__ == '__main__':
376 main()