Perl для системного администрирования

         

Двоичные журналы


Иногда не просто писать программы, имеющие дело с журналами. Вместо приятных на вид, легко анализируемых текстовых строк, некоторые средства ведения журналов создают двоичные файлы патентованного формата, которые нельзя проанализировать при помощи одной строки кода на Perl. К счастью, Perl не боится таких напастей. Рассмотрим несколько подходов к работе с такими файлами. Ниже приведены два различных примера двоичных журналов: файл wtmp в Unix и журналы событий NT/2000.

В главе 3 «Учетные записи пользователей» мы упоминали о регистрации пользователей для работы на машине с Unix. В большинстве Unix-систем регистрация в системе и завершение работы с ней регистрируются в файле wtmp. Если нужно узнать о «привычках» пользователя относительно регистрации (например, на какой машине он обычно регистрируется?), то необходимо обратиться к этому файлу.

В NT/2000 журналы событий играют более обобщенную роль. Они используются для регистрации практически всех событий, происходящих на машине, включая регистрацию работы пользователей, сообщения операционной системы, события системы безопасности и т. д. Их роль аналогична роли службы syslog в Unix.

Использование unpackQ

В Perl существует функция unpack(), специально созданная для анализа двоичных данных и структур. Давайте посмотрим, как ее можно использовать для работы с файлами wtmp. Формат wtmp отличается в различных системах Unix. В следующем примере мы будем иметь дело с файлами wtmp из SunOS 4.1.4 и Digital Unix 4.0, поскольку они достаточно просты. Вот как выглядит текстовое представление первых трех записей в файле wtmp из SunOS 4.1.4.

Если вы еще не знакомы со структурой этого файла, подобный «ASCII-дамп» (так он называется) данных выглядит как некий полуслучайный мусор. Как же нам разобраться со структурой этого файла?

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




