Script gọi bconsole thường chạy êm, không báo lỗi, và đôi khi làm sai hoàn toàn. Lỗi nguy hiểm nhất trong tự động hoá Bareos không phải lỗi làm script dừng, mà là lỗi làm script tin rằng mọi việc đã xong.
Bài 9 kết thúc bằng một checklist diễn tập định kỳ. Muốn chạy nó đều đặn, sớm muộn gì bạn cũng viết script quanh bconsole: kiểm Director còn sống, tạm tắt một job, chờ hết job đang chạy rồi reload. Bài này gom sáu cái bẫy hay gặp nhất khi làm việc đó. Mỗi bẫy có một đoạn script “sai” trông rất hợp lý, và một đoạn “đúng” đặt ngay sau.
Vì sao bconsole khó tự động hoá
bconsole được viết cho người gõ tay. Người đọc output bằng mắt, thấy lỗi thì dừng. Script thì chỉ có hai thứ để dựa vào: mã thoát và chuỗi ký tự trong output. Cả hai đều dễ đánh lừa:
- Mã thoát ít nói lên điều gì. bconsole thường thoát mã 0 kể cả khi không nối được Director.
- Output viết cho người đọc. Định dạng có thể đổi giữa các phiên bản, và câu báo lỗi dùng lại những từ của câu báo thành công.
- Một số trạng thái chỉ sống trong bộ nhớ. Lệnh chạy thành công, nhưng hiệu lực mất ở lần reload kế tiếp.
Nguyên tắc chung cho cả bài: tìm dấu hiệu thành công cụ thể, và đọc lại trạng thái thật sau mỗi thao tác ghi. Đừng suy ra kết quả từ việc lệnh chạy không lỗi.
Bẫy 1: bconsole thoát mã 0 dù không nối được Director
Đây là đoạn kiểm tra sức khoẻ hay gặp nhất, và cả hai cách viết dưới đều sai:
# SAI: tin vào mã thoát của bconsole
echo "status dir" | bconsole > /dev/null
if [ $? -eq 0 ]; then
echo "Director OK"
fi
# SAI: tìm chữ "Director" trong output
if echo "status dir" | bconsole | grep -q "Director"; then
echo "Director OK"
fi
Cách thứ nhất sai vì mã thoát là 0 ngay cả khi Director đã chết. Cách thứ hai sai tinh vi hơn. Câu báo lỗi kiểu Failed to connect to Director cũng chứa chữ “Director”, nên grep vẫn khớp. Script báo “Director OK” đúng vào lúc Director không trả lời.
Dấu hiệu chắc chắn là dòng bắt tay mà Director gửi về khi kết nối thành công, bắt đầu bằng 1000 OK. Cách viết đúng tìm đúng dòng đó, và bọc bconsole trong timeout để script không treo vô hạn:
#!/bin/bash
set -uo pipefail
# Chạy bconsole có giới hạn thời gian, giữ lại toàn bộ output để soi
bc() {
timeout 60 bconsole -c /etc/bareos/bconsole.conf <<< "$1"
}
out=$(bc "status dir")
rc=$?
if [ "$rc" -ne 0 ]; then
echo "bconsole lỗi hoặc quá giờ (mã $rc)" >&2
exit 2
fi
if ! grep -q '^1000 OK' <<< "$out"; then
echo "Không nối được Director:" >&2
tail -n 5 <<< "$out" >&2
exit 2
fi
echo "Director OK"
Lưu ý set -uo pipefail ở đầu, không có -e. Ở đây chúng ta muốn tự bắt mã thoát để in thông báo rõ ràng, thay vì để shell dừng im lặng.
Bẫy 2: lệnh disable chỉ sống trong bộ nhớ
Cần tạm dừng backup một máy chủ trong đợt bảo trì, cách nhanh nhất là disable job=… trong bconsole. Lệnh chạy ngay, status dir không còn thấy job đó trong lịch. Vấn đề là trạng thái này chỉ nằm trong bộ nhớ của Director:
# SAI: tắt job bằng lệnh bconsole rồi coi như xong
echo "disable job=backup-web01" | bconsole
# ...hôm sau có người thêm job mới và reload:
echo "reload" | bconsole
# backup-web01 lại chạy theo lịch như chưa từng bị tắt
Tài liệu Bareos có ghi rằng trạng thái bật/tắt đặt bằng lệnh sẽ mất khi reload. Nhưng người reload thường không phải người đã tắt job. Họ thêm một máy chủ mới, reload, và vô tình bật lại mọi thứ đang được tắt. Không có cảnh báo nào cả.
Cách bền là ghi Enabled = No vào chính file cấu hình, kiểm cú pháp, rồi mới reload. Sau reload, đọc lại trạng thái từ Director để chắc nó đã nhận:
#!/bin/bash
set -euo pipefail
JOB=backup-web01
FILE=/etc/bareos/bareos-dir.d/job/${JOB}.conf
TMP=$(mktemp)
BAK=${FILE}.bak
cp -p "$FILE" "$BAK"
# Bỏ mọi dòng Enabled cũ, chèn đúng MỘT dòng sau dòng Name.
# Chạy lại bao nhiêu lần kết quả vẫn như nhau.
awk '
/^[[:space:]]*Enabled[[:space:]]*=/ { next }
{ print }
/^[[:space:]]*Name[[:space:]]*=/ && !done { print " Enabled = No"; done = 1 }
' "$BAK" > "$TMP"
install -m 0640 -o root -g bareos "$TMP" "$FILE"
rm -f "$TMP"
# Kiểm cú pháp trước khi nạp; sai thì trả file cũ
if ! bareos-dir -t -c /etc/bareos > /dev/null 2>&1; then
cp -p "$BAK" "$FILE"
echo "Cấu hình lỗi, đã trả lại file cũ" >&2
exit 1
fi
echo "reload" | timeout 60 bconsole > /dev/null || true
# Đọc lại trạng thái THẬT từ Director, không tin lệnh reload
if echo "show job=${JOB}" | timeout 60 bconsole | grep -q 'Enabled[[:space:]]*=[[:space:]]*No'; then
echo "${JOB}: đã tắt bền"
else
echo "${JOB}: Director chưa nhận Enabled = No" >&2
exit 1
fi
Đoạn awk xoá mọi dòng Enabled cũ rồi chèn đúng một dòng mới. Chạy một lần hay mười lần, file cho ra vẫn giống hệt nhau. Tính chất đó sẽ quan trọng ở bẫy 5.
Nếu tạm dừng cả hệ thống, có thể áp cùng cách cho resource Schedule. Một Schedule mang Enabled = No trong file thì không job nào dùng nó khởi động được, dù ai đó reload bao nhiêu lần.
Bẫy 3: cắt khối Running Jobs bằng sed
Trước khi reload hay bảo trì, script thường hỏi “còn job nào đang chạy không”. Output của status dir có một khối Running Jobs:, nên người ta cắt khối đó ra:

