Prerequisites

Explanation

Script and services are placed in a home directory called jobs

  • scripts: /home/jobs/scripts/by-user/${running_username}

  • Systemd service files: /home/jobs/services/by-user/${running_username}

Instructions

Run as user

Instruction number

root

*

  1. create the jobs user and the directories

    useradd -m -s /bin/bash -U jobs
    mkdir -p /home/jobs/{script,services}/by-user
    chown -R jobs:jobs /home/jobs
    chmod -R 070 /home/jobs
    
  2. add the running username to the jobs group

    usermod -aG jobs ${running_username}
    
  3. when you change a service file run the deploy script as root. This deploy script will copy the Systemd files in the appropriate directories and run and enable services and timers. You need to install:

    • Python 3

    • fpyutils (see also the next section)

    to be able to run this script

    /home/jobs/services/deploy.py
      1#!/usr/bin/env python3
      2#
      3# deploy.py
      4#
      5# Copyright (C) 2019-2020 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
      6#
      7# This program is free software: you can redistribute it and/or modify
      8# it under the terms of the GNU General Public License as published by
      9# the Free Software Foundation, either version 3 of the License, or
     10# (at your option) any later version.
     11#
     12# This program is distributed in the hope that it will be useful,
     13# but WITHOUT ANY WARRANTY; without even the implied warranty of
     14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15# GNU General Public License for more details.
     16#
     17# You should have received a copy of the GNU General Public License
     18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
     19r"""Deploy the unit files."""
     20
     21import os
     22import pathlib
     23import shlex
     24import shutil
     25import subprocess
     26
     27import fpyutils
     28
     29SRC_DIR = '/home/jobs/services/by-user'
     30DST_DIR = '/etc/systemd/system'
     31
     32
     33class UserNotRoot(Exception):
     34    r"""The user running the script is not root."""
     35
     36
     37def get_unit_files(start_dir: str = '.', max_depth: int = 0) -> tuple:
     38    r"""Get the file names of the unit files."""
     39    timers = list()
     40    services = list()
     41    p = pathlib.Path(start_dir)
     42
     43    # A file has a fake depth of 1 even if we are at level 0 of the directory tree.
     44    # The reason of this is how the depth is computed below.
     45    max_depth += 1
     46
     47    # Match files and directories.
     48    for f in p.rglob('*'):
     49        if f.is_file() and len(
     50                pathlib.PurePath(f.relative_to(
     51                    pathlib.PurePath(start_dir))).parts) <= max_depth:
     52            if pathlib.PurePath(f).match('*.timer'):
     53                timers.append(f)
     54            if pathlib.PurePath(f).match('*.service'):
     55                services.append(f)
     56
     57    return timers, services
     58
     59
     60def copy_unit_files(unit_files: list, dst_dir: str = DST_DIR):
     61    r"""Copy multiple unit files."""
     62    for f in unit_files:
     63        shutil.copyfile(
     64            str(f), str(pathlib.Path(shlex.quote(dst_dir),
     65                                     shlex.quote(f.name))))
     66
     67
     68def start_and_enable_unit(unit_name: str, unit_type: str):
     69    r"""Start and enable services or timers."""
     70    if unit_type not in ['service', 'timer']:
     71        raise ValueError
     72
     73    print('unit: ' + unit_name + '.' + unit_type)
     74    o1 = subprocess.run(shlex.split('systemctl enable ' +
     75                                    shlex.quote(unit_name) + '.' + unit_type),
     76                        check=True,
     77                        capture_output=True).stderr.decode('UTF-8').strip()
     78    if o1 != str():
     79        print(o1)
     80
     81    status = subprocess.run(
     82        shlex.split('systemctl is-enabled ' + shlex.quote(unit_name) + '.' +
     83                    unit_type),
     84        check=True,
     85        capture_output=True).stdout.decode('UTF-8').strip()
     86    disable = True
     87    if status in ['enabled', 'enabled-runtime']:
     88        disable = False
     89    elif status in ['static']:
     90        # Completely disable units without the '[Install]' section.
     91        disable = True
     92
     93    if disable:
     94        try:
     95            o2 = subprocess.run(
     96                shlex.split('systemctl stop ' + shlex.quote(unit_name) + '.' +
     97                            unit_type),
     98                check=True,
     99                capture_output=True).stderr.decode('UTF-8').strip()
    100            if o2 != str():
    101                print(o2)
    102            o3 = subprocess.run(
    103                shlex.split('systemctl disable ' + shlex.quote(unit_name) +
    104                            '.' + unit_type),
    105                check=True,
    106                capture_output=True).stderr.decode('UTF-8').strip()
    107            if o3 != str():
    108                print(o3)
    109        except subprocess.CalledProcessError:
    110            # A new template unit, the ones with 'name@.service' as filename,
    111            # without the '[Install]' section cannot be stopped nor disabled.
    112            # See https://wiki.archlinux.org/index.php/Systemd#Using_units
    113            pass
    114    else:
    115        o2 = subprocess.run(
    116            shlex.split('systemctl start ' + shlex.quote(unit_name) + '.' +
    117                        unit_type),
    118            check=True,
    119            capture_output=True).stderr.decode('UTF-8').strip()
    120        if o2 != str():
    121            print(o2)
    122
    123
    124def start_and_enable_units(services: list, timers: list):
    125    r"""Start and enable all services and timers."""
    126    # Not all services have timer files but all timers have service files.
    127    # For these cases start and enable the service instead of the timer file.
    128    diff = list(set(services) - set(timers))
    129    for d in diff:
    130        start_and_enable_unit(d, 'service')
    131    for t in timers:
    132        start_and_enable_unit(t, 'timer')
    133
    134
    135def get_file_names_from_paths(unit_files: list):
    136    r"""Get a relative paths from absolute paths."""
    137    names = list()
    138    for u in unit_files:
    139        names.append(u.name)
    140    return names
    141
    142
    143def remove_file_extensions(unit_files: list):
    144    r"""Remove extension from file names."""
    145    names = list()
    146    for u in unit_files:
    147        names.append(pathlib.PurePath(u.stem))
    148    return names
    149
    150
    151if __name__ == '__main__':
    152    if os.getuid() != 0:
    153        raise UserNotRoot
    154
    155    new_timers, new_services = get_unit_files(SRC_DIR, 1)
    156    copy_unit_files(new_timers, DST_DIR)
    157    copy_unit_files(new_services, DST_DIR)
    158
    159    fpyutils.shell.execute_command_live_output('systemctl daemon-reload')
    160    fpyutils.shell.execute_command_live_output('systemctl reset-failed')
    161
    162    services = get_file_names_from_paths(remove_file_extensions(new_services))
    163    timers = get_file_names_from_paths(remove_file_extensions(new_timers))
    164    start_and_enable_units(services, timers)
    

Installation of fpyutils

Run as user

Instruction number

root

*

The recommended way, for the moment, is to install fpyutils globally (avaiable for all users)

  1. manually install the dependencies using APT

    apt-get install python3-requests python3-atomicwrites
    
  2. change the PIP repository. Update the configuration file

    /root/.config/pip/pip.conf
    1[global]
    2timeout = 60
    3index-url = https://pypi.franco.net.eu.org/simple
    
  3. install with pip

    pip3 install fpyutils>=2.1
    

Note

Debian packages exists for the Sid and Bookworm distributions