Добрый день! Хотелось бы рассказать Вам об очередном велосипедостроении. Просматривая Хабр, я наткнулся на замечательную статью: Bash: запускаем демон с дочерними процессами. После прочтения возникла идея написать что-нибудь полезное, с преферансом и куртизантками, куда же без этого.
ОС: Astra Linux 1.2 (1.3)
Из вводной следуют два вывода:
Основные моменты построения демона на bash рассказывать не буду, это прекрасно описано в статье указанной выше, поэтому перейдем сразу к рабочему телу :).
Для начала укажем переменные, которые будем использовать:
Т.к. вывод df нас не интересует, то получить информацию о состоянии файловой системы можно через stat. но для этого необходимо знать каталог, куда смонтирована данная файловая система. Эти данные хранятся в файле /proc/mounts, но есть небольшая заковырка, там имя диска может быть представлено как привычным именем устройства (например /dev/sda1), так и UUID(ом) устройства (например /dev/disk/by-uuid/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Для приведения всего этого в божеский вид, нам поможет утилита blkid (locale/print block device attributes).
Итак начнем заполнять функцию start(), проверку запуска от рута и проверку на вторую копию процесса опустим, перейдем сразу к составлению словаря соответствия имени устройства к точке монтирования
Как можете заметить в файле настроек есть переменная CHECK_DISKS которая является массивом проверяемых дисковых разделов. Размер, при котором необходимо устраивать панику указан в доступной для понимания человеком форме, для перевода используем функцию calculate_space_prefix. Функция получает размер и префикс, и переводит это хозяйство в байты.
Теперь рассмотрим основной цикл. В нем проходим по массиву checked_disks, в котором указан раздел и порог свободного места меньше которого необходимо ударятся во все тяжкие. Как говорилось выше, для получения информации о разделе используется команда stat, нам необходим следующий ее синтаксис.
Если мы не хотим, чтобы пользователь при получении письма счастья о том, что у него заканчивается место на разделе, сидел с калькулятором и пересчитывал байты в удобочитаемый вид, то напишем еще одну функцию.
Как видите, это та же функция calculate_space_prefix, только наоборот.
Итак, теперь все готово, для основного цикла сервиса. Комментариев там маловато, но думаю и без них основной принцип понятен: проверяй, проверяй и еще раз проверяй, а потом уже пиши письма.
Если кого заинтересует, то полный листинг сервиса под спойлером
Теперь о замеченном косяке (с которым лень разбираться и исправлять):
Вот вроде и все, о чем я хотел поведать. Всем бобра!
Вводная:
ОС: Astra Linux 1.2 (1.3)
Из вводной следуют два вывода:
- Нельзя устанавливать не сертифицированное ПО, иначе мы словим лютую попаболь с двух направлений (Заказчик и Руководство).
- Т.к. мы настоящие пионеры и не ищем легких путей, то вывод команды df нас не интересует.
Основные моменты построения демона на bash рассказывать не буду, это прекрасно описано в статье указанной выше, поэтому перейдем сразу к рабочему телу :).
Для начала укажем переменные, которые будем использовать:
# Эти две переменные думаю не надо объяснять PID_FILE="/run/ac_check_disk_space.pid" LOG_FILE="/var/log/ac_check_disk_space.log" # Период проверки # Префикс после числа может принимать следующие значения: # s - секунды # m - минуты # h - часы # d - дни # Если префикс не выставлен, то по умолчанию используются секунды CHECK_PERIOD="1m" # Форма записи: # Имя диска:Объем оставшегося места для срабатывания триггера # Пример записи для 2 дисков: # CHECK_DISKS=('/dev/sda1:10G' '/dev/sda3:10G') # Префик после числа может принимать следующие значения: # K - Килобайты # M - мегабайты # G - Гигабайты # Если префикс не выставлен, то по умолчанию используются байты CHECK_DISKS=('/dev/sda1:10G' '/dev/sda3:10G') # Переменные замены: # :host: - Имя хоста # :disk: - Имя диска # :mount_point: - Точка монтирования # :disk_total: - Общий объем диска # :disk_avaiable: - Объем доступный для прользователя # :disk_checked_size: - Порог срабатывания тригера MAIL_SUBJECT_TEMPLATE="ACHTUNG: :host: low disk space on :disk: mounted to :mount_point:!" MAIL_BODY_TEMPLATE="Details: Total disk size :disk_total:, Avaiable size: :disk_avaiable:, Trigger size: :disk_checked_size:" MAIL_RCPT=('somebody@domain.ru')
Т.к. вывод df нас не интересует, то получить информацию о состоянии файловой системы можно через stat. но для этого необходимо знать каталог, куда смонтирована данная файловая система. Эти данные хранятся в файле /proc/mounts, но есть небольшая заковырка, там имя диска может быть представлено как привычным именем устройства (например /dev/sda1), так и UUID(ом) устройства (например /dev/disk/by-uuid/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Для приведения всего этого в божеский вид, нам поможет утилита blkid (locale/print block device attributes).
Итак начнем заполнять функцию start(), проверку запуска от рута и проверку на вторую копию процесса опустим, перейдем сразу к составлению словаря соответствия имени устройства к точке монтирования
# Получаем списки дисков по именам и UUID disks=$(blkid | grep -v swap | awk '{print $1}' | sed -e s/://) uuids=$(blkid | grep -v swap | awk '{print $1}' | sed -e s/UUID=// | sed -e s/\*//g) # Инициализация массива привязки диска к точке монтирования mounts=() # Заполняем массив по имени диска for (( i=0; i<${#disks[*]}; i++ )); do mount_point=( `cat /proc/mounts | grep ${disks[$i]} | awk '{print $2}'` ) if [[ ! -z $mount_point ]]; then mounts=("${mounts[@]}" "${disks[$i]}:$mount_point") fi done # Заполняем массив по UUID for (( i=0; i<${#uuids[*]}; i++ )); do mount_point=( `cat /proc/mounts | grep ${uuids[$i]} | awk '{print $2}'` ) if [[ ! -z $mount_point ]]; then disk=`blkid -U ${uuids[$i]}` mounts=("${mounts[@]}" "$disk:$mount_point") fi done # Проверка, существуют ли разделы указанные в файле настройки и составление массива дисков для проверки exists=0 checked_disks=() for mount in "${mounts[@]}"; do mount_disk="${mount%%:*}" for check in "${CHECK_DISKS[@]}"; do check_disk="${check%%:*}" if [ $check_disk == $mount_disk ]; then check_size="${check##*:}" size=$(calculate_space_prefix $check_size) checked_disks=("${checked_disks[@]}" "$check_disk:$size") exists=1 fi done done if [ $exists -eq 0 ]; then echo "Can not find disks, please check your configuration file" exit 1 fi
Как можете заметить в файле настроек есть переменная CHECK_DISKS которая является массивом проверяемых дисковых разделов. Размер, при котором необходимо устраивать панику указан в доступной для понимания человеком форме, для перевода используем функцию calculate_space_prefix. Функция получает размер и префикс, и переводит это хозяйство в байты.
function calculate_space_prefix() { local value=$1 local result=$2 local size=0 local prefix="" prefix="${value: -1}" len="${#value}" len=$(($len - 1)) size="${value:0:$len}" case $prefix in "K") size=$(($size * 1024)) ;; "M") size=$(($size * 1048576)) ;; "G") size=$(($size * 1073741824)) ;; *) #size=$(($size * 1073741824)) ;; esac echo $size }
Теперь рассмотрим основной цикл. В нем проходим по массиву checked_disks, в котором указан раздел и порог свободного места меньше которого необходимо ударятся во все тяжкие. Как говорилось выше, для получения информации о разделе используется команда stat, нам необходим следующий ее синтаксис.
stat -f <точка монтирования> -c "%b %a %s" # Где: # %b - Общее количество блоков данных в файловой системе # %a - Количество свободных блоков, доступных для обычного пользователя # %s - Размер блока
Если мы не хотим, чтобы пользователь при получении письма счастья о том, что у него заканчивается место на разделе, сидел с калькулятором и пересчитывал байты в удобочитаемый вид, то напишем еще одну функцию.
function calculate_return_space_prefix() { local value=$1 local space=$2 local size=0 prefix="${value: -1}" case $prefix in "K") size=$(($space / 1024)) ;; "M") size=$(($space / 1048576)) ;; "G") size=$(($space / 1073741824)) ;; *) ;; esac echo $size }
Как видите, это та же функция calculate_space_prefix, только наоборот.
Итак, теперь все готово, для основного цикла сервиса. Комментариев там маловато, но думаю и без них основной принцип понятен: проверяй, проверяй и еще раз проверяй, а потом уже пиши письма.
# Основной цикл while [ 1 ]; do for checked in "${checked_disks[@]}"; do checked_disk="${checked%%:*}" checked_size="${checked##*:}" for mount in "${mounts[@]}"; do mount_disk="${mount%%:*}" mount_point="${mount##*:}" if [ $mount_disk == $checked_disk ]; then disk_all=( `stat -f $mount_point -c "%b"` ) disk_avaiable=( `stat -f $mount_point -c "%a"` ) disk_block_size=( `stat -f $mount_point -c "%s"` ) disk_all=$(($disk_all * $disk_block_size)) disk_avaiable=$(($disk_avaiable * $disk_block_size)) if [ $disk_avaiable -le $checked_size ]; then _log "Low disk size on $checked_disk mounted to $mount_point. Total size: $disk_all, avaiable size: $disk_avaiable, trigger size: $checked_size." # Переводим байты в удобочитаемый формат for check in "${CHECK_DISKS[@]}"; do check_disk="${check%%:*}" check_size="${check##*:}" if [ $check_disk == $checked_disk ]; then disk_all=$(calculate_return_space_prefix $check_size $disk_all) disk_avaiable=$(calculate_return_space_prefix $check_size $disk_avaiable) checked_size=$(calculate_return_space_prefix $check_size $checked_size) prefix="${check_size: -1}" fi done subject=`echo -e ${MAIL_SUBJECT_TEMPLATE} | sed -e "s|:host:|$host|g" | sed -e "s|:disk:|$checked_disk|g" | sed -e "s|:mount_point:|$mount_point|g" | sed -e "s|:disk_total:|${disk_all}${prefix}|g" | sed -e "s|:disk_avaiable:|${disk_avaiable}${prefix}|g" | sed -e "s|:disk_checked_size:|${checked_size}${prefix}|g"` body=`echo -e ${MAIL_BODY_TEMPLATE} | sed -e "s|:host:|$host|g" | sed -e "s|:disk:|$checked_disk|g" | sed -e "s|:mount_point:|$mount_point|g" | sed -e "s|:disk_total:|${disk_all}${prefix}|g" | sed -e "s|:disk_avaiable:|${disk_avaiable}${prefix}|g" | sed -e "s|:disk_checked_size:|${checked_size}${prefix}|g"` for rcpt in "${MAIL_RCPT[@]}"; do echo "$body" | mail -s "$subject" "$rcpt" done fi fi done done sleep "${CHECK_PERIOD}" done
Если кого заинтересует, то полный листинг сервиса под спойлером
Полный листинг
#!/usr/bin/env bash set -e set -m ### BEGIN INIT SCRIPT # Provides: ac_check_disk_space # Required-Start: $local_fs $syslog # Required-Stop: $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: ac_check_disk_space # Description: Service to monitoring disk space for Astra Linux ### END INIT SCRIPT usage() { echo -e "Usage:\n$0 (start|stop|restart)" } _log() { # Сдвигаем влево входные параметры #shift ts=`date +"%b %d %Y %H:%M:%S"` hn=`cat /etc/hostname` echo "$ts $hn ac_check_disk_space[${BASHPID}]: $*" } check_conf_file() { if [ -e "/etc/ac/check_disk_space.conf" ]; then source "/etc/ac/check_disk_space.conf" else echo "Can not find configuration file (/etc/ac/check_disk_space.conf)" exit 0 fi } function calculate_space_prefix() { local value=$1 local result=$2 local size=0 local prefix="" prefix="${value: -1}" len="${#value}" len=$(($len - 1)) size="${value:0:$len}" case $prefix in "K") size=$(($size * 1024)) ;; "M") size=$(($size * 1048576)) ;; "G") size=$(($size * 1073741824)) ;; *) #size=$(($size * 1073741824)) ;; esac echo $size } function calculate_return_space_prefix() { local value=$1 local space=$2 local size=0 prefix="${value: -1}" case $prefix in "K") size=$(($space / 1024)) ;; "M") size=$(($space / 1048576)) ;; "G") size=$(($space / 1073741824)) ;; *) ;; esac echo $size } start() { #trap 'echo "1" >> /tmp/test' 1 2 3 15 # Проверяем запуск от рута if [ $UID -ne 0 ]; then echo "Root privileges required" exit 0 fi # Проверяем наличие конфига check_conf_file # Проверка на вторую копию if [ -e ${PID_FILE} ]; then _pid=( `cat ${PID_FILE}` ) if [ -e "/proc/${_pid}" ]; then echo "Daemon already running with pid = $_pid" exit 0 fi fi touch ${LOG_FILE} # Получаем списки дисков по именам и UUID disks=( `blkid | grep -v swap | awk '{print $1}' | sed -e s/://` ) uuids=( `blkid | grep -v swap | awk '{print $2}' | sed -e s/UUID=// | sed -e s/\"//g` ) # Инициализация массива привязки диска к точке монтирования mounts=() # Заполняем массив по имени диска for (( i=0; i<${#disks[*]}; i++ )); do mount_point=( `cat /proc/mounts | grep ${disks[$i]} | awk '{print $2}'` ) if [[ ! -z $mount_point ]]; then mounts=("${mounts[@]}" "${disks[$i]}:$mount_point") fi done # Заполняем массив по UUID for (( i=0; i<${#uuids[*]}; i++ )); do mount_point=( `cat /proc/mounts | grep ${uuids[$i]} | awk '{print $2}'` ) if [[ ! -z $mount_point ]]; then disk=`blkid -U ${uuids[$i]}` mounts=("${mounts[@]}" "$disk:$mount_point") fi done # Проверка, существуют ли диски указанные в файле настройки и составление массива дисков для проверки exists=0 checked_disks=() for mount in "${mounts[@]}"; do mount_disk="${mount%%:*}" for check in "${CHECK_DISKS[@]}"; do check_disk="${check%%:*}" if [ $check_disk == $mount_disk ]; then check_size="${check##*:}" size=$(calculate_space_prefix $check_size) checked_disks=("${checked_disks[@]}" "$check_disk:$size") exists=1 fi done done if [ $exists -eq 0 ]; then echo "Can not find disks, please check your configuration file" exit 1 fi # Копия предыдущего лога cp -f ${LOG_FILE} ${LOG_FILE}.prev # Имя хоста host=( `cat /etc/hostname` ) # Демонизация процесса =) cd / exec > ${LOG_FILE} exec 2> /dev/null exec < /dev/null # Форкаемся ( # ; rm -f ${PID_FILE}; exit 255; # SIGHUP SIGINT SIGQUIT SIGTERM #trap '_log "Daemon stop"; rm -f ${PID_FILE}; cp ${LOG_FILE} ${LOG_FILE}.prev; exit 0;' 1 2 3 15 _log "Daemon started" # Основной цикл while [ 1 ]; do for checked in "${checked_disks[@]}"; do checked_disk="${checked%%:*}" checked_size="${checked##*:}" for mount in "${mounts[@]}"; do mount_disk="${mount%%:*}" mount_point="${mount##*:}" if [ $mount_disk == $checked_disk ]; then disk_all=( `stat -f $mount_point -c "%b"` ) disk_avaiable=( `stat -f $mount_point -c "%a"` ) disk_block_size=( `stat -f $mount_point -c "%s"` ) disk_all=$(($disk_all * $disk_block_size)) disk_avaiable=$(($disk_avaiable * $disk_block_size)) if [ $disk_avaiable -le $checked_size ]; then _log "Low disk size on $checked_disk mounted to $mount_point. Total size: $disk_all, avaiable size: $disk_avaiable, trigger size: $checked_size." # Переводим байты в удобочитаемый формат for check in "${CHECK_DISKS[@]}"; do check_disk="${check%%:*}" check_size="${check##*:}" if [ $check_disk == $checked_disk ]; then disk_all=$(calculate_return_space_prefix $check_size $disk_all) disk_avaiable=$(calculate_return_space_prefix $check_size $disk_avaiable) checked_size=$(calculate_return_space_prefix $check_size $checked_size) prefix="${check_size: -1}" fi done subject=`echo -e ${MAIL_SUBJECT_TEMPLATE} | sed -e "s|:host:|$host|g" | sed -e "s|:disk:|$checked_disk|g" | sed -e "s|:mount_point:|$mount_point|g" | sed -e "s|:disk_total:|${disk_all}${prefix}|g" | sed -e "s|:disk_avaiable:|${disk_avaiable}${prefix}|g" | sed -e "s|:disk_checked_size:|${checked_size}${prefix}|g"` body=`echo -e ${MAIL_BODY_TEMPLATE} | sed -e "s|:host:|$host|g" | sed -e "s|:disk:|$checked_disk|g" | sed -e "s|:mount_point:|$mount_point|g" | sed -e "s|:disk_total:|${disk_all}${prefix}|g" | sed -e "s|:disk_avaiable:|${disk_avaiable}${prefix}|g" | sed -e "s|:disk_checked_size:|${checked_size}${prefix}|g"` for rcpt in "${MAIL_RCPT[@]}"; do echo "$body" | mail -s "$subject" "$rcpt" done fi fi done done sleep "${CHECK_PERIOD}" done )& # Пишем pid потомка в файл echo $! > ${PID_FILE} } stop() { check_conf_file if [ -e ${PID_FILE} ]; then _pid=( `cat ${PID_FILE}` ) if [ -e "/proc/${_pid}" ]; then kill -9 $_pid result=$? if [ $result -eq 0 ]; then echo "Daemon stop." else echo "Error stop daemon" fi else echo "Daemon is not run" fi else echo "Daemon is not run" fi } restart() { stop start } case $1 in "start") start ;; "stop") stop ;; "restart") restart ;; *) usage ;; esac exit 0
Теперь о замеченном косяке (с которым лень разбираться и исправлять):
- Скрипт обрабатывает посылаемые ему сигналы с задержкой указанной в переменной CHECK_PERIOD, а не моментально. К сожалению, ни как не могу вспомнить как это называется, но зависит именно из-за цикла.
Вот вроде и все, о чем я хотел поведать. Всем бобра!