Skip to main content

EfaPi II: The Cron Job

After the adventures of Part I, I felt ready to embark on a quintessential linux-user activity: writing bash scripts to run as a cron job.

The goal was to automate local backups of the electronic log book of my rowing club. The software behind the log book is efa (elektronischer Fahrtbuch) from N. Michael, which we will be running on a raspberry pi.

It should be noted that there is a version of efa that does everything covered in this project out-of-box. This task was undertaken out of interest rather than necessity. Anyone wanting a similar setup should check out efaLive.

The basics

Tasks in Linux can be scheduled using the cron utility. A scheduled task, also called a cron job, can be a command or shell script. Since there is no built-in command for what I wanted to do, a script was in order.

Any language with a command line interface can be used to write shell scripts. This gives quite some freedom in the choice of language, and a natural choice for me would have been python. But I wanted an excuse to learn the basics of bash.

There are a few basic points to get started. The first line of each script should look familiar to anyone familiar with python:


#!/bin/bash
                

this is the 'shebang' that tells the program loader that this script should be run by the interpreter located at /bin/bash. The bash scripts will also need to be executable.

Finally, some care should be taken to avoid name collisions with existing commands. The which command is useful here. If the name is already taken, which returns the path to the existing script.

Hello, Bash!

Following tradition:


$ touch helloBash.sh
$ chmod u+x helloBash.sh
                

will create a file and make it executable for the current user. The file contents for the clasic greeting should be:


#!/bin/bash

echo "Hello, world!"
                

and then we can run the script from the terminal


$ ./helloBash.sh
Hello, world!
                

The echo command should be familiar from the command line. If you can use it with the command line, then you can use it in bash.

The backups

Initially I planned to just copy some of the files generated by efa, but having a look at the source code on github reveals that the built-in backup import requires a metadata file created by efa when it creates a backup.

Fortunately, efa has a built-in command line interface (CLI). There is a handy overview of the efaCLI in German on the docs wiki. As long as efa is running, we can use the efaCLI from another bash script to run efa-specific commands.

Checking the USB

It's a good idea start by checking if a USB stick is mounted and, if so, getting its mountpoint.

The first command of the backup script is findmnt to get what is effectively the path for the USB. The syntax to assign the output of a command to a variable is varName=$( command [args] ). I used df to find what name to give for the source (/dev/sda1 in this case).


usb=$( findmnt -rno TARGET /dev/sda1)
mounted=$?
                

The command is used with the flags rno. The -r flag escapes potentially unsafe characters. It shouldn't be necessary, but who knows how future USBs will be named. The -n flag prints the output without a header line. Because findmnt returns several columns of information, we use the -o TARGET to indicate that we only want the mountpoint information from the TARGET column.

The cryptic looking $? checks the exit status of the preceeding command. It returns 0 if successful or 1 if it fails. I use this value to exit the script directly if the usb isn't mounted.


if [[ ${mounted} -eq 1 ]]; then
    exit 1
fi
                

Now we start to see some proper bash syntax. This can be read as 'if the value of mounted equals 1, then exit the program.'

Creating the backup

If we're going to be automating use of the CLI, then we need some way to provide the admin login credentials. The efa docs provide the useful recommendation to create a hidden file called .efacred in the home directory with the login credentials. A quick test confirmed that we can now use the efaCLI without manually entering the password. The security implications will be handled later.

We also need names for the backup directory and file:


year=$( date +%Y )
dateStamp=$( date +%m%d )

backupDir="${usb}/${year}_backup"
backupFile="${backupDir}/efaBackup_${dateStamp}.zip"

if [[ ! -d $backupDir ]]; then
    mkdir "$backupDir"
fi
                

I use the date command with appropriate flags to get both the year and a date stamp. These values are used to generate the backup folder and file names. The if statement can be read as 'if the backup directory doesn't already exist, then make it'.

Now all that remains is actually making the backup. To do this, we need to start the efaCLI.sh script. There are some options here. I took the approach of giving the location of bash followed by the location of the script to be run.


