Bash: Messen in Sekunden und Millisekunden
Ich hatte bereits einmal einen ähnlichen Blog Eintrag geschrieben:
Bash: Ausführungszeit eines Kommandos in Millisekunden messen
Jene Methode basierte auf dem Parsing der Ausgabe des time Kommandos.
Nachfolgendes Beispiel-Snippet nimmt einen anderen Ansatz: Das Date-Kommando kann mit %N den Anteil der Nanosekunden zurückgeben. Man nimmt das date Kommando, um einen initialen Zeitstempel zu speichern. Zur Ausgabe der vergangenen Zeit liest man die Zeit erneut aus und berechnet die Differenz. Dazu könnte man bc hernehmen, aber das ist per Default auf einem Linux-Server vorinstalliert. Daher ist mal hier eine Variante mit awk:
export CW_timer_start # Step 1 # start/ reset timer # no parameter is required function start_timer(){ CW_timer_start=$( date +%s.%N ) } # Step 2 (as often you want) # get time in sec and milliseconds since start # no parameter is required function show_timer(){ local timer_end=$( date +%s.%N ) local totaltime=$( awk "BEGIN {print $timer_end - $CW_timer_start }" ) local sec_time=$( echo $totaltime | cut -f 1 -d "." ) test -z "$sec_time" && sec_time=0 local ms_time=$( echo $totaltime | cut -f 2 -d "." | cut -c 1-3 ) echo "$sec_time.$ms_time sec" }
Die Funktion start_timer setzt den initialen Wert bzw. ein Reset.
Die Funktion show_timer zeigt die vergangene Zeit in Sekunden + Punkt + 3stellige Millisekunden an.
Ein Snippet zum Messen sieht grob etwa so aus:
# reset timer start_timer # hier irgendwas machen # sei kreativ :-) # Ausführungszeit: echo "verbrauchte Zeit: $( show_timer )"
Die Ausgabe ist dann soetwas, wie:
verbrauchte Zeit: 0.014 sec
Mysqldump - trotz Exitcode 0 keine Daten im Dumpfile
Ich verwende diesen Aufruf, um Schemata auf dem Mysql Server zu dumpen:
mysqldump --opt --default-character-set=utf8 --flush-logs --single-transaction --no-autocommit --result-file="$_dumpfile" "$_dbname" 2>&1 $myrc=$?
… und stand vor dem Phänomen, dass der Exitcode 0 war - also eigentlich den Erfolg meldet - aber es keinerlei Daten im Dump gab.
Das Betriebssystem war ein Centos8 Stream. Das Problem lag in der Option –flush-logs, die nicht ausgeführt werden konnte, da das Verzeichnis /var/log/mysql/ (warum auch immer) nicht mehr vorhanden war. Dass ein leerer Dump ohne Daten erzeugt wird und kein Exitcode > 0, ist m.E. ein Fehler im mysqldump. Und den versuche ich, zu umschiffen.
Ich habe nach dem Dump noch einen Check hinzugefügt, der das Vorhandensein auf mind. ein CREATE oder aber INSERT Statements prüft. Oder anders: wenn man eine (korrekte) leere Datenbank ohne einzige Tabelle oder auch nur 1 einzigem gesicherten Datensatz hat, würde ein Fehler beim Backup gemeldet. Ich meine, damit lässt es sich leben.
mysqldump --opt --default-character-set=utf8 --flush-logs --single-transaction --no-autocommit --result-file="$_dumpfile" "$_dbname" 2>&1 $myrc=$? if [ $myrc -eq 0 ]; then if ! zgrep -iE "(CREATE|INSERT)" "$_dumpfile" >/dev/null then typeset -i local _iTables _iTables=$( mysql --skip-column-names --batch -e "use $_dbname; show tables ;" | wc -l ) if [ $_iTables -eq 0 ]; then echo "EMPTY DATABASE: $_dbname" else echo "ERROR: no data - the dump doesn't contain any CREATE or INSERT statement." # force an error $myrc=1 fi fi fi if [ $myrc -eq 0 ]; then echo "OK" # Und dann hier noch komprimieren... # gzip $_dumpfile" # ... und Erfolg der Kompression auswerten else echo "ERROR: mysqldump failed." fi
Update:
- 24.03.2022 - eine leere Datenbank würde als Fehler gemeldet - daher zähle ich mal noch die Tabellen
weiterführende Links:
- mysql.com: mysqldump
- mariadb.com: mysqldump
- IML open source: IML-Backup (mein Backup Tool an unserem Institut zum lokalen Dumpen div. DBs und Backup mit Restic/ Duplicity)
- DOCs: os-docs.iml.unibe.ch/iml-backup/
20 Jahre Axels Cron-wrapper
Ich habe grad in die History geschaut: 20 Jahre (!!!) nutze ich schon den meinigen Cronwrapper auf Linux/ Unix-Systemen.
Und ich bin noch immer am Aktualisieren und Verfeinern von dessen Skripten oder Dokumentation. Auch weil ich dessen Idee so mag. Alles ist OpenSource - GNU GPL 3.0.
Das Repository [1] enthält
- cronwrapper.sh - ein Wrapper - wenn man Cronjobs hat, dann stellt man den einfach mal vorn dran
- cronstatus.sh - das Skript zeigt den Status aller auf dem System befindlichen Cronjobs
- inc_cronfunctions.sh - ein Script, dass in Bash geschriebene Cronjobs gesourct werden kann, um div. nützliche Funktionen zu nutzen.
- Dokumentation im Markdown Format zur Installation und allen Features.
Der einfachste Einstieg ist, den Wrapper cronwrapper.sh zu verwenden: mit allen bestehenden Cronjobs, egal welcher Programmiersprache jene sind, lässt sich dieser ergänzen. Und man erhält out-of-the-box kleine nette Features.
- STDOUT und STDERR werden eingefangen und in ein normiertes Logfile geschrieben (ohne jenes explizit angeben zu müssen)
- Das Logfile wird normiert und ist mit grep nach Output oder Metadaten durchsuchbar
Alle Cronjobs, die den Wrapper verwenden, werden mit dem Skript cronstatus.sh aufgelistet und bewertet:
- weisen sie exitcode 0 auf?
- sind sie in der vorgegebenen Frist gestartet worden?
Diese Ausgabe kann man auch in einem Monitoring Script für die Überwachng aller Cronjobs auf seinen Systemen einbinden.
Und als Goodie gibt es ein Include file inc_cronfunctions.sh,welches hilfreich sein kann, sofern die Cronjobs in Bash geschrieben sind: eine Reihe nützlicher Funktionen werden darin bereitgestellt, um das Schreiben von “sicheren” und lesbaren Conjobs zu erleichtern.
weiterführende Links: (en)
- Github: cronwrapper
- Docs auf axel-hahn.de
PHP CLI und FPM-Service: /tmp besser meiden
Ich habe da eine PHP-Applikation, die ein Webinterface besitzt als auch per CLI einige Dinge - z.B. als Cronjob - aufruft.
Beide Arten von Skripten - bei Http Aufruf oder aber CLI - durchliefen dasselbe Init-Prozedere und referenzierten ein und dasselbe File.
$this->sTouchfile = sys_get_temp_dir() . '/some_file.tmp';
Oder besser: ich hatte es zumindest gemeint. sys_get_temp_dir() wurde auf dem Linux-System als “/tmp” aufgelöst. Also optisch war es dieselbe Datei: /tmp/some_file.tmp - aber Das Webinterface erkannte eine Aktion auf CLI Seite nicht … und umgekehrt das CLI nicht eine Schreibaktion aus dem Webinterface.
Bis ich dann mal auf die Idee kam und mir mit einem per Http aufgerufenem Skript eine spezifische Datei in /tmp/ anlegte und diese mal im Filesystem suchte. Unter dem PHP-FPM Service war als /tmp/ jenes “tmp” Verzeichnis verwendet worden:
/tmp/systemd-private-d1b7cf65cce54d4ca9f98c49cca1887f-httpd.service-NoEzTN/tmp/
Das hat mir das ungewöhnliche Verhalten auch sofort erklärt.
Man merke: /tmp sollte man meiden, wenn man gemeinsame Daten per http als auch CLI Daten teilt. Ich habe in meiner Applikation von sys_get_temp_dir() auf ein “tmp” unterhalb [Approot] umgestellt…
weiterführende Links:
Bash: Debug Infos und Fehler auf STDERR ausgeben
Manchmal möchte man Hilfsausgaben in seinem Bash-Skript haben. 2 kleine Hildfsfunktionen definiert:
- _wd für write debug infos zur Ausgabe von optional sichtbaren Kommentaren und
- _we für write error zum Einblenden von Fehlermeldungen.
# write a debug message in yellow to STDERR function _wd(){ test $DEBUG -eq 0 || echo -e "e[33m# DEBUG: $*e[0m" >&2 } # write error message in red to STDERR function _we(){ echo -e "e[31m# ERROR: $*e[0m" >&2 }
… und dann kann man in seinem Skript schreiben
# global var: enable debug output: 0|1 DEBUG=1 ... # example debug output _wd "show 3 oldest items in directory content" ls -ltr | head -3 ... # example error _we "Something went wrong :-/" exit 1
IML Appmonitor ist nun PHP 8.1 kompatibel
Der Appmonitor ist eine PHP-Applikation mit geringstmöglichen Anforderungen. Mit dieser lassen sich andere (Web-)Applikationen in einem Webfrontend überwachen und bei Statuswechsel Notifikationen per E-Mail und Slack an die am Projekt beteiligten Personen auslösen.
Das Applikationsmonitoring ergänzt unser System-Monitoring, weil es die applikationsseitigen Checks aus Sicht der Applikation und mit den Rechten der Applikation ausführt.
Der Appmonitor ist Freie Software, Opensource und untersteht der GNU GPL 3.0.
Nach gefühlt 2 Jahren habe ich am IML Appmonitor weitergecodet. Es sollte PHP 8 kompatibel werden. Und weil soeben PHP 8.1 erschienen ist, wurde es nun auch für ebenjene Version 8.1 fit gemacht:
Tadaaa: der Appmonitor ist die erste PHP 8.1 kompatible Opensource Applikation unseres Instituts :-)
Zudem wird die Plugin-Unterstützung vorangetrieben, es kam eine grafische Darstellung der Checks in einem ersten Wurf hinzu und die Möglichkeit, die Checks nach vorgegebenen Gruppen zu clustern. Die Notifikation eines Fehlers erfolgt nicht beim ersten Auftreten, sondern wird erst bei der 3. Wiederholung ausgelöst.
Ich bin gerade motiviert, weitere Updates einzubringen und nachzuliefern. Es gibt beispielsweise vorgefertigte Checks für einige PHP-Applikationen, wie Wordpress, Ilias LMS, Concrete5 und Matomo. Diese befinden sich in einem separaten Repository und werden in Kürze in das Repo integriert.
weiterführende Links:
Bash: Mehrfachaufrufe eines Skriptes sequentiell abarbeiten
Ich habe ein Shellskript. Dieses wird von mehreren Rechnern und auch von denen ggf. mehrfach aufgerufen.
Und nun möchte ich, dass viele dieser Aufrufe nicht parallel, sondern nacheinander ausgeführt werden - gern in der Reihenfolge des Aufrufs.
In diesem Fall war es ein Wrapper Skript [1], das mit Acme.sh [2] SSL Zertifikate via Let’s Encrypt [3] löst. Man kann ein Ansible [4] Skript parallelisieren: N Server möchten also gleichzeitig vielleicht dasselbe Zertifikat. Nur der erste Aufruf soll ein Zertifikat erstellen und die nachfolgenden Aufrufe, die “vielleicht” dasselbe Zertifikat bearbeitet haben wollen, sollen nicht ebenfalls das Zertifikat erstellen, sondern auf die Ausstellung des ersten Aufrufes warten und dann dessen erstellte Zertifikatsdateien verwenden.
Anm: Let’s Encrypt Live-Server blockieren nach zu vielen Aufrufen für dieselbe Domain für 1 Woche alle Zertifikats-Aktionen, was sehr unangenehm für ein produktives System “sein kann” - auch das möchte ich daher applikatorisch unterbinden.
Ein Queuing will ich nicht bauen oder Semaphoren nutzen. Alles zu kompliziert für diesen Zweck.
Mein Ansatz: die Prozessliste.
Ich baue zunächst eine Funktion - so rein für Wiederverwendungszwecke und/ oder für ein gesourctes Skript mit Funktionen. Aus der Prozessliste per ps -ef filtere ich alle Prozesse, die den Interpreter Bash enthalten und das Shellskript des eigenen Namens $0. Die Prozessliste wird nach der Spalte mit den PIDs numerisch sortiert. Das Filtern mit der Bash ist wichtig, dass nicht andere Prozesse mit demselben Shellskript - z.B. ein offener Editor, der das Skript gerade bearbeitet - versehentlich die echten Abrufe blockieren.
ps -ef | grep "bash.*$0" | grep -v "grep" | sort -k 2 -n
Wenn darin in der ersten Zeile …
[irgendein Kommando] | head -1
… die eigene Prozess ID $$ in der 2. Spalte steht, dann ist der gerade laufende Prozess derjenige mit der kleinsten PID und darf weiterlaufen.
Wenn nicht, gibt es ein sleep für ein paar Sekunden, bevor nochmals getestet wird.
Und so sieht die Funktion _wait_for_free_slot() dann aus:
showdebug=1 # "neverending" loop that waits until the current process is # the one with lowest PID function _wait_for_free_slot(){ local _bWait=true typeset -i local _iFirstPID=0 _wd "--- Need to wait until own process PID $$ is on top ... " while [ $_bWait = true ]; do _iFirstPID=$( ps -ef | grep "bash.*$0" | grep -v "grep" | sort -k 2 -n | head -1 | awk '{ print $2}' ) if [ $_iFirstPID -eq $$ ]; then _bWait=false _wd "OK. Go!" else _wd "- all instances" test ${showdebug} && ps -ef | grep "bash.*$0" | grep -v "grep" | sort -k 2 -n sleep 10 fi done } # write debug output if showdebug is set to 1 function _wd(){ test ${showdebug} && echo "DEBUG: $*" }
Und dann baue ich in mein Skript jene Funktion _wait_for_free_slot ein - immer dort, wo ich eine sequentielle Ausführung wünsche. Wenn ich dasselbe Skript mit einem Parameter für die Hilfe-Ausgabe starte oder die Liste wartender Instanzen auflisten lasse, dann will ich eigentlich nicht auf die Abarbeitung aller vorherigen Instanzen warten. Naja, ihr seht vielleicht schon, worauf es hinausläuft. Bei jeder Aktion zum Erstellen, Erneuern oder Löschen eines Zertifikats wird die sequentielle Ausführung erzwungen, indem die Funktion _wait_for_free_slot in die erste Zeile geschrieben wird. Und bei den anderen Funktionalitäten lässt man den evtl. parallelisierten Aufruf zu.
# # pulic function ADD certificate # function add(){ _wait_for_free_slot // actions follow here ... }
weiterführende Links:
- IML certman Wrapper für Acme.sh mit DNS Authentication
- DOCs: os-docs.iml.unibe.ch/iml-certman/
- Acme.sh Let’ Encrypt Client als Bash Implementierung
- Let’s Encrypt Kostenlose SSL Zertifikate
- Ansible Automations Plattform
Manjaro update - pacman (6.0.0-1) breaks dependency ‘pacman<5.3′ required by package-query
Der grafische Software-Manager wollte pacman nicht aktualisieren … und wegen jenes Konflikts alle möglichen Pakete auch nicht.
Dann aktualisere ich halt “pamac upgrade pacman” von Hand und schaue, was passiert.
axel@mypc~> pamac upgrade pacman (...) vulkan-intel 21.1.4-1 (21.1.2-1) extra 2.3 MB vulkan-radeon 21.1.4-1 (21.1.2-1) extra 1.7 MB wireless-regdb 2021.04.21-1 (2020.11.20-1) core 10.3 kB Wird installiert (10): syndication 5.84.0-1 extra 1.0 MB electron12 12.0.14-1 community 50.7 MB layer-shell-qt 5.22.3-1 extra 23.1 kB ksystemstats 5.22.3-1 extra 162.9 kB pahole 1.21-1 extra 262.9 kB nodejs-nopt 5.0.0-2 community 13.4 kB libpamac 11.0.1-3 (Ersetzt: pamac-common) extra 818.4 kB kio-fuse 5.0.1-1 extra 70.3 kB fakeroot 1.25.3-2 core bison 3.7.6-1 core Zu erstellen (2): package-query 1.12-1 (1.10-1) AUR zoom 5.7.1-1 (5.6.7-1) AUR Wird entfernt (1): pamac-common 10.0.6-2 (Konflikt mit: libpamac) Download-Größe gesamt: 2.3 GB Gesamtgröße installiert: 269.3 MB Gesamtgröße entfernt: 9.5 MB Build-Dateien bearbeiten : [e] Transaktion durchführen ? [e/j/N] j Warning: installing pacman (6.0.0-1) breaks dependency 'pacman<5.3' required by package-query Add package-query to remove Fehler: Failed to prepare transaction: could not satisfy dependencies: - removing package-query breaks dependency 'package-query>=1.9' required by yaourt, - if possible, remove yaourt and retry
Leider nein.
Beim Lesen von “installing pacman (6.0.0-1) breaks dependency ‘pacman<5.3' " dachte ich schon fast: oh, ein Henne-Ei-Problem?
Die Lösung:
Ich bin einfach dem letzten Rat in der Ausgabe gefolgt und habe yaourt entfernt.
axel@mypc~ [1]> pamac remove yaourt Vorbereitung... Abhängigkeiten werden überprüft... Wird entfernt (1): yaourt 1.9-2 Gesamtgröße entfernt: 849.9 kB Transaktion durchführen ? [j/N] j Removing yaourt (1.9-2)... [1/1] Vorgang erfolgreich abgeschlossen.
Und nun nochmal der Versuch eines Updates …
axel@mypc~> pamac upgrade pacman ...
… hurra, nun klappt es!
weiterführende Links:
- manjaro.org (en)
Linux: Kommando unter einem anderen User starten
Um die Frage des Titels zu beantworten, kommt man schnell auf ein … als root startet man
su - [USERNAME] -c [KOMMANDO]
Und wenn der User, unter dem ich das Kommando starten will, keine Shell hat? Tja, dann kommt eine Fehlermeldung der Art
This account is currently not available.
Wirklich sehr lange habe ich mir beholfen, dass ich dem User in der /etc/passwd eine Shell gegeben habe - das /usr/bin/nologin oder /bin/false wurde durch ein /bin/bash o.ä. ersetzt. Jaja, ganz generell fördern man [Kommando] oder [Kommando] –help hilfreiche Dinge zutage. Aber das ist völlig unnötig. Der “Trick” ist, dass man bei su mit dem Parameter -s eine Shell vorgibt, z.B. die /bin/sh.
su - [USERNAME] -c [KOMMANDO] -s /bin/bash
Um die Parameter wegzulassen und optisch zu vereinfachen, kann man auch eine kleine Funktion in sein Skript setzen:
# run a command as another posix user (even if it does not have a shell) # # example: # runas www-data "/some/where/mysript.sh" # # param string username # param string command to execute. Needs to be quoted. # param string optional: shell (default: /bin/sh) function runas(){ local _user=$1 local _cmd=$2 local _shell=$3 test -z "$_shell" && _shell=/bin/sh su $_user -s $_shell -c "$_cmd" }
Dann kann man im Skript etwas kürzer schreiben:
runas [USERNAME] [KOMMANDO]
weiterführende Links:
Mysql Replikation neu aufsetzen als Bash Skript
Wenn eine bestehende Mysql Replikation nicht mehr funktioniert, so half mir etliche Male im Laufe der Berufszeit als Webmaster beim Schweizer Radio DRS / Sysadmin Daseins an der Uni Bern ein top-down-Skript weiter. Wenn gerade Stress ist und man in der Situation nicht erst alle Kommandos zur Wiederinbetriebnahme des Slave nachschlagen mag, dann ist ist man froh, wenn man ein Skript dafür parat liegen hat.
reinit_mysql_replication.sh
Es gibt keine Parameter.
Installation:
Man benötigt einen Mysql Client. Ich hatte daher die Skripte am Mysql-Slave unterhalb /root zu liegen. Daher ist die Verbindung zum Slave ohne Passwort (wird aus der /root/.my.cnf gelesen).
Es wird auch vom eigenen Rechner aus funktionieren, der die Mysql-Ports von Master und Slave erreichen kann. Es ist nur “einen Tick” langsamer (sprich: bei vielen und/ oder grossen Datenbanken > 1 GB Gesamtgrösse nicht zu empfehlen).
Konfiguration:
Die *dist Datei ist umzukopieren und die Hostnamen und Root-Passwörter für Master + Slave sind zu setzen.
Start:
Das Skript reinit_mysql_replication.sh führt der Reihe nach folgende Aktionen aus: es …
- zeigt aktuellen Slave Status
- wartet dann auf ein RETURN, bevor die Replikation neu aufgesetzt wird
Aktionen zum Neuaufsetzen der Replikation: das Skript …
- liest die Position des Binlog am Master (per SQL “SHOW MASTER STATUS G;”)
- sperrt den Master für Schreibaktionen (”FLUSH TABLES WITH READ LOCK;”)
- holt aktuelle Datenbank-Dumps vom Master (mysqldump –all-databases –lock-all-tables …)
- hebt Schreibsperre am Master auf (”UNLOCK TABLES;”)
- stoppt den Slave (”STOP SLAVE G;”)
- importiert Dumps des Master auf dem Slave (cat $dumpMaster | mysql $paramdbSlave)
- setzt am Slave binfile und Position (”CHANGE MASTER TO MASTER_LOG_FILE = …” ; “CHANGE MASTER TO MASTER_LOG_POS = …”)
- startet den Slave (”START SLAVE G;”)
weiterführende Links: