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