Opennet Firmware
core.sh
gehe zur Dokumentation dieser Datei
1## @defgroup core Kern
2## @brief Logging, Datei-Operationen, DNS- und NTP-Dienste, Dictionary-Dateien, PID- und Lock-Behandlung, Berichte
3# Beginn der Doku-Gruppe
4## @{
5
6
7# Quelldatei für Standardwerte des Kern-Pakets
8ON_CORE_DEFAULTS_FILE="${IPKG_INSTROOT:-}/usr/share/opennet/core.defaults"
9# Pfad zur dnsmasq-Server-Datei zur dynamischen Aktualisierung durch Dienste-Erkennung
10DNSMASQ_SERVERS_FILE_DEFAULT="${IPKG_INSTROOT:-}/var/run/dnsmasq.servers"
11# DNS-Suffix, das vorrangig von den via olsrd publizierten Nameservern ausgeliefert werden soll
12INTERN_DNS_DOMAIN=on
13# Dateiname für erstellte Zusammenfassungen
14REPORTS_FILE="${IPKG_INSTROOT:-}/tmp/on_report.tar.gz"
15# Basis-Verzeichnis für Log-Dateien
16LOG_BASE_DIR="${IPKG_INSTROOT:-}/var/log"
17# maximum length of message lines (logger seems to resctrict lines incl. timestamp to 512 characters)
18LOG_MESSAGE_LENGTH=420
19# Verzeichnis für auszuführende Aktionen
20SCHEDULING_DIR="${IPKG_INSTROOT:-}/var/run/on-scheduling.d"
21# beim ersten Pruefen wird der Debug-Modus ermittelt
22DEBUG_ENABLED=
23# Notfall-DNS-Eintrag, falls wir noch keine nameservice-Nachrichten erhalten haben
24# aktuelle UGW-Server, sowie der DNS-Server von FoeBuD (https://digitalcourage.de/support/zensurfreier-dns-server)
25FALLBACK_DNS_SERVERS="192.168.0.246 192.168.0.247 192.168.0.248 85.214.20.141"
26# fuer Insel-UGWs benoetigen wir immer einen korrekten NTP-Server, sonst schlaegt die mesh-Verbindung fehl
27# aktuelle UGW-Server, sowie der openwrt-Pool
28FALLBACK_NTP_SERVERS="192.168.0.246 192.168.0.247 192.168.0.248 0.openwrt.pool.ntp.org"
29LATEST_STABLE_FIRMWARE_BASE_URL="https://downloads.opennet-initiative.de/openwrt/stable/latest"
30LATEST_STABLE_FIRMWARE_VERSION_INFO_URL="$LATEST_STABLE_FIRMWARE_BASE_URL/version.txt"
31LATEST_STABLE_FIRMWARE_UPGRADE_MAP_URL="$LATEST_STABLE_FIRMWARE_BASE_URL/device-upgrade-map.csv"
32CRON_LOCK_FILE=/var/run/on-cron.lock
33CRON_LOCK_MAX_AGE_MINUTES=15
34CRON_LOCK_WAIT_TIMEOUT_SECONDS=30
35OPENNET_API_URL="https://api.opennet-initiative.de/api/v1"
36
37
38# Aufteilung ueberlanger Zeilen
39_split_lines() {
40 local line_length="$1"
41 # ersetze alle whitespace-Zeichen durch Nul
42 # Gib anschliessend soviele Token wie moeglich aus, bis die Zeilenlaenge erreicht ist.
43 tr '\n\t ' '\0' | xargs -0 -s "$line_length" echo
44}
45
46
47## @fn msg_debug()
48## @param message Debug-Nachricht
49## @brief Debug-Meldungen ins syslog schreiben
50## @details Die Debug-Nachrichten landen im syslog (siehe ``logread``).
51## Falls das aktuelle Log-Level bei ``info`` oder niedriger liegt, wird keine Nachricht ausgegeben.
52msg_debug() {
53 # bei der ersten Ausfuehrung dauerhaft speichern
54 [ -z "$DEBUG_ENABLED" ] && \
55 DEBUG_ENABLED=$(uci_is_true "$(uci_get on-core.settings.debug false)" && echo 1 || echo 0)
56 [ "$DEBUG_ENABLED" = "0" ] || echo "$1" | _split_lines "$LOG_MESSAGE_LENGTH" | logger -t "$(basename "$0")[$$]"
57}
58
59
60## @fn msg_info()
61## @param message Log-Nachricht
62## @brief Informationen und Fehlermeldungen ins syslog schreiben
63## @details Die Nachrichten landen im syslog (siehe ``logread``).
64## Die info-Nachrichten werden immer ausgegeben, da es kein höheres Log-Level als "debug" gibt.
65msg_info() {
66 echo "$1" | _split_lines "$LOG_MESSAGE_LENGTH" | logger -t "$(basename "$0")[$$]"
67}
69
70## @fn msg_error()
71## @param message Fehlermeldung
72## @brief Die Fehlermeldungen werden in die Standard-Fehlerausgabe und ins syslog geschrieben
73## @details Jede Meldung wird mit "ERROR" versehen, damit diese Meldungen von
74## "get_potential_error_messages" erkannt werden.
75## Die error-Nachrichten werden immer ausgegeben, da es kein höheres Log-Level als "debug" gibt.
76msg_error() {
77 echo "$1" | _split_lines "$LOG_MESSAGE_LENGTH" | logger -s -t "$(basename "$0")[$$]" "[ERROR] $1"
78}
79
80
81## @fn append_to_custom_log()
82## @param log_name Name des Log-Ziels
83## @param event die Kategorie der Meldung (up/down/???)
84## @param msg die textuelle Beschreibung des Ereignis (z.B. "connection with ... closed")
85## @brief Hänge eine neue Nachricht an ein spezfisches Protokoll an.
86## @details Die Meldungen werden beispielsweise von den konfigurierten openvpn-up/down-Skripten gesendet.
88 local log_name="$1"
89 local event="$2"
90 local msg="$3"
91 local logfile
92 logfile=$(get_custom_log_filename "$log_name")
93 echo "$(date) openvpn [$event]: $msg" >>"$logfile"
94 # Datei kuerzen, falls sie zu gross sein sollte
95 local filesize
96 filesize=$(get_filesize "$logfile")
97 [ "$filesize" -gt 10000 ] && sed -i "1,30d" "$logfile"
98 return 0
99}
100
101
102## @fn get_custom_log_filename()
103## @param log_name Name des Log-Ziels
104## @brief Liefere den Inhalt eines spezifischen Logs (z.B. das OpenVPN-Verbindungsprotokoll) zurück.
105## @returns Zeilenweise Ausgabe der Protokollereignisse (aufsteigend nach Zeitstempel sortiert).
107 local log_name="$1"
108 # der Aufrufer darf sich darauf verlassen, dass er in die Datei schreiben kann
109 mkdir -p "$LOG_BASE_DIR"
110 echo "$LOG_BASE_DIR/${log_name}.log"
111}
112
113
114## @fn get_custom_log_content()
115## @param log_name Name des Log-Ziels
116## @brief Liefere den Inhalt eines spezifischen Logs (z.B. das OpenVPN-Verbindungsprotokoll) zurück.
117## @returns Zeilenweise Ausgabe der Protokollereignisse (aufsteigend nach Zeitstempel sortiert).
119 local log_name="$1"
120 local logfile
121 logfile=$(get_custom_log_filename "$log_name")
122 [ -e "$logfile" ] || return 0
123 cat "$logfile"
124}
125
126
127## @fn update_file_if_changed()
128## @param target_filename Name der Zieldatei
129## @brief Aktualisiere eine Datei, falls sich ihr Inhalt geändert haben sollte.
130## @details Der neue Inhalt der Datei wird auf der Standardeingabe erwartet.
131## Im Falle der Gleichheit von aktuellem Inhalt und zukünftigem Inhalt wird
132## keine Schreiboperation ausgeführt. Der Exitcode gibt an, ob eine Schreiboperation
133## durchgeführt wurde.
134## @return exitcode=0 (Erfolg) falls die Datei geändert werden musste
135## @return exitcode=1 (Fehler) falls es keine Änderung gab
137 local target_filename="$1"
138 local content
139 content="$(cat -)"
140 if [ -e "$target_filename" ] && echo "$content" | cmp -s - "$target_filename"; then
141 # the content did not change
142 trap "" EXIT && return 1
143 else
144 # updated content
145 local dirname
146 dirname=$(dirname "$target_filename")
147 [ -d "$dirname" ] || mkdir -p "$dirname"
148 echo "$content" > "$target_filename"
149 return 0
150 fi
151}
152
153
154## @fn update_dns_servers()
155## @brief Übertrage die Liste der als DNS-Dienst announcierten Server in die dnsmasq-Konfiguration.
156## @details Die Liste der DNS-Server wird in die separate dnsmasq-Servers-Datei geschrieben (siehe @sa DNSMASQ_SERVERS_FILE_DEFAULT).
157## Die Server-Datei wird nur bei Änderungen neu geschrieben. Dasselbe gilt für den Neustart des Diensts.
158## Diese Funktion sollte via olsrd-nameservice-Trigger oder via cron-Job ausgeführt werden.
160 trap 'error_trap update_dns_servers "$*"' EXIT
161 local host
162 local port
163 local service
164 # wenn wir eine VPN-Tunnel-Verbindung aufgebaut haben, sollten wir DNS-Anfragen über diese Crypto-Verbindung lenken
165 local preferred_servers
166 local use_dns
167 use_dns=$(uci_get on-core.settings.use_olsrd_dns)
168 # return if we should not use DNS servers provided via olsrd
169 uci_is_false "$use_dns" && return 0
170 local servers_file
171 local server_config
172 servers_file=$(uci_get "dhcp.@dnsmasq[0].serversfile")
173 # aktiviere die "dnsmasq-serversfile"-Direktive, falls noch nicht vorhanden
174 if [ -z "$servers_file" ]; then
175 servers_file="$DNSMASQ_SERVERS_FILE_DEFAULT"
176 uci set "dhcp.@dnsmasq[0].serversfile=$servers_file"
177 uci commit "dhcp.@dnsmasq[0]"
178 reload_config
179 fi
180 preferred_servers=$(if is_function_available "get_mig_tunnel_servers"; then get_mig_tunnel_servers "DNS"; fi)
181 # wir sortieren alphabetisch - Naehe ist uns egal
182 server_config=$(
183 get_services "dns" | filter_reachable_services | filter_enabled_services | sort | while read -r service; do
184 host=$(get_service_value "$service" "host")
185 port=$(get_service_value "$service" "port")
186 [ -n "$port" ] && [ "$port" != "53" ] && host="$host#$port"
187 # Host nur schreiben, falls kein bevorzugter Host gefunden wurde
188 [ -z "$preferred_servers" ] && echo "server=$host"
189 # Die interne Domain soll vorranging von den via olsrd verbreiteten DNS-Servern bedient werden.
190 # Dies ist vor allem fuer UGW-Hosts wichtig, die über eine zweite DNS-Quelle (lokaler uplink)
191 # verfügen.
192 echo "server=/$INTERN_DNS_DOMAIN/$host"
193 done
194 # eventuell bevorzugte Hosts einfuegen
195 for host in $preferred_servers; do
196 echo "server=$host"
197 done
198 )
199 # falls keine DNS-Namen bekannt sind, dann verwende eine (hoffentlich gueltige) Notfall-Option
200 [ -z "$server_config" ] && server_config=$(echo "$FALLBACK_DNS_SERVERS" | tr ' ' '\n' | sed 's/^/server=/')
201 echo "$server_config" | update_file_if_changed "$servers_file" || return 0
202 # es gab eine Aenderung
203 msg_info "updating DNS servers"
204 # Konfiguration neu einlesen
205 service dnsmasq restart 2>/dev/null || true
206}
207
208
209## @fn update_ntp_servers()
210## @brief Übertrage die Liste der als NTP-Dienst announcierten Server in die sysntpd-Konfiguration.
211## @details Die Liste der NTP-Server wird in die uci-Konfiguration geschrieben.
212## Die uci-Konfiguration wird nur bei Änderungen neu geschrieben. Dasselbe gilt für den Neustart des Diensts.
213## Diese Funktion sollte via olsrd-nameservice-Trigger oder via cron-Job ausgeführt werden.
214## @sa http://wiki.openwrt.org/doc/uci/system#remote_time_ntp
216 trap 'error_trap update_ntp_servers "$*"' EXIT
217 local host
218 local port
219 local service
220 local preferred_servers
221 local previous_entries
222 local use_ntp
223 previous_entries=$(uci_get "system.ntp.server")
224 use_ntp=$(uci_get "on-core.settings.use_olsrd_ntp")
225 # return if we should not use NTP servers provided via olsrd
226 uci_is_false "$use_ntp" && return
227 preferred_servers=$(if is_function_available "get_mig_tunnel_servers"; then get_mig_tunnel_servers "NTP"; fi)
228 # schreibe die Liste der NTP-Server neu
229 # wir sortieren alphabetisch - Naehe ist uns egal
230 if [ -n "$preferred_servers" ]; then
231 for host in $preferred_servers; do
232 echo "$host"
233 done
234 else
235 get_services "ntp" | filter_reachable_services | filter_enabled_services | sort | while read -r service; do
236 host=$(get_service_value "$service" "host")
237 port=$(get_service_value "$service" "port")
238 [ -n "$port" ] && [ "$port" != "123" ] && host="$host:$port"
239 echo "$host"
240 done
241 fi | uci_replace_list "system.ntp.server"
242 # Wir wollen keine leere Liste zurücklassen (z.B. bei einem UGW ohne Mesh-Anbindung).
243 # Also alte Werte wiederherstellen, sowie zusaetzlich die default-Server.
244 # Vor allem fuer den https-Download der UGW-Server-Liste benoetigen wir eine korrekte Uhrzeit.
245 [ -z "$(uci_get "system.ntp.server")" ] && \
246 for host in $previous_entries $FALLBACK_NTP_SERVERS; do uci_add_list "system.ntp.server" "$host"; done
247 apply_changes system
248}
249
250
251## @fn add_banner_event()
252## @param event Ereignistext
253## @param timestamp [optional] Der Zeitstempel-Text kann bei Bedarf vorgegeben werden.
254## @brief Füge ein Ereignis zum dauerhaften Ereignisprotokoll (/etc/banner) hinzu.
255## @details Ein Zeitstempel, sowie hübsche Formatierung wird automatisch hinzugefügt.
257 trap 'error_trap add_banner_event "$*"' EXIT
258 local event="$1"
259 # verwende den optionalen zweiten Parameter oder den aktuellen Zeitstempel
260 local timestamp="${2:-}"
261 [ -z "$timestamp" ] && timestamp=$(date)
262 local line=" - $timestamp - $event -"
263 # Steht unser Text schon im Banner? Ansonsten hinzufuegen ...
264 # bis einschliesslich Version v0.5.0 war "clean_restart_log" das Schluesselwort
265 # ab v0.5.1 verwenden wir "system events"
266 if ! grep -qE '(clean_restart_log|system events)' /etc/banner; then
267 echo " ------------------- system events -------------------" >>/etc/banner
268 fi
269 # die Zeile auffuellen
270 while [ "${#line}" -lt 54 ]; do line="$line-"; done
271 echo "$line" >>/etc/banner
272 sync
273}
274
275
276## @fn update_mesh_interfaces()
277## @brief Update mesh interfaces, routing daemons and policy routing
278## @details This function should be called whenever the list of interfaces changes.
281 if is_function_available update_olsr2_interfaces; then
282 update_olsr2_interfaces
283 fi
284}
285
286
287## @fn clean_restart_log()
288## @brief Alle Log-Einträge aus der banner-Datei entfernen.
290 awk '{if ($1 != "-") print}' /etc/banner >/tmp/banner
291 mv /tmp/banner /etc/banner
292 sync
293}
294
295
296## @fn _get_file_dict_value()
297## @param key das Schlüsselwort
298## @brief Auslesen eines Werts aus einem Schlüssel/Wert-Eingabestrom
299## @returns Den zum gegebenen Schlüssel gehörenden Wert aus dem Schlüssel/Wert-Eingabestrom
300## Falls kein passender Schlüssel gefunden wurde, dann ist die Ausgabe leer.
301## @details Jede Zeile der Standardeingabe enthält einen Feldnamen und einen Wert - beide sind durch
302## ein beliebiges whitespace-Zeichen getrennt.
303## Dieses Dateiformat wird beispielsweise für die Dienst-Zustandsdaten verwendet.
304## Zusätzlich ist diese Funktion auch zum Parsen von openvpn-Konfigurationsdateien geeignet.
305_get_file_dict_value() { local key="$1"; shift; { grep "^$key"'[[:space:]]' "$@" 2>/dev/null || true; } | while read -r key value; do echo -n "$value"; done; }
306
307
308## @fn _get_file_dict_keys()
309## @brief Liefere alle Schlüssel aus einem Schlüssel/Wert-Eingabestrom.
310## @returns Liste aller Schlüssel aus dem Schlüssel/Wert-Eingabestrom.
311## @sa _get_file_dict_value
312_get_file_dict_keys() { sed 's/[ \t].*//' "$@" 2>/dev/null || true; }
313
314
315## @fn _set_file_dict_value()
316## @param field das Schlüsselwort
317## @param value der neue Wert
318## @brief Ersetzen oder Einfügen eines Werts in einen Schlüssel/Wert-Eingabestrom.
319## @sa _get_file_dict_value
320_set_file_dict_value() {
321 local status_file="$1"
322 local field="$2"
323 local new_value="$3"
324 [ -z "$field" ] && msg_error "Ignoring empty key for _set_file_dict_value" && return
325 # Filtere bisherige Zeilen mit dem key heraus.
326 # Fuege anschliessend die Zeile mit dem neuen Wert an.
327 # Die Sortierung sorgt fuer gute Vergleichbarkeit, um die Anzahl der
328 # Schreibvorgaenge (=Wahrscheinlichkeit von gleichzeitigem Zugriff) zu reduzieren.
329 (
330 grep -v -w -s "$field" "$status_file" || true
331 echo "$field $new_value"
332 ) | sort | update_file_if_changed "$status_file" || true
333}
334
335
336## @fn get_on_core_default()
337## @param key Name des Schlüssels
338## @brief Liefere einen der default-Werte der aktuellen Firmware zurück (Paket on-core).
339## @details Die default-Werte werden nicht von der Konfigurationsverwaltung uci verwaltet.
340## Somit sind nach jedem Upgrade imer die neuesten Standard-Werte verfügbar.
341get_on_core_default() {
342 local key="$1"
343 _get_file_dict_value "$key" "$ON_CORE_DEFAULTS_FILE"
344}
345
346
347## @fn get_on_firmware_version()
348## @brief Liefere die aktuelle Firmware-Version zurück.
349## @returns Die zurückgelieferte Zeichenkette beinhaltet den Versionsstring (z.B. "0.5.0").
350## @details Per Konvention entspricht die Version jedes Firmware-Pakets der Firmware-Version.
351## Um locking-Probleme zu vermeiden, lesen wir den Wert direkt aus der control-Datei des Pakets.
352## Das ist nicht schoen - aber leider ist die lock-Datei nicht konfigurierbar.
353get_on_firmware_version() {
354 trap 'error_trap get_on_firmware_version "$*"' EXIT
355 local status_file="${IPKG_INSTROOT:-}/usr/lib/opkg/info/on-core.control"
356 [ -e "$status_file" ] || return 0
357 awk '{if (/^Version:/) print $2;}' <"$status_file"
358}
359
360
361## @fn get_on_firmware_version_latest_stable()
362## @brief Liefere die aktuellste bekannte stabile Firmware-Version zurück.
363get_on_firmware_version_latest_stable() {
364 trap 'error_trap get_on_firmware_version_latest "$*"' EXIT
365 local version
366 if version=$(http_request "$LATEST_STABLE_FIRMWARE_VERSION_INFO_URL"); then
367 echo "$version"
368 else
369 # In den ersten Minuten nach dem Anschalten ist typischerweise noch keine
370 # Netzwerkverbindung verfügbar - also melden wir keine Fehler.
371 if [ "$(get_uptime_minutes)" -gt 10 ]; then
372 msg_error "Failed to retrieve firmware version. Maybe there is no network connection."
373 fi
374 fi
375}
376
377
378get_on_firmware_version_latest_stable_if_outdated() {
379 trap 'error_trap get_on_firmware_version_latest_stable_if_outdated "$*"' EXIT
380 local latest_stable_version
381 local current_numeric_version
382 local most_recent_version
383 latest_stable_version=$(get_on_firmware_version_latest_stable)
384 [ -z "$latest_stable_version" ] && return 0
385 current_numeric_version=$(get_on_firmware_version | sed 's/-unstable-/-/')
386 most_recent_version=$(printf '%s\n' "$latest_stable_version" "$current_numeric_version" | sort -V | tail -1)
387 if [ "$most_recent_version" != "$current_numeric_version" ]; then
388 echo "$most_recent_version"
389 fi
390}
391
392
393## @fn get_on_firmware_upgrade_image_url()
394## @brief Versuche die URL eines Aktualisierungs-Image zu ermitteln.
395## @details Die Ausgabe ist leer, falls keine neuere stabile Version bekannt ist oder falls die
396## Device-Image-Map keinen Eintrag für das Geräte-Modell enthält.
397get_on_firmware_upgrade_image_url() {
398 trap 'error_trap get_on_firmware_upgrade_image_url "$*"' EXIT
399 local upgrade_map
400 local device_model_id
401 local upgrade_image_path
402 if upgrade_map=$(http_request "$LATEST_STABLE_FIRMWARE_UPGRADE_MAP_URL"); then
403 device_model_id=$(get_device_model_id)
404 if [ -n "$device_model_id" ]; then
405 upgrade_image_path=$(echo "$upgrade_map" | awk '{ if ($1 == "'"$device_model_id"'") print $2; }')
406 if [ -n "$upgrade_image_path" ]; then
407 echo "$LATEST_STABLE_FIRMWARE_BASE_URL/$upgrade_image_path"
408 fi
409 fi
410 fi
411}
412
413
414## @fn get_device_model_id()
415## @brief Ermittle die OpenWrt-Bezeichnung des Gerätemodells
416get_device_model_id() {
417 trap 'error_trap get_device_model_id "$*"' EXIT
418 # siehe /etc/board.json: model->id
419 sh <<EOF
420 . /usr/share/libubox/jshn.sh
421 $(jshn -R /etc/board.json)
422 json_select model
423 json_get_vars id
424 echo "\$id"
425EOF
426}
427
428
429## @fn get_on_ip()
430## @param on_id die ID des AP - z.B. "1.96" oder "2.54"
431## @param on_ipschema siehe "get_on_core_default on_ipschema"
432## @param interface_number 0..X (das WLAN-Interface ist typischerweise Interface #0)
433## @attention Manche Aufrufende verlassen sich darauf, dass *on_id_1* und
434## *on_id_2* nach dem Aufruf verfügbar sind (also _nicht_ als "local"
435## Variablen deklariert wurden).
436get_on_ip() {
437 local on_id="$1"
438 local on_ipschema="$2"
439 local interface_number="$3"
440 local on_id_1
441 local on_id_2
442 # das "on_ipschema" erwartet die Variable "no"
443 # shellcheck disable=SC2034
444 local no="$interface_number"
445 echo "$on_id" | grep -q '\.' || on_id=1.$on_id
446 # shellcheck disable=SC2034
447 on_id_1=$(echo "$on_id" | cut -d . -f 1)
448 # shellcheck disable=SC2034
449 on_id_2=$(echo "$on_id" | cut -d . -f 2)
450 eval echo "$on_ipschema"
451}
452
453
454## @fn get_main_ip()
455## @brief Liefere die aktuell konfigurierte Main-IP zurück.
456## @returns Die aktuell konfigurierte Main-IP des AP oder die voreingestellte IP.
457## @attention Seiteneffekt: die Variablen "on_id_1" und "on_id_2" sind anschließend verfügbar.
458## @sa get_on_ip
459get_main_ip() {
460 local on_id
461 local ipschema
462 on_id=$(uci_get on-core.settings.on_id "$(get_on_core_default on_id_preset)")
463 ipschema=$(get_on_core_default on_ipschema)
464 get_on_ip "$on_id" "$ipschema" 0
465}
466
467
468## @fn run_with_cron_lock()
469## @details Führe eine Aktion aus, falls das Lock für Cron-Jobs übernommen werden konnte
470## @params command alle Parameter werden als auszuführendes Kommando interpretiert
471run_with_cron_lock() {
472 local returncode
473 # Der Timeout ist nötig, weil alle cron-Jobs gleichzeitig gestartet werden. Somit treffen
474 # der minütige und der fünf-minütige cron-Job aufeinandern und möchten dasselbe Lock
475 # halten. Die maximale Wartezeit löst wahrscheinlich die meisten Konflikte.
476 if acquire_lock "$CRON_LOCK_FILE" "$CRON_LOCK_MAX_AGE_MINUTES" "$CRON_LOCK_WAIT_TIMEOUT_SECONDS"; then
477 set +e
478 "$@"
479 returncode=$?
480 set -e
481 rm -f "$CRON_LOCK_FILE"
482 return "$returncode"
483 fi
484}
485
486
487is_lock_available() {
488 local lock_file="$1"
489 local max_age_minutes="$2"
490 # Fehlerfall: die Lock-Datei existiert und ist nicht alt genug
491 [ ! -e "$lock_file" ] || is_file_timestamp_older_minutes "$lock_file" "$max_age_minutes"
492}
493
494
495## @fn acquire_lock()
496## @brief Prüfe ob eine Lock-Datei existiert und nicht veraltet ist.
497## @details Die folgenden Zustände werden behandelt:
498## A) die Datei existiert, ist jedoch veraltet -> Erfolg, Zeitstempel der Datei aktualisieren
499## B) die Datei existiert und ist noch nicht veraltet -> Fehlschlag
500## C) die Datei existiert nicht -> Erfolg, Datei wird angelegt
501## Warte notfalls einen Timeout ab, bis das Lock frei wird.
502## @returns Erfolg (Lock erhalten) oder Misserfolg (Lock ist bereits vergeben)
503acquire_lock() {
504 local lock_file="$1"
505 local max_age_minutes="$2"
506 local timeout="$3"
507 local timeout_limit
508 timeout_limit=$(( $(date +%s) + timeout ))
509 while ! is_lock_available "$lock_file" "$max_age_minutes"; do
510 if [ "$(date +%s)" -ge "$timeout_limit" ]; then
511 msg_info "Failed to acquire lock file: $lock_file"
512 trap "" EXIT && return 1
513 fi
514 sleep "$(( $(get_random 10) + 1 ))"
515 done
516 touch "$lock_file"
517 return 0
518}
519
520
521# Pruefe ob eine PID-Datei existiert und ob die enthaltene PID zu einem Prozess
522# mit dem angegebenen Namen (nur Dateiname - ohne Pfad) verweist.
523# Parameter PID-Datei: vollstaendiger Pfad
524# Parameter Prozess-Name: Dateiname ohne Pfad
525check_pid_file() {
526 trap 'error_trap check_pid_file "$*"' EXIT
527 local pid_file="$1"
528 local process_name="$2"
529 local pid
530 local current_process
531 if [ -z "$pid_file" ] || [ ! -e "$pid_file" ]; then trap "" EXIT && return 1; fi
532 pid=$(sed 's/[^0-9]//g' "$pid_file")
533 # leere/kaputte PID-Datei
534 [ -z "$pid" ] && trap "" EXIT && return 1
535 # Prozess-Datei ist kein symbolischer Link?
536 [ ! -L "/proc/$pid/exe" ] && trap "" EXIT && return 1
537 current_process=$(readlink "/proc/$pid/exe")
538 [ "$process_name" != "$(basename "$current_process")" ] && trap "" EXIT && return 1
539 return 0
540}
541
542
543## @fn apply_changes()
544## @param configs Einer oder mehrere uci-Sektionsnamen.
545## @brief Kombination von uci-commit und anschliessender Inkraftsetzung fuer verschiedene uci-Sektionen.
546## @details Dienst-, Netzwerk- und Firewall-Konfigurationen werden bei Bedarf angewandt.
547## Zuerst werden alle uci-Sektionen commited und anschliessend werden die Trigger ausgefuehrt.
548apply_changes() {
549 local config
550 # Zuerst werden alle Änderungen committed und anschließend die (veränderten) Konfiguration
551 # für den Aufruf der hook-Skript verwandt.
552 for config in "$@"; do
553 # Opennet-Module achten auch auf nicht-uci-Aenderungen
554 if echo "$config" | grep -q "^on-"; then
555 uci -q commit "$config" || true
556 echo "$config"
557 elif [ -z "$(uci -q changes "$config")" ]; then
558 # keine Aenderungen?
559 true
560 else
561 uci commit "$config"
562 echo "$config"
563 fi
564 done | grep -v "^$" | sort | uniq | while read -r config; do
565 run_parts "${IPKG_INSTROOT:-}/usr/lib/opennet/hooks.d" "$config"
566 done
567 return 0
568}
569
570
571# Setzen einer Opennet-ID.
572# 1) Hostnamen setzen
573# 2) IPs fuer alle Opennet-Interfaces setzen
574# 3) Main-IP in der olsr-Konfiguration setzen
575# 4) IP des Interface "free" setzen
576set_opennet_id() {
577 trap 'error_trap set_opennet_id "$*"' EXIT
578 local new_id="$1"
579 local network
580 local uci_prefix
581 local ipaddr
582 local main_ipaddr
583 local ipschema
584 local netmask
585 local if_counter=0
586 # ID normalisieren (AP7 -> AP1.7)
587 echo "$new_id" | grep -q '\.' || new_id=1.$new_id
588 # ON_ID in on-core-Settings setzen
589 prepare_on_uci_settings
590 uci set "on-core.settings.on_id=$new_id"
591 apply_changes on-core
592 # Hostnamen konfigurieren
593 find_all_uci_sections system system | while read -r uci_prefix; do
594 uci set "${uci_prefix}.hostname=AP-$(echo "$new_id" | tr . -)"
595 done
596 apply_changes system
597 # IP-Adressen konfigurieren
598 ipschema=$(get_on_core_default on_ipschema)
599 netmask=$(get_on_core_default on_netmask)
600 main_ipaddr=$(get_on_ip "$new_id" "$ipschema" 0)
601 for network in $(get_sorted_opennet_interfaces); do
602 uci_prefix=network.$network
603 [ "$(uci_get "${uci_prefix}.proto")" != "static" ] && continue
604 ipaddr=$(get_on_ip "$new_id" "$ipschema" "$if_counter")
605 uci set "${uci_prefix}.ipaddr=$ipaddr"
606 uci set "${uci_prefix}.netmask=$netmask"
607 if_counter=$((if_counter + 1))
608 done
609 # OLSR-MainIP konfigurieren
610 olsr_set_main_ip "$main_ipaddr"
611 apply_changes olsrd network
612}
613
614
615# Durchsuche eine Schluessel-Wert-Liste nach einem Schluessel und liefere den dazugehoerigen Wert zurueck.
616# Beispiel:
617# foo=bar baz=nux
618# Der Separator ist konfigurierbar.
619# Die Liste wird auf der Standardeingabe erwartet.
620# Der erste und einzige Parameter ist der gewuenschte Schluessel.
621get_from_key_value_list() {
622 local search_key="$1"
623 local separator="$2"
624 local key_value
625 local key
626 { sed 's/[ \t]\+/\n/g'; echo; } | while read -r key_value; do
627 key=$(echo "$key_value" | cut -f 1 -d "$separator")
628 [ "$key" = "$search_key" ] && echo "$key_value" | cut -f 2- -d "$separator" && break
629 true
630 done
631 return 0
632}
633
634
635## @fn replace_in_key_value_list()
636## @param search_key der Name des Schlüsselworts
637## @param separator der Name des Trennzeichens zwischen Wert und Schlüssel
638## @brief Ermittle aus einer mit Tabulatoren oder Leerzeichen getrennten Liste von Schlüssel-Wert-Paaren den Inhalt des Werts zu einem Schlüssel.
639## @returns die korrigierte Schlüssel-Wert-Liste wird ausgegeben (eventuell mit veränderten Leerzeichen oder Tabulatoren)
640replace_in_key_value_list() {
641 local search_key="$1"
642 local separator="$2"
643 local value="$3"
644 awk 'BEGIN { found=0; FS="'"$separator"'"; OFS=":"; RS="[ \t]"; ORS=" "; }
645 { if ($1 == "'"$search_key"'") { print "'"$search_key"'", '"$value"'; found=1; } else { print $0; } }
646 END { if (found == 0) print "'"$search_key"'", '"$value"' };'
647}
648
649
650# Wandle einen uebergebenene Parameter in eine Zeichenkette um, die sicher als Dateiname verwendet werden kann
651get_safe_filename() {
652 echo "$1" | sed 's/[^a-zA-Z0-9._\-]/_/g'
653}
654
655
656## @fn get_uptime_minutes()
657## @brief Ermittle die seit dem Systemstart vergangene Zeit in Minuten
658## @details Diese Zeit ist naturgemäß nicht für die Speicherung an Orten geeignet, die einen reboot überleben.
659get_uptime_minutes() {
660 awk '{print int($1/60)}' /proc/uptime
661}
662
663
664## @fn is_file_timestamp_older_minutes()
665## @brief Prüfe ob die Datei älter ist als die angegebene Zahl von Minuten.
666## @details Alle Fehlerfälle (Datei existiert nicht, Zeitstempel liegt in der Zukunft, ...) werden
667## als "veraltet" gewertet.
668## @returns True, falls die Datei existiert und älter als angegeben ist - ansonsten "False"
669is_file_timestamp_older_minutes() {
670 trap 'error_trap is_file_timestamp_older_minutes "$*"' EXIT
671 local filename="$1"
672 local limit_minutes="$2"
673 [ -e "$filename" ] || return 0
674 local file_timestamp
675 local timestamp_now
676 file_timestamp=$(date --reference "$filename" +%s 2>/dev/null | awk '{ print int($1/60) }')
677 # Falls die Datei zwischendurch geloescht wurde, ist das Lock nun frei.
678 [ -z "$file_timestamp" ] && return 0
679 timestamp_now=$(date +%s | awk '{ print int($1/60) }')
680 # veraltet, falls:
681 # * kein Zeitstempel
682 # * Zeitstempel in der Zukunft
683 # * Zeitstempel älter als erlaubt
684 if [ -z "$file_timestamp" ] \
685 || [ "$file_timestamp" -gt "$timestamp_now" ] \
686 || [ "$((file_timestamp + limit_minutes))" -lt "$timestamp_now" ]; then
687 return 0
688 else
689 trap "" EXIT && return 1
690 fi
691}
692
693
694## @fn is_timestamp_older_minutes()
695## @param timestamp_minute der zu prüfende Zeitstempel (in Minuten seit dem Systemstart)
696## @param difference zulässige Zeitdifferenz zwischen jetzt und dem Zeitstempel
697## @brief Prüfe, ob ein gegebener Zeitstempel älter ist, als die vorgegebene Zeitdifferenz.
698## @returns Exitcode Null (Erfolg), falls der gegebene Zeitstempel mindestens 'difference' Minuten zurückliegt.
699# Achtung: Zeitstempel aus der Zukunft oder leere Zeitstempel gelten immer als veraltet.
700is_timestamp_older_minutes() {
701 local timestamp_minute="$1"
702 local difference="$2"
703 [ -z "$timestamp_minute" ] && return 0
704 local now
705 now="$(get_uptime_minutes)"
706 # it is older
707 [ "$now" -ge "$((timestamp_minute + difference))" ] && return 0
708 # timestamp in future -> invalid -> let's claim it is too old
709 [ "$now" -lt "$timestamp_minute" ] && \
710 msg_info "WARNING: Timestamp from future found: $timestamp_minute (minutes since epoch)" && \
711 return 0
712 trap "" EXIT && return 1
713}
714
715
716## @fn get_uptime_seconds()
717## @brief Ermittle die Anzahl der Sekunden seit dem letzten Bootvorgang.
718get_uptime_seconds() {
719 cut -f 1 -d . /proc/uptime
720}
721
722
723## @fn run_delayed_in_background()
724## @param delay Verzögerung in Sekunden
725## @param command alle weiteren Token werden als Kommando und Parameter interpretiert und mit Verzögerung ausgeführt.
726## @brief Führe eine Aktion verzögert im Hintergrund aus.
727run_delayed_in_background() {
728 local delay="$1"
729 shift
730 (sleep "$delay" && "$@") </dev/null >/dev/null 2>&1 &
731}
732
733
734## @fn get_filesize()
735## @brief Ermittle die Größe einer Datei in Bytes.
736## @params filename Name der zu untersuchenden Datei.
737get_filesize() {
738 local filename="$1"
739 wc -c "$filename" | awk '{ print $1 }'
740}
741
742
743# Bericht erzeugen
744# Der Name der erzeugten tar-Datei wird als Ergebnis ausgegeben.
745generate_report() {
746 trap 'error_trap generate_report "$*"' EXIT
747 local fname
748 local pid
749 local reports_dir
750 local temp_dir
751 local tar_file
752 temp_dir=$(mktemp -d)
753 reports_dir="$temp_dir/report"
754 tar_file=$(mktemp)
755 msg_debug "Creating a report"
756 # die Skripte duerfen davon ausgehen, dass wir uns im Zielverzeichnis befinden
757 mkdir -p "$reports_dir"
758 cd "$reports_dir"
759 find /usr/lib/opennet/reports -type f | sort | while read -r fname; do
760 [ ! -x "$fname" ] && msg_info "skipping non-executable report script: $fname" && continue
761 "$fname" || msg_error "reports script failed: $fname"
762 done
763 # "tar" unterstuetzt "-c" nicht - also komprimieren wir separat
764 tar cC "$temp_dir" "report" | gzip >"$tar_file"
765 rm -r "$temp_dir"
766 mv "$tar_file" "$REPORTS_FILE"
767}
768
769
770## @fn get_potential_error_messages()
771## @param max_lines die Angabe einer maximalen Anzahl von Zeilen ist optional - andernfalls werden alle Meldungen ausgegeben
772## @brief Filtere aus allen zugänglichen Quellen mögliche Fehlermeldungen.
773## @details Falls diese Funktion ein nicht-leeres Ergebnis zurückliefert, kann dies als Hinweis für den
774## Nutzer verwendet werden, auf dass er einen Fehlerbericht einreicht.
775get_potential_error_messages() {
776 local max_lines="${1:-}"
777 local filters=
778 # 1) get_service_as_csv
779 # Wir ignorieren "get_service_as_csv"-Meldungen - diese werden durch asynchrone Anfragen des
780 # Web-Interface ausgeloest, die beim vorzeitigen Abbruch des Seiten-Lade-Vorgangs mit
781 # einem Fehler enden.
782 filters="${filters}|trapped.*get_service_as_csv"
783 # 2) openvpn.*Error opening configuration file
784 # Beim Booten des Systems wurde die openvpn-Config-Datei, die via uci referenziert ist, noch
785 # nicht erzeugt. Beim naechsten cron-Lauf wird dieses Problem behoben.
786 filters="${filters}|openvpn.*Error opening configuration file"
787 # 3) openvpn(...)[...]: Exiting due to fatal error
788 # Das Verzeichnis /var/etc/openvpn/ existiert beim Booten noch nicht.
789 filters="${filters}|openvpn.*Exiting due to fatal error"
790 # 4) openvpn(...)[...]: SIGUSR1[soft,tls-error] received, process restarting
791 # Diese Meldung taucht bei einem Verbindungsabbruch auf. Dieses Ereignis ist nicht
792 # ungewoehnlich und wird mittels des Verbindungsprotokolls bereits hinreichend gewuerdigt
793 filters="${filters}|openvpn.*soft,tls-error"
794 # 5) openvpn(...)[...]: TLS Error: TLS handshake failed
795 # Diese Meldung deutet einen fehlgeschlagenen Verbindungsversuch an. Dies ist nicht
796 # ungewoehnlich (beispielsweise auch fuer Verbindungstests).
797 filters="${filters}|openvpn.*TLS Error"
798 # 6) olsrd: /etc/rc.d/S65olsrd: startup-error: check via: '/usr/sbin/olsrd -f "/var/etc/olsrd.conf" -nofork'
799 # Falls noch kein Interface vorhanden ist (z.B. als wifi-Client), dann taucht diese Meldung
800 # beim Booten auf.
801 filters="${filters}|olsrd.*startup-error"
802 # 7) ucarp
803 # Beim Booten tauchen Fehlermeldungen aufgrund nicht konfigurierter Netzwerk-Interfaces auf.
804 # TODO: ucarp nur noch als nachinstallierbares Paket markieren (erfordert Aenderung der Makefile-Erzeugung)
805 filters="${filters}|ucarp"
806 # 8) olsrd: /etc/rc.d/S65olsrd: ERROR: there is already an IPv4 instance of olsrd running (pid: '1099'), not starting.
807 # Dieser Fehler tritt auf, wenn der olsrd_check einen olsrd-Neustart ausloest, obwohl er schon laeuft.
808 filters="${filters}|olsrd: ERROR: there is already an IPv4 instance of olsrd running"
809 # 9) openvpn(...)[...]: Authenticate/Decrypt packet error
810 # Paketverschiebungen nach dem Verbindungsaufbau - anscheinend unproblematisch.
811 filters="${filters}|openvpn.*Authenticate/Decrypt packet error"
812 # 10) olsrd: ... olsrd_setup_smartgw_rules() Warning: kmod-ipip is missing.
813 # olsrd gibt beim Starten generell diese Warnung aus. Wir koennen sie ignorieren.
814 filters="${filters}|olsrd.*olsrd_setup_smartgw_rules"
815 # 11) olsrd: ... olsrd_write_interface() Warning: Interface '...' not found, skipped
816 # Falls das wlan-Interface beim Bootvorgang noch nicht aktiv ist, wenn olsrd startet, dann erscheint diese
817 # harmlose Meldung.
818 filters="${filters}|olsrd.*Interface.*not found"
819 # 12) dropbear[...]: Exit (root): Error reading: Connection reset by peer
820 # Verbindungsverlust einer ssh-Verbindung. Dies darf passieren.
821 filters="${filters}|dropbear.*Connection reset by peer"
822 # 13) cron-error: nc.*: short write
823 # Falls die Routen via nc während eines olsrd-Neustarts ausgelesen werden, reisst eventuell die Socket-
824 # Verbindung ab - dies ist akzeptabel.
825 filters="${filters}|nc: short write"
826 # 14) openvpn(___service_name___)[...]: write UDPv4: Network is unreachable
827 # Beispielsweise bei einem olsrd-Neustart reisst die Verbindung zum UGW-Server kurz ab.
828 filters="${filters}|openvpn.*Network is unreachable"
829 # 15) wget: can't connect to remote host
830 # Eine frühe Geschwindigkeitsmessung (kurz nach dem Booten) darf fehlschlagen.
831 filters="${filters}|wget: can.t connect to remote host"
832 # 16) openvpn(...)[...]: Options error: Unrecognized option or missing parameter(s) in [PUSH-OPTIONS]:11: explicit-exit-notify (2.3.6)
833 # OpenVPN-Versionen, die ohne die "--extras"-Option gebaut wurden, unterstuetzen keine exit-Notification.
834 # Dies ist unproblematisch - es ist eher eine Sache der Höflichkeit..
835 filters="${filters}|openvpn.*Options error.*explicit-exit-notify"
836 # 17) ddns-scripts[...]: myddns_ipv4: ...
837 # ddns meldet leidet beim Starten einen Fehler, solange es unkonfiguriert ist.
838 filters="${filters}|ddns-scripts.*myddns_ipv[46]"
839 # 18) Collected errors:
840 # opkg-Paketinstallationen via Web-Interface erzeugen gelegentlich Fehlermeldungen (z.B. Entfernung
841 # abhängiger Pakete), die dem Nutzer im Web-Interface angezeigt werden. Diese Fehlermeldungen landen
842 # zusätzlich auch im log-Buffer. Da der Nutzer sie bereits gesehen haben dürfte, können wir sie ignorieren
843 # (zumal die konkreten Fehlermeldungen erst in den folgenden Zeilen zu finden und somit schlecht zu filtern
844 # sind).
845 filters="${filters}|Collected errors:"
846 # 19) uhttpd[...]: sh: write error: Broken pipe
847 # http-Requests die von seiten des Browser abgebrochen wurden
848 filters="${filters}|uhttpd.*: sh: write error: Broken pipe"
849 # 20) __main__ get_variable ...
850 # Der obige "Broken pipe"-Fehler unterbricht dabei auch die akuell laufende Funktion - dies ist
851 # sehr häufig die Variablen-Auslesung (seltsamerweise).
852 filters="${filters}|__main__ get_variable "
853 # 21) ERROR: Linux route add command failed
854 # Beim Aufbau er OpenVPN-Verbindung scheint gelegentlich noch eine alte Route verblieben zu sein.
855 # Diese Meldung ist wohl irrelevant.
856 filters="${filters}|ERROR: Linux route add command failed"
857 # 22) ... cannot open proc entry /proc/sys/net/ipv4/conf/none/ ...
858 # olsrd2 versucht auf /proc/-Eintraege zuzugreifen, bevor der Name des Netzwerk-Interface
859 # feststeht ("none"). Ignorieren.
860 filters="${filters}|cannot open proc entry /proc/sys/net/ipv4/conf/none/"
861 # 23) RTNETLINK answers: Network is unreachable
862 # bei einem OpenVPN-Verbindungsaufbau gehen die ersten Pakete verloren
863 filters="${filters}|RTNETLINK answers: Network is unreachable"
864 # 24) olsrd2: wrote '/var/run/olsrd2_dev'
865 # beim OLSRD2-Start wird diese Meldung auf stderr ausgegeben
866 filters="${filters}|olsrd2: wrote .*olsrd2_dev"
867 # 25) nl80211 not found
868 # Während der initialen wireless-Konfigurationsermittlung beim ersten Boot-Vorgang wird
869 # "iw" aufgerufen, auch wenn eventuell kein wifi-Interface vorhanden ist. In diesem Fall
870 # wird der obige Hinweis ausgegeben.
871 filters="${filters}|nl80211 not found"
872 # 26) OLSRd2[...]: WARN(os_interface) ...: Error, cannot open proc entry /proc/sys/net/ipv4/conf/on_wifi_1/... No such file or directory
873 # olsrd2 versucht auf /proc/-Eintraege mittels des Namens eines logischen
874 # Netzwerk-Interface (z.B. "on_eth_0") zuzugreifen, obwohl das System nur die physischen
875 # Interfaces kennt.
876 filters="${filters}|cannot open proc entry /proc/sys/net/ipv4/conf/on_"
877 # 27) OLSRd2[...]: WARN(os_interface) ...: WARNING! Could not disable the IP spoof filter
878 # Im Anschluss an den obigen (26) Fehlversuch, fuer ein logisches Netzwerk-Interface den
879 # rp_filter zu deaktivieren, wird diese Warnung ausgegeben. Sie ist nicht relevant.
880 filters="${filters}"'|WARN\‍(os_interface\‍).*Could not disable (the IP spoof filter|ICMP redirects)'
881 # 28) MediaTek Nand driver init, version v2.1 Fix AHB virt2phys error
882 # Im Boot-Log des EdgeRouter (ERX) taucht diese informative Meldung auf, die leider das
883 # Wort "error" enthält.
884 filters="${filters}"'|MediaTek Nand driver init, version v.* Fix AHB virt2phys error'
885 # 29) ath10k_pci 0000:00:00.0: Direct firmware load for ath10k/pre-cal-pci-0000:00:00.0.bin failed with error -2
886 # Die Nanostation AC loco schreibt folgende Fehlermeldung ins Kernel-Log.
887 # Das WLAN-Interface funktioniert jedoch scheinbar problemlos.
888 filters="${filters}"'|ath10k_pci 0000:00:00.0: Direct firmware load for ath10k/pre-cal-pci-0000:00:00.0.bin failed with error -2'
889 # 30) kern.err kernel: [..] print_req_error: I/O error, dev loop0, sector 490
890 # Beim Booten eines APU-Geräts tritt wohl die obige Fehlermeldung auf. Sie ist
891 # sicherlich irrelevant. Siehe https://dev.opennet-initiative.de/ticket/246.
892 filters="${filters}"'|kern.err kernel:.*print_req_error: I/O error, dev loop0, sector '
893 # System-Fehlermeldungen (inkl. "trapped")
894 # Frühzeitig Broken-Pipe-Fehler ("uhttpd[...]: sh: write error: Broken pipe") sowie die darauffolgende
895 # Zeile entfernen. Diese Fehler treten auf, wenn der Nutzer das Laden der Webseite unterbricht (z.B.
896 # durch frühe Auswahl einer neuen URL).
897 prefilter="uhttpd.*: sh: write error: Broken pipe"
898 # "sed /FOO/{N;d;}" löscht die Muster-Zeile, sowie die direkt nachfolgende
899 logread | sed "/$prefilter/{N;d;}" | grep -iE "(error|crash)" | grep -vE "(${filters#|})" | if [ -z "$max_lines" ]; then
900 # alle Einträge ausgeben
901 cat -
902 else
903 # nur die letzten Einträge ausliefern
904 tail -n "$max_lines"
905 fi
906}
907
908
909# Ersetze eine Zeile durch einen neuen Inhalt. Falls das Zeilenmuster nicht vorhanden ist, wird eine neue Zeile eingefuegt.
910# Dies entspricht der Funktionalitaet des "lineinfile"-Moduls von ansible.
911# Parameter filename: der Dateiname
912# Parameter pattern: Suchmuster der zu ersetzenden Zeile
913# Parameter new_line: neue Zeile
914line_in_file() {
915 trap 'error_trap line_in_file "$*"' EXIT
916 local filename="$1"
917 local pattern="$2"
918 local new_line="$3"
919 local line
920 # Datei existiert nicht? Einfach mit dieser Zeile erzeugen.
921 [ ! -e "$filename" ] && echo "$new_line" >"$filename" && return 0
922 # Datei einlesen - zum Muster passende Zeilen austauschen - notfalls neue Zeile anfuegen
923 (
924 while read -r line; do
925 if echo "$line" | grep -q "$pattern"; then
926 [ -n "$new_line" ] && echo "$new_line"
927 # die Zeile nicht erneut schreiben - alle zukuenftigen Vorkommen loeschen
928 new_line=
929 else
930 echo "$line"
931 fi
932 done <"$filename"
933 # die neue Zeile hinzufuegen, falls das Muster in der alten Datei nicht vorhanden war
934 grep -q "$pattern" "$filename" || echo "$new_line"
935 ) | update_file_if_changed "$filename" || true
936}
937
938
939# Pruefe, ob eine Liste ein bestimmtes Element enthaelt
940# Die Listenelemente sind durch beliebigen Whitespace getrennt.
941is_in_list() {
942 local target="$1"
943 local list="$2"
944 local token
945 for token in $list; do
946 [ "$token" = "$target" ] && return 0
947 true
948 done
949 # kein passendes Token gefunden
950 trap "" EXIT && return 1
951}
952
953
954# Liefere den Inhalt einer Variable zurueck.
955# Dies ist beispielsweise fuer lua-Skripte nuetzlich, da diese nicht den shell-Namensraum teilen.
956# Paramter: Name der Variable
957get_variable() {
958 local var_name="$1"
959 eval "echo \"\$$var_name\""
960}
961
962
963# Pruefe, ob die angegebene Funktion definiert ist.
964# Dies ersetzt opkg-basierte Pruefungen auf installierte opennet-Firmware-Pakete.
965is_function_available() {
966 local func_name="$1"
967 # "ash" liefert leider nicht den korrekten Wert "function" nach einem Aufruf von "type -t".
968 # Also verwenden wir die Textausgabe von "type".
969 # Die Fehlerausgabe von type wird ignoriert - im Falle der bash gibt es sonst unnoetige Ausgaben.
970 type "$func_name" 2>/dev/null | grep -q "function$" && return 0
971 trap "" EXIT && return 1
972}
973
974
975## @fn get_random()
976## @brief Liefere eine Zufallszahl innerhalb des gegebenen Bereichs.
977## @returns Eine zufällige Ganzzahl.
978get_random() {
979 local range="$1"
980 local random_number
981 # Setze eine "1" vor eine zufällige Anzahl von Ziffern (vermeide Oktal-Zahl-Behandlung).
982 # Begrenze die Anzahl von Ziffern, um Rundungen in awk zu vermeiden.
983 random_number="1$(dd if=/dev/urandom bs=10 count=1 2>/dev/null | md5sum | tr -dc "0123456789" | cut -c 1-6)"
984 printf "%d %d" "$range" "$random_number" | awk '{print $2 % $1; }'
985}
986
987
988## @fn get_local_bias_numer()
989## @brief Ermittle eine lokale einzigartige Zahl, die als dauerhaft unveränderlich angenommen werden kann.
990## @returns Eine (initial zufällig ermittelte) Zahl zwischen 0 und 10^8-1, die unveränderlich zu diesem AP gehört.
991## @details Für ein paar gleichrangige Sortierungen (z.B. verwendete
992## UGW-Gegenstellen) benötigen wir ein lokales Salz, um strukturelle
993## Bevorzugungen zu vermeiden.
994get_local_bias_number() {
995 trap 'error_trap get_local_bias_number "$*"' EXIT
996 local bias
997 bias=$(uci_get on-core.settings.local_bias_number)
998 # der Bias-Wert ist schon vorhanden - wir liefern ihn aus
999 if [ -z "$bias" ]; then
1000 # wir müssen einen Bias-Wert erzeugen: beliebige gehashte Inhalte ergeben eine akzeptable Zufallszahl
1001 bias=$(get_random 100000000)
1002 uci set "on-core.settings.local_bias_number=$bias"
1003 uci commit on-core
1004 fi
1005 echo -n "$bias" && return 0
1006}
1007
1008
1009## @fn system_service_check()
1010## @brief Prüfe ob ein Dienst läuft und ob seine PID-Datei aktuell ist.
1011## @param executable Der vollständige Pfad zu dem auszuführenden Programm.
1012## @param pid_file Der Name einer PID-Datei, die von diesem Prozess verwaltet wird.
1013## @deteils Dabei wird die 'service_check'-Funktion aus der openwrt-Shell-Bibliothek genutzt.
1014system_service_check() {
1015 local executable="$1"
1016 local pid_file="$2"
1017 local result
1018 # shellcheck disable=SC2034 source=openwrt/package/base-files/files/lib/functions/network.sh
1019 result=$(set +eu; . /lib/functions/service.sh; SERVICE_PID_FILE="$pid_file"; service_check "$executable" && echo "ok"; set -eu)
1020 [ -n "$result" ] && return 0
1021 trap "" EXIT && return 1
1022}
1023
1024
1025## @fn get_memory_size()
1026## @brief Ermittle die Größe des Arbeitsspeichers in Megabyte.
1027## @returns Der Rückgabewert (in Megabyte) ist etwas kleiner als der physische Arbeitsspeicher (z.B. 126 statt 128 MB).
1028get_memory_size() {
1029 local memsize_kb
1030 memsize_kb=$(grep "^MemTotal:" /proc/meminfo | sed 's/[^0-9]//g')
1031 echo $((memsize_kb / 1024))
1032}
1033
1034
1035# Liefere alle Dateien in einem Verzeichnis zurück, die entsprechend der "run-parts"-Funktionalität
1036# beachtet werden sollten.
1037_get_parts_dir_files() {
1038 local parts_dir="$1"
1039 local fname
1040 # Abbruch, falls es das Verzeichnis nicht gibt
1041 [ -e "$parts_dir" ] || return 0
1042 # ignoriere Dateinamen mit ungueltigen Zeichen (siehe 'man run-parts')
1043 find "$parts_dir" -maxdepth 1 | grep '/[a-zA-Z0-9_-]\+$' | sort | while read -r fname; do
1044 # ignoriere verwaiste symlinks
1045 [ -f "$fname" ] || continue
1046 # ignoriere Dateien ohne Ausführungsrechte
1047 [ -x "$fname" ] || continue
1048 echo "$fname"
1049 done
1050}
1051
1052
1053## @fn run_parts()
1054## @brief Führe alle Skripte aus, die in einem bestimmten Verzeichnis liegen und gewissen Konventionen genügen.
1055## @param rundir Verzeichnis, das die auszuführenden Skripte enthält
1056## @param weitere Paramter (falls erforderlich)
1057## @details Die Namenskonventionen und das Verhalten entspricht dem verbreiteten 'run-parts'-Werkzeug.
1058## Die Dateien müssen ausführbar sein.
1059run_parts() {
1060 trap 'error_trap run_parts "$*"' EXIT
1061 local rundir="$1"
1062 shift
1063 local fname
1064 _get_parts_dir_files "$rundir" | while read -r fname; do
1065 msg_debug "on-run-parts: executing $fname"
1066 # ignoriere Fehler bei der Ausfuehrung
1067 "$fname" "$@" || true
1068 done
1069}
1070
1071
1072## @fn schedule_parts()
1073## @brief Plant die Ausführung aller Skripte, die in einem bestimmten Verzeichnis liegen und gewissen Konventionen genügen.
1074## @param rundir Verzeichnis, das die auszuführenden Skripte enthält
1075## @param suffix optionaler Suffix wird ungefiltert an jeden auszufühenden Dateinamen gehängt (z.B. '2>&1 | logger -t cron-error')
1076## @details Die Namenskonventionen und das Verhalten entspricht dem verbreiteten 'run-parts'-Werkzeug.
1077## Die Dateien müssen ausführbar sein.
1078schedule_parts() {
1079 trap 'error_trap schedule_parts "$*"' EXIT
1080 local rundir="$1"
1081 local suffix="${2:-}"
1082 _get_parts_dir_files "$rundir" | while read -r fname; do
1083 if [ -n "$suffix" ]; then
1084 echo "$fname $suffix"
1085 else
1086 echo "$fname"
1087 fi | schedule_task
1088 done
1089}
1090
1091
1092## @fn run_scheduled_tasks()
1093## @brief Führe die zwischenzeitlich für die spätere Ausführung vorgemerkten Aufgaben aus.
1094## @details Unabhängig vom Ausführungsergebnis wird das Skript anschließend gelöscht.
1095run_scheduled_tasks() {
1096 trap 'error_trap run_scheduled_tasks "$*"' EXIT
1097 local fname
1098 local temp_fname
1099 local running_tasks
1100 [ -d "$SCHEDULING_DIR" ] || return 0
1101 # keine Ausführung, falls noch mindestens ein alter Task aktiv ist
1102 running_tasks=$(find "$SCHEDULING_DIR" -type f -name "*.running" | while read -r fname; do
1103 # veraltete Dateien werden geloescht und ignoriert
1104 # wir müssen uns an dem langsamsten Cron-Job orientieren:
1105 # - MTU-Test für UGWs: ca. 5 Minuten
1106 # - update_olsr_services: mehr als 5 Minuten
1107 is_file_timestamp_older_minutes "$fname" 30 && rm -f "$fname" && continue
1108 # nicht-veraltete Dateien fuehren zum Abbruch der Funktion
1109 msg_info "Skipping 'run_scheduled_task' due to an ongoing operation: $(tail -1 "$fname")"
1110 echo "$fname"
1111 done)
1112 [ -n "$running_tasks" ] && return 0
1113 # die ältesten Dateien zuerst ausführen
1114 find "$SCHEDULING_DIR" -type f | grep -v '\.running$' | xargs -r ls -tr | while read -r fname; do
1115 temp_fname="${fname}.running"
1116 # zuerst schnell wegbewegen, damit wir keine Ereignisse verpassen
1117 # Im Fehlerfall (eine race condition) einfach beim naechsten Eintrag weitermachen.
1118 mv "$fname" "$temp_fname" 2>/dev/null || continue
1119 { /bin/sh "$temp_fname" | logger -t "on-scheduled"; } 2>&1 | logger -t "on-scheduled-error ($fname)"
1120 rm -f "$temp_fname"
1121 done
1122}
1123
1124
1125## @fn schedule_task()
1126## @brief Erzeuge ein Start-Skript für die baldige Ausführung einer Aktion.
1127## @details Diese Methode sollte für Aufgaben verwendet werden, die nicht unmittelbar ausgeführt
1128## werden müssen und im Zweifelsfall nicht parallel ablaufen sollen (ressourcenschonend).
1129schedule_task() {
1130 trap 'error_trap schedule_task "$*"' EXIT
1131 local script_content
1132 local unique_key
1133 script_content=$(cat -)
1134 # wir sorgen fuer die Wiederverwendung des Dateinamens, um doppelte Ausführungen zu verhindern
1135 unique_key=$(echo "$script_content" | md5sum | awk '{ print $1 }')
1136 local target_file="$SCHEDULING_DIR/$unique_key"
1137 # das Skript existiert? Nichts zu tun ...
1138 [ -e "$target_file" ] && return 0
1139 mkdir -p "$SCHEDULING_DIR"
1140 echo "$script_content" >"$target_file"
1141}
1142
1143
1144## @fn schedule_parts()
1145## @brief Merke alle Skripte in einem Verzeichnis für die spätere Ausführung via 'run_scheduled_tasks' vor.
1146## @details Die Namenskonventionen und das Verhalten entspricht dem verbreiteten 'run-parts'-Werkzeug.
1147## Die Dateien müssen ausführbar sein.
1148schedule_parts() {
1149 trap 'error_trap schedule_parts "$*"' EXIT
1150 local schedule_dir="$1"
1151 local fname
1152 _get_parts_dir_files "$schedule_dir" | while read -r fname; do
1153 msg_debug "on-schedule-parts: scheduling $fname"
1154 # ignoriere Fehler bei der Ausfuehrung
1155 echo "$fname" | schedule_task
1156 done
1157}
1158
1159
1160## @fn read_data_bytes()
1161## @brief Bytes von einem Blockdevice lesen
1162## @param source das Quell-Blockdevice (oder die Datei)
1163## @param size die Anzahl der zu uebertragenden Bytes
1164## @param transfer_blocksize die Blockgroesse bei der Uebertragung (Standard: 65536)
1165## @details Die verwendete Uebertragung in grossen Bloecken ist wesentlich schneller als das byteweise EinlesenaKopie.sh_backup
1166## Der abschliessende unvollstaendige Block wird byteweise eingelesen.
1167read_data_bytes() {
1168 local size="$1"
1169 local transfer_blocksize="${2:-65536}"
1170 # "conv=sync" ist fuer die "yes"-Quelle erforderlich - sonst fehlt gelegentlich der letzte Block.
1171 # Es scheint sich dazu bei um eine race-condition zu handeln.
1172 dd "bs=$transfer_blocksize" "count=$((size / transfer_blocksize))" conv=sync 2>/dev/null
1173 [ "$((size % transfer_blocksize))" -ne 0 ] && dd bs=1 "count=$((size % transfer_blocksize))" 2>/dev/null
1174 true
1175}
1176
1177
1178## @fn get_flash_backup()
1179## @brief Erzeuge einen rohen Dump des Flash-Speichers. Dieser ermöglicht den Austausch des Flash-Speichers.
1180## @param include_private Kopiere neben den nur-Lese-Bereichen auch die aktuelle Konfiguration inkl. eventueller privater Daten.
1181## @details Alle mtd-Partition bis auf den Kernel und die Firmware werden einzeln kopiert und dann komprimiert.
1182## Beispiel-Layout einer Ubiquiti Nanostation:
1183## dev: size erasesize name
1184## mtd0: 00040000 00010000 "u-boot"
1185## mtd1: 00010000 00010000 "u-boot-env"
1186## mtd2: 00760000 00010000 "firmware"
1187## mtd3: 00102625 00010000 "kernel"
1188## mtd4: 0065d9db 00010000 "rootfs"
1189## mtd5: 00230000 00010000 "rootfs_data"
1190## mtd6: 00040000 00010000 "cfg"
1191## mtd7: 00010000 00010000 "EEPROM"
1192## Dabei ignorieren wir bei Bedarf "rootfs_data" (beschreibbarer Bereich der Firmware).
1193get_flash_backup() {
1194 trap 'error_trap get_flash_backup "$*"' EXIT
1195 local include_private="${1:-}"
1196 local name
1197 local size
1198 local blocksize
1199 local label
1200 # shellcheck disable=SC2034
1201 grep '^mtd[0-9]\+:' /proc/mtd | while read -r name size blocksize label; do
1202 # abschliessenden Doppelpunkt entfernen
1203 name="${name%:}"
1204 # hexadezimal-Zahl umrechnen
1205 size=$(echo | awk "{print 0x$size }")
1206 # Anfuehrungszeichen entfernen
1207 label=$(echo "$label" | cut -f 2 -d '"')
1208 # Firmware-Partitionen ueberspringen
1209 if [ "$label" = "rootfs" ]; then
1210 local rootfs_device="/dev/$name"
1211 local rootfs_full_size="$size"
1212 elif [ "$label" = "rootfs_data" ]; then
1213 # schreibe das komplette rootfs _ohne_ das aktuelle rootfs_data
1214 echo >&2 "Read: root-RO $((rootfs_full_size - size))"
1215 # Transfer blockweise vornehmen - byteweise dauert es zu lang
1216 read_data_bytes "($((rootfs_full_size - size)))" <"$rootfs_device"
1217 if [ -z "$include_private" ]; then
1218 echo >&2 "Read: root-zero ($size)"
1219 # erzeuge 0xFF
1220 # siehe http://stackoverflow.com/a/10905109
1221 tr '\0' '\377' </dev/zero | read_data_bytes "$size"
1222 else
1223 echo >&2 "Read: root-RW ($size)"
1224 # auch das private rootfs-Dateisystem (inkl. Schluessel, Passworte, usw.) auslesen
1225 read_data_bytes "$size" <"/dev/$name"
1226 fi
1227 elif [ "$label" = "firmware" ]; then
1228 echo >&2 "Skip: $label ($size)"
1229 # ignoriere die meta-Partition (kernel + rootfs)
1230 true
1231 else
1232 echo >&2 "Read: $label ($size)"
1233 cat "/dev/$name"
1234 fi
1235 done
1236}
1237
1238
1239## @fn has_flash_or_filesystem_error_indicators()
1240## @brief Prüfe ob typische Indikatoren (vor allem im Kernel-Log) vorliegen, die auf einen Flash-Defekt hinweisen.
1241has_flash_or_filesystem_error_indicators() {
1242 trap 'error_trap get_flash_backup "$*"' EXIT
1243 dmesg | grep -q "jffs2.*CRC" && return 0
1244 dmesg | grep -q "SQUASHFS error" && return 0
1245 # keine Hinweise gefunden -> wir liefern "nein"
1246 trap "" EXIT && return 1
1247}
1248
1249
1250## @fn request_field_from_api()
1251## @brief Request the content of a field from an API endpoint.
1252## @details The response is stored in memory - thus this function should not be used for huge
1253## amounts of data.
1254request_field_from_api() {
1255 local path="$1"
1256 local json_query="$2"
1257 local data
1258 # wget returns a non-zero exitcode in case of an HTTP error status
1259 data=$(wget -q -O - "$OPENNET_API_URL/$path" || true)
1260 [ -z "$data" ] && return
1261 # the "jsonfilter" call returns with an error, if the wanted field does not exist
1262 echo "$data" | jsonfilter -e "$json_query" || true
1263}
1264
1265
1266get_main_ip_for_ip() {
1267 local ip="$1"
1268 result=$(request_field_from_api "/accesspoint/$ip" "@.main_ip")
1269 [ -z "$result" ] && result=$(request_field_from_api "/interface/$ip/accesspoint/" "@.main_ip")
1270 echo "$result"
1271}
1272
1273
1274get_name_for_main_ip() {
1275 local main_ip="$1"
1276 echo "$main_ip" | sed -E 's/^192\.168\.(\d+)\.(\d+)$/AP\1-\2/'
1277}
1278
1279
1280get_location_for_main_ip() {
1281 local main_ip="$1"
1282 request_field_from_api "/accesspoint/$main_ip" "@.post_address"
1283}
1284
1285
1286# Request a URL via https or http.
1287http_request() {
1288 wget -q -O - "$@"
1289}
1290
1291# Ende der Doku-Gruppe
1292## @}
set eu case $1 in network wireless firewall on function update_olsr_interfaces
Definition: 500-on-olsr:9
get_custom_log_content(log_name)
Liefere den Inhalt eines spezifischen Logs (z.B. das OpenVPN-Verbindungsprotokoll) zurück.
Definition: core.sh:39
append_to_custom_log(log_name, event)
Hänge eine neue Nachricht an ein spezfisches Protokoll an.
Definition: core.sh:29
_get_file_dict_value(key)
Auslesen eines Werts aus einem Schlüssel/Wert-Eingabestrom.
Definition: core.sh:85
update_mesh_interfaces()
Update mesh interfaces, routing daemons and policy routing.
Definition: core.sh:72
update_ntp_servers()
Übertrage die Liste der als NTP-Dienst announcierten Server in die sysntpd-Konfiguration.
Definition: core.sh:62
msg_debug(message)
Debug-Meldungen ins syslog schreiben.
Definition: core.sh:9
msg_error(message)
Die Fehlermeldungen werden in die Standard-Fehlerausgabe und ins syslog geschrieben.
Definition: core.sh:22
clean_restart_log()
Alle Log-Einträge aus der banner-Datei entfernen.
Definition: core.sh:75
key
Definition: core.sh:85
shift
Definition: core.sh:85
update_file_if_changed(target_filename)
Aktualisiere eine Datei, falls sich ihr Inhalt geändert haben sollte.
Definition: core.sh:49
msg_info(message)
Informationen und Fehlermeldungen ins syslog schreiben.
Definition: core.sh:15
done
Definition: core.sh:85
get_custom_log_filename(log_name)
Liefere den Inhalt eines spezifischen Logs (z.B. das OpenVPN-Verbindungsprotokoll) zurück.
Definition: core.sh:34
update_dns_servers()
Definition: core.sh:55
add_banner_event(event, timestamp)
Füge ein Ereignis zum dauerhaften Ereignisprotokoll (/etc/banner) hinzu.
Definition: core.sh:68
while read r key value
Definition: core.sh:85
get_mig_tunnel_servers()
Ermittle die Server für den gewünschen Dienst, die via Tunnel erreichbar sind. @params stype Dienst-T...
Definition: on-openvpn.sh:77
filter_enabled_services()
Filtere aus einer Reihe eingehender Dienste diejenigen heraus, die nicht manuell ausgeblendet wurden.
Definition: services.sh:49
get_services(service_type)
Liefere alle Dienste zurueck, die dem angegebenen Typ zugeordnet sind. Falls kein Typ angegben wird,...
Definition: services.sh:68
get_service_value(key, default)
Auslesen eines Werts aus der Service-Datenbank.
Definition: services.sh:86
filter_reachable_services()
Filtere aus einer Reihe eingehender Dienste diejenigen heraus, die erreichbar sind.
Definition: services.sh:44
uci_replace_list()
Replace the items in a list. Wanted items are expected via stdin (one per line, uci_path).
Definition: uci.sh:41
uci_add_list(uci_path, new_item)
Füge einen neuen Wert zu einer UCI-Liste hinzu und achte dabei auf Einmaligkeit.
Definition: uci.sh:10
if[-d "/etc/on-firewall.d/"]
set eu grep root::etc shadow exit if command v chpasswd dev null
Definition: on-password:12
set eu on function print_services services log for dir in etc on services d var on services volatile d
Definition: services:13