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

         

Файлы узлов


Первый подход, используемый для решения проблемы связи IP-адресов с именами, является самым простым и очевидным: он заключается в создании специального файла, в котором хранится таблица соответствий IP-адресов и имен компьютеров. В системах Unix это файл /etc/ hosts, в MacOS это Macintosh HD:System Folder:Preferences:hosts и \$systemroot$\System32\Drivers\Etc\hosts в NT/2000. В NT/2000 также есть файл Imhosts, назначение которого несколько иное, но об этом мы поговорим позже. Вот как выглядит файл узлов в Unix:

127.0.0.1 localhost

192.168.1.1 everest.oog.org everest

192.168.1.2 riveridell.oog.org rivendell

Ограниченность такого подхода очень быстро становится очевидной. Если в домене oog.org TCP/IP-сети есть две связанные между собой машины, а менеджер сети хочет добавить третью, к которой надо обращаться по имени, ему придется отредактировать соответствующий файл на всех машинах. Если в oog.org появится еще одна машина, то придется поддерживать четыре файла узлов (по одному на каждой машине).

И хотя такое решение кажется совершенно непригодным, именно оно использовалось на заре появления Internet/ARPAnet. Если к сети добавлялись новые сайты, то файлы узлов необходимо было обновлять на всех сайтах, которые хотели общаться с новым. Центральный репозиторий, называемый информационным центром сети (NIC) (а точнее SRI-NIC, т. к. находился тогда на SRI), обновлял и распространял файл узлов для всей сети с именем HOSTS.TXT. Системные администраторы периодически загружали этот файл по FTP из каталога NETIN-FO на сервере SRI-NIC.

Файлы узлов используются и по сей день, несмотря на их ограниченность и замену технологиями, которые нам предстоит обсудить. Существуют ситуации, когда файлы узлов даже необходимы. Например, в SunOS машина обращается к собственному файлу /etc/hosts, чтобы определить свой IP-адрес. Файлы узлов также решают проблему «курицы и яйца», возникающую при загрузке машины. Если используемые машиной сетевые серверы имен определяются именами, то должен существовать способ определить их IP-адреса. Если же сетевые службы имен еще не действуют, то не существует способа (кроме как применять в поисках помощи широковещательные сообщения) получить эту информацию. Обычное решение - создать файл (в нем перечислено только несколько узлов), который будет использоваться для загрузки.




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

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

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

Обратите внимание на то, что анализ файлов узлов может быть очень простым:

open(HOSTS, "/etc/hosts") or die

"Невозможно открыть файл узлов:$'\п":

