خودکارسازی قابل اتکا با Bash
در این درس ۴ بخش
پیشنیاز این درس، خط فرمان، مجوزها، متغیرها و اسکریپت سادهٔ دورهٔ مقدماتی است. اینجا هدف ساختن فرمان طولانیتر نیست؛ میخواهیم عملیاتی بسازیم که هنگام شکست، نتیجهٔ قبلی را سالم نگه دارد و وضعیت قابل تشخیص برگرداند. نمونهٔ ما چند سطر دادهٔ ساختگی را مرتب و بهعنوان خروجی منتشر میکند. همین الگو برای تولید گزارش یا فایل پیکربندی کاربرد دارد. تمرین به Bash، ابزارهای GNU و flock نیاز دارد و با حساب معمولی اجرا میشود. هیچ مسیر سیستمی، شبکه یا دادهٔ شخصی خوانده نمیشود. پوشهٔ تصادفی زیر خانه، مرز تمام ورودیها و خروجیهاست؛ فرمانها را در همان ترمینال ادامه بده.
قرارداد شکست را پیش از کد بنویس
قرارداد این برنامه چنین است: خروج موفق صفر است؛ ورودی نامعتبر ۶۶، خروجی خالی ۶۵، اجرای همزمان ۷۵ و خطای آزمایشی ۷۰ برمیگرداند. این عددها قرارداد محلی تمریناند، نه معنای عمومی همهٔ برنامهها. پیام تشخیصی به stderr میرود و دادهٔ ورودی در پیامها چاپ نمیشود. از set -u برای متغیر تعریفنشده و pipefail برای وضعیت خط لوله استفاده میکنیم، ولی set -e را تضمین شکار تمام خطاها نمیدانیم. Bash در شرطها، بعضی فهرستهای منطقی و موقعیتهای دیگر استثنا دارد. بنابراین نتیجهٔ مراحل حساس را آشکارا بررسی میکنیم و رفتار مورد انتظار را در آزمون میسنجیم.
umask 077
LAB13=$(mktemp -d "$HOME/linux2-auto.XXXXXX") || exit 1
export LAB13
printf '%s\n' beta alpha > "$LAB13/input.txt"
cat > "$LAB13/publish.sh" <<'SCRIPT'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
: "${LAB13:?Export LAB13 before running}"
case "$LAB13" in
"$HOME"/linux2-auto.*) ;;
*) printf 'unexpected workspace\n' >&2; exit 64 ;;
esac
[[ -d "$LAB13" && ! -L "$LAB13" ]] || exit 64
src=$LAB13/input.txt
dest=$LAB13/status.txt
tmp=
cleanup() {
rc=$?
trap - EXIT
if [[ -n "$tmp" ]]; then rm -f -- "$tmp"; fi
exit "$rc"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
exec 9>"$LAB13/job.lock" || exit 73
flock -n 9 || { printf 'already running\n' >&2; exit 75; }
[[ -f "$src" && -r "$src" ]] || exit 66
tmp=$(mktemp "$LAB13/.status.XXXXXX") || exit 73
if ! LC_ALL=C sort -- "$src" > "$tmp"; then
printf 'generation failed\n' >&2
exit 74
fi
[[ -s "$tmp" ]] || exit 65
[[ "${FAIL_STAGE:-}" != before_publish ]] || exit 70
mv -fT -- "$tmp" "$dest" || exit 74
tmp=
printf 'published\n'
SCRIPT
bash -n "$LAB13/publish.sh"
bash "$LAB13/publish.sh"
cat "$LAB13/status.txt"خروجی نخست باید alpha و سپس beta باشد. قفل روی توصیفگر ۹ باز میماند و با پایان فرایند آزاد میشود؛ فایل قفل را هنگام اجرا حذف نکن، چون دو نام متفاوت برای inodeها میتواند هماهنگی را خراب کند. flock قفل مشارکتی است: همهٔ نویسندگان باید همین قرارداد را رعایت کنند و رفتار فایلسیستم شبکهای نیز باید جداگانه بررسی شود. trap روی EXIT فایل موقت باقیمانده را پاک میکند و کد خروج اصلی را نگه میدارد. برای INT و TERM خروج مشخص داریم، اما SIGKILL یا قطع برق فرصت اجرای cleanup نمیدهند؛ پس پاکسازی بهتنهایی راهبرد بازیابی کامل نیست.
انتشار اتمی و آزمون شکست
فایل موقت داخل همان پوشهٔ مقصد ساخته میشود؛ بنابراین روی همان فایلسیستم است. mv در این حالت برای فایل معمولی نام را جایگزین میکند و خواننده نسخهٔ قبلی یا نسخهٔ کامل جدید را میبیند. اگر موقت را در /tmp بسازی، جابهجایی میان فایلسیستمها ممکن است به کپی و حذف تبدیل شود و این ویژگی از بین برود. گزینهٔ GNU یعنی -T مانع تعبیر مقصد بهعنوان پوشه میشود. این اتمی بودن دربارهٔ مشاهدهٔ نام فایل است؛ تضمین ماندگاری پس از قطع برق، تراکنش چندفایلی یا اعتبار محتوای خروجی نیست. ابتدا تولید، سپس اعتبارسنجی، و در پایان انتشار انجام میشود.
cp -- "$LAB13/status.txt" "$LAB13/expected.txt"
if FAIL_STAGE=before_publish bash "$LAB13/publish.sh"; then
printf 'unexpected success\n'; exit 1
else
test "$?" -eq 70 || exit 1
fi
cmp -- "$LAB13/expected.txt" "$LAB13/status.txt" || exit 1
if compgen -G "$LAB13/.status.*" > /dev/null; then exit 1; fi
exec 8>"$LAB13/job.lock"
flock -n 8 || exit 1
if bash "$LAB13/publish.sh"; then exit 1; else
test "$?" -eq 75 || exit 1
fi
flock -u 8
exec 8>&-
mv -- "$LAB13/input.txt" "$LAB13/input.saved"
if bash "$LAB13/publish.sh"; then exit 1; else
test "$?" -eq 66 || exit 1
fi
: > "$LAB13/input.txt"
if bash "$LAB13/publish.sh"; then exit 1; else
test "$?" -eq 65 || exit 1
fi
mv -f -- "$LAB13/input.saved" "$LAB13/input.txt"
cmp -- "$LAB13/expected.txt" "$LAB13/status.txt" || exit 1
printf 'failure-path tests passed\n'آزمون بالا چهار حالت را بررسی میکند: شکست درست پیش از انتشار، رقابت برای قفل، ورودی مفقود و ورودی خالی. در همهٔ آنها فایل منتشرشده باید با نسخهٔ ذخیرهشده برابر بماند. عمداً مسیر شکست را مانند موفقیت بررسی میکنیم؛ صرف دیدن یک اجرای سبز کافی نیست. برای آزمودن خطای نوشتن، در ماشین مجازی میتوان شرایط فایلسیستم را کنترل کرد، اما پرکردن دیسک واقعی یا تغییر مجوز مسیرهای شخصی تمرین مناسبی نیست. این نمونه نیز خواندن همزمان ورودیِ در حال تغییر را هماهنگ نمیکند؛ تولیدکنندهٔ ورودی در سامانهٔ واقعی به قرارداد جداگانه نیاز دارد.
زمانبند را از منطق کار جدا کن
پس از اجرای دستی و آزمونها، میتوان دربارهٔ زمانبندی تصمیم گرفت. cron برای زمانبندی ساده مناسب است، ولی محیط اجرا، PATH، مسیر کاری و مقصد پیام خطا را باید صریح تعریف کرد. در سامانهٔ دارای systemd، یک timer همراه service وضعیت اجرا و ثبت رویداد منسجمتری دارد؛ Persistent برای timer تقویمی میتواند اجرای ازدسترفته را جبران کند. زمانبند کیفیت اسکریپت را تضمین نمیکند و جای کنترل شکست یا قفل را نمیگیرد. در این درس هیچ زمانبندی نصب نمیکنیم؛ ابتدا اجرای قابل پیشبینی میسازیم. درس بعد مدیریت وضعیت مطلوب با Ansible را به این تصویر اضافه میکند.
تمرین و پاسخ
تمرین: چرا چاپ مستقیم نتیجه با > روی status.txt قرارداد ما را میشکند؟ اگر دو اجرا همزمان شروع شوند، کدام تضمین قفل وابسته به همکاری برنامههاست؟ پاسخ: بازکردن مقصد با > همان ابتدا آن را کوتاه میکند؛ شکست میان تولید، نسخهٔ سالم را نابود میکند. قفل فقط اجرای برنامههایی را هماهنگ میکند که همان فایل قفل را قفل میکنند. تحویل درس، اسکریپت، کد خروج چهار شکست و مدرک مقایسهٔ خروجی قبل و بعد است. یک آزمون اضافی طراحی کن که خروجی مورد انتظار را مستقل از خود اسکریپت تعیین کند؛ برای مثال، فایل دوخطی ثابت را با cmp مقایسه کن.
منابع رسمی انگلیسی:
Bash Reference Manual — The Set Builtin: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
Bash Reference Manual — Bourne Shell Builtins: https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
util-linux — flock: https://kernel.googlesource.com/pub/scm/utils/util-linux/util-linux/+/refs/tags/v2.43-devel/sys-utils/flock.1.adoc
GNU Coreutils — mktemp: https://www.gnu.org/software/coreutils/manual/html_node/mktemp-invocation.html
GNU Coreutils — mv: https://www.gnu.org/software/coreutils/manual/coreutils.html#mv-invocation
systemd.timer: https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html
تألیف و مثالهای اختصاصی: تحریریه درسآوند. منابع رسمی انگلیسی برای مطالعهٔ تکمیلی معرفی شدهاند.
متن درسهای عمومی را بدون ورود بخوانید.