رفتن به محتوا
LoopX

پروژه: بکاپ خودکار و بررسی سلامت سرور

توی این درس یاد می‌گیری همه‌ی چیزهایی را که در این دوره خواندی، در دو ابزار واقعی کنار هم بگذاری و روی یک سرور نصب و زمان‌بندی کنی. اولی، lx-backup، از یک پوشه بکاپ فشرده می‌گیرد، بکاپ‌های قدیمی را می‌چرخاند (پاک می‌کند)، همه‌چیز را در فایل لاگ و syslog ثبت می‌کند، با قفل جلوی اجرای همزمان را می‌گیرد و اگر چیزی خراب شد فریاد می‌زند. دومی، lx-health، فضای دیسک، حافظه، بار CPU و وضعیت سرویس‌ها را بررسی می‌کند و با کد خروج استاندارد (۰ سالم، ۱ هشدار، ۲ بحرانی) گزارش می‌دهد. آخر کار، هر دو را در /usr/local/bin نصب و با crontab -e زمان‌بندی می‌کنی و می‌بینی cron واقعاً اجرایشان می‌کند.

مسئله: سروری که کسی نگاهش نمی‌کند

Section titled “مسئله: سروری که کسی نگاهش نمی‌کند”

سرور کوچک یک کسب‌وکار را تصور کن: یک سایت، یک دیتابیس، چند فایل آپلودی. هیچ‌کس هر روز واردش نمی‌شود. دو سؤال همیشه بی‌جواب می‌ماند: «اگر امشب دیسک بسوزد، بکاپ دیروز را داریم؟» و «اگر دیسک دارد پر می‌شود یا سرویسی خوابیده، کی می‌فهمیم؟». جواب حرفه‌ای این است که سرور خودش هر شب بکاپ بگیرد، بکاپ‌های قدیمی را پاک کند تا دیسک پر نشود، و هر چند دقیقه سلامتش را بررسی کند و فقط وقتی مشکلی هست صدا کند.

تشبیه: نگهبان شب و دفتر گزارش

Section titled “تشبیه: نگهبان شب و دفتر گزارش”

lx-backup مثل نگهبان شب است که هر شب سر ساعت از گاوصندوق یک نسخه برمی‌دارد، روی قفسه می‌گذارد، نسخه‌های خیلی قدیمی را دور می‌ریزد و همه را در دفتر گزارش (لاگ) می‌نویسد؛ اگر کلید گاوصندوق کار نکرد، زنگ خطر را می‌زند، نه اینکه بی‌صدا برود خانه. lx-health مثل سرکشی دوره‌ای است: هر چند دقیقه یک دور می‌زند، چراغ‌ها را نگاه می‌کند و فقط وقتی چیزی غیرعادی دید، گزارش می‌دهد. cron هم ساعت دیواری‌ای است که به هر دو می‌گوید کی کارشان را شروع کنند.

جریان کار lx-backup: قفل بگیر (اگر نسخه‌ی دیگری در حال اجراست، خارج شو)، آرشیو را در پوشه‌ی موقت بساز و سالم‌بودنش را بسنج، به مقصد منتقل کن، بکاپ‌های قدیمی‌تر از KEEP روز را پاک کن، و همه را در لاگ و syslog ثبت کن. هر جا خطایی رخ دهد، trap آن را گزارش و فایل‌های موقت را پاک می‌کند.
✓ چک‌لیست پروژه

مثال‌های عملی (قدم‌به‌قدم)

Section titled “مثال‌های عملی (قدم‌به‌قدم)”

این پروژه روی ماشین آزمایشی server اجرا شده که systemd، cron، rsyslog و Nginx واقعی دارد (همان ماشین درس‌های cron و سرویس‌ها در دوره‌ی لینوکس). همه‌ی دستورها با کاربر root است، چون ابزارها در /usr/local/bin نصب می‌شوند و بکاپ از پوشه‌های سیستمی گرفته می‌شود. روی سرور خودت جلوی دستورها sudo بگذار.

قدم ۰: داده‌ی نمونه و وضع سرور

Section titled “قدم ۰: داده‌ی نمونه و وضع سرور”

یک «فروشگاه» نمونه در /srv/shop می‌سازیم (فایل‌های سایت، آپلودها، یک dump دیتابیس) و Nginx را روشن می‌کنیم تا بعداً سلامتش را بسنجیم. (ماشین آزمایشی ما در هسته‌اش IPv6 ندارد، پس خط listen [::]:80 سایت پیش‌فرض Nginx را در آن غیرفعال کرده‌ایم؛ روی سرور واقعی لازم نیست.)

Terminal window
mkdir -p /srv/shop/{site,uploads,db} /root/project
echo "<h1>Shop</h1>" > /srv/shop/site/index.html
head -c 200000 /dev/urandom > /srv/shop/uploads/photo-1.jpg
head -c 150000 /dev/urandom > /srv/shop/uploads/photo-2.jpg
printf 'CREATE TABLE orders (id int);\nINSERT INTO orders VALUES (1),(2),(3);\n' > /srv/shop/db/shop.sql
systemctl start nginx
systemctl is-active nginx cron rsyslog
du -sh /srv/shop
bash --version | head -1
خروجی
active
active
active
368K /srv/shop
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)

اول رابط ابزار را می‌سازیم: راهنما، گزینه‌ها، اعتبارسنجی و کدهای خروج (درس getopts)، و تنظیمات سخت‌گیرانه (درس مدیریت خطا). هنوز هیچ بکاپی گرفته نمی‌شود:

