1# Common code for systemd based services.
2#
3# Prior to inheriting this class, recipes can define services like this:
4#
5# SYSTEMD_SERVICE:${PN} = "foo.service bar.socket baz@.service"
6#
7# and these files will be added to the main package if they exist.
8#
9# Alternatively this class can just be inherited and
10# ${PN}.service will be added to the main package.
11#
12# Other variables:
13# INHIBIT_SYSTEMD_RESTART_POLICY_${unit}
14#    Inhibit the warning that is displayed if a service unit without a
15#    restart policy is detected.
16#
17# SYSTEMD_SUBSTITUTIONS = "var:val:file"
18#    A specification for making python style {format} string
19#    substitutions where:
20#      var: the format string to search for
21#      val: the value to replace with
22#      file: the file in which to make the substitution
23#
24# SYSTEMD_USER_${PN}.service = "foo"
25# SYSTEMD_USER_${unit}.service = "foo"
26#    The user for the unit/package.
27#
28# SYSTEMD_ENVIRONMENT_FILE:${PN} = "foo"
29#    One or more environment files to be installed.
30#
31# SYSTEMD_LINK:${PN} = "tgt:name"
32#    A specification for installing arbitrary links in
33#    the ${systemd_system_unitdir} namespace, where:
34#      tgt: the link target
35#      name: the link name, relative to ${systemd_system_unitdir}
36#
37# SYSTEMD_OVERRIDE:${PN} = "src:dest"
38#    A specification for installing unit overrides where:
39#      src: the override file template
40#      dest: the override install location, relative to ${systemd_system_unitdir}
41#
42#    Typically SYSTEMD_SUBSTITUTIONS is used to deploy a range
43#    of overrides from a single template file.  To simply install
44#    a single override use "foo.conf:my-service.d/foo.conf"
45
46
47inherit obmc-phosphor-utils
48inherit systemd
49inherit useradd
50
51_INSTALL_SD_UNITS=""
52SYSTEMD_DEFAULT_TARGET ?= "multi-user.target"
53envfiledir ?= "${sysconfdir}/default"
54
55# Big ugly hack to prevent useradd.bbclass post-parse sanity checker failure.
56# If there are users to be added, we'll add them in our post-parse.
57# If not...there don't seem to be any ill effects...
58USERADD_PACKAGES ?= " "
59USERADD_PARAM:${PN} ?= ";"
60
61
62def SystemdUnit(unit):
63    class Unit(object):
64        def __init__(self, unit):
65            self.unit = unit
66
67        def __getattr__(self, item):
68            if item == 'name':
69                return self.unit
70            if item == 'is_activated':
71                return self.unit.startswith('dbus-')
72            if item == 'is_template':
73                return '@.' in self.unit
74            if item == 'is_instance':
75                return '@' in self.unit and not self.is_template
76            if item in ['is_service', 'is_target']:
77                return self.unit.split('.')[-1] == item
78            if item == 'base':
79                cls = self.unit.split('.')[-1]
80                base = self.unit.replace('dbus-', '')
81                base = base.replace('.%s' % cls, '')
82                if self.is_instance:
83                    base = base.replace('@%s' % self.instance, '')
84                if self.is_template:
85                    base = base.rstrip('@')
86                return base
87            if item == 'instance' and self.is_instance:
88                inst = self.unit.rsplit('@')[-1]
89                return inst.rsplit('.')[0]
90            if item == 'template' and self.is_instance:
91                cls = self.unit.split('.')[-1]
92                return '%s@.%s' % (self.base, cls)
93            if item == 'template' and self.is_template:
94                return '.'.join(self.base.split('@')[:-1])
95
96            raise AttributeError(item)
97    return Unit(unit)
98
99
100def systemd_parse_unit(d, path):
101    import configparser
102    parser = configparser.ConfigParser(strict=False)
103    parser.optionxform = str
104    parser.read('%s' % path)
105    return parser
106
107
108python() {
109    def check_sd_unit(d, unit):
110        searchpaths = d.getVar('FILESPATH', True)
111        path = bb.utils.which(searchpaths, '%s' % unit.name)
112        if not os.path.isfile(path):
113            # Unit does not exist in tree. Allow it to install from repo.
114            # Return False here to indicate it does not exist.
115            return False
116
117        parser = systemd_parse_unit(d, path)
118        inhibit = listvar_to_list(d, 'INHIBIT_SYSTEMD_RESTART_POLICY_WARNING')
119        if unit.is_service and \
120                not unit.is_template and \
121                unit.name not in inhibit and \
122                not parser.has_option('Service', 'Restart'):
123            bb.warn('Systemd unit \'%s\' does not '
124                'have a restart policy defined.' % unit.name)
125        return True
126
127
128    def add_default_subs(d, file):
129        for x in [
130                'base_bindir',
131                'bindir',
132                'sbindir',
133                'libexecdir',
134                'envfiledir',
135                'sysconfdir',
136                'localstatedir',
137                'datadir',
138                'SYSTEMD_DEFAULT_TARGET' ]:
139            set_doappend(d, 'SYSTEMD_SUBSTITUTIONS',
140                '%s:%s:%s' % (x, d.getVar(x, True), file))
141
142
143    def add_sd_unit(d, unit, pkg, unit_exist):
144        # Do not add unit if it does not exist in tree.
145        # It will be installed from repo.
146        if not unit_exist:
147            return
148
149        name = unit.name
150        unit_dir = d.getVar('systemd_system_unitdir', True)
151        set_doappend(d, 'SRC_URI', 'file://%s' % name)
152        set_doappend(d, 'FILES:%s' % pkg, '%s/%s' % (unit_dir, name))
153        set_doappend(d, '_INSTALL_SD_UNITS', name)
154        add_default_subs(d, name)
155
156
157    def add_sd_user(d, file, pkg):
158        opts = [
159            '--system',
160            '--home',
161            '/',
162            '--no-create-home',
163            '--shell /sbin/nologin',
164            '--user-group']
165
166        var = 'SYSTEMD_USER_%s' % file
167        user = listvar_to_list(d, var)
168        if len(user) == 0:
169            var = 'SYSTEMD_USER_%s' % pkg
170            user = listvar_to_list(d, var)
171        if len(user) != 0:
172            if len(user) != 1:
173                bb.fatal('Too many users assigned to %s: \'%s\'' % (var, ' '.join(user)))
174
175            user = user[0]
176            set_doappend(d, 'SYSTEMD_SUBSTITUTIONS',
177                'USER:%s:%s' % (user, file))
178            if user not in d.getVar('USERADD_PARAM:%s' % pkg, True):
179                set_doappend(
180                    d,
181                    'USERADD_PARAM:%s' % pkg,
182                    '%s' % (' '.join(opts + [user])),
183                    ';')
184            if pkg not in d.getVar('USERADD_PACKAGES', True):
185                set_doappend(d, 'USERADD_PACKAGES', pkg)
186
187
188    def add_env_file(d, name, pkg):
189        set_doappend(d, 'SRC_URI', 'file://%s' % name)
190        set_doappend(d, 'FILES:%s' % pkg, '%s/%s' \
191            % (d.getVar('envfiledir', True), name))
192        set_doappend(d, '_INSTALL_ENV_FILES', name)
193
194
195    def install_link(d, spec, pkg):
196        tgt, dest = spec.split(':')
197
198        set_doappend(d, 'FILES:%s' % pkg, '%s/%s' \
199            % (d.getVar('systemd_system_unitdir', True), dest))
200        set_doappend(d, '_INSTALL_LINKS', spec)
201
202
203    def add_override(d, spec, pkg):
204        tmpl, dest = spec.split(':')
205        set_doappend(d, '_INSTALL_OVERRIDES', '%s' % spec)
206        unit_dir = d.getVar('systemd_system_unitdir', True)
207        set_doappend(d, 'FILES:%s' % pkg, '%s/%s' % (unit_dir, dest))
208        add_default_subs(d, '%s' % dest)
209        add_sd_user(d, '%s' % dest, pkg)
210
211
212    if d.getVar('CLASSOVERRIDE', True) != 'class-target':
213        return
214
215    d.appendVarFlag('do_install', 'postfuncs', ' systemd_do_postinst')
216
217    pn = d.getVar('PN', True)
218    if d.getVar('SYSTEMD_SERVICE:%s' % pn, True) is None:
219        d.setVar('SYSTEMD_SERVICE:%s' % pn, '%s.service' % pn)
220
221    for pkg in listvar_to_list(d, 'SYSTEMD_PACKAGES'):
222        svc = listvar_to_list(d, 'SYSTEMD_SERVICE:%s' % pkg)
223        svc = [SystemdUnit(x) for x in svc]
224        tmpl = [x.template for x in svc if x.is_instance]
225        tmpl = list(set(tmpl))
226        tmpl = [SystemdUnit(x) for x in tmpl]
227        svc = [x for x in svc if not x.is_instance]
228
229        for unit in tmpl + svc:
230            unit_exist = check_sd_unit(d, unit)
231            add_sd_unit(d, unit, pkg, unit_exist)
232            add_sd_user(d, unit.name, pkg)
233        for name in listvar_to_list(d, 'SYSTEMD_ENVIRONMENT_FILE:%s' % pkg):
234            add_env_file(d, name, pkg)
235        for spec in listvar_to_list(d, 'SYSTEMD_LINK:%s' % pkg):
236            install_link(d, spec, pkg)
237        for spec in listvar_to_list(d, 'SYSTEMD_OVERRIDE:%s' % pkg):
238            add_override(d, spec, pkg)
239}
240
241
242python systemd_do_postinst() {
243    def make_subs(d):
244        all_subs = {}
245        for spec in listvar_to_list(d, 'SYSTEMD_SUBSTITUTIONS'):
246            spec, file = spec.rsplit(':', 1)
247            all_subs.setdefault(file, []).append(spec)
248
249        for f, v in all_subs.items():
250            subs = dict([ x.split(':') for x in v])
251            if not subs:
252                continue
253
254            path = d.getVar('D', True)
255            path += d.getVar('systemd_system_unitdir', True)
256            path += '/%s' % f
257            with open(path, 'r') as fd:
258                content = fd.read()
259            with open(path, 'w+') as fd:
260                try:
261                    fd.write(content.format(**subs))
262                except KeyError as e:
263                    bb.fatal('No substitution found for %s in '
264                        'file \'%s\'' % (e, f))
265
266
267    def install_envs(d):
268        install_dir = d.getVar('D', True)
269        install_dir += d.getVar('envfiledir', True)
270        searchpaths = d.getVar('FILESPATH', True)
271
272        for f in listvar_to_list(d, '_INSTALL_ENV_FILES'):
273            src = bb.utils.which(searchpaths, f)
274            if not os.path.isfile(src):
275                bb.fatal('Did not find SYSTEMD_ENVIRONMENT_FILE:'
276                    '\'%s\'' % src)
277
278            dest = os.path.join(install_dir, f)
279            parent = os.path.dirname(dest)
280            if not os.path.exists(parent):
281                os.makedirs(parent)
282
283            with open(src, 'r') as fd:
284                content = fd.read()
285            with open(dest, 'w+') as fd:
286                fd.write(content)
287
288
289    def install_links(d):
290        install_dir = d.getVar('D', True)
291        install_dir += d.getVar('systemd_system_unitdir', True)
292
293        for spec in listvar_to_list(d, '_INSTALL_LINKS'):
294            tgt, dest = spec.split(':')
295            dest = os.path.join(install_dir, dest)
296            parent = os.path.dirname(dest)
297            if not os.path.exists(parent):
298                os.makedirs(parent)
299            os.symlink(tgt, dest)
300
301
302    def install_overrides(d):
303        install_dir = d.getVar('D', True)
304        install_dir += d.getVar('systemd_system_unitdir', True)
305        searchpaths = d.getVar('FILESPATH', True)
306
307        for spec in listvar_to_list(d, '_INSTALL_OVERRIDES'):
308            tmpl, dest = spec.split(':')
309            source = bb.utils.which(searchpaths, tmpl)
310            if not os.path.isfile(source):
311                bb.fatal('Did not find SYSTEMD_OVERRIDE '
312                    'template: \'%s\'' % source)
313
314            dest = os.path.join(install_dir, dest)
315            parent = os.path.dirname(dest)
316            if not os.path.exists(parent):
317                os.makedirs(parent)
318
319            with open(source, 'r') as fd:
320                content = fd.read()
321            with open('%s' % dest, 'w+') as fd:
322                fd.write(content)
323
324
325    install_links(d)
326    install_envs(d)
327    install_overrides(d)
328    make_subs(d)
329}
330
331
332do_install:append() {
333        # install systemd service/socket/template files
334        [ -z "${_INSTALL_SD_UNITS}" ] || \
335                install -d ${D}${systemd_system_unitdir}
336        for s in ${_INSTALL_SD_UNITS}; do
337                install -m 0644 ${WORKDIR}/$s \
338                        ${D}${systemd_system_unitdir}/$s
339                sed -i -e 's,@BASE_BINDIR@,${base_bindir},g' \
340                        -e 's,@BINDIR@,${bindir},g' \
341                        -e 's,@SBINDIR@,${sbindir},g' \
342                        -e 's,@LIBEXECDIR@,${libexecdir},g' \
343                        -e 's,@LOCALSTATEDIR@,${localstatedir},g' \
344                        -e 's,@DATADIR@,${datadir},g' \
345                        ${D}${systemd_system_unitdir}/$s
346        done
347}
348