From 2239e90b32207a5658cde2b411226454cc084a80 Mon Sep 17 00:00:00 2001 From: Antoine Le Morvan Date: Fri, 14 Oct 2022 11:48:05 +0200 Subject: [PATCH 1/2] Writing systemd gems --- .../systemd_service_for_python_script.md | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 docs/gemstones/systemd_service_for_python_script.md diff --git a/docs/gemstones/systemd_service_for_python_script.md b/docs/gemstones/systemd_service_for_python_script.md new file mode 100644 index 0000000000..0755c852c7 --- /dev/null +++ b/docs/gemstones/systemd_service_for_python_script.md @@ -0,0 +1,176 @@ +--- +title: Systemd service for a python script +author: Antoine Le Morvan +contributors: Steven Spencer +date: 2022-10-14 +tags: + - python + - systemd + - cron +--- + +# Systemd service for a python script + +If, like many sysadmins, you are an affictionado of cron scripts launched with `* * * * * /I/launch/my/script.sh`, this article should make you think of another way to do it, using all the power and ease offered by systemd. + +We will write a python script that will provide a continuous loop to execute the actions you define. + +We will see how to run this script as a systemd service, view the logs in journalctl and see what happens if the script crashes. + +## Prerequisites + +Let's start by installing some python dependencies to use journalctl: + +``` +shell > sudo dnf install python36-devel systemd-devel +shell > pip3 install systemd +``` + +## Writing the script + +Let's consider the following script `my_service.py`: + +``` +""" +Sample script to run as script +""" +import time +import logging +import sys +from systemd.journal import JournaldLogHandler + +# Get an instance of the logger +LOGGER = logging.getLogger(__name__) + +# Instantiate the JournaldLogHandler to hook into systemd +JOURNALD_HANDLER = JournaldLogHandler() +JOURNALD_HANDLER.setFormatter(logging.Formatter( + '[%(levelname)s] %(message)s' +)) + +# Add the journald handler to the current logger +LOGGER.addHandler(JOURNALD_HANDLER) +LOGGER.setLevel(logging.INFO) + +class Service(): # pylint: disable=too-few-public-methods + """ + Launch an infinite loop + """ + def __init__(self): + + duration = 0 + + while True: + time.sleep(60) + duration += 60 + LOGGER.info("Total duration: %s", str(duration)) + # will failed after 4 minutes + if duration > 240: + sys.exit(1) + +if __name__ == '__main__': + + LOGGER.info("Starting the service") + Service() +``` + +We start by instantiating the necessary to send logs in journald. The script then launches an infinite loop and pauses for 60 seconds (which is the minimum of a cron execution, so we can go below this limitation). + +!!! Note + + I personally use this script in a more advanced form, which continuously queries a database and executes jobs based on the information retrieved via the rundeck API + +## Systemd integration + +Now that we have a script that can serve as a basis for your imagination, we can install it as a systemd service. + +Let's create this file `my_service.service` and copy it to `/etc/systemd/system/`. + +``` +[Unit] +Description=My Service +After=multi-user.target + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 my_service.py +WorkingDirectory=/opt/my_service/ + +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=my_service + +[Install] +WantedBy=multi-user.target +``` + +As you can see, the script is launched from `/opt/my_service/`. Remember to adapt the path to your script and the syslog identifier. + +Launch and enable the new service: + +``` +shell > sudo systemctl daemon-reload +shell > sudo systemctl enable my_service.service +shell > sudo systemctl start my_service.service +``` + +## Tests + +We can now view the logs via journalctl: + +``` +shell > journalctl -f -u my_service +oct. 14 11:07:48 rocky8 systemd[1]: Started My Service. +oct. 14 11:07:49 rocky8 __main__[270267]: [INFO] Starting the service +oct. 14 11:08:49 rocky8 __main__[270267]: [INFO] Total duration: 60 +oct. 14 11:09:49 rocky8 __main__[270267]: [INFO] Total duration: 120 +``` + +Now let's see what happens if the script crashes: + +``` +shell > ps -elf | grep my_service +4 S root 270267 1 0 80 0 - 82385 - 11:07 ? 00:00:00 /usr/bin/python3 my_service.py +shell > sudo kill -9 270267 +``` + +``` +shell > journalctl -f -u my_service +oct. 14 11:10:49 rocky8 __main__[270267]: [INFO] Total duration: 180 +oct. 14 11:11:49 rocky8 __main__[270267]: [INFO] Total duration: 240 +oct. 14 11:12:19 rocky8 systemd[1]: my_service.service: Main process exited, code=killed, status=9/KILL +oct. 14 11:12:19 rocky8 systemd[1]: my_service.service: Failed with result 'signal'. +oct. 14 11:12:19 rocky8 systemd[1]: my_service.service: Service RestartSec=100ms expired, scheduling restart. +oct. 14 11:12:19 rocky8 systemd[1]: my_service.service: Scheduled restart job, restart counter is at 1. +oct. 14 11:12:19 rocky8 systemd[1]: Stopped My Service. +oct. 14 11:12:19 rocky8 systemd[1]: Started My Service. +oct. 14 11:12:19 rocky8 __main__[270863]: [INFO] Starting the service +``` + +We can also wait 5 minutes for the script to crash by itself: (remove this for your production) + +``` +oct. 14 11:16:02 rocky8 systemd[1]: Started My Service. +oct. 14 11:16:03 rocky8 __main__[271507]: [INFO] Starting the service +oct. 14 11:17:03 rocky8 __main__[271507]: [INFO] Total duration: 60 +oct. 14 11:18:03 rocky8 __main__[271507]: [INFO] Total duration: 120 +oct. 14 11:19:03 rocky8 __main__[271507]: [INFO] Total duration: 180 +oct. 14 11:20:03 rocky8 __main__[271507]: [INFO] Total duration: 240 +oct. 14 11:21:03 rocky8 __main__[271507]: [INFO] Total duration: 300 +oct. 14 11:21:03 rocky8 systemd[1]: my_service.service: Main process exited, code=exited, status=1/FAILURE +oct. 14 11:21:03 rocky8 systemd[1]: my_service.service: Failed with result 'exit-code'. +oct. 14 11:21:03 rocky8 systemd[1]: my_service.service: Service RestartSec=100ms expired, scheduling restart. +oct. 14 11:21:03 rocky8 systemd[1]: my_service.service: Scheduled restart job, restart counter is at 1. +oct. 14 11:21:03 rocky8 systemd[1]: Stopped My Service. +oct. 14 11:21:03 rocky8 systemd[1]: Started My Service. +oct. 14 11:21:03 rocky8 __main__[271993]: [INFO] Starting the service +``` + +As you can see, the restart feature of systemd is very useful. + +## Conclusion + +Systemd and Journald provide us the tools to make robust and powerful scripts easily enough to replace our old good crontab scripts. + +I hope this solution will be useful for you. From b64ca66e0fcd902d489175c15c4d91447d8a91d7 Mon Sep 17 00:00:00 2001 From: sspencerwire Date: Fri, 14 Oct 2022 08:23:33 -0500 Subject: [PATCH 2/2] Update systemd_service_for_python_script.md * change the spelling of aficionado * As Systemd is a daemon, used the code block syntax for this and `journald` * added some minor words to improve clarity --- .../systemd_service_for_python_script.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/gemstones/systemd_service_for_python_script.md b/docs/gemstones/systemd_service_for_python_script.md index 0755c852c7..70cfde85e5 100644 --- a/docs/gemstones/systemd_service_for_python_script.md +++ b/docs/gemstones/systemd_service_for_python_script.md @@ -1,5 +1,5 @@ --- -title: Systemd service for a python script +title: `systemd` Service - Python Script author: Antoine Le Morvan contributors: Steven Spencer date: 2022-10-14 @@ -9,24 +9,24 @@ tags: - cron --- -# Systemd service for a python script +# `systemd` Service for a Python script -If, like many sysadmins, you are an affictionado of cron scripts launched with `* * * * * /I/launch/my/script.sh`, this article should make you think of another way to do it, using all the power and ease offered by systemd. +If, like many sysadmins, you are an aficionado of cron scripts launched with `* * * * * /I/launch/my/script.sh`, this article should make you think of another way to do it using all the power and ease offered by `systemd`. We will write a python script that will provide a continuous loop to execute the actions you define. -We will see how to run this script as a systemd service, view the logs in journalctl and see what happens if the script crashes. +We will see how to run this script as a `systemd` service, view the logs in journalctl, and see what happens if the script crashes. ## Prerequisites -Let's start by installing some python dependencies to use journalctl: +Let's start by installing some python dependencies needed for the script to use journalctl: ``` shell > sudo dnf install python36-devel systemd-devel -shell > pip3 install systemd +shell > sudo pip3 install systemd ``` -## Writing the script +## Writing the Script Let's consider the following script `my_service.py`: @@ -74,13 +74,13 @@ if __name__ == '__main__': Service() ``` -We start by instantiating the necessary to send logs in journald. The script then launches an infinite loop and pauses for 60 seconds (which is the minimum of a cron execution, so we can go below this limitation). +We start by instantiating the necessary variables to send logs in journald. The script then launches an infinite loop and pauses for 60 seconds (which is the minimum of a cron execution, so we can go below this limitation). !!! Note I personally use this script in a more advanced form, which continuously queries a database and executes jobs based on the information retrieved via the rundeck API -## Systemd integration +## Systemd Integration Now that we have a script that can serve as a basis for your imagination, we can install it as a systemd service. @@ -171,6 +171,6 @@ As you can see, the restart feature of systemd is very useful. ## Conclusion -Systemd and Journald provide us the tools to make robust and powerful scripts easily enough to replace our old good crontab scripts. +`systemd` and `journald` provide us the tools to make robust and powerful scripts easily enough to replace our old reliable crontab scripts. I hope this solution will be useful for you.