Category Archives: UNIX & Linux

systemd delayed service restart

Ich habe einige Monate damit rumgemacht, dass das Konglomerat Telegraf/Prometheus/Grafana von ziemlich starken Abhängigkeiten bei der Startreihenfolge der beteiligten Systeme geplagt ist. Meine Server rebooten alle einmal pro Woche automatisch innerhalb eines gewissen Zeitfensters und seit Grafana musste ich so alle 2-3 Wochen manuell eingreifen und Services neustarten, da insbesondere Telegraf davon ausgeht, dass Schnittstellen, auf die es selbst zugreift, beim eigenen Start erreichbar sein müssen. (Siehe auch: Fallacies of distributed computing.)

Die Frage war also, wie sieht die einfachste Lösung aus, um einen gegebenen Service 60 Minuten nach seinem Start noch einmal automatisch durchzustarten (mit sporadischen Lücken in den Daten kann ich am frühen Sonntagmorgen leben).

Timer und dedizierte Service-Units zu verteilen schien mir zu aufwändig, und ich habe unangenehm lange gegrübelt, wie die simpelste Lösung aussehen könnte, die einfach per systemctl edit foo.service (oder per anderweitig verteiltem systemd drop-in, ymmv) in die betreffende Unit eingebracht werden kann.

Angekommen bin ich schließlich bei dieser Bandwurmzeile:

# /etc/systemd/system/telegraf.service.d/override.conf
[Service]
ExecStartPost=-+sh -c 'systemctl --quiet is-active %N-restart || systemd-run --unit=%N-restart --on-active=1h systemctl restart %N'

Von rechts nach links:

  • systemd-run instanziiert einen transienten timer und service, der in einer Stunde systemctl restart ausführen wird. %N wird dabei durch den Namen der übergeordneten Unit (hier also: telegraf) ersetzt.
  • Der Name des transienten timer und service wird über die Option --unit vorgegeben um im vorherigen Schritt (eins weiter links) vorhersagbar zu sein.
  • systemd-run wird nur dann ausgeführt, wenn nicht bereits eine Unit aktiv ist, die den vorgegebenen Namen hat, den die transiente Unit bekommen würde, wenn sie noch auszuführen wäre.
  • Das ganze ist als Shell-Aufruf gewrappt, um den || – Operator zu haben.
  • ExecStartPost=+ führt als root aus, obwohl die übergeordnete Unit ihre Befehle unprivilegiert ausführt.
  • ExecStartPost=- ignoriert den Fehler aus dem Shell-Einzeiler, wenn die Unit bereits aktiv war. Es könnte ersatzweise auch ; true ans Ende des Einzeilers geschrieben werden.

This is not a nice post. This is a post about Gnome/GDM customization.

I recently got the opportunity to work on adapting the default Debian Gnome experience to a client’s corporate design, and it felt a LOT like reverse-engineering deeply into the undocumented.

I found the work to fall into a number of categories, which I will classify as “dconf policy”, “css”, “xml-manifest” and “packaging”.

GDM logodconf policy, packaging
GDM banner messagedconf policy
GDM background colorcss, xml-manifest
GDM wallpapercss, xml-manifest, packaging
Gnome default wallpaperdconf policy, packaging
Gnome default themedconf policy
Gnome shell pluginsdconf policy, packaging
Gnome UI and plugin defaultsdconf policy
Gnome wallpapersxml-manifest

Note I’m not familiar with any underlying Gnome/GTK philosopy aspects but come from a Linux engineering role and just need to get the job done.

Packaging

The “packaging” class really just means that required assets need to be packaged onto the system and that any shell plugins that should be enabled by default, must be installed.

GDM

The GDM-Settings workflow

For GDM customization, GDM-Settings proved immensely helpful for identifying where to make changes.

# Install via flatpak
sudo apt-get install flatpak gnome-software-plugin-flatpak
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak -y install io.github.realmazharhussain.GdmSettings

# Keep track of where we started off
touch /tmp/now

# Run gdm-settings
flatpak run io.github.realmazharhussain.GdmSettings