/root/project/lx-backup
#!/usr/bin/env bash
# lx-backup: compressed, rotated backups of a directory
#
# Usage: lx-backup [-h] [-v] [-n] [-d DEST] [-k DAYS] SOURCE
# Exit codes: 0 ok, 1 error, 2 usage error, 3 source not found, 75 already running
set -Eeuo pipefail
shopt -s inherit_errexit
readonly PROG=lx-backup
readonly E_USAGE=2 E_NOSRC=3 E_LOCKED=75
LOG_FILE=${LX_BACKUP_LOG:-/var/log/lx-backup.log}
LOCK_FILE=${LX_BACKUP_LOCK:-/run/lock/lx-backup.lock}
dest=/var/backups/lx
keep=7
verbose=false
dry_run=false
usage() {
cat <<EOF
Usage: $PROG [-h] [-v] [-n] [-d DEST] [-k DAYS] SOURCE
Create a compressed archive of SOURCE in DEST and delete archives
of the same SOURCE that are older than DAYS days.
Options:
-d DEST where to keep archives (default: $dest)
-k DAYS keep archives for DAYS days, 1-365 (default: $keep)
-n dry run: show what would happen, change nothing
-v verbose: also print log lines to the terminal
-h show this help
Log file: $LOG_FILE (and syslog, tag "$PROG")
Lock file: $LOCK_FILE
Exit codes: 0 ok, 1 error, $E_USAGE usage error, $E_NOSRC source not found,
$E_LOCKED another instance is running
EOF
}
die_usage() {
echo "$PROG: $*" >&2
echo "try '$PROG -h'" >&2
exit "$E_USAGE"
}
while getopts ":hvnd:k:" opt; do
case $opt in
h) usage; exit 0 ;;
v) verbose=true ;;
n) dry_run=true ;;
d) dest=$OPTARG ;;
k) keep=$OPTARG ;;
\?) die_usage "unknown option -$OPTARG" ;;
:) die_usage "-$OPTARG needs a value" ;;
esac
done
shift $((OPTIND - 1))
(( $# == 1 )) || die_usage "expected exactly one SOURCE"
src=${1%/}
[[ $keep =~ ^[0-9]+$ ]] || die_usage "-k must be a number, got '$keep'"
(( keep >= 1 && keep <= 365 )) || die_usage "-k must be 1-365, got '$keep'"
[[ $dest == /* ]] || die_usage "-d must be an absolute path, got '$dest'"
if [[ ! -d $src ]]; then
echo "$PROG: source '$src' is not a directory" >&2
exit "$E_NOSRC"
fi
echo "OK so far: src=$src dest=$dest keep=$keep dry_run=$dry_run verbose=$verbose"
Terminal window
cd /root/project
chmod +x lx-backup
shellcheck lx-backup && echo "shellcheck: clean"
./lx-backup -h
echo "==="
./lx-backup; echo "کد خروج: $?"
./lx-backup -k 0 /srv/shop; echo "کد خروج: $?"
./lx-backup -d backups /srv/shop; echo "کد خروج: $?"
./lx-backup /srv/nothing; echo "کد خروج: $?"
./lx-backup -v -k 14 /srv/shop/
خروجی
shellcheck: clean
Usage: lx-backup [-h] [-v] [-n] [-d DEST] [-k DAYS] SOURCE
Create a compressed archive of SOURCE in DEST and delete archives
of the same SOURCE that are older than DAYS days.
Options:
-d DEST where to keep archives (default: /var/backups/lx)
-k DAYS keep archives for DAYS days, 1-365 (default: 7)
-n dry run: show what would happen, change nothing
-v verbose: also print log lines to the terminal
-h show this help
Log file: /var/log/lx-backup.log (and syslog, tag "lx-backup")
Lock file: /run/lock/lx-backup.lock
Exit codes: 0 ok, 1 error, 2 usage error, 3 source not found,
75 another instance is running
===
lx-backup: expected exactly one SOURCE
try 'lx-backup -h'
کد خروج: 2
lx-backup: -k must be 1-365, got '0'
try 'lx-backup -h'
کد خروج: 2
lx-backup: -d must be an absolute path, got 'backups'
try 'lx-backup -h'
کد خروج: 2
lx-backup: source '/srv/nothing' is not a directory
کد خروج: 3
OK so far: src=/srv/shop dest=/var/backups/lx keep=14 dry_run=false verbose=true

اسکلت آماده است: هر ورودی غلطی قبل از هر کار واقعی با پیام روشن و کد درست رد می‌شود. چند تصمیم طراحی:

  • src=${1%/} اسلش آخر را برمی‌دارد تا /srv/shop/ و /srv/shop یکی باشند (درس رشته‌ها).
  • مقصد باید مسیر مطلق باشد؛ cron پوشه‌ی جاری مشخصی ندارد و مسیر نسبی در cron جای دیگری می‌رود.
  • LOG_FILE و LOCK_FILE از متغیر محیطی قابل‌تغییرند (${LX_BACKUP_LOG:-...}) تا بتوانی بدون root آزمایشش کنی.

قدم ۲: لاگ، syslog و اعلان خطا

Section titled “قدم ۲: لاگ، syslog و اعلان خطا”

ابزاری که شبانه اجرا می‌شود باید ردپا بگذارد. تابع log هر پیام را با زمان در فایل لاگ می‌نویسد؛ با -v روی صفحه هم چاپ می‌کند؛ و خطاها را به syslog هم می‌فرستد (با دستور logger)، جایی که ابزارهای مانیتورینگ سرور معمولاً نگاه می‌کنند. اعلان خطا را با trap می‌سازیم تا هیچ شکستی بی‌صدا نماند. این تکه را به‌جای خط آخر (echo "OK so far...") می‌گذاریم:

/root/project/part-log.sh
log() {
local level=$1
shift
local line
line="$(date '+%F %T') [$level] $*"
echo "$line" >> "$LOG_FILE"
if [[ $level == ERROR ]]; then
echo "$line" >&2
logger -t "$PROG" -p user.err -- "$*"
elif [[ $verbose == true ]]; then
echo "$line"
fi
}
# Optional extra alert: LX_ALERT_CMD receives the message on stdin
# (for example a script that sends an email or a chat message).
alert() {
log ERROR "$*"
if [[ -n ${LX_ALERT_CMD:-} ]]; then
echo "$PROG on $(hostname): $*" | $LX_ALERT_CMD || log ERROR "alert command failed"
fi
}
tmpdir=""
on_exit() {
local status=$?
[[ -n $tmpdir ]] && rm -rf -- "$tmpdir"
if (( status != 0 && status != E_LOCKED )); then
alert "backup of $src FAILED with status $status"
fi
}
trap on_exit EXIT
trap 'log ERROR "line $LINENO: \"$BASH_COMMAND\" failed"' ERR
log INFO "start: $src -> $dest (keep $keep days, dry run: $dry_run)"
Terminal window
cd /root/project
sed -i '/^echo "OK so far/d' lx-backup
cat part-log.sh >> lx-backup
shellcheck lx-backup && echo "shellcheck: clean"
./lx-backup -v /srv/shop
echo "--- فایل لاگ:"
cat /var/log/lx-backup.log
خروجی
shellcheck: clean
2026-10-04 12:01:52 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)
--- فایل لاگ:
2026-10-04 12:01:52 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)
  • logger -t TAG -p user.err یک پیام با برچسب و اولویت به syslog می‌فرستد (روی این سرور rsyslog آن را در /var/log/syslog می‌نویسد؛ روی سرورهای فقط-journald با journalctl -t lx-backup دیده می‌شود).
  • LX_ALERT_CMD یک قلاب (hook) است: اگر بعداً اسکریپتی برای ایمیل یا تلگرام داشتی، فقط این متغیر را تنظیم می‌کنی و به خود ابزار دست نمی‌زنی. اینجا عمداً بدون کوتیشن آمده تا مثل mail -s subject admin چندکلمه‌ای باشد (shellcheck در این حالت ایرادی نگرفت چون متغیر داخل pipe به‌عنوان دستور اجرا می‌شود؛ اگر گرفت، دلیل را در کامنت بنویس و disable کن).
  • on_exit فایل‌های موقت را پاک می‌کند و اگر کد خروج غیرصفر بود (جز «در حال اجراست»)، اعلان می‌دهد.

اگر بکاپ شب گذشته هنوز تمام نشده و cron نسخه‌ی بعدی را شروع کند، دو tar همزمان روی یک دیسک کار می‌کنند و هر دو کند یا خراب می‌شوند. flock (از util-linux) یک قفل روی یک فایل می‌گیرد که سیستم‌عامل نگهش می‌دارد؛ حتی اگر اسکریپت با kill -9 بمیرد، قفل خودکار آزاد می‌شود (برخلاف فایل قفل دستی که در درس مدیریت خطا ساختیم):

/root/project/part-lock.sh
# one instance at a time: fd 9 stays open (and locked) until the script exits
exec 9> "$LOCK_FILE"
if ! flock -n 9; then
log WARN "another $PROG is running; exiting"
echo "$PROG: another instance is running" >&2
exit "$E_LOCKED"
fi
Terminal window
cd /root/project
cat part-lock.sh >> lx-backup
shellcheck lx-backup && echo "shellcheck: clean"
echo "--- یک نسخه قفل را ۳ ثانیه نگه می‌دارد، نسخه‌ی دوم:"
( exec 9> /run/lock/lx-backup.lock; flock 9; sleep 3 ) &
sleep 0.5
./lx-backup /srv/shop; echo "کد خروج: $?"
wait
echo "--- بعد از آزاد شدن قفل:"
./lx-backup /srv/shop; echo "کد خروج: $?"
tail -n 3 /var/log/lx-backup.log
خروجی
shellcheck: clean
--- یک نسخه قفل را ۳ ثانیه نگه می‌دارد، نسخه‌ی دوم:
lx-backup: another instance is running
کد خروج: 75
--- بعد از آزاد شدن قفل:
کد خروج: 0
2026-10-04 12:01:53 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)
2026-10-04 12:01:53 [WARN] another lx-backup is running; exiting
2026-10-04 12:01:55 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)

exec 9> فایل فایل قفل را روی توصیف‌گر ۹ باز می‌کند و تا پایان اسکریپت باز نگه می‌دارد؛ flock -n 9 اگر قفل آزاد نبود فوراً (بدون انتظار) شکست می‌خورد. کد ۷۵ باعث شد on_exit اعلان خطا ندهد: «در حال اجراست» خطا نیست.

قدم ۴: ساختن آرشیو، امن و اتمی

Section titled “قدم ۴: ساختن آرشیو، امن و اتمی”

حالا خودِ بکاپ: آرشیو را در پوشه‌ی موقتی کنار مقصد می‌سازیم (تا mv روی همان دیسک و آنی باشد)، با tar -tzf سالم‌بودنش را می‌سنجیم و فقط بعد به اسم نهایی منتقل می‌کنیم. با -n فقط می‌گوییم چه کار می‌کردیم:

/root/project/part-archive.sh
base=$(basename "$src")
archive="$base-$(date +%Y%m%d-%H%M%S).tar.gz"
if [[ $dry_run == true ]]; then
log INFO "dry run: would create $dest/$archive"
else
mkdir -p "$dest"
tmpdir=$(mktemp -d -p "$dest" .tmp.XXXXXX)
tar -czf "$tmpdir/$archive" -C "$(dirname "$src")" "$base"
tar -tzf "$tmpdir/$archive" > /dev/null
mv "$tmpdir/$archive" "$dest/$archive"
size=$(du -h "$dest/$archive" | cut -f1)
log INFO "created $dest/$archive ($size)"
fi
Terminal window
cd /root/project
cat part-archive.sh >> lx-backup
shellcheck lx-backup && echo "shellcheck: clean"
./lx-backup -n -v /srv/shop
ls /var/backups/lx 2>/dev/null | wc -l
./lx-backup -v /srv/shop
ls -l /var/backups/lx | sed 1d | awk '{print $5, $9}'
tar -tzf /var/backups/lx/shop-*.tar.gz | head -n 5
خروجی
shellcheck: clean
2026-10-04 12:01:55 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: true)
2026-10-04 12:01:55 [INFO] dry run: would create /var/backups/lx/shop-20261004-120155.tar.gz
0
2026-10-04 12:01:55 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)
2026-10-04 12:01:55 [INFO] created /var/backups/lx/shop-20261004-120155.tar.gz (344K)
350759 shop-20261004-120155.tar.gz
shop/
shop/uploads/
shop/uploads/photo-1.jpg
shop/uploads/photo-2.jpg
shop/db/

با -n هیچ فایلی ساخته نشد. اجرای واقعی آرشیو را ساخت و فهرست داخلش نشان می‌دهد پوشه با مسیر نسبی (shop/...) ذخیره شده؛ موقع بازگردانی، هر جا خواستی بازش می‌کنی و مسیر مطلق سرور قبلی را لازم نداری. اسم پوشه‌ی موقت با نقطه شروع می‌شود (.tmp.XXXXXX) تا در ls معمولی دیده نشود و الگوی چرخش آن را نگیرد.

قدم ۵: چرخش بکاپ‌های قدیمی

Section titled “قدم ۵: چرخش بکاپ‌های قدیمی”

بدون پاک‌کردن، دیسک بالاخره پر می‌شود. ساده‌ترین شکل چرخش یک خط find است:

شکل ساده
find /backups -mtime +7 -delete

-mtime +7 یعنی «آخرین تغییر بیش از ۷ دوره‌ی ۲۴ساعته پیش» و -delete پاکشان می‌کند. در ابزار، آن را محدودتر می‌نویسیم: فقط در خود مقصد (-maxdepth 1)، فقط فایل، و فقط آرشیوهای همین مبدأ (تا بکاپ مبدأ دیگری که در همین پوشه است پاک نشود)؛ و -print تا اسم هر فایل پاک‌شده در لاگ بیاید:

/root/project/part-rotate.sh
find_old() {
find "$dest" -maxdepth 1 -type f -name "$base-*.tar.gz" -mtime +"$keep" "$@"
}
if [[ $dry_run == true ]]; then
while IFS= read -r old; do
log INFO "dry run: would delete $old"
done < <(find_old -print)
else
deleted=0
while IFS= read -r old; do
log INFO "deleted $old"
deleted=$((deleted + 1))
done < <(find_old -print -delete)
log INFO "rotation: deleted $deleted archive(s) older than $keep days"
fi
log INFO "done"

برای آزمایش، چند آرشیو «قدیمی» با touch -d می‌سازیم (تاریخ تغییرشان را عقب می‌بریم) و یک فایل از مبدأ دیگر:

Terminal window
cd /root/project
cat part-rotate.sh >> lx-backup
shellcheck lx-backup && echo "shellcheck: clean"
for d in 3 8 10 30; do
f="/var/backups/lx/shop-old-$d-days.tar.gz"
touch -d "$d days ago" "$f"
done
touch -d "30 days ago" /var/backups/lx/blog-old.tar.gz
echo "=== قبل:"
ls /var/backups/lx
echo "=== اجرای آزمایشی (-n):"
./lx-backup -n -v /srv/shop
echo "=== اجرای واقعی:"
./lx-backup -v /srv/shop
echo "=== بعد:"
ls /var/backups/lx
خروجی
shellcheck: clean
=== قبل:
blog-old.tar.gz
shop-20261004-120155.tar.gz
shop-old-10-days.tar.gz
shop-old-3-days.tar.gz
shop-old-30-days.tar.gz
shop-old-8-days.tar.gz
=== اجرای آزمایشی (-n):
2026-10-04 12:01:56 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: true)
2026-10-04 12:01:56 [INFO] dry run: would create /var/backups/lx/shop-20261004-120156.tar.gz
2026-10-04 12:01:56 [INFO] dry run: would delete /var/backups/lx/shop-old-30-days.tar.gz
2026-10-04 12:01:56 [INFO] dry run: would delete /var/backups/lx/shop-old-10-days.tar.gz
2026-10-04 12:01:56 [INFO] dry run: would delete /var/backups/lx/shop-old-8-days.tar.gz
2026-10-04 12:01:56 [INFO] done
=== اجرای واقعی:
2026-10-04 12:01:56 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)
2026-10-04 12:01:56 [INFO] created /var/backups/lx/shop-20261004-120156.tar.gz (344K)
2026-10-04 12:01:56 [INFO] deleted /var/backups/lx/shop-old-30-days.tar.gz
2026-10-04 12:01:56 [INFO] deleted /var/backups/lx/shop-old-10-days.tar.gz
2026-10-04 12:01:56 [INFO] deleted /var/backups/lx/shop-old-8-days.tar.gz
2026-10-04 12:01:56 [INFO] rotation: deleted 3 archive(s) older than 7 days
2026-10-04 12:01:56 [INFO] done
=== بعد:
blog-old.tar.gz
shop-20261004-120155.tar.gz
shop-20261004-120156.tar.gz
shop-old-3-days.tar.gz

اجرای آزمایشی سه فایل قدیمی‌تر از ۷ روز را نشان داد و چیزی پاک نکرد؛ اجرای واقعی همان سه را پاک کرد. آرشیو ۳ روزه ماند، و blog-old.tar.gz با اینکه ۳۰ روزه است ماند، چون مال مبدأ دیگری است. خروجی find با < <(...) (جایگزینی پروسه) به حلقه رسید تا شمارنده‌ی deleted در همان شل بماند (درس حلقه‌ها: cmd | while در پروسه‌ی جدا اجرا می‌شود).

ابزار پشتیبان را باید در حال شکست هم دید. یک فایل غیرقابل‌خواندن در مبدأ می‌گذاریم (در دنیای واقعی: دیسک خراب، مجوز اشتباه، فایلی که وسط کار پاک شد) و ابزار را با یک «اعلان» آزمایشی اجرا می‌کنیم که پیام را در یک فایل می‌نویسد:

Terminal window
cd /root/project
mkdir -p /srv/broken && echo "x" > /srv/broken/data.txt
mkdir /srv/broken/locked && chmod 000 /srv/broken/locked
useradd -M -s /usr/sbin/nologin backupuser 2>/dev/null
chown -R backupuser /var/backups/lx
cat > /root/project/fake-alert <<'EOF'
#!/usr/bin/env bash
# fake-alert: stands in for an email or chat notification
echo "ALERT RECEIVED: $(cat)" >> /tmp/alerts.txt
EOF
chmod +x /root/project/fake-alert
rm -f /tmp/alerts.txt
runuser -u backupuser -- env LX_BACKUP_LOG=/tmp/lx-test.log LX_BACKUP_LOCK=/tmp/lx-test.lock LX_ALERT_CMD=/root/project/fake-alert ./lx-backup /srv/broken
echo "کد خروج: $?"
echo "--- اعلان دریافت‌شده:"
cat /tmp/alerts.txt
echo "--- syslog:"
grep 'lx-backup' /var/log/syslog | tail -n 3 | sed -E 's/^[^ ]+ server //'
echo "--- پوشه‌ی موقت مانده؟"
find /var/backups/lx -name '.tmp.*' | wc -l
chown -R root /var/backups/lx
rm -rf /srv/broken /tmp/lx-test.* /tmp/alerts.txt
خروجی
tar: broken/locked: Cannot open: Permission denied
tar: Exiting with failure status due to previous errors
2026-10-04 12:01:56 [ERROR] line 120: "tar -czf "$tmpdir/$archive" -C "$(dirname "$src")" "$base"" failed
2026-10-04 12:01:56 [ERROR] backup of /srv/broken FAILED with status 2
کد خروج: 2
--- اعلان دریافت‌شده:
ALERT RECEIVED: lx-backup on server: backup of /srv/broken FAILED with status 2
--- syslog:
lx-backup: line 120: "tar -czf "$tmpdir/$archive" -C "$(dirname "$src")" "$base"" failed
lx-backup: backup of /srv/broken FAILED with status 2
--- پوشه‌ی موقت مانده؟
0

root همه‌چیز را می‌تواند بخواند، پس برای شکست واقعی، ابزار را با یک کاربر معمولی (runuser -u backupuser) و فایل‌های لاگ و قفل آزمایشی اجرا کردیم. هر لایه کارش را کرد: tar خطا داد و کد ۲ برگرداند؛ trap ERR خط و دستور را در لاگ نوشت؛ set -e ادامه را متوقف کرد؛ on_exit پوشه‌ی موقت را پاک کرد و اعلان فرستاد؛ پیام‌های خطا در syslog هم ثبت شدند؛ و کد خروج ۲ به صدازننده (اینجا ما، در شب واقعی cron) رسید. هیچ آرشیو نیمه‌کاره‌ای در مقصد نماند.

قدم ۷: lx-health، گزارش سلامت

Section titled “قدم ۷: lx-health، گزارش سلامت”

ابزار دوم چهار چیز را می‌سنجد و برای هر کدام OK، WARN یا CRIT می‌دهد. کد خروج از قرارداد رایج ابزارهای مانیتورینگ (مثل Nagios) پیروی می‌کند: ۰ سالم، ۱ هشدار، ۲ بحرانی. با -q فقط وقتی مشکلی هست چیزی چاپ می‌کند؛ برای cron مهم است، چون cron هر خروجی را برای صاحب کار ایمیل می‌کند.

/root/project/lx-health
#!/usr/bin/env bash
# lx-health: disk, memory, load and service health report
#
# Usage: lx-health [-h] [-q] [-w PCT] [-c PCT] [-m "MOUNTS"] [-s "SERVICES"]
# Exit codes (Nagios style): 0 OK, 1 WARNING, 2 CRITICAL, 3 usage error
set -euo pipefail
readonly PROG=lx-health
warn=80
crit=90
mounts="/"
services="nginx cron"
quiet=false
usage() {
cat <<EOF
Usage: $PROG [-h] [-q] [-w PCT] [-c PCT] [-m "MOUNTS"] [-s "SERVICES"]
-w PCT warning threshold for disk and memory use (default: $warn)
-c PCT critical threshold (default: $crit)
-m "MOUNTS" mount points to check (default: "$mounts")
-s "SERVICES" systemd services that must be active (default: "$services")
-q quiet: print nothing when everything is OK
-h show this help
Exit codes: 0 OK, 1 WARNING, 2 CRITICAL, 3 usage error
EOF
}
while getopts ":hqw:c:m:s:" opt; do
case $opt in
h) usage; exit 0 ;;
q) quiet=true ;;
w) warn=$OPTARG ;;
c) crit=$OPTARG ;;
m) mounts=$OPTARG ;;
s) services=$OPTARG ;;
\?) echo "$PROG: unknown option -$OPTARG" >&2; exit 3 ;;
:) echo "$PROG: -$OPTARG needs a value" >&2; exit 3 ;;
esac
done
for n in "$warn" "$crit"; do
[[ $n =~ ^[0-9]+$ ]] || { echo "$PROG: thresholds must be numbers" >&2; exit 3; }
done
(( warn < crit )) || { echo "$PROG: -w must be lower than -c" >&2; exit 3; }
status=0
lines=()
names=(OK WARN CRIT)
# add_result LEVEL MESSAGE (LEVEL: 0, 1 or 2)
add_result() {
local level=$1
shift
lines+=("$(printf '%-4s %s' "${names[$level]}" "$*")")
if (( level > status )); then
status=$level
fi
}
level_for() {
local pct=$1
if (( pct >= crit )); then echo 2
elif (( pct >= warn )); then echo 1
else echo 0
fi
}
# 1) disk usage per mount point
for m in $mounts; do
if ! pct=$(df -P "$m" 2>/dev/null | awk 'NR == 2 { sub("%", "", $5); print $5 }'); then
add_result 2 "disk $m: cannot read"
continue
fi
add_result "$(level_for "$pct")" "disk $m: ${pct}% used"
done
# 2) memory: used = total - available
mem_pct=$(awk '/^MemTotal:/ { t = $2 } /^MemAvailable:/ { a = $2 } END { printf "%d", (t - a) * 100 / t }' /proc/meminfo)
add_result "$(level_for "$mem_pct")" "memory: ${mem_pct}% used"
# 3) load average (1 minute) compared to the number of CPUs
read -r load1 _ < /proc/loadavg
cpus=$(nproc)
load_pct=$(awk -v l="$load1" -v c="$cpus" 'BEGIN { printf "%d", l * 100 / c }')
add_result "$(level_for "$load_pct")" "load: $load1 on $cpus CPUs (${load_pct}%)"
# 4) services
for s in $services; do
state=$(systemctl is-active "$s" 2>/dev/null || true)
if [[ $state == active ]]; then
add_result 0 "service $s: active"
else
add_result 2 "service $s: ${state:-unknown}"
fi
done
summary="HEALTH ${names[$status]} on $(hostname) at $(date '+%F %T')"
if (( status > 0 )); then
logger -t "$PROG" -p user.warning -- "$summary"
fi
if [[ $quiet == false || $status -gt 0 ]]; then
echo "$summary"
printf ' %s\n' "${lines[@]}"
fi
exit "$status"
Terminal window
cd /root/project
chmod +x lx-health
shellcheck lx-health && echo "shellcheck: clean"
./lx-health; echo "کد خروج: $?"
echo "=== -q وقتی همه‌چیز سالم است (هیچ خروجی):"
./lx-health -q; echo "کد خروج: $?"
خروجی
shellcheck: clean
HEALTH OK on server at 2026-10-04 12:01:56
OK disk /: 26% used
OK memory: 4% used
OK load: 0.10 on 4 CPUs (2%)
OK service nginx: active
OK service cron: active
کد خروج: 0
=== -q وقتی همه‌چیز سالم است (هیچ خروجی):
کد خروج: 0

همه سالم، کد ۰، و با -q سکوت کامل. حالا مشکل می‌سازیم: آستانه‌ها را پایین می‌آوریم (تا دیسک «پر» حساب شود) و Nginx را خاموش می‌کنیم:

Terminal window
cd /root/project
echo "=== آستانه‌ی پایین برای دیسک و حافظه:"
./lx-health -w 3 -c 50; echo "کد خروج: $?"
echo "=== Nginx خاموش:"
systemctl stop nginx
./lx-health -q; echo "کد خروج: $?"
systemctl start nginx
echo "=== syslog:"
grep 'lx-health' /var/log/syslog | tail -n 2 | sed -E 's/^[^ ]+ server //'
خروجی
=== آستانه‌ی پایین برای دیسک و حافظه:
HEALTH WARN on server at 2026-10-04 12:01:56
WARN disk /: 26% used
WARN memory: 4% used
OK load: 0.10 on 4 CPUs (2%)
OK service nginx: active
OK service cron: active
کد خروج: 1
=== Nginx خاموش:
HEALTH CRIT on server at 2026-10-04 12:01:56
OK disk /: 26% used
OK memory: 4% used
OK load: 0.10 on 4 CPUs (2%)
CRIT service nginx: inactive
OK service cron: active
کد خروج: 2
=== syslog:
lx-health: HEALTH WARN on server at 2026-10-04 12:01:56
lx-health: HEALTH CRIT on server at 2026-10-04 12:01:56

با آستانه‌ی پایین، دیسک یا حافظه (هر کدام بالای ۳٪) WARN شدند و کد ۱؛ با خاموشی Nginx، CRIT و کد ۲، و این بار -q هم گزارش را چاپ کرد چون مشکلی بود. هر وضعیت غیرسالم در syslog هم ثبت شد.

دو نکته‌ی طراحی:

  • lines یک آرایه است (درس آرایه‌ها) و status بدترین سطح دیده‌شده را نگه می‌دارد؛ گزارش آخر کار یک‌جا چاپ می‌شود.
  • در add_result به‌جای (( level > status )) && status=$level از if استفاده کردیم: آن شکل کوتاه وقتی شرط غلط است، کد خروج ۱ به تابع می‌دهد و با set -e اسکریپت را می‌بندد (درس مدیریت خطا).

قدم ۸: نصب و زمان‌بندی با crontab -e

Section titled “قدم ۸: نصب و زمان‌بندی با crontab -e”

ابزارها را با install در /usr/local/bin نصب می‌کنیم؛ install در یک قدم کپی می‌کند و مجوز را تنظیم می‌کند. /usr/local/bin جای استاندارد برنامه‌هایی است که خودت (نه مدیر بسته) نصب می‌کنی:

Terminal window
cd /root/project
install -m 755 lx-backup lx-health /usr/local/bin/
ls -l /usr/local/bin/lx-*
command -v lx-backup lx-health
خروجی
-rwxr-xr-x 1 root root 4130 Oct 4 12:01 /usr/local/bin/lx-backup
-rwxr-xr-x 1 root root 3088 Oct 4 12:01 /usr/local/bin/lx-health
/usr/local/bin/lx-backup
/usr/local/bin/lx-health

حالا زمان‌بندی در crontab کاربر root. برای آزمایش، اول هر دو را «هر دقیقه» می‌گذاریم تا ببینیم cron واقعاً اجرایشان می‌کند؛ بعد زمان واقعی را می‌گذاریم. crontab -e را در nano باز می‌کنیم (داخل یک ترمینال مجازی):

Terminal window
run_in_tmux() { tmux -u kill-server 2>/dev/null; tmux -u new-session -d -s t -x 110 -y 24 "$@"; sleep 1; }
screen() { tmux capture-pane -t t -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
rm -f /var/log/lx-backup.log
run_in_tmux "bash --norc"
tmux send-keys -t t "export EDITOR=nano; crontab -e" Enter; sleep 2
tmux send-keys -t t M-/; sleep 1
tmux send-keys -t t -l 'MAILTO=""'; tmux send-keys -t t Enter
tmux send-keys -t t -l '* * * * * /usr/local/bin/lx-backup -k 7 /srv/shop'; tmux send-keys -t t Enter
tmux send-keys -t t -l '* * * * * /usr/local/bin/lx-health -q -s "nginx cron" >> /var/log/lx-health.log 2>&1'; tmux send-keys -t t Enter
sleep 1
screen | tail -n 8
tmux send-keys -t t C-o; sleep 1; tmux send-keys -t t Enter; sleep 1; tmux send-keys -t t C-x; sleep 2
tmux send-keys -t t "crontab -l | grep -v '^#'" Enter; sleep 1
screen | tail -n 5
tmux -u kill-server
خروجی
# m h dom mon dow command
MAILTO=""
* * * * * /usr/local/bin/lx-backup -k 7 /srv/shop
* * * * * /usr/local/bin/lx-health -q -s "nginx cron" >> /var/log/lx-health.log 2>&1
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location M-U Undo
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line M-E Redo
MAILTO=""
* * * * * /usr/local/bin/lx-backup -k 7 /srv/shop
* * * * * /usr/local/bin/lx-health -q -s "nginx cron" >> /var/log/lx-health.log 2>&1
bash-5.2#

بعد از ذخیره (Ctrl+O، Enter، Ctrl+X)، crontab پیام installing new crontab داد. سه خط اضافه کردیم:

  • MAILTO="": cron هر خروجی کار را برای کاربر ایمیل می‌کند؛ این سرور سرویس ایمیل ندارد، پس خاموشش کردیم. (روی سروری که ایمیل دارد، MAILTO=you@example.com بهترین اعلان رایگان است، به‌ویژه با lx-health -q که فقط موقع مشکل چیزی چاپ می‌کند.)
  • مسیرها همه مطلق‌اند (/usr/local/bin/...)؛ به PATH cron تکیه نمی‌کنیم (پشت پرده را ببین).
  • خروجی lx-health را با >> فایل 2>&1 در لاگ ریختیم.

حالا دو دقیقه صبر می‌کنیم و ردپای cron را می‌گردیم:

Terminal window
sleep 125
echo "--- لاگ lx-backup (نوشته‌شده توسط اجرای cron):"
grep -E 'start|created|rotation' /var/log/lx-backup.log | tail -n 3
echo "--- syslog: cron اجرایشان کرد؟"
grep CRON /var/log/syslog | grep -E 'lx-(backup|health)' | tail -n 2 | sed -E 's/^[^ ]+ server //'
echo "--- لاگ lx-health (خالی یعنی همه سالم بود):"
wc -c < /var/log/lx-health.log
خروجی
--- لاگ lx-backup (نوشته‌شده توسط اجرای cron):
2026-10-04 12:04:01 [INFO] start: /srv/shop -> /var/backups/lx (keep 7 days, dry run: false)
2026-10-04 12:04:01 [INFO] created /var/backups/lx/shop-20261004-120401.tar.gz (344K)
2026-10-04 12:04:01 [INFO] rotation: deleted 0 archive(s) older than 7 days
--- syslog: cron اجرایشان کرد؟
CRON[2091]: (root) CMD (/usr/local/bin/lx-health -q -s "nginx cron" >> /var/log/lx-health.log 2>&1)
CRON[2090]: (root) CMD (/usr/local/bin/lx-backup -k 7 /srv/shop)
--- لاگ lx-health (خالی یعنی همه سالم بود):
0

cron هر دو را اجرا کرد: آرشیوهای تازه ساخته شد، syslog خط CMD برای هر کار دارد، و لاگ lx-health خالی ماند (یعنی همه سالم بود). حالا زمان‌بندی واقعی: بکاپ هر شب ساعت ۲:۳۰، سلامت هر ۵ دقیقه. این بار به‌جای ویرایشگر، crontab را از ورودی استاندارد نصب می‌کنیم (crontab - جدول را با متن داده‌شده جایگزین می‌کند):

Terminal window
crontab -l | sed -e 's|^\* \* \* \* \* /usr/local/bin/lx-backup|30 2 * * * /usr/local/bin/lx-backup|' -e 's|^\* \* \* \* \* /usr/local/bin/lx-health|*/5 * * * * /usr/local/bin/lx-health|' | crontab -
crontab -l | grep -v '^#'
خروجی
MAILTO=""
30 2 * * * /usr/local/bin/lx-backup -k 7 /srv/shop
*/5 * * * * /usr/local/bin/lx-health -q -s "nginx cron" >> /var/log/lx-health.log 2>&1

30 2 * * * یعنی «دقیقه‌ی ۳۰ ساعت ۲، هر روز» و */5 * * * * یعنی «هر ۵ دقیقه» (درس cron). ابزارها نصب و زمان‌بندی شده‌اند.

پشت پرده: کار cron در چه محیطی اجرا می‌شود؟

Section titled “پشت پرده: کار cron در چه محیطی اجرا می‌شود؟”

دلیل اینکه همه‌جا مسیر مطلق نوشتیم این است که cron کار را با یک محیط خیلی کوچک اجرا می‌کند، نه محیط ترمینال تو. یک کار موقت می‌سازیم که محیطش را در فایل بریزد:

Terminal window
saved=$(crontab -l)
{ echo "$saved"; echo '* * * * * env | sort > /tmp/cron-env.txt'; } | crontab -
sleep 65
echo "--- محیط کار cron:"
cat /tmp/cron-env.txt
echo "--- PATH ترمینال root برای مقایسه:"
echo "$PATH"
echo "$saved" | crontab -
rm -f /tmp/cron-env.txt
خروجی
--- محیط کار cron:
HOME=/root
LANG=C.UTF-8
LOGNAME=root
MAILTO=
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
PWD=/root
SHELL=/bin/sh
--- PATH ترمینال root برای مقایسه:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

فقط چند متغیر: HOME و LOGNAME کاربر، MAILTO که خودمان گذاشتیم، SHELL=/bin/sh، پوشه‌ی جاری (PWD) همان خانه، و یک PATH. نه EDITOR، نه چیزهای ~/.bashrc. پس:

  • روی Ubuntu، cron متغیر PATH را از فایل /etc/environment می‌خواند (همان که اینجا می‌بینی و حتی /snap/bin دارد). ولی این قاعده‌ی همه‌جا نیست: روی سیستمی که آن فایل را ندارد یا تنظیمش فرق دارد، PATH در cron فقط /usr/bin:/bin است و دستورهای /usr/local/bin و /usr/sbin با اسم خالی پیدا نمی‌شوند. به آن تکیه نکن: مسیر کامل بنویس یا در اول crontab PATH=... بگذار.
  • خط crontab با /bin/sh اجرا می‌شود، نه bash؛ چیزهای مخصوص bash را داخل اسکریپت بنویس (که shebang خودش را دارد)، نه در خود خط crontab.
  • پوشه‌ی جاری HOME است؛ مسیر نسبی جای دیگری می‌رود.

و اینکه logger کجا می‌نویسد: logger پیام را به سوکت syslog سیستم (/dev/log) می‌دهد؛ روی این سرور rsyslog آن را در /var/log/syslog می‌نویسد و journald هم نگهش می‌دارد:

Terminal window
logger -t lx-demo -p user.warning "hello from logger"
sleep 1
grep lx-demo /var/log/syslog | tail -n 1 | sed -E 's/^[^ ]+ server //'
journalctl -t lx-demo --no-pager -o cat | tail -n 1
خروجی
lx-demo: hello from logger
hello from logger

ابزارهای پروژه:

ابزار گزینه‌ها کد خروج
lx-backup -d DEST، -k DAYS، -n (آزمایشی)، -v، -h ۰ موفق، ۱ خطا، ۲ استفاده‌ی غلط، ۳ مبدأ نیست، ۷۵ در حال اجراست
lx-health -w PCT، -c PCT، -m MOUNTS، -s SERVICES، -q، -h ۰ سالم، ۱ هشدار، ۲ بحرانی، ۳ استفاده‌ی غلط

دستورهای سیستمی که در پروژه به کار رفت:

دستور کار
find DIR -name 'x-*.tar.gz' -mtime +7 -print -delete پاک‌کردن فایل‌های قدیمی‌تر از ۷ روز (با چاپ اسم)
tar -czf A -C PARENT DIR / tar -tzf A ساخت آرشیو با مسیر نسبی / بررسی سالم‌بودن
mktemp -d -p DIR .tmp.XXXXXX پوشه‌ی موقت کنار مقصد (برای mv اتمی)
exec 9> LOCK; flock -n 9 قفل انحصاری بدون انتظار
logger -t TAG -p user.err -- MSG نوشتن در syslog
install -m 755 FILE /usr/local/bin/ نصب با مجوز اجرا
crontab -e / crontab -l / crontab - ویرایش / دیدن / جایگزینی از ورودی
df -P MOUNT، /proc/meminfo، /proc/loadavg، nproc دیسک، حافظه، بار، تعداد CPU
systemctl is-active SERVICE وضعیت سرویس (active، inactive، failed)

زمان‌بندی‌های پرکاربرد:

خط crontab کی
30 2 * * * هر روز ساعت ۲:۳۰
*/5 * * * * هر ۵ دقیقه
0 3 * * 0 یکشنبه‌ها ساعت ۳
0 */6 * * * هر ۶ ساعت

۱) اسم خالی دستور در crontab

Section titled “۱) اسم خالی دستور در crontab”

پشت پرده: PATH در cron به تنظیم سیستم بستگی دارد و روی خیلی از سیستم‌ها فقط /usr/bin:/bin است. آنجا lx-backup بدون مسیر command not found می‌دهد (و چون MAILTO خالی است، کسی هم نمی‌فهمد). راه‌حل: مسیر کامل، یا PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin اول crontab.

در خط crontab، % یعنی «خط جدید» و باید \% نوشته شود (درس cron). date +%F داخل خود crontab خراب می‌شود. راه‌حل: منطق را در اسکریپت بگذار؛ خط crontab فقط اسکریپت را با گزینه‌ها صدا بزند (همان کاری که اینجا کردیم).

۳) چرخشی که همه‌چیز را پاک می‌کند

Section titled “۳) چرخشی که همه‌چیز را پاک می‌کند”
Terminal window
mkdir -p /tmp/rot && touch -d "30 days ago" /tmp/rot/important.db
dest=""
echo "find \"\$dest/\" -mtime +7 -delete → find \"$dest/\" -mtime +7 -delete"
echo "(یعنی از ریشه‌ی سیستم!)"
rm -rf /tmp/rot
خروجی
find "$dest/" -mtime +7 -delete → find "/" -mtime +7 -delete
(یعنی از ریشه‌ی سیستم!)

