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
/home/jobs/services/deploy.py#1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# 4# deploy.py 5# 6# Copyright (C) 2019-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com) 7# 8# This program is free software: you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation, either version 3 of the License, or 11# (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program. If not, see <http://www.gnu.org/licenses/>. 20r"""Deploy the unit files.""" 21 22import multiprocessing 23import os 24import pathlib 25import shlex 26import shutil 27import subprocess 28 29import fpyutils 30 31SRC_DIR = '/home/jobs/services/by-user' 32DST_DIR = '/etc/systemd/system' 33 34 35class UserNotRoot(Exception): 36 r"""The user running the script is not root.""" 37 38 39def get_unit_files(start_dir: str = '.', max_depth: int = 0) -> tuple: 40 r"""Get the file names of the unit files.""" 41 unit_file: list = list() 42 p = pathlib.Path(start_dir) 43 44 # A file has a fake depth of 1 even if we are at level 0 of the directory tree. 45 # The reason of this is how the depth is computed below. 46 max_depth += 1 47 48 # Match files and directories. 49 for f in p.rglob('*'): 50 if f.is_file() and len( 51 pathlib.PurePath(f.relative_to( 52 pathlib.PurePath(start_dir))).parts) <= max_depth: 53 if pathlib.PurePath(f).match('*.timer') or pathlib.PurePath( 54 f).match('*.service'): 55 unit_file.append(f) 56 57 return unit_file 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): 69 r"""Start and enable units.""" 70 o1 = subprocess.run(shlex.split( 71 'systemctl enable --full --quiet --no-reload --show-transaction --no-block ' 72 + shlex.quote(unit_name)), 73 check=True, 74 capture_output=True).stderr.decode('UTF-8').strip() 75 if o1 != str(): 76 print(o1) 77 78 # If `systemctl is-enabled` returns "disabled" its return value is 1. 79 # For this reason check is set to false. 80 status = subprocess.run( 81 shlex.split('systemctl is-enabled ' + shlex.quote(unit_name)), 82 check=False, 83 capture_output=True).stdout.decode('UTF-8').strip() 84 disable = True 85 if status in ['enabled', 'enabled-runtime']: 86 disable = False 87 elif status in ['static']: 88 # Completely disable units without the '[Install]' section. 89 disable = True 90 91 if disable: 92 try: 93 o2 = subprocess.run(shlex.split( 94 'systemctl disable --full --quiet --no-reload --show-transaction --no-block --now ' 95 + shlex.quote(unit_name)), 96 check=True, 97 capture_output=True).stderr.decode( 98 'UTF-8').strip() 99 if o2 != str(): 100 print(o2) 101 except subprocess.CalledProcessError: 102 # A new template unit, the ones with 'name@.service' as filename, 103 # without the '[Install]' section cannot be stopped nor disabled. 104 # See https://wiki.archlinux.org/index.php/Systemd#Using_units 105 pass 106 else: 107 o2 = subprocess.run(shlex.split( 108 'systemctl start --full --quiet --no-reload --show-transaction --no-block ' 109 + shlex.quote(unit_name)), 110 check=True, 111 capture_output=True).stderr.decode( 112 'UTF-8').strip() 113 if o2 != str(): 114 print(o2) 115 116 117def start_and_enable_units(unit_files: list) -> int: 118 r"""Start and enable all services and timers.""" 119 # Not all services have timer files but all timers have service files. 120 # For these cases start and enable the service instead of the timer file. 121 concurrent_workers: int = multiprocessing.cpu_count() 122 final_list: list = list() 123 timer_tmp: list = list() 124 service_tmp: list = list() 125 126 for unit in unit_files: 127 if unit.name.endswith('.timer'): 128 timer_tmp.append(pathlib.Path(unit.name).stem) 129 elif unit.name.endswith('.service'): 130 service_tmp.append(pathlib.Path(unit.name).stem) 131 132 # O(n**2). 133 for s in service_tmp: 134 if s in timer_tmp: 135 final_list.append(s + '.timer') 136 else: 137 final_list.append(s + '.service') 138 139 args = iter(final_list) 140 worker = start_and_enable_unit 141 142 with multiprocessing.get_context('fork').Pool( 143 processes=concurrent_workers) as pool: 144 try: 145 pool.imap_unordered(worker, args, 20) 146 except (KeyboardInterrupt, InterruptedError): 147 pool.terminate() 148 pool.join() 149 else: 150 pool.close() 151 pool.join() 152 153 return len(final_list) 154 155 156if __name__ == '__main__': 157 if os.getuid() != 0: 158 raise UserNotRoot 159 160 unit_files: int = get_unit_files(SRC_DIR, 1) 161 copy_unit_files(unit_files, DST_DIR) 162 163 fpyutils.shell.execute_command_live_output('systemctl daemon-reload') 164 fpyutils.shell.execute_command_live_output('systemctl reset-failed') 165 166 total_unit_files: int = start_and_enable_units(unit_files) 167 168 fpyutils.shell.execute_command_live_output('systemctl daemon-reload') 169 fpyutils.shell.execute_command_live_output('systemctl reset-failed') 170 171 print('\na total of ' + str(total_unit_files) + 172 ' units have been handled by deploy.py') 173 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
/root/.config/pip/pip.conf#1[global] 2timeout = 60 3index-url = https://pypi.franco.net.eu.org/simple
install with pip
Avvertimento
This is a dangerous operation. See:
pip3 install fpyutils>=2.2