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#
All instructions must be run be the root
user by default.
You should access the root user with
sudo -i
Specific instructions are available
when new users must be created
when commands need to be run be users other than
root
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
add the running username to the jobs group
usermod -aG jobs ${running_username}
when you change a service file run the
deploy script
asroot
. 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
1#!/usr/bin/env python3 2# 3# deploy.py 4# 5# Copyright (C) 2019-2022 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 multiprocessing 22import os 23import pathlib 24import shlex 25import shutil 26import subprocess 27 28import fpyutils 29 30SRC_DIR = '/home/jobs/services/by-user' 31DST_DIR = '/etc/systemd/system' 32 33 34class UserNotRoot(Exception): 35 r"""The user running the script is not root.""" 36 37 38def get_unit_files(start_dir: str = '.', max_depth: int = 0) -> tuple: 39 r"""Get the file names of the unit files.""" 40 unit_file: list = 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') or pathlib.PurePath( 53 f).match('*.service'): 54 unit_file.append(f) 55 56 return unit_file 57 58 59def copy_unit_files(unit_files: list, dst_dir: str = DST_DIR): 60 r"""Copy multiple unit files.""" 61 for f in unit_files: 62 shutil.copyfile( 63 str(f), str(pathlib.Path(shlex.quote(dst_dir), 64 shlex.quote(f.name)))) 65 66 67def start_and_enable_unit(unit_name: str): 68 r"""Start and enable units.""" 69 o1 = subprocess.run(shlex.split( 70 'systemctl enable --full --quiet --no-reload --show-transaction --no-block ' 71 + shlex.quote(unit_name)), 72 check=True, 73 capture_output=True).stderr.decode('UTF-8').strip() 74 if o1 != '': 75 print(o1) 76 77 # If `systemctl is-enabled` returns "disabled" its return value is 1. 78 # For this reason check is set to false. 79 status = subprocess.run( 80 shlex.split('systemctl is-enabled ' + shlex.quote(unit_name)), 81 check=False, 82 capture_output=True).stdout.decode('UTF-8').strip() 83 disable = True 84 if status in ['enabled', 'enabled-runtime']: 85 disable = False 86 elif status in ['static']: 87 # Completely disable units without the '[Install]' section. 88 disable = True 89 90 if disable: 91 try: 92 o2 = subprocess.run(shlex.split( 93 'systemctl disable --full --quiet --no-reload --show-transaction --no-block --now ' 94 + shlex.quote(unit_name)), 95 check=True, 96 capture_output=True).stderr.decode( 97 'UTF-8').strip() 98 if o2 != '': 99 print(o2) 100 except subprocess.CalledProcessError: 101 # A new template unit, the ones with 'name@.service' as filename, 102 # without the '[Install]' section cannot be stopped nor disabled. 103 # See https://wiki.archlinux.org/index.php/Systemd#Using_units 104 pass 105 else: 106 o2 = subprocess.run(shlex.split( 107 'systemctl start --full --quiet --no-reload --show-transaction --no-block ' 108 + shlex.quote(unit_name)), 109 check=True, 110 capture_output=True).stderr.decode( 111 'UTF-8').strip() 112 if o2 != '': 113 print(o2) 114 115 116def start_and_enable_units(unit_files: list) -> int: 117 r"""Start and enable all services and timers.""" 118 # Not all services have timer files but all timers have service files. 119 # For these cases start and enable the service instead of the timer file. 120 concurrent_workers: int = multiprocessing.cpu_count() 121 final_list: list = list() 122 timer_tmp: list = list() 123 service_tmp: list = list() 124 125 for unit in unit_files: 126 if unit.name.endswith('.timer'): 127 timer_tmp.append(pathlib.Path(unit.name).stem) 128 elif unit.name.endswith('.service'): 129 service_tmp.append(pathlib.Path(unit.name).stem) 130 131 # O(n**2). 132 for s in service_tmp: 133 if s in timer_tmp: 134 final_list.append(s + '.timer') 135 else: 136 final_list.append(s + '.service') 137 138 args = iter(final_list) 139 worker = start_and_enable_unit 140 141 with multiprocessing.get_context('fork').Pool( 142 processes=concurrent_workers) as pool: 143 try: 144 pool.imap_unordered(worker, args, 20) 145 except (KeyboardInterrupt, InterruptedError): 146 pool.terminate() 147 pool.join() 148 else: 149 pool.close() 150 pool.join() 151 152 return len(final_list) 153 154 155if __name__ == '__main__': 156 if os.getuid() != 0: 157 raise UserNotRoot 158 159 unit_files: int = get_unit_files(SRC_DIR, 1) 160 copy_unit_files(unit_files, DST_DIR) 161 162 fpyutils.shell.execute_command_live_output('systemctl daemon-reload') 163 fpyutils.shell.execute_command_live_output('systemctl reset-failed') 164 165 total_unit_files: int = start_and_enable_units(unit_files) 166 167 fpyutils.shell.execute_command_live_output('systemctl daemon-reload') 168 fpyutils.shell.execute_command_live_output('systemctl reset-failed') 169 170 print('\na total of ' + str(total_unit_files) + 171 ' units have been handled by deploy.py') 172 print('check units by running "systemctl"')
Installation of fpyutils#
The recommended way, for the moment, is to install fpyutils globally (avaiable for all users)
manually install the dependencies using APT
apt-get install python3-requests python3-atomicwrites
change the PIP repository. Update the
configuration file
1[global] 2timeout = 60 3index-url = https://pypi.franco.net.eu.org/simple
install with pip
Warning
This is a dangerous operation. See:
pip3 install fpyutils>=2.2