اگر dest به هر دلیلی خالی باشد، "$dest/" می‌شود / و find / -mtime +7 -delete هر فایل قدیمی سیستم را پاک می‌کند. راه‌حل: set -u، اعتبارسنجی مقصد (مسیر مطلق و غیرخالی، مثل قدم ۱)، -maxdepth 1 و الگوی اسم دقیق (-name "$base-*.tar.gz")، و همیشه اول با -n آزمایش.

۴) بکاپی که هیچ‌وقت بازگردانی نشده

Section titled “۴) بکاپی که هیچ‌وقت بازگردانی نشده”

بکاپی که امتحان نشده، بکاپ نیست. tar -tzf فقط سالم‌بودن آرشیو را نشان می‌دهد. راه‌حل: هر چند وقت یک بار یک آرشیو را واقعاً در یک پوشه‌ی آزمایشی باز کن و محتوا را مقایسه کن (تمرین دوم).

اگر دیسک بسوزد، بکاپ‌های /var/backups هم با آن رفته‌اند. راه‌حل: یک نسخه را به جای دیگری بفرست (سرور دیگر با rsync یا scp، درس SSH و آرشیو در دوره‌ی لینوکس)؛ قانون معروف ۳-۲-۱: سه نسخه، روی دو نوع رسانه، یکی بیرون از محل.

