summaryrefslogtreecommitdiffstats
path: root/meta/recipes-core
diff options
context:
space:
mode:
authorAlex Kiernan <alex.kiernan@gmail.com>2019-05-02 22:09:43 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2019-05-03 06:11:57 +0100
commit925e30cb104ece7bfa48b78144e758a46dc9ec3f (patch)
tree178a5aa62d5fbefb8f7821ed8a230285b4c95adb /meta/recipes-core
parentbc2ca0ea7e917d1e6b31d905eca77550a2907610 (diff)
downloadpoky-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-xmeta/recipes-core/systemd/systemd-systemctl/systemctl476
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
2echo "Started $0 $*" 2"""systemctl: subset of systemctl used for image construction
3 3
4ROOT= 4Mask/preset systemd units
5 5"""
6# parse command line params 6
7action= 7import argparse
8while [ $# != 0 ]; do 8import fnmatch
9 opt="$1" 9import os
10 10import re
11 case "$opt" in 11import sys
12 enable) 12
13 shift 13from collections import namedtuple
14 14from pathlib import Path
15 action="$opt" 15
16 services="$1" 16version = 1.0
17 cmd_args="1" 17
18 shift 18ROOT = Path("/")
19 ;; 19SYSCONFDIR = Path("etc")
20 disable) 20BASE_LIBDIR = Path("lib")
21 shift 21LIBDIR = Path("usr", "lib")
22 22
23 action="$opt" 23
24 services="$1" 24class 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
59done 59 continue
60if [ "$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
64fi 64 m = kv_re.match(line)
65 65 k = m.group('key')
66for 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/'` 87class 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 149def 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 170def 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." 180def 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 193def enable(root, service, location, services):
194 fi 194 if location.is_symlink():
195 done 195 # ignore aliases
196done 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
234def 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
245def mask(root, *services):
246 systemdir = root / SYSCONFDIR / "systemd" / "system"
247 for service in services:
248 add_link(systemdir / service, "/dev/null")
249
250
251def 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
279if __name__ == '__main__':
280 main()