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

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

Будьте осторожны при чтении данных

Будьте осторожны при чтении данных

При чтении важных данных, скажем, конфигурационных файлов, сначала протестируйте возможность небезопасных состояний. Например, стоит проверить, запрещена ли запись в файл и каталоги, в которых он находится (иначе, кто угодно может их испортить). Хороший способ подобной проверки можно найти в главе 8 книги «Perl Cookbook» («Perl: библиотека программиста») Тома Кристиансена (Тот Christiansen) и Натана Торкингтона (Nathan Torkington) (O'Reilly).
Другая забота - ввод пользователей. Никогда не считайте, что данным, поступающим от пользователей, можно доверять. Даже если вы явно просите пользователя: Пожалуйста, ответьте Да(У) или Нет(М):, ничто не помешает ему набрать 2049 случайных символов (либо от вредности со злым умыслом, либо потому, что его двухлетний ребенок занял освободившееся на минуту место за клавиатурой).
Ввод пользователей может быть причиной еще более серьезных проблем. Мой любимый пример- это использование нулевого байта «Poison NULL Byte», о котором сообщалось в статье о проблемах Perl в CGI. Обязательно прочитайте всю статью (ссылка на нее есть в конце этой главы). Неприятности возникают из-за отличий в обработке нулевого байта (\000) в Perl и в системных библиотеках С. Для Perl этот символ ничем не примечателен. Однако в библиотеках этот символ используется для обозначения конца строки.
На практике это означает, что у пользователя существует возможность обойти различные проверки. Один пример, приведенный в этой статье, - это программа, меняющая пароль пользователя:
if (Suser ne "root"){ <вызов соответствующей функции С>}
Если переменная $user установлена в значение root\000 (т. е. если за словом root следует нулевой байт), то приведенная выше проверка окажется удачной. Когда эта строка будет передана библиотеке, она будет воспринята просто как root, и пользователю удастся обойти проверку. Если эту ситуацию не отследить, то подобная «дыра» позволит получить доступ к произвольным файлам и другим ресурсам. Самый простой способ не пострадать от этой проблемы - подправить код, добавив что-то похожее на следующую строку:
Sinput =" tr/\000//d;
Это всего лишь один пример того, как ввод пользователя может вызвать проблемы. Именно поэтому в Perl существует специальное средство предосторожности - режим пометки (taint mode). Изучите страницу руководства perlsec, входящую в состав дистрибутива Perl, чтобы ознакомиться с отличным объяснением того, что такое отмеченные данные, а также с другими мерами предосторожности.
Будьте осторожны при записи данных
Если ваша программа может записывать или дописывать данные в любой файл локальной файловой системы, вы должны особенно заботиться о том, как, куда и когда записываются данные. В системах Unix это особенно важно, поскольку символические ссылки очень сильно упрощают подмену файлов и перенаправление. Если ваша программа написана не очень аккуратно, может оказаться, что она пишет не в тот файл или устройство. Существует два класса программ, в которых это соображение особенно важно.
В первый класс попадают программы, дописывающие данные в существующие файлы. Перед дописыванием в файл в вашей программе должна быть выполнена следующая последовательность шагов:
  • Используйте функцию stat() и обычные операторы проверки файлов для проверки атрибутов файлов. Убедитесь, что файл не является ни жесткой, ни символической ссылкой, что у него установлены нужные права и владельцы и т. д.
  • Откройте файл для дописывания.
  • Передайте файловый дескриптор функции stat().
  • Сравните значения, полученные на шагах 1 и 3, чтобы убедиться, что открытый файловый дескриптор соответствует нужному вам файлу.
  • Смотрите программу bigbuffy из главы 9 «Журналы», которая соблюдает эту последовательность шагов.
    Во второй класс попадают программы, использующие временные файлы или каталоги. Вы часто видели подобный код:
    open(TEMPFILE,">/tmp/temp.$$") or die "невозможно записать в /tmp/ temp.$$:$!\n";
    К сожалению, это недостаточно безопасно для многопользовательских систем. Последовательность идентификаторов процессов ($$) на большинстве машин легко предсказуема, а это означает, что также предсказуемо имя следующего временного файла, который будет использовать ваш сценарий. Если кто-то сможет предсказать это имя, он сможет оказаться там раньше вас. А это уже, как правило, плохие новости.
    В некоторых операционных системах есть библиотечные вызовы, которые генерируют имена временных файлов, используя современный алгоритм случайных значений. Чтобы проверить вашу операционную систему, вы можете запустить следующий код. Если получаемые имена кажутся вам достаточно случайными, вы можете полагаться на POSIX: :tmpnam(). Если нет, вы можете написать собственную функцию генерации случайных имен файлов:
    use POSIX qw(Tr4pnar-'):
    for (1..20){ print POSIX::tmpnam(),"\n"; }
    Как только у вас будет имя файла, которое нельзя отгадать, вам нужно будет открыть его безопасным образом:
    sysopen(TEMPFILE,$tmpname,0_RDWR|0_CREATjO_EXCL0666);
    Существует другой, более простой способ выполнить эти же два шага (получить имя и открыть временный файл). Метод 10: : File->new_trnpfi-1е() из модуля 10: : File не только подберет хорошее имя (если системные библиотеки это поддерживают), но и откроет файл для чтения и записи.
    Примеры использования POSIX: :tmpnam() и 10: : File->new_tmpfile(), a также другую информацию по этой теме вы можете найти в главе 7 книги рецептов «Perl Cookbook» («Perl: библиотека программиста»). В модуле File: :Temp Тима Дженнеса (Tim Jenness) также предпринимаются попытки обеспечить безопасные операции работы с временными файлами.

    Что вам нужно

    Что вам нужно

    Чтобы понять большую часть книги, вам следует знать основы языка и иметь кое-какие ресурсы под рукой. Давайте начнем с перечисления тех знаний, которыми вы должны обладать:
    Вы должны немного знать Perl
    В этой книге недостаточно места, чтобы приводить основы языка Perl, поэтому перед дальнейшим чтением вы должны изучить их самостоятельно. Познакомившись с содержанием таких книг, как «Learning Perl» (Изучаем Perl) Рэндала Шварца (Randal L. Schwartz) и Тома Кристиансена (Tom Christiansen) (O'Reilly) или «Learning Perl on Win32 Systems» (Изучаем Perl для Win32) Рэндала Шварца (Randal L. Schwartz), Эрика Олсона (Erik Olson) и Тома Кристиансена (Tom Christiansen) (O'Reilly), вы будете в хорошей форме, чтобы приступить к кодам из этой книги.
    Вы должны знать основы вашей операционной системы (систем)
    В этой книге предполагается, что у вас есть некоторый опыт работы с операционной системой (системами), которую вы собираетесь администрировать. Вы должны знать, как работать с этой операционной системой, как выполнять команды, искать документацию и т. д. Вы должны знать и основы более сложных технологий, существующих в операционной системе (например, WMI для Windows 2000 или SNMP).
    Вам может понадобиться знание особенностей вашей операционной системы (систем)
    Я предпринял попытку описать различия между основными операционными системами, но я не смог охватить все внутренние различия. В частности, каждый вариант Unix немного отличается от всех остальных. Таким образом, вам может понадобиться найти информацию об особенностях вашей операционной системы и разобраться, будет ли эта информация отличаться от описанной здесь.
    Из технических ресурсов вам понадобятся только две вещи: Perl
    Вам нужна копия Perl, установленная или доступная для каждой системы, которую вы хотите администрировать. С веб-сайта http:// www.perl.com вы можете загрузить дистрибутив либо с исходными кодами, либо в скомпилированном виде для конкретной операционной системы. В примерах этой книги применяется Perl 5.005 (в момент написания книги - это последняя стабильная версия1). В Unix мы используем дистрибутив Perl, скомпилированный из исходных кодов, на платформе Win32 - версию, распространяемую ActiveSta-te (build 522), а на MacOS - дистрибутив MacPerl (5.2.0r4).
    Возможность найти и установить модули Perl
    Следующий раздел этой главы посвящен информации о местонахождении и установке модулей, поскольку эти данные чрезвычайно важны. Мы предполагаем, что у вас есть знания и необходимые права для установки всех нужных модулей.
    В конце каждой главы приведен список номеров версий всех модулей, используемых в примерах этой главы. Информация о номерах версий приводится потому, что модули постоянно обновляются. Обновление не всегда сохраняет совместимость с предыдущими версиями, поэтому при возникновении трудностей эта информация поможет вам определить, изменялся ли модуль со времени издания книги.


    Избавьтесь от своих привилегий как можно быстрее

    Избавьтесь от своих привилегий как можно быстрее

    Иногда невозможно избежать необходимости запускать сценарий с правами привилегированного пользователя. Например, созданная вами программа доставки почты может потребовать возможности записывать в файл от имени любого пользователя в системе. Программы, подобные этой, должны отказываться от своего «всемогущества» как можно раньше во время своего выполнения.
    В программах на Perl, выполняющихся в Unix или Linux, можно установить переменные $< и $>:
    # Навсегда избавиться от привилегий ($<,$>} = (getpwnam('nobody'),getpwnam('nobody'));
    В результате, реальный и эффективный идентификаторы пользователя будут установлены равными идентификатору пользователя nobody, не являющемуся привилегированным пользователем. Если вы хотите подойти к проблеме более основательно, то можете также использовать переменные $( и $) для смены реального и эффективного идентификатора группы.
    В Windows NT и Windows 2000 вообще нет идентификаторов пользователей, но для избавления от привилегий существует схожий процесс. В Windows 2000 есть возможность, называемая «RunAs», которую можно использовать для запуска процесса от имени другого пользователя. В Windows NT и Windows 2000 пользователи с правами Act as part of the operating system могут выдавать себя за других пользователей. Эти права можно установить с помощью программы User Manager или User Manager for Domains:
  • В меню Policies выберите пункт User Rights.
  • Отметьте пункт Show Advanced User Rights.
  • Выберите Act as part of the operating system из выпадающего списка.
  • Выберите пункт Add... и определите пользователей или группы, которых вы хотите наделить этими правами. Если вы хотите предоставить такое право определенному пользователю, выберите пункт Show Users.
  • Вероятно, данному пользователю придется повторно зарегистрироваться в системе, чтобы эти изменения вступили в силу.
  • Вам также понадобится добавить права Replace a process level token и, в некоторых случаях, Bypass traverse checking (см. документацию по Win32: : AdminMisc). Как только вы присвоили эти права пользователю, он сможет запускать сценарии на Perl с функцией LogonAs(Jser() из модуля Дэвида Рота (David Roth) Win32: :AdminMisc, который можно найти на http://www.roth.net:
    use Win32::AdminMisc; die "Невозможно персонализировать $user\n"if
    (!Win32::AdminMisc::LogonAsUser('',$user,Suserpw);
    Замечание: здесь существует некоторая опасность, поскольку в отличие от предыдущего примера, вы должны передать пароль пользователя в функцию LogonAsUser().

    Избегайте состояний перехвата

    Избегайте состояний перехвата

    По мере возможности, старайтесь не писать программ, допускающих состояния перехвата. Обычное состояние перехвата начинается с предположения, что следующая последовательность допустима:
  • Ваша программа будет накапливать некоторые данные.
  • Ваша программа затем будет работать с этими данными.
  • Если пользователи могут проникнуть в эту последовательность на шаге, скажем 1,5, и выполнить некоторую замену данных, это может привести к неприятностям. Если им удается контролировать вашу программу на шаге 2, чтобы обработать данные, отличающиеся от тех, которые были на шаге 1, значит, им удалось использовать состояние перехвата (т. е., их программа выиграла состязание, чтобы получить данные). В другом случае состояние перехвата может произойти, если вы неверно работаете с блокировкой файлов.
    Состояния перехвата часто возникают в программах системного администрирования, которые на первом шаге сканируют файловую .систему, а на втором шаге изменяют данные. Бесчестные пользователи могут внести изменения в файловую систему сразу после сканирования, чтобы изменения были внесены в неверный файл. Убедитесь, что в вашем коде нет подобных «дыр».

    Эта книга покажет вам как

    Эта книга покажет вам, как

    В телевизионном шоу «Бэтмэн», популярном в 1966-1968 годах, у энергичной парочки были чудо-пояса с инструментами. Если Бэтмэну и Робину нужно было взобраться на здание, Бэтмэн говорил: «Быстрый Робин, абордажный крюк!». Или же Бэтмэн говорил: «Быстрый Робин, нокаутирующий газ!», и тут же нужный инструмент для борьбы с плохими парнями оказывался в руках у каждого. Цель этой книги - снабдить читателя таким поясом, необходимым для того, чтобы хорошо выполнять работу системного администратора.
    В каждой главе вы найдете:
    Понятную и четкую информацию о сфере действий системного администратора
    В каждой главе мы подробно рассказываем об одной из сфер действий системного администрирования. Число возможных сфер действий при многоплатформенном системном администрировании слишком велико, чтобы рассказать обо всем в одной книге. Лучшие книги по системному администрированию в Unix - «Essential System Administration» (Суть системного администрирования) Элен Фриш (Eileen Frisch) (O'Reilly & Associates) и «Unix System Administration Handbook» (UNIX: руководство системного администратора) Эви He-мет (Evi Nemeth), Гарта Снайдера (Garth Snyder) и Трента Р. Хейна (Trent R. Hein) (Prentice-Hall) - одна в два, а другая в три раза больше этой работы. Мы же рассмотрим вопросы, относящиеся к трем различным операционным системам: Unix, Windows NT/2000 и MacOS.
    В результате, приходилось выбирать, что включить в книгу, а что отложить. Были исключены темы, которые, на мой взгляд, приобретут существенно большее значение только в последующие пять лет. А такие важные технологии, как XML, были рассмотрены, потому что в ближайшее время они, наверняка, окажут заметное влияние на всю эту область. К сожалению, этот подход привел к тому, что такие вечные вопросы системного администрирования, как резервное копирование и печать, были вытеснены более новыми темами, подобными LDAP и SNMP. Навыки и инструменты, представленные в этой книге, могут помочь в тех областях, которые я пропустил, но детальные описания нужно искать где-то в другом месте.
    Я попытался собрать вместе достаточно информации о системном и сетевом администрировании для людей с различным уровнем опыта. Опытные профессионалы и новички могут почерпнуть из этой книги совершенно разную информацию, но каждый из них найдет для себя что-то интересное. В конце каждой главы приведен список источников информации, которые могут помочь глубже разобраться с выбранной темой.
    Для каждой темы или области, особенно, если ее изучение требует известного времени, я включил приложения с информацией, необходимой для того, чтобы быстро во всем разобраться. Даже если тема вам знакома, такие приложения могут помочь дополнить свои знания (например, выяснить, как нечто реализовано на другой операционной системе).
    Технологии и подходы Perl, которые можно использовать в системном администрировании
    Чтобы извлечь из книги все возможное, необходимо владеть некоторыми основами Perl. В каждой главе достаточно много разных по сложности программ, иногда простых и доступных новичкам, а порой требующих довольно серьезного знания Perl. Если будет встречаться технология, структура данных или идиома среднего или повышенного уровня сложности, я потрачу время, чтобы аккуратно рассмотреть ее шаг за шагом. Рассмотрев некоторые интересные технологии программирования на Perl, вы сможете включить их в свою практику. Я надеюсь, что программисты на Perl любого уровня смогут чему-нибудь научиться на приведенных примерах. И, по мере роста вашего уровня, вы сможете вернуться к этой книге и научиться чему-нибудь еще.
    Чтобы еще больше расширить такой опыт обучения, я буду часто приводить несколько способов решения одной и той же задачи при помощи Perl, а не единственный из возможных. Запомните девиз Perl: «Существует более одного способа сделать это». Все примеры, отражающие различные подходы, придуманы для того, чтобы лучше оснастить ваш пояс с инструментами: чем больше их будет у вас в руках, тем более правильный выбор вы сможете сделать, когда столкнетесь с новой проблемой.
    Иногда кажется очевидным, что одна технология имеет преимущество перед остальными. Но в этой книге описаны лишь некоторые ситуации, с которыми вы можете столкнуться, а решения, достаточно неуклюжие для одной проблемы, могут оказаться разгадкой для другой. Так что наберитесь терпения. Для каждого примера я попытаюсь показать вам как преимущества, так и недостатки разных подходов (и часто буду говорить, какой метод предпочитаю я).
    Принципы и лучшие приемы системного администрирования
    Как я уже говорил в начале этой главы, существуют лучшие и худшие способы выполнять задачи системного администрирования. В течение последних 15 лет я в качестве системного и сетевого администратора управляю весьма требовательным многоплатформенным окружением. В каждой главе я попытаюсь передать свой опыт, рассказывая о лучших приемах, которым я научился, и глубоких принципах, лежащих в их основе. Иногда я буду ссылаться на «военные рассказы прямо с передовой» из собственного опыта, используя их в качестве отправной точки для обсуждения. Будем надеяться, по мере чтения вся глубина искусства системного администрирования станет для вас очевидной.


    Наслаждайтесь

    Наслаждайтесь

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


    Нелегко быть всемогущим Перед

    Конечно же, используйте Perl. Но, по возможности, старайтесь избегать выполнения программ в привилегированном окружении. Большинство задач не требует привилегий пользователя root или Admi nistrator. Например, программа анализа журналов, вероятно, не должна выполняться с правами суперпользователя. Для выполнения этих автоматизированных действий создайте другого пользователя, наделенного меньшими привилегиями. Пусть у вас будет маленькая программа, наделенная привилегиями, которая будет передавать при необходимости данные этому пользователю, и затем этот пользователь будет выполнять анализ.

    Поиск и установка модулей

    Поиск и установка модулей

    Основное преимущество использования Perl для системного администрирования заключается в доступности свободного исходного кода модулей. Рассмотренные в этой книге модули можно найти в одном из трех источников:
    Полная сеть Perl-архивов (Comprehensive Perl Archive Network, CPAN)
    CPAN - это огромный архив исходного кода на Perl, документации, сценариев и модулей, продублированных на сотне сайтов по всему миру. Эту информацию можно найти по адресу http://www.cpan.org. Самый простой способ найти модули на CPAN - воспользоваться поисковой системой, разработанной и поддерживаемой Элэни Аштоном (Elaine Ashton), Грэхемом Баром (Graham Barr) и Клифтоном Поси (Clifton Posey) на http://search.cpan.org. Поле «CPAN Search:» облегчает задачу поиска нужных модулей.
    Репозитории для скомпилированных пакетов
    Скоро мы познакомимся с менеджером пакетов Perl (Perl Package Manager, PPM), очень важным инструментом для пользователей Perl на Win32. Этот инструмент соединяется с репозиториями (самый известный из которых расположен на ActiveState), чтобы получить собранные пакеты модулей. Адреса репозиториев можно найти в списке часто задаваемых вопросов о РРМ на http://www.ac tivestate.com/Products/ActivePerl/docs/faq/ActwePerl-faq2.html. Если пакет для Win32 получен не с ActiveState, я обязательно укажу на это. Пакеты для MacOS лучше всего искать на сайте MacPerl Module Porters по адресу http://pudge.net/mmp/.
    Отдельные веб-сайты
    Некоторые модули не попадают на CPAN или в репозитории РРМ. Я всегда буду говорить, где можно найти модуль, если он находится в необычном месте.
    Как установить модуль после того, как вы его найдете? Ответ зависит от того, какую операционную систему вы используете. В дистрибутив Perl входит документация по установке, которую можно найти в файле perlmodinstall.pod (наберите perldoc perlmodinstall, если хотите ее прочитать). В следующем разделе я приведу краткое описание действий, которые необходимо выполнить в каждой из операционных систем, рассмотренных в этой книге.
    Установка модулей в Unix
    В большинстве случаев этот процесс выглядит так:
  • Скачайте модуль и распакуйте его.
  • Запустите perl Makefile.PL, чтобы создать необходимый Makefile.
  • Запустите make, чтобы собрать пакет.
  • Запустите make test, чтобы выполнить все тестовые действия, включенные в модуль автором.
  • Запустите make install, чтобы установить модуль в отведенное на вашей системе место.
  • Если вы не хотите возиться с установкой вручную, то можете использовать написанный Андреасом Кенигом (Andreas J. Kbnig) модуль СРА. (модуль входит в состав Perl). Этот модуль позволит выполнить все эти действия, набрав:
    % perl -MCPAN -e shell
    cpan install modulename
    Модуль CPAN достаточно «умен», чтобы обрабатывать зависимости (т. е. если один модуль требует запуска другого модуля, то CPAN установит оба модуля автоматически). В СРАМ, кроме того, есть функция для поиска связанных модулей и пакетов. Я рекомендую набрать perldoc CPAN, чтобы ознакомиться со всеми полезными возможностями этого модуля.

    Системное администрирование это

    Какой помощи ждать от Perl

    В работе системного администратора используется любой и всякий язык программирования, если его применение приносит пользу. Так почему же в этой книге выбран именно Perl?
    Ответ на этот вопрос слышится в самой природе системного администрирования. Реми Эвард, мой друг и коллега, однажды описал работу системного администратора такими словами: «С одной стороны, у тебя есть набор ресурсов: компьютеры, сети, программное обеспечение и т. д. А с другой стороны, есть пользователи со своими нуждами и проектами - люди, которые хотят, чтобы их работа выполнялась. Наша задача заключается в том, чтобы состыковать эти два множества оптимальным способом, являясь при необходимости посредником между кругом расплывчатых нужд людей и техническим миром».
    Системное администрирование - это зачастую склеивание;
    Perl - один из наиболее подходящих для этого языков. Perl использовался для системного администрирования задолго до того, как появился WWW со всеми своими требованиями к механизмам склеивания.
    В Perl есть несколько других особенностей, свойственных принципам системного администрирования:
  • Это очевидный потомок различных командных интерпретаторов Unix и языка С - то, чем многие системные администраторы уверенно владеют.
  • Он доступен практически на всех современных операционных системах. И на каждой из них интерфейс остается практически одинаковым. Это очень важно для системных администраторов, работающих с несколькими платформами.
  • В нем есть отличные инструменты для работы с текстом, по доступу к базам данных и программированию для сети - трем основам профессии.
  • Основа языка может быть легко расширена благодаря тщательно продуманному механизму модулей.
  • Многочисленное и преданное сообщество пользователей потратило несметное количество часов на создание модулей практически для каждой задачи. Как правило, эти модули тщательно организованы (мы вскоре к этому вернемся). Такая поддержка от сообщества может быть очень значительной.
  • На Perl просто интересно программировать.
  • Для полноты картины следует заметить, что Perl не позволяет решить все мировые проблемы. Иногда он даже не подходит для программирования в области системного администрирования, потому что:
  • Механизм объектно-ориентированного программирования в Perl несколько странен. В этом отношении Python гораздо лучше.
  • Perl доступен не везде. На только что установленной системе вы скорее найдете командный интерпретатор Борна, а не Perl.
  • Perl не всегда прост и последователен и он довольно запутан. В Tel гораздо меньше сюрпризов.
  • Perl достаточно непрост в использовании, чтобы заставить вас не раз наступить на грабли.
  • Мораль такова - всегда выбирайте подходящий инструмент. Таким инструментом для меня чаще всего бывал Perl, поэтому и появилась эта книга.


    Ссылки на подробную информацию

    Ссылки на подробную информацию

    http://dwheeler.com/secure-programs/Secure-Programs-HOWTO.html
    -документ HOWTO (соображения «как сделать») о безопасном программировании в Linux, но рассмотренные в нем концепции и технологии применимы и в других ситуациях. http://www.cs.ucdavis.edu/~bishop/secprog.html
    содержит лучшие способы безопасного программирования от эксперта по безопасности Мэтта Бишопа (Matt Bishop). http://www.homeport.org/~adam/review.html -
    список указаний по написанию безопасного кода от Адама Шостака (Adam Shostack). http://www.dnaco.net/~kragen/security-holes.html -
    хороший документ о том, как искать дыры в защите (особенно в собственном коде) от Крегена Ситэйкера (Kragen Sitaker). http://www.shmoo.com/securecode/
    предлагает отличную коллекцию статей о том, как писать безопасные программы. «Perl CGI Problems»,
    Rain Forest Puppy (Phrack Magazine, 1999). Электронный вариант можно найти на http://www.insecure.org/news/P55 07.txt или в архивах Phrack на http://www.phrack.com/archwe.html. «Perl Cookbook»,
    Tom Christiansen, Nathan Torkington (O'Reilly, 1998) -эта книга рецептов содержит много хороших советов по созданию безопасных программ.

    Установка модулей на MacOS

    Установка модулей на MacOS

    Установка модулей на MacOS - это странный гибрид уже рассмотренных методов. Крис Нандор (Chris Nandor) собрал дистрибутив срап-тас (его можно найти либо на CPAN, либо на http://pudge.net/macperl), в состав которого входит перенесенная на MacOS версия CPAN, а также невероятное количество других модулей.
    После установки дистрибутива срап-тас можно при помощи CPAN загружать и устанавливать большинство модулей, реализованных исключительно на языке Perl. Нандор упростил эту задачу, написав небольшое приложение installme. Архивные файлы (т. е. файлы .tar.gz), переданные installme, будут разархивированы и установлены в стиле CPAN. Подробности об установке модулей на MacOS можно найти в расширенной версии документа perlmodinstall.pod, упомянутого ранее как macperl modinstall.pod. Его можно найти также и на http://pudge.net > macperl.

    Процесс установки модулей на платформе

    Установка модулей на Win32

    Процесс установки модулей на платформе Win32 совпадает с процессом установки модулей на Unix, но требует одного дополнительного шага — ррт. Если вы собираетесь устанавливать модули вручную, следуя инструкциям по Unix, то можете использовать программы, подобные WinZip (http://www.winzip.com), чтобы распаковать дистрибутив, и nmake (ftp://ftp.microsoft.com/Softlib/MSLFILES/nmakel5.exe) вместо make - для сборки и установки модуля.

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

    Система РРМ похожа на модуль CPAN. Для загрузки и установки специальных файлов архивов из репозиториев РРМ используется программа ppm.pl, написанная на Perl. Вы можете запустить ее, либо набрав ррт, либо выполнив команду perl ppm.pl из каталога bin:

    C:\Perl\bin>perl ppm.pl

    РРМ interactive shell (1.1.1) - type 'help' for available commands.

    PPM> install module-name

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

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

    Файловые системы

    Файловые системы

  • Perl приходит на помощь
  • Различия файловых систем
  • Прогулка по файловой системе
  • Обход файловой системы при помощи модуля File::Find
  • Работа с дисковыми квотами
  • Получение сведений об использовании файловой системы
  • Информация о модулях из этой главы
  • Источники подробной информации


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

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

    Имя Идентификатор на CPAN Версия
    File : : Find (входит в состав Perl)
    File : : Spec (входит в состав Perl)
    Cwd (входит в состав Perl)
    Win32: :File: :GetAttributes (входит в состав ActiveSta-te Perl)
    Win32 : : FileSecu rity (входит в состав ActiveState Perl)
    File : : Basename (входит в состав Perl)
    Getopt : : Std (входит в состав Perl) Quota
    TOMZO 1.2.3
    срап-тас (связка) CNANDOR 0.40
    Mac: :AppleEvents: : Simple CNANDOR 0.81
    Mac: :Glue CNANDOR 0.58


    Источники подробной информации

    Источники подробной информации

    Страницы руководства по perlport - просто неоценимый источник информации для Perl-программистов о различиях платформ. Крис Нандор (Chris Nandor) и Гурасами Сарати (Gurasamy Sarathy) дискутировали по поводу первых версий этого руководства на Perl Conference 2.0; Нандор опубликовал некоторые выдержки из этой беседы на http:// pudge.net/macperl/tpc/98.

    MacOS

    MacOS

    Несмотря на GUI-ориентированный подход, иерархическая файловая система MacOS (HFS, Hierarchical File System) также позволяет указывать текстовые имена файлов, хотя для этого нужно немного изловчиться. Абсолютные пути задаются в следующем виде: Диск/Имя_тома:Папка:Папка:Папка:Имя_файла. Отсутствие двоеточий указывает на то, что файл находится в текущем каталоге.
    В отличие от двух предыдущих операционных систем, пути в HFS считаются абсолютными, если не начинаются с разделителя пути (:). Путь HFS, начинающийся с двоеточия, является относительным. Есть небольшое отличие записи пути в MacOS по сравнению с другими файловыми системами - это количество разделителей, которое необходимо указывать при ссылке на объект, стоящий выше в иерархии каталогов. Например, в Unix используется ../../../FileNamc для обращения к файлу, находящемуся тремя уровнями выше текущего каталога. В MacOS понадобилось бы использовать четыре разделителя (т. е. File-Name), поскольку необходимо включить ссылку на текущий каталог помимо трех предыдущих уровней.
    В HFS длина имен файлов и каталогов ограничена 31 символом. В MacOS версии 8.1 был введен альтернативный многосимвольный формат, названный расширенным форматом MacOS, или HFS+, для поддержки Unicode в именах файлов длиной до 255 символов. И хотя файловая система HFS+ позволяет использовать такие длинные имена, на момент написания этой книги они еще не поддерживаются в MacOS.
    Еще более заметным отличием от предыдущих двух файловых систем (по крайней мере, с точки зрения программирования на Perl) является использование в MacOS понятия «fork» (ветвление) при хранении файлов. У каждого файла есть поток данных (data fork) и поток ресурсов (resource fork). В первом хранятся данные, а во втором содержатся различные ресурсы. В эти ресурсы могут входить исполняемый код (в случае, если это программа), определения пользовательского интерфейса (диалоговые окна, шрифты и т. д.) или любые другие компоненты, определяемые программистом. И хотя в этой главе мы не будем рассматривать ветвления, в MacPerl есть возможность чтения и записи в оба потока.
    В MacPerl основные операторы и функции работают только с потоком данных. Например, оператор —s возвращает только размер потока данных файла. Если вы хотите обратиться к потоку ресурсов файла, вам придется использовать дополнительные модули, входящие в состав Macintosh Toolbox.
    Каждый файл в файловой системе HFS также имеет два специальных тега: creator (создатель) и type (тип), позволяющие операционной системе идентифицировать, каким приложением был создан файл и какого он типа. Эти теги играют ту же роль, что и расширения, используемые в файловых системах FAT (например .doc или .ехе). Позже в этой главе мы увидим, как применять теги тип/создатель в собственных целях.

    в данной книге речь идет

    Microsoft Windows NT/2000

    Windows NT ( в данной книге речь идет о версии 4.0) поставляется с двумя поддерживаемыми файловыми системами: файловой системой FAT (таблица размещения файлов) и NTFS (файловая система NT). В Windows 2000 добавлена файловая система FAT32 - улучшенная версия FAT, позволяющая иметь разделы больших размеров и кластеры меньших размеров.

    В Windows NT используется расширенная версия файловой системы FAT из DOS. Перед тем как рассматривать расширенную версию, очень важно разобраться в недостатках исходной. В обычной файловой системе (FAT реального режима) имена файлов должны соответствовать формату 8.3. Это означает, что имена файлов или каталогов могут содержать не более восьми символов, за которыми должна следовать точка, а затем суффикс длиной не более трех символов. В отличие от Unix, где точка в имени файла не имеет специального назначения, в FAT можно использовать только одну точку в качестве разделителя между именем файла и его расширением (суффиксом).

    Позднее файловая система FAT была расширена до VFAT, или «FAT защищенного режима». Эта версия поддерживается в Windows NT и Windows 2000. VFAT скрывает от пользователей все ограничения, накладываемые на имена. Более длинные имена файлов без разделителей поддерживаются благодаря хитрому трюку. В VFAT используется цепь из нескольких стандартных слотов для имен файлов/каталогов, чтобы прозрачно встроить поддержку расширенных имен файлов в структуру обычной файловой системы FAT. В целях совместимости к каждому файлу и каталогу по-прежнему можно обратиться, используя псевдонимы в формате 8.3. Например, к каталогу с именем Downloaded Program Files можно обратиться, используя имя DOWNLO-1.

    Между VFAT и файловыми системами Unix существуют четыре основные различия:

  • Файловые системы FAT не чувствительны к регистру. В Unix попытка открыть файл, используя неверный регистр символов (например MYFAVORITEFILE вместо myfavoritefile), окончится неудачей. В FAT или VFAT это можно сделать без труда.
  • Второе различие - символы, выбранные для разделения компонент пути. В FAT в качестве разделителя вместо прямого слэша используется обратный слэш. При программировании в Perl это следует учитывать. В Perl обратный слэш является специальным симво лом. Пути с одинарными разделителями, заключенные в одинарные кавычки (т. е. $path= ' \dir\dir\fiienane '), не вызывают затруднений. Однако необходимость поставить несколько обратных слэшей рядом (например \\server\dir\file) может оказаться потенциально опасной. В таких случаях будьте бдительны и не забывайте удваивать идущие подряд обратные слэши. Некоторые функции и модули могут принимать пути с прямыми слэшами, но при программировании на это лучше не полагаться. Лучше перестраховаться и написать \\\\winnt\\teirip\\. . ., чем выяснить, что у вас ничего не получилось из-за того, что преобразование не было выполнено.
  • В FAT с файлами и каталогами связаны специальные флаги, назы ваемые атрибутами. Примеры атрибутов - «Read-only» (только для чтения) и «System» (системный).
  • Наконец, последнее отличие - указание корневого каталога. Корень в FAT начинается с указания буквы диска, на котором располагается эта файловая система. Например, абсолютный путь к файлу может быть указан так c:\home\cindy\docs\resume\current.doc.
  • Файловые системы FAT32 и NTFS имеют ту же семантику, что и VFAT. В них одинаково реализована поддержка длинных имен файлов и используется один и тот же способ обозначения корневого каталога. Поддержка длинных имен в NTFS несколько сложнее, т. к. там разрешено использование Unicode в именах файлов. Unicode - это многобайтовая кодировка, которую можно применять для представления всех символов всех языков планеты.

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

    Перед тем как перейти к другой операционной системе, очень важно хотя бы упомянуть UNC - универсальное соглашение об именовании. UNC - это соглашение о расположении объектов (в нашем случае файлов и каталогов) в сетевом окружении. Вместо имени диска и двоеточия, с которых начинается абсолютный путь, часть имя диска: заменяется на \\сервер\имя_ресурса. Это соглашение подвержено той же синтаксической двусмысленности обратного слэша в Perl, о которой мы уже говорили.

    Perl приходит на помощь Лэптопы

    Различия файловых систем

    Начнем с краткого обзора файловых систем, свойственных каждой из рассматриваемых операционных систем. Возможно, это не представляет для вас ничего нового, особенно, если у вас есть значительный опыт работы с какой-либо операционной системой. Но все же стоит обратить внимание на различия между файловыми системами (особенно на те, которые вам не знакомы), если вы собираетесь писать программы на Perl, работающие на разных платформах.
    Unix
    Все современные разновидности Unix поставляются с файловыми системами, семантика которых напоминает семантику их общего предка -файловой системы Berkeley Fast File System. Различные производители по-разному расширяли свои файловые системы (так, в Solaris добавили списки контроля доступа (Access Control Lists) для большей безопасности, в Digital Unix стали применять файловую систему advfs, основанную на транзакциях, и т. д.). Мы будем писать код, «приведенный к общему знаменателю», что позволит ему работать на различных Unix-платформах.
    Вершина, или корень файловой системы, в Unix обозначается символом прямого слэша (/'). Для того чтобы уникальным образом идентифицировать файл или каталог в файловой системе, мы строим путь, начинающийся со слэша, и добавляем в него каталоги, разделяя их слэшами, по мере прохода «вглубь» файловой системы. Последний компонент пути - нужный каталог или файл. В современных вариантах Unix имена каталогов и файлов чувствительны к регистру символов. При известном навыке в именах можно использовать практически все ASCII-символы, но если ограничиться буквенно-цифровыми символами и некоторыми знаками пунктуации, то в дальнейшем можно избежать лишних сложностей.

    Получение сведений об использовании файловой системы

    Получение сведений об использовании файловой системы

    Располагая методами контроля над использованном файловой системы, которые мы только что рассмотрели, вполне естественно проверить, как они работают. Чтобы закончить эту главу, давайте рассмотрим способ получения сведений об использовании файловой системы для каждой из операционных систем.
    MacOS - операционная система, для которой эта задача наиболее сложна. В MacOS есть процедура PBHGel из Macintosh Toolbox для получения информации о томах, но в настоящее время не существует модулей MacPerl, которые сделали бы простым вызов этой функции. Вместо этого мы используем обходной путь и поручим Finder запросить эту информацию для нас. Благодаря модулю связки это легко осуществить, но из-за необходимости предварительных действий в MacOS эта задача выполняется сложнее всего.
    Все используемые далее материалы основаны на работе Криса Нандора (Chris Nandor) и их можно найти на http://pudge.net или на CPAN. Наберитесь терпения, пока мы будем рассматривать процесс настройки шаг за шагом:
  • Установите связку срап-тас. В срап-тас входит модуль CPAN, ргс, написанный Андреасом Кенигом (Andreas J. Konig), и другие нужные методы, о которых говорилось в главе 1. Даже если вы не хотите получать сведения об использовании файловой системы в MacOS, эту связку лучше установить. После того как вы ее установите, обязательно выполните все инструкции из файла README.
  • Установите последнюю версию модуля Mac: :AppleEvents: :Simple, перетащив файл дистрибутива в installme.
  • Установите модуль Mac::Glue. «Крошка» installme распаковывает содержимое дистрибутива Mac : :Gluc в новый каталог в процессе установки. Обязательно запустите сценарии установки gluedialect и glue script adds из подкаталога scripts того каталога, в который распакован дистрибутив.
  • Создайте файл связки для Finder. Откройте System Folder и перетащите файл Finder на вершину gluemac, чтобы создать необходимый файл (и, что очень приятно, документацию для него).
  • Этот сложный процесс установки позволяет нам написать такую простенькую на вид программу:
    use Mac::61ue qw(:all);
    $fobj = newMac::61ue 'Finder';
    Svolumename = "Macintosh HD"; ft имя одного из смонтированных дисков.
    Stotal = $fobj->get($fobj->prop('capacity', disk => Svolumename), as => 'doub');
    $free = $fobj->get($fobj->prop('free_space', disk => Svolumename), as => 'doub');
    print "свободно $free байт из $total\n";
    Перейдем к более простым темам. Для того чтобы получить ту же информацию на компьютере с Win32, мы могли бы использовать модуль Win32: : AdminMisc, написанный Дэйвом Ротом (Dave Roth):
    use Win32::AdminMisc;
    ($total,$free) = Win32::AdminMisc::GetDriveSpace("c:\\");
    print "свободно $free байт из $total\n";
    И, наконец, закончим эту главу рассмотрением эквивалентного варианта для Unix. Существует несколько модулей для Unix, включая Fi-lesys: :DiskSpace Фабьена Тассена (Fabien Tassin), Filesys: :Df Яна Гафри (Ian Guthrie) и Filesys: :DiskFree Алана Баркли (Alan R. Barclay). В первых двух из них используется системный вызов statvf s(), а последний анализирует вывод Unix-команды df на всех поддерживаемых системах. Выбор какого-либо из этих модулей зависит от ваших предпочтений и от того, что поддерживается вашей операционной системой. Я предпочитаю Filesys: :Df, поскольку он предлагает массу возможностей и не запускает другой процесс (потенциальный риск, как уже говорилось в главе 1) во время запроса. Вот один из способов написания эквивалентного двум предыдущим примерам варианта:
    use Filesys::Df; $fobj = df("/");
    print $fobj->{su_bavail}*1024." байт в ".
    $fobj->{su_blocks}*1024." байт свободно\n";
    Нам необходимо выполнить некоторые расчеты (а именно: *1024), поскольку Filesys: : Df возвращает значения в блоках, а каждый блок равен 1024 байтам в нашей системе. Функции df () этого модуля может быть передан второй необязательный параметр, определяющий размер блока, если это необходимо. Следует также отметить в этом коде два запрошенных значения хэша. su_bavail и su_blocks - это возвращенные модулем значения размера диска и объема использованного пространства. В большинстве файловых систем Unix команда df выводит информацию об использованном пространстве, скрывая 10% диска, зарезервированных для суперпользователя. При желании узнать размер доступного и свободного в настоящий момент дискового пространства с точки зрения обычного пользователя мы должны использовать user_blocks и user_bavail.
    Мы только что видели ключевые фрагменты кода, при помощи которых можно создавать более сложные системы, наблюдающие и управляющие пространством на дисках. Эти наблюдатели за файловыми системами помогут вам решить проблемы с пространством на дисках еще до их появления.


    Прогулка по файловой системе Наверняка

    Обход файловой системы при помощи модуля File::Find

    Теперь, когда вы познакомились с основами просмотра файловой системы, покажем более быстрый и изящный способ сделать то же самое. В Perl есть модуль F; ie: : Fi nd, позволяющий эмулировать команду find из Unix. Самый простой способ начать работать с этим модулем - воспользоваться командой find2perl для генерирования кода-прототипа Perl.
    С flnd2perl не всегда легко работать в Perl в отличных от Unix-системах. Для ее выполнения пользователям MacOS, например, понадобится Macintosh Programmer's Workshop (MPW) либо им придется изменить исходный код, чтобы получать ffARGV в диалоговом окне. Вот пример кода, позаимствованный у Криса Нандора (Chris Nandor), соавтора «MacPerl: Power and Ease» (MacPerl: Сила и легкость):
    @ARGV = @ARGV 9 @ARGV :
    split '\s". MacPerl: :Ask( "Arguments'7' );
    Во всех перенесенных версиях есть модуль File; Fid, который используют команды find2perl и find.pl, так что ото не должно стать проблемой. Позже в этой главе мы покажем, как вызвать его напрямую.
    Предположим, что вам нужна программа для поиска файлов beeskneen в каталоге /home. Вот командная строка, использующая команду Unix find:
    % find /home -name beesknees -print
    Передадим те же параметры команде find2perl:
    % find2perl /home -name beesknees -print и получим:
    S'/usr/bin/oerl
    eval 'exec /usr/bin/perl -S $0 ${1 + 'W}' if Srunning under some shell;
    require "find.pl";
    ff Обходим нужные файловые системы
    &find('/home'):
    exit;
    sup wanted {
    /"bee^kneesS/ && p--irt("$narrev' ';.
    }
    Код, сгенерированный командой find2perl, довольно прямолинеен. Он загружает необходимую библиотеку find.pl при помощи оператора require, затем вызывает подпрограмму &гinc() с именем начального каталога. Вскоре мы обсудим назначение подпрограммы &war'terj( ), поскольку именно здесь будут находиться все интересные изменения, которые мы хотим изучить. Перед тем как вносить изменения в этот код, важно обратить внимание на те немногие моменты, которые могут не показаться очевидными при рассмотрении приведенного фрагмента:
  • Те, кто работал над модулем File: :Find, столкнулись с проблемой переносимости этого модуля на другие платформы. Внутренние подпрограммы модуля File: : Find действуют так, что один и тот же код работает и в Unix, и в MacOS, и в NT, и в VMS и т. д.
  • Хотя код, сгенерированный find2perl, на первый взгляд похож на код Perl 4 (например, тут используется require для загрузки файла .pi), find.pl в действительности устанавливает несколько псевдонимов из PerlS. Обычно бывает полезно заглянуть «под завесу», прежде чем использовать модуль в собственной программе. Если вам нужен исходный код модуля, уже установленного в системе, то, выполнив команду perl -F или следующую команду, вы получите список каталогов стандартных библиотек:
  • % perl-e 'print join("\n",@INC,"")'
    Давайте поговорим о подпрограмме &wanted(), которую мы изменим для своих нужд. Подпрограмма &wanted() вызывается для каждого найденного &find() (или &File: : Find: :f ind(), чтобы быть точным) файла или каталога при обходе файловой системы. Именно код из &war': j(} должен выбирать «интересные» файлы или каталоги и работать с ними. В примере, приведенном выше, сначала проверяется соответствие имени файла или каталога строке beesknees. Если они совпадают, оператор && заставляет Perl выполнить оператор print, чтобы вывести имя найденного файла.
    При создании собственных подпрограмм &wanted() нам придется учитывать два практических момента. Поскольку &w,,nted() вызывается по одному разу для имени каждого файла или каталога, важно сделать этот код коротким и аккуратным. Чем быстрее мы сможем выйти из подпрограммы &warted(), тем быстрее find сможет перейти к новому файлу или каталогу и тем быстрее будет выполняться вся программа. Также важно иметь в виду «закадровые» соображения совместимости, о которых мы недавно упоминали. Было бы позором одновременно иметь переносимый вызов &fina() и системно-зависимую подпрограмму &wanted() (кроме случаев, где этого невозможно избежать). Несколько подсказок, помогающих избежать такой ситуации, можно получить, посмотрев на текст модуля File: : Find.
    Для первого использования модуля File:: Find давайте перепишем пример, созданный для удаления core-файлов, и затем немного его расширим. Для начала наберем:
    # find2perl -name core -print что даст нам следующее: require "! ind.pi"
    &find('. ' ) exit,
    sub wanted !
    /"coreS/ && DrintC'Sname^i"):
    }
    Затем мы добавим -s к строке вызова Perl и изменим подпрограмму
    &wanted():
    sub wanted (
    /"core$/ && prin+("$i>ame\n") && dpfired $r && ::nifik($rapf4;
    При вызове программы с ключом -г мы получаем необходимую нам функцию (удаление core-файлов). Внесем небольшое изменение, добавляющее некоторую степень защиты нашему потенциально разрушительному коду:
    oud wanted {
    /"coreS/ && -s $nairie *& print("$name\n") &&
    defined $r s& i;nlink($name); }
    Теперь, перед тем как выводить имя файла или удалять его, мы проверяем, не является ли размер файла core нулевым. Некоторые пользователи иногда создают ссылку на /dev/null с именем core в своем домашнем каталоге, чтобы core-файлы в нем не сохранялись. Параметр —s указывается для того, чтобы убедиться, что по ошибке не будут уда лены ссылки или файлы нулевой длины. Если мы хотим действовать осторожнее, нам следует выполнить еще две дополнительные проверки:
  • Открыть файл и убедиться, что этот файл действительно является core-файлом. Сделать это можно как из Perl, так и вызвав команду Unix file. Определить, что файл действительно является core-файлом, может оказаться достаточно сложно в случае, если файловые системы смонтированы по сети с компьютерами другой архитектуры с иными форматами core-файлов.
  • Посмотреть на время изменения файла. Если кто-то в настоящий момент отлаживает программу, используя файл core, он вряд ли обрадуется, если вы утащите этот файл прямо «из-под нее».
  • Давайте на время отдохнем от мира Unix и посмотрим на примеры, имеющие отношение к MacOS и Windows NT/2000. Ранее в этой главе я уже говорил, что в MacOS у каждого файла есть два атрибута - созда телъ и тип, позволяющие операционной системе определить, какое приложение создало этот файл и какого он типа. Эти атрибуты хранятся в виде четырехсимвольных строк. Например, для текстового документа, созданного приложением SimpleText, эти атрибуты будут иметь значения ttxt (для создателя) и TEXT (для типа). Из Perl (только для MacPerl) мы можем получить эту информацию при помощи функции MacPerl: :GetFileInfo(). Синтаксис ее таков:
    $type = MacPerl::GetFileInfo(filename); или:
    ($creator,$type) = MacPerl::GetFileInfo(filename); Чтобы найти все текстовые файлы в файловой системе MacOS, мы можем выполнить следующее:
    use File::Find;
    &File::Find::find(\&wanted,"Macintosh HD:");
    sub wanted{ -f $_ && MacPerl;:GetFileInfo($_) eq "TEXT" &&
    print "$Find::File::name\n"; }
    Вы, должно быть, заметили, что это выглядит немного иначе, чем наши предыдущие примеры. Однако действует этот код точно так же. Мы просто вызываем процедуры из File: :Find напрямую, без find.pl. Кроме того, мы используем переменную $name, определенную в пространстве имен File: :Find, чтобы вывести абсолютный путь файла, а не только его имя. Взгляните на полный список переменных, определяемых File: : Find при обходе файловой системы (табл. 2.2).


    Работа с дисковыми квотами Perlсценарии

    Первый способ требует некоторой хитрости с нашей стороны. Совсем недавно мы упоминали процесс ручной установки дисковых квот: edquota вызывает редактор, в котором пользователь редактирует небольшой текстовый файл, после чего эти изменения используются для обновления информации о квотах. Не существует никаких указаний на то, какие данные необходимо вводить, чтобы внести изменения. В действительности, не существует даже ограничений на то, какой редактор будет применяться. Все, что нужно команде edquota, это программа, которую можно запустить и которая необходимым образом изменит маленький текстовый файл. Подойдет любой допустимый путь (заданный переменной окружения ;TI IOR) к такой программе. Почему бы не указать программе edquota сценарий на Perl? Давайте и рассмотрим такой сценарий в нашем следующем примере.
    Наш сценарий должен будет выполнять две задачи: во-первых, он должен принимать от пользователя аргументы командной строки, устанавливать соответствующим образом переменную окружения EDITOR и вызывать программу edquota. В свою очередь, edquota запустит другую копию нашей программы, которая и займется собственно редактированием этого временного файла. Ниже показана схема действий (Рисунок 2.1).
    Второй копии программы необходимо сообщить, что именно она должна изменить по требованию исходной программы. Как она получает эту информацию из первой копии, вызвавшей edquota, менее очевидно, чем этого бы хотелось. В странице руководства по edquota сказано: «Вызывается редактор vi(l), если только в переменной EDITOR не указано иное». Идея передать аргументы командной строки через EDITOR или другую переменную окружения довольно опасна хотя бы потому, что мы не знаем, как на это отреагирует утилита edquota. Поэтому нам придется полагаться на один из методов межпроцессного взаимодействия, доступных в Perl. Например, два процесса могут:
  • Передавать друг другу временный файл
  • Создать именованный канал и общаться по нему
  • Передавать AppleEvents (в MacOS)
  • Использовать объект синхронизации (inutex) или оговоренные ключи реестра (в NT/2000)
  • Общаться через сокеты
  • Использовать разделяемую память
  • И так далее. От вас как от программиста зависит, какой метод вы выберете, хотя зачастую определять выбор будут данные. Рассматривая их, вы будете принимать во внимание:
  • Направление соединения (одно- или двунаправленное?)
  • Частоту соединения (будет передано одно сообщение или несколько кусочков?)
  • Размер данных (будет это 10-мегабайтный файл или 20 символов?)
  • Формат данных (будет это двоичный файл или просто текст фиксированной ширины, разделенный определенным символом?)
  • Наконец, учитывайте то, насколько сложным вы хотите сделать свой сценарий.
    В нашем случае мы собираемся выбрать простой, но мощный метод о о мена информацией. Так как первый процесс должен передать второму только набор инструкций по изменению информации (какие квоты изменять и на какие значения), мы установим между ними стандартный канал Unix. Первый процесс пошлет запрос на изменение в поток вывода, а копия, запущенная программой edquota, прочитает эту информацию со своего потока ввода.
    Давайте напишем программу. Первое, что должна делать программа при запуске - это решить, какую роль она должна выполнять. Пусть при первом вызове она получает несколько аргументов командной строки (то, что надо изменить), тогда как во второй раз, уже вызванная программой edquota, она получает только один аргумент (имя временного файла). Программа требует наличия нескольких ключей командной строки, если вызывается более чем с одним аргументом, поэтому мы можем полагаться на данное допущение при выборе роли для нашего сценария. Вот как выглядит код, определяющий роль сценария:
    $edquota = "/usr/etc/edquota"; и путь к edquota
    Sautoedq = "/usr/adm/autoedquota"; ц полный путь к этому сценарию
    ft это первый или второй запуск?
    ft если присутствует более одного аргумента - это первый запуск
    if ($»ARGV > 0) {
    &ParseArgs;
    &CallEdquota; }
    и в противном случае это второй запуск, и мы должны выполнить редактирование
    else {
    &EdOuota(); }
    Рассмотрим код, вызываемый при первом запуске и используемый для анализа аргументов и вызова edquota через канал:
    sub ParseArgsf
    use Getopt: :Std;
    # для обработки параметров
    # Устанавливаем переменную $opt_u равной идентификатору и пользователя,
    $opt_f - равной имени файловой системы,
    $opt_s - в значение для мягкого ограничения и
    $opt_h -К в значение для жесткого ограничения getopt("u:f:s:h:");
    двоеточие говорит о том, что у этого
    # ключа есть аргумент die "ИСПОЛЬЗОВАНИЕ:
    $0 -u uid -f -s -h \n"
    if (<$opt_u || !$opt_f || !$opt_s || !$opt_n); }
    sub CallEdquotaf
    $ENV{"EDITOR"} = Sautoedq;
    записываем в
    # переменную окружения EDITOR путь к нашему сценарию
    operKEPROCFSS. "|$edquota $opt_u") or die
    "Невозможно запустить edquota :$! \r,":
    посылаем измененные строки во вторую копию сценария
    print EPROCESS "$opt_f|$opt^s|$opt_.h\n";
    close(EPROCESS); }
    Вот вторая часть выполняемого действия:
    sub EdOuota {
    Stfile = $ARGV[0];
    получаем имя временного файла edquota
    open(TEMPFILE, Stfile) or die "Невозможно открыть временный файл
    # открываем файл-черновик, можно было бы и использовать и
    new_tmpfile() open(NEWTEMP, ">$tfile.$$") or die "
    Невозможно открыть временный файл-черновик Stfile.$$:$!\";
    # получаем строку ввода из первого вызова и отсекаем символ \
    chomp($change = );
    my($fs,$soft,Shard) = split(/\|/,$change);
    разбираем ответ
    считываем из временного файла строку. Если она содержит
    информацию о файловой системе, которую мы хотим
    изменить, изменяем эти значения. Записываем строку
    (вероятно, измененную) в черновик, while (){
    перезаписываем временный файл измененным черновиком,
    так что изменения передаются edquota rename("Stfile.$$",Stfile)
    or die "Невозможно переименовать
    $tfile.$$ в $t*ile:$!\n":
    }
    Приведенная выше программа - это всего лишь скелет, но он все же описывает способ автоматического изменения квот. Если вы когда-то пытались изменять вручную квоты для многих пользователей, приведенный выше пример должен вас порадовать. Перед тем как использовать тот сценарий для внесения реальных изменений, необходимо добавить механизм, защищающий от внесения противоречащих друг другу изменений, а также механизм проверки ошибок. В любом случае, технология такого рода может вам пригодиться и в других ситуациях, помимо работы с квотами.

    Редактирование квот при помощи модуля Quota

    Редактирование квот при помощи модуля Quota

    Когда-то очень давно предыдущий метод (или, если быть честным, предыдущий «хак») был единственным способом автоматизировать изменения квот, если, конечно, вас не радовала перспектива редактирования системных вызовов из библиотеки С, чтобы встроить их в интерпретатор Perl. Теперь, когда механизм расширений Perl существенно упростил встраивание библиотечных вызовов в Perl, создание модуля Quota для Perl стало только делом времени. Благодаря Тому Зорнеру (Tom Zoerner) и другим процесс установки квот средствами Perl теперь намного проще, если этот модуль поддерживает вашу версию Unix. Если нет, предыдущий метод все равно будет работать нормально.
    Вот небольшой пример, в котором принимаются те же аргументы, что и в предыдущем:
    use Getopt::Std; use Quota:; '
    getopt("u:f:s:h:");
    die "USAGE: $0 -u uid -f -s -h \n" if (!$opt_u || !$opt_f || ISopt^s || !$opt_h);
    $dev = Quota::getcarg($opt_f) or die "Невозможно преобразовать путь
    $0ptc;f:$!\n";
    ($curblock,$soft,Shard,Sbtimeout,Scurinode,$isoft,Sihard,$itimeout)=
    Quota::query($dev,$uid) or die "Невозможно запросить квоту для
    $uid:$!\n";
    Quota::setqlim($dev,$opt_u,$opt_s,$opt_h,$isoft,$ihard,1) or die " Невозможно установить квоту:$!\n";
    После анализа аргументов остаются три простых шага: во-первых, мы используем Quota: :getcarg() для получения идентификатора устройства, который передается другим подпрограммам. Затем мы передаем этот идентификатор и идентификатор пользователя функции qjc-ta: :query(), чтобы получить текущие параметры квот. Нам нужны эти настройки, чтобы не нарушить ограничения, которые мы не будем изменять (например, число файлов). Наконец, мы устанавливаем квоту. Вот и все, всего лишь три строчки кода на Perl.
    Помните, что девиз Perl TMTOWTDI означает «существует более одного способа сделать это», но это вовсе не значит «несколько одинаково хороших способов».


    Сводка различий файловых систем

    Сводка различий файловых систем

    Ниже представлены те различия, о которых мы только что говорили, и некоторые другие интересные факты (табл. 2.1).


    Сравнение файловых систем

    Таблица 2.1. Сравнение файловых систем

    OS и файловая система Разде
    литель пути
    Чувстви
    тельность к регистру
    Длина
    имени
    файла
    Формат абсолют
    ного пути
    Формат относи
    тельного пути
    Уникальные возмож
    ности
    Unix (файловая система - Berkeley Fast File System и другие) / Да В зависимости от операционной системы /dir/file dir/file Дополнения в зависимости от операционной системы
    MacOS (HFS) : Да 31 символ (или 255 при использовании HFS+) volume:
    dir:file
    :dir:file Потоки данных/ ресурсов, атрибуты создатель/
    тип
    WinNT/2000 (NTFS) \ Нет 255 символов Drive:\
    dir\file
    dir\file ACL, атрибуты, Unicode в именах файлов
    DOS (BASIC FAT) \ Нет 8.3 Drive:\
    dir\file
    dir\file Атрибуты


    Переменные File Find

    Таблица 2.2. Переменные File::Find

    Переменная Смысл
    $_ Имя текущего файла
    $File: :Find: :dir Имя текущего каталога
    $File: : Find: : name Полный путь для текущего файла (т. е. $File: : Find :dir/$_)
    Вот похожий пример, но для NT/2000:
    use File::Find: use Win32::File:
    &File::Find':find(\&wapted."\\");
    sub wanton i -f $.. && ft значение переменной attr присваивается функцией
    # Win32::File::GetAttributes (Win32: :File: :GetAttnbutes($_. Sattr)) &&
    ($at.tr & HIDDEN) &&
    print "SFile: : Find: : narreV1",
    }
    Этот пример ищет по всей файловой системе на текущем диске все скрытые файлы (т. е. те файлы, у которых установлен атрибут HIDDEN). Этот код работает и на NTFS и на FAT.
    А вот пример для файловой системы NTFS, который ищет все файлы, если к ним разрешен полный доступ для специальной группы Everyone, и выводит их имена:
    use File::Find;
    use Win32: : FileSecunty;
    tt Определяем маску DACL для полного доступа $fullmask = Win32:
    : FileSecunty: :MakeMask(FULL);
    &find(\&wanted,"\\");
    sub wanted {
    fl Win32::FileSecurity::Get не любит файл подкачки и pagefile.sys, пропустить его
    next if ($_ eq "pagefile.sys"); (-f $_) &&
    Win32: :FileSecunty: :Get($_, \%users) &&
    (defined $users{"Everyone"}) &&
    ($users{"Everyone"} == Sfullmask) &&
    print "$File::Find::name\n";
    }
    В вышеприведенном коде мы запрашиваем все файлы у списка контроля доступа ACL (кроме файла подкачки Windows NT). Затем мы проверяем, есть ли в этом списке запись для группы Everyone. Если есть, мы сравниваем запись Everyone со значением для полного доступа (полученным MakeMask()) и выводим абсолютный путь файла, если они совпадают.
    А вот еще один пример из реальной жизни, демонстрирующий, насколько полезным может оказаться даже самый простой код. Недавно я пытался дефрагментировать (заново перестроенный) раздел NT на диске своего портативного компьютера, но все закончилось сообщением об ошибке Metadata Corruption Error (повреждение метаданных). Внимательно изучая веб-сайт производителя программного обеспечения, я нашел там замечание, что «такая ситуация может быть вызвана наличием файлов, длина имен которых превышает допустимую в Windows NT». Там было предложено найти эти файлы, копируя каждый каталог на новое место и сравнивая количество файлов в оригинале л Обход файловой системы при помощи модуля File::Find копии. Если в копии каталога файлов меньше, необходимо найти те файлы, которые не были скопированы.
    С учетом количества каталогов в моем разделе и времени, необходимого для выполнения этой процедуры, такое решение представляется просто нелепым. Вместо этого я написал следующее, используя уже обсужденные методы:
    require "find.pl";
    ft Обходим нужные файловые системы
    &find(\&wanted. '. ')'; print "max:$max\n";
    exit;
    sub wanted {
    return unless -f $_; if (length($_) > $maxlength)f $max = $name;
    Smaxlength = length($_); }
    if (length($name) > 200) { print $name,"\rT:}
    }
    В результате будут выведены имена файлов длиной более 200 символов, а также самое длинное найденное имя. Работа сделана, спасибо Perl.
    Давайте снова вернемся к Unix, чтобы закончить этот раздел довольно сложным примером. Идея, которая представляется слишком простой в контексте системного администрирования, но в итоге может принести огромную пользу - это понятие наделения пользователя полномочиями. Если ваши пользователи могут решить свои проблемы самостоятельно при помощи средств, которые вы им предоставляете, от этого все только выиграют.
    Большая часть этой главы посвящена решению проблем, возникающих при переполнении файловой системы. Зачастую это происходит из-за того, что пользователи недостаточно осведомлены о своем окружении, либо из-за того, что слишком обременительно выполнять операции по управлению дисковым пространством. Множество писем в службу поддержки начинаются со слов «В моем домашнем каталоге больше нет свободного места, но я не знаю из-за чего». Вот скелет сценария needspace, который может помочь пользователям, столкнувшимся с этой проблемой. Пользователь просто набирает needspace, и сценарий пытается найти в домашнем каталоге пользователя то, что
    можно удалить. Он ищет файлы двух типов: резервные копии и те файлы, которые можно автоматически создать заново. Давайте внимательно рассмотрим код:
    use File::Find; use File::Bascname;
    # массив расшипеннй файлов и расширений, из которых они могут быть получены
    % derivations = (" dvi" => '.tex".
    ".aux" => ".tex",
    ".toe" => ". tex"
    " . 0" =.' C".
    }
    Мы начнем с того, что загрузим нужные нам библиотеки: знакомый уже модуль File: :Fird и другую полезную библиотеку File: :Взяема;"е. Эта библиотека пригодится при разборе путей файлов. Затем мы инициализируем хэш-таблицу известными расширениями производных файлов; например, мы знаем, что если выполнить команду ТеХ или LaTeX для файла happy.tex, мы можем получить файл happy. Jui, и чти Обход файловой системы при помощи модуля File::Find happy. можно получить, скорее всего, скомпилировав файл happy.c компилятором С. Выражение «скорее всего» употреблено потому, что иногда требуется несколько исходных файлов, чтобы сгенерировать один файл. Однако мы можем делать только простые предположения, основываясь на расширениях файлов. Обобщенный анализ зависимостей - сложная задача, и мы даже не будем пытаться решать ее здесь.
    Затем мы определяем местонахождение домашнего каталога пользователя, получая идентификатор пользователя, выполняющего сценарий ($<), и передаем его функции getpwu;d(). qetowi,ii;() возвращает информацию из файла паролей в виде списка (подробности об этом позже); индекс массива ([7]) выбирает из этого списка элемент, соответствующий домашнему каталогу. Существуют способы получить эту информацию при помощи данных из командного интерпретатора (например, обратившись к переменной окружения $НОМЕ), но в таком виде код переносится лучше.
    Когда не надо использовать модуль File::Find
    Когда метод Filt: : Find не подходит? На ум приходят четыре ситуации:
  • Если файловая система, с которой вы работаете, не следует обычной семантике, вы не сможете применять этот модуль. Например, драйвер файловой системы NTFS для Linux, который я использовал при решении проблемы с упавшим компьютером, почему-то не выводил в пустых каталогах «. » или «.. ». Это очень мешало File: : F i nrl.
  • Если вам нужно изменять имена каталогов во время обхода файловой системы, File: :Finn теряется и начинает вести себя непредсказуемым образом.
  • Если вам нужно разыменовывать символические ссылки на каталоги (для Unix), Fi le: : Find пропустит их.
  • Если вам нужно обойти файловую систему, смонтированную на вашей машине (например, файловую систему Unix, смонтированную через NFS на машине с Windows), File: :Find будет использовать семантику, принятую для «родной» файловой системы.
  • Вряд ли вы столкнетесь с этими ситуациями, но если это произойдет, загляните в раздел этой главы, посвященный обходу файловой системы вручную.
    Получив домашний каталог, мы переходим в него и начинаем сканирование, используя вызов &f ind() так же, как и в предыдущих примерах:
    $homedir = (getpwuid($<))
    # находим домашний катало; пользователе
    chdir($hO!!ied! r) or
    die "Невозможно войти в хк, домашний каталог Shoinodi':$!";
    $1=1; # не буферизованный вывод в STDOUT print "Поиск";
    find(\&wanted. "."), # проходим по каталогам, &wanted выполняет
    # всю работу
    Вот как выглядит вызываемая нами подпрограмма &wanted(). Сначала она ищет core-файлы, а также резервные копии и автосохраненные файлы, остающиеся после редактирования в emacs. Мы считаем, что эти файлы можно удалить, не проверяя существование исходных файлов (вероятно, это небезопасное предположение). Если такие файлы найдены, их размеры и пути к ним сохраняются в хэше, ключами которого являются пути к файлам, а значениями - размеры этих файлов.
    В остальной части подпрограммы подобным образом отыскиваются производные файлы. Мы вызываем подпрограмму uBascFiiotx, для того чтобы убедиться, что эти файлы можно получить из других файлов этого же каталога. Если подпрограмма возвращает значение «истина», мы сохраняем имя файла и его размер для последующего использования:
    sub wanted {
    ищем core-файлы, сохраняем их и возвращаемся $_ eq "core" &&
    ($core{$File ::Find :: n came} = (siai( })[7]) && return;
    # ищем резервные копии и автосохраненье после редактирования файла
    ($emacs{$File::Find::name} = (stat(_) ))[?]) &&
    return;
    && ($tex{$File: :Find: :name} = (stat) && return;
    # ищем производные файлов
    &BaseFileExists($File::Find::name) &&
    ($doto{$File::Find::name} = (stat(__))[7J) && return;
    Вот текст подпрограммы, проверяющей,
    можно ли получить данный файл из другого файла в этом же каталоге
    (например, существует ли файл happy, если мы нашли файл );
    sub BaseFileExists {
    my($name,$path,Ssuffix) = &File::Basename::fileparse($_[0], '\. . *' );
    # если мы не знаем, как получить файл этого типа return
    unless (defined Sderivations-1 ;{$suffix});
    # все просто, мы видели исходный файл раньше return 1 if (defined
    $baseseen{$path. $riame. $der rivations{$suf fix}});
    # если файл (или ссылка на файл) существует и имеет ненулевой размер
    return 1 if (-s $name $derivations{$su '.,ffix} &&
    ++$baseseen
    {
    Вот как выполняется эта подпрограмма:
  • &File: : Basename: : fileparse() используется для выделения из пути имени файла, пути к файлу и его суг)ффикса (например resume.dvi, /home/cindy/docs/, .dui).
  • Затем суффикс файла проверяется, чтобы определить, считаем мы этот файл производным или нет. Если нет, мы возвращаем значение 0 (т. е. «ложь» в скалярном контексте).
  • Затем мы проверяем, встречался ли нам файл, исходный (base file) по отношению к данному, и если да, то возвращаем значение «истина». В некоторых ситуациях (в частности, в случае с TeX/LaTeX), из одного исходного файла можно получить несколько производных. Такая проверка ускоряет выполнение сценария, т. к. мы в этом случае избавлены от обхода файловой системы.
  • Если мы не встречали раньше исходный файл (с тем же именем, но другим расширением), то проверяем, существует ли он и больше ли нуля его размер. Если да, мы сохраняем информацию о файле и возвращаем 1 (т. е. «истина» в скалярном контексте).
  • Теперь нам остается только вывести информацию, которую мы собрали при обходе файловой системы:
    foreach my $path (keys %core){
    print "Найден core-файл, занимающий -1.&BytesToMeg($core{$path}).
    "MB в ",&File::Basename::dirname($path).".\n": }
    if (keys %emacs){
    print
    "Следующие файлы, скорее всего, являются резервными копиями, созданными emacs:\n";
    # изменяем путь, чтобы
    # вывод был аккуратнее print "$path ($emacs{$path} байт)\"
    }
    print "\пОни занимают ".&BytesToMeg($tempsize)."MB в сумме."; $tempsize=0; }
    if (keys %tex){
    print "Следующие файлы, скорее всего, можно получить заново, если
    # вывод был аккуратнее print "$path ($tex{$path} байт)":
    }
    print ЛпОни занимают ".&BytesToMeg(Stempsize),"MB в сумме.\п": $tenipsize=0: }
    if (keys %doto)! print
    "Следующие файлы, скорее, всего, можно получить, если вновь
    $path =" s/~$homedir/"/; tf изменяем путь, чтобы
    tt вывод был аккуратнее print "$path ($doto{$path} байт)";
    }
    print "\Они занимают ".&BytesToMeg($tempsize)."MB в сумме."; $tempsize=0; }
    sub BytesToMegl # преобразуем размер в байтах в формат ХХХМВ
    return sprintfCl%.2f,($_[0]/1024000)); }
    Прежде чем закончить этот разговор, надо заметить, что предыдущий пример можно расширять множеством способов. Пределов для программ такого типа просто не существует. Вот несколько идей:
  • Используйте более сложные структуры данных для хранения расширений производных файлов и найденных файлов. Приведенный выше код был написан с расчетом на то, чтобы его было легко читать людям, не очень хорошо разбирающимся со структурами данных в Perl. В нем используются повторяющиеся фрагменты и его довольно тяжело расширить, если в этом появится необходимость. В идеале было бы неплохо, чтобы все расширения производных файлов не были связаны со специальными хэшами (например %tex) в коде.
  • Ищите каталоги, в которых веб-броузеры кэшируют страницы (это очень распространенный источник потерянного пространства на диске).
  • Предложите пользователю удалить найденные файлы. Для удаления файлов используйте оператор unlink() и подпрограмму rmpath из модуля File: :Path.
  • Больше анализируйте файлы, вместо того чтобы строить предположения по их именам.


  • Учет различий файловых систем в Perl

    Учет различий файловых систем в Perl

    Perl может помочь создавать программы, в которых учитывается большинство особенностей файловых систем. В его состав входит модуль File::Spec, позволяющий нивелировать некоторые различия между файловыми системами. Например, если мы передаем компоненты пути методу catfile таким образом:
    use File: :Spec
    Path = File: : Spec -> cat r iie( "how1 . "iocs" ' г eSi;i:C doc")
    то в Windows NT/2000 переменная $path будет иметь значение home\cindy\docs\resume. doc, тогда как в Unix она будет иметь значение cindy/docs/resume.doc и т. д. В модуле File: :Spec также есть методы, например curdir и updir, возвращающие обозначения для текущего и родительского каталогов (например « » и « »). Методы этого модуля предоставляют абстрактный способ построения и манипулирования именами путей. Если вы предпочитаете не использовать объектно-ориентированный синтаксис, то модуль File: :Spec: : Functions предоставляет более короткий путь к методам из File: Spec.

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

    Чтение кода XML при помощи XML Parser

    Чтение кода XML при помощи XML::Parser

    Скоро мы рассмотрим еще один способ построения кода XML в Perl, но сначала вернемся к чтению того кода, который только что научились создавать. Нам необходима программа, которая будет анализировать очереди добавления и удаления учетных записей, а также основную базу данных.
    Можно было бы добавить анализатор XML. Но если с нашим ограниченным набором данных без использования регулярных выражений все бы и прошло, то в случае более сложных XML-данных это вряд ли получилось бы просто. Для обычного анализа проще применить модуль XML::Parser, первоначально написанный Ларри Уоллом (Larry Wall) (он был значительно расширен и поддерживается Кларком Купером (Clark Cooper)).
    XML: : Parser - это модуль, основанный на событиях. Такие модули работают, как брокеры на бирже. Перед началом торгов вы оставляете им ряд инструкций о том, какие действия необходимо предпринять, если произойдут конкретные события (например, продать тысячу акций, если цена упадет до 31/4, купить другие акции в начале торгового дня и т. д.). В случае с программами, основанными на событиях, возникающие ситуации называются событиями (events), а список инструкций о том, что делать в случае конкретного события, - обработчиками событий (event handlers). Обработчики - это обычно специальные подпрограммы, созданные для работы с конкретным событием. Некоторые называют их функциями обратного вызова (callback routines), т. к. они выполняются тогда, когда основная программа «вызывает нас обратно» после того, как наступят определенные условия. В случае с модулем XML: :Parser, события - это явления, такие как «начало обработки потока данных», «найден открывающий тег» и «найден комментарий». А обработчики будут выполнять что-то подобное: «вывести содержимое только что найденного элемента».
    Приступая к анализу данных, необходимо сначала создать объект XML:: Parser. При создании этого объекта следует указать, какой режим анализа или стиль (style) нужно применить. XML: : Parser поддерживает несколько стилей, поведение каждого из которых при анализе данных несколько отличается. Стиль анализа определяет, какие обработчики событий вызываются по умолчанию и каким образом структурированы возвращаемые анализатором данные (если они есть).
    Некоторые стили требуют, чтобы мы указывали связь между каждым событием, которое хотим обрабатывать вручную, и его обработчиком. Для событий, не подлежащих обработке, никаких особых действий применяться не будет. Эти связи хранятся в простой хэш-таблице, ключи в ней являются именами событий, которые мы хотим обрабатывать, а значения - ссылками на подпрограммы-обработчики. В стилях, требующих наличия таких связей, мы передаем хэш посредством именованного параметра Handlers (например, Handlers => {Star-\&start_handler}) при создании объекта анализатора.
    Мы будем применять стиль stream, который не требует этого шага инициализации. Он просто вызывает группу предопределенных обработчиков событий, если указанные подпрограммы были найдены в пространстве имен программы. Обработчики событий которые мы будем использовать, очень просты: StartTag, EndTag и Text. Все названия, кроме Text, говорят сами за себя. Text в соответствии с документацией XML: :Parser «вызывается прямо перед открывающим или закрывающим тегами с накопленным неразмеченным текстом из переменной $_». Мы будем применять его, когда нам понадобится узнать содержимое конкретного элемента.
    Вот какой код будет использоваться в нашем приложении для инициализации:
    use XML::Parser;
    use Data: : Dumper; tt используется для оформления отладочного вывода, а не
    и для анализа XML
    $р = new XML::Parser(ErrorContext => 3,
    Style => 'Stream',
    Pkg => 'Account::Parse');
    Этот код возвращает объект анализатора после передачи ему трех параметров. Первый, ErrorContext, передает анализатору требование вернуть три строки контекста из анализируемых данных в случае возникновения ошибки анализа. Второй устанавливает требуемый стиль анализа. Последний параметр, Pkg, сообщает анализатору, что подпрограммы обработчика событий необходимо искать в ином пространстве имен. Устанавливая этот параметр, мы распоряжаемся, чтобы анализатор искал функции &Account;:Parse: :StartTag(), &Account::Parse: :EndTag() и т.д., а не просто &StartTag(), &EndTag() и т. п. В данном случае это не имеет особого значения, но позволяет избежать ситуации, когда анализатор может случайно вызвать другую функцию с тем же именем StartTag(). Вместо того чтобы использовать параметр Pkg, можно было добавить в самое начало приведенной выше программы строку oackag-Account::Parse;.
    Теперь посмотрим на подпрограммы, выполняющие функции обработчика событий. Рассмотрим их по очереди:
    package Account::Parse:
    sub StartTag ,
    idef %rccorj : f ($_M1 eq "account"):
    &StartTag() вызывается каждый раз, когда встречается открывающий тег. Эта функция вызывается с двумя параметрами: ссылкой на объект и именем встреченного тега. Поскольку для каждой учетной записи будет создаваться новая запись в хэше, можно использовать StartTag0, чтобы обозначить начало новой записи (например, открывающий тег ). В этом случае удаляются значения из существующего кэша. Во всех остальных случаях мы возвращаемся, ничего не выполняя:
    sub Text {
    my $ce = $_[0]->current_element();
    $record{$ce}=$_ unless ($ce eq "account");
    На этот раз мы используем &Text() для заполнения кэша %record. Как и предыдущая функция, она тоже получает два параметра при вызове: ссылку на объект и «накопленный неразмеченный текст», который анализатор нашел между последним открывающим и закрывающим тегом. Для определения элемента, в котором мы находимся, используется метод current_element(). В соответствии с документацией по XML::Parser: :Expat этот метод «возвращает имя внутреннего элемента, открытого в данный момент». То обстоятельство, что имя текущего элемента не «account», гарантирует нам, что мы находимся внутри одного из подэлементов в , поэтому можно записать имя элемента и его содержимое:
    sub EndTag {
    print Data::Dumper->Dump([\%record], ["account"]) if ($_[1] eq "account");
    И именно сейчас мы должны сделать что-то конкретное вместо
    # того, чтобы просто печатать запись
    }
    Наш последний обработчик, &EndTag(), очень похож на первый &StartTag() с тем лишь исключением, что он вызывается тогда, когда мы находим закрывающий тег. Если мы дойдем до конца соответствующей учетной записи, то сделаем банальную вещь и напечатаем эту запись. Вот как может выглядеть такой вывод:
    Saccount = {
    'login' => 'bobf
    'type' => 'staff.
    'password' => 'password',
    fullname' => 'Bob Fate'.
    id' => '24-9057
    }:
    Saccount = {
    login' => 'we'idyf',
    type' => 'fatuity',
    password => p35Sv,Gn3
    'fullname' => 'Wendy Fate',
    'id' => '50-9057'
    }:
    Если мы захотим использовать это в нашей системе учетных записей, нам, вероятно, понадобится вызвать некую функцию, например ateAccount(\%recorcl), а не выводить запись при помощи Data: : Djrr.per.
    Теперь, когда мы познакомились с процедурами инициализации и обработчиками из XML: :Parser, нам нужно добавить код, чтобы действительно начать анализ:
    # обрабатывает записи для нескольких учетных записей из
    # одного XML-файла очереди
    open(FILE, Saddqueue) or die "Unable to open $addq'jeue:$' \n":
    # спасибо Джеффу Пиньяну за это мудрое сокращение
    read(FILE, $queuecontents, -s FILE);
    $p->parse("".Squeuecontents."");
    Этот фрагмент кода, вероятно, заставил вас приподнять бровь, а то и обе. В первых двух строчках мы открываем файл очереди и считываем его содержимое в скалярную переменную Squeuecontents. Третья строка могла бы показаться понятной, если бы не забавный аргумент, переданный функции parse(). Почему мы считываем содержимое файла очереди и заключаем его в теги XML вместо того, чтобы перейти к его анализу?
    А потому, что это хак. И надо сказать - не плохой. И вот почему эти «фокусы» необходимы для анализа нескольких элементов в одном файле очереди.
    Каждый XML-документ по определению должен иметь корневой элемент (root element). Этот элемент служит контейнером для всего документа; все остальные элементы являются его подэлементами. XML-анализатор ожидает, что первый встреченный им тег будет открывающим тегом корневого элемента документа, а последний тег - закрывающим тегом этого элемента. XML-документы, не соответствующие этой структуре, не считаются корректными (well-formed).
    Попытка смоделировать очередь в XML заставит нас призадуматься. Если ничего не сделать, то первым тегом, найденным в файле, будет . Все будет работать нормально до тех пор, пока анализатор не встретит закрывающий тег для этой записи. В этот момент анализатор завершит свою работу, даже если в очереди есть другие записи, потому что он посчитает, что дошел до конца документа.
    Мы без труда могли бы добавить открывающий тег () в начало очереди, но что делать с закрывающим тегом? Закрывающий тег корневого элемента всегда должен быть расположен в самом конце документа (и не иначе), а сделать это не просто, учитывая, что мы собираемся постоянно добавлять записи в этот файл.
    Можно было бы (но это довольно неприятно) достигать конца файла при помощи функции seek(), а затем двигаться назад (опять же при помощи seok()) и остановиться прямо перед последним закрывающим тегом. Затем мы могли бы записать нашу новую запись перед этим тегом, оставляя закрывающий тег в самом конце данных. Риск повредить данные (что, если мы перейдем не в ту позицию?) должен предостеречь вас от использования этого метода. Кроме того, этот метод сложно применять, если вы не можете точно определить конец файла, например, при чтении данных XML по сетевому соединению. В подобных случаях, вероятно, стоит буферизовать поток данных, чтобы можно было вернуться к концу данных после завершения соединения.
    Метод, показанный в предыдущем примере и предлагающий добавлять пару корневых тегов к существующим данным, может показаться хаком, но выглядит он гораздо элегантнее, чем другие решения. Впрочем, вернемся к более приятной теме.

    Чтение XML при помощи XML Simple

    Чтение XML при помощи XML::Simple

    Мы уже видели один метод, позволяющий анализировать XML-данные при помощи модуля XML::Parser. Чтобы соответствовать правилу TMTOWTDI, давайте снова обратимся к этой проблеме, немного упростив задачу. Многие писали собственные модули, построенные на XML::Parser, для анализа XML-документов и возврата данных в удобной для работы форме объектов/структур данных, к их числу относятся и XML::DOM Энно Дэрксена (Enno Derksen), XML::Grove, и ToObjects (часть libxml-perl) Кена Маклеода (Ken MacLeod), XML: : DT Xoce Xoa Диaca де Альмейды (Jose Joao Dias de Almeida), и XML: : Simple Гранта Маклина (Grant McLean). Из всех этих модулей, вероятно, проще всего использовать модуль XML::Simple. Он был создан для обработки небольших конфигурационных файлов на XML, что отлично подходит для нашей задачи.
    XML::Simple предоставляет две функции. Вот первая (в данном контексте):
    use XML::Simple;
    use Data:: Dumper;
    # нужен для вывода содержимого структура данных
    Squeuefile = "addqueije. xml":
    open(FILE, Squeuefiie) or die "Unable to open Squeuenle : $! \n":
    read(FILE. Squeuecontents, -s FILE):
    Squeue = XMLin("".Squejecontents.""):
    Содержимое Squeue мы выводим подобным образом:
    prim Da la : : Dumper->Di;
    mp( [Sqi.euc ] ["uueuo" ])
    Теперь это ссылка на данные, найденные в файле очереди, сохраненные в виде хэшей, ключами которого являются элементы .
    Мы используем именно такие ключи потому, что XML: : Simple позволяет распознавать в данных конкретные теги, выделяя их среди других в процессе преобразования. Если мы отключим эту возможность:
    Squeue = XMLin("".$queuecontents."",keyattr=>[]);
    то получим ссылку на хэш, где единственное значение является ссылкой на анонимный массив.
    Такая структура данных не очень полезна. Этот параметр можно определять по собственному желанию:
    $queue = XMLin("".$qucuecontents."",keyattr => ["login"]):
    Замечательно? Теперь мы можем удалить элементы из очереди в памяти, после того как обработаем их всего в одной строке:
    # например, Slogin - "bobf"; delete $queue->{account){$logi");
    Если мы хотим изменить значение, перед тем как записать его на диск (скажем, мы работаем с нашей основной базой данных), то это тоже просто сделать:
    # например, $login="wendyf"; $field="status"
    $queue->{account}{$login}{$field}="created";

    к классическому формату файла паролей

    Дополнительные поля в файлах паролей в BSD 4.4

    При смене BSD (Berkeley Software Distribution) с версии 4.3 на 4.4 к классическому формату файла паролей были добавлены две характерные особенности: дополнительные поля и формат двоичных баз данных, используемых для хранения информации об учетных записях.

    В BSD 4.4 в файле паролей между полями GID и GCOS появились новые поля. Первым было добавлено поле class. Оно позволяет системному администратору разбить все учетные записи системы на отдельные классы (например, для различных классов учетных записей могут существовать различные ограничения ресурсов, таких как время использования процессора). Кроме того, были добавлены поля change и expire, в которых хранятся данные о сроке продолжительности пароля и времени действия учетной записи. Подобные поля встретятся также и в формате следующего файла паролей в Unix.

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

    а не обычного текста для

    Формат двоичных баз данных в BSD 4.4

    Вторая характерная черта BSD - использование баз данных, а не обычного текста для хранения информации о паролях. В BSD файлы паролей хранятся в формате DB - значительно улучшенной версии старых библиотек DBM (Database Management). Это изменение позволяет cиcтеме быстро обращаться к информации о паролях.

    Программа pwd mkdb в качестве аргумента принимает имя текстового файла паролей, создает и переносит в нужное место два файла баз данных, а затем перемещает исходный текстовый файл в /etc/mas-ter.passwd. Две базы данных позволяют обеспечить механизм теневых паролей - они отличаются правами на чтение и содержимым поля, в котором хранится зашифрованный пароль. Более подробно мы поговорим об этом в следующем разделе.

    Perl может напрямую работать с DB-файлами (операции с самим форматом встречаются в главе 9 «Журналы»), но обычно я не рекомендую напрямую редактировать базы данных, пока система используется. Дело тут в блокировке: необходимо убедиться, что другие программы не читают и не записывают данные в файл паролей в тот момент, когда вы собираетесь редактировать его как базу данных. Стандартные программы, подобные chpasswd, выполняют необходимую блокировку самостоятельно. Ловкий прием, заключающийся в использовании переменной EDITOR, который мы употребили при работе с квотами в главе 2 «Файловые системы», можно применить и при вызове chpasswd.

    Группы в NT

    Группы в NT

    До сих пор при обсуждении идентификации пользователя я не упоминал о различиях между хранением этой информации на локальной машине и средствами какой-либо сетевой службы, например NIS. Для той информации, о которой шла речь, не было существенно, используется ли она на одной системе, на всех системах в сети или в рабочей группе. Чтобы обоснованно говорить о группах пользователей в NT/2000 и их связи с Perl, мы должны, к сожалению, отойти от этого соглашения. Мы остановимся на группах в Windows NT 4.0. В Windows 2000 был добавлен еще один уровень сложности, поэтому информацию о группах в Windows 2000 я вынес во врезку «Изменения групп в Windows 2000», которую вы найдете в этой главе.
    В NT информация о пользователях может храниться в одном из двух мест: в SAM на конкретной машине или в SAM на контроллере домена. Именно здесь проявляется различие между локальным пользователем, который может входить в систему и работать только на одной машине, и пользователем домена, который может регистрироваться на любой из машин в домене.
    Группы в NT также бывают двух типов: глобальные и локальные. Разница между ними заключается не совсем в том, чего можно было бы ожидать из названия. Неверно, что первая состоит из пользователей домена, а вторая из локальных пользователей. Так же неверно и то, что один тип пользователей имеет доступ только на одну машину, в то время как другой пользуется всей сетью, как могли ожидать лица, знакомые с Unix. Частично верно и одно определение, и другое, но давайте рассмотрим все подробно.
    Если начать с рассмотрения целей, лежащих за названием, и механизмом их реализации, станет немного яснее. Вот чего мы пытаемся достичь:
  • Учетные записи пользователей всего домена должны обслуживаться централизованно. Администраторы должны иметь возможность определить произвольное подмножество прав и привилегий пользователей, которые можно присвоить всей группе одновременно.
  • При желании, все машины домена должны иметь возможность использовать преимущества такого централизованного управления. Администратор отдельной машины по-прежнему должен иметь возможность создавать пользователей, «живущих» только на этой машине.
  • Администратор каждой машины должен иметь возможность решать, каким пользователям разрешать доступ к этой машине. Администратор должен иметь возможность делать это, используя группы, существующие в домене, а не задавать имена пользователей вручную.
  • Члены этих групп и локальные пользователи должны иметь возможность считаться равными с точки зрения администратора (речь идет о правах и прочем).
  • Глобальные и локальные группы позволяют нам добиться всего вышеперечисленного. В двух предложениях это можно объяснить так: в глобальные группы входят только пользователи домена. В локальные группы входят локальные пользователи, а также в них входят/импортируются пользователи глобальных групп.
    Вот простой пример для объяснения, как все это работает. Скажем, у вас есть домен NT для кафедры в университете, в котором уже созданы пользователи домена - студенты, преподаватели и служащие. Когда появляется новый исследовательский проект под названием Omphalos-kepsis, системные администраторы создают новую глобальную группу Global-Omph People. В данную глобальную группу входят все пользователи домена, занятые в этом проекте. Когда студенты и персонал присоединяются к проекту или выходят из него, они, соответственно, добавляются или удаляются из этой группы.
    Для этого проекта используется отдельный компьютерный класс. На машинах такого класса созданы гостевые учетные записи для нескольких представителей факультета с других кафедр (они не являются пользователями домена). Системный администратор этого класса делает следующее (разумеется, на Perl), чтобы запретить использовать компьютеры тем, кто не занят в этом проекте:
  • Создает на каждой машине локальную группу Local-Autnorized Omphies.
  • Добавляет в эту локальную группу локальные гостевые учетные записи.
  • Добавляет глобальную группу Global-Onph People в указанную локальную группу.
  • Добавляет право (права пользователей мы обсудим в следующем разделе) Log on Locally (регистрироваться локально) локальной группе Local-Authorized Omphies.
  • Удаляет право Log on Locally для всех остальных неавторизованных групп.
  • В результате только авторизованные локальные пользователи и пользователи из авторизованных глобальных групп могут регистрироваться на машинах этого класса. Как только в группу Global-Omph People добавляется новый пользователь, он автоматически получает право регистрироваться на этих машинах без каких-либо изменений учета. Как только вы освоитесь с понятием локальных/глобальных групп, такая схема покажется вам удобной.
    Подобная схема была бы совсем удобной, если бы не усложняла программирование на Perl. Во всех упомянутых модулях существуют отдельные функции для локальных и глобальных групп. Например, в Win32: :NetAdmin имеем:
    GroupCreate()
    GroupDelete()
    GroupGetAttributes()
    GroupSetAttributes()
    GroupAddUsers()
    GroupDeleteUsers( )
    GroupIsMember( )
    GroupGetMembers()
    LocalGroupCreate()
    LocalGroupDelete( )
    LocalGroupGetAttributes( )
    LocalGroupSetAttributes( )
    LocalGroupAddlisers( )
    LocalGroupDeleteUsers( )
    LocalGrouoIsMember( )
    LocalG-uijpGetMeTibers()


    создаются не простыми смертными

    Идентификаторы пользователей в NT/2000

    Идентификаторы пользователей в NT/ 2000 создаются не простыми смертными и их нельзя использовать повторно. В отличие от Unix, где мы просто берем следующий свободный идентификатор пользователя, в Windows NT/2000 операционная система уникальным образом генерирует эквивалентный идентификатор каждый раз при создании пользователя. Уникальный идентификатор пользователя (который в NT/2000 называется относительным идентификатором или RID, Relative ID) объединяется с идентификатором машины и домена, и вместе они образуют длинный идентификационный номер - идентификатор безопасности (SID, Security ID), который используется в качестве1 идентификатора пользователя (UID). Например, RID равный FOG, является частью длинного идентификатора SID, который выглядит так: S-1-5-21-2046255566-1111630368-2110791508-500

    RID - это то число, которое мы получаем в результате вызова функции UserGetMiscAttributes() в последнем примере. Вот так должен выглядеть код для получения идентификатора RID конкретного пользователя:

    use Win32::AdminMisc;

    Win32: :AdrninMisc: : UserGetMiscAttriuutest ' '.

    $user \%attribs): print $attnbs{USEFLUSER_ID}, "\n";

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

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

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

  • Вызвать исполняемый файл, включая:
  • Программу chown либо из пакета Microsoft NT Resource (коммерческий продукт, упомянутый далее), либо из дистрибутива Cygwin с http://www.cygnus.com (бесплатный).
  • Программу setowner, входящую в число утилит NTSEC, продаваемых Pedestal Software на http://www.pedestalsoftware.com. Я предпочитаю ее, т. к. программа отличается гибкостью и при этом требует наименьших затрат.
  • Использовать модуль Win32: : Perms, написанный Дэвидом Ротом (David Roth), который можно найти на http://www.oth.net/perl/perms. Вот простой пример, изменяющий владельца каталога и его содержимое, включая подкаталоги:
  • $acl = new Win3?: Pernis();

    $acl->0wner($NewAccountName):

    Sresult = $aci->SetReci;rse($dir);

    $acl->Close():

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

    Рекомендуемая дополнительная литература Файлы паролей в Unix

    http://www.freebsd.org/cgi/man.cgi.
    Здесь можно получить доступ в оnline-режиме к страницам руководств для *BSD и других вариантов Unix. Это очень удобный способ сравнить форматы файлов и команды системного администрирования (useradd и пр.) для нескольких операционных систем. «Practical Unix & Internet Security»,
    (2nd Edition), Simson Garfinkel, Gene Spafford (O'Reilly, 1999). Отличный источник информации о файлах паролей. Администрирование пользователей в NT
    http://Jenda.Krynicky.cz
    - еще один сайт с полезными модулями в Win32 для администрирования пользователей. http://windows.microsoft.com/windows2000/en/server/help/ -
    справка Windows 2000. (Переходите к разделу Active Directory-»Concepts-) Understanding Active Directory-> Understanding Groups). Это хороший обзор новых механизмов групп в Windows 2000. http://www.actiuestate.com/support /mailing_lists.htm.
    Здесь можно найти списки рассылки Perl-Win32-Admin и Perl-Win32-Users. Оба списка и их архивы представляют собой просто бесценный источник информации для программистов Win32. «Win32 Perl Programming: The Standard Extensions»,
    Dave Roth (Mac-millan Technical Publishing, 1999) в настоящее время лучший источник по программированию модулей для Win32 Perl. «Windows NT User Administration»,
    Ashley J. Meggitt, Timothy D. Ritchey (O'Reilly, 1997). http://www.mspress.com.
    Издатели Microsoft NT Resource Kit. Они также предлагают возможность подписки для получения доступа к самым последним утилитам из RK. http://www.roth.net.
    Домашняя страница для Win32: :AdminMisc, Win32: :Perms и других модулей для Win32, используемых для администрирования пользователей.

    Информация о пользователях в Unix

    Информация о пользователях в Unix

    При обсуждении этой темы мы будем иметь дело лишь с несколькими ключевыми файлами, поскольку в них хранится постоянная информация о пользователе. Говоря «постоянная», я имею в виду атрибуты, которые существуют до тех пор, пока существует пользователь, сохраняясь даже тогда, когда пользователь не зарегистрирован в системе. Иначе я буду называть это учетной записью. Если в системе у вас есть учетная запись, вы можете зарегистрироваться и стать пользователем данной системы.
    Существование пользователя в системе начинается с того момента, когда информация о нем впервые заносится в файл паролей (или же служба каталогов обеспечивает аналогичную информацию). Пользователь «уходит со сцены», когда эта запись удаляется. Рассмотрим подробнее, как хранится эта информация.
    Классический файл паролей в Unix
    Начнем мы с «классического» формата файла паролей, а затем перейдем к более сложным вопросам. Я называю данный формат классическим, потому что на нем основаны все существующие в настоящее время форматы файлов паролей в Unix. Более того, он и сейчас встречается во многих вариантах Unix, включая SunOS, Digital Unix и Linux. Обычно это файл /etc/passwd, содержащий последовательность текстовых ASCII-строк, причем каждая строка соответствует одной учетной записи или является ссылкой на другую службу каталогов. Любая строка файла состоит из нескольких полей, разделенных двоеточиями. Мы внимательно рассмотрим все эти поля, после того как научимся их получать.
    Вот пример строки из файла /etc/passwd:
    dnb:fMP.olmno4jGA6:6700:520:David N. Blank-Edelman:/home/dnb:/bin/zsh
    Существует по крайней мере два способа получать подобную информацию средствами Perl:
    1. К файлу можно обратиться «вручную», рассматривать его как обычный текстовый и соответствующим образом анализировать:
    Spasswd = "/etc/passwd":
    open(PW, Spasswd) or die "Невозможно открыть $passwd:$' '-n":
    while (){
    ($name,$passwd,$iiid.$gid. $дсоз. $dir. $srelL ,) = split (/:/):
    <далее следует ваша программа>
    }
    close(PW);
    2. Другой способ позволяет «предоставить все полномочия системе». В этом случае нам будут доступны некоторые библиотечные вызовы Unix, которые проанализируют файл за нас. Тогда последний пример можно переписать так:
    while( ($пагле, Spasswd, $uid,$gid,$gcos,$dir,$shell) = getpwent( ) ){
    <далее следует ваша программа>
    }
    endpwent();
    Употребление системных вызовов содержит еще одно преимущество: их можно автоматически использовать с любой из служб имен (например, NIS). Вскоре мы рассмотрим и другие системные вызовы (включая более простой способ применения getpwent( )), а пока разберемся с полями, которые получили в наших примерах:1
    Имя
    В этом поле хранится короткое (обычно не длиннее восьми символов), уникальное в пределах системы регистрационное имя пользователя. Функция getpwent( ), которую мы уже видели в предыдущем примере в списочном контексте, возвращает значения данного поля, если вызывается в скалярном контексте:
    $name = getpwent( ),
    Идентификатор пользователя (UID)
    В Unix-системах идентификатор пользователя (UID) зачастую более важен, чем регистрационное имя. Все файлы в системе принадлежат пользователю с каким-либо идентификатором, а не регистрационным именем. Если в файле /etc/passwd поменять регистрационное имя пользователя, обладающего идентификатором 2397, с danielr на drinehart, то мгновенно владельцем всех его файлов станет пользователь drinehart. Для операционной системы идентификатор пользователя - постоянная информация. При выделении ресурсов и выяснении прав ядро и файловые системы следят за идентификаторами, а не регистрационными именами. Регистрационное имя можно считать информацией, внешней для операционной системы; эта информация существует, чтобы упростить жизнь пользователя.
    Вот простой пример, позволяющий установить очередной доступный уникальный идентификатор в файле паролей. Достаточно выяснить максимальный идентификатор и использовать его для создания следующего номера:
    Spasswd = "/etc/passwd";
    open(PW,$passwd) or die "Невозможно открыть $passwd:$!\n";
    while (){
    @fields = splitC/:/);
    Shighestuid = (Shighestuid < $fields[2]) ? $fields[2] : $highestuid:
    }
    close(PW);
    print "Следующий доступный идентификатор: " . ++$highestuid . "\n";
    Ниже перечислены другие полезные функции и переменные, имеющие отношение к именам и идентификаторам пользователей (табл. 3.1).


    Информация о пользователях в Windows

    NT/2000 хранит постоянную информацию о пользователях в базе данных SAM (Security Accounts Manager, диспетчер учетных записей в системе защиты). База данных SAM - это часть реестра NT/2000, находящаяся в %SYSTEMROOT%/system32/config. Файлы, входящие в состав реестра, хранятся в двоичном формате, следовательно, обычные функции для работы с текстом в Perl нельзя применять для чтения или внесения изменений в эту базу данных. Теоретически, если NT/2000 не запущена, можно использовать операторы над двоичными данными (раск() и unpack()) для работы с SAM, но такой способ безумен и мучителен.
    К счастью, существуют более удачные методы доступа к этой информации и работы с ней в Perl.
    Один из способов - вызвать внешнюю программу, которая обесепечит ваше взаимодействие с операционной системой. На каждой машине с NT/2000 есть команда net, она позволяет добавлять, удалять и просматривать данные о пользователях, net довольно странная и ограниченная команда и, вероятно, к ее использованию стоит прибегать в крайнем случае.
    Вот так, например, команда net выполняется на машине с двумя учетными записями:
    C:\>net users
    User accounts for \\HOTDIGGITYDOG
    ------------------------------------------------------
    Administrator Guest
    The command completed successfully.
    При необходимости, вывод этой команды было бы просто разобрать из Perl. Помимо net существуют и другие коммерческие пакеты, в состав которых входят программы, запускаемые из командной строки и выполняющие те же действия.
    Другой подход - использовать модуль Wi п32: : Net Admin (входящий в состав дистрибутива ActiveState Perl) или один из модулей, созданных для расширения функциональности Win32:: NetArMn. В их число входят модули Win32: :AdminMisc Дэвида Рота (David Roth, модуль находится на http://www.roth.net) и Win32: :UserAdmiri (описанный Эшли Мэггитом (Ashley Meggitt) и Тимоти Ритчи (Timothy Ritchey) в книге «Windows NT User Administration» (Windows NT: Администрирование пользователей), модуль можно найти на ftp://ftp.oreilly.com/pub/ examples/windows/winuser/).
    Для выполнения большинства операций с пользователями я предпочитаю модуль Win32: .-AdrcinMisc, поскольку он предлагает множество инструментов системного администрирования, кроме того, Рот активно поддерживает его в нескольких форумах. И хотя доступная в Сети документация по этому модулю очень хороша, лучшая документация -это книга самого автора «Win32 Perl Programming: The Standard Extensions» (Программирование на Perl для Win32: стандартные расширения) (Macmillan Technical Publishing). Такую книгу всегда полезно иметь под рукой, если вы собираетесь писать на Perl программы для Win32. Приведу пример, перечисляющий пользователей на локальной машине, а также некоторые сведения об этих пользователях. Выводимые в примере строки похожи на строки из файла /etc/passwd в Unix:
    use Win32::AdminMisc;
    и получаем список всех локальных пользователей
    Win32::AdminMisc::GetUsers('','',\@users) or
    die "Невозможно получить список пользователей: $!\п";
    П получаем их атрибуты и выводим их foreach $user (@users){
    Win32::AdminMisc::UserGetMiscAttributes('',$user,\%att ribs)
    or warn " Невозможно получить атрибуты: $!\п"; print join(":",$user,
    ' * '
    $attribs{USER_USER_ID},
    $attribs{USER_PRIMARY_GROUP_ID},
    $attribs{USER_COMMENT},
    $attribs{USER_FULL_NAME},
    $attribs{USER_HOME_DIR_DRIVE}.
    $attribs{USER_HOME DIR}, "),"\n";
    }
    Наконец, вы можете использовать модуль Win32: : OLE для доступа к интерфейсам активных служб каталогов (ADSI, Active Directory Service Interfaces). Данная служба встроена в Windows 2000 и ее можно установить на Windows NT 4.0. Эта тема и соответствующие примеры будут подробно рассмотрены в главе 6 «Службы каталогов».
    Другие примеры программ на Perl для работы с пользователями в NT/2000 встретятся позже, а пока вернемся к обсуждению различий между пользователями в Unix и Windows NT/2000.

    Microsoft Windows NT/ Windows 2000 Resource Kits

    Microsoft Windows NT/ Windows 2000 Resource Kits

    «У вас должен быть установлен NT 4.0 Server и/или Workstation Resource Kit» - в этом, обычно, единодушны и серьезные администраторы NT, и средства информации. Microsoft Press опубликовал два больших тома, каждый из которых полон жизненно необходимой информации об одной из версий операционной системы NT/2000. Ценность этих книг заключается не столько в сведениях, сколько в компакт-дисках, распространяемых вместе с книгами. На компакт-дисках есть множество важных утилит для администрирования NT/2000. Утилиты, поставляемые с книгой по NT/2000 Server, содержат и утилиты, входящие в компакт-диск для версии NT Workstation/Windows 2000 Professional. Если вам придется выбирать одну из книг, предпочтите издание, посвященное NT/2000 Server.
    Многие из этих утилит распространяются группой разработчиков NT/2000, написавших собственные программы, поскольку они нигде не смогли найти нужные им инструменты. Например, в состав этих утилит входят программы для добавления пользователей, изменения информации о безопасности файловой системы, отображения установленных драйверов принтеров, работы с профилями, помощи с отладкой служб доменов и обозревателя сети и т. д.
    Инструменты из пакета дополнительных программ поставляются «как есть» (as is), иными словами, они практически не поддерживаются. Такая политика «неподдержки» может показаться грубой, но она преследует важную цель - дать возможность Microsoft предоставить администраторам множество полезных программ и не заставлять их платить непомерно много за поддержку. В программах из этого пакета есть некоторые мелкие ошибки, но, в целом, они работают замечательно. Обновления, исправляющие ошибки в некоторых утилитах, публикуются на веб-сайте Microsoft.
    Массив ©rights теперь содержит набор строк, описывающих все права учетной записи Guest.
    Узнать, чему соответствует API-имя ( Application Program Interface, интерфейс прикладного программирования) того или иного права пользователя, может оказаться непростой задачей. Самый легкий способ выяснить, каким правам какие имена соответствуют, - ознакомиться с документацией SDK (Software Developement Kit, набор инструментальных средств разработки программного обеспечения), которая находится на http://msdn.microsoft.com. Нужную документацию отыскать легко, потому что Хелберг сохранил имена стандартных функций SDK для функций в Perl. Чтобы найти имена доступных прав, достаточно поискать в MSDN (Microsoft's Developer Network) «LsaEnumerateAccountRights», и мы быстро их отыщем.
    Подобная информация полезна и для изменения прав пользователей. Например, если мы хотим разрешить пользователю Guest выключать (останавливать) систему, мы можем применить следующее:
    use Win32::Lanman;
    unless (Win32::Lanman::LsaLookupNames($server ['Guest'],
    \@info)) {
    die " Невозможно найти SID; ".Win32::Lanman::GetLastError()."\n"
    }
    unless (Win32::Lanman::LsaAddAccountRights($server,
    ${$info[0]}{sid}, [&SE^SHUTDOWN_NAME])) {
    die " Невозможно изменить права: ". Win32::Lanman::GetLastError()."\n"
    }
    На этот раз мы нашли право SE_SHUTDOWN_NAME в документации по SDK и применили подпрограмму &SE__SHUTDOWN_NAME (определенную в Win32: : Lanman), возвращающую значение этой константы SDK.
    Win32: : Lanman: : LsaRemoveAccountRights() - это функция. Она используется для лишения прав и принимает аргументы, схожие с теми, которые применяет функция для добавления прав.
    Перед тем как перейти к другим темам, необходимо упомянуть, что в Win32:: Lanman входит также и функция, действующая аналогично неудачному интерфейсу диспетчера пользователей, о котором мы говорили раньше. Вместо того чтобы сопоставлять пользователей с правами, мы можем сопоставлять права с пользователями. Применяя функцию Win32: : Lanman: : LsaEnumerateAccountsWithUserRight(), мы можем получить список идентификаторов (SID), у которых есть определенные поля. Иногда такое знание может сослужить добрую службу.

    Практически все, что мы говорили

    Отличия групп в Windows 2000

    Практически все, что мы говорили о локальных и глобальных группах в NT, также относится и к Windows 2000, но существует несколько характерных особенностей, о которых необходимо упомянуть:

  • В Windows 2000 используются активные каталоги (Active Directory, более подробную информацию о них можно найти в главе 6) для хранения информации о пользователях. Это означает, что информация о глобальных группах теперь хранится в активном каталоге на контроллере домена, а не в его SAM.
  • Локальные группы теперь называются локальными группами домена.
  • Была добавлена третья, пространственная (scope) группа. Помимо глобальных и локальных групп домена в Windows 2000 добавились универсальные группы. Универсальные группы, по существу, разрывают границы домена. В них могут входить учетные записи, глобальные группы и универсальные группы из любого места каталога. В локальные группы домена могут входить как глобальные группы, так и универсальные группы.
  • На момент написания этой книги стандартные модули Perl для администрирования учетных записей еще не учитывали таких изменений. Эти модули можно по-прежнему использовать, поскольку интерфейсы NT4 SAM пока еще действуют, но они не смогут применять новые возможности. Поэтому данная врезка единственное место, .где мы упоминаем об этих различиях в Windows 2000. Подробную информацию вам придется искать в описании интерфейсов служб активных каталогов (Active Directory Service Interfaces, ADSI), о которых речь пойдет в главе 6.

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

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

    применяемые для шифрования паролей, ограничивающих

    Пароли в NT/2000

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

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

    Почему настоящие системные администраторы создают системы учетных записей

    Почему настоящие системные администраторы создают системы учетных записей

    Системные администраторы делятся на две категории: ремесленники и архитекторы. Ремесленники большую часть своего времени проводят в не посредственном контакте с подробностями внутреннего устройства ОС. Они знают множество тайн об аппаратном и программном обеспечении, которое они администрируют. Если что-то идет не так, как надо, они знают, какую использовать команду, файл, или какой «гаечный ключ» нужно применить. Талантливые ремесленники могут поразить вас способностью определить и исправить неполадки, находясь даже в соседней комнате от «проблемной» машины.
    Архитекторы же тратят время, осматривая компьютерные пространства с высоты. Они мыслят более абстрактно, решая, как сформировать более сложные системы из отдельных частей. Архитекторы озабочены вопросами масштабируемости, расширяемости и повторного использования.
    Администраторы обоих типов вносят важный вклад в системное администрирование. Я больше всего уважаю системных администраторов, которые могут быть ремесленниками, но при этом предпочитают действовать как архитекторы. Они решают проблему, а потом определяют, какие изменения в системе можно сделать, чтобы избежать повторения ошибки в дальнейшем. Они думают о том, как даже маленькие усилия с их стороны могут послужить для дальнейшего выигрыша.
    Отлично действующее компьютерное окружение требует, чтобы архитекторы работали с ремесленниками в тесном взаимодействии. Ремесленники больше всего полезны при работе в рамках, созданных архитекторами. В автомобильном мире ремесленники нужны для сборки и ремонта машин. Но ремесленники расчитывают на то, что проектировщики машин разрабатывают трудно ломаемые и быстро ремонтируемые автомобили. Чтобы хорошо выполнять свою работу, им нужна инфраструктура, напоминающая сборочный цех, инструкция по эксплуатации и канал поставок запасных частей. Если архитектор хорошо выполняет свою работу, работа ремесленника становится проще.
    Какое отношение это имеет к предмету нашего обсуждения? Что ж, вероятно, ремесленники будут применять имеющиеся в операционной системе инструменты для работы с пользователями. Они даже могут пойти дальше и написать небольшие сценарии, упрощающие такие задачи, как добавление пользователей, Архитектор, посмотрев на эту проблему, тут же начнет создавать систему ведения учетных записей. Архитектор задумается над такими вопросами:
  • Природа повторяющихся действий при работе с пользователями и способы, позволяющие максимально автоматизировать данный процесс.
  • Тип информации, собираемой системой ведения учетных записей, и условия, при которых правильно созданная система может послужить основой для других действий. Например, как службу каталогов LDAP (Lightweight Directory Access Protocol) и инструменты для автоматического создания веб-страниц можно добавить к такой системе.
  • Защита данных в системе учетных записей (т. е. безопасность).
  • Создание системы, которая масштабируется при увеличении числа пользователей.
  • Создание системы, которую можно использовать и на других машинах.
  • Как другие системные администраторы решают такие проблемы.
  • Упоминание о создании отдельной базы данных заставляет некоторых нервничать. Они думают так: «Теперь мне нужно покупать действительно дорогую коммерческую базу данных, отдельный компьютер, на котором она будет работать, и нанимать администратора баз данных». Если у вас в системе тысячи или десятки тысяч учетных записей, с которыми необходимо работать, - да, вам понадобится все это (хотя можно обойтись и некоммерческими базами данных, такими как Postgres и MySQL). В этом случае переходите к главе 7 «Администрирование баз данных SQL», чтобы подробно узнать о работе с подобными базами данных в Perl.
    Но когда в этой главе я говорю база данных, то употребляю этот термин в самом широком смысле слова. Плоские файлы вполне подойдут в нашем случае. Пользователи Windows даже могут работать с файлами баз данных Access (например database.mdb). В целях переносимости в этом разделе для различных создаваемых компонентов мы будем использовать простые текстовые базы данных. Но чтобы это было более интересным, базы данных будут в формате XML. Если вы никогда раньше не имели дела с XML, пожалуйста, потратьте немного времени и ознакомьтесь с приложением С «Восьмиминутное руководство по XML».
    Почему XML? У XML есть несколько свойств, которые делают его хорошим выбором для подобных файлов и других конфигурационных файлов системного администрирования:
  • XML - это текстовый формат, следовательно, для работы с ним мы можем использовать наши обычные Perl-приемы, чтобы легко г ним работать.
  • XML очень понятен и практически самодокументирован. Разбирая файл, разделенный определенными символами, такой как /etc passwd, не всегда просто определить, какому полю соответствует какая часть строки. В XML этой проблемы нет, поскольку каждое поле можно окружить очевидным тегом.
  • Располагая правильным анализатором, XML может являться также и самопроверяющим. Если применять анализатор, проверяющий синтаксис, то будет очень просто найти ошибки в формате, т. к. этот файл не будет верно разобран в соответствии с определением типа документа (DTD). Модули, которые мы будем применять в этой главе, основаны на анализаторе, не проверяющем синтаксис, но сейчас проводится важная работа по добавлению проверки синтаксиса. Один из шагов в этом направлении - модуль XML: :Checker, являющийся частью libxml-enno Энно Дерксена (Enno Derksen). Анализатор, даже не проверяющий синтаксис, все-таки способен найти много ошибок, если он проверяет формат документа.
  • XML достаточно гибок для описания практически любой текстовой информации. Эта гибкость означает, что вы можете применять одну библиотеку анализатора для всех данных, а не писать новый анализатор для каждого нового формата.
  • Мы будем использовать текстовые файлы в XML-формате для основного файла, в котором хранятся учетные записи, и для очереди добавления/удаления.
    И в этом случае вы увидите, что правило TMTOWTDI по-прежнему действует. Для каждой операции с XML, которая нам понадобится, мы рассмотрим или, по крайней мере, упомянем несколько способов ее выполнения. Обычно, собирая подобную систему, лучше ограничить число реализованных опций, а действуя таким образом, вы сможете понять, какие возможности программирования существуют при работе с XML из Perl.

    Подпрограммы для создания и удаления учетных записей в Unix

    Подпрограммы для создания и удаления учетных записей в Unix

    Начнем с примеров кода для создания учетных записи в Unix. Большая часть этого кода будет элементарной, поскольку мы избрали легкий путь. Наши подпрограммы для создания и удаления учетных записей вызывают команды с необходимыми аргументами, входящие в состав операционной системы, для «добавления пользователей», «удаления пользователей» и «смены пароля».
    Зачем нужна эта очевидная попытка отвертеться? Этот метод приемлем, поскольку известно, что программы, входящие в состав операционной системы, хорошо «уживаются» с другими компонентами. В частности, этот метод:
  • Не забывает о блокировке (т. е. позволяет избежать проблем с поврежденными данными, которые могут возникнуть, если две программы пытаются одновременно записать данные в файл паролей).
  • Справляется с вариациями в файле паролей (включая шифрование пароля), о чем упоминалось раньше.
  • Наверняка справится со схемами авторизации и механизмами распространения паролей, существующими в этой операционной системе. Например, в Digital Unix добавляющая пользователей внешняя программа может напрямую работать и с NIS-картами на основном сервере.
  • Применение внешних программ для создания и удаления учетных записей обладает такими недостатками:
    Различия операционных систем
    В каждую операционную систему входит свой собственный набор программ, расположенных в разных местах и принимающих несколько различные аргументы. Это редкий пример совместимости, однако практически во всех распространенных вариантах Unix (включая Linux, но исключая BSD) используются максимально совместимые программы для удаления и создания пользователей: useradd и user-del. В вариантах BSD применяются adduser и rmuser, две программы со сходным назначением, но совершенно разными аргументами. Подобные различия могут значительно усложнить наш код.
    Соображения безопасности
    Вызываемые программы с переданными им аргументами будут видны всем, кто употребляет команду ps. Если создавать учетные записи только на защищенной машине (например, на основном сервере), риск утечки данных значительно снизится.
    Зависимость от программы.
    Если внешняя программа почему-либо изменится или будет удалена, то нашей системе учетных записей настанет «полный капут».
    Потеря контроля
    Нам приходится считать часть процесса создания учетной записи неделимым. Другими словами, когда запущена внешняя программа, мы не можем вмешаться в этот процесс и добавить какие-либо свои собственные операции. Выявление ошибок и процесс восстановления становятся более сложными.
    Эти программы редко делают все
    Вероятнее всего, что данные программы не выполняют все действия, необходимые для формирования учетной записи на вашей машине. Возможно, вам понадобится добавить некоторых пользователей в некоторые вспомогательные группы, включить их в список рассылки на вашей системе или же добавить пользователей к файлу лицензии коммерческого продукта. Для обработки подобных действий вам придется написать дополнительные программы. Это, конечно, не проблема, наверняка любая система учетных записей, которую вы придумаете, потребует от вас большего, чем просто вызвать пару внешних программ. Более того, это не удивит большинство системных администраторов, потому что их работа меньше всего похожа на беззаботную прогулку по парку.
    В случае с нашей демонстрационной системой учетных записей преимущества перевешивают недостатки, поэтому посмотрим на примеры кодов, в которых используется вызов внешних программ. Чтобы ничего не усложнять, мы покажем пример программы, работающей только на локальной машине с Linux и Solaris, и проигнорируем все трудности, вроде NIS и вариаций BSD. Если вам хочется посмотреть на более сложный пример этого метода в действии, поищите семейство модулей Cf gTie Рэнди Мааса (Randy Maas).
    Вот основная программа, необходимая для создания учетной записи:
    # На самом деле эти переменные надо определить в центральном
    # конфигурационном файле
    сluseraddex = "/usr/sbin/useradd"; ft путь к useradd $passwdex = "/bin/passwd";
    # путь к passwd $homel)nixdirs = "/home"; ft корневой каталог
    # домашних каталогов Sskeldir = "/home/skel"; ft прототип домашнего
    # каталога $defshell = "/bin/zsh"; ft интерпретатор no
    # умолчанию
    sub CreateUnixAccount{
    my ($account,$record) = @_;
    # конструируем командную строку, используя:
    ft -с = поле комментария
    ft -d = домашний каталог
    и -д = группа (считаем равной типу пользователя)
    ft -m = создать домашний каталог
    ft -k = и скопировать файлы из каталога-прототипа
    » -s = интерпретатор по умолчанию
    # (можно также использовать -G group, group, group для
    # добавления пользователя к нескольким группам)
    my @cmd = (Suseraddex,
    "-с", $record->{"fullname"},
    "-d", "$homeUnixdirs/$account",
    "-g", $record->{"type"},
    "-m",
    "-k", $skeldir,
    "-s", Sdefshell,
    laccount);
    print STOERR "Creating account..."; ^
    my $result = Oxff & system @cmd;
    # код возврата 0 в случае успеха и не 0 при неудаче,
    и поэтому необходимо инвертирование
    if (!$result){
    print STDERR "failed.\n";
    return "Suseraddex failed"; } else {
    print STDERR "succeeded.\n"; }
    print STDERR "Changing passwd...";
    unless ($result = &InitUnixPasswd($account,$record->{"password"»){
    print STDERR "succeeded.\n";
    return ""; > else {
    print STDERR "failed.\n";
    return $result; } >
    В результате необходимая запись будет добавлена в файл паролей, будет создан домашний каталог для учетной записи и скопированы некоторые файлы окружения (.profile, .tcshrc, .zshrc, и т.д.) из каталога-прототипа.
    Обратите внимание, что мы используем отдельный вызов для установки пароля. Команда useradd на некоторых операционных системах (например, Solaris) оставляет учетную запись заблокированной до тех пор, пока для этой учетной записи не будет вызвана программа pass-wd. Подобный процесс требует известной ловкости рук, поэтому мы оформим данный шаг как отдельную подпрограмму, чтобы оставить в стороне подробности. Об этой подпрограмме мы еще поговорим, а пока рассмотрим «симметричный» код, удаляющий учетные записи:
    # На самом деле эти переменные надо устанавливать в центральном
    # конфигурационном файле
    $userdelex = "/usr/sbin/userdel";
    # путь к userdel
    sub DeleteUnixAccount{
    my ($account,Srecord) = @_;
    # конструируем командную строку, используя:
    # -г - удалить домашний каталог
    my @cmd = (Suserdelex, "-r", $account);
    print STDERR "Deleting account.,.";
    my $result - Oxffff & system tJcmd;
    tt код возврата 0 соответствует успеху, не 0 - неудаче,
    № поэтому необходимо инвертирование
    if ('$result){
    print STDERR "succeeded.\n";
    return ""; } else {
    print STDERR "failed.\n":
    return "Suserdelex failed";
    }
    }
    Перед тем как перейти к операциям с учетными записями в NT, разберемся с подпрограммой InitUmxPasswdO, о которой упоминалось раньше. Чтобы завершить создание учетной записи (по крайней мере, в Solaris), необходимо изменить ее пароль при помощи стандартной команды passwd. Обращениеpasswd изменит пароль для этой учетной записи.
    Звучит просто, но тут затаилась проблема. Команда passwd запрашивает пароль у пользователя. Она принимает меры предосторожности, чтобы убедиться, что общается с настоящим пользователем, взаимодействуя напрямую с его терминалом. В результате следующий код работать не будет:
    и такой код РАБОТАТЬ НЕ БУДЕТ open(PW,"|passwd Saccount")
    print PW $olcipasswd, "\n"; print PW Snewpasswd,"\n";
    На этот раз мы должны быть искуснее, чем обычно; нам нужно как-то заставить команду passed думать, что она имеет дело с человеком, а не программой на Perl. Этого можно достичь, если использовать модуль Expect.pm, написанный Остином Шутцом (Austin Schutz), - ведь он устанавливает псевдотерминал (pty), внутри которого выполняется другая программа. Expect.pm основан на известной Tel-программе Expect Дона Либеса (Don Libes). Этот модуль входит в семейство модулей, взаимодействующих с программами. В главе 6 мы рассмотрим его близкого «родственника», модуль Net: :Telnet Джея Роджерса (Jay Rogers).
    Эти модули действуют в соответствии со следующей моделью: они ждут вывода программы, посылают ей на ввод данные, ждут ответа, посылают некоторые данные и т. д. Приведенная ниже программа запускает команду passed в псевдотерминале и ждет до тех пор, пока та запросит пароль. Поддержание «разговора» с passwd не должно требовать усилий:
    use Expect;
    sub InitUnixPasswd {
    my (Saccount,Spasswd) = @_;
    ft вернуть объект
    my $pobj - Expect->spawn($passwdex, Saccount);
    die "Unable to spawn $passwdex:$!\n" unless (defined SpoDj):
    it не выводить данные на стандартный вывод (т. е.
    # работать молча)
    $pobj->log_stdout(0);
    # Подождать запроса на ввод пароля и запроса на повторение
    # пароля, ответить. $pobj->expect(10,"New password: ");
    и Linux иногда выводит подсказки раньше, чем он готов к вводу,
    print $pob] "$passwd\r"
    $pobi->expect(10, "Re-enter new password: "); print $pob] "$passwd\r";
    it работает"
    Sresul: = (defined ($pobj-~ expectdO.
    "successfully changeo")) ? "" : "password crarac.
    failed"): в закрываем обьект, ждем 15 секунд, пока процесс завершится
    $pobj-'suft_close();
    return $resuJ t.:
    }
    Модуль Expect.pm очень хорошо подходит для этой подпрограммы, но стоит отметить, что он годится для куда более сложных операций. Подробную информацию можно найти в документации и руководстве по модулю Expect.pm.


    Подпрограммы для создания и удаления учетных записей в Windows NT/2000

    Процесс создания и удаления учетных записей в Windows NT/2000 несколько проще, чем в Unix, поскольку стандартные вызовы API для этой операции существуют в NT. Как и в Unix, мы могли бы вызвать внешнюю программу, чтобы выполнить подобную работу (например, вездесущую команду net с ключом USERS/ADD), но проще использовать API-вызовы из многочисленных модулей, о некоторых из которых мы уже говорили. Функции для создания учетных записей есть, например, в Win32::NetAdmin, Win32: :UserAdmin, Win32API::Net и Win32::Lanman. Пользователям Windows 2000 лучше ознакомиться с материалом по ADSI в главе 6.

    Выбор одного из этих модулей, в основном, дело вкуса. Чтобы разобраться в отличиях между ними, рассмотрим существующие вызовы для создания пользователей. Эти вызовы описаны в документации Network Management SDK на http://msdn.microsoft. com (если вы ничего не можете найти, поищите «NetUserAdd»). NetllserAdd() и другие вызовы принимают в качестве параметра информационный уровень данных. Например, если информационный уровень равен 1, структура данных на С, передаваемая вызову для создания пользователя, выглядит так:

    typedef struct JJSER_INFO_1 {

    LPWSTR usri1_name;

    LPWSTR usri1_oassworc';

    DWORD usril_passwora_age:

    DWORD usril_oriv:

    LPWSTR usril_home_dir;

    LPWSTR usri1_comment;

    DWORD usri1_flags:

    LPWSTR usri1_script_pat!i:

    }

    Если используется информационный уровень, равный 2, структура значительно расширится:

    typedef struct _UbER_INrG;

    LPWSTR usn2_name;
    LPWSTP lisri? password:
    DWORD usri2_password_age:
    DWORD usn2_priv:
    LPWSTR Lisri2_home_dir;
    LPWSTR usri2_conwient :
    DWORD usri2_flags;
    LPWSTR usri2_scnpt_path;
    DWORD usri2_auth_f lags;
    LPWSTR usri2_fiJll_name:
    LPWSTR usri2_usr_comment;
    LPWSTR usri2_parms:
    LPWSTR usri2_workstations:
    DWORD usri2_last_logon;
    DWORD usn2_last_logoff ;
    DWORD usri2_acct_expires;
    DWORD usri2_max_storage;
    DWORD usri2_units_per_week;
    PBYTE usri2_logon_hours;
    DWORD usri2_bad_pw^count;
    DWORD usri2_num_logons;
    LPWSTR usri2_logon_server;
    DWORD usri2_country_code;
    DWORD usri2_code_page;
    He обязательно много знать об этих параметрах или даже вообще о С, чтобы понять, что при изменении уровня увеличивается количество информации, которое можно передать при создании пользователя. Кроме того, каждый последующий уровень является надмножеством предыдущего.

    Какое это имеет отношение к Perl? Каждый упомянутый модуль требует принять два решения:

  • Нужно ли объяснять программистам на Perl, что такое «информационный уровень»?
  • Какой информационный уровень (т. е. сколько параметров) может использовать программист?
  • Модули Win32API: :Net и Win32: :UserAdmin позволяют программисту выбрать информационный уровень. Win32; : NetAdmin и Win32: : Lanrnan этого не делают. Из всех этих модулей Win32; :NetAdmin применяет наименьшее число параметров; в частности, вы не можете определить поле на этапе создания пользователя. Если вы решите применять модуль Win32; :NetAcmin, вам, скорее всего, придется дополнить его вызовами из другого модуля, чтобы установить те параметры, которые он устанавливать не позволяет. Если вы остановитесь на комбинации Win32 : : NetAarn.in и Win32 : : AaminMisc, вам стоит обратиться к многократно упомянутой книге Рота, поскольку это отличный справочник по модулю Win32: : NetAdmin, по которому нет достаточного количества документации.

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

    use Win32: :Lanman; tt для создания учетной записи

    use Win32::Perms; # для установки прав на домашний каталог

    $homeNTdirs = "\\\\homeserver\\home"; # корневой каталог

    # домашних каталогов

    sub CreateNTAccount{

    my ($account,$record) = @_;

    П создаем учетную запись на локальной машине

    # (т. е., первый параметр пустой)

    $result = Win32::Lanman::NetUserAdd("",

    {'name' => Saccount,

    'password' => $record->{password},

    'home_dir' => "$homeNTdirs\\$account",

    'full_name' => $record->{fullname}});

    return Win32::Lanman::6etLastError() unless ($result);

    добавляем в нужную ЛОКАЛЬНУЮ группу

    (предварительно мы Я получаем SID учетной записи)

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

    die "SID lookup error: ".Win32::Lanman::6etLastError()."\n"

    unless (Win32: :Lanman: :LsalookupNames("", [$account],

    \@info)); $result = Win32::Lanman::NetLocalGroupAddMember("",

    $record->{type), ${$info[0]){sid»;

    return Win32::Lanman::GetLastError() unless (Sresult);

    # создаем домашний каталог

    mkdir "$homeNTdirs\\$account",0777 or

    return "Unable to make honedir:$!";

    № устанавливаем ACL и владельца каталога

    $acl = new Win32::Perms("$homeNTdirs\\$account");

    $acl->0wner($account);

    # мы предоставляем пользователю полный контроль за

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

    # (потому и два различных вызова)

    DIRECTORY | СОНТШЕВ_ШЕИТ_АСЕ);

    $acl->Allow($account, FULL, -

    FILE|OBJECT_INHERIT_ACE|IMHERIT_ONLY_ACE);

    $result = $acl->Set(); $acl->Close();

    return($result ? "" : Sresult); }

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

    use Win32: iLanman;

    для удаления учетной записи

    use File::Path;

    для рекурсивного удаления каталогов

    sub DeleteNTAccount{

    my($account,$record) = @_;

    # удаляем пользователя только из ЛОКАЛЬНЫХ групп.

    Если мы № хотим удалить их и из глобальных групп, мы можем убрать

    слово "Local" из двух вызовов Win32::Lanman::NetUser

    (например, NetUserGetGroups)

    die "SID lookup error: ".Win32::Lanman::GetLastError()."\n"

    unless (Win32::Lanman::LsaLookupNames("",

    [Saccount], \@info));

    Win32::Lanman::NetUserGetLocalGroups($server, Saccount, ",

    \@groups); foreach $group (@groups){

    print "Removing user from local group ".

    $group->{name}."...";

    print(Win32::Lanman::NetLocalGroupDelMember("",

    $group->{name}, ${$info[0]}{sid})?

    "succeeded\n" : "FAILED\n"); }

    tt удалить эту учетную запись с локальной машины

    (т. е., перый параметр пустой) Sresult = Win32::Lanman::NetUserDel("", Saccount);

    return Win32::Lanman::GetLastError() if ($result);

    удалить домашний каталог и его содержимое

    Sresult = rmtree("$homeNTdirs\\$account",0,1);

    rmtree возвращает число удаленных файлов, так что если мы

    удалили более нуля элементов, то скорее всего все прошло 8 успешно return Sresult;

    }

    Заметьте, для удаления домашнего каталога здесь используется переносимый модуль File: :Path. Если бы мы хотели сделать что-то специфичное для Win32, например, переместить домашний каталог в корзину, то могли бы сделать это при помощи модуля Win32: :File Op Йенды Крыники (Jenda Krynicky), который можно найти на http://jen da.krynicky.cz/. В таком случае мы применили бы Wi n32: ; F: 1еОр и изменили бы строку, включающую rmtrc-e(), на:

    # удалим каталог в корзину, потенциально подтверждая

    # действие пользователем, если для этой учетной записи

    # необходимо подтверждать такие операции

    $result = Recycle("$homeNTdirs\\$account");

    В данном модуле есть функция Delete(), которая выполняет то же, что и rmtree() менее переносимым (правда, более быстрым) способом.

    В Unix действия, предпринимаемые пользователем,

    Права пользователей в NT/2000

    Последнее различие между информацией о пользователях в Unix и NT/2000, о котором мы поговорим, - это понятие «пользовательских прав». В Unix действия, предпринимаемые пользователем, ограничиваются как правами доступа к файлам, так и различиями между суперпользователем и остальными пользователями. В NT/2000 права реализованы гораздо сложнее. Пользователи (и группы) могут быть наделены особой силой, которая становится частью информации о пользователях. Например, если предоставить обычному пользователю право Change the System Time (Изменение системного времени), то он сможет изменять настройки системных часов.

    Некоторые считают, что такая концепция прав пользователей сбивает с толку, поскольку они предпринимали попытки прибегнуть к помощи отвратительного диалогового окна User Rights Policy (Политика прав пользователей) из NT 4.0 в приложениях User Manager (Диспетчер пользователей) или User Manager for Domains (Диспетчер пользователей для доменов). В этом диалоговом окне информация представлена виде, прямо противоположном тому, в котором большинство пользователей ожидают ее там увидеть. Она содержит перечень возможных прав пользователей и предлагает добавить группы или пользователей к списку тех, у кого такие права уже есть. Вот как выглядит это диалоговое окно (Рисунок 3.1) пользовательских прав в действии.

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

    Один из возможных подходов - вызвать программу ntrights.exe из Microsoft NT Resource Kit. Если вы об этом никогда не слышали, обязательно прочитайте следующую врезку.

    Работать с ntrights.exe очень легко; достаточно вызывать эту программу из Perl, как любую другую (т. е., применяя обратные кавычки или функцию system()). В этом случае мы обратимся к ntrights.exe при помощи такой командной строки:

    чтобы предоставить право пользователю или группе (на машине rnachi-пепате, имя которой указывать не обязательно). Чтобы отнять право, необходимо применить такой синтаксис:

    С:\>ntrights.exe -г +u [-m \\vachintnaive]

    Пользователи Unix знакомы с употреблением символов + и - (как в chmod), в данном случае для ключа -г, чтобы предоставить или лишить привилегий. Список допустимых имен (например, SetSys::emti-mePrivilege для разрешения устанавливать системное время) можно найти в документации Microsoft NT Resource Kit no команде ntrights

    Второй подход, с использованием только Perl, связан с применением модуля Win32: : Lanman, написанного Йенсом Хелбергом (Jens Helberg), который можно найти либо на ftp://ftp.roth.net/pub/ntperl/Others/ Lanman/, либо на http://jenda.krynicky.cz. Начнем с того, что рассмотрим процесс получения прав для учетной записи. Этот процесс состоит из нескольких шагов, поэтому рассмотрим его подробно, шаг за шагом.

    Сначала необходимо загрузить модуль:

    use Win32::Lanman;

    Затем следует получить идентификатор (SID) для учетной записи, с которой надо работать. В следующем примере мы получим SID для учетной записи Guest:

    unless(Win32: : Lanman: : LsaLookupNames($server, [ 'Guest' ]. \@info)

    { die "Невозможно найти SID':

    ".Win32::Lanman::Get LastError()."\n";

    }

    @info - это массив ссылок на анонимные хэши, каждый элемент которого соответствует отдельной учетной записи (в нашем случае это один-единственный элемент для учетной записи Guest). В каждом хэ-ше есть такие ключи: domain, do;nainsid, relativeid, sid и use. На следующем шаге нас будет интересовать только ключ sid. Теперь мы можем узнать о правах этой учетной записи:

    unless (Win32: : Lanman :: LsaEnurierateAccountRights($server.

    ${$info[0]}{sid}, \@rights)){ die "Невозможно узнать права:

    "32: :Lanmai .GetL.asrError() "\r"\

    Прочее

    Прочее

    http://www.mcs.anl.gov/~evard.
    Домашняя страница Реми Эварда (Re-my Evard), Использование нескольких баз данных для автоматического генерирования конфигурационных файлов - это лучший прием, который показан в нескольких местах моей книги; спасибо Эварду за идею этого метода. И хотя сейчас подобный прием применяется на многих сайтах, я впервые столкнулся с ним при знакомстве со средой Tenwen, которую он создал (как описано в статье, ссылка на которую есть с домашней страницы Эварда). Чтобы ознакомиться с работой этого метода, загляните в раздел «Implemented the Hosts Database». http://www.rpi.edu/~finkej/.
    Содержит несколько статей Иона Финки (Jon Finke) по использованию реляционных баз данных в системном администрировании.

    Сценарии

    Сценарии

    Теперь, когда мы разобрались с базой данных, самое время написать сценарии для выполнения периодических или каждодневных действий, необходимых при системном администрировании. Эти сценарии построены на низкоуровневой библиотеке компонентов (Account.pm), которую мы создали, объединив в один файл все только что написанные подпрограммы. Такая подпрограмма позволяет убедиться, что все необходимые модули загружены:
    sub InitAccountf
    use XML: :Writer;
    Srecord = { fields => [login, fullname,id,type,password]};
    $addqueue = "addqueue"; tt имя файла очереди добавления
    Sdelqueue = "delqueue"; ft имя файла очереди удаления
    $maindata = "accountdb"; tt имя основной базы данных
    ft учетных записей
    if ($"0 eq "MSWin32"){
    require Win32::Lanman;
    require Win32::Perms;
    require File::Path;
    ft местоположение файлов учетных записей
    Saccountdir = "\\\\server\\accountsystem\\";
    ft списки рассылки
    $maillists = "$accountdir\\maillists\\";
    ft корневой каталог домашних каталогов
    $homeNTdirs = "\\\\nomeserver\\home";
    ft имя подпрограммы, добавляющей учетные записи
    Saccountadd = "CreateNTAccount";
    ft имя подпрограммы, удаляющей учетные записи
    Saccountdel = "DeleteNTAccount": }
    else {
    require Expect;
    и местоположение файлов учетных записей
    $accountdir = "/usr/accountsystem/";
    в списки рассылки
    Smaillists = "Saccountdir/maillists/";
    tt местоположение команды useradd
    Suseraddex = ",/usr/sbin/useradd";
    tt местоположение команды userdel
    Suserdelex = "/usr/sbin/userdel";
    tt местоположение команды passwd
    Spasswdex = "/bin/passwd";
    tt корневой каталог домашних каталогов
    ShomeUnixdirs = "/home";
    tt прототип домашнего каталога
    $skeldir = "/home/skel";
    ft командный интерпретатор по умолчанию
    Sdefshell = "/bin/zsh";
    ft имя подпрограммы, добавляющей учетные записи
    Saccountadd = "CreateUnixAccount";
    tt имя подпрограммы, удаляющей учетные записи
    Saccountdel = "DeleteUnixAccount";
    }
    }
    Рассмотрим сценарий, обрабатывающий очередь добавления:
    use Account;
    use XML;:Simple;
    SlnitAccount;
    считываем низкоуровневые подпрограммы
    &ReadAddQueue;
    tt считываем и анализируем очередь добавления
    &ProcessAddQueue;
    tt пытаемся создать все учетные записи
    &DisposeAddQueue;
    ft записываем учетную запись либо в основную
    tt базу данных, либо обратно в очередь, если
    tt возникли какие-то проблемы
    tt считываем очередь добавления в структуру данных $queue
    sub ReadAddQueue{
    open(ADD,Saccountdir.Saddqueue) or
    die "Unable to open ".Saccountdir.$addqueue.":$!\n";
    read(ADD, Squeuecontents, -s ADD);
    close(ADD);
    Squeue = XMLin("".Squeuecontents."",
    keyattr => ["login"]);
    ft обходим в цикле структуру данных, пытаясь создать учетную
    запись для каждого запроса (т. е. для каждого ключа)
    sub ProcessAddQueue{
    foreach my Slogin (keys %{$queue->{account}})
    {
    sub InitAccountf
    use XML: :Writer;
    Srecord = { fields => [login, fullname,id,type,password]};
    $addqueue = "addqueue"; tt имя файла очереди добавления
    Sdelqueue = "delqueue"; ft имя файла очереди удаления
    $maindata = "accountdb"; tt имя основной базы данных
    ft учетных записей
    if ($"0 eq "MSWin32"){
    require Win32::Lanman;
    require Win32::Perms;
    require File::Path;
    ft местоположение файлов учетных записей
    Saccountdir = "\\\\server\\accountsystem\\";
    ft списки рассылки
    $maillists = "$accountdir\\maillists\\";
    ft корневой каталог домашних каталогов
    $homeNTdirs = "\\\\nomeserver\\home";
    ft имя подпрограммы, добавляющей учетные записи
    Saccountadd = "CreateNTAccount";
    ft имя подпрограммы, удаляющей учетные записи
    Saccountdel = "DeleteNTAccount": }
    else {
    require Expect;
    и местоположение файлов учетных записей
    $accountdir = "/usr/accountsystem/";
    в списки рассылки
    Smaillists = "Saccountdir/maillists/";
    tt местоположение команды useradd
    Suseraddex = ",/usr/sbin/useradd";
    tt местоположение команды userdel
    Suserdelex = "/usr/sbin/userdel";
    tt местоположение команды passwd
    Spasswdex = "/bin/passwd";
    tt корневой каталог домашних каталогов
    ShomeUnixdirs = "/home";
    tt прототип домашнего каталога
    $skeldir = "/home/skel";
    ft командный интерпретатор по умолчанию
    Sdefshell = "/bin/zsh";
    ft имя подпрограммы, добавляющей учетные записи
    Saccountadd = "CreateUnixAccount";
    tt имя подпрограммы, удаляющей учетные записи
    Saccountdel = "DeleteUnixAccount";
    }
    }
    Рассмотрим сценарий, обрабатывающий очередь добавления:
    use Account;
    use XML;:Simple;
    SlnitAccount;
    считываем низкоуровневые подпрограммы
    &ReadAddQueue;
    считываем и анализируем очередь добавления
    &ProcessAddQueue;
    пытаемся создать все учетные записи
    &DisposeAddQueue;
    записываем учетную запись либо в основную
    tt базу данных, либо обратно в очередь, если
    tt возникли какие-то проблемы
    tt считываем очередь добавления в структуру данных $queue
    sub ReadAddQueue{
    open(ADD,Saccountdir.Saddqueue) or
    die "Unable to open ".Saccountdir.$addqueue.":$!\n";
    read(ADD, Squeuecontents, -s ADD);
    close(ADD);
    Squeue = XMLin("".Squeuecontents."",
    keyattr => ["login"]);
    ft обходим в цикле структуру данных, пытаясь создать учетную
    запись для каждого запроса (т. е. для каждого ключа)
    sub ProcessAddQueue{
    foreach my Slogin (keys %{$queue->{account}}){
    Sresult = &$accountadd($login,
    $queue->{account}->{$login});
    if (!$result){
    $queue->{account}->{$login}{status} = "created";
    }
    else {
    $queue->{account}->{$login}{status} =
    "error:$result";
    }
    }
    }
    ft теперь снова обходим структуру данных. Каждую учетную запись
    # со статусом "created," добавляем в основную базу данных. Все
    и остальные записываем обратно в файл очереди, перезаписывая
    tt все его содержимое.
    sub DisposeAddQueue{
    foreach my Slogin (keys %{$queue->{account}}){
    if ($queue->{account}->{$login}{status} eq "created")!
    $queue->{account}->{$login}{login} = Slogin;
    $queue->{account}->{$login}{creation_date} = time;
    &AppendAccountXML($accountdir.$maindata,
    $queue->{account}->{$login});
    delete $queue->{account}->{$login};
    next;
    }
    }
    # To, что осталось сейчас в Squeue, - это учетные записи,
    # которые невозможно создать
    # перезаписываем файл очереди
    open(ADD,">".$accountdir.$addqueue) or
    die "Unable to open ".$accountdir.$addqueue.":$!\n";
    П если есть учетные записи, которые не были созданы,
    # записываем их
    if (scalar keys %{$queue->{account}}){
    print ADD XMLout(&TransformForWrite($queue),
    rootname => undef);
    }
    close(ADD);
    }
    Сценарий, обрабатывающий очередь удаления, очень похож:
    use Account;
    use XML::Simple;
    SlnitAccount;
    # считываем низкоуровневые подпрограммы
    &ReadDelOueue;
    # считываем и анализируем очередь удаления
    &ProcessDelQueue;
    пытаемся удалить все учетные записи
    &DisposeDelQueue;
    удаляем учетную запись либо из основной
    базы данных, либо записываем ее обратно в
    в очередь, если возникли какие-то проблемы
    и считываем очередь удаления в структуру данных $queue
    sub ReadDelQueue{
    open(DEL,Saccountdir.Sdelqueue) or
    die "Unable to open ${accountdir}${delqueue}:$!\n";
    read(OEL, Squeuecontents, -s DEL);
    close(DEL);
    Squeue = XMLin("".$queuecontents."",
    keyattr => ["login"]);
    }
    # обходим в цикле структуру данных, пытаясь удалить учетную
    # запись при каждом запросе (т, е. для каждого ключа)
    sub ProcessDelQueue{
    foreach my Slogin (keys %{$queue->{account}}){
    Sresult = &$accountdel($login,
    $queue->{account}->{$login});
    if (!$result){
    Squeue->{account}->{$login}{status} = "deleted";
    }
    else {
    $queue->{account}->{$login}{status} =
    "error:$result";
    }
    >
    }
    # считываем основную базу данных и затем вновь обходим в цикле
    структуру Squeue. Для каждой учетной записи со статусом
    "deleted," изменяем информацию в основной базе данных. Затем
    записываем в базу данных. Все, что нельзя удалить, помещаем
    # обратно в файл очереди удаления. Файл перезаписывается,
    sub DisposeDelQueue{
    &ReadMainDatabase;
    foreach my Slogin (keys %{$queue->{account}}){
    if ($queue->{account}->{Slogin}{status} eq "deleted"){
    unless (exists $maindb->{account}->{$login}){
    warn "Could not find Slogin in $maindata\n";
    next;
    }
    $maindb->{account}->{$login}{status} = "deleted";
    $maindb->{account}->{$login}{deletion_date} = time;
    delete $queue->{account}->{$login};
    next;
    }
    &WriteMainDatabase;
    # все, что сейчас осталось в Sqjjeue, - это учетные записи,
    # которые нельзя удалить
    open(DEL,">".$accountdir.$delqueue) or die "Unable to open ".
    $accountdir.$delqueue.":$!\n";
    if (scalar keys %{$queue->{account}}){
    print DEL XMLout(&TransformForWrite($queue), rootname => undef);
    }
    close(DEL); }
    sub ReadMainDatabase{
    open(MAIN,$accountdir.$maindata) or
    die "Unable to open ".$accountdir.$maindata.":$!\n";
    read (MAIN, $dbcontents, -s MAIN);
    close(MAIN); $maindb = XMLin("".Sdbcontents. "",
    keyattr => ["login"]); }
    sub WriteMainDatabase{
    # замечание: было бы «гораздо безопаснее* записывать данные
    # сначала во временный файл и только если они были записаны
    # успешно, записывать их окончательно open(MAIN,">".
    $accountdir.Smaindata) or
    die "Unable to open ".$accountdir.$maindata.":$!\n";
    print MAIN XMLout(&TransformForWrite($maindb),
    rootname => undef); close(MAIN); }
    Можно написать еще множество сценариев. Например, мы могли бы применять сценарии, осуществляющие экспорт данных и проверку согласованности. В частности, совпадает ли домашний каталог пользователя с типом учетной записи из основной базы данных? Входит ли пользователь в нужную группу? Нам не хватит места, чтобы рассмотреть весь спектр таких программ, поэтому завершим этот раздел небольшим примером экспортирования данных. Речь уже шла о том, что хотелось бы завести отдельные списки рассылки для пользователей различного типа. В следующем примере из основной базы данных считываются данные и создается набор файлов, содержащих имена пользователей (по одному файлу для каждого типа пользователей):
    use Account; И только чтобы найти файлы use XML::Simple:
    &InitAccount;
    SReadMainDatabase:
    &WriteFiles:
    open(MAIN,Saccountdir.Smaindata) or
    die "Unauie to open ".Saccountdir.$maindata "-$'\r";
    read (MAIN, Sdbcontents. -s MAIN);
    ciose(MAIN): Smaindb = XMLin("
    ".Sdbcontents." /maincm>",
    keyattr -> [""]):
    }
    обходим в цикле списки, собираем списки учетных записей
    определенного типа и сохраняем им в хэше списков. Затем
    записываем содержимое каждого ключа в отдельный файл.
    sub WriteFiles {
    foreach my Saccount (@{$niaindb->{account}}){
    next if $account->{status}
    eq "deleted"; push(@{$types{$account->{type}}},
    $account->{login}); }
    foreach $type (keys %types){
    open(OUT,">".Smalllists.Stype) or die "Unable to write to
    ".Saccountdir.$maillists.$type.": $!\n";
    print OUT ]0in("\n",sort @{$types{$type}})."\n"; close(OUT);
    }
    }
    Если посмотреть в каталог списков рассылки, то можно увидеть:
    > dir
    faculty staff
    Каждый из этих файлов содержит соответствующий список учетных записей пользователей.

    Система учетных записей Заключение

    Система учетных записей. Заключение

    Рассмотрев все четыре компонента системы учетных записей, подведем итоги и поговорим о том, что было пропущено (в узком, а не в широком смысле):
    Проверка ошибок
    В нашей демонстрационной программе выполняется проверка лишь небольшого числа ошибок. Любая уважающая себя система учетных записей увеличивается на 40-50% в объеме из-за проверки ошибок на каждом шаге своего выполнения.
    Масштабируемость
    Наша программа, скорее всего, сможет работать на мелких и средних системах. Но каждый раз, когда встречается фраза «прочитать весь файл в память», это должно звучать для вас предупреждением.
    Чтобы повысить масштабируемость, нужно по крайней мере изменить способ получения и хранилище данных. Модуль XML : : Twig Мишеля Родригеса (Michel Rodriguez) может разрешить эту проблему, т. к. он работает с большими XML-документами, не считывая их при этом целиком в память.
    Безопасность
    Это относится к самому первому элементу списка выводов - проверке ошибок. Помимо таких громадных дыр, в смысле безопасности, как хранение паролей открытым текстом, мы также не выполняем никаких других проверок. Нет даже попыток убедиться, что используемым источникам данных, например, файлам очередей, можно доверять. Стоит добавить еще 20-30% кода, чтобы позаботиться о таких моментах.
    Многопользовательская среда
    В коде не предусмотрена возможность одновременной работы нескольких пользователей или даже нескольких сценариев. И это, вероятно, самый большой недочет созданной программы. Если одновременно запустить один сценарий, добавляющий учетные записи, и другой, дописывающий учетные записи в очередь, то вероятность повредить или потерять данные будет очень велика. Это настолько важная тема, что ее стоит обсудить перед тем, как завершить этот раздел.
    Один из способов разобраться в многопользовательской среде с работой - добавить блокировку файлов. Блокировка позволяет нескольким сценариям действовать одновременно. Если сценарий собирается читать или писать в файл, он может попытаться сначала файл заблокировать. Если это возможно, значит, с файлом можно работать. Если его заблокировать нельзя (потому что другой сценарий использует этот файл), то сценарий знает, что запрещено выполнять операции, которые могут повредить данные. С блокировкой и многопользовательской работой связаны гораздо более серьезные сложности; обратитесь к любой информации по операционным или распределенным системам. Серьезные проблемы могут возникнуть при работе с файлами, расположенными на сетевых файловых системах, где может и не быть хорошего механизма блокировки. Вот несколько советов, которые могут вам пригодиться, если вы коснетесь этой темы при использовании Perl.
  • Существуют мудрые способы уходить от проблем. Мой любимый способ - использовать программу lockfile, входящую в состав популярной программы фильтрации почты procmail, которую можно найти на http://www.procmail.org. Процедура установки procmail принимает усиленные меры, чтобы определить безопасные стратегии блокировки для используемой файловой системы, lockfile делает именно то, что можно ожидать, глядя на ее название, скрывая при этом основные сложности.
  • Если вы не хотите применять внешнюю программу, существует масса модулей, выполняющих блокировку. Например, File :Flock Дэвида Мюир Шарнофа (David Muir Sharnoff), Fi ±e: : LuckD i; из книги «Perl Cookbook» («Perl: Библиотека программиста») Тома Крис тиансена (Tom Christiansen) и Натана Торкингтона (Nathan Tor-kington) (O'Reilly) и его версия для Win95/98 Вильяма Херейры (William Herrera) под названием File: : FIockDiг, File: : Lock Кеннета Альбановски (Kenneth Albanowski), File : Lockf Поля Хенсона (Paul Henson) и Lockfile: :Simple Рафаеля Манфреди (Raphael Manfredi). В основном, они отличаются интерфейсом, хотя File: : FIockDi г и Lockf ile:: Simple пытаются выполнять блокировку, не используя функцию f lock() из Perl. Они могут быть полезными на таких платформах, как MacOS, где эта функция не поддерживается. Осмотритесь и выбирайте тот модуль, который больше всего вам подходит.
  • Блокировку проще всего выполнить правильно, если не забыть заблокировать файл, перед тем как изменять данные (или считывать данные, которые могли измениться), и снимать блокировку только после того, как убедитесь, что данные были записаны (например, после того как файл будет закрыт). Подробную информацию по этой теме можно найти в упомянутой уже «книге рецептов», в списке часто задаваемых вопросов Perl FAQ, в документации по функции f lock() из модуля DB_File и из документации по Perl.
  • Мы завершаем наш разговор об администрировании пользователей и о том, как можно перевести эти операции на другой уровень, применив подход архитектора. В этой главе мы уделили особое внимание началу и концу жизненного цикла учетной записи. В следующей главе мы поговорим о том, что делают пользователи между этими двумя моментами.

    Создание системы учетных записей

    Центральная часть любой системы учетных записей - это база данных. Некоторые администраторы используют только файл /etc/ pass-wd или базу данных SAM для хранения записей о пользователях системы, но такое решение часто оказывается недальновидным. Помимо информации, о которой мы уже говорили, в отдельной базе данных можно хранить метаданные о каждой учетной записи: например, дату создания учетной записи, срок ее действия, номера телефонов пользователей и прочие сведения. Когда появляется такая база данных, ее можно применять не только для работы с учетными записями. Она годится для создания списков рассылки, служб LDAP и индексации веб-страниц пользователей.

    Создание XMLданных при помощи XML Simple

    Создание XML-данных при помощи XML::Simple

    Упоминание «записать его на диск» возвращает нас обратно к методу создания XML-данных, который мы обещали показать. Вторая функция из XML: : Simple принимает ссылку на структуру данных и генерирует XML-данные: rootname определяет имя корневого элемента, мы могли бы использовать XMLdecl. чтобы добавить объявление XML print XMLout($queue, rootname =>"queue"),
    В результате получаем (отступы сделаны для удобства чтения):
    password="password" status="to_be_created"
    fullname="Bob Fate" id="24-9C57" />
    password="password"
    status="to_be_created"
    fullname="Wendy Fate" id="50-9057" />

    Мы получили отличный XML-код, но его формат несколько отличается от формата наших файлов с данными. Данные о каждой учетной записи представлены в виде атрибутов одного элемента , a не в виде вложенных элементов. В XML: :Simple есть несколько правил, руководствуясь которыми, он преобразовывает структуры данных. Два из них можно сформулировать так (а остальные можно найти в документации): «отдельные значения преобразуются в XML-атрибуты», а «ссылки на анонимные массивы преобразуются во вложенные XML-элементы».
    Чтобы получить «верный» XML-документ («верный» означает «в том же стиле и того же формата, что и наши файлы данных»).
    Кошмар, не правда ли? Но у нас есть варианты для выбора. Мы можем:
  • Изменить формат наших файлов данных. Это похоже на крайнюю меру.
  • Изменить способ, которым XML: :Simple анализирует наш файл. Чтобы получить такую структуру данных (Рисунок 3.6), мы могли бы использовать функцию XMLin() несколько иначе:

    Squeue = XMLin("",Squeuecontents."",
    forcearray=>1, keyattr => [""]):

    Но если мы перекроим способ чтения данных, чтобы упростить запись, то потеряем семантику кэшей, упрощающих поиск и обработку данных.
  • Выполнить некую обработку данных после чтения, но до записи. Мы могли бы прочитать данные в нужную нам структуру (так же, как делали это раньше), применить эти данные в нужном месте, а затем преобразовать структуру данных в один из вариантов, получаемых модулем XML: : Simple, перед тем как записать ее.
  • Вариант номер 3 кажется более разумным, так что последуем ему. Вот подпрограмма, которая принимает одну структуру данных (Рисунок 3.5), и преобразует ее в другую структуру данных (Рисунок 3.6). Объяснение примера будет приведено позже:
    sub TransforuiForWrite{ my $queueref = shift;
    my Stoplevel = scalar each %$queueref;
    foreach my $user (keys %{$queueref->{$toplevel}}){
    my %innerhash =map {$_, [$queueref->
    {$toplevel}{$user}{$J] }
    keys %<$queueref->{Stoplevel}{$user}};
    $innerhash{'login'} = [$user];
    push @outputarray, \%innerhash; }
    Soutputref = { Stoplevei => \@outnui.array};
    return $outputref:
    }
    Теперь подробно рассмотрим подпрограмму TrarsformForWate().
    Если вы сравните две структуры (Рисунок 3.5, Рисунок 3.6), то заметите в них кое-что общее: это внешний хэш, ключом которого в обоих случаях является account. В следующей строке видно, как получить имя этого ключа, запрашивая первый ключ из хэша, на который указывает $que-ueref:
    my Stoplevel = scalar each :6$i;eref:
    Интересно взглянуть на закулисную сторону создания этой структуры данных:
    my %innernabh =
    mар {$_. [$queuer-ef-><$toplevel){$use'-}{$_ M 1
    keys %{$queueref->{$toplevelf{$user}};
    В этом отрывке кода мы используем функцию "iap(), чтобы обойти все ключи, найденные во внутреннем хэше для каждой записи (т. е. login, type, password и status). Ключи возвращаются в такой строке:
    keys %{$queueref->{$toplevel}{$user}};
    Просматривая ключи, можно с помощью тар вернуть два значения для каждого из них: сам ключ и ссылку на анонимный массив, содержащий его значение:
    шар {$_, [$queueref->{$topleve]}{$user}{$_n }
    Список, возвращаемый тар(), выглядит так:
    (login,[bobf], type,[staff], password,[password]...)
    Он имеет формат ключ-значение, где значения хранятся как элементы анонимного массива. Этот список можно присвоить хэшу %innerhash, чтобы заполнить внутреннюю хэш-таблицу для получаемой структуры данных (my %innerhash =). Кроме того, к хэшу следует добавить ключ login, соответствующий рассматриваемому пользователю:
    $innerhash{'login'} = [$user];
    Структура данных, которую мы пытаемся создать, - это список подобных хэшей, поэтому после того как будет создан и определен внутренний хэш, необходимо добавить ссылку на него в конец списка, т. к. он и представляет получаемую структуру данных:
    push @outputarray. \%innerhash:
    Такую процедуру следует повторить для каждого ключа login из первоначальной структуры данных (один на каждую запись об учетной записи). После того как это будет сделано, у нас появится список ссылок на хэши в той форме, которая нам нужна. Мы создаем анонимный хэш с ключом, совпадающим с внешним ключом из первоначальной структуры данных, и значением, равным нашему списку хэшей. Ссылку на этот анонимный хэш можно возвратить обратно вызывающей программе. Вот и все:
    Soutputref = { Stoplevel => \SO'..tputarray}: return Soutputre?:
    Теперь, располагая &TransforrnForWrite(), мы можем написать программу для чтения, записи наших данных и работы с ними:
    Squeue = XMLin("".$queuecontents."",keyattr => ["login"]);
    print OUTPLITFILE XMLojt(Ti ansfonnFor Write($queuu), г ootname => "queue");
    Записанные и прочитанные данные будут иметь один и тот же формат.
    Перед тем как закрыть тему чтения и записи данных, следует избавиться от несоответствий:
    1. Внимательные читатели, наверное, заметили, что одновременное использование XML: : Write г и XML: : Simple в одной и той же программе для записи данных в очередь может оказаться непростым делом. Если записывать данные при помощи XML: : Simple, то они будут вложены в корневой элемент по умолчанию. Если же применять XML: : Write г (или просто операторы print) для записи данных, вложения не произойдет, т. е. нам придется опять прибегнуть к хаку "". Squeuecontents. "". Возникает неудачный уровень синхронизации чтения-записи между программами, анализирующими и записывающими данные в XML-формате.
    Чтобы избежать этой проблемы, надо будет использовать продвинутую возможность модуля XML: .'Simple: если XMLoutO передать параметр rootname с пустым значением или значением undef, то возвращаются данные в XML-формате без корневого элемента. В большинстве случаев так поступать не следует, потому что в результате образуется неправильный (синтаксически) документ, который невозможно проанализировать. Наша программа позволяет этим методом воспользоваться, но такую возможность не стоит применять необдуманно.
    2. И хотя в примере этого нет, мы должны быть готовы к обработке ошибок анализа. Если файл содержит синтаксически неверные данные, то анализатор не справится и прекратит работу (согласно спецификации XML), остановив при этом и всю программу в случае, если вы не примете мер предосторожности. Самый распространенный способ справиться с этим из Perl - заключить оператор анализа в eval() и затем проверить содержимое переменной $@ после завершения работы анализатора. Например:
    eval {$p->parse("".Squeuecontents."")};
    if ($@) { сделать что-то для обработки ошибки перед выходом.. };
    Другим решением было бы применение известного модуля из разряда XML: : Checker, т. к. он обрабатывает ошибки разбора аккуратнее.
    Низкоуровневая библиотека компонентов
    Теперь, когда мы умеем отследить данные на всех этапах, включая то, как они получаются, записываются, читаются и хранятся, можно перейти к рассмотрению их использования глубоко в недрах нашей системы учетных записей. Мы собираемся исследовать код, который действительно создает и удаляет пользователей. Ключевой момент этого раздела заключается в создании библиотеки повторно используемых компонентов. Чем лучше вам удастся разбить систему учетных записей на подпрограммы, тем проще будет внести лишь небольшие изменения, когда придет время переходить на другую операционную систему или что-либо менять. Это предупреждение может показаться ненужным, но единственное, что остается постоянным в системном администрировании, - это постоянные изменения.

    Создание XMLфайла из Perl

    Создание XML-файла из Perl

    Давайте вернемся к событиям, о которых мы говорили в разделе «Права пользователей в NT/2000». Тогда речь шла о том, что необходимо записать информацию об учетной записи, получаемую посредством функции Collectlnformation(), в файл очереди. Но мы так и не видели примеров программы, выполняющей эту задачу. Давайте посмотрим, как записывается этот файл в формате XML.
    Проще всего создать XML-файл при помощи простых операторов, но мы поступим лучше. Модули ХМl : Бенджамина Холзмана (Benjamin Holzman) и XML: Дэвида Меггинеона (David Megginson) могут упростить этот процесс и сделать его менее подверженным ошибкам. Они могут обработать такие детали, как соответствие открывающих/закрывающих тегов, а также позаботятся об экранировании специальных символов (<, >, & и т. д.). Вот пример программы, применяемой в нашей системе учетных записей для создания кода XML при помощи модуля XML: :Writer:
    sub AppendAccountXML {
    получаем полный путь к файлу
    my $filename = shift;
    ft получаем ссылку на анонимный хэш записи
    ту Irecord = shift;
    и XML::Writer использует объекты IO::File для управления и выводом use 10::File;
    # дописываем в этот файл $fh = new 10: :File("»$filename") or die "Unable tt append to file:$!\n";
    # инициализируем модуль XML::Writer и говорим ему
    # записывать данные в файловый дескриптор
    $fh use XML;;Writer; my $w = new XML::Writer(OUTPUT => $fh);
    # записываем открывающий тег для каждой записи $w->startTag("account");
    # записываем открывающие/закрывающие внутренние теги и
    # данные в
    foreach my $field (keys %{$record}){
    print $fh "\n\t";
    $w->startTag($field);
    $w->characters($$record{$field});
    $w->endTag;
    }
    print $fh "\n";
    # записываем закрывающий тег для каждой записи
    $w->endTag;
    $w->end;
    $fh->close();
    }
    Теперь можно использовать всего лишь одну строчку, чтобы получить данные и записать их в файл очереди:
    &AppondAcco:jntXML($addqueue. ^Collect In formation);
    Вот что получается в результате работы этой подпрограммы:

    bobf
    Boh Fate'/fiil lr.ame>
    24-9057
    staff
    passwora
    to_be_created

    Да, мы храним пароли открытым текстом. Это очень плохая идея, и даже в случае с нашей демонстрационной системой стоит дважды подумать, прежде чем ее использовать. В настоящей системе учетных записей надо либо шифровать пароль перед тем, как помещать его в очередь, либо вообще не хранить его там.
    Функция AppendAccountXML() будет применяться еще раз, когда мы захотим записать данные в очередь удаления и в нашу базу данных учетных записей.
    Использование модуля XML: :Writer в подпрограмме AppendAccountXMLO имеет несколько преимуществ:
  • Код получается достаточно читаемым, и тот, кто хоть немного разбирается в языках разметки, сразу же сориентируется в именах startTagO, charactersQ и endTag().
  • И хотя в нашем случае этого не понадобится, функция character () обеспечивает некоторую защиту - она экранирует зарезервированные символы, например символ (>).
  • Мы не должны запоминать последний открывающий тег, чтобы потом добавить соответствующий закрывающий. XML: : Write г заботится об этом за нас и позволяет вызвать функцию eridTag(), не указывая, какой закрывающий тег нам нужен. В нашем случае отслеживание парных тегов не так существенно, поскольку отсутствует их глубокая вложенность, но такая возможность становится очень важной в других ситуациях, где используются более сложные элементы.


  • Переменные и функции имеющие отношение к именам и идентификаторам пользователей

    Таблица 3.1. Переменные и функции, имеющие отношение к именам и идентификаторам пользователей

    Функция/ Переменная Использование
    getpwnam($name) В скалярном контексте возвращает идентификатор, соответствующий этому регистрационному имени; в списочном контексте возвращает все поля данной записи из файла паролей
    getpwuid(Suid) В скалярном контексте возвращает регистрационное имя, соответствующее данному идентификатору; в списочном контексте возвращает все поля данной записи из файла паролей
    $> Соответствует эффективному идентификатору пользователя текущей выполняющейся программы на Perl
    $< Соответствует реальному идентификатору пользователя текущей выполняющейся программы на Perl
    Идентификатор первичной группы (GID)
    В многопользовательских системах пользователи их группы часто работают с файлами и другими ресурсами совместно. В Unix существует механизм, позволяющий работать с группами пользователей. Учетная запись в системе может входить в несколько групп, но при этом принадлежать она должна только одной главной группе (primary group). Поле GID в файле паролей соответствует как раз первичной группе для данной учетной записи.
    Имена групп, их идентификаторы и члены группы обычно перечислены в файле /etc/group. Чтобы включить учетную запись в несколько групп, необходимо просто указать ее в нескольких местах данного файла. В некоторых операционных системах существует жесткое ограничение на число групп, которым может принадлежать учетная запись (а значит, и пользователь). Чаще всего ограничение равно 8. Вот пример пары строк из файла /etc/group:
    bin : : 2:root,bin,daemon
    sys: iSiroot.bin.sys.adu
    Первое поле - это имя группы, второе - пароль (в некоторых системах может употребляться пароль для присоединения к группе), третье - идентификатор группы и последнее поле - список пользователей в группе.
    Способы объединения пользователей в группы зависят от конкретного узла, поскольку границы (и административные, и границы проектов) везде разные. Таким образом, группы можно создавать для разделения различных пользователей (студенты, продавцы и т. д.), по выполняемым действиям (операторы резервных копий, сетевые администраторы и т. д.) либо по назначению учетных записей (резервные учетные записи и пр.).
    Работа с файлами групп средствами Perl очень похожа на процесс разбора файла passwd из предыдущих примеров. Его можно считать обычным текстовым файлом либо применять специальные функции для выполнения подобной задачи. Посмотрите на функции и переменные, имеющие отношение к группам (табл. 3.2).


    Переменные и функции имеющие отношение к именам и идентификаторам групп

    Таблица 3.2. Переменные и функции, имеющие отношение к именам и идентификаторам групп

    Функция/ Переменная Используется
    getgrent() В скалярном контексте возвращает имя группы; в списочном контексте возвращает поля: Sname, Soasswd, $gia, Sm'Xiers
    get.grnarfi(Snane) В скалярном контексте возвращает идентификатор группы; в списочном контексте возвращает те же поля, что и функция getgi-ent( )
    getgrgio($gid) В скалярном контексте возвращает имя группы; в списочном контексте возвращает те же поля, что и функция дё-^"-'
    $) Соответствует эффективному идентификатору группы текущей выполняемой программы
    $( Соответствует реальному идентификатору группы текущей выполняемой программы
    «Зашифрованный» пароль
    Мы уже рассмотрели три основных поля, в которых содержится информация о пользователе в Unix. Следующее поле не является частью хранимой информации, но оно подтверждает права, обязанности и привилегии, присущие пользователю с конкретным идентификатором. Именно так компьютер узнает, что тому, кто выдает себя за пользователя mguerre, позволено присвоить конкретный идентификатор. Существуют и другие, лучшие формы авторизации (например, использование криптографических методов с открытым ключом), но этот способ унаследован от ранних версий Unix.
    Очень часто в этом поле в файле паролей можно увидеть лишь звездочку (*). Подобный знак применяется для запрещения регистрации пользователя в системе, без удаления при этом самой учетной записи.
    Работа с паролями пользователей - это отдельная тема. Ей будет посвящена глава 10 «Безопасность и наблюдение за сетью».
    Поле GCOS
    Поле GCOS самое бесполезное (с точки зрения компьютера). Обычно в этом поле записано полное имя пользователя (например «Рой Дж. Бив»). Часто люди добавляют туда должность и/или номер телефона.
    Системные администраторы, заботящиеся о приватности пользователей (чему и следует быть), должны проверять содержимое данного поля. Это стандартный путь для определения соответствия между реальным именем пользователя и его регистрационным именем. В большинстве Unix-систем такое поле находится в файле /etc/passwd, доступном всем для чтения, следовательно, эта информация может попасть в руки кого угодно в системе. Многие программы, почтовые клиенты и демоны finger-запросов обращаются к этому полю при добавлении регистрационного имени пользователя к какой-то информации. Если у вас есть необходимость скрыть реальные имена пользователей от других людей (например, если речь идет о политических диссидентах, федеральных свидетелях или известных персонах), вы обязательно должны следить за этим полем.
    В качестве дополнительной информации: если вы поддерживаете сайт с менее развитой пользовательской базой, было бы неплохо запретить пользователям изменять их поле GCOS на случайные строки (по тем же причинам, по которым выбранные пользователями регистрационные имена могут вызвать проблемы). Вряд ли вы придете в восторг, увидев в своем файле паролей бранные выражения или иную непрофессиональную информацию.
    Домашний каталог
    Следующее поле содержит имя домашнего каталога пользователя. Это тот каталог, откуда начинается работа с системой. Обычно здесь хранятся файлы, определяющие настройки пользователя.
    В целях безопасности очень важно, чтобы запись в домашний каталог была разрешена только его владельцу. Домашние каталоги, доступные для записи всем, открывают возможность хакерских действий. Правда, существуют ситуации, когда домашние каталоги, доступные для записи только самим владельцам, тоже вызывают проблемы. Например, в случае с ограниченными интерпретаторами (если пользователи могут регистрироваться в системе только для выполнения определенных задач без права изменять что-либо в системе) домашние каталоги, доступные для записи пользователю, категорически запрещены.
    Вот пример кода на Perl, который позволяет убедиться, что все домашние каталоги пользователей принадлежат своим владельцам и недоступны для записи остальным:
    use User::pwent; use File::stat;
    ft замечание: этот код очень сильно загрузит машину, если
    # домашние каталоги монтируются автоматически
    while($pwent = getpwent()){
    # убеждаемся, что это действительно каталог, даже если
    # он спрятан за символическими ссылками
    Sdirinfo = stat($pwent->dir."/."); unless (defined $dirinfo){
    warn "Невозможно получить информацию о ".$pwent->dir.": $!\n"; next;
    }
    warn «Домашний каталог пользователя ".$pwent->name." не имеет в
    ладельца с корректным uid (". $dirinfo->uid." вместо ".$pwent->uid.")!\n"
    # ($dirinfo->uid != $pwent->uid);
    # каталог может быть доступным всем для записи, если
    # у него установлен «бит-липучка" (т. е. 01000),
    # подробности в странице руководства по chraod
    warn $pwent->name."'s homedir is world-writable!\n"
    if ($dirinfo->mode & 022 and (!$stat->mode & 01000));
    }
    endpwent();
    Этот пример на вид несколько отличается от предыдущих, поскольку в нем используются два замысловатых модуля Тома Кристиансе-на (Tom Christiansen): User: :cweit и FiJe: :stai. Эти модули изменяют функции getpwcrH() и stat(), заставляя их возвращать значения, отличные от ранее упомянутых. Когда загружены модули Fi le: : stat, эти функции возвращают объекты вместо списков или скалярных значений. У каждого объекта есть метод, названный по имени поля, которое было бы возвращено в списочном контексте. Поэтому такой код:
    $gid = (stat("filena(ne"))[5]:
    можно переписать гораздо понятнее :
    use File;:stat;
    $stat = stat("filename"):
    $gid = $stat->gid:
    или даже так:
    use File;:stat;
    $gid = stat("filename")->gid;
    Командный интерпретатор пользователя
    Последнее поле классического файла паролей - это поле, соответствующее командному интерпретатору пользователя. Обычно это один из интерпретаторов (sh, csh, tcsh, ksh, zsh), но это может быть и путь к любой исполняемой программе или сценарию. Время от времени, некоторые ради шутки (но наполовину всерьез) устанавливают в качестве своего командного интерпретатора по умолчанию интерпретатор Perl. По крайней мере, в один интерпретатор (zsh) хотят всерьез встроить интерпретатор Perl, но этого пока еще не случилось. Тем не менее, были предприняты серьезные попытки создать командный интерпретатор Perl shell (http:/ /www.focusrese-arch.com/gregor/psh/), а также встроить Perl в редактор Emacs, который легко может заменить целую операционную систему (http:// john-edwin-tobey.org/perlmacs/1).
    Бывают ситуации, когда необходимо указать в этом поле нечто отличное от стандартного командного интерпретатора. Например, если вы хотите создать учетную запись, работающую с системой меню, вы можете поместить в данное поле имя такой программы. В этом случае стоит принять некоторые меры предосторожности, чтобы пользователь, применяющий эту учетную запись, не получил бы доступ к командному интерпретатору, иначе не миновать бед. Часто встречаемая ошибка - включение в такое меню почтовой программы, которая позволяет запускать редактор или инструмент постраничного просмотра для чтения или редактирования почты. Оба эти средства могут иметь возможность выхода в интерпретатор.
    Список доступных в системе стандартных командных интерпретаторов часто хранится в файле /etc/shells, видимо, для удобства демона FTP. Большинство FTP-демонов не позволят обычному пользователю подсоединиться к системе, если их командный интерпретатор, заданный в /etc/passwd (или сетевом файле паролей), не присутствует в /etc/shells. Вот пример на Perl, который докладывает об учетных записях с неподтвержденными командными интерпретаторами:
    use User::pwert:
    Sshells = "/etc/shells";
    open (SHELLS,Sshells) or die "Невозможно окрыть;
    while(){
    chomp:
    $oksnell{$_}++;
    }
    close(SHELLS);
    while($pwent = getpwent()){
    warn $pwent->name." has a bad shell (".$pwent->shell.")!\n"
    unless (exists $okshell{$pwent->shell});
    }
    endpwent();

    Теневые пароли

    Теневые пароли

    Не следует забывать, насколько важна защита содержимого поля GCOS, т. к. целым рядом различных механизмов эта информация доступна для всех. Другая, менее доступная, но довольно уязвимая информация - это список зашифрованных паролей всех пользователей системы. И хотя эти пароли зашифрованы, одно то, что они хранятся в файле, доступном всем для чтения, вносит изрядную степень риска. Некоторые части файла паролей должны быть доступны всем для чтения (например, связь между регистрационным именем и идентификатором пользователя), но не весь файл. Нет никакой необходимости выставлять на всеобщее обозрение список зашифрованных паролей, т. к. пользователи могут попытаться запустить программы для взлома паролей.
    Одна из возможных альтернатив - перенести все пароли в специальный файл, читать который сможет только суперпользователь. Этот второй файл известен как файл «теневых паролей», т. к. в нем хранятся строки, затеняющие записи из обычного файла паролей.
    Вот как все это работает: оригинальный файл паролей остается нетронутым, за одним небольшим исключением. Вместо зашифрованного пароля в это поле помещается специальный символ или символы, которые говорят о том, что используется механизм затенения паролей. Обычно это символ х, но в BSD используется *.
    Я слышал, что существуют некоторые пакеты (для поддержки теневых паролей), вставляющие в это поле специальную строку символов, которая выглядит обычной. Если ваш файл паролей попадет в руки злоумышленника, то он потратит много времени, взламывая случайные строки, не имеющие ничего общего с настоящими паролями.
    Большинство операционных систем используют файл теневых паролей для хранения дополнительной информации об учетной записи. Такой формат включает дополнительные поля, которые мы видели в BSD-файлах, и в них хранится информация об истечении срока действия учетной записи и информация о смене пароля.
    В большинстве случаев обычные Perl-функции, подобные getcwenb(), могут работать с файлами теневых паролей. Если стандартные библиотеки С, входящие в состав операционной системы, делают то, что нужно., то Perl тоже будет делать все верно. Говоря «делать то, что нужно», я подразумеваю, что если ваши сценарии на Perl запускаются с подходящими привилегиями (с привилегиями суперпользователя), то эти функции будут возвращать зашифрованный пароль. В остальных случаях пароль этим функциям не доступен.
    Значительно хуже, если вы захотите получить дополнительные поля из файла теневых паролей. Perl может и не вернуть их вам. Эрик Истабрукс (Eric Estabrooks) написал модуль Passwd: :Solar is, но он будет полезен только при работе в Solaris. Если эти поля имеют для вас принципиальное значение или вы хотите действовать наверняка, то, как ни грустно (это противоречит моим рекомендациям использовать getpwent()), но часто проще открыть файл shadow и получить нужные значения вручную.

    Учетные записи пользователей

    Учетные записи пользователей

  • Информация о пользователях в Unix
  • Информация о пользователях в Windows NT/2000
  • Создание системы учетных записей для работы с пользователями
  • Информация о модулях из этой главы
  • Рекомендуемая дополнительная литература
  • Попробуйте выбрать правильный ответ на следующий вопрос. Если бы не было пользователей, то системные администраторы:
  • Были бы добрее.
  • Исчезли бы с лица земли.
  • Независимо от того, что говорят по этому поводу системные администраторы в «трудную минуту», b) - лучший ответ на этот вопрос. Как я уже говорил в главе 1, в конце концов, системное администрирование нужно для того, чтобы сделать возможным применение доступных технологий.
    Откуда тогда все это ворчание? Пользователи вносят в системы и сети, которые мы администрируем, две вещи, значительно их усложняющие: неопределенность и индивидуальность. О неопределенности мы поговорим в следующей главе при обсуждении активности пользователей, а пока обратимся к индивидуальности.
    В большинстве случаев пользователи хотят сохранять собственную индивидуальность. И дело даже не в том, что они предпочитают иметь уникальные имена,- они рассчитывают иметь и свое личное «имущество». Они хотят, чтобы у них была возможность сказать: «Это мои файлы. Храню я их в своих каталогах. Печатаю их на моей принтерной квоте. И я могу сделать их доступными на моей домашней странице в Сети». Современные операционные системы ведут учет всей этой информации для каждого пользователя.
    Но кто следит за всеми учетными записями в системе или в сети? Кто ответственен за их создание, защиту и распределение? Рискну предподожить и скажу «вы, дорогой читатель». Ну а если не вы лично, то утилиты, которые вы создаете, и которые действуют от вашего имени в качестве посредников. Эта глава призвана помочь читателю справиться с такой ответственностью.
    Начнем разговор о пользователях с рассмотрения информации, которая формирует их индивидуальность, и способов хранения этой информации в системе. Начнем мы с пользователей операционной системы всех вариантов Unix, а затем рассмотрим те же проблемы в Windows NT/2000. Для текущих версий MacOS такая проблема не актуальна, поэтому в данной главе MacOS упоминаться не будет. Познакомившись с этой информацией для обеих операционных систем, мы постараемся создать систему ведения учетных записей.


    XML

    XML

    За последние два года появилось огромное количество материала по XML. Приведенные ниже источники информации - это лучшее, что, на мой взгляд, существует для тех, кто ничего не знает о XML. Когда я писал эту книгу, изданий по XML для Perl еще не было, но мне известно, что несколько подобных проектов уже существует.
    http://msdn.microsoft.com/xml
    и http://www.ibm.com/developer/xml -оба содержат обилие информации. И Microsoft, и IBM очень серьезно настроены по отношению к XML. http://www.activestate.com/support/mailing__lists.htm -
    содержит список рассылки Perl-XML. Он (и его архивы) один из лучших источников данной информации. http://www.w3.org/TR/1998/REC-xml-19980210.
    Спецификация XML1.0. Любой, кто делал что-то на XML, наверняка читал спецификацию. Если вам нужно что-либо более подробное, чем справочник, я советую почитать версии с комментариями. http://www.xml.com.
    Хороший источник статей и ссылок, посвященных XML. Кроме того, здесь можно найти отличную версию спецификации с комментариями Тима Брэя (Tim Bray), одного из ее авторов. «XML: The Annotated Specification»,
    Bob DuCharme (Prentice Hall, 1998). Еще одна отличная версия спецификации с комментариями и примерами кода на XML. «XML Pocket Reference»,
    Robert Eckstein (O'Reilly, 1999). Краткое, но на удивление полное введение в XML для нетерпеливых.

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

    Действия пользователей Управление

    Управление процессами в MacOS

    «Управление» - слишком громко сказано для функциональных возможностей, предоставляемых MacOS, поскольку последняя является не многопользовательской, а просто многозадачной операционной системой. Используя модуль Mac::Processes, можно взаимодействовать с менеджером процессов Macintosh (Macintosh Process Manager) при помощи MacOS Toolbox API для управления процессами. В случае частого применения этого модуля, стоит поискать руководство «Inside Ма-cintosh:Processes>> о работе с менеджером процессов.
    При загрузке Mac: :Processes с помощью стандартной директивы i.sc Mac::Processes, инициализируется специальный хэш %Process. Этот хэш - магический, в нем всегда отображается состояние текущего процесса при помощи возможности Perl, именуемой «связанные переменные». Каждый раз при обращении к содержимому кэша возвращается информация о процессах, запущенных в настоящий момент в системе. Чтобы просмотреть список серийных номеров теку щих процессов (Process Serial Number , PSN - так в MacOS называются идентификаторы процессов), надо просто запросить список ключей этого хэша:
    use Mac: Processes:
    print map{"$_\n"} keys %Process:
    Подробную информацию по любому процессу можно получить, пора ботав со значениями, возвращаемыми для каждого ключа. Каждая запись в хэше содержит объект, представляющий структуру. Чтобы получить поля этой структуры, необходимо вызвать соответствующие методы объекта. Более подробно о том, что представляет собой каждое поле, можно узнать в книге «Inside Macintosh:Processes». В настоящее время существуют методы processNumber (), processType(), processSignatureO, processSize(), procbssMoae(), processLocat.: -on(), processLauncher(),processLaunchDate(), processActiveTlmef)и processAppSpec().
    Нетрудно вывести список запущенных процессов с их именами:
    use Mac::Processes;
    while(($psn, $psi) = each (%Process)){
    $name = $psi->processName():
    write; }
    format STDOUT_TOP =
    Process Serial Number Process Name
    format STDOUT =
    @««« @<««««««««««««««««
    $psn, $name
    Результат таков:
    Process Serial Number Process Name
    8192 FaxMonitor
    8193 Queue Watcher
    8194 Finder
    8195 Serial Port Monitor 8198 MacPerl
    Теперь, когда вы знаете, какие процессы у вас запущенны, совершенно естественно попытаться ими управлять. К сожалению, здесь практически ничего нельзя сделать. Самое большое, на что мы способны, - это перевести процесс в интерактивный режим при помощи SetFrontProcess($psn). У нас даже нет возможности напрямую его завершить (Perl-функция kill() не реализована). Лучшее, что можно сделать, послать выполняющемуся приложению событие AppleEvent, чтобы сообщить, что процесс должен быть завершен. Самый простой способ сделать это - применить модуль Mac: :Apps: : Launch Криса Нандора (Chris Nandor). В нем есть функция QuitApps(), которая позволяет завершить работу приложения, располагая его ID. В Мае:: Apes: . Launcn есть еще несколько полезных функций для запуска приложений и перевода их из/в интерактивный режим. Делается это так же, как и при использовании Мае: :Processes. Теперь наступает время операционной системы, в которой управление процессами менее ограничено.


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

    Получение и установка модуля Win32::1Ргос происходят несколько сложнее, чем бывает с другими модулями. Сам модуль вместе с остальными модулями Рамдэна можно найти на http://www.generation.net, -aminer/Perl/. Чтобы использовать Win32: : IProc, вам также понадобится загрузить еще два модуля: Win32: :ISync Рамдэна и Win32.: API Алдо Калпини (Aldo Calpini). Первый можно найти на сайте Рамдэна, второй в репозитории модулей ActiveState или на http://dada.perl.it/.
    Некоторые из модулей Рамдэна устанавливаются вручную без помощи команды ррт и требуют небольших изменений исходного кода. Вот полный рецепт для установки. Я считаю, что вы распаковали дистрибутивы и собираетесь устанавливать их в Perl, скомпилированный ActiveState и установленный в каталоге C:\Perl:
  • ррт install Win32-API
  • md c:\Perl\site\lib\auto\Win32\Sync и C:\Perl\site\lib\auto\Win32\ Iproc
  • Скопируйте timer.dll и sync.dll в c:\Perl\site\lib\auto\Win32\Sync
  • Скопируйте iprocnt.dll, psapi.dll и iproc.dll в C:\Perl\site\lib\auto\ Win32\Iproc
  • Скопируйте iproc.pm, iipc.pm и isync.pm в C:\Perl\site\lib\Win32\
  • Измените строки DLLPath в iproc.pm на следующие:
  • my($DLLPath) ="C:\\Perl\\site\\lib\\auto\\Win32\\Iproc\\IProc,dll":
    my($DLLPath1)="C:\\Perl\\site\\lib\\auto\\Win32\\Iproc\\IprocNT.dll";
    my($DLLPath2)="C:\\Perl\\site\\lib\\auto\\Win32\\Sync\\Sync.dll";
    Установка Win32::Setupsup
    Если вы хотите установить модуль Win32: :Setunsi:p вручную и/или изучить его исходный код, вы можете найти ZIP-архив модуля на ftp: ftp.roth.net/pub/NTPerl/Others/SetupSup/. Если же вы предпочитаете установить его простым способом в существующий ActiveState, то можете соединиться с архивом модулей Йенды Крыницки (Jenda Kry-nicky) и установить его, используя обычный способ ррт. Инструкции о том, как это сделать, можно найти на сайте http:/ /Jenda.Krynicky.c:.
    Сложность в том, что документация в формате pod неверно форматируется, если вызывать ее при помощи perldoc или устанавливать в HTML. Документация в конце setupsup.pm (вероятнее всего, вы найдете ее в <ваш каталог Perl \site\lib\Win32\) гораздо более верная. Если вы попытаетесь узнать, как использовать этот модуль, я советую открыть сам файл в обычном текстовом редакторе и просмотреть те части, которые являются документацией.


    Использование файловой системы /ргос

    Использование файловой системы /ргос

    В большинстве современных вариантов Unix существует интересное добавление - файловая система /ргос. Эта загадочная файловая система не имеет ничего общего с хранением данных. Она обеспечивает «файлоподобный» интерфейс к таблице запущенных процессов. Для каждого из них в этой файловой системе существует «каталог», название которого совпадает с идентификатором процесса. В этом каталоге есть целый ряд «файлов», предоставляющих информацию о данном процессе. В один из этих файлов разрешена запись, что и позволяет управлять самим процессом.
    Это действительно мудрая идея и это замечательно. Плохо то, что каждый производитель/команда разработчиков, поддержав эту мудрую концепцию, разбежались каждый в своем направлении. В результате файлы, которые можно найти в каталогах /ргос, часто специфичны для различных вариантов операционной системы, отличаясь как по именам, так и по формату. Описание того, какие файлы доступны и что в них хранится, вам придется искать на страницах руководства (обычно в разделах 4, 5 или 8) по ргосfs или mount jjrocfs.
    Единственное переносимое использование файловой системы /ргос -это нумерация запущенных процессов. Если нам нужно только перечислить идентификаторы процессов и их владельцев, мы можем применять операторы Perl по работе с каталогами и Istat ( ) :
    opendir(PROC, "/proc") or die
    "Невозможно открыть /proc:$!\n";
    while (defined($_= readdir(PROC))){
    next if ($_ eq " . " or $_ eq "..");
    next unless /"\d+$/;
    # отфильтровываем все случайные файлы, названия
    # которых могут являться идентификаторами процессов
    print "$_\t". getpwjid((lstat "/Ргос/$_" )[4] ) , "\n";
    > closedir(PROC);
    Для того чтобы получить подробную информацию о процессе, следует открыть нужный двоичный файл из каталогов в /ргос и воспользоваться функцией unpack(). Обычно это файл status или psinfo. На страницах только что упомянутых руководств есть подробная информация о С-структуре, которую можно найти в этом файле, или, по крайней мере, есть ссылка на включаемый (include) файл, в котором эта структура документирована. Поскольку эти форматы зависят от операционной системы (и версии ОС), вы снова столкнетесь с проблемами переносимости программы.
    Вероятно, вы уже чувствуете себя растерянным, поскольку все рассмотренные варианты требуют, чтобы в программе были учтены все версии каждой операционной системы, которую нам надо поддерживать. К счастью, в запасе у нас есть еще одна возможность, и она может помочь.

    Использование инструментария управления окнами (Window Management Instrumentation WMI)

    Использование инструментария управления окнами (Window Management Instrumentation, WMI)

    Перед тем как перейти еще к одной операционной системе, рассмотрим последний подход к управлению процессами в NT/2000. Этот подход следовало бы назвать «Страной будущего», поскольку в нем используется пока еще не очень распространенная, но уже пробивающаяся технология. Инструментарий управления окнами (WMI) доступен в Windows 2000 (и в NT4.0 с установленным SP4+). Со временем, когда Windows 2000 широко распространится, WMI вполне может стать важной частью администрирования NT/2000.
    К сожалению, WMI относится к числу технологий не для слабонервных, потому что очень быстро становится чересчур сложной. Она основана на объектно-ориентированной модели, которая позволяет представить не только данные, но и отношения между объектами. Например, можно создать связь между веб-сервером и дисковым массивом RAID, в котором хранятся данные с этого сервера, обеспечивающую, в случае неисправности массива RAID, сообщение и о проблеме с вебсервером. Не желая вдаваться во все эти сложности, мы дадим здесь лишь поверхностный обзор WMI в небольшом и простом введении, сопровождаемом примерами.
    Если вы хотите познакомиться с этой технологией подробнее, я рекомендую загрузить документацию по WMI, обучающее руководство «LearnWBM» и WMI SDK из раздела «WMI» сайта http://msdn.microsoft.com /developer/sdk. Также взгляните на информацию, представленную на веб-сайте Distributed Management Task Force на http:// www.dtmf.org. Тем временем, начнем с краткого экскурса.
    WMI - это реализация и расширение (от Microsoft) неудачно названной инициативы Web-Based Enterprise Management или WBEM. И хотя такое название вызывает в воображении что-то связанное с броузерами, эта технология не имеет практически ничего общего с World Wide Web. Компании, входившие в состав Distributed Management Task Force (DMTF), хотели придумать что-то, что могло бы упростить выполнение задач управления при помощи броузеров. Забыв про название, можно сказать, что WBEM определяет модель данных для информации управления. WBEM обеспечивает спецификацию для организации, доступа и перемещения этих данных. WBEM также предлагает связующий интерфейс для работы с данными, доступными из других протоколов, например, Simple Network Management Protocol (SNMP) (о котором мы будем говорить в главе 10 «Безопасность и наблюдение за сетью») и Common Management Information Protocol (CMIP). Данные в WBEM организованы при помощи Общей информационной модели (Common Information Model, CIM). CIM - источник силы и сложности в WBEM/WMI. Она предоставляет расширяемые модели данных, содержащие объекты и классы объектов для любой физической или логической сущности, которой можно захотеть управлять. Например, в ней есть классы объектов для целых сетей и объекты для отдельного слота машины. Существуют объекты для настроек как аппаратного обеспечения, так и приложений программного обеспечения. Помимо этого CIM позволяет определять классы объектов, описывающие связи между другими объектами.
    Модель данных документирована в двух частях: в спецификации CIM и схеме CIM. Первая описывает, как (как данные определяются, их связь с другими стандартами управления и т. д.); вторая - что (т. е. сами объекты). Это деление может напомнить о связи SNMP SMI и МШ (подробный материал в главе 10).
    На практике вы будете чаще обращаться к схеме CIM, чем к спецификации, когда вы освоитесь с тем, как представляются данные. Формат схемы, названный форматом управляемых объектов (Managed Object Format, MOF), довольно просто читать.
    Схема CIM состоит из двух слоев:
  • Центральная модель (core model) для объектов и классов, полезна для всех типов взаимодействия WBEM.
  • Общая модель (common model) для объектов, которые не зависят от создателя и операционной системы. Внутри общей модели в настоящее время определены пять областей: системы, устройства, приложения, сети и физический уровень.
  • На вершине этих двух слоев может быть любое число расширенных схем (Extension schema), определяющих объекты и классы для информации, зависящей от создателя и информационной системы.
    Самая важная часть WMI, которая отличает ее от обычных реализаций WBEM, - это схема Win32, расширенная схема для информации, специфичной для Win32, построенная на центральной и общей модели. WMI также добавляется к общей структуре WBEM, обеспечивая механизмы доступа к данным CIM, специфичные для Win32. Используя это расширение схемы и набор методов доступа к данным, мы можем выяснить, как управлять процессами из Perl средствами WMI.
    Два из этих методов доступа: ODBC (открытый интерфейс взаимодействия с базами данных) и COM/DCOM (модель составных компонентов распределенная модель составных компонентов) будут более полно рассмотрены в других главах этой книги. В примерах будет использоваться модель COM/DCOM, поскольку ODBC разрешает лишь запрашивать информацию у WMI (хотя и в простой, похожей на присущую базам данных манере). COM/DCOM позволяет и запрашивать информацию, и взаимодействовать с ней, что очень важно для «управляющей» части контроля над процессами.
    Приведенные далее примеры программ на Perl не выглядят такими уж трудными, и вас могут удивить слова «очень быстро становится чересчур сложным». Приведенный ниже код выглядит простым, потому что:
  • Мы касаемся только поверхности WMI. Мы даже не затрагиваем таких понятий, как «ассоциации» (т. е. связи между объектами и классами объектов).
  • Мы выполняем только простые операции управления. Управление процессами в таком контексте состоит из опроса исполняемых процессов и возможности их завершения. Эти операции легко осуществляются в WMI при использовании расширения схемы Win32.
  • В наших примерах спрятана вся сложность перевода документации WMI и примеров программ с VBscript/JScript на Perl.
  • В примерах скрыта неясность процессов отладки. Когда код на Perl, имеющий отношений к WMI, завершается с ошибками, выдается очень мало информации, которая могла бы помочь найти их причину. Да, вы получите сообщения об ошибках, но в них никогда не будет сказано ОШИБКА: ПРОБЛЕМА ЗАКЛЮЧАЕТСЯ В СЛЕДУЮЩЕМ. .. Скорее всего, вы получите что-нибудь подобное wbemErrFailed 0x8004100 или вообще пустую структуру данных. Справедливости ради надо сказать, что большая часть такой неясности возникает благодаря участию Perl в этом процессе. Он действует в качестве интерфейса к целому ряду довольно сложных многоуровневых операций, которые не утруждают себя передачей содержательных сообщений в случае возникновения проблем.
  • Это звучит довольно мрачно. Поэтому позвольте предложить совет, воспользоваться которым стоит перед тем, как рассматривать сами примеры:
  • Изучите любые примеры, использующие модуль Win32. :OLE, которые сможете найти. Список рассылки Win32-Users на ActiveState и его архивы на http://www.activestate.com - хороший источник подобной информации. Если сравнить их с эквивалентными примерами на VBscript, то станут понятны необходимые идиомы трансляции. Кроме того, вам может помочь раздел «ADSI (Интерфейсы активных служб каталогов)» главы 6.
  • Подружитесь с отладчиком Perl и используйте его для тщательной проверки фрагментов кода в качестве части процесса обучения. Другой способ тестирования на платформе Win32 отрывков кода на Perl - применение программы TurboPerl Вильяма Смита (William P. Smith), ее можно найти на http://users.erols.com/turboperL/, совместно с модулями dumpvar.pl или Data: :Dumper. В ней бывают сбои (я советую чаще сохранять исправления), но обычно она может помочь в создании заготовок кода на Perl. Другие инструменты интегрированной среды разработки также могут обладать подобными возможностями.
  • Всегда держите под рукой копию WMI SDK. Его документация и примеры кода на VBscript очень полезны.
  • Чаще используйте броузер объектов WMI в WMI SDK. Он поможет вам разобраться со структурой.
  • Теперь перейдем к Perl. Первоначальная наша задача - определить, какую информацию о процессах в Win32 можно получить и как ее использовать.
    Сначала нужно установить соединение с пространством имен (names расе) WMI. Пространство имен определяется в WMI SDK как «единица для группировки классов и экземпляров для управления их областью действия и видимостью». Нам необходимо соединение с корнем стандартного пространства имен cimv2, в котором содержатся все интересующие нас данные.
    Кроме того, потребуется установить соединение с соответствующим уровнем привилегий. Программа должна иметь право отлаживать процесс и представлять нас; другими словами, она должна выполняться от имени пользователя, запустившего сценарий. Установленное соединение позволит получить объект Win32_Procoss (как это определяется в схеме Win32).
    Существуют как простой, так и сложный способы создать это соединение. В первом примере будут приведены оба способа, так что читатель сможет решить, чего стоит каждый из них. Вот сложный способ с объяснениями.
    use Win32::OLE('in1);
    Sserver = ''; 8 соединение с локальной машиной
    Я получаем объект SWbemLocator
    $lob] = Win32: :OLE->new
    ( 'WbemScripting, SWoe:riLocat:or')
    or die "Невозможно создать объект локатор: ' .Win32: :CLE->LasTf.-"rori
    и определяем, что сценарий выполняется с празам!'
    # используем зю для пслуче^'р объекта bWbe;rServ:c»^ Ssobj = $iol". ->
    or oie M^acjMcvhc создать ооье^.т сеов-' ".Win32 :OLt-.>LastErrcr (). \:V ,
    и получаем объект схемы
    Sprocschm = $sobj->Get('Win32_Process');
    Сложный способ включает в себя:
  • Получение объекта локатора, используемого для нахождения соединения с объектом-сервером
  • Установку прав, т. е. программа будет выполняться с нашими привилегиями
  • Использование этого объекта для получения соединения с ci/w2-пространством имен WMI
  • Применение этого соединения для получения объекта Win32_Process
  • Все это можно сделать за один шаг, если использовать COM moniker's display name. В соответствии с WMI SDK, «в модели составных объектов (СОМ) моникер - это стандартный механизм для инкапсуляции местоположения другого СОМ-объекта и связи с ним. Текстовое представление моникера называется отображаемым именем». Вот и простой способ в действии:
    usa Win32::OLECirT);
    Sprocschm = Win32::OLE->GetObject(
    'winmgmts: {impersonationLevel=impersonate}! Wiri32_Process ')
    or die "Невозможно создать объект сервера: ".Win32: :OLE->LastError()."\n":
    Теперь, когда у нас есть объект Win32_Process, можно с его помощью получить нужные части схемы, представляющие собой процессы в Win32. В их число входят все доступные свойства и методы Win32_Pro-cess, которые годятся к употреблению. Применяемая программа довольно проста; единственно, что не вполне очевидно, - это использование оператора in в Win32: :01_Е. Чтобы объяснить это, нам придется немного отклониться от темы.
    Объект $procschm имеет два специальных свойства: Properties и Methods. В каждом из них хранится специальный дочерний объект, известный как collection object в терминологии СОМ. Объект collection object является родительским контейнером для других объектов; в этом случае в них хранятся объекты описания свойств (Properties_) и методов (Methods) схемы. Оператор in возвращает массив ссылок на каждый дочерний объект контейнера. Располагая таким массивом, можно обойти все его элементы в цикле, возвращая на каждой итерации свойство Name каждого дочернего объекта. О других известных применениях in можно узнать из раздела «ADSI (Интерфейсы активных служб каталогов)» главы 6. Вот как выглядит сама программа:
    use Win32::OLE('in'):
    соединяемся с пространством имен, даем указание действовать
    с правами пользователя и получаем объект Win32_process,
    просто используя отображаемое имя
    Sprocschm = Win32 -OLE->GetObject(
    'winmgmtr, : {impersor,ationl_evel = impersonate} ' win32._Process )
    or die "Невозможно создать объект сервера: " .i/nn32' .OLE- > Last etc :().
    print "--- Prope-ties ---\n";
    print join("\n" , map {$_->{Name}
    }
    (in $procschm->{Properties_} )}:
    print "\n--- Methods ---\n";
    printoin("\n",map {$_->{Name}
    }
    (in $procschm->{Methods_l ;}:
    Вывод (на машине с NT4.0) выглядит примерно так:
    — Properties —
    Caption
    CreationCiassName
    CreationDate
    CSCreationClassName
    CSName
    Description
    ExecutablePath
    ExecutionState
    Handle
    InstallDate
    KernelModeTime
    MaximumWorkingSetSize
    MinimumWorkingSetSize
    Name
    OSCreationClassName
    OSNarne
    PageFaults
    PageFilellsage
    PeakPageFileUsage
    PeakWorkingSetSize
    Priority
    Processld
    QuotaNonPagedPoolUsage
    QuotaPagedPoolUsage
    QuotaPeakNonPagedPoolUsage
    QuotaPeakPagedPoolUsage
    Status
    TerminationDate
    User'ModeTime
    WindowsVersion
    WorkingSetSize
    --- Methods ---
    Create
    Terminate
    GetOwner
    GetOwrierSui
    Рассмотрим это подробнее. Чтобы получить список запущенных процессов, нужно запросить все экземпляры объектов Win32_Process:
    use Win32::OLE('in'):
    it выполняем все первоначальные шаги в одном цикле
    $sob] = Win32::OLE->GetOcject(
    'winmgmts:{impersonationLeveI=inpersorate}') or die
    "Невозможно создать объект сервера: ".Win32: :OLE->LastError() "V"
    foreach Sprocess (in $sobj->InstancesOf("Win32_Process")){
    print $process->{Name)." имеет pid #".$process->{Process!d}, "\n"; }
    Первоначальное отображаемое имя не включает путь к определенному объекту (т. е. мы отбросили ! Win32_Process). Итак, получен объект связи с сервером. В результате вызова метод InstancesOf () возвращает объект-коллекцию (collection object), который содержит все экземпляры конкретного объекта. Наш код обращается к каждому объекту и выводит его свойства Name и Processld. В итоге, у нас есть список всех запущенных процессов.
    Чуть менее великодушный подход к обойденным в цикле процессам позволил бы использовать один из методов, указанных в приведенном выше списке:
    foreach $process (in $sobj->InstancesOf ("Win32__Process")){
    $process->Terminate(1); }
    В результате, все работающие процессы будут завершены. Я не рекомендую вам запускать эту программу в таком виде; подправьте ее в соответствии с вашими нуждами, сделав более конкретной.
    Теперь у вас есть необходимые знания, чтобы начать использовать WMI для управления процессами. В WMI есть \Уш32-расширения для многих других частей операционной системы, включая реестр и журнал событий.
    Вот и все, что мы хотели сказать об управлении процессами в WinNT и 2000. Теперь перейдем к последней операционной системе.

    Использование модуля Proc ProcessTable

    Использование модуля Proc::ProcessTable

    Дэниел Дж. Урист (Daniel J. Urist) (с помощью нескольких добровольцев) написал модуль Proc: :ProcessTable, предоставляющий единый интерфейс к таблице процессов для всех основных вариантов операционной системы Unix. Он скрывает от вас причуды различных реализаций /ргос и kmem, позволяя писать более переносимые программы.
    Просто загрузите модуль, создайте объект Ргос: :ProcessTable: :Proces-и используйте методы этого объекта:
    use Proc: :ProcessTable;
    $tobj = new Proc: : ProcessTable;
    Этот объект использует механизм связанных переменных (tied variable) для представления текущего состояния системы. Для обновления! этого объекта не требуется вызывать специальную функцию - он перечитывает таблицу процессов при каждом обращении к нему. Это похоже на хэш %Process, знакомый нам по обсуждению модуля Мае : Р ses ранее в этой главе.
    Чтобы получить нужную информацию, следует вызвать метод
    : : Sproctable = $tobj->table();
    table() возвращает ссылку на массив, элементы которого представляют собой ссылки на объекты процессов. Каждый из этих объектов имеет свой собственный набор методов, возвращающих информацию об этом процессе. Например, вот как можно получить список идентификаторов процессов и их владельцев:
    use Proc::ProcessTable:
    Stobj = new Proc::ProcessTable:
    Sproctable = $tobj->table(); for (ia>$proctable){
    print $_->pid."\t". getpwuid($_->uid)."\n";
    }
    Список методов, доступных в вашей операционной системе, можно получить при помощи метода fields() объекта Proc: : ProcessTable (т.е. $tobj).
    В Proc:: ProcessTable также есть три дополнительных метода у каждого объекта процесса: kill(), priorityO и pgrp(), которые являются всего лишь интерфейсом к встроенным функциям, упомянутым в начале этого раздела.
    Чтобы опять вернуться к общей задаче, посмотрим на применение способов контроля над процессами. Мы начали изучать управление процессами в контексте действий пользователя, поэтому сейчас рассмотрим несколько маленьких сценариев, посвященных этим действиям. В примерах мы будем использовать Proc:: ProcessTable в Unix, но сами идеи не зависят от операционной системы.
    Первый пример из документации по Proc: : ProcessTable: use Proc::ProcessTable;
    $t = new Proc::ProcessTable; foreach $p ((5>{$t->table}){
    if ($p->pctmem > 95){ $p->kill(9);
    }
    }
    Эта программа «отстреливает» все процессы, занимающие 95% памяти в тех вариантах операционной системы Unix, где поддерживается метод pctmem() (а он поддерживается в большинстве случаев). В таком виде пример, вероятно, слишком «безжалостен», чтобы использовать его в реальной жизни. Было бы благоразумно добавить перед командой kill() что-то подобное:
    print "собираемся убрят;. " Sn-^pid. "\t". get. owuid($p->uid).
    "Vi": print "выполнять9 (yes'''i'0 " chomp($ans = о):
    next unless (Sans eq "yes"):
    Здесь может возникнуть состояние перехвата: не исключено, что во время задержки, вызванной вопросом к пользователю, состояние системы изменится. Учитывая, что мы в данном случае работаем только с «большими» процессами, которые вряд ли меняют свое состояние в течение короткого времени, такой вариант, скорее всего, пройдет нормально. Если вы хотите подойти к этому вопросу более педантично, вам, наверное, стоит получить сначала список процессов, которые вы хотите завершить, спросить пользователя, а затем проверить еще раз состояние таблицы процессов и только потом их завершать.
    Бывают случаи, когда завершение процесса - это слишком легкая расплата. Иногда важно засечь, что процесс действительно работает, чтобы предпринять необходимые меры (скажем, поставить пользователя на место). Например, политика нашего сайта запрещает применять IRC-роботы. Роботы - это процессы-демоны, которые соединяются с IRC-серверами и выполняют автоматические действия. И хотя роботы могут использоваться в благих целях, в настоящее время они, в основном, играют асоциальную роль в IRC. Кроме того, мы обращали внимание на взлом системы безопасности из-за того, что первое (и часто единственное), что делал взломщик, — это запускал IRC-робота. Короче говоря, нам важно заметить присутствие таких процессов, а не завершать их работу.
    Чаще других сейчас используется робот под названием eggdrop. Выяснить, запущен ли в системе процесс с таким именем, можно при помощи следующей программы:
    use Proc::ProcessTable;
    open(LOG, "»$logf ile") or die
    "Невозможно открыть журнал для дозаписи:
    $t = new Proc::ProcessTable; foreach $p (@{$t->table})
    { if ($p->fname() =" /eggdrop/i){ print LOG time."\t".
    getpwuid($p->uid).
    "\t".$p->fname()."\n": >
    }
    close(LOG);
    Тот, кто подумает: «Эта программа не так уж и хороша! Все, что нужно сделать, чтобы ускользнуть от этой проверки, так это всего лишь переименовать исполняемый файл», будет абсолютно прав. Мы попытаемся написать менее простодушный код, ищущий роботов, в самом последнем разделе этой главы.
    А пока рассмотрим еще один пример, в котором Perl помогает управлять процессами пользователей. До сих пор все наши примеры касались отрицательных явлений. Рассмотренные программы имели дело со злонамеренными или жадными к ресурсам процессами. Теперь посмотрим на что-нибудь более жизнерадостное.
    Существуют ситуации, когда системному администратору необходимо узнать, какие программы применяются пользователями в системе. Иногда это необходимо сделать для программного обеспечения, лицензия которого запрещает его одновременное использование сверхнормативным числом потребителей. В таких случаях обычно применяется специальный механизм. Иногда подобные сведения необходимы, чтобы иметь возможность перейти на другую систему. Если вы переносите пользователей с одной системы на другую, вам необходимо убедиться, что все программы, работающие на старом месте, будут доступны и на новом.
    Один подход к решению этой задачи - заменить каждую доступную пользователям исполняемую программу, не входящую в состав операционной системы, на оболочку, которая сначала запишет, какая программа была вызвана, а затем и запустит ее. Это сложно реализовать, если в системе доступно множество программ. Кроме того, есть и побочный эффект - запуск каждой программы требует большего времени.
    Если точность не важна и достаточно знать только приблизительную оценку набора работающих программ, можно применить Ргос::Рго-cessTable. Ниже приведена программа, которая активизируется каждые пять минут и проверяет состояние текущих процессов. Она просто ведет учет всех найденных имен процессов, причем те процессы, которые встречались в предыдущий раз, она во второй раз не учитывает. Ежечасно программа записывает результаты и начинает подсчет заново. Пятиминутное ожидание объясняется тем, что обход таблицы процессов является ресурсоемкой операцией (обычно), а мы хотим, чтобы эта программа как можно меньше загружала систему:
    use Proc::ProcessTable;
    Sinterval = 600;
    5 минут перерыва
    Spartofhour = 0; (f отмечаем позицию часа, в которой мы находимся
    Stop] = new Proc: : ProcessTabie;
    создаем човый объект
    tt вечный цикл, сбор данных каждые Sintervai секунд
    № и сброс этих данных один раз в час
    while(1){
    ucollectstats;
    &dumpandreset if (Spartofhour >= 3600);
    sleep($interval), }
    сбор статистики по пролессу sub collectsrars ( my($process);
    foreach Sprocess (@{$tobj->table}){
    мы должны игнорировать себя next if ($process->pid() == $$);
    сохраняем информацию о процессе для следующего запуска
    push(@last,Sprocess->pid(),$process->fname());
    игнорируем процесс, если мы его уже видели
    next if ($last{$process->pid()} eq $process->fname());
    если не видели, запоминаем его
    $collection{$process->fname()}++; }
    и устанавливаем хэш %last, используя текущую таблицу %last = ©last;
    Spartofhour += $interval; }
    выводим результаты и сбрасываем значения счетчиков
    sub dumpandreset{
    print scalar localtime(time). C'-"x50),"\n";
    for (sort reverse_value_sort keys %collection){ write;
    undef %collection; Spartofhour = 0; }
    (обратная) сортировка no значениям хэша %collection и по
    именам ключей
    sub reverse_value_sort{
    return $collection{$b} <=> $collection{$a} || $a cmp $b;
    }
    format STDOUT = @««««« »»
    $_, $collection{$._}
    format STDOUT_TOP = Name Count
    Существует множество способов улучшить эту программу. Она могла бы отслеживать процессы для каждого пользователя (т. е. записывать один экземпляр вызова программы для каждого пользователя), собирать ежедневную статистику, выводить информацию в виде диаграммы и т. д. Все зависит только от того, где вы хотите ее применять.

    Использование модуля Win32 IProc

    Использование модуля Win32::IProc

    Второй подход - применять модуль Win32: : IProc Амина Мюлэй Рамдэна (Amine Moulay Ramdane). И хотя название подсказывает, казалось бы, очевидный выбор, но Win32: : Iproc, в действительности, гораздо полезнее для нас, чем Win32: :Process. У Win32: : Process есть один значительный недостаток, который тут же выводит модуль из борьбы: он создан для работы с процессами, которые были запущены им самим. В то время как нас больше интересуют процессы, запущенные другими пользователями. Если вам не удается установить модуль Win32: :IProc, загляните в раздел «Информация о модулях из этой главы» .
    Сначала необходимо создать объект процесса подобным образом:
    use Win32::IProc;
    и обратите внимание на регистр.
    Обязательно должно быть "IProc"
    $pobj = new Win32::IProc or die
    "Невозможно создать объект proccess: $!\n";
    Такой объект обычно используется в качестве трамплина, с которого запускаются методы объекта. Например, чтобы получить список всех запущенных процессов, можно написать:
    $pobj-> EnumProccesses(\@processlist) or
    die "Невозможно получить список процессов:$!\n";
    @processlist - это массив ссылок на анонимные хэши. В каждом анонимном хэше есть два ключа: ProcessName и Processld с их значениями. Такой код позволяет аккуратно вывести нужную информацию:
    use Win32::IProc;
    $pobj=new Win32::IProc or die
    $pobj->EnumPrecesses(VSprocessiis и or
    die "Невозможно получитэ список процессор:$г\п';
    foreach Sprocess (@processlist){
    $pid = $process-x{ProcessId};
    Snair.e - $cessNama}; write:
    }
    format STDOUT_TCP =
    Process ID Process Name
    format STDOUT =
    @<««« @««« ««««««««««««
    $pid, $name
    Получаем результат:
    Process ID Process Name
    ======= ===========
    0 System-Idle
    2 System
    25 smss.exe
    39 winlogon.exe
    41 services.exe
    48 lsass.exe
    78 spoolss.exe
    82 OKSERVICE.EXE
    Отличие от действий pulist.exe заключается в том, что Win32: : IP roc не может сообщить вам пользовательский контекст для процесса. Если эта информация для вас важна, то следует применять pulist.exe.
    pulist.exe может вывести только один тип информации, а что может Win32: - сейчас будет ясно. Допустим, вам хочется узнать не только о запущенных процессах, но и о том, какие исполняемые программы и динамически загружаемые библиотеки (.сШ) использует каждый процесс. Получить эту информацию просто:
    импортируем постоянную FULLPATH.
    чтоб показывать пути к
    библиотекам, может быть и NOPATH use
    Win32:.IProc "FULLPATH": Spobj = пел Win32::IProc:
    $pobj ->EnuTiProcesses(\5processlist) or die
    "Невозможно список процессов : $;
    foreach Sp^ocess (?processlisr){ print "\n".
    $p^ocess->(D''ocessNaTie!
    "\n".(' = ' x length($orocess->{ProcessNa~e})!. "-n":
    Vswodules,FULLPATH); print join("\n", map {lc $_->
    {Modul'eName}} ^modules), "\n":
    }
    GetProcessModules() получает идентификатор процесса, ссылку на массив и флаг, говорящий о том, возвращать ли полный путь к библиотеке. Элементами массива, на который мы ссылаемся, являются ссылки на анонимные хэши, содержащие информацию о каждой библиотеке, используемой этим процессом. В нашем примере собираются имена всех библиотек. тар() используется для того, чтобы обойти весь массив ссылок, разыменовывать каждый анонимный хэш и получить значение ключа ModuleName.
    Вот отрывок полученных данных:
    smss.exe
    \systemroot\system32\smss.exe c:\winnt\system32\ntdll.dll
    winlogon.exe
    \??\с:\winnt\system32\winlogon.exe
    c:\winnt\system32\ntdll.dll
    c:\winnt\system32\msvcrt.dll
    c:\winnt\system32\kernel32.dll
    c:\winnt\system32\advapi32.dll
    c:\winnt\system32\user32.dll
    c:\winnt\systein32\gdi32.dll
    c:\winnt\system32\rpcrt4.dll
    c:\winnt\system32\userenv.dll
    c:\winnt\system32\shell32.dll
    c:\winnt\system32\shlwapi.dll
    с:\winnt\system32\comctl32.dll
    c:\winnt\system32\netapi32.dll
    С:\winnt\system32\netrap. dll
    c:\winnt\system32\samlib.dll
    c:\winnt\system32\winmm. dll
    с:\winnt\system32\cwcmmsys.dll
    c:\winnt\system32\cwcfm3.dll
    c:\winnt\system32\msgina.dll
    c:\winnt\system32\rpclts1.dll
    c:\winnt\system32\rpcltcl. all. . .
    Но давайте пойдем еще дальше. Совсем немного усилий следует приложить, чтобы больше узнать о запущенных процессах. Для получения необходимой информации сначала нужно определить дескриптор этого процесса.
    Дескриптор процесса можно рассматривать как открытое соединение с данным процессом. Чтобы выяснить разницу между дескриптором процесса и его идентификатором, проведем аналогию со стоянкой прицепов. Если каждый прицеп на стоянке считать процессом, то адрес прицепа можно считать идентификатором процесса. Это способ найти нужный прицеп. Дескриптор процесса - это что-то наподобие телефонных линий, труб и проводов, подведенных к прицепу. Когда прицеп подсоединен к этим коммуникациям, вы можете не только найти нужный прицеп, но и передавать в него информацию.
    Для получения дескриптора процесса, если у нас есть его идентификатор, используем метод Ореп() из модуля Win32: : IProc. Ореп() принимает идентификатор процесса, флаг доступа, флаг наследования и ссылку на скалярное значение, в котором хранится дескриптор. В следующем примере запроса мы будем использовать флаги доступа, которых достаточно для получения информации о процессе. Подробную информацию об этих флагах можно найти в документации по Win32: :IProc и в разделе «Processes and Threads» документации Win32 SDK по основным службам, которую можно найти на http://msdn.microsoft.com. Дескрипторы процессов, открытые при помощи Ореп(), необходимо закрыть при помощи CloseHandle().
    Зная дескриптор процесса, можно использовать метод Kill() для завершения его работы:
    завершить процесс и заставить его вернуть именно этот код
    $pobj->Kill($handle.$exitcode);
    Но дескрипторы процессов следует применять не только для того, чтобы завершать работу процесса. Например, можно использовать такие методы, как GetStatusQ, чтобы больше узнать о процессе. Вот пример кода, который выводит информацию о времени для заданного идентификатора процесса:
    use Win32::IProc qw(
    PROCESS_QUERY_INFORMATION INHERITED DIGITAL);
    $pobj = new Win32::IProc;
    $pobj->Open($ARGV[0],PROCESS_QUERY_INFORMATION, INHERITED. \$handle) or warn
    "Невозможно получить дескриптор:".$pobj->LastError()."\n";
    ft DIGITAL = время в понятном формате $pouj->
    GetStatus($handle,\$statusinfo.DIGITAL):
    $pobj->CloseHandle($handle):
    while ((Sprocname,$vaiue)=eacn %$statusinfo){
    print "$procname: $value\n":
    }
    В результате получается что-то приблизительно следующее:
    Kernelrii?L-: 00;00:22:442:270 Fxituate:
    ExitTime:
    CreationDate: 29/7/1999 CreationTime:
    17:09:28:100
    UserTime:
    00:00:11:566:632
    Теперь известно, когда процесс был запущен и сколько системного времени он занимает. Поля ExitDate и ExitTime пусты, поскольку процесс все еще активен. Вы могли бы спросить, как эти поля, в принципе, могут оказаться не пустыми, если для получения дескриптора нужно использовать идентификатор работающего процесса? На этот вопрос есть два ответа. Во-первых, можно получить дескриптор для работающего процесса, а затем заставить этот процесс завершиться до того, как вы закроете дескриптор. GetStatusO в таком случае вернет информацию о завершении работы для умершего процесса. Вторая возможность получить эту информацию- использовать метод Сгеate(), о котором мы пока еще не знаем.
    Create О
    позволяет запускать процессы из Win32: так же, как и в случае с Win32::Process. Если запустить процесс при помощи модуля, то объект процесса ($pobj), который до сих пор не обсуждался, будет содержать информацию о самом процессе и потоках. Обладая этой информацией, вы сможете делать любопытные вещи, например, манипулировать приоритетами потоков и окнами этого процесса. Мы не собираемся рассматривать эти возможности, но упомянуть о них следует, чтобы спокойно перейти к следующему модулю.

    Использование модуля Win32 Setupsup

    Использование модуля Win32::Setupsup

    Если упоминание о манипуляции окнами процесса, приведенное в конце предыдущего раздела, возбудило ваше любопытство, вам понравится наш следующий подход. На этот раз мы рассмотрим модуль Win32: :Setupsup Йена Хелберга (Jens Helberg). Он называется «Setup-sup», потому что первоначально был разработан для использования при установке программного обеспечения (при частом применении программы Setup.exe).
    Некоторые инсталляторы можно запускать в так называемом «тихом режиме» для полной автоматизации установки. В этом режиме они не задают никаких вопросов и не просят нажимать кнопки «ОК>>, освобождая администратора от необходимости сидеть нянькой при инсталляторе. Если такой режим не поддерживается механизмом установки (а подобных случаев очень много), это сильно усложняет жизнь системного администратора. Win32: .Setupsup помогает справиться с такими трудностями. Он позволяет найти информацию о работающих процессах и работать с ними (либо завершить эти процессы, если вы того пожелаете).
    Обратитесь к разделу «Информация о модулях из этой главы», чтобы узнать, как получить и установить модуль Win32: Используя Win32: :Setupsup, получить список выполняемых процессов очень просто. Вот слегка измененная версия примера, который можно увидеть в последнем разделе:
    use Win32::Setupsup:
    $machine = "";
    получаем список на текущей ма^/не
    Win32::Setupsup::GetProcessList
    ($machine, \@processlist. \@threaalist i a-die
    "Ошибка получения списка процессов:
    ". Win32 :Serupsjjp: :GetLa;:: Ем^г(). ", '
    pop(@processlist);
    # удалить фальшивую запись, всегда
    добавляемую к списку foreach Sprocesslist ((aprocesslist){
    $pid = $processlist->{pid}:
    $name = $processlist->{name};
    write; }
    format STDOUT_TOP =
    Process ID Process Name
    format STDOUT =
    @<««« @««««««««««««««
    $pid, $name
    Завершение процессов тоже очень просто:
    KillProcess($pid, Sexitvalule, Ssystemprocessflag) or die
    "Невозможно завершить процесс:
    ".Win32: :Setupsup: ,GetLast.Error()
    Два последних аргумента необязательны. Первый завершает процесс и, соответственно, устанавливает его код завершения (по умолчанию это 0). Второй аргумент позволяет вам завершать системные процессы (при условии, что у вас есть право Debug Prog rans).
    Но все это очень скучно. Мы можем перейти на другой уровень манипулирования процессами, взаимодействуя с окнами, которые откры ты запущенным процессом. Чтобы вывести список окон, доступных на рабочем столе, применим:
    Win32: :Setuosup: : EnuTiWiPdows( \awinduwiisi;) or die
    Win32: :Setuosp: :GetUstError( @windowlist
    теперь содержит список дескрипторов окон, которые выглядят как обычные числа, если их напечатать. Чтобы узнать больше о каждом окне, можно использовать несколько различных функций. Например, чтобы прочитать заголовки всех окон, воспользуемся функцией GetWindowText():
    use Win32::Setupsup;
    Win32::Setupsup::EnumWindOws(\@windowlist) or die
    "Ошибка получения списка процессов:
    ".Win32::Setupsup::GetLastError()."\n"
    foreach Swhandle (@windowlist){
    if (Win32::Setupsup::GetWindowText($whandle,\$text)){
    print "$whandle: $text","\n"; }
    else { warn
    "Невозможно получить текст для Swhandle" .
    Win32::Setupsup::Get LastError()."\n"; >
    }
    Вот небольшой отрывок получаемых данных:
    66130: chapter02 - Microsoft Word
    66184: Style
    194905150:
    66634: setupsup - WordPad
    65716: Fuel
    328754: DDE Server Window
    66652:
    66646:
    66632: OleMainThreadWndName
    Как видите, у некоторых окон есть заголовки, а у некоторых их нет. Внимательные читатели могли заметить в этом отрывке еще кое-что любопытное. Окно 66130 принадлежит сеансу Microsoft Word, запущенному в настоящий момент (в нем набиралась эта глава). Окно 66184 смутно напоминает название еще одного окна, связанного с Microsoft Word. Как мы можем узнать, действительно ли эти окна взаимосвязаны?
    В Win32: :Setupsup также есть функция EnumChildWindows(), которая позволяет вывести список дочерних окон для любого окна. Используем ее для вывода иерархии текущего окна:
    use Win32::Setupsup:
    # получаем список
    Win32: : Setupsup: : FnunWindows(^.®windowlist) or
    die "Ошибка получения процессов:
    ".Win32: 'Setiipsup: : Gf;: LastError(). "\n":
    превращаем список дескрипторов окон в хэш
    ЗАМЕЧАНИЕ: в результате преобразования
    элементами хэиа становятся обычные числа,
    а не дескрипторы окон. Некоторые функции,
    например GetWindowProperties
    (которую мы скоро рассмотрим),
    не могут использовать эти преобразованные v/c.ra.
    Будьте осторожны,
    for (is>windowlist){$windowlist{$_}++;
    }
    и проверяем наличие дочерних окон для каждого окна
    foreach $whandle (@windowlist){
    (Win32: :Setupsup: : EnumChildWindows($whandIe. \ichildren)){
    сохраняем отсортированный список дочерних окон
    $children{$whandle} = [sort {$a <=>$b} ©children]:
    tt удаляем все дочерние окна из главного хзша,
    в результате всех итераций %windowlist будет
    содержать только родительские окна без
    соответствующих дочерних
    foreach $child (@children){
    delete $wir.dewlist{$child};
    }
    }
    }
    обходим в цикле список родительских окон
    и тех окон, у которых нет дочерних,
    и рекурсивно печатаем дескриптор каждого
    окна и его дочерние окна (если они есть)
    foreach my $window (sort {$a <=> $b} keys %windowlist){ &printfamily($window,0);
    }
    выводим дескриптор заданного окна и его дочерних окон
    (рекурсивно) sub printfamily {
    начальное окно, насколько глубоко мы ушли по дереву?
    my($startwindow,Sievel) = @_;
    выводим дескриптор окна с соответствующим отступом
    print((" " х Slevel)."$startwindow\n").
    return unless (exists $children{$startwindow>):
    к дочерних окон не дело сделано
    противном случае мы должны
    roreach Schildwir.dow (@{
    }
    Есть еще одна функция, о которой надо сказать, перед тем как двигаться дальше: GetWindowPropertiesO. Функция GetWindowPropertiesO вмещает в себя остальные свойства окон. Например, используя GetWindowPropertiesO, можно получить идентификатор процесса, создавшего конкретное окно. Это разумно совместить с некоторыми из только что рассмотренных возможностей модуля Win32: : IProc.
    В документации к модулю Win32: :Setupsup есть список свойств, и к ним можно обратиться. Используем одно из них для написания очень простой программы, которая выведет размеры окна на экране. GetWindowPropertiesO принимает три аргумента: дескриптор окна, ссылку на массив, содержащий имена запрашиваемых свойств, и ссылку на хэш, где будут храниться результаты запроса. Вот какой код мы применим для этого:
    Win32: :Setupsup: :GetW:
    ndowProperties($ARGV[0], ["reef, "id"], \%info);
    print "\t" . $info{rect}{top} . "\n";
    print $info{rect}{left} . " -" . $ARGV[0] .
    "- " . $info{rect}{right} . "\n";
    print "\t" , $info{rect}{bottom} . "\n";
    Вывод получается несколько вычурным. Вот как выглядит вывод размеров (координат верхнего, левого, правого и нижнего края) окна с дескриптором 66180:
    154
    272 -66180- 903
    595
    GetWindowPropertiesO возвращает специальную структуру данных только для одного свойства rect. Все остальные будут представлены в хэше в виде обычных ключей и значений. Если вы не уверены в свойствах, возвращаемых Perl для конкретного окна, воспользуйтесь утилитой windowse, которую можно найти на http://greatis.virtualave.net/ products.htm.
    Разве теперь, когда мы знаем, как определить различные свойства окон, не было бы логично научиться изменять некоторые из этих свойств? Например, было бы полезно изменять заголовок окна. С такими возможностями мы могли бы создавать сценарии, использующие заголовок окна в качестве индикатора состояния: "Prestidigitation In Progress ... 32% complete" Чтобы внести эти изменения, достаточно одного вызова функции:
    Win32::Setupsup:SetWindow
    Text($handle,Stext);
    Свойство rect тоже можно установить таким образом. Следующие строки заставляют указанное окно переместиться в заданную позицию экрана:
    use Win32::Setupsup;
    $info{rect}{left} = 0;
    $info{rect}{nght} = 600;
    $info{rect}{top} = 10;
    $info{rect}{bottom}= 500;
    Win32::Setupsup::SetWindow
    Properties($ARGV[0], \%info);
    Самую впечатляющую функцию я приберег напоследок. При помощи SendKeysO можно послать произвольные комбинации клавиш любому окну на рабочем столе. Например:
    use Win32::Setupsup;
    $texttosend = "\\DN\\Low in trie gums";
    Win32::Setupsup::SendKeys($ARGV[0],Stexttosend, '',0);
    В результате, в указанное окно будет послан текст, предваряемый символом «курсор вниз». Аргументы SendKeysO очень просты: дескриптор окна, посылаемый текст, флаг, определяющий, нужно ли активизировать окно для каждого сочетания клавиш, и необязательный интервал между сочетаниями клавиш. Коды специальных символов, таких как «курсор вниз», окружаются обратными слэшами. Список допустимых кодов можно найти в документации к модулю.
    С помощью этого модуля мы попадаем на иной уровень управления процессами. Теперь мы можем удаленно управлять приложениями (и частями операционной системы), не взаимодействуя явно с этими приложениями. Нам не нужна поддержка командной строки или специальных API. У нас есть возможность писать сценарии для GUI, что очень полезно во множестве задач системного администрирования.

    Изучение структуры ядра

    Изучение структуры ядра

    Я упомянул эту возможность только для полноты картины. Можно написать программу, которая будет открывать устройство, подобное /dev/ kmem, и обращаться к структурам памяти ядра. Таким образом можно добраться до текущей таблицы процессов и прочитать ее. Учитывая, что сделать это трудно (разобраться вручную в сложной двоичной структуре), а полученный результат не будет обладать абсолютно никакой переносимостью (любое изменение даже версии операционной системы сделает, скорее всего, вашу программу неработоспособной), я настоятельно рекомендую не пользоваться такой возможностью.
    Тем, кто все же не прислушается к этому совету, придется вспомнить документацию по функциям pack(), unpack() и заголовочным файлам для вашего ядра. Откройте файл памяти ядра (часто это /dev/kmem), затем выполняйте read() и unpack(). Вам может понадобиться изучить исходники таких программ, как top (ищите на ftp://ftp.groupsys.com/ pub/top), выполняющих эти же задачи, используя большое количество кода на С. В следующем разделе мы рассмотрим слегка улучшенную версию этого метода.

    Отслеживание операций с файлами

    Попытка найти файлы, открытые другими пользователями, вернее всего сработает, если применять программу, работающую в командной строке, - nthandle Марка Руссиновича (Mark Russinovich), ее можно найти на http://www.sysinternals.com. Она позволяет показать все открытые дескрипторы на определенной системе. Вот как выглядит ее вывод:
    System pid: 2
    10: File C:\WINNT\SYSTEM32\CONFIG\SECURITY
    84: File C:\WINNT\SYSTEM32\CONFIG\SAM.LOG
    cc: File C:\WINNT\SYSTEM32\CONFIG\SYSrEM
    dO: File C:\WINNT\SYSTEM32\CONFIG\SECURITY.LOG
    d4: File C:\WINNT\SYSTEM32\CONFIG\DEFAULT
    e8: File C:\WINNT\SYSTEM32\CONFIG\SYSTEM.ALT
    fc: File C:\WINNT\SYSTEM32\CONFIG4SOFTWARE.LOG
    118: File C:\WINNT\SYSTEM32\CONFIG\SAM
    128: File C:\pagefile.sys
    134: File C:\WINNT\SYSTEM32\CONFIG\DEFAULT. LOG
    154: File С:\WNNT\3YSTEM32\CON'FIG;'SOFTWARE
    1bO: File \3evice\NafiedPipe\
    294: File C:\WINNT\PROFILES\Adnirustrator\ntLSer.aa-.; OG
    2a4: File C:\WINNT\PROFILES\AdminisTrator\NTUSEH.DAT
    SMSS.EXE pid: 27 (NT AUTHORITY:SYSTEM)
    4: Section С:\WINNT\SYSTEM32\5MSS.EXE
    c: File С'\WINNT
    28: File C:\WINNT\SYSTEM
    Можно также запросить информацию по конкретным файлам и каталогам:
    > nthandle c:\temp
    Handle V1.11
    Copyright (С) 1997 Mark Russinovich
    http://www.sysinternals.com
    WINWORD.EXE pid: 652
    C:\TEMP\~DFF2B3.tmp WINWORD.EXE pid: 652
    C:\TEMP\~DFA773.tmp WINWORD.EXE pid: 652
    C:\TEMP\~DF9l3E.tmp
    Программа nthandle позволяет получить эту информацию по конкретному процессу при помощи ключа -р.
    Использовать ее из Perl очень просто, поэтому не будем приводить примеры. Вместо этого посмотрим на подобную, но более интересную операцию - аудит.
    NT/2000 позволяет эффективно отслеживать изменения в файлах, каталогах или иерархии каталогов. Вы могли бы учитывать постоянное повторение операции stat() над нужным объектом, но это потребовало бы слишком больших затрат процессорного времени. В NT/2000 отслеживание изменений можно поручить операционной системе.
    Относительно безболезненно эту работу выполняют два модуля: Win32: :ChangeNotify Кристофера Мадсена (Christopher J. Madsen) и Win32: :AdvNotify Амина Мюлей Рамдана (Amine Moulay Ramdane). В примерах этого раздела будет использоваться второй, т. к. он более гибкий.
    Работа с модулем Win32: : AdvNotify- это многошаговый процесс.
    На следующем шаге нужно создать следящий поток (monitoring thread) для интересующего нас каталога. Win32: :AdvNotify позволяет следить сразу за набором каталогов, для этого необходимо лишь создать несколько потоков. Мы же будем следить только за одним каталогом:
    Sthread = $aob]->StartThread(Directory => 'C:\terr.c'.
    Filter => All, WatchSubtree -> 0) or die "Невозможно начать поток\п":
    Первый параметр этого метода говорит сам за себя; рассмотрим остальные.
    Установив Filter в одно из приведенных значений (табл. 4.1) или в их комбинацию (SETTING 1 | SETTING2 | SETTINGS. ..), можно следить за различными типами изменений.


    Отслеживание операций в Unix

    Отслеживание операций в Unix

    Для отслеживания операций с файлами и сетью в Unix можно использовать один и тот же подход. Это один из тех редких случаев, когда вызов внешней программы намного предпочтительней. Вик Абель (Vic Abell) преподнес чудесный подарок системным администраторам, написав программу Isof (LiSt Open Files), которую можно найти на ftp:// vic.cc.purdue.edu/pub/tools/unix/lsof. Isof позволяет отобразить подробную информацию об открытых в настоящий момент файлах и сетевых соединениях на Unix-машине. По-настоящему удивительной эту программу делает ее переносимость. Последняя версия программы (на момент написания этой книги) работает по крайней мере на 18 видах Unix и поддерживает различные версии этих операционных систем.
    Вот как выглядит вывод Isof для одного из запущенного мной процесса. Isof выводит очень длинные строки, поэтому, чтобы сделать информацию более читаемой, после каждой строки вывода я добавил пустую строку.
    COMMAND PID USER FO TYPE DEVICE SIZE/OFF NODE NAME
    netscape 21065 dno cwd VOIR 172,289"; 8192 12129 /-OTie
    netscape 21065 dnb txt VREG 172,1246 1438236Д 656749
    /net/ arch-solans (fileserver-./vol/systems/arch-solaris)
    netscape 21065 dnb txt VREG 32,6 54656 35172
    /usr (,/dev/ dsk/cOtOdOs6)
    netscape 21065 dnb txt VREG 32;6 146740 6321
    /ubr/lro/ libelf.so.1
    netscape 21065 dnb txt VREG 32,6 69292 102611
    /usr (/dev/ dsk/cOtOdOs6)
    netscape 21065 dnb txt VREG 32,6 21376 79751
    /usr/iib/ locale/en_US/en_US.so.1
    netscape 21065 dnb txt VREG 32,6 19304 5804
    /usrЛib/ libmp.so.2
    netscape 21065 dnb txt VREG 32,6 98284 22860
    usr/onenwi:' lib/libICE.so.6
    netscape 21065 dnb txt VREG 32,6 46576 22891
    /usr/opftrwiv lib/libSM.so.6
    netscape 21065 drib txt VREG 32.6 1014020 5810
    /!jsr/::.t; libc.so.1
    netscape 21065 dnb txt VREG 32.6 105788 5849
    /usr/ln; libm.so.1
    netscape 21065 dnb txt VREG 32,6 721924 5806
    netscape 21065 ar.b txt VREG 32 6 156196 5774
    netscape 21065 ri^t 0. VCHP 2-1.3 Ot73 5853 > .
    pseudo/bls@C:3-> ttcoirpat ->lcter"b>:neii->pis netscape 21065 dnb 3u VCHR 13,12 oto 5821
    /devices/ pseudo/mm@0:zero
    netscape 21065 dnb 7u FIFO Ox6034d264 C;1 47151 PIPE-> Ox6034d1eO
    netscape 21065 dnb 8u met Ox6084cb68 Oxfb210er, TCP host. cos.
    ne^.. edu:46575->host2.ccs,neu. edu:6000 (ESTABLISHED)
    netscape 21065 dnb 29u met 0x60642848 Ot215868 TCP nost, ccs. re.. edu:46758->
    www.mind-bright.se:80 (CLOSE_ WAIT)
    Из этого примера можно понять, насколько мощна эта команда. Мы можем увидеть текущий рабочий каталог (VDIR), обычные файлы (VREG), символьные устройства (VCHR), каналы (FIFO) и сетевые соединения (inet), открытые этим процессом.
    Самый простой способ применить программу Isof из Perl - вызвать ее в специальном режиме «field» (-F). В этом режиме вывод программы делится на специальным образом отмеченные и разделенные поля, вместо использования колонок в стиле ps. Это позволяет надежно проанализировать и распознать вывод.
    У этого способа вывода результатов есть одна особенность. Вывод организован в виде «наборов процессов» (process sets) и «наборов файлов» (file sets), как их называет автор. Набор процессов - это набор полей, относящихся к одному процессу; набор файлов - это подобный же набор для файла. Все приобретет больший смысл, если включить режим разбивки на поля с параметром 0. В этом случае поля будут разделены символом NUL (ASCII 0), а наборы - символом NL (ASCII 12). Вот как будет выглядеть предыдущий вариант вывода команды, если использовать режим разбивки на поля (NUL представлен в виде символов ~@):
    p21065~@cnetscape~@u6700"@Ldnb~@
    fcwd"@a ~@1 "@tVDIR"@DOx2bOOb4b"@s8192"@i12129"@n/home/dnb"@
    ftxt"@a ~@1 >tVREG"@DOx2b004de"@s14382364"@i656749"@n/net/arch-solaris (fileserver:/vol/systems/arch-solaris)"@
    ftxt~@a "@1 ~@tVREG"(5)DOx800006~@s54656~@i35172~@n/usr (/dev/dsk/cOtOdOs6)"@ ftxt"@a "@1 "@tVREG"@OOx800006"@s146740"@i6321"@n/usr/lib/libelf.so.1"@ ftxt"@a "@1 "@tVREG"@DOx800006"@s40184"@i6089"ian/usr (/dev/dsk/cOtOdOs6)"@ ftxt"(5>a "@1 "@tVREG"@DOx800006"@s69292"@i102611"@n/usr (/dev/dsk/cOtOdOs6)"@
    ftxt"@a ~@1 •@tVREG"@DOx800006"@s21376"@i79751"@n/usr/lib/locale/en_US/ en_US.so.1"@
    ftxt"@a ~@1 "@tVREG"№Ox800006"@s19304"@i5804"@n/usr/lib/libmp. so. 2"@
    ftxt"@a "@1 ~@tVREG"@DOx800006"@s98284"@i22860"@n/L>sr/openwin/lib/ libICE.so.6"@ ftxt~@a ~@1 "@tVREG"@DOx800006"@s46576"@i22891"@n/usr/openwin/lib/ libSM.so.6"<°>
    ftxt"@a ~@1 "@tVREG"@DOx800006"@s1014020"@i5810"@n/usr/lib/libc.so.1"@ ftxt~@a ~@1 ~@tVREG~№Ox800006~@s105788~@i5849~n/usr/lib/libm. so. 1"@ ftxt"@a "@1 "§tVREG"@DOx800006'@s721924"@i5806"@n/usr/lib/libnsl.so.Г@
    ftxt"@a "@1 "@tVREG"@DOx800006"§s166196"@i5774"@n/usr/lib/ld.so.Г@ fO"@au~@l "@tVCHR"@DOx600003"iao73~(ai5863"(an/devices/pseudo/ pts@0:3->ttcompat->ldterm->ptem->pts"@
    f3"@au"@l "@tVCHR"@DOx34000c"@oO"§i5821"@n/devices/pseudo/mm(s)0:zero"(8i f7"@au"@l "@tFIFO"@dOx6034d264"@o1"@i47151"@nPIPE->Ox6034d1eO"@
    f8"@au"@l "@tinet"@dOx6084cb68"@o270380692"(9>PTCP''@nhost.ccs.neu.edu:46575-> host2.cos.neu.edu: 6000"@TST=ESTABLISHED~@
    f29"@au"@l "@tinet"@dOx60642848"@o215868~@PTCP"@nhost.ccs.neu.edu:46758-> www.mindbright.se: 80"(a>TST=CLOSE_WAIT~@
    Давайте разберемся с этими данными. Первая строка - это набор процессов (это можно понять по первому символу р):
    p21065~(acnetscape~@u6700~@Ldnb~(a
    Каждое поле начинается с буквы, определяющей его содержимое (о -идентификатор процесса (pid), с - команда, и - идентификатор пользователя (uid) и L - имя пользователя (login)), и заканчивается символом разделителя. Все поля в строке создают набор процесса. Все последующие строки вплоть до очередного набора процесса описывают открытые файлы/сетевые соединения процесса, описываемого своим набором.
    Давайте используем этот режим. Если необходимо вывести список всех открытых файлов и процессов, обращающихся к ним, можно применить такую программу:
    use Text::Wrap;
    Slsofexec = "/usr/local/Din/lsof"; 8 путь к Isof
    8 режим (F)ield, разделитель NUL (0), показывать (L)ogin,
    8 тип файла (t)ype и имя файла (n)ame
    $lsofflag = "-FLOtn";
    open(LSOF,"$lsofexec $lsofflag[") or
    die "Невозможно запустить $lsofexec:$!\n";
    while(){
    8 работаем с набором процесса if (substr($_,0,1) eq "p"){
    ($pid,$login) = split(/\0/);
    $pid = substr($pid,1,length(Spid)); }
    # работаем с набором файла.
    # Замечание: мы интересуемся только обычными файлами
    if (substr($_,0,5) eq "tVREG"){
    ($type,$pathname) = split(/\0/);
    # процесс может дважды открыть один и тот же файл,
    # поэтому мы должны убедиться, что запишем его
    # только один раз
    next if ($seen{$pathname} eq $pid); $seen{$pathname} = $pid;
    Spathname = substr($pathname,1,length($pathname));
    push(@{$paths{$pathname}},$pid);
    }
    close(LSOF);
    for (sort keys %paths){
    print "$_:\n";
    print wrap("\t","\t",join(" ",@{$paths{$_}})),"\n";
    }
    В этом случае Isof будет показывать только некоторые из полей. Можно обойти в цикле весь вывод, собирая имена файлов и идентификаторы процессов в хэш списков. Когда будут обработаны все выведенные данные, следует ввести имена файлов в виде отформатированного списка процессов (спасибо Дэвиду Шарноффу (David Muir Sharnof f) за модуль Text: :Wrap):
    /usr (/dev/dsk/cOtOdOs6):
    115 117 128 145 150 152 167 171 184 191 200 222 232 238
    247 251 276 285 286 292 293 296 297 298 4244 4709 4991
    4993 14697 20946 21065 24530 25080 27266 27603
    /usr/bin/tcsh:
    4246 4249 5159 14699 20949
    /usr/bin/zsh:
    24532 25082 27292 27564
    /usr/dt/lib/libXm.so.S: 21065 21080
    /usr/lib/ld.so.1:
    115 117 128 145 150 152 167 171 184 191 200 222 232 238
    247 251 267 276 285 286 292 293 296 297 298 4244 4246 4249 4709 4991
    4993 5159 14697 14699 20946 20949 21065
    21080 24530 24532 25080 25082 25947 27266 27273 27291
    27292 27306 27307 27308 27563 27564 27603
    /usr/lib/libc.so.1:
    267 4244 4246 4249 4991 4993 5159 14697 14699 20949
    21065 21080 24530 24532 25080 25082 25947 27273 27291
    27292 27306 27307 27308 27563 27564 27603
    Чтобы показать последний код, относящийся к отслеживанию операций с файлами и сетью в Unix, вернемся к поиску запущенных IRC-po-ботов из приведенного ранее примера. Существует более надежный способ найти такие процессы-демоны, чем изучение таблицы процессов. Пользователь может скрыть имя робота, переименовав исполняемый файл, но для того чтобы спрятать сетевое соединение, ему придется очень хорошо потрудиться. В большинстве случаев это соединение с сервером на портах 6660-7000. Программа Isof позволяет без труда отыскивать такие процессы:
    Slsofexec = "/usr/local/bin/lsof" Slsofflag = "-FLOc -iTCP: 6660-7000";
    ft это срез хэша. используемый для предварительной загрузки
    и таблицы хэшей, существование этих ключей мы будем проверять
    и позже. Обычно это записывается так:
    ft %approvedclients = ("ircll" => undef, "xirc" => undef, ...);
    ft (но такой вариант - отличная идея, воплощенная Марком-
    ft Джейсоном Доминусом(МагК-иа50п Dominus))
    @>approvedclients{"ircH" , "xirc" , "pirc"} = ();
    open(LSOF, "$lsofexec $lsofflag|") or die "Невозможно запустить $lsofexec:$! \n" ;
    while(){
    ($pid,$command,$login) = /p(\d+)\000
    c(.+)\000 L(\w+)\000/x;
    warn "$login использует неразрешенный клиент с именем
    $conimand (pid $pid)l\n" unless (exisTs $approvedclients{$command});
    close(LSOF);
    Это самая простая проверка из всех возможных. Она позволяет отловить тех, кто догадается переименовать eggdrop б pine или -tcsh, и тем более тех, кто даже не попытается спрятать своего робота. Тем не менее, она подвержена тому же недостатку, что и предыдущий вариант тестирования. Если пользователь достаточно умен, он может переименовать робота во что-то из списка «разрешенных клиентов». Чтобы продолжить игру в кошки-мышки, можно предпринять еще как минимум два шага:
  • Запустить Isof для проверки того, что файл, открытый для данной исполняемой программы, известен как тот, который мог быть открыт этой программой, а не произвольный файл из файловой системы.
  • Применить методы управления процессами для проверки того, что пользователь запускает программу из существующего интерпретатора. Если это единственный процесс, запущенный пользователем (т. е. если пользователь оставил его, а сам завершил работу), он, вероятно, является демоном, а значит и роботом.
  • Игра в кошки-мышки привела нас к точке, позволяющей завершить эту главу. В главе 3 мы говорили, что пользователи совершенно непредсказуемы. Они делают такие вещи, которые системные администраторы не могут даже предвидеть. Известно старое изречение: «Защиты от дураков не существует, потому что дураки изобретательны». С этим фактом придется считаться при программировании на Perl для администрирования пользователей. В итоге вы будете писать более надежные программы. Когда одна из ваших программ начнет «ругаться» из-за того, что пользователь сделал что-то неожиданное, вы сможете спокойно сидеть и наслаждаться человеческой изобретательностью.

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

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

    http://pudget.net/macperl -
    домашняя страница модулей Криса Нан-дора (Chris Nandor). Нандор один из самых активных разработчиков модулей MacPerl (и соавтор книги, ссылка на которую приведена ниже). http://www.activestate.com/ support /mailing_lists.htm.
    Здесь находятся списки рассылки Perl-Win32 Admin и Perl-Win32-Users. Оба списка и их архивы - просто бесценные ресурсы для программистов Win32. http://www.microsoft.com/management
    - домашняя страница всех технологий управления Microsoft, включая WMI. http://www.sysinternals.com -
    домашняя страница программы nthan-dle (на этом сайте она называется просто Handle) и многих других полезных утилит для NT/2000. На родственном сайте http:// www.winternals.com продаются отличные коммерческие утилиты. «MacPerl:Power and Ease»,
    Vicki Brown, Chris Nandor (Prime Time Freeware, 1998) - лучшая книга о модулях для MacPerl. Стоит также обратить внимание на веб-сайт издателя http://www.macperl.com. http://www.dmtf.org-
    домашняя страница Distributed Management Task Force и просто хороший источник информации по WBEM. http://www.mspress.com -
    издатели Microsoft NT Resource Kit. Можно зарегистрироваться и получить доступ к последним утилитам из RK.

    Параметры Filter в Win32 AdvNotifу

    Таблица 4.1. Параметры Filter в Win32::AdvNotifу

    Параметр Отмечает
    FILE_NAME Создание, удаление, переименование файла(ов)
    DIR_NAME Создание или удаление каталога(ов)
    ATTRIBUTES Изменение атрибутов любого каталога
    SIZE Изменение размера любого файла
    LAST_ WRITE Изменение даты модификации файла(ов)
    CREATION Изменение даты создания файла(ов)
    SECURITY Изменение информации безопасности (ACL и пр.) файла(ов)
    Значение АИ из приведенного примера- это всего лишь постоянная, объединяющая все варианты выбора. Если не указать параметр Filter при вызове метода, то по умолчанию будет использоваться АИ. Параметр WatchSubtree определяет, необходимо ли следить только за указанным каталогом или за каталогом и всеми его подкаталогами.
    StartThreadO создает поток, но проверка начинается только после того, как поступает распоряжение об этом:
    $thread->EnableWatch() or die
    "Невозможно начать наблюдение\п";
    Существует также функция OisableWatch(), которую необходимо использовать в программе для завершения проверки.
    Мы следим за нужным объектом, но как узнать, изменилось ли что-нибудь? Надо придумать что-то, что позволило бы потоку сообщить нам об изменениях, за которыми мы наблюдаем. Здесь тот же подход, что и в главе 9 «Журналы» при обсуждении сетевых сокетов. Обычно следует вызывать функции, которые заблокированы до тех пор, пока ничего не происходит:
    while($thread->Wait(INFINITE)){
    print "Что-то изменилось1\п":
    last if ($changes++ == 5):
    }
    Этот цикл while() вызовет метод Wait() для нашего потока. До тех пор пока потоку нечего сообщить, вызов будет заблокирован. Обычно Wair() принимает в качестве параметра число миллисекунд, равное времени ожидания. Мы же передаем специальное значение, которое соответствует «бесконечному ожиданию». Когда Wait() возвращает значение, следует вывести сообщение и ждать дальше, если только уже не были замечены пять других изменений. Теперь можно закончить:
    $thread->Terminnte() undef $aobj;
    Наша программа пока еще не очень полезна. Нам известно, что что-то изменилось, но мы не знаем ни что изменилось, ни как это произошло. Чтобы исправить эту ситуацию, изменим тело цикла wniie() и добавим определение формата:
    while($thread->Wait(INFINITE)){
    while ($thread->Read(\@status)){ foreach Sevent (@status){
    Sfilename = $event->{FileName); $time = $event->{DateTime};
    Saction = $ActionName{$event->{Action}}; write; } } }
    format STDOUT =
    @««««««««« @««««««««« @«««««««««
    Sfilename,$time,Saction
    format STDOUT_TOP =
    File Name Date Action
    Основное изменение- это добавление метода Read(). Он получает информацию об изменении и заполняет элементы списка ©status ссылками на хэш. Каждая ссылка указывает на анонимный хэш, который выглядит примерно так:
    {'FileName' => "GLF2425.TMP',
    'DateTime' => '11/08/1999 06:23:25р',
    'Directory' => 'C:\temp', 'Action' => 3 }
    Для каждого изменения могут произойти несколько событий, отсюда и необходимость вызывать Read() в цикле while(). Если соответствующим образом разыменовать содержимое этих хэшей и применить к ним форматирование, то получатся примерно такие данные:
    File Name Date Acti01
    "DF400E.tmp 11/08/1999 07:29:56p
    FILE_.ACTION_PEMO\,'F.D
    "DF6C5C. fnp 11/08/1999 07'29'56p
    F1LF_AC1ION_ADDED
    ~OF6E66.tmp 11/08/1999 07:29:56р
    FILE_ACTION_ADOED
    ~DF6E5C.tmp 11/08/1999 07:29:56р
    FILE_ACriON_REMOVEO
    К сожалению, отслеживание операций с сетью в NT/2000 впечатляет намного меньше. В идеале, как администратор вы хотели бы знать, какой процесс (а, следовательно, какой пользователь) открыл сетевой порт. Печально, но я не знаю ни одного модуля и ни одного инструмента, которые могли бы предоставить такую информацию. Существует один коммерческий инструмент, работающий в командной строке, под названием TCPVstat, который может показать связь процессов с использованием сети. TCPVstat можно найти в пакете TCPView Professional Edition, который доступен на http://www.winternals.com.
    Если использовать только некоммерческие инструменты, то придется иметь дело лишь со списком сетевых портов, открытых в настоящее время. Для этого следует применять другой модуль Рамдана -Win32: :1рНе1р. Вот как выглядит код, печатающий нужную информацию:
    use Win32: -.IpHelp;
    # замечание: в данном случае регистр "IpHelp"
    имеет значение' my $iobj = new Win32::IpHelp;
    # заполняем список хэшем хэшей $iobj->GetTcpTable(\(a>table,1);
    foreach Sentry (@table){
    print $entry->{LocalIP}-> $entry->{LocalPort}->{ValLie}. " -> ";
    print $entry->{fiemoteIP}->{Value} . ":" .
    Sentry->{RemotePort}->{Value}."\n";
    }
    Посмотрим, как можно сделать то же самое в Unix.

    Мы вкратце рассмотрим четыре различных

    Управление процессами в NT/2000

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

    Используем Microsoft Resource Kit

    В главе 3 «Учетные записи пользователей» упоминалось, что NT Resource Kit является отличным источником для сценариев и информации. Из этого пакета будут использоваться две программы: pulist.exe и kill.exe. Первая выводит список процессов, вторая- «убивает» их. В этом пакете есть еще одна утилита tlist.exe, похожая на pulist.exe, которая может вывести все процессы в списке, удобном для чтения, но ей не достает некоторых возможностей pulist.exe. Например, pulist.exe может вывести список процессов не только текущего, но и другого компьютера.

    Вот фрагмент из вывода командыpulist:

    Process PID User

    TAPISRV.EXE 119 NT AUTHORITY\SYSTEM

    TpChrSrv.exe 125 NT AUTHORITY\SYSTEM

    RASMAN.EXE 131 NT AUTHORITY\SYSTEM

    mstask.exe 137 NT AUTHORITY\SYSTEM

    mxserver.exe 147 NT AUTHORITY\SYSTEM

    PSTORES.EXE 154 NT AUTHORITY\SYSTEM

    NDDEAGNT.EXE 46 OMPHALOSKEPSIS\Administrator

    explorer.exe 179 OMPHAlOSKEPSIS\Administrator

    SYSTRAY.EXE 74 OMPHALOSKEPSIS\Administrator

    cardview.exe 184 OMPHAlOSKEPSIS\Administrator

    ltmsg.exe 167 OMPHALOSKEPSIS\Administrator

    daemon.exe 185 OMPHALOSKEPSIS\Adrdinistrator

    Применять pulist.exe из Perl очень просто. Вот один из способов:

    $pulistexe = "
    \\bin\\PULIST.EXE":

    местоположение программы open

    (PULIST. "$pulistexe|")

    "Невозможно выполнить Spulistexe.S! ''

    scalar : и удаляем первую строку заголовка

    while(defined($_=)){

    print "$pranie.$pid:$puser\n":

    close(PULIST):

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

    Программа kill.exe использует два различных способа завершения работы процессов. Один из них - это так называемая «вежливая смерть»: kill.exe попросит подтверждения на завершение работы процесса. Но если добавить к командной строке ключ //, действия kill.exe /f будут скорее напоминать манеру истинных Perl-функций - он завершит работу процесса с особенной предвзятостью.

    Управление процессами в Unix Стратегии

    Во всех современных вариантах операционной системы Unix есть команда ps, применяемая для получения списка запущенных процессов. Однако в каждом конкретном случае она расположена в различных местах, а аргументы командной строки, которые она принимает, тоже не совпадают. Отсюда и проблема с ее применением: она недостаточно переносима.
    Еще более неприятная проблема - это сложность анализа вывода (который тоже отличается в различных версиях). Вот как выглядит вывод команды ps на машине с SunOS:
    USER PID %CPU %MEM SZ RSS TT STAT START TIME COMMAND
    dnb 385 0.0 0.0 268 0 p4 IW Jul 2 0:00 /bin/zsh
    drib 24103 0.0 2.610504 1092 p3 S Aug 10 35:49 emacs
    dnb 389 0.0 2.5 3604 1044 p4 S Jul 2 60:16 emacs
    remy 15396 0.0 0.0 252 0 p9 IW Jul 7 0:01 -zsn (zsh)
    sys 393 0.0 0.0 28 0 7 IW Jul 2 0: 02 in, ider.td
    dnb 29488 0.0 0.0 68 0 p5 IW 20:15 0:00 scree:
    dnb 29544 0.0 0.4 24 148 p7 R 20:39 0:00 less
    dnb 5707 0.0 0.0 260 0 p6 IW Jul 24 0,00 -)
    root 28766 0,0 0.0 244 0 "> IW 13:20 0:00 - 0 xd
    Обратите внимание на третью строку. Два столбца в ней слились вместе и при анализе вывода разобраться в этом будет непросто. Нет, это возможно, но просто действует на нервы. В некоторых вариантах Unixj дела обстоят лучше, но это обстоятельство следует учитывать. Программа на Perl, применяемая в этом случае, прямолинейна: в ней используются орем() для запуска ps, whilei

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

    Файлы узлов Первый подход используемый

    Теперь можно заняться более интересным делом - генерированием файлов узлов. Пусть у нас есть следующий файл базы данных для всех узлов в сети:
    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 () {
    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.


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

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

    Модуль Идентификатор на 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


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

    Использование 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++), написанная программа может выполняться так же быстро, как и вызов внешней программы.
  • Потенциально, это переносимая программа - все зависит только от того, что именно сделал автор модуля. Везде, где можно установить модуль, программа будет работать.
  • Как и в первом рассмотренном случае, написать программу можно быстро и просто, если кто-то другой сделает за вас всю работу, происходящую «за сценой». Вам не нужно знать, как работает модуль; вы только должны знать, как его применять.
  • Код используется повторно. Нет необходимости каждый раз изобретать велосипед.
  • Недостатки данного подхода:
  • Снова появилась зависимость. На этот раз необходимо убедиться, что модуль доступен вашей программе. Приходится поверить, что автор модуля проделал хорошую работу.
  • Может не существовать подходящего вам модуля или он может не запуститься на выбранной вами операционной системе.
  • В большинстве случаев я предпочитаю использовать уже существующие модули. Тем не менее, для выполнения поставленной задачи подойдет любой подход. Существует несколько способов сделать одно и то же - значит, вперед, действуйте!


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

    Использование 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 () {
    « игнорировать, пока не дойдем до "Name: next until (/"Name:/);
    следующая строка - это ответ Address: chomp($results{$server} = );
    # удаляем имя поля
    die "Ошибка, вывода nslookup \n" unless /Address/;
    $results{$server} =" s/Addfess(es)?:\s+//;
    все, с nslookup мы закончили last;
    }
    close(NSLOOK);
    }
    Преимущества такого подхода:
  • Это короткая программа, которую можно быстро написать (вероятно, ее даже можно построчно перевести из настоящего сценария командного интерпретатора).
  • Нет необходимости писать запутанный код для работы с сетью.
  • Применяется подход в стиле Unix, когда язык общего назначения используется для соединения нескольких маленьких специальных программ, выполняющих требуемые задачи в связке, вместо того чтобы писать большую монолитную программу.
  • Это может оказаться единственным выходом, если нельзя написать программу для взаимодействия клиента и сервера на Perl; в частности, если вам нужно обратиться к серверу, который требует использования особого клиента без каких-либо альтернатив.
  • Недостатки такого подхода:
  • Появляется зависимость от другой программы за пределами сценария. А если эта программа недоступна? Что делать, если изменится формат вывода данной программы?
  • Это работает медленнее. Каждый раз, для того чтобы выполнить запрос, необходимо запустить новый процесс. От подобной нагрузки можно избавиться, если установить двунаправленный канал с процессом nslookup, который открыт все то время, когда он нужен. Правда, это потребует несколько большего опыта программирования, но это стоит сделать, если вы решите использовать и улучшать подобный подход.
  • У вас меньше контроля. Во всех деталях реализации приходится полагаться на милость внешней программы. Например, в данном случае nslookup (если быть более точным, то библиотека разыменования, которую использует nslookup) обрабатывает тайм-ауты сервера, повторные попытки запросов и дописывает списки поисков доменов.


  • MIS NIS+ и WINS Разработчики из

    В состав операционной системы 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.

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

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

    Напечатать нужные части - это только начало. Значительное преимущество употребления отдельной базы данных, которая преобразовывается в другую форму, состоит в возможности выполнения проверки ошибок во время преобразования. Раньше уже отмечалось, что подобный контроль позволяет избавиться от проблем, вызванных такими мелкими неприятностями, как опечатки, еще до того, как данные будут распространены. Вот как будет выглядеть предыдущий пример, если в него добавить проверку опечаток:
    Sdatafile ="./dataoase":
    Srecorcsep = "- = -\;n:
    # по одной записи за один раз
    print "#\n\# host file - GENERATED BY $0\n# DO NOT EDIT BY HAND!\n#\n";
    while () {
    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-> $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 поддержит ваши профессиональные и эстетические стремления.

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

    Проверка работы 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( ).

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

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

    Если вы «продвинутый системный администратор», вы можете решить, что вызывать внешнюю программу не следует. Вы можете захотеть реализовать запросы к 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) {
    # записываем значения в
    %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) = ($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 знать было не нужно).
  • Вам, вероятно, придется самостоятельно справиться с различиями между операционными системами (в предыдущем подходе они были скрыты благодаря тому, что эту работу выполнил автор внешней программы).


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

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

    «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) Несмотря

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

    Службы имен TCP/IP

    Службы имен TCP/IP

  • Файлы, узлов
  • NIS, NIS+ u WINS
  • Служба доменных имен (DNS)
  • Информация о модулях из этой главы
  • Рекомендуемая дополнительная литература

  • В настоящее время большая часть «разговоров» между компьютерами происходит по протоколу управления передачей (Transmission Control Protocol), который, в свою очередь, выполняется на более низком уровне, называемом межсетевым протоколом (Internet Protocol). Эти два протокола обычно объединяются в один акроним TCP/IP. Каждой машине в TCP/IP-сети должен быть присвоен хотя бы один уникальный численный идентификатор, называемый IP-адресом. IP-адреса обычно записываются в формате NNN.NNN.N.N, например, 192.168.1.9.
    В то время как машины без затруднений обращаются друг к другу при помощи строк чисел, разделенных точками, большинство людей от этой идеи не в восторге. TCP/IP потерпел бы полное фиаско как протокол, если бы пользователям пришлось запоминать уникальные последовательности из 12 цифр для каждой машины, к которой они обращаются. Необходимо было придумать механизмы для преобразования IP-адресов в имена, понятные людям.
    В этой главе рассказано об эволюции сетевых служб имен, позволяющих обращаться к данным на сайте www.oog.org, а не 192.168.1.9, а также о том, что происходит за сценой. По ходу дела мы будем сопровождать примеры из истории изрядным количеством полезных советов о том, как Perl помогает работать с этой важной частью любой сетевой инфраструктуры.

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

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

    Конфигурационные файлы 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 () {
    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 () {
    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 () {
    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.

    Массив возвращенный функцией split ()

    Таблица 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\п...), который следует просто присвоить хэшу. После создания хэша можно напечатать нужные нам части.

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

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

    Перед тем как перейти к следующему способу преобразования 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. В крайнем случае, всегда можно вернуться к предыдущим версиям, если какие-либо изменения приведут сеть в неработоспособное состояние.

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

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

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

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

    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.

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

    ADSI (Интерфейсы служб активных

    ADSI можно считать оболочкой вокруг произвольной службы каталогов, действующей в рамках ADSL В среде интерфейса есть провайдеры (providers), которые представляют собой реализации ADSI для LDAP, WinNT4.0 и Novell Directory Service. Говоря на «языке» ADSI, каждая из этих служб каталогов (WinNT не является службой каталогов) и домены данных (data domains) называются пространством имен (namespaces). ADSI предоставляет единый способ запроса и изменения данных, найденных в этих пространствах имен.
    Чтобы понять ADSI, необходимо иметь представление об объектной модели компонентов COM (Component Object Model), на которой построен интерфейс ADSL О модели СОМ существует много книг, но следует остановится на таких ключевых понятиях:
  • Все, с чем работают в СОМ, - это объекты (objects).
  • Объекты имеют интерфейсы (interfaces), обеспечивающие набор методов (methods), применяемых для взаимодействия с этими объектами. Из Perl можно использовать методы, предоставляемые или наследуемые от интерфейса под названием IDispatch. К счастью, большинство методов ADSI, предоставляемых интерфейсами ADSI и их производными (например, lADsUser, lADsComputer, lADsPrint-Queue), унаследованы от IDispatch.
  • Значения, инкапсулируемые объектом, запрашиваемые и изменяемые посредством этих методов, называются свойствами (properties). В этой главе будут рассматриваться два типа значений: свойства, определяемые интерфейсом (interface-definedproperties), и свойства, определяемые схемой (schema-defined properties). Иными словами, первые будут определяться как часть интерфейса, а вторые - в объекте схемы. Подробнее об этом говорится чуть ниже. Если в данной главе не будут явно упоминаться «свойства схемы», значит, подразумевать следует свойства интерфейса.
  • Все это относится к стандартным понятиям объектно-ориентированного программирования. Сложности начинаются, когда сталкиваются терминологии ADSI/COM и других объектно-ориентированных миров, подобных LDAP.
    Например, в ADSI рассматривается два различных типа объектов: лист (leaf) и контейнер (container). Объект-лист содержит данные; объект-контейнер содержит другие объекты, т. е. является для них родительским (parent). В LDAP самыми точными определениями для этих терминов были бы «элемент» и «точка ветвления». С одной стороны, мы говорим об объектах со свойствами, а с другой - об элементах с атрибутами. Как разобраться с подобными разногласиями, если учесть, что оба названия определяют одни и те же данные?
    Вот как можно это понимать: в действительности, сервер LDAP обеспечивает доступ к дереву элементов и связанных с ними атрибутов. Когда интерфейс ADSI используется вместо LDAP для получения элемента дерева, ADSI вытягивает элемент из сервера LDAP, заворачивает его в несколько слоев блестящей оберточной бумаги и передает вам в качестве СОМ-объекта. Для получения содержимого этой посылки следует применять нужные методы, которые теперь называются «свойствами». Если внести изменения в свойства данного объекта, можно вернуть объект ADSI, чтобы последний распаковал информацию и передал ее обратно в дерево LDAP.
    Вполне разумным выглядит вопрос: «А почему бы не обратиться напрямую к серверу LDAP?» На этот вопрос есть два хороших ответа: если мы знаем, как использовать ADSI для работы с одним типом служб каталогов, то мы знаем, как работать со всеми ними (или, по крайней мере, с теми, которые имеют ADSI-провайдеры). Второй ответ будет дан чуть позже, когда станет ясно, как можно упростить программирование служб каталогов при помощи инкапсуляции ADSL
    Необходимо ввести понятие AdsPaths, чтобы перейти к ADSI-програм-мированию в Perl. ADsPaths предоставляет нам уникальный способ ссылаться на объекты из любого пространства имен.
    Вот как выглядят примеры ADsPath из документации ADSI SDK:
    WinNT://MyDornain/MyServer/User
    WinNT://MyDomain/JohnSmith,user
    LDAP://ldapsvr/CN=TopHat,DC=DEV,DC=MSFT,DC=COM,[^Internet
    LDAP://MyDomain.microsoft.com/CN=TopH,DC=DEV,DC=MSF[,DC=COM.0=Internet
    Это не совпадение, что они похожи на URL, т. к. и URL и ADsPath служат одним и тем же целям. Они оба пытаются обеспечить недвусмысленный способ сослаться на данные, предоставляемые различными службами данных. В случае с AdsPath из LDAP используется синтаксис LDAP URL из RFC, упомянутых в приложении В (RFC2255).
    Более подробно AdsPath будет рассматриваться при обсуждении двух уже упомянутых пространств имен - WinNT и LDAP. Но сначала разберемся, как, в общих чертах, ADSI используется из Perl.

    ADSI

    ADSI

    http://cwashington.netreach.net/ -
    еще один хороший сайт (посвящен не только Perl) по созданию сценариев для ADSI и других технологий от Microsoft. http://www.microsoft.com/adsi - канонический источник информации по ADSI; обязательно загрузите отсюда ADSI SDK. http://opensource.activestate.com/authors/tobyeverett
    - содержит коллекцию документации по использованию ADSI из Perl Тоби Эверета. http://www.15seconds.com-
    еще один хороший сайт (посвящен не только Perl) по созданию сценариев для ADSI и других технологий от Microsoft. «Windows 2000 Active Directory»,
    by Alistair G. Lowe-Norris (O'Reilly, 1999).

    Добавление элементов при помощи LDIF

    Добавление элементов при помощи LDIF

    Перед тем как рассматривать общие методы добавления элементов в каталог LDAP, давайте вспомним о названии этой книги и рассмотрим технологию, полезную, в основном, системным администраторам и администраторам каталогов. Она использует формат данных, помогающий загрузить данные на сервер каталогов. Мы рассмотрим способы записи и чтения LDIF.
    LDIF, определенный в нескольких стандартах RFC предлагает простое текстовое представление для элементов каталогов. Вот простой пример LDIF из последнего чернового стандарта Гордона Гуда (Gordon Good):
    version: 1
    dn: cn=Barbara Jensen, ou=Product Development, ac=airius, dc=com
    objectclass: top
    objectclass: person
    objectclass: organizationalPerson
    en: Barbara Jensen
    en: Barbara J Jensen
    en: Babs Jensen
    sn: Jensen
    uid: bjensen
    telephonenumber: +1 408 555 1212
    description: A big sailing fan.
    dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com
    objectclass: top
    objectclass: person
    objectclass: organizationalPerson
    en: Bjorn Jensen
    sn: Jensen
    telephonenunber: +1 408 555 1212
    Формат должен быть вам понятен. После номера версии LDIF перечислены DN-имена каждого элемента, определения objectclass и атрибуты. Разделителем элементов является пустая строка.
    Наша первоочередная задача - научиться создавать файлы LDIF из существующих элементов каталогов. Кроме того что мы обеспечим себе данные для следующего раздела (в котором рассматривается чтение файлов LDIF), такая возможность позволит использовать LDIF-файлы
    любым способом при помощи обычных операций Perl, работающих с текстом.
    При обсуждении поиска в LDAP было показано, как вывести элементы в формате LDIF. Изменим код предыдущего примера так, чтобы он записывал данные в файл:
    use Mozilla::LDAP::Conn: use Mozilla::LDAP::LDIF:
    <выполняем связывание и поиск>
    open(LDIF,">$LDIFfile1") or die ""Невозможно записать в SLDIFfile:$!\n": П создаем новый объект LDIF и передаем дескриптор Sldif = new Mozula: :LDAP: :LDIF(\*LDIF);
    while (Sentry) (
    $ldif->wr :reOneEntry($entry): Sentry = $c->nextEntry():
    $c->close(); close(LDIF):
    Модуль Mozilla: : LDAP располагает методом writeEntries(), позволяющим принять массив элементов и записать их подобным образом.
    Используя Net: : LDAP, изменить первоначальную программу еще проще. Вместо:
    $ldif = new Net::LDAP::LDIF("-"); применим:
    Sldif = new Net::LDAP::LDIF($filename,"w");
    для записи выводимых данных в указанный файл, а не на стандартный вывод.
    Теперь совершим обратное действие и прочитаем файлы LDIF (вместо того, чтобы в них записывать). Методы объекта из модуля, о котором пойдет речь, позволяют легко добавить элементы в каталог.
    При чтении LDIF-данных из Perl осуществляется процесс, обратный тому, который применялся в предыдущих примерах для записи. Каждый список элементов считывается и преобразуется в экземпляр объекта элемента, который затем передается соответствующему методу изменения каталога. Оба модуля считывают и анализируют данные, так что процесс довольно прост. Например, с использованием Mozilla I DAP можно написать такую программу:
    use Mozilla::LDAP::Conn; use Mozilla::LDAP::LDIF;
    Sserver = $ARGV[0];
    SLDIFfile = $ARGV[1]
    Sport = getservbynameC'ldap"."tcp") II "389"
    Srootdn = "cn=Manager, ou=Systems, dc=ccs, dc-hogwarts, dc=edu"; $pw = "secret";
    считываем файл LDIF, указанный втооым азгумо--о« в
    и командной строке
    open(LDIF,"SLDIFflie") or die "Невозможно отквыгь $LDIF*iie:$!\n";
    Sldif = rew Mozilla::LOAP::LDIF(\*LDIF):
    анализируем все элеменгь сохраняем их з 3ertri.es
    Gentries = $ldif->readEnrnes():
    close(LOIF):
    tt неанонимное соединение
    $c = new Mozilla::LDAP::Conn($server,$port.Srootdn,$pw);
    die "Невозможно соединиться с $server\n" unless $c:
    № обходим в цикле список элементов, добавляя их на каждой итерации for (gentries)!
    $c->add($_); ft добавляем этот элемент в каталог
    warn "Ошибка при добавлении ". $_->getDN(),": ".$c->getErrorString()."\n"
    if $c->getErrorCode(); } $c->close():
    В этом примере отражено применение методов getErrorCodeO и getErrorString() для получения любых ошибок (и сообщения о них), происходящих в процессе загрузки данных. Ошибки могут появиться по целому ряду причин, включая дублирование DN/RDN-имен, нарушение схемы, проблемы с иерархией и т. д., так что очень важно проверить их при изменении элемента.
    И еще одно замечание, перед тем как перейти к рассмотрению Net:: LDAP: в этом и последующих примерах в демонстрационных целях используется корневое DN-имя (manager DN). Обычно же, если можно избежать применения такого контекста в повседневной работе, это следует делать. Образец правильной настройки LDAP-сервера включает создание могущественной учетной записи или группы учетных записей (которые не являются корневым DN-именем) для управления каталогами. При создании собственных приложений не забывайте этот совет.
    Для Net: : LDAP программа, добавляющая LDIF-элемент, выглядит таким образом:
    use Net::LDAP;
    use Net::LDAP::LDIF:
    $server = SARGV[0]:
    SLDIFfile = SARGV[1]:
    Sport = getserveyfiaTiei. "Idap" 'tcu") || '389';
    Srootdn = 'cn=Manager, ou=Systems. dc=ccs, dc="ogwarts. cc=ea^':
    spw = ' secret ': П считываем файл LDIF. указанный вторым аргументом в 8 командной строк-з
    » последний 'nipaw'-rp '>" для чтения, "w" для запис.' $ldif = пел Ne*' .'_CAP'-LDIF($LDIFfile. "r"): Gentries = $1(1:f >-^ad():
    $с = new Net::LDAPfSserver, port => $por*) n<-
    die "Невозможно соединиться с Sserver: $д'-п"
    $c->bind(dn => $rootdn, password => $pw) or die л^'бка пр/ с-зязкаа-^:.- $@\n";
    for (@entries)i
    $res - $c->add($_):
    warn "Ошибка при добавлении ". $_-^dri(). -.од ошибки ". $'ts->cudc?.
    if $res->code(); }
    $c->unbind();
    Несколько замечаний к этому примеру:
  • При желании можно объединить два оператора чтения LDIF в одну строку:
  • gentries = new Net::LDAP::LOIF($LDIFflie."r")->read;
  • Если попытка add() не удалась, следует запросить десятичный код ошибки. Например, возможно такое сообщение:

  • Ошибка при добавлении cn=Ursula Hampster, oiJ=Almnn.i Association.
    ou=People,
    o=University of Michigan, c=US: код ошибки 68
    Если сервер возвращает текстовое сообщение, метод его г () получает его так же, как это было в примере с Moziila: : LDAP:
    print "Сообщение об ошибке: ".$res->error."\п":
    Безопаснее было бы проверить код возврата, как в предыдущем примере, поскольку серверы LDAP не всегда передают текстовые сообщения об ошибках в своих ответах. Если нужно преобразовать
    десятичный код ошибки в сообщение об ошибке или в название сообщения, модуль Net; :LDAP: :UtiI предлагает для этого две подпрограммы.
  • 68 в десятичной системе счисления - ото 44 в шестнадцатеричной.
  • Теперь нам известно, что мы пытались добавить элемент из файла LDIF, который уже существовал в каталоге.

    Добавление элементов при помощи стандартных операций LDAP

    Добавление элементов при помощи стандартных операций LDAP

    На этот раз мы заглянем вглубь процесса добавления элементов, чтобы научиться создавать и заполнять элементы вручную, не считывая их из файла, как в прошлый раз. Два модуля обрабатывают этот процесс по-разному, поэтому работать с ними следует отдельно. Модуль Миilia:: LDAP ближе к классическому стилю объектно-ориентированного программирования. Создадим новый экземпляр объекта:
    use Mozilla::LDAP::Entry;
    $e = new Mozilla::LDAP::Entry()
    и начнем его заполнять. Следующий шаг - дать элементу отличительное имя DN. Это можно сделать при помощи метода setDN():
    $e->setDN("uid=jay,
    ou=systems,
    ou=people,
    dc=ccs,
    dc=hogwarts,
    dc=edu");
    Для заполнения других атрибутов, таких как objectClass, следует пойти по одному из двух путей. Можно сделать ряд предположений относительно структуры данных, используемой для представления элемента (по существу, это хэш списков), и заполнить ее напрямую:
    $е->{сп} = ['Jay Sekora'];
    В данном случае используется имя атрибута в качестве ключа хэша и ссылка на анонимный массив, хранящий данные. Модуль Mozilla: : LDAP ожидает, что значениями хэша будут ссылки на массив, а не сами данные, так что следующее, хоть и выглядит заманчиво, но будет неверным:
    # воплощенное зло (или, по крайней мере, просто неверно)
    $e->{cn} = 'Jay Sekora;
    В качестве альтернативы можно действовать наверняка и применять метод объекта для добавления данных:
    $e->addValue('en', 'Jay Sekora1);
    Для добавления нескольких значений атрибуту нужно повторно вызывать метод addValue():
    $e->addValue( 'title', 'Unix SysAdmin');
    $e->addValue('title' 'Part-time Lecturer'):
    Мне больше по душе второй подход, т. к. при его использовании менее вероятно, что программа перестанет работать, если в следующих версиях модуля изменится способ представления данных.
    После того как элемент заполнен, можно вызвать метод ado() для внесения его в каталог. Вот маленький сценарий, который добавляет элемент в каталог. В качестве аргументов командной строки он принимает имя сервера, идентификатор пользователя (будет использоваться как часть отличительного имени) и общее имя:
    use Mozilla::LDAP::Conn;
    Sserver = $ARGV[0];
    Sport = getservbyrianieC'ldap". "tcp") ii "389"
    Ssuffix = "ou=People, ou=Systems, dc=ccs, dc=hogwarrs. ac=edu":
    Srootdn = "cn=Manager, ou=Systems. dc=ccs. oc=hogwarrs, dc=edu":
    $pw = "secret";
    ft неанонимное соединение
    $c = new Mozilla::LDAP::Conn($server,Sport,Srootdn,$pw):
    die "Невозможно соединиться с $server\n" unless $c:
    $e = new Mozilla::LDAP::Entry;
    ft DN-имя - это идентификатор пользователя плюс суффикс,
    ft определяющий, куда поместить его в дереве каталогов
    $e->setDN("uid=$ARGV[1],Ssuffix");
    $e->addValue('uid', $ARGV[1]);
    $e->addValue('cn', $ARGV[2]);
    $c->add($e);
    die "Ошибка при добавлении: ". $c->getErrorString(),"\n" if $c-
    >getErrorCode();
    Обратите внимание, что в программе не выполняется проверка ошибок при вводе. Если вы пишете сценарий, который действительно может использоваться в интерактивном режиме, необходимо проверять вводимые данные, чтобы убедиться, что в них нет неэкранированных специальных символов, подобных запятым. Обратитесь к ранее приведенному «совету с совой» за разъяснениями о том, как заключать в кавычки значения атрибутов.
    Теперь перейдем к Net: :LDAP. При желании процесс добавления элементов для Net: : LDAP может быть менее объектно-ориентированным. В него входят модуль Entry (Net:: LDAP: : Entry) и конструктор для экземпляра объекта элемента. Однако он содержит еще одну функцию add(), которая способна принимать структуру данных для добавления элемента за один шаг:
    $res = $c->add(
    dn => 'uid=jay, ou=systems, ou=people. dc=ccs, dc=hogwarts, dc=edi/ attr =>
    [ 'en' => 'Jay Sekora', ' sn => 'Sekora', 'mail' => 'jayguy@ccs.hogwarts.edj', 'title' = -
    [ 'Sysadmin'.' Part-time Lect-jrer' ]. 'uid' => 'jayguy' ]
    На этот раз add() передается два аргумента. Первый - это DN-имя для элемента; второй - ссылка на анонимный массив пар атрибут-значение. Обратите внимание, что атрибуты с несколькими значениями, например title, определяются при помощи вложенного анонимного массива. Тем, кто привык работать со структурами данных в Perl и кому не нравится объектно-ориентированный стиль программирования, такой подход придется больше по душе.

    Finger простая служба каталогов

    Finger: простая служба каталогов

    Finger и WHOIS - отличные примеры простых служб каталогов. Finger, в особенности, предоставляет доступную только для чтения информацию о пользователях на машине (впрочем, скоро мы увидим более творческий подход к его применению). Более поздние версии Finger, такие как сервер GNU Finger и его производные, имеют расширенную разновидность этой функциональности, они позволяют обращаться к одной машине и получать информацию от всех машин из вашей сети.
    Finger был одной из первых широко используемых служб каталогов. Когда-то очень давно при необходимости выяснить адрес электронной почты пользователя на другом узле или даже на вашем собственном лучшим решением было применение команды finger. Команда finger harry@hogwarts.edu сообщала адрес электронной почты Гарри, будь он harry, hpotter или даже что-то менее очевидное (правда, эта команда выводила список всех остальных учащихся школы с именем Гарри). И
    хотя finger применяется до сих пор, популярность команды со временем уменьшилась, т. к. вошли в обиход домашние страницы и свободное получение информации о пользователе стало делом проблематичным.
    Использование протокола Finger из Perl - еще один хороший пример правила TMTOWTDI. Когда я первый раз искал на CPAN хоть что-то, выполняющее операции с Finger, мне не удалось найти ни одного такого модуля. Сейчас-то там можно натолкнуться на модуль Net: : Finger Дениса Тейлора (Dennis Taylor), который появился спустя примерно шесть месяцев после моего первого посещения. Скоро вы с ним познакомитесь, а пока предположим, что его не существует, и не упустим
    случая выяснить, как применять более общий модуль, позволяющий «связываться» по специфическому протоколу.
    Протокол Finger - это очень простой текстовый протокол на базе TCP/IP. В соответствии с определением в RFC1288, он ожидает стандартное TCP-соединение на порту 79. После установки соединения клиент передает простую строку, завершающуюся последовательностью CRLF. В этой строке запрашивается информация либо о конкретном пользователе, либо обо всех пользователях на машине, если строка пустая. Сервер отвечает на запрос и закрывает соединение после передачи потока данных. Можно увидеть, как это происходит, если подсоединиться к порту Finger на удаленной машине напрямую при помощи telnet:
    $ telnet kantine.diku.dk 79
    Trying 192.38.109.142 . . .
    Connected to kantine.diku.dk.
    Escape character is '"]'.
    cola
    Login: cola Name: RHS Linux User
    Directory: /home/cola Shell: /bin/noshell
    Never logged in.
    No mail.
    Plan:
    Current state of the coke machine at DIKU
    This file is updated every 5 seconds
    At the moment, it's necessary to use correct change.
    This has been the case the last 19 hours and 17 minutes
    Column 1 is currently «empty*.
    It's been 14 hours and 59 minutes since it became empty.
    31 items were sold from this column before it became empty. Column 2 contains some cokes.
    It's been 2 days, 17 hours, and 43 minutes since it was filled.
    Meanwhile, 30 items have been sold from this column. Column 3 contains some cokes.
    It's been 2 days, 17 hours, and 41 minutes since it was filled.
    Meanwhile, 11 items have been sold from this column. Column 4 contains some cokes.
    It's been 5 days, 15 hours, and 28 minutes since it was filled.
    Возврат каретки + перевод строки, т. е. символы с ASCII-кодами 13 и!0.
    Finger: простая служба каталогов 207
    Meanwhile, 26 items have been sold from this column. Column 5 contains some cokes.
    It's been 5 days, 15 hours, and 29 minutes since it was filled.
    Meanwhile. 18 items have been sold from this column Column 6 contains some coke-lights.
    It's been 5 days, 15 hours, and 30 minutes since it «as fillec.
    Meanwhile, 16 items have been sold from this column.
    Connection closed by foreign host. $
    В данном примере мы напрямую соединились с портом Finger, набрали имя пользователя «cola», и сервер вернул требуемую информацию.
    Я выбрал этот узел и этого пользователя только затем, чтобы показать, какие чудеса творились на заре появления Интернета. Серверы Finger превратились в службы для задач различного вида. В этом случае кто угодно может посмотреть, заполнен ли автомат газированных напитков, расположенный на факультете компьютерных наук в университете Копенгагена. Примеры странных устройств, подключенных к серверам Finger, можно найти на страницах Беннета Йи (Bennet Yee) в
    «Internet Accessible Coke Machines» и «Internet Accessible Machines» H&http://www.cs.ucsd.edu/~bsy/fun.html.
    Перенесем сетевое соединение, установленное с помощью telnet, в мир Perl. Средствами Perl можно открыть сокет и общаться через него. Вместо того чтобы применять низкоуровневые команды сокета, воспользуемся модулем Net::Telnet Джея Роджера (Jay Roger) и познакомимся с семейством модулей, работающих с сетевыми соединениями.
    Другие модули этого семейства (некоторые из них будут применяться в иных главах) включают Comm.pl Эрика Арнольда (Eric Arnold), Expect.pm Остина Шатца (Austin Schutz) и хорошо известный, но устаревший и непереносимый модуль chat2.pl Рэндала Л. Шварца (Randal L. Schwartz).
    Net::Telnet устанавливает соединение и обеспечивает четкий интерфейс для отправки и получения данных через это соединение. Кроме того, Net::Telnet предоставляет удобные механизмы сканирования шаблонов, позволяющие программам наблюдать за определенными
    ответами от другого сервера, но в примере подобные свойства использоваться не будут.
    Вот Net: :Telnet-версия простого Finger-клиента. Эта программа принимает аргумент в виде user@finger_server. Если имя пользователя пропущено, будет возвращен список всех активных пользователей с сервера. Если пропущено имя узла, будет запрашиваться локальный
    узел:
    use Net::Telnet;
    ($username.$host) = solit(/W, $ARGV[0]);
    Shost = Shost ? Shost : 'localhost';
    создаем новое соединение
    $cn = new Net::Telnet(Host => $host,
    Port => ' finger');
    посылаем имя пользователя
    unless ($cn->print( "$username")){ tt может быть "/W Susernarre"
    $cn->close:
    die "Невозможно отправить строку: ".$cn->errmg."\n";
    собираем все полученные данные, останавливаясь при завершении соединения while (defined ($ret = Scn^get)1) {
    $data .= $ret; }
    # закрываем соединение $cn->close;
    # отображаем полученные данные print $data;
    В RFC1288 определено, что перед именем пользователя, отправляемым на сервер, можно добавить ключ /W для «вывода подробной информации о пользователе», поэтому в программу добавлен комментарий об этом ключе.
    Если нужно соединиться, используя помимо Finger другой текстовый протокол на основе TCP, можно применить очень похожую программу. Например, для соединения с сервером Daytime (который выводит локальное время) используется очень похожая программа:
    use Net::Telnet;
    $host = $ARGV[0] ? $ARGV[0] : 'localhost';
    Sen = new Net::Telnet(Host => Shost,
    Port => 'daytime');
    while (defined ($ret = $cn->get)2) {
    Sdata .= $ret; } $cn->close:
    print Sdata;
    Теперь читатель имеет представление о том, насколько легко создавать типовые сетевые клиенты на основе TCP. Если кто-то уже потратил время и написал модуль, специально созданный для работы с протоколом, все окажется еще проще. В случае с Finger можно воспользоваться модулем Net Finger и заменить все вызовом одной функции:
    use Net::Finger;
    # fingerO принимает строку useriahost и возвращает полученные дачные print finger($ARGV[0]);
    Желая показать все варианты, упомянем о возможности вызвать внешнюю программу (если она существует):
    ($username,$host) = split('@',$ARGV[0]); $host = $host 7 $host : 'localhost';
    местоположение команды finger executable, пользователи MacOS
    этим методом воспользоваться не могут
    Sfingerex = ($"0 eq "MSWin32") ?
    $ENV{'SYSTEMROOT'}."\\System32\\finger" : "/usr/ucb/finger"; # (также может быть и /usr/bin/finger)
    print 'Sfingerex ${username}\@${host}'
    Вы познакомились с тремя различными способами выполнения Finger-запросов. Третий метод, вероятно, самый неудачный, т. к. в нем порождается другой процесс. Net:. Finger обрабатывает простые Finger-запросы; все остальное может очень хорошо выполнить Net: : Tel net
    или родственные ему модули.


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

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

    Finger
    «RFCl 288:The Finger User Information Protocol», D. Zimmerman, 1991.

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

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

    Семейство модулей Win32::OLE, поддерживаемое Жаном Дюбуа (Jan Dubois) и Гурусами Сарати (Gurusamy Sarathy), предоставляет мост от Perl к ADSI (который построен на СОМ как часть OLE). После загрузки основного модуля он используется для запроса ADSI-объектов:
    use Win32::OLE;
    Sadsobj = Win32::OLE->GetObject($ADsPath) or
    die "Невозможно получить объект для $ADsPath\n";
    Win32: :OI_E->GetObject() принимает моникер (moniker) OLE (уникальный идентификатор объекта, в данном случае это ADsPath) и возвращает объект ADSL Этот вызов также обрабатывает процесс связывания (binding) с объектом, уже рассмотренный при обсуждении LDAP. По умолчанию связывание с объектом производится от имени пользователя, запускающего сценарий.
    Этот совет может уберечь вас от испуга. Если выполнить эти две строчки кода в отладчике и изучить содержимое возвращаемой ссылки на объект, то можно увидеть нечто подобное:
    DB<3> x Sadsobj
    О Win32::OLE=HASH(Ox10feOd4)
    empty hash
    He волнуйтесь. Win32: :OLE использует все могущество связанных переменных (tied variables). Кажущаяся пустой структура данных, как по волшебству, передаст информацию из объекта, если верно к ней обратиться.
    Для доступа к значениям свойств интерфейса объекта ADSI используется ссылка на хэш:
    Инструменты ADSI
    Для использования материала из этой главы необходимо установить ADSI хотя бы на одной машине в сети. Эта машина может служить (через DCOM) ADSI шлюзом для остальных машин. Посетите сайт Тоби Эверета (Toby Everett), ссылка на который приведена ниже, чтобы узнать подробнее, как настроить ADSI для работы с DCOM.
    Любая машина с Windows 2000 имеет встроенный в операционную систему интерфейс ADSL Для всех остальных Win32-Ma-шин придется загрузить и установить бесплатный дистрибутив ADSI 2.5, находящийся в http://www.microsoft.com/adsi. По этой же ссылке вы найдете документацию по ADSI, включая ad-si25.chm, сжатую помощь в формате HTML, содержащую лучшую доступную документацию по ADSL
    Даже если вы работаете с Windows 2000, я советую загрузить ADSI SDK с сайта Microsoft по указанной ссылке, поскольку в него входит эта документация и удобный броузер объектов ADSI под названием AdsVW. SDK поставляется с примерами программирования ADSI на нескольких языках, включая Perl. К сожалению, примеры из текущего дистрибутива ADSI полагаются на устаревший модуль OLE.pm, так что, в лучшем случае, вы сможете получить несколько советов, но не надо использовать эти примеры в качестве стартовой точки.
    Перед тем как начать писать программы, стоит загрузить броузер объектов ADSI Тоби Эверета (написанный на Perl) с http:// opensource.activestate.com/authors/tobyeverett. Он научит вас перемещаться по пространствам имен ADSL Обязательно посетите этот сайт, начиная карьеру программиста ADSI, поскольку он является одним из лучших доступных сайтов по применению ADSI из Perl.
    $value = $adsobj->{key}
    Например, если этот объект имеет свойство Name, определенное как часть его интерфейса (а так и есть), вы можете применить:
    print $adsobj->{Name}."\п";
    При помощи такой же записи можно присваивать значения свойствам интерфейсов:
    $adsob]->{FullNanie}= "Oog":
    Свойства объекта ADSI хранятся в кэше (называемом кэшем свойств (property cache)). Первый запрос к свойствам объекта заполняет дан- ный кэш. Последующие запросы к тем же свойствам позволяют получить информацию из этого кэша, а не из службы каталогов. Если вы хотите вручную заполнить кэш, можно вызвать методы Getlnf"() или GetInfoEx() (расширенная версия Getlr.foо) для данного экземпляра объекта, применяя синтаксис, который скоро будет рассмотрен.
    Из-за того что первое считывание информации происходит автоматически, методы GetlnfoO и GetInfoEx() часто остаются незамеченными. Существуют ситуации, когда эти методы следует употреблять, хотя в книге такие случаи рассматриваться не будут. Вот две подобные ситуации:
  • Некоторые свойства объектов можно получить, только явно вызвав GetInfoEx(). LDAP-провайдер Microsoft Exchange 5.5 представляет собой самый характерный пример, поскольку многие из его свойств не доступны, если не вызвать сначала GetInfoEx(). Детальную информацию об этой несовместимости можно найти на http://ope.nso-urce.activestate.com/authors/tobyeverett.
  • Если несколько человек имеют право изменять в каталоге данные, то вызванный вами объект может быть кем-то преобразован, пока вы с ним работаете. Если это произойдет, данные в кэше свойств этого объекта устареют. Getlnfо() и GetInfoEx() обновят этот кэш.
  • Для обновления службы каталогов и источников данных, предоставляемых через ADSI, после изменения объекта нужно вызвать специальный метод Setlnfo(). SetlnfoO сбрасывает изменения из кэша свойств в службу каталогов и источники данных. (Это должно напомнить вам о необходимости вызывать метод upoate() в Mozilla: :LDAP. В данном случае идея та же.)
    Вызывать методы экземпляра объекта ADSI не сложно:
    $adsobj->Method($argijments. . .)
    Поэтому, если бы мы изменили свойства объекта, как это предлагалось сделать в предыдущем предупреждении, то могли бы использовать такую строку сразу же после кода, вносящего изменения:
    $adsob]->Set!nfo():
    В результате данные из кэша свойств помещаются обратно в службу каталогов или источник данных.
    Он возвращает ошибку, полученную в результате последней операции OLE. Применение ключа -и> с Perl (т. е. peri ~w script) также приводит к подробным сообщениям о неудачных попытках OLE-операций. Зачастую эти сообщения об ошибках - единственная помощь, которая вам доступна, так что попытайтесь с толком ее использовать. ADSI-код, который до сих пор рассматривался, выглядел как обычный код на Perl, поскольку внешне они похожи. Теперь перейдем к бо лее сложным вопросам.

    Изменение атрибутов элемента

    Изменение атрибутов элемента

    Теперь перейдем к более распространенным операциям - изменению атрибутов и значений атрибутов элемента. В этом случае тоже существуют значительные различия между модулями mgziи not : :LOAP. Применяя Мо/ИЛа: : LDAP для изменения атрибута элемента. необходимо использовать .один из методов, представленных в табл. 6.5.


    Изменение имен элементов

    Изменение имен элементов

    Последние операции с LDAP, которые мы рассмотрим, касаются двух типов изменений элементов LDAP. Первый тип - это изменение DN- и RDN-имен. Преобразовать RDN-имя элемента просто, и эта операция поддерживается обоими модулями. Вот версия для Mozilla: : LDAP:
    use Mozilla::LDAP::Conn:
    $c->modifyRDN($newRDN.SoldDN.$delold) or
    die "Невозможно переименовать элемент'".
    $c->getErrO''St":pg(). "\n".
    В приведенном отрывке все должно быть понятно, за исключением параметра $delod метода Mjai;'yRDN(). Если он равен 0, то LDAP-библиотеки удалят из элементов значения, совпадающие с измененными RDN-именами. Например, если первично в RDN-имени элемента содержался атрибут ; (от «location», местоположение), но само RDN-имя было изменено, то старый атрибут 1 элемента будет удален и останется только новое значение.
    Вот эквивалентный вариант для переименования элемента в Net: : LUA:
    use Net::iDAP;
    $res = $c->inoddn($oldDN
    newrdn => SnewRDN.
    deleteoldrdn => 1);
    "Невозможно переименовать, код ошибки it".
    $res->code() if $ies->oede()
    В действительности метод moddn() модуля Net;: LDAP может гораздо больше, чем показано в предыдущем примере. До сих пор изменялось только RDN-имя элемента, в то время как местоположение элемента в иерархии дерева каталогов оставалось прежним. В LDAP версии 3 появилась более мощная операция для переименования, позволяющая произвольным образом менять местоположение элемента в дереве каталогов. Метод moddn(), вызванный с дополнительным параметром т„ superior, предоставляет доступ к такой возможности. Если добавить параметр таким образом:
    Sresult = $c->nioddn($oldDN.
    newrdn => SnewRDN,
    deleteoldrdn => 1,
    newsuperioi" => SparentDN);
    die "Невозможно переименовать, код ошибки #".
    $res->code() if $res->codef).
    то элемент из SoldDN будет перенесен и станет дочерним элементом DN-имени, определенного в SparentDN. Гораздо эффективнее использовать этот метод, а не последовательность add() или delete(), как требовалось раньше, для перемещения элементов в дереве каталогов, но подобная возможность поддерживается не всеми LDAP-серверами. В любом случае, если вы скрупулезно проектируете структуру дерева каталогов, вам реже придется переносить элементы с места на место.

    Как же узнать чтонибудь об объекте?

    Как же узнать что-нибудь об объекте?

    До сих пор мы избегали одного большого и, возможно, самого важного вопроса. Скоро нам придется работать с объектами из двух пространств имен. Уже понятно, как получить и установить свойства объектов и как вызвать методы для этих объектов, но все справедливо только в случае, если известны названия этих свойств и методов. Откуда берутся эти названия? Как их можно найти?
    Нет единого места, в котором можно найти ответы на эти вопросы, но существует несколько источников, из которых можно почерпнуть нужную информацию для формирования практически всей картины. Первое место - это документация по ADSI, особенно та помощь, о которой говорилось во врезке «Инструменты для ADSI». В этом файле содержится огромное количество материала. Для ответа на наш вопрос о названиях свойств и методов нужно начать с Active Directory Service Interfaces 2.5—>ADSI Reference—>ADSI System Providers.
    Иногда имена методов можно найти только в документации, но существует другой, более интересный подход для поиска названий свойств. Можно использовать метаданные, предоставляемые самим ADSI. Именно здесь на сцену выходят свойства схемы, о которых говорилось раньше.
    Каждый объект ADSI имеет свойство под названием Schema, которое связывает ADsPath с его объектом схемы. В частности, следующий пример:
    use Win32::OLE:
    SADsPath = "WinNT://BEESKNEES. computer": Sadsob] = Win32::OLE->GetObject($ADsPath) or
    die "Невозможно получить объект для $ADsPatn\n";
    print "Это обьекг ". $adsob]->{Class}. . схема находится r.vi".
    $adsobj->{Schema}."\n"; выведет:
    Это объект Computer, схема находится в:
    WinNT://Do[r.ainName/Schorra/Co""p .
    Значение $adsobj->{Schema} - это путь ADsPath к объекту, описывающему схему для объектов класса Computer в этом домене. Здесь мы используем термин «схема» в том же смысле, что и в разговоре про схемы LDAP. В LDAP схемы определяют, какие атрибуты могут и должны присутствовать в элементах определенных классов объектов. В ADSI схема содержит ту же информацию об объектах определенного класса и их свойства схемы.
    При желании посмотреть на возможные имена атрибутов объекта следовало бы взглянуть на значения двух свойств объекта схемы: MauJatoryProperties и OptionalProperties. Изменим предыдущий оператор
    print:
    $schmobj = Win32::OLE->GetObject($adsobj->{Schema}) or
    die "Невозможно получить объект для $ADsPath\n";
    print join("\n",@{$schmob]->{MandatoryProperties}},
    @{$schmob]->{OptionalProperties}}),"\n";
    Тогда получится:
    Owner
    Division
    OperatingSystem
    OperatingSystemVersion
    Processor
    ProcessorCount
    Теперь известны возможные имена свойств схемы в пространстве имен WinNT для объектов Computer. Отлично.
    Свойства схемы получаются и устанавливаются несколько иначе, чем свойства интерфейсов. Свойства интерфейсов обрабатываются примерно так:
    получение и установка свойств ИНТЕРФЕЙСОВ
    lvalue = $obj->{property); $ob]->{property} = SvaTje;
    Свойства схемы получаются и устанавливаются при помощи специальных методов:
    # получение и установка свойств СХЕМЫ Svalue = $obj->Get("property"); $obj->Put("property" "value"):
    Все, что касается свойств интерфейсов, о чем говорилось до сих пор, остается справедливым и для свойств схемы (т. е. кэш свойств, и т. д.). Помимо необходимости применения специальных методов для получения и установки значений, единственное, что отличает данные свойства, - это их имена. Иногда один и тот же объект может иметь два различных имени для одного и того же свойства, одно для свойств интерфейса, другое для свойств схемы. Например, два этих свойства получают основные настройки для пользователя:
    $1еп = $userot)j-> $len = $userobj->Get("MinPasswordLength"); и то же самое свойство с>,о:-ь
    Наличие двух типов свойств обусловлено тем, что свойства интерфейса существуют в виде части модели СОМ. Разработчики, определяя интерфейс при создании программы, также определяют свойства интерфейса. Позже, если они хотят расширить набор свойств, им приходится изменять и СОМ-интерфейс, и любой код, использующий этот интерфейс. В ADSI разработчики могут изменить свойства схемы в провайдере без необходимости изменять лежащий в основе СОМ интерфейс этого провайдера. Очень важно разобраться с обоими типами свойств, т. к. иногда некоторые данные объекта доступны через свойства только одного типа.
    На практике, если вы ищете только названия свойств интерфейса или схемы и не собираетесь писать программы для их поиска, я рекомендую использовать ADSI-броузер Тоби Эверета, о котором я упоминал ранее. Вот пример этого броузера в действии.
    Как альтернативный вариант упомянем программу ADSIDump из каталога General примеров SDK, которая может вывести содержимое всего дерева ADSL

    LDAP сложная служба каталогов

    LDAP: сложная служба каталогов

    Службы LDAP (Lightweight Directory Access Protocol, облегченный протокол доступа к каталогам) и ADSI гораздо богаче и более сложны в обращении. В настоящее время существуют две популярные версии протокола LDAP (версия 2 и версия 3; при необходимости номер версии будет указываться). Этот протокол быстро стал промышленным стандартом для доступа к каталогам. Системные администраторы воспользовались протоколом LDAP, т. к. он предлагал способ централизовать и сделать доступной всю информацию об инфраструктуре. Помимо стандартного «каталога компании» существуют такие примеры приложений:
  • Шлюзы NIS-K-LDAP
  • Шлюзы Finger-K-LDAP
  • Всевозможные базы данных аутентификации (в частности, для использования в Сети)
  • Объявления о ресурсах (какие машины и периферийные устройства доступны)
  • Кроме того, LDAP является базой для других сложных служб каталогов, подобных активным каталогам Microsoft (Microsoft Active Directory), о которых пойдет речь в разделе «ADSI (Интерфейсы служб активных каталогов)».
    Даже в случае, когда LDAP применяется только для ведения «домашней» телефонной книги, существуют веские причины научиться использовать этот протокол. LDAP-серверы можно администрировать при помощи этого же протокола; что очень напоминает серверы баз
    данных SQL, которые тоже можно администрировать средствами SQL. И в этом случае Perl предлагает отличные механизмы склейки для автоматизации задач администрирования LDAP. Но сначала нужно убедиться, что протокол LDAP разобран и понятен.
    В приложении В «Десятиминутное руководство по LDAP» приводится краткое введение в LDAP для тех, кто не знаком с протоколом. Самая большая преграда, встающая перед системным администратором при изучении LDAP, - это неуклюжая терминология, унаследованная от
    родительских протоколов службы каталогов Х.500. LDAP является упрощенной версией Х.500, но, к сожалению, терминология от этого легче не стала. Стоит потратить время на приложение В и изучение терминов - тогда вам будет проще понять, как использовать LDAP из Perl.
    Программирование LDAP на Perl
    Как и с многими другими задачами системного администрирования в Perl, первым делом при программировании LDAP следует выбрать нужный модуль. Хотя LDAP еще не самый сложный протокол, но это уже и не обычный текстовый протокол. В результате, создать что-нибудь, «говорящее» на LDAP, задача нетривиальная. К счастью, другие авторы уже сделали эту работу за нас: Грэм Бар (Graham Barr) написал модуль Net: : LDAP, а Лейф Хедстром (Leif Hedstrom) и Клейтон Донли (Clayton Donley) создали модуль Mozilla: :LDAP (также известный как PerLDAP). Отметим некоторые различия между этими двумя модулями (табл. 6.1).


    LDAP

    LDAP

    «An Internet Approach to Directories»,
    Netscape, 1997 - отличное введение в LDAP (http://developer.netscape.com/docs/manuulsildap ldap.html). «An LDAP Roadmap & FAQ»,
    Jeff Hodges, 1999 (http://www.kingsmo-untain.com/idapRoadmap.shtmi). http://www.ogre.com/ldap/
    n http://www.Unc-dev.com, - домашние страницы соавторов PerLDAP. http://www.openLdap.org/ -
    свободно распространяемый LDAP-repwu. находится в стадии активной разработки. http://www.umich.edu/~dlrsvcs/ldap/index.html - домашняя страница «прародителя» служб каталогов OpenLDAP и Netscape. Некоторая документация представляет интерес до сих пор. «Implementing LDAP»,
    Mark Wilcox (Wrox Press, 1999). «LDAP-HOWTO»,
    Mark Grennan, 1999 (http://www.grennan.com/ldap-HOWTO.html). «LDAP Overview Presentation»,
    Bruce Greenblatt, 1999 (http://www.di-rectory-applications.com/presentation/). «LDAPProgramming Directory-Enabled Applications With Lightweight Directory Access Protocol»,
    Tim Howes and Mark Smith (Macmillan Technical Publishing, 1997). Netscape Directory Server Administrator's/Installation/Deployment Guides and SDK documentation (http://developer.netscape.com/docs/ manuals/directory.html).
    «RFC1823:The LDAP Application Program Interface»,
    T. Howes, M. Smith, 1995. «RFC2222:Simple Authentication and Security Layer (SASL)», J.
    Myers, 1997. «RFC2251 .-Lightweight Directory Access Protocol (v3)»,M.
    Wahl, T. Howes, S.Kille, 1997. «RFC2252:Lightweight Directory Access Protocol (v3)Attribute Syntax Definitions»,
    M.Wahl, A. Coulbeck, T. Howes, S. Kille, 1997. «RFC2254:The String Representation of LDAP Search Filters»,
    T. Howes, 1997. «RFC2255:The LDAP URL Format»,
    T. Howes, M. Smith, 1997. «RFC2256.-A Summary of the X.500(96) User Schema for use with LDAPuS»,
    M. Wahl, 1997. « The LDAP Data Interchange Format (LDIF)-Technical Specification»
    (в состоянии разработки), Gordon Good, 1999 (можно найти на http:// search.ietf.org/internet-drafts/draft-good-ldap-ldif-OX.txt, где X - это номер текущей версии). «Understanding and Deploying Ldap Directory Services»,
    Tim Howes, Mark Smith, Gordon Good (Macmillan Technical Publishing, 1998). «UnderstandingLDAP»,
    Heinz Jonner, Larry Brown, Franz-Stefan Hin-ner, Wolfgang Reis, Johan Westman, 1998. Превосходное введение в LDAP (http://www.redbooks.ibm.com/ abstracts/sg244986.html).

    Первоначальное LDAPсоединение

    Первоначальное LDAP-соединение

    Соединение с аутентификацией - это, обычно, первый шаг в любой клиент-серверной LDAP-транзакции. На «языке» LDAP это называется «связыванием с сервером» (binding to the server). B LDAPv2 требовалось связаться с сервером перед отправкой команд, в LDAPvS усло-
    вия не такие жесткие.
    Связывание с LDAP-сервером выполняется в контексте определенного отличительного имени (Distinguished name, DN), описанного как привязанное отличительное имя (bind DN) для данного сеанса. Такой контекст похож на регистрацию пользователя в многопользовательской системе. В многопользовательской системе текущее регистрационное имя (по большей части) определяет уровень доступа пользователя к данным в этой системе. В LDAP именно привязанное отличительное имя определяет, какие данные на LDAP-сервере доступны для просмотра и изменения. Существует также специальное корневое отличительное имя (root Distinguished Name), дабы не путать его с относительным (Relative Distinguished Name) именем, для которого не существует акронима. Корневое отличительное имя имеет полный контроль над всем деревом, что очень похоже на регистрацию с правами пользователя root в Unix или с правами пользователя Administrator в
    NT/2000. На некоторых серверах это имя называется manager DN.
    Если клиент не предоставляет аутентификационной информации (например, DN-имя и пароль) во время связывания или вообще не связывается с сервером до оправки команд, это называется анонимной аутентификацией (anonymous authentication). Анонимно зарегистрированные клиенты обычно получают очень ограниченный доступ к данным на сервере.
    В спецификации LDAPvS определяется два типа связывания: простой и SASL. В простом связывании для аутентификации используются обычные текстовые пароли. SASL (Simple Authentication and Security Layer, слой простой аутентификации и безопасности) - это расширенный интерфейс аутентификации, определенный в RFC2222, позволяющий авторам клиентов/серверов встраивать различные схемы аутентификации, подобные Kerberos и одноразовым паролям. Когда клиент соединяется с сервером, он запрашивает определенный механизм аутентификации. Если сервер его поддерживает, он начнет диалог, соответствующий такому механизму, для аутентификации клиента. Во время этого диалога клиент и сервер могут договориться об уровне безопасности (например, «весь трафик между нами будет зашифрован при помощи TLS»), применяемом после завершения аутентификации.
    Некоторые серверы и клиенты LDAP добавляют еще один метод аутентификации к SASL и стандартному простому способу. Этот метод является побочным продуктом использования LDAP по зашифрованным SSL (Secure Socket Layer, уровень защищенных сокетов). Для установки этого канала серверы и клиенты LDAP обмениваются криптографическими сертификатами на основе открытого ключа так же, как веб-сервер и броузеры при работе по протоколу HTTPS. LDAP-серверу
    можно дать указание использовать в качестве аутентификационной информации только надежный клиентский сертификат (trusted client's certificate). Из доступных Perl-модулей только PerLDAP предла-
    гает LDAPS (зашифрованные SSL-соединения). В примерах, для того чтобы не слишком усложнять их, будем пользоваться только простой аутентификацией и незашифрованными соединениями.
    Вот как выполняется простое соединение и его завершение средствами Perl:
    use Mozilla::LDAP::Conn;
    используем пустые $binddn и Spasswd анонимной связи
    $с = new Mozilla: :LDAP: :Conn($server, Sport, SPinddn, Spasswd);
    die "Невозможно соединиться с Sserver" unless $c;
    $c->close(); или:
    use Net::LDAP;
    $c = Net:;LDAP->new($server1 port => Sport) ск
    die " Невозможно соединиться с Sserver: $?\n" не передаем параметры для связи $c->bind($:ii"ddn, password -> Spasswd) or
    die "Невозможно соединиться: $@\п";
    В Mozilla: : LDAP: :Conn создание нового объекта соединения также связано с сервером. В Net: : LDAP этот процесс состоит из двух шагов. Для инициализации соединения без выполнения привязывания в Mozilla: : LDAP необходимо использовать функцию (ldap_init()) из не объектно-ориен-
    тированного модуля Mozilla: :LDAP::API.
    Приготовьтесь тщательно заключить в кавычки значения атрибутов
    Небольшой совет перед тем, как перейти к дальнейшему программированию на Perl: если в относительном отличительном имени есть атрибут, значение которого содержит один из следующих символов: « + », «(пробел)», «, », « '», «>», «<» или «; », необходимо либо заключить значение в кавычки, либо экранировать эти символы обратным слэшем (\). Если значение содержит кавычки, их также нужно экранировать при помощи обратного слэша. Обратные слэши в значениях тоже экранируются обратными слэшами.
    Если вы не будете аккуратны, то недостаток кавычек может сыграть с вами злую шутку.

    Поиск

    Поиск

    Эта последняя сложность, которую следует обсудить, перед тем как двигаться дальше. В разделе «LDAP: сложная служба каталогов» мы провели достаточно времени в разговорах о поиске в LDAP. Но в мире ADSI мы вряд ли услышим хоть слово по этому поводу. Все из-за того, что в Perl (и любом другом языке, в котором используется тот же OLE-интерфейс автоматизации) поиск с ADSI очень сложен; более того, поиск поддеревьев и поиск, в котором используются не самые простые фильтры, мучительно сложен. (Все остальное не так плохо.) Сложный поиск проблематичен, т. к. для его выполнения необходимо выйти за пределы ADSI и использовать совершенно иную методологию для получения данных (не говоря уже о том, что придется выучить новые акроним от Microsoft).
    Но тот, кто занимается системным администрированием, привык смеяться над сложностями, так что начнем с простого поиска, а потом пе рейдем к более сложным вопросам. Простой поиск, затрагивающий один объект (пространство base) или его непосредственных потомков
    (пространство one), можно выполнить вручную при помощи Perl. Сделать это можно так:
  • Для одного объекта получите нужные свойства и используйте обычные операторы сравнения для определения соответствия:
  • Для поиска дочерних объектов примените технологии доступа к контейнерам, о которых говорилось раньше, а затем изучите каждый дочерний объект. Несколько примеров поиска такого типа будут рассмотрены очень скоро.
  • Для того чтобы выполнить более сложный поиск, затрагивающий, скажем, все дерево каталогов или поддерево, вам придется переключиться на использование другой технологии «промежуточного уровня» под названием ADO (ActiveX Data Objects, объекты данных ActiveX). ADO предоставляет языкам сценариев интерфейс к базам уровня Microsoft OLE DB. OLE DB обеспечивает общий интерфейс, ориентированный на базы данных, к источникам данных, подобным реляционным базам данных и службам каталогов. В нашем случае ADO будет применяться для «разговора» с ADSI (который, в свою очередь, общается с самой службой каталогов). Поскольку ADO - это методология, ориентированная на базы данных, рассматриваемая программа предваряет материал об ODBC, о котором речь пойдет в главе 7.
    ADO работает только с провайдером LDAP ADSI. В пространстве имен WinNT она работать не будет.
    ADO - это отдельная тема, которая лишь затрагивает службы каталогов, поэтому будет рассмотрен только один пример с короткими пояснениями, остальные примеры работают с ADSL Дополнительную информацию об ADO можно найти на http://www.microsoft.com/ado.
    Вот пример программы, выводящей имена всех групп, найденных в данном домене. Детальное обсуждение программы приведено ниже.
    use Win32::OLE 'in';
    # получаем объект ADO, устанавливаем провайдер, открываем соединение
    $с = Win32::OLE->new("ADODB.Connection");
    $c->{Provider} = "ADsDSOObject";
    $c->OpenC'ADSI Provider");
    die Win32::OLE->LastError() if Win32::OLE->LastError();

    подготавливаем и выполняем запрос $ADsPath = "LDAP://ldapserver/dc=example,dc=com";
    $rs = $c->Execute("<$ADsPath>;(objectClass=Group);Name;SubTree");
    die Win32::OLE->LastError() if Win32::OLE->LastError();
    until ($rs->EOF){
    print $rs->Fields(0)->{Value},"\n"; $rs->MoveNext;
    $rs->Close; $c->Close;
    Блок кода после загрузки модуля получает экземпляр объекта ADO Connection, устанавливает имя провайдера для этого экземпляра объекта, а затем просит его открыть соединение. Соединение открывается от имени пользователя, запускающего сценарий, хотя можно было установить другие свойства объекта, позволяющие изменить такое поведение.
    Затем выполняется собственно поиск при помощи Execute(). Поиск можно осуществлять средствами одного из двух «диалектов»: SQL или ADSL Диалект ADSI, как видно из программы, использует командную строку, состоящую из четырех аргументов, каждый из которых разделен точкой с запятой.2 Вот эти аргументы:
  • ADsPath (в угловых скобках), определяющий сервер и базовое DN-имя для поиска.
  • Фильтр поиска (применяется тот же синтаксис LDAP-фильтров, что упоминался раньше).
  • Имя или имена (разделенные запятыми) возвращаемых свойств.
  • Пространство поиска: либо Base, либо OneLevel, либо SubTree (в соответствии со стандартом LDAP).
  • Execute() возвращает ссылку на первый из объектов ADO RecordSe, получаемых в результате запроса. По очереди запрашивается каждый из объектов RecordSet, распаковываются объекты, которые в нем содержатся, и выводится свойство Value, возвращаемое методом Fields() для каждого из этих объектов. Свойство Value содержит значение, которое запрашивалось в командной строке (имя объекта Group). Вот как выглядит отрывок получаемых данных на машине с Windows 2000:
    Administrators
    Users
    Guests
    Backup Operators
    Replicator
    Server Operators
    Account Operators
    Print Operators
    DHCP Users
    DHCP Administrators
    Domain Computers
    Domain Controllers
    Schema Admirs
    Enterprise Admins
    Cert Publishers
    Domain Admins
    Domain Users Domain Guests Group Policy Admins RAS and IAS Serveis DnsAdmins DnsUpdateProxy

    Представление элементов в Perl

    Представление элементов в Perl

    Эти примеры программ могут вызвать ряд вопросов о представлении элементов и о работе с ними, — в частности, как сами элементы хранятся и обрабатываются в программе на Perl. Дополняя рассказ о поиске в LDAP, ответим на некоторые из них, хотя позже в разделе о добавле-
    нии и изменении элементов подобные вопросы будут рассматриваться подробно.
    Если Mozilla: : LDAP выполняет поиск и возвращает экземпляр объекта элемента, то можно обратиться к отдельным атрибутам этого элемента, применяя синтаксис, используемый в Perl при работе с хэшами списков. $entry->{attributenaiTie} - это список значений атрибута с таким именем. Я выделил слово «список», т. к. атрибуты даже с одним значением хранятся в анонимном списке, на который ссылается этот ключ хэша. Для получения единственного значения атрибута необходимо использовать запись $entry->{aUr ннг.'л аге}->[0]. Некоторые методы модуля Mozilla: : LDAP: : Entry возвращают атрибуты элемента (табл. 6.3).


    Работа с группами через ADSI

    Работа с группами через ADSI

    Для перечисления доступных групп достаточно лишь немного подправить программу, выводящую список пользователей. Меняется только такая строка:
    print $adsobj->{Name},"\n"
    if ($adsobj->{Class) eq "Group"):
    Создание и удаление групп выполняется при помощи тех же методов CreateO и Oelete(), которые применялись для создания и удаления учетных записей. Единственное различие - первый аргумент нужно изменить на «group». Вот так:
    $д = $с->Сгеа Sgro.ip- ary):
    Для добавления пользователя в группу (определяемую при помощи GroupName) после ее создания используется следующее:
    use Win32::OLE:
    SADsPath = "WinNT://DomainNane ''GroupNcine gro,,c"
    $g = Win32: :OLE->GetOuject($ADsPatii) or
    Здесь действуют те же правила относительно локальных пользователей и пользователей домена (глобальных), которые мы рассмотрели выше. Для того чтобы добавить пользователя домена в группу, $ийо<"-ADsPath должна указывать на пользователя на PDC для этого домена.
    Для удаления пользователя из группы применяйте:
    $c->Reniove($userADsPath);

    Работа с объектами контейнер/коллекция

    Работа с объектами контейнер/коллекция

    Ранее в этом разделе уже упоминались два типа объектов ADSI: лист и контейнер. Объект-лист представляет собой только данные, тогда как контейнер (известный еще как коллекция - в терминах OLE/COM) содержит другие объекты. Еще одно отличие двух типов объектов в контексте ADSI состоит в том, что объект-лист не имеет дочерних объектов в иерархии, а у контейнеров такие объекты есть.
    Объекты-контейнеры требуют специальной обработки, т. к. в большинстве случаев нас интересуют данные, инкапсулированные их дочерними объектами. Существует два способа обратиться к таким объектам из Perl. Win32: :OLE имеет специальную функцию под названием in(), которая недоступна по умолчанию, если модуль загружается стандартным способом. Если необходимо получить к ней доступ, надо в начале программы использовать следующее:
    use Win32::OLE 'in';
    in() возвращает список ссылок на дочерние объекты, хранящиеся в этом контейнере. Это позволяет писать легко читаемые программы на Perl:
    foreacn Schiid (in $adsobj){
    print $child->!Name}
    Другой путь заключается в том, чтобы загрузить один из полезных потомков
    Win32: :OLF под названием Win32: :OLE: :Enum. Win32: :0! F.' : f.-",:'1 ->new()
    создает объект-перечислитель из какого-либо объекта-контейнера:
    use Win32 : : OLE: : Enuti:
    $onobj = Win32: :OLE: : Enurr->new($adsobj);
    Для этого объекта можно вызвать несколько методов и получить дочерние объекты $adscoj. Подобный подход должен напомнить вам способ, применяемый в операциях поиска с Moziila: : LOAP; процесс тот же самый.
    Идентификация объекта-контейнера
    Заранее нельзя узнать, является ли объект контейнером. Не существует способа из Perl «спросить» объект, не контейнер ли он. Максимум, что можно сделать, - попытаться создать объект-перечислитель и, если эта попытка не удастся, фиксировать данный результат. Вот короткий пример, который делает именно это:
    use Win32::OLE;
    use Win32::OLE::Enum:
    eval {$enobj = Win32::OLE::Enum->new($adsobj)};
    print "Объект " . ($@ ? "не " : "") . "является контейнером \п";
    Второй способ - посмотреть на другие источники, описывающие этот объект. Все это плавно перетекает в третью сложность.

    Работа с очередями и заданиями печати через ADSI

    Работа с очередями и заданиями печати через ADSI

    Вот как можно определить названия очередей на определенном сервере и модели принтеров, используемых для обслуживания этих очередей:
    use Win32::OLE 'in';
    $ADsPatn="WinNT://DomainName/PrintServerName, computer";
    $c = Win32::OLE->GetObject($ADsPath) or die "Невозможно получить $ADsPath\n";
    foreach Sadsobj (in $c){
    print $adsobj->{Name}.":".Sadsobj->{Model}."\n"
    if ($adsobj->{Class} eq "PrintQueue"); }
    После того как стало известно название очереди печати, можно напрямую связаться с ней для запросов и управления:
    use Win32::OLE 'in';
    # таблица получена из раздела
    # 'Active Directory Service Interfaces 2.5->ADSI Referen.ce->
    # ADSI Interfaces->Dynamic Object Interfaces->IADsPrintQueueOperations->
    lADsPrintOueueOperations Property Methods' (уф!) из ADSI 2.5 SDK
    %status =
    (0x00000001 => 'PAUSED', 0x00000002 => 'PENDING_DELETION',
    0x00000003 => 'ERROR' , 0x00000004 => 'PAPER_JAM',
    0x00000005 => 'PAPER_OUT', 0x00000006 => 'MANUAL_FEED',
    0x00000007 => 'PAPER_PROBLEM', 0x00000008 => 'OFFLINE',
    0x00000100 => 'IO_ACTIVE', 0x00000200 => 'BUSY',
    0x00000400 => 'PRINTING', 0x00000800 => 'OUTPUT_BIN_FULL',
    0x00001000 => 'NOTJWAILABLE'. 0x00002000 => 'WAITING',
    0x00004000 => 'PROCESSING', 0x00008000 => 'INITIALIZING'.
    0x00010000 => 'WARMING_UP'. 0x00020000 => 'TONER_LOW.
    0x00040000 => TJO_TONER'. 0x00030000 => 'PAGE_PUNT'.
    0x00100000 => 'USER_INTERVENTION', 0x00200000 => 'OUT_OF_MEMORY'.
    0x00400000 => 'DOOR_OPEFJ', 0x00800000 => 'SEflVERJNKNOWN'.
    0x01000000 => 'POWER_SAVE'):
    SADsPath = "^inNT' //PrintServerNafre/PrintQ-jeueNatne":
    $p = tvin32: :OLE->GetOoject($ADsPath) or aie "Невозможно полу-п-ть $ADsPat'.\^".
    print "Состояние принтера " . $c->{Name}
    ((exists $p--"i;jt au.s}; ? $status<$c-'{atatub) t . Nji ACilVL )
    Объект Printoutip имеет несколько методов для контроля очереди печати: PausoO. Это позволяет управлять действиями самой очереди. А что если мы захотим изучить или обработать конкретные задачи из очереди?
    Для того чтобы добраться до самих заданий, необходимо вызвать метод PnntJobs() объекта возвращает коллекцию, состоящую из объектов PrintJob, каждый из которых имеет ряд свойств и методов. Например, вот как можно показать список заданий из определенной очереди:
    use Win32::OLE
    и таблица получена из раздела
    и Active Directory Service Interfaces 2.5->ADSI Reference
    Я ADSI Interfaces->Dynamic Object Interfaces-MADsPrintJotiOporai зспз->
    fl lAOsPrintJobOperations Property Methods' (двойное уф) в ADSI 2.5 SDK
    %status = (0x00000001 => 'PAUSED', 0x00000002 => 'ERROR'.
    0x00000004 => 'DELETING',0x00000010 => 'PRINTING',
    0x00000020 => 'OFFLINE', 0x00000040 ^> 'PAPEROUT'.
    0x00000080 => 'PRINTED1, 0x00000100 -> 'DELETED1):
    SADsPath = "WinNT://PrintServerName/PrintQueueNamc":
    $p = Win32: :GLE->GetObject($AQsPath) or
    die "Невозможно лолучип, SADsPaV'\'i
    $jobs = $p->PrintJobs(); foreach $job (in $jobs){ print $]ob->{User( .
    $status{$job->{status}} . "\n"; }
    Каждое задание можно приостановить (Раиse()) и продолжить.

    Работа с пользователями через ADSI

    Работа с пользователями через ADSI

    Для получения списка пользователей домена применяется следующее:
    use Win32::OLE 'in':
    SADsPath = "WinNT://DomainName/PDCName,computer":
    $c = Win32::OLE->GetObject($ADsPath) or
    die "Невозможно получить $ADsPath\n":
    foreach $adsobj (in $c){
    print $adsobj->{Nane}."\n" if ($adsobj->{Class} eq "User"): }
    Для создания нового пользователя и установки его полного имени (свойство Full Name):
    use Win32::OLE;
    $ADsPatn="WinNT://Domain Name/ComputerName,computer":
    $c = Win32::OLE->GetOaject($ADsPatn) or
    die "Невозможно получить SADsPath г":
    tt создаем и возвращаем объект User $u = $c->Create("jser".$usernaTie);
    Su->SetInfc(): tt н,жно создать лолазона^еля. чеойд тем как меня~^ з^а-е-/-
    Name тедогус^.'м $u->{Fjll'Janei = З'Л'.г.;: ,;.-. $bj->3eil';fo()
    Если Cciroutf rfJri! r - это первичный контроллер домена (Primary Domain Controller), то мы создали пользователя домена. Если нет, этот пользователь будет локальным для данной машины. Эквивалентная программа создания глобального пользователя (при помощи LDAP нельзя создавать локальных пользователей) в Active Directory выглядит так:
    use Win32::OLE;
    SADsPath = "LDAP://ldapserver,CN=Users,dc=example.dc=coro";
    $c = Win32: :OLE->GetObject($ADsPath) or die "Невозможно получить SADsPafAr"
    # создаем и возвращаем объект User $u=$c->Create("user","cn=".Scommonname);
    Su->{samAccountName} = Susername;
    # нужно сначала создать пользователя в каталоге, а потом менять значения $u->Set!nfo();
    # пробел между "Full" и "Name" требуется при работе с пространством имен LDAP:
    $u->{'Full Name'} = $fullname; $u->Set!nfo();
    Для удаления пользователя нужно внести лишь небольшие изменения:
    use Win32::OLE;
    SADsPath = "WinNT://DomainName/ComputerName,computer";
    $c = Win32::OLE->GetObject($ADsPath) or die "Невозможно получить $ADsPath\n";
    # удаляем обьект User, заметьте, мы в границах контейнера
    $c->Delete("user",Susername);
    $u->Set!nfo();
    Изменить пароль пользователя можно при помощи единственного метода:
    use Win32::OLE;
    SADsPath = "WinNT://DomainName/ComputerName/".Susername;
    $u = Win32::OLE->GetObject($ADsPath) or
    die "Невозможно получить $ADsPath\n":
    $u->ChangePasssword($oldpassword,Snewpassword): $u->Set!nfo();

    Работа с разделяемыми ресурсами через ADSI

    Работа с разделяемыми ресурсами через ADSI

    Теперь займемся более интересными задачами ADSI, адресованными посвященным. Можно применять ADSI, чтобы предоставить к совместное пользование часть локального дискового пространства на машине:
    use Win32::OLE;
    SADsPath = "WinNT://ComputerName/1 anira'iserver":
    $c = Win32: :ClEI->GotObjecT
    $s = $c->Create("filesnare".Sshareramo): $s->{path} = 'C:\directory1:
    Перед тем как перейти к другим задачам, хочу воспользоваться случаем и напомнить вам о необходимости обратиться к документации SDK перед работой с каким-либо из этих ADSI-объектов. Кое-какие не<>;ки-данности могут оказаться полезными. Если вы заглянете и раздел Acti ve Directory Service Interfaces 2.5—>ADSI Reference—> ADSI Interfaces—> Persistent Object Interfaces—> lADsFileShare файла помощи ADSI 2.5, то увидите, что объект f liesnare имеет свойство CurrentUser-Count, которое соответствует количеству пользователей, подсоединенных в настоящее время к разделяемому ресурсу. Этот нюанс может очень сильно пригодиться.

    Работа со службами NT/2000 через ADSI

    Работа со службами NT/2000 через ADSI

    В последнем наборе примеров рассмотрим, как находить, запускать и останавливать службы на машине с NT/2000. Как и другие примеры из этой главы, эти короткие программки необходимо запускать с достаточными привилегиями для осуществления выполняемых действий.
    Для получения списка служб на машине и их состояний можно использовать такую программу:
    us о w.l п32 : . ol& 1 г П эта таблица получена из раздела
    Я 'Active Directory Service Interfaces 2.5->ADSI fieference->
    И ADSI Interfaces->Dynamic Object Inter 'acec
    » IADsServiceOperatio:iS Proper*.> Methods' AC/SI
    %status =
    (0x00000001 => 'STOPPED'. 0x00000002= START_PEMDIHG',
    0x00000003 => 'STGP_PENDING'. 0x00000004=:- RUNNING',
    0x00000005 => 'CONTINUE_PENDING' .0x00000006 => ' PAUSE_PEtJDING '.
    0x00000007 => 'PAUSED'. 0x00000008 => 'ERROR'):
    SADsPath = "W]nNT://DomainNarr,e/ConiputerNa!ne, computer":
    $C = Win32: :OLE->GctObj'oct($ADsPath) or die "Невозможно получить $ADsPatn\;i":
    foreach $adsobj (in $c){ print Sadsobj^tDisplayNarr.
    if ($adsobj->{Class) eq "Service"); }
    Для запуска, остановки, приостановки или продолжения работы службы вызываются очевидные методы (Star t(), Stop() и т. д.). Вот как можно запустить службу Network Time на машине с Windows 2000, если ранее она была остановлена:
    use Win32::OLE;
    SADsPath = "WinNT://DomainName/ComputerName/W32Tirne, service";
    $s = Win32::OLE->GetObject($ADsPath) or die "Невозможно получить $ADsPath\n";
    $s->Start();
    tf можно в этом месте проверять в цикле состояние
    пока служба не будет запущена
    Во избежание потенциальных конфликтов имен пользователей и компьютеров, можно переписать предыдущий пример:
    use Win32::OLE;
    $d = Win32: :OLE->GetObject("WinNT://Do(r'ain");
    $c = $d->GetObiect( "Computer". Scornputername):
    $s = $c->GetOb]ect("Service". "W32Tiiie"):
    $s->Start():
    Эти примеры должны подсказать вам, какой контроль над системой можно получить при помощи ADSI из Perl. Службы каталогов и их интерфейсы могут быть весьма могущественной частью вашей компьютерной инфраструктуры.

    Служба каталогов WHOIS

    Служба каталогов WHOIS

    WHOIS - это еще одна полезная служба каталогов, предоставляющая доступную только для чтения информацию. WHOIS обеспечивает услуги, подобные телефонному справочнику для машин, сетей и людей. Некоторые крупные организации, такие как IBM, UC Berkeley и MIT, предоставляют услуги WHOIS, но самые известные WHOIS-серверы принадлежат InterNIC и другим компаниям, занимающимся вопросами регистрации в Интернете, в том числе RIPE (имеет дело с европей-
    скими IP-адресами) и APNIC (Asia/Pacific, Азиатские/Тихоокеанские адреса).
    При необходимости связаться с системным администратором какого-либо узла, чтобы сообщить ему о подозрительных действиях в сетях, следует использовать службу WHOIS для получения контактной информации. В большинстве операционных систем для выполнения WHOlS-запросов существуют как GUI-инструменты, так и инструменты, запускаемые из командной строки. Типичный запрос в Unix выглядит так:
    % whois -h whois.networksolutions.com brandeis.edu

    Registrant;
    Brandeis University (BRANDEIS-DOM)
    Information Technology Services
    Waltham, MA 02454-9110
    US
    Domain Name: BRANDEIS.EDU
    Administrative Contact:
    Koskovich, Bob (BK138) user@BRANDEIS.EDU
    +1-781-555-1212 (FAX) +1-781-555-1212 Technical Contact, Zone Contact:
    Hostmaster, Brandeis С (RCG51) hostmaster@BRANDEIS.EDU
    +1-781-555-1212 (FAX) +1-781-555-1212 Billing Contact:
    Koskovich, Bob (BK138) user@BRANDEIS.EDU
    +1-781-555-1212 (FAX) +1-781-555-1212
    Record last updated on l3-0ct-1999.
    Record created on 27-May-1987.
    Database last updated on 19-Dec-1999 17:42:19 EST.
    Domain servers in listed order:
    LILITH.UNET.BRANDEIS.EDU 129.64.99.12
    FRASIER.UNET.BRANDEIS.EDU 129. 64.99.11
    DIAMOND. CS.BRANDEIS.EDU 129. 64. 2. 3
    DNSAUTH1.SYS.GTEI.NET 4.2.49.2
    DNSAUTH2.SYS.GTEI.NET 4.2.49.3
    Если же нужно выяснить владельца определенного диапазона IP-адресов, то и тут поможет WHOIS:
    % whois -h whois.arin.net 129.64.2
    Brandeis University (NET-BRANDEIS) 415 South Street Waltham, MA 02254
    Netname: BRANDEIS Netnumber: 129.64.0.0
    Coordinator:
    Koskovich, Bob (BK138-ARIN) user@BRANDEIS.EDU 617-555-1212 Служба каталогов WHOIS 211
    Domain System inverse mapping provided by:
    BINAH.CC.BRANQEIS.EDU 129.64.1.3 NIC.NEAR.NET 192.52.71,4
    NOC.CERF.NET 192.153.156.22
    Record last updated on 10-Jul-97.
    Database last updated on 9-Oct-98 16:10:44 EOT
    The ARIN Registration Services Host contains ONLY Internet Network Information: Networks, ASN's, and related POC's. Please use the whois server at rs.internic.net for DOMAIN related Information and nic.mil for NIPRNET Information.
    В предыдущем примере применялся WHOIS-клиент из Unix, работающий в командной строке. В Windows NT и MacOS подобные клиенты не входят, тем не менее, это не должно остановить пользователей данных систем от получения доступа к нужной информации. Существует
    много условно бесплатных клиентов, но не так трудно с помощью модуля Net: : Whois создать на Perl очень простой клиент (модуль Net: : Whois первоначально был написан Чипом Салзенбергом (Chip Salzenberg), а теперь поддерживается Даной Хьюдес (Dana Hudes)). Следующий
    код - это лишь несколько измененная версия примера из документации, поставляемой вместе с модулем:
    use Net::Whois;
    fl запрашиваем сервер, возвращая объект с результатами my $w = new Net::Whois::Domain $ARGV[0] or
    die "Невозможно соединиться с сервером Whois\n", die "Никакой информации о домене $ARGV[0] не найдено\п " unless ($w->ok),
    и выводим части этого объекта
    print "Домен: ", $w->domain, "\n";
    print "Имя: ", $w->name, "\n";
    print "Тег: ", $w->tag, "\n";
    print "Адрес:\n", map { " $_\n" } $w->address;
    print "Страна: ", $w->country, "\n";
    print "Запись создана: ".$w->record_created."\n";
    print "Запись обновлена: ",$w->record_updated."\n";
    и выводим серверы имен ($w->servers returns a list of lists)
    print "Серверы именДп", map { " $$_[0] ($$_[1])\n" } @{$w->servers;
    tt выводим список контактов ($w->contacts returrs a nasn of lists)
    my($c,$t).
    if ($c = $w->contacts) {
    print "Contacts :\n";
    for $t (sort keys %$c) { print " St.\n": 212 Глава 6. Службы каталогов
    print map { "\t$_\n" } @{$$c{$t}}; } }
    Запрос WHOIS сервера InterNIC/Network Solutions - это простой процесс. Для возвращения результата применяется мо«дуль Net::Whois: : Domain. Методы этого объекта, названные в соответствии с полями, которые получает WHOIS-запрос, обеспечивают доступ к данным.
    WHOIS предстоит сыграть значительную роль в главе 8 «Электронная почта», а сейчас перейдем к более сложным службам каталогов. Мы уже начали этот переход, переключаясь со службы Finger на WHOIS. Между рассмотренными способами использования Finger и WHOIS существует важное различие - структура.
    Вывод Finger отличается от реализации к реализации. И хотя существуют некоторые соглашения, форму он имеет свободную. WHOIS-cepвер InterNIC/Network Solutions возвращает данные более постоянной структуры. Можно рассчитывать на то, что у каждой записи будут, по крайней мере, поля Name, Address и Domain. Модуль Net: :Whois полагается на эту структуру и анализирует результаты, разбивая их на поля. Существует еще один модуль Випула Вед Пракаша (Vipul Ved Prakash) -
    Net: :Xwhois, который делает шаг вперед, обеспечивая интерфейс для анализа информации, по-разному отформатированной различными WHOIS-серверами.
    И хотя в протоколе WHOIS нет никакого упоминания о полях, вызываемые нами модули начинают полагаться на структуру информации. Службы каталогов, о которых пойдет речь, более серьезно относятся к этой структуре.


    Службы каталогов Что такое каталог?

    Что такое каталог?

    В главе 7 «Администрирование баз данных SQL» мною сделано предположение, согласно которому мир системного администрирования представляет собой базу данных. Каталоги - хороший пример такой оценки. Отметим некоторые явные характеристики каталогов, чтобы в дальнейшем разговоре различать «базы данных» и «каталоги»:
    Использование сети
    Каталоги практически всегда связаны сетью. В отличие от некоторых баз данных, расположенных на той же машине, что и их клиенты (как хорошо известный файл /etc/passwd), службы каталогов
    обычно предоставляются по сетям.
    Простое взаимодействие/манипуляция данными
    Базы данных зачастую имеют сложные языки запросов для получения и обработки данных. Самый распространенный из них - SQL, ему уделено внимание в главе 7 и приложении D «Пятнадцатиминутное руководство по SQL». Взаимодействие с каталогами значительно проще. Клиент каталогов обычно выполняет только элементарные операции и не использует для работы с сервером никакого специального языка.
    Иерархичность
    Современные службы каталогов поддерживают древоподобные информационные структуры, в то время как базы данных в целом - нет.
    Читаем много, пишем мало
    Современные службы каталогов оптимизированы под очень специфичную передачу данных. При обычном использовании количество операций чтения/запросов к службе каталогов во много раз превосходит количество операций записи/обновлений.
    Если вам встретится нечто, похожее на базу данных, но обладающее приведенными выше характеристиками, то, скорее всего, вы имеете дело с каталогом. Во всех четырех службах каталогов, которые мы рассмотрим, эти характеристики несложно заметить.


    Сравнение двух LDAPмодулей

    Таблица 6.1. Сравнение двух LDAP-модулей

    Возможность Net::LDAP Mozilla::LDAP (PerLDAP)
    Переносимость Зашифрован-
    ные SSL-сеансы
    Асинхронные
    операции
    Только Perl Да
    Да
    Требует Mozilla/Netscape LDAP C-SDK (исход-
    ный код свободно доступен). SDK компилиру-
    ется на многих вариантах Unix, NT и MacOS Да
    Только с не объектно-ориентированными API
    низкого уровня
    Оба модуля обладают функциональностью, необходимой для выполнения обсуждаемых ниже простых задач, связанных с системным администрированием, но предоставляют ее немного различными способами. Это .создает, с точки зрения обучения, редкую возможность увидеть, как два разных автора реализовали важные модули, применимые в одной и той же области. Скрупулезное сравнение обоих модулей поможет разобраться с процессом их создания, что и будет показано в
    главе 10 «Безопасность и наблюдение за сетью». Для облегчения сравнения в большей части примеров из этого раздела приведен синтаксис обоих LDAP-модулей. Строка use modulename в тексте каждого примера подскажет, какой модуль используется на этот раз.
    В демонстрационных целях мы почти равнозначно будем использовать коммерческий сервер Netscape 4.0 Directory Server и свободно распространяемый сервер OpenLDAP (они находятся на http://www.netscape.com и http:// www.openldap.org). В состав обоих серверов входят практически идентичные утилиты командной строки, которые можно использовать для прототипирования и проверки программ на Perl.

    Операторы сравнения LDAP

    Таблица 6.2. Операторы сравнения LDAP

    Оператор Значение
    = Точное совпадение значений. Может означать и частичное совпадение, если в определении используется * (например cn=Tiin 0*).
    =* Соответствует всем элементам, у которых есть значения для атри-
    бута , независимо от того, каковы эти значения.
    Если вместо указать *, будет проверяться нали-
    чие именно этого атрибута в элементе (например, сп=* выберет эле-
    менты, у которых есть атрибуты сп).
    -= Приблизительное совпадение значений.
    >= Больше либо равно значению.
    <= Меньше либо равно значению.
    Это очень похоже на Perl, но не заблуждайтесь. Две конструкции, которые могут смутить знатоков Perl, это ~= и =*. Первая из них не имеет ничего общего с регулярными выражениями; она ищет приблизительное соответствие с указанным значением. В этом случае определение
    «приблизительное» зависит от сервера. Большинство серверов применяют алгоритм, первоначально используемый в soundex для определения совпадающих значений при поиске слов, которые «произносятся, как» заданное значение (в английском языке), но записываются иначе.
    Другая конструкция, которая может конфликтовать с вашими знаниями Perl, - это оператор =. Помимо проверки точного совпадения значений (как строковых, так и численных), оператор = можно использовать вместе с символом * в виде префикса или суффикса в качестве символов подстановки, подобно тому как это происходит в командных интерпретаторах. Например, сл=а* получит все элементы, имена которых (common name) начинаются с буквы «а». Строка сп=*а* выполнит именно то, чего вы ждете, и найдет все элементы, в атрибуте ел которых есть буква «а».
    Можно объединить в одну строку два или более простых фильтра Ottribute пате>, , ottribute value> при помощи логических операторов, создав таким образом более сложный фильтр.
    Он имеет следующий вид:
    ( ()
    () () ... )
    Те, кто знаком с LISP, без труда разберутся с подобным синтаксисом; всем остальным придется просто запомнить, что оператор, объединяющий простые формы поиска, записывается первым. Чтобы найти элементы, удовлетворяющие обоим критериям поиска А и В, нужно использовать запись (&(А)(В)). Для элементов, удовлетворяющих критериям А или В или С, следует применить (|(А)(В)(С)). Восклицательный знак отрицает указанный критерий: так, А и не В записывается следующим образом: (&(А)(!В)). Составные фильтры можно объединять друг с другом, чтобы создавать фильтры поиска произвольной сложности. Вот пример составного фильтра для поиска всех Финкель-
    штейнов, работающих в Бостоне:
    (&(sn=Finkelstein)(l=Boston))
    Следующий фильтр ищет человека, чья фамилия либо Финкельштейн, либо Хайндс:
    (|(sn=Finkelstein)(sn=Hinds))
    Для поиска всех Финкелыптейнов, работающих не в Бостоне:
    (&(sn=FinKelstein)((l=Boston)))
    Для поиска всех Финкелыптейнов или Хайндсов, работающих не в Бостоне:
    (&(|(sn=Finkelstein)(sn=Hinds))(!l=Boston))
    Тот, кто захочет поэкспериментировать с алгоритмом soundex, может воспользоваться модулем Марка Милке (Mark Mielke) Text:: Soundex. LDAP:
    Вот два примера программ, принимающих имя LDAP-сервера и фильтр и возвращающих результаты запроса:
    use Mozilla::LDAP::Conn;
    Sserver = $ARGV[0];
    Sport = getservbyname("ldap","tcp") || "389"
    Sbasean = "c=US";
    Sscope = "sub";
    $c = new Mozilla::LDAP::Conn($server, Sport, "",""); анонимное соединение die "Невозможно связаться с $server\n" unless $c;
    Sentry = $c->search($basedn, Sscope, $ARGV[1]);
    die "Ошибка поиска: ". Sc->getErrorString(),"\n" if Sc->getErrorCode()
    обрабатываем полученные от search() значения while (Sentry) {
    $entry->printLDIF();
    Sentry = $c->nextEntry(); } $c->close();
    use Net::LDAP;
    use Net::LDAP::LDIF;
    Sserver = $ARGV[0];
    Sport = getservbyname("ldap","tcp") | "389";
    Sbasedn = "c=US";
    Sscope = "sub";
    $c = new Net::LDAP($server, port=>$port) or
    die "Невозможно соединиться с Sserver: $@\n"; $c->bind() or die "Unable to bind: $@\n"; анонимное соединение
    Ssearchobj = $c->search(base => Sbasedn, scope => Sscope,
    filter => $ARGV[1]);
    die " Неуспешный поиск, номер ошибки и",Ssearchobj->code() if Ssearcnobj- >code();
    ft обрабатываем полученные от search() значения if (Ssearchobj){
    Sldif = new Net::LDAP::LDIF("-");
    $ldif->write($searchobj->entries());
    $ldif->done();
    А вот отрывок из получаемых данных:
    $ Idapsrch ldap.bigfoot.com '(sn=Pooh)'
    dn: cn="bear pooh", mail=poohbear219(s>hotmail. com. c=US,o=hotmail. com
    mail: poohbear219iahotmail.com
    en: bear pooh
    o: hotmail.com
    givenname: bear
    surname: pooh
    Перед тем как улучшить этот пример, посмотрим на код, обрабатывающий результаты, полученные от search(). Это одно из тех мест, где модули отличаются моделью программирования. Оба примера возвращают одну и ту же информацию в формате LDIF (LDAP Data Interchange Format, формат обмена данными LDAP), о котором речь пойдет позже, но данные они получают совершенно разными способами.
    Модель Mozilla: : LDAP остается справедливой для подпрограмм анализа поиска, описанных в спецификации С API в RFC1823. Если поиск был успешным, возвращается первый найденный элемент. Для просмотра результатов необходимо последовательно запросить следующие эле-
    менты. Вывод содержимого каждого получаемого элемента выполняет метод printLDIF().
    Модель программирования Net: : LDAP имеет больше сходства с определением протокола из RFC2251. Результаты поиска LDAP возвращаются в объекты сообщений. Для получения списка элементов из этих пакетов в предыдущем примере использовался метод entries(). Вывод
    всех элементов вместе выполняет метод из смежного модуля Net:: LDAP: :LDIF. Для последовательного вывода всех элементов, как было с printLDIF() в первом примере, можно использовать похожий метод write(), но показанный выше вызов более эффективен.
    Немного поработаем с предыдущим примером. Как уже отмечалось в данной главе, поиск можно выполнять быстрее, ограничив количество возвращаемых в результате атрибутов. С модулем Mozilla: : LDAP это настолько же просто, насколько просто добавить дополнительные параметры в вызов метода search():
    use Mozilla::LDAP::Conn;
    Sentry = $c->search($basedn,$scope,$ARGV[1],0,@attr):
    Первый дополнительный параметр - это логический флаг, определяющий, будут ли значения атрибутов опущены в результатах поиска. Значение по умолчанию - ложь (0), т. к. в большинстве случаев нас интересуют не только имена атрибутов.
    Следующий дополнительный параметр - это список имен возвращаемых атрибутов. Знатоки Perl заметят, что список внутри списка интерполируется, так что последняя строка эквивалентна строке (ее можно так и прочитать):
    Sentry = $c->search($basedn,$scope,$ARGV[1],0,$attr[0],$aTtr[1].$attr[2]. ..):
    Если мы изменим строку первоначального примера:
    Sentry = $c->searcn($basedn.Sscope,$ARGV[1]);
    на:
    @attr = qw(mail);
    Sentry = $c->search($basedn,Sscope,$ARGV[l],0,@attr);
    то получим следующий результат, в котором для элемента будут показаны только атрибуты DN и mail:
    dn: cn="bear pooh",mail=poohbear219@notmail.com,c=US,o=hotmail.com mail: poohbear219@hotmail.com
    Изменения, которые необходимо внести, чтобы получить определенные атрибуты средствами Net: : LDAP, тоже не сложны:
    use Net::LDAP;
    # можно было бы добавить "typesonly => 1" для получения только
    # типов атрибутов, как и в предыдущем случае для первого
    # необязательного параметра
    Ssearchobj = $c->search(base => Sbasedn, filter => $ARGV[1], attrs => \@attr);
    Обратите внимание, что Net: : LDAP принимает ссылку на массив, а не сами значения массива, как в случае с Mozilla: : LDAP.

    Методы Mozilla LDAP Entry

    Таблица 6.3. Методы Mozilla::LDAP::Entry

    Вызов метода Возвращает
    Sent ry- >exists( $att r name ) true, если элемент имеет атрибут с таким именем
    $entry->hasValue($attrname,$att rvalue) true, если элемент имеет названный атрибут с указанным значением
    $entry->matchValue($attrnamet $att rvalue Так же, как и предыдущий, только ищется соответствие регулярному выражению, определенному в качестве значения атрибута
    $entry->size($attrname) Количество значений этого атрибута (обычно 1, если только атрибут не обладает несколькими значениями)
    Отдельные методы имеют дополнительные параметры, узнать о которых можно в документации по Mozilla:: LDAP:: Ent ry.
    Из программы видно, что методы для доступа к атрибутам элементов в Net:: LDAP несколько отличаются. После проведения поиска все результаты инкапсулируются в один объект. Получить отдельные атрибуты каждого элемента из этого объекта можно, применив один из двух способов.
    Во-первых, модуль может преобразовать все полученные элементы в одну большую структуру данных, доступную пользователям. Ssearchobj ->as_struct() возвращает структуру данных, представляющую собой хэш хэшей списков. Метод возвращает ссылку на хэш, ключами которого являются DN-имена полученных элементов. Значения ключей - это ссылки на анонимные хэши, ключами которых являются имена атрибутов. Ключам соответствуют ссылки на анонимные массивы, содержащие значения данных атрибутов (Рисунок 6.1).
    Вывести первое значение атрибута сп для всех элементов из структуры данных позволяет такой код:
    $searchstruct = $searchobj->as_struct; for (keys %$searchstruct){
    print $searchstruet->{$._}{en}[0], "\r";
    Можно также сначала использовать один из этих методов и выделить объекты для отдельных элементов из объекта, возвращаемого в результате поиска:
    Методы Mozilla LDAP Entry

    Рисунок 6.1. Структура данных, возвращаемая методом as_struct()
    возвращает указанный элемент
    Sentry = $searchob]->entry($entrynum);
    действует подобно shift() в Perl для списка элементов Sentry = $searchobj->shift_entry;
    действет подобно рор() в Perl для списка элементов Sentry = $searchobj->pop_entry;
    возвращает все элементы в виде списка ©entries = $searcnobj->entries;
    После того как получен объект элемента, можно применить один из указанных методов (табл. 6.4).


    Методы элементов Net LDAP

    Таблица 6.4. Методы элементов Net::LDAP

    Вызов метода Возвращает
    $entry->get($attrname)
    $entry->attributes()
    Значение атрибута в указанном элементе
    Список имен атрибутов для этого элемента
    Можно объединить эти методы в довольно четкую цепочку. Например, следующая строка получает значение атрибута сп первого возвращаемого элемента:
    Svalue = $searchobj->entry(1)->get(cn)
    Теперь, когда вы умеете получать доступ к отдельным атрибутам и значениям, возвращаемым в результате поиска, посмотрим, как поместить подобные данные в каталог сервера.

    Методы изменения элементов в Mozilla LDAP

    Таблица 6.5. Методы изменения элементов в Mozilla::LDAP

    Метод действие
    $entry->addValue($attrname, Sattrvalue) Добавляет указанное значение заданному атрибуту в указанном элементе.
    $entry-> removeValue($attrname! Sattrvalue) Удаляет указанное значение для заданного атрибута указанного элемента. Если это значение единственное для атрибута, то удаляется и весь атрибут.
    $entry-> setValue($attrname, $attrvalue1,...) Изменяет значения указанного атрибута в заданное значение или значения.
    $entry-> rerTTOve(Sattrname) Удаляет указанный атрибут (вместе со значениями) из элемента.
    После того как внесены все изменения элементов (при помощи перечисленных методов), нужно вызвать метод update() для данного LDAP-соединения, чтобы распространить эти изменения на сервер каталогов. update() вызывается со ссылкой на элемент в качестве аргумента (т. е. $cupdate($entry)).
    Применим эти методы для глобального поиска и замены. Рассмотрим такой сценарий: один из отделов вашей компании переводят из Бостона в Индиану. Эта программа изменит все элементы, местоположением которых является Бостон:
    use Mozilla::LDAP::Conn;
    Sserver = $ARGV[0];
    Sport = getservbyname("ldap","tcp") || "389";
    Sbasedn = "dc=ccs,dc=hogwarts,dc=edu";
    Sscope = "sub";
    Srootdn = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
    $pw = "secret";

    неанонимное соединение $c = new Mozilla;:LDAP::
    Conn(Sserver,Sport.Srootdn,$pw);
    die "Невозможно соединиться с сервером
    $server\n" unless $c;
    tt
    обратите внимание, что мы запрашиваем как можно меньше информации для ускорения поиска Sentry = $c->search($Pasedn, Sscope, "(l=Boston)", 1, ");
    die "Ошибка поиска;". $c->getErrorStnng().
    "\n" if $c->gettrrorCode();
    if ($entry){ . - -while(Sentry)!
    $entry->removeVali;e("l". "Boston");
    $entry->addValue("l", "Indiana");
    $c->update($entry);
    die 'Ошибка при обновлении:" .
    $c->getErrorString() . "\n"
    if $c-'getErrorCode(); Sentry = $c->nextEntry(); }; }
    $c->close();
    Для изменения элементов в Net: : LDAP применяется другой подход. В нем все только что рассмотренные методы модуля Mozilla:. LDAP объединены в одном «суперметоде» modify(). Параметры, передаваемые этому методу, и определяют его функциональность (табл. 6.6).


    Методы для изменения элементов в Net LDAP

    Таблица 6.6. Методы для изменения элементов в Net::LDAP

    Параметр Действие
    add => {Sattrname => Sattrvalue} Добавляет указанный элемент с заданным значением.
    add => {Sattrname => [$attrvalue1, $attrvalue2. . . ]} Добавляет указанный атрибут с заданным набором значений.
    delete => {Sattrname => Sattrvalue} I Удаляет указанный атрибут с заданным значением.
    delete => {Sattrname => []} delete => [Sattrnamel, $attrname2. . . ] replace => {Sattrname => Sattrvalue}
    Удаляет атрибут или набор атрибутов независимо от их значений. Действует, как add, только заменяет текущее значение указанного атрибута. Если Sattrvalue является ссылкой на пустой анонимный список ([]), метод становится синонимом для приведенной выше операции удаления.
    Обязательно обратите внимание на знаки пунктуации в предыдущей таблице. Некоторые параметры принимают ссылки на анонимные хэши, другие - на анонимные массивы. Если их перепутать, трудностей не миновать.
    Можно объединять несколько таких параметров в одном и том же вызове modify, но это представляет собой потенциальную проблему. Когда modify вызывается с набором параметров, например, так:
    $c->modify($dn, replace => {'!' => "Medfora"},
    add => {'1' =N "Boston"). add => {'1 => "Cambridge"});
    нет никаких гарантий, что указанные операции добавления будут выполняться после замены. Если необходимо, чтобы операции выполнялись в определенном порядке, можно применять синтаксис, подобный только что рассмотренному. Вместо использования набора дискретных параметров можно передать единственный массив, содержащий очередь команд. Вот как это работает: Tiod:fy() принимает параметр changes, значение которого- список. Данный список считается набором пар. Первая половина пары - это операция, которую необходимо выполнить, вторая половина - ссылка на анонимный массив, содержащий данные для этой операции. Например, если мы хотим гарантировать, что операции из предыдущего фрагмента кода выполнятся в нужном порядке, то можем написать:
    $c->Tiodify($dn. changes =>
    [ replace -;" ['!' => "Kedfrr'd"].
    add --> ['!' => "Boston"],
    add =>['!' => "Caubndge"]
    ]);
    Внимательно посмотрите на пунктуацию: она отличается от других параметров, которые приводились раньше.
    Учитывая информацию, передаваемую функции modify(), можно переписать для Net: : ЮАР предыдущую программу, меняющую Бостон на Индиану:
    use Net::LDAP;
    $server = $ARGV[0];
    Sport = getservbynameC'ldap", "tcp") |j "389";
    Sbasedn = "dc=ccs,dc=hogwarts,c!c=edu";
    $scope = "sub";
    Srootdn = "c!i=Manager, ou=Syste'ns, dc=ccs, dc-hogwarrs, do-edu".
    $pw = "secret",
    $c = new Net::LDAP($server, port => Sport) or
    die "Невозможно соединиться с сервером Ssorver
    $«>'\n": $c->bind(dn --> S'ootrin. password => $pw) or
    die "Ошибка при соедимении; $@\n";
    Ssearchob] = $c->search(base => Soasedn, fiiiei => "(l-Bosion)".
    scope => $scope, attrs -s [''}.
    typeso.il у => 1): dio "Ошибка поиска: ". Ssear-chonj->er! or
    if (SsearchobJ){
    (Sentries = $searcnopj->entries;
    ОГ ( aPI't ' .OS ) {
    Собираем все вместе
    Теперь, когда нам известны все основные LDAP-функции, напишем несколько небольших сценариев для системного администрирования. Мы импортируем базу данных машин из главы 5 «Службы имен TCP/IP» на сервер LDAP и затем сгенерируем некую полезную информацию, основываясь на LDAP-запросах. Вот пара выдержек из этого простого файла (для того только, чтобы напомнить вам формат):
    name: shimmer
    address: 192.168.1.11
    aliases: shim shimmy shimmydoodles
    owner: David Davis
    department: software
    building: main
    room: 909
    manufacturer: Sun
    model: UltraGO
    name: bendir address: 192.168.1,3 aliases:
    ben bendoodles owner: Cindy Coltrane department:
    IT building: west room: 143
    manufacturer: Apple model: 7500/100
    Первое, что нужно сделать, приготовить сервер каталогов для приема этих данных. Мы будем использовать нестандартные атрибуты, так что придется обновить схему сервера. Различные серверы выполняют это по-разному. Например, сервер каталогов Netscape имеет симпатичную графическую консоль Directory Server Console для подобных изменений. Другие серверы требуют внесения изменений в текстовые конфигурационные файлы. Работая с OpenLDAP, можно использовать нечто подобное в файле, включенном основным конфигурационным файлом для определения собственных пользовательских классов объектов для машины:
    objectclass machine requires-:
    ,-• п ullOrtS
    a-:a;;es building, room.
    manufacturer, model
    После того как сервер настроен нужным образом, можно подумать об импортировании данных. Один из вариантов - провести загрузку большой единой операцией с помощью LDIF. Если приведенный выше отрывок из базы данных напомнил вам о формате LDIF, значит, вы на правильном пути. Эта схожесть упрощает преобразование. Тем не менее, нужно остерегаться ловушек:
    Продолжающиеся строки
    В нашей базе данных нет элементов, значения которых занимали бы несколько строк, иначе следовало бы убедиться, что вывод удовлетворяет стандарту LDIF. Стандарт LDIF требует, чтобы все длинные строки начинались строго с одного пробела.
    Разделители элементов
    Между элементами в базе данных в качестве разделителя используется симпатичная последовательность -=-. Два разделителя строк (т. е. пустая строка) должны находиться между элементами LDIF, так что нужно будет удалить эту последовательность из вводимых данных.
    Разделители атрибутов
    В настоящее время в наших данных есть только один атрибут с несколькими значениями: aliases (псевдонимы). LDIF обрабатывает многозначные атрибуты, перечисляя каждое значение на отдельной строке. Если встретится несколько атрибутов, то понадобится специальный код, печатающий для каждого значения отдельную строку. Если бы не эта особенность, программа, преобразующая наш формат в LDIF, представляла бы собой одну строку кода на Perl.
    Но даже и с этими ловушками программа преобразования на удивление проста:
    Sdatafile = "database";
    Srecordsep = "-=-\n";
    Ssuffix = "ou=data, ou=systems, dc=ccs. dc=hogwarts. dc=edu";
    Sobjectclass = «EOC;
    objectclass: top
    objectclass: machine
    EOC
    open(DATA, Sdatafile) or aie "Невозможно открыть Sdataf ile: $''.n";
    Модули Perl не работают с зги», даже если в специфики-;'!' while () {
    ft выводим заголовок для каждого элемента if (/name:\s-(.-)/){
    print "arr сп=$1, $suffix\n":
    print Soijjoctclass;
    print "en: $1\n":
    next: I tt обрабатываем многозначный атрибут aliases
    if (s/~aliases:\s*//){
    @aliases = split:
    foreach $name (@aliases){ print "aliases: $name\n";
    }
    next; }
    ft обрабатываем конец разделителя записей if ($_ eq $recordsep){
    print "\n";
    next; }
    ft в противном случае просто печатаем найденный атрибут print;
    close(OATA);
    Если выполнить эту программу, то она выведет файл LDIF,
    выглядящий примерно так:
    dn: cn=shimmer, ou=data, ou=systems, dc=ccs, dc=hogwarts. dc=edu
    objectclass: top
    objectclass: machine
    en: shimmer
    address: 192.168.1.11
    aliases: shim
    aliases: shimmy
    aliases: shimmydoodles
    owner: David Davis
    department: software
    building: main
    room: 909
    manufacturer: Sun
    model: UltraGO
    dn: cn=bendir, ou=data. Ob=systems, ac=ccs, dc=hogwarts, dc=edu
    objectclass: top
    objectclass: machine
    en: bendir
    address: 192.168. 1.3
    aliases: ben aliases: bendoodles owner: Cindy Colt rant; department:
    building1 west room: 143
    manufacturer: Apple model: 7500/100
    Имея этот LDIF-файл, можно применять одну из программ, распространяемых с сервером, для загрузки этих данных на сервер. Например, Idif2ldbm, входящий в состав обоих серверов OpenLDAP и Netscape Directory Server, считывает LDIF-файл и напрямую импортирует его в формат сервера каталогов, избавляя от необходимости проходить через LDAP. Хотя эта программа используется только при неработающем сервере, она может обеспечить самый быстрый способ загрузки большого количества данных. Если нельзя остановить сервер, можно применить только что написанную на Perl программу для чтения LDIF-файлов и передать подобный файл на LDAP-сервер.
    Добавим еще один способ: вот программа, в которой пропускается промежуточный шаг создания LDIF-файла, и наши данные напрямую импортируются на LDAP-сервер:
    use Net::LDAP;
    use Net::LDAP::Entry;
    Sdatafile = "database";
    Srecordsep = "-=-":
    Sserver = $ARGV[0];
    Sport = getservbynameC'ldap","tcp") || "389";
    Ssuffix = "ou=data, ou=systems, dc=ccs, dc=hogwarts, dc=edu";
    $rootdn = "cn=Manager. o=University of Michigan, c=US";
    $pw = "secret";
    $c = new Net::LDAP($server.port => Sport) or
    die "Невозможно соединиться с сервером Sserver: $@\n";
    $c->bind(dn => Srootdn. password => Spw) or die "Ошибка при соединен;',
    open(DATA,Sdatafile) or die "Невозможно открыть $datafile:$!\n";
    while () {
    chomp;
    в
    начале новой записи создаем if (,/"nare:\S'(. •)/'){
    $d^="cn=$1. Ssbffix":
    Sentry = new fiet: : LDAP 'E:"t'\.
    $еп:г^,-аса( с *' . $i)
    ne:xt i
    $entry-vad next }
    $entry->add( "objectciass". [ "тор", "riacii'ie" ]):
    $entry->dn($dn);
    $res = $c->add($entry);
    warn "Ошибка добавления для "
    undef Sentry;
    next; }
    ft добавляем все остальные атрибуты
    $entry->add(split(':\s*')); # считаем, что у атрибута только одно значение >
    close(DATA); $c->unbind();
    После того как данные были импортированы на сервер, можно приступить к довольно любопытным вещам. В следующих примерах мы будем поочередно обращаться к двум LDAP-модулям. Для краткости в каждом примере не будет повторяться заголовок, в котором устанавливаются конфигурационные переменные, и код для соединения с сервером.
    Так что же можно сделать с данными, расположенными на сервере LDAP? Можно на лету создать файл узлов:
    use Mozilla::LDAP;
    Sentry = $c->search($basedn,'one','(objectclass=machirie) ,0.
    'en','address','aliases'); die "Ошибка поиска:". $c->getErrorString()."\n" if $c->getErrorCode( ;.
    if (SentryM
    print "#\n\U host file - GENERATED BY $0\n
    tt
    DO NOT EDIT BY HAND!\n«\n"; while(Sentry)! print $entry->{adflress}[0]."\t". $ег!!-у->{сп}[0]," ".
    jdir(' ' .?{$e^rry->{a:iasesM V " i": $entry = Sc-'-nextEntr1, (): }; ) $o->close();
    Вот что получается:
    host file - GENERATED BY Idap2host.s
    В DO NOT EDIT BY HAND
    192.168.1.11 shimmer shim shimmy sriimmydoodles
    192.168.1,3 bendir ben bendoodles
    192.168.1.12 Sulawesi sula su-lee 192.168.1.55
    sander sandy mickey mickeydoo
    Можно найти имена всех машин, произведенных Apple:
    use Net::LDAP;
    Ssearchobj = $csearch(base => Sbasedn,
    filter => "(manufacturer=Apple)", scope => 'one', attrs => ['en']);
    die "Ошибка поиска: ".$searchobj->error()."\n" if ($searchobj->code());
    if ($searchobj){
    for ($searchobj->entries){ print $_->get('en'),"\n";
    $c->unbind();
    Вот и результат:
    bendir Sulawesi
    Можно сгенерировать список владельцев машин:
    use Mozilla::LDAP;
    Sentry = $c~>search($basedn,'one','(objectclass=machine)',0,
    'en','owner'): die "Ошибка поиска:". $c->getErrorString()."\n" if $c->getErrorCode():
    if ($entry){
    while($entry){
    push(@{$owners{Sentry->{owner}[0]}}. $er,try->{cn}[0]); Sentry = $c->nextEntry();
    };
    $c->close():
    for (sort ke>s %owners){
    ADSI (Интерфейсы служб активных каталогов) 241
    Получилось так:
    Alex Rollins: sadder
    Cindy Coltrane: oenciir
    David Davis: sfurrmer
    Ellen Monk: Sulawesi
    Заодно можно проверить, является ли владельцем машины пользователь с текущим идентификатором (псевдоаутентификация):
    use Mozilla::LDAP::Conn; use Sys::Hostname;
    $user = (getpwuid($<))[6];
    Shostname = hostname;
    $hostname =" s/"([".]+)\..«/$1/; # удаляем имя домена из имени узла
    Sentry = $c->search("cn=$hostname,$suffix",'base',"(owner=$user)",1,''):
    if ($entry){
    print "Владелец ($user) зарегистрирован на машине $hostname.\n"; } else {
    print "Suser не является владельцем этой машины ($hostname)\n."; } $c->close();
    Эти отрывки должны показать, как можно использовать доступ к LDAP из Perl для системного администрирования, и вдохновить вас, на создание собственных программ. В следующем разделе эти идеи будут перенесены на новый уровень, что позволит нам увидеть целый интерфейс администрирования, построенный на основе LDAP.

    Удаление элементов

    Удаление элементов

    Удаление элементов из каталога - это простое дело (и необратимое, так что будьте осторожны). Вот отрывок программы, из которой, для краткости, снова удален код, реализующий соединение с сервером:
    use Mozilla::LDAP::Conn;
    П если у вас есть элемент, вы можете использовать
    if $c->delete($entry->getDN()) $c->delete($dn) or
    die "Невозможно удалить элемент: ". $c->getErrorString()."\n";
    use Net::LOAP;
    $res = $c->delete($dn);
    die "Невозможно удалить, код ошибки #".
    $res->code() if $res->code();
    Важно обратить внимание на то, что в обоих модулях delete ()удаляет по одному элементу за один раз. Если необходимо убрать поддерево целиком, сначала следует найти все дочерние элементы этого поддерева, используя пространство sub или one, а затем обойти в цикле возвращаемые значения, удаляя элементы на каждой итерации. После того как уничтожены дочерние элементы, можно удалить вершину этого поддерева.

    Выполнение поиска в LDAP

    Выполнение поиска в LDAP

    Буква «D» в LDAP означает Directory (т. е. каталог), и наиболее распространенной операцией с каталогами является поиск. Для начала знакомства с LDAP неплохо выяснить, как искать информацию. Поиск в LDAP определяется такими понятиями:
    Откуда начинать поиск
    Это называется базовым относительным именем (base DN) или базой поиска (search base). Базовое DN-имя представляет собой всего лишь DN-имя элемента в дереве каталогов, от которого начинается поиск.
    Где искать
    Это называется пространством (scope) поиска. Пространство может быть трех видов: base (поиск только по базовому DN-имени), one (поиск по уровню, лежащему непосредственно под базовым DN-
    именем, не включая само базовое DN-имя) или sub (поиск по базовому DN-имени и всему дереву, лежащему ниже).
    Что искать
    Это называется фильтрами поиска (search filter). О фильтрах и их определении мы поговорим очень скоро.
    Что возвращать
    С целью ускорения операций поиска можно выбрать, какие атрибуты должны возвращаться для каждого элемента, найденного при помощи фильтров поиска. Кроме того, можно запросить, чтобы возвращались только имена атрибутов, а не их значения. Это полезно, когда нужно узнать, у каких элементов есть данные атрибуты, но совсем не важно, что эти атрибуты содержат.
    В Perl поиск выглядит примерно так (процесс соединения заменен многоточиями):
    use Mozilla::LDAP::Conn;
    Sentry = $c->search($basedn, Sscope, $fiiter)
    die "Неуспешный поиск: ". $c->getErrorString()."\n" if $c->getErrorCode():
    или:
    use Net::LDAP;
    Ssearchobj = $c->search(base => Sbasedn, scope => $scope.
    filter => $filter);
    die "Неуспешный поиск, номер ошибки #".
    Ssearchobj->code() if Sseatchooj- >code();
    Перед тем как перейти к полному примеру, поговорим о загадочном параметре $fliter. Простые фильтры поиска имеют следующий вид:
    Comparison operator> ottribute value>
    где определяется в RFC2254 как один из операторов, перечисленных в табл. 6.2.


    Выполнение распространенных задач при помощи пространства имен WinNT и LDAP

    Выполнение распространенных задач при помощи пространства имен WinNT и LDAP

    Теперь, когда мы разобрались со «списком» сложностей, можно перейти к выполнению некоторых распространенных задач системного администрирования, используя ADSI из Perl. Цель - дать понять, ка кие задачи можно решать при помощи представленной информации об ADSL Затем рассмотреть и использовать код, который пригоден для написания собственных программ.
    Для этих целей будет использоваться одно из двух пространств имен. Первое пространство - WinNT, которое предоставляет доступ к объектам Windows NT 4.0, таким как пользователи, группы, принтеры, службы и т. д.
    Второе - это наш старый знакомый - LDAP. LDAP мы выбираем про вайдером при переходе к Windows 2000 и ее службе Active Directory, основанной на LDAP. Большинство объектов WinNT также доступны через LDAP. Ведь даже в Windows 2000 существуют задачи, которые можно выполнить, только используя пространство имен WinNT (например, создание учетных записей на локальной машине).
    Программы, работающие с этими различными пространствами имен, похожи друг на друга (в конце концов, частично в этом и заключается смысл применения ADSI), но необходимо обратить внимание на два важных различия. Во-первых, формат ADsPath немного отличается. В соответствии с ADSI SDK, ADsPath в WinNT может иметь следующий вид:
    WinNT: [//DomainNarre[/Cor;puterName[/'Ou;iectNa!T!e[. classNarne] ]]]
    WinNT: [//DomainName[/ObjectNa.ne[, CjassNane]]]
    WinNT:[//ComputerName,computer] WinNT:
    ADsPath в LDAP выглядит так:
    LDAP://HostName[: PortNumberj[/DisTinguisneaNar;e]
    Обратите внимание, что при работе с NT 4 ADsPath в LDAP требует указывать имя сервера (в Windows 2000 это изменилось). Это означает, что пространство имен LDAP нельзя просмотреть с верхнего уровня, как пространство WinNT, т. к. необходимо указать начальный сервер. В пространстве имен WinNT любой может применить ADsPath или просто Wr'NT. для начала поиска в иерархии доменов. Также обратите внимание, что свойства объектов в двух пространствах имен похожи, но не идентичны. Например, можно обратиться к одним и тем же объектам из обоих пространств имен WinNT и LDAP, но обратиться к некоторым свойствам Active Directory конкретного объекта пользователя можно только через пространство имен LDAP.
    Особенно важно заметить различия между схемами в этих двух пространствах имен. Например, класс Use для WinNT не имеет обязательных свойств, тогда как класс User в LDAP требует наличия свойств samAccountNarne в каждом объекте пользователя.
    Не забывая об этих различиях, посмотрим на сам код. В целях экономии места пропустим большую часть проверок ошибок, но рекомендуется запустить сценарий с ключом -w и добавить в текст программы примерно такие строки:
    die "Ошибка OLE: ".Win32: :OLE->LastError()
    if Win32: :OLE->Lar,tError():

    WHOIS

    WHOIS

    ftp://sipb.mit.edu/pub/whois/whois-servers.list
    - это список наиболее крупных WHOIS-серверов. «RFC954:NICNAME/WHOIS»,
    К. Harrenstien, M. Stahl, and E. Fein ler, 1985.

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

    Администрирование баз данных SQL

    Взаимодействие с SQL-сервером из Perl

    Существует два стандартных способа взаимодействия с SQL-сервером: DBI (DataBase Interface) и ODBC (Open DataBase Connectivity). Когда-то DBI был стандартом Unix, a ODBC - стандартом Win32, но эти границы начали расплываться после того, как ODBC стал доступным в мире Unix, a DBI был перенесен на Win32. Еще сильнее стирает эти границы пакет DBD: :ODBC - DBD-модуль, «говорящий» на ODBC из DBI.
    DBI и ODBC очень похожи как в своем предназначении, так и в использовании, поэтому рассматриваться они будут одновременно. И DBI, и ODBC можно считать «промежуточным программным обеспечением» (middleware). Они создают уровень абстракции, позволяющий программисту писать программы, применяя вызовы DBI/ODBC, не имея представления об API какой-либо конкретной базы данных. Передать эти вызовы на уровень, зависящий от баз данных, дело DBI ODBC. Модуль DBI обращается для этого к драйверу DBD; менеджер ODBC вызывает зависящий от источника данных ODBC драйвер, который заботится обо всех частностях, необходимых для соединения с сервером. В обоих случаях получается по меньшей мере трехъярусная модель:
  • Лежащая в основе система управления базами данных (Oracle, MySQL, Sybase, Microsoft SQL Server и т. д.).
  • Уровень, зависящий от базы данных, который выполняет созданные программистом запросы к серверу. Программисты не работают с этим уровнем напрямую; они используют третий ярус. В DBI с этим уровнем имеет дело специальный модуль DBD. Для взаимодействия с базой данных Oracle будет применяться модуль DBL): : Огчс : В процессе компилирования модули DBD обычно связываются с клиентской библиотекой данного сервера, поставляемой создателем сервера. В ODBC с этим уровнем работает ODBC-драйвер, зависящий от источника данных и устанавливаемый поставщиком.
  • Мост над пропастью между базами данных Unix и NT/2000
    Очень часто системные администраторы, работающие в многоплатформенном окружении, задают вопрос: «Как я могу использовать Microsoft SQL Server из Unix?» Если центральная система администрирования или наблюдения построена на Unix, то установка нового MS-SQL-сервера представляет собой трудную задачу. Я знаю три способа справиться с этой ситуацией. Второй и третий способы не зависят от SQL-сервера, поэтому даже если вы применяете не Microsoft SQL-сервер, однажды они могут вам пригодиться.
  • Скомпилируйте и используйте DBD: : Sybase. Модуль DBD: : Зуиазн потребует некоторых библиотек для соединения с базой данных. Существует два набора библиотек, а этого более чем достаточно. Первый набор - библиотеки Sybase OpenClient - может быть доступен для вашей платформы (так, они бесплатно распространяются с некоторыми дистрибутивами Linux как часть пакета Sybase Adaptive Server Enterprise). Если вы используете MS-SQL-сервер версии 6.5 или ниже, то DBD: :Sybase, собранный с этими библиотеками, будет работать. Если это сервер версии 7.0 или выше, для совместимости может понадобиться «файл-заплата» от Microsoft. Информацию о нем следует искать на http://su.pport.Microsoft.com/suppor-t/kb/articlcti/ q239/8/83.asp (KB статья Q239883). Второй вариант - установить библиотеки FreeTDS, которые можно найти на http:// www.freetds.org. Изучите инструкции на этом сайте, чтобы собрать нужную версию для своего сервера.
  • Используйте OBD: :Proxy. Этот модуль DBD входит в состав DBI. Он позволяет на машине с MS-SQL-сервером запустить дополнительный маленький, который будет служить прозрачным прокси-сервером для запросов от Unix-клиентов.
  • Получите и применяйте Unix ODBC из DBD; :ODBC. Некоторые разработчики, включая MERANT (http://www.merant.com) и OpenLink Software (http://www.openlinksw.com), продают такое программное обеспечение; но стоит попытаться использовать то, что было создано разработчиками из проекта Open Source. Подробную информацию можно найти на странице freeODBC Брайана Джепсона (Brian Jepson) на http://users.ids.net/-bjep son/freeODBC. Вам понадобится и драйвер ODBC для Unix-платформы (разработанный производителем базы данных) и менеджер ODBC (подобный urixODBC или iGDBC).
  • Уровень независимого от базы данных интерфейса прикладного программирования (API). Очень скоро мы будем писать сценарии на Perl, взаимодействующие с этим уровнем. В DBI он известен как уровень DBI (т. е. будут выполняться DBI-вызовы). В ODBC обычно происходит взаимодействие с менеджером ODBC-драйверов через вызовы ODBC API.
  • Прелесть ситуации состоит в том, что код, написанный для DBI или ODBC, можно без осложнений переносить с одного сервера на другой. API-вызовы остаются прежними, поскольку не зависят от используемой базы данных. Эта идея остается справедливой практически во всех случаях программирования баз данных. К сожалению, программы, которые мы будем писать (т. е. предназначенные для администрирования баз данных), обречены зависеть от сервера, т. к. практически нет двух серверов, которые администрировались бы хоть отдаленно похожим образом. Опытные системные администраторы любят переносимые решения, но, увы, их не ожидают.
    Впрочем, это грустные размышления, лучше посмотрим, как использовать DBI и ODBC. Обе технологии выполняют одни и те же действия, поэтому может показаться, что в объяснениях или по крайней мере в заголовках присутствует избыточность.
    В следующем разделе мы будем исходить из того, что сервер баз данных и необходимые модули Perl уже установлены. В некоторых примерах кода для DBI будет использоваться сервер MySQL; а для ODBC -Microsoft SQL Server.

    Где выполняется вся работа?

    Где выполняется вся работа?

    Вопрос, который может возникнуть при написании SQL-программ из Perl, звучит так: «Нужно ли обрабатывать данные на сервере при помощи SQL или на клиенте при помощи Perl? » Часто SQL-функции на сервере (например, SUMO) и операторы Perl пересекаются.
    Так, было бы эффективнее использовать ключевое слово DISTINCT, чтобы удалить повторяющиеся записи из возвращаемых данных, перед тем как передавать их программе на Perl, даже если эту операцию легко можно выполнить в самом Perl.
    К сожалению, существует слишком много переменных, чтобы можно было быстро и точно решить, какой метод применять. Вот несколько факторов, которые следует учитывать:
  • Насколько эффективно сервер обрабатывает определенный запрос?
  • Сколько данных обрабатывается?
  • Сколько нужно обрабатывать данные и насколько сложна эта обработка?
  • Каковы скорость сервера, клиента и сети (если она используется)?
  • Хотите ли вы, чтобы код можно было перенести на другой сервер баз данных?
  • Зачастую приходится испытать оба способа, прежде чем сделать выбор.

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

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

    Модуль Идентификатор на CPAN Версия
    DBI TIMB 1.13
    Msql-Mysql-модули (OBD: :mysql) JWIED 1.2210
    DBD: : Sybase MEWP 0.21
    Win32 :: ODBC (с http://www.roth.net) GBARR 970208


    Использование DBI Вот что следует

    Таблица 7.1. DBI-методы получения данных

    Имя Возвращает Возвращает, если больше нет записей
    fetchrow_arrayref ( ) Ссылка на анонимный массив со значениями, являющимися полями следующей записи undef
    fetchrow_array() Массив со значениями, являющимися полями следующей записи Пустой список
    fetchrow_hashref ( ) Ссылка на анонимный хэш, ключами которого являются имена полей, а значениями - значения полей следующей записи undef
    fetchall_arrayref () Ссылка на массив массивов Ссылка на пустой массив
    Посмотрим, как работают эти методы в нашем случае. Для каждого из примеров будем считать, что перед вызовом метода выполнено следующее:
    $sth = $dbh->prepare(q{SELECT name,ipaddr,dept from nosts}) or
    die "Невозможно подготовить запрос: ".$dbh->errstr." \n":
    $sth->execute or die "Невозможно выполнить запрос: ". $dbh-->orr
    Вот метод fetchrow_arrayref () в действии:
    while (Saref = $sth->fetchrow_arrayref){ print "name: " .
    $aref~>[0] . "\n"; print "ipaddr: " . $aref->[1] . "\n";
    print "dept: " . $aref->[2] . "\n":
    А теперь рассмотрим «удобный» метод fe*chall_array"of (). Он загружает весь полученный результат в одну структуру данных, возвращая ссылку на массив ссылок. При использовании данного метода будьте осторожны и учитывайте размер запросов, поскольку результаты целиком считываются в память. Если размер результата равен 100 Гбайт, это может вызвать проблемы.
    Каждая ссылка выглядит точно так же, как и результат вызова метода fetchrow_an ayref ().
    Вот какой код выводит результат запроса целиком:
    $aref_aref = $sth->fetchall_arrayref;
    foreach $rowref (@$aref_aref){
    print "name: " . $rowref->[0] . "\n";
    print "ipaddr: " . $rowref->[1] . "\n";
    print "dept: " . $rowref->[2] . "\n";
    print '-'x30."\n": }
    Этот пример применим только к конкретному набору данных, поскольку предполагается, что количество полей фиксировано и они следуют в определенном порядке. Например, считается, что имя машины возвращается в первом поле запроса ($rowref->[0]).
    Можно переписать предыдущий пример и сделать его более общим, если использовать атрибуты (часто называемые метаданными) дескриптора команды. В частности, если после запроса посмореть на $sth->{NUM_OF_FIELDS}, то можно узнать количество полей в полученных данных. $sth->{NAME} содержит ссылку на массив названий полей.
    Обязательно изучите документацию по DBI, чтобы подробно узнать об остальных метаданных.
    Шаг 5: Закройте соединение с сервером
    С DBI это очень просто сделать: сообщаем серверу, что данные из дескриптора команды больше
    и не нужны (необязательно, т. к. мы собираемся завершить
    # соединение)
    $sth->finish;
    ft разорвать соединение дескриптора с базой данных
    $dbh->disconnect;
    Что еще надо сказать о DBI
    Осталось еще две темы, которые стоит обсудить, прежде чем переходить к ODBC. Во-первых, это набор методов, которые я называю «удобными» (shortcut methods). Методы, приведенные в табл. 7.2, объединяют перечисленные шаги 3 и 4.


    Использование ODBC Основные шаги

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

    Очень много времени и энергии уходит на настройку SQL-сервера и объектов, расположенных на нем. Способ документирования подобной информации может пригодиться в ряде ситуаций. Если база данных будет повреждена, а резервной копии не окажется, вам придется восстанавливать все ее таблицы. При необходимости перенести данные с одного сервера на другой важно знать конфигурацию источника и приемника данных. Даже для собственных баз данных может пригодиться возможность просмотреть карту таблиц.
    Чтобы передать всю «прелесть» непереносимой (nonportable) природы администрирования баз данных, приведу пример реализации одной простой задачи для трех различных SQL-серверов с использованием как DBI, так и ODBC. Каждая из этих программ делает одно и то же: выводит список всех баз данных на сервере, их таблицы и структуру каждой таблицы. Эти сценарии можно очень легко расширить для предоставления более подробной информации о каждом объекте. Например, бывает полезно узнать, в каких полях есть значения ti'J.; или NOT NULL. Вывод всех трех программ выглядит одинаково:
    — sysad'ii — hosts
    i.ame [c:;ar(30)J ipaddr [char(lb)
    aliases [char(50)]
    owner [char(40)J
    dept [char(15)]
    bldg [char(10)]
    room [char(4)]
    manuf [char(10)J
    model [char(10)] —hpotter—
    customers
    cid [char(4)J
    cname [varchar(l3)]
    city [varchar(20)J
    discnt [real(7>] agents
    aid [char(3)]
    aname [varchar(13)]
    city [varchar(20)]
    percent [int(10)] products
    pid [char(3)]
    pname [varchar(13)]
    city [varchar(20)]
    quantity [int(10)]
    price [real(7)] orders
    ordno [int(10>]
    month [char(3)]
    cid [char(4)]
    aid [char(3)]
    pid [char(3)]
    qty [int(K))]
    dollars [real(7)
    Сервер MySQL и DBI
    Вот как выглядит способ получить эту информацию с сервера MySQL с использованием DBI. Существующее в MySQL дополнение команды SHOW очень упрощает эту задачу:
    use DBI;
    print "Введите имя пользователя: ";
    chomp($user = );
    print "Введите пароль для $user: chomp($pw = )
    Sstart = "mysql"; tf первоначально будем подсоединяться к этой базе данных
    О соединяемся с базой данных
    $dbh = DBI->connect("DBI:mysql:$start".$user.$pw);
    die "Невозможно соединиться: ".$DBI::errstr."\n"
    unless (defined $don): ft ищем базы данных на сервере
    $sth=$doh->prepartj(q;SHOW DATABASES}) or
    die "Невозможно подготовить запрос show dataoaset; ".
    die "Невозмохо заполнить запрос
    push(@dcs.$a'-e*->[0]): > $sth->finish;
    # ищем таблица в каждой базе данных foreach $db (!g>dbs) { print "---$db---\n";
    $sth=$dbh->prepare(qq{SHOW TABLES FROM $db}) or
    die "Невозможно подготовить запрос
    show tables: ". $dbh->t:r rs*
    die "Невозможно выполнить запрос show tables: ".
    $dbh->crr:
    (g>tables=();
    while (Saref = $sth->fetchrow_arrayref) {
    push(Stables,$aref->[0]); }
    $sth->finish;
    tt
    ищем информацию о полях для каждой таблицы foreach Stable (^tables) { print "\t$table\n":
    $sth=$dbh->prepare(qq[SHOW COLUMNS FROM Stable FROM Sob!) o'
    die "Невозможно подготовить запрос show columns: ". $cihh-^errstr."\n";
    $sth->execute or die "Невозможно выполнить запрос show columns: ".
    while (Saref = $sth->fetchrow_arrayref) {
    print "\tAt".Saref->[0]." [". $aref->[1 ]. "]\n":
    $stn->finisn \ I
    } Sdbr->d;.scor/iec::
    Добавим несколько комментариев к приведенной программе:
  • Мы соединяемся с начальной базой данных только для того, чтобы удовлетворить принятой в DBI семантике соединения, но такой контекст возникает не обязательно благодаря командам.
  • Если читатель думает, что команды подготовки и выполнения запросов SHOW TABLES и SHOW COLUMNS являются отличными кандидатами на использование заполнителей, то он совершенно прав. К сожалению, именно эта комбинация DBD драйвера /сервера не поддерживает заполнители в таком контексте (по крайней мере, это было так в период написания данной книги). В следующем примере мы столкнемся с подобной ситуацией.
  • Имя пользователя базы данных и его пароль запрашиваются интерактивно, поскольку альтернативы (прописывание их в коде или передача в командной строке, при которых любой, кто просматривает список процессов, сможет их увидеть) еще хуже. В данном случае символы пароля будут отображаться при вводе. Для большей осторожности стоит применять что-то подобное Те г тт. : Reaakey, чтобы подавить отображение символов.


  • Мониторинг состояния сервера

    Мониторинг состояния сервера

    В качестве последнего примера рассмотрим несколько способов наблюдения за состоянием SQL-сервера. Программы такого рода по своей природе похожи на службы наблюдения за сетью, уже рассмотренные в главе 5 «Службы имен TCP/IP».

    Наблюдение за использованием процессорного времени на SQLсервере

    Наблюдение за использованием процессорного времени на SQL-сервере

    В последнем примере этой главы DBI будет выводить обновляемую раз в минуту строку состояния, содержащую информацию об использовании процессорного времени на SQL-сервере. Чтобы сделать задачу более интересной, можно из одного и того же сценария одновременно наблюдать за двумя серверами. Комментарий к сценарию последует позже:
    use DBI:
    Ssyadmin = "sa':
    print "Пароль администратора Sybase:
    chomp($sypw = ) Smsadmin = "sa";
    print "Пароль администратора базы данных MS-SOL' ";
    chomp($mspw = );
    ft соединяемся с сервером Sybase
    Ssydbh = DBI->connect("dbi:Sybase:server=SYBASE",Ssyadmin.Ssypw);
    die "Невозмржно соединиться с сервером
    Sybase: $D8I::errstr\n" unless (defined Ssydbh);
    # включаем параметр ChopBlanks, чтобы удалить концевые пробелы из столбцов
    $sydbh->{ChopBlanks} = 1;
    п
    соединяемся с сервером MS-SOL (очень здорово, что мы можем # использовать для этого OBD::Sybase! )
    Smsdbh = DBI->connect("dbi:Sybase:server=MSSQL",Smsadmin,Smspw);
    die "Невозможно соединиться с сервером mssql: $DBI::errstr\n" unless (defined Smsdbh);
    # включаем параметр ChopBlanks, чтобы удалить концевые пробелы $msdbh->{ChopBlanks} = 1;
    $1=1; # выключаем буферизацию вывода STOOUT
    и инициализируем обработчик сигнала с тем, чтобы можно было
    # корректно завершиться $SIG{INT} = suu (Sbyebye = 1;};
    и бесконечный цикл, который завершится при установке
    ft нашего флага прерывания while (1) {
    last if (Sbyebye);
    и запускаем хранимую процедуру sp_monitor Ssystn = $sydbh->prepare(q{sp_monitor» or
    die "Невозможно подготовить sy sp_monitor:".$sydbh->errstr."\n";
    $systh->execute or
    die "Невозможно выполнить sy sp_monitor;".$sydbh->errstr."\n";
    цикл для получения нужных нам строк, и мы знаем, что у нас есть все, что нужно, когда мы
    # получаем информацию
    cpu_busy wbile($href = $systh->fetchrow_hasnref or
    $systh->{syb^more_results}) {
    К есть то, что нужно, перестаем спрашивать
    last if (defined $href->{cpu_busy}); ! $systh->firush;
    ft заменяем все, кроме %, значениями, которые мы
    в получили
    for (keys %{$href}) <
    $nref->{$_} =" s/. '-(\d+%)/\V; }
    # собираем все нужные нам данные в одну строку
    $info - "Sybase: (". $nref-Xcpu_busy}. "
    CPU), ". "(".$href->{io_busy}." 10), ". "(".$href->{idle}." idle) ";
    отлично, тпг.ерь сделаем то же самое и для другого сервера
    (MS-SQL)
    Smssth = $msdbh->prepare(q{sp_monitor}) or
    die "Невозможно подготовить ms spjnonitor;".
    $o)sdoh->errstr. "\л': $Disstb->execute or
    die "Невозможно выполнить ms spjronitor:".
    $nsdbn->errstr."\n": while($href = $mssth->
    fetchrow^hashref or $mssth->{syb_more_results})
    {
    # есть то, что нужно, перестаем спрашивать
    last if (defined $href->{cpu_busy»:
    }
    $mssth->finish;
    # заменяем все, кроме % for (keys %{$href» {
    Sirifo .= "MSSQL: (" . $href->{' cpu_busy'}." CPU), ".
    "C'.$href->{'io_busy'}." 10), ".
    "(".$riref->{'idle'}." idle)"; print " "x78,"\r"; print $info,"\r";
    sleep(5) unless (Sbyebye); }
    # попадаем сюда, только если мы прервали цикл
    $sydbh->disconnect;
    $msdbh->disconnect;
    Сценарий выводит эту строку на экран и обновляет ее каждые пять секунд:
    Sybase: (33% CPU), (33% 10), (0% idle) MSSQL: (0% CPU), (0%!0), (100% idle)
    Основу данной программы составляет хранимая процедура
    sp_mon:tor,
    существующая как на Sybase-, так и на MS-SQL-сервере.
    Вывод sp_mo-nitor выглядит примерно так:
    last_run current_run seconds
    Аид 3 1998 12:05АМ Аид 3 1998 12:05АМ 1
    cpu_busy io_busy idle
    0(0)-0% 0(0)-0% 40335(0)^0%
    packets^received packets_sent packet_errors
    1648(0) 1635(0) 0(0)
    total_read total_write total_errors connections
    391(0) 180(0) 0(0) 11(0)
    К сожалению, sp_monitor показывает непереносимую особенность Sybase, которая прекочевала к MS-SQL: множественные наборы результатов. Каждая из строк возвращается в виде отдельного результата. Модуль DBD: : Sybase справляется с этим, устанавливая специальный атрибут команды. Вот как возникла эта проверка:
    while($href = $systh->fetchrow_hashref or
    $systh->{syb_more_results}) {
    и вот почему следовало выйти из цикла до того, как были замечены нужные поля:
    # есть то, что нужно, перестаем спрашивать
    last if (defined $href->{cpu_busy});
    Сама программа будет выполняться в вечном цикле до тех пор, пока не получит сигнал прерывания (наиболее вероятно, что это будет нажатие клавиш + пользователем). Получив такой сигнал, мы делаем самую безопасную вещь из тех, что можно сделать с обработчиком сигнала, и устанавливаем флаг возврата. Подобную технологию рекомендуют использовать на страницах perlipc для безопасной обработки сигналов. После получения сигнала INT будет установлен соответствующий флаг, который выбросит нас из цикла на следующей итерации. Получение этого сигнала позволяет программе «деликатно» закрыть дескрипторы баз данных, перед тем как сбросить «этот бренный шум».
    Эта небольшая программа всего лишь затронула возможности наблюдения за состоянием сервера, доступные нам. Было бы несложно, взяв полученные от sp_monitor результаты, построить график, чтобы получить более наглядное представление о том, как используется сервер. Но... оставим заботы об украшательстве читателю.


    Наблюдение за свободным пространством

    Наблюдение за свободным пространством

    Если на мгновение вникнуть в технические тонкости, то сервер баз данных - это место для хранения разного добра. И если места для его хранения не остается, то это либо плохо, либо очень плохо. Так что программы, помогающие следить за свободным и занятым пространством на сервере, действительно очень полезны. Посмотрим на DBI-программу, созданную для наблюдения за дисковым пространством на сервере Sybase.
    Вот отрывок вывода программы, которая в графическом виде показывает, как любая база данных использует место на сервере. В каждом разделе отражено, сколько процентов пространства занято данными и журналом. В следующей диаграмме d соответствует пространству, занятому данными, а 1 - журналам. Для каждой диаграммы указан объем занятого и доступного пространства (в процентах):
    | ddddddd |15.23%/5MB
    hpotter--------1 |
    | |0.90%/5МВ
    | ddddddd |15.23%/5MB
    dumbledore-----1 |
    | |1.52%/5МВ
    (dddddddd |16.48%/5MB
    hgranger------- |
    | |1.52%/5МВ
    Iddddddd |15.23%/5MB
    rweasley-------1
    1 |3.40%/5МВ
    (ddddddddddddddddddddddddddd |54.39%/2MB hagrid---------1 |
    I - no log I
    Вот как генерировался этот вывод:
    use DBI;
    Sad.Tiin = 'sa':
    print "Введите паролэ дл? $adn:r-:
    cho'iip($ow = <'STOIN,.-).
    $dbh = DBI->connect('dbi:Sybase:',$admin,$pw);
    die "Невозможно соединиться: $DBI::errstr\n" unless (defined $dbh);
    ft получаем имена баз данных на сервере
    $sth = $dbh->prepare(q{SELECT name from sysdatabases}) or
    die "Невозможно подготовить запрос к sysdatabases: ".$abh->errstr."\n": $sth->execute or
    die "Невозможно выполнить запрос к sysdatabases: ".$dbh->errstr. "\n":
    while ($aref = $sth->fetchrow_arrayref) { push(@dbs, $aref->[0]);
    }
    $sth->finish;
    tt
    получаем состояние для каждой из баз данных foreach $db (@dbs) { ff получаем и суммируем значения из поля size для всех
    ft сегментов, не относящихся к журналам регистрации
    $size = &querysum(qq{SELECT size FROM master.dbo.sysusages
    WHERE dbid = db_id(\'$db\')
    AND segmap != 4});
    ft получаем и суммируем значения из поля size для сегмента,
    ft соответствующего журналам регистрации $logsize = &querysum(qq
    {SELECT size FROM master.dbo.sysusages
    WHERE dbid = db_id(\'$db\')
    AND segmap =4}):
    ft переходим к другой базе данных и получаем информацию об
    ft используемом пространстве $dbh->do(q{use $db}) or
    die "Невозможно изменить базу данных на $db: ".$dbh->errstr."\n":
    ft мы использовали функцию reserved_pgs, чтобы вернуть
    ft количество страниц, используемых под данные (doampg) и
    индекс (ioampg)
    $used=&querysum(q{SELECT reserved_pgs(id,doampg)+reserved_pgs(id.ioampg)
    FROM sysindexes
    WHERE id != 8});
    ft то же самое, только на этот раз получаем информацию по
    ft журналам регистрации
    $logused=&querysum(q{SELECT reserved_pgs(id, doampg)
    FROM sysindexes
    WHERE id=8}):
    ft выводим информацию в графическом виде
    &graph($do,Ssize,Slogsize,$used,Slogused): } $dbh->disconnect:
    ft готовим/выполняем запрос SELECT, получаем сумму результатов sub querysum
    {
    my($query) = shift; rm/($sth $aref, Ison);
    $sth = $dbh->prepare($query) or
    die "Невозможно подготовить запрос $que-~, .
    Sdbn- -errstr.
    $sth->execute or
    die "Невозможно выполнить запрос Sqjery ".
    while ($aref=$stn->fetcnrow_arrayref) {
    $sum += $aref->[0]: } $sth->finish;
    $sum; }
    # выводим в виде диаграммы имя базы данных, ее размер, размер журнала
    регистрации и информацию об использовании пространства sub graph
    {
    my($dbname,Ssize,Slogsize,Sused,Slogused) = @_;
    в строка для информации об использовании пространства данными
    tt
    использованное пространство и общий обьем, отведенный под данные printf("%.2f", ($used/$size-100)V,
    print "%/". (($size * $pages)/1024)."MB\n";
    print Sdbname.'-'x(14-length($dbname)).'-|'.(' 'x 49)."|\n";
    if (defined Slogsize) { n строка для информации об
    tt
    использовании пространства под журналы регистрации print ' 'х15 . "|- no log".(' 'х 41)."|\п": }
    print "\n"; }
    Читатель, разбирающийся в SQL, вероятно, удивится, зачем использовать специальную подпрограмму для суммирования данных из одного столбца вместо того, чтобы применить отличный оператор SUM из SQL. querysum() придумана только в качестве примера того, что можно сделать на лету из Perl. Подпрограмма на Perl подходит, скорее, для более сложных задач. Например, если нужно отдельно просуммировать данные, выбирая их по регулярному выражению, лучите это сделать из Perl, чем обращаться к серверу и просить его составить таблицы (даже если это можно сделать).

    ODBC

    ODBC

    http://www.microsoft.com/odbc -
    информация об ODBC от Microsoft. Можно также поискать информацию об ODBC и о библиотеках ODBC в MDAC SDK на сайте http://msdn.microsoft.com. http://www.roth.net/perl/odbc/ -
    официальная страница Win32: : ODBC. «Win32 Perl Programming: The Standard Extensions»,
    Dave Roth (Mac-millan Technical Publishing, 1999). Книга автора Win32: :ODBC, на настоящий момент это лучший справочник по программированию модулей для Win32 Perl.

    Прочее

    Прочее

    http://sybooks.sybase.com -
    вся документация от Sybase с удобным интерфейсом поиска и простой системой навигации. Иногда бывает полезна не только для решения вопросов по Sybase/MS-SQL, но и для общих вопросов по SQL. http://www.mbay.net/~mpeppler/ ~
    домашняя страница Майкла Пепплера (автора SybPerl и DBD: : Sybase). Содержит информацию не только по Sybase, но и по программированию баз данных в целом.

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

    «Advanced Perl Programming»,
    Sriram Srinivasan (O'Reilly, 1997). http://www.symbolstone.org/technology/perl/DBI/index.html -
    официальная домашняя страница DBI; это должно быть вашей первой остановкой. «Programming the Perl DBI»,
    Alligator Descartes, Tim Bunce (O'Reilly, 2000).

    Сервер MSSQL и ODBC

    Сервер MS-SQL и ODBC

    Наконец, вот код для получения той же информации с сервера MS-SQL через ODBC. Заметьте, что применяемый синтаксис SQL практически идентичен предыдущему примеру благодаря связи Sybase/MS-SQL. Интересны отличия между этим примером и предыдущим:
  • Использование DSN, которое предоставляет нам контекст базы данных по умолчанию, так что нет необходимости указывать, где искать таблицу sysdatabases.
  • Употребление $dbh->DropCursor() в качестве грубой аналогии $sth-fi-nish.
  • Неудобный синтаксис, который приходится применять для запуска хранимых процедур. Информацию о хранимых процедурах и других подобных аномалиях ищите на веб-страницах по Win32: : ODBC.
  • Вот как выглядит программа:
    use Win32::ODBC;
    print "Введите имя пользователя: ";
    chomp($user = );
    print "Введите пароль для $user: "; chomp($pw = );
    $dsn="sysadm"; и имя источника данных, которое мы используем
    # ищем доступные DSN, создаем переменную $dsn,
    если она еще не существует
    die "Невозможно запросить доступные DSN",Win32::ODBC::Error()."\n"
    unless (%dsnavail = Win32::ODBC::DataSources());
    if (Idefined $dsnavail{$dsn}) {
    die "невозможно создать DSN:".Win32::ODBC::Error()."\n"
    unless (Win32::ODBC::ConfigDSN(ODBC_ADD_DSN. "SQL Server", ("DSN=$dsn",
    "DESCRIPTION=DSN for PeriSysAdm",
    "SERVER=mssql.happy.edu". "DATABASE=master",
    "NETWORK=DBMSSOCN".
    библиотека сокетов TCP/IP ))); }
    it
    соединение с основной базой данных $dbh = new Win32: :ODBC("DSN=$dsn;UID=$iJSer:PWD=$pw: "):
    die "Невозможно соединиться с DSN $dsn:".Win32:
    # ищем базы данных на сервере
    if (defined $dbh->Sql(q{SELECT name from sysdatabases})){
    die "Невозможно послать запрос к базе данных:" .Win32: :ODBC: Error().
    while ($dbh->FetchRow()){
    push(@dbs, $doh->0ata("name"));
    }
    $dbh->DropCursor();
    п
    ищем пользовательские таблицы в каждой базе данных foreach $db (@dbs) {
    if (defined $dbh->Sql("use $db")){
    die "Невозможно изменить базу данных на $db:" .
    Win32::ODBC::Error() . "\n"; >
    print "---$db---\n"; @tables=(); if (defined $dbh->
    Sql(q{SELECT name from sysobjects
    WHERE type="U"})){ die "Невозможно запросить таблицы из $db:" .
    Win32::ODBC::Error() . "\n"; } while ($dbh->FetchRow()) {
    push(@tables,$dbh->Data("name")); > $dbh->DropCursor();
    ft ищем информацию о полях для каждой таблицы
    foreach Stable (©tables) { print "\t$table\n";
    if (defined $dbh->Sql(" {call sp_columns (\'$table\')} ")){
    die "Невозможно запросить поля из таблицы Stable:".
    Win32::ODBC::Error() . "\n"; >
    while ($dbh->FetchRow()) { @cols=();
    @cols=$dbh->Data("COLUMN_NAME","TYPE.NAME","PRECISION");
    print "\t\t",$cols[0]," [",$cols[1],"(",$cols[2],")]\n";
    } $dbh->DropCursor();
    }
    }
    $dbh->Close();
    "SQL Server","DSN=$dsn"))


    Сервер Sybase и DBI

    Сервер Sybase и DBI

    В этом подразделе представлен аналог для Sybase. Внимательно просмотрите программу, а после этого поговорим о некоторых существенных моментах:
    use DBI;
    print "Введите имя пользователя: "; chomp($user = );
    print "Введите пароль для $user: "; chomp($pw = );
    $dbh = DBl->connect('dbi:Sybase'',Suser,$pw);
    die "Невозможно соединиться: $DBI::errstr\n" unless (defined $dbh);
    ищем базы данных на сервере $sth = $dbh->prepare(q{SELECT name from master dbo.sysdatarases}) cr
    die "Невозможно подготовить запрос к sysdatabases: ".
    $db'i->er rst r . "\n", $stli->oxecute or
    die "Невозможно выполнить запрос к sysdarabases: '.
    $dori->errstr. "\п";
    while (Sarof = $sth->fetchrow_arrayref) (
    push((3dbs, $aref->[0]): }
    $sth->finisn:
    foreach $cm (Mbs) {
    $dbh->do("USE $do") or
    die "Невозможно использовать $db: ".
    ®tables=():
    while ($агч'( - $.::;->fotchrow_arrayref) {
    die "Невозможно изменить Sdb: ".
    S'Jor->err-str."' n":
    # ищем поля для каждой таблицы foreach Stable (tables) { print "\t$table\n";
    $sth=$dbh->prepare(qq{EXEC bp_colunns Stable}) or
    die "Невозможно подготовить запрос sp^columns ", $obh-:-err.v
    $sth->execute or
    die "Невозможно выполнить запрос sp^columns: ".$dbh->errstr.
    while ($aref = $sth->fetchrow_arrayref) {
    print "\t\t",$aref->[3]," [",$aref->[5],"(",
    $aref->[6],")]\n": }
    $sth->finish; ! }
    $dbh->discohnect or warn "Невозможно отсоединиться: ".
    $dbh->errstr."\n";
    Вот обещанные заметные моменты:
  • Sybase хранит информацию о базах данных и таблицах в специальных системных таблицах sysdatabases и sysobjects. Каждая база данных содержит таблицу sysobjects, в то время как сервер хранит обобщенную информацию о них в одной таблице sysdatabases, расположенной в основной базе данных. Мы используем более явный синтаксис databases, owner, table в первом запросе SELECT, чтобы недвусмысленно обратиться именно к этой таблице. Для перехода к sysobjects каждой базы данных можно применять этот же синтаксис, вместо того чтобы явно переключаться между ними при помощи USE. Более того, как и при переходе в каталог средствами cd, такой контекст упрощает написание других запросов.
  • Запрос SELECT к sysobjects применяет ключевое слово iVHE-L, чтобы вернуть информацию только о пользовательских таблицах. Это было сделано для ограничения размера вывода. Если бы мы хотели включить также и все системные таблицы.
  • Складывается впечатление, что заполнители в DBD: : Sybase реализованы так для того, чтобы препятствовать их употреблению с хранимыми процедурами. Будь реализация другой, следовало бы использовать заполнители в EXEC sp_columns.


  • Удобные методы DBI

    Таблица 7.2. Удобные методы DBI

    Название Объединяет в себе следующие методы
    select row_a r ray ref (Sstmnt) prepare(Sstmnt), execute( ), fetchrow_arrayref ( )
    selectcol_arrayref ($stmnt) prepare($stmnt), execute( ), (@{fetchrow_arrayref()})[0] (т. е. возвращает первое поле для каждой записи)
    select rowar ray (Sstmnt) prepare(Sstmnt), execute( ), fetchrow_array( )
    Во-вторых, заслуживает внимания способность DBI связывать переменные с результатами запроса. Методы bind_coL() и bina_ccl :r.r s() используются для автоматического помещения результатов запроса в указанную переменную или список переменных. Обычно это заменяет дополнительный шаг, а то и два при написании программы. Ниже приведен пример, включающий bind_colu"ns():
    die "Невозможно выполнить запрос:".$dbh~>errstr" \n";
    К эти переменные получат 1-й, 2-й и 3-й столбы из SELECT
    $rc = $sth->Pind_columns(\$name, \$ipaddr, \$dept):
    while ($sth->fetchrow_arrayref){
    tt
    $name, Sipaddr и $dept автоматически получают значения из tt
    результатов запроса

    Учетные записи баз данных

    Учетные записи баз данных

    Как упоминалось раньше, администраторы баз данных вынуждены иметь дело с рядом тех же вопросов, с которыми борются системные администраторы, в частности, с поддержкой регистрационных имен и учетных записей. Например, днем на работе мы ведем занятия по программированию баз данных. Каждый студент, посещающий эти занятия, получает регистрационное имя на сервере Sybase и свою собственную (пусть и маленькую) базу данных, с которой он может работать. Вот упрощенная версия программы, которую мы используем для создания таких баз данных и регистрационных имен:
    use OBI;
    и ИСПОЛЬЗОВАНИЕ: syaccreate
    Sadmin = 'sa';
    print "Введите пароль для Sadmin: ";
    chomp($pw = );
    $user=$ARGV[0];
    # генерируем фиктивный пароль, основываясь на имени
    # пользователя, записанном в обратном порядке, и дополняем его
    дефисами, чтобы его длина составляла 6 символов
    Sgenpass = reverse join(''. reverse split(//,$user)):
    Sgenpass .= "-" x (6-length($genpass));
    # вот перечень SQL-команд, которые используем
    tt мы: 1) создаем базу данных на устройстве USER_DISK
    с журналом регистрации в USER_LOG ft 2)
    добавляем регистрационное имя для пользователя
    на сервере, делая новую базу базой по умолчанию
    # 3) переключаемся на вновь созданную базу данных
    # 4) изменяем владельца базы данных на пользователя
    (^commands = ("create database Suser on USER_DISK=5 log on USER_LOG=5", "
    sp_addlogin $user,\"$genpass\",Suser". "use $user", "sp_changedbowner $user"):
    Я соединяемся с сервером
    $dbh = DB!->connect('dbi:Sybase:',Sadmin.$pw):
    die "Невозможно соединиться: $DBI: errstr•n" unless (defined Sdbh):
    $dbh->disconnect:
    Поскольку эта задача заключается в выполнении ряда команд, которые не возвращают данных, можно записать их в компактном цикле, в котором вызывается повторно $dbh->ao(). Можно было бы использовать полностью идентичный сценарий для удаления этих учетных записей и баз данных, когда занятия завершатся:
    use DBI;
    # ИСПОЛЬЗОВАНИЕ: syacdelete
    $admin = 'sa':
    print "Введите пароло для Sadmin: ":
    chomp($pw = );
    $user=$ARGV[0];
    #
    перечень SQL-команд, которые мы будем использовать; мы удаляем базу данных пользователя
    и удаляем регистрационное имя с сервера
    ©commands = ("drop database $user", "sp_droplogin Suser");
    # соединяемся с сервером
    $dbh = OBI->connect('dbi:Sybase:',$admin,$pw);
    die "Невозможно соединиться: $DBI::errstr\n" unless(defined $dbh);
    # обходим в цикле массив команд, выполняя их последовательно for (©commands) {
    $dbh->do($_) or die "Невозможно $_: " . $dbh->errstr . "\n": }
    $dbh->disconnect or warn "Невозможно рассоединигься: " . $dbh->errstr . "\n":
    Существует много функций, связанных с учетными записями, которые можно добавить в эту программу. Вот лишь некоторые идеи:
    Проверка паролей
    Утилита соединяется с сервером и получает список баз данных и учетных записей; затем пытается установить связь, применяя ненадежные пароли (регистрационные имена, пустые пароли, пароли по умолчанию).
    Карта пользователей
    Создаем список регистрационных имен и баз данных, доступных пользователям с этими регистрационными именами.
    Управление паролями
    Система ограничения срока действия паролей.


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

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

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

    Модуль Идентификатор на CPAN Версия
    Mac: :Glue CNANDOR 0.58
    Win32 . : OLE (входит в состав ActiveState Perl) JDB 1.11
    Mail: : Mailer (можно найти в MailTools) GBARR 1.13
    Text::Wrap (можно найти в Text-Tabs+Wrap, также распространяется с Perl) MUIR 98.112902
    10: '.Socket (можно найти в 10, кроме того, распространяется с Perl) GBARR 1.20
    Mail : : Internet (можно найти в MailTools) GBARR 1.13
    Mail: : Heade r (можно найти в MailTools) GBARR 1.13
    Mail: : Folder: :Mbox (можно найти в Mail: : Folder) KJOHNSON 0.07
    Socket (распространяется с Perl)
    BerkeleyDB PMQS 0.10
    Net: :Tclnet JROGERS 3.01
    DB_File (распространяется с Perl) PMQS 1.72


    Использование IPC специфичных для операционной системы

    Использование IPC, специфичных для операционной системы

    В MacOS или Windows NT можно управлять почтовым клиентом, используя IPC (Interprocess Communication, межпроцессные взаимодействия).
    Я не знаю о существовании версий sendmail для MacOS, но в нем для управления почтовым клиентом можно применять AppleScript:
    $to="someone\@example.com"; $from="me\@example.com"; $subject="Hi there"; $body="message body\n";
    MacPerl: :DoAppleScnpt(«EOC); tell application "Eudora"
    make message at end of mailbox "out"
    -- 0 is the current message
    set field \"from\" of message 0 to \"$frorn\"
    set field \"to\" of message 0 to \"$to\"
    set field \"subject\" of message 0 to \"$subject\"
    set body of message 0 to \"$body\"
    queue message 0
    connect with sending without checking
    quit
    end tell EOC В этом примере запускается очень простая программа AppleScript, которая общается с почтовым клиентом Eudora. Сценарий создает новое сообщение, помещает его в очередь для отправки, а затем отдает инструкции почтовому клиенту об отправке сообщения из очереди перед выходом.
    Еще один, более эффективный способ написать подобный сценарий состоит в том, чтобы использовать модуль Мае: : G1 ие, уже рассмотренный в главе 2 «Файловые системы».
    use Mac::Glue ':glue ;
    $e=new Mac::Glue 'Eudora';
    $to="someone\@example.com"; $f rom="me\(<}>exainple. com";
    $subject="Hi there"; $body=" $e->make(
    new => 'message',
    at => location(end => $e->obj(mailbox => 'Out')) );
    $e->set($e->obj(field => from => message => 0), to =4 $froin):
    $e->set($e->obj(field => to => message => 0), to => $to);
    $e->set($e->ob](field => subject => message => 0), to => Ssubject);
    $e->set($e->prop(body => message => 0), to => $body);
    $e->queue($e->obj(message => 0)); $е->соппесц sending => 1, checking => 0);
    $e->quit;
    В NT можно обратиться к библиотеке Collaborative Data Objects Library (раньше она называлась Active Messaging), простой в использовании надстройке на архитектуре MAPI (интерфейс прикладного программирования систем передачи сообщений). Вызвать эту библиотеку для управления таким почтовым клиентом, как Outlook можно, применив модуль Win32: :OLE следующим образом:
    $to="me\(Sexample.com"; $subject="Hi there";
    $body="message body\n";
    use Win32::OLE;
    # инициализируем OLE и COINIT_OLEINITIALIZE,
    необходимые при и использовании объектов MAPI.Session
    Win32: :OLE->Initialize(Win32: :OLE: :COINIT__OLEINITIALI7E): die Win32: :OLE->LastEr'-or(), "\n"
    if Win32: :OLE->LastErrc-( V
    создаем объект сессии, который вызовет logoff при уничтожени
    my Ssession = Win32::OLE->new('MAPI.Session','Logoff);
    die Win32::OLE->LastError();"\n" if Win32::OLE->LastError();
    № регистрируемся в этой сессии, используя OL98 Internet Profile по
    $session->Logon('Microsoft Outlook Internet Settings').
    die Win32: :OLE->LastError(),"\n" if Win32::OLE->LastError();
    создаем обьект message
    my Smessage = $session->Outbox->Messages->Add:
    die Win32:'OLE->LastError(),"\n" if Win32::OLE->LastError():
    Я создаем обьект recipient
    my Srecipient = $message->Recipients->Add;
    die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError();
    заполняем данными объект recipient
    $recipient->{Name} = $to;
    $recipient->{Type} = 1; n 1 = "To:", 2 = "Cc:", 3 = "Вес:"
    все адреса должны быть расшифрованы по справочнику
    # (в этом случае, скорее всего, по вашей адресной книге)
    Полные адреса расшифровываются сами в себя, так что эта
    строка в большинстве случаев не изменит обьект recipient
    $ recipient->Resolve();
    die Win32: :OLE->LastError(). "\r> if Win32: :OLE->LastError();
    tt заполняем строку Subject: и тело сообщения
    $message->{Subject} = Ssubject; $message->{Text} = Sbody;
    tt помещаем сообщение в очередь для отправки
    1-й аргумент = сохранить копию сообщения
    2-й аргумент = позволить пользователю изменить сообщение
    tt перед отправкой в диалоговом окне
    № 3-й аргумент = родительское окно диалога, если 2-й аргумент True
    $message->Send(0, О, О):
    die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError();
    tt явно уничтожить объект Ssession, вызвав
    $session->Logoff undef Ssession,
    В отличие от предыдущего примера, программа всего лишь помещает письмо в очередь. Это уже дело почтового клиента (такого как Outlook) или транспортного агента (например Exchange) периодически инициировать отправку почты. Существует CDO/AM 1.1 - метод для объекта Session под названием DeiiverNow(), обращающийся к MAPI с заданием очистить все очереди входящих и исходящих сообщений. К сожалению, в некоторых ситуациях он недоступен или не работает, поэтому его нет и в предыдущем примере. В упомянутом примере управление MAPI производится «вручную» при помощи вызовов OLE. Если вы хотите использовать MAPI, «не пачкая рук», можно применить модуль Win32: :МАР1, который берет на себя все функции (модуль находится на http://www.generation.net/ -aminer/Perl/ ).
    Программы, полагающиеся на AppleScript/Apple Events или MAPI, так же непереносимы, как и вызов программы sendmall. Они берут на себя часть работы, но относительно неэффективны. К этим методам нужно прибегать в последнюю очередь.

    Электронная почта

    Электронная почта

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

  • В отличие от других глав книги, здесь не обсуждается администрирование какой-либо службы, технологии или предметной области. Вместо этого мы собираемся рассмотреть, как использовать электронную почту из Perl в качестве инструмента системного администрирования.
    В контексте системного администрирования Perl может быть полезен, как посылая, так и отправляя почту. Электронная почта - это отличный способ сообщить о чем-либо: например о том, что в программе происходит что-то не то; или о результатах выполнения автоматического процесса (скажем, службы планировщика заданий или сгоп), или об изменениях чего-то, за чем необходимо следить. Мы выясним, как посылать такую почту из Perl, а также узнаем о некоторых ловушках, связанных с отправкой почты самим себе.
    Также мы рассмотрим, применение Perl для обработки входящей почты, повышая ее эффективность. Perl поможет бороться со спамом и разбираться с вопросами пользователей.
    Будем впредь считать, что у вас уже есть надежная, работоспособная почтовая система. Кроме того, будем исходить из того, что в ней применяются протоколы, соответствующие спецификации IETF для отправки и получения почты. В примерах этой главы используется протокол SMTP (простой протокол передачи почты, RFC821), а сообщения соответствуют RFC822. Но всему свое время.

    Контроль над частотой отправки почты

    Контроль над частотой отправки почты

    Самый простой способ избежать лишней почты - добавить в программу меры предосторожности, чтобы устанавливать задержку между сообщениями. Если сценарий запущен постоянно, то очень просто запомнить время отправки последнего сообщения:
    $last_sent = time;
    Если программа запускается один раз в N минут или часов через сгоп в Unix или механизмы планирования задач NT, эту информацию можно переписать в файл, состоящий из одной строки, и считывать его при следующем запуске программы. В подобном случае обязательно обратите внимание на меры предосторожности, перечисленные в главе 1 «Введение».
    В зависимости от ситуации можно поэкспериментировать с временем задержки. В этом примере показана экспоненциальная задержка (exponential backoff):
    $max = 24*60*60; и максимальная задержка в секундах (1 день)
    Sunit = 60;
    увеличиваем задержку относительно этого значения (1минута)
    # интервал времени, прошедший с момента отправки предыдущего
    # сообщения и последняя степень 2, которая использовалась для
    # расчета интервала задержки. Созданная нами подпрограмма
    # возвращает ссылку на анонимный массив с этой информацией
    sub time_closure {
    my($stored_sent,$stored_power)=(0,-1); return sub {
    (($stored_sent,$stored_power) = @_) if @_; [$stored_sent,$stored_power]; > };
    $last_data=&time_closure; # создаем замыкание
    ft возвращаем значение "истина" при первом вызове и затем после
    # задержки
    sub expbackoff {
    my($last_sent,$last_power) = @{&$last_data};
    # возвращаем true, если это первое наше обращение или если
    # текущая задержка истекла с тех пор, как мы спрашивали
    последний раз. Если мы возвращаем значение true, мы
    запоминаем время последнего утвердительного ответа и
    увеличиваем степень двойки, чтобы вычислить задержку.
    if (!$last_sent or ($last_sent +
    (($unit -.$last_power >= $max) 9
    $max : $unit * 2**$last_power) <= time())){
    &$last_data(time().++$last„power); return 1;
    }
    else {
    return 0; } >
    Подпрограмма expbackoffQ возвращает значение true (1), если нужно отправить сообщение, и false (0), если нет. При первом вызове она возвращает true, а затем быстро увеличивает время задержки до тех пор, пока значение t rue не станет появляться лишь раз в день.
    Чтобы сделать программу более интересной, я применил необычную конструкцию под названием замыкание (closure) для хранения времени последней отправки сообщения и последней степени двойки, используемой для расчета задержки. Замыкание используется как способ скрытия важных переменных от остальной программы. В данной маленькой программе это было сделано из любопытства, но польза от такой технологии очень быстро становится очевидной в программах большего размера, где более вероятно, что другой код может случайно перезаписать значения этих переменных. Вот, вкратце, как работают замыкания.
    Подпрограмма &time_closure() возвращает ссылку на анонимную подпрограмму, по существу, на небольшой отрывок кода без имени. Позже данная ссылка будет вызывать этот код, используя стандартный синтаксис символических ссылок: &$last_data. Код из анонимной подпрограммы возвращает ссылку на массив, поэтому и используется такая масса знаков пунктуации, чтобы получить доступ к возвращаемым данным:
    my($last_sent,$last_power) = @{&$last_data};
    Вот и вся тайна, которая скрывается за замыканиями: поскольку ссылка создается в том же блоке, что и переменные $stored_seri: и $sto-red_power (посредством my()), то они схватываются в уникальном контексте. Переменные $stored_sent и $stored_power можно прочитать и изменить только при выполнении кода из этой ссылки. Кроме того, они сохраняют свои значения между вызовами. Например:
    создаем замыкание $last_data=&time._closure: вызываем подпрограмму, устанавливающую значения переменных
    &$last_data(1,1);
    и пытаемся изменить их за пределами подпрел раммы
    $stored__sent - $stored_power = 2:
    выводим их текущие значения, используя подпрограмму
    print "@{&$last_data}\n":
    Результатом выполнения этого кода будет "1 1", хотя и создается впечатление, что в третьей строке были изменены значения переменных $stored_sent и $stored_power. Да, значения глобальных переменных с теми же именами были изменены, но невозможно затронуть копии, защищенные замыканиями.
    Можно говорить о переменной из замыкания как о спутнике на орбите планеты. Спутник удерживается гравитацией планеты, так что куда движется планета, туда перемещается и спутник. Позицию спутника можно описать только относительно планеты: чтобы найти спутник, сначала нужно отыскать планету. Каждый раз, когда вы находите планету, там же будет и спутник, на том же месте, где был и прошлый раз. Можно считать, что переменные из замыкания находятся на орбите вокруг ссылки на анонимную подпрограмму, отдельно от вселенной остальной программы.
    Но оставим астрофизику в покое и вернемся к рассуждениям об отправке почты. Иногда лучше, чтобы программа вела себя как двухлетний ребенок, жалующийся с течением времени все чаще. Вот еще одна программа, похожая на предыдущий пример. На этот раз со временем увеличивается количество дополнительно посылаемых сообщений. Начинаем мы с отправки сообщений один раз в день, а затем уменьшаем время задержки до тех пор, пока минимальная задержка не станет равной пяти минутам:
    $тах = 60*60»24;
    максимальная задержка в секундах (1 день)
    $min = 60*5; tt минимальная задержка в секундах (5 минут)
    $unit = 60; tt уменьшаем задержку относительно этого значения (1 минута)
    $start_power = int log($max/$unit)/log(2): # ищем ближайшую степень двойки
    sub time_closure {
    my($last_sent,$last_power)=(0,$start_power+l); return sub {
    (($last_sent, $last_p_; n keep exponent positive
    $last_power = ($last_power > 0) 9
    $last:_power : 0; [Siast^sent,$last_power]; } };
    $last_data=&t ime_clusiire;
    n создаем замыкание
    возвращаем ггче при первом вызове и затем после роста
    it экспоненты sub exprampup {
    my($last_sent,$last_power) = @{&$last_data}.
    возвращаем true, если это первое обращение или если
    текущая задержка истекла с момента последнего обиащен/я.
    Если сообщение отправляется, то мш запоминаем время
    последнего ответа и увеличиваем
    $min : $unit * 2**$last_power) <= time())){
    &$last_data(time(),++$last_power1): return 1;
    }
    else
    {
    return 0; } }
    В обоих примерах вызывалась дополнительная подпрограмма (&$last_data), которая позволяла выяснить, когда было отправлено последнее сообщение и как вычислялась задержка. Позже, при необходимости изменить программу, такое деление позволит изменить способ хранения состояния. Например, если переписать программу так, чтобы она выполнялась периодически, а не постоянно, то замыкание совсем нетрудно заменить обычной подпрограммой, сохраняющей нужные данные в текстовом файле и потом считывающей их оттуда.

    Контролируем количество сообщений

    Контролируем количество сообщений

    Другая разновидность синдрома «чрезмерной отправки почты» - это проблема «каждый в сети за себя». Если все машины из сети решат послать вам чуточку почты, вы вполне можете пропустить что-то действительно важное в этом потоке сообщений. Было бы лучше, если бы все сообщения отправлялись в центральный репозиторий. А затем в собранном виде почта поступала бы в одном сообщении.
    Давайте рассмотрим несколько надуманный пример. Предположим, что каждая машина в сети записывает в разделяемый каталог файл, состоящий из одной строки. Имя каждого файла совпадает с именем машины и в каждом из них хранятся результаты научных вычислений, сделанных прошлой ночью. В файле будет одна строка следующего формата:
    имя-узла
    удача-или-неудача
    количество-завершенных-вычислений
    Программа, проверяющая эту информацию и отсылающая результаты, может выглядеть так:
    use Mail::Mailer; use Text::Wrap;
    tf список машин, отправляющих сообщения
    $repolist = "/project/machinelist";
    ft каталог, куда они записывают файлы
    Srepodir = "/project/reportddir";
    it
    разделитель файловой системы, используется для переносимости. Можно было бы использовать модуль File::Spec
    $separator= "/";
    # отправляем почту "с" этого адреса
    $reportfromaddr = "project\@example. corn";
    # отправляем почту на этот адрес Sreporttoaddr = "project\@example.com";
    tf считываем список машин в хэш. Позже будем вынимать из этого
    # хэша по мере доклада машин, оставив только те машины, которые
    не принимали участие в действии
    open(LIST,$repolist) or die "Невозможно открыть список
    $repolist:$!\n"; while(){
    chomp;
    $missing{$_}=1;
    $machines++; }
    # считываем все файлы из центрального каталога
    и замечание: этот каталог должен автоматически очищаться другим
    # сценарием
    opendir(REPO,Srepodir) or die "Невозможно открыть каталог $repodir:$!\n";
    while(defined($statfile=readdir(REPO))){
    next unless -f Srepodir.$separator.$statfile;
    # открываем каждый файл и считываем информацию о состоянии
    open(STAT,$repodir.$separator.$statfile) or die "Невозможно открыть Sstatfile:$!\n";
    chornp($report = );
    ($hostname.$result,$details)=spiit(' ',$report,3);
    warn "В файле Sstatfile утверждается, что он был сгенерирован машиной
    Shostname1 \n" if($hostname ne Sstatfile);
    имя узла больше не считается пропущенным
    delete $missing{$nostname}; # заполняем значениями хэши
    (Sresult eq "success")}
    $success{$nostname}=$details;
    $succeeded++: } else {
    $fail{$hostname}=$details:
    $failed++; }
    close(STAT); } closedir(REPO);
    # создаем информативный заголовок для сообщения if (Ssucceeded == $machines){
    Ssubject = "[report] Success: $machines"; }
    elsif ($failed == Smachines or scalar keys %missing >= Smachines) {
    Ssubject = "[report] Fail: Smachines"; } else
    { ;
    Ssubject = "[report] Partial: Ssucceeded ACK, Sfailed NACK".
    ((%missing) ? ", ".(scalar keys %missing)1." MIA" : ""); }
    # создаем объект mailer и заполняем заголовки $type="sendmail";
    my Smaller = Mail::Mailer->new($type) or die "Невозможно создать новый объект:$!\п";
    $mailer->open({From=>$reportf romaddr,
    To=>$reporttoaddr. Subject=>$subject})! or die "Невозможно заполнить объект mailer:$!\n";
    » создаем тело сообщения !
    print $mailer "Run report from $0 on " . scalar localtime(tine) . "\n":
    if (keys %success){
    print Smaller "\n==Succeeded==\n";
    foreach $hostname (sort keys %success){
    print Smaller "$hostname: $success{$hostname}\n":
    } } 308
    if (keys %fail){
    print Smaller "\n==Failed==\n";
    foreach Snostname (sort, keys %fail){
    print Smaller "Shostname: $fail{$hostname}\n"
    } }
    if (keys %missing){
    print Smaller "\n==Missing==\n";
    print Smaller wrap("","".join(" ".sort keys %missing)),"\n"; }
    # отправляем сообщение $mailer->close;
    Сначала программа считывает список имен машин, участвующих в данном предприятии. Затем, чтобы проверить, встречались ли машины, не поместившие файл в общий каталог, используется хэш, созданный на основе этого списка. Мы открываем каждый файл из данного каталога и выделяем из него информацию о состоянии. Получив результаты, создаем сообщение и отправляем его.
    Получается такой отчет:
    Date: Wed, 14 Apr 1999 13:06:09 -0400 (EOT)
    Message-Id: <199904141706.NAA08780@example.com> Subject: [report]
    Partial: 3 ACK, 4 NACK, 1 MIA To: project(s>example. con From: project@example.com
    Run report from reportscript on Wed Apr 14 13:06:08 1999
    ==Succeeded==
    barney: computed 23123 oogatrons betty: computed 6745634
    oogatrons fred: computed 56344 oogatrons
    ==Failed==
    bambam: computed 0 oogatrons dino: computed 0
    oogatrons pebbles: computed 0
    oogatrons wilma: computed 0 oogatrons
    ==Missing== mrslate
    Другой способ изучить подобные результаты состоит в том, чтобы создать демон журналов регистрации и посылать отчет от каждой машины через сокет. Сначала взгляните на код для сервера. Он совпадает с кодом из предыдущего примера. Рассмотрим новую программу и обсудим ее важные особенности:
    use 10::Socket;
    use Text::Wrap: ft используется для создания аккуратного вывода
    список машин, посылающих отчеты Srepolist = "/project/machinelist":
    номер порта для соединения с клиентами Sserverport = '9967' ;
    Sloadmachines: # загружаем список ма^ин
    # настраиваем нашу сторону сокета
    Sreserver = 10::Socket::INET->new(LocalPort => Sserverport,
    Proto => "tcp",
    Type => SOCK_STREAM,
    Listen => 5,
    Reuse => 1) or die "Невозможно настроить сокет на нашей стороне: $!\п";
    и начинаем слушать порт в ожидании соединений while(
    ($connectsock,Sconnectaddr) = $reserver->accept()){
    # имя подсоединившегося клиента
    Sconnectname = gethostbyaddr((sockaddr_in($connectadar))[1],AF_INЈT):
    chomp($report=$connectsock->getline);
    ($hostname,$result,$details)=split(' ',$report,3);
    в если нужно сбросить информацию, выводим готовое к
    # отправке сообщение и заново инициализируем все
    хэши/счетчики
    if (Shostname eq "DUMPNOW'H
    &printmail($connectsock);
    close($connectsock):
    undef %success;
    undef %fail:
    Ssucceeded = Sfailed = 0:
    &loadmachines;
    next: }
    warn "$connectname говорит, что был сгенерирован $nostnarce' \r-."
    if($hostname ne Sconnectnaiie): delete $n;issiP.g{Shostna"rie}:
    ($result eq "success")!
    $success{Shostnare}=$deraiIs:
    $succeeded++: / else 1
    $fail{$hostrame}=$dera;ls:
    $fai!ed++. }
    close($connectsock); } close($reserver):
    # загружаем список машин из заданного файла sub loadmachines
    undef %missing;
    undef Smachines;
    open(LIST,$repolist) or die "Невозможно открыть список Srepolist:$!\n";
    while(){
    chomp;
    $missing{$_}=1;
    $machines++; } }
    выводим готовое к отправке сообщение. Первая строка - тема,
    # последующие строки - тело сообщения sub printmail<
    (Ssocket) = $_[0];
    if (Ssucceded == $machines){
    Ssubject = "[report] Success: Smachines"; }
    elsif ($failed == Smachines or scalar keys %missing >= Smachines) {
    Ssubject = "[report] Fail: Smachines"; } else {
    Ssubject = "[report] Partial: Ssucceeded ACK, Sfailed NACK".
    ((%missing) ? ", ".(scalar keys %missing)1." MIA" : ""); }
    print Ssocket "$subject\n":
    print Ssocket "Run report from $0 on ".scalar localtime(time)."\n";
    if (keys %success){
    print Ssocket "\n==Succeeded==\n";
    foreach Shostname (sort keys %success){ print
    Ssocket "Shostname: $success{$hostname}\n":
    }
    }
    if (keys %fail){ print Ssocket "\n==Failed==\n":
    foreach Shostname (sort keys %fail)< print $socket "Shostname: $fail{$hostname}\n"; } }
    if (keys %nissing){ print Ssocket " \n==Missing==\n":
    print $socket wrapC"1."" join(" ".sort keys Vrissi^g) ).'"
    }
    Кроме переноса части кода в отдельные подпрограммы, главное изменение заключается в том, что добавлен код для работы с сетью. Модуль 10: : Socket позволяет без труда открывать и использовать сокеты, которые можно сравнить с телефоном. Сначала нужно установить свою сторону сокета (10: :Socket->new()), как бы включая свой телефон, а затем ждать «звонка» от клиента (10: :Socket»accept()). Программа приостановлена (или «заблокирована») до тех пор, пока не установлено соединение. Когда соединение установлено, запоминается имя подсоединившегося клиента. Затем из сокета считывается строка ввода.
    Мы предполагаем, что строка ввода выглядит точно так же, как и строки из отдельных файлов в предыдущем примере. Единственное различие - это загадочное имя узла DUMPNOW. Если это имя встречается, подсоединившемуся клиенту выводится тема и тело готового к отправке сообщения, при этом сбрасываются все счетчики и хэш-таб-лицы. За отправку сообщения, полученного от сервера, ответственен клиент. Теперь посмотрим на пример клиента и узнаем, что он может сделать с этим сообщением:
    use 10::Socket;
    номер порта для соединения с клиентом
    Sserverport = "9967";
    и имя сервера
    $servername = "reportserver";
    ft преобразуем имя в IP-адрес
    Sserveraddr = inet_ntoa(scalar gethostbyname($servername));
    Sreporttoaddr = "project\@example.com";
    Sreportf romaddr = "project\(g>example.com";
    Sreserver = 10::Socket::INET->new(PeerAddr => Jserveraddr.
    PeerPort => $serverport Proto => "tcp". Type => SCCK^STREAM) or ale
    "Невозможно создать сокет -а нашей стороне:: $!\п":
    if ($ARGV;;0] re "-ni"){
    print Sreserver $ARGV[0]: v else {
    use Mail::Mailer;
    print Sreserver "DUMPNOW\T;
    chomp($subject = <$reserver.>) $body = join("",<$reserver>);
    $type="send!rmil";
    my Smaller = Mail::Mailer->new($type) or die
    "Невозможно создать новый обьект niailer-$' \n";
    $mailer->open({
    From => $reportfromaddr To => $reporttoaddr, Subject => Ssubject » or die
    "Невозможно заполнить объект mailer:$!\n";
    print Smaller $body; $mailer->close; }
    close($reserver);
    Эта программа проще. Сначала открывается сокет с сервером. В большинстве случаев ему передается информация о состоянии (полученная в командной строке как $ARGV[0]) и соединение закрывается. При желании создать клиент-серверную систему регистрации, подобную этой, вероятно, нам пришлось бы перенести данный клиентский код в подпрограмму и вызывать ее из другой, гораздо более крупной.
    Если передать сценарию ключ -т, он отправит серверу «DUMPNOW» и прочитает полученную от него строку темы сообщения и тело сообщения. Затем этот вывод передается модулю Mail: : Mailer и отправляется в виде почтового сообщения при помощи той же программы, которую мы видели раньше.
    Для ограничения размера примера и для того, чтобы не уходить в сторону от дискуссии, здесь представлен лишь костяк кода для клиента и сервера. В нем нет ни проверки ошибок или ввода, ни управления доступом, ни авторизации (в сети любой, получивший доступ к серверу, может взять с него данные), ни постоянного хранилища данных (а что, если машина не работает?), ни даже мало-мальских мер предосторожности. Мало того, в каждый момент времени можно обрабатывать только один запрос. Если клиент остановится в середине транзакции, мы «влипли». Более изощренные примеры можно найти в книгах «Advanced Perl Programming» (Углубленное программирование на Perl) Шрирама Шринивасана (Sriram Srinivasan) и «Perl Cookbook» («Perl: Библиотека программиста») Тома Кристиансена (Tom Christiansen) и Натана Торкингтона (Nathan Torkington), обе выпущены издательством O'Reilly. Модуль Net:: Daemon Джошена Вьедмана (Jochen Wi-edmann) также поможет создавать более сложные программы-демоны.
    Однако пора вернуться к рассмотрению других ошибок, допускаемых в программах для системного администрирования, отправляющих почтовые сообщения.

    Общение напрямую по почтовым протоколам

    Общение напрямую по почтовым протоколам

    Последний выбор - написать программу, общающуюся с почтовым сервером на его родном языке. Большая часть этого языка документирована в RFC821. Вот как выглядит основной обмен данными по SMTP (Simple Mail Transfer Protocol, простой протокол передачи почты). Данные, которые мы посылаем, выделены жирным шрифтом:
    %
    telnet example.com 25 -соединяемся с SMTP-портом на example.com
    Trying 192.168.1.10 ... Connected to example.com. Escape character is '"]'.
    220 mailhub.example.com ESMTP Sendmail 8.9.1a/8.9.1: Sun, 11 Apr 1999 15:32:16 -0400 (EOT)
    HELD client.example.com -идентифицируеммашину,с которой мы.
    пришли (можно использовать EHLO)
    250 mailhub.example.com Hello dnb@client.example.com [192.168.1.11]. pienSt;' to meet you
    MAIL FROM: «jnb@example.com> - определяем отправителя 250 ... Sender ok
    RCPT TO: - определяем получателя
    250 ... Recipient ok
    DATA - начинаем отправлять данные, не забывая о некоторых ключевых заголовках
    354 Enter mail, end with "." on a line by itself From:
    David N. Blank-Edelman (David N. Blank-Edelman) To: dnbeexample.com Subject: SMTP - хороший протокол
    Просто хочу напомнить себе о том, насколько я люблю SMTP. С миром, dNb
    - завершаем сообщение
    250 РАА266Р4 Message accepted for delivery QUIT - конец сессии
    221 mailhuD.example.com closing connection Cor.rectici closed by foreign host.
    Несложно записать в сценарий подобную беседу. Можно было бы использовать модуль Socket или что-нибудь вроде Net: : Telnet, как в главе 6 «Службы каталогов». Но существует несколько хороших модулей для отправки почты, которые упрощают эту задачу. Среди них модуль Женды Крыницки (Jenda Krynicky) Mail; :Se'-oer, Mai 1: :Sendmail Мили-вожа Ивковича (Milivoj Ivkovic) и Mail::Ma:Ier из пакета MailTools Грэхема Бара (Graham Barr). Все эти модули не зависят от операционной системы и будут работать практически везде, где доступен современный дистрибутив Perl. Мы рассмотрим Mail: :Mailer, поскольку он предлагает единый интерфейс к двум способам отправки почты, которые обсуждались до сих пор. Как и в случае с большинством модулей, написанных в объектно-ориентированном стиле, первый шаг заключается в создании экземпляра нового объекта:
    use Mail::Mailer;
    $f rom="me\@example. coin"; $to="you\@example.com";
    $subject="Hi there"; $body="message body\n";
    $type="srnto"; $server="mail.example,com";
    my Smaller = Mail::Mailer->new($type, Server -> $server) or die
    "Невозможно создать новый объект mailer:$'\n".
    Переменная $type позволяет выбрать один из следующих типов поведения:
    smtp
    Посылает почту, обращаясь к модулю Net: :SMTP (часть пакета lib-net), доступному и для большинства не-Unix версий Perl. Если используется MailTools версии 1.13 или выше, можно задать имя SMTP-сервера, применяя приведенную выше символику =>. В противном случае, придется устанавливать имя сервера во время процедуры установки libnet.
    mail
    Отправка почты при помощи почтового агента mail (или любого другого, который задан вторым необязательным аргументом). Это напоминает недавнее использование AppleScript и MAPI.
    sendmail
    Отправка почты с помощью программы sendmail, как и в первом случае из данного раздела.
    Кроме того, можно установить переменную окружения PtRL_MAILERS, чтобы изменить путь, установленный по умолчанию для поиска программ (например, sendmail) в системе.
    Вызов метода ореп() для нашего объекта Mail:: Mailer заставляет последний выполнять роль дескриптора для исходящего сообщения. В этом вызове передаются заголовки сообщения ссылке на анонимный хэш:
    $mailer->open({From => $from, То => $to.
    Subject => Ssubject}) or die "Невозможно заполнить объект mailer
    Тело сообщения выводится в этот псевдодескриптор, который потом закрывается для отправки сообщения:
    print Smailer $body: $mailer->close;
    Этого вполне достаточно, чтобы отправка почты из Perl не зависела от системы.
    В зависимости от того, какой тип поведения $type был выбран при работе с модулем, могут оказаться скрытыми (а могут и не оказаться) более сложные вопросы, относящиеся к МТА, о которых уже говорилось. В предыдущем примере использовалось поведение smtp, а это означает, что программа должна быть достаточно умна, чтобы обрабатывать такие сбои как недоступность сервера. Приведенный пример не настолько «сообразителен». Обязательно позаботьтесь о таких моментах, когда будете писать программы.

    Отправка почты Начнем с рассмотрения

    Если вы работаете на Win32, то вам повезло, т. к. я знаю по крайней мере о трех версиях sendmail, перенесенных под Win32:
  • трех версиях sendmail, перенесенных под Win32:
  • Перенесенная версия sendmail от Cygwin (http://dome/weeg.ui-owa.edu/pub/domestic/sos/ports)
  • Коммерческая версия sendmail от Mercury Systems (http://www.de-mobuilder.com/sendmail.htm)
  • Коммерческая версия Sendmail for NT от Sendmail, Inc. (http: www.sendmail.com)
  • Тем, кому нужно что-то менее тяжеловесное и кто хочет внести некоторые изменения в программу на Perl, чтобы поддержать различные аргументы командной строки, возможно, помогут достичь цели другие программы для Win32:
  • blat (http://www.interlog.com/~tcharron/blat.html)
  • netmall95 (http://www.geocitles.com/SlliconValley/Lakes/2382/net mail.html)
  • wmailto (http://www.impaqcomp.com/jgaa/wmailto.html)
  • Преимущества такого подхода состоят в том, что можно выбросить из сценария все сложности отправки почты. Хороший агент передачи почты (МТА) пытается повторно соединиться с почтовым сервером, если тот в данный момент недоступен, выбирает нужный целевой сервер (ищет записи Mail eXchanger в DNS и осуществляет переходы между ними), при необходимости переписывает заголовки, справляется с внезапными коллизиями и т. д. Если можно избежать необходимости заботиться обо всем этом в Perl, то это просто замечательно.

    Отслеживание спама Теперь когда

    На некоторых сайтах ведутся локальные черные списки узлов, известных как распространители спама. Такая тактика была взята на вооружение, когда спам только начал появляться, и некоторые провайдеры отказывались принимать меры даже против самых закоренелых клиентов-спамеров. В ответ на это в основных агентах передачи почты появились механизмы, отвергающие соединения от узлов и доменов, входящих в список антисоциальных.
    Можно использовать такой черный список, чтобы разобраться, проходило ли сообщение через узлы, известные своим спамерством. Ясно, что в черном списке нет сервера, передавшего нам почту (иначе с ним просто не было бы установлено соединение), но любой из остальных почтовых серверов, указанный в заголовках Received:, вполне может там быть.
    Не существует способа написать одну программу, проверяющую все возможные черные списки агентов передачи почты, поскольку разные агенты хранят эту информацию в различных форматах. Большая часть узлов в Интернете в настоящее время применяет в качестве агента передачи почты sendmail, так что в нашем примере будет применяться его формат черного списка. В новых версиях sendmail черный спи- сок хранится в базе данных при помощи библиотек Berkeley DB 2.X, доступных на http://www.sleepycat.com.
    Поль Маркес (Paul Marquess) написал модуль BerkeleyDB, специально предназначенный для работы с библиотеками Berkeley 2.x/3.x. Это может сбить с толку, поскольку в документации по DB_File, еще одному известному модулю Маркеса, входящему в состав дистрибутива Perl, также рекомендуется применять библиотеки 2.х/З.х. DB_File использует библиотеки Berkeley DB 2.х/З.х в «режиме совместимости» (в частности, библиотека собирается с ключом --enable-compat185, так что доступен API версии 1.x API). Модуль BerkeleyDB позволяет программисту на Perl применять расширенные возможности из API версии 2.х/З.х.
    Агент передачи sendmail использует формат BerkeleyDB 2.х/З.х, так что нужно включить модуль BerkeleyDB. Вот пример, который выводит содержимое локального черного списка:
    Sblacklist = "/etc/mail/blacklist.db"; use BerkeleyDB;
    Ясвяжем хэш %blist с черным списком, используя Berkeley DB
    # для получения значений
    tie %blist, 'BerkeleyDB::Hash', -Filename => Sblacklist or die
    "Невозможно открыть файл $filenane: $! SBerkeleyDB::Error\n" ;
    # обходим в цикле каждый ключ и значение из этого файла, и
    выводя только записи REJECT while(($key,$value) = each %blist){
    в списке также могут быть записи "OK", "RELAY" и др. next
    if ($value ne "REJECT");
    print "$key\n": }
    Принимая за основу этот код, можно написать подпрограмму, проверяющую, находится ли данный узел или домен (содержащий этот узел) в черном списке. Если нужно узнать об узле mallserver.spam-mer.com, следует обойти в цикле все записи из черного списка (в котором могут находиться mailserver.spammer.com, spammer.com или даже просто spammer), чтобы проверить, содержатся ли в имени узла какие-либо записи из него.
    Существует много способов написать на Perl программу, сравнивающую список значений с какими-либо данными. Но для того чтобы программа была эффективной и интересной, мы будем использовать две продвинутые технологии. Они созданы для уменьшения числа компиляций регулярных выражений, которые применяются в ходе выполнения программы. Каждый раз, когда программа использует «новое» регулярное выражение, Perl должен компилировать его заново. Например, в этом отрывке кода Perl вынужден обрабатывать новое значение на каждой итерации:
    вообразите себе внешний цикл, в котором этот код вызывается
    множество раз foreach Smatch (qw(alewife davis porter harvard central кепааН park))
    {
    Sstation =" /Smatch/ and print "found our station stop'": }
    Этот процесс требует больших вычислительных затрат, так что если бы можно было сократить количество вычислений, программа стала бы намного эффективней. Время, потраченное на компиляцию регулярных выражений, становится значительным в программах, где в цикле рассматривается список различных регулярных выражений.
    Вот пример первой технологии, созданной для решения указанной проблемы:
    use BerkeleyDB;
    Sblacklist = "/etc/mail/blacklist.db";
    &loadblist;
    и принимаем имя узла в качестве аргумента командной строки и
    сообщаем, если оно есть в черном списке
    if (defined &checkblist($ARGV[0])){
    print "*** $found найден в черном списке \п"; }
    И
    загружаем черный список в массив анонимных подпрограмм sub loadblist{ tie %blist, 'BerkeleyDB::Hash', -Filename => Sblacklist or die
    "Невозможно открыть Sfilename:
    $! $BerkeleyDB::ErrorXn" ;
    while(my($key,$value) = each %blist){
    # в черном списке могут быть "OK", "RELAY" и пр. next if ($value ne "REJECT");
    push(@blisttests, eval 'sub {$_[0] =~ \Q$key/o and $key}'); } }
    sub checkblist{
    my($line) = shift:
    foreach Ssubref (@blisttests){
    return Sfound if (Sfound = &$subref($line)); }
    return undef: } В этом примере используются анонимные подпрограммы - технология, продемонстрированная в книге Джозефа Хола (Joseph Hall) «Effective Perl Programming» (Эффективное программирование на Perl) (Addison Wesley). Для каждой записи из черного списка создается анонимная подпрограмма. Каждая подпрограмма сверяет переданные ей данные с одним из элементов черного списка. Если они совпадают, такая запись возвращается. Ссылки на эти подпрограммы хранятся в списке. Вот строка, в которой создается подпрограмма и ссылка на нее добавляется к списку:
    push(@blisttests, eval 'sub <$_[0] =" /\0$key/o and $key}');
    Так что, если в черном списке есть запись spammer, ссылка на код, добавленная в массив, будет указывать на подпрограмму, по сути эквивалентную следующей:
    sub {
    $_[0] =" /\Qspammer/o and "spammer"; }
    в начале регулярного выражения присутствует для того, чтобы точки (как в .сот) или другие зарезервированные знаки пунктуации не считались бы метасимволами регулярных выражений.
    Позже в программе будет обойден в цикле список ссылок на код и выполнена каждая маленькая подпрограмма для переданных данных. Если результатом каких-либо из этих вычислений окажется значение «истина», мы вернем код возврата подпрограммы:
    return $found if (Sfound = &$subref($line));
    Компиляция регулярного выражения, которая нас так беспокоит, происходит всего один раз - при создании ссылки на подпрограмму. Можно вызывать каждую подпрограмму столько раз, сколько надо, не теряя время на компиляцию регулярного выражения.
    Существует и другой, чуть менее продвинутый подход, который можно применять, если у вас Perl версии 5.005 или выше. В Perl 5.005 была введена новая синтаксическая конструкция, названная «прекомпи-лируемым регулярным выражением», которая делает подобную задачу несколько проще. Переписать код, используя эту новую конструкцию, можно было бы примерно так:
    sub loadblist{
    tie %blist, 'BerkeleyDB::Hash', -Filename => $blacklist or die
    "Невозможно открыть файл $Шегше:
    $BerkeleyDB: :Error\n" ;
    while(my($key,$vaiue) = eac^- %blist){
    # в черном списке могут бьть запис/ "OK". "RELAY" и пр. next
    (Svalue ne "PEJECT") push('SDlisttests, [qr/\Q$i sub checkblisu
    my($iine) = shift;
    foreach my Stest (§blisttests){
    my($re,$kr;v) = a{$test}
    return $key i* ($line =" /$re/): }
    return undef; }
    На этот раз ссылка была перенесена на анонимный массив в@blist test. Первый элемент этого массива - скомпилированное регулярное выражение, созданное с применением нового синтаксиса qr/Y. Это позволяет сохранить регулярное выражение после его компиляции. Такая форма обработки значительно увеличивает скорость выполнения программы при дальнейшем поиске соответствия. Второй элемент анонимного массива - сама запись из черного списка, которая будет возвращена при найденном соответствии скомпилированному регулярному выражению.

    Поиск в черном списке для всего Интернета

    Поиск в черном списке для всего Интернета

    В последнем примере программы на вопрос «Спамер ли это?» мы отвечали, руководствуясь собственным мнением об узле или домене, не принимая во внимание опыт остальных пользователей Интернета. Существуют несколько спорные службы, предлагающие простой доступ к глобальным черным спискам спамеров или известных узлов, открыто допускающих ретрансляцию почты. Две хорошо известные службы такого типа - Realtime Blackhole List (RBL) от Mail Abuse Prevention System и Open Relay Behaviour-modification System (ORBS). Для получения доступа к этим спискам:
  • Измените на обратный порядок следования элементов проверяемого IP-адреса. Например, 192.168.1. 34 станет 34.1.168.192.
  • Добавьте специальное имя домена к полученному числу. Для проверки адреса в RBL необходимо использовать 34.1.168.192. rbl.
  • Выполните запрос к DNS-серверу для данного адреса. Если вы получите положительный ответ (т. е. запись о ресурсах А), это означает, что данный IP-адрес находится в черном списке.
  • Несколько менее спорным является список Dial-up User List, также поддерживаемый силами специалистов из Mail Abuse Prevention System. Это список диапазонов IP-адресов, динамически присваиваемых модемным пулам. Теоретически, SMTP-соединения не должны исходить от какого-либо из этих узлов. Почта с таких узлов должна отправляться через почтовый сервер провайдера (которого нет в этом списке).
    Вот один из способов проверить, находится ли IP-адрес в каком-либо из этих списков:
    sub checkaddr{
    my($ip,Sdomain) = @_;
    return undef unless (defined Sip);
    my $lookupip = join('.',reverse split(/\./,$ip));
    if (gethostbyname($lookupip.$domain)){
    return Sip; } else {
    return undef; } }
    Очень скоро эта подпрограмма будет добавлена в предпоследний пример этого раздела. А пока, располагая существенно большим объемом информации о каждом из заголовков Received:, попробуем вычислить человека или людей, ответственных за администрирование каждой машины из списка. Модуль Net: ;Whois, уже рассмотренный в главе 6, вероятно, первым будет использоваться для решения этой проблемы.
    К сожалению, этот модуль специализируется только на получении информации о связи имен и доменов (name-to-domain information). Кроме того, он «предполагает», что информация будет представлена в виде, используемом InterNIC. Нам могут понадобиться сведения о связи IP-адресов и доменов (IP address-to-domain information) от WHOIS-серверов на http://whois.arin.net (American Registry for Internet Numbers), http://whois.ripe.net (European IP Address Allocations) и http:// whois.apnic.net (Asia Pacific Address Allocations). Отсутствие соответствующего модуля - первое препятствие, которое необходимо преодолеть.
    Но даже если бы мы знали, как соединиться со всеми этими реестрами и обработать их различные форматы вывода, было бы неясно, к какому из них нужно обращаться для поиска информации о данном IP-адресе. Нам необходимо определить, к какому серверу нужно обратиться, и это второе препятствие. К счастью, если обратиться к ARIN с запросом по адресу, не принадлежащему его базе данных, он направит нас на нужный реестр. Так что, если мы спросим ARIN об адресе из Японии, он отправит нас на APNIC.
    Для преодоления первого препятствия можно использовать модуль общего назначения, подобный Net: :Telnet из главы 6. Другой путь - уже рассмотренный модуль 10: :Socket. Что выбрать- дело личных предпочтений, ну и, конечно, необходима возможность доступа к нему с вашей платформы.
    Служба WHOIS работает на порту 43 TCP, хотя ее имя будет использоваться только в целях предосторожности. С WHOIS-сервером очень просто общаться. Необходимо соединиться, выполнить запрос (в нашем случае это IP-адрес) и получить ответ. Программа, запрашивающая произвольный WHOIS-сервер, очень проста:
    sub getwhois{
    my($ip) = shift; my(Sinfo);
    $cn = new Net::Telnet(Host => Swhoishost, Port => 'wliois',
    Errmode => "return", Timeout => 30)
    or die "Невозможно установить -соединении с
    Swhoishost connection:$!\n";
    unless ($cn->print($ip."\n")){
    $cn->close;
    die "Невозможно послать
    $ip на Swhoishost: ".$cn->errmsg."Vv }
    while ($ret = $cn->get){
    Sinfo ,=$ret; };
    $cn->close;
    return $info; 1
    Для преодоления второго препятствия, состоящего в выборе нужного реестра, есть, по крайней мере, две возможности. Можно послать запрос к http://whois.arin.net и проанализировать ответ. Например, вот запись диалога с ARIN по поводу IP-адреса японской машины. Жирный шрифт используется для выделения текста, введенного человеком:
    X telnet whois.arin.net 43
    Trying 192.149.252.22 ..
    Connected to whois. arm. not Escape character is '"] 210.161.92.226
    Asia Pacific Network Information Center (NETBLK-APNIC-CIDR-BLK)
    Level 1 - 33 Park Roan1 Milton, 4064 AU
    Netname: APNIC-CIDR-BLK2
    Netblock: 210.0.0.0 - 211.255.255.0
    Coordinator;
    Administrator, System (SA90-ARIN)
    sysadn®APNIC,NET +61-7-3367-0490
    Domain System inverse mapping provided by:
    SVC01.APNIC.NET 202.12.28.131
    NS.TELSTRA.NET 203.50.0.137
    NS.KRNIC.NET 202.30.64.21
    NS.RIPE.NET 193.0.0.193
    -** please refer to whois.apnic.net for more information *«*
    *«* before contacting APNIC
    *-« use whois -h whois.apnic.net *»*
    Record last updated on 04-Mar-99.
    Database last updated on 19-Apr-99 16:14:16 EOT.
    Подобные результаты означают, что запрос нужно послать к http:// whois.apnic.net.
    Другой способ - послать запрос к «умному» WHOIS-серверу, который сделает всю работу сам. Из них мне больше всего нравится сервер -http://whois.geektools.com. «Умник» проанализирует ваш запрос, отправит его на нужный WHOIS-сервер и вернет результаты. Тому, кто пользуется этой службой, не нужно беспокоиться о том, на каком именно сервере хранится информация.
    Чтобы код программы сильно не разрастался, а мы не отвлекались от обсуждаемой темы, будем пользоваться вторым (более простым) способом.
    Поместим все эти маленькие запросы в одну большую программу и запустим ее. Итак, если нужно выполнить все приведенные выше подпрограммы с нашим примером сообщения:
    use Mail:'Header: use Socket; use BerkeleyDB;
    use Net::Telnet;
    $header = new Mail-Header \*STDIN;
    Sheader ->unfold('Received'); @received = $header->get('Received'):
    Srbldomain = ".rbl.maps.vix.com" Sorbsdomain = ".relays.orbs.org";
    Sduldomain = ".dul.maps.vix.com": Sblacklist = "/etc/mail/blacklist.db":
    Swhoishost = "whois.geektools.com";
    &loadblist;
    for (reverse @received){ chomp;
    parseline($_);
    if (!defined $ehelo and !defined Svalidname and !defined $validip){ print "$Дп";
    >
    else {
    Sflags = (&checkaddr($validip:$rbldomain) ? "R" : ""):
    # в RBI/' $flags .= (&checkaddr($validip,$orbsdomairi) ? "0" : "");
    в ORBS9 Sflags ,= (&checkaddr($validip. Sduldomain) ? "D......);
    $flags .= (&checkblist($_) 9 "B" : ""): # в нашем спи
    $flags .= (&checkrev($validip,$validname) ? "L" "");
    (I rev-.;o.;k: push(@iplist,Svalidip);
    write; } }
    for (@iplist){
    print "\nWHOIS info for $_:\n"; print &getwhois($_); >
    format STDOUT =
    @««««««««<«« @<«««««<'<««««««<«««««
    Sehelo.Svalidname.Svalidip.Sflags
    то будут получены такие результаты (слегка сокращенные):
    login_0246.whynot.net mx.whynot.net 206.212.231 88 ;
    extreme host-209-214-9-150 :r.a 209.214.9.150 Or
    isiteinc.cori www.isiteinc.com 206,136 243 2
    WHOIS info for 206,212.231.88:
    WHOIS info for 209.214.9.150;
    BellSouth.net Inc. (NETBLK-BELLSNET-BLK4)
    1100 Ashwood Parkway
    Atlanta. GA 30338
    Netname: BELLSNET-BLK4
    Netblock: 209.214.0.0 - 209.215.255.255
    Maintainer: BELL
    Coordinator-...
    WHOIS info for 206.136.243.2:
    Brainsell Incorporated (NET-ISITEINC)
    4105-R Laguna St.
    Coral Gaoles, FL 33146
    US
    Netname: ISITEINC Netnumber: 206.136.243.0
    Coordinator:...
    Гораздо лучше! Теперь нам известно:
  • Спамер дал неверные ответы HELO/EHLO.
  • Первый узел, по всей вероятности, фальшивый (не удалась попытка разыменования, и по нему нет информации WHOIS).
  • Сообщение, скорее всего, попало в сеть через соединение по телефонным линиям.
  • Два из этих адресов уже находятся в нашем черном списке.
  • ORBS они тоже не нравятся.
  • Кроме того, нам известна контактная информация для связи с провайдером.
  • Perl помог разобраться с непрошеной коммерческой почтой.
    Впрочем, спам это очень неприятная вещь. Лучше перейдем к более приятной теме, например, к взаимодействию пользователей средствами электронной почты.

    Получение почты

    Получение почты

    Обсуждая в этом разделе получение почты, мы не будем говорить о ее сборе (fetching). Передача почты с одной машины на другую не представляет особого интереса. Модули Mail: :POP3Client Сина Дауда (Sean Dowd) и Mail: :Cclient Малколма Битти (Malcolm Beattie) легко могут передать почту по протоколам POP (Post Office Protocol) или ШАР (Internet Message Access Protocol). Гораздо интереснее посмотреть, что с этой почтой делать после ее получения, и именно на этом мы и остановимся.
    Начать следует с основ, поэтому рассмотрим инструменты, позволяющие разбить как отдельные сообщения, так и почтовые ящики. Чтобы рассмотреть первое, вновь обратимся к пакету MailTools Грэма Бара (Graham Barr), на этот раз прибегнем к модулям Mail : :Interne: и Mail : :Header.
    Разбиение отдельных сообщений
    Модули Mail: :Internet и Mail: :Header предлагают удобный способ разбить заголовки почтового сообщения, соответствующего RFC822. RFC822 определяет формат почтового сообщения, включая имена допустимых заголовков и их форматов.
    Модулю Mail: : Internet необходимо передать либо файловый дескриптор файла с сообщением, либо ссылку на массив, содержащий его строки:
    use Hail::Internet: $messagefile = "mail";
    open(MESSAGE,"Smessagefile") or die "Невозможно открыть $messagefile:$!\n";
    Smessage = new Mail::Internet VMESSAGE;
    close(MESSAGE);
    Если мы хотим анализировать сообщение, поступающее в стандартный поток ввода (т. е. переданное через конвейер на стандартный ввод), можно поступить так:
    use Mail::Internet;
    Smessage = new Mail: :Internet VSTDIN;
    Mail: : Internet возвращает экземпляр объекта сообщения. Чаще всего с этим экземпляром объекта будет применяться один из двух методов: body() и head(). Метод body() возвращает ссылку на анонимный массив, содержащий строки тела сообщения. head() более интересен и предлагает плавное продолжение модуля Mail: : Header.
    При загрузке Mail::Internet неявно загружается Mail::Header. Если вызвать метод head() модуля Mail: :Internet, он вернет экземпляр объекта заголовка Mail: :Header. Это будет тот же экземпляр объекта, который можно получить, если использовать не Mail: :Internet, а напрямую Mail: :Header:
    use Mail::Header; Smessagefile = "mail";
    open(MESSAGE,"Smessagefile") or die "Невозможно открыть $messagefile:$!\n"
    $header = new Mail:: Header VMESSAGE;
    close(MESSAGE):
    Объект $header содержит заголовки сообщения и предлагает несколько удобных способов для получения данных. Например, чтобы вывести отсортированный список встречающихся имен заголовков (которые в модуле называются «тегами»), можно добавить такую строчку в конец предыдущего примера:
    print join("\n",sort $header->tags);
    В зависимости от сообщения можно увидеть что-то подобное нижеследующему:
    Сс
    Date
    From
    Message-Id
    Organization
    Received
    Reply-To
    Sender
    Subject
    To
    Необходимо получить все заголовки Received: из сообщения. Вот как это можно сделать:
    received = $header->get("Received");
    Часто методы Mail: :Header используются вместе с объектом Mail: : Internet. Если применять Mail: : Internet для возвращения объекта, содержащего и тело и заголовки сообщения, можно сцепить вместе некоторые методы из этих модулей:
    ©received = $message->head->get("Received"):
    Обратите внимание, что get()вызывается в списочном контексте. В скалярном контексте метод вернул бы только первое вхождение этого тега, в случае, если бы мы не задали вторым аргументом порядковый номер тега. Например, get("Received", 2) вернет вторую строку Receivec: из сообщения. Модуль Mail: : Header предоставляет и другие методы для удаления и добавления тегов в заголовки; подробную информацию можно найти в документации.

    Пропуск темы сообщения Строка

    Недостаточная информация в теле сообщения

    Эта ошибка попадает в ту же категорию, что и предыдущая. Если сценарий собирается сообщать о проблемах или ошибках в почтовых сообщениях, значит сгенерировать какую-то информацию, которая должна там присутствовать. Они сводятся к каноническим вопросам журналистики:
    Кто?
    Какой сценарий сообщает об ошибке? Добавьте содержимое переменной $0 (если не устанавливали ее явно), чтобы показать полный путь к текущему сценарию. Сообщите о версии сценария, если таковая у него имеется.
    Где?
    Сообщите что-то о том месте в сценарии, где возникает проблема. Функция caller () из Perl возвращает всю нужную для этого информацию:
    замечание: то, что возвращает са!1ег(). может зависеть от версии Perl, так что обязательно загляните в документацию по perlfunc
    (Spackage, $filename, $line, $subroutine, Shasargs,
    $wantarray, Sevaltext, $is_require) = caller($frames);
    Где $f rames - это количество нужных фреймов на стеке (если вызывались подпрограммы из подпрограмм). Чаще всего вы будете устанавливать $f rames в 1. Вот пример списка, возвращаемого функцией caller() в середине кода для сервера из последнего полного примера:
    ('main','repserver1,32,'main:iprintmail1,1,undef)
    Подобная запись указывает, что сценарий, запущенный из файла repserver в строке 32, находился в пакете main. В этот момент выполнялся код из подпрограммы main: .printmail (у нее есть аргументы, кроме того, она не вызывается в списочном контексте).
    Если вы не хотите вручную применять caller(), можете воспользоваться отчетом о проблемах, предоставляемым модулем Carp.
    Когда?
    Опишите состояние программы в момент возникновения ошибки. К примеру, какой была последняя строка прочтенных данных?
    Почему?
    Если сумете, ответьте на незаданный читателем вопрос: «Зачем беспокоить меня этим почтовым сообщением?» Ответ может быть очень простым, например: «данные об учетных записях не были полностью обработаны», «DNS-сервер сейчас недоступен» или «в серверной пожар». Это даст читающему представление о предмете разговора (и, возможно, побудит к изучению).
    Что?
    Ну и наконец, надо сказать о том, что же пошло не так раньше всего. Вот небольшой пример на Perl, который охватывает все эти пункты:
    use ext::Wrap;
    sub problemreport {
    9 $shortcontext - описание проблемы в одной строке
    # Susercontext - подробное описание проблемы Получение почты 315
    Snextstep - лучшее предположение о том, что делать, чтобы исправить
    проблему
    my($shortcontext,Susercontext,Snextstep) = @_: my
    ($filename, $line, Ssubroutine) = (caller(1) )[1,2,3]:
    push(@return,"Проблема с Sfilename $shortcortext\n '):
    push(@return,"** Сообщение о проблеме с Sfilename ***\n\n"):
    push(@return,fill("","","- Проблема: Susercontext") "\r\n")
    push(@return,"- Место: строка Sline файла Sfilename в
    $subroutine\n\n"); push(@return,"- Произошла: ".scalar localtime(time)."\n\n");
    push(@return,"- Дальнейшие действия: $nextstep\n"):
    \@return; }
    sub fireperson {
    Sreport = &problemreport("компьютер горит ", «EOR, «EON);
    При составлении отчета загорелась задняя часть компьютера.
    Случилось это сразу же после обработки пенсионного плана для ORA. EOR
    Пожалуйста, потушите пожар, а потом продолжайте работу. EON
    print @{$report}; } &fireperson;
    Обращение к &problemreport выведет, начиная с темы сообщения, отчет о проблеме, согласующийся с Mail: : Mailer, как и в предыдущих примерах.
    Теперь, разобравшись с отправкой почты, перейдем к другой стороне медали.


    Распространенные ошибки при отправке

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

    Разбиение почтового ящика

    Разбиение почтового ящика

    Перейти на следующий уровень, где мы разобьем на части почтовые ящики, довольно просто. Если почта хранится в формате «классического mbox» или qmail (еще один агент передачи почты, подобный sendma.il), можно использовать модуль Mail : : Folder Кевина Джонсона (Kevin Johnson). Многие из распространенных почтовых агентов (не в Unix), таких как Eudora, тоже хранят почту в классическом формате Unix mbox, так что этот модуль может быть полезным на многих платформах. Что-то подобное мы уже видели:
    use Mail': Folder: :Mbox: для классическое фермата Unix mbox
    Sfolder = new Mail::Folder('mbox',"filename1 )
    Конструктор new() принимает тип формата почтового ящика и имя файла для анализа. Он возвращает экземпляр объекта folder, через который можно запрашивать, добавлять, удалять и изменять сообщения. Чтобы получить шестое сообщение, нужно применить следующее:
    Smessage = $folder->get_message(6);
    Теперь Smessage содержит экземпляр объекта Mail: : Internet. С этим экземпляром объекта можно применять все только что обсужденные методы. Если вам нужен только заголовок сообщения, можно использовать:
    $header = $folder->get_header(6);
    Здесь нет никаких сюрпризов; возвращается ссылка на экземпляр объекта Mail: .'Header. Чтобы узнать о других доступных методах, загляните в документацию по Mail:: Folder.

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

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

    «Advanced Perl Programming»,
    Sriram Srinivasan (O'Reilly, 1997)- в книге есть хороший раздел о программировании серверов. «Effective Perl Programming»,
    Joseph Hall, Randal Schwartz (Addison Wesley, 1998) - полезная книга, в которой можно найти множество идиом Perl. http://www.cauce.org/ -
    сайт от Coalition Against Unsolicited Email (коалиция против непрошеной почты). Существует много сайтов, посвященных борьбе со спамом; этот сайт неплох для начала. Здесь можно найти ссылки на множество других ресурсов, включая те, на которых описан подробный аналиа почтовых заголовков. http://www.eudora.com/developers/scripting.html -
    содержит информацию по Eudora и ссылки на другие источники по AppleScript. http://www.microsoft.com
    и http://msdn.microsoft.com - содержат информацию по «MAPI», «active messaging» и «CDO». Названия этой технологии уже менялись дважды, так что я не решаюсь привести точную ссылку. На сайтах Microsoft содержится масса полезной информации (особенно в разделе про библиотеки MSDN) по этим темам, но она постоянно перемещается с места на место. «Perl Cookbook»,
    Tom Christiansen, Nathan Torkington (O'Reilly, 1998). В этой книге также рассматриваются вопросы программирования серверов. «RFC821 .-Simple Mail Transfer Protocol»,
    J. Postel, 1982. «RFC822:Standard for the format of ARPA Internet text messages»,
    D. Crocker, 1982. «RFC954.-NICNAME/WHOIS»,
    K. Harrenstien, M. Stahl, E. Feinler, 1985.

    Увеличение почты в службу поддержки

    Увеличение почты в службу поддержки

    Даже если у вас нет официальной службы поддержки, наверняка существует несколько адресов, куда пользователи могут посылать свои вопросы и сообщения о трудностях. У электронной почты, как средства для связи по вопросам поддержки, есть несколько преимуществ:
  • Все можно хранить и отслеживать, в отличие от разговоров в коридорах.
  • Электронная почта асинхронна; системный администратор может читать почту и отвечать на нее в более спокойные вечерние часы.
  • По желанию, это может быть индивидуальная, групповая или широковещательная рассылка. Если 14 человек пишут об одном и том же (скажем, об одних и тех же сообщениях спамеров), есть возможность ответить им всем одновременно, когда проблема будет решена.
  • Почту легко переслать тому, кто разбирается в обсуждаемом деле или ответственен за конкретные службы.
  • Все это веские причины использовать электронную почту для связи по вопросам поддержки. Однако есть у электронной почты и недостатки:
  • Если проблема касается самой почтовой системы или у пользователя что-то не ладится с электронной почтой, то для связи нужно применять что-то другое.
  • Пользователи могут и будут писать в сообщениях все что угодно. Нет никакой гарантии, что в письме будет информация, которая пригодится для решения проблемы или помощи пользователю. Возможно, вы даже не поймете, зачем нужно было такое сообщение. Это приводит к головоломке, о которой стоит поговорить в данном разделе.
  • Мое любимое письмо в службу поддержки приведу точно в таком виде, в каком оно было получено, за исключением имени автора:
    Date: Sat, 28 Sep 1996 12:27:35 -0400 (EOT) From:
    Special User To:
    systems@example.com Subject: [Req. 89531] printer help
    something is wrong and I
    have know idea what (что-то случилось, и я не имею понятия, что именно)
    Если бы пользователь не упомянул слово «принтер» в теме сообщения, не было бы никаких указаний на то, с чего начать, и нам, вероятно, пришлось бы думать, что и впрямь случилось нечто ужасное. Конечно, это самый крайний случай. Чаще вы будете получать примерно такую почту:
    From: Another user
    Subject: [Req 814563] broken macine
    To. systems§exarriple com
    Date: Wed, 11 Mar 1998 10:59:42 -0500 (EST)
    С малиной krakatoa. example, com
    Пользователи посылают подобные письма, лишенные содержания, не со зла. Мне кажется, что корень всех бед в полном несоответствии представлений о компьютерной среде у пользователей и системных администраторов.
    Для большинства пользователей видимая структура компьютерной среды ограничена клиентской машиной, на которой они работают, соседним принтером и их хранилищем данных (т. е. домашним каталогом). Для системного администратора структура совсем иная. Он видит ряд серверов, предоставляющих услуги клиентам, у каждого из которых может быть множество периферийных устройств. На каждой машине может быть установлено различное программное обеспечение и все они могут находиться в различном состоянии (системная загрузка, конфигурация и т. д.).
    Для пользователя вопрос «С какой машиной проблемы?» кажется странным. Они говорят об одном компьютере, о том, на котором работают сейчас. Неужели это не очевидно? Системному администратору столь же странной кажется просьба «помогите с принтером»; в конце концов, он следит за многими принтерами.
    Также обстоит дело и со спецификой проблемы. Системные администраторы всего мира каждый день скрежещут зубами, получая почту, в которой сказано: «Мой компьютер не работает, не могли бы вы помочь мне?». Они знают, что «не работает» может относиться к множеству симптомов, у каждого из которых есть свои причины. Для пользователя, столкнувшегося с тремя зависаниями компьютера за неделю, слова «не работает» выглядят вполне конкретными.
    Один из способов разобраться с таким расхождением - четко определить, какие данные посылать в сообщениях. На некоторых сайтах пользователь должен послать сообщение о проблеме, употребляя определенную форму или приложение. Беда такого подхода в том, что пользователи редко испытывают удовольствие от лишних движений мышью и нажатий клавиш, необходимых только для того, чтобы сообщить о проблеме или задать вопрос. Чем больше усилий нужно приложить, тем меньше вероятность, что кто-то воспользуется таким механизмом. Неважно, насколько хорошо продумана форма и какой у нее дизайн, если никто не захочет ею пользоваться. Вопросы в коридорах снова станут нормой. Снова вернулись назад?
    Что ж, если применить Perl, может быть и нет. Perl наверняка поможет увеличить количество нормальной почты и поучаствовать в процессе поддержки. Один из первых шагов системного администратора выяснить местоположение: «Где случилась проблема? С каким принтером? С каким компьютером?». И так далее.
    Вот костяк программы, которую я назвал suss, представляющая собой «скелет» для сбора информации. Программа изучает сообщение и пытается выяснить, с какой машиной оно связано. В результате часто можно определить имя узла для писем из категории «С моим компьютером проблемы», не вступая ради этого в дальнейшую беседу с рассеянным пользователем. Имя узла - хорошая отправная точка в процессе поиска возникших проблем.
    Программа suss применяет очень простой алгоритм для разгадывания имени узла (обычно, поиск в хэше для каждого слова из сообщения). Сначала изучается тема сообщения, затем его тело и, наконец, выполняется поиск по заголовкам Received:. Вот упрощенная версия, считывающая файл /etc/hosts, чтобы определить имена узлов:
    use Mail::Internet; $localdomain = ".example.com";
    ft считываем файл /etc/hosts
    open(HOSTS,"/etc/hosts") or die "He могу открыть файл узлов\п";
    while(defined($_ = )){
    next if /"ft/; ft пропускаем комментарии
    next if /"$/; ft пропускаем пустые строки
    next if /monitor/i; ft пример вводящего в заблуждение узла
    ft выделяем первое имя узла и переводим его в нижний регистр
    Smachine = lc((split)[1]);
    $machine =~ s/\Q$localdomain\E$//oi;
    t удаляем имя домена
    $macriines{$machine}++ unless $macnines{$machine}; }
    ft анализируем сообщение
    $message = new Mail: :Internet VSTDIN;
    $message->head->unfold();
    ft проверяем тему сообщения
    my Ssubject = $message->head->get('Subject');
    Ssubject =' s/[.,;?]//g;
    for (split(/\s+/,Ssubject)) {
    if (exists $machines{lc $_}) {
    print "subject: $_\n";
    $found++; } } exit if $found;
    ft проверяем тело сообщения
    chomp(my @body = @{$message->body()}):
    my Sbody = join(" ",@body);
    $body =" s/t"\w\s]/ /g: и удаляем знак
    @body{split( ', lc $bcdy)i = ().
    for (keys %body) {
    if (exists $machines!ic $_}) {
    print "body: $_\n";
    $*ound+-i-; } }
    exit if $found,
    # последняя надежда: проверяем последнюю строку
    Received: Sreceived -(reverse $message->head->get('Received'))[0];
    Sreceived =" s/\0$localdomain\E//g; for (split(/\s+/,Sreceived)) {
    if (exists $machines{lc $_}) { print "received: $_\n";
    }
    Несколько комментариев к программе:
  • Простота проверки становится проблемой, когда мы сталкиваемся с вполне приемлемыми именами узлов, подобных monitor. Если имена узлов, являющиеся обычными словами, могут появиться в сообщениях, вам придется либо специально их обработать, как было сделано с next if /monitor/i, либо придумать более сложную схему анализа, что предпочтительнее.
  • Мы используем срез хэша (§body{...}), чтобы ускорить поиск по телу сообщения. За один шаг из сообщения выделяются все уникальные слова. Чтобы разобраться с этой конструкцией, можно прочитать ее изнутри. Во-первых, split() возвращает из сообщения список всех «слов» (в нижнем регистре). Эти слова используются как ключи для хэша %body. Поскольку имена ключей в хэше повторяться не могут, он будет содержать только уникальные слова из тела сообщения. Именно подобные возможности делают программирование на Perl приятным.
  • Теперь применим эту программу. Вот два настоящих сообщения в службу поддержки:
    Received: from strontium.example.com
    (strontium.example.com [192.168.1.114])
    by mailhub.example.com (8.8.4/8.7.3) with ESMTP id RAA27043
    for ; Thu, 27 Mar 1997 17:07:44 -0500 (EST)
    From: User Person
    Received: (user@localhost)
    by strontium.example.com (8.8.4/8.6.4) id RAA10500 for systems;
    Thu, 27 Mar 1997 17:07:41 -0500 (EST)
    Message-Id: <199703272207.RAA10500@strontium.example.com
    Subject: [Req #11509] Monitor
    To: systems@example.com
    Date: Thu, 27 Mar 1997 17:07:40 -0500 (EST)
    Hi,
    My monitor is flickering a little bit and it is tiresome
    when working with it to much.
    Is it possible to fix it or changing the monitor? Thanks. User,
    Received: from example.com (user2@example.com [192.168.1.7])
    by mailhost.example.com (8.8.4/8.7.3) with SMTP id SAA00732
    for : Thu. 27 Mar 1997 18:34:54 -0500 (EST)
    Date: Thu, 27 Mar 1997 18:34:54 -0500 (EST)
    From: Another User
    To: systems@example.com
    Subject: [Req 811510] problems with two computers
    Message-Id:
    In Jenolen (in room 292), there is a piece of a disk stuck in it.
    In intrepid, there is a disk with no cover
    (or whatever you call that silver thing) stuck in it.
    We tried to turn off intrepid, but it wouldn't work.
    Wo (the proctor on duty and I) tried to get the disk piece out, but it didn't. work.
    The proctor in charge decided to put signs on them saying 'out of order'
    AnotherUser После запуска программы для этих двух сообщений мы получили:
    received: strontium и:
    body: jenolen body: intrepid
    Оба узла были найдены верно и для этого понадобился лишь небольшой отрывок простого кода. Шагнем дальше и предположим, что поступило такое письмо:
    Received: from [192.168.1.118]
    (buggypeak.example.com [192.168.1.118])
    by mailhost.example.com (8.8.6/8.8.6)
    with SMTP id JAA16638 for ;
    Tue, 4 Aug 1998 09:07:15 -0400 (EOT)
    Message-Id:
    Date: Tue, 4 Aug 1998 09:07:16 -0400
    To: systems@example.com
    From: user@example.com (Nice User)
    Subject: [Req «15746] printer
    Could someone please persuade my printer to behave and print like
    printer should9 Thanks much :)
    -Nice User.
    Пользователь, должно быть, не знает, что вы «пасете стадо» из 30 принтеров. Но можно применить Perl и чуть-чуть наблюдательности, чтобы сделать умные догадки. Пользователи стараются печатать на принтерах, расположенных ближе всего к тому компьютеру, за которым в данный момент работают. Если бы можно было определить машину, с которой отправлена почта, вероятно, удалось бы вычислить и принтер. Существует много способов получить информацию о связи компьютер-принтер, например, из отдельного файла, из поля в базе данных узлов, о которой упоминалось в главе 5, или даже из службы каталогов LDAP. Вот простой пример, в котором используется простая база данных компьютеров и связанных с ними принтеров:
    use Mail: .'Internet; use DB_File;
    Slocaldomain = ".example.com";
    # printdb - это файл Berkeley DB.
    Ключи - имена узлов, значения - принтеры Sprintdb = "printdb";
    п анализируем сообщение
    Smessage = new Mail::Internet \*STDIN;
    $message->head->unfold();
    # проверяем тему сообщения
    my Ssubject = $head->get('Subject');
    if (Ssubject =" /print(er|ing)?/i){
    ff ищем машину-отправителя (ситаем, что используется формат заголовков Sendmail)
    $received = (reverse $rr,essage->head->get( 'Received' ))[0];
    ($host) =
    $received =" /"from \S+ \((?:\S+@)?(\S+)\Q$localdomain\E \[/; }
    tie %printdb, "DB_File",Sprintdb or die "Невозможно подключиться к базе данных
    Sprintdb:$!\n";
    print "Проблема на машине Shost может быть связана с принтером " .
    $pnntdb{$host} . "Дп";
    untie %printdb;
    Если в теме сообщения упоминаются слова «печать», «принтер» или «напечатать», мы выделяем имя узла из заголовка Received:. Для получения этой информации можно применить одно регулярное выражение, т. к. известен формат, используемый для заголовков Received: в нашей сети. Зная имя узла, нетрудно найти связанный с ним принтер в базе данных Berkeley DB. Конечный результат выглядит так:
    Проблема на машине buggypea* может быть связана с niroshige. Потратив время на изучение структуры своего окружения, вы найдете разные способы получать больше пользы от почты, доставленной в службу поддержки. Приведенные в этом разделе примеры невелики и созданы для того, чтобы заставить вас задуматься о возможностях. Как еще могут помочь программы, читающие почту (возможно, это почта, отправленная другими программами)? Perl предоставляет много способов проанализировать почту, рассмотреть ее в широком контексте и затем использовать найденную информацию.


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

    Анализ журналов Некоторые системные

    Самый простой подход - обычное «считывание и подсчет». Мы читаем поток данных из журнала, ищем интересующие нас данные и увеличиваем значение счетчика, когда их находим. Вот простой пример, подсчитывающий, сколько раз перегружалась машина, в котором использован файл wtmpx из Solaris 2.6:*
    # шаблон для wtmpx из Solaris 2.6, подробности смотрите в
    # документации no pack()
    Stemplate = "А32 А4 А32 1 s s2 x2 12 1 x20 S A257 x";
    # определяем размер записи $recordsize = length(pack($template,())); и открываем файл
    open(WTMP,"/var/adm/wtmpx") or die "Невозможно открыть wtmpx:$!\n";
    # считываем по одной записи
    while (read(WTMP,Srecord,Srecordsize)) {
    ($ut_user,$ut_id,$ut_line,$ut_pid,$ut_type,$ut_e_termination,
    $ut_e_exit,$tv_sec,$tv_usec,$ut_session,
    $ut_syslen,$ut_host)= unpack($template,Srecord);
    if ($ut_line eq "system boot"){
    print "rebooted ".scalar localtime($tv_sec)."\n"; $reboots++;
    }
    }
    clOse(WTMP);
    print "Общее число перезагрузок: $reboots\n";
    Расширим этот подход и рассмотрим пример сбора статистики при помощи Event Log из Windows NT. Как говорилось раньше, механизм ведения журналов в NT хорошо разработан и довольно сложен. Эта сложность несколько пугает начинающих программистов на Perl. Для получения основной информации из журналов мы будем использовать некоторые подпрограммы из модулей для Win32.
    Программы в NT и компоненты операционной системы записывают свои действия, регистрируя «события» в одном из журналов событий. Регистрация событий операционной системой сопровождается записью основной информации, например, времени наступления события, имени программы или функции операционной системы, зарегистрировавших событие, типа наступившего события (просто информативное или что-то более серьезное) и т. д.
    В отличие от Unix, само описание события, т. е. сообщение, не хранится вместе с записью о событии. Вместо этого в журнал помещается идентификатор EventlD. Этот идентификатор содержит ссылку на определенное сообщение, хранящееся в библиотеке (.dll). Получить сообщение по идентификатору не просто. Этот процесс требует поиска нужной библиотеки в реестре и ее загрузки вручную. К счастью, этот процесс в текущей версии модуля Win32: : EventLog выполняется автоматически (ищите $Win32:: EventLog: :GetMessageText в первом примере с использованием Win32:: Eventlog).
    В следующем примере мы сгенерируем простую статистику по числу записей в журнале System, содержащую сведения о том, откуда они поступили, и об уровне их важности. Мы напишем эту программу несколько иначе, чем первый пример в этой главе.
    Первый наш шаг - загрузить модуль Win32: : EventLog, обеспечивающий связь между Perl и программами для работы с журналами событий в Win32. Затем мы инициализируем хэш-таблицу, которая будет использоваться для хранения результатов вызовов программ чтения журналов. Обычно Perl заботится об этом за нас, но иногда стоит добавить подобный код ради тех, кто будет впоследствии читать программу. Наконец, мы определяем небольшой список типов событий, который позже будет использоваться для печати статистики:
    use Win32::EventLog;
    my %event=('Length', NULL,
    'RecordNumber',NULL, TimeGenerateo",
    NULL. TimeWritten',NULL, 'EventID',NULL, 'EventType',NULL, 'Category',NULL, 'ClosingRecordNumber' .
    NULL, 'Source',NULL, 'Computer',NULL, 'Strings',NULL, 'Data',NULL,):
    tt
    частичный список типов событий, то есть тип 1 -- "Error" К 2 -- "Warning" и т. д.
    @types = ("","Error","Warning","","Information");
    Наш следующий шаг - открытие журнала событий System. Open() помещает дескриптор EventLog в $EventLog, который можно использовать для соединения с этим журналом:
    Win32::EventLog::Open($EventLog,'System'.'') or die "Невозможно открыть журнал System:$~E\n";
    Получив этот дескриптор, мы можем использовать его для подсчета событий в журнале и получения номера самой старой записи:
    $EventLog->Win32::EventLog::GetNumber($numevents);
    $EventLog->Win32::EventLog::Get01dest($oldestevent);
    Эта информация указывается в первом операторе Read(), позиционирующем нас прямо перед первой записью. Это эквивалентно переходу в начало файла при помощи функции seek():
    $EventLog->Win32::EventLog::Read((EVENTLOG_SEEK_READ |
    EVENTLOG^FORWARDS_READ). $numevents + $oldestevent, $event);
    Теперь в простом цикле прочитываем все записи. Флаг EVENTLOG_SEQ-UENTIAL_READ говорит: «Продолжайте читать с позиции после последней прочитанной записи». Флаг EVENTIOG_FORWARDS_READ перемещает нас вперед в хронологическом порядке. Третий аргумент Read() - смещение, в данном случае равное 0, потому что мы продолжаем с той же позиции, на которой остановились. Считывая каждую запись, мы записываем в хэш-таблицу счетчиков ее источник (Source) и тип события (EventType).
    обходим в цикле все события, записывая количество различных
    источников (Source) и типов событий
    (EventTypes) for ($i=0;$i<$numevents;$i++)
    {
    $EventLog->Read((EVENTLOG_SEQUENTIAL READ
    EVENTLOG_FORWARDS_READ) 0, Sevent):
    $source{$event->{Source}}++:
    $types{$event->{EventType}}++: }
    it
    выводим полученные результаты print "--> Event Log Source Totals:\n"; for (sort keys %source) {
    print "$_: $source{$_}\n": }
    print "-"x30,"\n";
    print "-->Event Log Type Totals:\n"; for (sort keys %types) {
    print "$types[$_J: $types{$_}\n"; }
    print "-"x30,"\n";
    print "Total number of events: $numevents\n";
    Мои результаты выглядят так: -->
    Event Log Source Totals: Application Popup:
    4 BROWSER: 228 DCOM: 12 Dhcp: 12 EventLog:
    351 Mouclass: 6 NWCWorkstation: 2 Print: 27 Rdr: 12
    RemoteAccess: 108 SNMP: 350 Serial: 175
    Service Control Manager: 248 Sparrow: 5 Srv:
    201 msbusmou: 162 msi8042: 3 msinport:
    162 mssermou: 151 qic117: 2
    --> Event Log Type Totals: Error: 493 Warning:
    714 Information: 1014
    Total number of events: 2220
    Как я и обещал, вот пример кода, полагающегося на подобную программу для вывода содержимого журнала событий. В нем используется программа ElDump Джеспера Лоритсена (Jesper Lauritsen), которую можно загрузить с http://www.ibt.ku.dk/jesper/JespersNTtools.htm. ElDump похожа на DumpEl из NT Resource Kit:
    Seldump = 'c:\bin\eldump'; # путь к ElDump
    И выводим поля данных. оаз,:згяя их т/льдой ("). .
    К текста сообщения (быстрее)
    open(ELDUMP."Seldump $dumpflagsj") or die "Невозможно загустить $eidump:$!\n";
    print STDERR "Считываем системный журнал.1:
    while(){
    ($date, $time, $source, $type. Scategory Sevent, $i.ser, Scom.puter) =
    split('-');
    $$type{$source}+-t;
    print STDERR "."; } print STDERR "done.\n";
    close(ELDUMP);
    # для каждого типа события выводим источники и количество
    событий
    foreach $type (qw(Error Warning Information
    AuditSuccess AuditFailure)){
    print "-" x 65,"\n";
    print uc($type)."s by source:\n";
    for (sort keys %$type){
    print "$_ ($$type{$_})\n";
    } } print "-" x 65,"\n";
    Вот выдержка из получаемых данных:
    ERRORS by source:
    BROWSER (8)
    Cdrom (2)
    DOOM (15)
    Dhcp (2524)
    Disk (1)
    EventLog (5)
    RemoteAccess (30)
    Serial (24)
    Service Control Manage1" ("00)
    Sparrow (2)
    atapi (2)
    i8042prt (4i
    WARNINGS by soiree:
    BROWSER (80)
    Cdrom (22)
    Dhcp (76)
    Print (8)
    Srv (82)

    Блокировка ввода в программах обработки журналов

    Блокировка ввода в программах обработки журналов

    Я уже говорил, что это упрощенная версия программы bigbuffy. С упрощением реализации, в особенности на различных платформах, связана неприятная особенность этой версии: во время сброса данных т диск она не может продолжать считывать ввод. Во время сброса буфера программе, посылающей свой вывод bigbuffy, операционная система может дать указание приостановить операции, пока не будет очищен ее буфер вывода. К счастью, сброс данных происходит быстро v. окно, в котором это может произойти, будет очень маленьким, но это все равно неприятно.
    Вот два возможных решения этой проблемы:
  • Переписать bigbuffy, используя двойную буферизацию и многозадачность. Вместо одного буфера можно создать два. Во время получения сигнала программа будет записывать журнал во второй буфер до тех пор, пока дочерний процесс или другой поток обрабатывает сброс данных из первого буфера. При получении следующей сигнала буферы вновь меняются местами.
  • Переписать bigbuffy, чтобы разделить чтение и запись при сброс данных в файл. Самая простая версия этого подхода предполагает что несколько строк записываются в файл каждый раз после про чтения новой строки. Это может оказаться не простым делом, если журнал «разорван» и не поступает постоянным потоком. Вряд л! кому-то захочется ждать новую строку вывода для того, чтобы можно было сбросить буфер на диск. Так что придется использовать тайм-ауты или некий механизм внутренних часов, чтобы справиться с этой проблемой.
  • Оба этих подхода трудно реализовать так, чтобы они были преносимыми между различными платформами, отсюда и упрощенная версия; приведенная в книге.
    Безопасность в программах, обрабатывающих журналы
    Вы могли заметить, что в bigbuffy операциям открытия файлов вывода и записи в них уделяется внимания больше, чем обычно. Это пример защищенного (оборонительного) стиля программирования, о котором упоминалось уже в разделе «Ротация журналов». Если эта программа предназначена для отладки сервера, почти наверняка она будет запущена привилегированным пользователем. Очень важно продумать все ситуации, которые могут привести к тому, что программой кто-то злоупотребит.
    Например, представьте ситуацию, когда файл, в который выводятся данные, был злонамеренно заменен ссылкой на другой файл. Если наивно открыть и записать данные в этот файл, можно обнаружить, что мы перезаписали какой-нибудь важный файл, например /etc/passwd. Даже если мы проверим файл вывода данных перед самым его открытием, злоумышленник может подменить его перед тем, как мы действительно начнем записывать в него данные. Во избежание таких неприятностей можно использовать такой сценарий:
  • Мы проверяем, существует ли файл, в который выводятся данные. Если да, мы выполняем lstat(), чтобы получить о нем информацию.
  • Открываем файл в режиме до записи.
  • Перед тем как собственно записать в него данные, мы выполняем lstat() для открытого файлового дескриптора и проверяем, тот же это файл, что мы ожидаем, или нет. Если это другой файл (т. е. кто-то заменил его ссылкой прямо перед открытием), мы не записываем в него данные и выводим соответствующее предупреждение. Этот шаг позволяет избежать состояния перехвата, о котором говорилось в главе 1.
  • Если дописывать данные не надо, то можно открыть временный файл со случайным именем (чтобы его нельзя было угадать заранее) и потом переименовать его.
    Подобные «уловки» необходимы в большинстве Unix-систем, поскольку первоначально Unix создавался без особой заботы о безопасности. Брешь в безопасности, связанная с символическими ссылками, не является проблемой в NT4, т. к. они являются малоиспользуемой частью подсистемы POSIX, не проблема это и в MacOS, поскольку тут не существует понятия «привилегированный пользователь».

    Черные ящики

    Черные ящики

    В мире Perl часто случается так, что когда вы пытаетесь написать что-то широко используемое, кто-то другой публикует свое решение этой задачи раньше. Это дает возможность просто передать свои данные в уже готовый модуль и получить результаты, не задумываясь о том, как выполняется данная задача. Это часто называют «подходом черного ящика».
    Один такой пример - это пакет SyslogScan Рольфа Харольда Нельсона (Rolf Harold Nelson). Раньше мы уже отмечали, что анализ почтового журнала sendmail может оказаться непростой задачей из-за информации о состоянии. Часто со строкой связана одна или несколько родственных строк, перемешанных с другими строками в этом же журнале. Пакет SyslogScan предоставляет простой способ обратиться к информации о доставке каждого сообщения, так что нет необходимости вручную просматривать файл и выбирать оттуда все связанные строки. Этот пакет позволяет найти в журнале определенные адреса и предоставляет некоторую статистику по найденным сообщениям.
    Пакет SyslogScan объектно-ориентированный, так что первым делом нужно загрузить модуль и создать новый экземпляр объекта:
    use SyslogScan::DeliveryIterator;
    список почтовых журналов
    syslog $maillogs = ["/var/log/mail/maillog"];
    $iterator = new SyslogScan::DeliveryIterator(syslogList => $maillogs):
    Метод new модуля SyslogScan: :DeliveryIterator возвращает итератор (iterator), т. е. указатель в файле, двигающийся от одной строки о доставке сообщения к другой. Применяя итератор, мы избавляемся от необходимости просматривать файл в поисках всех строк, относящихся к конкретному сообщению. Если вызвать метод next() для этого итератора, он вернет нас обратно к объекту доставки. Этот объект хранит информацию о доставке, прежде распределенную по нескольким строкам в журнале. Например, следующий код:
    while ($delivery = $iterator -> next()){ print $delivery->{Sender}." -> ".
    join(",",@{$delivery->{ReceiverList}}),"\n"; }
    позволяет получить такую информацию:
    root@host.ccs.neu.edu -> user1@cse.scu.edu
    owner-freebsd-java-digest@freebsd.org ->
    user2@ccs.neu.edu rootiahost.ccs.neu.edu ->
    user3@ccs.neu.edu
    Можно сделать еще лучше. Если передать итератор из SyslogScan методу new модуля SyslogScan: :Summary, new примет весь вывод метода next итератора и вернет итоговый объект. Этот объект содержит итоговую информацию по всем доставкам сообщений, которые только может вернуть итератор.
    Но SyslogScan переносит эту функциональность на другой уровень. Если передать последний объект методу new из SyslogScan: : ByGroup, мы получим объект bygroup, в котором вся информация сгруппирована по доменам и приводится статистика по этим группам. Вот как применяется то, о чем мы только что говорили:
    use SyslogScan::DeliveryIterator;
    use SyslogScan::Summary;
    use SyslogScan::ByGroup
    use SyslogScan::Usage;
    П местоположение maillog
    Smaillogs = ["/var/log/mail/maillog"];
    # получаем для этого файла итератор
    $iterator = new SyslogScan::DeliveryIterator(syslogList => $maillogs);
    # передаем итератор в ::Summary, получаем объект summary (сводка)
    Ssummary = new SyslogScan::Summary($iterator);
    tt передаем сводку в ::ByGroup и получаем обьект stats-by-group
    # (статистика по группам)
    Sbygroup = new SyslogScan::ByGroup($summary);
    ft выводим содержимое этого объекта foreach $group (sort keys %$bygroup){
    ($bmesg,$bbytes)=@{$bygroup->{$group}->
    {groupUsage}->getBroadcastVolume()}; ($smesg,$sbytes)=@{$bygroup->{$group}->
    {groupllsage}->getSendVolume()}; ($rmesg,$rbytes)=@{$bygroup->{$group}->
    {groupUsage}->getfieceiveVolume()}; ($rmesg,$rbytes)=@{$bygroup->{$group}->
    {groupUsage}->getReceiveVolume()}; write; }
    format STDOUT.TOP =
    Name Bmesg BByytes Smesg SBytes Rmesg Rbytes
    format STDOUT =
    @««««<«««« @»>» @»»>» @»>» @»»»> @»»> @»»»>
    Sgroup,Sbmesg,Sbbytes,$smesg,Ssbytes,$rmesg,$rbytes
    Результат представляет собой подробный отчет по количеству широковещательных, отправленных и полученных сообщений и их размера в байтах. Вот отрывок из получаемых результатов:
    Name Bmesg BByytes Smesg SByres Rmesg Roytes
    globalserve net 1 1245 1 1245 0 0
    gloDe.com 0 0 0 0 1 2040
    Положительная сторона такого подхода в том, что можно сделать многое благодаря тяжелой работе, проделанной автором модуля или сценария, не прикладывая больших усилий со своей стороны. Отрицательная сторона - необходимость во всем полагаться на код автора. В нем могут быть ошибки или может использоваться подход, не устраивающий вас. Всегда стоит сначала ознакомиться с программой, прежде чем «дать ей зеленую улицу» у себя на сайте.

    Данные с состоянием и без Помимо

    Проблемы с пространством на диске

    Недостаток программ, ведущих полезные и подробные журналы, заключается в том, что для хранения этих данных нужно место на диске. Это касается всех трех операционных систем, рассмотренных в данной книге: Unix, MacOS и Windows NT/2000. Среди них, вероятно, в NT/2000 это вызывает меньше всего проблем, потому что центральный механизм ведения журналов имеет встроенную поддержку автоматического отсечения. В MacOS центрального механизма ведения журналов нет, зато можно запустить несколько серверов, которые с удовольствием выведут в журналы достаточно данных, чтобы заполнить пространство на диске, дай им только такую возможность.
    Обычно задача поддержания приемлемого размера для журналов ложится на плечи системного администратора. Большинство производителей Unix предоставляют некий механизм управления размерами журналов вместе с операционной системой, но он часто обслуживает только определенный набор журналов на машине. Как только на машине появляется новая служба, ведущая свой отдельный журнал, возникает необходимость подправить (или даже отбросить) используемый механизм.
    Ротация журналов
    Распространенное решение проблемы с дисковым пространством - ротация журналов. (Необычное решение мы рассмотрим позже в этом разделе). По истечении определенного времени или после того, как будет достигнут определенный размер файла, текущий журнал будет переименован, например, в logfile.O. Последующая запись будет производиться в пустой файл. В следующий раз процесс повторяется, но сперва резервный файл (logfile.O) переименовывается (например в logfile.l). Этот процесс повторяется до тех пор, пока не будет создано определенное количество резервных файлов. После этого самый старый резервный файл удаляется. Вот как выглядит графическое представление такого процесса.
    Этот метод позволяет отвести под журналы приемлемое конечное дисковое пространство. Обратите внимание на способ ротации журналов и функции Perl, необходимые для выполнения каждого шага (табл. 9.2).


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

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

    Иногда не просто писать программы, имеющие дело с журналами. Вместо приятных на вид, легко анализируемых текстовых строк, некоторые средства ведения журналов создают двоичные файлы патентованного формата, которые нельзя проанализировать при помощи одной строки кода на Perl. К счастью, Perl не боится таких напастей. Рассмотрим несколько подходов к работе с такими файлами. Ниже приведены два различных примера двоичных журналов: файл wtmp в Unix и журналы событий NT/2000.
    В главе 3 «Учетные записи пользователей» мы упоминали о регистрации пользователей для работы на машине с Unix. В большинстве Unix-систем регистрация в системе и завершение работы с ней регистрируются в файле wtmp. Если нужно узнать о «привычках» пользователя относительно регистрации (например, на какой машине он обычно регистрируется?), то необходимо обратиться к этому файлу.
    В NT/2000 журналы событий играют более обобщенную роль. Они используются для регистрации практически всех событий, происходящих на машине, включая регистрацию работы пользователей, сообщения операционной системы, события системы безопасности и т. д. Их роль аналогична роли службы syslog в Unix.

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

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

    Модуль Идентификатор UaCPAN Версия
    Win32 : : EvcntLog (распространяется с ActivePerl) 0.062
    Logfile: : Rotate PAULG 1.03
    Getopt : : Long (распространяется с Perl) 2.20
    Time : : Local (распространяется с Perl) 1.01
    SyslogScan RHNELSON 0.32
    DB_File (распространяется с Perl) PMQS 1.72
    FreezeThaw ILYAZ 0.3
    Sys : : Hostname (распространяется с Perl)
    Fcntl (распространяется с Perl) 1.03
    DBI TIMB 1.13


    Использование баз данных встроенных вРег!

    Использование баз данных, встроенных вРег!

    До тех пор пока данных не слишком много, можно применять только Perl. В качестве примера мы будем использовать расширенную версию вездесущего «искателя брешей в системе безопасности». До сих пор наша программа имела дело с соединениями только на одной машине. Как поступить, если захочется узнать о регистрации злоумышленников и на других наших машинах?
    Первый шаг - поместить все данные из wtmp для наших машин в ту или иную базу данных. Будем считать, что все машины имеют прямой доступ к некоторым разделяемым каталогам через некую сетевую файловую систему наподобие NFS. Перед тем как двигаться дальше, необходимо выбрать формат базы данных.
    В качестве «формата баз данных для Perl» я выбрал формат Berkeley DB. Я беру «формат баз данных для Perl» в кавычки потому, что хоть поддержка DB встроена в Perl, сами библиотеки DB необходимо достать в другом месте (http://www.sleepycat.com) и установить их до того, как поддержка Perl будет скомпилирована. Ниже приведено сравнение между различными поддерживаемыми форматами баз данных (табл. 9.4).

    Использование баз данных

    Использование баз данных

    Последний подход, который мы обсудим, для своей реализации требует других знаний помимо Perl. Так что мы просто рассмотрим технологию, которая со временем, вероятно, станет более популярной.
    Все рассмотренные предыдущие примеры хорошо работают с данными приемлемого размера и на машинах с приемлемым количеством памяти, но они не масштабируемы. В ситуациях, когда у вас много данных, особенно если они поступают из различных источников, естественным инструментом становятся базы данных.
    Существует по крайней мере два способа использования баз данных из Perl. Первый из них я называю методом «только Perl». В этом случае все действия осуществляются в Perl или в библиотеках, тесно связанных с Perl. Во втором применяются модули, например из семейства DBI, позволяющие сделать Perl клиентом баз данных, таких как MySQL, Oracle или MS-SQL. Рассмотрим оба подхода для обработки и анализа журналов.

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

    Использование 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).


    Кольцевой буфер

    Кольцевой буфер

    Мы только что рассмотрели традиционный способ ротации журналов для контроля за пространством, занимаемым постоянно растущими журналами. Позвольте представить вам более необычный подход, который вы можете добавить в свою копилку.
    Вот обычный сценарий: выполняется отладка сервера, который выводит целый поток данных в журнал. Нас интересует только малая часть всех этих данных, вероятно, только те строки, которые выводятся сервером после выполнения определенных тестов на определенном клиенте. Если сохранять в журнале весь вывод, как обычно, это быстро заполнит жесткий диск. Ротация журналов с нужной частотой при таком количестве выводимых данных замедлит работу сервера. Что же делать?
    Я написал программу bigbuffy для решения этой головоломки, применив совершенно незамысловатый подход, bigbuffy считывает построчно поступающие на вход данные. Эти строки сохраняются в кольцевом буфере определенного размера. Когда буфер заполняется, он начинает вновь заполняться с вершины. Этот процесс чтения-записи продолжается до тех пор, пока bigbuffy не получит сигнал от пользователя. Получив сигнал, программа сбрасывает текущее содержимое буфера в файл и возвращается в свой нормальный цикл. На диске же остается лишь «окошко» в потоке данных из журнала, в котором показаны только те данные, которые нужны.
    bigbuffy
    можно использовать в паре с программой наблюдения за службой (подобной тем, которые можно найти в главе 5 «Службы имен TCP/IP»). Как только наблюдающая программа (монитор) замечает проблему, она может послать сигнал bigbuffy сбросить содержимое буфера на диск. Теперь у нас есть выдержка из журнала, относящаяся как раз к нужной проблеме (считаем, что буфер достаточно велик и монитор вовремя ее заметил). Вот упрощенная версия bigbuffy. Этот код длиннее примеров из предыдущей главы, но он не очень сложный. Мы будем его использовать в качестве трамплина для разрешения некоторых важных вопросов, таких как блокировка ввода и безопасность:
    Souffsize = 200;
    размер кольцевого буфера по умолчанию (строчках) use Getopt::Long;
    анализируем параметры GetOptions("buffsize=i" => \$Duffsize, "dumpfile=s" => \$dumpfile);
    устанавливаем обработчик сигнала и инициализируем счетчик &setup; простой цикл прочитать строку - сохранить строку
    while (<>){
    и помещаем строку в структуру данных. Заметьте, мы делаем 8 это сначала, даже если получаем сигнал. Лучше записать 8 лишнюю строчку, чем потерять строку данных, если в 8 процессе сброса данных что-то пойдет не так.
    $buffer[$whatline] = $_;
    8 куда деть следующую строку? (Swhatline %= $buffsize)++;
    8 если получаем сигнал, сбрасываем текущий буфер if ($dumpnow) {
    &dodump(); } }
    sub setup {
    die "ИСПОЛЬЗОВАНИЕ: $0 [--buffsize=] --dump-ile=" unless (length($dumpfile));
    $SIG{ 'USR1'} = \&di;mpnow; n устанавливаем обработчик
    Swhatline = 1; и начальная строка кольцевого буфера
    простой обработчик сигнала, который просто устанавливает фла-8 исключения, см. perlipc(l) sub dumpnow {
    Sdumpnow = 1;
    }
    флаг, существует ли уже файл my(@firststat,@secondstat); п для хранения вывода Istats
    Sdumpnow = 0; № сбрасываем флаг и обработчик сигнала $SIG{ 'USR1'} = \&dumpnow;
    if (-e Sdumpfile and (! -f Sdumpfile or -1 Sdurripfile)) {
    warn "ПРЕДУПРЕЖДЕНИЕ: файл для сброса данных существует и не является, обычным текстовым файлом, пропускаем сброс данных.\п";
    return undef; }
    # необходимо принять специальные меры предосторожности при
    # дописывании. Следующий набор операторов "if" выполняет
    # несколько проверок при открытии файла для дописывания if (-e Sdumpfile) {
    Sexists = 1;
    unless(@firststat = Istat $dumpfile){
    warn "Невозможно выяснить состояние Sdumpfile, пропускаем сброс данных.\n";
    return undef; } if ($firststat[3] != 1) {
    warn "Sdumpfile - жесткая ссылка, пропускаем сброс данных.\n";
    return undef; } }
    unless (open(DUMPFILE, "$dumpfile")){
    warn "Невозможно открыть Sdumpfile для дописывания,
    пропускаем сброс данных.\п"; return undef; > if (Sexists) {
    unless (@secondstat = Istat DUMPFILE){
    warn "Невозможно выяснить состояние открытого файла Sdumpfile,
    пропускаем сброс данных.\п"; return undef; }
    if ($firststat[0] != $secondstat[0] or
    # проверяем номер устройства $firststat[1] != $secondstat[1] or
    проверяем mode $firststat[7] != $secondstat[7]) tt проверяем размеры {
    warn "ПРОБЛЕМА БЕЗОПАСНОСТИ: Istats не совпадают,
    пропускаем сброс данных,\п"; return undef:
    }
    Sline = Swhatline;
    print DUMPFILE "-".scalar(Iocaltime). C'-"x50)."\n";
    do < Проблемы с пространством на диске 357
    И если буфер не полный
    last unless (defined $buffer[$line]) print DUMPFILE $buffer[$line];
    Sline = (Sline == Sbuffsize) 9 1 : $iine+l; } while (Siine '= Swhatline);
    close(DUMPFILE):
    П проталкиваем активный буфер, чтобы не повторяв данные
    # при последующем сбросе их в файл $whatline = 1; Sbuffer = ();
    return 1;
    }
    Подобная программа может найти несколько интересных применений.

    Процесс «прочиталзапомнил»

    Процесс «прочитал-запомнил»

    Крайность, противоположная предыдущему подходу (там мы «пробегали» по данным как можно быстрее), заключается в чтении их в память и последующей обработке после чтения. Рассмотрим несколько версий этой стратегии.
    Для начала приведем простой пример: скажем, у нас есть журнал FTP-сервера и требуется узнать, какие файлы скачивались чаще других. Вот несколько строк из журнала FTP-сервера wu-ftpd:
    Sun Dec 27 05:18:57 1998 1 nic.funet.fi 11868 /net/ftp.funet.fi/CPAN/
    MIRRORING.FROM a _ о a cpan@perl.org ftp 0 *
    Sun Dec 27 05:52:28 1998 25 kju.hc.congress.ccc.de 269273 /CPAN/doc/FAQs/FAQ/
    PerlFAQ.html a _ о a mozilla@ ftp 0 *
    Sun Dec 27 06:15:04 1998 1 rising-sun.media.mit.edu 11868 /CPAN/
    MIRRORING. FROM b __ о a root@rising-sun. media, mit. edu ftp 0 *
    Sun Dec 27 06:15:05 1998 1 rising-sun.media.mit.edu 35993 /CPAN/RECENT.html b
    о а root@rising-sun.media.mit.edu ftp 0
    А вот список полей, из которых состоят приведенные выше строки (все подробности о каждом поле ищите в страницах руководств xferlog(B) сервера wu-ftpd).
    Номер поля Имя поля
    0 current-time (текущее время)
    1 transfer-time (время передачи, в секундах)
    2 remote-host (удаленный узел)
    3 filesize (размер файла)
    4 filename (имя файла)
    5 transfer-type (тип передачи)
    6 special-action-flag (специальный флаг)
    7 direction (направление)
    8 access-mode (режим доступа)
    9 use r name (имя пользователя)
    10 service-name (имя службы)
    11 authentication-method (меод аутентификации)
    12 authenticated-user-id (идентификатор аутентифицированного пользователя)
    Вот пример программы, сообщающей о том, какие файлы передавались чаще других:
    Sxferlog = "/var/adin/Iog/xferlog";
    open(XFERLOG,Sxferlog) or die "Невозможно открыть Sxferlog•$!\n":
    while (){
    $files{(sDlit)[8]}++ }
    close(XFERLOG);
    for (sort {$files{$b} <=> $files{$a}||$a cmp $b} keys %files){
    print "$_:$files{$_}\n"; >
    Мы считываем каждую строку файла, используя имя файла в качестве ключа кэша, и увеличиваем значение для этого ключа. Имя файла выделяется из каждой строки журнала при помощи индекса массива, ссылающегося на определенный элемент списка, возвращенного функцией split():
    $files{(split)[8]>++;
    Вы могли заметить, что элемент, на который мы ссылаемся (8), отличается от 8-го поля из списка полей xferlog, приведенного выше. Это печальные последствия того, что в оригинальном файле отсутствуют разделители полей. Мы разбиваем строки из журнала по пробелам (что является значением по умолчанию для split()), так что дата разбивается на пять отдельных элементов списка.
    В этом примере применяется искусный прием - сортировку значений выполняет анонимная функция sort:
    for (sort {$files{$b} <=> $files{$a}||$a cmp $b} keys %files){
    Обратите внимание, что переменные $а и $b в первой части расположены не в алфавитном порядке. Это приводит к тому, что sort выводит элементы в обратном порядке, т. е. первыми отображаются самые «популярные» файлы. Вторая часть анонимной функции sort ( I $a cmp $о) гарантирует, что файлы с одинаковой популярностью будут перечислены в отсортированном порядке.
    Для того чтобы этот сценарий подсчитывал только некоторые файлы и каталоги, можно задать регулярное выражение в качестве первого аргумента для сценария. Например, если добавить
    next unless /$ARGV[0]/o;
    в цикл while(), можно будет задавать регулярные выражения для ограничения учитываемых файлов. Давайте посмотрим на другой пример подхода «прочитал-запомнил», в котором используется программа «поиска брешей» из предыдущего раздела. В предыдущем примере выводилась информация только об успешной регистрации с сайтов злоумышленника. Узнать о неудавшихся попытках мы не можем. Чтобы получить такую информацию, мы рассмотрим другой файл журнала.

    Регулярные выражения

    Регулярные выражения

    Регулярные выражения - одна из самых важных составляющих анализа журналов. Регулярные выражения используются как сито для отсева интересных данных от данных, не представляющих интереса. В этой главе применяются только основные регулярные выражения, но вы вполне можете создавать более сложные для собственных нужд. Так, применение подпрограмм или технологии создания регулярных выражений из предыдущей главы позволяет использовать их еще более эффективно.
    Время, потраченное на получение навыков работы с регулярными выражениями, окупится с лихвой и не раз. Регулярные выражения лучше всего изучать по книге Джеффри Фридла (Jeffrey Friedl) «Mastering Regular Expressions» (Волшебство регулярных выражений) (O'Reilly).
    Эта проблема иллюстрирует один из недостатков Unix: информация из журналов в Unix-системах хранится в различных местах и в различных форматах. Для того чтобы справиться с этими различиями, существует не так много инструментов (к счастью, у нас есть Perl). Нередко приходится использовать более одного источника данных для решения подобных задач.
    Журнал, который сейчас больше всего нам пригодится, - это журнал, сгенерированный через syslog инструментом tcpwrappers, который предоставляет программы и библиотеки, позволяющие контролировать доступ к сетевым службам. Любую сетевую службу, например tel net, можно настроить так, чтобы все сетевые соединения для нее обрабатывались сначала программой tcpwrappers. После того как соединение будет установлено, программа tcpwrappers регистрирует попытку соединения через syslog и затем либо передает соединение настоящей службе, либо предпринимает некоторые действия (например, разрывает соединение). Решение, разрешить ли данное соединение, основывается на нескольких правилах, введенных пользователем (например, разрешать лишь для некоторых исходящих узлов), tcpwrappers также может принять меры предосторожности и послать запрос к DNS-серве-ру, чтобы убедиться, что соединение устанавливается оттуда, откуда ожидается. Кроме того, программу можно настроить так, чтобы в журнале регистрировалось имя пользователя, устанавливающего соединение (через протокол идентификации, описанный в RFC931), если это возможно. Более подробное описание tcpwrappers можно найти в книге Симеона Гарфинкеля (Simson Garfinkel) и Джина Спаффорда (Gene Spafford) «Practical Unix & Internet Security» (Unix в практическом использовании и межсетевая безопасность) (O'Reilly).
    Мы же просто добавим несколько строк к предыдущей программе, в которых просматривается журнал tcpwrappers (в данном случае tcpdlog) для поиска соединений с подозрительных узлов, найденных нами в wtmp. Если добавить этот код в конец предыдущего примера,

    местоположение журнала tcpd Stcpdlog = "/var/log/tcpd/tcpdlog";
    Shostlen = 16; Я максимальная длина имени узла в файле wtmp
    print "-- просматриваем tcpdlog --\n";
    open(TCPDLOG,Stcpdlog) or die "Невозможно прочитать $tcpdlog:$!\n";
    while(){
    next if !/connect from /; tt нас беспокоят только соединения (Sconnecto,Sconnectfrom) = /(.+):\s+connect from\s+(.+)/; Sconnectfrom =" s/~.+@//;
    tt
    tcpwrappers может регистрировать имя узла целиком, а не # только первые N символов, как некоторые журналы wtmp. В
    результате необходимо усечь имя узла до той же длины, что
    Вив wtmp файле, если мы собираемся искать имя узла в кэше
    Sconnectfrom = substr($connectfrom.O,Shostlen);
    print if (exists $contacts{$connectfrom} and Sconnectfrom !~ /$ignore/o);
    то мы получим данные, подобные этим:
    -- ищем узлы, с которых регистрировался пользователь --
    user host.ccs.neu Fri Apr 3 13:41;47
    -- ищем другие соединения с этих узлов --
    user2 host.ccs.neu Thu Oct 9 17:06:49
    user2 host.ccs.neu Thu Oct 9 17:44:31
    user2 host.ccs.neu Fri Oct 10 22:00:41
    user2 host,ccs.neu Wed Oct 15 07:32:50
    user2 host.ccs.neu Wed Oct 22 16:24:12
    -- просматриваем tcpdlog --
    Jan 12 13:16:29 riost2 in. rshd[866]: connect from user4@host. ccs. neu. ed^
    Jan 13 14:38:54 hosts in, rlogind[4761]: connect from user5@host. ccs, nei., f-з ,
    Jan 15 14:30:17 host4 in.ftpd[18799]: connect from user6@host.ccs.reu.ecu
    Jan 16 19:48:19 hosts in.ftpd[5131]: connect from user7@host.ccs.neu.ca.,
    Читатели могли обратить внимание, что в приведенных выше результатах были замечены соединения, устанавливаемые в различное время. В файле wtmp были зарегистрированы соединения, установленные в период с 3 апреля по 22 октября, тогда как tcpwrappers показывает только январские соединения. Разница в датах говорит о том, что файлы wtmp и файлы tcpwrappers имеют различную скорость ротации. Необходимо учитывать такие детали, если вы пишете программу, которая полагается на то, что файлы журналов относятся к одному и тому же периоду времени.
    В качестве последнего и более сложного примера, демонстрирующего подход «прочитал-запомнил», рассмотрим задачу, требующую объединения данных с состоянием и без него. Для того чтобы получить более полную картину действий на сервере wu-ftpd, можно установить соответствие между информацией о регистрации в системе из файла wtmp и информацией о передаче файлов, записанной в файле xferlog сервера wu-ftpd. Было бы здорово увидеть, когда начался сеанс работы с FTP-сервером, когда он закончился и какие файлы передавались в течение этого сеанса.
    Вот отрывок вывода программы, которую мы собираемся написать. В нем показаны четыре FTP-сеанса за март. В первом сеансе на машину был передан один файл. В двух других файлы были переданы с этой машины, а в течение последнего сеанса файлов не было передано вообще:
    Thu Mar 12 18:14:30 1998-Thu Mar 12 18:14:38 1998 pitpc.ccs.neu.ed -> /home/dnb/makemod
    Sat Mar 14 23:28:08 1998-Sat Mar 14 23:28:56 1998 traal-22.ccs.neu <- /home/dnb/.emacs19
    Sat Mar 14 23:14:05 1998-Sat Mar 14 23:34:28 1998 traal-22.ccs.neu <- /home/dnb/lib/emacs19/
    cperl-mode.el <- /home/dnb/lib/emacs19/filladapt.el
    Wed Mar 25 21:21:15 1998-Wed Mar 25 21:36:15 1998 traal-22.ccs.neu (no transfers in xferlog)
    Получить такие данные не очень просто, поскольку приходится добавить данные без информации о состоянии в журнал данных с информацией о состоянии. В журнале xferlog приводится только время, когда была совершена передача файла, и узел, участвующий в этой предаче. В журнале wtmp приводится информация о соединениях и завершении соединений других узлов с сервером. Давайте посмотрим, как объединить эти два типа данных при помощи подхода «прочитал-запомнил». В этой программе мы определим некоторые переменные, а затем вызовем подпрограммы для выполнения каждой задачи :
    для преобразования дата ->время-в-1)п1х (количество секунд с начала эпохи) use Time::Local
    Sxferlog = "/var/log/xferlog":
    местоположение журнала передачи файлов
    $wtmp = "/var/adm/wtmp"; g местоположение wtmp $template = "A8 A8 A16 1";
    шаблон для wtmp в SunOS 4.1.4 Srecordsize = length(pack($template,()));
    размер каждой записи в wtmp Shostlen = 16;
    максимальная длина имени узла в wtmp
    карта соответствий имени месяца с номером
    %month = qw{Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11};
    &ScanXferlog;
    просматриваем журнал передачи файлов
    &ScanWtmp;
    просматриваем журнал wtmp
    &ShowTransfers;
    приводим в соответствие и выводим информацию о передачах
    Теперь рассмотрим процедуру, читающую журнал xferlog:
    просматриваем журнал передачи файлов сервера wu-ftpd и
    заполняем структуру данных %transfers
    sub ScanXferlog {
    local($sec, $min,$hours,$mday,$mon,$year); my($time,$rhost,$fname,Sdirection);
    print STDERR "Просматриваю Sxferlog...";
    open(XFERLOG,$xferlog) or
    die "Невозможно открыть $xferlog:$!\n";
    while (){
    Я используем срез массива для выбора нужных полей
    ($mon,$mday,$time,Syear,$rhost,$fname,Sdirection) = (split)[1,2,3,4,6,8,11];
    Я добавляем к имени файла направление передачи, 81- это передача на сервер
    $fname = (Sdirection eq '!' ? "-> " : "<- ") . $fnarne;
    и преобразуем время передачи к формату времени в Unix
    ($hours,$min,$sec) = split(':',Stime); Sunixdate =
    timelocal($sec,$min,Shours,$mday,$month{$mon},Syear);
    Я помещаем данные в хэш списка списков:
    push((s>{$transfers{substr($rhost, 0, Shostlen)}},
    [Sunixdate,Sfname]); }
    close(XFERLOG); print STDERR "Готово.\n"; }
    Строка push(), вероятно, заслуживает объяснения.
    В этой строке формируется хэш списка списков, выглядящий примерно так:
    $transfers{hostname} =
    ([timel, filenamel], [time2. filena'ne2]1 [time3 filenames] ..)
    Ключами хэша %transfers являются имена узлов, инициирующих передачи файлов. При создании каждой записи мы укорачиваем имя узла до максимальной длины, допустимой в wtmp.
    Для каждого узла мы сохраняем список пар, состоящих из времени передачи файла и его имени. Время сохраняется в «секундах, прошедших с начала эпохи» для упрощения дальнейшего сравнения. Подпрограмма timelocalO из модуля Time: : Local помогает выполнить преобразование времени к этому стандарту. Поскольку мы просматриваем журнал передачи файлов, записанный в хронологическом порядке, список пар тоже строится в хронологическом порядке, а это нам пригодится позже.
    Теперь перейдем к просмотру wtmp :
    просматриваем файл wtmp и заполняем структуру sessions
    информацией о ftp-сеансах sub ScanWtmp {
    my($record, $tty,$name,$host,$time,%connections);
    print STDERR "Просматриваю $wtmp...\n";
    open(WTMP,$wtmp) or die "Невозможно открыть $wtmp:$!\n";
    while (read(WTMP,$record, Srecordsize)) {
    it если запись начинается не с ftp, даже не пытаемся ее
    разбирать (unpack). ЗАМЕЧАНИЕ: мы получаем зависимость от
    формата wtmp в обмен на скорость next if (substr($record,0,3) ne "ftp");
    ($tty,$name,$nost,$time)=unpack($template,$ record);
    и если мы находим запись об открытии соединения, мы
    создаем хэш списка списков. Список списков позже № будет использован в качестве стэка.
    if ($name and substr($name,0,1) ne "\0"){
    push(@{$connections{$tty}},[$host,$time]); }
    # если мы находим запись о закрытии соединения, пытаемся и найти ей пару в записях об открытии соединений,
    найденных раньше else {
    unless (exists $connections{$tty}){
    warn "Найдено только завершение соединения с $tty:" .
    scalar localtime($time)."\n"; next;
    # будем использовать предыдущую запись об открытии
    # соединения и эту запись о закрытии соединения в
    качестве записи об одном сеансе. Чтобы сделать
    это, мы создаем список списков, где каждый список
    имеет вид (hostname, login, logout) push((8>sessions,
    [@{shift @{$connections{$tty}H, Stime]):
    ft если для этого терминала больше нет соединений в
    стэке, удаляем запись из хэша delete $connections{$tty>
    unless (@{$connections<$tty}});
    }
    }
    close(WTMP);
    print STDERR "Готово.\n";
    }
    Давайте посмотрим, что происходит в этой программе. Мы считываем по одной записи из файла wimp. Если эта запись начинается с ftp, мы знаем, что это сеанс FTP. Как говорится в комментарии, строка кода, в которой принимается это решение, явно привязана к формату записи в wtmp. Будь поле tty не первым полем записи, эта проверка не сработала бы. Однако возможность узнать, что строка не представляет для нас интереса, не выполняя для этого unpack(), того стоит.
    Когда мы находим строчку, начинающуюся с ftp, мы разбиваем ее, чтобы выяснить, относится она к открытию FTP-соединения или к закрытию. Если это открытие соединения, то мы записываем его в %connections, структуру данных, хранящую сводку по открытым соединениям. Как и %transfers из предыдущей подпрограммы, это хэш списка списков, на этот раз его ключами являются терминалы для каждого соединения. Каждое значение этого хэша - это набор пар, представляющих имя узла, установившего соединение, и время установки соединения.
    Зачем нужна такая сложная структура данных для слежения за открытием соединений? К сожалению, в wtmp нет простых пар строк «открытие-закрытие открытие-закрытие открытие-закрытие». Например, посмотрим на строки из wtmp (их выводила наша первая в этой главе программа, работающая с wtmp):
    ftpd1833:dnb:ganges.ccs.neu.e:Fn Mar 27 14:04:47 1998
    ttyp7:(logout):(logout):Fri Mar 27 14:05:11 1998
    ftpd1833:dnb:hotdiggitydog-he:Fri Mar 27 14:05:20 1998
    ftpd1833:(logout):(logout):Fri Mar 27 14:06:20 1998
    ftpd1833:(logout): (logout):Fn Mar 27 14:06:43 1998
    Обратите внимание на две записи об открытии FTP-соединения на одном и том же терминале (1-я и 3-я строчки). Если бы мы сохраняли по одному соединению для терминала в простом хэше, то потеряли бы информацию о первом соединении, встретив второе.
    Вместо этого мы в качестве стека используем список списков, ключами которого являются терминалы из %conrecnons. Когда встречается запись об открытии соединения, пара (host. iu push(@sessions,[@{shift @{$connections{$tty}}},$time]);
    Давайте разберемся с этой строкой «изнутри», чтобы все прояснить. Выделенная жирным часть строки возвращает ссылку на стек/список открытых соединений для данного терминала:
    push(@sessions, [@{shift (*{$connections{$tty}}},$time]);
    Эта часть выбрасывает из стека ссылку на первое соединение:
    push(@sessions,[@{shift @{$connections{$tty}}},$time]);
    Мы разыменовываем ее, чтобы получить сам список (host, login-time) для соединения. Если поместить эту пару в начало другого списка, заканчивающегося временем соединения, Perl интерполирует пары для соединения, и мы получим один список из трех элементов. Теперь у насесть группа (host, login-time, logout-time):
    push(@sessions,[©{shift @{$connections{$tty}}>,$time]):
    Теперь, когда у нас есть все составляющие (узел, начало соединения и конец соединения) для сеанса FTP в одном списке, можно добавить ссылку на этот список в список (sessions, который будет использоваться позже:
    push(@sessions, [@{shift @{$connections{$tty}}}, $time]);
    Благодаря одной очень насыщенной строке у нас есть список сеансов.
    Чтобы завершить работу в подпрограмме &ScanWtmp, необходимо проверить, пуст ли стэк для каждого терминала, т. е. проверить, что не осталось больше записей об открытии соединения. Если это так, можно удалить эту запись из хэша; мы знаем, что соединение завершилось:
    delete $connections{$tty} unless (@{$connectio.ns{$tty}});
    Настало время поставить в соответствие два различных набора данных. Эта задача ложится на плечи подпрограммы &ShowTransfers. Для каждого сеанса она выводит список из трех элементов, относящихся к соединению, и файлы, переданные во время этого сеанса.
    обходим в цикле журнал соединений, ставя в соответствие и сеансы с передачами файлов
    sub ShowTransfers { local($session);
    foreach Ssession (@sessions){
    # выводим время соединения print scalar localtime($$session[1]) . "-" .
    scalar localtime($$session[2]) . " $$session[0]\n";
    ищем все файлы, переданные в этом сеансе и выводим их
    print &FindFiles(@{$session}),"\n"; } }
    Вот самая сложная часть, в которой приходится решать, передавались ли файлы в течение сеанса связи:
    возвращает все файлы, переданные в течение данного сеанса sub FindFiles{
    my($rhost,$login,Slogout) = @_;
    my($transfer,@found);
    простой случай, передачи файлов не было
    unless (exists $transfers{$rhost}){
    return "\t(no transfers in xferlog)\n"; }
    простой случай, первая запись о передаче файлов записана
    # после регистрации
    ($transfers{$rhost}->[0]->[0] > $logout){
    return "\t(no transfers in xferlog)\n"; }
    ищем файлы, переданные во время сеанса
    foreach $transfer (@{$transfers{$rhost}}){
    если передача до регистрации
    next if ($$transfer[0] < Slogin);
    и если передача после регистрации
    last if ($$transfer[0] > Slogout);
    если мы уже использовали эту запись next unless (defined $$transfer[1]);
    Первым делом можно исключить простые случаи. Если мы не нашли записей о передаче файлов, выполненной этим узлом, или если первая передача произошла после завершения интересующего нас сеанса, это означает, что в течение данного сеанса файлы не передавались.
    Если нельзя исключить простые случаи, необходимо просмотреть список всех передач. Мы проверяем, произошла ли передача, связанная с данным узлом, после начала сеанса, но до его завершения. Мы переходим к следующей передаче, если какое-либо из этих утверждений неверно. Кроме того, мы прекращаем проверку остальных передач для этого узла, когда находим запись о передаче, произошедшей после завершения соединения. Помните, уже говорилось о том, что все записи о передаче файлов добавляются в структуру данных в хронологическом порядке? Это окупается именно здесь.
    Последняя проверка перед тем, как решить, засчитывать ли запись о передаче файла, выглядит несколько странно:
    Я если мы уже использовали эту запись next unless (defined $$transfer[1]);
    Если два анонимных сеанса с одного и того же узла происходят в одно и то же время, то нет никакого шанса выяснить, к какому из них относится запись о передаче этого файла. Ни в одном из журналов просто не существует информации, которая могла бы нам в этом помочь. Лучшее, что можно тут сделать, - определить правило и придерживаться его. Здесь правило такое: «Приписывать передачу первому возможному соединению». Эта проверка и последующий undef проводят его в жизнь.
    Если последняя проверка пройдена, мы объявляем о победе и добавляем имя файла к списку файлов, переданных в течение этого сеанса. После этого выводим информацию о сеансах и выполненных передачах файлов.
    Подобные программы, в которых выполняется поиск взаимосвязей, могут быть довольно сложными, особенно когда они объединяют источники данных, связи между которыми не являются достаточно четкими. Так что давайте посмотрим, можно ли подойти к этому проще.

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

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

    «Essential System Administration»,
    (2nd Edition), Eleen Frisch (O'Reilly, 1995). А книге есть хорошее, краткое введение в syslog. http://www.heysoft.de/index.htm -
    домашняя страница Франка Хэйне (Frank Heyne) - человека, предоставляющего программное обеспечение для анализа журнала событий в Win32. Также здесь есть хороший список часто задаваемых вопросов по Event Log. http://www.le-berre.com/ -
    домашняя страница Филиппа Ле Вера (Philippe Le Berre); содержит отличный отчет по использованию Win32 EventLog и других пакетов для Win32. «Managing NT Event Logs with Perl for Win32»,
    Bob Wells, Windows NT Magazine, February/March 1998. «Practical Unix & Internet Security»,
    (2nd Edition), Simson Garfinkel, Gene Spafford (O'Reilly, 1996). Еще одно хорошее (и несколько более подробное) введение в syslog, также содержит информацию по tcpwrappers. «Windows NT Event Logging»,
    James D. Murray (O'Reilly, 1998).

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

    Таблица 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(){
    $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, выводящие события из журнала в текстовом виде. Позже мы посмотрим на эти утилиты в действии.

    Способ ротации журналов из Perl

    Таблица 9.2. Способ ротации журналов из Perl

    Процесс Perl
    Переименуйте старые журналы, присвоив им следующий номер. renamed или &File: :Copy: :move() если переносить файлы с одной файловой системы на другую.
    Если необходимо, сообщите процессу, создающему файл журнала, о необходимости закрыть текущий файл и приостановить запись до тех пор, пока она не будет разрешена. kill () для программ, принимающих сигналы, system () или (обратные кавычки), если необходимо вызвать для этого другую программу.
    Скопируйте или переместите файлы журналов, которые сейчас использовались, в другой файл. &File: : Сору для копирования, rename( ), чтобы переименовать (или &File: :Copy: :move() при перемещении с одной файловой системы на другую).
    Если необходимо, урежьте текущий файл журнала. truncate () или open (FILE, "> filename").
    Если необходимо, пошлите сигнал процессу о необходимости приостановить запись в журнал. Шаг 2 из этой таблицы.
    При желании сожмите или обработайте скопированный файл. system( ) или обратные кавычки для запуска программы сжатия или другого программного кода, выполняющего обработку.
    Удалите самые старые копии файлов. stat( ), чтобы выяснить размер файла и даты, unlink( ) для удаления файлов.
    На эту тему существует много вариаций. Все, кому не лень, писали собственные сценарии для ротации журналов. Так что не удивительно, что такой модуль существует. Рассмотрим модуль Logf lie: .Rotate Пола Гэмпа (Paul Gampe).
    Logfile: : Rotate использует объектно-ориентированный подход для создания нового экземпляра объекта для журнала и для выполнения методов этого экземпляра. Сначала мы создаем новый экземпляр с заданными параметрами (табл. 9.3).


    Параметры Logflle Rotate

    Таблица 9.3. Параметры Logflle::Rotate

    Параметр Назначение
    File Имя файла журнала для ротации
    Count (необязательный, по умолчанию: 7) Число хранимых копий файлов
    Gzip (необязательный, по умолчанию: путь, найденный при сборке Perl) Полный путь к программе сжатия gzlp
    Signal Код, выполняемый после завершения ротации, как в шаге 5 (табл. 9.2)
    Вот небольшой пример программы, в которой используются эти параметры:
    use Logfile: .'Rotate;
    Slogfile = new Logfile:;Rotate(
    File => "/var/adm/log/syslog", Count => 5,
    Gzip => "/usr/local/bin/gzip", Signal => sub (
    open PID, "/etc/syslog.pid" or
    die "Невозможно открыть pid-фа/.л :$'\n"; chomp($pid = ); close PID;
    # сначала надо проверить допустимость kill 'HUP', $pid; } ):
    В результате выполнения этого фрагмента программы указанный журнал будет заблокирован и будет подготовлен модуль для ротации данного журнала. После того как этот объект создан, сама ротация журнала не представляет никакого труда:
    $logfile->rotate(); undef Slogfile;
    Строка undef нужна для того, чтобы убедиться, что файл журнала будет разблокирован после ротации (он заблокирован до тех пор, пока существует объект журнала).
    Как говорится в документации, если с модулем работает привилегированный пользователь (например, пользователь root), необходимо кое-что учитывать. Во-первых, Logf lie. : Rotate прибегает к системному вызову для запуска программы gzip, что является потенциальной дырой в безопасности. Во-вторых, подпрограмма Signal должна быть реализована «оборонительным» способом. В предыдущем примере мы не проверяли, что идентификатор процесса, полученный из /etc/sys log.pid, действительно является идентификатором процесса для syslog. Лучше было бы использовать таблицу процессов, о чем мы говорили в главе 4 «Действия пользователей», перед тем как посылать сигнал через kill(). Более подробно советы по «защищенному» программированию приведены в главе 1 «Введение».

    Сравнение поддерживаемых

    Теперь рассмотрим один из способов обращения с очень большими наборами данных. Вам может потребоваться загрузить данные в более сложную базу данных SQL (коммерческую или нет) и запрашивать из нее информацию, используя SQL. Тем, кто не знаком с SQL, я рекомендую ознакомиться с приложением D «Пятнадцатиминутное руководство по SQL», перед тем как смотреть на этот пример.
    Заполнить базу данных можно так:
    use ОВГ
    use Sys .
    $db = "drib":
    ищем месгоголожс.чие iasr (-х '/bin/last" and $]astex = "/от/ lasc")
    (-x "/usr/ucb/last" ana Slastex = '/азг7ьсо.' ;ast"):
    подсоединяемся к базе даинь.х Sybase
    переходим иа базу данных, которую мы будем использовать $dbh->do("use $db") or die "Невозможно перейти к $db: ".$dbh->errstr."\n";
    в
    создаем таблицу iastinfo, если ее еще не существует unless ($dbh->selectrow_array( q{SELECT name from sysobjects WHERE name="lastinfo"})){
    $dbh->do(q{create table Iastinfo (username char(8),
    localhost char(40), otherhost varchar(75), when char(18))}) or
    die "Невозможно создать таблицу Iastinfo: ".$dbh->errstr."\n"; }
    Sthishost = Shostnanie:
    $sth = $dbh->prepare(
    qq{INSERT INTO lastinfo(username,localhost,otherhost.when) VALUES (?, 'Sthishosf, ?, ?)}) or die
    "Невозможно подготовить запрос insert: ". Sdrj.h-^errstr , "\n":
    open(LAST."Slastexj") or die "Невозможно выполни:» программу Slastex
    while (){
    next if /"reboot\.s/ or /'shutdown'.s/ or
    /"ftp';S/ or / "ivt-rpv.s/:
    $wher> = $!non " ".Scare." ". $tme:
    Sst1-->sxec-J'te;$j3e',SbcЈt.$/.ter^ close(LAST);
    $c)bh->di scanned;
    Теперь можно использовать базу данных по назначению. Вот набор простеньких SQL-запросов, которые легко можно выполнить из Perl при помощи интерфейсов DBI или ODBC, о которых мы говорили в главе 7 «Администрирование баз данных SQL»:
    -- сколько всего записей в таблице
    9 select count (*) from lastinfo;
    10068
    -- сколько пользователей было зарегистрировано
    9 select count (distinct username) from lastinfo;
    237
    -- сколько различных узлов устанавливали соединение с нашими машинами
    select count (distinct otherhost) from lastinfo;
    1000
    -- на каких локальных машинах регистрировался пользователь
    "dr.b"? select distinct localhost from lastinfo where username = "dnb": localhost
    hostl host2
    Эти примеры должны помочь читателю прочувствовать, как можно «исследовать» данные, когда они хранятся в настоящей базе данный Каждый из этих запросов требует для выполнения лишь около секунды. Базы данных могут быть быстрым, мощным инструментом дл системного администрирования.
    Анализ журналов - бесконечная тема для разговора. К счастью, 1 глава снабдила вас кое-какими инструментами и некоторым вдохновением.


    Вариация на тему предыдущего примера

    Вариация на тему предыдущего примера

    Простая вариация предыдущего подхода включает в себя многократный обход данных. Иногда это необходимо в случае с данными большого объема и ситуаций, когда сначала приходится просмотреть все данные, чтобы отличить интересные данные от неинтересных. В плане реализации это означает, что после первого обхода данных надо:
  • Перейти обратно к началу потока данных (который может быть файлом) при помощи seek( )или API-вызова.
  • или
  • Закрыть и вновь открыть дескриптор файла. Зачастую это единственный выбор, когда читаются данные из вывода программы, подобной last.
  • Вот пример, когда такой подход может пригодиться. Представьте, что надо справиться с проблемой в защите, связанной с тем, что кто-то получил несанкционированный доступ к одной из учетных записей. Один из первых вопросов, который приходит на ум, - «был ли получен доступ и к другим учетным записям с той же машины?». Найти полный ответ на такой кажущийся простым вопрос может оказаться сложнее, чем кажется. Давайте попробуем решить эту проблему. Приведенный ниже отрывок кода для SunOS принимает в качестве первого аргумента имя пользователя и необязательное регулярное выражение в качестве второго, чтобы отфильтровать узлы, которые мы хотим проигнорировать:
    Stemplate = "А8 А8 А16 1"; # для SunOS 4.1.x Srecordsize = length(pack($template,())); ($user,$ignore) = @ARGV;
    print "-- ищем узлы, с которых регистрировался пользователь Suser --\n"; open(WTMP,"/var/adm/wtrcp") or die "Невозможно открыть wtmp:$!\n": while (read(WTMP,Srecord.Srecordsize)) {
    ($tty, $name,$host:$time)=unpack($template.$reccKd):
    if ($user eq $name){
    next if (defined Signore and $host =" /$ignore/o); if (length($host) > 2 and 'exists $contacts{$nost}) {
    $connect = localti.Tie($time):
    $contacts{$ROSt}=$time:
    write: >
    print "-- ищем другие соединения с этих узлов --\п"; die "Невозможно перейти в начало wtmp:$'\n" unless (seek(WTMP,0,0));
    while (read(WTMP,$record.$recordsize)) {
    ($tty,Sname,$hostt $time)=unpack($template,$record);
    ft если это запись не о завершении работы с системой и нас
    # интересует этот узел и это соединение установлено для
    ft «другой» учетной записи, тогда записываем эти данные
    if (substr($name,1,1) ne "\0" and exists $contacts{$host} and $name ne $user){
    Sconnect = localtime($time); write;
    }
    } close(WTMP);
    ft вот формат вывода, вероятно, его потребуется скорректировать
    и в зависимости от шаблона
    format STDOUT =
    @«««« @««««««« @«««««««««
    $name,$host,Sconnect
    Сначала программа просматривает файл wtmp в поисках записей о регистрации в системе пользователей под «скомпрометированным» именем. По мере нахождения таких записей пополняется хэш, в который записываются имена всех узлов, где регистрировался пользователь под этим именем. Затем программа возвращается к началу файла и просматривает его заново, выполняя на этот раз поиск записей о соединениях с узлов из списка, и выводит совпадения по мере их появления. Не составит труда изменить эту программу так, чтобы она просматривала все файлы из каталога, в котором хранятся файлы ротации журнала wtmp.
    Единственная проблема этой программы - ее «узкая специализация». Она будет искать только точное совпадение имен узлов. Если злоумышленник регистрировался в системе, используя динамический адрес, получаемый от провайдера (что бывает часто), то очень велика вероятность, что имена узлов будут отличаться при каждом соединении. Тем не менее, даже неполные решения, подобные этому, очень сильно помогают.
    Помимо простоты, у рассмотренного нами подхода есть еще преимущества: он быстрее и требует меньших затрат памяти по сравнению с другими методами. Он лучше всего справляется с журналами, в которых регистрируются данные без поддержки состояния, о которых мы говорили раньше в этой главе. Но иногда, особенно при работе с данными с состоянием, необходимо использовать другие методы.

    Журналы Текстовые журналы Двоичные

    Текстовые журналы

    Журналы бывают разных типов, следовательно, нам нужно использовать различные подходы к их обработке. Самые распространенные журналы - полностью состоящие из строк текста. Популярные серверные пакеты, такие как Apache (веб), INN (новости Usenet) и Sendmail (электронная почта) записывают в журналы огромное количество текста. Большая часть журналов на Unix-машинах выглядит одинаково, потому что все они создаются одной и той же программой, известной под именем syslog. Файлы, созданные syslog, можно считать обычными текстовыми файлами.
    Вот простая программа на Perl, ищущая слово «error» в текстовом файле журнала:
    open(LOG,"logfile") or die "Невозможно открыть журнал:$!\n"; while(){
    print if /\ber ror\b/i
    }
    close(LOG):
    Тем, кто хорошо знает Perl, вероятно не терпится сократить ее до одной строки. Пожалуйста:
    perl -ne 'print if /\berror\b/i' logfile


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

    Безопасность и наблюдение за сетью

    Безопасность и наблюдение за сетью

  • Обращаем внимание на неожиданные или несанкционированные изменения
  • Обращайте внимание на подозрительную активность
  • Протокол SNMP
  • Опасность на проводе
  • Предотвращение подозрительных действий
  • Информация о модулях из этой главы
  • Рекомендуемая дополнительная литература

  • Любой разговор о безопасности сопряжен с риском. Существует по крайней мере три ловушки, которые могут обречь на неудачу разговор о безопасности:
  • Под словом «безопасность» для разных людей скрываются различные вещи. Если вы придете на конференцию ученых, изучающих Древнюю Грецию и Рим, и спросите их о Риме, первый вдохновенно прочтет вам лекцию об акведуках (инфраструктура и снабжение), второй расскажет о Римской империи (идеология и политика), третий растолкует о римских легионах (армия), четвертый поведает о Сенате (администрация) и т. д. Необходимость иметь дело сразу с каждой гранью безопасности - это первая ловушка.
  • Люди думают, что что-то может быть безопасным, будь то программа, компьютер, сеть или что-либо еще. Мы не претендуем на то, чтобы показать, как сделать что-то безопасным; но попробуем помочь вам сделать что-то более безопасным или уж, в крайнем случае, поможем различать, когда что-нибудь является менее безопасным. Безопасность это континуум.
  • Наконец, одна из самых серьезных ловушек в этом деле - специфичность. Верно, что сущность безопасности часто заключается и деталях, но это постоянно меняющийся набор деталей. Факт, вы залатали дыры А, В и С на своей системе, гарантирует только (да и то не всегда), что именно эти дыры больше не будут источником проблем. Это никак не поможет, если будет найдена дыра D. Вот почему эта глава рассказывает о принципах и инструментах для увеличения безопасности. Читатель не найдет здесь ничего о том, как исправить конкретную неприятность, например, переполнение буфера, ненадежный ключ реили доступный всем для записи системный файл.
  • Один из хороших способов ввязаться в дискуссию об этих принципах - выяснить, как безопасность проявляется в физическом мире. Как в реальном, так и в виртуальном мире все сводится к опасениям. Будет ли то, чем я дорожу, повреждено, потеряно или обнаружено? Могу ли я сделать что-то, чтобы предотвратить это? Происходит ли это прямо сейчас?
    Если разобраться, как обходятся с этими опасениями в реальном мире, то можно уяснить, как справляться с ними и в нашей области. Один из способов разобраться с такими опасениями - придумать более надежный способ оградить пространство. В случае с физическим пространством мы используем конструкции, подобные банковским сейфам; если речь идет об интеллектуальном пространстве, мы применяем методы сокрытия данных, например, гриф «совершенно секретно» или шифрование. Но это бесконечная игра в догонялки. На каждый час, потраченный на проектирование системы безопасности, приходится как минимум один час, потраченный на поиск способа обойти ее. В нашем случае есть еще полчища скучающих подростков с компьютерами и раздраженных уволенных служащих, которым просто не терпится применить где-то излишки энергии.
    Более правильный подход к повышению безопасности, выдержавший испытание временем, заключается в том, чтобы прибегнуть к услугам специального человека, уменьшающего количество опасений. Когда-то давным-давно не было ничего более утешительного, чем звук шагов ночного сторожа, обходящего город и проверяющего, что все дома заперты и находятся в безопасности. Мы будем использовать этот не совсем обычный образ в качестве отправной точки наших исследований безопасности и наблюдения за сетью с помощью Perl.


    Другие ресурсы

    Другие ресурсы

    «Advanced Perl Programming»,
    Sriram Srinivasan (O'Reilly, 1997). В книге есть хороший раздел о создании модулей. http://www.bb4.com и http://www.kernel.org/software/mon/ -
    домашние страницы BigBrother и Моп, два удачных примера пакетов, обеспечивающих общий интерфейс для наблюдения за событиями в реальном времени (в отличие от наблюдения за уже свершившимся в MRTG и Cricket). http://www.tcpdump.org -
    домашняя страница libpcap и tcpdump. «RFC793:Transmission Control Protocol», J. Postel, 1981.

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

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

    Модуль Идентификатор на CPAN Версия
    Getopt : : Std (входит в состав Perl) 1.01
    Digest: :MD5 GAAS 2.09
    Net: :DNS MFUHR 0.12
    FreezeThaw ILYAZ 0.3
    File : : Find (входит в состав Perl)
    Net: :SNMP DTOWN 3.01
    SNMP GSM 3.10
    Net : : Ping (входит в состав Perl) RMOSE 2.02
    Net: :Pcap TIMPOTTER 0.03
    Net: :PcapUtils TIMPOTTER 0.01
    NetPacket TIMPOTTER 0.01
    Term: :ReadKe> KJALB 2.14


    Изменения локальной файловой системы

    Изменения локальной файловой системы

    Файловые системы - это отличное место для начала исследований программ, следящих за изменениями. Мы собираемся исследовать способы проверки неизменности важных файлов, в частности, исполняемых файлов операционной системы или файлов, связанных с безопасностью
    (например /etc/passwd или msgina.dll). Изменения, внесенные в эти файлы без ведома администратора, часто являются признаками вмешательства злоумышленника. В сети существует ряд довольно сложных инструментов, которые устанавливают троянские версии важных файлов и заметают следы. Это самые злобные изменения, которые можно обнаружить. С другой стороны, иногда просто полезно знать, что важные файлы изменились (особенно если одну и ту же систему администрируют несколько человек). Технологии, которые мы рассмотрим, будут
    одинаково хорошо работать в обоих случаях.
    Самый простой способ выяснить, был ли файл изменен - использовать функции stat() и lstat(). Эти функции принимают имя файла или файловый дескриптор и возвращают массив с информацией об этом файле. Единственное различие между этими двумя функциями проявляется в операционных системах, подобных Unix, поддерживающих символические ссылки. В таких случаях 1 stat () применяется для получения информации о файле, на который указывает ссылка, а не о самой символической ссылке. На всех остальных операционных системах информация, возвращаемая функцией lstat(), будет совпадать с информацией, возвращаемой функцией stat().
    Использовать stat() или lstat() очень просто:
    @information = stat("filename");
    Как сказано в главе 3 «Учетные записи пользователей», можно также применять модуль File: :Stat Тома Кристиансена, чтобы получить эту же информацию, используя объектно-ориентированный синтаксис.
    Информация, возвращаемая функциями stat() и lstat(), зависит от операционной системы. stat() и lstat() происходят от системных вызовов в Unix, поэтому Perl-документация по этим функциям ссылается на значения, возвращаемые в Unix. Можно посмотреть (табл. 10.1). как эти значения соотносятся с тем, что возвращается функцией в Windows NT/2000 и MacOS. В первых двух столбцах приведены порядковый номер поля и его описание для систем Unix.


    Изменения сетевых служб

    Изменения сетевых служб

    Мы узнали, как обнаружить изменения в локальных файловых системах. Как насчет того, чтобы заметить изменения на других машинах или в службах, ими поддерживаемых? Мы уже видели способы запроса NIS и DNS в главе 5 «Службы имен TCP/IP». Не должна вызвать затруднений проверка изменений в повторяющихся запросах к этим службам. Например, можно притвориться вторичным сервером и запросить копию данных (т. е. выполнить зонную пересылку) с сервера для определенного домена, если, конечно, DNS-сервер настроен так, что позволит сделать это:
    use Net::DNS;
    принимает два аргумента в командной строке: первый - сервер
    имен, к которому посылается запрос, а второй - интересующий
    и нас доме"
    $server = new Net::DNS::Resolver.
    $server-:^.ar;eservers($ARGV[C]);
    ur;n+ 3 ' OERR 'Выполняется ''еррдоча.
    i:c $sorver-.'Or rorst гц-.д jr,:ess (de'ired ьу(У-(-}\
    for Srecord (@zone){
    $record-'>pr int;
    }
    Объединим эту идею с MD5. Вместо того чтобы получать информацию о зоне, давайте просто сгенерируем для нее подпись:
    use Net::DNS:
    use FreezeTnaw qiv{f reeze!;
    use Digest::MD5 qw(md5):
    Sserver = new Net: :DNS::Resolver:
    $server->naineservers($ARGV[0]):
    print STDERR "Выполняется передача,..";
    @zone = $server->axfr($ARGV[1]);
    die $server->errorstring unless (defined @zone);
    print STDERR "готово.\n";
    $zone = join('',sort map(freeze($_),@zone));
    print "MD5 fingerprint for this zone transfer is: ";
    print Digest::MD5->new->add($zone)->hexdigest,"\n";
    MD5 работает со скалярными данными (сообщение), но не со структурами типа списка кэшей, как @zone. Вот почему нужна такая строчка:
    Szone = join(",sort map(freeze($_),@zone));
    Для преобразования каждой записи из структуры данных @zone в обычные строки воспользуемся модулем FreezeThaw, который мы уже видели в главе 9 «Журналы». Перед тем как записи будут склеены в одно большое скалярное значение, они будут отсортированы. Сортировка позволяет проигнорировать порядок, в котором возвращались записи при пересылке зоны.
    Пересылка всего файла зоны с сервера - это крайняя мера, особенно, если зона большая, поэтому имеет смысл наблюдать только за важным подмножеством адресов. Такой пример можно найти в главе 5. Кроме того, в целях безопасности было бы неплохо разрешить выполнение зонных пересылок только на минимальном количестве машин.
    Все, что мы видели до сих пор, не очень помогло нам преодолеть затруднения. Возможно, следует прояснить несколько вопросов:
  • Что если кто-то подделает базу данных MD5 подписей и подставит действительные подписи под поддельные троянские файлы или изменения служб?
  • Что если кто-то подделает ваш сценарий таким образом, что он будет только создавать видимость проверки подписей со значениями из базы данных?
  • Что если кто-то сделает что-нибудь с модулем MD5 на вашей системе?
  • Это уже, конечно, предел паранойи, но что если кто-то сделает что-то с самим исполняемым файлом Perl, одной из его разделяемых библиотек или самим ядром операционной системы?
  • Обычные ответы на эти вопросы (какими бы они ни были неудовлетворительными): храните хорошие копии всего, что имеет отношение к этому процессу (базы данных подписей, модули, Perl и т. д.) на устройствах, к которым разрешен доступ только для чтения.
    Эта головоломка - еще одна иллюстрация бесконечности безопасности.
    Всегда можно найти что-то, чего можно опасаться.

    Обращаем внимание на неожиданные или несанкционированные изменения

    Обращаем внимание на неожиданные или несанкционированные изменения

    Хороший сторож замечает перемены. Он знает, когда что-то оказывается не на месте в вашем окружении. Если ценного мальтийского сокола заменят подделкой, сторож будет первым, кто должен это заметить. Точно так же пользователь хочет услышать рев сирены, если кто-то изменит или заменит основные файлы в системе. В большинстве случаев эти изменения будут безвредными. Но когда кто-то впервые действительно нарушит безопасность вашей системы и начнет делать что-то с файлами /bin/login, msgina.dll или Finder, то вы, заметив это, будете настолько счастливы, что простите все предыдущие ложные тревоги.

    Обращайте внимание на подозрительную

    К сожалению, зачастую мы учимся замечать признаки подозрительной активности только в результате потерь и желания избежать их в дальнейшем. Достаточно всего нескольких взломов, чтобы заметить, что злоумышленники часто действуют по определенным шаблонам и оставляют за собой предательские улики. Зная, что эти признаки собой представляют, заметить их из Perl не сложно.
    После каждого взлома системы безопасности очень важно уделить некоторое время анализу случившегося. Документируйте (по мере своих знаний), куда проникли взломщики, какие инструменты и «дыры» они использовали, что они
    сделали, кого еще атаковали, что вы сделали в ответ и т. д.
    Заманчиво было бы вернуться к обычной жизни и забыть про взлом. Если вы не подвергнетесь этому соблазну, то позже поймете, что инцидент научил вас кое-чему, и что вы не просто потеряли время и силы. Принцип Ницше - «то, что вас не убивает, делает вас сильнее» - часто справедлив и в системном администрировании.
    Например, очень часто взломщики, а особенно менее опытные, пытаются замести следы, создавая «скрытые» каталоги для хранения данных. В Unix и Linux они помещают свои программы и вывод подслушивающих программ в каталоги с именами «...» (точка точка точка), (точка пробел) или «Mail» (пробел Mail). Такие имена, скорее всего, останутся без внимания при беглом просмотре вывода команды Is.
    При помощи инструментов, о которых мы узнали в главе 2, можно легко написать программу для поиска таких имен. Ниже приведена программа, использующая модуль File: :Find (вызываемый find.pl) для
    поиска «ненормальных» имен каталогов.
    require "find.pl";
    Обходим нужные файловые системы
    &find('.');
    sub wanted {
    (-d $_) and каталог
    $_ ne "." and $_ ne ".." and tt не . и не ..
    (/["-.a-zA-ZO-9+.:;_"$»()]/ or
    содержит "плохой" символ
    /~V {3, }/ or ft или начинается как минимум с ipe>
    точек
    /"-/) and
    или начинается с дефиса
    print......&nice($name)." '\n";
    }
    печатаем "хорошую" версию имени каталога, то есть, не раскрывая управляющие символы. Эта подпрограмма -- лишь
    немного измененная версия &unctrl() из dumpvar.pl sub nice {
    my($name) = $_[0];
    $name =' s/([\001-\037\177])/'"'.pack('с'.ord($1)"64)/eg:
    $name;
    }
    Помните врезку «Регулярные выражения» из главы 9? Подобные программы, тщательно анализирующие файловую систему, - еще один пример, где высказанная в главе 9 мысль справедлива. Эффективность таких программ зачастую зависит от качества и количества используемых в них регулярных выражений. Слишком мало регулярных выражений - и вы пропустите то, что хотели найти. Слишком много регулярных выражений или они неэффективны - и ваша программа будет выполняться чрезмерно долго и захватит непомерное количество ресурсов. Если использовать слишком общие регулярные выражения - будет найдено много ложных совпадений. Здесь тонкий баланс.

    Опасность на проводе SNMP хорош

    Вот правдивая история об этом, рассказывающая, как в таких случаях может помочь Perl. Однажды субботним вечером я, как обычно, зарегистрировался на одной из машин в моей сети, чтобы почитать почту, и, к своему удивлению, обнаружил, что наш почтовый и веб-серверы находятся практически «при смерти». Попытки прочитать или отправить почту или посмотреть на веб-сайт заканчивались медленными ответами, обрывами соединений и невозможностью установить соединение. Почтовая очередь начинала достигать критического размера.
    Первым делом я проверил состояние серверов. Интерактивные ответы были нормальными, загрузка процессора - велика, но не смертельна. Признаком проблем было количество запущенных почтовых процессов. В соответствии с файлами журналов большее, чем обычно, количество процессов было вызвано тем, что многие транзакции не были завершены. Повисшими были процессы, запущенные для обработки входящих соединений, они и увеличивали загрузку. А эта загрузка уже мешала появлению новых исходящих соединений. Такое странное поведение сети заставило меня изучить таблицу текущих соединений сервера с помощью netstat.
    Последний столбец вывода netstat говорил о том, что с внешним миром действительно было установлено много соединений. Большой неприятностью было состояние этих соединений. Вместо того чтобы выглядеть примерно так:
    tcp 0 0 n-ailhub.3322 mail.nel.aone. nc. gui* р ESTABLISHED
    tcp О О rnail'iub.3320 cdunet.edunet.dk.smtp CLOSE., WAIT
    tcp 0 0 man 1-н,!). '723 k-aken. -wret, wne. sr',p ESTABLISHED
    tcp 0 0 mail hub. 1709 pi over. net. D' idg. silt.:: CLOSE_WAIT
    они больше напоминали следующее:
    tcp 0 0 TiaiihuD. 3322 nail. mel. aone ne. sr.tp Si'N_PGiyD
    tcp 0 0 r!'.ailnuD. 3320 ed ,riet. edunc-т.. gk. smp Sr'N_RCVD
    tec 0 0 railnub 1723 гтаке^, nvnet .v,ne. sr^D SYN RCVD
    tcp 0 0 pailf'oO. 1709 olover.ret or :dc;. s:"n; CLOSE wAI~
    На первый взгляд, это было похоже на классическую атаку типа «отказ от обслуживания» (Denial of Service), называемую SYN Flood, или атакой SYH-ACK. Давайте отвлечемся на некоторое время и поговорим о том, как работает протокол TCP/IP, чтобы понять, что собой представляют эти атаки.
    Каждое TCP/IP-соединение начинается с «рукопожатия» между участниками. Это позволяет и инициатору, и получателю сообщить о своей готовности к беседе. Первый шаг предпринимает инициатор соединения. посылая получателю пакет SYN (от SYNchronize - синхронизировать). Если получатель готов к «общению», он посылает в ответ пакет 6Y', >, подтверждение (от ACKnowledgment) запроса, и записывает в таблице отложенных соединений, что должна начаться беседа. Инициатор отвечает на пакет SYM-ACK пакетом АСК, подтверждая, что пакет '-. <'1-М>: был получен. При получении пакета АСК получатель удаляет запись из таблицы, и начинается передача данных.
    По крайней мере, именно так все и должно происходить. В случае с атакой SYN Flood, злоумышленник посылает на машину лавину пакетов;'. ',. часто подделывая при этом адрес источника. Ничего не подозревающая машина посылает по поддельным адресам пакеты SYN-АСК и добавляет запись в таблицу ожидающих соединений для каждого полученного пакета SYN. Эти записи остаются в таблице до тех пор, пока операционная система не признает их устаревшими по прошествии определенного вре -мени. Если было послано достаточно много пакетов, таблица ожидающих запросов переполнится, и все попытки установить вполне законное соединение завершатся неудачей. А это приведет к появлению описанных мною симптомов и подобному выводу netstat.
    Но в выводе команды netstat была одна аномалия, которая ставила под сомнение мой диагноз - разнообразие узлов в таблице. Возможно, что кто-то обладает программой с отличными способностями к подделкам, но обычно соединения устанавливаются с меньшего числа фальшивых узлов. Кроме того, многие из этих узлов казались настоящими. Ничего не прояснили и даже ухудшили ситуацию некоторые выполненные мною проверки. Попытки выполнить команды ping или traceroute для случайно выбранных узлов из списка, предоставленного командой net stat, иногда завершались успешно, а иногда нет. Мне не хватало данных. Надо было лучше разобраться с соединениями по этим удаленным узлам. Тут мне на помощь пришел Perl.
    Поскольку я писал программу, находясь « под дулом пистолета », для ра -боты с самой сложной частью проблемы у меня получился очень маленький сценарий, полагающийся на вывод двух внешних программ, анализирующих сеть и проделывающих наиболее сложную часть работы. Я покажу вам эту версию, используя ее в качестве трамплина для дальнейшего улучшения.
    На этот раз задача свелась к одному вопросу: могу ли я добраться до уз лов, пытающихся связаться со мной? Для поиска узлов, пытающихся связаться с моей машиной, я воспользовался программой clog Брайена Митчела (Brian Mitchell), которую можно найти на ftp://coast.cx.pn> due.edu/pub/mirrors/ftp.saturn.net/clog. Для прослушивания сети в поисках запросов TCP-соединений, т. е. пакетов SYN, clog использует библиотеку ИЬрсарот Lawrence Berkeley National Laboratory's Network Re search Group. Эту же библиотеку использует и эффективная программа наблюдения за сетью tcpdump. Библиотека libpcap с ftp://ftp.ee.lbl.gov/lib pcap.tar.Z работает и для машине Linux. Перенесенная версия libpcap для NT/2000 доступна на http://netgroup-serv.polito.it/windump/ или http:// www.ntop.org/libpcap.html, но хотелось бы также увидеть версию и для MacOS.
    clog
    сообщает о пакетах SYN таким образом: Маг 02 11:21|192.168.1.51|1074|192,168.1,104(113 Маг 02 11:21|192.168.1.51|1094|192.168.1.104|23
    Из примера видно, что получено два запроса на соединение от машины 192.168.1.51 к 192.168.1.104. Первый- это попытка соединиться с портом 113 (ident), а второй - с портом 23 (telnet).
    Программа clog помогла мне выяснить, какие узлы пытались установить соединение со мной. Но мне надо было знать, могу ли я до них добраться. Эта задача выпала на долю программы fping Роланда Дж. Шемерса III (Roland J. Schemers III). Программа fping, которую можно найти на http://www.stanford.edu/~schemers/docs/fping/fping.html, - это быстрая и шикарная версия программы ping для тестирования работоспособности сети в Unix и его вариантах. Воспользовавшись этими двумя внешними программами, получаем маленькую программу на Perl:
    Sclogex = "/usr/local/bin/clog"; # местоположение/ключи для
    clog Sfpingex = "/us'r/local/bin/fping -r1"; # местоположение/ключи для fping
    Slocalnet = "192.168.1"; ft префикс локальной сети
    open CLOG, "$clogex|" or die "Невозможно запустить ciog:$!\n"; while(){
    ($date,$onghost.$ongport, Sdesthost,Sdestport) = split(/\|/);
    next if (Songhost =" /~$localnet/);
    next if (exists $cache{$onghost});
    print 'Sfpingex Sorighost';
    $cache{$orighost}=l; }
    Эта программа запускает команду clog и считывает ее вывод до бесконечности. Поскольку наша внутренняя сеть вне подозрений, каждый узел сравнивается с префиксом локальной сети и весь трафик из локальной сети игнорируется.
    На этот раз, как и в последнем примере, мы используем некоторое кэширование. Мы - добропорядочные жители сети и не собираемся закидывать внешние машины множеством пакетов ping, так что мы следим за тем, к каким узлам мы уже обращались с запросами. Флаг ~rl у fping служит для ограничения количества попыток обращения к узлу программой fping (по умолчанию предпринимается три попытки). Эту программу необходимо выполнять с повышенными привилегиями, т. к. и программе clog, и программе fping нужен привилегированный доступ к сетевому интерфейсу компьютера. Вывод этой программы выглядит так:
    199.174,175.99 is unreacrable
    128,148.157 143 is unreachablo
    204.241.60.5 is alive
    199.2.26.116 is unreachable
    199.172.62.5 is unreacnable
    130.111.39.100 is alive
    207.70.7.25 is unreachabie
    198.214.63.11 is alive
    129.186.1.10 is alive
    Очевидно, что здесь творится нечто подозрительное. С чего вдруг половина узлов доступна, а половина - нет? Перед тем как ответить на этот вопрос, давайте посмотрим, что можно сделать для улучшения этой программы. Первый шаг - избавиться от зависимости от внешних программ. Умение прослушивать сеть и посылать пакеты ping из Perl открывает целый диапазон возможностей. Сначала позаботимся о том, чтобы удалить простую зависимость.
    Модуль Net: : Ping Рассела Мосмана (Russell Mosemann), который можно найти в дистрибутиве Perl, помогает проверить работоспособность сети. Net: : Ping позволяет посылать пакеты ping трех типов и проверять возвращаемые ответы: ICMP, TCP и UDP. ICMP-пакеты (Internet Control Message Protocol)- это «классика ping», и их посылает подавляющее большинство программ, производных от ping. У пакетов этого типа есть два недостатка:
    1. Как и в случае с программой, вызывающей clog/fping, все сценарии Net: :Ping, использующие ICMP, необходимо выполнять с повышенными привилегиями.
    2. Perl на MacOS в настоящее время не поддерживает ICMP. Возможно, в будущем это будет исправлено, а пока не следует забывать об этом ограничении переносимости.
    Два других варианта пакетов Net: :Ping -это пакеты TCP и UDP. В обоих случаях пакеты посылаются на порт службы echo удаленной машины. Эти две возможности обеспечивают переносимость, но вам они могут показаться менее надежными, чем ICMP. ICMP встроен во все стандартные стеки TCP/IP, но не на всех машинах может быть запущена служба echo. В результате, если ICMP не отфильтровывается намеренно, вы получите ответы на ICMP-пакеты с большей вероятностью, чем на пакеты других типов.
    В Net: :Ping применяется стандартная модель объектно-ориентированного программирования, поэтому первым делом нужно создать новый экземпляр объекта ping: use Net::Ping;
    $p = new Net::Ping("icmp"):
    Этот объект очень просто использовать:
    if ($c->pir,g("hcst")){
    prmt "oirg succeeded '' " ': else{
    pr:it "p:ng faiied\n": }
    Теперь вернемся к сложной части нашего первоначального сценария, прослушиванию сети с помощью clog. К сожалению, с этого момента нам придется отпустить пользователей MacOS. Программа на Perl, которую мы собираемся рассматривать, привязана к библиотеке libpcap, о которой мы говорили раньше, поэтому применение программы где-либо, кроме Unix и его вариаций, затруднено или невозможно.
    Первый шаг, который необходимо выполнить, - собрать библиотеку libpcap. Я советую скомпилировать и tcpdump. Как и в случае с утилитами из командной строки для UCD-SNMP, tcpdump можно использовать для выяснения возможностей libpcap перед тем, как писать на Perl, а также для перепроверки кода, написанного для этой программы.
    Имея libpcap, легко скомпилировать модуль Net: :Pcap, первоначально написанный Питером Листером (Peter Lister), а затем полностью переписанный Тимом Поттером (Tim Potter). Этот модуль открывает вам доступ ко всем возможностям libpcap. Давайте посмотрим, как можно использовать его для поиска пакетов SYN, как это делает clog.
    Программа начинается с запроса о доступном/допускающем прослушивание сетевом интерфейсе и его настройках:
    use Net::Pcap;
    # поиск сетевого устройства, допускающего прослуиивание
    $dev = Net: :Рсар: : icoKupdev(\$en ) ;
    die "Невозможно найти подходящее устройство: $err\n" unless Sdev;
    tf выясняем номер и маску сет;.> эгого \'Стоойстра die "Невозможно выяснить информацию об устройстве:
    В большинстве функций libpcap действуют соглашения о кодах возврата, принятые в С, и возвращают 0 в случае успеха и -1 в случае неудачи, поэтому в программах, использующих Net: : Рсар, часто применяется идиома с:е :*' . . В страницах руководства по рсар(З) можно выяснить смысл аргументов, передаваемых каждой функции.
    Получив информацию о сетевом интерфейсе, можно сообщить libpcap о намерении прослушивать сеть (вместо того, чтобы читать пакеты из сохраненного ранее файла пакетов). Net :Рса;> 'km," _r.vo возвращает дескриптор, используемый для ссылки на этот сеанс:
    открываем интерфейс для "живого" захвата
    die "HeB03vof.no пол>чить дескригтоо $е:- -Г ariesb Sceseriar:
    Программа libpcap позволяет перехватывать как весь сетевой трафик, так и ограниченное подмножество пакетов, выбираемое в соответствии с заданным критерием фильтрации. Ее фильтрующий механизм очень эффективен, поэтому зачастую лучше напрямую вызвать его, чем потом в программе отсеивать ненужные пакеты. В данном случае нас интересуют только пакеты SYN.
    Что собой представляет пакет SYN? Чтобы понять это, нужно иметь представление о том, как собираются TCP-пакеты. Посмотрите на рисунок из RFC973, где приведен TCP-пакет и его заголовок.
    Пакет SYN - это тот пакет, в заголовке которого установлен только флаг SYN . Для того чтобы lihpcap «знала», что надо перехватывать только такие пакеты, следует опреде лить, какой именно байт в пакете она должна искать. Каждый штрих н;1 верху соответствует одному биту, так что нетрудно подсчитать байты Тот же пакет.
    Нам необходимо проверить, равен ли 13-й байт двоичному числу 00000010 (десятичное число 2). В качестве фильтра нам нужна строка. Если бы мы хотели найти пакеты, у которых установлен по крайней мере флаг SYN, то могли бы использовать строку tcp[13] & 2 ! = 0. Этот фильтр затем компилируется в программу фильтрации и устанавливается:
    Sprog = "tcp[13]- = 2";
    # компилируем и устанавливаем "программу фильтрации" die "Невозможно скомпилировать $prog\n"
    if (Net::Pcap::compile($descript ,\$compprog,Sprog,0SneLmasK)) ; die "Невозможно установить фильтр\п"
    if (Net::Pcap::setfilter($descript,Scompprog));
    Еще чуть-чуть и можно запускать libpcap. Но перед этим нужно определить, что делать с найденными пакетами. Для каждого пакета, соответствующего фильтру, она может по нашему выбору запустить подпрограмму обратного вызова. Этой подпрограмме передаются три аргумента:
    1. Пользовательский строковый идентификатор, который может устанавливаться в начале перехвата и позволяет процедуре различать несколько сеансов перехвата пакетов.
    2. Ссылка на хэш, описывающая заголовок пакета (отметки о времени и пр.).
    3. Копия всего пакета.
    Начнем мы с очень маленькой процедуры, выводящей длину полученного пакета:
    sub printpacketiength {
    print length($_[2]),"\n": }
    Имея нужную подпрограмму, мы ищем пакеты SYIJ:
    die "Невозможно перехватить пакеты: ". Not : : Рсар: :ge:err(Sdebcr ipt) \:~"
    if (Net, :Pcap: : loop($descr ip:. -i. &p- -.tpao^i-fjir. ' )).
    die "Невозможно закрыта уст роиство\п" if (Net: :Pcap; :close($descnpt)):
    Второй аргумент -1 метода Net: : Рсар: : 1оор( )определяет количество пакетов, которые мы хотим перехватить до выхода. В данном случае мы будем перехватывать пакеты до бесконечности.
    Приведенная выше программа перехватывает пакеты SYN и выводит их длину, но это не совсем то, чего мы хотели добиться в начале этого раздела. Нам нужна программа, которая будет искать пакеты SYN из других сетей и попытается «прощупать» (ping) источник. У нас есть практически все; пока не хватает только способа, позволяющего определить источник полученного SYN-пакета.
    Как и в примере из главы 5, работающем с DNS, нам придется разбить пакет на части. Обычно такая процедура требует обращения к спецификации (RFC) и создания нужных шаблонов unpack(). Тим Поттер (Tim Potter) проделал сложную работу и написал несколько модулей Net Packet: NetPacket::Ethernet, NetPacket::IP, NetPacket::TCP, NetPacket::ICMP и т. д. Каждый из них поддерживает два метода: strip() и decode().
    Метод strip() просто возвращает данные из пакета, выкидывая все, что касается уровня сети. Запомните, что TCP/IP-пакет в сети Ethernet - это, на самом деле, обычный пакет TCP, «обернутый» в пакет IP, а тот, в свою очередь, обернут в пакет Ethernet. Так что если $pkt хранит TCP/IP-пакет, то NetPacket:: Ethernet: :strip($pkt) вернет IP-пакет (удалив уровень Ethernet). Если бы нам нужна была TCP-часть от $pkt, можно было бы использовать NetPacket: :IP: : st r ip( Net Packet: : Ethernet: : stnp($packet)) для удаления и 1Р-, и Ethernet-уровня.
    decode() продвигается глубже еще на один шаг. Он разбивает пакет на его составляющие и возвращает экземпляр объекта, содержащего все эти части. Например:
    NetPacket::TCP->decode(
    NetPacket: :IP: : strip( Net Packet: ; Ethernet: :st>-ip($packet)))
    вернет экземпляр объекта со следующими полями:
    Поле Описание
    src_port TCP-порт источника
    dest_port TCP-порт приемника
    Seqnum Порядковый номер
    Acknum Номер подтверждения
    Hien Длина заголовка
    Reserved 6-битное «зарезервированное» пространство в TCP-заголовке
    Flags Флаги URG, АСК, PSH, RST, SYN и FIN
    Winsize Размер TCP-окна
    Cksum Контрольная сумма
    Urg Указатель на экстренные данные
    Options Любые TCP-параметры в двоичном виде
    Data Данные для пакета
    Это уже должно быть знакомо читателю (Рисунок 10.2). Чтобы выяснить порт приемника для пакета, можно сделать следующее:
    $pt = NetPacket::TCP->decode( NetPacket: :IP: :stnp(
    NetPacket::Ethernet::strip($packet )))->{dest_port};
    Теперь соберем все вместе и кое-что изменим. Поттер создал оболочку для инициализации и циклов Net: : Рсар и выпустил ее как модуль Net: : PcapUtils. Модуль обрабатывает некоторые из выполняемых нами шагов, делая наши программы короче. Продемонстрируем все это л действии, учитывая все, что мы узнали в последнем разделе:
    use Net::PcapUtils;
    use NetPaeket::Ethernet; use NetPacket::IP;
    use Net::Ping;
    # локальная сеть $localnet = "192.168.1";
    # фильтр для поиска SYN-пакетов не из локальной сети $prog = "tcp[13] = 2 and src net not Slocalnet";
    $| =1: tt снимаем буферизацию с STDIO
    sub grab_ip_and_pmg{
    my ($arg,$hdr,$pkt) = @_ ;
    ft получаем IP-адрес источника $src in = lietPacket: : IP->(Jecoae(
    print "$src_]p is ". (($p-'>ping($src_ip)} 9
    "alive" : "unreachable"). "\,i" unless $cache{$src_ip}++; }
    Теперь, когда мы достигли своей цели и написали целиком на Perl (хоть и с помощью некоторых модулей, являющихся Perl-оболочками для программ на С) программу, позвольте мне рассказать, чем закончилась эта история.
    В воскресенье утром центральная группа поддержки из другого отдела нашла ошибку в настройках маршрутизатора. Студент в одном из общежитий установил на свою машину Linux и неверно настроил демон маршрутизации сети. Эта машина посылала широковещательные сообщения в университетскую сеть о том, что она является маршрутом по умолчанию. Неправильно настроенный маршрутизатор, обслуживающий наш отдел, услышав это сообщение, безропотно изменил таблицу маршрутизации и добавил второй маршрут во внешний мир. Пакеты будут приходить из внешнего мира, и этот маршрутизатор, честно выполняя свои обязанности, поровну разделит ответы между двумя маршрутами. Такое распределение, когда «один пакет посылается настоящему маршрутизатору, другой посылается на машину студента, один - настоящему маршрутизатору, другой - на машину студента», создало асимметричную ситуацию маршрутизации. Когда фиктивный маршрут был удален и были задействованы фильтры, предотвращающие появление такой ситуации в дальнейшем, наша жизнь вернулась к нормальному ритму. Я не буду говорить, что стало со студентом, вызвавшим эту проблему.
    В этом разделе вы познакомились с применением модулей Net: :Р. Net: : PcapUtils и семейства модулей NetPacket: : * для диагностики. Не останавливайтесь на этом! Эти модули позволяют написать множество программ, способных помочь разобраться с проблемами в сети или активно наблюдать за сетью в поисках опасности.

    Поиск проблемных образцов

    Поиск проблемных образцов

    Теперь воспользуемся тем, что мы узнали в главе 9 и перейдем дальше. Только что мы говорили о поиске подозрительных объектов; давайте теперь рассмотрим образцы (patterns), которые могут быть признаками подозрительной активности. Покажем это на примере программы, выполняющей примитивный анализ журналов в поисках потенциальных взломов.
    Пример строится на предположении, что большинство пользователей, удаленно регистрирующихся в системе, делают это из одного и того же места или нескольких мест. Обычно они регистрируются либо с одной машины, либо используя адреса из диапазона, принадлежащего одному и тому же провайдеру. Если вы обнаружите, что пользователь регистрировался из нескольких доменов, это верный признак того, что данная учетная запись была «взломана» и ее пароль стал доступен многим. Очевидно, что такое предположение не справедливо для постоянно перемещающихся пользователей, но если вы обнаружите, что пользователь регистрировался сначала из Бразилии, а потом из Финляндии с интервалом в два часа, то это достаточный повод заподозрить что-то неладное.
    Рассмотрим программу, реализующую поиск таких признаков. Сама программа написана для Unix, но демонстрируемые в ней приемы не зависят от платформы. Во-первых, это встроенная документация.
    Неплохо поместить нечто подобное ближе к началу программы для тех, кто будет смотреть на исходный код. Перед тем как двигаться дальше, обязательно взгляните, какие аргументы поддерживаются
    программой:
    sub usage {
    print «"EOU"
    lastcheck - проверяет вывод команды last,
    ИСПОЛЬЗОВАНИЕ: lastchecK [args]. где вместо может быть:
    -i: для IP-адресов, считан-^ сеть класса С одним 'доменом
    -л: помощь (это сообщение)
    /usr/ucb/last
    exit:
    Сначала мы анализируем аргументы из командной строки, просматриваются аргументы программы и соответствующим образом устанавливается $ор1_<буква_фла!а>. Двоеточие после буквы
    говорит о том, что этот параметр принимается как аргумент:
    use Getopt::Std: ft сгиндарт.ньи: процессор
    &usage if (defined $opt_h):
    it допустимое количество уникальных
    Smaxdomains = (defined $opt_ni) 9 Sopt.r : 3:
    В следующих строчках реализуется выбор, сделанный в пользу переносимости (но в ущерб эффективности) - об этом мы говорили в главе 9. На этот раз мы решили вызвать внешнюю программу. Для того чтобы сделать программу менее переносимой, но несколько более эффективной, можно было использовать uripack(), о чем тоже говорилось в той
    главе:
    Slastex = (defined $opt_l) ? $opt_l : "/usr/ucb/last":
    open(LAST,"$lastex|") [| die "Невозможно выполнить программу Slastex:$!\n";
    Перед тем как двигаться дальше, давайте взглянем на хэш списков, с помощью которого программа обрабатывает данные, полученные от last. Ключами этого хэша являются имена пользователей, а значениями - ссылки на список уникальных доменов, с которых регистрировался пользователь.
    К примеру, запись может выглядеть так:
    Suserinfo { laf } = [ 'ccs.neu.edu', 'xerox,com', 'foobar.edu' ]
    Эта запись говорит о том, что пользователь laf регистрировался с доменов ccs.neu.edu, xerox.com и foobar.edu.
    Начинаем мы с того, что обходим в цикле вывод команды last. На нашей системе он выглядит примерно так:
    Cindy pts/10 sinai.ccs.neu.ea Fri Mar 27 13:51 still logged in
    michael pts/3 regulus. ccs. neu Fn Mar 27 13:51 still logged I"'
    david pts/5 fruity-pebbles.с Fri Mar 27 13:48 still logged in
    deborah pts/5 grape-nuts.ccs.n Fri Mar 27 11:43 - 11:53 (00:09)
    barbara pts/3 152,148.23,66 Fri Mar 27 10:48 - 13:20 (02:31)
    Jerry pts/3 nat16. asoer-tec с Fri Mar 27 09:24 - 09:26 (00:01)
    Заметьте, что имена узлов (в 3-й колонке) в выводе команды last усечены. В главе 9 мы уже говорили об ограничениях на длину имени узла, но до сих пор мы обходили стороной это препятствие. Когда мы попробуем заполнить нашу структуру данных, проблемы станут очевидными.
    Раньше в цикле while мы пытались пропустить строчки, содержащие данные, которые нас не интересуют. Как правило, проверка особых случаев в самом начале цикла до какой-либо обработки данных (на-
    пример, при помощи spnt()) - неплохая идея. Это позволяет программе быстро определить, что можно пропустить определенную строчку и перейти к дальнейшему чтению данных:
    while (){
    игнорируем специальных погьзонателей
    next if /~rePoot\s]"shutdown\s|~flp\s/:
    если использовался ключ -и д/'.я определения конкретного
    пользователя, пропускаем все записи, не относящиеся к
    нему (имя сохраняется в $opt_u функцией getopts)
    next if (defined $opt_u && !/"$opt_u\s/);
    игнорируем вход с консоли X
    next if /:0\s+:0/;
    # ищем имя пользователя, терминал и имя удаленного узла
    ($user, $tty,$host) = split;
    игнорируем, если запись в журнале имеет "плохое" имя
    пользователя
    next if (length($user) < 2);
    игнорируем, если для данного имени нет информации о домене
    next if $host !" Д./;
    ищем доменное имя узла (см. приведенное ниже объяснение)
    $dn = &domain($host);
    игнорируем, если доменное имя фиктивное
    next if (length ($dn) < 2);
    игнорируем эту строку, если она находится в домене,
    заданном ключом -f
    next if (defined $opt_f && ($dn =' /~$opt_f/));
    если мы не встречали раньше имя этого пользователя,
    просто создаем список доменов для этого пользователя и
    сохраняем эту информацию в хэше списков
    unless (exists $userinfo{$user}){
    $userinfo{$user} = [$dn];
    в противном случае нам придется нелегко:
    см. приведенное ниже объяснение
    else {
    &AddToIfo($user.Sdr):
    closed-AST).
    Теперь рассмотрим отдельные подпрограммы, предназначенные для разрешения сложных ситуаций в программе. Первая подпрограмма &domain() принимает полностью заданное доменное имя, т. е. имя узла с полным доменным именем, и возвращает лучшую догадку о доменном имени для этого узла. Есть две причины, по которым подпрограмма должна быть довольно умна:
  • Не все имена узлов из журналов будут именами. Это вполне может быть и IP-адрес. В этом случае, если пользователь устанавливает ключ -г, мы полагаем, что любой получаемый нами IP-адрес - это
    адрес сети класса С, разделенной на подсети по границе байта. На практике это означает, что доменным именем мы считаем первые три октета адреса. Это позволяет нам считать регистрацию в системе с адресов 192.168.1.10 и 192.168.1.12 регистрацией из одного логического источника. Вероятно, это не лучшее предположение, но это лучшее, что мы можем сделать, не обращаясь при этом к другому источнику информации (да и в большинстве случаев это работает). Если пользователь не указывает ключ -i, мы считаем весь IP- адрес доменом.
  • Как говорилось раньше, имена узлов могут быть усечены. Это приводит к тому, что мы имеем дело с неполными записями, подобными grape-nuts, ccs. n и nat16. aspentec.c. Это не так страшно, как кажется, потому что полностью определенное имя домена в журнале каждый раз будет усекаться на одном и том же месте. В подпрограмме &AddToInfo() мы попробуем сделать все возможное, чтобы справиться с этим ограничением. Но об этом чуть позже.
  • А пока вернемся к программе:
    принимаем полностью определенное имя домена и пытаемся
    определить домен
    sub domain{
    ищем IP-адреса
    if ($„[0] =' /-\d+\.\d+v\d+v\d+$/) {
    если пользователь не указал ключ -i. просто
    возвращаем IP-адрес как есть
    unless (defined $opt_i){
    return $_[0];
    }
    иначе возвращаем все. кроме последнего октета
    else {
    $_[0] =~ /(.-)\.\d+$/:
    return $1;
    (
    }
    переводим все в нижний регистр, чтобы потом было
    # проще и быстрее обрабатывать информации!
    S. [.и] = 1с($_[С]);
    }
    }
    Следующая очень короткая подпрограмма заключает в себе самую сложную часть программы. Подпрограмма &AodToTr ч;() работает с усеченными именами узлов и сохраняет информацию в хэш-таблицу. Мы применим способ сравнения подстрок, который может пригодиться и в ином контексте.
    В нашем случае было бы неплохо, если бы все эти имена доменов (читались бы и сохранялись бы в массиве уникальных имен доменов для пользователя как одно имя:
    ccs.neu.edu
    ccs.neu.ed
    ccs. n
    Решая, является ли имя домена уникальным, необходимо проверить три вещи:
    1. Совпадает ли имя домена полностью с чем-нибудь, что уже сохранено для этого пользователя?
    2. Является ли это имя домена подстрокой уже сохраненных данных?
    3. Являются ли подстрокой проверяемого имени домена сохраненные данные?
    Если верно что-либо из этого списка, значит, нет необходимости добавлять новую запись к структуре данных, поскольку эквивалентная под строка уже сохранена в списке доменов для пользователя. Если выполняется пункт 3, мы заменим сохраненную запись текущей, если, конечно, мы сохраняем строки максимальной длины. Внимательные читатели могли заметить, что выполнение первых двух пунктов можно проверять одновременно, поскольку точное совпадение эквивалентно совпадению подстроки по всем символам.
    Если же не справедлив ни один из этих случаев, то необходимо сохранить новую запись. Посмотрим сначала на код, а потом обсудим, как он работает:
    цио AodTolPfc!
    проверка 1-го и 2-го случаев: есть л/ полное или
    и частичное совпадение9
    renirn If (1ndOvC$ $п;гИ -- IV
    и проверка 3-го случая, го есть. явлй^:.;ч ,-;,, :-с„отоокоп
    # сохраненные данные
    if (index($un. $_.) > -1){
    return:
    }
    # в противном случае это новый домен, добавляем его в список
    push @{$userinfo{$user}}, $dn:
    }
    Конструкция @{$userinfo{$user}} возвращает список доменов, сохраненных для этого пользователя. Мы обходим в цикле все элементы из этого списка, проверяя, можно ли найти среди них $dn. Если можно, то
    мы выходим из подпрограммы, т. к. эквивалентная подстрока уже сохранена.
    Если эта проверка пройдена, то можно перейти к пункту 3. Мы проверяем каждую запись из списка, чтобы выяснить, встречается ли она в текущем домене. Мы заменяем запись из списка на текущий домен, если совпадение найдено, тем самым сохраняя более длинную из двух строк.
    Поскольку это не вредит, замена производится и при точном совпадении. Мы переписываем запись, используя специальное свойство операторов fо г и fо reach в Perl. Присваивая значение переменной $_ в середине цикла for, Perl в действительности присваивает значение текущему элементу списка. Переменная цикла становится псевдонимом для переменной списка. После того как мы поменяли местами значения, можно выходить из подпрограммы. Если были пройдены все три проверки, то
    в последней строке к списку доменов для пользователя добавляется рассматриваемое имя домена.
    Это все, что касается «кровавых» деталей просмотра файла и создания структуры данных. Чтобы завершить эту программу, рассмотрим всех найденных пользователей и проверим, со скольких доменов они регистрировались (т. е. выясним длину сохраненного для каждого из них списка). По тем записям, для которых найдено больше доменов, чем можно, мы выводим полный список:
    for (sort keys %usermfo){
    if ($#{$jsenr-fo{$J} > $."iaxaora:"s)!
    }
    РГЩГ "V:' Протокол SNMH
    Что ж, программу вы видели и вас, вероятно, интересует, действительно ли работает этот метод. Вот реальный отрывок вывода этой программы для пользователя, чей пароль был украден:
    38.254.131
    bj.edu
    ccs.neu.ed
    dac.neu.ed
    hials.no
    ipt. a
    tntl.bosl
    tntl.bost
    tntl. dia
    tnt2.bos
    tntS.bos
    tnt4.bo
    toronto4.di
    Некоторые из этих записей выглядят нормально для пользователя, живущего в Бостоне. Однако запись toronto4.di выглядит несколько подозрительной, а сайт hials.no вообще находится в Норвегии. Схвачены с поличным!
    Программу можно усовершенствовать, добавив проверку времени или сравнение с другими журналами, например, полученных при помощи tcpwrappers. Но как видите, поиск шаблонов часто важен сам по себе.

    Предотвращение подозрительных

    Таблица 10.2. Файлы, созданные командой h2xs -A n Cracklib

    Чтобы получить нужную нам функциональность, следует изменить два файла. Начнем с более сложного: склейки с кодом на С. Вот как эта функция определяется в документации libcrack:
    Имя файла Описание
    Cracklib/ Cracklib. рт Заглушка с прототипами и документацией
    Cracklib/Cracklib.xs Склейка с кодом на С
    Cracklib/Makefile.PL Код на Perl для создания файла Makefile
    Cracklib /test.pl Тестовый код прототипа
    Cracklib/Changes Документирование версий
    Cracklib/MANIFEST Список файлов, входящих в состав модуля
    char pw
    char 'dictpatn
    Директива PROTOTYPES создает Perl-прототипы для функций из этого файла. В программе, которую мы пишем, это не имеет значения, но мы включаем директиву для подавления предупреждений в процессе сборки.
    Сразу же после определения функции мы описываем, как она вызывается и что возвращает:
    CODE:
    RETVAL = (char «)FascistCheck(pw,dictpath);
    OUTPUT:
    RETVAL
    RETVAL - это настоящая склейка. Она представляет собой точку передачи между кодом на С и интерпретатором Perl. Именно тут мы говорим Perl, что он должен получить строку символов, возвращенную библиотечной функцией FascistCheck(), и сделать их доступными в качестве возвращаемого значения (т.е. OUTPUT) Perl-функции Cracklib: : Fas-cistCheck(). Больше нам не придется иметь дело с кодом на С.
    В другом файле, который нужно поменять, мы изменим только одну строку. Нам требуется добавить еще один аргумент в вызов WriteMake-f ile() в Makefile.PL, чтобы убедиться, что Perl может найти файл ПЬ-crack.a. Вот как выглядит эта новая строка в нашем контексте:
    'LIBS' => [''], tt например, '-1т'
    'MYEXTLIB' => '/usr/local/lib/libcrack$(LIB_EXT)' tt местоположение cracklio
    'DEFINE' => '', например DHAVE_SOMETHING'
    Это тот минимум, который необходим для работы модуля. Если мы наберем:
    perl Makefile.PL
    make
    make install
    то сможем начать использовать наш модуль примерно так:
    use Cracklib:
    use Term: : ReadKey: tt для чтения паролей
    Sdictpath = "/usr/local/etc/cracklib/pw_dict";
    prL.t "Введите пароль: ":
    RearJMode 2; tt отключаем зьвод символов ";;
    chomp($pw = Headline):?} читаем пароль
    ReadMode 0: tt возвращаем *езмл з поедьллцее :ост:)«"ие
    print "\n" $result = Oacklib: : FascistCheck($pw Sclictpath); i f (df.'f". ""<: ?' f'S'i1
    Но не стоит использовать этот модуль в таком виде. Давайте, перед тем как устанавливать модуль, доведем его до профессионального уровня.
    Во-первых, добавим сценарий, позволяющий удостовериться, что модуль работает корректно. Сценарий должен вызывать нашу функцию с некоторыми известными значениями и сообщать каким-нибудь специфичным образом, получил ли он правильные ответы. В самом начале проверки нужно напечатать диапазон номеров тестов. Например, если мы собираемся провести 10 тестов, нужно сначала напечатать 1. 10. Затем для каждого выполняемого теста следует напечатать либо «ok », либо «not ok» и номер теста. Стандартная программа сборки модуля интерпретирует этот вывод и выводит пользователю итоги результатов проверки.
    h2xs
    предоставляет пример сценария проверки, который можно изменять. Создадим каталог t (стандартный каталог, назначенный по умолчанию для проверки модуля) и переименуем test.pl в t/cracklib.t. Вот фрагмент кода на Perl, который нужно добавить в конец t /crack-lib.t для выполнения ряда тестов: местоположение файлов словарей Sclictpath = "/usr/local/etc/pw_dict";
    проверочные строки и известные для них ответы cracklib %test =
    ("happy" => "it is too short",
    "a" => "it's WAY too short",
    "asdtasdf" => "it does not contain enough DIFFERENT characters"
    "minicomputer" => "it is based on a dictionary word"
    "ftm2tgr3fts" => ""):
    Просматриваем в цикле все ключи из хэша. проверяя, возвращает
    ли cracklib предполагаемые ответа. Если да. то пишем "ок", в
    противном случае -- "not ok"
    $tcst.iur = 2;
    fo^each $pw (кеуз %test){
    my (Sresult) = Crackiib::FascistCnecK($pw.Sdictpatn); if ((defined $гез„1:
    ana Sresul-f c-q S:est{$p.';)) or (!ae!iriea Sresb.l: ctj StesiiSpw} eq "");' pri''t "0'' ". $':estnui!!++, "
    else i
    Всего было сделано шесть тестов (пять из хэша %test и проверка загрузки модуля), значит, нужно изменить строку из t/cracklib.t с:
    BEGIN {$|=1: print "1. . 1\: ) на:
    BEGIN {$|=1: print "1..6\"; }
    Теперь можно набрать make test и Makefile и запустить программу проверки, чтобы убедиться, что модуль работает верно.
    Разумеется, сценарий проверки очень важен, но наш сценарий вряд ли заслужит уважение, если мы пропустим такой решающий компонент, как документацию. Потратьте время и дополните файлы Cracklib.pm и Changes, заменив заглушки на полезную информацию о модуле. Также неплохо добавить файл README или INSTALL, в котором рассказано, как собрать модуль, где найти нужные компоненты, такие как libcrack, приведены примеры программ и т. д. Об этих новых файлах и переименовании файла test.pl нужно сказать в файле MANIFEST, чтобы не вводить в заблуждение программу компиляции модуля.
    Наконец, установите модуль там, где нужно. Используйте вызовы Cracklib: : FascistCheck() везде, где нужно установить или сменить пароли. Если количество плохих паролей в вашей системе уменьшится, «ночной сторож» с удовольствием одобрит вас.


    Протокол SNMP Давайте отвлечемся

    Один из способов использовать протокол SNMP из Perl - вызвать программу, работающую в командной строке, наподобие UCD-SNMP, при веденной в демонстрационных целях в приложении Е. Этот процесс безопасность и наблюдение за сетью виден и ничем не отличается от вызова внешних программ, о чем мы раньше упоминали в книге. Ничему новому тут научиться нельзя, так что не будем уделять этому подходу много времени. Приведу лишь одно предостережение: тем, кто использует SNMPvl или SNMPv2C, скорее всего, придется указывать имя сообщества (community name) в командной строке. Если эта программа выполняется в многопользовательской системе, любой, кто обратится к списку процессов, сможет увидеть имя сообщества и завладеть «ключами от города». Эта угроза существует в
    примерах, выполняемых в командной строке из приложения Е, но она становится более серьезной в автоматически выполняемых программах, которые неоднократно вызывают внешние программы. Лишь для наглядности в следующих примерах имя узла и имя сообщества определяются в командной строке. В настоящих программах от этого нужно избавиться.
    Если мы не вызываем внешнюю программу для выполнения SNMP-oneраций из Perl, другим вариантом является использование модуля SNMP. Существует по крайней мере три очень похожих модуля: Net: : SNMP Дэвида М. Тауна (David M. Town), SNMP_Session.pm, написанный Саймоном Лейненом (Simon Leinen) и «SNMP Extension Module v3.1.0 for the UCD SNMPvS Library» (Модуль SNMP расширений vS.l.O для библиотек UCD SNMPvS, который мы будем называть просто SNMP из-за способа его загрузки) Дж. С. Марзота (G.S. Marzot). Все три модуля реализуют SNMPvl. Net::SNMP и SNMP частично поддерживают SNMPv2. И лишь в SNMP предлагается некоторая поддержка SNMPvS.
    Помимо различного уровня поддержки протокола SNMP, самое большое различие между этими модулями заключается в их зависимости от внешних библиотек. Первые два (Net: :SNMP и SNMP_Session.pm) реализованы только на Perl, a SNMP должен быть скомпонован с прекомпилированной библиотекой UCD-SNMP. Основной недостаток применения SNMP - это дополнительная зависимость и лишний шаг компиляции (если считать, что вы можете собрать библиотеку UCD-SNMP на своей платформе).
    Положительная сторона зависимости от библиотеки UCD-SNMP в том, что она придает модулю дополнительную силу и мощь. Например, SNMP может анализировать файлы описания административных баз данных (Management Information Base, MIB) и выводить для анализа SNMP-na-
    кеты, чего не могут два других модуля. Для уменьшения разницы в возможностях существуют другие модули (например модуль SNMP: :MIB: :Compiler Фабьена Тассэна (Fabien Tassin) способен анализировать MIB), но если нужно, чтобы один модуль выполнял всю работу, то лучше модуля SNMP ничего нет.
    Давайте рассмотрим небольшой пример на Perl. Для того чтобы узнать количество интерфейсов на определенном устройстве, можно обратиться к переменной interfaces. ;fN'.jmDe<". Сделать это при помощи модуля Net: : SNMP очень просто:
    use Net::SNMP;
    в качестве аргументов задаются имя >зяа и /msi еоибцес! в,;
    (Ssession. $<;rro' ) = Net: :SNMP->session(,Hostnare = SARGVfOJ,
    С(Ж.
    die "Ошибка сеанса: Serror" unless ($snssion):
    S iso. org. dod internet, mgnit. mib-2. interfaces. ff-bmoer . 0 =
    1.36.1.2.1.2.1.0
    Sresult = $session->get_request("l.3.6.1.2.1.2.1.0'):
    die "Ошибка запроса: ". $session--->error unless (defined Sresult):
    $session->close;
    print "Количество интерфейсов: ".$result->{"1.3.6.1.2.1. 2. 1. 0"). "\n":
    Если указать на рабочую станцию с интерфейсом обратной петли и интерфейсом Ethernet, программа вернет: Количество интерфейсов: 2; если же указать на портативный компьютер с интерфейсом обратной петли и интерфейсами Ethernet и РРР, то программа вернет Количество интерфейсов: 3; для небольшого маршрутизатора она вернет Количество интерфейсов: 7.
    Важно обратить внимание на использование идентификаторов объекта (Object Identifiers, OID) вместо имен переменных. И Net::SNMP, и SNMP_Session.pm обрабатывают взаимодействие только по протоколу SNMP. Они не претендуют на выполнение каких-либо второстепенных задач, связанных с SNMP, например, анализ описаний SNMP MIB. Для выполнения этих действий нужно обратиться к другим модулям, таким как SNMP: :MIB: : Compiler или SNM.P_uЈt/.pm Майка Митчела (Mike Mitchell) для применения их с SNMP_Sess ion. pm (не путайте с модулем SNMP: : Utn '. Вейна Маркетта (Wayne Marquette), который используется с модулем SNMP).
    Для тех, кто предпочитает работать с текстовыми идентификаторами вместо численных, не создавая самостоятельно таблицу соответствия и не используя дополнительные модули, остается единственный вариант - обратиться к модулю SNMP, в котором есть встроенный анализатор MIB. Давайте рассмотрим таблицу ARP (Address Resolution Proto col, протокол преобразования адресов) на машине при помощи этого модуля:
    use SNMP;
    в качестве ап'умен'ов задаются имя
    Ssessio0 = new SNMP: : SessionfDestHosr -> $ARGVTO].
    UseSprintValue => 1) die "Ошибка создания сессии: $SNMP: : Session. fr'orStr" unless
    (defined Ssession)
    n создаем структуру данных дли команды getnex4
    $vars = new SNMP: :VarList([' iDNettoMediaNof Acc'-f-ss ]
    ['ipNetToMediaPhysAdd'eoF'}):
    получаем первую запись
    ($ip,$mac) = $session->getnext($vars):
    die $session->{ErrorStr} if ($session->(ErrorStr});
    # и все последующие
    while (!$session->{ErrorStr} and
    $$vars[0]-->tag eq "ipNetToMediaNetAddress"){
    print "$ip -> $mac\n":
    ($ip,$mac) = $session->getnext($vars):
    };
    Вот как выглядит пример вывода этой программы:
    192.168.1.70 -> 8:0:20:21:40:51
    192.168.1.74 -> 8:0:20:76:7с:85
    192.168.1.98 -> 0:сО:95:еО:5с:1с
    Этот пример похож на предыдущий, где использовался модуль Net: : SNMP. Для выявления различий рассмотрим его подробнее:
    use SNMP;
    Ssession = new SNMP::Session(DestHost => $ARGV[0], Community => $ARGV[1],
    UseSpnntValue => 1);
    После загрузки модуля SNMP мы создаем объект сессии так же, как и в случае с Net: :SNMP. Дополнительный аргумент UseSprintValue => 1 указывается лишь для того, чтобы выводить возвращаемые значения более аккуратно. Если этого не сделать, то Ethernet-адреса будут выводиться в закодированном виде.
    # создаем структуру данных для команды getnext
    $vars = new SNMP::VarList(['ipNetToMediaNetAddress'].
    ['ipNetToMediaPhysAddress']);
    SNMP со своими командами использует такие простые строки, как sys-Descr. О, но предпочитает работать со специальным объектом, который называет «Varbind». Модуль применяет эти объекты для хранения значений, возвращаемых в результате запросов. Например, в нашей программе для отправки запроса get-next-request вызывается метод getnext(), прямо как в примере таблицы маршрутизации из приложения Е. Правда, на этот раз SNMP сохранит полученные индексы в Varbind, и нам не придется вручную следить за ними. Используя этот модуль, достаточно передать Varbind методу getnext, если необходимо получить следующее значение.
    Varbind - это обычный анонимный Perl-массив, состоящий из четырех элементов: oir , i id, vai и type. Нас интересуют только. Первый элемент, obj - это объект, к которому посылается запрос. он может
    быть задан в одном из нескольких форматов. В данном случае мы пользуемся форматом leaf identifier, т. е. определяем лист дерева, с которым мы связаны. IpNetToMediaNetAddress - это лист дерева:
    iso.org. dod. internet, mgmt. mb-
    ip. ipMetToMediaTable. ipNetToMediatntry. ioNetTcMe3ia:jetAcd'ess
    Второй элемент в Varbind- это iid, или идентификатор экземпляра (instance identifier). В предыдущем примере мы использовали только (например system. sysDescr. 0), поскольку видели объекты, имеющие
    только один экземпляр. Скоро мы увидим примеры, где iid может иметь и другие значения, отличные от нуля. Например, позже мы сошлемся на определенный сетевой интерфейс в коммутаторе с несколькими Ethernet-интерфейсами. Для get необходимо указывать только два компонента Varbind- obj и iid. Методу getnext iid не нужен, т. к. он по умолчанию возвращает следующий экземпляр.
    В приведенной выше строке используется метод VarList(), создающий список из двух Varbind, для каждого из которых определен только один элемент obj. Этот список мы предаем методу getnext():
    # получаем первую запись
    ($ip,$mac) = $session->getnext($vars);
    die $session->{ErrorStr} if ($session->{ErrorStr});
    getnext() возвращает значения, полученные из запроса, и соответствующим образом обновляет структуры данных Varbind. Теперь остается только вызывать get next () до тех пор, пока мы не дойдем до конца таблицы:
    while (!$session->{ErrorStr} and
    $$vars[0]->tag eq "ipNetToMediaNetAddress"){
    print "Sip -> $mac\n";
    ($ip,$mac) = $session->getnext($vars);
    };
    Давайте вернемся к миру безопасности, чтобы рассмотреть последний пример о SNMP. Задачу, которую мы будем решать, сложно или, по крайней мере, скучно выполнять при помощи имеющихся утилит командной строки.
    Задача заключается в следующем: вас попросили выследить в комму тируемой сети Ethernet (switched Ethernet network) плохо ведущего себя пользователя. Единственная информация, которой вы обладаете, это Ethernet-адрес машины, на которой работает пользователь. Это но Ethernet-адрес, который содержится в файле (сам файл можно хранить в базе данных узлов, рассмотренной в главе 5, если эту базу несколько расширить), а прослушать коммутируемую сеть у вас не получится, так что придется проявить сообразительность, чтобы вычислить эту машину.
    Лучший выход из этого положения - обратиться к одному или всем коммутаторам и узнать, видели ли они этот адрес на одном из своих портов.
    Для
    большей конкретизации скажем, что сеть состоит из нескольких коммутаторов Cisco Catalyst 5500; это даст нам возможность указать на конкретные переменные MIB. Основные методы, которые мы будем использовать для решения этой проблемы, также применимы для других продуктов и других производителей. Если информация относится только к определенному коммутатору или производителю, мы об этом скажем. А теперь давайте шаг за шагом рассмотрим решение проблемы. Как и раньше, сначала необходимо выполнить поиск по корректным файлам модулей MIB. Обратившись к службе технической поддержки Cisco, мы узнаем, что нам понадобится доступ к четырем объектам:
  • vlanTable, которую можно найти в enterprises.Cisco.workgroup.cis-coStackMIB. vlanGrp из описания CISCO-STACK-MIB.
  • dotldTpFdbTablc (таблица прозрачной трансляции портов), которую можно найти в dotldBridge. dotldTp из описания RFC1493 BRIDGE-MIB.
  • dotldBasePortTable, которую можно найти в dotldBridge. rlotldBase в том же RFC.
  • ifXTable, которую можно найти в RFC1573 IF-MIB (Интерфейсы).
  • Зачем нужны четыре различные таблицы? В каждой из них есть что-то нужное нам, но ни в одной нет целиком всей информации, которую мы ищем. Первая таблица предоставляет список VLAN (Virtual Local Area Networks, виртуальные локальные сети), или виртуальных «сегментов сети» на коммутаторе. В Cisco решили хранить на коммутаторе отдельные таблицы для каждой виртуальной локальной сети, поэтому нам придется за один раз запрашивать информацию об одной виртуальной локальной сети. Подробнее об этом чуть позже.
    Вторая таблица предоставляет список Ethernet-адресов и номер порта (bridgeport) на коммутаторе, на котором этом адрес был замечен последний раз. К сожалению, этот номер порта в коммутаторе является внутренним параметром, и он не соответствует имени физического порта на нем же. Нам нужно знать имя физического порта, т. е. с какой сетевой карты и порта последний раз «общалась» машина с указанным Ethernet-адресом, так что нужно «копать» дальше.
    Таблицы, связывающей номер порта (bridge port) с именем физического порта, не существует (что было бы очень просто), но dot loBasePo позволяет выяснить соответствие между номером порта и номером нн- Протокол SNMP интерфейса. Имея номер интерфейса, можно найти его в таблице ifXTable
    и получить имя порта.
    Вот схема четырехуровневого согласования, необходимого для выполнения поставленной перед нами задачи (Рисунок 10.1).
    А вот программа, в которой все четыре таблицы собраны вместе для вывода нужной информации:
    use SNMP;
    tt Дополнительные модули MIB, нужные нам, которые можно найти з
    # том же каталоге, что и сценарий
    $ENV{'MIBFILES' } =
    "CISCO-SMI.my:FDDI-SMT73-MIB.my:CISCO-STACK-MIB.my:BRIDGE-MIB.my ' :
    tf соединяемся и получаем список виртуальных локальных сетей с
    tf этого коммутатора
    Ssession = new SNMP::Session(DestHost => $ARGV[0],
    Community => $ARGV[1]);
    die "Ошибка создания сессии: $SNMP::Session::ErrorStr" unless
    (defined Ssession);
    if enterprises. cisco.workgroup.ciscoStackMIB. vlanGrp. vlanTable. vlanEnrry
    tt из CISCO-STACK-MIB
    Svars = new SNMP::VarList(['vlanlndex' ]);
    Svlan = $session->getnext($vars);
    die $session->{ErrorStr) if ($session->{ErrorStr});
    while (!$sess]on->{ErrorStr} and $$vars[0]->tag eq "vlanlndex"){
    tf Ha CISCO CATALYST 5XXX просто не может быть более 1000
    tt виртуальных локальных сетей (этот предел, скорее всего,
    tf отличается для различных коммутаторов)
    push(@vlans,$vlan) if $vlan < 1000;
    Svlan = $session->getnext($vars):
    };
    undef Ssesaion,$vars;
    на Cisco 5000
    UseSprintVaiue => 1);
    die ириска создания сессии. SSNMP . : ьеьъ^и! ..сr ruiЫ
    I,: less (defined Ssession)
    dotldBi idgo.aotldTp.dotldTpFdoTable.do-.rdTpFaprr/ -,
    # .13 RFC1493 BRIDGE-MIB
    $vars = new S'iMP: :VarLisi(['dotiaTprcoAcaress1 ]. [ coticToFaoPort' ]):
    (Smacaddr, Sportnum) = $sessior,->getnex* ($vars):
    die $session-> {ErrorStr} if" ($session->{Erroi'Str/):
    while ('$session->{ErrprStr} and
    $$vars[0]-nag eq "dot1dTpFdpAddress"){
    n ddtldBridge.dotIdBase.dotIdBasePortTable.dotIdBasePortEn try
    и из RFC1493 BRIDGE-MIB
    $ifnum =
    (exists $ifnum{$portnum/) ? $ifnum{$portnuir\} :
    ($ifnum{$portnum} =
    Ssession->get("dotIdBasePortIfIndex\.Sportnum")):
    # из ifMIB.ifMIBODjects.ifXTable.ifXEntry из RFC1573 IF-MIB
    Sportname =
    (exists $portname{$ifnum}) ? $portnarne{$ifnum} :
    ($portname{$ifnum}=$session->get("ifName\.Sifnum"));
    print "Smacaddr в виртуальной локальной сети $vlan на $portname\n":
    (Smacaddr,Sportnum) = $session->getnext($vars):
    }:
    undef Ssession, $vars, %ifnum, ^oportname1;
    }
    Если вы уже читали приложение Е, то большая часть программы должна быть вам знакома. Приведем лишь комментарии по новым фрагментам:
    $ЕМУ{ 'MIBFILES'h
    "CISCO-SMI.пу:FODI-SMT73-MIB.my;CISCO-STACK-MIB.my:BRIDGE-MIB.my":
    Эта программа устанавливает переменную окружения MIBFILES для библиотеки UCD-SNMP. Будучи установленной, эта переменная дает инструкцию библиотеке проанализировать приведенный список дополнительных файлов для определения объекта MIB. В этом списке присутствует один странный файл модуля MIB - FDDI SMT73MIB.my. Он добавлен из-за того, что CISCO-STACK-MIB.my имеет следующий оператор для включения некоторых определений из других записей MIB:
    IMPORTS
    MODULE-IDEiiTITr. OBJECT-TYPE. Integor32, IpAaor-sb,
    FROM SNMPv2-SMI
    DisplayString, RowStatus
    FROM SNMPv2-TC
    fddimibPORTSMTIndex, fddimibPORTIndex
    FROM FDDI-SMT73-MIB
    OwnerString
    FROM IF-MIB
    MODULE-COMPLIANCE, OBJECT-GROUP
    FROM SNMPv2-CONF
    workgroup
    FROM CISCO-SMI;
    Хотя мы и не ссылаемся на объекты, использующие fddimibPORTSMTI dex или fddimibPORTIndex, мы все же добавляем (намеренно) этот файл в список, чтобы анализатор MIB не выдавал сообщений. Все остальные определения MIB из этого оператора IMPORTS включаются либо в списке, либо в списке по умолчанию библиотеки. При работе с MIB вам часто придется заглядывать в раздел IMPORTS модуля MIB для изучения зависимостей этого модуля.
    Двигаясь дальше, мы находим еще один странный оператор:
    $sess]on = new SNMP::Session(DestHost => $ARGV[0]
    Community => $ARGV[ 1]. >". Svlan.
    UseSprintValue => 1):
    Вместо того чтобы просто передать имя сообщества, введенное пользователем, мы дописываем к нему нечто вроде .WL AN-NUMBER. На жаргоне Cisco это означает «индексация строки сообщества». При работе с виртуальными сетями и мостами устройства Cisco следят за несколькими «экземплярами» MIB, по одному на каждую виртуальную сеть. Наша программа выполняет одни и те же запросы для каждой виртуальной сети, найденной на коммутаторе:
    Sif.n'jrr =
    Приведем два комментария к этому отрывку. Во-первых, по ряду причин мы используем в качестве аргумента get:() простую строку. Хотя с таким же успехом это могло быть что-то более Varbind-подобное:
    Во-вторых, обратите внимание, что тут мы выполняем простое кеширование. Перед тем как выполнять де, мы смотрим в простую хэп:-таблицу (%1*пич), чтобы проверить, выполнялся ли уже этот запрос. Ее ли нет, то запрос выполняется, а его результаты помещаются таблицу. После просмотра всей виртуальной локальной сети кэширующий хэш удаляется (uruef %i'Tj:ii)1, чтобы исключить возможность
    дезинформации при использовании данных для другой виртуальной
    локальной сети.
    При написании программ для работы с SNMP о таких вещах стоит помнить всегда. Если вы хотите « пожалеть » свою сеть и сетевые устройства, очень важно посылать возможно более компактные запросы как можно реже. Если не проявить в этом благоразумия, то, отвечая на шквал запросов, устройство выделит меньше ресурсов для выполнения обычных задач.
    Вот отрывок из полученных в результате выполнения программы данных:
    "00 10 1F 2D F8 FB " в виртуальной локальной сети 1 на 1/1
    "00 10 1F 2D F8 FD " в виртуальной локальной сети 1 на 1/1
    "08 00 36 8В А9 03 " в виртуальной локальной сети 115 на 2/18
    "08 00 36 ВА 16 03 " в виртуальной локальной сети 115 на 2/3
    "08 00 36 D1 СВ 03 " в виртуальной локальной сети 115 на 2/15
    Эту программу улучшить нетрудно. Помимо более аккуратного и более упорядоченного вывода можно сохранять состояния между запусками. При каждом запуске программа могла бы сообщать об изменениях: какие адреса появились, какие порты изменились и т. д. Правда, нужно заметить, что большая часть коммутаторов относится к разновидности «обучаемых», поэтому они считают устаревшими записи об адресах, которые они давно не встречали. Это означает, то программу необходимо запускать как минимум с той же периодичностью, с которой устаревает информация о порте.

    Сравнение значений возвращаемых функцией stat( )

    Таблица 10.1. Сравнение значений, возвращаемых функцией stat( )

    Описание поля в Unix Действительно в
    NT/2000
    Действительно в MacOS
    0 Номер устройства файловой системы Да (порядковый но-
    мер диска)
    Да (но является vRefNum)
    1 Inode Нет (всегда 0) Да (но filelD/dirlD)
    2 Режим файла (тип и права) Да Да (но 777 для каталогов и приложений, 666 для незаблокированных документов, 444 для заблокированных документов)
    3 Количество (жестких) ссылок на файл Да (для NTFS) Нет (всегда 1)
    4 Численный идентификатор владельца файла Нет (всегда 0) Нет (всегда 0)
    5 Численный идентификатор группы владельца файла Нет (всегда 0) Нет (всегда 0)
    6 Идентификатор устройства (только для специальных файлов) Да (порядковый номер диска) Нет (всегда null)
    7 Размер файла в байтах Да (но не включает размер каких-либо альтернативных
    потоков данных)
    Да (но возвращает только размер данных)
    8 Время последнего доступа относительно начала эпохи Да Да (только эпоха начинается на 66 лет раньше, чем в Unix, то есть 1/1/1904, и значение то же, что и для поля №9) "
    9 Время последней модификации относительно начала эпохи Да Да (только эпоха начинается 1/1/1904 и значение то же, что и для поля №8)
    10 Время последнего изменения inode относительно начала эпохи Да (но время создания файла) Да (только эпоха начинается 1/1/1904, и это время создания файла)
    11 Предпочтительный размер блока для ввода/вывода Нет (всегда null) Да
    12 Количество занятых блоков Нет (всегда null) Да
    Для возвращения атрибутов, специфичных для операционной системы, в других He-Unix-версиях Perl помимо stat() и lstat() используются специальные функции. Рассказ о таких функциях, как Perl::Getr41oInf u() и Win32: :FileSecunу : ujt.(), можно найти в главе 2 «Файловые системы».
    После того как с помощью stat() для файла будут получены значения, на следующем шаге надо будет сравнить «интересные» значения с уже известными. Если они изменились, значит, изменилось и что-то в этом файле. Ниже приведена программа, которая генерирует строку значений ista l () и проверяет для файлов некоторые из этих значений. Мы намеренно исключили 8-е поле (время последнего доступа), потому что оно меняется при каждом прочтении файла.
    Программа принимает либо аргумент -р filename, чтобы вывести значения lstat() для заданного файла, либо аргумент -с filename, чтобы проверить значения lstat() для всех файлов, перечисленных в filename.
    use Getopt::Std;
    используем это для создания более симпатичного вывода позже в &pnntchanged()
    @statnames = qw(dev ino mode nlink uid gid rdev size mtime ctime blksize blocks);
    getopt('p:c:');
    die "Использование: $0 [-p |-c ]\n" unless ($opt_p or $opt_c);
    if ($opt_p)(
    die "Невозможно получить информацию о файле $opt._p:
    unless (-с $opt_p): print $opt_p."|",]OinC |',(lstat($opt_p))[0..7,9..12]),"\n":
    exit:
    if ($opt_c){
    oper.(CFILE,$opt_c) or
    die "Невозможно открыть файл $opt_c:$!\": while(){ cho;:!p:
    ssavecstats = spl-'('\:' ); die Неверное количество полей в строке, начинающейся с
    $savedstats[C']\'T jniess (Sttsaveustits == 12): s<,i.r rentstats = (Isratv $savedstat3[0]) )[0. 7,9. 12]
    }
    close(CFILE);
    }
    sub printchanged{
    my($saved. $curre!it)= ®_:
    выводим имя файла после того. выбрасываем его из массива, прочитанного из файла
    prin: shift §{$saved}.":\n":
    for (my $i=0; $1 < $#{$saved};$!++){
    if ($saved->[$i] ne $current->[$i]){
    print "\t".$statnames[$i]." is now ".$current->[$i];
    print " (should be ".$saved->[$i].")\n":
    } }
    Для использования этой программы можно набрать checkfile -p /etc/passwd » checksumfile. В файле checksumfile теперь будет храниться строка, которая выглядит так:
    /etc/passwd|1792|11427]33060)1|0|0|24959|607|921016509|921016509|8192|2
    Этот шаг нужно повторить для каждого файла, за которым мы наблюдаем. Затем вызов сценария с аргументом checkfile -с checksumfile будет сообщать обо всех изменениях. Например, если я удалю один символ из /etc/passwd, сценарию это не понравится, и он выведет такое сообщение:
    /etc/passwd:
    size is now 606 (should be 607)
    mtime is now 921020731 (should be 921016509)
    сtime is now 921020731 (should be 921016509)
    Перед тем как двигаться дальше, необходимо сказать об одном приеме, который мы применили в программе. В следующей строке проверяется равенство двух списков (сделано это на скорую руку):
    if ("®savedstats[1..12]' ne "@currentstats"):
    Perl автоматически преобразовывает список в строку, склеивая элементы списка через пробел:
    joint" ",ssavedstats[1. . 12]))
    и затем уже сравнивает получившиеся строки. Этот прием хорошо работает для коротких списков, в которых имеет значение порядок и количество элементов. В большинстве других случаев необходимо использовать итеративный подход или хэши, как описано в списках часто задаваемых вопросов perlfaq, входящих в состав Perl.
    Теперь, когда вы выяснили атрибуты файлов, я вынужден вас огорчить. Проверка того, что атрибуты файлов не изменились, - это хорошая идея, но не больше. Не представляет большого труда изменить файл, оставив неизменными такие атрибуты, как время доступа и модификации. В Perl даже есть функция, предназначенная для изменения времени доступа и модификации. Так что пришло время применить более мощные инструменты.
    Обнаружение изменений в данных - это одна из сильных сторон алгоритмов, известных как криптографические хэш-функции («message-di-gest algorithms»). Вот как Рон Райвест (Ron Rivest) описывает алгоритм «RSA Data Security, Inc. MD5 Message-Digest Algorithm» в RFC1321:
    Алгоритм на вводе принимает сообщение произвольной длины и создает подпись (message digest или fingerprint) длиной 128 бит. Считается, что просто невозможно создать два сообщения, у которых совпадали бы подписи; также невозможно создать сообщение, подпись которого совпадала бы с заранее заданной.
    Для нас это означает, что если применить к файлу алгоритм MD5, то он будет снабжен уникальной подписью. Если данные из этого файла изменятся, то независимо от того, насколько они незначительны, подпись файла тоже будет изменена. Самый простой способ воспользоваться
    этой чудесной возможностью из Perl - применить модуль LUgos, : : МУ:> из семейства модулей Digest.
    Использовать модуль Digest:: MD5 просто. Нужно создать объект, добавить в него данные при помощи методов add () miHaridfile(), а затем попросить модуль создать подпись.
    Можно сделать нечто подобное для подсчета подписи MD5 для файла паролей в Unix:
    use Digest: :MD5 qw(rnd5); $md5 = new Digest::MD5:
    open(PASSWD. "/Gtc/f.'asswd") or die "Невозмонсть открыть pass,vd$' ":
    acdf :Ie(PASSWD):
    ciose(PASSlvD)
    prit 3r!d5->hexd:g-2s: . "v "",
    В документации по Digest: :MD5 сказано, что для создания более компактных программ можно связывать несколько методов вместе. Так, предыдущую программу можно переписать:
    Файлы perlfaql. pod, perlfaq2.pod ... perlfaq[N].pod.
    use Digest::MD5 qw(md5);
    open(PASSWD. Vei-c/pabswri") or 'I:e "Ненот.' print Digest: :MD5--'neiA->addfi](](PASSWD) close(PASSWD):
    Обе программы выводят следующее:
    a6f905e6h45a65a7e03dOS09448b501c
    Если в файл внести незначительные изменения, то вывод станет другим. Вот что получилось, когда я поменял местами всего два символа в файле паролей:
    335679с4с97а381523034331a06df3e7
    Теперь любые изменения становятся очевидными. Давайте расширим предыдущую программу проверки атрибутов и добавим к ней MD5:
    use Getopt::Std;
    use Digest::MD5 qw(md5);
    @statnames =
    qw(dev ino mode nlink uid gid rdev size mtime ctinie blksize blocks md5):
    getopt('p:c:');
    die "Использование: 0 [-p |-c ]\n"
    unless ($opt_p or $opt_c):
    if ($opt^.p){
    die "Невозможно получить информацию о файле $opt_p:$'\n"
    unless (e $opt_p);
    open(F.$opt_p) or die "Невозможно открыть $opt_p:$'\r";
    $d.igest = Digest: ;MD5->new-^addfile(F)->hexaigest:
    ciose(F):
    print $opt__p, "|", ]oin
    "|$digest", "\n":
    exit:
    }
    if ($ppt_c){
    open(CFILE.$opt_c) or
    die "Невозможно открыть Файл;.
    wnilc (){
    c'-.o^p:
    ssavedstats = spli r(
    far,, rp'.tstats = (lstat($s;ivenstars[OJ))[0 . Л9..121:
    doSi:( h )
    &pr unchanged (\3savudstats. Vicur: и its: a; .1)
    if ("«?savedstars[1 13]"
    close(CFILE):
    }
    sub printcharigcd {
    my($saved,$cnrrent)= ®_:
    print shift @{$saved).":\n";
    for (my $i=0; $1 <= $(({$saved}; $!++){
    if ($saved->[$i] ne $current->[$i]){
    print " P\$statnames[$i]." is now ", $current->[$i ]:
    print " (".$saved->[$i].")\n";

    }

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

    Десятиминутное руководство по LDAP

    Десятиминутное руководство по LDAP

  • Организация данных в LDAP

  • LDAP (Lightweight Directory Access Protocol, облегченный протокол доступа к сетям) - это одна из самых значительных служб каталогов, существующих в настоящее время. Вероятно, со временем системные администраторы будут работать с LDAP-серверами и клиентами в различном контексте. Это руководство представляет собой введение в номенклатуру LDAP и концепции, необходимые для использования материала из главы 6 «Службы каталогов».
    Все действия в LDAP производятся над структурой данных, называемой элементом (entry). Схему, показанную на Рисунок В.1, имеет смысл хорошо себе представлять, когда будут рассматриваться составляющие элемента.
    Элемент состоит из нескольких компонентов, называемых атрибута ми (attributes), в которых хранятся данные для этого элемента. В терминах баз данных они похожи на поля записи. В главе б Perl используется для хранения списка машин из каталога LDAP. У каждого элемента, соответствующего машине, есть такие атрибуты, как имя, модель, местоположение, владелец и т. д.
    Помимо имени атрибут состоит из типа (type) и набора значений (values), соответствующих этому типу. Если вы храните информацию о сотрудниках, то у элемента может быть атрибут phone (телефон) типа telephoneNumber. Значениями этого атрибута будут номера телефонов сотрудников. Тип имеет также синтаксис, определяющий, какие данные можно использовать (строки, числа и т. д.), как они сортируются и как их применять при поиске (чувствительность к регистру).
    У каждого элемента есть специальный атрибут objectClass, содержащий несколько значений, которые вместе с настройками сервера и пользовательскими настройками определяют, какие атрибуты должны и могут существовать для этого определенного элемента.
    Рассмотрим детальнее атрибут objectClass, поскольку он иллюстрирует некоторые важные качества LDAP и позволяет избавиться от жаргона, с которым мы раньше не встречались. Рассматривая атрибут оbjectClass, нужно обратить внимание на следующее:
    LDAP объектно-ориентирован
    Каждое значение атрибута objectClass является именем класса объекта. Эти классы либо определяют набор атрибутов, которые могут или обязаны быть в элементе, либо расширяют определения, унаследованные из другого класса.
    Вот пример: атрибут objectClass элемента может содержать строку residentialPerson. В RFC2256 с устрашающим названием «A Summary of the X. 500(96) User Schema for use with LDAPv3» класс : е
    sidentialPerson определяется так:
    residentialPerson
    ( 2.5.6.10 NAME 'residentialPerson1 SUP person STRUCTURAL MUST 1 MAY ( businessCategory $ x121Address $ registeredAddress $ destinationlndicator $ preferredDeliveryMethod $ telexNumfcer $ teletexTerminalldentifier $ telephoneNumber $ internationaliSDNNumber $
    facsimileTelephoneNumber $ preferredDeliver\Methoo $ street S postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ I ) )
    В определении сказано, что элемент класса residentialPerscr должен иметь атрибут 1 (сокращение от locality) и может иметь целый набор других атрибутов (registeredAoaress, costOticeBox и т.д.).
    Ключевая часть спецификации - это строка SUP пгкаог. Это означает, что родительским классом (тем, от которого наследует свои атрибуты) является класс person. Данное определение выглядит так:
    person
    ( 2.5.6.6 NAME persun SUP lup STRUCTURAL MUST v s- $ MAY ( ussrPassworri $ te]ephoneNL;mhcr $ secvMso $ ciesc- .pr.ur ) )
    Значит, элемент класса residentialPerson должен иметь атрибуты (surname - фамилия), ел (common name - имя) и ; (locality - местоположение) и может иметь атрибуты, перечисленные в разделах MAY
    из этих двух отрывков из RFC. Кроме того, известно, что oersun - это вершина иерархии объектов для residentialPerson, поскольку его родительским классом является специальный абстрактный класс top.
    В большинстве случаев можно выйти из положения, если использовать предопределенные стандартные классы объектов. Для того чтобы создать элементы с атрибутами, которых нельзя найти в существующем классе, целесообразно найти ближайший класс и все построить на нем, как в случае с residentiaiPerson, построенном на person.
    LDAP происходит из мира баз данных
    Второе качество, которое можно увидеть ъ objectClass, - это корни LDAP, уходящие в базы данных. Набор классов объектов, определяющих атрибуты элементов, на сервере LDAP называются схемой
    (schema). RFC, процитированный выше, - это один пример спецификации схемы LDAP. В данной книге мы не будем касаться того, что имеет отношение к схеме. Как и в случае с проектированием ба-
    зы данных, проектированию схемы можно посвятить целую книгу, но вы должны быть, по крайней мере, знакомы с термином «схема», т. к. он всплывет позже.
    LDAP не ограничен хранением информации строго в структуре дерева
    Последнее, что нужно сказать об objectClass для того, чтобы перейти от рассмотрения одного элемента к более общей картине, относится к следующему: на вершине иерархии объектов в предыдущем примере находился класс top, но существует еще один квази-суперкласс, заслуживающий упоминания, класс alias. Если alias указан, то этот элемент действительно является псевдонимом для другого элемента (определяемого атрибутом aUaseaObjectNacie данного элемента). LDAP поощряет иерархические структуры в виде дерева, но он их не требует. Очень важно об этом помнить при написании программ, чтобы не сделать неверных предположений относительно иерархии данных на сервере.


    Организация данных в LDAP

    Организация данных в LDAP

    До сих пор мы говорили только об одном элементе, но спрос на каталоги, содержащие только один элемент, очень мал. Как только мы станем рассматривать каталоги, содержащие много элементов, перед нами тотчас встанет вопрос, с которого начиналось данное приложение: как найти что-либо?
    Все, что обсуждалось до этого момента, подпадает под определение, именуемое в спецификации LDAP «информационной моделью». Эта та часть, которая устанавливает правила представления информации, Но для ответа на наш вопрос необходимо рассмотреть «модель имен LDAP, определяющую, как информация организована.
    Если вы посмотрите на Рисунок В.1, то увидите, что мы рассмотрели все составляющие элемента, кроме его имени. У каждого элемента есть имя, известное как DN, или его отличительное имя (Distinguished Name). DN состоит из строки RDN, или относительных отличительных имен (Relative Distinguished Names). К отличительным именам мы скоро вернемся, но сначала остановимся на составляющих блоках RDN.
    RDN состоит из одной или нескольких пар «имя-значение атрибута» (name-value). Например, cn=Jay Sekora (где en- это «common name», или имя) вполне может быть относительным отличительным именем, Имя атрибута - сп, а его значение - Jay Sckora.
    Ни в спецификации LDAP, ни в спецификации Х.500 не указано, из каких атрибутов должно состоять RDN. Единственное, что требуется, уникальность RDN на каждом уровне в иерархии каталогов. Такое oграничение существует из-за того, что LDAP ничего не знает о «третьем элементе четвертой ветви дерева каталогов» и должен полагаться на уникальные имена на каждом уровне, чтобы различать на нем элементы. Посмотрим, как это ограничение действует на практике.
    Возьмем, к примеру, еще одно значение RDN: cn=Ronert Smith. Вероятно, это не лучший выбор для RDN, поскольку даже в средней организации, скорее всего, существует не один Роберт Смит. Если в организации работает много людей, а иерархия LDAP довольно плоская, то подобные пересечения имен вполне вероятны. Более правильный элемент состоит из двух атрибутов, например cn=Rcbert Sriith + i=B(Атрибуты в RDN объединяются при помощи знака «плюс».)
    Но с исправленным RDN, к которому добавлен атрибут 1 (местоположение), по-прежнему будут проблемы. Вероятно, мы отложили конфликт имен, но не исключили полностью вероятность его возникновения. Более того, если Смит переедет, нам придется изменить и КВл элемента и местоположение (атрибут 1) в этом элементе. Вероятно, самое лучшее относительное имя, которое можно использовать, уникальный и неизменный идентификатор данного человека. В частности, можно выбрать электронный адрес человека, тогда RDN изменится на uid = rsrnith. Этот пример должен дать читателю представление о принимаемых в схемах решениях.
    Проницательные читатели заметят, что мы на самом деле не расширили сферу обзора, а по-прежнему возимся с единственным элементом. Обсуждение RDN было прелюдией, а вот и настоящий переход: элементы организованы в виде древоподобной структуры, известной как информационное дерево каталогов (Directory Information Tree, DIT), или просто дерево каталогов. Лучше применять последний термин, поскольку в Х.500 аббревиатура DIT обычно служит для обозначения одного общего дерева, похожего на глобальную иерархию DNS или MIB, о которой речь пойдет позже, при обсуждении SNMP.
    А теперь снова вернемся к отличительным именам. Каждый элемент из дерева каталогов можно найти по его отличительному имени (DN), состоящему из относительного имени элемента (RDN), за которым следуют все RDN (разделяемые запятыми или точками с запятой), найденные при движении от него вверх к корневому элементу. Если перемещаться в направлении, указанном стрелками (Рисунок В.2), и запоминать при движении относительные имена, то можно создать DN для каждого выделенного элемента.
    Для первого элемента получилось бы такое DN (отличительное имя):
    cn=Robert Smith. l-n:ain campus. ou=CCS, o=Hogwarts School, c=US
    Для второго:
    uid=rsmith, ou=syste-ins, ou=people, dc=ccs, dc-hogwarts, dc=edu
    ou - это сокращение от «organizational unit» (подразделение организации), о - сокращение от «organization» (организация), dc - «domain component» (компонент домена, подобный DNS), а с - сокращение от «country» (страна) (не имеет ничего общего с Улицей Сезам).
    Часто проводится аналогия между DN и абсолютным именем пути файловой системы, но DN гораздо больше похож на почтовый адрес, т. к. он тоже записывается в порядке от частного к общему. Почтовый адрес
    Pat Hinds
    288 St. Bucky Avenue
    Anywhere, MA 02104
    USA
    начинается с частного (человек) и заканчивается самым общим компонентом (страна). То же имеет место и в DN. В примерах DN такой порядок хорошо заметен.
    Вершина дерева каталогов называется суффиксом каталога, т. к. это последняя составляющая каждого отличительного имени из данного дерева каталогов. При создании иерархической инфраструктуры с использованием нескольких делегированных LDAP-серверов суффиксы очень важны. Применяя концепцию LDAPvS, известную под названием referral, можно добавить элемент в дерево каталогов, в котором говорится: «За всеми элементами с этим суффиксом обращайтесь к тому серверу». Направления указываются при помощи LDAP URL, которые очень похожи на обычные URL. Единственное их отличие - это ссылка на определенное имя или другую информацию, имеющую отношение к LDAP. Вот пример такой ссылки из RFC2255, определяющего формат LDAP URL:
    ldap://lcap. ltd. unich. edJ/o=U-nversity%20o*%20Micr:3an.

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

    Два ключевых термина XML

    Два ключевых термина XML

    Не изучив эти два важных термина, нельзя «далеко уйти» в XML. Говорят, что XML-данные корректны (well-formed), если они имеют верный XML-синтаксис и следуют правилам грамматики (соответствие тегов и т. д.). Часто проверка документа на корректность позволяет выявить опечатки в XML-файлах. Это уже преимущество, если данные, с которыми вы имеете дело, содержат конфигурационную информацию, как в случае с базой данных имеющихся машин, отрывок из
    которой приведен выше.
    Говорят, что XML-данные действительны (valid), если они удовлетворяют правилам, установленным одним из механизмов определения данных. Например, если ваши данные удовлетворяют DTD, то это действительные XML-данные.
    Действительные данные по определению корректны, но обратное верно не всегда. Вполне могут существовать корректные данные без ассоциированной с ними DTD или схемы. Если такие данные верно анализируются, то они корректны, но не действительны.


    Пережитки

    Пережитки

    Вот три термина, встречающиеся в литературе по XML, которые могут запутать новичка:
    Attribute (атрибут)
    Это описание элемента, являющееся частью открывающего тега.
    Если воспользоваться предыдущим примером, то в атрибутом будет src="picture. jpg". О том, когда применять содержимое элемента, а когда атрибуты, в мире XML ведутся споры. Самые лучшие указания именно по этой теме можно найти на http://www.oasis-open.org/cover/elementsAndAttrs.html.
    СDАТА
    Термин CDATA (Character Data, символьные данные) используется в двух смыслах. В первом он чаще всего относится ко всему в XML-документе, что не является разметкой (тегами и т. д.). А во втором подразумевает понятие разделов CDATA (sections). Объявление раздела CDATA указывает, что анализатор XML не должен рассматривать данный раздел, даже если он содержит текст, который может быть построен как разметка.
    PCDATA
    В аннотации Тима Брея к спецификации XML (упомянутой ранее) приводится следующее определение:
    Строка PCDATA является сокращением от «Parsed Character Data». Это еще одно наследие SGML; «parsed» подразумевает, что процессор XML будет просматривать текст в поисках разметки, обозначаемой символами < и &.
    Можно считать их данными, состоящими из CDATA и, вероятно, некоторой разметки. Большая часть XML-данных попадает под это определение.
    В изучении XML имеются свои тонкости. Это маленькое руководство должно помочь вам приступить к работе.

    Восьмиминутное руководство по XML

    Восьмиминутное руководство по XML

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


    XML это язык разметки

    XML - это язык разметки

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

    quiooi
    129A < ooti>
    Har Pottiv


    XML требователен Несмотря на гибкость

    XML требователен Несмотря на гибкость, XML порой более требователен, чем HTML. В нем существуют правила, которым должны подчиняться данные. Довольно сжато они описаны в спецификации XML, которую можно найти на http://www.w3.org/TR/1998/REC-xml-19980210. Я советую обратиться к какой-либо из аннотированных версий, подобных версии Тима Брея (Tim Bray) с http://www.xml.com или книге Роберта Дюшарма (Robert Ducharme) «XML: The Annotated Specification» (XML: Аннотированная спецификация) (Prentice Hall), вместо того чтобы изучать официальную спецификацию. Первая свободно доступна в «онлайне», а во второй приведено много примеров XML-кода.

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

    либо так:

    Слэш в конце последнего тега сообщает XML-анализатору, что он является одновременно и открывающим, и закрывающим тегом. Данные и окружающие их открывающий и закрывающий теги называются элементом (element).
    2. Открывающие и закрывающие теги должны в точности соответствовать друг другу. Нельзя изменять их регистр. Если используется открывающий тег , то закрывающим должен быть и не тег с любой другой комбинацией регистров. В этом отношении HTML гораздо более снисходителен.
    Это два основных правила из спецификации XML. Но иногда автор определяет собственные правила, которым должен подчиняться анализатор XML. Под «подчиняться» следует подразумевать «выдавать предупреждения» или «останавливать анализ» при чтении XML-данных.
    Если использовать в качестве примера предыдущее определение машины в базе данных, то можно ввести дополнительное правило: «Все элементы должны содержать элементы <папе> и Эти правила определяются менее очевидным образом, чем все остальное, что будет рассмотрено, т. к. в настоящее время существует несколько конкурирующих и дополняющих друг друга предложений
    для определения «языка». В конце концов, XML станет самоопределяемым (т. е. структуру документа будет описывать либо сам документ, либо нечто с ним связанное).
    В текущей спецификации XML используется DTD (Document Type Definition, определение типа документа), основа SGML. Вот небольшой пример кода из спецификации XML, в котором определение типа находится в начале самого документа:

    ]>
    Hello. world!
    В первой строке примера задается версия XML и применяемая кодировка для документа (в данном случае это Unicode). Следующие три строки определяют типы данных из этого документа. В последней строке приводится сам документ (элемент ).
    При желании определить способ, с помощью которого подтверждалась бы правильность кода в первом примере из начала приложения, следовало бы добавить в начало файла нечто подобное:





    ]>
    Это определение требует, чтобы данные, соответствующие машине, состояли бы из элементов name, department, room, owner и ipaddress (именно в таком порядке). Каждый из этих элементов описывается как PCDATA (см. раздел «Пережитки» в конце данного приложения).
    В другом популярном предложении, которое пока не является спецификацией, рекомендовано в DTD-подобных целях использовать описания данных под названием схемы (schemas). Сами схемы пишутся на XML. Вот пример кода схемы, использующей реализацию от Microsoft с http://www.w3.org/TR/1998/ NOTE-XML-data/:
    <9XML version '1.0 ?> x'iHrs="i;r"r' schemtis-niicrcsof!












    Технология схем XML до сих пор (на момент написания книги) находится в стадии обсуждения. XML-данные, которые использовались в приведенном выше примере, являются всего лишь одним из предложений, рассматриваемым группой Working Group. Поскольку эта технология очень быстро развивается, я советую следить за текущими стандартами (их можно найти на http://www.w3.org) и за тем, насколько совместимо с ними ваше программное обеспечение.
    Как и вполне зрелый механизм DTD, так и новый механизм схем очень быстро могут стать запутанными, поэтому оставим дальнейшие дискуссии книгам, посвященным XML/SGML.


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

    Добавление данных в таблицу

    Добавление данных в таблицу

    Теперь у нас есть пустая таблица; так что рассмотрим два способа добавления в нее данных. Вот первый способ:
    USE sysadm INSERT hosts VALUES (
    'shimmer',
    '192.168.1.11',
    'shim shimmy shimmydoodles',
    'David Davis',
    'Software',
    'Main',
    '309',
    'Sun',
    'UItra60' )
    В первой строке мы сообщаем серверу, что собираемся работать с объектами из базы данных sysadm. Во второй строке выбирается таблица hosts и в нее добавляются строки - по одному полю за один раз.
    Такой вариант команды INSERT добавляет в таблицу всю строку целиком (ту, в которой определены все поля). Чтобы добавить строку только частично, можно указать, какие поля следует дописать, например, так:
    USE sysadm
    INSERT hosts (name.ioaddr.owner) VALUES ( 'bendir', '192.168.1.3', 'Cindv Coltrane )
    Команда INSERT завершится с ошибкой, если попытаться добавить строку, в которой определены не все обязательные (NOT NULL) поля.
    INSERT можно использовать и для добавления данных из одной таблицы в другую, такое применение будет рассмотрено позже. Во всех остальных примерах будем считать, что таблица hosts заполнена до конца при помощи команды INSERT в первой форме.


    Добавление результатов запроса в другую таблицу

    Добавление результатов запроса в другую таблицу

    На некоторых SQL-серверах можно на лету создать новую таблицу, содержащую результаты запроса, при помощи ключевого слова INTO:
    USE sysadm
    SELECT name,ipaddr INTO itmachines FROM hosts WHERE deot = IT'
    Этот оператор работает так же, как и предыдущие, за одним исключением: результаты данного запроса помещаются в таблицу под названием itmachines. На некоторых серверах такая таблица создается на лету в случае, если она еще не существует. Этот оператор можно считать эквивалентом оператора «>» в большинстве систем Unix и командных интерпретаторах NT.
    Отдельные серверы баз данных (как MySQL) не поддерживают оператор SELECT INTO; для выполнения этого действия в них нужно применять команду INSERT. Другие серверы, например MS-SQL и Sybase, требуют установки специального флага на базу данных для использования SELECT INTO, иначе команда завершится с ошибкой.

    Дополнительные аспекты SQL

    Дополнительные аспекты SQL

    Перед тем как завершить это руководство, нужно упомянуть о более «продвинутых» темах из SQL, с которыми вы можете столкнуться.
    Представления
    Некоторые SQL-серверы позволяют создавать различные представления (views) таблицы, которые похожи на волшебные постоянные запросы SELECT. Когда вы создаете представление при помощи специального запроса SELECT, результаты запроса не исчезают и ведут себя как отдельная таблица. Как ко всякой таблице, к ним можно посылать запросы. Изменения представлений с некоторыми ограничениями передаются оригинальной таблице или таблицам.
    Заметьте, я сказал таблицам. Именно тут проявляется волшебство представлений: можно создать представление таблицы, состоящее из объединения этой и другой таблицы. Такое представление ведет себя как одна большая виртуальная таблица. Изменения представления передаются обратно таблицам, участвующим в объединении, создающем представление.
    Можно также создать представление, новое поле которого будет состоять из результатов вычислений между другими полями этой таблицы, почти как в электронных таблицах. Представления полезны и для более обычных целей, например, для упрощения запросов (т. е. можно будет выбрать меньшее количество полей) и реструктуризации дан ных (т. е. представление данных остается первоначальным, даже если меняются поля в структуре таблицы).
    Вот как создать представление, используемое для упрощения запросов:
    USE sysadm
    CREATE VIEW ipddr_vie/,- AS SELEC''" nde. loaddr FROM hosts
    Теперь можно применить очень простой запрос, чтобы получить только ту информацию, которая нам нужна:
    USE sysadm
    SELECT FROM maddr view
    А вот результат запроса:
    name ipaddr
    shimmer 192.168.1.11
    bendir 192.168,1.3
    sander 192.168. 1 55
    Sulawesi 192 168,1,12
    Как и таблицы, представления можно удалять, используя раоновидность команды DROP:
    USE sysadm
    DROP VIEW ipaddr_view

    Хранимые процедуры

    Хранимые процедуры

    Большинство систем баз данных позволяют загружать на сервер SQL код, где он хранится в оптимизированном, проанализированном виде для быстрого выполнения. Такой код называется хранимыми процедурами (stored procedures). Хранимые процедуры часто являются важ
    ным компонентом SQL для администраторов, поскольку на них основана большая доля управления сервером. Например, чтобы изменить владельца базы данных sysadm в Sybase, нужно выполнить следующее:
    USE sysadm sp_changedbowner "jay"
    Примеры вызова хранимых процедур можно найти в главе 7. Теперь, когда вы знакомы с основами SQL, можно браться и за нее.

    Изменение информации в таблице

    Установление связей между таблицами

    Реляционные базы данных предлагают множество способов установить связи между данными из двух или более таблиц. Этот процесс называется объединением («joining») таблиц. Объединения очень быстро могут стать сложными, учитывая количество используемых запросов и точный контроль, который программист имеет над возвращаемыми данными. Тем, кого интересуют такие детали, лучше заглянуть в книгу по SQL.
    Рассмотрим один пример объединения. Здесь будет использоваться таблица под названием contracts, в которой содержится информация о гарантиях на каждую машину (табл. D.3).


    Курсоры

    Курсоры

    До сих пор сервер возвращал все результаты запроса по его завершении. Иногда бывает предпочтительнее получать ответ построчно. Чаще всего это справедливо при встраивании SQL-запросов в другие программы. Если запрос возвращает десятки тысяч строк, очень велика вероятность, что вам захочется обработать результаты построчно, а не хранить все в памяти для дальнейшего использования. Этот метод применяется в большинстве случаев, когда необходимо обращаться к SQL из Perl. Вот маленькая программа на SQL, в которой показано употребление курсоров на сервере Sybase или MS-SQL:
    USE sysadm
    -- объявляем переменные DECLARE (ahostname character(30) DECLARE @ip character(15)
    -- объявляем курсор
    DECLARE hosts_curs CURSOR FOR SELECT name,ipaddr FROM hosts
    -- открываем курсор OPEN hosts_curs
    -- обходим в цикле таблицу, получая по одной строке за один раз -- до тех пор, пока не получим ошибку FETCH hosts_curs INTO shostname,gip WHILE (@@fetch_status = 0) BEGIN
    PRINT "----"
    PRINT (Briostriame
    PRINT @ip
    FETCH hosts_curs INTO Shostname.^iD END -- закрываем курсор (это не обязательно если -- далее следует DEALLOCATE) CLOSE hosts._curs
    -- снимаем определение (unde'ine) курсора DEALLOCATE hosts_curs
    В результате получается следующее:
    shimmer 192.163.1.11
    bendir 192.168.1.3
    sander 192.168.1.55
    Sulawesi 192.168.1.12

    Пятнадцатиминутное руководство

    Таблица D.I. База данных машин

    name ipaddr aliases owner dept bldg room manuf model
    shimmer 192.168.
    1.11
    shim
    shimmy
    shimmy-
    doodles
    David
    Davis
    soft-
    ware
    main 309 Sun Ultra60
    bendir 192.168.
    1.3
    ben ben-
    doodles
    Cindy
    Col-
    trane
    IT west 143 Apple 7500/
    100
    sander 192.168.
    1.55
    sandy
    micky
    micky-
    doo
    Alex
    Rol-
    lins
    IT main 1101 Inter-
    graph
    TD-325
    Sulawesi 192.168.
    1.12
    sula
    sulee
    Ellen
    Monk
    design main 1116 Apple G3


    Получение подмножества строк из таблицы

    Получение подмножества строк из таблицы

    Базы данных не были бы такими интересными, если бы из них нельзя было получить некое подмножество данных. В SQL употребляется команда SELECT, в которую добавлено ключевое слово WHERE для определения условия:
    USE sysadm
    SELECT - FROM hosts WHERE blog = "Ma:r!'
    В результате получаем:
    name ipaddr aliases owner .\i--.r.
    bldg room manuf model
    shimmer 192.168.1,11 shim shimmy shimmydoodles David Cav.s but",-, -.-
    Main 309 Sun Ultra60
    sander 192.168 1.55 sandy iricky mickydoo Alex РоП.гь I"
    Main 1101 Intergraph TD-325
    Sulawesi 192.168.1.12 sula su-lee Ell"' Monk Се
    Main 1116 Apple G3
    С ключевым словом WHERE можно использовать стандартные условные операторы, применяемые в программировании:
    =>>=<<=<>
    В отличие от Perl, в SQL нет отдельных операторов для сравнения строк и чисел.
    Условные операторы можно объединять посредством AND/OR и отрицать при помощи NOT. Проверить, является ли поле пустым, позволяет оператор IS NULL, а проверить обратное - IS NOT NULL. Например, этот фрагмент SQL-кода выведет список машин, для которых в таблице не указаны владельцы:
    USE sysadm
    SELECT name FROM hosts WHERE owner IS NULL
    Если требуется найти все строки, в которых значения некоторого поля равны одному из указанных, можно использовать оператор IN для задания списка:
    USE sysadm
    SELECT name FROM nosts WHERE dept IN ('IT', 'Software')
    Ответом будет список машин из отделов «IT» и «Software». SQL также позволяет получить строки, совпадающие с диапазоном значений (полезнее всего это применять с численными данными и датами), при помощи оператора BETWEEN. Вот пример запроса, возвращающий список машин, находящихся в основном здании на десятом этаже:
    USE sysadm
    SELECT name FROM hosts WHERE (bldg = 'Main') AND
    (room BETWEEN '1000' AND '1999')
    Наконец, ключевое слово WHERE можно использовать с LIKE для выбора строк при помощи слабого механизма соответствия шаблону (слабого в сравнении с регулярными выражениями в Perl). Например, следующий запрос выбирает все машины, в псевдонимах которых встречается строка «doodles»:
    USE sysadm
    SELECT name FROM nosts WHERE aliases LIKE '%dooales%'
    Обратите внимание, какие метасимволы поддерживаются (табл. D.2).


    Простая обработка данных возвращаемых в результате запросов

    Простая обработка данных, возвращаемых в результате запросов

    У оператора SELECT существуют два полезных ключевых слова: DJSTINC1 и ORDER BY. Первое позволяет изъять из запроса повторяющиеся записи. При желании получить список всех различных производителей, представленных в таблице hosts, можно было бы использовать DISTINCT:
    USE sysadm
    SELECT DISTINCT manuf FROM hosts
    При необходимости получить отсортированные данные, можно было бы применить ORDER BY:
    USE sysadm
    SELECT name.ipaddr.dept,owner FROM hosts ORDER BY aept
    SQL имеет несколько операторов, преобразующих данные, возвращаемые в результате запроса. Такая возможность позволяет изменять имена полей, выполнять итоговые вычисления и вычисления внутри и между полями, изменять формат выводимых полей, осуществлять подзапросы и совершать множество иных действий. За информацией о различных ключевых словах, используемых с SELECT, обратитесь к литературе, посвященной SQL.

    Создание/удаление баз данных и таблиц

    Создание/удаление баз данных и таблиц

    В самом начале сервер пуст и в нем нет объектов, которые могут быть нам полезны. Давайте построим свою базу данных:
    CREATE DATABASE sysadm ON userdev=10 LOG ON userlog=5 GO
    Данная команда создает 10-мегабайтную базу данных на устройстве jserdev с 5-мегабайтным файлом журнала на устройстве userlog. Эта команда специфична для серверов Sybase/Microsoft SQL Server, т. к. создание баз данных (если оно вообще выполняется) на разных серверах производится поразному.
    Команда GO применяется с интерактивными клиентами баз данных и служит указанием на то, что необходимо выполнить предыдущую команду. Но это не SQL-оператор. В следующих примерах будем считать, что команду GO необходимо выполнять после каждого SQL-оператора, если используется один из таких клиентов. Кроме того, комментарии в SQL будут обозначаться при помощи «- -».
    Чтобы удалить эту базу данных, необходимо выполнить команду DROP:
    DROP DATABASE sysadm
    Теперь создадим пустую таблицу, в которой будет храниться информация из табл. D.I.
    USE sysadm
    -- Последнее напоминание: перед тем как выполнить следующую
    -- команду, необходимо набрать GO (если вы используете
    -- интерактивный клиент)
    CREATE TABLE hosts (
    name characterise) NOT NULL,
    ipaddr character(15) NOT NULL,
    aliases character(SO) NULL,
    owner character(40) NULL,
    dept character(15) NULL,
    bldg character(IO) NULL,
    room character(4) NULL,
    manuf character(IO) NULL,
    model

    ) character(IO) NULL
    Сначала мы указываем, какая база данных (sysadm) будет использоваться. Оператор USE оказывает действие только в том случае, когда он выполняется отдельно до запуска других команд, поэтому ему нужен собственный оператор GO.
    Затем мы создаем таблицу, указывая ее имя, тип и длину данных, а также настройки NULL/NOT NULL для каждого столбца. Теперь немного поговорим о типах данных.
    В таблице можно хранить различные типы данных, включая числа, даты, текст и даже изображения и другие двоичные данные. Столбцы в ней создаются для хранения данных определенного типа. У нас
    скромные потребности, поэтому таблица состоит из столбцов, хранящих простые строки characters (символов). В SQL можно строить определяемые пользователями псевдонимы типов данных, такие как ip_address или employeeid. Определяемые пользователями типы данных применяются при создании таблиц для поддержки читаемости ее структуры, а также для форматов данных, которые не должны меняться от столбца к столбцу в разных таблицах.
    Последний набор параметров в предыдущей команде определяет, обязательным ли является поле. Если параметр равен NOT NULL, то новую строку нельзя добавить в случае, если в этом поле у нее нет данных. В нашем примере важны имя машины и ее IP-адрес, так что следует объявить эти поля как NOT NULL. Все остальные поля необязательны (хоть и желательны). Помимо NULL/NOT NULL существуют и другие ограничения, накладываемые на поля для согласованности данных. Например, чтобы убедиться, что две машины не называются одинаково, можно изменить строку
    name character(30) NOT NULL
    на:
    name character(30) NOT NULL CONSTRAINT unique_name UNIQUE
    Это ограничение мы называем unique_name. Введенные названия позволяют получать более осмысленные сообщения об ошибках, которые выдаются при нарушении ограничений. Изучите документацию вашего сервера, чтобы выяснить, какие еще ограничения можно применять к таблицам.
    Удалить таблицу из базы данных значительно проще, чем создать ее:
    USE sysadm
    DROP TABLE hosts

    D 2 Метасимволы LIKE

    Таблица D.2. Метасимволы LIKE

    Метасимвол Значение Ближайший эквивалент из
    регулярных выражений Perl
    % Ноль или более символов .*
    - Один символ .
    [] Один символ из указанного списка или диапазона []
    В некоторых серверах баз данных добавлены расширения к SQL, позволяющие применять регулярные выражения в операторах SELECT. Например, в MySQL существует оператор REGEXP, который можно использовать с SELECT. REGEXP не обладает всей силой регулярных выражений Perl, но он значительно увеличивает их гибкость по сравнению со стандартными метасимволами SQL.

    D 3 Таблица Contracts

    Таблица D.3. Таблица Contracts

    name servicevendor start date enddate
    bendir Dec 09-09-1995 06-01-1998
    sander Intergraph 03-14-1998 03-14-1999
    shimmer Sun 12-12-1998 12-12-2000
    Sulawesi Apple 11-01-1995 11-01-1998
    Вот один из способов установить отношение между таблицей hosts и таблицей contracts при помощи объединения:
    SELECT name.servicevendor,enddato FROM r,nr\ -rifts, hosts WHERE contracts, name = nosn. r;vne
    Проще всего понять этот код, если начать читать его с середины. Условие FROM contacts, '"osrs говорит серверу о том, что связь устанавливается между таблицами contracts и hosts. Условие WHERE hosts, name сообщает, что мы ищем совпадения между строками из таблиц contracts и hosts, основываясь на содержимом поля из каждой таблицы. Наконец, строка SELECT, определяет поля, которые мы хотим включить в получаемые данные.


    Запрос информации

    Запрос информации

    Являясь системным администратором, чаще всего вы будете применять SQL-команду SELECT, которая используется для получения информации с сервера. Перед тем как говорить об этой команде, нужно заметить, что SELECT - это пропуск в мир SQL. Мы покажем только самые простые формы данной команды. Умение создавать хорошие запросы (и умение проектировать базы данных, к которым легко такие запросы строить) - это искусство, и более подробно этот вопрос рассматривается в книгах, целиком посвященных SQL и базам данных.
    В самой простой форме SELECT служит для получения информации о сервере и соединении. В этом случае не нужно определять источник данных. Вот два примера:
    -- оба зависят от производителя базы данных SELECT ©sfiERVERNAME SELECT VERSIONO.
    Первый оператор возвращает имя сервера для Sybase или MS-SQL; второй - текущую версию сервера MySQL.
    Получение всех записей из таблицы
    Для получения всех данных из таблицы hosts применяется такой SQL код:
    USE sysach
    SE! ЕС Г - FROM
    В результате возвращаются все строки и поля, причем поля следуют в той последовательности, в которой они определялись при создании базы данных:
    name icacdr aliases
    bldg roo"i Tianuf r.odel
    shimmer 192.168.1.11 snm shimmy sh:rnmydood.es David Dav:
    Main 309 Sun UTfra60
    bendir 192.168.1.3 Den bendoodles Cindy Co;
    West 143 Apple 7500/100
    sander 192.168.1.55 sandy micky mickydoo Alex Rollins IT
    Main 1101 Intergraph TD-325
    Sulawesi 192.168.1.12 sula su-lee Ellen Monk Design
    Main 1116 Apple G3
    Если нужно получить только определенные поля, следует явно указать их имена:
    USE sysadm
    SELECT name,ipaddr FROM hosts
    Когда мы определяем поля по имени, они возвращаются в той последовательности, в которой указывались, независимо от порядка, используемого при создании таблицы. Например, для получения связи IP-адресов со зданиями можно применить следующую команду:
    USE sysadm
    SELECT bldg.ipaddr FROM hosts
    В результате получим:
    bldg ipaddr
    Main 192.168.1.11
    West 192.168.1,3
    Main 192.168,1.55
    Main 192.168.1.12

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

    Таблица Е.1. Поддеревья узла internet( 1)

    Поддерево Описание
    Directory(1 ) Каталог OSI
    mgmt(2) Стандартные объекты RFC
    Experimental^) Эксперименты с Internet
    Private(4) Зависит от производителя
    Security(S) Безопасность
    snmpV2(6) Описание работы SNMP
    Поскольку нас интересует применение SNMP для управления устройствами, мы обратимся к ветви mgmt(2). Первый узел под mgmt(2) - сам MIB (это почти рекурсия). А раз существует только один MIB, то единственный узел под mgmt(2) - это mib-2(1).
    По-настоящему MIB начинается с этого уровня в дереве. Мы ищем первую группу ветвей, называемых группами объектов, где хранятся переменные, к которым хотим обратиться:
    system(1)
    interfaces(2)
    at(3)
    ip(4)
    icmp(5)
    tcp(6)
    udp(7)
    egp(8)
    cmot(9)
    transmission(10)
    snmp(11)
    Раз мы ищем «системное описание» SNMP-переменной, будет логично посмотреть в группу system( 1). Первый узел в этом дереве - sysDescг (1). Мы нашли то, что искали.
    Зачем связываться с обходом деревьев? В результате такого путешествия можно получить идентификатор объекта sysDescr(1). Идентификатор объекта, или OID, - это всего лишь набор чисел, разделенных точками, с каждого уровня дерева, которые встречались на пути к объекту. Вот как это выглядит в графическом виде.
    Поскольку OID для дерева Интернета - это 1. 3. 6.1, то OID для системной группы объектов- 1.3.6.1.2.1.1, a OID для объекта sysDescr-1.3.6.1.2.1.1.1.
    Если нужно воспользоваться таким идентификатором на практике, то для получения значения этой переменной следует добавить к идентификатору еще одно число. Нам нужно добавить 0, что соответствует
    первому (и единственному, т. к. устройство не может иметь более одного описания) экземпляру этого объекта.
    Что ж, давайте так и поступим; воспользуемся этим идентификатором, рассматривая SNMP в действии. В этом приложении будут применяться в демонстрационных целях инструменты из пакета UCD-SNMP. Па-
    кет UCD-SNMP, который можно найти на http://ucd-snmp.ucdavis.edu/, является отличной бесплатной реализацией SNMPvl и SNMPvS. Мы используем именно эту реализацию SNMP, т. к. один из модулей Perl
    связан с его библиотекой, но и все остальные клиенты, которые могут посылать SNMP-запросы, действуют практически так же. После знакомства с SNMP-утилитами командной строки читателю будет легко переходить на эквиваленты в Perl.
    Инструменты командной строки из UCD-SNMP требуют ставить первой точку в ОШ/имени переменной, если оно начинается от вершины дерева. В противном случае считается, что OID/имя переменной начинается с вершины дерева mib-2. Вот два способа получить системное описание для машины solarisbox:
    $ snmpget solarisbox public .1.3.6.1.2.1.1.1.0 $ snmpget solarisbox public .iso.6rg.dod.internet.mgmt.mib-2.sys- tem. sysDescr. 0
    Обе эти строки позволяют получить одно и то же:
    system.sysDescr.О = Sun SNMP Agent, Ultra-1
    Но вернемся к теории. Очень важно помнить, что «Р» в SNMP означает протокол (Protocol). Сам SNMP - это всего лишь протокол для взаимодействия между элементами в управляющей инфраструктуре.
    Операции, или единицы данных протокола (Protocol Data Units, PDU), считаются простыми (simple). Вот какие единицы PDU встречаются чаще всего, особенно при программировании на Perl:
    get-request
    get-request- «рабочая лошадка» в семействе PDU. Посредством get-request опрашивают SNMP-объект, для того чтобы получить значение SNMP-переменной. Очень многие при работе с SNMP никогда не используют ничего, кроме этой операции.
    get-next-request
    get-next-request очень похож на get-request, только он возвращает элемент из MIB, который находится после указанного («первый лексикографический наследник» (first lexicographic successor) в
    терминах RFC). Эта операция бывает полезна, если необходимо найти все элементы из логической таблицы. Например, можно послать несколько последовательных запросов get-next-requests для
    обращения к каждой строке из ARP-таблицы рабочей станции.
    Очень скоро будет приведен пример этой операции в действии.
    set-request
    set-request делает именно то, что можно ожидать; он пытается изменить значение SNMP-переменной. Эта операция служит для изменения конфигурации SNMP-совместимых устройств.
    trap/snmpV2-trap
    trap - это имя SNMPvl, a snmpV2-trap - имя из SNMPv2/3. Обсуждение прерываний лежит за рамками данной книги, но, коротко говоря, они позволяют получить информацию о событиях (перезагрузка или достижение границы счетчиком) с SNMP-совместимой машины без явного запроса подобной информации.
    response
    response - это операция, применяемая для передачи ответа любой из операций. Ее можно использовать для ответа на get-request, для сообщения о том, успешно ли завершилась операция set-request и т. д.
    Вам редко придется явно применять эту операцию при программировании, т. к. большая часть SNMP-библиотек, программ и Perl-модулей обрабатывают ответы автоматически. Тем не менее, очень
    важно понимать не только правила построения запросов, но и особенности генерирования ответов.
    Если читатель никогда раньше не имел дела с SNMP, естественная реакция на приведенный выше список была бы такой: «Что это? Получить, установить, сказать о том, что что-то произошло, и это все?» Но
    простой по замыслу создателей SNMP отнюдь не противоположность мощному. Если производитель SNMP-устройства правильно выберет переменные, при помощи протокола можно будет сделать практически все. Классический пример из RFC - это перезагрузка SNMP-совместимого устройства. Специальных операций для «запроса на перезагрузку» может и не существовать, но производитель сумеет легко реализовать эту операцию, используя триггерную переменную SNMP для хранения количества секунд, прошедших до загрузки. Когда эта переменная изменяется через set-request, перезагрузку устройства можно выполнить в указанное время.
    Но если доступны такие возможности, то что предпринимается в плане безопасности, чтобы любой, у кого есть SNMP-клиент, не смог бы перегрузить вашу машину? В более ранних версиях протокола меха-
    низм защиты был очень слаб. На самом деле, некоторые даже расшифровывали аббревиатуру SNMP как «Security Not My Problem» (Безопасность — не моя проблема) из-за слабого механизма аутентификации
    в SNMPvl. Для того чтобы разобраться в тонкостях механизма защиты, т. е. понять, кто, что и как, нужно разобраться с терминологией, так что потерпите.
    SNMPvl и SNMPv2C позволяют определить административные отношения между SNMP-объектами, известными как сообщества (соттиnltles). Сообщества - это способ группировки SNMP-агентов со сходными ограничениями на доступ с управляющими объектами, которые удовлетворяют этим ограничениям. Все объекты из сообщества имеют одно и то же имя сообщества. Для доказательства того, что вы являетесь членом сообщества, необходимо знать только имя сообщества.
    Это и есть составляющая «кто имеет доступ?»
    Что касается составляющей «к чему у них есть доступ?», в RFC1157 части MIB, имеющие отношение к определенному элементу сети, называются SNMP MIB view. Например, SNMP-совместимый тостер не
    будет поддерживать те же конфигурационные переменные SNMP, что и SNMP-совместимый маршрутизатор.
    Каждый объект в М1В определен как read-on Ly (доступный только для чтения), read-write (доступный для чтения/записи) или попе (нет доступа). Это называется режимом SNMP-доступа (SNMP access mode)
    для объекта. Если объединить SNMP MIB view и режим SNMP-доступа, будет получен профиль сообщества SNMP (SNMP community pro file), описывающий тип доступа, предоставленный переменным в MIB определенным сообществом.
    Теперь, объединив части кто и что, мы получим политику доступа SNMP (SNMP access policy), описывающую, какие типы доступа предлагают друг другу члены определенного сообщества.
    Как все это работает в реальной жизни? Вы настраиваете маршрутизатор или рабочую станцию так, чтобы они входили как минимум в два сообщества, одно из которых контролирует доступ для чтения, а другое - доступ для чтения и записи. Часто их называют сообществами public и private, в соответствии с популярными именами по умолчанию для этих сообществ. Например, можно добавить это как часть
    конфигурационной информации для маршрутизатора Cisco:
    ! устанавливаем имя доступного только для чтения сообщества в
    MyPublicCommunityName
    snmp-server community MyPublicCommunityName RO
    ! устанавливаем имя доступного для чтения и записи сообщества в
    MyPrivateCommunityName
    snmp-server community MyPrivateCommunityName RW
    В Solaris можно было бы добавить эти строчки в файл /etc/snmp/conf/snmpd.conf:
    read-community MyPublicCommunityName write-community MyPrivateCommunityName
    SNMP-запросы к любому из этих устройств будут использовать имя сообщества MyPublicCommunityName для получения доступа к переменным, доступным только для чтения, и MyPrivateComiiunityName для изменения переменных, доступных для чтения и записи, на этих устройствах.
    Имя сообщества затем выступает в качестве псевдопароля для получения SNMP-доступа к устройству. Это довольно убогая схема безопасности. Имя сообщества не только передается открытым текстом в каждом SNMP-пакете, но оно также пытается защитить доступ посредством механизма «безопасность через скрытность».
    В более поздних версиях SNMP, в частности в версии 3, безопасность протокола была значительно улучшена. В RFC2274 и RFC2275 определяется пользовательская модель безопасности (User Security Model, USM) и модель управления доступом «View-Based» (View-Based Access Control Model, VACM). USM предоставляет криптографическую защиту аутентификации и шифрует сообщения. VACM предлагает исчерпывающий механизм управления доступом для объектов MIB. Эти механизмы пока еще являются относительно новыми и нереализованными (например, только один из Perl-модулей поддерживает их, но и такая поддержка появилась совсем недавно). Мы не будем обсуждать здесь эти механизмы, но вам, вероятно, стоит внимательно изучить RFC, т. к. популярность SNMPvS увеличивается.
    SNMP на практике
    Теперь, когда вы получили изрядную дозу теории SNMP, применим эти знания на практике. Вы уже знаете, как запросить системное описание для машины (помните краткое введение, приведенное ранее).
    Рассмотрим еще два примера: запрос времени непрерывной работы системы и таблицы маршрутизации IP. До сих пор вы полагались только на мои слова при поиске местоположения и имени SNMP-переменной в MIB. Это нужно изменить, поэтому первый шаг при запросе информации через SNMP - это процесс,
    который я называю «MIB groveling» («раболепство перед MIB»):
    Шаг1
    Найти нужный документ MIB. Если вы ищете независимые от устройства настройки, которые можно встретить на любом SNMP-устройстве, скорее всего, они найдутся в RFC1213. Если вам нужны имена переменных, зависящих от производителя, например, переменной, в которой хранится «цвет мерцающей лампочки на передней панели конкретного ATM-коммутатора», то следует обратиться к производителю коммутатора и запросить копию их модуля MIB (MIB module). Я педантично отношусь к применяемым терминам, потому что нередко можно услышать, как люди неверно говорят: «Мне нужна база MIB для этого устройства». В мире существует только одна база MIB; все остальное находится где-то в ее структуре (обычно где-то под ветвью private(4)).
    Шаг 2
    Найти в описаниях MIB нужную вам SNMP-переменную. Чтобы упростить второй шаг, разберемся с форматом.
    Описания MIB совсем не так страшны, когда вы привыкаете к ним. Они выглядят как один длинный набор объявлений переменных, похожий на то, что можно найти в исходном коде. Это не совпадение, по-
    тому что они и есть объявления переменных. Если производитель ответственно подойдет к созданию своего модуля, то в нем будет достаточно комментариев, как и в любом хорошем исходном тексте.
    Информация MIB записывается в соответствии со стандартным соглашением OSI (Open Systems Interconnection, взаимодействие открытых систем) подмножества ASN.l (Abstract Syntax Notation One, язык для описания абстрактного синтаксиса данных). Описание этого подмножества и другие сведения об описании данных для SNMP можно найти в RFC под названием «Structure for Management Information» (SMI).
    Они дополняют RFC, определяющие протокол SNMP и текущую базу МШ. Например, самое последнее (на момент готовности рукописи этой книги) описание протокола SNMP можно найти в RFC1905, описание
    самой последней базы MIB, с которой работает протокол, - в RFC1907, a SMI для этой базы MIB - в RFC2578. Я обращаю на это ваше внимание, поскольку нередко приходится обращаться к разным документам при поиске определенного SNMP-объекта.
    Воспользуемся этими знаниями, чтобы решить первую задачу: выясним, используя SNMP, время непрерывной работы машины. Вопрос довольно общий, так что вероятность найти нужную SNMP-переменную в RFC1213 очень велика. Быстрый поиск «uptime» в RFC1213 позволяет получить такой отрывок из ASN.1:
    sysUpTime OBJECT-TYPE
    SYNTAX TirueTicks
    ACCESS read-only
    STATUS mandatory
    DESCRIPTION
    "The time (in hundredths of a second) since the network management portion of the system was last re-initialized."
    ::= { system 3 }
    Внимательно разберемся в этом определении:
    sysUpTime OBJECT-TYPE
    Эта строка определяет объект под названием sysUpTime.
    SYNTAX TimeTicks
    Это объект типа TimeTicks. Типы объектов определяются в SMI, о них упоминалось совсем недавно.
    ACCESS read-only
    Этот объект доступен только для чтения через SNMP (т. е. get-req-uest); его нельзя изменить (т. е. set-request).
    STATUS mandatory
    Данный объект должен быть реализован в любом SNMP-агенте.
    DESCRIPTION...
    Это текстовое описание объекта. Всегда внимательно читайте все, что написано в подобном поле. В этом определении нас ждет сюрприз. sysUpTime показывает только то время, которое прошло с момента последней инициализации части системы, имеющей отношение к управлению сетью («the network management portion of the system was last re-initialized»). Это означает, что мы можем узнать только время непрерывной работы с момента последнего запуска SNMP-агента. Почти всегда это время совпадает с временем непрерывной работы самой системы, так что если вы заметите отклонение, то причина, скорее всего, будет в этом.
    : := { system 3 }
    Эта строка определяет, где именно в дереве MIB хранится объект. Объект sysUpTime - это третья ветвь дерева системной группы объектов. Подобная информация также позволяет получить часть идентификатора объекта, которая понадобится позже.
    При желании запросить эту переменную с машины solarisbox в сообществе, доступном только для чтения, можно было бы использовать такую командную строку из UCD-SNMP:
    $ snmpget solarisbox MyPublicCommunityName system.sysUpTime.0
    В результате получим:
    system.sysUpTime.О = Timeticks: (5126167) 14:14:21.67
    To есть агент был последний раз инициализирован 14 часов назад.
    В примерах из этого приложения подразумевается, что SNMP-агенты настроены таким образом, что принимают запросы от запрашивающих узлов. В целом, если можно разрешить SNMP-доступ только с небольшого подмножества узлов, это следует сделать.
    «Нужно знать» - это отличный принцип безопасности, которому надо следовать. Хорошо было бы ограничить сетевые службы, предоставляемые каждой машиной или устройством. Если не нужно предоставлять сетевую службу, ее лучше отключить. Если служба необходима, ограничьте
    доступ к ней только теми устройствами, которым это «нужно знать».
    Пришло время привести второй, более совершенный SNMP-пример:
    сброс содержимого таблицы маршрутизации устройства. Сложность этого примера состоит в том, что надо обрабатывать группу скалярных данных как одну логическую таблицу. Для получения этих данных вы-
    полним операцию get-next-request. Наш первый шаг на пути к цели - найти описание MIB таблицы маршрутизации. Поиск по «route» в RFC1213, в конечном счете, выдает следующее определение:
    -- The IP rowing table contains an for each route
    -- prese"tiy k'lOvvn to this entity
    -- известного в настоящий момент этому элементу )
    ipRouteTable OBJECT-TYPE
    SYNTAX SEOUhNCt OF IpHouteEntry
    ACCESS not-accessible
    STATUS mandatory
    DESCRIPTION
    "This entity's IP R&.jting table."
    Это определение не слишком отличается от того, которое мы видели минутой раньше. Отличия есть только в строках ACCESS и SYNTAX. Строка ACCESS предупреждает, что этот объект является всего лишь структурной единицей, представляющей целую таблицу, а не настоящей переменной, к которой можно направить запрос. Строка SYNTAX говорит о том, что таблица состоит из целого ряда объектов IpRouteEntгу. Посмотрим на начало определения IpRouteEntry:
    ipRouteEntry OBJECT-TYPE
    SYNTAX IpRouteEntry
    ACCESS not-accessible
    STATUS mandatory
    DESCRIPTION
    "A route to a particular destination."
    INDEX { ipRouteDest }
    ::= { ipRouteTable 1 }
    Строка ACCESS говорит, что мы нашли еще один «заполнитель» (placeholder), на этот раз для каждой строки из таблицы. Но этот заполнитель может нам кое-что поведать. Можно обратиться к каждой строке,
    используя индексацию, т. е. объект ipRouteDest для каждой строки.
    Если эти несколько уровней определений вас утомляют, обратитесь за помощью к Perl. Будем считать, что мы имеем дело с хэшем списков в Perl. Ключом хэша для строки будет переменная ipRouteDest. Значени-
    ем этого хэша будет ссылка на список, содержащий другие элементы этой строки (т. е. остальную информацию о маршруте).
    Определение ipRouteEntry выглядит так:
    ipRouteEntry ::=
    SEQUENCE {
    ipRojteDest
    IpAddress
    ipRoutelfIndex
    INTEGER.
    ipRouteMetrid
    INTEGER.
    ipRouteMetric2
    INTEGER.
    ipRouteHetric3
    INTEGER
    ipRouteMetric4
    INTEGER,
    ]pRouluNextHop
    IpAddress.
    ipRoufnTypp
    INTEGER
    ipRouteProto
    INTEGER,
    ipRouteAge
    INTEGER,
    ipRouteMask
    IpAddress,
    ipRouteMetricS
    INTEGER,
    ipRoutelnfo
    OBJECT IDENTIFIER
    }
    Теперь вы видите элементы, составляющие каждую строку в таблице MIB продолжается описанием этих элементов. Вот два первых определения:
    ipRouteOesf OBJECT-TYPE
    SYNTAX IpAddress
    ACCESS "Gad-write
    STATUS mandatory
    DESCRIPTION
    "Tne destination IP address of this route. An
    entry with a value of 0.0.0.0 Is considered a
    default route. Multiple routes to a single
    destination can appear in the rabid, bu1; access to
    such multiple eniries is depende:ч о:'; the tabJe-
    access mechanisms defined by the network
    management protocol in use."
    : :-- ! ipRouteEntry 1
    }
    ipRoutelflndex OBJECT-TYPE
    SYNTAX INTEGER
    ACCESS read-write
    STATUS mandatory
    DESCRIPTION
    "The index value vvhich jnicueiy identifies tke
    local i.VLbrface th^ougn ,-;ricn the next r'cp jf t:ks
    interface as identified b« tre sa~:e value of
    ]fli;oex."
    Графическое представление ipRouteTable поможет вам разобраться с этой информацией.
    Если вы разобрались с этой частью MIB, то на следующем шаге следует запросить информацию. Данный процесс называется «обходом таблицы». В большинство SNMP-пакетов входит утилита командной строки, называемая либо snmptable, либо snmp-tbl, выполняющая такой обход, но утилиты могут и не предоставить нужной вам степени контроля. Например, возможно, нужна не вся таблица маршрутизации, а
    лишь список ipRouteNextHops. Кроме того, в некоторых SNMP-пакетах Perl просто нет подпрограмм для обхода деревьев. Так что имеет смысл знать, как выполнять все эти действия вручную.
    Чтобы было проще понять, как это делается, я приведу информацию, которую мы в конечном итоге собираемся получать от устройства. Это позволит вам увидеть, как каждый шаг добавляет в таблицу еще одну строку собираемых данных. Если зарегистрироваться на удаленной машине (вместо того чтобы использовать SNMP для удаленного запроса) и набрать netstat —nr для сброса таблицы маршрутизации, вывод может выглядеть так:
    default 192.168.1.1 UoS 0 215345 tu0
    127.0.0.1 127.0,0.1 UH 8 5404381 lo0
    192.168.1/24 192.168.1.189 U 15 9222638 tu0
    Это соответствует внутреннему интерфейсу обратной петли по умолчанию и локальным сетевым маршрутам.
    Теперь посмотрим, как получить часть этой информации через утилиты UCD-SNMP. В данном примере нас интересуют только первые два столбца данных (направление маршрута и адрес следующего узла).
    Мы выполняем первоначальный запрос к первому экземпляру этих двух переменных в таблице. Все, что выделено жирным шрифтом, - это одна длинная команда, и здесь она разбита на две строчки только
    для разборчивости:
    $ snmpgetnext computer public ip.IpRouteTable.ipRouteEntry.ipRouteDest
    Ip.ipRouteTable.ipRouteEntry.ipflouteNextHop
    ip.ipRouteTable.ipRouteEntry.ipRouteDest.0.0.0.0 = IpAddress: 0.0.0.0
    ip.ipRouteTable.ipRouteEntry.ipRouteNextHop.0.0.0,0 = IpAddress: 192.168.1.1
    Следует обратить внимание на две части подобного ответа. Первая - это данные, возвращаемые после знака равенства. 0.0.0.0 означает «маршрут по умолчанию», так что подобная информация соответствует первой строке таблицы маршрутизации из приведенного выше примера. Вторая важная часть ответа - это добавленные к именам переменных .0.0.0.0. Это индекс для элемента ipRouteEnrry, представляющего строку таблицы.
    Получив первую строку, можно выполнить еще один вызов get-next-request, на этот раз, используя индекс. Запрос get-next-request всегда возвращает следующий элемент из MIB, так что достаточно передать
    ему индекс строки, которую мы только что получили, для перехода к следующей после нее строке:
    $ snmpgetnext gold public ip.ipRouteTable. ipRouteEntry. ipRouteDest. 0.0.0.0
    ip.ipRouteTable.ipRouteEntry.ipRouteNextHop. 0. 0. 0. 0
    ip.ipRouteTable.ipRouteEntry.ipRou+eDest.127. 0.0. 1 = IpAddress: 127.0.0.1
    ip.ipRouteTable.ipRouteEntry.ipRouteNextHop.127.0. 0.1 = IpAddress: 127.0.0.1
    Вероятно, читатель уже догадался, каким будет следующий шаг. Мы используем еще один запрос get-next-equest при помощи фрагмента 127.0.1 (индекс) ответа ip. ipRouteTable. ipRouteEntry. ipRouteDest. 127. 0.0.1;
    $ snmpgetnext gold public ip.ipRouteTable. ipRouteEntry. ipRouteDest, 127.0.0.1
    ip.ipRouteTable.ipRouteEntry.ipRouteNextHop. 127.0.0.1
    ip. ipRouteTable. ipRouteEnfy. ioRouteDest 192,168.1 = IpAadress: 192.168 " "
    ip.ipRouteTable,ipRouteEntry.ipRouteNextHop.192,168.I1.0 = loAddress:
    192.168.1.139
    Если вы посмотрите на вывод команды netstat, приведенный выше, то увидите, что мы достигли своей цели и получили все строки из таблицы маршрутизации. Как бы мы узнали об этом, если бы по иронии судьбы у нас не было полученного ранее вывода команды netstat? В обычных обстоятельствах следовало действовать как обычно и послать запрос:
    $ snmpgetnext gold public
    ip.ipRouteTable.ipRouteEntry.ipfiouteDest.192.168.1.0
    ip.ipRouteTable.ipRouteEntry.ipRouteNextHop. 192.168.1.0
    ip.ipRouteTable.ipRouteEntry.ipRoutelflndex.0.0.0.0 = 1
    ip.ipRouteTable.ipRouteEntry.ipRouteType.0.0.0.0 = indirect(4)
    Стоп, ответ не совпадает с запросом! Ведь запрашивали ipRouteDest и ipRouteNextHop, а получили ipRoutelflndex и ipRouteType. Мы вышли за пределы таблицы ipRouteTable. SNMP-запрос get-next-request честно выполнил свой долг и вернул «первого лексикографического последователя» (first lexicographic successor) из MIB для каждого объекта из запроса. Если посмотреть в определение ipRouteEntry из RFC1213, то можно увидеть, что ipRouteIf!ndex(2) следует за ipRouteDest(l), a ipRou-teType(8) действительно следует за ipRouteNextHop(7).
    Так что ответ на вопрос, заданный минуту назад: «Как узнать, что мы получили все данные из таблицы?», будет таким: «Когда вы заметите, что вышли за пределы таблицы». С точки зрения программирования
    это равнозначно тому, чтобы проверить, возвращается ли в ответ на ваш запрос одна и та же строка или префикс OID. Например, можно убедиться, что все ответы на запрос о ipRouteDest содержали либо
    ip. ipRouteTable. ipRouteEntry. ipRouteDest, либо 1. 3. 6.1. 2.1.4. 21.1.1.
    Познакомившись с основами SNMP, можно возвращаться к главе 10 и узнать, как применять SNMP из Perl. Также стоит проверить ссылки в конце главы 10 для получения более подробной информации по SNMP.


    

        Программирование: Языки - Технологии - Разработка