۶) cron ای که بی‌صدا شکست می‌خورد

Section titled “۶) cron ای که بی‌صدا شکست می‌خورد”

خروجی کار cron اگر MAILTO خالی باشد و به فایل هم نرود، گم می‌شود. راه‌حل: ابزار خودش لاگ بنویسد (مثل lx-backup)، خطاها به syslog بروند، و یک اعلان واقعی (ایمیل یا LX_ALERT_CMD) داشته باشی.

✎ تمرینآسان

به lx-health یک بررسی دیگر اضافه کن: اگر جدیدترین فایل در پوشه‌ی بکاپ (/var/backups/lx) قدیمی‌تر از ۲۶ ساعت بود، CRIT بدهد («آخرین بکاپ خیلی قدیمی است»)؛ این همان چیزی است که نشان می‌دهد بکاپ شبانه اجرا نشده. با touch -d آزمایشش کن.

دیدن جواب
cd /root/project
cat > check-fresh.sh <<'EOF'
#!/usr/bin/env bash
# check-fresh.sh: the newest archive must be younger than MAX_HOURS
set -euo pipefail
dir=${1:-/var/backups/lx}
max_hours=${2:-26}
newest=$(find "$dir" -maxdepth 1 -type f -name '*.tar.gz' -printf '%T@ %p\n' | sort -n | tail -n 1)
if [[ -z $newest ]]; then
echo "CRIT no backups in $dir"
exit 2
fi
age_h=$(( ($(date +%s) - ${newest%%.*}) / 3600 ))
if (( age_h > max_hours )); then
echo "CRIT newest backup is ${age_h}h old: ${newest#* }"
exit 2
fi
echo "OK newest backup is ${age_h}h old: ${newest#* }"
EOF
chmod +x check-fresh.sh
shellcheck check-fresh.sh && echo "shellcheck: clean"
./check-fresh.sh; echo "کد خروج: $?"
mkdir -p /tmp/old-bk && touch -d "2 days ago" /tmp/old-bk/shop-x.tar.gz
./check-fresh.sh /tmp/old-bk; echo "کد خروج: $?"
./check-fresh.sh /tmp/empty-nothing 2>/dev/null || echo "کد خروج: $?"
rm -rf /tmp/old-bk
خروجی
shellcheck: clean
OK newest backup is 0h old: /var/backups/lx/shop-20261004-120401.tar.gz
کد خروج: 0
CRIT newest backup is 48h old: /tmp/old-bk/shop-x.tar.gz
کد خروج: 2
کد خروج: 1

