Application: "LDAP 389-AS/DS v1.3", "Apache2", "Bash".
Задача: визуализировать статистику о количестве подключений пользователей к LDAP-сервису, без выдачи сведений о персоналиях.
Статистика нам потребовалась для отслеживания процесса миграции со старого LDAP-сервера на новые инстансы, с попутной нормализацией политики использования учётных записей. От описываемого здесь сервиса требовалось лишь показать таблицы с перечнями комбинаций "источник подключения - пользователь" для каждого инстанса. Этого легко добиться регулярным сканированием журналов событий "389-DS" и генерированием простейшей HTML-страницы с таблицей.
Картинок с примерами здесь не будет - слишком служебная информация на них - но выглядит это примерно как в аналогичном web-сервисе для "Eduroam".
Настройка web-сервера "Apache2".
В комплекте с дистрибутивом "389-AS" уже поставляется web-сервер "Apache2" - им и воспользуемся, дабы не плодить сущности.
Привычно чуть поднастроим web-сервер, велев ему поменьше о себе рассказывать всем встречным:
# vi /etc/apache2/conf-enabled/security.conf
....
ServerSignature Off
....
ServerTokens Prod
....
ServerSignature Off
....
ServerTokens Prod
....
Для нашего локального web-сервиса устойчивость к большим нагрузкам ни к чему, а потому уменьшим аппетиты "Apache2", велев ему запускать поменьше потоков:
# vi /etc/apache2/mods-enabled/mpm_event.conf
....
<IfModule mpm_event_module>
StartServers 1
MinSpareThreads 5
MaxSpareThreads 25
ThreadLimit 32
ThreadsPerChild 12
MaxRequestWorkers 150
MaxConnectionsPerChild 0
</IfModule>
....
<IfModule mpm_event_module>
StartServers 1
MinSpareThreads 5
MaxSpareThreads 25
ThreadLimit 32
ThreadsPerChild 12
MaxRequestWorkers 150
MaxConnectionsPerChild 0
</IfModule>
....
Опишем конфигурацию простейшего сайта с публикацией HTML-странички - но только посредством HTTPS, на случай, если понадобится наладить на входе аутентификацию:
# vi /etc/apache2/sites-available/ldap0.example.net.conf
<VirtualHost ldap0.example.net:80>
ServerName ldap0.example.net
Redirect / https://ldap0.example.net/
</VirtualHost>
<VirtualHost ldap0.example.net:443>
ServerName ldap0.example.net
ErrorLog ${APACHE_LOG_DIR}/ldap0.example.net.error.log
CustomLog ${APACHE_LOG_DIR}/ldap0.example.net.access.log combined
<IfModule mod_ssl.c>
SSLEngine on
SSLProtocol all -SSLv2
SSLHonorCipherOrder on
SSLCipherSuite HIGH:MEDIUM:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:!aECDH:+SHA1:+MD5:+HIGH:+MEDIUM
SSLOptions +StrictRequire
SSLCompression off
SSLCACertificateFile /etc/ssl/apache2/intermediate.crt
SSLCertificateFile /etc/ssl/apache2/example.net.crt
SSLCertificateKeyFile /etc/ssl/apache2/example.net.key.decrypt
</IfModule>
DocumentRoot /var/www/ldap0.example.net/www
<Directory /var/www/ldap0.example.net/www>
Options None
AllowOverride None
Order Deny,Allow
Deny from all
Allow from 10.0.0.0/8
Allow from 172.16.0.0/12
Allow from 192.168.0.0/16
</Directory>
</VirtualHost>
ServerName ldap0.example.net
Redirect / https://ldap0.example.net/
</VirtualHost>
<VirtualHost ldap0.example.net:443>
ServerName ldap0.example.net
ErrorLog ${APACHE_LOG_DIR}/ldap0.example.net.error.log
CustomLog ${APACHE_LOG_DIR}/ldap0.example.net.access.log combined
<IfModule mod_ssl.c>
SSLEngine on
SSLProtocol all -SSLv2
SSLHonorCipherOrder on
SSLCipherSuite HIGH:MEDIUM:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:!aECDH:+SHA1:+MD5:+HIGH:+MEDIUM
SSLOptions +StrictRequire
SSLCompression off
SSLCACertificateFile /etc/ssl/apache2/intermediate.crt
SSLCertificateFile /etc/ssl/apache2/example.net.crt
SSLCertificateKeyFile /etc/ssl/apache2/example.net.key.decrypt
</IfModule>
DocumentRoot /var/www/ldap0.example.net/www
<Directory /var/www/ldap0.example.net/www>
Options None
AllowOverride None
Order Deny,Allow
Deny from all
Allow from 10.0.0.0/8
Allow from 172.16.0.0/12
Allow from 192.168.0.0/16
</Directory>
</VirtualHost>
Активируем "Apache2" (в процессе настройки "389-AS" мы его отключили) и SSL-модуль:
# systemctl enable apache2
# a2enmod ssl
# /etc/init.d/apache2 start
# a2enmod ssl
# /etc/init.d/apache2 start
Удаляем конфигурацию сайта "по умолчанию", подключаем нашу, проверяем синтаксическую корректность таковой и применяем изменения:
# rm /etc/apache2/sites-enabled/000-default.conf
# rm /etc/apache2/sites-enabled/default-ssl.conf
# ln -s /etc/apache2/sites-available/ldap0.example.net.conf /etc/apache2/sites-enabled/ldap0.example.net.conf
# apache2 -t && /etc/init.d/apache2 reload
# rm /etc/apache2/sites-enabled/default-ssl.conf
# ln -s /etc/apache2/sites-available/ldap0.example.net.conf /etc/apache2/sites-enabled/ldap0.example.net.conf
# apache2 -t && /etc/init.d/apache2 reload
Настройка LDAP-сервера "389-DS".
В нагруженных LDAP-серверах журналы событий быстро разрастаются и становятся неудобными для частого к ним обращения, из-за размера, в "389-DS" по умолчанию ограниченного 100MB. При этом в день их запросто набегает по триста-пятьсот мегабайт. Чтобы облегчить процедуру выборки свежих данных, есть смысл уменьшить размер файлов журналов с одновременным увеличением их количества:
$ ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -W
dn: cn=config
changetype: modify
replace: nsslapd-accesslog-maxlogsperdir
nsslapd-accesslog-maxlogsperdir: 25
replace: nsslapd-accesslog-logrotationtimeunit:
nsslapd-accesslog-logrotationtimeunit: day
replace: nsslapd-accesslog-logrotationtime:
nsslapd-accesslog-logrotationtime: 1
replace: nsslapd-accesslog-maxlogsize
nsslapd-accesslog-maxlogsize: 50
^d
changetype: modify
replace: nsslapd-accesslog-maxlogsperdir
nsslapd-accesslog-maxlogsperdir: 25
replace: nsslapd-accesslog-logrotationtimeunit:
nsslapd-accesslog-logrotationtimeunit: day
replace: nsslapd-accesslog-logrotationtime:
nsslapd-accesslog-logrotationtime: 1
replace: nsslapd-accesslog-maxlogsize
nsslapd-accesslog-maxlogsize: 50
^d
В старых инсталляциях "389-AS/DS" события фиксируются в журналах с указанием времени с точностью только до секунд, чего явно недостаточно для последующего анализа - в одну секунду могут несколько подключения и аутентификаций состоятся. Исправляем это, увеличивая точность фиксирования времени события до наносекунд:
$ ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -W
dn: cn=config
changetype: modify
replace: nsslapd-logging-hr-timestamps-enabled
nsslapd-logging-hr-timestamps-enabled: on
^d
changetype: modify
replace: nsslapd-logging-hr-timestamps-enabled
nsslapd-logging-hr-timestamps-enabled: on
^d
О формате даты и времени в журналах "389-DS".
Дату и время события "389-DS" фиксирует в неудобном для автоматизированной обработки американском виде, и лучше всего сразу преобразовывать его к строке ("YYYY-MM-DD HH:MM:SS.SSS") в формате "ISO8601", одинаково хорошо воспринимаемом людьми и обрабатываемом СУБД вроде "SQLite" - в "Bash" это делается примерно так:
$ echo "[20/Dec/2019:10:09:40.377075423 +0700] conn=1948" | sed -E "s/\[([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4}):([0-9]{2}):([0-9]{2}):([0-9]{2})\.([0-9]+)\s([\+0-9]+)\].*/\3-"$(echo \\2 | date +%m)"-\1 \4:\5:\6.\7 \8/"
2019-12-20 10:09:40.377075423 +0700
Пишем скрипт анализа данных и визуализации статистики.
Подход с простым чтением журнала события "389-DS", вычленением интересующих нас данных, формирования по ним статистики с последующим непосредственным отображением оказался крайне неэффективен - слишком большие у находящегося под наблюдением сервиса файлы журналов. Самая большая часть времени - до 99% от общего - уходит на чтение файлов журналов. Пришлось придумать способ ступенчатой загрузки интересующих нас событий (вначале всех, и потом лишь свежие данные, порционно) в промежуточную БД "SQLite", а уже оттуда SQL-запросами вынимать нужную нам статистику.
Устанавливаем APT-пакет с нужной мини-СУБД:
# aptitude install sqlite3
Заготавливаем месторасположение для скрипта, "базы данных" статистики и результирующей web-страницы::
# mkdir -p /usr/local/etc/ds-logstat
# mkdir -p /var/log/ds-logstat
# mkdir -p /var/www/ldap0.example.net/www
# mkdir -p /var/log/ds-logstat
# mkdir -p /var/www/ldap0.example.net/www
Составляем конфигурационный файл, описывающий месторасположение всех задействованных ресурсов:
# vi /usr/local/etc/ds-logstat/main.conf
# Имя LDAP-инстанса, журналы событий которого будут читаться
# (автоматически будет добавлен префикс "/var/log/slapd-")
INSTANCE="ldap0-example-net"
# Директория для файлов "базы данных" сервиса
DATADIR="/var/log/ds-logstat"
# Задаём имя файла "базы данных"
DBF="${DATADIR}/main.db"
# Формируем строки запуска и конфигурации "SQLite3"
SQLEXE=`which sqlite3`
SQLCFG="/usr/local/etc/ds-logstat/.sqliterc"
# Задаём региональный формат для вычленения даты из журнала событий "389-DS"
export LC_TIME="en_US.UTF-8"
# Явно указываем использовать как разделитель символ "переноса строки"
IFS=$'\n'
# (автоматически будет добавлен префикс "/var/log/slapd-")
INSTANCE="ldap0-example-net"
# Директория для файлов "базы данных" сервиса
DATADIR="/var/log/ds-logstat"
# Задаём имя файла "базы данных"
DBF="${DATADIR}/main.db"
# Формируем строки запуска и конфигурации "SQLite3"
SQLEXE=`which sqlite3`
SQLCFG="/usr/local/etc/ds-logstat/.sqliterc"
# Задаём региональный формат для вычленения даты из журнала событий "389-DS"
export LC_TIME="en_US.UTF-8"
# Явно указываем использовать как разделитель символ "переноса строки"
IFS=$'\n'
Подготавливаем для программного клиента "SQLite" набор параметров, которые сделают подключение немного стабильнее и быстрее:
# vi /usr/local/etc/ds-logstat/.sqliterc
-- Suppress output service messages
.output /dev/null
-- Lock DB file, making it unavailable for other
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA journal_mode = MEMORY;
.output stdout
.output /dev/null
-- Lock DB file, making it unavailable for other
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA journal_mode = MEMORY;
.output stdout
Сам bash-скрипт сбора и визуализации статистики получился немаленький - пора бы уже на "Python" или "Go" начать такое писать - но все этапы я прокомментировал в коде, так что должно быть всё очевидно:
# vi /usr/local/bin/ds-logstat.sh && chmod ug+x /usr/local/bin/ds-logstat.sh
#!/bin/bash
# Загружаем переменные конфигурации
. /usr/local/etc/ds-logstat/main.conf
# Принимаем аргументы
LEVEL=${1} # {total|last}
# Описываем функцию пакетной записи
function sql-drop {
SQL_PACK_LINE=""
# Добавляем к пакету данных сведения о подключениях
if [ -s "${BUF_CONNS_PACK}" ] ; then
SQL_PACK_LINE="INSERT OR REPLACE INTO ll_conns (date, conn, ip) VALUES "$(cat "${BUF_CONNS_PACK}" | sed 's/,$//')";"
echo -n "" > "${BUF_CONNS_PACK}"
fi
# Добавляем к пакету данных сведения об аутентификациях
if [ -s "${BUF_BINDS_PACK}" ] ; then
SQL_PACK_LINE=${SQL_PACK_LINE}"INSERT OR REPLACE INTO ll_binds (date, conn, binddn) VALUES "$(cat "${BUF_BINDS_PACK}" | sed 's/,$//')";"
echo -n "" > "${BUF_BINDS_PACK}"
fi
# Отправляем SQL-пакет на запись в "базу данных"
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "${SQL_PACK_LINE}"
[ "${?}" -eq "0" ] || { echo -e "\nError writing to SQLite database.\n"; return ${?}; }
return ${?}
}
# Создаём временные буферные файлы
BUF_CONNS_PACK=$(mktemp --tmpdir=/dev/shm);
BUF_BINDS_PACK=$(mktemp --tmpdir=/dev/shm);
# Создаём заготовки таблиц в БД
# (для отладки) ${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "DROP TABLE IF EXISTS ll_conns; DROP TABLE IF EXISTS ll_binds;"
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "CREATE TABLE IF NOT EXISTS ll_conns (date INT PRIMARY KEY, conn INT, ip TEXT); CREATE TABLE IF NOT EXISTS ll_binds (date INT PRIMARY KEY, conn INT, binddn TEXT);"
# Задаём ограничение выборки данных по времени создания записи (в минутах)
if [ "${LEVEL}" == "total" ] ; then
CTLIMIT="10080" # week
elif [ "${LEVEL}" == "last" ] ; then
CTLIMIT="480" # 8 hour
else
CTLIMIT="120" # 2 hour
fi
# Выбираем и перебираем строки журнала из текстового файла
COUNT="0" ; COUNT_PACK="0"
find /var/log/dirsrv/slapd-${INSTANCE} -type f -mmin -${CTLIMIT} -name 'access*' -exec grep -ie 'fd=.*slot=.*connection\|conn=.*bind.*dn=.*' {} \; | \
while read STRING ; do
# Показываем статистику по чтению данных
(( COUNT ++ ))
let "FLAG_COUNT = ${COUNT} % 10"
[ "${FLAG_COUNT}" == "0" ] && echo -n "."
# Извлекаем и преобразуем к формату "ISO8601" дату фиксирования события
LOG_DATE=$(echo "${STRING}" | awk 'match($0, /^\[(.*)\]\s/, result) {print result[1];}' | sed -E "s/([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4})(:)/\3-"$(echo \\2 | date +%m)"-\1 /")
# Проверяем корректность распознавания даты и времени события
date "+%Y-%m-%d %H:%M:%S.%N %z" -d "${LOG_DATE}" > /dev/null 2>&1 || continue
# Переводим строку даты и времени в число наносекунд от "unix-эпохи"
# (SQLite3 не поддерживает операции со строковыми датами)
LOG_DATE_NSEC=$(date +%s -d "${LOG_DATE}")$(date +%N -d "${LOG_DATE}")
# Обрабатываем только строки в интересующем нас диапазоне даты и времени
# (в секунде - 1000000000 наносекунд; вычисляем разницу в минутах)
DATEDIFF=$(( ( $(date +%s)$(date +%N) - ${LOG_DATE_NSEC} ) / 60000000000 ))
[[ "${CTLIMIT}" -eq "0" || "${DATEDIFF}" -le "${CTLIMIT}" ]] || continue
# Откидываем записи с датами "из будущего"
[[ "${DATEDIFF}" -lt "0" ]] && continue
# Выявляем событие установления подключения
if echo "${STRING}" | grep -qi 'fd=.*slot=.*connection' ; then
CONN_CID=$(echo "${STRING}" | awk 'match($0, /\sconn=([0-9]*)\s/, result) {print result[1];}')
CONN_IP=$(echo "${STRING}" | awk 'match($0, /\sfrom\s([0-9\.]*)\s/, result) {print result[1];}')
# Не обрабатываем некоторые события
[ "${CONN_IP}" == "127.0.0.1" ] && continue
# Добавляем в пакет данных сведения сопоставления IP-адреса клиента и номера соединения
echo -n "("${LOG_DATE_NSEC}", "${CONN_CID}", '"${CONN_IP}"')," >> "${BUF_CONNS_PACK}"
(( COUNT_PACK ++ ))
else # (если это не событие подключения - значит аутентификации (BIND)
BIND_CID=$(echo "${STRING}" | awk 'match($0, /\sconn=([0-9]*)\s/, result) {print result[1];}')
BIND_DN=$(echo "${STRING}" | awk 'match($0, /\sdn=\"(.*)\"\s/, result) {print result[1];}')
# Не обрабатываем некоторые события
[ "${BIND_DN}" == "cn=Replication Manager,cn=config" ] && continue
# Маскируем идентификаторы пользователей (анонимизируем статистику)
BIND_DN=$(echo "${BIND_DN}" | sed -e 's/^uid=.*,ou=People/uid=*****,ou=People/I')
# Добавляем в пакет данных сведения сопоставления номера соединения клиента и BindDN
echo -n "("${LOG_DATE_NSEC}", "${BIND_CID}", '"${BIND_DN}"')," >> "${BUF_BINDS_PACK}"
(( COUNT_PACK ++ ))
fi
# Регулярно сбрасываем в БД накапливаемые данные
let "FLAG_COUNT_PACK = ${COUNT_PACK} % 100"
if [[ "${COUNT_PACK}" != "0" && "${FLAG_COUNT_PACK}" == "0" ]] ; then
COUNT_PACK="0"
echo -n "*"
sql-drop
fi
done
echo
# Сбрасываем в "базу данных" накопившиеся данные
sql-drop
# Начинаем рисовать HTML-страницу
unset BODY 2>/dev/null
BODY=${BODY}"<!DOCTYPE HTML>\n\
<html>\n\
<head>\n\
<meta charset=\"utf-8\">\n\
<font face=\"sans-serif\">\n\
<meta http-equiv=\"refresh\" content=\"600\">\n\
<title>LDAP Usage Statistics (Example)</title>\n\
<style>\n\
a {text-decoration: none;}\n\
a:visited {color: blue;}\n\
table {text-align: left; border-collapse: collapse; margin: 0pt; padding: 0pt;}\n\
tr {border: none; margin: 0pt; padding: 0pt;}\n\
th {background-color: #F5F5F5; border: #C0C0C0 1px solid; text-align: center; font-size: 90%; margin: 0pt; padding-left: 4pt; padding-top: 2pt; padding-right: 4pt; padding-bottom: 2pt;}\n\
td {border: #C0C0C0 1px solid; text-align: left; vertical-align: top; margin: 0pt; padding-left: 4pt; padding-top: 2pt; padding-right: 4pt; padding-bottom: 2pt;}\n\
</style>\n\
</head>\n\
<body>\n\
<h2 style=\"color: #333333;\">LDAP Usage Statistics (Example)</h2>\n\
<a href=\"https://ldap0.example.net\" style=\"font-size: 120%;\">ldap0.example.net</a> <span style=\"color: #808080;\">(this server)</span><br />\n\
<a href=\"https://ldap1.example.net\" style=\"font-size: 120%;\">ldap1.example.net</a>
\n"
BODY=${BODY}"<h3 style=\"color: #333333;\">Today:</h3>\n"
BODY=${BODY}"<table style='font-size: 12pt'>\n"
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<th>Client IP</th><th>Bind DN</th><th>Amount</th>\n"
BODY=${BODY}"</tr>\n"
# Вычисляем дату и время за сутки до выборки, в наносекундах
NSTIME_ONE_DAY="$(date +%s -d '-24 hour')000000000"
# Вычленяем перечень IP-адресов и перебираем их
CONN_IPS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT ip FROM ll_conns WHERE date > "${NSTIME_ONE_DAY}" ORDER BY ip ASC;" 2>/dev/null)
for CONN_IPS_ITEM in ${CONN_IPS[@]} ; do
# (дата и время в наносекундах; аутентификация ожидается не позже, чем через минуту после соединения)
# Вычленяем перечень DN-ов и перебираем их
BINDS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT binddn FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND ll_conns.date > "${NSTIME_ONE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
for BINDS_ITEM in ${BINDS[@]} ; do
# Выборкой из БД подсчитываем количество подключений BinDN-а с IP-адреса
BINDS_COUNT=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(binddn) FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND binddn = '"${BINDS_ITEM}"' AND ll_conns.date > "${NSTIME_ONE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
# Непустые записи показываем
if [ ! -z "${BINDS_ITEM}" ] ; then
# Отображаем данные в табличной строке
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<td>$(echo "${CONN_IPS_ITEM}")</td>"
BODY=${BODY}"<td>$(echo "${BINDS_ITEM}" | tr '[:upper:]' '[:lower:]')</td>"
BODY=${BODY}"<td style=\"text-align: right;\">$(echo "${BINDS_COUNT}")</td>"
BODY=${BODY}"</tr>\n"
fi
done
done
BODY=${BODY}"</table>\n"
# Вычисляем дату и время за трое суток до выборки, в наносекундах
NSTIME_THREE_DAY="$(date +%s -d '-3 days')000000000"
# Выясняем, имеются ли данные за указанный период
CHECK_EXST=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(ip) FROM ll_conns WHERE date > "${NSTIME_THREE_DAY}" AND date < "${NSTIME_ONE_DAY}";" 2>/dev/null)
if [ "${CHECK_EXST}" -ge "1" ] ; then
BODY=${BODY}"<h3 style=\"color: #333333;\">Last three days:</h3>\n"
BODY=${BODY}"<table style='font-size: 12pt'>\n"
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<th>Client IP</th><th>Bind DN</th><th>Amount</th>\n"
BODY=${BODY}"</tr>\n"
# Вычленяем перечень IP-адресов и перебираем их
CONN_IPS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT ip FROM ll_conns WHERE date > "${NSTIME_THREE_DAY}" ORDER BY ip ASC;" 2>/dev/null)
for CONN_IPS_ITEM in ${CONN_IPS[@]} ; do
# (дата и время в наносекундах; аутентификация ожидается не позже, чем через минуту после соединения)
# Вычленяем перечень DN-ов и перебираем их
BINDS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT binddn FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND ll_conns.date > "${NSTIME_THREE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
for BINDS_ITEM in ${BINDS[@]} ; do
# Выборкой из БД подсчитываем количество подключений BinDN-а с IP-адреса
BINDS_COUNT=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(binddn) FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND binddn = '"${BINDS_ITEM}"' AND ll_conns.date > "${NSTIME_THREE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
# Непустые записи показываем
if [ ! -z "${BINDS_ITEM}" ] ; then
# Отображаем данные в табличной строке
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<td>$(echo "${CONN_IPS_ITEM}")</td>"
BODY=${BODY}"<td>$(echo "${BINDS_ITEM}" | tr '[:upper:]' '[:lower:]')</td>"
BODY=${BODY}"<td style=\"text-align: right;\">$(echo "${BINDS_COUNT}")</td>"
BODY=${BODY}"</tr>\n"
fi
done
done
BODY=${BODY}"</table>\n"
fi
# Вычисляем дату и время за неделю до выборки, в наносекундах
NSTIME_SEVEN_DAY="$(date +%s -d '-7 days')000000000"
# Выясняем, имеются ли данные за указанный период
CHECK_EXST=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(ip) FROM ll_conns WHERE date > "${NSTIME_SEVEN_DAY}" AND date < "${NSTIME_THREE_DAY}";" 2>/dev/null)
if [ "${CHECK_EXST}" -ge "1" ] ; then
BODY=${BODY}"<h3 style=\"color: #333333;\">Last week:</h3>\n"
BODY=${BODY}"<table style='font-size: 12pt'>\n"
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<th>Client IP</th><th>Bind DN</th><th>Amount</th>\n"
BODY=${BODY}"</tr>\n"
# Вычленяем перечень IP-адресов и перебираем их
CONN_IPS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT ip FROM ll_conns WHERE date > "${NSTIME_SEVEN_DAY}" ORDER BY ip ASC;" 2>/dev/null)
for CONN_IPS_ITEM in ${CONN_IPS[@]} ; do
# (дата и время в наносекундах; аутентификация ожидается не позже, чем через минуту после соединения)
# Вычленяем перечень DN-ов и перебираем их
BINDS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT binddn FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND ll_conns.date > "${NSTIME_SEVEN_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
for BINDS_ITEM in ${BINDS[@]} ; do
# Выборкой из БД подсчитываем количество подключений BinDN-а с IP-адреса
BINDS_COUNT=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(binddn) FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND binddn = '"${BINDS_ITEM}"' AND ll_conns.date > "${NSTIME_SEVEN_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
# Непустые записи показываем
if [ ! -z "${BINDS_ITEM}" ] ; then
# Отображаем данные в табличной строке
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<td>$(echo "${CONN_IPS_ITEM}")</td>"
BODY=${BODY}"<td>$(echo "${BINDS_ITEM}" | tr '[:upper:]' '[:lower:]')</td>"
BODY=${BODY}"<td style=\"text-align: right;\">$(echo "${BINDS_COUNT}")</td>"
BODY=${BODY}"</tr>\n"
fi
done
done
BODY=${BODY}"</table>\n"
fi
BODY=${BODY}"<br /><span style=\"font-size: 90%; color: #808080;\">Collection time: "$(date +'%Y-%m-%d %H:%M')" (updated hourly)</span>\n"
BODY=${BODY}"<h3 style=\"color: #333333;\">For LDAP customers:</h3>\n"
BODY=${BODY}" <a href=\"ss-rootCA.crt\" target=\"_blank\">Self-Signed Root CA certificat</a><br />"
BODY=${BODY}"\n</body>\n</html>"
echo -e "${BODY}" 2>/dev/null > /var/www/ldap0.example.net/www/index.html
chown www-data:www-data /var/www/ldap0.example.net/www/index.html
# Удаляем временные буферные файлы
rm -f "${BUF_CONNS_PACK}"
rm -f "${BUF_BINDS_PACK}"
# Удаляем ненужные для статистики сведения
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "DELETE FROM ll_conns WHERE date < "${NSTIME_SEVEN_DAY}";" 2>/dev/null
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "DELETE FROM ll_binds WHERE date < "${NSTIME_SEVEN_DAY}";" 2>/dev/null
exit ${?}
# Загружаем переменные конфигурации
. /usr/local/etc/ds-logstat/main.conf
# Принимаем аргументы
LEVEL=${1} # {total|last}
# Описываем функцию пакетной записи
function sql-drop {
SQL_PACK_LINE=""
# Добавляем к пакету данных сведения о подключениях
if [ -s "${BUF_CONNS_PACK}" ] ; then
SQL_PACK_LINE="INSERT OR REPLACE INTO ll_conns (date, conn, ip) VALUES "$(cat "${BUF_CONNS_PACK}" | sed 's/,$//')";"
echo -n "" > "${BUF_CONNS_PACK}"
fi
# Добавляем к пакету данных сведения об аутентификациях
if [ -s "${BUF_BINDS_PACK}" ] ; then
SQL_PACK_LINE=${SQL_PACK_LINE}"INSERT OR REPLACE INTO ll_binds (date, conn, binddn) VALUES "$(cat "${BUF_BINDS_PACK}" | sed 's/,$//')";"
echo -n "" > "${BUF_BINDS_PACK}"
fi
# Отправляем SQL-пакет на запись в "базу данных"
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "${SQL_PACK_LINE}"
[ "${?}" -eq "0" ] || { echo -e "\nError writing to SQLite database.\n"; return ${?}; }
return ${?}
}
# Создаём временные буферные файлы
BUF_CONNS_PACK=$(mktemp --tmpdir=/dev/shm);
BUF_BINDS_PACK=$(mktemp --tmpdir=/dev/shm);
# Создаём заготовки таблиц в БД
# (для отладки) ${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "DROP TABLE IF EXISTS ll_conns; DROP TABLE IF EXISTS ll_binds;"
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "CREATE TABLE IF NOT EXISTS ll_conns (date INT PRIMARY KEY, conn INT, ip TEXT); CREATE TABLE IF NOT EXISTS ll_binds (date INT PRIMARY KEY, conn INT, binddn TEXT);"
# Задаём ограничение выборки данных по времени создания записи (в минутах)
if [ "${LEVEL}" == "total" ] ; then
CTLIMIT="10080" # week
elif [ "${LEVEL}" == "last" ] ; then
CTLIMIT="480" # 8 hour
else
CTLIMIT="120" # 2 hour
fi
# Выбираем и перебираем строки журнала из текстового файла
COUNT="0" ; COUNT_PACK="0"
find /var/log/dirsrv/slapd-${INSTANCE} -type f -mmin -${CTLIMIT} -name 'access*' -exec grep -ie 'fd=.*slot=.*connection\|conn=.*bind.*dn=.*' {} \; | \
while read STRING ; do
# Показываем статистику по чтению данных
(( COUNT ++ ))
let "FLAG_COUNT = ${COUNT} % 10"
[ "${FLAG_COUNT}" == "0" ] && echo -n "."
# Извлекаем и преобразуем к формату "ISO8601" дату фиксирования события
LOG_DATE=$(echo "${STRING}" | awk 'match($0, /^\[(.*)\]\s/, result) {print result[1];}' | sed -E "s/([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4})(:)/\3-"$(echo \\2 | date +%m)"-\1 /")
# Проверяем корректность распознавания даты и времени события
date "+%Y-%m-%d %H:%M:%S.%N %z" -d "${LOG_DATE}" > /dev/null 2>&1 || continue
# Переводим строку даты и времени в число наносекунд от "unix-эпохи"
# (SQLite3 не поддерживает операции со строковыми датами)
LOG_DATE_NSEC=$(date +%s -d "${LOG_DATE}")$(date +%N -d "${LOG_DATE}")
# Обрабатываем только строки в интересующем нас диапазоне даты и времени
# (в секунде - 1000000000 наносекунд; вычисляем разницу в минутах)
DATEDIFF=$(( ( $(date +%s)$(date +%N) - ${LOG_DATE_NSEC} ) / 60000000000 ))
[[ "${CTLIMIT}" -eq "0" || "${DATEDIFF}" -le "${CTLIMIT}" ]] || continue
# Откидываем записи с датами "из будущего"
[[ "${DATEDIFF}" -lt "0" ]] && continue
# Выявляем событие установления подключения
if echo "${STRING}" | grep -qi 'fd=.*slot=.*connection' ; then
CONN_CID=$(echo "${STRING}" | awk 'match($0, /\sconn=([0-9]*)\s/, result) {print result[1];}')
CONN_IP=$(echo "${STRING}" | awk 'match($0, /\sfrom\s([0-9\.]*)\s/, result) {print result[1];}')
# Не обрабатываем некоторые события
[ "${CONN_IP}" == "127.0.0.1" ] && continue
# Добавляем в пакет данных сведения сопоставления IP-адреса клиента и номера соединения
echo -n "("${LOG_DATE_NSEC}", "${CONN_CID}", '"${CONN_IP}"')," >> "${BUF_CONNS_PACK}"
(( COUNT_PACK ++ ))
else # (если это не событие подключения - значит аутентификации (BIND)
BIND_CID=$(echo "${STRING}" | awk 'match($0, /\sconn=([0-9]*)\s/, result) {print result[1];}')
BIND_DN=$(echo "${STRING}" | awk 'match($0, /\sdn=\"(.*)\"\s/, result) {print result[1];}')
# Не обрабатываем некоторые события
[ "${BIND_DN}" == "cn=Replication Manager,cn=config" ] && continue
# Маскируем идентификаторы пользователей (анонимизируем статистику)
BIND_DN=$(echo "${BIND_DN}" | sed -e 's/^uid=.*,ou=People/uid=*****,ou=People/I')
# Добавляем в пакет данных сведения сопоставления номера соединения клиента и BindDN
echo -n "("${LOG_DATE_NSEC}", "${BIND_CID}", '"${BIND_DN}"')," >> "${BUF_BINDS_PACK}"
(( COUNT_PACK ++ ))
fi
# Регулярно сбрасываем в БД накапливаемые данные
let "FLAG_COUNT_PACK = ${COUNT_PACK} % 100"
if [[ "${COUNT_PACK}" != "0" && "${FLAG_COUNT_PACK}" == "0" ]] ; then
COUNT_PACK="0"
echo -n "*"
sql-drop
fi
done
echo
# Сбрасываем в "базу данных" накопившиеся данные
sql-drop
# Начинаем рисовать HTML-страницу
unset BODY 2>/dev/null
BODY=${BODY}"<!DOCTYPE HTML>\n\
<html>\n\
<head>\n\
<meta charset=\"utf-8\">\n\
<font face=\"sans-serif\">\n\
<meta http-equiv=\"refresh\" content=\"600\">\n\
<title>LDAP Usage Statistics (Example)</title>\n\
<style>\n\
a {text-decoration: none;}\n\
a:visited {color: blue;}\n\
table {text-align: left; border-collapse: collapse; margin: 0pt; padding: 0pt;}\n\
tr {border: none; margin: 0pt; padding: 0pt;}\n\
th {background-color: #F5F5F5; border: #C0C0C0 1px solid; text-align: center; font-size: 90%; margin: 0pt; padding-left: 4pt; padding-top: 2pt; padding-right: 4pt; padding-bottom: 2pt;}\n\
td {border: #C0C0C0 1px solid; text-align: left; vertical-align: top; margin: 0pt; padding-left: 4pt; padding-top: 2pt; padding-right: 4pt; padding-bottom: 2pt;}\n\
</style>\n\
</head>\n\
<body>\n\
<h2 style=\"color: #333333;\">LDAP Usage Statistics (Example)</h2>\n\
<a href=\"https://ldap0.example.net\" style=\"font-size: 120%;\">ldap0.example.net</a> <span style=\"color: #808080;\">(this server)</span><br />\n\
<a href=\"https://ldap1.example.net\" style=\"font-size: 120%;\">ldap1.example.net</a>
\n"
BODY=${BODY}"<h3 style=\"color: #333333;\">Today:</h3>\n"
BODY=${BODY}"<table style='font-size: 12pt'>\n"
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<th>Client IP</th><th>Bind DN</th><th>Amount</th>\n"
BODY=${BODY}"</tr>\n"
# Вычисляем дату и время за сутки до выборки, в наносекундах
NSTIME_ONE_DAY="$(date +%s -d '-24 hour')000000000"
# Вычленяем перечень IP-адресов и перебираем их
CONN_IPS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT ip FROM ll_conns WHERE date > "${NSTIME_ONE_DAY}" ORDER BY ip ASC;" 2>/dev/null)
for CONN_IPS_ITEM in ${CONN_IPS[@]} ; do
# (дата и время в наносекундах; аутентификация ожидается не позже, чем через минуту после соединения)
# Вычленяем перечень DN-ов и перебираем их
BINDS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT binddn FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND ll_conns.date > "${NSTIME_ONE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
for BINDS_ITEM in ${BINDS[@]} ; do
# Выборкой из БД подсчитываем количество подключений BinDN-а с IP-адреса
BINDS_COUNT=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(binddn) FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND binddn = '"${BINDS_ITEM}"' AND ll_conns.date > "${NSTIME_ONE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
# Непустые записи показываем
if [ ! -z "${BINDS_ITEM}" ] ; then
# Отображаем данные в табличной строке
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<td>$(echo "${CONN_IPS_ITEM}")</td>"
BODY=${BODY}"<td>$(echo "${BINDS_ITEM}" | tr '[:upper:]' '[:lower:]')</td>"
BODY=${BODY}"<td style=\"text-align: right;\">$(echo "${BINDS_COUNT}")</td>"
BODY=${BODY}"</tr>\n"
fi
done
done
BODY=${BODY}"</table>\n"
# Вычисляем дату и время за трое суток до выборки, в наносекундах
NSTIME_THREE_DAY="$(date +%s -d '-3 days')000000000"
# Выясняем, имеются ли данные за указанный период
CHECK_EXST=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(ip) FROM ll_conns WHERE date > "${NSTIME_THREE_DAY}" AND date < "${NSTIME_ONE_DAY}";" 2>/dev/null)
if [ "${CHECK_EXST}" -ge "1" ] ; then
BODY=${BODY}"<h3 style=\"color: #333333;\">Last three days:</h3>\n"
BODY=${BODY}"<table style='font-size: 12pt'>\n"
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<th>Client IP</th><th>Bind DN</th><th>Amount</th>\n"
BODY=${BODY}"</tr>\n"
# Вычленяем перечень IP-адресов и перебираем их
CONN_IPS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT ip FROM ll_conns WHERE date > "${NSTIME_THREE_DAY}" ORDER BY ip ASC;" 2>/dev/null)
for CONN_IPS_ITEM in ${CONN_IPS[@]} ; do
# (дата и время в наносекундах; аутентификация ожидается не позже, чем через минуту после соединения)
# Вычленяем перечень DN-ов и перебираем их
BINDS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT binddn FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND ll_conns.date > "${NSTIME_THREE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
for BINDS_ITEM in ${BINDS[@]} ; do
# Выборкой из БД подсчитываем количество подключений BinDN-а с IP-адреса
BINDS_COUNT=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(binddn) FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND binddn = '"${BINDS_ITEM}"' AND ll_conns.date > "${NSTIME_THREE_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
# Непустые записи показываем
if [ ! -z "${BINDS_ITEM}" ] ; then
# Отображаем данные в табличной строке
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<td>$(echo "${CONN_IPS_ITEM}")</td>"
BODY=${BODY}"<td>$(echo "${BINDS_ITEM}" | tr '[:upper:]' '[:lower:]')</td>"
BODY=${BODY}"<td style=\"text-align: right;\">$(echo "${BINDS_COUNT}")</td>"
BODY=${BODY}"</tr>\n"
fi
done
done
BODY=${BODY}"</table>\n"
fi
# Вычисляем дату и время за неделю до выборки, в наносекундах
NSTIME_SEVEN_DAY="$(date +%s -d '-7 days')000000000"
# Выясняем, имеются ли данные за указанный период
CHECK_EXST=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(ip) FROM ll_conns WHERE date > "${NSTIME_SEVEN_DAY}" AND date < "${NSTIME_THREE_DAY}";" 2>/dev/null)
if [ "${CHECK_EXST}" -ge "1" ] ; then
BODY=${BODY}"<h3 style=\"color: #333333;\">Last week:</h3>\n"
BODY=${BODY}"<table style='font-size: 12pt'>\n"
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<th>Client IP</th><th>Bind DN</th><th>Amount</th>\n"
BODY=${BODY}"</tr>\n"
# Вычленяем перечень IP-адресов и перебираем их
CONN_IPS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT ip FROM ll_conns WHERE date > "${NSTIME_SEVEN_DAY}" ORDER BY ip ASC;" 2>/dev/null)
for CONN_IPS_ITEM in ${CONN_IPS[@]} ; do
# (дата и время в наносекундах; аутентификация ожидается не позже, чем через минуту после соединения)
# Вычленяем перечень DN-ов и перебираем их
BINDS=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT DISTINCT binddn FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND ll_conns.date > "${NSTIME_SEVEN_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
for BINDS_ITEM in ${BINDS[@]} ; do
# Выборкой из БД подсчитываем количество подключений BinDN-а с IP-адреса
BINDS_COUNT=$(${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "SELECT COUNT(binddn) FROM ll_conns INNER JOIN ll_binds ON ll_conns.conn = ll_binds.conn WHERE ip = '"${CONN_IPS_ITEM}"' AND binddn = '"${BINDS_ITEM}"' AND ll_conns.date > "${NSTIME_SEVEN_DAY}" AND (ll_binds.date - ll_conns.date) >= 0 AND (ll_binds.date - ll_conns.date) < 60000000000;" 2>/dev/null)
# Непустые записи показываем
if [ ! -z "${BINDS_ITEM}" ] ; then
# Отображаем данные в табличной строке
BODY=${BODY}"<tr>\n"
BODY=${BODY}"<td>$(echo "${CONN_IPS_ITEM}")</td>"
BODY=${BODY}"<td>$(echo "${BINDS_ITEM}" | tr '[:upper:]' '[:lower:]')</td>"
BODY=${BODY}"<td style=\"text-align: right;\">$(echo "${BINDS_COUNT}")</td>"
BODY=${BODY}"</tr>\n"
fi
done
done
BODY=${BODY}"</table>\n"
fi
BODY=${BODY}"<br /><span style=\"font-size: 90%; color: #808080;\">Collection time: "$(date +'%Y-%m-%d %H:%M')" (updated hourly)</span>\n"
BODY=${BODY}"<h3 style=\"color: #333333;\">For LDAP customers:</h3>\n"
BODY=${BODY}" <a href=\"ss-rootCA.crt\" target=\"_blank\">Self-Signed Root CA certificat</a><br />"
BODY=${BODY}"\n</body>\n</html>"
echo -e "${BODY}" 2>/dev/null > /var/www/ldap0.example.net/www/index.html
chown www-data:www-data /var/www/ldap0.example.net/www/index.html
# Удаляем временные буферные файлы
rm -f "${BUF_CONNS_PACK}"
rm -f "${BUF_BINDS_PACK}"
# Удаляем ненужные для статистики сведения
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "DELETE FROM ll_conns WHERE date < "${NSTIME_SEVEN_DAY}";" 2>/dev/null
${SQLEXE} -batch -init "${SQLCFG}" "${DBF}" "DELETE FROM ll_binds WHERE date < "${NSTIME_SEVEN_DAY}";" 2>/dev/null
exit ${?}
Таким образом, из кода уже наверняка понятно, что первоначально нужно запустить длительный процесс загрузки всех доступных данных:
# /usr/local/bin/ds-logstat.sh total
В процессе наладки может понадобиться запросить загрузку данных за восемь последних часов:
# /usr/local/bin/ds-logstat.sh last
Ну а обычный запрос по расписанию раз в час будет забирать данные последних двух часов:
# /usr/local/bin/ds-logstat.sh
Я наладил запуск скрипта ежечасно в обычном режиме (забор данных последних двух часов):
# vi /etc/crontab
....
# Regularly start collecting statistics on the LDAP "389-DS"
0 */1 * * * root /usr/local/bin/ds-logstat.sh 1>/dev/null &
# Regularly start collecting statistics on the LDAP "389-DS"
0 */1 * * * root /usr/local/bin/ds-logstat.sh 1>/dev/null &