Migrating User Crontab to Systemd Timers

The following is a brief description of how I migrated one of my user crontab entries to user systemd timers as part of my migration from Ubuntu 20.04 to Arch Linux. In my previous installation I had a few user cron jobs which ran daily tasks, such as backing up my home directory and producing a daily report on the value of an inventory. In the move to Arch, I took the opportunity to re-implement these using systemd timers. As with user cron jobs, systemd permits the creation of timer units by a user.

As with system timers, user timers are constructed using two files, a .service file to describe the task, and a .timer file to describe when it is to be run. However, the traditional way of doing this using cron is in a single, user crontab file as follows:

Original User Crontab

AWS_CONFIG_FILE=/home/rw/.aws/config
AWS_CREDENTIALS_FILE=/home/rw/.aws/credentials
AWS_PROFILE=rw

0 1 * * * /usr/bin/env bash -c 'cd /home/rw/devel/calc/ && source /home/rw/devel/calc/venv/bin/activate && python3 get_value.py'

In the above example, which runs at 1am every day, Cron will execute the chain of commands as follows: cd to the task directory, activate the Python virtual environment, and run the script. The user cron job was created by simply typing crontab -e, entering the line above, and writing the file to disk. Cron would take care of the rest. The python script executed by Cron uses AWS boto3 commands to push data in to AWS. For this reason we need the script to know where to find our AWS credentials and configuration, and provide these with the local Environment variable definitions in the Crontab.

Converting to Systemd Timers

To convert this to systemd, we first need to know how and where to create timers. As mentioned earlier, systemd timers consist of two files (.service and .timer), and these are stored in ~/.config/systemd/user. Let’s first create the .service while to describe the task we want to run.

To create the task (unit in systemd vocabulary) description, we edit the following in: ~/.config/systemd/user/get-value.service:

[Unit]
Description=Update inventory data and push data to AWS

[Service]
Type=oneshot
ExecStart=/usr/bin/env bash -c 'cd /home/rw/devel/calc/ && source /home/rw/devel/calc/venv/bin/activate && python3 get_value.py'
Environment="AWS_CONFIG_FILE=/home/rw/.aws/config"
Environment="AWS_CREDENTIALS_FILE=/home/rw/.aws/credentials"
Environment="AWS_PROFILE=rw"

[Install]
WantedBy=default.target

The file begins by specifying the [Unit] section with a description. This is a human-readable description of the task (unit) which tells us the purpose of the Unit. The next section is the [Service] section, which specifies the service itself.

We only want to run this task once, so it is given a Type of oneshot. We then specify the task itself using ExecStart and the same command line from our existing Crontab entry. Remember, the task in question is a Python script which also uses some AWS SDK for Python (also known as boto3 commands to push data in to AWS. For this reason we need the script to know where to find our AWS credentials and configuration, and do so by setting the Environment variables shown.

The final section [Install] lets systemd know the unit’s dependencies. In this case we use default.target to use the default dependency for Timer units. We could also be more specific and use timers.target. We now need to specify the .timer part of the unit.

To create the timer specification, we edit the following in: ~/.config/systemd/user/get-value.timer:

[Unit]
Description=Run get-value every day
RefuseManualStart=no

[Timer]
Persistent=true
OnCalendar=*-*-* 01:00:00
Unit=get-value.service

[Install]
WantedBy=timers.target

Again, the first section in this file is [Unit] in order to provide a Description and a statement to say that we allow this unit to be run manually from the command line if required (RefuseManualStart directive).

We then specify the Timer itself. The first statement Persistent=true tells systemd to store the last run time of the unit on disk so that if the task will be run when systemd timer is started if its next scheduled run time has already passed. The next line, OnCalendar will run the task on every day, of every week, of every year, at 1am. Finally we specify the Unit service to be run with Unit=get-value.service, which is the name of the service unit specified above and which is in the same directory as this .timer file.

The final section is the [Install] section, where we specify the unit’s dependencies. In this case we use timers.target, just to be specific.

Enabling the Timer

After creating the two files above, enable the systemd timer as follows:

Firstly, reload systemd configuration to pull in the new units.

systemctl --user daemon-reload

(optional) You can try out the new service without scheduling future events:

systemctl --user start get-value

Now enable the timer in systemd.

systemctl --user enable get-value.timer

You can check that it is loaded successfully with list-timers.

systemctl --user list-timers --all

Done!


Related Posts


Published

Category

Linux, Systemd, Cron

Tags

Contact