find -printf '%T@ %p\n' زمان تغییر هر فایل را به ثانیه (از ۱۹۷۰) و مسیرش را می‌دهد؛ sort -n | tail -n 1 جدیدترین را برمی‌دارد. ${newest%%.*} قسمت صحیح ثانیه‌ها را جدا می‌کند و ${newest#* } مسیر را. در lx-health همین منطق یک بلوک دیگر با add_result 2 ... می‌شود. (روی پوشه‌ی ناموجود، find خطا داد و set -e اسکریپت را بست؛ بهتر است اول [[ -d $dir ]] را بسنجی.)

✎ تمرینمتوسط

آزمایش بازگردانی: اسکریپت lx-restore-test بنویس که جدیدترین آرشیو یک مبدأ را در یک پوشه‌ی موقت باز کند و با خودِ مبدأ مقایسه کند (diff -r)؛ اگر یکی بودند OK و کد ۰، وگرنه تفاوت‌ها و کد ۱. پوشه‌ی موقت در هر حالت پاک شود. یک بار بلافاصله بعد از بکاپ، و یک بار بعد از تغییر یک فایل در مبدأ اجرایش کن.

دیدن جواب
cd /root/project
cat > lx-restore-test <<'EOF'
#!/usr/bin/env bash
# lx-restore-test: restore the newest archive of SOURCE and compare it
# Usage: lx-restore-test SOURCE [BACKUP_DIR]
set -euo pipefail
src=${1:?usage: lx-restore-test SOURCE [BACKUP_DIR]}
src=${src%/}
dir=${2:-/var/backups/lx}
base=$(basename "$src")
newest=$(find "$dir" -maxdepth 1 -type f -name "$base-*.tar.gz" -printf '%T@ %p\n' | sort -n | tail -n 1)
newest=${newest#* }
[[ -n $newest ]] || { echo "no archive of $base in $dir" >&2; exit 2; }
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
tar -xzf "$newest" -C "$work"
if diff -r "$src" "$work/$base"; then
echo "OK: $(basename "$newest") matches $src"
else
echo "DIFFERENT: $(basename "$newest") does not match $src" >&2
exit 1
fi
EOF
chmod +x lx-restore-test
shellcheck lx-restore-test && echo "shellcheck: clean"
lx-backup /srv/shop
./lx-restore-test /srv/shop; echo "کد خروج: $?"
echo "--- مبدأ تغییر کرد:"
echo "<h1>Shop v2</h1>" > /srv/shop/site/index.html
./lx-restore-test /srv/shop; echo "کد خروج: $?"
خروجی
shellcheck: clean
OK: shop-20261004-120517.tar.gz matches /srv/shop
کد خروج: 0
--- مبدأ تغییر کرد:
diff -r /srv/shop/site/index.html /tmp/tmp.cLJmqOeW7Y/shop/site/index.html
1c1
< <h1>Shop v2</h1>
---
> <h1>Shop</h1>
DIFFERENT: shop-20261004-120517.tar.gz does not match /srv/shop
کد خروج: 1

diff -r دو درخت پوشه را فایل‌به‌فایل مقایسه می‌کند و اگر تفاوتی نبود کد ۰ می‌دهد. بار دوم تفاوت را دقیقاً نشان داد (این یعنی آرشیو مال قبل از تغییر است؛ طبیعی است). این اسکریپت را هفته‌ای یک بار با cron اجرا کن و خروجی‌اش را به اعلان وصل کن.

✎ تمرینسخت

چرخش با حداقل نسخه: چرخش فعلی اگر بکاپ‌گرفتن چند روز متوالی شکست بخورد، در نهایت همه‌ی آرشیوها را پاک می‌کند (همه قدیمی‌تر از ۷ روز می‌شوند). یک گزینه‌ی -m MIN به چرخش اضافه کن: صرف‌نظر از سن، همیشه دست‌کم MIN آرشیو جدیدتر باقی بمانند. منطقش را جدا (به‌صورت یک اسکریپت) بنویس و روی ۶ آرشیو که همه قدیمی‌اند، با MIN=3 آزمایش کن.

دیدن جواب
cd /root/project
cat > rotate-min.sh <<'EOF'
#!/usr/bin/env bash
# rotate-min.sh DIR BASE KEEP_DAYS MIN_COPIES
# delete BASE-*.tar.gz older than KEEP_DAYS, but always keep the MIN_COPIES newest
set -euo pipefail
dir=$1 base=$2 keep=$3 min=$4
mapfile -t all < <(find "$dir" -maxdepth 1 -type f -name "$base-*.tar.gz" -printf '%T@ %p\n' | sort -rn | cut -d' ' -f2-)
echo "found ${#all[@]} archive(s); protecting the newest $min"
for (( i = min; i < ${#all[@]}; i++ )); do
f=${all[i]}
if [[ -n $(find "$f" -mtime +"$keep") ]]; then
rm -f -- "$f"
echo "deleted $(basename "$f")"
fi
done
EOF
chmod +x rotate-min.sh
shellcheck rotate-min.sh && echo "shellcheck: clean"
mkdir -p /tmp/rot
for d in 10 11 12 13 14 15; do
touch -d "$d days ago" "/tmp/rot/shop-day$d.tar.gz"
done
./rotate-min.sh /tmp/rot shop 7 3
echo "--- مانده:"
ls /tmp/rot
rm -rf /tmp/rot
خروجی
shellcheck: clean
found 6 archive(s); protecting the newest 3
deleted shop-day13.tar.gz
deleted shop-day14.tar.gz
deleted shop-day15.tar.gz
--- مانده:
shop-day10.tar.gz
shop-day11.tar.gz
shop-day12.tar.gz

mapfile (درس آرایه‌ها) فهرست آرشیوها را از جدید به قدیم (sort -rn روی زمان) در آرایه می‌ریزد؛ حلقه از اندیس min شروع می‌شود، پس min آرشیو جدیدتر هرگز بررسی نمی‌شوند. برای بقیه، find "$f" -mtime +"$keep" اگر فایل قدیمی بود اسمش را چاپ می‌کند (غیرخالی). با اینکه هر ۶ آرشیو بالای ۷ روز بودند، سه جدیدترشان (۱۰، ۱۱ و ۱۲ روزه) ماندند.

⚡ بررسی سریع

چرا lx-backup آرشیو را اول در یک پوشه‌ی موقت کنار مقصد می‌سازد و بعد mv می‌کند؟

؟ آزمونک
  1. روی سروری در crontab نوشته‌ای * * * * * lx-health و هیچ اتفاقی نمی‌افتد؛ در ترمینال کار می‌کند. محتمل‌ترین علت؟

  2. flock -n 9 بعد از exec 9> /run/lock/x.lock چه می‌کند وقتی نسخه‌ی دیگری قفل را دارد؟

  3. lx-health با سرویس خاموش کد ۲ برمی‌گرداند. این عدد طبق کدام قرارداد است و چه فایده‌ای دارد؟

  4. find "$dest/" -mtime +7 -delete وقتی dest خالی باشد چه می‌کند؟

  5. چرا در lx-health نوشتیم if (( level > status )); then status=$level; fi به‌جای (( level > status )) && status=$level؟

  6. cron خروجی lx-health -q را برای root ایمیل می‌کند. چرا -q این کار را مفید می‌کند؟

  • ابزار خوب Bash یک رابط دارد (getopts، -h، اعتبارسنجی، کدهای خروج مستند) و یک هسته (کار اصلی) که با set -Eeuo pipefail، trap و لاگ محافظت می‌شود.
  • lx-backup: قفل flock، آرشیو در پوشه‌ی موقت کنار مقصد + tar -tzf + mv اتمی، چرخش با find "$dest" -maxdepth 1 -name "$base-*.tar.gz" -mtime +KEEP -delete، لاگ در فایل و logger، اعلان خطا با trap EXIT و قلاب LX_ALERT_CMD، و -n برای اجرای آزمایشی.
  • lx-health: دیسک (df -P)، حافظه (/proc/meminfo)، بار (/proc/loadavg و nproc)، سرویس‌ها (systemctl is-active)؛ کد ۰/۱/۲ و -q که فقط موقع مشکل حرف می‌زند.
  • نصب: install -m 755 ... /usr/local/bin/. زمان‌بندی: crontab -e (یا crontab -l | ... | crontab -)، با مسیر مطلق، MAILTO، و منطق داخل اسکریپت (نه داخل خط crontab).
  • cron محیط کوچکی دارد (/bin/sh، پوشه‌ی جاری خانه، PATH وابسته به سیستم)؛ ردپایش در grep CRON /var/log/syslog.
  • بکاپ را بازگردانی کن تا مطمئن شوی کار می‌کند، و یک نسخه را بیرون از سرور نگه دار.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
find "$dest" -maxdepth 1 -name "$base-*.tar.gz" -mtime +7 -print -deleteچرخش امن بکاپ‌های قدیمی
tar -czf "$tmp/$a" -C "$(dirname "$src")" "$(basename "$src")"آرشیو با مسیر نسبی
tar -tzf archive.tar.gz > /dev/nullبررسی سالم‌بودن آرشیو
tmp=$(mktemp -d -p "$dest" .tmp.XXXXXX)پوشه‌ی موقت کنار مقصد
exec 9> /run/lock/x.lock; flock -n 9 || exit 75یک نسخه در هر لحظه
logger -t lx-backup -p user.err -- "msg"نوشتن در syslog
install -m 755 tool /usr/local/bin/نصب ابزار
crontab -e crontab -l crontab -l | sed ... | crontab -ویرایش، دیدن و جایگزینی crontab
30 2 * * * /usr/local/bin/lx-backup /srv/shopهر شب ۲:۳۰
*/5 * * * * /usr/local/bin/lx-health -qهر ۵ دقیقه، فقط موقع مشکل خروجی
grep CRON /var/log/syslogآیا cron کار را اجرا کرد؟
df -P / systemctl is-active nginxدیسک و وضعیت سرویس