- LoopX
- اسکریپتنویسی Bash
- اسکریپت حرفهای
- پروژه: بکاپ خودکار و بررسی سلامت سرور
پروژه: بکاپ خودکار و بررسی سلامت سرور
توی این درس یاد میگیری همهی چیزهایی را که در این دوره خواندی، در دو ابزار واقعی کنار هم بگذاری و روی یک سرور نصب و زمانبندی کنی. اولی، lx-backup، از یک پوشه بکاپ فشرده میگیرد، بکاپهای قدیمی را میچرخاند (پاک میکند)، همهچیز را در فایل لاگ و syslog ثبت میکند، با قفل جلوی اجرای همزمان را میگیرد و اگر چیزی خراب شد فریاد میزند. دومی، lx-health، فضای دیسک، حافظه، بار CPU و وضعیت سرویسها را بررسی میکند و با کد خروج استاندارد (۰ سالم، ۱ هشدار، ۲ بحرانی) گزارش میدهد. آخر کار، هر دو را در /usr/local/bin نصب و با crontab -e زمانبندی میکنی و میبینی cron واقعاً اجرایشان میکند.
مسئله: سروری که کسی نگاهش نمیکند
Section titled “مسئله: سروری که کسی نگاهش نمیکند”سرور کوچک یک کسبوکار را تصور کن: یک سایت، یک دیتابیس، چند فایل آپلودی. هیچکس هر روز واردش نمیشود. دو سؤال همیشه بیجواب میماند: «اگر امشب دیسک بسوزد، بکاپ دیروز را داریم؟» و «اگر دیسک دارد پر میشود یا سرویسی خوابیده، کی میفهمیم؟». جواب حرفهای این است که سرور خودش هر شب بکاپ بگیرد، بکاپهای قدیمی را پاک کند تا دیسک پر نشود، و هر چند دقیقه سلامتش را بررسی کند و فقط وقتی مشکلی هست صدا کند.
تشبیه: نگهبان شب و دفتر گزارش
Section titled “تشبیه: نگهبان شب و دفتر گزارش”lx-backup مثل نگهبان شب است که هر شب سر ساعت از گاوصندوق یک نسخه برمیدارد، روی قفسه میگذارد، نسخههای خیلی قدیمی را دور میریزد و همه را در دفتر گزارش (لاگ) مینویسد؛ اگر کلید گاوصندوق کار نکرد، زنگ خطر را میزند، نه اینکه بیصدا برود خانه. lx-health مثل سرکشی دورهای است: هر چند دقیقه یک دور میزند، چراغها را نگاه میکند و فقط وقتی چیزی غیرعادی دید، گزارش میدهد. cron هم ساعت دیواریای است که به هر دو میگوید کی کارشان را شروع کنند.
نقشهی پروژه
Section titled “نقشهی پروژه”مثالهای عملی (قدمبهقدم)
Section titled “مثالهای عملی (قدمبهقدم)”این پروژه روی ماشین آزمایشی server اجرا شده که systemd، cron، rsyslog و Nginx واقعی دارد (همان ماشین درسهای cron و سرویسها در دورهی لینوکس). همهی دستورها با کاربر root است، چون ابزارها در /usr/local/bin نصب میشوند و بکاپ از پوشههای سیستمی گرفته میشود. روی سرور خودت جلوی دستورها sudo بگذار.
قدم ۰: دادهی نمونه و وضع سرور
Section titled “قدم ۰: دادهی نمونه و وضع سرور”یک «فروشگاه» نمونه در /srv/shop میسازیم (فایلهای سایت، آپلودها، یک dump دیتابیس) و Nginx را روشن میکنیم تا بعداً سلامتش را بسنجیم. (ماشین آزمایشی ما در هستهاش IPv6 ندارد، پس خط listen [::]:80 سایت پیشفرض Nginx را در آن غیرفعال کردهایم؛ روی سرور واقعی لازم نیست.)
mkdir -p /srv/shop/{site,uploads,db} /root/projectecho "<h1>Shop</h1>" > /srv/shop/site/index.htmlhead -c 200000 /dev/urandom > /srv/shop/uploads/photo-1.jpghead -c 150000 /dev/urandom > /srv/shop/uploads/photo-2.jpgprintf 'CREATE TABLE orders (id int);\nINSERT INTO orders VALUES (1),(2),(3);\n' > /srv/shop/db/shop.sqlsystemctl start nginxsystemctl is-active nginx cron rsyslogdu -sh /srv/shopbash --version | head -1activeactiveactive368K /srv/shopGNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)قدم ۱: اسکلت lx-backup
Section titled “قدم ۱: اسکلت lx-backup”اول رابط ابزار را میسازیم: راهنما، گزینهها، اعتبارسنجی و کدهای خروج (درس getopts)، و تنظیمات سختگیرانه (درس مدیریت خطا). هنوز هیچ بکاپی گرفته نمیشود:
#!/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 runningset -Eeuo pipefailshopt -s inherit_errexit
readonly PROG=lx-backupreadonly E_USAGE=2 E_NOSRC=3 E_LOCKED=75LOG_FILE=${LX_BACKUP_LOG:-/var/log/lx-backup.log}LOCK_FILE=${LX_BACKUP_LOCK:-/run/lock/lx-backup.lock}
dest=/var/backups/lxkeep=7verbose=falsedry_run=false
usage() { cat <<EOFUsage: $PROG [-h] [-v] [-n] [-d DEST] [-k DAYS] SOURCE
Create a compressed archive of SOURCE in DEST and delete archivesof 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_FILEExit codes: 0 ok, 1 error, $E_USAGE usage error, $E_NOSRC source not found, $E_LOCKED another instance is runningEOF}
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" ;; esacdoneshift $((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"cd /root/projectchmod +x lx-backupshellcheck lx-backup && echo "shellcheck: clean"./lx-backup -hecho "==="./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: cleanUsage: lx-backup [-h] [-v] [-n] [-d DEST] [-k DAYS] SOURCE
Create a compressed archive of SOURCE in DEST and delete archivesof 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.lockExit codes: 0 ok, 1 error, 2 usage error, 3 source not found, 75 another instance is running===lx-backup: expected exactly one SOURCEtry 'lx-backup -h'کد خروج: 2lx-backup: -k must be 1-365, got '0'try 'lx-backup -h'کد خروج: 2lx-backup: -d must be an absolute path, got 'backups'try 'lx-backup -h'کد خروج: 2lx-backup: source '/srv/nothing' is not a directoryکد خروج: 3OK 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...") میگذاریم:
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 EXITtrap 'log ERROR "line $LINENO: \"$BASH_COMMAND\" failed"' ERR
log INFO "start: $src -> $dest (keep $keep days, dry run: $dry_run)"cd /root/projectsed -i '/^echo "OK so far/d' lx-backupcat part-log.sh >> lx-backupshellcheck lx-backup && echo "shellcheck: clean"./lx-backup -v /srv/shopecho "--- فایل لاگ:"cat /var/log/lx-backup.logshellcheck: clean2026-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فایلهای موقت را پاک میکند و اگر کد خروج غیرصفر بود (جز «در حال اجراست»)، اعلان میدهد.
قدم ۳: قفل با flock
Section titled “قدم ۳: قفل با flock”اگر بکاپ شب گذشته هنوز تمام نشده و cron نسخهی بعدی را شروع کند، دو tar همزمان روی یک دیسک کار میکنند و هر دو کند یا خراب میشوند. flock (از util-linux) یک قفل روی یک فایل میگیرد که سیستمعامل نگهش میدارد؛ حتی اگر اسکریپت با kill -9 بمیرد، قفل خودکار آزاد میشود (برخلاف فایل قفل دستی که در درس مدیریت خطا ساختیم):
# one instance at a time: fd 9 stays open (and locked) until the script exitsexec 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"ficd /root/projectcat part-lock.sh >> lx-backupshellcheck 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 "کد خروج: $?"waitecho "--- بعد از آزاد شدن قفل:"./lx-backup /srv/shop; echo "کد خروج: $?"tail -n 3 /var/log/lx-backup.logshellcheck: clean--- یک نسخه قفل را ۳ ثانیه نگه میدارد، نسخهی دوم:lx-backup: another instance is runningکد خروج: 75--- بعد از آزاد شدن قفل:کد خروج: 02026-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; exiting2026-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 فقط میگوییم چه کار میکردیم:
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)"ficd /root/projectcat part-archive.sh >> lx-backupshellcheck lx-backup && echo "shellcheck: clean"./lx-backup -n -v /srv/shopls /var/backups/lx 2>/dev/null | wc -l./lx-backup -v /srv/shopls -l /var/backups/lx | sed 1d | awk '{print $5, $9}'tar -tzf /var/backups/lx/shop-*.tar.gz | head -n 5shellcheck: clean2026-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.gz02026-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.gzshop/shop/uploads/shop/uploads/photo-1.jpgshop/uploads/photo-2.jpgshop/db/با -n هیچ فایلی ساخته نشد. اجرای واقعی آرشیو را ساخت و فهرست داخلش نشان میدهد پوشه با مسیر نسبی (shop/...) ذخیره شده؛ موقع بازگردانی، هر جا خواستی بازش میکنی و مسیر مطلق سرور قبلی را لازم نداری. اسم پوشهی موقت با نقطه شروع میشود (.tmp.XXXXXX) تا در ls معمولی دیده نشود و الگوی چرخش آن را نگیرد.
قدم ۵: چرخش بکاپهای قدیمی
Section titled “قدم ۵: چرخش بکاپهای قدیمی”بدون پاککردن، دیسک بالاخره پر میشود. سادهترین شکل چرخش یک خط find است:
find /backups -mtime +7 -delete-mtime +7 یعنی «آخرین تغییر بیش از ۷ دورهی ۲۴ساعته پیش» و -delete پاکشان میکند. در ابزار، آن را محدودتر مینویسیم: فقط در خود مقصد (-maxdepth 1)، فقط فایل، و فقط آرشیوهای همین مبدأ (تا بکاپ مبدأ دیگری که در همین پوشه است پاک نشود)؛ و -print تا اسم هر فایل پاکشده در لاگ بیاید:
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"filog INFO "done"برای آزمایش، چند آرشیو «قدیمی» با touch -d میسازیم (تاریخ تغییرشان را عقب میبریم) و یک فایل از مبدأ دیگر:
cd /root/projectcat part-rotate.sh >> lx-backupshellcheck 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"donetouch -d "30 days ago" /var/backups/lx/blog-old.tar.gzecho "=== قبل:"ls /var/backups/lxecho "=== اجرای آزمایشی (-n):"./lx-backup -n -v /srv/shopecho "=== اجرای واقعی:"./lx-backup -v /srv/shopecho "=== بعد:"ls /var/backups/lxshellcheck: clean=== قبل:blog-old.tar.gzshop-20261004-120155.tar.gzshop-old-10-days.tar.gzshop-old-3-days.tar.gzshop-old-30-days.tar.gzshop-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.gz2026-10-04 12:01:56 [INFO] dry run: would delete /var/backups/lx/shop-old-30-days.tar.gz2026-10-04 12:01:56 [INFO] dry run: would delete /var/backups/lx/shop-old-10-days.tar.gz2026-10-04 12:01:56 [INFO] dry run: would delete /var/backups/lx/shop-old-8-days.tar.gz2026-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.gz2026-10-04 12:01:56 [INFO] deleted /var/backups/lx/shop-old-10-days.tar.gz2026-10-04 12:01:56 [INFO] deleted /var/backups/lx/shop-old-8-days.tar.gz2026-10-04 12:01:56 [INFO] rotation: deleted 3 archive(s) older than 7 days2026-10-04 12:01:56 [INFO] done=== بعد:blog-old.tar.gzshop-20261004-120155.tar.gzshop-20261004-120156.tar.gzshop-old-3-days.tar.gzاجرای آزمایشی سه فایل قدیمیتر از ۷ روز را نشان داد و چیزی پاک نکرد؛ اجرای واقعی همان سه را پاک کرد. آرشیو ۳ روزه ماند، و blog-old.tar.gz با اینکه ۳۰ روزه است ماند، چون مال مبدأ دیگری است. خروجی find با < <(...) (جایگزینی پروسه) به حلقه رسید تا شمارندهی deleted در همان شل بماند (درس حلقهها: cmd | while در پروسهی جدا اجرا میشود).
قدم ۶: آزمایش شکست
Section titled “قدم ۶: آزمایش شکست”ابزار پشتیبان را باید در حال شکست هم دید. یک فایل غیرقابلخواندن در مبدأ میگذاریم (در دنیای واقعی: دیسک خراب، مجوز اشتباه، فایلی که وسط کار پاک شد) و ابزار را با یک «اعلان» آزمایشی اجرا میکنیم که پیام را در یک فایل مینویسد:
cd /root/projectmkdir -p /srv/broken && echo "x" > /srv/broken/data.txtmkdir /srv/broken/locked && chmod 000 /srv/broken/lockeduseradd -M -s /usr/sbin/nologin backupuser 2>/dev/nullchown -R backupuser /var/backups/lxcat > /root/project/fake-alert <<'EOF'#!/usr/bin/env bash# fake-alert: stands in for an email or chat notificationecho "ALERT RECEIVED: $(cat)" >> /tmp/alerts.txtEOFchmod +x /root/project/fake-alertrm -f /tmp/alerts.txtrunuser -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/brokenecho "کد خروج: $?"echo "--- اعلان دریافتشده:"cat /tmp/alerts.txtecho "--- syslog:"grep 'lx-backup' /var/log/syslog | tail -n 3 | sed -E 's/^[^ ]+ server //'echo "--- پوشهی موقت مانده؟"find /var/backups/lx -name '.tmp.*' | wc -lchown -R root /var/backups/lxrm -rf /srv/broken /tmp/lx-test.* /tmp/alerts.txttar: broken/locked: Cannot open: Permission deniedtar: Exiting with failure status due to previous errors2026-10-04 12:01:56 [ERROR] line 120: "tar -czf "$tmpdir/$archive" -C "$(dirname "$src")" "$base"" failed2026-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"" failedlx-backup: backup of /srv/broken FAILED with status 2--- پوشهی موقت مانده؟0root همهچیز را میتواند بخواند، پس برای شکست واقعی، ابزار را با یک کاربر معمولی (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 هر خروجی را برای صاحب کار ایمیل میکند.
#!/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 errorset -euo pipefail
readonly PROG=lx-healthwarn=80crit=90mounts="/"services="nginx cron"quiet=false
usage() { cat <<EOFUsage: $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 errorEOF}
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 ;; esacdonefor 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=0lines=()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 pointfor 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 - availablemem_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 CPUsread -r load1 _ < /proc/loadavgcpus=$(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) servicesfor 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}" fidone
summary="HEALTH ${names[$status]} on $(hostname) at $(date '+%F %T')"if (( status > 0 )); then logger -t "$PROG" -p user.warning -- "$summary"fiif [[ $quiet == false || $status -gt 0 ]]; then echo "$summary" printf ' %s\n' "${lines[@]}"fiexit "$status"cd /root/projectchmod +x lx-healthshellcheck lx-health && echo "shellcheck: clean"./lx-health; echo "کد خروج: $?"echo "=== -q وقتی همهچیز سالم است (هیچ خروجی):"./lx-health -q; echo "کد خروج: $?"shellcheck: cleanHEALTH 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 را خاموش میکنیم:
cd /root/projectecho "=== آستانهی پایین برای دیسک و حافظه:"./lx-health -w 3 -c 50; echo "کد خروج: $?"echo "=== Nginx خاموش:"systemctl stop nginx./lx-health -q; echo "کد خروج: $?"systemctl start nginxecho "=== 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:56lx-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 جای استاندارد برنامههایی است که خودت (نه مدیر بسته) نصب میکنی:
cd /root/projectinstall -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 باز میکنیم (داخل یک ترمینال مجازی):
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.logrun_in_tmux "bash --norc"tmux send-keys -t t "export EDITOR=nano; crontab -e" Enter; sleep 2tmux send-keys -t t M-/; sleep 1tmux send-keys -t t -l 'MAILTO=""'; tmux send-keys -t t Entertmux send-keys -t t -l '* * * * * /usr/local/bin/lx-backup -k 7 /srv/shop'; tmux send-keys -t t Entertmux 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 Entersleep 1screen | tail -n 8tmux send-keys -t t C-o; sleep 1; tmux send-keys -t t Enter; sleep 1; tmux send-keys -t t C-x; sleep 2tmux send-keys -t t "crontab -l | grep -v '^#'" Enter; sleep 1screen | tail -n 5tmux -u kill-server# m h dom mon dow commandMAILTO=""* * * * * /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 RedoMAILTO=""* * * * * /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/...)؛ بهPATHcron تکیه نمیکنیم (پشت پرده را ببین). - خروجی
lx-healthرا با>> فایل 2>&1در لاگ ریختیم.
حالا دو دقیقه صبر میکنیم و ردپای cron را میگردیم:
sleep 125echo "--- لاگ lx-backup (نوشتهشده توسط اجرای cron):"grep -E 'start|created|rotation' /var/log/lx-backup.log | tail -n 3echo "--- 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 (خالی یعنی همه سالم بود):0cron هر دو را اجرا کرد: آرشیوهای تازه ساخته شد، syslog خط CMD برای هر کار دارد، و لاگ lx-health خالی ماند (یعنی همه سالم بود). حالا زمانبندی واقعی: بکاپ هر شب ساعت ۲:۳۰، سلامت هر ۵ دقیقه. این بار بهجای ویرایشگر، crontab را از ورودی استاندارد نصب میکنیم (crontab - جدول را با متن دادهشده جایگزین میکند):
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>&130 2 * * * یعنی «دقیقهی ۳۰ ساعت ۲، هر روز» و */5 * * * * یعنی «هر ۵ دقیقه» (درس cron). ابزارها نصب و زمانبندی شدهاند.
پشت پرده: کار cron در چه محیطی اجرا میشود؟
Section titled “پشت پرده: کار cron در چه محیطی اجرا میشود؟”دلیل اینکه همهجا مسیر مطلق نوشتیم این است که cron کار را با یک محیط خیلی کوچک اجرا میکند، نه محیط ترمینال تو. یک کار موقت میسازیم که محیطش را در فایل بریزد:
saved=$(crontab -l){ echo "$saved"; echo '* * * * * env | sort > /tmp/cron-env.txt'; } | crontab -sleep 65echo "--- محیط کار cron:"cat /tmp/cron-env.txtecho "--- PATH ترمینال root برای مقایسه:"echo "$PATH"echo "$saved" | crontab -rm -f /tmp/cron-env.txt--- محیط کار cron:HOME=/rootLANG=C.UTF-8LOGNAME=rootMAILTO=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/binPWD=/rootSHELL=/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با اسم خالی پیدا نمیشوند. به آن تکیه نکن: مسیر کامل بنویس یا در اول crontabPATH=...بگذار. - خط crontab با
/bin/shاجرا میشود، نه bash؛ چیزهای مخصوص bash را داخل اسکریپت بنویس (که shebang خودش را دارد)، نه در خود خط crontab. - پوشهی جاری
HOMEاست؛ مسیر نسبی جای دیگری میرود.
و اینکه logger کجا مینویسد: logger پیام را به سوکت syslog سیستم (/dev/log) میدهد؛ روی این سرور rsyslog آن را در /var/log/syslog مینویسد و journald هم نگهش میدارد:
logger -t lx-demo -p user.warning "hello from logger"sleep 1grep lx-demo /var/log/syslog | tail -n 1 | sed -E 's/^[^ ]+ server //'journalctl -t lx-demo --no-pager -o cat | tail -n 1lx-demo: hello from loggerhello from loggerجدولهای مرجع
Section titled “جدولهای مرجع”ابزارهای پروژه:
| ابزار | گزینهها | کد خروج |
|---|---|---|
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 * * * |
هر ۶ ساعت |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) اسم خالی دستور در 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
Section titled “۲) % در خط crontab”در خط crontab، % یعنی «خط جدید» و باید \% نوشته شود (درس cron). date +%F داخل خود crontab خراب میشود. راهحل: منطق را در اسکریپت بگذار؛ خط crontab فقط اسکریپت را با گزینهها صدا بزند (همان کاری که اینجا کردیم).
۳) چرخشی که همهچیز را پاک میکند
Section titled “۳) چرخشی که همهچیز را پاک میکند”mkdir -p /tmp/rot && touch -d "30 days ago" /tmp/rot/important.dbdest=""echo "find \"\$dest/\" -mtime +7 -delete → find \"$dest/\" -mtime +7 -delete"echo "(یعنی از ریشهی سیستم!)"rm -rf /tmp/rotfind "$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 فقط سالمبودن آرشیو را نشان میدهد. راهحل: هر چند وقت یک بار یک آرشیو را واقعاً در یک پوشهی آزمایشی باز کن و محتوا را مقایسه کن (تمرین دوم).
۵) بکاپ روی همان دیسک
Section titled “۵) بکاپ روی همان دیسک”اگر دیسک بسوزد، بکاپهای /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/projectcat > check-fresh.sh <<'EOF'#!/usr/bin/env bash# check-fresh.sh: the newest archive must be younger than MAX_HOURSset -euo pipefaildir=${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 2fiage_h=$(( ($(date +%s) - ${newest%%.*}) / 3600 ))if (( age_h > max_hours )); then echo "CRIT newest backup is ${age_h}h old: ${newest#* }" exit 2fiecho "OK newest backup is ${age_h}h old: ${newest#* }"EOFchmod +x check-fresh.shshellcheck 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-bkshellcheck: cleanOK newest backup is 0h old: /var/backups/lx/shop-20261004-120401.tar.gzکد خروج: 0CRIT newest backup is 48h old: /tmp/old-bk/shop-x.tar.gzکد خروج: 2کد خروج: 1find -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/projectcat > 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 pipefailsrc=${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"' EXITtar -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 1fiEOFchmod +x lx-restore-testshellcheck 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: cleanOK: shop-20261004-120517.tar.gz matches /srv/shopکد خروج: 0--- مبدأ تغییر کرد:diff -r /srv/shop/site/index.html /tmp/tmp.cLJmqOeW7Y/shop/site/index.html1c1< <h1>Shop v2</h1>---> <h1>Shop</h1>DIFFERENT: shop-20261004-120517.tar.gz does not match /srv/shopکد خروج: 1diff -r دو درخت پوشه را فایلبهفایل مقایسه میکند و اگر تفاوتی نبود کد ۰ میدهد. بار دوم تفاوت را دقیقاً نشان داد (این یعنی آرشیو مال قبل از تغییر است؛ طبیعی است). این اسکریپت را هفتهای یک بار با cron اجرا کن و خروجیاش را به اعلان وصل کن.
چرخش با حداقل نسخه: چرخش فعلی اگر بکاپگرفتن چند روز متوالی شکست بخورد، در نهایت همهی آرشیوها را پاک میکند (همه قدیمیتر از ۷ روز میشوند). یک گزینهی -m MIN به چرخش اضافه کن: صرفنظر از سن، همیشه دستکم MIN آرشیو جدیدتر باقی بمانند. منطقش را جدا (بهصورت یک اسکریپت) بنویس و روی ۶ آرشیو که همه قدیمیاند، با MIN=3 آزمایش کن.
دیدن جواب
cd /root/projectcat > 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 newestset -euo pipefaildir=$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")" fidoneEOFchmod +x rotate-min.shshellcheck rotate-min.sh && echo "shellcheck: clean"mkdir -p /tmp/rotfor 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 3echo "--- مانده:"ls /tmp/rotrm -rf /tmp/rotshellcheck: cleanfound 6 archive(s); protecting the newest 3deleted shop-day13.tar.gzdeleted shop-day14.tar.gzdeleted shop-day15.tar.gz--- مانده:shop-day10.tar.gzshop-day11.tar.gzshop-day12.tar.gzmapfile (درس آرایهها) فهرست آرشیوها را از جدید به قدیم (sort -rn روی زمان) در آرایه میریزد؛ حلقه از اندیس min شروع میشود، پس min آرشیو جدیدتر هرگز بررسی نمیشوند. برای بقیه، find "$f" -mtime +"$keep" اگر فایل قدیمی بود اسمش را چاپ میکند (غیرخالی). با اینکه هر ۶ آرشیو بالای ۷ روز بودند، سه جدیدترشان (۱۰، ۱۱ و ۱۲ روزه) ماندند.
آزمونک
Section titled “آزمونک”چرا lx-backup آرشیو را اول در یک پوشهی موقت کنار مقصد میسازد و بعد mv میکند؟
اگر tar وسط کار شکست بخورد، فقط پوشهی موقت (که trap پاکش میکند) آسیب میبیند.
روی سروری در crontab نوشتهای * * * * * lx-health و هیچ اتفاقی نمیافتد؛ در ترمینال کار میکند. محتملترین علت؟
به PATH در cron تکیه نکن: مسیر کامل بنویس یا PATH را اول جدول تعریف کن.
flock -n 9 بعد از exec 9> /run/lock/x.lock چه میکند وقتی نسخهی دیگری قفل را دارد؟
-n یعنی بدون انتظار (non-blocking). قفل flock با بستهشدن fd، حتی با kill -9، آزاد میشود.
lx-health با سرویس خاموش کد ۲ برمیگرداند. این عدد طبق کدام قرارداد است و چه فایدهای دارد؟
همین اسکریپت را میشود بدون تغییر به یک سیستم مانیتورینگ وصل کرد.
find "$dest/" -mtime +7 -delete وقتی dest خالی باشد چه میکند؟
set -u، اعتبارسنجی مسیر، -maxdepth 1 و الگوی اسم دقیق جلوی این فاجعه را میگیرند.
چرا در lx-health نوشتیم if (( level > status )); then status=$level; fi بهجای (( level > status )) && status=$level؟
آخرین دستور تابع، کد خروج تابع است.
cron خروجی lx-health -q را برای root ایمیل میکند. چرا -q این کار را مفید میکند؟
خروجی خالی یعنی cron ایمیلی نمیفرستد.
جمعبندی
Section titled “جمعبندی”- ابزار خوب 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 | دیسک و وضعیت سرویس |