# See what changed
find / -type f -newer /tmp/now 2>/dev/null | egrep -v '^/(dev|run|proc|sys|home|var|tmp)'

For this post, I will stick with the default dconf policy filename used by GDM-Settings.

Logo and banner

# /etc/dconf/db/gdm.d/95-gdm-settings
[org/gnome/login-screen]
logo='/usr/share/icons/hicolor/48x48/apps/gvim.png'
banner-message-enable=true
banner-message-text='Welcome to VIMnux'

dconf needs an accompanying profile definition, /etc/dconf/profile/gdm:

user-db:user
system-db:gdm

dconf update needs to be run after modifying these files.

Background color and wallpaper

GDM background settings are hidden deep in the global gnome-shell theme CSS, which itself is hidden in /usr/share/gnome-shell/gnome-shell-theme.gresource.

GDM-Settings completely hides the tedious process of drilling down to the CSS away from the user, which is great from a user perspective, but not what I needed for my customizations. I went with the following workflow for unpacking the files. gresource list lists the file names contained in the gresource file, gresource extract extracts them one by one.

# Unpack /usr/share/gnome-shell/gnome-shell-theme.gresource
# to a temporary directory:
T=$(mktemp -d /tmp/gres.XXX); printf "Resources tempdir: %s\n" $T
cd $T
while read R
do
  gresource extract /usr/share/gnome-shell/gnome-shell-theme.gresource $R > $(basename $R)
done < <(gresource list /usr/share/gnome-shell/gnome-shell-theme.gresource)

At this point, the only file I’m interested in is gnome-shell.css, where I set a black background for my application.

.login-dialog { background: transparent; }
#lockDialogGroup { background-color: rgb(0,0,0); }

Similar CSS for a wallpaper:

.login-dialog { background: transparent; }
#lockDialogGroup {
  background-image: url('file:///usr/share/backgrounds/gnome/wood-d.webp');
  background-position: center;
  background-size: cover;
}

Reassembly of the gresource file requires an XML manifest which I generate using the following script, manifest.py:

#!/usr/bin/env python3

import os, sys, glob
import xml.etree.ElementTree as ET
from io import BytesIO

os.chdir(sys.argv[1])
gresources = ET.Element('gresources')
gresource = ET.SubElement(gresources, 'gresource', attrib = {'prefix': '/org/gnome/shell/theme'})
for resourcefile in glob.glob('*'):
    file = ET.SubElement(gresource, 'file')
    file.text = resourcefile

out = BytesIO()
xmldoc = ET.ElementTree(gresources)
ET.indent(xmldoc)
xmldoc.write(out, encoding='utf-8', xml_declaration=True)
print(out.getvalue().decode())

First generate the XML manifest, then compile the gresources file.

# Generate XML
./manifest.py $T > gnome-shell-theme.gresource.xml

# Compile gresources (glib-compile-resources from libglib2.0-dev-bin)
glib-compile-resources gnome-shell-theme.gresource.xml --sourcedir=$T --target=gnome-shell-theme.gresource

Someone over here decided to indirect /usr/share/gnome-shell/gnome-shell-theme.gresource via /etc/alternatives, do whatever you like.

Note that on the systems I tested this on, gdm.css and gdm3.css could be left out of the gresource file and all changes were made in gnome-shell.css.

Gnome Wallpapers

Speaking of XML, Wallpapers can be installed to someplace intuitive such as /usr/share/backgrounds/corporate but must be accompanied by another XML manifest in /usr/share/gnome-background-properties, which I generate using another XML generator, properties-xml.py:

#!/usr/bin/env python3

import os, sys, glob
import xml.etree.ElementTree as ET
from io import BytesIO

os.chdir(sys.argv[1])
dirname='/usr/share/backgrounds/corporate'
wallpapers = ET.Element('wallpapers')
for wallpaper in glob.glob('*'):
    wallpaper_element = ET.SubElement(wallpapers, 'wallpaper', attrib = {'deleted': 'false'})
    filename = ET.SubElement(wallpaper_element, 'filename')
    filename.text = f"{dirname}/{wallpaper}"
    name = ET.SubElement(wallpaper_element, 'name')
    name.text = wallpaper
    options = ET.SubElement(wallpaper_element, 'options')
    options.text = 'zoom'
    pcolor = ET.SubElement(wallpaper_element, 'pcolor')
    pcolor.text = '#000000'
    scolor = ET.SubElement(wallpaper_element, 'scolor')
    scolor.text = '#ffffff'

