ВЛАДИМИР ЧИЖИКОВ
Сага о биллинге, или Считаем трафик на FreeBSD (ng_ipacct + perl+ MySQL)
Часть 1
Рано или поздно перед каждым системным администратором встает вопрос подсчета интернет-трафика. И тут уже не важны причины – проверить ли провайдера или проконтролировать, какой объем трафика израсходовал подключенный пользователь, и выставить счет. Конечно, систем биллинга сейчас много. И найти их в Интернете не проблема, если задаться целью это сделать. Но многие хорошие и гибкие системы учета трафика, как правило, дороги или имеют достаточно сложный интерфейс, а некоторые затрудняют использование тех же Squid или oops.
В общем, из этой ситуации мне виделось два выхода – либо писать что-то свое, либо переделывать существующее. Исходя из соображений, что в компании уже создан корпоративный сервер статистики с единой системой авторизации и прочего, у меня не возникало особого желания прикручивать, например, тот же NetAms к нему, хотя последний и не лишен ряда достоинств и преимуществ.
Да и многим организациям, которым предоставлен доступ в Интернет нашей компанией, не нужно то море статистики, которое выдает система биллинга.
В итоге решение создать свою систему учета перевесило все остальное. Необходимо было просто посчитать, сколько трафика прошло через интерфейс.
Так все начиналось. Развилось это в большой набор скриптов, которые не только считали объем трафика, но и определяли, локальный он или нет, какова доля локального трафика от общего объема, а также позволяли просмотреть все задействованные порты и протоколы, и сколько именно трафика пришлось на каждый из них.
Что ж, скрипты это хорошо, но без самого главного, сердца всей этой системы, программы ng_ipacct, автором которой является Роман Палагин, ничего бы и не было. Эта программа, если так можно выразиться, является вариацией на тему ipacctd.
Ipacctd работает с ipfw, а вот ng_ipacct уже с NETGRAPH, плюс она работает как модуль ядра. Почему именно ng_ipacct, а не просто ipacctd? NETGRAPH имеет ряд преимуществ. Наверняка многие замечали, как отличается объем трафика, который считаешь при помощи ipfw, и тот, который прислал провайдер со счетом. Объясняется все достаточно просто, ipfw отрабатывает не все пакеты, поступившие в bpf – пакетный фильтр системы. NETGRAPH выступает в данном случае как промежуточное звено, как маленькое кольцо, через которое проходят пакеты, считаются и перенаправляются дальше. Одно из его преимуществ – он работает на уровне ядра, используя минимум времени процессора и памяти. Тонкости работы и его возможности описаны в статье «Все о NETGRAPH» Арчи Коббса (перевод статьи на русский язык можно посмотреть на http://www.opennet.ru/docs/RUS/netgraph_freebsd/index.html). Мы же разберем, как установить ng_ipacct и сам NETGRAPH. Что ж, приступим.
Перед тем, как делать какие-либо шаги, скажу, что все это протестировано на FreeBSD 5.2.1-RELEASE-p10, 5.3-RELEASE-p4, 4.10-RELEASE-p3, 4.11-RELEASE. Стоит обратить внимание, что с переходом на верссию 5.3 и выше потребуется пересборка ng_ipacct. Также пересобрать его потребуется и при каждой новой компиляции ядра (на 5-й ветке).
Таким образом, исходные данные есть. Возьмемся за netgraph. Загружать в память его можно, используя два метода: запускать нужные модули при старте либо вкомпилировать сразу же в ядро. Мне предпочтителен последний вариант. Для этого нужно просто перекомпилировать ядро с его поддержкой.
Делается все достаточно просто. Рассмотрим на примере для FreeBSD-4.10.
Первым делом идем в /usr/src/sys/i386/conf/ и смотрим LINT-файл:
# cd /usr/src/sys/i386/conf/
# less LINT
options NETGRAPH #netgraph(4) system
options NETGRAPH_ASYNC
options NETGRAPH_BPF
options NETGRAPH_CISCO
options NETGRAPH_ECHO
options NETGRAPH_ETHER
options NETGRAPH_FRAME_RELAY
options NETGRAPH_HOLE
options NETGRAPH_IFACE
options NETGRAPH_KSOCKET
options NETGRAPH_L2TP
options NETGRAPH_LMI
# MPPC compression requires proprietary files (not included)
#options NETGRAPH_MPPC_COMPRESSION
options NETGRAPH_MPPC_ENCRYPTION
options NETGRAPH_ONE2MANY
options NETGRAPH_PPP
options NETGRAPH_PPPOE
options NETGRAPH_PPTPGRE
options NETGRAPH_RFC1490
options NETGRAPH_SOCKET
options NETGRAPH_TEE
options NETGRAPH_TTY
options NETGRAPH_UI
options NETGRAPH_VJC
|
То есть опций достаточно много и есть из чего выбрать. Для избежания проблем с разного рода устройствами можно их все включить в наше ядро, но в самом простом случае (считаем только с ethernet-устройства) нам потребуются только такие опции в ядре:
options NETGRAPH
options NETGRAPH_ETHER
options NETGRAPH_SOCKET
options NETGRAPH_TEE
|
Дальнейшие наши действия заключаются в компиляции ядра.
# config SKIF
– конфигурирование файла ядра, в моем случае это SKIF.
Если ошибок в файле не было выявлено, то она выдаст:
Don"t forget to do a ``make depend""
Kernel build directory is ../../compile/SKIF
|
Это маленькое напоминание о том, что необходимо сделать make depend, и где это сделать.
# cd ../../compile/SKIF && make depend && make && make install && make clean && rehash
– полный список команд, необходимый для того, чтобы перейти и скомпилировать наше ядро. Достаточно удобный, если никаких ошибок не ожидается, но если возникнут, то выяснить, на каком этапе они произошли, будет проблематично. Посему команды лучше выполнять по отдельности.
После всех этих манипуляций перезагрузим сервер.
# shutdown -r now
После перезагрузки мы получаем чистое ядро с поддержкой NETGRAPH.
Что ж, часть работы выполнена. Устанавливаем ng_ipacct. Первым делом смотрим порты, имеющиеся в системе. Там присутствует только ipacct:
# cd /usr/ports/
# make search key=ipacct
Port: ipacctd-1.46
Path: /usr/ports/net-mgmt/ipacctd
Info: IP accounting using divert socket
Maint: skv@FreeBSD.org
B-deps:
R-deps:
|
Сам же ng_ipacct можно найти здесь: ftp://ftp.wuppy.net.ru/pub/FreeBSD/local/kernel/ng_ipacct.
На сервере присутствуют версии как для четвертой, так и для пятой ветки FreeBSD. Они неидентичны, так реализация NETGRAPH в этих версиях FreeBSD заметно отличается. Основное отличие – синхронизация. В RELENG_4 она осуществляется через уровни прерываний, о которых можно почитать в man 9 spl. Весь код netgraph должен выполняться на уровне splnet.
Все граничные ноды, осуществляющие связь между NETGRAPH и другой подсистемой, например, ng_ether, переходят в уровень splnet, перед тем как отправить данные в граф. Если это невозможно, то данные ставятся в очередь и позже раздаются в нужной последовательности. Любые внешние вызовы, которые работают с netgraph, тоже должны первым делом вызывать splnet(). Таким образом, в одну единицу времени может существовать только один контекст выполнения NETGRAPH, и конфликтовать ему не с кем.
В RELENG_5 ядро многонитевое (multithreads), и синхронизация netgraph осуществляется с помощью мьютексов (блокировок, используемых для реализации гарантированной исключительности) и атомарных операций. Ноды передают друг другу объекты (items) различных типов: данные (mbufs), сообщения (ng_mesg), ссылки на функции. У объекта есть атрибут – reader или writer.
Нода может одновременно обрабатывать сколько угодно reader items или только одну writer item. По умолчанию объекты с данными – readers, а все остальные writers. Однако это можно указать как на уровне конкретных объектов, так и на уровне хуков (hooks).
Важным является то, что в момент, когда выполняется код внутри ноды, тред не держит ни одного мьютекса, что позволяет граничным нодам вызывать методы других подсистем, избегая LOR (Lock order reversal – блокирования устанавливаемых изменений).
То есть это грозит нам как минимум тем, что один и тот же ng_ipacct не будет работать на разных ветках FreeBSD.
Что ж, скачиваем и распаковываем.
# tar xfvz ng_ipacct-20040109.tar.gz
# cd ng_ipacct/
# make && make install && make clean && rehash
Все, что нужно в нем прописать, это:
- Прослушиваемые интерфейсы INTERFACES=«ed0» – здесь это будет ed0. Для того чтобы указать более одного интерфейса, нужно перечислить их через запятую.
- VERBOSE=1 – уровень расширенного вывода статистики, по умолчанию в скрипте 1, которая выведет нам дополнительно, кроме IP-адреса источника и назначения количества пакетов и байт, еще и порты и протоколы, которые использовались. Стоит обратить внимание, что названия протоколов, если указан расширенный вывод (VERBOSE=1), будут отображены в числовом, а не буквенном виде. Что обозначает каждый номер, можно посмотреть в /etc/protocols/.
- THRESHOLD=50000 – количество записей, которые будут храниться программой в памяти. На этот параметр стоит обратить особое внимание, так как неправильно подобранный размер threshold может привести к потери части данных или даже к панике ядра. Это возможно по той причине, что ng_ipacct работает на уровне ядра и ей не будет доступна полностью вся память, имеющаяся на машине, а только малая часть, зарезервированная непосредственно под ядро. В результате переполнения памяти, выделенной системе на ядро, может произойти паника со всеми вытекающими последствиями, в лучшем случае остановка сервера и потеря записей относительно трафика, прошедшего через него. Поэтому если у вас менее 128 Мб памяти, стоит себя ограничить 4000-5000 записями и чаще снимать статистику, чтобы не потерять нужные данные.
Для снятия статистики в ng_ipacct необходимо проделать следующее: передать данные в checkpoint (контрольную точку), вывести статистику при помощи show из контрольной точки и очистить контрольную точку.
Вот так это делается для интерфейса rl0:
# ipacctctl rl0_ip_acct:rl0 checkpoint
# ipacctctl rl0_ip_acct:rl0 show
# ipacctctl rl0_ip_acct:rl0 clear
После show вы увидите все пакеты, которые проходили через интерфейс. Статистика выводится в достаточно удобном CISCO-формате:
ip_источника port_источника ip_назначения port_назначения протокол пакетов байт
Обычный режим имеет несколько другой формат вывода:
ip_источника ip_назначения пакетов байт
Стоит отметить, что имеется проблема с кодировками в man ipacctctl, просмотреть его удастся разве что в браузере.
Но это легко вылечить:
# zcat /usr/share/man/man8/ipacctctl.8.gz | nroff -man | gzip > /usr/share/man/cat8/ipacctctl.8.gz
Если вас интересует исключительно возможность поднять ng_ipacct, то на этом можно остановиться.
Мы же проследуем дальше, ибо этого мне было мало. Мне требовалось, чтобы все данные хранились в базе MySQL для каждого хоста и интерфейса, разнесенные по дате и времени.
Вот теперь опишем основные требования, которые были предъявлены биллингу (системе учета трафика – кому как больше нравится).
- Система должна хранить данные не только поинтерфейсно, но и по хостам, чтобы быстро разделить трафик между разными хостами/роутерами, с которых считывается статистика. При этом количество интерфейсов различно и их наименование может совпадать (куда ни глянь, почти везде есть rl0 или fxp0 или ed0).
- База должна разделять трафик за текущий и предыдущий месяцы самостоятельно и иметь возможность предоставить пользователю отчет за каждый из них, чтобы таблицы бессмысленно не росли. Гораздо проще обработать одну маленькую за месяц, чем одну большую за год с выборкой за месяц. Просмотр статистики за предыдущие месяцы может быть необходим для отчета перед начальством или выставления счета клиенту, если такой имеется.
- В случае недоступности MySQL-сервера необходимо хранить полученные данные локально до тех пор, пока не будет устранена причина недоступности сервера базы данных. После чего данные автоматически должны быть перенесены в базу при следующем сеансе.
- Единый конфигурационный файл с удобным и интуитивно понятным содержанием.
- Графический или веб-интерфейс для удобного отображения статистики.
- Неплохо было бы, чтобы система, где необходимо, отличала локальный трафик от внешнего.
В принципе этот список можно продолжить, приведенные же требования являются ключевыми.
Итак, создадим, исходя из этого, наш конфигурационный файл. Все свои скрипты и программы я размещаю в папки, расположенные в /usr/local/script. В дальнейшем я буду отталкиваться именно от такого расположения папки, если у вас путь будет отличен от моего, внесите необходимые коррективы.
Создаем рабочую папку со скриптами:
# mkdir -p /usr/local/script/ng_stat
# chown skif:wheel /usr/local/script/ng_stat
Смена владельца выполняется с целью защитить систему, в случае если наши скромные потуги в области программирования окажутся небезопасны. По крайней мере никто не увидит, что написано внутри скрипта, а значит, ломать его будет труднее.
# mkdir /usr/local/script/ng_stat/etc
# mkdir /usr/local/script/ng_stat/bin
Этим мы создали папки, где будут лежать наши конфигурационные и исполняемые файлы.
Что ж, создадим конфигурационный файл и внесем первые параметры. По мере продвижения мы будем дополнять его нужными параметрами.
# cd /usr/local/script/ng_stat/etc
Здесь мы создадим файл настройки ng_stat.conf и внесем следующие строки.
# Имя сервера, где находится база данных статистики
server_db = freebsd
# Имя базы данных, где будет сохраняться статистика
db_name = ng_stat
# Имя пользователя для доступа к базе
db_user = nguser
# Пароль для доступа к базе
db_pass = rfn.if
# Имя хоста, с которого снимается статистика
listen_host = freebsd2
# Имена интерфейсов, которые прослушиваются на компьютере. Указывать через запятую
listen_interfaces = rl0
Думаю пояснений к строкам приведенного конфигурационного файла не нужно.
Вначале откажемся от поставляемого в комплекте с ng_ipacct скрипта для его старта и остановки. Лучше напишем свой:
# cd /usr/local/script/ng_stat/bin
# touch ng_stat_start.pl
Данный скрипт будет служить нам скелетом для последующих.
Итак, первое, что мы сделаем, это объявим основной набор переменных:
#!/usr/bin/perl -w
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
Все переменные созвучны описанным в конфигурационном файле и являются глобальными для него. Внеся заранее значение «test» в них, мы избежали проблемы получить в самом неподходящем месте undef. Но обратите внимание, что прослушиваемые интерфейсы обозначены не переменной, а массивом. Сделано это потому, что интерфейсов может быть несколько, а не один.
Почему были внесены такие непонятные значения переменных? Объясняется все достаточно просто. Во-первых, сюда можно внести значения реальных данных по умолчанию, которые будут считываться. Во-вторых, если на этапе отладки будут проблемы, изменив значения, вы сможете выяснить, с какой переменной у вас непорядок и где.
open (CONFIG, "/usr/local/script/ng_stat/etc/ng_stat.conf");
while (<CONFIG>) {
}
close (CONFIG);
Этими строками открывается конфигурационный файл и при помощи while полностью считывается и закрывается. Обратите внимание, что в данном случае используется полное указание пути к файлу в явном виде, а впоследствии будем указывать его неявно, через переменные.
Что ж, первое, что нам нужно сделать, это разобрать строки, которые поочередно считывает while до тех пор, пока не дойдет до конца файла. Но среди полезной информации конфигурационный файл несет в себе комментарии. От них нужно избавиться. Для этого в Perl имеется мощнейшие инструменты поиска в строках/словах. Один из них – конструкция вида m/шаблон/ограничитель, им и воспользуемся, условившись, что комментарием будет символ #:
$comment = '#';
if(/^$comment/) {
print "Комментарий\n";
}
else {
# разбор строк, не ограниченных комментарием
}
Объясним конструкцию if ... else: если в начале строки присутствует символ комментария, то на экран будет выведено сообщение «Комментарий», в противном случае строка пойдет по else. Вывод сообщений о наличии комментариев нам необходим только на этапе отладки. Кстати, можете проверить, как скрипт работает, впоследствии он будет закомментирован. Но этого мало, необходимо разобрать и полезную строку.
($param,$arg) = split("=",$_);
chomp $param;
chomp$arg;
$param =~ s/s//g;
$arg =~ s/s//g;
Для разбора использовалась функция split, которая на основе разделителя «=», заданного еще в конфигурационном файле, разбила все полезные строки на две части: параметр и аргумент. Чтобы избавиться от пробельных символов, используется оператор замены s/шаблон/замена/ограничитель.Так как необходимо избавиться от пробельных символов, а не поменять их на что-то другое, мы не используем параметр «замена», оставляя его пустым. Модификатор s означает любой пробельный символ.
Перед этим были убраны из обоих переменных символы перевода строки при помощи chomp.
Если в строке присутствуют не только символы пробела, но и табуляции или если их несколько, то придется прибегнуть к следующей конструкции:
$param =~ s/[s ]+//g;
$arg =~ s/[s ]+//g;
Теперь необходимо присвоить каждой объявленной переменной ее истинное значение, находящееся в конфигурационном файле. В этом нам поможет конструкция следующего вида:
if ($param eq "server_db"){
$serverdb = $arg;
}
Объясним. Если левая часть полученной из файла строки соответствует server_db (смотрим наш конфигурационный файл), то правая часть присвоится соответственной переменной.
Но у нас же есть еще несколько значений параметра в одной из строк. Их мы должны, предварительно разобрав, занести в массив.
#!/usr/bin/perl -w
use DBI;
use POSIX ":sys_wait_h";
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
my $iface_set = "no";
my @ng_modules;
my $ng_modules_def = "netgraph,ng_ether,ng_socket, ї
ng_tee,ng_ipacct";
my$threshold = 5000;
#########################
# Читаем конфигурационный файл.
#########################
open (CONFIG, "/usr/local/script/ng_stat/etc/ng_stat.conf");
while (<CONFIG>) {
$comment = '#';
if(/^$comment/) {
# print "Коментарий\n";
}
else {
($param,$arg) = split("=",$_);
chomp $param;
chomp $arg;
my $razdel = "";
$param =~ s/[\s\t]+/$razdel/g;
$arg =~ s/[\s\t]+/$razdel/g;
if ($param eq "server_db"){
$serverdb = $arg;
}
if ($param eq "db_name"){
$dbname = $arg;
}
if ($param eq "db_user") {
$dbuser = $arg;
}
if ($param eq "db_pass") {
$dbpass = $arg;
}
if ($param eq "table_auth") {
$table_auth = $arg;
}
if ($param eq "table_protocols") {
$table_proto = $arg;
}
if ($param eq "listen_host") {
$listen_host = $arg;
}
if ($param eq "listen_interfaces") {
my $coma = ',';
if (defined $arg) {
$iface_set = "ok";
if ($arg ne ""){
if ($arg =~ m/$coma/ ) {
@listen_interf=split($coma,$arg);
}
else {
@listen_interf = $arg;
}
}
}
}
if ($param eq "ng_modules") {
my $coma = ',';
if ($arg =~ m/$coma/ ){
@ng_modules = split($coma,$arg);
}
else {
@ng_modules = split ($coma,$ng_modules_def);
}
}
}
}
close (CONFIG);
if (!defined $listen_interf[0]) {
print "Установите, пожалуйста, в режим прослушивания хотя бы один интерфейс.\n";
}
else {
&check_kld_modules;
&listening;
}
Как видите, мы считали все параметры, и в случае если интерфейс по какой-либо причине установлен не будет, то на экран будет выдано сообщение об этом. А если все нормально, то в массив будут внесены необходимые имена интерфейсов (например, rl0, rl1,rl2,fxp0) и после проверки массива @listen_interf на наличие в нем не пустых значений будут выполнены подпрограммы: &check_kld_ modules и &listening.
Первая проверяет, какие из обязательных модулей загружены. При необходимости будет проведена их загрузка.
Вторая включает режим прослушивания интерфейсов.
Рассмотрим первую.
subcheck_kld_modules {
my @modules;
my $pid;
my $ng_module_cfg;
my $chk_ng_file = "/tmp/ng_file";
my $check_ng = 'kldstat -v | grep ng';
$check_ng = "$check_ng";# " > $chk_ng_file";
my $check_netgraph = 'kldstat -v | grep netgraph';
$check_netgraph = "$check_netgraph";#" >> $chk_ng_file";
# $pid = fork;
@modules =split ("\n", `$check_ng && $check_netgraph`);
my $mod;
if (defined $modules[0]) {
foreach my $modules (@modules) {
$modules=~ s/\d+//g;
if ($modules =~ s/.ko//g) {
#
}
else {
$modules =~ s/[\s\t]+//g;
$mod = "$mod $modules ";
}
}
chop $mod;
foreach my $ng_modules (@ng_modules) {
if ($mod=~m/$ng_modules/g){
# print "$mod содержит $ng_modules\n";
}
else {
my ($pid,$kid);
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"\n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
}
}
}
else {
foreach my $ng_modules (@ng_modules) {
my ($pid,$kid);
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"\n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
}
}
}
Сначала объявляются действующие только в переделах этого модуля массивы и переменные. В нашем случае это @modules, куда будут заноситься все модули netgraph, присутствующие в ядре или загруженные на данный момент. $check_netgraph и $check_ng переменные, в которых записаны команды, проверки загруженных модулей ядра.
Команда эта достаточно проста и имеет вид:
# kldstat –v
234 dummynet
235 if_gif
236 ipfw
237 if_loop
238 ng_async
239 ng_bpf
4 1 0xc272d000 4000 ng_ipacct.ko
Containsmodules:
Name
246 ng_ipacct
|
Как вы можете заметить, вывод немаленький, поэтому пришлось его урезать. Нам нужны не все модули, а только те, которые имеют отношение к NETGRAPH. Этим и займутся переменные, когда их используют как значения для оператора exec.
Чтобы получить список загруженных модулей, используется split и обратные кавычки, в качестве разделителя выступает символ переноса строки:
@modules =split (" ", `$check_ng && $check_netgraph`);
Дальше пойдем по проверенному пути, а именно – выясним, имеются ли в массиве хоть какие-то данные.
Если полученный массив не пустой, то мы выполним проверку, какие модули нам необходимо подгрузить для работы.
В данном случае информация о том, из какого файла был загружен модуль (linux.ko, logo_server.ko или что-то другое), не нужна. Также не нужны ID загруженных модулей. Для их удаления используется все тот же m//:
$modules=~ s/d+//g;
«d» означает любой цифровой символ.
После удаления ID проверяется, что присутствует в выводе – информация о том, из какого модуля загрузился файл или сам модуль. На файл указывает присутствие расширения «.ko» в строке. А потому все полученные строки, где присутствует «.ko», подлежат удалению. В листинге вы видите, что на месте совпадения if с «.ko» стоит комментарий. Если хотите, можете провести синтаксический разбор и вывести на экран имя того модуля, который был загружен вручную.
Нам же интересно только то, что находится после else. Вывод kldstat имеет пять колонок (Id,Refs,Address,Size,Name). Все они разделены между собой пробельными символами. К тому же первые колонки пусты и заполнены именно этими самыми пробельными символами. Так как нам необходима только одна колонка Name, то необходимо удалить все пробельные символы. Для удобства дальнейших манипуляций мы заносим отобранные элементы в одну строку:
if ($modules =~ s/.ko//g) {
#
}
else {
$modules =~ s/[s ]+//g;
$mod = "$mod $modules ";
}
Здесь необходимо остановиться и вернуться немного назад. Только что мы получили список загруженных модулей. Хорошо, но этого мало. Необходимо еще знать, какие нам нужны для работы, и если их нет – загрузить.
# Загружаемые модули NETGRAPH, необходимые для интерфейсов, которые будет обслуживать программа.
# По умолчанию загружаются следующие модули: netgraph, ng_ether,ng_socket,ng_tee,ng_ipacct
ng_modules = netgraph,ng_ether,ng_socket,ng_tee,ng_ipacct
И соответственно считать их. Для этого нужно также ввести еще несколько основных переменных. Точнее, массив и переменную.
my@ng_modules;
my$ng_modules_def = "netgraph,ng_ether,ng_socket, ng_tee,ng_ipacct";
Данные из последней переменной будут загружены в массив в случае отсутствия в конфигурационном файле хотя бы одного модуля netgraph.
Считывание необходимых к загрузке модулей нужно добавить к open ... close(CONFIG):
if ($param eq "ng_modules") {
my $coma = ",";
if ($arg =~ m/$coma/ ){
@ng_modules = split($coma,$arg);
} else {
@ng_modules = split ($coma,$ng_modules_def);
}
Теперь у нас есть необходимый список модулей. Можем проверить, нужно что-то загружать или нет.
Для этого необходимо проделать достаточно простую операцию. Проверить наличие значения каждого элемента полученного массива @ng_modules в строке $mod. Основываясь на том, есть или нет такое значение массива в строке, и будет производиться загрузка соответствующего модуля.
foreach my $ng_modules (@ng_modules) {
if ($mod=~m/$ng_modules/g){
# print "$mod содержит $ng_modules\n";
}
else {
my $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"\n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
undef $pid;
}
}
В этом примере выполнение внешней команды – загрузка модуля – производится посредством exec. Особенностью exec является то, что по выполнении этой функции производится останов программы и выход из процесса. Но так как присутствует необходимость загрузить не один модуль, то логичнее было бы использовать system. Но, по соображениям безопасности, это произвести нельзя. Решением этой проблемы является разделение программы на различные процессы. Для этого уже существует функция fork.
Немного поясню, как она работает. По выполнении функции существующий процесс разделяется на два: родительский и дочерний. Сама функция возвращает два значения в случае удачного выполнения: номер ID для дочернего процесса в родительский и 0 в дочерний. Почему ноль, а не номер полученного процесса? Потому что дочерний процесс может в любой момент времени получить ID родительского, вызвав функцию getppid. Родительский же процесс получает ID дочернего потому, что способов узнать из всего объема процессов дочерний у него просто нет, или я его не знаю.
Возможен также и третий вариант. Когда fork возвращает неопределенное значение undef. Это означает, что по какой-либо причине разделение на процессы не произошло.
Так же обратите внимание на такие строчки кода:
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их. Ошибка!" and die "Выход! ";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен! ";
}
} until $kid=$pid;
На данном этапе они не так актуальны. Здесь не сильно важна последовательность загрузки модулей и прочих процессов, но ниже, когда будет происходить запуск сбора статистики на интерфейсах, придерживание последовательности выполняемых команд будет первостепенным. Что же делает этот блок? Ключевым к нему является всего одна функция waitpid. Она аналогична в некоторой степени функции wait, но ждет завершения определенного дочернего процесса с указанным ID, в данном случае полученного при fork $pid. Функция возвращает одно из трех возможных значений:
- PID завершенного процесса.
- 0 – если флаги, что указаны, задают не блокирующий вызов, а процесс еще не завершен.
- -1 – если дочерних процессов нет.
Родительский процесс на время выполнения waitpid как бы засыпает, ожидая результата. В итоге комбинацией fork + exec + waitpid мы добиваемся жесткой очередности выполнения как всех команд, так и сопутствующего программного кода. Вот эти особенности и использовались для запуска внешних программ.
Но проверить и загрузить нужные модули мало. Нужно еще начать собирать статистику на интерфейсе. Для этого считываем параметр threshold из конфигурационного файла. Следовательно, необходимо создать глобальную переменную:
my $threshold = 5000;
Мы ее создали и присвоили значение 5000 строк по умолчанию. В конфигурационном файле можно задать и другое значение.
# Отнеситесь внимательно к выбору этого параметра. Он указывает, сколько записей будет храниться в буфере.
# По умолчанию значение равно 5000, но если у вас меньше 128 Мб памяти – уменьшите его. Значение во многом
# зависит от того, какая полоса пропускания на вашем канале, и от того насколько он загружен. Для 128 Кб и 64 Мб
# можно будет смело установить и 10000 записей, при условии снятия cтатистики хотя бы раз в 15-20 минут.
# Для канала в 2 Мбита этого времени будет уже слишком много
threshold = 5000
Теперь считаем параметр из файла:
if ($param eq "threshold") {
$threshold = $arg;
}
Все. Основные переменные заданы, конфигурационный файл на данном этапе заполнен полностью.
Что ж, приступим к запуску.
Я сразу приведу полный листинг модуля, а потом лишь поясню некоторые моменты, ибо сам по себе модуль достаточно прост, в нем только команды fork и exec.
sub listening{
my $pid;
$ngctl = "/usr/sbin/ngctl";
$ipacctctl = "/usr/local/sbin/ipacctctl";
while (@listen_interf){
$interface = shift @listen_interf;
#/usr/sbin/ngctl mkpeer ${IFACE}: tee lower right
$mkpeer = "$ngctl mkpeer $interface\: tee lower right";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Создание и подключение нового NETGRAPH-узла к уже существующему:\n $mkpeer\n";
exec "$mkpeer" or die "Ошибка создания нового узла NETGRAPH!\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl connect ${IFACE}: lower upper left
$connect = "$ngctl connect $interface\: lower upper left";
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Соединение двух NETGRAPH-узлов на интерфейсе:\n$connect\n";
exec "$connect" or die "Ошибка соединения двух NETGRAPH-узлов!\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl name ${IFACE}:lower ${IFACE}_acct_tee
$name = "$ngctl name $interface\:lower $interface\_acct_tee ";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Присвоение имени созданному узлу:\n$name\n";
exec "$name" or die "Ошибка на этапе присвоения имени созданному узлу!\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl mkpeer ${IFACE}_acct_tee: ipacct right2left ${IFACE}_in
$mkpeer = "$ngctl mkpeer $interface\_acct_tee: ipacct right2left $interface\_in";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Создание и подключение нового NETGRAPH-узла к уже существующему:\n $mkpeer\n";
exec "$mkpeer" or die "Ошибка создания нового узла NETGRAPH!\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl name ${IFACE}_acct_tee:right2left ${IFACE}_ip_acct
$name = "$ngctl name $interface\_acct_tee:right2left $interface\_ip_acct";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Присвоение имени созданному узлу:\n$name\n";
exec "$name" or die "Ошибка на этапе присвоения имени созданному узлу!\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl connect ${IFACE}_acct_tee: ${IFACE}_ip_acct: left2right ${IFACE}_out
$connect = "$ngctl connect $interface\_acct_tee: $interface\_ip_acct: left2right $interface\_out";
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Соединение двух NETGRAPH-узлов на интерфейсе:\n$connect\n";
exec "$connect" or die "Ошибка соединения двух NETGRAPH-узлов!\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE verbose $VERBOSE
$verbose = "$ipacctctl ї
$interface\_ip_acct:$interface verbose 1";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Установка режима вывода информации:\n$verbose\n";
exec "$verbose" or die "Ошибка установки режима вывода информации\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE threshold $THRESHOLD
$set_threshold = "$ipacctctl $interface\_ip_acct:$interface threshold $threshold";
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Установка THRESHOLD:\n$set_threshold\n";
exec "$set_threshold" or die "Ошибка установки параметра THRESHOLD\n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
}
}
Первые две встречающиеся переменные – исполняемые файлы для netgraph и ng_ipacct, точнее, пути к ним.
Следующим шагом является чтение из массива @listen_ interf поочередно всех занесенных туда интерфейсов и включение на них «прослушивания».
При помощи mkpeer мы создаем новый узел (nodes) к уже существующему.
При помощи connect соединяются узлы.
При помощи name присваиваем имя узлу.
Наиболее интересными является $ipacctctl $interface\_ip_ acct:$interface verbose 1 – здесь мы задаем, в каком режиме будет отображаться статистика. Нам необходим расширенный, посему устанавливаем значение 1. Должен отметить, что в man ipacctctl стоит значение on – вероятнее всего, что это ошибка, ибо такое значение не влияет на формат вывода статистики.
В последнем – $ipacctctl $interface\_ip_acct:$interface threshold $threshold – мы указываем количество записей threshold.
По ходу выполняется разветвление процессов, ожидание завершения дочернего, с последующим обнулением $pid, куда записывалось значение ID дочернего процесса. Здесь и всплывает важность waitpid для скрипта. Ибо выполняться все эти команды должны именно в строгой последовательности, а не как им заблагорассудится.
В принципе стартовый скрипт создан.
Что в итоге получилось, можно глянуть в ng_stat_start.pl (www.samag.ru/source).
Сделав при помощи chmod файл исполняемым, можно пробовать его выполнить. Тут поджидает первый неприятный сюрприз. Данный скрипт выполняется с правами root. Что ж, на данном этапе можно его запустить и с такими правами.
# sudo ./ng_stat_start.pl
Внимательно смотрите за выводом. Отсутствие «лишнего» говорит о том, что старт прошел без замечаний. Вот пример того, что скрипт выводит при старте:
Загрузка необходимого модуля ng_ether
Загрузка необходимого модуля ng_socket
Загрузка необходимого модуля ng_tee
|
Создание и подключение нового NETGRAPH-узла к уже существующему:
/usr/sbin/ngctl mkpeer fxp1: tee lower right |
Соединение двух NETGRAPH-узлов на интерфейсе:
/usr/sbin/ngctl connect fxp1: lower upper left |
Присвоение имени созданному узлу:
/usr/sbin/ngctl name fxp1:lower fxp1_acct_tee |
Создание и подключение нового NETGRAPH-узла к уже существующему:
/usr/sbin/ngctl mkpeer fxp1_acct_tee: ipacct right2left fxp1_in |
Присвоение имени созданному узлу:
/usr/sbin/ngctl name fxp1_acct_tee:right2left fxp1_ip_acct |
Соединение двух NETGRAPH-узлов на интерфейсе:
/usr/sbin/ngctl connect fxp1_acct_tee: fxp1_ip_acct: left2right fxp1_out |
Установка режима вывода информации:
/usr/local/sbin/ipacctctl fxp1_ip_acct:fxp1 verbose 1 |
Установка THRESHOLD:
/usr/local/sbin/ipacctctl fxp1_ip_acct:fxp1 threshold 7000 |
Для себя можете добавить что-либо, если необходима дополнительная информация при запуске. Для отработки разных этапов работы скрипта советую ввести конструкции типа print «Проверяем переменную $lin». Это поможет проконтролировать получение значений переменными в скрипте и получить своеобразный отладчик. Но в данном случае это полностью рабочий скрипт, а посему весь мусор отладки убран.
Запустить мало, необходимо еще и уметь остановить.
Для этого создадим похожий на ng_stat_start.pl скрипт ng_stat_stop.pl. В принципе их можно было бы объединить в один, но так проще.
Итак, содержимое абсолютно идентично первому файлу, за исключением того, что отсутствуют sub и в конструкции if ... else содержится следующее:
if (!defined $listen_interf[0]) {
print "Установите, пожалуйста, в режим прослушивания хотя бы один интерфейс.\n";
}
else {
foreach my $interface (@listen_interf){
#/usr/sbin/ngctl shutdown ${IFACE}_acct_tee:
$shutdown = "$ngctl shutdown $interface\_acct_tee:";
my $pid;
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Отключение созданных узлов на интерфейсе:\n$shutdown\n";
exec "$shutdown";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
# sleep 1;
$shutdown = "$ngctl shutdown $interface\:";
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Отключение NETGRAPH на интерфейсе:\n$shutdown\n";
exec "$shutdown";
exit;
}
}
else {
print "Фатальная ошибка ветвления!\n.................\n";
die "Разделение на процессы невозможно.\n Принудительный выход из дочернего процесса: $!\n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.\n Ошибка!" and die "Выход!\n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!\n";
}
} until $kid=$pid;
undef $pid;
}
}
Здесь производится чтение из массива всех нужных интерфейсов и последовательно выполняется для них отключение.
Посмотреть, как он выглядит полностью, можно в ng_ stat_stop.pl (www.samag.ru/source).
Теперь у нас есть скрипты для старта и остановки ng_ ipacct. Но и этого мало. Нужно сделать запуск и останов системы при включении и отключении сервера. А посему напишем простенький скриптик на shell:
#!/bin/sh
case "$1" in
start)
/usr/local/script/ng_stat/bin/ng_stat_start.pl
echo"ng_stat"
;;
stop)
/usr/local/script/ng_stat/bin/ng_stat_stop.pl
;;
*)
echo ""
echo "Usage: `basename $0` { start | stop }"
""
;;
esac
Сохраним его под названием ng_stat.sh. Когда система учета будет готова, достаточно лишь скопировать скрипт в /usr/local/etc/rc.d/, чтобы ng_stat запустился при старте или отключился при выключении питания.
Половина дела, самая важная его часть, готова. Система стартовала там, где надо и с нужными параметрами. Осталось за малым – получить статистику.
В следующей части статьи будет рассмотрено, как получить статистику от ng_ipacct, и передать ее в MySQL для последующего хранения и использования, а также приведен пример того, как получить наши результаты обратно.