Maintenance

Software

Kernel

See also

  • filesystem - Where does update-initramfs look for kernel versions? - Ask Ubuntu 1

RAID

Run periodical RAID data scrubs on hard drives and SSDs.

See also

  • ubuntu - How to wipe md raid meta? - Unix & Linux Stack Exchange 2

  • RAID data scrubbing 3

  1. install the dependencies

    apt-get install mdadm python3-yaml python3-requests
    
  2. install fpyutils. See reference

  3. create the jobs directories. See reference

    mkdir -p /home/jobs/{scripts,services}/by-user/root
    
  4. create the script

    /home/jobs/scripts/by-user/root/mdadm_check.py
      1#!/usr/bin/env python3
      2#
      3# Copyright (C) 2014-2017 Neil Brown <neilb@suse.de>
      4#
      5#
      6#    This program is free software; you can redistribute it and/or modify
      7#    it under the terms of the GNU General Public License as published by
      8#    the Free Software Foundation; either version 2 of the License, or
      9#    (at your option) any later version.
     10#
     11#    This program is distributed in the hope that it will be useful,
     12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
     13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     14#    GNU General Public License for more details.
     15#
     16#    Author: Neil Brown
     17#    Email: <neilb@suse.com>
     18#
     19# Copyright (C) 2019-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
     20r"""Run RAID tests."""
     21
     22import collections
     23import multiprocessing
     24import os
     25import pathlib
     26import sys
     27import time
     28
     29import fpyutils
     30import yaml
     31
     32# Constants.
     33STATUS_CLEAN = 'clean'
     34STATUS_ACTIVE = 'active'
     35STATUS_IDLE = 'idle'
     36
     37
     38class UserNotRoot(Exception):
     39    """The user running the script is not root."""
     40
     41
     42class NoAvailableArrays(Exception):
     43    """No available arrays."""
     44
     45
     46class NoSelectedArraysPresent(Exception):
     47    """None of the arrays in the configuration file exists."""
     48
     49
     50def get_active_arrays():
     51    active_arrays = list()
     52    with open('/proc/mdstat', 'r') as f:
     53        line = f.readline()
     54        while line:
     55            if STATUS_ACTIVE in line:
     56                active_arrays.append(line.split()[0])
     57            line = f.readline()
     58
     59    return active_arrays
     60
     61
     62def get_array_state(array: str):
     63    return open('/sys/block/' + array + '/md/array_state', 'r').read().rstrip()
     64
     65
     66def get_sync_action(array: str):
     67    return open('/sys/block/' + array + '/md/sync_action', 'r').read().rstrip()
     68
     69
     70def run_action(array: str, action: str):
     71    with open('/sys/block/' + array + '/md/sync_action', 'w') as f:
     72        f.write(action)
     73
     74
     75def main_action(array: str, config: dict):
     76    action = devices[array]
     77    go = True
     78    while go:
     79        if get_sync_action(array) == STATUS_IDLE:
     80            message = 'running ' + action + ' on /dev/' + array + '. pid: ' + str(
     81                os.getpid())
     82            run_action(array, action)
     83            message += '\n\n'
     84            message += 'finished pid: ' + str(os.getpid())
     85            print(message)
     86
     87            if config['notify']['gotify']['enabled']:
     88                m = config['notify']['gotify'][
     89                    'message'] + ' ' + '\n' + message
     90                fpyutils.notify.send_gotify_message(
     91                    config['notify']['gotify']['url'],
     92                    config['notify']['gotify']['token'], m,
     93                    config['notify']['gotify']['title'],
     94                    config['notify']['gotify']['priority'])
     95            if config['notify']['email']['enabled']:
     96                fpyutils.notify.send_email(
     97                    message,
     98                    config['notify']['email']['smtp_server'],
     99                    config['notify']['email']['port'],
    100                    config['notify']['email']['sender'],
    101                    config['notify']['email']['user'],
    102                    config['notify']['email']['password'],
    103                    config['notify']['email']['receiver'],
    104                    config['notify']['email']['subject'])
    105
    106            go = False
    107        if go:
    108            print('waiting ' + array + ' to be idle...')
    109            time.sleep(config['generic']['timeout_idle_check'])
    110
    111
    112if __name__ == '__main__':
    113    if os.getuid() != 0:
    114        raise UserNotRoot
    115
    116    configuration_file = sys.argv[1]
    117    config = yaml.load(open(configuration_file, 'r'), Loader=yaml.SafeLoader)
    118    devices = dict()
    119    for dev_element in config['devices']:
    120        key = dev_element.keys()
    121        device = list(key)[0]
    122        devices[device] = dev_element[device]
    123
    124    active_arrays = get_active_arrays()
    125    dev_queue = collections.deque()
    126    if len(active_arrays) > 0:
    127        for dev in active_arrays:
    128            if pathlib.Path('/sys/block/' + dev + '/md/sync_action').is_file():
    129                state = get_array_state(dev)
    130                if state == STATUS_CLEAN or state == STATUS_ACTIVE or state == STATUS_IDLE:
    131                    try:
    132                        if devices[dev] != 'ignore' and dev in devices:
    133                            dev_queue.append(dev)
    134                    except KeyError:
    135                        pass
    136
    137    if len(active_arrays) == 0:
    138        raise NoAvailableArrays
    139    if len(dev_queue) == 0:
    140        raise NoSelectedArraysPresent
    141
    142    while len(dev_queue) > 0:
    143        for i in range(0, config['generic']['max_concurrent_checks']):
    144            if len(dev_queue) > 0:
    145                ready = dev_queue.popleft()
    146                p = multiprocessing.Process(target=main_action,
    147                                            args=(
    148                                                ready,
    149                                                config,
    150                                            ))
    151                p.start()
    152        p.join()
    
  5. create a configuration file

    /home/jobs/scripts/by-user/root/mdadm_check.yaml
     1#
     2# mdadm_check.yaml
     3#
     4# Copyright (C) 2014-2017 Neil Brown <neilb@suse.de>
     5#
     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 2 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#    Author: Neil Brown
    18#    Email: <neilb@suse.com>
    19#
    20# Copyright (C) 2019-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
    21
    22generic:
    23    # The maximum number of concurrent processes.
    24    max_concurrent_checks: 2
    25
    26    # In seconds.
    27    timeout_idle_check: 10
    28
    29# key:      RAID array name without '/dev/'.
    30# value:    action.
    31devices:
    32    md1: 'check'
    33    md2: 'ignore'
    34    md3: 'check'
    35
    36notify:
    37    email:
    38        enabled: true
    39        smtp_server: 'smtp.gmail.com'
    40        port: 465
    41        sender: 'myusername@gmail.com'
    42        user: 'myusername'
    43        password: 'my awesome password'
    44        receiver: 'myusername@gmail.com'
    45        subject: 'mdadm operation'
    46    gotify:
    47        enabled: true
    48        url: '<gotify url>'
    49        token: '<app token>'
    50        title: 'mdadm operation'
    51        message: 'starting mdadm operation'
    52        priority: 5
    

    Important

    • do not prepend /dev to RAID device names

    • possible values: check, repair, idle, ignore

      • ignore will make the script skip the device

      • use repair at your own risk

    • absent devices are ignored

    • run these commands to get the names of RAID arrays

      lsblk
      cat /proc/mdstat
      
  6. create a Systemd service unit file

    /home/jobs/services/by-user/root/mdadm-check.service
     1[Unit]
     2Description=mdadm check
     3Requires=sys-devices-virtual-block-md1.device
     4Requires=sys-devices-virtual-block-md2.device
     5Requires=sys-devices-virtual-block-md3.device
     6After=sys-devices-virtual-block-md1.device
     7After=sys-devices-virtual-block-md2.device
     8After=sys-devices-virtual-block-md3.device
     9
    10[Service]
    11Type=simple
    12ExecStart=/home/jobs/scripts/by-user/root/mdadm_check.py /home/jobs/scripts/by-user/root/mdadm_check.yaml
    13User=root
    14Group=root
    15
    16[Install]
    17WantedBy=multi-user.target
    
  7. create a Systemd timer unit file

    /home/jobs/services/by-user/root/mdadm-check.timer
    1[Unit]
    2Description=Once a month check mdadm arrays
    3
    4[Timer]
    5OnCalendar=Monthly
    6Persistent=true
    7
    8[Install]
    9WantedBy=timers.target
    
  8. fix the permissions

    chmod 700 /home/jobs/{scripts,services}/by-user/root
    
  9. run the deploy script

