Ограниченный сбор наблюдений с помощью Bash
В предыдущем уроке появился сценарий подсчёта статусов. Теперь добавим к его результату несколько наблюдений о машине: время, версию ядра, заполнение файловой системы, слушающие TCP-порты и краткий перечень процессов. Цель — получить воспроизводимый набор материалов для исследования DeskQueue. Сборщик не изменяет службу и не пытается автоматически исправить её состояние. Если наблюдение не удалось, это тоже часть материала, которую следует сохранить.
Самостоятельный снимок stages/14 учебного архива содержит предыдущий summary.sh, тот же синтетический TSV и новый collect.sh. Команды рассчитаны на Ubuntu 24.04 с Bash 5.x и указанными в сценарии утилитами. Режим basic не требует работающего приложения. Режим service дополнительно обращается к отдельно подготовленной учебной службе deskqueue.service и локальному /ready. При подготовке материала оба режима не выполнялись; каталог настоящих наблюдений создаст читатель.
Список разрешённых действий
Принимаем только абсолютный путь к лаборатории и одно из двух названий режима. Не принимаем строку произвольной команды. Диспетчер case выбирает заранее записанные действия, а функция record выполняет уже сформированный набор аргументов. Между пользовательским текстом и исполнением нет eval. Поэтому слово service расширяет известный список наблюдений, а не предоставляет доступ к любым возможностям оболочки.
Функция передаёт аргументы через "$@". В этой форме каждое переданное значение сохраняет собственные границы. Например, параметр времени журнала содержит пробелы, но остаётся одним аргументом. Объединять все аргументы в строку и затем повторно разбирать её не требуется. Такой порядок отделяет описание команды от её запуска и позволяет записать безопасный перечень выбранных действий для последующего чтения.
Список процессов ограничен полями PID, имя команды и состояние. Аргументы процессов не собираем: в них иногда оказываются пароли или другие чувствительные значения. Также не выгружаем переменные окружения, конфигурацию приложения и файлы секретов. Системный журнал всё равно может содержать чувствительные сообщения, поэтому режим service следует использовать только в собственной лаборатории, а собранные материалы просматривать перед передачей кому-либо.
Новый каталог для каждого наблюдения
Именуем результат по UTC-времени и PID сборщика. Сначала создаём родительский evidence, затем обычным mkdir создаём новый вложенный каталог. Если такое имя уже занято, продолжающего добавления в старый отчёт не будет. Это помогает отличить один запуск от другого и не смешивать материалы, собранные в разные моменты. Сам PID здесь часть имени, а не удостоверение подлинности отчёта.
umask 077 ограничивает права новых файлов владельцем. Новая папка обычно получает режим 700, обычные файлы — 600. Настройка не исправляет права ранее существующих каталогов. Лаборатория должна находиться в принадлежащем читателю каталоге, куда посторонние пользователи не могут подменять пути и символические ссылки. Это предпосылка нашего небольшого сценария, а не универсальная защита при запуске с повышенными правами.
Полный сборщик
Для каждого действия сохраняем четыре файла: описание аргументов, стандартный вывод, поток ошибок и числовой статус. Объединение stdout и stderr выглядело бы удобнее при чтении, но скрыло бы различие между данными и диагностикой инструмента. Даже успешная команда иногда пишет замечание в stderr. Поэтому наличие непустого файла ошибок само по себе не равнозначно неудачному завершению.
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
export LC_ALL=C
if (( $# < 1 || $# > 2 )); then
printf 'Usage: bash collect.sh ABSOLUTE_LAB_ROOT [basic|service]\n' >&2
exit 2
fi
if [[ $1 != /* || ! -d $1 ]]; then
printf 'LAB_ROOT must be an existing absolute directory\n' >&2
exit 2
fi
if root=$(cd -- "$1" && pwd -P); then :; else exit 1; fi
mode=${2:-basic}
case "$mode" in
basic|service) ;;
*) printf 'Unknown collection mode\n' >&2; exit 2 ;;
esac
if [[ ! -r "$root/scripts/summary.sh" || ! -r "$root/fixtures/access.tsv" ]]; then
printf 'The laboratory must contain summary.sh and access.tsv\n' >&2
exit 2
fi
for tool in date mkdir timeout uname df ss ps bash awk; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'Required tool is missing: %s\n' "$tool" >&2
exit 2
fi
done
if [[ $mode == service ]]; then
for tool in systemctl journalctl curl sleep; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'Required service tool is missing: %s\n' "$tool" >&2
exit 2
fi
done
fi
if stamp=$(date -u '+%Y%m%dT%H%M%SZ'); then :; else exit 1; fi
output="$root/evidence/run-$stamp-$$"
if ! mkdir -p -- "$root/evidence" || ! mkdir -- "$output"; then
printf 'Cannot create a new evidence directory\n' >&2
exit 1
fi
failed=0
LAST_STATUS=0
record() {
local name=$1
shift
local result
if ! printf '%q ' "$@" > "$output/$name.command" ||
! printf '\n' >> "$output/$name.command"; then
printf 'Cannot save the command for %s\n' "$name" >&2
exit 1
fi
if timeout --kill-after=1s 5s "$@" > "$output/$name.stdout" 2> "$output/$name.stderr"; then
result=0
else
result=$?
fi
if ! printf '%s\n' "$result" > "$output/$name.status"; then
printf 'Cannot save the status for %s\n' "$name" >&2
exit 1
fi
LAST_STATUS=$result
if (( result != 0 )); then failed=1; fi
}
for item in clock kernel storage sockets processes summary; do
case "$item" in
clock) record clock date -u '+%Y-%m-%dT%H:%M:%SZ' ;;
kernel) record kernel uname -sr ;;
storage) record storage df -P "$root" ;;
sockets) record sockets ss -ltn ;;
processes) record processes ps -eo pid,comm,stat ;;
summary) record summary bash "$root/scripts/summary.sh" "$root/fixtures/access.tsv" 3 ;;
esac
done
if [[ $mode == service ]]; then
record service systemctl show deskqueue.service --no-pager \
--property=LoadState,ActiveState,SubState,Result,MainPID
record journal journalctl --unit=deskqueue.service --since='10 minutes ago' \
--lines=50 --no-pager --output=short-iso
attempt=1
while (( attempt <= 3 )); do
record "readiness-$attempt" curl --disable --noproxy 127.0.0.1 --fail --silent --show-error --max-time 2 \
http://127.0.0.1:3000/ready
if (( LAST_STATUS == 0 )); then break; fi
if (( attempt < 3 )); then sleep 1; fi
attempt=$((attempt + 1))
done
fi
if ! printf 'mode\t%s\nfailed_observations\t%s\n' "$mode" "$failed" > "$output/collection.tsv"; then
exit 1
fi
printf '%s\n' "$output"
exit "$failed"
Функция сначала забирает имя наблюдения, затем shift оставляет только команду и её аргументы. Внутри if команда может завершиться ненулевым статусом без немедленного прерывания всей работы. Статус сохраняется сразу, до следующего printf. Если не удалось записать сам протокол, сборщик прекращает работу: оставшийся без статусов каталог нельзя представить как полный результат. Уже созданные файлы остаются для исследования причины.
Время ожидания ограничено
Внешний timeout даёт каждому действию пять секунд. После этого он отправляет сигнал завершения, а параметр --kill-after=1s добавляет короткий срок перед принудительным прекращением. Код 124 обычно обозначает истечение первоначального срока; 137 возможен при принудительном завершении. Другие коды относятся к самому инструменту или вызываемой команде. Детали приведены в справке timeout.
Тайм-аут — ограничение длительности наблюдения, а не диагноз DeskQueue. Если чтение журнала не укладывается в срок, это ещё не доказывает отказ приложения. Сохраним его код, поток ошибок и частичный вывод, затем выберем отдельный следующий вопрос. Также ограничили журнал пятьюдесятью строками за десять минут. Такой фрагмент может пропустить важное более раннее событие, поэтому в выводах нужно указывать границы выборки.
Повторы не превращаются в вечный цикл
Для /ready разрешены максимум три попытки. Запрос имеет собственное ограничение две секунды и общий внешний тайм-аут. После успешного ответа цикл прекращается; между неудачными попытками делается короткая пауза. Число попыток видно в условии while, а счётчик увеличивается в одном месте. Нет скрытого ожидания «до тех пор, пока всё исправится», которое удерживало бы расписание бесконечно.
curl --disable первым параметром отключает скрытые настройки curlrc, а --noproxy направляет запрос к loopback напрямую. Параметры описаны в официальной справке curl. curl --fail считает HTTP-ошибку неудачным наблюдением. Отказ подключения, статус 503 и тайм-аут имеют разные числовые результаты, которые останутся в отдельных файлах. Содержимое успешного /ready говорит о текущей готовности API, включая его связь с PostgreSQL, согласно контракту курса Node. Сопоставьте сохранённое тело с ожидаемым {ready:true}: один код curl не удостоверяет личность приложения на порту. Наблюдение не проверяет всю бизнес-логику и не гарантирует, что готовность сохранится после завершения запроса.
Флаг failed_observations остаётся единицей, даже если после первой неудачи следующая попытка успешна. Это намеренное правило: каталог содержит хотя бы одно неуспешное наблюдение. Оно помогает сохранить факт кратковременной проблемы. Окончательное состояние нужно читать по последней попытке, а последовательность — по всем трём возможным файлам. Одно итоговое число не заменяет такую интерпретацию.
Полученный материал связываем с вопросом
Из корня ops-lab задайте абсолютный путь именно к снимку. Это важно: сборщик ищет свои входы внутри переданной лаборатории, а не рядом с текущим терминалом.
lab_root="$(pwd -P)/stages/14"
bash "$lab_root/scripts/collect.sh" "$lab_root" basic
При доступных инструментах ожидается новый каталог evidence/run-... внутри снимка и шесть групп файлов. Учебная сводка должна содержать двенадцать записей и три ответа 5xx. Порты, процессы и состояние диска зависят от машины читателя; заранее напечатать их достоверные значения невозможно. Даже отсутствие слушателя 3000 в режиме basic не является ошибкой самого сборщика: этот режим сохраняет таблицу сокетов целиком.
В рабочей заметке сопоставьте наблюдение, сохранённый файл и следующее действие. Например, отсутствующий слушатель направляет к состоянию службы; работающий слушатель и неуспешный /ready направляют к готовности приложения и базе. Итог этапа — ограниченная процедура сбора с раздельными доказательствами. Следующий шаг перенесёт этот же сценарий в расписание, сохранив его параметры, права и границы времени.