# SAI: cắt từ tiêu đề tới dòng ==== đầu tiên
running=$(echo "status dir" | bconsole | sed -n '/^Running Jobs:/,/^====/p' | grep -c 'backup-')
if [ "$running" -eq 0 ]; then
echo "Không có job chạy, reload được"
fi
Đoạn này trông đúng, và in ra đúng khối cần xem khi không có job nào. Nhưng khi có job, ngay dưới dòng tiêu đề cột là một dòng kẻ =====. sed dừng ở dòng kẻ đó, tức là dừng trước dòng job đầu tiên. Kết quả trông y hệt “không có job chạy”, đúng vào lúc đang có job.
Cách đúng là cắt theo khối kế tiếp, không theo dòng kẻ, rồi tìm câu khẳng định rõ ràng:
#!/bin/bash
set -uo pipefail
out=$(echo "status dir" | timeout 60 bconsole)
grep -q '^1000 OK' <<< "$out" || { echo "Không nối được Director" >&2; exit 2; }
# Lấy trọn khối từ "Running Jobs:" tới trước "Terminated Jobs:"
khoi=$(awk '/^Running Jobs:/{p=1} /^Terminated Jobs:/{p=0} p' <<< "$out")
if grep -q '^No Jobs running\.' <<< "$khoi"; then
echo "Không có job chạy"
else
echo "Đang có job chạy:"
echo "$khoi"
exit 1
fi
Script coi “không thấy câu No Jobs running.” là “có job chạy”. Nếu định dạng output đổi ở phiên bản sau, script sẽ nghiêng về phía an toàn: từ chối reload, thay vì reload giữa lúc có job. Khi cần độ chắc cao hơn, truy vấn catalog trực tiếp theo cột trạng thái của bảng Job thường ổn định hơn đọc output dành cho người.
Bẫy 4: reload báo lỗi vẫn có thể đã chạy
Lệnh reload có thể quá giờ vì Director đang bận, hoặc trả output có chữ lỗi. Phản xạ tự nhiên là coi như chưa reload. Nhưng “lệnh báo lỗi” và “Director chưa nạp cấu hình” là hai chuyện khác nhau. Director có thể đã nạp xong, chỉ là phản hồi không về kịp tới script.
Nếu script tin vào thông báo lỗi, nó có thể dừng lại trong khi cấu hình mới đã có hiệu lực. Ngược lại, nếu tin vào mã thoát 0, nó có thể đi tiếp khi Director chưa nạp gì. Cả hai hướng đều sai vì cùng một lý do: quyết định dựa vào kết quả lệnh, không dựa vào trạng thái.
#!/bin/bash
set -uo pipefail
JOB=backup-web01
# Lệnh reload có thể quá giờ hoặc báo lỗi mà Director vẫn đã nạp xong.
# Vì vậy không quyết định theo kết quả lệnh, mà theo trạng thái đọc lại.
for lan in 1 2 3; do
echo "reload" | timeout 60 bconsole > /dev/null 2>&1 || true
if echo "show job=${JOB}" | timeout 60 bconsole | grep -q 'Enabled[[:space:]]*=[[:space:]]*No'; then
echo "Director đã nạp cấu hình mới (lần thử ${lan})"
exit 0
fi
sleep $(( lan * 30 ))
done
echo "Sau 3 lần vẫn chưa thấy cấu hình mới, cần người kiểm tra" >&2
exit 1
Vòng lặp này thử lại được vì reload thừa một lần thường vô hại. Điều kiện dừng là trạng thái đọc lại từ Director, không phải output của lệnh reload. Quy ước đơn giản: không chắc thì coi như chưa xong, và thử lại khi rảnh.
Khi có nhiều script cùng muốn reload, nên gom về một chỗ duy nhất. Một hàng đợi nhỏ nhận yêu cầu, chờ tới lúc không có job chạy, rồi reload một lần cho tất cả. Cách này tránh được việc hai script reload chồng nhau, hoặc một script reload khi script khác đang ghi file dở.
Bẫy 5: chạy lại thao tác không idempotent sau một lần lỗi
Một thao tác bị báo lỗi giữa chừng không có nghĩa là nó chưa chạy gì. Có thể nó đã chạy một phần, hoặc chạy hết rồi mới lỗi ở bước sau. Chạy lại một thao tác kiểu “nối thêm” lúc đó sẽ tạo ra bản trùng:
# SAI: nối thêm dòng; chạy lại sau một lần lỗi sẽ thành hai dòng
sed -i '/Name = backup-web01/a\ Enabled = No' /etc/bareos/bareos-dir.d/job/backup-web01.conf
Lần đầu, lệnh sed chạy xong nhưng bước sau báo lỗi. Người vận hành chạy lại, và file giờ có hai dòng Enabled = No. Với một khối cấu hình lớn hơn, bạn có thể có hai bản cùng một resource trong một file. Tuỳ trường hợp, Bareos báo lỗi cú pháp ở lần reload sau, hoặc lặng lẽ chấp nhận, và cả hai đều khó truy ra nguyên nhân.
Có hai cách phòng. Cách thứ nhất là chỉ dùng thao tác idempotent: ghi đè nguyên khối, hoặc xoá rồi chèn như đoạn awk ở bẫy 2. Cách thứ hai là đọc trạng thái trước khi thử lại:
#!/bin/bash
set -euo pipefail
FILE=/etc/bareos/bareos-dir.d/job/backup-web01.conf
# Trước khi thử lại: đọc trạng thái hiện tại, đừng đoán
so_dong=$(grep -c '^[[:space:]]*Enabled[[:space:]]*=' "$FILE" || true)
echo "Số dòng Enabled hiện có: ${so_dong}"
sha256sum "$FILE"
case "$so_dong" in
0) echo "Chưa có, an toàn để thêm" ;;
1) echo "Đã có một dòng, không làm gì thêm"; exit 0 ;;
*) echo "Có ${so_dong} dòng trùng, dừng lại để người kiểm tra" >&2; exit 1 ;;
esac
Ghi lại kích thước file trước và sau mỗi lần sửa là thói quen rẻ. File bị nhân đôi thường chỉ lộ ra nhờ kích thước tăng bất thường.
Bẫy 6: quên yes, script tưởng job đã chạy
Lệnh run trong bconsole hỏi xác nhận trước khi đưa job vào hàng đợi. Chạy qua pipe mà thiếu từ khoá yes, bconsole gặp hết stdin trước khi nhận được câu trả lời:
# SAI: thiếu "yes", bconsole chờ xác nhận rồi gặp hết stdin
echo "run job=backup-web01 level=Full" | bconsole
echo "Đã chạy backup Full"
Script in “Đã chạy backup Full”, nhưng thường không có job nào được tạo. Cách đúng là thêm yes, rồi lấy JobId từ output làm bằng chứng:
#!/bin/bash
set -uo pipefail
out=$(echo "run job=backup-web01 level=Full yes" | timeout 60 bconsole)
jobid=$(grep -oE 'Job queued\. JobId=[0-9]+' <<< "$out" | grep -oE '[0-9]+$' || true)
if [ -z "$jobid" ]; then
echo "Job không được đưa vào hàng đợi:" >&2
tail -n 5 <<< "$out" >&2
exit 1
fi
echo "Đã xếp hàng JobId=${jobid}"
Có JobId, các bước sau theo dõi được đúng job đó bằng wait jobid=.
Bảng tổng hợp
| Bẫy | Triệu chứng | Cách đúng |
|---|---|---|
| Tin mã thoát bconsole | Báo OK khi Director đã chết | Tìm dòng 1000 OK, bọc timeout |
| Tìm chữ “Director” | Câu lỗi cũng khớp | Tìm dấu hiệu thành công, không tìm từ khoá chung |
disable bằng lệnh | Job tự bật lại sau reload | Ghi Enabled = No vào file, kiểm cú pháp, reload, đọc lại |
Cắt Running Jobs theo dòng ==== | Không thấy job đầu tiên | Cắt tới Terminated Jobs:, tìm No Jobs running. |
| Tin kết quả lệnh reload | Dừng sai hoặc đi tiếp sai | Đọc lại trạng thái, không chắc thì thử lại |
| Chạy lại lệnh “nối thêm” | Dòng hoặc khối cấu hình bị nhân đôi | Thao tác idempotent, đọc trạng thái trước khi thử lại |
Thiếu yes khi run | Không có job nào được tạo | Thêm yes, lấy JobId làm bằng chứng |
Vài thói quen chung khi viết script quanh bconsole
- Mỗi thao tác ghi đi kèm một bước đọc lại. Tắt job thì
show job=, chạy job thì lấy JobId, reload thì kiểm cấu hình đã có hiệu lực. - Luôn có
timeout. Script chạy bằng cron mà treo thì không ai thấy, và lần chạy sau có thể chồng lên. - Kiểm cú pháp bằng
bareos-dir -ttrước mọi lần reload. Giữ bản sao file cũ để trả lại ngay khi kiểm không qua. - Dùng giá trị mặc định an toàn. Không chắc có job chạy hay không thì coi như có. Không chắc đã reload chưa thì coi như chưa.
- Đo trên hệ thống thật trước khi tin một mẫu chuỗi. Chạy lệnh ở cả trạng thái bình thường lẫn lỗi, chép output thật ra, rồi mới viết
grep.
Tóm tắt
- bconsole thường thoát mã 0 kể cả khi không nối được Director; hãy tìm dòng
1000 OK. disablebằng lệnh mất sau reload; muốn bền phải ghiEnabled = Novào file.- Output viết cho người đọc dễ đánh lừa script; cắt theo khối và tìm câu khẳng định rõ ràng.
- Lệnh báo lỗi vẫn có thể đã chạy; quyết định theo trạng thái đọc lại, không theo kết quả lệnh.
- Viết thao tác idempotent, và đọc trạng thái thật trước khi thử lại sau một lần lỗi.
Bài sau: Khi WebUI không đủ: thiết kế portal sinh cấu hình Bareos — gom các nguyên tắc trên thành một công cụ quản trị: cấu hình sinh từ cơ sở dữ liệu, kiểm trước khi nạp, và lan can để không đụng vào cấu hình viết tay.
Bài viết thuộc chuỗi “Bareos từ cài đặt tới vận hành” của đội kỹ thuật Cloudzone.