xref: /OK3568_Linux_fs/yocto/poky/meta/recipes-core/systemd/systemd-systemctl/systemctl (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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    def __init__(self, root, path, instance_unit_name):
30        self.sections = dict()
31        self._parse(root, path)
32        dirname = os.path.basename(path.name) + ".d"
33        for location in locations:
34            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):
39                self._parse(root, path2)
40
41    def _parse(self, root, path):
42        """Parse a systemd syntax configuration file
43
44        Args:
45            path: A pathlib.Path object pointing to the file
46
47        """
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
52
53        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)
59
60        with path.open() as f:
61            for line in f:
62                if skip_re.match(line):
63                    continue
64
65                line = line.strip()
66                m = section_re.match(line)
67                if m:
68                    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')]
73                    continue
74
75                while line.endswith("\\"):
76                    line += f.readline().rstrip("\n")
77
78                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())
84
85    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):
168    def __init__(self, path, unit):
169        self.path = path
170        self.unit = unit
171
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
182            if path.exists() or path.is_symlink():
183                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
199    def enable(self, caller_unit=None):
200        # 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)
203        instance_unit_name = None
204        if template:
205            instance = template.group('instance')
206            if instance != "":
207                instance_unit_name = self.unit
208            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
219        config = SystemdFile(self.root, path, instance_unit_name)
220        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'):
237                try:
238                    if caller_unit != also:
239                        SystemdUnit(self.root, also).enable(unit)
240                except SystemdUnitNotFoundError as e:
241                    sys.exit("Error: Systemctl also enable issue with  %s (%s)" % (service, e.unit))
242
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:
282            try:
283                SystemdUnit(root, service).enable()
284            except SystemdUnitNotFoundError:
285                sys.exit("Error: Systemctl preset_all issue in %s" % service)
286
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()
302    parser.add_argument('command', nargs='?', choices=['enable', 'mask',
303                                                     '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
320    command = args.command
321    if not command:
322        parser.print_help()
323        return 0
324
325    if command == "mask":
326        for service in args.service:
327            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))
331    elif command == "enable":
332        for service in args.service:
333            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))
337    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()
349