Все программы операционной системы, читающие и пишущие в файл wtmp, берут определение файла из одного коротенького включаемого файла С, который скорее всего расположен в /usr/include/utmp.h. Интересующая нас часть файла начинается с определения структуры данных С, которая будет использоваться для хранения информации. Если мы поищем struct utmp {, то найдем нужную нам часть. Строки, следующие за struct utmp {, определяют каждое поле этой структуры. Каждая из этих строк сопровождается комментарием в стиле /* се/ .

Чтобы почувствовать, насколько могут отличаться две различные версии wtmp, сравним отрывки из uimp.h для двух операционных систем:

SunOS 4.1.4:

struct utmp {

char ut_line[8]; /* tty name */





cnar ut_name[8]; /* user id «/

char ut__host[16]: /* nost name, if remote •/

long ut_tiine; /'* time on */ }:

Digital Unix 4.0:

slr.ict jT^ip !

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

Давайте построим шаблон по кусочкам, принимая за основу структуру на С из файла utmp.h в SunOS. Многие буквы разрешается использовать в шаблонах и здесь рассказывается именно о них, но вообще-то вы должны обратиться к разделу pack() из руководстваperlfunc за подробными разъяснениями. Создание шаблонов - не всегда простое занятие; периодически компиляторы С дополняют поля структуры для того, чтобы выровнять их по требуемой границе памяти. Команда pstruct, входящая в состав Perl, часто помогает справиться с подобными особенностями.

С нашим форматом данных таких сложностей не возникает. Посмотрите на анализ файла utmp.h (табл. 9.1).

Таблица 9.1. Преобразование кода на С из utmp.h в шаблон unpack( )




Код на С



Шаблон unpack()



Буква шаблона/повтор

char ut_line[8];

A8

Строка ASCII (дополнена пробелами) длиной 8 байт

char ut_name[8];

A8

Строка ASCII (дополнена пробелами) длиной 8 байт

char ut_host[16];

A16

Строка ASCII (дополнена пробелами) длиной 16 байт

long ut_time;

1

«Длинное» целое значение со знаком (может и не совпадать с размером значения «long» на конкретной машине)
<


Шаблоны созданы, теперь используем их в настоящей программе:

шаблон, который мы собираемся передать unpack()

Stemplate = "А8 А8 А16 1";

ft используем pack(), чтобы определить размер (в байтах) каждой записи

Srecordsize = length(pack($template,()));

ft открываем файл

open(WTMP, "/var/adrc/wtmp") or die "Невозможно открыть wtT.D:$! \i":

# считываем его по одной записи

while (read(WTMP,SrecordSrecordsize)) {

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

($tty. $narne. $host $time)=unoack($temolate. Srecorc).

# специальным образом обрабатываем записи

if (Sname and substr($name. 0.1) г.е "\0"){

print "$tty:$name:$nobt : "

scalar localtime($time),"\n"; }

else <

print "$tty:(logout).(logout):",

scalar localtime(Stime),"\n";

i i

}

tt закрываем файл close(WTMP);

Вот как выглядит вывод этой маленькой программы:

":reboot::Мол Nov 17 15:24:30 1997

:0:dnb::0:Mon Nov 17 15:35:08 1997

ttyp8:user:host.mcs.anl.go:Mon Nov 17 18:09:49 1997

ttyp6:dnb:limbO-114.ccs.ne:Mon Nov 17 19:03:44 1997

ttyp6:(logout):(logout):Mon Nov 17 19:26:26 1997

ttyp1:dnb:traal-22.ccs.neu:Mon Nov 17 23:47:18 1997

ttyp1:(logout):(logout):Tue Nov 18 00:39:51 1997

Приведем пару комментариев:

В SunOS завершение работы с терминалов определенного типа отмечается символом с кодом 0 в первой позиции, поэтому:

if ($name and substr($name,1,1) ne "\0")

{

read() принимает в качестве третьего аргумента количество байт, которые нужно прочесть. Вместо того чтобы жестко определить размер записи как «32», мы воспользовались удобным свойством функции pack(). Если этой функции передать пустой список, то она возвращает пустую или заполненную пробелами строку размером, совпадающим с размером записи. Это позволяет передать функции pack() произвольный шаблон и узнать ее размер:

$recordsize = length(pack($template,()));



Вызов внешней программы



Работа с файлами wtmp - настолько распространенная задача, что в Unix есть специальная команда под названием last, предназначенная для вывода двоичных файлов в формате, удобном для человека. Вот образец ее вывода, показывающий примерно те же данные, что и в предыдущем примере:



dnb ttyp6 traal-22.ccs.neu Mon Nov 17 23:47 - 00:39 (00:52)

dnb ttypl traal-22.ccs.neu Mon Nov 17 23.47 - 00:39 (GO'52

dnb ttyps l:mbo-114.ccs.ne Mon Nov 17 19:03 - 19:26 (00'22)

user ttypS host.mcs.anl.go Mon Nov 17 18:09 - crash (27+11:50)

dnb '0 :0 Mo Nov 17 15:35 - 17:35 (4»P2 PC '

reboot " Mon Nov 17 15:24

Мы свободно можем вызывать программы, такие как last из Perl. Эта программа выводит все уникальные имена пользователей, найденные в текущем файле wtmp:

open( LAST. ' Slasrexec j"') or 02 "Невозможно Запустить

Sastexec :$!';:' while(<LAST>){

$user = (solit)[0]:

print "$user"."\n" unless exisfs $seen{$use"};

$seen{$user}='': } close(LAST) or die "Невозможно правильно закрыть канал:$!\п":

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

Но вы можете рассчитывать на то, что команда last, читающая данный формат, будет присутствовать на вашей системе, независимо от каких-либо изменений формата. В случае применения метода unpack() придется создать и поддерживать различные строки шаблонов для каждого формата файла wtmp, который планируется использовать.

Самый большой недостаток такого метода по сравнению с unpack() -это увеличение сложности анализа полей, выполняемого в программе. В случае с unpack() все необходимые поля извлекаются автоматически. При использовании last можно столкнуться с данными, которые сложно разобрать при помощи split() или регулярных выражений:

user console Weo Oct 14 20:35 - 20:37 (00:01)

user pts/12 208.243,191.21 Wed Oct 14 09:19 - 18:12 (08:53)

user pts/17 208.243.191,21 Tue Oct 13 13:36 - 17:09 (03:33)

reboot system boot Tue Oct 6 14:13

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



Использование API операционной системы для ведения журналов

Давайте перейдем к службе Event Log Service Windows NT/2000, чтобы рассмотреть этот подход. Как мы уже упоминали, в этом случае, к сожалению, журналы хранятся не в текстовых файлах. Самый лучший и единственный поддерживаемый способ, позволяющий добраться до этих данных, заключается в применении набора специальных API-вызовов. Большинство пользователей для получения этих данных полагаются на программу Event Viewer.

К счастью, существует модуль, написанный Джесси Доэрти (Jesse Dougherty) (и обновленный Мартином Поли (Martin Pauley) и Бретом Гиддингсом (Bret Giddings)), обеспечивающий простой доступ к API-вызовам Event Log. Вот простая программа, которая выводит список событий из журнала System в формате, подобном syslog. Позже мы подробно рассмотрим более сложную версию этой программы.

use Win32::EventLog;

П

у каждого события есть тип, вот как выглядят самые

и распространенные типы %type = (1 => "ERROR",

2 => "WARNING", 4 =<• "INFORMATION", 8 => "AUDIT_SUCCESS", 16 => "AUDIT_FAILURE");

# если это значение установлено, мы также получаем полный текст

# каждого сообщения при каждом вызове

Read() $Win32::EventLog::GetMessageText = 1;

fl открываем журнал событий

System Slog = new Win32::EventLog("System") or die

 "Невозможно открыть системный журнал:$~Е\п";

# читаем его по одной записи, начиная с первой

while ($log->Read((EVENTLOG_SEQUENTIAL_READ|EVENTLOG_FORWARDS_READ),

1,$entry)){

print scalar localtime($entry->{TimeGenerated})." ";

print $entry->{Computer}."[".($entry->{EventID} &

Oxffff)."] ";

print Sentry->{Source}.":".$type{$entry->{EventType}}; print $entry->{Message};

}

В NT/2000 существуют также утилиты, работающие из командной строки, такие как last, выводящие события из журнала в текстовом виде. Позже мы посмотрим на эти утилиты в действии.






Содержание раздела