Nothing serious, just a few notes I like to share with friends and colleagues who, like me, script around curl.
curl -f / --fail
I try to use --fail whenever I can, because why would I want to exit zero on server errors?
$ curl -L https://download.grml.org/grml64-small_2024.02.iso.NO
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at ftp.fau.de Port 443</address>
</body></html>
$ echo $?
0
$ curl -f -L https://download.grml.org/grml64-small_2024.02.iso.NO
curl: (22) The requested URL returned error: 404
$ echo $?
22
curl --fail-with-body
I have a CI/CD situation where curl calls a webhook and it’s incredibly useful to see its error message in case of failure.
$ curl --fail https://binblog.de/xmlrpc.php
curl: (22) The requested URL returned error: 405
$ curl --fail-with-body https://binblog.de/xmlrpc.php
curl: (22) The requested URL returned error: 405
XML-RPC server accepts POST requests only.
set -o pipefail
When curl‘s output gets piped to any other command, I try to remember to set -o pipefail along with curl --fail so if curl fails, the pipe exits non-zero.
#!/usr/bin/env bash
url='https://download.grml.org/grml64-small_2024.02.iso.NONO'
if curl -s -f -L "${url}" | sha256sum
then
echo "Success."
else
echo "Failure."
fi
set -o pipefail
if curl -s -f -L "${url}" | sha256sum
then
echo "Success."
else
echo "Failure."
fi
curl --connect-timeout
Useful to get quicker response in scripts instead of waiting for the system’s default timeouts.
curl -w / --write-out
This may be over the top most of the time, but I have one situation that requires extremely detailed error handling. (The reason being a bit of a foul split DNS situation in the environment, long story.) This is where I use --write-out to analyze the server response.
Username:password authentication is a thing, no matter how much it’s discouraged. Here’s how to at least hide username and password from the process list.
$ chmod 600 ~/.netrc
$ cat ~/.netrc
machine binblog.de
login foo
password bar
$ curl -v -o /dev/null -n https://binblog.de
...
* Server auth using Basic with user 'foo'
...
To use any other file instead of ~/.netrc, use --netrc-file instead.
forkstat (8) – a tool to show process fork/exec/exit activity
High load without a single obvious CPU consuming process (not related to the Nextcloud shenanigans above) led me to forkstat(8):
Forkstat is a program that logs process fork(), exec(), exit(), coredump and process name change activity. It is useful for monitoring system behaviour and to track down rogue processes that are spawning off processes and potentially abusing the system.
$ sudo tee /etc/apt/sources.list.d/mozillateam-ppa.list <<Here
deb https://ppa.launchpadcontent.net/mozillateam/ppa/ubuntu jammy main
deb-src https://ppa.launchpadcontent.net/mozillateam/ppa/ubuntu jammy main
Here
$ sudo tee /etc/apt/trusted.gpg.d/mozillateam.asc < <(curl 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x0ab215679c571d1c8325275b9bdb3d89ce49ec21')
Nach etlichen Sekunden-Stromausfällen, durchaus auch mal in schneller Folge nacheinander, hatte mich ein 40-minütiger Stromausfall endgültig über die Kante geschubst, und ich wollte meine Rechner mit unterbrechungsfreien Stromversorgungen ausstatten.
Ziel war eine USV-Integration, die:
Den angeschlossenen Rechner bei Stromausfall zuverlässig herunterfährt.
Und ihn auch zuverlässig wieder startet.
Utopische Batterielaufzeiten, um irgendwelche Uptimes zu retten, sind bei mir kein Thema, denn alle Systeme, die keine Eingabe einer Passphrase benötigen (also alle bis auf eines), reboote ich wöchentlich aus der Crontab.
First things first: Warum nicht den Marktführer? Warum nicht … APC?
Meine Meinung zu APC ist nicht die beste. Zum einen stört mich enorm, dass APC gefühlt immer noch die exakt selbe Hardware verkauft, sogar original ohne USB, die ich in einem anderen Jahrhundert(!) als Vertriebler im Großhandel verhökert habe. Der apcupsd für Linux scheint seit Ewigkeiten unmaintained, und die Hinweise zu den APC-Hardwaregenerationen bei den Network UPS Tools sind alles andere als ermutigend.
Hardwareauswahl
Der Weg zur richtigen Hardware, die die gewünschte Integration leistet, war steinig und von sehr schweren Pappkartons begleitet.
Für die meisten Anwendungsfälle tut es tatsächlich die wirklich billig-billige 50-Euro-USV von “BlueWalker PowerWalker”, wie sie der kleine Computerladen im Nachbardorf in allen Ausprägungen führt. Der Sinus ist hier allerdings nicht wirklich rund, sondern sehr sehr eckig, so dass er nicht mit jedem PC-Netzteil harmoniert.
Ein Gerät aus der “CSW”-Serie, “Clean Sine Wave” für ca. 150 Euro ebenfalls von “BlueWalker PowerWalker” weigerte sich, das System nach Wiederherstellung der Stromversorgung zuverlässig wieder hoch zu fahren.
Eine “Cyberpower”-USV hatte das beste User-Interface direkt am Panel, zählte die Sekunden der jeweiligen Timings live runter, war aber leider Dead On Arrival mit einem Akku, der wie ein Stein runterfiel, ohne dem angeschlossenen System wenigstens mal 30 Sekunden Zeit zum Runterfahren zu geben.
Nachdem ich einige Wochen Frust geschoben hatte, ging es wieder mit einer PowerWackler weiter, diesmal mit der BlueWalker PowerWalker VI 800 SW. Ein Billiggerät, sieht billig aus, hat ein aus einem Blickwinkel von ca. 0.5 Grad ablesbares LC-Display, und: Funktioniert! Der Sinus ist ulkig windschief, das tut der Funktion aber keinen Abbruch.
Integration
Nach den ersten Tests und der Erkundung der Möglichkeiten, standen meine Wünsche endgültig fest:
30 Sekunden nach dem Stromausfall soll das System runterfahren.
Kommt innerhalb der 30 Sekunden der Strom wieder, soll der Shutdown abgebrochen werden.
60 Sekunden nach dem Shutdown soll das System ausgeschaltet werden.
Kommt während oder nach dem Shutdown der Strom wieder, soll die USV wissen, dass sie das Ding jetzt durchziehen und das System trotzdem aus- und nach einer Wartezeit wieder einschalten soll.
Ist der Stromausfall beendet, soll das System wieder automatisch eingeschaltet werden.
Mit der richtigen USV ist all das problemlos zu konfigurieren. Leider habe ich mir ärgerlich viel Zeit um die Ohren geschlagen, weil ich immer wieder Fehler auf meinem System in meiner Konfiguration gesucht habe.
NUT-Architektur
Die Network UPS Tools (“NUT”) teilen ihren Stack in 3 1/2 Schichten auf:
Der NUT-Treiber übernimmt die Kommunikation mit der USV und stellt sie modellunabhängig den nachgeordneten Schichten zur Verfügung.
Der NUT-Server stellt die Events der USV per TCP bereit, für localhost, oder auch für per Netzwerk angebundene Systeme, die keine lokale USV haben.
Der NUT-Monitor reagiert auf Events, die er vom Server erhält, hierbei kann der Server entweder lokal laufen, oder über das Netzwerk erreicht werden.
Der NUT-Scheduler als Teil des NUT-Monitor führt diese Events aus und verfolgt sie im zeitlichen Ablauf.
Ich habe mich überall für Konfigurationen vom Typ “Netserver” entschieden, bei denen aber der NUT-Server hinter einer lokalen Firewall für Verbindungen von außen geblockt ist.
NUT-Treiber
Der NUT-Treiber ist, wenn man einmal akzeptiert hat, dass die USVen alle buggy Firmware haben und man nie bei NUT die Schuld für Fehlfunktionen zu suchen hat, ganz einfach zu konfigurieren. Außer der Auswahl des passenden Subtreibers ist lediglich zu beachten, dass die USV-Firmwares die Timings mal in Sekunden, mal in Minuten und mal gemischt(!) entgegennehmen. Bei manchen darf auch kein ondelay von unter 3 Minuten konfiguriert werden. Was weiß denn ich. Eine /etc/nut/ups.conf:
# /etc/nut/ups.conf für BlueWalker PowerWalker VI 800 SW
maxretry = 3 # Erforderlich
[ups]
driver = blazer_usb # Wahrscheinlichste Alternative: usbhid-ups
port = auto
offdelay = 60 # Zeit bis zum Ausschalten nach Shutdown in Sekunden
ondelay = 3 # Mindestwartezeit bis zum Wiedereinschalten in Minuten
NUT-Server
Der NUT-Server ist etwas unübersichtlich zu konfigurieren, insbesondere bei der Rollenzuweisung im Rahmen seiner Userverwaltung. Die zentrale Konfigurationsdatei /etc/nut/nut.conf ist aber noch äußerst übersichtlich:
# /etc/nut/nut.conf
MODE=netserver
/etc/nut/upsd.confhabe ich inhaltlich leer gelassen (Voreinstellung, alles auskommentiert), hier können für den Netzwerkbetrieb Zertifikate und/oder für den lokalen Betrieb die Bindung auf Localhost konfiguriert werden.
In /etc/nut/upsd.users wird der User angelegt, mit dem sich der NUT-Monitor beim Server anmelden wird. Bei “upsmon master” scheint es sich um eine Art Macro zu handeln, das bestimmte Rechte für den User vorkonfiguriert; die Doku ist nicht allzu verständlich und es ist möglich, dass die expliziten “actions” hier redundant konfiguriert sind. Ansonsten wird hier explizit festgelegt, dass der User “upsmon” mit dem Passwort “xxx” “Instant Commands” an die USV senden darf, dass er mit SET diverse Einstellungen an ihr vornehmen darf, und dass er den FSD, den Forced Shutdown, einleiten darf.
# /etc/nut/upsd.users
[upsmon]
password = xxx
instcmds = ALL
actions = SET
actions = FSD
upsmon master
NUT-Monitor
Der NUT-Monitor ist die Kernkomponente, die tatsächlich den Shutdown des Systems einleiten und/oder abbrechen wird.
Zunächst muss die Kommunikation mit der USV namens “ups” mit dem User “upsmon” etabliert werden. “master” bedeutet, dass die USV hier am System lokal angeschlossen ist, die 1 ist eine Metrik für den Fall, dass mehrere USVen angeschlossen sind. Erhaltene Events werden an den NUT-Scheduler delegiert, und es sollen ausschließlich die Events ONLINE und ONBATT behandelt werden.Hier nur die relevanten zu ändernden Zeilen aus /etc/nut/upsmon.conf:
Dem NUT-Scheduler wird der Pfad zu einem Shellscript übergeben, das den Shutdown des Systems handhaben wird. Die beiden Werte PIPEFN und LOCKFN haben keine Voreinstellungen und müssen sinnvoll belegt werden. Hier die komplette /etc/nut/upssched.conf:
# /etc/nut/upssched.conf
# https://networkupstools.org/docs/user-manual.chunked/ar01s07.html
CMDSCRIPT /usr/local/sbin/upssched-cmd
PIPEFN /run/nut/upssched.pipe
LOCKFN /run/nut/upssched.lock
AT ONBATT * START-TIMER onbatteryshutdown 30
AT ONLINE * CANCEL-TIMER onbatteryshutdown
AT ONBATT * EXECUTE onbattery
AT ONLINE * EXECUTE online
Wenn der Event ONBATT behandelt wird, die USV sich also im Batteriebetrieb befindet:
Wird ein Timer gestartet, der in 30 Sekunden das CMDSCRIPT mit dem Argument onbatteryshutdown ausführen wird.
Wird das CMDSCRIPT ausgeführt mit dem Argument onbattery, das die eingeloggten User über den Stromausfall und den in 30 Sekunden bevorstehenden Shutdown informiert.
Wenn der Event ONLINE behandelt wird, die USV sich also nicht mehr im Batteriebetrieb befindet:
Wird der zuvor gestartete Timer abgebrochen.
Wird das CMDSCRIPT ausgeführt mit dem Argument online, das die eingeloggten User über den abgebrochenen Shutdown informiert.
CMDSCRIPT /usr/local/sbin/upssched-cmd
Das Herz des Systems ist natürlich in liebevoller Manufakturqualität selbstgescriptet. Der Shutdown selbst wird mit /sbin/upsmon -c fsd bei NUT-Server in Auftrag gegeben, der theoretisch auch noch die Aufgabe hätte, die Shutdowns von per Netzwerk angebundenen Systemen abzuwarten. Bei diesem Forced Shutdown sagt NUT-Server der USV Bescheid, dass der Shutdown jetzt durchgezogen wird und sie nach der im NUT-Treiber konfigurierten offdelay die Stromversorgung auch wirklich aus- und nach Wiederherstellung der Stromversorgung, oder einer Mindestwartezeit, wieder einschalten soll.
#!/usr/bin/env bash
me_path="$(readlink -f "$0")"
case "${1}" in
'onbattery')
/usr/bin/logger -p daemon.warn -t "${me_path}" "UPS on battery."
/usr/bin/wall <<-Here
$(figlet -f small BLACKOUT)
$(figlet -f small BLACKOUT)
+++++ SYSTEM WILL SHUT DOWN IN 30 SECONDS. +++++
Here
;;
'onbatteryshutdown')
/usr/bin/logger -p daemon.crit -t "${me_path}" "UPS on battery, forcing shutdown."
/usr/bin/wall <<-Here
$(figlet -f small BLACKOUT)
$(figlet -f small BLACKOUT)
+++++ SYSTEM IS SHUTTING DOWN N O W. +++++
Here
/sbin/upsmon -c fsd
;;
'online')
/usr/bin/logger -p daemon.warn -t "${me_path}" "UPS no longer on battery."
/usr/bin/wall <<-Here
$(figlet -f small SHUTDOWN)
$(figlet -f small ABORTED)
Power restored. Shutdown aborted. Have a nice day. <3
Here
;;
*)
/usr/bin/logger -p daemon.info -t "${me_path}" "Unrecognized command: ${1}"
echo '?'
;;
esac
…and this is how it’s done, the simplest way possible. I initially heard about this technique from Jan-Piet Mens, a large-scale fiddler unlike me, and have fully committed to it.
Write a Markdown file with a manpage structure and a tiny bit of syntactic legalese at the top. I’ll call mine demo.7.md, but I’ve also gone with having it double as a README.md in the past.
% demo(7) | A demo manual page
# Name
**demo** - A demo manual page
# Synopsis
`demo` (No arguments are supported)
# History
Introduced as an example on a random blog post
# See also
* pandoc(1)
Convert to a manual page markup using pandoc(1) and view the manpage:
pandoc --standalone --to man demo.7.md -o demo.7
man -l demo.7
That’s your quick-and-dirty WYSIWYG manual page.
(Update Sep. 29, 2023: Fixed missing “.7” in final man -l invocation.)
Ich bin wirklich dazu übergangen, mich komplett auf systemd-Timer statt Crontab-Einträge einzulassen, lediglich die Zeitangaben für kalendergebundene Events machen mir dauerhaft zu schaffen. Mir ist ein Rätsel, dass die allwissenden systemd-Entwickler darauf verzichtet haben, eine zusätzliche Konfigurationsmöglichkeit über die bekannten, selbsterklärenden und intuitiv verständlichen Crontab-Spezifikationen zu akzeptieren. Hier also eine Handvoll Beispiele:
Wann
Crontab
OnCalendar
Täglich um Uhr
45 13 * * *
13:45:00
Alle 5 Minuten
*/5 * * * *
*:00/5:00
Montags, Mittwoch, Donnerstag um Uhr
14 9 * * 1,3,5
Mon,Wed,Fri 09:14:00
Montag bis Freitag um Uhr
0 4 * * 1-5
Mon..Fri 04:00:00
Jeden Monatsersten um Uhr
0 6 1 * *
*-*-1 06:00:00
Alle 5 Minuten von 06:00 – 17:55
*/5 6-17 * * *
06..17:00/5:00
⚠️ Achtung: In der Tabelle sind non-breakable Spaces enthalten. ⚠️ Die Werte sind somit nicht für copy&paste geeignet.
Zum Testen mit systemd-analyze wird die jeweilige Definition mit Anführungszeichen übergeben:
Alternativ, insbesondere bei hoher Ausführungsfrequenz oder bei Überschneidungsgefahr, benutze ich, wenn ich schon in systemd unterwegs bin, heute gern monotone Timer.
Versuche, hybride Timer mit Elementen aus beiden Timer-Typen zu konfigurieren, haben bei mir keine Fehlermeldungen produziert, aber der OnCalendar-Teil der Konfiguration wurde ignoriert. Das genaue Verhalten scheint nicht definiert zu sein.
WantedBy sollte bei Timern im Systemkontext auf timers.target lauten, da damit die NTP-Synchronisation vorm Start des Timer sichergestellt ist. Im Userkontext (Wallpaperchanger, Zeiterfassung, dies das) steht nur default.target zur Verfügung.
Inb4, warum überhaupt auf systemd-Timer statt crontab einlassen? Ganz einfach, weil systemd-Timer absolut narrensicher zu managen sind:
Du willst einen systemd-Timer verteilen/managen/paketieren? Kein Problem. Zwei Dateien nach /etc/systemd/system packen, systemctl daemon-reload, systemctl enable, fertig. Anders als in der Crontab musst du dir keine Meta-Syntax ausdenken, um zu identifizieren, ob der Eintrag schon da ist, und um ihn punktgenau löschen zu können.
Du willst einen vorinstallierten Timer anpassen? Ebenfalls kein Problem. Du legst ein Drop-In daneben, in dem du deine neue Timer-Spezifikation hinterlegst, etwa /etc/systemd/system/apt-daily.timer.d/local.conf als Drop-In für /lib/systemd/system/apt-daily.timer.
See also TGT0006, this is just as useful for downgrading privileges on the fly.
“When was the last time apt-get on that Debian/Ubuntu machine installed package upgrades?”
Reliably answering this is a lot harder than it looks, subject of countless discussions and really does need to parse /var/log/apt/history.log, which is painful.
The script below maintains a file /var/log/apt/lastupgrade with the last upgrade’s time stamp, for further processing.
Does NOT track invocations of apt-get upgrade that did not lead to package upgrades.
Does NOT look behind logfile rotations, which should not be a problem because it’s closely hooked to dpkg.
/usr/sbin/apt-lastupgrade:
#!/bin/bash
while IFS=: read -r key value
do
if [[ "${key}" == 'Start-Date' ]]
then
upgraded=0
elif [[ "${key}" == 'Upgrade' ]]
then
upgraded=1
elif [[ "${key}" == 'End-Date' ]]
then
if [[ ${upgraded} -eq 1 ]]
then
printf -v lastupgrade "%s" "${value}"
fi
upgraded=0
fi
done < /var/log/apt/history.log
if [[ -v lastupgrade ]]
then
tee /var/log/apt/lastupgrade <<-Here
# Timestamp of last upgrade: ${lastupgrade}
Here
touch -d "${lastupgrade}" /var/log/apt/lastupgrade
fi
tl;dr: Accounts expire as soon as UTC reaches the expiration date. In today‘s installment of my classic shame-inducing series “UNIX basics for UNIX professionals”, I want to talk about account (and password) expiration in /etc/shadow on Linux. The expiration time is specified as days since january 1st, 1970. In the case of account expiration, the according value can be found in the second to last field in /etc/shadow. Account expiration can be configured using the option „-E“ to the „chage“ tool. In this case, I want the user „games“, which I‘ll be using for demonstration purposes, to expire on the 31st of december, 2017:
# chage -E 2017-12-31 games
Using the „-l“ option, I can now list the expiration date of the user:
# chage -l games
[…]
Account expires : Dec 31, 2017
[…]
The first thing to be taken away here is that, as I can only use a number of days, I can not let a user expire at any given time of day. In /etc/shadow, I have now:
This of course can to be converted to a readable date:
# date --date='1970-01-01 00:00:00 UTC 17531 days'
Sun Dec 31 01:00:00 CET 2017
So, will the account still be usable on december 31st? Let‘s change it‘s expiration to today (the 7th of July, 2017) to see what happens:
# date
Fri Jul 7 12:58:32 CEST 2017
# chage -E today games
# chage -l games
[…]
Account expires : Jul 07, 2017
[…]
# su - games
Your account has expired; please contact your system administrator
[…]
I’m now only left with the question whether this expiration day is aligned on UTC or local time.
# getent shadow | awk -F: '/^games:/{print $8}'
17354
# date --date='1970-01-01 00:00:00 UTC 17354 days'
Fri Jul 7 02:00:00 CEST 2017
I‘ll stop my NTP daemon, manually set the date to 00:30 today and see if the games user has already expired:
# date --set 00:30:00
Fri Jul 7 00:30:00 CEST 2017
# su - games
This account is currently not available.
This is the output from /usr/sbin/nologin, meaning that the account is not expired yet, so I know for sure that the expiration date is not according to local time but to UTC. Let‘s move closer to our expected threshold:
# date --set 01:30:00
Fri Jul 7 01:30:00 CEST 2017
# su - games
This account is currently not available.
Still not expired. And after 02:00:
# date --set 02:30:00
Fri Jul 7 02:30:00 CEST 2017
# su - games
Your account has expired; please contact your system administrator
So, in order to tell from a script whether an account has expired, I simply need to get the number of days since 1970-01-01. If this number is greater or equal to the value in /etc/shadow, the user has expired.
DAYSSINCE=$(( $(date +%s) / 86400 )) # This is days till now as per UTC.
EXPIREDAY=$(getent shadow | awk -F: '/^games:/{print $8}')
if [[ $DAYSSINCE -ge $EXPIREDAY ]] # Greater or equal
then
EXPIRED=true
fi
One last thought: We’ve looked at a time zone with a small offset from UTC. What about timezones with larger offsets, in the other direction?
If we move the timezone to the east, further into the positive from UTC, it will behave the same as here in CEST and the account will expire sometime during the specified day, when UTC hits the same date.
If we move the timezone far to the west, like e.g. PST, and an absolute date is given to “chage -E“, the account will probably expire early, the day before scheduled expiration. I was not able to find anything useful on the web and even my oldest UNIX books from the 1990s mention password expiration only casually, without any detail. Active use of password expiration based on /etc/shadow seems to be uncommon. The code that seems to do the checking is here and it does not appear to care about time zones at all.
Any comments that clarify the behaviour in negative offsets from UTC will be appreciated.
Figuring this out took me quite a bit of time. In the end, I approached the starter of this hilariously useless CentOS mailing list thread, who assured me that indeed he had found a way to configure MD-RAID in the installer, and behold, here’s how to install CentOS 7 with glorious old-school software RAID. In the “Installation Destination” screen, select the drives you want to install onto and “I will configure partitioning”. Then click “Done”: In the “Manual Partitioning” screen, let CentOS create the partitions automatically, or create your own partitioning layout. I will let CentOS create them automatically for this test. Apparently due to restrictions in the Installer, /boot is required, but can’t be on a logical volume, so it appears as primary partition /dev/sda1. The root and swap volumes are in a volume group named centos. The centos volume group will need to be converted to RAID 1 first. Select the root volume and find the “Modify…” button next to the Volume Group selection drop-down. A window will open. In this window, make sure both drives are selected and select “RAID 1 (Redundancy)” from the “RAID Level” drop-down. Repeat this for all volumes in the centos volume group. If you are using the automatic partition layout, note at this point, how, after this step, the file system sizes have been reduced to half their size. As the final step, select the /boot entry and use the “Device Type” drop-down to convert /boot to a “RAID” partition. A new menu will appear, with “RAID 1 (Redundancy)” pre-selected. The sda1 subscript below the /boot file system will change into the “boot” label once you click anywhere else in the list of file systems. Click “Done”, review the “Summary of Changes”, which should immediately make sense if you have ever configured MD-RAID, and the system will be ready for installation.
That’s a bit of a stupid question. Of course you know what the slash in crontab(5) does, everyone knows what it does. I sure know what it does, because I’ve been a UNIX and Linux guy for almost 20 years. Unfortunately, I actually didn’t until recently. The manpage for crontab(5) says the following: It’s clear to absolutely every reader that */5 * * * * in crontab means, run every 5 minutes. And this is the same for every proper divisor of 60, which there actually are a lot of: 2, 3, 4, 5, 6, 10, 12, 15, 20, 30 However, */13 * * * * does not mean that the job will be run every 13 minutes. It means that within the range *, which implicitly means 0-59, the job will run every 13th minute: 0, 13, 26, 39, 52. Between the :52 and the :00 run will be only 8 minutes. Up to here, things look like a simple modulo operation: if minute mod interval equals zero, run the job. Now, let’s look at 9-59/10 * * * *. The range starts at 9, but unfortunately, our naive modulo calculation based on wall clock time fails. Just as described in the manpage, the job will run every 10th minute within the range. For the first time at :09, after which it will run at :19 and subsequently at :29, :39, :49 and :59 and then :09 again. Let’s look at a job that is supposed to run every second day at 06:00 in the morning: 0 6 */2 * *. The implied range in */2 is 1-31, so the job will run on all odd days, which means that it will run on the 31st, directly followed by the 1st of the following month. The transitions from April, June, September and November to the following months will work as expected, while after all other months (February only in leap years), the run on the last day of the month will be directly followed by one on the next day. The same applies for scheduled execution on every second weekday at 06:00: 0 6 * * */2. This will lead to execution on Sunday, Tuesday, Thursday, Saturday and then immediately Sunday again. So, this is what the slash does: It runs the job every n steps within the range, which may be one of the default ranges 0-59, 0-23, 1-31, 1-11 or 0-7, but does not carry the remaining steps of the interval into the next pass of the range. The “every n steps” rule works well with minutes and hours, because they have many divisors, but will not work as expected in most cases that involve day-of-month or day-of-week schedules. But we all knew this already, didn’t we?
What goes up, must come down. Ask any system administrator.