/bin/bash /opt/efa2/efaCLI.sh admin@localhost/test -cmd "backup create all ${backupFile}"
                

where 'test' is the name of the project I want to backup.

Removing old backups

I will happily admit that this is a silly thing to do. The backup for the small test project that I made weighed in at 25 kB. Let's say a real project is a whopping 1 MB. For a daily backup to a 16 GB drive, I should start to worry about space in about... 40 years.

Regardless, I decided to write a script to delete the backups after two years.

Similar to the backup script, the cleanup script checks if the usb is mounted and gets the current year. This time a regular expression will also be useful:


re='^[0-9]{4}$'
                

The regular expression isn't as bad as it looks. It translates to 'the first character (^) must be a digit ([0-9]), there must be four digits ({4}) and the last character must be a digit ($)'. In other words, this expression will match four digit integers, which we will assume are year labels.

We then check for and remove files or directories in usb that are more than two years old.


re='^[0-9]{4}$'
currentYear=$( date +%Y )

for entry in "${usb}/"*
do
    name=$( basename $entry )
    year=$( echo $name | cut -d'_' -f1)
    if [[ $year =~ $re ]]
    then
        age=$(( $currentYear-$year ))
        if [[ $age -gt 2 ]]
        then
            rm -r $entry
        fi
    fi
done
                

basename returns the name of the file or directory instead of the full path. echo and a pipe ( | ) are used to pass the name to the command cut, which slices the string into substrings using the underscore character as the delimiter. cut then returns the first field (-f1 ). So if the current entry is the directory 2020_backup, the value of year is 2020.

We then check if year is an integer. The =~ inside double brackets will check if the value matches the pattern of the regular expression. If it matches, then we assume that the integer is the year label. If the difference from the current year is greater than (-gt) 2, the file or directory is deleted.

Security Issues

One of the first things I did in Part I was add a keyboard shortcut for opening terminal windows. Now I've added a file with the efa admin password saved in plain text. This means it is now trivial for anyone to get the efa admin password.

Fortunately, efa can be configured so that the efa window is always on top. This can be done through the GUI for the admin mode via the expert mode of the configuration menu.

The keyboard shortcut will still open new terminal windows. However, they won't be accessible unless the efa window is closed, which is an action that requires the admin password.

The cron job

At last, we come to the cron jobs. We can schedule running our backup and cleaner with crontab


$ crontab -e
                

This initializes a crontab for the user (efa2) and lets us choose what editor we want to use. The basic syntax for a cron job is minute hour day-of-month month day-of-week command

Our rowing club meets on Wednesdays, Saturdays and Sundays. So I scheduled the backup to run at 2 AM the following day (Monday, Thursday and Sunday), and the cleaner to run every January 1st at 2:10 AM.


0 2 * * 1,4,7 /home/efa2/bin/efaBackup.sh
10 2 1 1 * /home/efa2/bin/backupCleaner.sh
                

I also decided to make a log file in efa2's home folder and edited the crontab:


0 2 * * 1,4,7 /home/efa2/bin/efaBackup.sh > /home/efa2/cronjob.log 2>&1
10 2 1 1 * /home/efa2/bin/backupCleaner.sh > /home/efa2/cronjob.log 2>&1
                

The > symbol redirects standard output to the file cronjob.log, over-writing the file each time. >> could be used to append instead. 2>&1 redirects the standard error output to standard out, which itself was already directed to the log file.

A test run confirmed that everything worked, and the adventures of the efaPi finally came to a close.

Conclusions

Apparently my long-standing reluctance to mess with bash was not totally unfounded. It has plenty of traps for novices. bash also now has the distinction of being the first language where I struggled to initialize a variable (no spaces around the assignment operator!).

But I'm also really glad that I did finally spend some time with bash. Once I passed the first hurdles, I found myself really enjoying working on the scripts and finally scheduling my first cron job. This was easily my favorite part of the working on the efaPi.