OpenBSD daily(8) on Linux
Not long ago, I had to setup an Ubuntu VM (I'm running them through
bhyve by the way, and it's really
sweet). But the day after, to my surprise, the lovely heartbeat email
$hostname daily run output
I've been used to was nowhere to be
found. After a quick check, the Ubuntu cron "periodic" stuff doesn't
output anything unless something go wrong.
I could easily add scripts into the
/etc/cron.daily directory, but then I would
have to handle the output "by hand" if they should or should not report
something and the email subject would be the ugly cron line:
Cron <root@$hostname> test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
Ultimately I realized that my dream was something like OpenBSD daily(8) that I am used to (they say old habits die hard). Since they are simple POSIX shell scripts, I've hacked them a bit to run on Ubuntu.
/etc/daily
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | # # $OpenBSD: daily,v 1.88 2016/04/29 13:05:33 schwarze Exp $ # From: @(#)daily 8.2 (Berkeley) 1/25/94 # # For local additions, create the file /etc/daily.local. # To get section headers, use the function next_part in daily.local. # umask 022 PARTOUT=/var/log/daily.part MAINOUT=/var/log/daily.out install -o 0 -g 0 -m 600 /dev/null $PARTOUT install -o 0 -g 0 -m 600 -b /dev/null $MAINOUT start_part() { TITLE=$1 exec > $PARTOUT 2>&1 } end_part() { exec >> $MAINOUT 2>&1 test -s $PARTOUT || return echo "" echo "$TITLE" cat $PARTOUT } next_part() { end_part start_part "$1" } run_script() { f=/etc/$1 test -e $f || return if [ `stat --format '%A%u' $f | cut -b1,6,9,11-` != '---0' ]; then echo "$f has insecure permissions, skipping:" ls -l $f return fi . $f } start_part "Running daily.local:" run_script "daily.local" # NOTE: Removed OpenBSD specific daily stuff. next_part "Services that should be running but aren't:" if systemctl --quiet is-failed '*'; then systemctl --failed fi next_part "Checking subsystem status:" if [ "X$VERBOSESTATUS" != X0 ]; then echo "" echo "disks:" df -ikl if [ -x /sbin/dump ]; then echo "" /sbin/dump W fi; fi next_part "network:" if [ "X$VERBOSESTATUS" != X0 ]; then netstat -ivn fi end_part [ -s $MAINOUT ] && { uname -a echo uptime cat $MAINOUT } 2>&1 | mail -s "`hostname` daily output" root # NOTE: removed OpenBSD security(8) stuff. |
/etc/weekly and /etc/monthly were easier:
/etc/weekly
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | # # $OpenBSD: weekly,v 1.27 2015/08/14 03:02:07 rzalamena Exp $ # # For local additions, create the file /etc/weekly.local. # To get section headers, use the function next_part in weekly.local. # umask 022 PARTOUT=/var/log/weekly.part MAINOUT=/var/log/weekly.out install -o 0 -g 0 -m 600 /dev/null $PARTOUT install -o 0 -g 0 -m 600 -b /dev/null $MAINOUT start_part() { TITLE=$1 exec > $PARTOUT 2>&1 } end_part() { exec >> $MAINOUT 2>&1 test -s $PARTOUT || return echo "" echo "$TITLE" cat $PARTOUT } next_part() { end_part start_part "$1" } run_script() { f=/etc/$1 test -e $f || return if [ `stat --format '%A%u' $f | cut -b1,6,9,11-` != '---0' ]; then echo "$f has insecure permissions, skipping:" ls -l $f return fi . $f } start_part "Running weekly.local:" run_script "weekly.local" # NOTE: Removed OpenBSD specific weekly stuff. end_part rm -f $PARTOUT [ -s $MAINOUT ] && mail -s "`hostname` weekly output" root < $MAINOUT |
/etc/monthly
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | # # $OpenBSD: monthly,v 1.13 2011/01/19 06:18:05 david Exp $ # # For local additions, create the file /etc/monthly.local. # To get section headers, use the function next_part in monthly.local. # umask 022 PARTOUT=/var/log/monthly.part MAINOUT=/var/log/monthly.out install -o 0 -g 0 -m 600 /dev/null $PARTOUT install -o 0 -g 0 -m 600 -b /dev/null $MAINOUT start_part() { TITLE=$1 exec > $PARTOUT 2>&1 } end_part() { exec >> $MAINOUT 2>&1 test -s $PARTOUT || return echo "" echo "$TITLE" cat $PARTOUT } next_part() { end_part start_part "$1" } run_script() { f=/etc/$1 test -e $f || return if [ `stat --format '%A%u' $f | cut -b1,6,9,11-` != '---0' ]; then echo "$f has insecure permissions, skipping:" ls -l $f return fi . $f } start_part "Running monthly.local:" run_script "monthly.local" end_part rm -f $PARTOUT [ -s $MAINOUT ] && mail -s "`hostname` monthly output" root < $MAINOUT |
Then, the system's /etc/crontab has to be adapted to actually run them. I used the same timing as OpenBSD (starting at line 17):
/etc/crontab
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # /etc/crontab: system-wide crontab # Unlike any other crontab you don't have to run the `crontab' # command to install the new version when you edit this file # and files in /etc/cron.d. These files also have username fields, # that none of the other crontabs do. SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin # m h dom mon dow user command 17 * * * * root cd / && run-parts --report /etc/cron.hourly 25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) 47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) 52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) # # OpenBSD-like daily stuff. 30 1 * * * root /bin/sh /etc/daily 30 3 * * 6 root /bin/sh /etc/weekly 30 5 1 * * root /bin/sh /etc/monthly |
Finally, here is my /etc/daily.local. It checks for packages and security updates along with Let's Encrypt SSL certificates renewal:
/etc/daily.local
1 2 3 4 5 6 7 8 9 10 11 12 13 | # hacked from /etc/update-motd.d/90-updates-available to only show something # when there are update(s) available. next_part "Ubuntu packages:" stamp="/var/lib/update-notifier/updates-available" if [ -r "$stamp" ]; then awk 'int($1) {print}' "$stamp" fi next_part "renew Let's Encrypt certificates:" letsencrypt --text --noninteractive --agree-tos renew # XXX: no way of knowing with this client version if we actually have renewed a # certificat, so reload nginx anyway. systemctl reload nginx |
And that's it! Let me know if this post was useful to you and if I missed anything of course. Happy new year by the way :)