Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions docs/gemstones/systemd_service_for_python_script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
title: `systemd` Service - 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 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.

## Prerequisites

Let's start by installing some python dependencies needed for the script to use journalctl:

```
shell > sudo dnf install python36-devel systemd-devel
shell > sudo 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 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

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 reliable crontab scripts.

I hope this solution will be useful for you.