[Из песочницы] Сервис мониторинга свободного места на Bash
Добрый день! Хотелось бы рассказать Вам об очередном велосипедостроении. Просматривая Хабр, я наткнулся на замечательную статью: Bash: запускаем демон с дочерними процессами. После прочтения возникла идея написать что-нибудь полезное, с преферансом и куртизантками, куда же без этого.Вводная: ОС: 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, а не моментально. К сожалению, ни как не могу вспомнить как это называется, но зависит именно из-за цикла. Вот вроде и все, о чем я хотел поведать. Всем бобра!