При чтении важных данных, скажем, конфигурационных файлов, сначала протестируйте возможность небезопасных состояний. Например, стоит проверить, запрещена ли запись в файл и каталоги, в которых он находится (иначе, кто угодно может их испортить). Хороший способ подобной проверки можно найти в главе 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 после приглашения ррт, чтобы получить информацию о том, как использовать эти команды.
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 имеем:
Идентификаторы пользователей в 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 конкретного пользователя:
Вы не сможете (каким бы то ни было нормальным способом) заново создать пользователя после того, как он был удален. И даже если вы создадите пользователя с тем же самым именем, его идентификатор безопасности (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.
Практически все, что мы говорили о локальных и глобальных группах в 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; # для установки прав на домашний каталог
(т. е., перый параметр пустой) 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:
@info - это массив ссылок на анонимные хэши, каждый элемент которого соответствует отдельной учетной записи (в нашем случае это один-единственный элемент для учетной записи Guest). В каждом хэ-ше есть такие ключи: domain, do;nainsid, relativeid, sid и use. На следующем шаге нас будет интересовать только ключ sid. Теперь мы можем узнать о правах этой учетной записи:
${$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-код, но его формат несколько отличается от формата наших файлов с данными. Данные о каждой учетной записи представлены в виде атрибутов одного элемента accojnt>, a не в виде вложенных элементов. В XML: :Simple есть несколько правил, руководствуясь которыми, он преобразовывает структуры данных. Два из них можно сформулировать так (а остальные можно найти в документации): «отдельные значения преобразуются в XML-атрибуты», а «ссылки на анонимные массивы преобразуются во вложенные XML-элементы».
Чтобы получить «верный» XML-документ («верный» означает «в том же стиле и того же формата, что и наши файлы данных»).
Кошмар, не правда ли? Но у нас есть варианты для выбора. Мы можем:
Изменить формат наших файлов данных. Это похоже на крайнюю меру.
Изменить способ, которым XML: :Simple анализирует наш файл. Чтобы получить такую структуру данных (Рисунок 3.6), мы могли бы использовать функцию XMLin() несколько иначе:
Но если мы перекроим способ чтения данных, чтобы упростить запись, то потеряем семантику кэшей, упрощающих поиск и обработку данных.
Выполнить некую обработку данных после чтения, но до записи. Мы могли бы прочитать данные в нужную нам структуру (так же, как делали это раньше), применить эти данные в нужном месте, а затем преобразовать структуру данных в один из вариантов, получаемых модулем 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]ogin> Boh Fate'/fiil lr.ame> 24-9057 staff passwora to_be_created5tatus>
Да, мы храним пароли открытым текстом. Это очень плохая идея, и даже в случае с нашей демонстрационной системой стоит дважды подумать, прежде чем ее использовать. В настоящей системе учетных записей надо либо шифровать пароль перед тем, как помещать его в очередь, либо вообще не хранить его там.
Функция 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 и получить нужные значения вручную.
Попробуйте выбрать правильный ответ на следующий вопрос. Если бы не было пользователей, то системные администраторы:
Были бы добрее.
Исчезли бы с лица земли.
Независимо от того, что говорят по этому поводу системные администраторы в «трудную минуту», 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. Теперь перейдем к последней операционной системе.
Второй подход - применять модуль 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), который до сих пор не обсуждался, будет содержать информацию о самом процессе и потоках. Обладая этой информацией, вы сможете делать любопытные вещи, например, манипулировать приоритетами потоков и окнами этого процесса. Мы не собираемся рассматривать эти возможности, но упомянуть о них следует, чтобы спокойно перейти к следующему модулю.
Я упомянул эту возможность только для полноты картины. Можно написать программу, которая будет открывать устройство, подобное /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. ..), можно следить за различными типами изменений.
Вторая упомянутая программа - это 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++), написанная программа может выполняться так же быстро, как и вызов внешней программы.
Потенциально, это переносимая программа - все зависит только от того, что именно сделал автор модуля. Везде, где можно установить модуль, программа будет работать.
Как и в первом рассмотренном случае, написать программу можно быстро и просто, если кто-то другой сделает за вас всю работу, происходящую «за сценой». Вам не нужно знать, как работает модуль; вы только должны знать, как его применять.
Код используется повторно. Нет необходимости каждый раз изобретать велосипед.
Недостатки данного подхода:
Снова появилась зависимость. На этот раз необходимо убедиться, что модуль доступен вашей программе. Приходится поверить, что автор модуля проделал хорошую работу.
Может не существовать подходящего вам модуля или он может не запуститься на выбранной вами операционной системе.
В большинстве случаев я предпочитаю использовать уже существующие модули. Тем не менее, для выполнения поставленной задачи подойдет любой подход. Существует несколько способов сделать одно и то же - значит, вперед, действуйте!
Это короткая программа, которую можно быстро написать (вероятно, ее даже можно построчно перевести из настоящего сценария командного интерпретатора).
Нет необходимости писать запутанный код для работы с сетью.
Применяется подход в стиле 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-пакеты, чего при использовании 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), содержащую, в том числе, сведения о конфигурационных файлах, создание которых рассматривается ниже.
В настоящее время большая часть «разговоров» между компьютерами происходит по протоколу управления передачей (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.
Присмотримся внимательно к содержимому списка. Начиная с элемента $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";
Можно также сначала использовать один из этих методов и выделить объекты для отдельных элементов из объекта, возвращаемого в результате поиска:
Значение атрибута в указанном элементе Список имен атрибутов для этого элемента
Можно объединить эти методы в довольно четкую цепочку. Например, следующая строка получает значение атрибута сп первого возвращаемого элемента:
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, только заменяет текущее значение указанного атрибута. Если 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.
В этом подразделе представлен аналог для 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.
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 результатов запроса
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:
Преимущества такого подхода состоят в том, что можно выбросить из сценария все сложности отправки почты. Хороший агент передачи почты (МТА) пытается повторно соединиться с почтовым сервером, если тот в данный момент недоступен, выбирает нужный целевой сервер (ищет записи 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