while (defined ($_ = <HOSTS>)) {

next if";

# пропускаем строки, являющиеся комментариями next if /"$/;

пропускаем пустые строки

удаляем комментарии и

предваряющие их пробелы ($ip, @names) = split;

die "IP-адрес Sip уже встречался!\п"

if (exists $addrs{$ip}); $addrs{$ip) = [@names]; for (@narnes){



die "Имя узла $_ уже встречалось!\п" if (exists $names{$_});

$names{$J = $ip;

}

}

close(HOSTS);

В этом примере просматривался файл /etc/hosts (пропускались пустые строки и комментарии) и были созданы две структуры данных для дальнейшего использования. Первая структура данных - это хэш списков имен узлов, ключами которого являются IP-адреса. Вот как будет выглядеть такая структура данных для рассмотренного файла узлов:

$addrs{'127.0.0.1'} = ['localhost'];

$addrs{'192.168.1.2 } = [ rivendell.oog.org'.'rivencell']:

$addrs{'192.168.1.1'} = ['everest.oog,org'.'everes:']:

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

$narres! 'localhost' ;='127.0.0.



Snakes {'eve rest'}

$names{'eve rest.oog.org'}='192.168.

$names{'rivendell'}='192.168,1.2

Snakes{'rivendell.oog.org'}= 192.168.

Заметьте, что в простой процесс анализа этого файла мы добавили дополнительную функциональность. Мы проверяем, не встречаются ли в файле повторяющиеся имена и IP-адреса (и то и другое - тревожный симптом для TCP/IP-сети). Работая с данными, относящимися к сети, используйте каждую возможность, чтобы проверить отсутствие ошибок и неверной информации. Лучше выявить ошибки в самом начале, чем потом пострадать от них, когда данные распространятся уже по всей сети. К такому важному вопросу следует еще раз вернуться в этой главе.



Генерирование файлов узлов



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

name: shimmer

address: 192.168.1.11

aliases: shim shimmy shimmydoodles

owner: David Davis

department: software

building: main

room: 909

manufacturer: Sun

model: Ultra60

name: bendir address: 192.168.1.3 aliases: ben bendoodles owner: Cindy Coltrane department: IT building: west room: 143

manufacturer: Apple model: 7500/100

name: Sulawesi address: 192.168.1.12 aliases: sula su-lee owner: Ellen Monk department: design building: main room: 1116 manufacturer: Apple model: 7500/100 name: sander address: 192.168.1.55 aliases: sandy micky mickydoo owner: Alex Rollins department: IT building: main room: 1101

manufacturer: Intergraph model: TD-325

Формат очень простой:

имя_поля: значение, причем -=- используется в качестве разделителя между записями. Вероятно, вам потребуются иные поля или у вас будет слишком много записей, чтобы хранение их в одном «плоском» файле было оправдано. И хотя в этой главе применяется один плоский файл, принципы, приведенные здесь, не зависят от используемой базы данных.

Вот пример программы, которую можно применять для анализа подобного файла и генерирования файла узлов:

Sdatafile ="./database"; $recordsep = "-=-\n";



open(DATA,Sdatafile) or die "Невозможно открыть файл с данными:$!\п"; $/=$recordsep; и подготовка к чтению файла базы данных по одной записи

print "#\n# host file - GENERATED BY $0\n# DO NOT EDIT BY HAND!\n#\n"; while (<DATA>) {

chomp; и удалить разделитель записей

П разбить на key1,value1,...bingo, хэш записей

%record = split /:\s*|Wm;

print "$record{address}\t$record{name} $record{aliases}\n"; } close(DATA);

Вот что получается:

#

# host file - GENERATED BY createhosts

« DO NOT EDIT BY HAND!

192.168.1.11 shimmer shim shimmy shimmydoodles 192.168.1.3 bendir ben bendoodles

192.168.1.12 Sulawesi sula su-lee

1Q9 1RR 1 R^ sanrlpr чяпН\/ mir.k\/ rnirk\/r1nn

Теперь посмотрим на некоторые более интересные Perl-технологии ил этого небольшого отрывка программы. Первое необычное наше действие - установка переменной $,/. Начиная отсюда, Perl считает кусочки текста, заканчивающиеся символами - = -у\ одной записью. Это означает, что while за один раз прочитает всю запись и присвоит ее переменной $ .

Вторая интересная вещь - это технология присвоения значений средствами split. Наша цель состоит в получении хэша, ключами которого являются имена полей, а значениями - значения полей. Зачем нам это надо, станет понятно позже, когда мы будем расширять пример. Первый шаг заключается в разбиении $_ на части при помощи spiuO. Массив, который получается в результате работы split(), приведен в табл. 5.1.

Таблица 5.1. Массив, возвращенный функцией split ()




Элемент



Значение

$record[0]

Name

$record[1]

Shimmer

$record[2]

Address

$record[3]

192.168. 1.11

$rocord[4]

Aliases

$record[5]

Shim shimmy shimmydoodles

$record[6]

Owner

$record[7]

David Davis

$record[8]

Department

$record[9]

Software

$record[10]

Building

$record[11]

Main

$record[12]

Room

$record[13]

909

$record[14]

Manufacturer

$record[l5]

Sun

$record[16]

Model

$record[17]

UltraGO
<


Присмотримся внимательно к содержимому списка. Начиная с элемента $record[0], у нас есть список пар ключ-значение (т. е. ключ=Пате, значение=5183, значение=192.168.1,11\п...), который следует просто присвоить хэшу. После создания хэша можно напечатать нужные нам части.



Вы уже приняли религию «Базы данных для системного администрирования»?



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

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

Если вы еще не осознали всей мудрости этого «лучшего подхода», к концу главы сами в этом убедитесь.



Проверка ошибок в процессе генерирования файла узлов



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



Sdatafile ="./dataoase":

Srecorcsep = "- = -\;n:

# по одной записи за один раз

print "#\n\# host file - GENERATED BY $0\n# DO NOT EDIT BY HAND!\n#\n";

while (<DATA>) {

chomp; n удаляем разделитель записей

# разбиваем на key1,value1,... bingo, хэш записей

%record = split /:\s*|\n/m;

# проверка на неверные имена узлов

if ($record{name} =" /["-.a-zA-ZO-9]/) {

warn "!!!! $record{name} содержит недопустимые для имени узла символы

пропускаем...\п";

next;

}

# проверка на неверные псевдонимы

if ($record{aliases} =" /[~-.a-zA-ZO-9\s]/) {

warn "!!!! $record{name} содержит недопустимые для псевдонима символы,

пропускаем...\п";

next;

}

# проверка на пропущенные адреса

if (! $record{address» {

warn "!!!! $record{name} не имеет IP-адреса, пропускаем...\n";

next;

}

tt проверка на одинаковые адреса

if (defined $addrs{$record{address}>) {

warn "!!!! Дублируется IP-адрес: $record{name} &

$addrs{$record{address}}, пропускаем...\n";

next:

}

else {

$addrs{$record{address}} = $record{name};

}

print "$record{address}\t$record{name} $record{aliases}\n";

}

ClOse(DATA);

Улучшение полученного файла узлов

Позаимствуем из главы 9 «Журналы» процесс анализа выполняемого преобразования. Мы можем автоматически добавить полезные заголовки, комментарии и разделители к получаемым данным. Вот как выглядит результат преобразования той же самой базы данных:

#

К host file - GENERATED BY createhosts3 » DO NOT EDIT BY HAND!

 Converted by David N. Blank-Edelman (dnb) on Sun Jun 7 00:43:24 1998

# number of hosts in the design department: 1.

ft number of hosts in the software department: 1.

# number of hosts in the IT department: 2.

# total number of hosts: 4

#

tt Owned by Cindy Coltrane (IT): west/143

if ($record->{aliases) =" /[~-.a-zA-ZO-9\s]/) {

warn "MM ". $record->{name} '.

содержит недопустимые для псевдонима символы, пропускаем,..\n";



next; }

tt проверка на пропущенные адреса if (!$record->{address}) {

warn "!!! ! ".$record->{name} .

не имеет IP-адреса, пропускаем,.Дл";

next; >

# проверка на совпадающие адреса

if (defined $addrs{$record->{address}}) {

warn "Ml! Дублируется IP-адрес:". $record->{name}.

" & ".$addrs{$record->{address}}.", пропускаем.. ";

next; 1 else {

$addrs{$record->{address}} = $record-><name}; }

$entries{$record->{name}} = $record; n добавляем это в хэш

tt хэшей } close(DATA);

It печатаем симпатичный заголовок

print "#\n\« host file - GENERATED BY $0\n# DO NOT EDIT BY HAND!\nff\n";

print "« Converted by $user on ".scalar(localtime). "\ntt\n";

# подсчитываем число записей для каждого отдела

Я и сообщаем об этом

foreach my Sentry (keys %entries){

Sdepts{Sentries{Sentry}->{department}}++; }

foreach my Sdept (keys %depts) {

print "n number of hosts in the

Sdept department: $depts{$dept},\n"; )

print "tt total number of hosts: ".

scalar(keys %entries). "\n#\n\n";

tt обходим в цикле все узлы, выводя комментарий и саму запись f

oreach my Sentry (keys %entries) {

print "tt Owned by ", $entries{$entry}->{owner}," (",

$entries{$entry>->{department},"): ",

Sentries{Sentry}->{building}."/",

Sentries{Sentry}->{room},"\n";

print $entries{$entry}->{address},"\t",

$entries{$entry}->{name}." ",

Sentries{Sentry}->{aliases},"\n\n"; }

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

Можно было сохранять отдельную хэш-таблицу для каждого поля (подобно тому, как это было сделано в примере needspace из главы 2 «Файловые системы»), но красота приведенного метода состоит в его поддерживаемости. Если затем понадобится добавить в базу данных поле serial_number, нам не придется менять используемый для анализа файла код, это поле само по себе появится. Недостаток же в том, что синтаксис Perl таков, что наш код выглядит более сложным, чем он есть на самом деле.



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

Чтобы построить большую структуру данных (хэш хэшей), достаточно связать каждый новый анонимный хэш с основной хэш-таблицей %entries и создать ключ со связанным с ним значением, являющимся ссылкой на этот только что заполненный анонимный хэш. Когда каждый хэш пройдет обработку, ключами %entries будут имена всех машин, а значениями - ссылки на хэш-таблицы, содержащие значения всех полей, связанных с этим именем (IP-адрес, номер кабинета и т. д.).

Вероятно, вам бы хотелось, чтобы вывод был отсортирован по IP-адресам? Никаких вопросов, просто добавьте процедуру сортировки, изменив:

foreach my Sentry (keys %entries) { на:

foreach my Sentry (sort byaddress keys %entries) { и добавьте:

sub byaddress {

@a = split(/\./,$entries{$a}->{address}):

@b = split(/\./.$e"tries{$b}-''{address)):

($a[0]<=>$b[OJ) ,!

($а[1]<=>$Ь[1!) ! i

($a[2]<=>$b[21) !|

($a[3]<=>$D[3]l;

Вот как будут выглядеть отсортированные данные:

И Owned by Cindy Coltranc (IT): west/143 192.168.1,3

 tjendir ben bei.doooles

П Owned by David Davis (software): inai'i/909

192.168.1.11 shimmer snm siimr, sniiMiydoodies

n Owned by Ellen Monk (design): rain/1116

192.168.1.12 Sulawesi sula su-lee

# Owned by Alex Rollins (IT): rnain/1101 192.168.1.55

sander sandy micky mickydoo

Сделайте так, чтобы полученные данные вам нравились. Пусть Perl поддержит ваши профессиональные и

эстетические стремления.



Внедрение системы контроля исходного кода



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



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

Регистрации всех изменений важных файлов.

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

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

Подобные средства контроля очень полезны для системного администратора. Проверка ошибок, добавленная в разделе «Проверка ошибок в процессе генерирования файла узлов», поможет справиться лишь с некоторыми опечатками и синтаксическими ошибками, но не спасет от семантических ошибок (таких как удаление важного имени узла, присвоение ошибочного IP-адреса, ошибка в имени узла). В процесс преобразования можно добавить проверку и семантических ошибок, но отловить все возможные погрешности все равно не удастся. Мы уже говорили, что защиты от дураков не существует, потому что они изобретательны.

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

Время

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

База данных

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

В качестве системы контроля исходного кода я выбрал систему контроля версий RCS. У RCS есть несколько возможностей, дружественных к Perl и системному администрированию:



RCS работает на многих платформах. Существуют версии GNU RCS 5.7 для большинства Unix-систем, Windows NT, MacOS и т. д.

У нее вполне определенный интерфейс командной строки. Все действия можно выполнить из командной строки даже в операционной системе, где в основном применяется графический интерфейс.

Ее очень легко использовать. Небольшой набор команд для выполнения основных операций можно выучить за пять минут (приложение А «Пятиминутное руководство по RCS»).

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

RCS бесплатна. Исходный код GNU-версии RCS распространяется свободно, кроме того, доступны уже скомпилированные версии для большинства систем. Исходный код RCS можно найти на ftp://\ ftp.gnu.org/gnu/rcs.

Если вы никогда не работали с RCS, загляните сначала в приложение А. Впредь будем считать, что вы знакомы с основным набором команд RCS.

Крэйг Фретер (Craig Freter) написал объектно-ориентированный модуль Res, который упрощает применение RCS из Perl. Для этого необходимо:

Загрузить модуль.

Указать, где расположены команды RCS.

Создать новый объект Res, настроить его в соответствии с файлом, который вы используете.

Вызвать необходимые методы объекта (названные в соответствии с командами RCS).

Добавим модуль Res в программу генерации файла узлов, чтобы увидеть, как он работает, и применим другой способ вывода данных. Теперь они будут записываться в определенный файл, а не в стандартный поток вывода STDOUT, как было раньше. Ниже приведен только измененный код. Пропущенные строки, представленные в виде «...», можно найти в предыдущем примере:

$outputfile="hosts.$$";

# временный файл для вывода

$target="hosts";

# где сохранить преобразованные данные

open(OUTPUT,"> Soutputfile") or die

"Невозможно записать в файл



$outputfile:$!\n";

print OUTPUT "»\n\# host file - GENERATED BY $0\n

tt DO NOT EDIT BY HAND! \nft\n"; print OUTPUT "

Converted by $user on ".scalar(localtime)."\nfl\n";

foreach my $dept (keys %depts) {

print OUTPUT "tt number of hosts in the $dept department:

$depts{$dept}.\n"; }

print OUTPUT "tt total number of hosts: ".scalar(keys %entries)."\ne\n\n";

обходим в цикле все узлы и выводим комментарий tt вместе с самой записью

foreach my Sentry (sort byaddress keys %entries) { print OUTPUT

"# Owned by ",$entries{$entry}->{owner},"

 (", $entries{$entry}->{department},"): ",

$entries{$entry}->{building},"/".

Sentries{$entry}->{room},"\n";

print OUTPUT

$entries{$entry}->{address},"\t", $entries{$entry}->{name},"

 ", Sentries{$entry}->{aliases},"\n\n"; }

Close(OUTPUT);

use Res;

 путь к RCS

Rcs->bindir(Vusr/local/bin1);

создаем новый RCS-объект

my $rcsobj = Rcs->new;

ft передаем ему имя получаемого файла

$rcsobj->file($target);

tt получаем его из репозитория RCS (он уже должен быть там)

$rcsobj->co('-!');

ft переименовываем вновь созданный файл

rename($outputfile,$target) or

die "Невозможно переименовать Soutputfile в $target:$!\n";

помещаем его в репозиторий RCS

$rcsobj->ci("-u","-m"."Converted by $user on ".scalar(localtime));

В данном примере предполагалось, что целевой файл хотя бы один раз помещался в репозиторий.

Взглянув на фрагмент записей из rlog hosts, можно понять, как действует программа:

revision 1.5

date: 1998/05/19 23:34:16; author: dnb; state: Exp; lines: +1 -1

Converted by David N. Blank-Edelman (dnb) on Tue May 19 19:34:16 1998

revision 1.4

date: 1998/05/19 23:34:05; author: eviltwin; state: Exp; lines: +1 -1

Converted by Divad Knalb-Namlede (eviltwin) on Tue May 19 19:34:05 1998

revision 1.3

date: 1998/05/19 23:33:35; author: dnb; state: Exp; lines: +20 -0

Converted by David N. Blank-Edelman (dnb) on Tue May 19 19:33:16 1998

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






Информация о модулях из этой главы


Модуль

Идентификатор на CPAN

Версия

Res CFRETER 0.09
Net: :NIS RIK а2
Data : : Dumper (входит в состав Perl)

GSAR 2.101
10 : : Socket (входит в состав Perl)

GBARR 1.20
Net:: DNS MFUHR 0.12



MIS, NIS+ и WINS


Разработчики из Sun Microsystems, осознав, что «редактирование по одному файлу на каждой машине» вряд ли можно назвать масштабируемым подходом, придумали нечто под названием желтые страны цы (Yellow Pages или YP). YP были созданы для распространения информации из конфигурационных файлов сетевого масштаба, таких как /etc/hosts, /etc/passwd, /etc/services и т. д. В этой главе мы оетано вимся на

использовании «желтых страниц» в качестве информационной службы сети для предоставления сведений о связи между именем машины и ее IP-адресом.

YP были переименованы в

Информационную службу сети (Network Information Service или NIS) в 1990 г., когда компания British Telecom (совместно с юристами) заявила о правах на торговую марку «Yellow Pages» в Великобритании. Призрак «желтых страниц» по-прежнему витает во многих Unix-системах, и сегодня для команд и библиотечных вызовов NIS употребляются имена

у peat, ypmatch, yppush и т. д. Все современные разновидности Unix поддерживают NIS. На машинах с NT можно использовать NIS для авторизации, если применить специальные библиотеки авторизации, но о существовании NIS-серверов для NT я не знаю. Мне также не известно о существовании NIS для MacOS.

В NIS администратор определяет одну или несколько машин как серверы, от которых другие машины получают клиентский сервис. Один из серверов является главным (master), все остальные - подчиненные (slave). На главном сервере хранятся основные копии текстовых файлов (например /etc/hosts или /etc/passwd), которые используют все машины. Эти файлы изменяются на главном сервере и затем распространяются на подчиненные.

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

Карты - это другое название основных файлов, уже преобразованных в формат базы данных Unix DBM и распространенных на подчиненные серверы. С подробностями этого процесса преобразования (который включает в себя makedbm и некоторые другие изменения текста) можно ознакомиться в файле Makefile, в большинстве случаев расположенном в /var/yp. Группа NIS-серверов и клиентов, использующих одни и те же карты, называется NIS-доменом.




NIS значительно упрощает администрирование сетей. Так, если в сети oog.org появляются новые машины, то добавить их в существующую сеть совсем не сложно. Менеджер сети редактирует файл узлов на главном NIS-сервере и «проталкивает» новую версию на все подчиненные. Теперь каждый клиент из NIS-домена будет «знать» о существовании новых машин. NIS обеспечивает легкость администрирования, объединенную с избыточностью (если один сервер недоступен, клиент может обратиться к другому) и распределением нагрузки (не все клиенты в сети используют один и тот же сервер).

Теперь, когда мы знакомы с теорией, можно посмотреть, как Perl помогает в вопросах, связанных с NIS. Начнем мы с процесса размещения данных в NIS. Вы, наверное, удивитесь, но это уже практически сделано. Файлы узлов, созданные в предыдущем разделе, можно импортировать в NIS, просто перенеся их в нужное место в каталоге исходных файлов на главном сервере и активировав обычные механизмы принудительной рассылки (проталкивания, push mechanisms), как правило, для этого надо выполнить make в /var/yp. По умолчанию Makefile из каталога /var/yp

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

Обычно имеет смысл создать отдельный каталог для исходных файлов NIS-карт и соответствующим образом изменить Makefile. Это позволит хранить различные данные для главного сервера и остальных членов NIS-домена. Например, вы можете не захотеть, чтобы файл /etc/passwd с главного сервера применялся в качестве файла паролей для всего домена, или наоборот.

Более интересная задача - получить данные из NIS, запрашивая NIS-сервер. Проще всего это сделать при помощи модуля Net: : NIS Рика Ха-риса (Rik Harris). Данный модуль, начиная с 1995 года, находится в состоянии альфа-версии, но, тем не менее, он вполне рабочий.

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

use Net::NIS;

NIS-домен по умолчанию



Sdomain = Net: :NIS: :yp_get_default_domain();

считываем карту

(Sstatus, $info) = Net::NIS::yp_all($domain."hosts.byname");

oreach my $name (sort keys %{$info}){

print "$name => $info->{$name}\n";

}

Сначала необходимо обратиться к локальному узлу для получения имени домена. Используя эту информацию, можно вызвать функцию Net: :NIS: :yp_all() и получить карту. Функция возвращает переменную состояния (фиктивную, как видно из сноски) и ссылку на хэш-таблицу, содержащую данные из этой карты. Мы выводим эту информацию, применяя обычный синтаксис разыменования.

Если нужно узнать только IP-адрес одного узла, эффективнее было бы запросить у сервера именно это значение:

use Net::NIS;

Shostname = "olaf.oog.org";

Sdomain = Net::NIS::yp_get_default_domain();

(Sstatus. Sinfo) = Net: :NIS: :yp_match(Sdomain. "hosts. byrame". Sfiostr. w.t.'

print $info, "\n":

Функция Net: :NIS: :yp_match() возвращает еще одну фиктивную переменную состояния и значение (скаляр), соответствующее запрашиваемой информации.

Если не удается скомпилировать модуль Net: : NIS или он просто не работает, всегда остается возможность вызвать внешнюю программу. Например, так:

(5>hosts='<nyrb K>/ypcat hosts' или так:

open(YPCAT,"<путь K>/ypcat hostsl");

while (<YPCAT>){...}

Завершит данный раздел пример, в котором применяются оба способа. Этот маленький, но полезный фрагмент программы получает список текущих NIS-серверов и опрашивает каждый из них при помощи программы yppoll. Если какой-либо из серверов не отвечает как полагается, выводится соответствующее сообщение:

use Net::NIS;

Syppollex = "/usr/etc/yp/yppoll";

 полный путь к программе yppoll

Sdomain = Net::NIS::yp_get_default_domain();

(Sstatus,Sinfo) = Net::NIS::yp_all($domain,"ypservers");

foreach my $name (sort keys %{$info}) {

Sanswer = 'Syppollex -h Sname hosts.byname':

if (Sanswer !" /has order number/) { warn "Sname отвечает неверно1\n";



}

 }



NIS+



В состав операционной системы Solaris входит NISH— следующая версия NIS. В NIS+ решены многие из наиболее серьезных проблем, которые были в NIS, в частности, проблема безопасности. К сожалению (а может быть и к счастью, т. к. NIS+ администрировать несколько NIS, NIS+nWINS 181

сложнее), NIS+ не стала столь популярной в мире Unix, как NIS. До недавнего времени она практически не поддерживалась на машинах, созданных не в Sun. NIS+ постепенно «приживается» в стандартных дистрибутивах Linux, благодаря работе Торстена Кукука (Thorsten Kukuk) (http://www.suse.de/~kukuk/nisplus/index.html), но она отнюдь не преобладает в мире Unix и ее просто не существует в NT и MacOS.

Принимая во внимание то, что NIS+ используется незначительно, говорить о ней в книге мы больше не будем. Если вам необходимо работать с NIS+ из Perl, можете применять еще один модуль Хариса - Net::NISPlus.



Windows-служба имен Интернета (WINS)



Когда в Microsoft стали использовать свой патентованный сетевой протокол NetBIOS поверх TCP/IP (NetBT), возникла необходимость решать проблему соответствия IP-адресов и имен узлов. Первым решением стало использование файла Imhosts, спроектированного по аналогии со стандартным файлом узлов. Но это было быстро дополнено NIS-подобным механизмом. В NT 3.5 появилась централизованная схема под названием Windows-служба имен Интернета (Windows Internet Name Service, или WINS). WINS несколько отличается от NIS:

WINS специализируется на распространении информации о соответствии имен узлов IP-адресам. В отличие от NIS, эта служба не применяется для централизованного распространения другой информации (например, паролей, карты портов и групп пользователей).

WINS-сервера получают большую часть из распространяемой информации от предварительно настроенных клиентов (такую информацию можно предварительно загрузить). После получения IP-адреса либо вручную, либо через протокол динамической конфигурации узла (Dynamic Host Configuration Protocol, DHCP) WINS-клиенты ответственны за регистрацию и перерегистрацию своей информации. В этом состоит различие с NIS, там клиенты запрашивают информацию у сервера и, за исключением паролей, не обновляют на нем информацию.

WINS, как и NIS, позволяет иметь несколько серверов, повышающих надежность и разделяющих загрузку, по принципу«тяни-толкай». В Windows 2000 WINS вышла из употребления (читай «от нее избавились»), вместо нее теперь используется служба динамических доменных имен (Dynamic Domain Name Service), являющаяся расширением DNS-системы, о которой мы очень скоро поговорим еще.

Учитывая, что WINS больше не существует, мы не будем приводить примеров для работы с ней. В настоящее время работа с WINS напрямую из Perl поддерживается очень слабо. Я не знаю о существовании модулей, созданных специально для работы с WINS. В этом случае лучше всего вызывать некоторые утилиты, работающие в командной строке из Windows NT Server Resource Kit, например WINSCHK и WINSCL.






Рекомендуемая дополнительная литература


«DNS and BIND»,

3rd Edition, Paul Albitz, Cricket Liu (O'Reilly, 1998).

«RFC849: Suggestions For Improved Host Table Distribution»,

Mark Crispin, 1983.

«RFC881: The Domain Names Plan and Schedule»,

J. Postel, 1983.

«RFC882: Domain Names: Concepts And Facilities»,

P. Mockapetris, 1983.

«RFC1035: Domain Names: Implementation And Specification»,

P. Mockapetris, 1987.



Служба доменных имен (DNS)


Несмотря на то, что NIS и WINS крайне полезны, им все же недостает некоторых свойств, что делает их непригодными для использования во «всем Интернете».

Масштабируемость

Хотя эти схемы применяются к нескольким серверам, каждый сервер должен обладать полной копией информации о топологии сети.1 Такую информацию следует скопировать на каждый сервер, а этот процесс требует времени, если сеть становится достаточно большой. Кроме того, WINS страдает из-за своей динамической модели регистрации. Некоторое число WINS-клиентов своими регистрационными запросами может расплавить от перегрузки любое количество WINS-серверов для всего Интернета.

Управление

До сих пор мы говорили только о технических аспектах, но это не единственная сторона администрирования. NIS. в особенности, требует единственного центра администрирования. Тот, кто управляет главным сервером, управляет и всем NIS-домеиом, который этот сервер «возглавляет». Любые изменения в пространстве сетевых имен должны пройти через такого сторожа. Этот принцип не будет работать в пространстве имен размером во весь Интернет.

Для борьбы с недостатками, присущими сопровождению файлов узлов или NIS/NIS+/WINS-noflo6HbiM системам, была создана новая модель под названием служба доменных имен (DNS). В DNS пространство имен сети разделено на несколько «доменов верхнего уровня». Любой из них можно разделить на домены меньшего размера и т. д. В каждой точке деления следует назначить сторону, ответственную за контроль над этой частью пространства имен, что позволяет разобраться с вопросами администрирования.

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




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

В такой схеме ни один сервер не должен знать о топологии всей сети, большинство запросов обрабатывается локально, за локальными администраторами сохраняется локальный контроль, и в результате все счастливы. У DNS есть преимущество перед другими службами -большинство других систем, подобных NIS и WINS, можно интегрировать с DNS. Например, NIS-серверы в SunOS можно настроить так, чтобы они обращались к DNS-серверу, если клиент запрашивает у них имя узла, о котором сервер не знает. Результаты этого запроса возвращаются как стандартные NIS-ответы, так что клиенты и не догадываются о каких-либо дополнительных действиях. DNS-серверы Microsoft обладают схожей функциональностью: если клиент запрашшшет у DNS-сервера Microsoft адрес локальной машины, о которой ему неизвестно, то DNS-сервер можно настроить так, чтобы он пересылал этот запрос WINS-серверу от имени клиента.



Генерирование конфигурационных файлов DNS



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

Данные хранятся в отдельной базе данных (одна и та же база может и, вероятно, должна быть источником для всех файлов, о которых идет речь).

Данные преобразуются в формат вывода по нашему выбору, при этом проверяются ошибки.

Используется RCS (или эквивалентная система контроля версий) для хранения предыдущих версий файлов.

В случае с DNS второй шаг необходимо расширить, поскольку здесь процесс преобразования оказывается более сложным. Сложности нужно преодолевать, поэтому было бы неплохо иметь под рукой книгу Пола Альбица (Paul Albitz) и Крикета Лью (Cricket Liu) DNS and BIND («DNS и BIND», O'Reilly), содержащую, в том числе, сведения о конфигурационных файлах, создание которых рассматривается ниже.





Создание административного заголовка



Конфигурационные файлы DNS начинаются с административного заголовка, в нем представлена информация о сервере и данных, которые он обслуживает. Самая важная часть этого заголовка - запись о ресурсах SOA (Start of Authority). Запись SOA содержит:

Имя административного домена, обслуживаемого данным DNS-cep-вером.

Имя первичного DNS-сервера этого домена.

Контактную информацию об администраторе (администраторах) DNS-сервера.

Порядковый номер конфигурационного файла (подробнее об этом рассказывается чуть ниже).

Значения тайм-аутов регенерации (refresh) и повторного обращения (retry) для вспомогательных серверов (т. е. информация о том, когда необходимо синхронизировать данные с первичным сервером).

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

Вот как может выглядеть этот заголовок:

@ IN SOA dns.oog.org. hostmaster.oog.org. (

1998052900 ; serial

10800 ; refresh 3

600 ; retry

604800 ; expire

43200) ; TTL

@ IN NS dns.oog.org.

Булыпая часть информации добавляется в начало конфигурационного файла каждый раз при его создании. Единственное, о чем нужно побеспокоиться, - это о порядковом номере. Один раз в X секунд (X определяется из значения регенерации) вторичные серверы имен сверяются с первичными серверами, чтобы узнать, нужно ли обновить данные. Современные вторичные DNS-серверы (подобные BIND v8+ или Microsoft DNS) могут быть сконфигурированы так, что будут сверяться с основным сервером в то время, когда на последнем меняются данные. В обоих случаях вторичный сервер запрашивает на первичном запись SOA. Если порядковый номер записи SOA первичного сервера больше порядкового номера, хранимого на вторичном сервере, то произойдет перенос информации о зоне (вторичный сервер загрузит новые данные), В итоге, важно увеличивать порядковый номер каждый раз при создании нового конфигурационного файла. Многие из проблем с DNS вызваны неполадками при обновлении порядкового номера.



Существует по крайней мере два способа сделать так, чтобы порядковый номер всегда увеличивался:

Считывать предыдущий конфигурационный файл и увеличивать найденное там значение.

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

Ниже приведен пример программы, где применяется комбинация этих двух методов для создания допустимого заголовка файла зоны DNS. Порядковый номер будет представлен в виде, который рекомендуют использовать Альбиц и Лью в своей книге (YYYYMMDDXX, где Y=rofl, М=месяц, В=день и ХХ=двузначный счетчик, позволяющий вносить более одного изменения за день):

ft получаем текущую дату в формате YYYYMMDD

@localtime = localtime;

Stoday = sprintf("%04d%02d%02d",$localtime[5]+1900,

$localtime[4]+1,

$localtime[3]);

# имя пользователя как в NT/2000, так и в Unix

$user = ($"0 eq "MSWin32")? $ENV{USERNAME} :

(getpwuid($<))[6]." (".(getpwuid($<))[0].")";

sub GenerateHeader{ my($header);

открываем старый файл, если это возможно, и считываем

# порядковый номер, принимая во внимание формат старого файла

if (open (OLDZONE,$target)){ while (<OLDZONE>) {

next unless (/(\d{8}).«serial/); Soldserial = $1; last; }

close (OLDZONE); } else {

Soldserial = "00000000";

иначе начинаем с О >

К если предыдущий порядковый номер соответствует

# сегодняшнему дню, то увеличиваем последние 2 цифры, в

# противном случае используем новый номер для сегодняшнего дня

Solddate = substr($oldserial,0,8);

Scount = ((Solddate == $today) ? substr($oldserial,8,2)+1 : 0);

Sserial = sprintf("%8d%02d",$today,Scount);

П начало заголовка

$header .= "; Файл зоны dns - СОЗДАН

$0\л": Sheader .= ";

НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ1\п:\п"; Sheader .= ";

преобразован пользователем $user в ".scalar((localtime)). "\n;\n";

# пересчитать число записей для каждого отдела и сообщить



foreach my Sentry (keys %entries){

$depts{$entries{$entry}->{department}}++;

}

foreach my $dept (keys %depts) {

Sheader .= "; число узлов в отделе Sdept:

$depts{$dept}.\n"; } Sheader .= ";

всего узлов: ",scalar(keys %entries)."\n;\n\n";

Sheader .= «"EOH";

@ IN SOA dns.oog.org. hostmaster.oog.org. (

Sserial ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL

@ IN NS dns.oog.org.

EOH

return Sheader;

}

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



Создание нескольких конфигурационных файлов



После того как написан верный заголовок для конфигурационных файлов, осталось решить еще одну проблему. Правильно настроенный DNS-c-зрвер поддерживает как прямое преобразование (имен в IP-адреса), так и обратное преобразование (IP-адресов в имена) для каждого домена (или зоны), который он обслуживает. Для этого надо иметь два конфигурационных файла на каждую зону. Самый лучший способ их синхронизировать - создавать файлы в одно и то же время.

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

Чтобы не усложнять сценарий, я сделал ряд предположений относительно данных, самые важные из которых касаются топологии сети и пространства имен. Я считаю, что сеть состоит из одной подсети класса С с одной зоной DNS. В результате, необходимо создать один файл для прямого преобразования имен и один для обратного. Добавить код для работы с несколькими подсетями и зонами (т. е. создать отдельные файлы для каждой) будет несложно.



Вот, вкратце, что мы делаем:

Считываем файл базы данных в хэш хэшей, проверяя при этом данные.

Генерируем заголовок.

Записываем данные в файл для прямого преобразования (из имен в IP-адреса) и помещаем его под контроль RCS.

Записываем данные в файл для обратного преобразования (из IP-адресов в имена) и помещаем его под контроль RCS.

Вот как выглядит пример и получаемые в результате файлы:

use Res;

Sdatafile = "./database";

база данных узлов

Soutputfile = "zone.$$";

временный файл для вывода

$target = "zone.db";

получаемый файл

$revtarget = "rev.db";

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

$defzone = ".oog.org";

# создаваемая по умолчанию зона

Srecordsep = "-=-\n";

 получаем текущую дату в формате YYYYMMDD

@localtime = localtime;

$today = sprintf("%04d%02d%02d",$localtime[5]+1900,

$localtime[4]+l,

$localtime[3]);

 имя пользователя, как в NT/2000, так и

Unix $user = ($"0 eq "MSWin32")? $ENV{USERNAME} :

(getpwuid($<))[6]." (".(getpwuid($<))[0].")"; $/ = Srecordsep;

 считываем файл базы данных

open(DATA, Sdatafile) or die "Ошибка! Невозможно открыть datafile;$!\n";

while (<DATA>) {

chomp; # удаляем разделитель записей

 разбиваем на key!,value"! @record = split /:\s*|\n/m;

$record ={}; # создаем ссылку на пустой хэш

%{$record} = @record;

# заполняем его значениями из ©record

в ищем ошибки в именах узлов

if ($record->{name} =" /["-.a-zA-ZO-9]/) {

warn "!!!! ",$record->{name} .

встретились недопустимые в именах узлов символы, пропускаем.. Дп";

next; }

# ищем ошибки в псевдонимах

if ($record->{aliases} =" /["-.a-zA-ZO-9\s]/) {

warn "!!!! " . $record->{name} .

встретились недопустимые в псевдонимах символы, пропускаем.. .\п";

next; }

# ищем пропущенные адреса unless ($record->{address}) {

warn "!!!! " . $record->{name} .



нет IP-адреса, пропускаем.. Дп"; next; }

# ищем повторяющиеся адреса

if (defined $addrs{$record->{address}}) {

warn "!!!! Повторение IP-адреса:" . $record->{name}.

" & " . $addrs{$record->{address}} . ", пропускаем. . Дп"; next;

>

else {

$addrs{$record->{address}} = $record->{name};

}

$entries{$record->{name}} = Srecord; # добавляем это в хэш хэшей

}

close(DATA);

Sheader = &GenerateHeader;

 создаем файл прямого преобразования open(OUTPUT,"> Soutputfile") or

die "Ошибка! Невозможно записать в $outputfile:$!\n": print OUTPUT $header;

foreach my Sentry (sort byaddress keys %entries) { print OUTPUT

"; Владелец -- ",$entries{$_}->{owner},"

(", $entries{$entry}->{department},"):

Sentries{$entry}->{building},"/", Sentries{$entry}->{room), "\n";

tt выводим запись А

printf OUTPUT "%-20s\tIN A %s\n",

Sentries{Sentry}->{name},Sentries{Sentry>->{address};

it выводим записи CNAMES (псевдонимы)

if (defined $entries{$entry}->{aliases}){

foreach my Salias (split(p ',$entries{$entry}->{aliases}))

{

printf OUTPUT "%-20s\tIN CNAME %s\n",Salias,

Sentries{Sentry}->{name}: > }

print OUTPUT "\n"; }

close(OUTPUT);

Rcs->bindir('/usr/local/bin'); my Srcsobj = Rcs->new;

$rcsobj->file($target);

$rcsobj->co('-!');

rename($outputfile,Starget) or

die "Ошибка! Невозможно переименовать

Soutputfile в Starget:$!\n": $rcsobj->ci("-u","-m"."

Преобразовано пользователем Suser в ".scalar(localtime));

ft создаем файл обратного преобразования open(OUTPUT,"> Soutputfile") or

die "Ошибка! Невозможно записать в $outputfile:$!\n"; print OUTPUT Sheader;

foreach my Sentry (sort byaddress keys %entries) { print OUTPUT

"; Владелец-- ",$entries{$entry}->{owner}," (",

Sentries{Sentry}->{department},"): ",



$entries{$entry}->{building},"/",

Sentries{Sentry}->{room},"\n";

printf OUTPUT "%-3d\tIN PTR %s$defzone.\n\n",

(split/\./,$entries{$entry}->{address})[3],

$entnes{$entry}->{name};

clOse(OUTPUT);

$rcsobj->file($revtarget);

$rcsob]->co( '-1');

предполагаем, что целевой файл по крайней

# мере один раз извлекался из репозитория  rename($outputfile,Srevtarget) or

die "Ошибка! Невозможно переименовать

Soutputfile в Srevtarget;$!\n";

$rcsobj->ci("-u","-m"."Преобразовано пользователем

$user в ".scalar(localtime));

sub GenerateHeader{ my(Sheader);

if (open(OLDZONE,$target)){ while (<OLDZONE>) {

next unless (/(\d{8}).«serial/); $oldserial = $1; last; }

close(OLDZONE); } else {

Soldserial = "000000"; }

$olddate = substr($oldserial,0,6);

$count = (Solddate == $today) ? substr($oldserial,6,2)+1 : 0;

Sserial = sprintf("%6d%02d",$today,Scount);

$header .= "; файл зоны dns - СОЗДАН $0\п";

Sheader .= "; HE РЕДАКТИРУЙТЕ ВРУЧНУЮ!\n;\n";

Sheader .= "; Преобразован пользователем Suser в ".scalar(localtime)."\n;\n":

П подсчитываем число узлов в каждом отделе foreach Sentry (keys %entries){

$depts{$entries{$entry)->{department}}++; } foreach $dept (keys %depts) {

Sheader .= "; в отделе $dept $depts{$dept} машин.\n";

X

Sheader ,= "; общее число машин: ".scalar(keys %entries)."\nff\n\n";

Sheader .= «"EOH";

@ IN SOA dns.oog.org. hostmaster.oog.org. (

$serial : serial 10800 ; refresh 3600 : retry 604800 ; expire 43200) ; TTL

@ IN NS dns.dog.org

ЕОН

return $header; }

sub byaddress {

@a = split(/\./,$entries{$a}->{address});

@b = split(/\./,$entries{$b}->{address});

($a[0]<=>$b[0]) ||

($a[1]<=>$b[1]) ||

($a[2]<=>$b[2]) ||

($a[3]<=>$b[3]); }

Вот какой файл получается для прямого преобразования (zone.db):

файл зоны dns - СОЗДАН createdns НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ!



Преобразован пользователем David N. Blank-Edelman (dnb); в Fri May 29 15:46:46 1998

в отделе design 1 машин. в отделе software 1 машин, в отделе IT 2 машин, общее число машин: 4

@ IN SOA dns.oog.org. hostmaster.oog.org. (

1998052900 ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL

@ IN NS dns.oog.org.

; Владелец -- Cindy Coltrane (marketing): west/143 bendir IN A 192.168.1.3

ben IN CNAME bendir

bendoodles IN CNAME bendir

; Владелец -- David Davis (software): main/909 shimmer IN A 192.168.1.11

shim IN CNAME shimmer

shimmy IN CNAME shimmer

shimmydoodles IN CNAME shimmer

; Владелец -- Ellen Monk (design): main/1116 Sulawesi IN A 192.168.1.12

sula IN CNAME Sulawesi

su-lee IN CNAME Sulawesi

; Владелец -- Alex Rollins (IT): main/1101 sender IN A 192.168.1.55

sandy IN CNAME sander

micky IN CNAME sander

mickydoo IN CNAME sander

А вот как выглядит файл для обратного преобразования (rev.db):

файл зоны dns - СОЗДАН createdns НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ!

Преобразован пользователем David N. Blank-Edelman (dnb); в Fri May 29 15:46:46 1998

в отделе design 1 машин, в отделе software 1 машин, в отделе IT 2 машин, общее число машин: 4

@ IN SOA dns.oog.org. hostmaster.oog.org. (

1998052900 ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL

@ IN NS dns.oog.org.

; Владелец -- Cindy Coltrane (marketing): west/143 3 IN PTR bendir.oog.org.

; Владелец -- David Davis (software): main/909

11 IN PTR shimmer.oog.org.

; Владелец -- Ellen Monk (design): main/1116

12 IN PTR sulawesi.oog.org.

; Владелец -- Alex Rollins (IT): main/1101 55 IN PTR sander.oog.org.

Этот метод создания файлов открывает перед нами много возможностей. До сих пор мы генерировали файлы, используя содержимое одного текстового файла базы данных. Запись из базы данных считывалась и записывалась в файл, возможно, подвергаясь при этом форматированию. Таким образом, в создаваемые файлы попадали только записи из базы данных.

Иногда бывает полезно, чтобы сценарий добавлял в процессе преобразования свои предопределенные данные. Например, в случае с конфигурационными файлами DNS можно улучшить сценарий преобразования так, чтобы он добавлял записи MX (Mail eXchange), указывающие на центральный почтовый сервер, для каждого узла из базы данных. Простое изменение нескольких строк кода с таких:



К выводим запись А

printf OUTPUT "%-20s\tIN A %s\rT, Sentries{Sentry}->{name},Sentries{Sentry}->{address}:

на следующие:

# выводим запись А

printf OUTPUT "%-20s\tIN A %s\n",

$entries{Sentry}->{name},Sentries{Sentry}->{address};

и выводим запись MX

print OUTPUT " IN MX 10 $mailserver\n";

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



Проверка работы DNS: итеративный подход



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

Например, для системного/сетевого администратора очень многое зависит от ответа на вопрос «Все ли DNS-серверы работают?». В ситуации, когда необходимо найти неисправности, практически настолько же важно знать, «Все ли серверы работают с одной и той же информацией?», или, более точно, «Отвечают ли они одинаково на одинаковые запросы? Синхронизированы ли они?». Данный раздел посвящен подобным вопросам.

По главе 2 можно судить, как действует основной принцип Perl «Всегда существует несколько способов сделать это». Именно такое свойство делает Perl отличным языком для «итеративной разработки». Итеративная разработка - это один из способов описания эволюционного процесса, имеющего место при создании программ системного администрирования (и не только), выполняющих определенную задачу. В случае с Perl можно быстро написать рабочую программу на скорую руку, а позднее вернуться к сценарию и переписать его более элегантным образом. Возможно, будет еще и третья итерация, на этот раз уже с использованием другого подхода к решению задачи.



Существует три различных подхода к одной и той же проблеме проверки согласованности DNS. Они представлены в том порядке, которому, действительно, мог бы последовать человек, пытаясь найти решение, а затем его совершенствуя. Этот порядок отражает взгляд на то, как решение проблемы может развиваться в Perl; ибо ваше отношение к подходу может меняться. Третий способ, использующий модуль Net: : DNS, вероятно, самый простой и наиболее защищенный от ошибок. Но существуют ситуации, когда Net: : DNS применять нельзя, поэтому сначала приведем несколько собственных решений. Обязательно обратите внимание на все за и против, перечисленные после каждого рассмотренного подхода.

Вот наша задача: написать сценарий на Perl, принимающий имя узла и проверяющий список DNS-серверов, чтобы убедиться, что все они возвращают одну и ту же информацию об узле. Чтобы упростить задачу, будем считать, что узел имеет единственный статический IP-адрес (т. е. у него один сетевой адаптер и один IP-адрес).

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

$hostname = $ARGV[0];

©servers = qw(nameserver1 nameserver2 nameserverS);

# серверы имен

foreach $server (servers) {

&lookupadrjress($hostname, $server);

 заполняем %results

}

%inv = reverse %results;

# инвертируем полученный хэш

if (keys %inv > 1) {

print "Между DNS-серверами есть разногласия";

use Data::Dumper;

print Data::Dumper->Dump([\%results],["results"]), "\n"; }

Для каждого из DNS-серверов, перечисленных в списке @servers, вызывается подпрограмма &lookupaddress(), которая обращается к DNS-серверу, чтобы получить IP-адрес заданного имени узла, и помещает результаты в хэш %results. Для каждого DNS-сервера в хэше %results есть запись, значением которой является IP-адрес, возвращаемый этим сервером (ключом является имя сервера).

Существует много способов определить, равны ли друг другу значения из хэша %results (т. е. убедиться, что все DNS-серверы возвращают одну и ту же информацию в ответ на запрос). Мы инвертируем хэш %results в другую хэш-таблицу, преобразовывая все ключи в значения и наоборот. Если все значения из %results одинаковы, то в инвертированном хэше должен быть только один ключ. Если ключей несколько, значит, мы выловили прокол, и поэтому вызываем Data: :Duniper->Durrip() для СлужОа доменных имен вывода содержимого %results, над которым будет ломать голову системный администратор.



Вот как может выглядеть примерный результат, если что-то идет не так:

Между DNS-серверами есть разногласия: $results = {

nameserverl => '192.168.1.2',

nameserver2 => '192. 168. 1.5' ,

nameserverS => ' 192. 168. 1.2' ,

Теперь посмотрим на альтернативы подпрограмме &lookupaddress( ).



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



Если у вас есть опыт работы в Unix или вы уже программировали на других языках сценариев помимо Perl, то первая попытка может сильно походить на сценарий командного интерпретатора. Внешняя программа, вызываемая из Perl сценария, выполняет всю сложную работу:

use Data::Dumper;

Shostname = $ARGV[0];

Snslookup = "/usr/local/bin/nslookup";

# путь к nslookup ©servers = qw(nameserver1 nameserver2 nameserverS);

 имена серверов имен foreach Sserver (©servers) {

&lookupaddress($hostname,Sserver); в заполняем %results }

%inv = reverse %results;

 # инвертируем полученный хэш

if (scalar(keys %inv) > 1) {

print "Между DNS-серверами есть разногласияДп";

print Data::Dumper->Dump([\%results],["results"]),"\n"; }

» обращаемся к серверу, чтобы получить IP-адрес и прочую

# информацию для имени узла, переданного в программу в

командной строке. Результаты записываем в хэш %results sub lookupaddress

my($hostname,Sserver) = @_;

open(NSLOOK,"$nslookup Shostname Sserver|") or

die "Невозможно запустить nslookup:$!\n";

while (<NSLOOK>) {

« игнорировать, пока не дойдем до "Name: next until (/"Name:/);

 следующая строка - это ответ Address: chomp($results{$server} = <NSLOOK>);

# удаляем имя поля

die "Ошибка, вывода nslookup \n" unless /Address/;

$results{$server} =" s/Addfess(es)?:\s+//;

 все, с nslookup мы закончили last;

}

close(NSLOOK);

}

Преимущества такого подхода:

Это короткая программа, которую можно быстро написать (вероятно, ее даже можно построчно перевести из настоящего сценария командного интерпретатора).



Нет необходимости писать запутанный код для работы с сетью.

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

Это может оказаться единственным выходом, если нельзя написать программу для взаимодействия клиента и сервера на Perl; в частности, если вам нужно обратиться к серверу, который требует использования особого клиента без каких-либо альтернатив.

Недостатки такого подхода:

Появляется зависимость от другой программы за пределами сценария. А если эта программа недоступна? Что делать, если изменится формат вывода данной программы?

Это работает медленнее. Каждый раз, для того чтобы выполнить запрос, необходимо запустить новый процесс. От подобной нагрузки можно избавиться, если установить двунаправленный канал с процессом nslookup, который открыт все то время, когда он нужен. Правда, это потребует несколько большего опыта программирования, но это стоит сделать, если вы решите использовать и улучшать подобный подход.

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

обрабатывает тайм-ауты сервера, повторные попытки запросов и дописывает списки поисков доменов.



Работа напрямую с сетевыми сокетами



Если вы «продвинутый системный администратор», вы можете решить, что вызывать внешнюю программу не следует. Вы можете захотеть реализовать запросы к DNS, не используя ничего, кроме Perl. Это означает, что нужно будет создавать вручную сетевые пакеты, передавать их по сети и затем анализировать результаты, получаемые от сервера.

Вероятно, это самый сложный пример из всех, приведенных в книге. Написан он после обращения к дополнительным источникам информации, в которых можно найти несколько примеров существующего кода (включая модуль Майкла Фура (Michael Fuhr), показанный в следующем разделе). Вот что происходит на самом деле. Запрос к DNS-серверу состоит из создания специального сетевого пакета с определенным заголовком и содержимым, отправки его на DNS-сервер, получения ответа от сервера и его анализа.



Каждый DNS-пакет ( из тех, которые нас интересуют) может иметь до пяти различных разделов:

Header(Заголовок)

Содержит флаги и счетчики, относящиеся к запросу или ответу (присутствует всегда).

Question (Запрос)

Содержит вопрос к серверу (присутствует в запросе и повторяется при ответе).

Answer (Ответ)

Содержит все данные для ответа на DNS-запрос (присутствует в пакете DNS-ответа).

Authority (Полномочия)

Содержит информацию о том, можно ли получать авторитетные ответы.

Additional (Дополнительно)

Содержит любую информацию, которую вернет сервер помимо прямого ответа на вопрос.

Наша программа имеет дело только с первыми тремя из этих разделов. Для создания необходимой структуры данных для заголовка DNS-na-кета и его содержимого используется набор команд oack(). Эти структуры данных передаются модулю 10: :Socket, который посылает их в виде пакета. Этот же модуль получает ответ и возвращает его для обработки (при помощи unpackO). Умозрительно такой процесс не очень сложен.

Но перед тем как посмотреть на саму программу, нужно сказать об одной особенности в этом процессе. В RFC1035 (Раздел 4.1.4) определяются два способа представления доменных имен в DNS-пакетах: несжатые и сжатые. Под несжатым доменным именем подразумевается полное имя домена (например host.oog.org)

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

hostS в longsubdomain.longsubdomain.oog.org, вместо того чтобы каждый раз включать лишние байты для longsubdo-main.longsubdomain.oog.org. Нам необходимо обработать оба представления, поэтому и существует подпрограмма &decompress. Дальше обойдемся без фанфар и взглянем на код:

use 10: '.Socket;



Shostname = $ARGV[0];

$defdomain = ".oog.org"; # домен по умолчанию

^servers = qw(nameserver1 nameserver2 nameserverS);

имена серверов имен

foreach Iserver (©servers) {

<Slookupaddress($nostname,$server);

# записываем значения в

%results

}

%inv = reverse ^results; # инвертируем полученный хэш

if (scalar(keys %inv) > 1) { # проверяем, сколько в нем элементов

print "Между DNS-серверами есть разногласия:\п";

use Data::Dumper;

print Data::Dumper->Dump([\%results],["results"]),"\n";

}

sub lookupaddress{

my($hostname,$server) = @_;

my($qname,$rna(tre,$header,$question,$lformat,@>labels,$count);

local($position,$buf);

Конструируем заголовок пакета

$header = pack("n C2 n4",

++$id, # идентификатор запроса

1, # поля qr, opcode, aa, tc, rd (установлено только rd)

0, # rd, ra

1,  один вопрос (qdcount)

0,  нет ответов (ancount)

О, п нет записей ns в разделе authority (nscount)

0); tf нет rr addtl (arcount)

 если в имени узла нет разделителей,

 дописываем домен по умолчанию

(index($hostname,'.') == -1) {

Shostname .= Sdefdomain;

} # конструируем раздел qname пакета (требуемое доменное имя)

for (split(/\./,$riostname)) {

$lformat .= "С а* ";

$labels[$count++]=length;

$labels[$count++]=$_;

}

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

да

Squestion = pack($lformat."С п2",

©labels,

0, # конец меток

1, # qtype A

1); # qclass IN

да

да посылаем пакет серверу и читаем ответ

$sock = new 10::Socket::INET(PeerAddr => Sserver,

PeerPort => "domain",

Proto => "udp");

$sock->send($header.$question);

 используется UDP, так что максимальный размер пакета известен

$sock->recv($buf,512);

close($sock);

 узнаем размер ответа, так как мы собираемся отслеживать

 позицию в пакете при его анализе (через Sposition)

Srespsize = length($buf);

распаковываем раздел заголовка

да

($id,

$qr_opcode_aa_tc_rd,

$rd_ra,

Sqdcount,



$ancount,

Snscount,

Sarcount) = unpack("n C2 n4",$buf);

if (!$ancount) <

warn "Невозможно получить информацию для $hostname с Sserver!\n";

return;

}

 распаковываем раздел вопроса

tt раздел вопроса начинается после 12 байтов

($position,$qname) = <Sdecompress(12);

($qtype,$qclass)=unpack('§'.Sposition.'n2',Sbuf);

tt переходим к концу вопроса

Sposition += 4;

nntt

tttttt распаковываем все записи о ресурсах

ttntt

for ( ;$ancount;$ancount--){

(Sposition,$rname) = &decompress($position);

(Srtype,Srclass,$rttl,$rdlength)=

unpack('@'.Sposition.'n2 N n',$buf);

Sposition +=10;

tt следующую строку можно изменить и использовать более

# сложную структуру данных; сейчас мы подбираем

 последнюю возвращенную запись

$results{$server}=

join('.',unpack('@'.Sposition.'C'.$rdlength,$buf));

Sposition +=$rdlength; } >

О обрабатываем информацию, "сжатую" в соответствии с RFC1035

# мы переходим в первую позицию в пакете и возвращаем

# найденное там имя (после того как разберемся с указателем

# сжатого формата) и место, которое мы оставили в конце

# найденного имени sub decompress {

my($start) = $_[0]; my($domain,$i,Slenoct);

for ($i=$start;$i<=$respsize;) {

$lenoct=unpack('@'.$i.'C', $buf); n длина метки

if (! Slenoct){ tt 0 означает, что этот раздел обработан

$i++;

last; }

if (Slenoct == 192) { tt встретили указатель,

tt следовательно, выполняем рекурсию

Sdomain.=(&decompress((unpack('@'.$i.'n',$buf) & 1023)))[1];

$i+=2;

last } else { tt в противном случае это простая метка

$domain.=unpack('@г.++$i.'a'.Slenoct,$buf).'. ';

$i += Slenoct; }

return($i,Sdomain);

}

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

Преимущества этого подхода заключаются в следующем:



Он не зависит от каких- либо других программ. Нет необходимости разбираться в работе других людей.

Это настолько же быстро, а может быть, и еще быстрее, чем вызов внешней программы.

Проще обработать параметры ситуации (тайм-ауты и прочее). Недостатки же такого подхода в том, что:

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

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

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



Использование Net::DNS



Как уже говорилось в главе 1, одна из сильных сторон Perl заключается в поддержке обширным сообществом разработчиков, создающих программы, которые могут применяться другими людьми. Если необходимо сделать на Perl нечто, на ваш взгляд, универсальное, то высока вероятность того, что кто-то уже написал модуль для работы с подобной проблемой. В данном случае можно воспользоваться отличным модулем Net: :DNS Майкла Фура (Michael Fuhr), который упростит работу. Чтобы справиться с нашей задачей, необходимо создать новый объект, передать ему имя DNS-сервера, к которому следует обратиться, указать, что нужно послать запрос, и затем применить имеющиеся методы для анализа ответов:

use Net::DNS;

&lookupaddress($hostname,$server); # заполняем значениями %results }

%inv = reverse %results; » инвертируем полученный хэш if (scalar(keys %inv) > 1) { tt проверяем, сколько в нем элементов

print "Между DNS-серверами есть разногласия:\п";

use Data:: Dumper;

print Data::Dumper->Dump([\%results],["results"]),"\n"; }

tt всего лишь несколько измененный пример из страниц руководства по Net::DNS sub lookupaddress{

my($hostname,$server) = @_;



$res = new Net::DNS::Resolver; $res->nameservers($server); Spacket = $res->query($hostname);

if (!$packet) {

warn "Невозможно получить данные о Shostname с $server!\n";

return; }

# сохраняем последний полученный ответ RR foreach $rr ($packet->answer) {

$results{$server}=$rr->address;

 }

}

Преимущества такого подхода:

Помимо прочего, получаемый код легко читать.

Написать его можно быстрее.

В зависимости от того, как реализован применяемый модуль (только на Perl или с использованием библиотечных вызовов из С или C++), написанная программа может выполняться так же быстро, как и вызов внешней программы.

Потенциально, это переносимая программа - все зависит только от того, что именно сделал автор модуля. Везде, где можно установить модуль, программа будет работать.

Как и в первом рассмотренном случае, написать программу можно быстро и просто, если кто-то другой сделает за вас всю работу, происходящую «за сценой». Вам не нужно знать, как работает модуль; вы только должны знать, как его применять.

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

Недостатки данного подхода:

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

Может не существовать подходящего вам модуля или он может не запуститься на выбранной вами операционной системе.

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