Pull to refresh

Сервис мониторинга свободного места на Bash

Reading time 9 min
Views 17K
Добрый день! Хотелось бы рассказать Вам об очередном велосипедостроении. Просматривая Хабр, я наткнулся на замечательную статью: Bash: запускаем демон с дочерними процессами. После прочтения возникла идея написать что-нибудь полезное, с преферансом и куртизантками, куда же без этого.

Вводная:

ОС: Astra Linux 1.2 (1.3)

Из вводной следуют два вывода:

  1. Нельзя устанавливать не сертифицированное ПО, иначе мы словим лютую попаболь с двух направлений (Заказчик и Руководство).
  2. Т.к. мы настоящие пионеры и не ищем легких путей, то вывод команды 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


Теперь о замеченном косяке (с которым лень разбираться и исправлять):

  1. Скрипт обрабатывает посылаемые ему сигналы с задержкой указанной в переменной CHECK_PERIOD, а не моментально. К сожалению, ни как не могу вспомнить как это называется, но зависит именно из-за цикла.

Вот вроде и все, о чем я хотел поведать. Всем бобра!
Tags:
Hubs:
+7
Comments 31
Comments Comments 31

Articles