S.M.A.R.T.

Run periodical S.M.A.R.T. tests on hard drives and SSDs. The provided script supports only /dev/disk/by-id names.

See also

  • A collection of scripts I have written and/or adapted that I currently use on my systems as automated tasks 4

  1. install the dependencies

    apt-get install hdparm smartmontools python3-yaml python3-requests
    
  2. install fpyutils. See reference

  3. identify the drives you want to check S.M.A.R.T. values

    ls /dev/disk/by-id
    

    See also the udev rule file /lib/udev/rules.d/60-persistent-storage.rules. You can also use this command to have more details of specific drives

    hdparm -I /dev/disk/by-id/${drive_name}
    # or
    hdparm -I /dev/sd${letter}
    
  4. create the jobs directories. See reference

    mkdir -p /home/jobs/{scripts,services}/by-user/root
    chmod 700 -R /home/jobs/{scripts,services}/by-user/root
    
  5. create the script

    /home/jobs/scripts/by-user/root/smartd_test.py
      1#!/usr/bin/env python3
      2#
      3# smartd_test.py
      4#
      5# Copyright (C) 2019-2021 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"""Run S.M.A.R.T tests on hard drives."""
     20
     21import json
     22import os
     23import pathlib
     24import re
     25import shlex
     26import subprocess
     27import sys
     28
     29import fpyutils
     30import yaml
     31
     32
     33class UserNotRoot(Exception):
     34    """The user running the script is not root."""
     35
     36
     37def get_disks() -> list:
     38    r"""Scan all the disks."""
     39    disks = list()
     40    for d in pathlib.Path('/dev/disk/by-id').iterdir():
     41        # Ignore disks ending with part-${integer} to avoid duplicates (names
     42        # corresponding to partitions of the same disk).
     43        disk = str(d)
     44        if re.match('.+-part[0-9]+$', disk) is None:
     45            try:
     46                ddict = json.loads(
     47                    subprocess.run(
     48                        shlex.split('smartctl --capabilities --json ' +
     49                                    shlex.quote(disk)),
     50                        capture_output=True,
     51                        check=False,
     52                        shell=False,
     53                        timeout=30).stdout)
     54                try:
     55                    # Check for smart test support.
     56                    if ddict['ata_smart_data']['capabilities'][
     57                            'self_tests_supported']:
     58                        disks.append(disk)
     59                except KeyError:
     60                    pass
     61            except subprocess.TimeoutExpired:
     62                print('timeout for ' + disk)
     63            except subprocess.CalledProcessError:
     64                print('device ' + disk +
     65                      ' does not support S.M.A.R.T. commands, skipping...')
     66
     67    return disks
     68
     69
     70def disk_ready(disk: str, busy_status: int = 249) -> bool:
     71    r"""Check if the disk is ready."""
     72    # Raises a KeyError if disk has not S.M.A.R.T. status capabilities.
     73    ddict = json.loads(
     74        subprocess.run(shlex.split('smartctl --capabilities --json ' +
     75                                   shlex.quote(disk)),
     76                       capture_output=True,
     77                       check=True,
     78                       shell=False,
     79                       timeout=30).stdout)
     80    if ddict['ata_smart_data']['self_test']['status']['value'] != busy_status:
     81        return True
     82    else:
     83        return False
     84
     85
     86def run_test(disk: str, test_length: str = 'long') -> str:
     87    r"""Run the smartd test."""
     88    return subprocess.run(
     89        shlex.split('smartctl --test=' + shlex.quote(test_length) + ' ' +
     90                    shlex.quote(disk)),
     91        capture_output=True,
     92        check=True,
     93        shell=False,
     94        timeout=30).stdout
     95
     96
     97if __name__ == '__main__':
     98    if os.getuid() != 0:
     99        raise UserNotRoot
    100
    101    configuration_file = shlex.quote(sys.argv[1])
    102    config = yaml.load(open(configuration_file, 'r'), Loader=yaml.SafeLoader)
    103
    104    # Do not prepend '/dev/disk/by-id/'.
    105    disks_to_check = shlex.quote(sys.argv[2])
    106    disks_available = get_disks()
    107
    108    for d in config['devices']:
    109        dev = '/dev/disk/by-id/' + d
    110        if config['devices'][d]['enabled'] and dev in disks_available:
    111            if disks_to_check == 'all' or disks_to_check == d:
    112                if disk_ready(dev, config['devices'][d]['busy_status']):
    113                    print('attempting ' + d + ' ...')
    114                    message = run_test(
    115                        dev, config['devices'][d]['test']).decode('utf-8')
    116                    print(message)
    117                    if config['devices'][d]['log']:
    118                        if config['notify']['gotify']['enabled']:
    119                            m = config['notify']['gotify'][
    120                                'message'] + ' ' + d + '\n' + message
    121                            fpyutils.notify.send_gotify_message(
    122                                config['notify']['gotify']['url'],
    123                                config['notify']['gotify']['token'], m,
    124                                config['notify']['gotify']['title'],
    125                                config['notify']['gotify']['priority'])
    126                        if config['notify']['email']['enabled']:
    127                            fpyutils.notify.send_email(
    128                                message,
    129                                config['notify']['email']['smtp_server'],
    130                                config['notify']['email']['port'],
    131                                config['notify']['email']['sender'],
    132                                config['notify']['email']['user'],
    133                                config['notify']['email']['password'],
    134                                config['notify']['email']['receiver'],
    135                                config['notify']['email']['subject'])
    136                else:
    137                    # Drop test requests if a disk is running a test in a particular moment.
    138                    # This avoid putting the disks under too much stress.
    139                    print('disk ' + d + ' not ready, checking the next...')
    
  6. create a configuration file

    includes/home/jobs/scripts/by-user/root/smartd_test.yaml
     1#
     2# smartd_test.yaml
     3#
     4# Copyright (C) 2019-2020 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
     5#
     6# This program is free software: you can redistribute it and/or modify
     7# it under the terms of the GNU General Public License as published by
     8# the Free Software Foundation, either version 3 of the License, or
     9# (at your option) any later version.
    10#
    11# This program is distributed in the hope that it will be useful,
    12# but WITHOUT ANY WARRANTY; without even the implied warranty of
    13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14# GNU General Public License for more details.
    15#
    16# You should have received a copy of the GNU General Public License
    17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18
    19devices:
    20    ata-disk1:
    21        enabled: true
    22        test: 'long'
    23        log: true
    24        busy_status: 249
    25    ata-disk2:
    26        enabled: true
    27        test: 'long'
    28        log: false
    29        busy_status: 249
    30    ata-diskn:
    31        enabled: true
    32        test: 'long'
    33        log: true
    34        busy_status: 249
    35
    36notify:
    37    gotify:
    38        enabled: true
    39        url: '<gotify url>'
    40        token: '<app token>'
    41        title: 'smart test'
    42        message: 'starting smart test on'
    43        priority: 5
    44    email:
    45        enabled: true
    46        smtp_server: 'smtp.gmail.com'
    47        port: 465
    48        sender: 'myusername@gmail.com'
    49        user: 'myusername'
    50        password: 'my awesome password'
    51        receiver: 'myusername@gmail.com'
    52        subject: 'smartd test'
    

    Important

    • absent devices are ignored

    • devices must be explicitly enabled

    • do not prepend /dev/disk/by-id/ to drive names

    • run a short test to get the busy_status value.

      smartctl -t short /dev/disk/by-id/${drive_name}
      

      You should be able to capture the value while the test is running by looking at the Self-test execution status: line. In my case it is always 249, but this value is not hardcoded in smartmontools’ source code

      smartctl --all /dev/disk/by-id/${drive_name}
      
  7. use this Systemd service unit file

    /home/jobs/services/by-user/root/smartd-test.ata_disk1.service
    1[Unit]
    2Description=execute smartd on ata-disk1
    3
    4[Service]
    5Type=simple
    6ExecStart=/home/jobs/scripts/by-user/root/smartd_test.py /home/jobs/scripts/by-user/root/smartd_test.yaml ata-disk1
    7User=root
    8Group=root
    
  8. use this Systemd timer unit file

    /home/jobs/services/by-user/root/smartd-test.ata_disk1.timer
    1[Unit]
    2Description=Once every two months smart test ata-disk1
    3
    4[Timer]
    5OnCalendar=*-01,03,05,07,09,11-01 00:00:00
    6Persistent=true
    7
    8[Install]
    9WantedBy=timers.target
    
  9. fix the permissions

    chmod 700 -R /home/jobs/scripts/by-user/smartd_test.*
    chmod 700 -R /home/jobs/services/by-user/root
    
  10. run the deploy script