out = BytesIO()
xmldoc = ET.ElementTree(wallpapers)
ET.indent(xmldoc)
xmldoc.write(out)
print('<?xml version="1.0" encoding="UTF-8"?>')
print('<!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">')
print(out.getvalue().decode())

Which I run as follows:

./properties-xml.py backgrounds > corporate.xml

/usr/share/backgrounds/corporate/* and /usr/share/gnome-background-properties/corporate.xml then get packaged onto the system.

Gnome Extensions and Defaults

At this point, a dconf user profile needs to be introduced:

$ cat /etc/dconf/profile/user 
user-db:user
system-db:local

(Things get easier from here.)

Default-enabled extensions

I chose to enable the extensions and set related defaults in /etc/dconf/db/local.d/99-extensions:

[org/gnome/shell]
enabled-extensions=[ 'ding@rastersoft.com', 'no-overview@fthx', 'dash-to-dock@micxgx.gmail.com', 'ubuntu-appindicators@ubuntu.com', 'TopIcons@phocean.net' ]

[org/gnome/shell/extensions/dash-to-dock]
dock-fixed=true
background-opacity=0.2
transparency-mode='FIXED'

dconf update needs to be run after modifying this file.

Other Gnome defaults

dconf watch /

dconf watch / in a terminal makes it possible to take note of what configuration options change as changes are being made. They can now be made the defaults in a policy file such as /etc/dconf/db/local.d/99-misc-defaults:

[org/gnome/desktop/wm/preferences]
button-layout='appmenu:minimize,maximize,close'

[org/gnome/terminal/legacy]
theme-variant='dark'

[org/gnome/desktop/interface]
color-scheme='prefer-dark'
gtk-theme='Adwaita-dark'

dconf update needs to be run after modifying this file.

tl;dr: Example customization package

A debian package that provides live examples, can be found here: https://github.com/mschmitt/gnome-local-custom

Too good to #0012

Today: Mounting tar archives, a novel take on uptime, ipxe escapes.


ratarmountAccess large archives as a filesystem efficiently, e.g., TAR, RAR, ZIP, GZ, BZ2, XZ, ZSTD archives

$ virtualenv ~/.local/ratarmount
$ ~/.local/ratarmount/bin/pip3 install -U ratarmount
$ ~/.local/ratarmount/bin/ratarmount
$ install -D ~/.local/ratarmount/bin/ratarmount ~/bin/

$ mkdir -p ~/mnt
$ curl -O https://data.iana.org/time-zones/tzdata-latest.tar.gz
$ ratarmount tzdata-latest.tar.gz ~/mnt
$ find ~/mnt
$ fusermount -u ~/mnt

tuptimeReport historical and statistical real time of the system, keeping it between restarts. Total uptime

tuptime calculates overall uptime of the system it’s running on. It also flags shutdowns as “BAD” if it comes up without having been gracefully stopped before.

As I grew up in an age where uptime braggery was common even among professionals, my entirely unreasonable use case here is to determine uptime since the previous unclean shutdown:

function tuptime-graceful () {
        local tuptime_since=1
        local temp_array
        while read -r line 
        do
                if [[ "${line}" =~ ' BAD ' ]]
                then
                        read -r -a temp_array <<< "${line}"
                        tuptime_since=$(( temp_array[0] + 1 ))
                        break
                fi
        done < <(tuptime --table --order e --reverse)
        tuptime --since "${tuptime_since}"
}

Ampersand in ipxe script

Example is from a Debian preseed environment where preseed/late_command downloads and executes a shell script

set amp &
set latecmd in-target wget ${script_url} ${amp}${amp} in-target bash script.sh
kernel [...] preseed/late_command="${latecmd}"

Failsafe curl

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.

curl_http_status="$(curl -o "${tmpfile}" --write-out '%{http_code}\n' "${url}")"
curl_exit_status=$?

Update: curl versions from 8.3.0 allow writing out to files.

curl -o "${tmpfile}" --write-out '%output{http_status.txt}%{http_code}' "${url}"
curl_exit_status=$?
curl_http_status="$(< http_status.txt)"

curl -n / --netrc / [ --netrc-file ]

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.

Too good to #0011

Mozilla Firefox APT special

The Mozilla Firefox APT repository is incompatible with legacy apt-mirror. Here’s how I install apt-mirror2 as a dedicated python-virtualenv

# apt-get install virtualenv
# virtualenv --no-setuptools /usr/local/apt-mirror2/
# /usr/local/apt-mirror2/bin/pip3 install -U apt-mirror
# /usr/local/apt-mirror2/bin/apt-mirror --help
  • Repeat as-is to update.
  • Here’s the bug that neccessitates the --no-setuptools option: “ModuleNotFoundError: No module named ‘debian'”

mirror.list entry for the Mozilla Firefox APT repository:

deb-all [signed-by=/path/to/packages-mozilla-org.gpg] https://packages.mozilla.org/apt mozilla main
deb-amd64 [signed-by=/path/to/packages-mozilla-org.gpg] https://packages.mozilla.org/apt mozilla main

How to convert Mozilla’s sloppy ASCII-armored PGP key:

$ curl -s -O https://packages.mozilla.org/apt/repo-signing-key.gpg
$ file repo-signing-key.gpg
repo-signing-key.gpg: PGP public key block Secret-Key
$ mv repo-signing-key.gpg repo-signing-key
$ gpg --dearmor repo-signing-key
$ file repo-signing-key.gpg
repo-signing-key.gpg: OpenPGP Public Key Version 4, Created Tue May 4 21:08:03 2021, RSA (Encrypt or Sign, 2048 bits); User ID; Signature; OpenPGP Certificate

Too good to #0010

In today’s installment:

  • “Headless” Nextcloud
  • Monitoring of fork activity

Mount Nextcloud files via rclone+Webdav, as a systemd user unit

# ~/.config/systemd/user/nextcloud-mount.service
[Unit]
Description=Mount Nextcloud via rclone-webdav

[Service]
ExecStartPre=mkdir -p %h/Nextcloud
ExecStart=rclone mount --vfs-cache-mode full --verbose nextcloud_webdav: %h/Nextcloud/
ExecStop=fusermount -u %h/Nextcloud/
ExecStopPost=rmdir %h/Nextcloud

[Install]
WantedBy=default.target

Sync instead of mount

Nextcloud via Webdav is absurdly slow, so maybe use nextcloudcmd instead, which unfortunately does not have its own daemonization:

# ~/.netrc (chmod 600)
machine my.nextcloud.example.com
login myuser
password *** (app password)
# ~/.config/systemd/user/nextcloudcmd.service
[Unit]
Description=nextcloudcmd (service)

[Service]
ExecStart=nextcloudcmd -n --silent %h/Nextcloud https://my.nextcloud.example.com
# ~/.config/systemd/user/nextcloudcmd.timer
[Unit]
Description=nextcloudcmd (timer)

[Timer]
OnStartupSec=60
OnUnitInactiveSec=300

[Install]
WantedBy=default.target

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 forkstat # (that's all)

Too good to #0009

In this episode:

  • urlwatch for new daily Ubuntu Server ISO
  • systemd-run ephemeral timers as replacement for at
  • Mozillateam Firefox on Debian
  • systemd service: ExecStartPre as root
  • gdm3 autosuspend/shutdown behaviour

urlwatch for new daily Ubuntu Server ISO

Somewhat desparate because at the time of starting this post, the (pre-beta, non-LTS, not blaming anyone) server image in question was badly broken.

---
name: Ubuntu Server Daily ISO
url: https://cdimage.ubuntu.com/ubuntu-server/daily-live/current/SHA256SUMS
filter:
  - grep: .*-live-server-amd64.iso
---

systemd-run ephemeral timers as replacement for at

Goes great with “hardened” systems that deny use of at(1).

Run a command 60 seconds from now, via the user’s private systemd (after logout only if session lingering is enabled).

systemd-run --user --on-active=60s -- logger --tag foo "Hello world"

Run a command 2 minutes from now, privileged or as a specific user via the global systemd:

sudo systemd-run --uid="${LOGNAME}" --on-active=2m -- touch /tmp/hello

Insights

systemctl --user list-timers
journalctl --user -u 'run-*.timer'
sudo systemctl list-timers
sudo journalctl -u 'run-*.timer'

Mozillateam Firefox on Debian

$ 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')

systemd service: ExecStartPre as root

[Service]
...
User=nonroot
Group=nonroot
ExecStartPre=+install -d /var/lib/theservice -o nonroot -g nonroot
ExecStart=/usr/sbin/theservice

See systemd.service, “special executable prefixes”.


gdm3 autosuspend/shutdown behaviour

Debian:

$ sudo apt-get install dbus-x11
$ sudo chsh -s /bin/bash Debian-gdm
$ sudo -i -u Debian-gdm
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type
'suspend'
$ dbus-launch gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type
$ exit
$ sudo chsh -s /bin/false Debian-gdm

Arch/Garuda:

$ sudo chsh -s /bin/bash gdm
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type
'suspend'
$ dbus-launch gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type
$ exit
$ sudo chsh -s /usr/bin/nologin gdm

Die USV-Kampagne 2023

DA HAT DOCH WAS GEZUCKT

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.conf habe 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:

# /etc/nut/upsmon.conf (excerpt)
MONITOR ups@localhost 1 upsmon xxx master
NOTIFYCMD /sbin/upssched
NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC

NUT-Scheduler

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

Administration/Troubleshooting

Status der USV abfragen:

upsc ups
upsc ups battery.charge
upsc ups ups.status

Unterstützte Kommandos der USV abfragen:

upscmd -u upsmon -p xxx -l ups

Kommando absetzen:

upscmd -u upsmon -p xxx ups <kommando>
upscmd -u upsmon -p xxx ups test.battery.start.quick
upscmd -u upsmon -p xxx ups test.battery.start.deep

Treiber bei der Interaktion mit der USV zuschauen (sollte eigentlich nie erforderlich sein):

/lib/nut/usbhid-ups -DDD -a ups
/lib/nut/usbhid-ups -DDDD -a ups
/lib/nut/usbhid-ups -DDDDD -a ups

Und welche USV ist nun zu empfehlen?

Es gilt leider nur diese eine Empfehlung, die irgendwo auf der NUT-Homepage auftaucht:

Jede, bei deren Kauf man sicher weiß, dass man ein uneingeschränktes Rückgaberecht hat.

Too good to #0008

rinetd-style circuit level gateway in systemd

This accepts port 465/tcp and forwards all connections to a service running somewhere else on 1194/tcp.

The socket unit accepts the connection on port 465:

# /etc/systemd/system/tcp465-to-tcp1194.socket
[Unit]
Description="openvpn 465/tcp to 1194/tcp (socket)"

[Socket]
ListenStream=465

[Install]
WantedBy=sockets.target

systemd-socket-proxyd connects to the backend:

# /etc/systemd/system/tcp465-to-tcp1194.service
[Unit]
Description="openvpn 465/tcp to 1194/tcp (service)"

[Service]
ExecStart=/lib/systemd/systemd-socket-proxyd 10.12.13.14:1194
User=proxy

(Anyone old enough to remember that this was called a plug-gateway in the TIS Firewall Toolkit?)


Python pip/virtualenv/pipenv micro-HOWTO

Clone project with wacky dependencies:

git clone https://github.com/example/project.git

Install dependencies (from requirements.txt):

pipenv install (-r requirements.txt)

Run:

pipenv run ./script

Template for git commit message

Create the template, I prefer it outside the repository:

(blank line)
(blank line)
foo#1234 is the neverending story I'm constantly working on

Configure the path, relative to the repository root:

git config commit.template ../commit-template-for-foo.txt

“OMG, I see you have committed a manual page…”

…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.)