Projekt 4: monitor, który krzyczy raz, a nie co pięć minut
Po tej lekcji: kursant ma skrypt, który co pięć minut sprawdza dyski, pamięć, obciążenie i usługi, pamięta poprzedni stan i wysyła mail tylko wtedy, gdy coś się zepsuło, nadal jest zepsute po kilku godzinach albo zostało naprawione; do tego umie skonfigurować wysyłanie poczty z serwera.
Wstęp
Pierwszy monitor, jaki napisałem, był bardzo sumienny. Co pięć minut sprawdzał dysk i jeśli było powyżej progu, wysyłał mail. Dysk zapełnił się w piątek wieczorem. W poniedziałek miałem 288 maili dziennie, razy trzy dni. Wiesz, ile z nich przeczytałem? Zero. Bo po trzecim założyłem filtr, który je chował. A w niedzielę w nocy, między nimi, przyszedł jeden inny: „usługa mariadb: inactive”. Też poszedł do filtra. To się nazywa zmęczenie alarmami i jest prawdziwym problemem w każdym zespole utrzymania. Bajka o chłopcu, który wołał „wilk!”, tylko chłopiec jest skryptem w Bashu i nie ma sumienia. Dziś piszemy monitor, który ma pamięć. Krzyczy, gdy coś się zepsuje. Milczy, gdy dalej jest zepsute. Przypomina po kilku godzinach. I mówi „naprawione”, żebyś wiedział, że możesz iść spać.
Plan: dyżurny z notesem
Wyobraź sobie dyżurnego w budynku. Co godzinę robi obchód: kotłownia, magazyn, winda. I ma notes. Jeśli winda stoi, a w notesie z ostatniego obchodu jest „jechała”, dzwoni do kierownika. Jeśli winda stoi, a w notesie już jest „stoi, dzwoniłem o 14:00”, nie dzwoni drugi raz o 15:00. Zadzwoni wieczorem, jeśli dalej stoi. A gdy winda ruszy, zadzwoni z dobrą wiadomością. Nasz skrypt to ten dyżurny. Obchód obejmuje:
- Dyski. Zajętość miejsca i i-węzłów dla każdego lokalnego systemu plików. Pełne i-węzły przy wolnym miejscu to klasyk z lekcji 100: miliony malutkich plików, a
df -hmówi, że wszystko w porządku. - Pamięć. Ile jest dostępnej, nie wolnej. Różnica za chwilę.
- Obciążenie. Load average z 5 minut porównany z liczbą rdzeni.
- Usługi. Czy
ssh,cron,apache2i co tam jeszcze masz, są aktywne. Notes to folder/var/lib/monitorz małym plikiem na każdą kontrolę: ostatni stan i czas ostatniego powiadomienia.
Poczta z serwera: skrzynka na rogu
Zanim monitor zacznie krzyczeć, musi mieć czym. Serwer sam z siebie nie umie dostarczyć maila do Twojej skrzynki. Mógłby próbować bezpośrednio, ale wielu dostawców chmury blokuje wychodzący port 25, a poczta z przypadkowego adresu IP ląduje w spamie albo nigdzie. Dlatego serwer oddaje list listonoszowi: prawdziwemu serwerowi poczty firmy albo usługi pocztowej, z logowaniem, na porcie 587 z szyfrowaniem. A jak programy na serwerze oddają list? Od dziesięcioleci jest jedna umowa: komenda /usr/sbin/sendmail. To skrzynka pocztowa na rogu. Każdy program, od crona po nasz skrypt, wrzuca do niej list, a co się dzieje dalej, to już sprawa poczty. Kto stoi za tą skrzynką, wybierasz sam:
- Postfix jako klient wysyłający, przerabiany w lekcji 87. Pełna poczta z kolejką, ponawia wysyłkę, gdy listonosz chwilowo nie odbiera. Działa tak samo na Ubuntu i RHEL.
- msmtp, malutki program, który po prostu łączy się z serwerem SMTP i oddaje list. Bez kolejki, bez demona. [Ubuntu] pakiet
msmtp-mtasam podstawia się pod/usr/sbin/sendmail. Uwaga: wyklucza się z Postfixem, bo obaj chcą być tą jedną skrzynką. [RHEL]msmtpjest w EPEL. Konfiguracja msmtp dla całego systemu leży w/etc/msmtprc:
defaults
auth on
tls on
tls_trust_file system
logfile /var/log/msmtp.log
account firma
host smtp.mojafirma.pl
port 587
from web1@mojafirma.pl
user web1@mojafirma.pl
passwordeval cat /etc/msmtp-haslo
account default : firmaport 587 z tls on to szyfrowanie przez STARTTLS: połączenie zaczyna się otwartym tekstem i zaraz przechodzi na szyfrowane. passwordeval bierze hasło z wyniku komendy, dzięki czemu samo hasło leży w osobnym pliku /etc/msmtp-haslo z prawami 600 i nie wycieknie, gdy ktoś pokaże konfigurację koledze. account default : firma znaczy „domyślne konto dziedziczy wszystko po koncie firma”. Najlepiej załóż dla serwerów osobną skrzynkę albo hasło aplikacji, a nie używaj swojego prywatnego konta. Jak serwer wycieknie, wymieniasz jedno hasło, a nie swoje życie.
Skrypt: pełny kod
Plik /usr/local/sbin/monitor.sh:
#!/usr/bin/env bash
# monitor.sh — dysk, pamięć, obciążenie i usługi; mail tylko przy zmianie stanu
# Użycie: monitor.sh (sprawdź; z crona co 5 minut)
# monitor.sh -m (wyślij mail testowy i zakończ)
set -euo pipefail
# ---------- Ustawienia domyślne (nadpisuje je /etc/monitor.conf) ----------
PROG_DYSK=85 # alarm, gdy system plików zajęty w co najmniej tylu %
PROG_INODY=90 # to samo dla i-węzłów
PROG_PAMIEC=10 # alarm, gdy dostępnej pamięci zostało mniej niż tyle %
PROG_LOAD="" # alarm, gdy load z 5 minut większy; pusty = 2 x liczba rdzeni
USLUGI=(ssh cron) # nazwy usług systemd ([RHEL]: sshd crond)
POMIN=('/snap/*') # punkty montowania, których nie sprawdzać (wzorce)
MAIL_DO="" # adres na alarmy; pusty = tylko dziennik
MAIL_OD="monitor@$(hostname -f 2>/dev/null || hostname)"
PRZYPOMNIJ_PO=360 # minuty: przypomnij, gdy problem dalej trwa
STAN=/var/lib/monitor
KONF=${MONITOR_KONF:-/etc/monitor.conf}
# shellcheck source=/dev/null
[[ -r $KONF ]] && . "$KONF"
PROG_LOAD=${PROG_LOAD:-$(( $(nproc) * 2 ))}
HOST=$(hostname -s)
TERAZ=$(date +%s)
declare -A WYNIK=() # klucz -> OK albo ALARM
declare -A OPIS=() # klucz -> opis dla człowieka
# sprawdz KLUCZ CZY_ZLE OPIS — zapisz wynik jednej kontroli
sprawdz() {
if (( $2 )); then WYNIK[$1]=ALARM; else WYNIK[$1]=OK; fi
OPIS[$1]=$3
}
# wyslij_mail TEMAT — treść ze standardowego wejścia, przez sendmail (Postfix albo msmtp)
wyslij_mail() {
local temat=$1
{
printf 'From: %s\nTo: %s\n' "$MAIL_OD" "$MAIL_DO"
printf 'Subject: =?UTF-8?B?%s?=\n' "$(printf '%s' "$temat" | base64 -w0)"
printf 'Date: %s\nMIME-Version: 1.0\n' "$(date -R)"
printf 'Content-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n'
cat
} | /usr/sbin/sendmail -t -i
}
install -d -m 700 "$STAN"
exec 9>"$STAN/.lock"
flock -n 9 || exit 0 # poprzedni przebieg jeszcze trwa: nic nie rób
if [[ ${1:-} == -m ]]; then
[[ -n $MAIL_DO ]] || { echo "Ustaw MAIL_DO w $KONF" >&2; exit 1; }
echo "Test z $HOST, $(date '+%F %T'). Jeśli to czytasz, alarmy dojdą." \
| wyslij_mail "[monitor] $HOST: test powiadomień"
echo "Wysłano mail testowy do $MAIL_DO"; exit 0
fi
# ---------- 1. Dyski: miejsce i i-węzły ----------
while read -r zajete inody punkt; do
for w in "${POMIN[@]}"; do
# shellcheck disable=SC2053 # $w to celowo wzorzec, nie napis
[[ $punkt == $w ]] && continue 2
done
zajete=${zajete%\%}; inody=${inody%\%}
[[ $inody == - ]] && inody=0 # np. btrfs nie liczy i-węzłów
sprawdz "dysk:$punkt" $(( zajete >= PROG_DYSK )) "$punkt zajęty w $zajete% (próg $PROG_DYSK%)"
sprawdz "inody:$punkt" $(( inody >= PROG_INODY )) "$punkt: i-węzły zajęte w $inody% (próg $PROG_INODY%)"
done < <(df --local --output=pcent,ipcent,target -x tmpfs -x devtmpfs -x squashfs -x overlay | tail -n +2)
# ---------- 2. Pamięć ----------
read -r pam_calk pam_dost < <(awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{print t, a}' /proc/meminfo)
proc_dost=$(( pam_dost * 100 / pam_calk ))
sprawdz pamiec $(( proc_dost < PROG_PAMIEC )) \
"dostępna pamięć: $proc_dost% ($(( pam_dost / 1024 )) MiB z $(( pam_calk / 1024 )) MiB, próg $PROG_PAMIEC%)"
# ---------- 3. Obciążenie ----------
read -r _ load5 _ < /proc/loadavg
sprawdz load "$(awk -v l="$load5" -v p="$PROG_LOAD" 'BEGIN{print (l > p) ? 1 : 0}')" \
"load average z 5 minut: $load5 (próg $PROG_LOAD przy $(nproc) rdzeniach)"
# ---------- 4. Usługi ----------
for u in "${USLUGI[@]}"; do
if [[ $(cat /proc/1/comm) == systemd ]]; then
stan=$(systemctl is-active "$u" 2>/dev/null || true)
else
# bez systemd (kontener): sprawdzamy, czy działa proces o tej nazwie
case $u in ssh) p=sshd ;; *) p=$u ;; esac
if pgrep -x "$p" >/dev/null; then stan=active; else stan=inactive; fi
fi
sprawdz "usluga:$u" "$([[ $stan == active ]] && echo 0 || echo 1)" "usługa $u: $stan"
done
# ---------- 5. Porównanie z poprzednim stanem i powiadomienia ----------
zmiany=() przypomnienia=() aktualne=()
for k in $(printf '%s\n' "${!WYNIK[@]}" | sort); do
plik=$STAN/$(printf '%s' "$k" | tr '/:' '__')
poprzedni=OK; wyslano=0
[[ -f $plik ]] && read -r poprzedni wyslano < "$plik"
teraz=${WYNIK[$k]}
printf '%-6s %s\n' "$teraz" "${OPIS[$k]}"
[[ $teraz == ALARM ]] && aktualne+=("ALARM ${OPIS[$k]}")
if [[ $teraz != "$poprzedni" ]]; then
if [[ $teraz == ALARM ]]; then zmiany+=("NOWY ALARM ${OPIS[$k]}"); else zmiany+=("NAPRAWIONE ${OPIS[$k]}"); fi
wyslano=$TERAZ
logger -t monitor -p "user.$([[ $teraz == ALARM ]] && echo warning || echo notice)" \
"$teraz: ${OPIS[$k]}" 2>/dev/null || true
elif [[ $teraz == ALARM ]] && (( TERAZ - wyslano >= PRZYPOMNIJ_PO * 60 )); then
przypomnienia+=("NADAL ŹLE ${OPIS[$k]}")
wyslano=$TERAZ
fi
printf '%s %s\n' "$teraz" "$wyslano" > "$plik"
done
if (( ${#zmiany[@]} + ${#przypomnienia[@]} > 0 )); then
if (( ${#aktualne[@]} > 0 )); then temat="[monitor] $HOST: ALARM (${#aktualne[@]})"; else temat="[monitor] $HOST: wszystko OK"; fi
tresc=$(
echo "Serwer: $HOST Czas: $(date '+%F %T')"
echo
printf '%s\n' "${zmiany[@]}" "${przypomnienia[@]}" | sed '/^$/d'
echo
echo "Aktualne problemy (${#aktualne[@]}):"
printf ' %s\n' "${aktualne[@]:-brak}"
echo
echo "Najwięksi zjadacze pamięci:"
ps -eo pid,rss,comm --sort=-rss | head -n 4 || true
)
if [[ -n $MAIL_DO ]]; then
if wyslij_mail "$temat" <<< "$tresc"; then
echo ">> wysłano: $temat"
else
logger -t monitor -p user.err "nie udało się wysłać maila do $MAIL_DO" 2>/dev/null || true
echo ">> BŁĄD wysyłki maila" >&2
fi
else
echo ">> (bez MAIL_DO) $temat"
fi
fi
exit $(( ${#aktualne[@]} > 0 ))I przykładowe /etc/monitor.conf dla serwera z projektu 2:
# /etc/monitor.conf
PROG_DYSK=85
PROG_PAMIEC=10
USLUGI=(ssh cron apache2 mariadb) # [RHEL]: (sshd crond httpd mariadb)
POMIN=('/snap/*' '/boot/efi')
MAIL_DO=admin@mojafirma.pl
MAIL_OD=web1@mojafirma.plMAIL_OD ustaw na ten sam adres, co from w msmtp. Wiele serwerów poczty odrzuca listy, w których nadawca w nagłówku nie zgadza się z kontem, które się zalogowało. Jak list z cudzym podpisem.
Co tu jest ciekawego
Dwie szatnie. WYNIK i OPIS to tablice asocjacyjne, czyli nasze szatnie z nazwiskami z modułu o skryptach. Klucz to na przykład dysk:/srv/dane albo usluga:ssh, a pod nim wisi wynik i opis. Dzięki temu dodanie nowej kontroli to jedna linijka sprawdz KLUCZ CZY_ZLE OPIS, a cała logika powiadomień działa sama.
Dostępna, a nie wolna pamięć. Linux zajmuje wolny RAM na pamięć podręczną dysku, bo pusta pamięć to zmarnowana pamięć. Dlatego „wolnej” jest zwykle mało i nic w tym złego. Monitor, który patrzy na MemFree, krzyczy bez przerwy. Jądro w /proc/meminfo podaje MemAvailable: szacunek, ile pamięci programy mogą dostać bez wpychania czegoś do swapu. To jest ta liczba z kolumny available w free -h. Lodówka pełna resztek z wczoraj nie jest pełna, resztki się wyrzuci, gdy przyjdzie nowe jedzenie.
df --output. Zamiast ciąć awk-iem wynik df -h, prosimy df dokładnie o kolumny: procent miejsca, procent i-węzłów, punkt montowania. --local pomija dyski sieciowe, a -x wyklucza systemy plików w pamięci, które tylko by szumiały. Złota zasada skryptów: poproś narzędzie o dokładnie ten format, który chcesz, zamiast parsować ładny wydruk dla ludzi.
Ułamki przez awk. Load average to liczba z przecinkiem, na przykład 0.08, a arytmetyka Basha zna tylko liczby całkowite. Dlatego porównanie robi awk, który wypisuje 1 albo 0. $(( 0.08 > 4 )) skończyłoby się błędem składni.
Usługi z zapasowym planem. Na normalnym serwerze pytamy kierownika zmiany: systemctl is-active. Bez systemd, na przykład w kontenerze Dockera, skrypt szuka procesu o tej nazwie przez pgrep -x. Jedyny wyjątek w tłumaczeniu: usługa ssh to proces sshd.
Notes, czyli stan na dysku. Dla każdej kontroli plik w $STAN z dwiema wartościami: ostatni stan i czas ostatniego powiadomienia. Ukośniki i dwukropki w kluczu zamieniamy przez tr na podkreślniki, bo dysk:/srv/dane nie może być nazwą pliku. Mail idzie w trzech sytuacjach: zmiana z OK na ALARM, zmiana z ALARM na OK i ALARM trwający dłużej niż PRZYPOMNIJ_PO minut od ostatniego maila. W monitoringu nazywa się to alarmowaniem na zbocze: liczy się zmiana, a nie stan.
Polskie znaki w temacie. Nagłówki maila z definicji są w ASCII. Temat „test powiadomień” wysłany wprost potrafi dojść jako krzaki. Zapis =?UTF-8?B?...?= to standardowy sposób na znaki spoza ASCII w nagłówku: tekst w Base64 z informacją, że to UTF-8. Program pocztowy odkoduje go sam. Treść listu dostaje nagłówek Content-Type: text/plain; charset=UTF-8.
sendmail -t -i. -t każe wziąć adresata z nagłówka To:, a -i mówi, że samotna kropka w linijce nie kończy listu. Te same opcje rozumie Postfix i msmtp, więc skrypt nie wie i nie musi wiedzieć, kto stoi za skrzynką.
Kod wyjścia. Skrypt kończy się kodem 1, gdy jest jakikolwiek ALARM. Dzięki temu ten sam skrypt możesz podłączyć pod inny system monitorowania, który rozumie tylko kciuk w górę albo w dół. [dla zaawansowanych: || true przy ps | head nie jest ozdobą. head kończy się po czterech linijkach, a jeśli ps jeszcze pisze, dostaje SIGPIPE. Z pipefail całe podstawienie $( ) zwróciłoby wtedy błąd, a set -e zabiłby monitor w momencie wysyłania alarmu. Najgorszy możliwy moment.]
Uruchomienie: cała awaria w sześciu krokach
W laboratorium podmontowałem jako /srv/dane plik 20 MB z systemem ext4 przez urządzenie loop, żeby dało się go szybko zapełnić. Zamiast prawdziwego serwera poczty postawiłem na porcie 2525 malutki serwer SMTP, który każdy list zapisuje do pliku. W konfiguracji USLUGI=(ssh). Najpierw mail testowy, zawsze najpierw mail testowy:
sudo monitor.sh -mWysłano mail testowy do admin@firma.labKrok 1, wszystko działa:
OK / zajęty w 45% (próg 85%)
OK /srv/dane zajęty w 1% (próg 85%)
OK /: i-węzły zajęte w 4% (próg 90%)
OK /srv/dane: i-węzły zajęte w 1% (próg 90%)
OK load average z 5 minut: 0.08 (próg 4 przy 2 rdzeniach)
OK dostępna pamięć: 90% (7246 MiB z 8027 MiB, próg 10%)
OK usługa ssh: active
kod=0Cisza, żaden mail. Krok 2, ktoś wrzuca wielki plik:
sudo dd if=/dev/zero of=/srv/dane/wielki.plik bs=1M count=13 status=none
sudo monitor.shALARM /srv/dane zajęty w 98% (próg 85%)
...
>> wysłano: [monitor] web1: ALARM (1)
kod=1Krok 3, pięć minut później, dysk dalej pełny: kontrola wypisuje ALARM, ale żadnego >> wysłano. Dyżurny zajrzał do notesu, już dzwonił. Krok 4, pada SSH:
ALARM /srv/dane zajęty w 98% (próg 85%)
ALARM usługa ssh: inactive
>> wysłano: [monitor] web1: ALARM (2)Tak wygląda ten mail w skrzynce:
Temat: [monitor] web1: ALARM (2)
Serwer: web1 Czas: 2026-11-04 08:25:15
NOWY ALARM usługa ssh: inactive
Aktualne problemy (2):
ALARM /srv/dane zajęty w 98% (próg 85%)
ALARM usługa ssh: inactive
Najwięksi zjadacze pamięci:
PID RSS COMMAND
8 20316 python3
220 4112 ps
172 4076 bashW temacie nowa sprawa, a w treści pełen obraz, więc o trzeciej w nocy z telefonu wiesz, czy wstać. Krok 5: żeby nie czekać sześciu godzin na przypomnienie, uruchomiłem skrypt z PRZYPOMNIJ_PO=0 i przyszedł mail z NADAL ŹLE przy obu problemach. Krok 6, naprawa: kasuję plik, startuję SSH.
>> wysłano: [monitor] web1: wszystko OKTemat: [monitor] web1: wszystko OK
NAPRAWIONE /srv/dane zajęty w 1% (próg 85%)
NAPRAWIONE usługa ssh: active
Aktualne problemy (0):
brakPięć maili na całą awarię, liczy się z testowym. Mój pierwszy monitor wysłałby w tym czasie kilkaset. A notes po wszystkim:
sudo ls /var/lib/monitor; sudo cat /var/lib/monitor/dysk__srv_danedysk__ dysk__srv_dane inody__ inody__srv_dane load pamiec usluga_ssh
OK 1793777116dysk__ to katalog główny /, któremu tr zamienił ukośnik na podkreślnik. [dla zaawansowanych: awarię usługi ćwicz na VM, z prawdziwym systemctl stop ssh, oczywiście z otwartą konsolą maszyny wirtualnej, a nie przez SSH, które właśnie zatrzymujesz.]
Harmonogram
Budzik co pięć minut, plik /etc/cron.d/monitor:
# /etc/cron.d/monitor
*/5 * * * * root /usr/local/sbin/monitor.sh >/dev/null 2>&1Wersja z timerem systemd wygląda tak samo jak przy kopiach z projektu 3, tylko w kopia.timer zamieniasz OnCalendar na *:0/5, czyli co pięć minut od pełnej godziny:
systemd-analyze calendar '*:0/5' Original form: *:0/5
Normalized form: *-*-* *:00/5:00
Next elapse: Wed 2026-11-04 08:30:00 CETJak to działa pod spodem
Co się dzieje, gdy skrypt wrzuca list do /usr/sbin/sendmail, a za skrzynką stoi msmtp? msmtp otwiera połączenie TCP z serwerem poczty i prowadzi z nim rozmowę w protokole SMTP. To zwykły tekst, linijka po linijce, jak rozmowa przy okienku na poczcie. Opcja -v pokazuje ją całą. Tu z moim testowym serwerem, strzałka --> to my, <-- to serwer:
printf 'Subject: test\n\nhej\n' | msmtp -v admin@firma.lab<-- 220 sink ESMTP
--> EHLO localhost
<-- 250-sink
<-- 250 8BITMIME
--> MAIL FROM:<monitor@web1.lab>
<-- 250 OK
--> RCPT TO:<admin@firma.lab>
<-- 250 OK
--> DATA
<-- 354 dawaj
--> From: monitor@web1.lab
--> Date: Wed, 04 Nov 2026 08:25:17 +0100
--> Subject: testPrzywitanie, „od kogo”, „do kogo”, „oto treść”, kropka na końcu. Z prawdziwym serwerem zaraz po EHLO pojawi się STARTTLS, przejście na szyfrowanie, i AUTH, logowanie. Ważny szczegół: MAIL FROM i RCPT TO to koperta, a From: i To: w treści to nagłówki listu. Mogą się różnić, tak jak adres na kopercie może się różnić od nagłówka listu w środku. Na tym zresztą opiera się większość sztuczek spamerów i dlatego serwery poczty tak pilnie porównują jedno z drugim. A load average? Przypomnienie z modułu o procesach: to średnia liczba procesów, które chcą pracować albo czekają na dysk. Kolejka do kas w Biedronce. Na dwóch rdzeniach, czyli dwóch kasach, load 2 znaczy, że każda kasa ma jednego klienta. Load 4 znaczy, że za każdym stoi drugi. Dlatego próg liczymy z nproc, a nie wpisujemy na sztywno: skrypt skopiowany na serwer z 32 rdzeniami sam się dostosuje.
Ściąga
| Komenda | Flaga | Co robi |
|---|---|---|
df | --output=pcent,ipcent,target | tylko wybrane kolumny, wygodne do skryptów |
df | --local -x TYP | bez dysków sieciowych i bez wskazanych typów |
systemctl | is-active USŁUGA | wypisuje active, inactive albo failed |
pgrep | -x NAZWA | szuka procesu o dokładnie tej nazwie |
ps | -eo pid,rss,comm --sort=-rss | procesy posortowane od największej pamięci |
logger | -t ZNACZNIK -p user.warning | wpis do dziennika z priorytetem |
sendmail | -t -i | adresat z nagłówka, kropka nie kończy listu |
msmtp | -v | pokazuje całą rozmowę SMTP |
msmtp | -C PLIK | użyj wskazanego pliku konfiguracji |
base64 | -w0 | koduje bez łamania linii |
systemd-analyze | calendar '*:0/5' | pokazuje, kiedy zadziała wyrażenie czasu |
⚠ Typowy błąd
Instalujesz msmtp, odpalasz monitor.sh -m i zamiast maila:
msmtp: account default not found: no configuration file availableI kod wyjścia 78. msmtp jest, ale nie ma ani /etc/msmtprc, ani ~/.msmtprc u użytkownika, który go uruchomił. Pułapka jest podwójna. Jeśli konfigurację zrobiłeś w swoim domowym folderze, to test z Twojego konta zadziała, a monitor z crona, jako root, już nie, bo root ma inny dom. Działa ręcznie, a z crona nie: wracamy do lekcji 87, jak co tydzień. Dlatego konfiguracja serwerowa idzie do /etc/msmtprc, a test robisz dokładnie tak, jak zrobi to cron: sudo monitor.sh -m. Drugi wariant tego samego: mail testowy z serwera w chmurze nie dochodzi, a w /var/log/msmtp.log wisi przekroczony czas połączenia. Sprawdź port. Jeśli w konfiguracji masz 25, zmień na 587, bo wielu dostawców chmury blokuje wychodzący port 25 właśnie po to, żeby ich serwery nie rozsyłały spamu. nc -zv smtp.mojafirma.pl 587 z lekcji 71 powie Ci w sekundę, czy listonosz w ogóle otwiera drzwi. I trzeci, najbardziej podstępny: monitor działa, ale nikt nie monitoruje monitora. Ktoś zakomentował linię w cronie i od miesiąca jest cisza, którą bierzesz za spokój. Prosta obrona: raz w tygodniu, na przykład w poniedziałek rano, osobny wpis w cronie odpala monitor.sh -m. Gdy w poniedziałek nie przyjdzie mail testowy, wiesz, że zepsuł się sam alarm.
✎ Zadanie
Rozbuduj monitor z projektu 4 o sprawdzanie, ile dni zostało do wygaśnięcia certyfikatu HTTPS Twojej strony; poniżej 14 dni ma wysłać ostrzeżenie.
Pokaż rozwiązanie
Zadanie brzmiało: monitor z projektu 4 ma sprawdzać, ile dni zostało do końca ważności certyfikatu HTTPS, i poniżej 14 dni krzyczeć. Zrobiłem z tego osobny mały skrypt, bo przyda się też poza monitorem.
#!/usr/bin/env bash
# sprawdz_cert.sh — ile dni zostało do końca ważności certyfikatu HTTPS
# Użycie: sprawdz_cert.sh HOST [PORT] [PROG_DNI]
set -euo pipefail
HOST="${1:?Użycie: $0 HOST [PORT] [PROG_DNI]}"
PORT="${2:-443}"
PROG="${3:-14}"
# 1. Pobieramy certyfikat i wyciągamy z niego datę końca ważności
if ! koniec=$(timeout 10 openssl s_client -connect "${HOST}:${PORT}" \
-servername "$HOST" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null); then
echo "BŁĄD: nie udało się pobrać certyfikatu z ${HOST}:${PORT}" >&2
logger -t monitor -p user.err "Certyfikat ${HOST}:${PORT} — nie da się pobrać"
exit 2
fi
koniec="${koniec#notAfter=}" # "Nov 13 21:40:00 2026 GMT"
# 2. Liczymy dni: (sekundy końca - sekundy teraz) / 86400
dni=$(( ( $(date -d "$koniec" +%s) - $(date +%s) ) / 86400 ))
if (( dni < PROG )); then
echo "UWAGA: certyfikat ${HOST} wygasa za ${dni} dni (${koniec})"
logger -t monitor -p user.warning "Certyfikat ${HOST} wygasa za ${dni} dni"
exit 1
fi
echo "OK: certyfikat ${HOST} ważny jeszcze ${dni} dni (do ${koniec})"openssl s_client puka do serwera jak przeglądarka i dostaje certyfikat. openssl x509 -enddate czyta z niego datę „ważny do”. date -d zamienia tę datę na sekundy od 1970 roku, odejmujemy „teraz”, dzielimy przez liczbę sekund w dobie. -servername jest ważne: na jednym adresie IP często siedzi kilka stron i bez tego serwer może pokazać certyfikat nie tej strony, o którą pytasz.
Jak to przetestować, nie czekając dwa i pół miesiąca, aż Twój certyfikat zacznie umierać? Robimy sobie certyfikat, który żyje tylko 10 dni, i stawiamy na chwilę malutki serwer TLS z samego openssl:
openssl req -x509 -newkey rsa:2048 -nodes -keyout k10.pem -out c10.pem -days 10 -subj /CN=localhost
openssl req -x509 -newkey rsa:2048 -nodes -keyout k90.pem -out c90.pem -days 90 -subj /CN=localhost
openssl s_server -accept 18443 -cert c10.pem -key k10.pem -quiet &
openssl s_server -accept 18444 -cert c90.pem -key k90.pem -quiet &
./sprawdz_cert.sh localhost 18443; echo "kod: $?"
./sprawdz_cert.sh localhost 18444; echo "kod: $?"
./sprawdz_cert.sh localhost 18999; echo "kod: $?"UWAGA: certyfikat localhost wygasa za 9 dni (Nov 13 21:33:41 2026 GMT)
kod: 1
OK: certyfikat localhost ważny jeszcze 89 dni (do Feb 1 21:33:41 2027 GMT)
kod: 0
BŁĄD: nie udało się pobrać certyfikatu z localhost:18999
kod: 2Czemu 9, a nie 10? Bo dzielenie w Bashu obcina ułamki: 9 dni i 23 godziny to dla niego 9. Przy ostrzeżeniach to dobra cecha, lepiej krzyknąć dzień za wcześnie niż dzień za późno. Po teście zabij oba serwery: kill %1 %2.
W monitorze z projektu 4 dopisujesz jedno wywołanie obok sprawdzania dysku i pamięci, a gdy kod wyjścia jest różny od zera, przekazujesz wypisany komunikat do tej samej funkcji, która wysyła maila. Trzy różne kody wyjścia to nie fanaberia: 1 znaczy „certyfikat zaraz padnie”, 2 znaczy „w ogóle nie mogę go pobrać”, czyli pewnie leży cała strona. To dwa różne poziomy paniki.
Co dalej
Monitor mówi, że coś się dzieje. Ale co się działo przez całą noc, kto próbował się włamać i które strony sypały błędami, wiedzą tylko logi. W projekcie 5 piszemy analizator logów, który co rano robi z nich krótki raport dzienny.