diff options
author | Alex Kiernan <alex.kiernan@gmail.com> | 2019-05-02 22:09:43 +0100 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2019-05-03 06:11:57 +0100 |
commit | 925e30cb104ece7bfa48b78144e758a46dc9ec3f (patch) | |
tree | 178a5aa62d5fbefb8f7821ed8a230285b4c95adb /meta/recipes-core | |
parent | bc2ca0ea7e917d1e6b31d905eca77550a2907610 (diff) | |
download | poky-925e30cb104ece7bfa48b78144e758a46dc9ec3f.tar.gz |
systemctl-native: Rewrite in Python supporting preset-all and mask
Rewrite systemctl-native in Python so that extending/testing it is
easier.
Now that the systemd class sets up service presets instead of actively
enabling services, the 'enable' and 'disable' subcommands for systemctl
are not actually used anywhere. As such, we can remove these to make
sure that nobody inadvertently introduces new uses of them.
This implementation covers `preset-all` and `mask` which are the only
options used in the current code, but should be readily extensible to
other commands.
We use `preset-all` at image construction time to populate the symlinks
used by systemd.
(From OE-Core rev: 86f5a2383692ac1ab01dce534c1a5c5f32ec4b35)
Signed-off-by: Alex Kiernan <alex.kiernan@gmail.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/recipes-core')
-rwxr-xr-x | meta/recipes-core/systemd/systemd-systemctl/systemctl | 476 |
1 files changed, 280 insertions, 196 deletions
diff --git a/meta/recipes-core/systemd/systemd-systemctl/systemctl b/meta/recipes-core/systemd/systemd-systemctl/systemctl index 2bc6489617..d7d4e0d29a 100755 --- a/meta/recipes-core/systemd/systemd-systemctl/systemctl +++ b/meta/recipes-core/systemd/systemd-systemctl/systemctl | |||
@@ -1,196 +1,280 @@ | |||
1 | #!/bin/sh | 1 | #!/usr/bin/env python3 |
2 | echo "Started $0 $*" | 2 | """systemctl: subset of systemctl used for image construction |
3 | 3 | ||
4 | ROOT= | 4 | Mask/preset systemd units |
5 | 5 | """ | |
6 | # parse command line params | 6 | |
7 | action= | 7 | import argparse |
8 | while [ $# != 0 ]; do | 8 | import fnmatch |
9 | opt="$1" | 9 | import os |
10 | 10 | import re | |
11 | case "$opt" in | 11 | import sys |
12 | enable) | 12 | |
13 | shift | 13 | from collections import namedtuple |
14 | 14 | from pathlib import Path | |
15 | action="$opt" | 15 | |
16 | services="$1" | 16 | version = 1.0 |
17 | cmd_args="1" | 17 | |
18 | shift | 18 | ROOT = Path("/") |
19 | ;; | 19 | SYSCONFDIR = Path("etc") |
20 | disable) | 20 | BASE_LIBDIR = Path("lib") |
21 | shift | 21 | LIBDIR = Path("usr", "lib") |
22 | 22 | ||
23 | action="$opt" | 23 | |
24 | services="$1" | 24 | class SystemdFile(): |
25 | cmd_args="1" | 25 | """Class representing a single systemd configuration file""" |
26 | shift | 26 | def __init__(self, root, path): |
27 | ;; | 27 | self.sections = dict() |
28 | mask) | 28 | self._parse(root, path) |
29 | shift | 29 | |
30 | 30 | def _parse(self, root, path): | |
31 | action="$opt" | 31 | """Parse a systemd syntax configuration file |
32 | services="$1" | 32 | |
33 | cmd_args="1" | 33 | Args: |
34 | shift | 34 | path: A pathlib.Path object pointing to the file |
35 | ;; | 35 | |
36 | preset) | 36 | """ |
37 | shift | 37 | skip_re = re.compile(r"^\s*([#;]|$)") |
38 | 38 | section_re = re.compile(r"^\s*\[(?P<section>.*)\]") | |
39 | action="$opt" | 39 | kv_re = re.compile(r"^\s*(?P<key>[^\s]+)\s*=\s*(?P<value>.*)") |
40 | services="$1" | 40 | section = None |
41 | cmd_args="1" | 41 | |
42 | shift | 42 | if path.is_symlink(): |
43 | ;; | 43 | try: |
44 | --root=*) | 44 | path.resolve() |
45 | ROOT=${opt##--root=} | 45 | except FileNotFoundError: |
46 | cmd_args="0" | 46 | # broken symlink, try relative to root |
47 | shift | 47 | path = root / Path(os.readlink(str(path))).relative_to(ROOT) |
48 | ;; | 48 | |
49 | *) | 49 | with path.open() as f: |
50 | if [ "$cmd_args" = "1" ]; then | 50 | for line in f: |
51 | services="$services $opt" | 51 | if skip_re.match(line): |
52 | shift | 52 | continue |
53 | else | 53 | |
54 | echo "'$opt' is an unkown option; exiting with error" | 54 | line = line.rstrip("\n") |
55 | exit 1 | 55 | m = section_re.match(line) |
56 | fi | 56 | if m: |
57 | ;; | 57 | section = dict() |
58 | esac | 58 | self.sections[m.group('section')] = section |
59 | done | 59 | continue |
60 | if [ "$action" = "preset" -a "$service_file" = "" ]; then | 60 | |
61 | services=$(for f in `find $ROOT/etc/systemd/system $ROOT/lib/systemd/system $ROOT/usr/lib/systemd/system -type f 2>1`; do basename $f; done) | 61 | while line.endswith("\\"): |
62 | services="$services $opt" | 62 | line += f.readline().rstrip("\n") |
63 | presetall=1 | 63 | |
64 | fi | 64 | m = kv_re.match(line) |
65 | 65 | k = m.group('key') | |
66 | for service in $services; do | 66 | v = m.group('value') |
67 | if [ "$presetall" = "1" ]; then | 67 | if k not in section: |
68 | action="preset" | 68 | section[k] = list() |
69 | fi | 69 | section[k].extend(v.split()) |
70 | if [ "$action" = "mask" ]; then | 70 | |
71 | if [ ! -d $ROOT/etc/systemd/system/ ]; then | 71 | def get(self, section, prop): |
72 | mkdir -p $ROOT/etc/systemd/system/ | 72 | """Get a property from section |
73 | fi | 73 | |
74 | cmd="ln -s /dev/null $ROOT/etc/systemd/system/$service" | 74 | Args: |
75 | echo "$cmd" | 75 | section: Section to retrieve property from |
76 | $cmd | 76 | prop: Property to retrieve |
77 | exit 0 | 77 | |
78 | fi | 78 | Returns: |
79 | 79 | List representing all properties of type prop in section. | |
80 | service_base_file=`echo $service | sed 's/\(@\).*\(\.[^.]\+\)/\1\2/'` | 80 | |
81 | if [ -z `echo $service | sed '/@/p;d'` ]; then | 81 | Raises: |
82 | echo "Try to find location of $service..." | 82 | KeyError: if ``section`` or ``prop`` not found |
83 | service_template=false | 83 | """ |
84 | else | 84 | return self.sections[section][prop] |
85 | echo "Try to find location of template $service_base_file of instance $service..." | 85 | |
86 | service_template=true | 86 | |
87 | instance_specified=`echo $service | sed 's/^.\+@\(.*\)\.[^.]\+/\1/'` | 87 | class Presets(): |
88 | fi | 88 | """Class representing all systemd presets""" |
89 | 89 | def __init__(self, scope, root): | |
90 | # find service file | 90 | self.directives = list() |
91 | for p in $ROOT/etc/systemd/system \ | 91 | self._collect_presets(scope, root) |
92 | $ROOT/lib/systemd/system \ | 92 | |
93 | $ROOT/usr/lib/systemd/system; do | 93 | def _parse_presets(self, presets): |
94 | if [ -e $p/$service_base_file ]; then | 94 | """Parse presets out of a set of preset files""" |
95 | service_file=$p/$service_base_file | 95 | skip_re = re.compile(r"^\s*([#;]|$)") |
96 | service_file=${service_file##$ROOT} | 96 | directive_re = re.compile(r"^\s*(?P<action>enable|disable)\s+(?P<unit_name>(.+))") |
97 | fi | 97 | |
98 | done | 98 | Directive = namedtuple("Directive", "action unit_name") |
99 | if [ -z "$service_file" ]; then | 99 | for preset in presets: |
100 | echo "'$service_base_file' couldn't be found; exiting with error" | 100 | with preset.open() as f: |
101 | exit 1 | 101 | for line in f: |
102 | fi | 102 | m = directive_re.match(line) |
103 | echo "Found $service in $service_file" | 103 | if m: |
104 | 104 | directive = Directive(action=m.group('action'), | |
105 | # If any new unit types are added to systemd they should be added | 105 | unit_name=m.group('unit_name')) |
106 | # to this regular expression. | 106 | self.directives.append(directive) |
107 | unit_types_re='\.\(service\|socket\|device\|mount\|automount\|swap\|target\|target\.wants\|path\|timer\|snapshot\)\s*$' | 107 | elif skip_re.match(line): |
108 | if [ "$action" = "preset" ]; then | 108 | pass |
109 | action=`egrep -sh $service $ROOT/etc/systemd/user-preset/*.preset | cut -f1 -d' '` | 109 | else: |
110 | if [ -z "$action" ]; then | 110 | sys.exit("Unparsed preset line in {}".format(preset)) |
111 | globalpreset=`egrep -sh '\*' $ROOT/etc/systemd/user-preset/*.preset | cut -f1 -d' '` | 111 | |
112 | if [ -n "$globalpreset" ]; then | 112 | def _collect_presets(self, scope, root): |
113 | action="$globalpreset" | 113 | """Collect list of preset files""" |
114 | else | 114 | locations = [SYSCONFDIR / "systemd"] |
115 | action="enable" | 115 | # Handle the usrmerge case by ignoring /lib when it's a symlink |
116 | fi | 116 | if not BASE_LIBDIR.is_symlink(): |
117 | fi | 117 | locations.append(BASE_LIBDIR / "systemd") |
118 | fi | 118 | locations.append(LIBDIR / "systemd") |
119 | # create the required symbolic links | 119 | |
120 | wanted_by=$(sed '/^WantedBy[[:space:]]*=/s,[^=]*=,,p;d' "$ROOT/$service_file" \ | 120 | presets = dict() |
121 | | tr ',' '\n' \ | 121 | for location in locations: |
122 | | grep "$unit_types_re") | 122 | paths = (root / location / scope).glob("*.preset") |
123 | 123 | for path in paths: | |
124 | required_by=$(sed '/^RequiredBy[[:space:]]*=/s,[^=]*=,,p;d' "$ROOT/$service_file" \ | 124 | # earlier names override later ones |
125 | | tr ',' '\n' \ | 125 | if path.name not in presets: |
126 | | grep "$unit_types_re") | 126 | presets[path.name] = path |
127 | 127 | ||
128 | for dependency in WantedBy RequiredBy; do | 128 | self._parse_presets([v for k, v in sorted(presets.items())]) |
129 | if [ "$dependency" = "WantedBy" ]; then | 129 | |
130 | suffix="wants" | 130 | def state(self, unit_name): |
131 | dependency_list="$wanted_by" | 131 | """Return state of preset for unit_name |
132 | elif [ "$dependency" = "RequiredBy" ]; then | 132 | |
133 | suffix="requires" | 133 | Args: |
134 | dependency_list="$required_by" | 134 | presets: set of presets |
135 | fi | 135 | unit_name: name of the unit |
136 | for r in $dependency_list; do | 136 | |
137 | echo "$dependency=$r found in $service" | 137 | Returns: |
138 | if [ -n "$instance_specified" ]; then | 138 | None: no matching preset |
139 | # substitute wildcards in the dependency | 139 | `enable`: unit_name is enabled |
140 | r=`echo $r | sed "s/%i/$instance_specified/g"` | 140 | `disable`: unit_name is disabled |
141 | fi | 141 | """ |
142 | 142 | for directive in self.directives: | |
143 | if [ "$action" = "enable" ]; then | 143 | if fnmatch.fnmatch(unit_name, directive.unit_name): |
144 | enable_service=$service | 144 | return directive.action |
145 | if [ "$service_template" = true -a -z "$instance_specified" ]; then | 145 | |
146 | default_instance=$(sed '/^DefaultInstance[[:space:]]*=/s,[^=]*=,,p;d' "$ROOT/$service_file") | 146 | return None |
147 | if [ -z $default_instance ]; then | 147 | |
148 | echo "Template unit without instance or DefaultInstance directive, nothing to enable" | 148 | |
149 | continue | 149 | def collect_services(root): |
150 | else | 150 | """Collect list of service files""" |
151 | echo "Found DefaultInstance $default_instance, enabling it" | 151 | locations = [SYSCONFDIR / "systemd"] |
152 | enable_service=$(echo $service | sed "s/@/@$(echo $default_instance | sed 's/\\/\\\\/g')/") | 152 | # Handle the usrmerge case by ignoring /lib when it's a symlink |
153 | fi | 153 | if not BASE_LIBDIR.is_symlink(): |
154 | fi | 154 | locations.append(BASE_LIBDIR / "systemd") |
155 | mkdir -p $ROOT/etc/systemd/system/$r.$suffix | 155 | locations.append(LIBDIR / "systemd") |
156 | ln -s $service_file $ROOT/etc/systemd/system/$r.$suffix/$enable_service | 156 | |
157 | echo "Enabled $enable_service for $r." | 157 | services = dict() |
158 | else | 158 | for location in locations: |
159 | if [ "$service_template" = true -a -z "$instance_specified" ]; then | 159 | paths = (root / location / "system").glob("*") |
160 | disable_service="$ROOT/etc/systemd/system/$r.$suffix/`echo $service | sed 's/@/@*/'`" | 160 | for path in paths: |
161 | else | 161 | if path.is_dir(): |
162 | disable_service="$ROOT/etc/systemd/system/$r.$suffix/$service" | 162 | continue |
163 | fi | 163 | # implement earlier names override later ones |
164 | rm -f $disable_service | 164 | if path.name not in services: |
165 | [ -d $ROOT/etc/systemd/system/$r.$suffix ] && rmdir --ignore-fail-on-non-empty -p $ROOT/etc/systemd/system/$r.$suffix | 165 | services[path.name] = path |
166 | echo "Disabled ${disable_service##$ROOT/etc/systemd/system/$r.$suffix/} for $r." | 166 | |
167 | fi | 167 | return services |
168 | done | 168 | |
169 | done | 169 | |
170 | 170 | def add_link(path, target): | |
171 | # create the required symbolic 'Alias' links | 171 | try: |
172 | alias=$(sed '/^Alias[[:space:]]*=/s,[^=]*=,,p;d' "$ROOT/$service_file" \ | 172 | path.parent.mkdir(parents=True) |
173 | | tr ',' '\n' \ | 173 | except FileExistsError: |
174 | | grep "$unit_types_re") | 174 | pass |
175 | 175 | if not path.is_symlink(): | |
176 | for r in $alias; do | 176 | print("ln -s {} {}".format(target, path)) |
177 | if [ "$action" = "enable" ]; then | 177 | path.symlink_to(target) |
178 | mkdir -p $ROOT/etc/systemd/system | 178 | |
179 | ln -s $service_file $ROOT/etc/systemd/system/$r | 179 | |
180 | echo "Enabled $service for $alias." | 180 | def process_deps(root, config, service, location, prop, dirstem): |
181 | else | 181 | systemdir = SYSCONFDIR / "systemd" / "system" |
182 | rm -f $ROOT/etc/systemd/system/$r | 182 | |
183 | echo "Disabled $service for $alias." | 183 | target = ROOT / location.relative_to(root) |
184 | fi | 184 | try: |
185 | done | 185 | for dependent in config.get('Install', prop): |
186 | 186 | wants = root / systemdir / "{}.{}".format(dependent, dirstem) / service | |
187 | # call us for the other required scripts | 187 | add_link(wants, target) |
188 | also=$(sed '/^Also[[:space:]]*=/s,[^=]*=,,p;d' "$ROOT/$service_file" \ | 188 | |
189 | | tr ',' '\n') | 189 | except KeyError: |
190 | for a in $also; do | 190 | pass |
191 | echo "Also=$a found in $service" | 191 | |
192 | if [ "$action" = "enable" ]; then | 192 | |
193 | $0 --root=$ROOT enable $a | 193 | def enable(root, service, location, services): |
194 | fi | 194 | if location.is_symlink(): |
195 | done | 195 | # ignore aliases |
196 | done | 196 | return |
197 | |||
198 | config = SystemdFile(root, location) | ||
199 | template = re.match(r"[^@]+@(?P<instance>[^\.]*)\.", service) | ||
200 | if template: | ||
201 | instance = template.group('instance') | ||
202 | if not instance: | ||
203 | try: | ||
204 | instance = config.get('Install', 'DefaultInstance')[0] | ||
205 | service = service.replace("@.", "@{}.".format(instance)) | ||
206 | except KeyError: | ||
207 | pass | ||
208 | if instance is None: | ||
209 | return | ||
210 | else: | ||
211 | instance = None | ||
212 | |||
213 | process_deps(root, config, service, location, 'WantedBy', 'wants') | ||
214 | process_deps(root, config, service, location, 'RequiredBy', 'requires') | ||
215 | |||
216 | try: | ||
217 | for also in config.get('Install', 'Also'): | ||
218 | enable(root, also, services[also], services) | ||
219 | |||
220 | except KeyError: | ||
221 | pass | ||
222 | |||
223 | systemdir = root / SYSCONFDIR / "systemd" / "system" | ||
224 | target = ROOT / location.relative_to(root) | ||
225 | try: | ||
226 | for dest in config.get('Install', 'Alias'): | ||
227 | alias = systemdir / dest | ||
228 | add_link(alias, target) | ||
229 | |||
230 | except KeyError: | ||
231 | pass | ||
232 | |||
233 | |||
234 | def preset_all(root): | ||
235 | presets = Presets('system-preset', root) | ||
236 | services = collect_services(root) | ||
237 | |||
238 | for service, location in services.items(): | ||
239 | state = presets.state(service) | ||
240 | |||
241 | if state == "enable" or state is None: | ||
242 | enable(root, service, location, services) | ||
243 | |||
244 | |||
245 | def mask(root, *services): | ||
246 | systemdir = root / SYSCONFDIR / "systemd" / "system" | ||
247 | for service in services: | ||
248 | add_link(systemdir / service, "/dev/null") | ||
249 | |||
250 | |||
251 | def main(): | ||
252 | if sys.version_info < (3, 4, 0): | ||
253 | sys.exit("Python 3.4 or greater is required") | ||
254 | |||
255 | parser = argparse.ArgumentParser() | ||
256 | parser.add_argument('command', nargs=1, choices=['mask', 'preset-all']) | ||
257 | parser.add_argument('service', nargs=argparse.REMAINDER) | ||
258 | parser.add_argument('--root') | ||
259 | parser.add_argument('--preset-mode', | ||
260 | choices=['full', 'enable-only', 'disable-only'], | ||
261 | default='full') | ||
262 | |||
263 | args = parser.parse_args() | ||
264 | |||
265 | root = Path(args.root) if args.root else ROOT | ||
266 | command = args.command[0] | ||
267 | if command == "mask": | ||
268 | mask(root, *args.service) | ||
269 | elif command == "preset-all": | ||
270 | if len(args.service) != 0: | ||
271 | sys.exit("Too many arguments.") | ||
272 | if args.preset_mode != "enable-only": | ||
273 | sys.exit("Only enable-only is supported as preset-mode.") | ||
274 | preset_all(root) | ||
275 | else: | ||
276 | raise RuntimeError() | ||
277 | |||
278 | |||
279 | if __name__ == '__main__': | ||
280 | main() | ||