Important

To avoid tests being interrupted you must avoid putting the disks to sleep, therefore, programs like hd-idle must be stopped before running the tests.

Updates

Update action

This script can be used to update software not supported by the package manager, for example Docker images.

Important

Any aribtrary command can be configured.

See also

  • A collection of scripts I have written and/or adapted that I currently use on my systems as automated tasks 4

  1. install the dependencies

    apt-get install python3-yaml python3-requests
    
  2. install fpyutils. See reference

  3. create the script

    /home/jobs/scripts/by-user/root/update_action.py
     1#!/usr/bin/env python3
     2#
     3# update_action.py
     4#
     5# Copyright (C) 2021-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"""update_action.py."""
    20
    21import shlex
    22import sys
    23
    24import fpyutils
    25import yaml
    26
    27
    28def send_notification(message: str, notify: dict):
    29    m = notify['gotify']['message'] + '\n' + message
    30    if notify['gotify']['enabled']:
    31        fpyutils.notify.send_gotify_message(
    32            notify['gotify']['url'],
    33            notify['gotify']['token'], m,
    34            notify['gotify']['title'],
    35            notify['gotify']['priority'])
    36    if notify['email']['enabled']:
    37        fpyutils.notify.send_email(message,
    38                                   notify['email']['smtp_server'],
    39                                   notify['email']['port'],
    40                                   notify['email']['sender'],
    41                                   notify['email']['user'],
    42                                   notify['email']['password'],
    43                                   notify['email']['receiver'],
    44                                   notify['email']['subject'])
    45
    46
    47if __name__ == '__main__':
    48    def main():
    49        configuration_file = shlex.quote(sys.argv[1])
    50        config = yaml.load(open(configuration_file, 'r'), Loader=yaml.SafeLoader)
    51
    52        # Action types. Preserve this order.
    53        types = ['pre', 'update', 'post']
    54        services = config['services']
    55
    56        for service in services:
    57            for type in types:
    58                for cmd in services[service]['commands'][type]:
    59                    for name in cmd:
    60                        retval = fpyutils.shell.execute_command_live_output(cmd[name]['command'], dry_run=False)
    61                        if cmd[name]['notify']['success'] and retval == cmd[name]['expected_retval']:
    62                            send_notification('command "' + name + '" of service "' + service + '": OK', config['notify'])
    63                        elif cmd[name]['notify']['error'] and retval != cmd[name]['expected_retval']:
    64                            send_notification('command "' + name + '" of service "' + service + '": ERROR', config['notify'])
    65
    66    main()
    
  4. create a configuration file

    includes/home/jobs/scripts/by-user/root/update_action.mypurpose.yaml
     1#
     2# update_action.mypurpose.yaml
     3#
     4# Copyright (C) 2021-2022 Franco Masotti (franco \D\o\T masotti {-A-T-} tutanota \D\o\T com)
     5#
     6# This program is free software: you can redistribute it and/or modify
     7# it under the terms of the GNU General Public License as published by
     8# the Free Software Foundation, either version 3 of the License, or
     9# (at your option) any later version.
    10#
    11# This program is distributed in the hope that it will be useful,
    12# but WITHOUT ANY WARRANTY; without even the implied warranty of
    13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14# GNU General Public License for more details.
    15#
    16# You should have received a copy of the GNU General Public License
    17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18
    19notify:
    20    email:
    21        enabled: true
    22        smtp_server: 'smtp.gmail.com'
    23        port: 465
    24        sender: 'myusername@gmail.com'
    25        user: 'myusername'
    26        password: 'my awesome password'
    27        receiver: 'myusername@gmail.com'
    28        subject: 'update action'
    29    gotify:
    30        enabled: true
    31        url: '<gotify url>'
    32        token: '<app token>'
    33        title: 'update action'
    34        message: 'update action'
    35        priority: 5
    36
    37services:
    38    hello:
    39        commands:
    40            pre:
    41                - stop_service:
    42                    # string
    43                    command: 'systemctl stop docker-compose.hello.service'
    44                    # integer
    45                    expected_retval: 0
    46                    # boolean: {true,false}
    47                    notify:
    48                        success: true
    49                        error: true
    50            update:
    51                - pull:
    52                    command: 'pushd /home/jobs/scripts/by-user/root/docker/hello && docker-compose pull'
    53                    expected_retval: 0
    54                    notify:
    55                        success: true
    56                        error: true
    57                - build:
    58                    command: 'pushd /home/jobs/scripts/by-user/root/docker/hello && docker-compose build --pull'
    59                    expected_retval: 0
    60                    notify:
    61                        success: true
    62                        error: true
    63            post:
    64                - start_service:
    65                    command: 'systemctl start docker-compose.hello.service'
    66                    expected_retval: 0
    67                    notify:
    68                        success: true
    69                        error: true
    70    goodbye:
    71        commands:
    72            pre:
    73                - stop_service:
    74                    command: 'systemctl stop docker-compose.goodbye.service'
    75                    expected_retval: 0
    76                    notify:
    77                        success: true
    78                        error: true
    79            update:
    80                - pull_only:
    81                    command: 'pushd /home/jobs/scripts/by-user/root/docker/goodbye && docker-compose pull'
    82                    expected_retval: 0
    83                    notify:
    84                        success: true
    85                        error: true
    86            post:
    87                - start_service:
    88                    command: 'systemctl start docker-compose.goodbye.service'
    89                    expected_retval: 0
    90                    notify:
    91                        success: true
    92                        error: true
    
  5. use this Systemd service unit file

    /home/jobs/services/by-user/root/update-action.mypurpose.service
     1[Unit]
     2Description=Update action mypurpose
     3Wants=network-online.target
     4After=network-online.target
     5
     6[Service]
     7Type=simple
     8ExecStart=/home/jobs/scripts/by-user/root/update_action.py /home/jobs/scripts/by-user/root/update_action.mypurpose.yaml
     9User=root
    10Group=root
    
  6. use this Systemd timer unit file

    /home/jobs/services/by-user/root/update-action.mypurpose.timer
    1[Unit]
    2Description=Update action mypurpose monthly
    3
    4[Timer]
    5OnCalendar=monthly
    6Persistent=true
    7
    8[Install]
    9WantedBy=timers.target
    
  7. fix the permissions

    chmod 700 -R /home/jobs/scripts/by-user/update_action.*
    chmod 700 -R /home/jobs/services/by-user/root
    
  8. run the deploy script

Footnotes

1

https://askubuntu.com/questions/759802/where-does-update-initramfs-look-for-kernel-versions CC BY-SA 3.0, copyright (c) 2016-2017, askubuntu contributors

2

https://unix.stackexchange.com/questions/411206/how-to-wipe-md-raid-meta CC BY-SA 3.0, copyright (c) 2017, stackexchange contributors

3

https://blog.franco.net.eu.org/notes/raid-data-scrubbing.html CC-BY-SA 4.0, copyright (c) 2019-2021, Franco Masotti

4(1,2)

https://software.franco.net.eu.org/frnmst/automated-tasks GNU GPLv3+, copyright (c) 2019-2022, Franco Masotti