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

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

Вот вроде и все, о чем я хотел поведать. Всем бобра!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 31

    +10
    По поводу самой проверки свободного места, чем вам не угодил
    df -h
    
    ?
      0
      Во первых, в разных версиях Астры разный вывод.
      Во вторых, да же не знаю, было свободное время, и стало интересно как можно еще получить данные параметры.
        +2
        что, даже «LANG=C df -Pk» разный вывод?
          0
          Возможно не корректно выразился, в одной версии мы видим имя устройства (/dev/sdaX), в другой устройство по uuid (/dev/disk/by-uuid/....).
          Так что, почему бы и не через stat попробывать? =)
            0
            Какая разница, если нужны mountpoints по-хорошему?
              0
              Разницы никакой, просто на вкус и цвет фломастеры разные. Просто захотелось посмотреть не через df, может кого-то заинтересует.
      0
      С современными init-системами нет совершенно никакого смысла делать явный форк и вручную управлять локами, pid-файлами и логгированием. Достаточно написать скрипт, который будет запускаться и делать работу, выводя все свои действия в stdout, а управление жизненным циклом и логгирование поручить системе. В том же systemd достаточно написать простенький конфиг, но при этом ваша программа уменьшится раза в полтора.
        0
        Я это понимаю, но есть такая маленькая проблемка. Операционная система которую я указал в секции «Вводная», а так же я с ней работаю. Сертифицирована для работы с ГТ (то бишь гос. тайна), с грифом до СС (сов. секретно) включительно. Это накладывает свои ограничения, самое главное звучит так, вы не имеете права устанавливать несертифицированное ПО на систему. Скрипты например должны проходить тематические исследования на отсутствие не декларируемых возможностей. как-то так.
          0
          Мм, а как это связано с форматом скрипта? Я вижу здесь проблему, только если в этой ОС нету нормальной системы инициализации, но про это ничего во вводной сказано не было :)
            0
            Используется System V init. Если там можно делать как Вы написали, буду рад узнать.=) Просто раньше пробовал писать демоны (ради собственного развития), везде в туториалах были такие шаги.
              0
              Нет, к сожалению, непосредственно в sysvinit такого нет, хотя вроде бы есть сторонние утилиты для этого. Вообще он сейчас активно депрекейтится практически всеми основными дистрибутивами, но инерция пока что очень сильна, так что ничего удивительного, что соответствующие туториалы появляются снова и снова)
                +2
                В том-то и дело, что сторонние использовать нельзя. А когда это придет в основную ветку, пройдет лет тцать =) Поэтому велосипеды мой выбор =)
                  0
                  Попробуйте, может есть дебиановский start-stop-daemon? Его много где используют помимо Debian.
          0
          Да какой systemd? Это ж для cron-а задачка
            0
            Да, вы, наверное, правы, раз скрипт только логгирует данные. Просто смутила вообще вся эта возня с демонизацией.
              0
              Конечно можно и в крон, просто написал же, мы же калмыцкие пионеры, сначала придумываем себе трудности, потом мужественно преодолеваем. Ну и плюс, мне надоело на объекте читать список из 7+ заданий крона =)
            +1
            Это же «сертифицированная ОС». Systemd в ней только года через два появится, а то и позже :)
              +3
              Угу, для примера назову еще несколько: МСВС, Заря.
              Бро, поверь, появится очень очень позже =)
            0
            > Скрипт обрабатывает посылаемые ему сигналы с задержкой указанной в
            > переменной CHECK_PERIOD, а не моментально.
            > … зависит именно из-за цикла


            Здесь не цикл виновал, а sleep. Сигнал отправляется bash'у, им же получается и обрабатывается, но bash в скрипте — это то, что находится между вызовами внешних утилит, то есть как только по завершении sleep'а к bash'у вернётся управление, так он сразу же сделает всё, что от него требуется…
              0
              Ну типо того, в Borland Delphi вроде бы была специальная функция для таких случаев, что-то типа Process Events, не помню уже.
              +3
              в этой системе что, нет ни питона, ни перла?
                0
                Есть и то и то.
                +3
                Мусор с претензией.
                  0
                  Да без проблем.
                  +6
                  Простите, а всё вышенаписанное, является сертифицированным ПО? Я к тому, что понятие «ПО» мало соотносится с понятием «тьюринг-полноты инструментария».
                    +1
                    Здесь как договоришься в 8 управлением =)
                  • UFO just landed and posted this here
                      0
                      В смысле?
                      • UFO just landed and posted this here
                      0
                      не префикс, а суффикс
                        +5
                        image

                        Only users with full accounts can post comments. Log in, please.