Perl для системного администрирования
Perl для системного администрирования
Будьте осторожны при чтении данных
Будьте осторожны при чтении данныхПри чтении важных данных, скажем, конфигурационных файлов, сначала протестируйте возможность небезопасных состояний. Например, стоит проверить, запрещена ли запись в файл и каталоги, в которых он находится (иначе, кто угодно может их испортить). Хороший способ подобной проверки можно найти в главе 8 книги «Perl Cookbook» («Perl: библиотека программиста») Тома Кристиансена (Тот Christiansen) и Натана Торкингтона (Nathan Torkington) (O'Reilly).
Другая забота - ввод пользователей. Никогда не считайте, что данным, поступающим от пользователей, можно доверять. Даже если вы явно просите пользователя: Пожалуйста, ответьте Да(У) или Нет(М):, ничто не помешает ему набрать 2049 случайных символов (либо от вредности со злым умыслом, либо потому, что его двухлетний ребенок занял освободившееся на минуту место за клавиатурой).
Ввод пользователей может быть причиной еще более серьезных проблем. Мой любимый пример- это использование нулевого байта «Poison NULL Byte», о котором сообщалось в статье о проблемах Perl в CGI. Обязательно прочитайте всю статью (ссылка на нее есть в конце этой главы). Неприятности возникают из-за отличий в обработке нулевого байта (\000) в Perl и в системных библиотеках С. Для Perl этот символ ничем не примечателен. Однако в библиотеках этот символ используется для обозначения конца строки.
На практике это означает, что у пользователя существует возможность обойти различные проверки. Один пример, приведенный в этой статье, - это программа, меняющая пароль пользователя:
if (Suser ne "root"){ <вызов соответствующей функции С>}
Если переменная $user установлена в значение root\000 (т. е. если за словом root следует нулевой байт), то приведенная выше проверка окажется удачной. Когда эта строка будет передана библиотеке, она будет воспринята просто как root, и пользователю удастся обойти проверку. Если эту ситуацию не отследить, то подобная «дыра» позволит получить доступ к произвольным файлам и другим ресурсам. Самый простой способ не пострадать от этой проблемы - подправить код, добавив что-то похожее на следующую строку:
Sinput =" tr/\000//d;
Это всего лишь один пример того, как ввод пользователя может вызвать проблемы. Именно поэтому в Perl существует специальное средство предосторожности - режим пометки (taint mode). Изучите страницу руководства perlsec, входящую в состав дистрибутива Perl, чтобы ознакомиться с отличным объяснением того, что такое отмеченные данные, а также с другими мерами предосторожности.
Будьте осторожны при записи данных
Если ваша программа может записывать или дописывать данные в любой файл локальной файловой системы, вы должны особенно заботиться о том, как, куда и когда записываются данные. В системах Unix это особенно важно, поскольку символические ссылки очень сильно упрощают подмену файлов и перенаправление. Если ваша программа написана не очень аккуратно, может оказаться, что она пишет не в тот файл или устройство. Существует два класса программ, в которых это соображение особенно важно.
В первый класс попадают программы, дописывающие данные в существующие файлы. Перед дописыванием в файл в вашей программе должна быть выполнена следующая последовательность шагов:
Во второй класс попадают программы, использующие временные файлы или каталоги. Вы часто видели подобный код:
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:
use Win32::AdminMisc; die "Невозможно персонализировать $user\n"if
(!Win32::AdminMisc::LogonAsUser('',$user,Suserpw);
Замечание: здесь существует некоторая опасность, поскольку в отличие от предыдущего примера, вы должны передать пароль пользователя в функцию LogonAsUser().
Избегайте состояний перехвата
Избегайте состояний перехватаПо мере возможности, старайтесь не писать программ, допускающих состояния перехвата. Обычное состояние перехвата начинается с предположения, что следующая последовательность допустима:
Состояния перехвата часто возникают в программах системного администрирования, которые на первом шаге сканируют файловую .систему, а на втором шаге изменяют данные. Бесчестные пользователи могут внести изменения в файловую систему сразу после сканирования, чтобы изменения были внесены в неверный файл. Убедитесь, что в вашем коде нет подобных «дыр».
Эта книга покажет вам как
Эта книга покажет вам, какВ телевизионном шоу «Бэтмэн», популярном в 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 -MCPAN -e shell
cpan install modulename
Модуль CPAN достаточно «умен», чтобы обрабатывать зависимости (т. е. если один модуль требует запуска другого модуля, то CPAN установит оба модуля автоматически). В СРАМ, кроме того, есть функция для поиска связанных модулей и пакетов. Я рекомендую набрать perldoc CPAN, чтобы ознакомиться со всеми полезными возможностями этого модуля.
Системное администрирование это
Какой помощи ждать от PerlВ работе системного администратора используется любой и всякий язык программирования, если его применение приносит пользу. Так почему же в этой книге выбран именно Perl?
Ответ на этот вопрос слышится в самой природе системного администрирования. Реми Эвард, мой друг и коллега, однажды описал работу системного администратора такими словами: «С одной стороны, у тебя есть набор ресурсов: компьютеры, сети, программное обеспечение и т. д. А с другой стороны, есть пользователи со своими нуждами и проектами - люди, которые хотят, чтобы их работа выполнялась. Наша задача заключается в том, чтобы состыковать эти два множества оптимальным способом, являясь при необходимости посредником между кругом расплывчатых нужд людей и техническим миром».
Системное администрирование - это зачастую склеивание;
Perl - один из наиболее подходящих для этого языков. Perl использовался для системного администрирования задолго до того, как появился WWW со всеми своими требованиями к механизмам склеивания.
В Perl есть несколько других особенностей, свойственных принципам системного администрирования:
Ссылки на подробную информацию
Ссылки на подробную информациюhttp://dwheeler.com/secure-programs/Secure-Programs-HOWTO.html
-документ HOWTO (соображения «как сделать») о безопасном программировании в Linux, но рассмотренные в нем концепции и технологии применимы и в других ситуациях. http://www.cs.ucdavis.edu/~bishop/secprog.html
содержит лучшие способы безопасного программирования от эксперта по безопасности Мэтта Бишопа (Matt Bishop). http://www.homeport.org/~adam/review.html -
список указаний по написанию безопасного кода от Адама Шостака (Adam Shostack). http://www.dnaco.net/~kragen/security-holes.html -
хороший документ о том, как искать дыры в защите (особенно в собственном коде) от Крегена Ситэйкера (Kragen Sitaker). http://www.shmoo.com/securecode/
предлагает отличную коллекцию статей о том, как писать безопасные программы. «Perl CGI Problems»,
Rain Forest Puppy (Phrack Magazine, 1999). Электронный вариант можно найти на http://www.insecure.org/news/P55 07.txt или в архивах Phrack на http://www.phrack.com/archwe.html. «Perl Cookbook»,
Tom Christiansen, Nathan Torkington (O'Reilly, 1998) -эта книга рецептов содержит много хороших советов по созданию безопасных программ.
Установка модулей на MacOS
Установка модулей на MacOSУстановка модулей на MacOS - это странный гибрид уже рассмотренных методов. Крис Нандор (Chris Nandor) собрал дистрибутив срап-тас (его можно найти либо на CPAN, либо на http://pudge.net/macperl), в состав которого входит перенесенная на MacOS версия CPAN, а также невероятное количество других модулей.
После установки дистрибутива срап-тас можно при помощи CPAN загружать и устанавливать большинство модулей, реализованных исключительно на языке Perl. Нандор упростил эту задачу, написав небольшое приложение installme. Архивные файлы (т. е. файлы .tar.gz), переданные installme, будут разархивированы и установлены в стиле CPAN. Подробности об установке модулей на MacOS можно найти в расширенной версии документа perlmodinstall.pod, упомянутого ранее как macperl modinstall.pod. Его можно найти также и на http://pudge.net > macperl.
Процесс установки модулей на платформе
Установка модулей на Win32Процесс установки модулей на платформе Win32 совпадает с процессом установки модулей на Unix, но требует одного дополнительного шага — ррт. Если вы собираетесь устанавливать модули вручную, следуя инструкциям по Unix, то можете использовать программы, подобные WinZip (http://www.winzip.com), чтобы распаковать дистрибутив, и nmake (ftp://ftp.microsoft.com/Softlib/MSLFILES/nmakel5.exe) вместо make - для сборки и установки модуля.
Некоторые модули в процессе сборки требуют компиляции исходных файлов на С. У многих пользователей Perl на Win32 нет нужного программного обеспечения для компиляции, поэтому в ActiveState создали менеджер пакетов Perl для работы с дистрибутивами скомпилированных модулей.
Система РРМ похожа на модуль CPAN. Для загрузки и установки специальных файлов архивов из репозиториев РРМ используется программа ppm.pl, написанная на Perl. Вы можете запустить ее, либо набрав ррт, либо выполнив команду perl ppm.pl из каталога bin:
C:\Perl\bin>perl ppm.pl
РРМ interactive shell (1.1.1) - type 'help' for available commands.
PPM> install module-name
ррт, как и CPAN, может искать для вас список доступных и установленных модулей. Наберите help после приглашения ррт, чтобы получить информацию о том, как использовать эти команды.
Perl для системного администрирования
Файловые системы
Файловые системыИнформация о модулях из этой главы
Информация о модулях из этой главы| Имя | Идентификатор на CPAN | Версия |
| File : : Find (входит в состав Perl) | ||
| File : : Spec (входит в состав Perl) | ||
| Cwd (входит в состав Perl) | ||
| Win32: :File: :GetAttributes (входит в состав ActiveSta-te Perl) | ||
| Win32 : : FileSecu rity (входит в состав ActiveState Perl) | ||
| File : : Basename (входит в состав Perl) | ||
| Getopt : : Std (входит в состав Perl) Quota |
TOMZO | 1.2.3 |
| срап-тас (связка) | CNANDOR | 0.40 |
| Mac: :AppleEvents: : Simple | CNANDOR | 0.81 |
| Mac: :Glue | CNANDOR | 0.58 |
Источники подробной информации
Источники подробной информацииСтраницы руководства по perlport - просто неоценимый источник информации для Perl-программистов о различиях платформ. Крис Нандор (Chris Nandor) и Гурасами Сарати (Gurasamy Sarathy) дискутировали по поводу первых версий этого руководства на Perl Conference 2.0; Нандор опубликовал некоторые выдержки из этой беседы на http:// pudge.net/macperl/tpc/98.
MacOS
MacOSНесмотря на GUI-ориентированный подход, иерархическая файловая система MacOS (HFS, Hierarchical File System) также позволяет указывать текстовые имена файлов, хотя для этого нужно немного изловчиться. Абсолютные пути задаются в следующем виде: Диск/Имя_тома:Папка:Папка:Папка:Имя_файла. Отсутствие двоеточий указывает на то, что файл находится в текущем каталоге.
В отличие от двух предыдущих операционных систем, пути в HFS считаются абсолютными, если не начинаются с разделителя пути (:). Путь HFS, начинающийся с двоеточия, является относительным. Есть небольшое отличие записи пути в MacOS по сравнению с другими файловыми системами - это количество разделителей, которое необходимо указывать при ссылке на объект, стоящий выше в иерархии каталогов. Например, в Unix используется ../../../FileNamc для обращения к файлу, находящемуся тремя уровнями выше текущего каталога. В MacOS понадобилось бы использовать четыре разделителя (т. е. File-Name), поскольку необходимо включить ссылку на текущий каталог помимо трех предыдущих уровней.
В HFS длина имен файлов и каталогов ограничена 31 символом. В MacOS версии 8.1 был введен альтернативный многосимвольный формат, названный расширенным форматом MacOS, или HFS+, для поддержки Unicode в именах файлов длиной до 255 символов. И хотя файловая система HFS+ позволяет использовать такие длинные имена, на момент написания этой книги они еще не поддерживаются в MacOS.
Еще более заметным отличием от предыдущих двух файловых систем (по крайней мере, с точки зрения программирования на Perl) является использование в MacOS понятия «fork» (ветвление) при хранении файлов. У каждого файла есть поток данных (data fork) и поток ресурсов (resource fork). В первом хранятся данные, а во втором содержатся различные ресурсы. В эти ресурсы могут входить исполняемый код (в случае, если это программа), определения пользовательского интерфейса (диалоговые окна, шрифты и т. д.) или любые другие компоненты, определяемые программистом. И хотя в этой главе мы не будем рассматривать ветвления, в MacPerl есть возможность чтения и записи в оба потока.
В MacPerl основные операторы и функции работают только с потоком данных. Например, оператор —s возвращает только размер потока данных файла. Если вы хотите обратиться к потоку ресурсов файла, вам придется использовать дополнительные модули, входящие в состав Macintosh Toolbox.
Каждый файл в файловой системе HFS также имеет два специальных тега: creator (создатель) и type (тип), позволяющие операционной системе идентифицировать, каким приложением был создан файл и какого он типа. Эти теги играют ту же роль, что и расширения, используемые в файловых системах FAT (например .doc или .ехе). Позже в этой главе мы увидим, как применять теги тип/создатель в собственных целях.
в данной книге речь идет
Microsoft Windows NT/2000Windows 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 существуют четыре основные различия:
В 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. Наберитесь терпения, пока мы будем рассматривать процесс настройки шаг за шагом:
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( ), поскольку именно здесь будут находиться все интересные изменения, которые мы хотим изучить. Перед тем как вносить изменения в этот код, важно обратить внимание на те немногие моменты, которые могут не показаться очевидными при рассмотрении приведенного фрагмента:
Давайте поговорим о подпрограмме &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 указывается для того, чтобы убедиться, что по ошибке не будут уда лены ссылки или файлы нулевой длины. Если мы хотим действовать осторожнее, нам следует выполнить еще две дополнительные проверки:
$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. Например, два процесса могут:
В нашем случае мы собираемся выбрать простой, но мощный метод о о мена информацией. Так как первый процесс должен передать второму только набор инструкций по изменению информации (какие квоты изменять и на какие значения), мы установим между ними стандартный канал 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
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
$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/$_) |
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 не подходит? На ум приходят четыре ситуации:
Получив домашний каталог, мы переходим в него и начинаем сканирование, используя вызов &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
{
Вот как выполняется эта подпрограмма:
foreach my $path (keys %core){
print "Найден core-файл, занимающий -1.&BytesToMeg($core{$path}).
"MB в ",&File::Basename::dirname($path).".\n": }
if (keys %emacs){
"Следующие файлы, скорее всего, являются резервными копиями, созданными 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
Учет различий файловых систем в PerlPerl может помочь создавать программы, в которых учитывается большинство особенностей файловых систем. В его состав входит модуль 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. Третья строка могла бы показаться понятной, если бы не забавный аргумент, переданный функции 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("
Содержимое Squeue мы выводим подобным образом:
prim Da la : : Dumper->Di;
mp( [Sqi.euc ] ["uueuo" ])
Теперь это ссылка на данные, найденные в файле очереди, сохраненные в виде хэшей, ключами которого являются элементы
Мы используем именно такие ключи потому, что XML: : Simple позволяет распознавать в данных конкретные теги, выделяя их среди других в процессе преобразования. Если мы отключим эту возможность:
Squeue = XMLin("
то получим ссылку на хэш, где единственное значение является ссылкой на анонимный массив.
Такая структура данных не очень полезна. Этот параметр можно определять по собственному желанию:
$queue = XMLin("
Замечательно? Теперь мы можем удалить элементы из очереди в памяти, после того как обработаем их всего в одной строке:
# например, 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), чтобы запретить использовать компьютеры тем, кто не занят в этом проекте:
Подобная схема была бы совсем удобной, если бы не усложняла программирование на Perl. Во всех упомянутых модулях существуют отдельные функции для локальных и глобальных групп. Например, в Win32: :NetAdmin имеем:
| GroupCreate() GroupDelete() GroupGetAttributes() GroupSetAttributes() GroupAddUsers() GroupDeleteUsers( ) GroupIsMember( ) GroupGetMembers() |
LocalGroupCreate() LocalGroupDelete( ) LocalGroupGetAttributes( ) LocalGroupSetAttributes( ) LocalGroupAddlisers( ) LocalGroupDeleteUsers( ) LocalGrouoIsMember( ) LocalG-uijpGetMeTibers() |
создаются не простыми смертными
Идентификаторы пользователей в NT/2000Идентификаторы пользователей в NT/ 2000 создаются не простыми смертными и их нельзя использовать повторно. В отличие от Unix, где мы просто берем следующий свободный идентификатор пользователя, в Windows NT/2000 операционная система уникальным образом генерирует эквивалентный идентификатор каждый раз при создании пользователя. Уникальный идентификатор пользователя (который в NT/2000 называется относительным идентификатором или RID, Relative ID) объединяется с идентификатором машины и домена, и вместе они образуют длинный идентификационный номер - идентификатор безопасности (SID, Security ID), который используется в качестве1 идентификатора пользователя (UID). Например, RID равный FOG, является частью длинного идентификатора SID, который выглядит так: S-1-5-21-2046255566-1111630368-2110791508-500
RID - это то число, которое мы получаем в результате вызова функции UserGetMiscAttributes() в последнем примере. Вот так должен выглядеть код для получения идентификатора RID конкретного пользователя:
use Win32::AdminMisc;
Win32: :AdrninMisc: : UserGetMiscAttriuutest ' '.
$user \%attribs): print $attnbs{USEFLUSER_ID}, "\n";
Вы не сможете (каким бы то ни было нормальным способом) заново создать пользователя после того, как он был удален. И даже если вы создадите пользователя с тем же самым именем, его идентификатор безопасности (SID) все равно будет отличаться. У нового пользователя не будет доступа к файлам и ресурсам его предшественника.
По этой причине в некоторых книгах по NT рекомендуется переименовывать учетные записи, которые наследуются от других людей. Если к новому работнику должны перейти все файлы и привилегии уходящего работника, следует скорее переименовать существующую учетную запись, чтобы сохранить SID, чем создавать новую учетную запись, переписывать все файлы и затем удалять старую. Лично я нахожу такой способ передачи учетных записей несколько грубоватым, поскольку в этом случае новый работник наследует все поврежденные и бесполезные настройки реестра от своего предшественника. Но это самый удобный способ, а иногда это важно.
Частично эта рекомендация связана с мучениями при передаче права собственности на файлы. В Unix привилегированный пользователь может сказать: «Изменить права владения для всех этих файлов так, чтобы они перешли к новому пользователю». В NT, однако, право на владение нельзя дать, его можно только получить. К счастью, существует два способа обойти это ограничение и считать, что мы используем семантику Unix. В Perl мы можем:
$acl->0wner($NewAccountName):
Sresult = $aci->SetReci;rse($dir);
$acl->Close():
Информация о модулях из этой главы
Рекомендуемая дополнительная литература Файлы паролей в Unixhttp://www.freebsd.org/cgi/man.cgi.
Здесь можно получить доступ в оnline-режиме к страницам руководств для *BSD и других вариантов Unix. Это очень удобный способ сравнить форматы файлов и команды системного администрирования (useradd и пр.) для нескольких операционных систем. «Practical Unix & Internet Security»,
(2nd Edition), Simson Garfinkel, Gene Spafford (O'Reilly, 1999). Отличный источник информации о файлах паролей. Администрирование пользователей в NT
http://Jenda.Krynicky.cz
- еще один сайт с полезными модулями в Win32 для администрирования пользователей. http://windows.microsoft.com/windows2000/en/server/help/ -
справка Windows 2000. (Переходите к разделу Active Directory-»Concepts-) Understanding Active Directory-> Understanding Groups). Это хороший обзор новых механизмов групп в Windows 2000. http://www.actiuestate.com/support /mailing_lists.htm.
Здесь можно найти списки рассылки Perl-Win32-Admin и Perl-Win32-Users. Оба списка и их архивы представляют собой просто бесценный источник информации для программистов Win32. «Win32 Perl Programming: The Standard Extensions»,
Dave Roth (Mac-millan Technical Publishing, 1999) в настоящее время лучший источник по программированию модулей для Win32 Perl. «Windows NT User Administration»,
Ashley J. Meggitt, Timothy D. Ritchey (O'Reilly, 1997). http://www.mspress.com.
Издатели Microsoft NT Resource Kit. Они также предлагают возможность подписки для получения доступа к самым последним утилитам из RK. http://www.roth.net.
Домашняя страница для Win32: :AdminMisc, Win32: :Perms и других модулей для Win32, используемых для администрирования пользователей.
Информация о пользователях в Unix
Информация о пользователях в UnixПри обсуждении этой темы мы будем иметь дело лишь с несколькими ключевыми файлами, поскольку в них хранится постоянная информация о пользователе. Говоря «постоянная», я имею в виду атрибуты, которые существуют до тех пор, пока существует пользователь, сохраняясь даже тогда, когда пользователь не зарегистрирован в системе. Иначе я буду называть это учетной записью. Если в системе у вас есть учетная запись, вы можете зарегистрироваться и стать пользователем данной системы.
Существование пользователя в системе начинается с того момента, когда информация о нем впервые заносится в файл паролей (или же служба каталогов обеспечивает аналогичную информацию). Пользователь «уходит со сцены», когда эта запись удаляется. Рассмотрим подробнее, как хранится эта информация.
Классический файл паролей в Unix
Начнем мы с «классического» формата файла паролей, а затем перейдем к более сложным вопросам. Я называю данный формат классическим, потому что на нем основаны все существующие в настоящее время форматы файлов паролей в Unix. Более того, он и сейчас встречается во многих вариантах Unix, включая SunOS, Digital Unix и Linux. Обычно это файл /etc/passwd, содержащий последовательность текстовых ASCII-строк, причем каждая строка соответствует одной учетной записи или является ссылкой на другую службу каталогов. Любая строка файла состоит из нескольких полей, разделенных двоеточиями. Мы внимательно рассмотрим все эти поля, после того как научимся их получать.
Вот пример строки из файла /etc/passwd:
dnb:fMP.olmno4jGA6:6700:520:David N. Blank-Edelman:/home/dnb:/bin/zsh
Существует по крайней мере два способа получать подобную информацию средствами Perl:
1. К файлу можно обратиться «вручную», рассматривать его как обычный текстовый и соответствующим образом анализировать:
Spasswd = "/etc/passwd":
open(PW, Spasswd) or die "Невозможно открыть $passwd:$' '-n":
while (
($name,$passwd,$iiid.$gid. $дсоз. $dir. $srelL ,) = split (/:/):
<далее следует ваша программа>
}
close(PW);
2. Другой способ позволяет «предоставить все полномочия системе». В этом случае нам будут доступны некоторые библиотечные вызовы Unix, которые проанализируют файл за нас. Тогда последний пример можно переписать так:
while( ($пагле, Spasswd, $uid,$gid,$gcos,$dir,$shell) = getpwent( ) ){
<далее следует ваша программа>
}
endpwent();
Употребление системных вызовов содержит еще одно преимущество: их можно автоматически использовать с любой из служб имен (например, NIS). Вскоре мы рассмотрим и другие системные вызовы (включая более простой способ применения getpwent( )), а пока разберемся с полями, которые получили в наших примерах:1
Имя
В этом поле хранится короткое (обычно не длиннее восьми символов), уникальное в пределах системы регистрационное имя пользователя. Функция getpwent( ), которую мы уже видели в предыдущем примере в списочном контексте, возвращает значения данного поля, если вызывается в скалярном контексте:
$name = getpwent( ),
Идентификатор пользователя (UID)
В Unix-системах идентификатор пользователя (UID) зачастую более важен, чем регистрационное имя. Все файлы в системе принадлежат пользователю с каким-либо идентификатором, а не регистрационным именем. Если в файле /etc/passwd поменять регистрационное имя пользователя, обладающего идентификатором 2397, с danielr на drinehart, то мгновенно владельцем всех его файлов станет пользователь drinehart. Для операционной системы идентификатор пользователя - постоянная информация. При выделении ресурсов и выяснении прав ядро и файловые системы следят за идентификаторами, а не регистрационными именами. Регистрационное имя можно считать информацией, внешней для операционной системы; эта информация существует, чтобы упростить жизнь пользователя.
Вот простой пример, позволяющий установить очередной доступный уникальный идентификатор в файле паролей. Достаточно выяснить максимальный идентификатор и использовать его для создания следующего номера:
Spasswd = "/etc/passwd";
open(PW,$passwd) or die "Невозможно открыть $passwd:$!\n";
while (
@fields = splitC/:/);
Shighestuid = (Shighestuid < $fields[2]) ? $fields[2] : $highestuid:
}
close(PW);
print "Следующий доступный идентификатор: " . ++$highestuid . "\n";
Ниже перечислены другие полезные функции и переменные, имеющие отношение к именам и идентификаторам пользователей (табл. 3.1).
Информация о пользователях в Windows
NT/2000 хранит постоянную информацию о пользователях в базе данных SAM (Security Accounts Manager, диспетчер учетных записей в системе защиты). База данных SAM - это часть реестра NT/2000, находящаяся в %SYSTEMROOT%/system32/config. Файлы, входящие в состав реестра, хранятся в двоичном формате, следовательно, обычные функции для работы с текстом в Perl нельзя применять для чтения или внесения изменений в эту базу данных. Теоретически, если NT/2000 не запущена, можно использовать операторы над двоичными данными (раск() и unpack()) для работы с SAM, но такой способ безумен и мучителен.К счастью, существуют более удачные методы доступа к этой информации и работы с ней в Perl.
Один из способов - вызвать внешнюю программу, которая обесепечит ваше взаимодействие с операционной системой. На каждой машине с NT/2000 есть команда net, она позволяет добавлять, удалять и просматривать данные о пользователях, net довольно странная и ограниченная команда и, вероятно, к ее использованию стоит прибегать в крайнем случае.
Вот так, например, команда net выполняется на машине с двумя учетными записями:
C:\>net users
User accounts for \\HOTDIGGITYDOG
------------------------------------------------------
Administrator Guest
The command completed successfully.
При необходимости, вывод этой команды было бы просто разобрать из Perl. Помимо net существуют и другие коммерческие пакеты, в состав которых входят программы, запускаемые из командной строки и выполняющие те же действия.
Другой подход - использовать модуль Wi п32: : Net Admin (входящий в состав дистрибутива ActiveState Perl) или один из модулей, созданных для расширения функциональности Win32:: NetArMn. В их число входят модули Win32: :AdminMisc Дэвида Рота (David Roth, модуль находится на http://www.roth.net) и Win32: :UserAdmiri (описанный Эшли Мэггитом (Ashley Meggitt) и Тимоти Ритчи (Timothy Ritchey) в книге «Windows NT User Administration» (Windows NT: Администрирование пользователей), модуль можно найти на ftp://ftp.oreilly.com/pub/ examples/windows/winuser/).
Для выполнения большинства операций с пользователями я предпочитаю модуль Win32: .-AdrcinMisc, поскольку он предлагает множество инструментов системного администрирования, кроме того, Рот активно поддерживает его в нескольких форумах. И хотя доступная в Сети документация по этому модулю очень хороша, лучшая документация -это книга самого автора «Win32 Perl Programming: The Standard Extensions» (Программирование на Perl для Win32: стандартные расширения) (Macmillan Technical Publishing). Такую книгу всегда полезно иметь под рукой, если вы собираетесь писать на Perl программы для Win32. Приведу пример, перечисляющий пользователей на локальной машине, а также некоторые сведения об этих пользователях. Выводимые в примере строки похожи на строки из файла /etc/passwd в Unix:
use Win32::AdminMisc;
и получаем список всех локальных пользователей
Win32::AdminMisc::GetUsers('','',\@users) or
die "Невозможно получить список пользователей: $!\п";
П получаем их атрибуты и выводим их foreach $user (@users){
Win32::AdminMisc::UserGetMiscAttributes('',$user,\%att ribs)
or warn " Невозможно получить атрибуты: $!\п"; print join(":",$user,
' * '
$attribs{USER_USER_ID},
$attribs{USER_PRIMARY_GROUP_ID},
$attribs{USER_COMMENT},
$attribs{USER_FULL_NAME},
$attribs{USER_HOME_DIR_DRIVE}.
$attribs{USER_HOME DIR}, "),"\n";
}
Наконец, вы можете использовать модуль Win32: : OLE для доступа к интерфейсам активных служб каталогов (ADSI, Active Directory Service Interfaces). Данная служба встроена в Windows 2000 и ее можно установить на Windows NT 4.0. Эта тема и соответствующие примеры будут подробно рассмотрены в главе 6 «Службы каталогов».
Другие примеры программ на Perl для работы с пользователями в NT/2000 встретятся позже, а пока вернемся к обсуждению различий между пользователями в Unix и Windows NT/2000.
Microsoft Windows NT/ Windows 2000 Resource Kits
Microsoft Windows NT/ Windows 2000 Resource Kits«У вас должен быть установлен NT 4.0 Server и/или Workstation Resource Kit» - в этом, обычно, единодушны и серьезные администраторы NT, и средства информации. Microsoft Press опубликовал два больших тома, каждый из которых полон жизненно необходимой информации об одной из версий операционной системы NT/2000. Ценность этих книг заключается не столько в сведениях, сколько в компакт-дисках, распространяемых вместе с книгами. На компакт-дисках есть множество важных утилит для администрирования NT/2000. Утилиты, поставляемые с книгой по NT/2000 Server, содержат и утилиты, входящие в компакт-диск для версии NT Workstation/Windows 2000 Professional. Если вам придется выбирать одну из книг, предпочтите издание, посвященное NT/2000 Server.
Многие из этих утилит распространяются группой разработчиков NT/2000, написавших собственные программы, поскольку они нигде не смогли найти нужные им инструменты. Например, в состав этих утилит входят программы для добавления пользователей, изменения информации о безопасности файловой системы, отображения установленных драйверов принтеров, работы с профилями, помощи с отладкой служб доменов и обозревателя сети и т. д.
Инструменты из пакета дополнительных программ поставляются «как есть» (as is), иными словами, они практически не поддерживаются. Такая политика «неподдержки» может показаться грубой, но она преследует важную цель - дать возможность Microsoft предоставить администраторам множество полезных программ и не заставлять их платить непомерно много за поддержку. В программах из этого пакета есть некоторые мелкие ошибки, но, в целом, они работают замечательно. Обновления, исправляющие ошибки в некоторых утилитах, публикуются на веб-сайте Microsoft.
Массив ©rights теперь содержит набор строк, описывающих все права учетной записи Guest.
Узнать, чему соответствует API-имя ( Application Program Interface, интерфейс прикладного программирования) того или иного права пользователя, может оказаться непростой задачей. Самый легкий способ выяснить, каким правам какие имена соответствуют, - ознакомиться с документацией SDK (Software Developement Kit, набор инструментальных средств разработки программного обеспечения), которая находится на http://msdn.microsoft.com. Нужную документацию отыскать легко, потому что Хелберг сохранил имена стандартных функций SDK для функций в Perl. Чтобы найти имена доступных прав, достаточно поискать в MSDN (Microsoft's Developer Network) «LsaEnumerateAccountRights», и мы быстро их отыщем.
Подобная информация полезна и для изменения прав пользователей. Например, если мы хотим разрешить пользователю Guest выключать (останавливать) систему, мы можем применить следующее:
use Win32::Lanman;
unless (Win32::Lanman::LsaLookupNames($server ['Guest'],
\@info)) {
die " Невозможно найти SID; ".Win32::Lanman::GetLastError()."\n"
}
unless (Win32::Lanman::LsaAddAccountRights($server,
${$info[0]}{sid}, [&SE^SHUTDOWN_NAME])) {
die " Невозможно изменить права: ". Win32::Lanman::GetLastError()."\n"
}
На этот раз мы нашли право SE_SHUTDOWN_NAME в документации по SDK и применили подпрограмму &SE__SHUTDOWN_NAME (определенную в Win32: : Lanman), возвращающую значение этой константы SDK.
Win32: : Lanman: : LsaRemoveAccountRights() - это функция. Она используется для лишения прав и принимает аргументы, схожие с теми, которые применяет функция для добавления прав.
Перед тем как перейти к другим темам, необходимо упомянуть, что в Win32:: Lanman входит также и функция, действующая аналогично неудачному интерфейсу диспетчера пользователей, о котором мы говорили раньше. Вместо того чтобы сопоставлять пользователей с правами, мы можем сопоставлять права с пользователями. Применяя функцию Win32: : Lanman: : LsaEnumerateAccountsWithUserRight(), мы можем получить список идентификаторов (SID), у которых есть определенные поля. Иногда такое знание может сослужить добрую службу.
Практически все, что мы говорили
Отличия групп в Windows 2000Практически все, что мы говорили о локальных и глобальных группах в NT, также относится и к Windows 2000, но существует несколько характерных особенностей, о которых необходимо упомянуть:
Таким образом, не исключено, что в программах для выполнения одной и той же операции понадобится употребить две функции. Например, если нужно получить список всех групп, в которые может входить пользователь, придется вызвать две функции - одну для локальных, а другую для глобальных групп. Приведенные функции характеризуются своими названиями. Детальное описание можно найти в документации и книге Рота.
Вот короткий совет из книги Рота: чтобы получить список локальных групп, ваша программа должна выполняться с привилегиями администратора, но имена глобальных групп должны быть доступны всем пользователям.
применяемые для шифрования паролей, ограничивающих
Пароли в NT/2000Алгоритмы, применяемые для шифрования паролей, ограничивающих доступ к владениям пользователей в NT/2000 и Unix, криптографически несовместимы. Вы не можете передавать зашифрованные пароли из одной операционной системы в другую, что бывает необходимо для смены пароля или создания учетных записей. В результате, два набора паролей приходится использовать и/или хранить синхронно. Это различие - просто проклятье всех системных администраторов, вынужденных управлять смешанным окружением Unix-NT/2000. Некоторые администраторы обходят эти проблемы, используя специальные модули авторизации - как коммерческие, так и прочие.
Если вы не применяете специальные механизмы авторизации, то единственное, что вы можете сделать как программист на Perl, - создать систему, благодаря которой пользователи смогут представлять свои пароли в виде обычного текста. Такие пароли позволяют выполнять связанные с ними операции (изменение пароля и пр.), различные в каждой операционной системе.
Почему настоящие системные администраторы создают системы учетных записей
Почему настоящие системные администраторы создают системы учетных записейСистемные администраторы делятся на две категории: ремесленники и архитекторы. Ремесленники большую часть своего времени проводят в не посредственном контакте с подробностями внутреннего устройства ОС. Они знают множество тайн об аппаратном и программном обеспечении, которое они администрируют. Если что-то идет не так, как надо, они знают, какую использовать команду, файл, или какой «гаечный ключ» нужно применить. Талантливые ремесленники могут поразить вас способностью определить и исправить неполадки, находясь даже в соседней комнате от «проблемной» машины.
Архитекторы же тратят время, осматривая компьютерные пространства с высоты. Они мыслят более абстрактно, решая, как сформировать более сложные системы из отдельных частей. Архитекторы озабочены вопросами масштабируемости, расширяемости и повторного использования.
Администраторы обоих типов вносят важный вклад в системное администрирование. Я больше всего уважаю системных администраторов, которые могут быть ремесленниками, но при этом предпочитают действовать как архитекторы. Они решают проблему, а потом определяют, какие изменения в системе можно сделать, чтобы избежать повторения ошибки в дальнейшем. Они думают о том, как даже маленькие усилия с их стороны могут послужить для дальнейшего выигрыша.
Отлично действующее компьютерное окружение требует, чтобы архитекторы работали с ремесленниками в тесном взаимодействии. Ремесленники больше всего полезны при работе в рамках, созданных архитекторами. В автомобильном мире ремесленники нужны для сборки и ремонта машин. Но ремесленники расчитывают на то, что проектировщики машин разрабатывают трудно ломаемые и быстро ремонтируемые автомобили. Чтобы хорошо выполнять свою работу, им нужна инфраструктура, напоминающая сборочный цех, инструкция по эксплуатации и канал поставок запасных частей. Если архитектор хорошо выполняет свою работу, работа ремесленника становится проще.
Какое отношение это имеет к предмету нашего обсуждения? Что ж, вероятно, ремесленники будут применять имеющиеся в операционной системе инструменты для работы с пользователями. Они даже могут пойти дальше и написать небольшие сценарии, упрощающие такие задачи, как добавление пользователей, Архитектор, посмотрев на эту проблему, тут же начнет создавать систему ведения учетных записей. Архитектор задумается над такими вопросами:
Но когда в этой главе я говорю база данных, то употребляю этот термин в самом широком смысле слова. Плоские файлы вполне подойдут в нашем случае. Пользователи Windows даже могут работать с файлами баз данных Access (например database.mdb). В целях переносимости в этом разделе для различных создаваемых компонентов мы будем использовать простые текстовые базы данных. Но чтобы это было более интересным, базы данных будут в формате XML. Если вы никогда раньше не имели дела с XML, пожалуйста, потратьте немного времени и ознакомьтесь с приложением С «Восьмиминутное руководство по XML».
Почему XML? У XML есть несколько свойств, которые делают его хорошим выбором для подобных файлов и других конфигурационных файлов системного администрирования:
И в этом случае вы увидите, что правило TMTOWTDI по-прежнему действует. Для каждой операции с XML, которая нам понадобится, мы рассмотрим или, по крайней мере, упомянем несколько способов ее выполнения. Обычно, собирая подобную систему, лучше ограничить число реализованных опций, а действуя таким образом, вы сможете понять, какие возможности программирования существуют при работе с XML из Perl.
Подпрограммы для создания и удаления учетных записей в Unix
Подпрограммы для создания и удаления учетных записей в UnixНачнем с примеров кода для создания учетных записи в Unix. Большая часть этого кода будет элементарной, поскольку мы избрали легкий путь. Наши подпрограммы для создания и удаления учетных записей вызывают команды с необходимыми аргументами, входящие в состав операционной системы, для «добавления пользователей», «удаления пользователей» и «смены пароля».
Зачем нужна эта очевидная попытка отвертеться? Этот метод приемлем, поскольку известно, что программы, входящие в состав операционной системы, хорошо «уживаются» с другими компонентами. В частности, этот метод:
Различия операционных систем
В каждую операционную систему входит свой собственный набор программ, расположенных в разных местах и принимающих несколько различные аргументы. Это редкий пример совместимости, однако практически во всех распространенных вариантах 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; |
Какое это имеет отношение к Perl? Каждый упомянутый модуль требует принять два решения:
Теперь читателю должно быть понятно, почему выбор модуля - это де ло личных предпочтений. Хорошей стратегией было бы сначала решить, какие параметры важны для вас, а затем найти модуль, который их поддерживает. Для наших демонстрационных подпрограмм мы выбираем модуль Win32: : Lanman. Вот какой код можно применить для создания и удаления пользователей в нашей системе учетных записей:
use Win32: :Lanman; tt для создания учетной записи
use Win32::Perms; # для установки прав на домашний каталог
$homeNTdirs = "\\\\homeserver\\home"; # корневой каталог
# домашних каталогов
sub CreateNTAccount{
my ($account,$record) = @_;
П создаем учетную запись на локальной машине
# (т. е., первый параметр пустой)
$result = Win32::Lanman::NetUserAdd("",
{'name' => Saccount,
'password' => $record->{password},
'home_dir' => "$homeNTdirs\\$account",
'full_name' => $record->{fullname}});
return Win32::Lanman::6etLastError() unless ($result);
добавляем в нужную ЛОКАЛЬНУЮ группу
(предварительно мы Я получаем SID учетной записи)
# Мы считаем, что имя группы совпадает с типом учетной
die "SID lookup error: ".Win32::Lanman::6etLastError()."\n"
unless (Win32: :Lanman: :LsalookupNames("", [$account],
\@info)); $result = Win32::Lanman::NetLocalGroupAddMember("",
$record->{type), ${$info[0]){sid»;
return Win32::Lanman::GetLastError() unless (Sresult);
# создаем домашний каталог
mkdir "$homeNTdirs\\$account",0777 or
return "Unable to make honedir:$!";
№ устанавливаем ACL и владельца каталога
$acl = new Win32::Perms("$homeNTdirs\\$account");
$acl->0wner($account);
# мы предоставляем пользователю полный контроль за
# каталогом и всеми файлами, которые будут в нем созданы
# (потому и два различных вызова)
DIRECTORY | СОНТШЕВ_ШЕИТ_АСЕ);
$acl->Allow($account, FULL, -
FILE|OBJECT_INHERIT_ACE|IMHERIT_ONLY_ACE);
$result = $acl->Set(); $acl->Close();
return($result ? "" : Sresult); }
Программа для удаления пользователей выглядит так:
use Win32: iLanman;
для удаления учетной записи
use File::Path;
для рекурсивного удаления каталогов
sub DeleteNTAccount{
my($account,$record) = @_;
# удаляем пользователя только из ЛОКАЛЬНЫХ групп.
Если мы № хотим удалить их и из глобальных групп, мы можем убрать
слово "Local" из двух вызовов Win32::Lanman::NetUser
(например, NetUserGetGroups)
die "SID lookup error: ".Win32::Lanman::GetLastError()."\n"
unless (Win32::Lanman::LsaLookupNames("",
[Saccount], \@info));
Win32::Lanman::NetUserGetLocalGroups($server, Saccount, ",
\@groups); foreach $group (@groups){
print "Removing user from local group ".
$group->{name}."...";
print(Win32::Lanman::NetLocalGroupDelMember("",
$group->{name}, ${$info[0]}{sid})?
"succeeded\n" : "FAILED\n"); }
tt удалить эту учетную запись с локальной машины
(т. е., перый параметр пустой) Sresult = Win32::Lanman::NetUserDel("", Saccount);
return Win32::Lanman::GetLastError() if ($result);
удалить домашний каталог и его содержимое
Sresult = rmtree("$homeNTdirs\\$account",0,1);
rmtree возвращает число удаленных файлов, так что если мы
удалили более нуля элементов, то скорее всего все прошло 8 успешно return Sresult;
}
Заметьте, для удаления домашнего каталога здесь используется переносимый модуль File: :Path. Если бы мы хотели сделать что-то специфичное для Win32, например, переместить домашний каталог в корзину, то могли бы сделать это при помощи модуля Win32: :File Op Йенды Крыники (Jenda Krynicky), который можно найти на http://jen da.krynicky.cz/. В таком случае мы применили бы Wi n32: ; F: 1еОр и изменили бы строку, включающую rmtrc-e(), на:
# удалим каталог в корзину, потенциально подтверждая
# действие пользователем, если для этой учетной записи
# необходимо подтверждать такие операции
$result = Recycle("$homeNTdirs\\$account");
В данном модуле есть функция Delete(), которая выполняет то же, что и rmtree() менее переносимым (правда, более быстрым) способом.
В Unix действия, предпринимаемые пользователем,
Права пользователей в NT/2000Последнее различие между информацией о пользователях в Unix и NT/2000, о котором мы поговорим, - это понятие «пользовательских прав». В Unix действия, предпринимаемые пользователем, ограничиваются как правами доступа к файлам, так и различиями между суперпользователем и остальными пользователями. В NT/2000 права реализованы гораздо сложнее. Пользователи (и группы) могут быть наделены особой силой, которая становится частью информации о пользователях. Например, если предоставить обычному пользователю право Change the System Time (Изменение системного времени), то он сможет изменять настройки системных часов.
Некоторые считают, что такая концепция прав пользователей сбивает с толку, поскольку они предпринимали попытки прибегнуть к помощи отвратительного диалогового окна User Rights Policy (Политика прав пользователей) из NT 4.0 в приложениях User Manager (Диспетчер пользователей) или User Manager for Domains (Диспетчер пользователей для доменов). В этом диалоговом окне информация представлена виде, прямо противоположном тому, в котором большинство пользователей ожидают ее там увидеть. Она содержит перечень возможных прав пользователей и предлагает добавить группы или пользователей к списку тех, у кого такие права уже есть. Вот как выглядит это диалоговое окно (Рисунок 3.1) пользовательских прав в действии.
Было бы лучше, если бы права пользователей разрешалось добавлять и удалять, а не наоборот. В действительности, именно так мы и будем поступать, используя Perl.
Один из возможных подходов - вызвать программу ntrights.exe из Microsoft NT Resource Kit. Если вы об этом никогда не слышали, обязательно прочитайте следующую врезку.
Работать с ntrights.exe очень легко; достаточно вызывать эту программу из Perl, как любую другую (т. е., применяя обратные кавычки или функцию system()). В этом случае мы обратимся к ntrights.exe при помощи такой командной строки:
чтобы предоставить право пользователю или группе (на машине rnachi-пепате, имя которой указывать не обязательно). Чтобы отнять право, необходимо применить такой синтаксис:
С:\>ntrights.exe -г
Пользователи Unix знакомы с употреблением символов + и - (как в chmod), в данном случае для ключа -г, чтобы предоставить или лишить привилегий. Список допустимых имен (например, SetSys::emti-mePrivilege для разрешения устанавливать системное время) можно найти в документации Microsoft NT Resource Kit no команде ntrights
Второй подход, с использованием только Perl, связан с применением модуля Win32: : Lanman, написанного Йенсом Хелбергом (Jens Helberg), который можно найти либо на ftp://ftp.roth.net/pub/ntperl/Others/ Lanman/, либо на http://jenda.krynicky.cz. Начнем с того, что рассмотрим процесс получения прав для учетной записи. Этот процесс состоит из нескольких шагов, поэтому рассмотрим его подробно, шаг за шагом.
Сначала необходимо загрузить модуль:
use Win32::Lanman;
Затем следует получить идентификатор (SID) для учетной записи, с которой надо работать. В следующем примере мы получим SID для учетной записи Guest:
unless(Win32: : Lanman: : LsaLookupNames($server, [ 'Guest' ]. \@info)
{ die "Невозможно найти SID':
".Win32::Lanman::Get LastError()."\n";
}
@info - это массив ссылок на анонимные хэши, каждый элемент которого соответствует отдельной учетной записи (в нашем случае это один-единственный элемент для учетной записи Guest). В каждом хэ-ше есть такие ключи: domain, do;nainsid, relativeid, sid и use. На следующем шаге нас будет интересовать только ключ sid. Теперь мы можем узнать о правах этой учетной записи:
unless (Win32: : Lanman :: LsaEnurierateAccountRights($server.
${$info[0]}{sid}, \@rights)){ die "Невозможно узнать права:
"32: :Lanmai .GetL.asrError() "\r"\
Прочее
Прочееhttp://www.mcs.anl.gov/~evard.
Домашняя страница Реми Эварда (Re-my Evard), Использование нескольких баз данных для автоматического генерирования конфигурационных файлов - это лучший прием, который показан в нескольких местах моей книги; спасибо Эварду за идею этого метода. И хотя сейчас подобный прием применяется на многих сайтах, я впервые столкнулся с ним при знакомстве со средой Tenwen, которую он создал (как описано в статье, ссылка на которую есть с домашней страницы Эварда). Чтобы ознакомиться с работой этого метода, загляните в раздел «Implemented the Hosts Database». http://www.rpi.edu/~finkej/.
Содержит несколько статей Иона Финки (Jon Finke) по использованию реляционных баз данных в системном администрировании.
Сценарии
СценарииТеперь, когда мы разобрались с базой данных, самое время написать сценарии для выполнения периодических или каждодневных действий, необходимых при системном администрировании. Эти сценарии построены на низкоуровневой библиотеке компонентов (Account.pm), которую мы создали, объединив в один файл все только что написанные подпрограммы. Такая подпрограмма позволяет убедиться, что все необходимые модули загружены:
sub InitAccountf
use XML: :Writer;
Srecord = { fields => [login, fullname,id,type,password]};
$addqueue = "addqueue"; tt имя файла очереди добавления
Sdelqueue = "delqueue"; ft имя файла очереди удаления
$maindata = "accountdb"; tt имя основной базы данных
ft учетных записей
if ($"0 eq "MSWin32"){
require Win32::Lanman;
require Win32::Perms;
require File::Path;
ft местоположение файлов учетных записей
Saccountdir = "\\\\server\\accountsystem\\";
ft списки рассылки
$maillists = "$accountdir\\maillists\\";
ft корневой каталог домашних каталогов
$homeNTdirs = "\\\\nomeserver\\home";
ft имя подпрограммы, добавляющей учетные записи
Saccountadd = "CreateNTAccount";
ft имя подпрограммы, удаляющей учетные записи
Saccountdel = "DeleteNTAccount": }
else {
require Expect;
и местоположение файлов учетных записей
$accountdir = "/usr/accountsystem/";
в списки рассылки
Smaillists = "Saccountdir/maillists/";
tt местоположение команды useradd
Suseraddex = ",/usr/sbin/useradd";
tt местоположение команды userdel
Suserdelex = "/usr/sbin/userdel";
tt местоположение команды passwd
Spasswdex = "/bin/passwd";
tt корневой каталог домашних каталогов
ShomeUnixdirs = "/home";
tt прототип домашнего каталога
$skeldir = "/home/skel";
ft командный интерпретатор по умолчанию
Sdefshell = "/bin/zsh";
ft имя подпрограммы, добавляющей учетные записи
Saccountadd = "CreateUnixAccount";
tt имя подпрограммы, удаляющей учетные записи
Saccountdel = "DeleteUnixAccount";
}
}
Рассмотрим сценарий, обрабатывающий очередь добавления:
use Account;
use XML;:Simple;
SlnitAccount;
считываем низкоуровневые подпрограммы
&ReadAddQueue;
tt считываем и анализируем очередь добавления
&ProcessAddQueue;
tt пытаемся создать все учетные записи
&DisposeAddQueue;
ft записываем учетную запись либо в основную
tt базу данных, либо обратно в очередь, если
tt возникли какие-то проблемы
tt считываем очередь добавления в структуру данных $queue
sub ReadAddQueue{
open(ADD,Saccountdir.Saddqueue) or
die "Unable to open ".Saccountdir.$addqueue.":$!\n";
read(ADD, Squeuecontents, -s ADD);
close(ADD);
Squeue = XMLin("
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("
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("
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("
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.
Создание системы учетных записей
Центральная часть любой системы учетных записей - это база данных. Некоторые администраторы используют только файл /etc/ pass-wd или базу данных SAM для хранения записей о пользователях системы, но такое решение часто оказывается недальновидным. Помимо информации, о которой мы уже говорили, в отдельной базе данных можно хранить метаданные о каждой учетной записи: например, дату создания учетной записи, срок ее действия, номера телефонов пользователей и прочие сведения. Когда появляется такая база данных, ее можно применять не только для работы с учетными записями. Она годится для создания списков рассылки, служб LDAP и индексации веб-страниц пользователей.Создание XMLданных при помощи XML Simple
Создание XML-данных при помощи XML::SimpleУпоминание «записать его на диск» возвращает нас обратно к методу создания XML-данных, который мы обещали показать. Вторая функция из XML: : Simple принимает ссылку на структуру данных и генерирует XML-данные: rootname определяет имя корневого элемента, мы могли бы использовать XMLdecl. чтобы добавить объявление XML print XMLout($queue, rootname =>"queue"),
В результате получаем (отступы сделаны для удобства чтения):
fullname="Bob Fate" id="24-9C57" />
status="to_be_created"
fullname="Wendy Fate" id="50-9057" />
Мы получили отличный XML-код, но его формат несколько отличается от формата наших файлов с данными. Данные о каждой учетной записи представлены в виде атрибутов одного элемента
Чтобы получить «верный» XML-документ («верный» означает «в том же стиле и того же формата, что и наши файлы данных»).
Кошмар, не правда ли? Но у нас есть варианты для выбора. Мы можем:
Squeue = XMLin("
forcearray=>1, keyattr => [""]):
Но если мы перекроим способ чтения данных, чтобы упростить запись, то потеряем семантику кэшей, упрощающих поиск и обработку данных.
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("
print OUTPLITFILE XMLojt(Ti ansfonnFor Write($queuu), г ootname => "queue");
Записанные и прочитанные данные будут иметь один и тот же формат.
Перед тем как закрыть тему чтения и записи данных, следует избавиться от несоответствий:
1. Внимательные читатели, наверное, заметили, что одновременное использование XML: : Write г и XML: : Simple в одной и той же программе для записи данных в очередь может оказаться непростым делом. Если записывать данные при помощи XML: : Simple, то они будут вложены в корневой элемент по умолчанию. Если же применять XML: : Write г (или просто операторы print) для записи данных, вложения не произойдет, т. е. нам придется опять прибегнуть к хаку "
Чтобы избежать этой проблемы, надо будет использовать продвинутую возможность модуля XML: .'Simple: если XMLoutO передать параметр rootname с пустым значением или значением undef, то возвращаются данные в XML-формате без корневого элемента. В большинстве случаев так поступать не следует, потому что в результате образуется неправильный (синтаксически) документ, который невозможно проанализировать. Наша программа позволяет этим методом воспользоваться, но такую возможность не стоит применять необдуманно.
2. И хотя в примере этого нет, мы должны быть готовы к обработке ошибок анализа. Если файл содержит синтаксически неверные данные, то анализатор не справится и прекратит работу (согласно спецификации XML), остановив при этом и всю программу в случае, если вы не примете мер предосторожности. Самый распространенный способ справиться с этим из Perl - заключить оператор анализа в eval() и затем проверить содержимое переменной $@ после завершения работы анализатора. Например:
eval {$p->parse("
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);
# записываем открывающий тег для каждой записи
# записываем открывающие/закрывающие внутренние теги и
# данные в
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);
Вот что получается в результате работы этой подпрограммы:
Да, мы храним пароли открытым текстом. Это очень плохая идея, и даже в случае с нашей демонстрационной системой стоит дважды подумать, прежде чем ее использовать. В настоящей системе учетных записей надо либо шифровать пароль перед тем, как помещать его в очередь, либо вообще не хранить его там.
Функция AppendAccountXML() будет применяться еще раз, когда мы захотим записать данные в очередь удаления и в нашу базу данных учетных записей.
Использование модуля XML: :Writer в подпрограмме AppendAccountXMLO имеет несколько преимуществ:
Переменные и функции имеющие отношение к именам и идентификаторам пользователей
Таблица 3.1. Переменные и функции, имеющие отношение к именам и идентификаторам пользователей| Функция/ Переменная | Использование |
| getpwnam($name) | В скалярном контексте возвращает идентификатор, соответствующий этому регистрационному имени; в списочном контексте возвращает все поля данной записи из файла паролей |
| getpwuid(Suid) | В скалярном контексте возвращает регистрационное имя, соответствующее данному идентификатору; в списочном контексте возвращает все поля данной записи из файла паролей |
| $> | Соответствует эффективному идентификатору пользователя текущей выполняющейся программы на Perl |
| $< | Соответствует реальному идентификатору пользователя текущей выполняющейся программы на Perl |
В многопользовательских системах пользователи их группы часто работают с файлами и другими ресурсами совместно. В Unix существует механизм, позволяющий работать с группами пользователей. Учетная запись в системе может входить в несколько групп, но при этом принадлежать она должна только одной главной группе (primary group). Поле GID в файле паролей соответствует как раз первичной группе для данной учетной записи.
Имена групп, их идентификаторы и члены группы обычно перечислены в файле /etc/group. Чтобы включить учетную запись в несколько групп, необходимо просто указать ее в нескольких местах данного файла. В некоторых операционных системах существует жесткое ограничение на число групп, которым может принадлежать учетная запись (а значит, и пользователь). Чаще всего ограничение равно 8. Вот пример пары строк из файла /etc/group:
bin : : 2:root,bin,daemon
sys: iSiroot.bin.sys.adu
Первое поле - это имя группы, второе - пароль (в некоторых системах может употребляться пароль для присоединения к группе), третье - идентификатор группы и последнее поле - список пользователей в группе.
Способы объединения пользователей в группы зависят от конкретного узла, поскольку границы (и административные, и границы проектов) везде разные. Таким образом, группы можно создавать для разделения различных пользователей (студенты, продавцы и т. д.), по выполняемым действиям (операторы резервных копий, сетевые администраторы и т. д.) либо по назначению учетных записей (резервные учетные записи и пр.).
Работа с файлами групп средствами Perl очень похожа на процесс разбора файла passwd из предыдущих примеров. Его можно считать обычным текстовым файлом либо применять специальные функции для выполнения подобной задачи. Посмотрите на функции и переменные, имеющие отношение к группам (табл. 3.2).
Переменные и функции имеющие отношение к именам и идентификаторам групп
Таблица 3.2. Переменные и функции, имеющие отношение к именам и идентификаторам групп| Функция/ Переменная | Используется |
| getgrent() | В скалярном контексте возвращает имя группы; в списочном контексте возвращает поля: Sname, Soasswd, $gia, Sm'Xiers |
| get.grnarfi(Snane) | В скалярном контексте возвращает идентификатор группы; в списочном контексте возвращает те же поля, что и функция getgi-ent( ) |
| getgrgio($gid) | В скалярном контексте возвращает имя группы; в списочном контексте возвращает те же поля, что и функция дё-^"-' |
| $) | Соответствует эффективному идентификатору группы текущей выполняемой программы |
| $( | Соответствует реальному идентификатору группы текущей выполняемой программы |
Мы уже рассмотрели три основных поля, в которых содержится информация о пользователе в Unix. Следующее поле не является частью хранимой информации, но оно подтверждает права, обязанности и привилегии, присущие пользователю с конкретным идентификатором. Именно так компьютер узнает, что тому, кто выдает себя за пользователя mguerre, позволено присвоить конкретный идентификатор. Существуют и другие, лучшие формы авторизации (например, использование криптографических методов с открытым ключом), но этот способ унаследован от ранних версий Unix.
Очень часто в этом поле в файле паролей можно увидеть лишь звездочку (*). Подобный знак применяется для запрещения регистрации пользователя в системе, без удаления при этом самой учетной записи.
Работа с паролями пользователей - это отдельная тема. Ей будет посвящена глава 10 «Безопасность и наблюдение за сетью».
Поле GCOS
Поле GCOS самое бесполезное (с точки зрения компьютера). Обычно в этом поле записано полное имя пользователя (например «Рой Дж. Бив»). Часто люди добавляют туда должность и/или номер телефона.
Системные администраторы, заботящиеся о приватности пользователей (чему и следует быть), должны проверять содержимое данного поля. Это стандартный путь для определения соответствия между реальным именем пользователя и его регистрационным именем. В большинстве Unix-систем такое поле находится в файле /etc/passwd, доступном всем для чтения, следовательно, эта информация может попасть в руки кого угодно в системе. Многие программы, почтовые клиенты и демоны finger-запросов обращаются к этому полю при добавлении регистрационного имени пользователя к какой-то информации. Если у вас есть необходимость скрыть реальные имена пользователей от других людей (например, если речь идет о политических диссидентах, федеральных свидетелях или известных персонах), вы обязательно должны следить за этим полем.
В качестве дополнительной информации: если вы поддерживаете сайт с менее развитой пользовательской базой, было бы неплохо запретить пользователям изменять их поле GCOS на случайные строки (по тем же причинам, по которым выбранные пользователями регистрационные имена могут вызвать проблемы). Вряд ли вы придете в восторг, увидев в своем файле паролей бранные выражения или иную непрофессиональную информацию.
Домашний каталог
Следующее поле содержит имя домашнего каталога пользователя. Это тот каталог, откуда начинается работа с системой. Обычно здесь хранятся файлы, определяющие настройки пользователя.
В целях безопасности очень важно, чтобы запись в домашний каталог была разрешена только его владельцу. Домашние каталоги, доступные для записи всем, открывают возможность хакерских действий. Правда, существуют ситуации, когда домашние каталоги, доступные для записи только самим владельцам, тоже вызывают проблемы. Например, в случае с ограниченными интерпретаторами (если пользователи могут регистрироваться в системе только для выполнения определенных задач без права изменять что-либо в системе) домашние каталоги, доступные для записи пользователю, категорически запрещены.
Вот пример кода на Perl, который позволяет убедиться, что все домашние каталоги пользователей принадлежат своим владельцам и недоступны для записи остальным:
use User::pwent; use File::stat;
ft замечание: этот код очень сильно загрузит машину, если
# домашние каталоги монтируются автоматически
while($pwent = getpwent()){
# убеждаемся, что это действительно каталог, даже если
# он спрятан за символическими ссылками
Sdirinfo = stat($pwent->dir."/."); unless (defined $dirinfo){
warn "Невозможно получить информацию о ".$pwent->dir.": $!\n"; next;
}
warn «Домашний каталог пользователя ".$pwent->name." не имеет в
ладельца с корректным uid (". $dirinfo->uid." вместо ".$pwent->uid.")!\n"
# ($dirinfo->uid != $pwent->uid);
# каталог может быть доступным всем для записи, если
# у него установлен «бит-липучка" (т. е. 01000),
# подробности в странице руководства по chraod
warn $pwent->name."'s homedir is world-writable!\n"
if ($dirinfo->mode & 022 and (!$stat->mode & 01000));
}
endpwent();
Этот пример на вид несколько отличается от предыдущих, поскольку в нем используются два замысловатых модуля Тома Кристиансе-на (Tom Christiansen): User: :cweit и FiJe: :stai. Эти модули изменяют функции getpwcrH() и stat(), заставляя их возвращать значения, отличные от ранее упомянутых. Когда загружены модули Fi le: : stat, эти функции возвращают объекты вместо списков или скалярных значений. У каждого объекта есть метод, названный по имени поля, которое было бы возвращено в списочном контексте. Поэтому такой код:
$gid = (stat("filena(ne"))[5]:
можно переписать гораздо понятнее :
use File;:stat;
$stat = stat("filename"):
$gid = $stat->gid:
или даже так:
use File;:stat;
$gid = stat("filename")->gid;
Командный интерпретатор пользователя
Последнее поле классического файла паролей - это поле, соответствующее командному интерпретатору пользователя. Обычно это один из интерпретаторов (sh, csh, tcsh, ksh, zsh), но это может быть и путь к любой исполняемой программе или сценарию. Время от времени, некоторые ради шутки (но наполовину всерьез) устанавливают в качестве своего командного интерпретатора по умолчанию интерпретатор Perl. По крайней мере, в один интерпретатор (zsh) хотят всерьез встроить интерпретатор Perl, но этого пока еще не случилось. Тем не менее, были предприняты серьезные попытки создать командный интерпретатор Perl shell (http:/ /www.focusrese-arch.com/gregor/psh/), а также встроить Perl в редактор Emacs, который легко может заменить целую операционную систему (http:// john-edwin-tobey.org/perlmacs/1).
Бывают ситуации, когда необходимо указать в этом поле нечто отличное от стандартного командного интерпретатора. Например, если вы хотите создать учетную запись, работающую с системой меню, вы можете поместить в данное поле имя такой программы. В этом случае стоит принять некоторые меры предосторожности, чтобы пользователь, применяющий эту учетную запись, не получил бы доступ к командному интерпретатору, иначе не миновать бед. Часто встречаемая ошибка - включение в такое меню почтовой программы, которая позволяет запускать редактор или инструмент постраничного просмотра для чтения или редактирования почты. Оба эти средства могут иметь возможность выхода в интерпретатор.
Список доступных в системе стандартных командных интерпретаторов часто хранится в файле /etc/shells, видимо, для удобства демона FTP. Большинство FTP-демонов не позволят обычному пользователю подсоединиться к системе, если их командный интерпретатор, заданный в /etc/passwd (или сетевом файле паролей), не присутствует в /etc/shells. Вот пример на Perl, который докладывает об учетных записях с неподтвержденными командными интерпретаторами:
use User::pwert:
Sshells = "/etc/shells";
open (SHELLS,Sshells) or die "Невозможно окрыть;
while(
chomp:
$oksnell{$_}++;
}
close(SHELLS);
while($pwent = getpwent()){
warn $pwent->name." has a bad shell (".$pwent->shell.")!\n"
unless (exists $okshell{$pwent->shell});
}
endpwent();
Теневые пароли
Теневые паролиНе следует забывать, насколько важна защита содержимого поля GCOS, т. к. целым рядом различных механизмов эта информация доступна для всех. Другая, менее доступная, но довольно уязвимая информация - это список зашифрованных паролей всех пользователей системы. И хотя эти пароли зашифрованы, одно то, что они хранятся в файле, доступном всем для чтения, вносит изрядную степень риска. Некоторые части файла паролей должны быть доступны всем для чтения (например, связь между регистрационным именем и идентификатором пользователя), но не весь файл. Нет никакой необходимости выставлять на всеобщее обозрение список зашифрованных паролей, т. к. пользователи могут попытаться запустить программы для взлома паролей.
Одна из возможных альтернатив - перенести все пароли в специальный файл, читать который сможет только суперпользователь. Этот второй файл известен как файл «теневых паролей», т. к. в нем хранятся строки, затеняющие записи из обычного файла паролей.
Вот как все это работает: оригинальный файл паролей остается нетронутым, за одним небольшим исключением. Вместо зашифрованного пароля в это поле помещается специальный символ или символы, которые говорят о том, что используется механизм затенения паролей. Обычно это символ х, но в BSD используется *.
Я слышал, что существуют некоторые пакеты (для поддержки теневых паролей), вставляющие в это поле специальную строку символов, которая выглядит обычной. Если ваш файл паролей попадет в руки злоумышленника, то он потратит много времени, взламывая случайные строки, не имеющие ничего общего с настоящими паролями.
Большинство операционных систем используют файл теневых паролей для хранения дополнительной информации об учетной записи. Такой формат включает дополнительные поля, которые мы видели в BSD-файлах, и в них хранится информация об истечении срока действия учетной записи и информация о смене пароля.
В большинстве случаев обычные Perl-функции, подобные getcwenb(), могут работать с файлами теневых паролей. Если стандартные библиотеки С, входящие в состав операционной системы, делают то, что нужно., то Perl тоже будет делать все верно. Говоря «делать то, что нужно», я подразумеваю, что если ваши сценарии на Perl запускаются с подходящими привилегиями (с привилегиями суперпользователя), то эти функции будут возвращать зашифрованный пароль. В остальных случаях пароль этим функциям не доступен.
Значительно хуже, если вы захотите получить дополнительные поля из файла теневых паролей. Perl может и не вернуть их вам. Эрик Истабрукс (Eric Estabrooks) написал модуль Passwd: :Solar is, но он будет полезен только при работе в Solaris. Если эти поля имеют для вас принципиальное значение или вы хотите действовать наверняка, то, как ни грустно (это противоречит моим рекомендациям использовать getpwent()), но часто проще открыть файл shadow и получить нужные значения вручную.
Учетные записи пользователей
Учетные записи пользователейОткуда тогда все это ворчание? Пользователи вносят в системы и сети, которые мы администрируем, две вещи, значительно их усложняющие: неопределенность и индивидуальность. О неопределенности мы поговорим в следующей главе при обсуждении активности пользователей, а пока обратимся к индивидуальности.
В большинстве случаев пользователи хотят сохранять собственную индивидуальность. И дело даже не в том, что они предпочитают иметь уникальные имена,- они рассчитывают иметь и свое личное «имущество». Они хотят, чтобы у них была возможность сказать: «Это мои файлы. Храню я их в своих каталогах. Печатаю их на моей принтерной квоте. И я могу сделать их доступными на моей домашней странице в Сети». Современные операционные системы ведут учет всей этой информации для каждого пользователя.
Но кто следит за всеми учетными записями в системе или в сети? Кто ответственен за их создание, защиту и распределение? Рискну предподожить и скажу «вы, дорогой читатель». Ну а если не вы лично, то утилиты, которые вы создаете, и которые действуют от вашего имени в качестве посредников. Эта глава призвана помочь читателю справиться с такой ответственностью.
Начнем разговор о пользователях с рассмотрения информации, которая формирует их индивидуальность, и способов хранения этой информации в системе. Начнем мы с пользователей операционной системы всех вариантов Unix, а затем рассмотрим те же проблемы в Windows NT/2000. Для текущих версий 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:
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 состоит из двух слоев:
Самая важная часть WMI, которая отличает ее от обычных реализаций WBEM, - это схема Win32, расширенная схема для информации, специфичной для Win32, построенная на центральной и общей модели. WMI также добавляется к общей структуре WBEM, обеспечивая механизмы доступа к данным CIM, специфичные для Win32. Используя это расширение схемы и набор методов доступа к данным, мы можем выяснить, как управлять процессами из Perl средствами WMI.
Два из этих методов доступа: ODBC (открытый интерфейс взаимодействия с базами данных) и COM/DCOM (модель составных компонентов распределенная модель составных компонентов) будут более полно рассмотрены в других главах этой книги. В примерах будет использоваться модель COM/DCOM, поскольку ODBC разрешает лишь запрашивать информацию у WMI (хотя и в простой, похожей на присущую базам данных манере). COM/DCOM позволяет и запрашивать информацию, и взаимодействовать с ней, что очень важно для «управляющей» части контроля над процессами.
Приведенные далее примеры программ на Perl не выглядят такими уж трудными, и вас могут удивить слова «очень быстро становится чересчур сложным». Приведенный ниже код выглядит простым, потому что:
Сначала нужно установить соединение с пространством имен (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');
Сложный способ включает в себя:
usa Win32::OLECirT);
Sprocschm = Win32::OLE->GetObject(
'winmgmts: {impersonationLevel=impersonate}! Wiri32_Process ')
or die "Невозможно создать объект сервера: ".Win32: :OLE->LastError()."\n":
Теперь, когда у нас есть объект Win32_Process, можно с его помощью получить нужные части схемы, представляющие собой процессы в Win32. В их число входят все доступные свойства и методы Win32_Pro-cess, которые годятся к употреблению. Применяемая программа довольно проста; единственно, что не вполне очевидно, - это использование оператора in в Win32: :01_Е. Чтобы объяснить это, нам придется немного отклониться от темы.
Объект $procschm имеет два специальных свойства: Properties и Methods. В каждом из них хранится специальный дочерний объект, известный как collection object в терминологии СОМ. Объект collection object является родительским контейнером для других объектов; в этом случае в них хранятся объекты описания свойств (Properties_) и методов (Methods) схемы. Оператор in возвращает массив ссылок на каждый дочерний объект контейнера. Располагая таким массивом, можно обойти все его элементы в цикле, возвращая на каждой итерации свойство Name каждого дочернего объекта. О других известных применениях in можно узнать из раздела «ADSI (Интерфейсы активных служб каталогов)» главы 6. Вот как выглядит сама программа:
use Win32::OLE('in'):
соединяемся с пространством имен, даем указание действовать
с правами пользователя и получаем объект Win32_process,
просто используя отображаемое имя
Sprocschm = Win32 -OLE->GetObject(
'winmgmtr, : {impersor,ationl_evel = impersonate} ' win32._Process )
or die "Невозможно создать объект сервера: " .i/nn32' .OLE- > Last etc :().
print "--- Prope-ties ---\n";
print join("\n" , map {$_->{Name}
}
(in $procschm->{Properties_} )}:
print "\n--- Methods ---\n";
printoin("\n",map {$_->{Name}
}
(in $procschm->{Methods_l ;}:
Вывод (на машине с NT4.0) выглядит примерно так:
— Properties —
Caption
CreationCiassName
CreationDate
CSCreationClassName
CSName
Description
ExecutablePath
ExecutionState
Handle
InstallDate
KernelModeTime
MaximumWorkingSetSize
MinimumWorkingSetSize
Name
OSCreationClassName
OSNarne
PageFaults
PageFilellsage
PeakPageFileUsage
PeakWorkingSetSize
Priority
Processld
QuotaNonPagedPoolUsage
QuotaPagedPoolUsage
QuotaPeakNonPagedPoolUsage
QuotaPeakPagedPoolUsage
Status
TerminationDate
User'ModeTime
WindowsVersion
WorkingSetSize
--- Methods ---
Create
Terminate
GetOwner
GetOwrierSui
Рассмотрим это подробнее. Чтобы получить список запущенных процессов, нужно запросить все экземпляры объектов Win32_Process:
use Win32::OLE('in'):
it выполняем все первоначальные шаги в одном цикле
$sob] = Win32::OLE->GetOcject(
'winmgmts:{impersonationLeveI=inpersorate}') or die
"Невозможно создать объект сервера: ".Win32: :OLE->LastError() "V"
foreach Sprocess (in $sobj->InstancesOf("Win32_Process")){
print $process->{Name)." имеет pid #".$process->{Process!d}, "\n"; }
Первоначальное отображаемое имя не включает путь к определенному объекту (т. е. мы отбросили ! Win32_Process). Итак, получен объект связи с сервером. В результате вызова метод InstancesOf () возвращает объект-коллекцию (collection object), который содержит все экземпляры конкретного объекта. Наш код обращается к каждому объекту и выводит его свойства Name и Processld. В итоге, у нас есть список всех запущенных процессов.
Чуть менее великодушный подход к обойденным в цикле процессам позволил бы использовать один из методов, указанных в приведенном выше списке:
foreach $process (in $sobj->InstancesOf ("Win32__Process")){
$process->Terminate(1); }
В результате, все работающие процессы будут завершены. Я не рекомендую вам запускать эту программу в таком виде; подправьте ее в соответствии с вашими нуждами, сделав более конкретной.
Теперь у вас есть необходимые знания, чтобы начать использовать WMI для управления процессами. В WMI есть \Уш32-расширения для многих других частей операционной системы, включая реестр и журнал событий.
Вот и все, что мы хотели сказать об управлении процессами в WinNT и 2000. Теперь перейдем к последней операционной системе.
Использование модуля Proc ProcessTable
Использование модуля Proc::ProcessTableДэниел Дж. Урист (Daniel J. Urist) (с помощью нескольких добровольцев) написал модуль Proc: :ProcessTable, предоставляющий единый интерфейс к таблице процессов для всех основных вариантов операционной системы Unix. Он скрывает от вас причуды различных реализаций /ргос и kmem, позволяя писать более переносимые программы.
Просто загрузите модуль, создайте объект Ргос: :ProcessTable: :Proces-и используйте методы этого объекта:
use Proc: :ProcessTable;
$tobj = new Proc: : ProcessTable;
Этот объект использует механизм связанных переменных (tied variable) для представления текущего состояния системы. Для обновления! этого объекта не требуется вызывать специальную функцию - он перечитывает таблицу процессов при каждом обращении к нему. Это похоже на хэш %Process, знакомый нам по обсуждению модуля Мае : Р ses ранее в этой главе.
Чтобы получить нужную информацию, следует вызвать метод
: : Sproctable = $tobj->table();
table() возвращает ссылку на массив, элементы которого представляют собой ссылки на объекты процессов. Каждый из этих объектов имеет свой собственный набор методов, возвращающих информацию об этом процессе. Например, вот как можно получить список идентификаторов процессов и их владельцев:
use Proc::ProcessTable:
Stobj = new Proc::ProcessTable:
Sproctable = $tobj->table(); for (ia>$proctable){
print $_->pid."\t". getpwuid($_->uid)."\n";
}
Список методов, доступных в вашей операционной системе, можно получить при помощи метода fields() объекта Proc: : ProcessTable (т.е. $tobj).
В Proc:: ProcessTable также есть три дополнительных метода у каждого объекта процесса: kill(), priorityO и pgrp(), которые являются всего лишь интерфейсом к встроенным функциям, упомянутым в начале этого раздела.
Чтобы опять вернуться к общей задаче, посмотрим на применение способов контроля над процессами. Мы начали изучать управление процессами в контексте действий пользователя, поэтому сейчас рассмотрим несколько маленьких сценариев, посвященных этим действиям. В примерах мы будем использовать Proc:: ProcessTable в Unix, но сами идеи не зависят от операционной системы.
Первый пример из документации по Proc: : ProcessTable: use Proc::ProcessTable;
$t = new Proc::ProcessTable; foreach $p ((5>{$t->table}){
if ($p->pctmem > 95){ $p->kill(9);
}
}
Эта программа «отстреливает» все процессы, занимающие 95% памяти в тех вариантах операционной системы Unix, где поддерживается метод pctmem() (а он поддерживается в большинстве случаев). В таком виде пример, вероятно, слишком «безжалостен», чтобы использовать его в реальной жизни. Было бы благоразумно добавить перед командой kill() что-то подобное:
print "собираемся убрят;. " Sn-^pid. "\t". get. owuid($p->uid).
"Vi": print "выполнять9 (yes'''i'0 " chomp($ans = о):
next unless (Sans eq "yes"):
Здесь может возникнуть состояние перехвата: не исключено, что во время задержки, вызванной вопросом к пользователю, состояние системы изменится. Учитывая, что мы в данном случае работаем только с «большими» процессами, которые вряд ли меняют свое состояние в течение короткого времени, такой вариант, скорее всего, пройдет нормально. Если вы хотите подойти к этому вопросу более педантично, вам, наверное, стоит получить сначала список процессов, которые вы хотите завершить, спросить пользователя, а затем проверить еще раз состояние таблицы процессов и только потом их завершать.
Бывают случаи, когда завершение процесса - это слишком легкая расплата. Иногда важно засечь, что процесс действительно работает, чтобы предпринять необходимые меры (скажем, поставить пользователя на место). Например, политика нашего сайта запрещает применять IRC-роботы. Роботы - это процессы-демоны, которые соединяются с IRC-серверами и выполняют автоматические действия. И хотя роботы могут использоваться в благих целях, в настоящее время они, в основном, играют асоциальную роль в IRC. Кроме того, мы обращали внимание на взлом системы безопасности из-за того, что первое (и часто единственное), что делал взломщик, — это запускал IRC-робота. Короче говоря, нам важно заметить присутствие таких процессов, а не завершать их работу.
Чаще других сейчас используется робот под названием eggdrop. Выяснить, запущен ли в системе процесс с таким именем, можно при помощи следующей программы:
use Proc::ProcessTable;
open(LOG, "»$logf ile") or die
"Невозможно открыть журнал для дозаписи:
$t = new Proc::ProcessTable; foreach $p (@{$t->table})
{ if ($p->fname() =" /eggdrop/i){ print LOG time."\t".
getpwuid($p->uid).
"\t".$p->fname()."\n": >
}
close(LOG);
Тот, кто подумает: «Эта программа не так уж и хороша! Все, что нужно сделать, чтобы ускользнуть от этой проверки, так это всего лишь переименовать исполняемый файл», будет абсолютно прав. Мы попытаемся написать менее простодушный код, ищущий роботов, в самом последнем разделе этой главы.
А пока рассмотрим еще один пример, в котором Perl помогает управлять процессами пользователей. До сих пор все наши примеры касались отрицательных явлений. Рассмотренные программы имели дело со злонамеренными или жадными к ресурсам процессами. Теперь посмотрим на что-нибудь более жизнерадостное.
Существуют ситуации, когда системному администратору необходимо узнать, какие программы применяются пользователями в системе. Иногда это необходимо сделать для программного обеспечения, лицензия которого запрещает его одновременное использование сверхнормативным числом потребителей. В таких случаях обычно применяется специальный механизм. Иногда подобные сведения необходимы, чтобы иметь возможность перейти на другую систему. Если вы переносите пользователей с одной системы на другую, вам необходимо убедиться, что все программы, работающие на старом месте, будут доступны и на новом.
Один подход к решению этой задачи - заменить каждую доступную пользователям исполняемую программу, не входящую в состав операционной системы, на оболочку, которая сначала запишет, какая программа была вызвана, а затем и запустит ее. Это сложно реализовать, если в системе доступно множество программ. Кроме того, есть и побочный эффект - запуск каждой программы требует большего времени.
Если точность не важна и достаточно знать только приблизительную оценку набора работающих программ, можно применить Ргос::Рго-cessTable. Ниже приведена программа, которая активизируется каждые пять минут и проверяет состояние текущих процессов. Она просто ведет учет всех найденных имен процессов, причем те процессы, которые встречались в предыдущий раз, она во второй раз не учитывает. Ежечасно программа записывает результаты и начинает подсчет заново. Пятиминутное ожидание объясняется тем, что обход таблицы процессов является ресурсоемкой операцией (обычно), а мы хотим, чтобы эта программа как можно меньше загружала систему:
use Proc::ProcessTable;
Sinterval = 600;
5 минут перерыва
Spartofhour = 0; (f отмечаем позицию часа, в которой мы находимся
Stop] = new Proc: : ProcessTabie;
создаем човый объект
tt вечный цикл, сбор данных каждые Sintervai секунд
№ и сброс этих данных один раз в час
while(1){
ucollectstats;
&dumpandreset if (Spartofhour >= 3600);
sleep($interval), }
сбор статистики по пролессу sub collectsrars ( my($process);
foreach Sprocess (@{$tobj->table}){
мы должны игнорировать себя next if ($process->pid() == $$);
сохраняем информацию о процессе для следующего запуска
push(@last,Sprocess->pid(),$process->fname());
игнорируем процесс, если мы его уже видели
next if ($last{$process->pid()} eq $process->fname());
если не видели, запоминаем его
$collection{$process->fname()}++; }
и устанавливаем хэш %last, используя текущую таблицу %last = ©last;
Spartofhour += $interval; }
выводим результаты и сбрасываем значения счетчиков
sub dumpandreset{
print scalar localtime(time). C'-"x50),"\n";
for (sort reverse_value_sort keys %collection){ write;
undef %collection; Spartofhour = 0; }
(обратная) сортировка no значениям хэша %collection и по
именам ключей
sub reverse_value_sort{
return $collection{$b} <=> $collection{$a} || $a cmp $b;
}
format STDOUT = @««««« »»
$_, $collection{$._}
format STDOUT_TOP = Name Count
Существует множество способов улучшить эту программу. Она могла бы отслеживать процессы для каждого пользователя (т. е. записывать один экземпляр вызова программы для каждого пользователя), собирать ежедневную статистику, выводить информацию в виде диаграммы и т. д. Все зависит только от того, где вы хотите ее применять.
Использование модуля Win32 IProc
Использование модуля Win32::IProcВторой подход - применять модуль Win32: : IProc Амина Мюлэй Рамдэна (Amine Moulay Ramdane). И хотя название подсказывает, казалось бы, очевидный выбор, но Win32: : Iproc, в действительности, гораздо полезнее для нас, чем Win32: :Process. У Win32: : Process есть один значительный недостаток, который тут же выводит модуль из борьбы: он создан для работы с процессами, которые были запущены им самим. В то время как нас больше интересуют процессы, запущенные другими пользователями. Если вам не удается установить модуль Win32: :IProc, загляните в раздел «Информация о модулях из этой главы» .
Сначала необходимо создать объект процесса подобным образом:
use Win32::IProc;
и обратите внимание на регистр.
Обязательно должно быть "IProc"
$pobj = new Win32::IProc or die
"Невозможно создать объект proccess: $!\n";
Такой объект обычно используется в качестве трамплина, с которого запускаются методы объекта. Например, чтобы получить список всех запущенных процессов, можно написать:
$pobj-> EnumProccesses(\@processlist) or
die "Невозможно получить список процессов:$!\n";
@processlist - это массив ссылок на анонимные хэши. В каждом анонимном хэше есть два ключа: ProcessName и Processld с их значениями. Такой код позволяет аккуратно вывести нужную информацию:
use Win32::IProc;
$pobj=new Win32::IProc or die
$pobj->EnumPrecesses(VSprocessiis и or
die "Невозможно получитэ список процессор:$г\п';
foreach Sprocess (@processlist){
$pid = $process-x{ProcessId};
Snair.e - $cessNama}; write:
}
format STDOUT_TCP =
Process ID Process Name
format STDOUT =
@<««« @««« ««««««««««««
$pid, $name
Получаем результат:
Process ID Process Name
======= ===========
0 System-Idle
2 System
25 smss.exe
39 winlogon.exe
41 services.exe
48 lsass.exe
78 spoolss.exe
82 OKSERVICE.EXE
Отличие от действий pulist.exe заключается в том, что Win32: : IP roc не может сообщить вам пользовательский контекст для процесса. Если эта информация для вас важна, то следует применять pulist.exe.
pulist.exe может вывести только один тип информации, а что может Win32: - сейчас будет ясно. Допустим, вам хочется узнать не только о запущенных процессах, но и о том, какие исполняемые программы и динамически загружаемые библиотеки (.сШ) использует каждый процесс. Получить эту информацию просто:
импортируем постоянную FULLPATH.
чтоб показывать пути к
библиотекам, может быть и NOPATH use
Win32:.IProc "FULLPATH": Spobj = пел Win32::IProc:
$pobj ->EnuTiProcesses(\5processlist) or die
"Невозможно список процессов : $;
foreach Sp^ocess (?processlisr){ print "\n".
$p^ocess->(D''ocessNaTie!
"\n".(' = ' x length($orocess->{ProcessNa~e})!. "-n":
Vswodules,FULLPATH); print join("\n", map {lc $_->
{Modul'eName}} ^modules), "\n":
}
GetProcessModules() получает идентификатор процесса, ссылку на массив и флаг, говорящий о том, возвращать ли полный путь к библиотеке. Элементами массива, на который мы ссылаемся, являются ссылки на анонимные хэши, содержащие информацию о каждой библиотеке, используемой этим процессом. В нашем примере собираются имена всех библиотек. тар() используется для того, чтобы обойти весь массив ссылок, разыменовывать каждый анонимный хэш и получить значение ключа ModuleName.
Вот отрывок полученных данных:
smss.exe
\systemroot\system32\smss.exe c:\winnt\system32\ntdll.dll
winlogon.exe
\??\с:\winnt\system32\winlogon.exe
c:\winnt\system32\ntdll.dll
c:\winnt\system32\msvcrt.dll
c:\winnt\system32\kernel32.dll
c:\winnt\system32\advapi32.dll
c:\winnt\system32\user32.dll
c:\winnt\systein32\gdi32.dll
c:\winnt\system32\rpcrt4.dll
c:\winnt\system32\userenv.dll
c:\winnt\system32\shell32.dll
c:\winnt\system32\shlwapi.dll
с:\winnt\system32\comctl32.dll
c:\winnt\system32\netapi32.dll
С:\winnt\system32\netrap. dll
c:\winnt\system32\samlib.dll
c:\winnt\system32\winmm. dll
с:\winnt\system32\cwcmmsys.dll
c:\winnt\system32\cwcfm3.dll
c:\winnt\system32\msgina.dll
c:\winnt\system32\rpclts1.dll
c:\winnt\system32\rpcltcl. all. . .
Но давайте пойдем еще дальше. Совсем немного усилий следует приложить, чтобы больше узнать о запущенных процессах. Для получения необходимой информации сначала нужно определить дескриптор этого процесса.
Дескриптор процесса можно рассматривать как открытое соединение с данным процессом. Чтобы выяснить разницу между дескриптором процесса и его идентификатором, проведем аналогию со стоянкой прицепов. Если каждый прицеп на стоянке считать процессом, то адрес прицепа можно считать идентификатором процесса. Это способ найти нужный прицеп. Дескриптор процесса - это что-то наподобие телефонных линий, труб и проводов, подведенных к прицепу. Когда прицеп подсоединен к этим коммуникациям, вы можете не только найти нужный прицеп, но и передавать в него информацию.
Для получения дескриптора процесса, если у нас есть его идентификатор, используем метод Ореп() из модуля Win32: : IProc. Ореп() принимает идентификатор процесса, флаг доступа, флаг наследования и ссылку на скалярное значение, в котором хранится дескриптор. В следующем примере запроса мы будем использовать флаги доступа, которых достаточно для получения информации о процессе. Подробную информацию об этих флагах можно найти в документации по Win32: :IProc и в разделе «Processes and Threads» документации Win32 SDK по основным службам, которую можно найти на http://msdn.microsoft.com. Дескрипторы процессов, открытые при помощи Ореп(), необходимо закрыть при помощи CloseHandle().
Зная дескриптор процесса, можно использовать метод Kill() для завершения его работы:
завершить процесс и заставить его вернуть именно этот код
$pobj->Kill($handle.$exitcode);
Но дескрипторы процессов следует применять не только для того, чтобы завершать работу процесса. Например, можно использовать такие методы, как GetStatusQ, чтобы больше узнать о процессе. Вот пример кода, который выводит информацию о времени для заданного идентификатора процесса:
use Win32::IProc qw(
PROCESS_QUERY_INFORMATION INHERITED DIGITAL);
$pobj = new Win32::IProc;
$pobj->Open($ARGV[0],PROCESS_QUERY_INFORMATION, INHERITED. \$handle) or warn
"Невозможно получить дескриптор:".$pobj->LastError()."\n";
ft DIGITAL = время в понятном формате $pouj->
GetStatus($handle,\$statusinfo.DIGITAL):
$pobj->CloseHandle($handle):
while ((Sprocname,$vaiue)=eacn %$statusinfo){
print "$procname: $value\n":
}
В результате получается что-то приблизительно следующее:
Kernelrii?L-: 00;00:22:442:270 Fxituate:
ExitTime:
CreationDate: 29/7/1999 CreationTime:
17:09:28:100
UserTime:
00:00:11:566:632
Теперь известно, когда процесс был запущен и сколько системного времени он занимает. Поля ExitDate и ExitTime пусты, поскольку процесс все еще активен. Вы могли бы спросить, как эти поля, в принципе, могут оказаться не пустыми, если для получения дескриптора нужно использовать идентификатор работающего процесса? На этот вопрос есть два ответа. Во-первых, можно получить дескриптор для работающего процесса, а затем заставить этот процесс завершиться до того, как вы закроете дескриптор. GetStatusO в таком случае вернет информацию о завершении работы для умершего процесса. Вторая возможность получить эту информацию- использовать метод Сгеate(), о котором мы пока еще не знаем.
Create О
позволяет запускать процессы из Win32: так же, как и в случае с Win32::Process. Если запустить процесс при помощи модуля, то объект процесса ($pobj), который до сих пор не обсуждался, будет содержать информацию о самом процессе и потоках. Обладая этой информацией, вы сможете делать любопытные вещи, например, манипулировать приоритетами потоков и окнами этого процесса. Мы не собираемся рассматривать эти возможности, но упомянуть о них следует, чтобы спокойно перейти к следующему модулю.
Использование модуля Win32 Setupsup
Использование модуля Win32::SetupsupЕсли упоминание о манипуляции окнами процесса, приведенное в конце предыдущего раздела, возбудило ваше любопытство, вам понравится наш следующий подход. На этот раз мы рассмотрим модуль Win32: :Setupsup Йена Хелберга (Jens Helberg). Он называется «Setup-sup», потому что первоначально был разработан для использования при установке программного обеспечения (при частом применении программы Setup.exe).
Некоторые инсталляторы можно запускать в так называемом «тихом режиме» для полной автоматизации установки. В этом режиме они не задают никаких вопросов и не просят нажимать кнопки «ОК>>, освобождая администратора от необходимости сидеть нянькой при инсталляторе. Если такой режим не поддерживается механизмом установки (а подобных случаев очень много), это сильно усложняет жизнь системного администратора. Win32: .Setupsup помогает справиться с такими трудностями. Он позволяет найти информацию о работающих процессах и работать с ними (либо завершить эти процессы, если вы того пожелаете).
Обратитесь к разделу «Информация о модулях из этой главы», чтобы узнать, как получить и установить модуль Win32: Используя Win32: :Setupsup, получить список выполняемых процессов очень просто. Вот слегка измененная версия примера, который можно увидеть в последнем разделе:
use Win32::Setupsup:
$machine = "";
получаем список на текущей ма^/не
Win32::Setupsup::GetProcessList
($machine, \@processlist. \@threaalist i a-die
"Ошибка получения списка процессов:
". Win32 :Serupsjjp: :GetLa;:: Ем^г(). ", '
pop(@processlist);
# удалить фальшивую запись, всегда
добавляемую к списку foreach Sprocesslist ((aprocesslist){
$pid = $processlist->{pid}:
$name = $processlist->{name};
write; }
format STDOUT_TOP =
Process ID Process Name
format STDOUT =
@<««« @««««««««««««««
$pid, $name
Завершение процессов тоже очень просто:
KillProcess($pid, Sexitvalule, Ssystemprocessflag) or die
"Невозможно завершить процесс:
".Win32: :Setupsup: ,GetLast.Error()
Два последних аргумента необязательны. Первый завершает процесс и, соответственно, устанавливает его код завершения (по умолчанию это 0). Второй аргумент позволяет вам завершать системные процессы (при условии, что у вас есть право Debug Prog rans).
Но все это очень скучно. Мы можем перейти на другой уровень манипулирования процессами, взаимодействуя с окнами, которые откры ты запущенным процессом. Чтобы вывести список окон, доступных на рабочем столе, применим:
Win32: :Setuosup: : EnuTiWiPdows( \awinduwiisi;) or die
Win32: :Setuosp: :GetUstError( @windowlist
теперь содержит список дескрипторов окон, которые выглядят как обычные числа, если их напечатать. Чтобы узнать больше о каждом окне, можно использовать несколько различных функций. Например, чтобы прочитать заголовки всех окон, воспользуемся функцией GetWindowText():
use Win32::Setupsup;
Win32::Setupsup::EnumWindOws(\@windowlist) or die
"Ошибка получения списка процессов:
".Win32::Setupsup::GetLastError()."\n"
foreach Swhandle (@windowlist){
if (Win32::Setupsup::GetWindowText($whandle,\$text)){
print "$whandle: $text","\n"; }
else { warn
"Невозможно получить текст для Swhandle" .
Win32::Setupsup::Get LastError()."\n"; >
}
Вот небольшой отрывок получаемых данных:
66130: chapter02 - Microsoft Word
66184: Style
194905150:
66634: setupsup - WordPad
65716: Fuel
328754: DDE Server Window
66652:
66646:
66632: OleMainThreadWndName
Как видите, у некоторых окон есть заголовки, а у некоторых их нет. Внимательные читатели могли заметить в этом отрывке еще кое-что любопытное. Окно 66130 принадлежит сеансу Microsoft Word, запущенному в настоящий момент (в нем набиралась эта глава). Окно 66184 смутно напоминает название еще одного окна, связанного с Microsoft Word. Как мы можем узнать, действительно ли эти окна взаимосвязаны?
В Win32: :Setupsup также есть функция EnumChildWindows(), которая позволяет вывести список дочерних окон для любого окна. Используем ее для вывода иерархии текущего окна:
use Win32::Setupsup:
# получаем список
Win32: : Setupsup: : FnunWindows(^.®windowlist) or
die "Ошибка получения процессов:
".Win32: 'Setiipsup: : Gf;: LastError(). "\n":
превращаем список дескрипторов окон в хэш
ЗАМЕЧАНИЕ: в результате преобразования
элементами хэиа становятся обычные числа,
а не дескрипторы окон. Некоторые функции,
например GetWindowProperties
(которую мы скоро рассмотрим),
не могут использовать эти преобразованные v/c.ra.
Будьте осторожны,
for (is>windowlist){$windowlist{$_}++;
}
и проверяем наличие дочерних окон для каждого окна
foreach $whandle (@windowlist){
(Win32: :Setupsup: : EnumChildWindows($whandIe. \ichildren)){
сохраняем отсортированный список дочерних окон
$children{$whandle} = [sort {$a <=>$b} ©children]:
tt удаляем все дочерние окна из главного хзша,
в результате всех итераций %windowlist будет
содержать только родительские окна без
соответствующих дочерних
foreach $child (@children){
delete $wir.dewlist{$child};
}
}
}
обходим в цикле список родительских окон
и тех окон, у которых нет дочерних,
и рекурсивно печатаем дескриптор каждого
окна и его дочерние окна (если они есть)
foreach my $window (sort {$a <=> $b} keys %windowlist){ &printfamily($window,0);
}
выводим дескриптор заданного окна и его дочерних окон
(рекурсивно) sub printfamily {
начальное окно, насколько глубоко мы ушли по дереву?
my($startwindow,Sievel) = @_;
выводим дескриптор окна с соответствующим отступом
print((" " х Slevel)."$startwindow\n").
return unless (exists $children{$startwindow>):
к дочерних окон не дело сделано
противном случае мы должны
roreach Schildwir.dow (@{
}
Есть еще одна функция, о которой надо сказать, перед тем как двигаться дальше: GetWindowPropertiesO. Функция GetWindowPropertiesO вмещает в себя остальные свойства окон. Например, используя GetWindowPropertiesO, можно получить идентификатор процесса, создавшего конкретное окно. Это разумно совместить с некоторыми из только что рассмотренных возможностей модуля Win32: : IProc.
В документации к модулю Win32: :Setupsup есть список свойств, и к ним можно обратиться. Используем одно из них для написания очень простой программы, которая выведет размеры окна на экране. GetWindowPropertiesO принимает три аргумента: дескриптор окна, ссылку на массив, содержащий имена запрашиваемых свойств, и ссылку на хэш, где будут храниться результаты запроса. Вот какой код мы применим для этого:
Win32: :Setupsup: :GetW:
ndowProperties($ARGV[0], ["reef, "id"], \%info);
print "\t" . $info{rect}{top} . "\n";
print $info{rect}{left} . " -" . $ARGV[0] .
"- " . $info{rect}{right} . "\n";
print "\t" , $info{rect}{bottom} . "\n";
Вывод получается несколько вычурным. Вот как выглядит вывод размеров (координат верхнего, левого, правого и нижнего края) окна с дескриптором 66180:
154
272 -66180- 903
595
GetWindowPropertiesO возвращает специальную структуру данных только для одного свойства rect. Все остальные будут представлены в хэше в виде обычных ключей и значений. Если вы не уверены в свойствах, возвращаемых Perl для конкретного окна, воспользуйтесь утилитой windowse, которую можно найти на http://greatis.virtualave.net/ products.htm.
Разве теперь, когда мы знаем, как определить различные свойства окон, не было бы логично научиться изменять некоторые из этих свойств? Например, было бы полезно изменять заголовок окна. С такими возможностями мы могли бы создавать сценарии, использующие заголовок окна в качестве индикатора состояния: "Prestidigitation In Progress ... 32% complete" Чтобы внести эти изменения, достаточно одного вызова функции:
Win32::Setupsup:SetWindow
Text($handle,Stext);
Свойство rect тоже можно установить таким образом. Следующие строки заставляют указанное окно переместиться в заданную позицию экрана:
use Win32::Setupsup;
$info{rect}{left} = 0;
$info{rect}{nght} = 600;
$info{rect}{top} = 10;
$info{rect}{bottom}= 500;
Win32::Setupsup::SetWindow
Properties($ARGV[0], \%info);
Самую впечатляющую функцию я приберег напоследок. При помощи SendKeysO можно послать произвольные комбинации клавиш любому окну на рабочем столе. Например:
use Win32::Setupsup;
$texttosend = "\\DN\\Low in trie gums";
Win32::Setupsup::SendKeys($ARGV[0],Stexttosend, '',0);
В результате, в указанное окно будет послан текст, предваряемый символом «курсор вниз». Аргументы SendKeysO очень просты: дескриптор окна, посылаемый текст, флаг, определяющий, нужно ли активизировать окно для каждого сочетания клавиш, и необязательный интервал между сочетаниями клавиш. Коды специальных символов, таких как «курсор вниз», окружаются обратными слэшами. Список допустимых кодов можно найти в документации к модулю.
С помощью этого модуля мы попадаем на иной уровень управления процессами. Теперь мы можем удаленно управлять приложениями (и частями операционной системы), не взаимодействуя явно с этими приложениями. Нам не нужна поддержка командной строки или специальных API. У нас есть возможность писать сценарии для GUI, что очень полезно во множестве задач системного администрирования.
Изучение структуры ядра
Изучение структуры ядраЯ упомянул эту возможность только для полноты картины. Можно написать программу, которая будет открывать устройство, подобное /dev/ kmem, и обращаться к структурам памяти ядра. Таким образом можно добраться до текущей таблицы процессов и прочитать ее. Учитывая, что сделать это трудно (разобраться вручную в сложной двоичной структуре), а полученный результат не будет обладать абсолютно никакой переносимостью (любое изменение даже версии операционной системы сделает, скорее всего, вашу программу неработоспособной), я настоятельно рекомендую не пользоваться такой возможностью.
Тем, кто все же не прислушается к этому совету, придется вспомнить документацию по функциям pack(), unpack() и заголовочным файлам для вашего ядра. Откройте файл памяти ядра (часто это /dev/kmem), затем выполняйте read() и unpack(). Вам может понадобиться изучить исходники таких программ, как top (ищите на ftp://ftp.groupsys.com/ pub/top), выполняющих эти же задачи, используя большое количество кода на С. В следующем разделе мы рассмотрим слегка улучшенную версию этого метода.
Отслеживание операций с файлами
Попытка найти файлы, открытые другими пользователями, вернее всего сработает, если применять программу, работающую в командной строке, - nthandle Марка Руссиновича (Mark Russinovich), ее можно найти на http://www.sysinternals.com. Она позволяет показать все открытые дескрипторы на определенной системе. Вот как выглядит ее вывод:System pid: 2
10: File C:\WINNT\SYSTEM32\CONFIG\SECURITY
84: File C:\WINNT\SYSTEM32\CONFIG\SAM.LOG
cc: File C:\WINNT\SYSTEM32\CONFIG\SYSrEM
dO: File C:\WINNT\SYSTEM32\CONFIG\SECURITY.LOG
d4: File C:\WINNT\SYSTEM32\CONFIG\DEFAULT
e8: File C:\WINNT\SYSTEM32\CONFIG\SYSTEM.ALT
fc: File C:\WINNT\SYSTEM32\CONFIG4SOFTWARE.LOG
118: File C:\WINNT\SYSTEM32\CONFIG\SAM
128: File C:\pagefile.sys
134: File C:\WINNT\SYSTEM32\CONFIG\DEFAULT. LOG
154: File С:\WNNT\3YSTEM32\CON'FIG;'SOFTWARE
1bO: File \3evice\NafiedPipe\
294: File C:\WINNT\PROFILES\Adnirustrator\ntLSer.aa-.; OG
2a4: File C:\WINNT\PROFILES\AdminisTrator\NTUSEH.DAT
SMSS.EXE pid: 27 (NT AUTHORITY:SYSTEM)
4: Section С:\WINNT\SYSTEM32\5MSS.EXE
c: File С'\WINNT
28: File C:\WINNT\SYSTEM
Можно также запросить информацию по конкретным файлам и каталогам:
> nthandle c:\temp
Handle V1.11
Copyright (С) 1997 Mark Russinovich
http://www.sysinternals.com
WINWORD.EXE pid: 652
C:\TEMP\~DFF2B3.tmp WINWORD.EXE pid: 652
C:\TEMP\~DFA773.tmp WINWORD.EXE pid: 652
C:\TEMP\~DF9l3E.tmp
Программа nthandle позволяет получить эту информацию по конкретному процессу при помощи ключа -р.
Использовать ее из Perl очень просто, поэтому не будем приводить примеры. Вместо этого посмотрим на подобную, но более интересную операцию - аудит.
NT/2000 позволяет эффективно отслеживать изменения в файлах, каталогах или иерархии каталогов. Вы могли бы учитывать постоянное повторение операции stat() над нужным объектом, но это потребовало бы слишком больших затрат процессорного времени. В NT/2000 отслеживание изменений можно поручить операционной системе.
Относительно безболезненно эту работу выполняют два модуля: Win32: :ChangeNotify Кристофера Мадсена (Christopher J. Madsen) и Win32: :AdvNotify Амина Мюлей Рамдана (Amine Moulay Ramdane). В примерах этого раздела будет использоваться второй, т. к. он более гибкий.
Работа с модулем Win32: : AdvNotify- это многошаговый процесс.
На следующем шаге нужно создать следящий поток (monitoring thread) для интересующего нас каталога. Win32: :AdvNotify позволяет следить сразу за набором каталогов, для этого необходимо лишь создать несколько потоков. Мы же будем следить только за одним каталогом:
Sthread = $aob]->StartThread(Directory => 'C:\terr.c'.
Filter => All, WatchSubtree -> 0) or die "Невозможно начать поток\п":
Первый параметр этого метода говорит сам за себя; рассмотрим остальные.
Установив Filter в одно из приведенных значений (табл. 4.1) или в их комбинацию (SETTING 1 | SETTING2 | SETTINGS. ..), можно следить за различными типами изменений.
Отслеживание операций в Unix
Отслеживание операций в UnixДля отслеживания операций с файлами и сетью в Unix можно использовать один и тот же подход. Это один из тех редких случаев, когда вызов внешней программы намного предпочтительней. Вик Абель (Vic Abell) преподнес чудесный подарок системным администраторам, написав программу Isof (LiSt Open Files), которую можно найти на ftp:// vic.cc.purdue.edu/pub/tools/unix/lsof. Isof позволяет отобразить подробную информацию об открытых в настоящий момент файлах и сетевых соединениях на Unix-машине. По-настоящему удивительной эту программу делает ее переносимость. Последняя версия программы (на момент написания этой книги) работает по крайней мере на 18 видах Unix и поддерживает различные версии этих операционных систем.
Вот как выглядит вывод Isof для одного из запущенного мной процесса. Isof выводит очень длинные строки, поэтому, чтобы сделать информацию более читаемой, после каждой строки вывода я добавил пустую строку.
COMMAND PID USER FO TYPE DEVICE SIZE/OFF NODE NAME
netscape 21065 dno cwd VOIR 172,289"; 8192 12129 /-OTie
netscape 21065 dnb txt VREG 172,1246 1438236Д 656749
/net/ arch-solans (fileserver-./vol/systems/arch-solaris)
netscape 21065 dnb txt VREG 32,6 54656 35172
/usr (,/dev/ dsk/cOtOdOs6)
netscape 21065 dnb txt VREG 32;6 146740 6321
/ubr/lro/ libelf.so.1
netscape 21065 dnb txt VREG 32,6 69292 102611
/usr (/dev/ dsk/cOtOdOs6)
netscape 21065 dnb txt VREG 32,6 21376 79751
/usr/iib/ locale/en_US/en_US.so.1
netscape 21065 dnb txt VREG 32,6 19304 5804
/usrЛib/ libmp.so.2
netscape 21065 dnb txt VREG 32,6 98284 22860
usr/onenwi:' lib/libICE.so.6
netscape 21065 dnb txt VREG 32,6 46576 22891
/usr/opftrwiv lib/libSM.so.6
netscape 21065 drib txt VREG 32.6 1014020 5810
/!jsr/::.t; libc.so.1
netscape 21065 dnb txt VREG 32.6 105788 5849
/usr/ln; libm.so.1
netscape 21065 dnb txt VREG 32,6 721924 5806
netscape 21065 ar.b txt VREG 32 6 156196 5774
netscape 21065 ri^t 0. VCHP 2-1.3 Ot73 5853 > .
pseudo/bls@C:3-> ttcoirpat ->lcter"b>:neii->pis netscape 21065 dnb 3u VCHR 13,12 oto 5821
/devices/ pseudo/mm@0:zero
netscape 21065 dnb 7u FIFO Ox6034d264 C;1 47151 PIPE-> Ox6034d1eO
netscape 21065 dnb 8u met Ox6084cb68 Oxfb210er, TCP host. cos.
ne^.. edu:46575->host2.ccs,neu. edu:6000 (ESTABLISHED)
netscape 21065 dnb 29u met 0x60642848 Ot215868 TCP nost, ccs. re.. edu:46758->
www.mind-bright.se:80 (CLOSE_ WAIT)
Из этого примера можно понять, насколько мощна эта команда. Мы можем увидеть текущий рабочий каталог (VDIR), обычные файлы (VREG), символьные устройства (VCHR), каналы (FIFO) и сетевые соединения (inet), открытые этим процессом.
Самый простой способ применить программу Isof из Perl - вызвать ее в специальном режиме «field» (-F). В этом режиме вывод программы делится на специальным образом отмеченные и разделенные поля, вместо использования колонок в стиле ps. Это позволяет надежно проанализировать и распознать вывод.
У этого способа вывода результатов есть одна особенность. Вывод организован в виде «наборов процессов» (process sets) и «наборов файлов» (file sets), как их называет автор. Набор процессов - это набор полей, относящихся к одному процессу; набор файлов - это подобный же набор для файла. Все приобретет больший смысл, если включить режим разбивки на поля с параметром 0. В этом случае поля будут разделены символом NUL (ASCII 0), а наборы - символом NL (ASCII 12). Вот как будет выглядеть предыдущий вариант вывода команды, если использовать режим разбивки на поля (NUL представлен в виде символов ~@):
p21065~@cnetscape~@u6700"@Ldnb~@
fcwd"@a ~@1 "@tVDIR"@DOx2bOOb4b"@s8192"@i12129"@n/home/dnb"@
ftxt"@a ~@1 >tVREG"@DOx2b004de"@s14382364"@i656749"@n/net/arch-solaris (fileserver:/vol/systems/arch-solaris)"@
ftxt~@a "@1 ~@tVREG"(5)DOx800006~@s54656~@i35172~@n/usr (/dev/dsk/cOtOdOs6)"@ ftxt"@a "@1 "@tVREG"@OOx800006"@s146740"@i6321"@n/usr/lib/libelf.so.1"@ ftxt"@a "@1 "@tVREG"@DOx800006"@s40184"@i6089"ian/usr (/dev/dsk/cOtOdOs6)"@ ftxt"(5>a "@1 "@tVREG"@DOx800006"@s69292"@i102611"@n/usr (/dev/dsk/cOtOdOs6)"@
ftxt"@a ~@1 •@tVREG"@DOx800006"@s21376"@i79751"@n/usr/lib/locale/en_US/ en_US.so.1"@
ftxt"@a ~@1 "@tVREG"№Ox800006"@s19304"@i5804"@n/usr/lib/libmp. so. 2"@
ftxt"@a "@1 ~@tVREG"@DOx800006"@s98284"@i22860"@n/L>sr/openwin/lib/ libICE.so.6"@ ftxt~@a ~@1 "@tVREG"@DOx800006"@s46576"@i22891"@n/usr/openwin/lib/ libSM.so.6"<°>
ftxt"@a ~@1 "@tVREG"@DOx800006"@s1014020"@i5810"@n/usr/lib/libc.so.1"@ ftxt~@a ~@1 ~@tVREG~№Ox800006~@s105788~@i5849~n/usr/lib/libm. so. 1"@ ftxt"@a "@1 "§tVREG"@DOx800006'@s721924"@i5806"@n/usr/lib/libnsl.so.Г@
ftxt"@a "@1 "@tVREG"@DOx800006"§s166196"@i5774"@n/usr/lib/ld.so.Г@ fO"@au~@l "@tVCHR"@DOx600003"iao73~(ai5863"(an/devices/pseudo/ pts@0:3->ttcompat->ldterm->ptem->pts"@
f3"@au"@l "@tVCHR"@DOx34000c"@oO"§i5821"@n/devices/pseudo/mm(s)0:zero"(8i f7"@au"@l "@tFIFO"@dOx6034d264"@o1"@i47151"@nPIPE->Ox6034d1eO"@
f8"@au"@l "@tinet"@dOx6084cb68"@o270380692"(9>PTCP''@nhost.ccs.neu.edu:46575-> host2.cos.neu.edu: 6000"@TST=ESTABLISHED~@
f29"@au"@l "@tinet"@dOx60642848"@o215868~@PTCP"@nhost.ccs.neu.edu:46758-> www.mindbright.se: 80"(a>TST=CLOSE_WAIT~@
Давайте разберемся с этими данными. Первая строка - это набор процессов (это можно понять по первому символу р):
p21065~(acnetscape~@u6700~@Ldnb~(a
Каждое поле начинается с буквы, определяющей его содержимое (о -идентификатор процесса (pid), с - команда, и - идентификатор пользователя (uid) и L - имя пользователя (login)), и заканчивается символом разделителя. Все поля в строке создают набор процесса. Все последующие строки вплоть до очередного набора процесса описывают открытые файлы/сетевые соединения процесса, описываемого своим набором.
Давайте используем этот режим. Если необходимо вывести список всех открытых файлов и процессов, обращающихся к ним, можно применить такую программу:
use Text::Wrap;
Slsofexec = "/usr/local/Din/lsof"; 8 путь к Isof
8 режим (F)ield, разделитель NUL (0), показывать (L)ogin,
8 тип файла (t)ype и имя файла (n)ame
$lsofflag = "-FLOtn";
open(LSOF,"$lsofexec $lsofflag[") or
die "Невозможно запустить $lsofexec:$!\n";
while(
8 работаем с набором процесса if (substr($_,0,1) eq "p"){
($pid,$login) = split(/\0/);
$pid = substr($pid,1,length(Spid)); }
# работаем с набором файла.
# Замечание: мы интересуемся только обычными файлами
if (substr($_,0,5) eq "tVREG"){
($type,$pathname) = split(/\0/);
# процесс может дважды открыть один и тот же файл,
# поэтому мы должны убедиться, что запишем его
# только один раз
next if ($seen{$pathname} eq $pid); $seen{$pathname} = $pid;
Spathname = substr($pathname,1,length($pathname));
push(@{$paths{$pathname}},$pid);
}
close(LSOF);
for (sort keys %paths){
print "$_:\n";
print wrap("\t","\t",join(" ",@{$paths{$_}})),"\n";
}
В этом случае Isof будет показывать только некоторые из полей. Можно обойти в цикле весь вывод, собирая имена файлов и идентификаторы процессов в хэш списков. Когда будут обработаны все выведенные данные, следует ввести имена файлов в виде отформатированного списка процессов (спасибо Дэвиду Шарноффу (David Muir Sharnof f) за модуль Text: :Wrap):
/usr (/dev/dsk/cOtOdOs6):
115 117 128 145 150 152 167 171 184 191 200 222 232 238
247 251 276 285 286 292 293 296 297 298 4244 4709 4991
4993 14697 20946 21065 24530 25080 27266 27603
/usr/bin/tcsh:
4246 4249 5159 14699 20949
/usr/bin/zsh:
24532 25082 27292 27564
/usr/dt/lib/libXm.so.S: 21065 21080
/usr/lib/ld.so.1:
115 117 128 145 150 152 167 171 184 191 200 222 232 238
247 251 267 276 285 286 292 293 296 297 298 4244 4246 4249 4709 4991
4993 5159 14697 14699 20946 20949 21065
21080 24530 24532 25080 25082 25947 27266 27273 27291
27292 27306 27307 27308 27563 27564 27603
/usr/lib/libc.so.1:
267 4244 4246 4249 4991 4993 5159 14697 14699 20949
21065 21080 24530 24532 25080 25082 25947 27273 27291
27292 27306 27307 27308 27563 27564 27603
Чтобы показать последний код, относящийся к отслеживанию операций с файлами и сетью в Unix, вернемся к поиску запущенных IRC-po-ботов из приведенного ранее примера. Существует более надежный способ найти такие процессы-демоны, чем изучение таблицы процессов. Пользователь может скрыть имя робота, переименовав исполняемый файл, но для того чтобы спрятать сетевое соединение, ему придется очень хорошо потрудиться. В большинстве случаев это соединение с сервером на портах 6660-7000. Программа Isof позволяет без труда отыскивать такие процессы:
Slsofexec = "/usr/local/bin/lsof" Slsofflag = "-FLOc -iTCP: 6660-7000";
ft это срез хэша. используемый для предварительной загрузки
и таблицы хэшей, существование этих ключей мы будем проверять
и позже. Обычно это записывается так:
ft %approvedclients = ("ircll" => undef, "xirc" => undef, ...);
ft (но такой вариант - отличная идея, воплощенная Марком-
ft Джейсоном Доминусом(МагК-иа50п Dominus))
@>approvedclients{"ircH" , "xirc" , "pirc"} = ();
open(LSOF, "$lsofexec $lsofflag|") or die "Невозможно запустить $lsofexec:$! \n" ;
while(
($pid,$command,$login) = /p(\d+)\000
c(.+)\000 L(\w+)\000/x;
warn "$login использует неразрешенный клиент с именем
$conimand (pid $pid)l\n" unless (exisTs $approvedclients{$command});
close(LSOF);
Это самая простая проверка из всех возможных. Она позволяет отловить тех, кто догадается переименовать eggdrop б pine или -tcsh, и тем более тех, кто даже не попытается спрятать своего робота. Тем не менее, она подвержена тому же недостатку, что и предыдущий вариант тестирования. Если пользователь достаточно умен, он может переименовать робота во что-то из списка «разрешенных клиентов». Чтобы продолжить игру в кошки-мышки, можно предпринять еще как минимум два шага:
Рекомендуемая дополнительная литература
Рекомендуемая дополнительная литератураhttp://pudget.net/macperl -
домашняя страница модулей Криса Нан-дора (Chris Nandor). Нандор один из самых активных разработчиков модулей MacPerl (и соавтор книги, ссылка на которую приведена ниже). http://www.activestate.com/ support /mailing_lists.htm.
Здесь находятся списки рассылки Perl-Win32 Admin и Perl-Win32-Users. Оба списка и их архивы - просто бесценные ресурсы для программистов Win32. http://www.microsoft.com/management
- домашняя страница всех технологий управления Microsoft, включая WMI. http://www.sysinternals.com -
домашняя страница программы nthan-dle (на этом сайте она называется просто Handle) и многих других полезных утилит для NT/2000. На родственном сайте http:// www.winternals.com продаются отличные коммерческие утилиты. «MacPerl:Power and Ease»,
Vicki Brown, Chris Nandor (Prime Time Freeware, 1998) - лучшая книга о модулях для MacPerl. Стоит также обратить внимание на веб-сайт издателя http://www.macperl.com. http://www.dmtf.org-
домашняя страница Distributed Management Task Force и просто хороший источник информации по WBEM. http://www.mspress.com -
издатели Microsoft NT Resource Kit. Можно зарегистрироваться и получить доступ к последним утилитам из RK.
Параметры Filter в Win32 AdvNotifу
Таблица 4.1. Параметры Filter в Win32::AdvNotifу| Параметр | Отмечает |
| FILE_NAME | Создание, удаление, переименование файла(ов) |
| DIR_NAME | Создание или удаление каталога(ов) |
| ATTRIBUTES | Изменение атрибутов любого каталога |
| SIZE | Изменение размера любого файла |
| LAST_ WRITE | Изменение даты модификации файла(ов) |
| CREATION | Изменение даты создания файла(ов) |
| SECURITY | Изменение информации безопасности (ACL и пр.) файла(ов) |
StartThreadO создает поток, но проверка начинается только после того, как поступает распоряжение об этом:
$thread->EnableWatch() or die
"Невозможно начать наблюдение\п";
Существует также функция OisableWatch(), которую необходимо использовать в программе для завершения проверки.
Мы следим за нужным объектом, но как узнать, изменилось ли что-нибудь? Надо придумать что-то, что позволило бы потоку сообщить нам об изменениях, за которыми мы наблюдаем. Здесь тот же подход, что и в главе 9 «Журналы» при обсуждении сетевых сокетов. Обычно следует вызывать функции, которые заблокированы до тех пор, пока ничего не происходит:
while($thread->Wait(INFINITE)){
print "Что-то изменилось1\п":
last if ($changes++ == 5):
}
Этот цикл while() вызовет метод Wait() для нашего потока. До тех пор пока потоку нечего сообщить, вызов будет заблокирован. Обычно Wair() принимает в качестве параметра число миллисекунд, равное времени ожидания. Мы же передаем специальное значение, которое соответствует «бесконечному ожиданию». Когда Wait() возвращает значение, следует вывести сообщение и ждать дальше, если только уже не были замечены пять других изменений. Теперь можно закончить:
$thread->Terminnte() undef $aobj;
Наша программа пока еще не очень полезна. Нам известно, что что-то изменилось, но мы не знаем ни что изменилось, ни как это произошло. Чтобы исправить эту ситуацию, изменим тело цикла wniie() и добавим определение формата:
while($thread->Wait(INFINITE)){
while ($thread->Read(\@status)){ foreach Sevent (@status){
Sfilename = $event->{FileName); $time = $event->{DateTime};
Saction = $ActionName{$event->{Action}}; write; } } }
format STDOUT =
@««««««««« @««««««««« @«««««««««
Sfilename,$time,Saction
format STDOUT_TOP =
File Name Date Action
Основное изменение- это добавление метода Read(). Он получает информацию об изменении и заполняет элементы списка ©status ссылками на хэш. Каждая ссылка указывает на анонимный хэш, который выглядит примерно так:
{'FileName' => "GLF2425.TMP',
'DateTime' => '11/08/1999 06:23:25р',
'Directory' => 'C:\temp', 'Action' => 3 }
Для каждого изменения могут произойти несколько событий, отсюда и необходимость вызывать Read() в цикле while(). Если соответствующим образом разыменовать содержимое этих хэшей и применить к ним форматирование, то получатся примерно такие данные:
File Name Date Acti01
"DF400E.tmp 11/08/1999 07:29:56p
FILE_.ACTION_PEMO\,'F.D
"DF6C5C. fnp 11/08/1999 07'29'56p
F1LF_AC1ION_ADDED
~OF6E66.tmp 11/08/1999 07:29:56р
FILE_ACTION_ADOED
~DF6E5C.tmp 11/08/1999 07:29:56р
FILE_ACriON_REMOVEO
К сожалению, отслеживание операций с сетью в NT/2000 впечатляет намного меньше. В идеале, как администратор вы хотели бы знать, какой процесс (а, следовательно, какой пользователь) открыл сетевой порт. Печально, но я не знаю ни одного модуля и ни одного инструмента, которые могли бы предоставить такую информацию. Существует один коммерческий инструмент, работающий в командной строке, под названием TCPVstat, который может показать связь процессов с использованием сети. TCPVstat можно найти в пакете TCPView Professional Edition, который доступен на http://www.winternals.com.
Если использовать только некоммерческие инструменты, то придется иметь дело лишь со списком сетевых портов, открытых в настоящее время. Для этого следует применять другой модуль Рамдана -Win32: :1рНе1р. Вот как выглядит код, печатающий нужную информацию:
use Win32: -.IpHelp;
# замечание: в данном случае регистр "IpHelp"
имеет значение' my $iobj = new Win32::IpHelp;
# заполняем список хэшем хэшей $iobj->GetTcpTable(\(a>table,1);
foreach Sentry (@table){
print $entry->{LocalIP}->
print $entry->{fiemoteIP}->{Value} . ":" .
Sentry->{RemotePort}->{Value}."\n";
}
Посмотрим, как можно сделать то же самое в Unix.
Мы вкратце рассмотрим четыре различных
Управление процессами в NT/2000Мы вкратце рассмотрим четыре различных способа работы с процессами в NT/2000, поскольку каждый из них открывает перед нами двери к увлекательным возможностям, лежащим за пределами нашего обсуждения. Сначала мы остановимся на двух задачах: поиске всех запущенных процессов и завершении работы некоторых из них.
Используем Microsoft Resource Kit
В главе 3 «Учетные записи пользователей» упоминалось, что NT Resource Kit является отличным источником для сценариев и информации. Из этого пакета будут использоваться две программы: pulist.exe и kill.exe. Первая выводит список процессов, вторая- «убивает» их. В этом пакете есть еще одна утилита tlist.exe, похожая на pulist.exe, которая может вывести все процессы в списке, удобном для чтения, но ей не достает некоторых возможностей pulist.exe. Например, pulist.exe может вывести список процессов не только текущего, но и другого компьютера.
Вот фрагмент из вывода командыpulist:
Process PID User
TAPISRV.EXE 119 NT AUTHORITY\SYSTEM
TpChrSrv.exe 125 NT AUTHORITY\SYSTEM
RASMAN.EXE 131 NT AUTHORITY\SYSTEM
mstask.exe 137 NT AUTHORITY\SYSTEM
mxserver.exe 147 NT AUTHORITY\SYSTEM
PSTORES.EXE 154 NT AUTHORITY\SYSTEM
NDDEAGNT.EXE 46 OMPHALOSKEPSIS\Administrator
explorer.exe 179 OMPHAlOSKEPSIS\Administrator
SYSTRAY.EXE 74 OMPHALOSKEPSIS\Administrator
cardview.exe 184 OMPHAlOSKEPSIS\Administrator
ltmsg.exe 167 OMPHALOSKEPSIS\Administrator
daemon.exe 185 OMPHALOSKEPSIS\Adrdinistrator
Применять pulist.exe из Perl очень просто. Вот один из способов:
$pulistexe = "\\bin\\PULIST.EXE":
местоположение программы open
(PULIST. "$pulistexe|")
"Невозможно выполнить Spulistexe.S! ''
scalar
while(defined($_=
print "$pranie.$pid:$puser\n":
close(PULIST):
Вторая упомянутая программа - это kill.exe. Ее тоже просто использовать. В качестве аргумента она Принимает либо идентификатор процесса, либо часть имени задачи. В целях безопасности я рекомендую использовать идентификаторы процессов, потому что иначе очень легко убить не тот процесс, который нужно.
Программа kill.exe использует два различных способа завершения работы процессов. Один из них - это так называемая «вежливая смерть»: kill.exe
Управление процессами в 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;
}
}
Преимущества такого подхода:
Использование nslookup
Использование nslookupЕсли у вас есть опыт работы в Unix или вы уже программировали на других языках сценариев помимо Perl, то первая попытка может сильно походить на сценарий командного интерпретатора. Внешняя программа, вызываемая из Perl сценария, выполняет всю сложную работу:
use Data::Dumper;
Shostname = $ARGV[0];
Snslookup = "/usr/local/bin/nslookup";
# путь к nslookup ©servers = qw(nameserver1 nameserver2 nameserverS);
имена серверов имен foreach Sserver (©servers) {
&lookupaddress($hostname,Sserver); в заполняем %results }
%inv = reverse %results;
# инвертируем полученный хэш
if (scalar(keys %inv) > 1) {
print "Между DNS-серверами есть разногласияДп";
print Data::Dumper->Dump([\%results],["results"]),"\n"; }
» обращаемся к серверу, чтобы получить IP-адрес и прочую
# информацию для имени узла, переданного в программу в
командной строке. Результаты записываем в хэш %results sub lookupaddress
my($hostname,Sserver) = @_;
open(NSLOOK,"$nslookup Shostname Sserver|") or
die "Невозможно запустить nslookup:$!\n";
while (
« игнорировать, пока не дойдем до "Name: next until (/"Name:/);
следующая строка - это ответ Address: chomp($results{$server} =
# удаляем имя поля
die "Ошибка, вывода nslookup \n" unless /Address/;
$results{$server} =" s/Addfess(es)?:\s+//;
все, с nslookup мы закончили last;
}
close(NSLOOK);
}
Преимущества такого подхода:
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->
tt хэшей } close(DATA);
It печатаем симпатичный заголовок
print "#\n\« host file - GENERATED BY $0\n# DO NOT EDIT BY HAND!\nff\n";
print "« Converted by $user on ".scalar(localtime). "\ntt\n";
# подсчитываем число записей для каждого отдела
Я и сообщаем об этом
foreach my Sentry (keys %entries){
Sdepts{Sentries{Sentry}->{department}}++; }
foreach my Sdept (keys %depts) {
print "n number of hosts in the
Sdept department: $depts{$dept},\n"; )
print "tt total number of hosts: ".
scalar(keys %entries). "\n#\n\n";
tt обходим в цикле все узлы, выводя комментарий и саму запись f
oreach my Sentry (keys %entries) {
print "tt Owned by ", $entries{$entry}->{owner}," (",
$entries{$entry>->{department},"): ",
Sentries{Sentry}->{building}."/",
Sentries{Sentry}->{room},"\n";
print $entries{$entry}->{address},"\t",
$entries{$entry}->{name}." ",
Sentries{Sentry}->{aliases},"\n\n"; }
Самое значительное отличие данного примера от предыдущего - это способ представления данных. Поскольку в предыдущем примере не было необходимости получать информацию из хэша после печати его значений, мы могли использовать единственный хэш. Но в этом случае мы решили прочитать данные из файла в более сложную структуру данных (хэш хэшей), чтобы проанализировать их перед тем как печатать.
Можно было сохранять отдельную хэш-таблицу для каждого поля (подобно тому, как это было сделано в примере needspace из главы 2 «Файловые системы»), но красота приведенного метода состоит в его поддерживаемости. Если затем понадобится добавить в базу данных поле serial_number, нам не придется менять используемый для анализа файла код, это поле само по себе появится. Недостаток же в том, что синтаксис Perl таков, что наш код выглядит более сложным, чем он есть на самом деле.
Посмотреть на все это можно еще проще: мы будем анализировать файл точно так же, как и в предыдущем примере. Разница лишь в том, что каждую запись мы будем сохранять в новом анонимном хэше. Анонимные хэши ничем не отличаются от обычных, только обращаться к ним приходится не по имени, а по ссылке.
Чтобы построить большую структуру данных (хэш хэшей), достаточно связать каждый новый анонимный хэш с основной хэш-таблицей %entries и создать ключ со связанным с ним значением, являющимся ссылкой на этот только что заполненный анонимный хэш. Когда каждый хэш пройдет обработку, ключами %entries будут имена всех машин, а значениями - ссылки на хэш-таблицы, содержащие значения всех полей, связанных с этим именем (IP-адрес, номер кабинета и т. д.).
Вероятно, вам бы хотелось, чтобы вывод был отсортирован по IP-адресам? Никаких вопросов, просто добавьте процедуру сортировки, изменив:
foreach my Sentry (keys %entries) { на:
foreach my Sentry (sort byaddress keys %entries) { и добавьте:
sub byaddress {
@a = split(/\./,$entries{$a}->{address}):
@b = split(/\./.$e"tries{$b}-''{address)):
($a[0]<=>$b[OJ) ,!
($а[1]<=>$Ь[1!) ! i
($a[2]<=>$b[21) !|
($a[3]<=>$D[3]l;
Вот как будут выглядеть отсортированные данные:
И Owned by Cindy Coltranc (IT): west/143 192.168.1,3
tjendir ben bei.doooles
П Owned by David Davis (software): inai'i/909
192.168.1.11 shimmer snm siimr, sniiMiydoodies
n Owned by Ellen Monk (design): rain/1116
192.168.1.12 Sulawesi sula su-lee
# Owned by Alex Rollins (IT): rnain/1101 192.168.1.55
sander sandy micky mickydoo
Сделайте так, чтобы полученные данные вам нравились. Пусть Perl поддержит ваши профессиональные и эстетические стремления.
Проверка работы DNS итеративный подход
Проверка работы DNS: итеративный подходМы потратили значительное время на создание конфигурационных файлов, используемых сетевыми службами имен, но это всего лишь одна из задач системного и сетевого администратора. Для поддержания сети в рабочем состоянии необходимо постоянно проверять данные службы, чтобы убедиться, что они ведут себя верно.
Например, для системного/сетевого администратора очень многое зависит от ответа на вопрос «Все ли DNS-серверы работают?». В ситуации, когда необходимо найти неисправности, практически настолько же важно знать, «Все ли серверы работают с одной и той же информацией?», или, более точно, «Отвечают ли они одинаково на одинаковые запросы? Синхронизированы ли они?». Данный раздел посвящен подобным вопросам.
По главе 2 можно судить, как действует основной принцип Perl «Всегда существует несколько способов сделать это». Именно такое свойство делает Perl отличным языком для «итеративной разработки». Итеративная разработка - это один из способов описания эволюционного процесса, имеющего место при создании программ системного администрирования (и не только), выполняющих определенную задачу. В случае с Perl можно быстро написать рабочую программу на скорую руку, а позднее вернуться к сценарию и переписать его более элегантным образом. Возможно, будет еще и третья итерация, на этот раз уже с использованием другого подхода к решению задачи.
Существует три различных подхода к одной и той же проблеме проверки согласованности DNS. Они представлены в том порядке, которому, действительно, мог бы последовать человек, пытаясь найти решение, а затем его совершенствуя. Этот порядок отражает взгляд на то, как решение проблемы может развиваться в Perl; ибо ваше отношение к подходу может меняться. Третий способ, использующий модуль Net: : DNS, вероятно, самый простой и наиболее защищенный от ошибок. Но существуют ситуации, когда Net: : DNS применять нельзя, поэтому сначала приведем несколько собственных решений. Обязательно обратите внимание на все за и против, перечисленные после каждого рассмотренного подхода.
Вот наша задача: написать сценарий на Perl, принимающий имя узла и проверяющий список DNS-серверов, чтобы убедиться, что все они возвращают одну и ту же информацию об узле. Чтобы упростить задачу, будем считать, что узел имеет единственный статический IP-адрес (т. е. у него один сетевой адаптер и один IP-адрес).
Перед тем как перейти к рассмотрению всех подходов, взглянем на сердцевину кода, который будем применять:
$hostname = $ARGV[0];
©servers = qw(nameserver1 nameserver2 nameserverS);
# серверы имен
foreach $server (servers) {
&lookupadrjress($hostname, $server);
заполняем %results
}
%inv = reverse %results;
# инвертируем полученный хэш
if (keys %inv > 1) {
print "Между DNS-серверами есть разногласия";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]), "\n"; }
Для каждого из DNS-серверов, перечисленных в списке @servers, вызывается подпрограмма &lookupaddress(), которая обращается к DNS-серверу, чтобы получить IP-адрес заданного имени узла, и помещает результаты в хэш %results. Для каждого DNS-сервера в хэше %results есть запись, значением которой является IP-адрес, возвращаемый этим сервером (ключом является имя сервера).
Существует много способов определить, равны ли друг другу значения из хэша %results (т. е. убедиться, что все DNS-серверы возвращают одну и ту же информацию в ответ на запрос). Мы инвертируем хэш %results в другую хэш-таблицу, преобразовывая все ключи в значения и наоборот. Если все значения из %results одинаковы, то в инвертированном хэше должен быть только один ключ. Если ключей несколько, значит, мы выловили прокол, и поэтому вызываем Data: :Duniper->Durrip() для СлужОа доменных имен вывода содержимого %results, над которым будет ломать голову системный администратор.
Вот как может выглядеть примерный результат, если что-то идет не так:
Между DNS-серверами есть разногласия: $results = {
nameserverl => '192.168.1.2',
nameserver2 => '192. 168. 1.5' ,
nameserverS => ' 192. 168. 1.2' ,
Теперь посмотрим на альтернативы подпрограмме &lookupaddress( ).
Работа напрямую с сетевыми сокетами
Работа напрямую с сетевыми сокетамиЕсли вы «продвинутый системный администратор», вы можете решить, что вызывать внешнюю программу не следует. Вы можете захотеть реализовать запросы к DNS, не используя ничего, кроме Perl. Это означает, что нужно будет создавать вручную сетевые пакеты, передавать их по сети и затем анализировать результаты, получаемые от сервера.
Вероятно, это самый сложный пример из всех, приведенных в книге. Написан он после обращения к дополнительным источникам информации, в которых можно найти несколько примеров существующего кода (включая модуль Майкла Фура (Michael Fuhr), показанный в следующем разделе). Вот что происходит на самом деле. Запрос к DNS-серверу состоит из создания специального сетевого пакета с определенным заголовком и содержимым, отправки его на DNS-сервер, получения ответа от сервера и его анализа.
Каждый DNS-пакет (из тех, которые нас интересуют) может иметь до пяти различных разделов:
Header(Заголовок)
Содержит флаги и счетчики, относящиеся к запросу или ответу (присутствует всегда).
Question (Запрос)
Содержит вопрос к серверу (присутствует в запросе и повторяется при ответе).
Answer (Ответ)
Содержит все данные для ответа на DNS-запрос (присутствует в пакете DNS-ответа).
Authority (Полномочия)
Содержит информацию о том, можно ли получать авторитетные ответы.
Additional (Дополнительно)
Содержит любую информацию, которую вернет сервер помимо прямого ответа на вопрос.
Наша программа имеет дело только с первыми тремя из этих разделов. Для создания необходимой структуры данных для заголовка DNS-na-кета и его содержимого используется набор команд oack(). Эти структуры данных передаются модулю 10: :Socket, который посылает их в виде пакета. Этот же модуль получает ответ и возвращает его для обработки (при помощи unpackO). Умозрительно такой процесс не очень сложен.
Но перед тем как посмотреть на саму программу, нужно сказать об одной особенности в этом процессе. В RFC1035 (Раздел 4.1.4) определяются два способа представления доменных имен в DNS-пакетах: несжатые и сжатые. Под несжатым доменным именем подразумевается полное имя домена (например host.oog.org) в пакете. Этот способ ничем не примечателен. Но если это же доменное имя встретится в пакете еще несколько раз, то, скорее всего, оно будет представлено в сжатом виде во всех вхождениях, кроме первого. В сжатом представлении информация (или ее часть) о домене заменяется двубайтовым указателем на несжатое представление этого же доменного имени. Это позволяет использовать в пакете hostl, host2 и hostS в longsubdomain.longsubdomain.oog.org, вместо того чтобы каждый раз включать лишние байты для longsubdo-main.longsubdomain.oog.org. Нам необходимо обработать оба представления, поэтому и существует подпрограмма &decompress. Дальше обойдемся без фанфар и взглянем на код:
use 10: '.Socket;
Shostname = $ARGV[0];
$defdomain = ".oog.org"; # домен по умолчанию
^servers = qw(nameserver1 nameserver2 nameserverS);
имена серверов имен
foreach Iserver (©servers) {
%results
}
%inv = reverse ^results; # инвертируем полученный хэш
if (scalar(keys %inv) > 1) { # проверяем, сколько в нем элементов
print "Между DNS-серверами есть разногласия:\п";
use Data::Dumper;
print Data::Dumper->Dump([\%results],["results"]),"\n";
}
sub lookupaddress{
my($hostname,$server) = @_;
my($qname,$rna(tre,$header,$question,$lformat,@>labels,$count);
local($position,$buf);
Конструируем заголовок пакета
$header = pack("n C2 n4",
++$id, # идентификатор запроса
1, # поля qr, opcode, aa, tc, rd (установлено только rd)
0, # rd, ra
1, один вопрос (qdcount)
0, нет ответов (ancount)
О, п нет записей ns в разделе authority (nscount)
0); tf нет rr addtl (arcount)
если в имени узла нет разделителей,
дописываем домен по умолчанию
(index($hostname,'.') == -1) {
Shostname .= Sdefdomain;
} # конструируем раздел qname пакета (требуемое доменное имя)
for (split(/\./,$riostname)) {
$lformat .= "С а* ";
$labels[$count++]=length;
$labels[$count++]=$_;
}
да конструируем вопрос
да
Squestion = pack($lformat."С п2",
©labels,
0, # конец меток
1, # qtype A
1); # qclass IN
да
да посылаем пакет серверу и читаем ответ
$sock = new 10::Socket::INET(PeerAddr => Sserver,
PeerPort => "domain",
Proto => "udp");
$sock->send($header.$question);
используется UDP, так что максимальный размер пакета известен
$sock->recv($buf,512);
close($sock);
узнаем размер ответа, так как мы собираемся отслеживать
позицию в пакете при его анализе (через Sposition)
Srespsize = length($buf);
распаковываем раздел заголовка
да
($id,
$qr_opcode_aa_tc_rd,
$rd_ra,
Sqdcount,
$ancount,
Snscount,
Sarcount) = unpack("n C2 n4",$buf);
if (!$ancount) <
warn "Невозможно получить информацию для $hostname с Sserver!\n";
return;
}
распаковываем раздел вопроса
tt раздел вопроса начинается после 12 байтов
($position,$qname) =
tt переходим к концу вопроса
Sposition += 4;
nntt
tttttt распаковываем все записи о ресурсах
ttntt
for ( ;$ancount;$ancount--){
(Sposition,$rname) = &decompress($position);
(Srtype,Srclass,$rttl,$rdlength)=
unpack('@'.Sposition.'n2 N n',$buf);
Sposition +=10;
tt следующую строку можно изменить и использовать более
# сложную структуру данных; сейчас мы подбираем
последнюю возвращенную запись
$results{$server}=
join('.',unpack('@'.Sposition.'C'.$rdlength,$buf));
Sposition +=$rdlength; } >
О обрабатываем информацию, "сжатую" в соответствии с RFC1035
# мы переходим в первую позицию в пакете и возвращаем
# найденное там имя (после того как разберемся с указателем
# сжатого формата) и место, которое мы оставили в конце
# найденного имени sub decompress {
my($start) = $_[0]; my($domain,$i,Slenoct);
for ($i=$start;$i<=$respsize;) {
$lenoct=unpack('@'.$i.'C', $buf); n длина метки
if (! Slenoct){ tt 0 означает, что этот раздел обработан
$i++;
last; }
if (Slenoct == 192) { tt встретили указатель,
tt следовательно, выполняем рекурсию
Sdomain.=(&decompress((unpack('@'.$i.'n',$buf) & 1023)))[1];
$i+=2;
last } else { tt в противном случае это простая метка
$domain.=unpack('@г.++$i.'a'.Slenoct,$buf).'. ';
$i += Slenoct; }
return($i,Sdomain);
}
Надо заметить, что эта программа не является точным эквивалентом предыдущего примера, потому что мы не пытаемся эмулировать все нюансы поведения nslookup (тайм-ауты, повторные попытки и списки поиска). Рассматривая все три подхода, представленные здесь, обязательно обратите внимание на такие различия.
Преимущества этого подхода заключаются в следующем:
Рекомендуемая дополнительная литература
Рекомендуемая дополнительная литература«DNS 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:Службы имен TCP/IP
Службы имен TCP/IPВ настоящее время большая часть «разговоров» между компьютерами происходит по протоколу управления передачей (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 содержит:
@ 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 вызваны неполадками при обновлении порядкового номера. Существует по крайней мере два способа сделать так, чтобы порядковый номер всегда увеличивался:
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. В результате, необходимо создать один файл для прямого преобразования имен и один для обратного. Добавить код для работы с несколькими подсетями и зонами (т. е. создать отдельные файлы для каждой) будет несложно.
Вот, вкратце, что мы делаем:
use Res;
Sdatafile = "./database";
база данных узлов
Soutputfile = "zone.$$";
временный файл для вывода
$target = "zone.db";
получаемый файл
$revtarget = "rev.db";
получаемый файл для обратного преобразования
$defzone = ".oog.org";
# создаваемая по умолчанию зона
Srecordsep = "-=-\n";
получаем текущую дату в формате YYYYMMDD
@localtime = localtime;
$today = sprintf("%04d%02d%02d",$localtime[5]+1900,
$localtime[4]+l,
$localtime[3]);
имя пользователя, как в NT/2000, так и
Unix $user = ($"0 eq "MSWin32")? $ENV{USERNAME} :
(getpwuid($<))[6]." (".(getpwuid($<))[0].")"; $/ = Srecordsep;
считываем файл базы данных
open(DATA, Sdatafile) or die "Ошибка! Невозможно открыть datafile;$!\n";
while () {
chomp; # удаляем разделитель записей
разбиваем на key!,value"! @record = split /:\s*|\n/m;
$record ={}; # создаем ссылку на пустой хэш
%{$record} = @record;
# заполняем его значениями из ©record
в ищем ошибки в именах узлов
if ($record->{name} =" /["-.a-zA-ZO-9]/) {
warn "!!!! ",$record->{name} .
встретились недопустимые в именах узлов символы, пропускаем.. Дп";
next; }
# ищем ошибки в псевдонимах
if ($record->{aliases} =" /["-.a-zA-ZO-9\s]/) {
warn "!!!! " . $record->{name} .
встретились недопустимые в псевдонимах символы, пропускаем.. .\п";
next; }
# ищем пропущенные адреса unless ($record->{address}) {
warn "!!!! " . $record->{name} .
нет IP-адреса, пропускаем.. Дп"; next; }
# ищем повторяющиеся адреса
if (defined $addrs{$record->{address}}) {
warn "!!!! Повторение IP-адреса:" . $record->{name}.
" & " . $addrs{$record->{address}} . ", пропускаем. . Дп"; next;
>
else {
$addrs{$record->{address}} = $record->{name};
}
$entries{$record->{name}} = Srecord; # добавляем это в хэш хэшей
}
close(DATA);
Sheader = &GenerateHeader;
создаем файл прямого преобразования open(OUTPUT,"> Soutputfile") or
die "Ошибка! Невозможно записать в $outputfile:$!\n": print OUTPUT $header;
foreach my Sentry (sort byaddress keys %entries) { print OUTPUT
"; Владелец -- ",$entries{$_}->{owner},"
(", $entries{$entry}->{department},"):
Sentries{$entry}->{building},"/", Sentries{$entry}->{room), "\n";
tt выводим запись А
printf OUTPUT "%-20s\tIN A %s\n",
Sentries{Sentry}->{name},Sentries{Sentry>->{address};
it выводим записи CNAMES (псевдонимы)
if (defined $entries{$entry}->{aliases}){
foreach my Salias (split(p ',$entries{$entry}->{aliases}))
{
printf OUTPUT "%-20s\tIN CNAME %s\n",Salias,
Sentries{Sentry}->{name}: > }
print OUTPUT "\n"; }
close(OUTPUT);
Rcs->bindir('/usr/local/bin'); my Srcsobj = Rcs->new;
$rcsobj->file($target);
$rcsobj->co('-!');
rename($outputfile,Starget) or
die "Ошибка! Невозможно переименовать
Soutputfile в Starget:$!\n": $rcsobj->ci("-u","-m"."
Преобразовано пользователем Suser в ".scalar(localtime));
ft создаем файл обратного преобразования open(OUTPUT,"> Soutputfile") or
die "Ошибка! Невозможно записать в $outputfile:$!\n"; print OUTPUT Sheader;
foreach my Sentry (sort byaddress keys %entries) { print OUTPUT
"; Владелец-- ",$entries{$entry}->{owner}," (",
Sentries{Sentry}->{department},"): ",
$entries{$entry}->{building},"/",
Sentries{Sentry}->{room},"\n";
printf OUTPUT "%-3d\tIN PTR %s$defzone.\n\n",
(split/\./,$entries{$entry}->{address})[3],
$entnes{$entry}->{name};
clOse(OUTPUT);
$rcsobj->file($revtarget);
$rcsob]->co( '-1');
предполагаем, что целевой файл по крайней
# мере один раз извлекался из репозитория rename($outputfile,Srevtarget) or
die "Ошибка! Невозможно переименовать
Soutputfile в Srevtarget;$!\n";
$rcsobj->ci("-u","-m"."Преобразовано пользователем
$user в ".scalar(localtime));
sub GenerateHeader{ my(Sheader);
if (open(OLDZONE,$target)){ while (
next unless (/(\d{8}).«serial/); $oldserial = $1; last; }
close(OLDZONE); } else {
Soldserial = "000000"; }
$olddate = substr($oldserial,0,6);
$count = (Solddate == $today) ? substr($oldserial,6,2)+1 : 0;
Sserial = sprintf("%6d%02d",$today,Scount);
$header .= "; файл зоны dns - СОЗДАН $0\п";
Sheader .= "; HE РЕДАКТИРУЙТЕ ВРУЧНУЮ!\n;\n";
Sheader .= "; Преобразован пользователем Suser в ".scalar(localtime)."\n;\n":
П подсчитываем число узлов в каждом отделе foreach Sentry (keys %entries){
$depts{$entries{$entry)->{department}}++; } foreach $dept (keys %depts) {
Sheader .= "; в отделе $dept $depts{$dept} машин.\n";
X
Sheader ,= "; общее число машин: ".scalar(keys %entries)."\nff\n\n";
Sheader .= «"EOH";
@ IN SOA dns.oog.org. hostmaster.oog.org. (
$serial : serial 10800 ; refresh 3600 : retry 604800 ; expire 43200) ; TTL
@ IN NS dns.dog.org
ЕОН
return $header; }
sub byaddress {
@a = split(/\./,$entries{$a}->{address});
@b = split(/\./,$entries{$b}->{address});
($a[0]<=>$b[0]) ||
($a[1]<=>$b[1]) ||
($a[2]<=>$b[2]) ||
($a[3]<=>$b[3]); }
Вот какой файл получается для прямого преобразования (zone.db):
файл зоны dns - СОЗДАН createdns НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ!
Преобразован пользователем David N. Blank-Edelman (dnb); в Fri May 29 15:46:46 1998
в отделе design 1 машин. в отделе software 1 машин, в отделе IT 2 машин, общее число машин: 4
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL
@ IN NS dns.oog.org.
; Владелец -- Cindy Coltrane (marketing): west/143 bendir IN A 192.168.1.3
ben IN CNAME bendir
bendoodles IN CNAME bendir
; Владелец -- David Davis (software): main/909 shimmer IN A 192.168.1.11
shim IN CNAME shimmer
shimmy IN CNAME shimmer
shimmydoodles IN CNAME shimmer
; Владелец -- Ellen Monk (design): main/1116 Sulawesi IN A 192.168.1.12
sula IN CNAME Sulawesi
su-lee IN CNAME Sulawesi
; Владелец -- Alex Rollins (IT): main/1101 sender IN A 192.168.1.55
sandy IN CNAME sander
micky IN CNAME sander
mickydoo IN CNAME sander
А вот как выглядит файл для обратного преобразования (rev.db):
файл зоны dns - СОЗДАН createdns НЕ РЕДАКТИРУЙТЕ ВРУЧНУЮ!
Преобразован пользователем David N. Blank-Edelman (dnb); в Fri May 29 15:46:46 1998
в отделе design 1 машин, в отделе software 1 машин, в отделе IT 2 машин, общее число машин: 4
@ IN SOA dns.oog.org. hostmaster.oog.org. (
1998052900 ; serial 10800 ; refresh 3600 ; retry 604800 ; expire 43200) ; TTL
@ IN NS dns.oog.org.
; Владелец -- Cindy Coltrane (marketing): west/143 3 IN PTR bendir.oog.org.
; Владелец -- David Davis (software): main/909
11 IN PTR shimmer.oog.org.
; Владелец -- Ellen Monk (design): main/1116
12 IN PTR sulawesi.oog.org.
; Владелец -- Alex Rollins (IT): main/1101 55 IN PTR sander.oog.org.
Этот метод создания файлов открывает перед нами много возможностей. До сих пор мы генерировали файлы, используя содержимое одного текстового файла базы данных. Запись из базы данных считывалась и записывалась в файл, возможно, подвергаясь при этом форматированию. Таким образом, в создаваемые файлы попадали только записи из базы данных.
Иногда бывает полезно, чтобы сценарий добавлял в процессе преобразования свои предопределенные данные. Например, в случае с конфигурационными файлами DNS можно улучшить сценарий преобразования так, чтобы он добавлял записи MX (Mail eXchange), указывающие на центральный почтовый сервер, для каждого узла из базы данных. Простое изменение нескольких строк кода с таких:
К выводим запись А
printf OUTPUT "%-20s\tIN A %s\rT, Sentries{Sentry}->{name},Sentries{Sentry}->{address}:
на следующие:
# выводим запись А
printf OUTPUT "%-20s\tIN A %s\n",
$entries{Sentry}->{name},Sentries{Sentry}->{address};
и выводим запись MX
print OUTPUT " IN MX 10 $mailserver\n";
приведет к тому, что почта, посылаемая на любой из узлов домена, будет направляться на машину $mailserver. Если эта машина настроена так, что может обрабатывать почту для всего домена, то мы задействовали очень важный компонент инфраструктуры (централизованную обработку почты), добавив всего лишь одну строчку кода на Perl.
Массив возвращенный функцией split ()
Таблица 5.1. Массив, возвращенный функцией split ()| Элемент | Значение |
| $record[0] | Name |
| $record[1] | Shimmer |
| $record[2] | Address |
| $record[3] | 192.168. 1.11 |
| $rocord[4] | Aliases |
| $record[5] | Shim shimmy shimmydoodles |
| $record[6] | Owner |
| $record[7] | David Davis |
| $record[8] | Department |
| $record[9] | Software |
| $record[10] | Building |
| $record[11] | Main |
| $record[12] | Room |
| $record[13] | 909 |
| $record[14] | Manufacturer |
| $record[l5] | Sun |
| $record[16] | Model |
| $record[17] | UltraGO |
Внедрение системы контроля исходного кода
Внедрение системы контроля исходного кодаПеред тем как перейти к следующему способу преобразования IP-адресов в имена, хотелось бы добавить к процессу создания файла узлов еще одну хитрую возможность обработки, поскольку один-единственный файл приобретает общесетевое значение. Ошибка в этом файле повлияет на всю сеть. Чтобы обезопасить себя, нужен способ, выполняющий откат изменений, нарушивших файл. Особенно необходимо иметь возможность вернуться назад к предыдущим версиям файла.
Самый элегантный способ создать подобную машину времени - добавить к процессу систему контроля исходного кода. Такой контроль используется разработчиками с целью:
Можно, наверное, предположить, что было бы лучше применить систему контроля исходного кода к процессу редактирования первоначальной базы данных, но есть две веские причины, по которым очень важно применить ее к данным, получаемым в результате:
Время
Для большого набора данных процесс преобразования может занять некоторое время. Если ваша сеть «упала», и вам необходимо вернуться к предыдущей версии, то необходимость наблюдать, пока Perl сгенерирует нужный файл, вас просто обескуражит (и все это при условии, что вы смогли сразу добраться до Perl).
База данных
Если для хранения данных вы будете использовать настоящую базу данных (а часто это правильный выбор), то может просто не существовать способа применить к ней систему контроля версий. Вероятно, вам придется писать собственные механизмы контроля версий для процесса редактирования базы данных.
В качестве системы контроля исходного кода я выбрал систему контроля версий RCS. У RCS есть несколько возможностей, дружественных к Perl и системному администрированию:
Крэйг Фретер (Craig Freter) написал объектно-ориентированный модуль Res, который упрощает применение RCS из Perl. Для этого необходимо:
$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 больше не существует, мы не будем приводить примеров для работы с ней. В настоящее время работа с 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 О модели СОМ существует много книг, но следует остановится на таких ключевых понятиях:
Например, в 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
ADSIhttp://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();
Несколько замечаний к этому примеру:
Ошибка при добавлении 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 предлагает для этого две подпрограммы.
Добавление элементов при помощи стандартных операций 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() часто остаются незамеченными. Существуют ситуации, когда эти методы следует употреблять, хотя в книге такие случаи рассматриваться не будут. Вот две подобные ситуации:
Вызывать методы экземпляра объекта 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->
Наличие двух типов свойств обусловлено тем, что свойства интерфейса существуют в виде части модели СОМ. Разработчики, определяя интерфейс при создании программы, также определяют свойства интерфейса. Позже, если они хотят расширить набор свойств, им приходится изменять и СОМ-интерфейс, и любой код, использующий этот интерфейс. В ADSI разработчики могут изменить свойства схемы в провайдере без необходимости изменять лежащий в основе СОМ интерфейс этого провайдера. Очень важно разобраться с обоими типами свойств, т. к. иногда некоторые данные объекта доступны через свойства только одного типа.
На практике, если вы ищете только названия свойств интерфейса или схемы и не собираетесь писать программы для их поиска, я рекомендую использовать ADSI-броузер Тоби Эверета, о котором я упоминал ранее. Вот пример этого броузера в действии.
Как альтернативный вариант упомянем программу ADSIDump из каталога General примеров SDK, которая может вывести содержимое всего дерева ADSL
LDAP сложная служба каталогов
LDAP: сложная служба каталоговСлужбы LDAP (Lightweight Directory Access Protocol, облегченный протокол доступа к каталогам) и ADSI гораздо богаче и более сложны в обращении. В настоящее время существуют две популярные версии протокола LDAP (версия 2 и версия 3; при необходимости номер версии будет указываться). Этот протокол быстро стал промышленным стандартом для доступа к каталогам. Системные администраторы воспользовались протоколом LDAP, т. к. он предлагал способ централизовать и сделать доступной всю информацию об инфраструктуре. Помимо стандартного «каталога компании» существуют такие примеры приложений:
Даже в случае, когда 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 работает только с провайдером 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 Вот эти аргументы:
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
Служба каталогов WHOISWHOIS - это еще одна полезная служба каталогов, предоставляющая доступную только для чтения информацию. 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| Оператор | Значение |
| = | Точное совпадение значений. Может означать и частичное совпадение, если в определении |
| =* | Соответствует всем элементам, у которых есть значения для атри- бута Если вместо чие именно этого атрибута в элементе (например, сп=* выберет эле- менты, у которых есть атрибуты сп). |
| -= | Приблизительное совпадение значений. |
| >= | Больше либо равно значению. |
| <= | Меньше либо равно значению. |
«приблизительное» зависит от сервера. Большинство серверов применяют алгоритм, первоначально используемый в soundex для определения совпадающих значений при поиске слов, которые «произносятся, как» заданное значение (в английском языке), но записываются иначе.
Другая конструкция, которая может конфликтовать с вашими знаниями Perl, - это оператор =. Помимо проверки точного совпадения значений (как строковых, так и численных), оператор = можно использовать вместе с символом * в виде префикса или суффикса в качестве символов подстановки, подобно тому как это происходит в командных интерпретаторах. Например, сл=а* получит все элементы, имена которых (common name) начинаются с буквы «а». Строка сп=*а* выполнит именно то, чего вы ждете, и найдет все элементы, в атрибуте ел которых есть буква «а».
Можно объединить в одну строку два или более простых фильтра Ottribute пате>,
Он имеет следующий вид:
(
(
Те, кто знаком с 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, если только атрибут не обладает несколькими значениями) |
Из программы видно, что методы для доступа к атрибутам элементов в Net:: LDAP несколько отличаются. После проведения поиска все результаты инкапсулируются в один объект. Получить отдельные атрибуты каждого элемента из этого объекта можно, применив один из двух способов.
Во-первых, модуль может преобразовать все полученные элементы в одну большую структуру данных, доступную пользователям. Ssearchobj ->as_struct() возвращает структуру данных, представляющую собой хэш хэшей списков. Метод возвращает ссылку на хэш, ключами которого являются DN-имена полученных элементов. Значения ключей - это ссылки на анонимные хэши, ключами которых являются имена атрибутов. Ключам соответствуют ссылки на анонимные массивы, содержащие значения данных атрибутов (Рисунок 6.1).
Вывести первое значение атрибута сп для всех элементов из структуры данных позволяет такой код:
$searchstruct = $searchobj->as_struct; for (keys %$searchstruct){
print $searchstruet->{$._}{en}[0], "\r";
Можно также сначала использовать один из этих методов и выделить объекты для отдельных элементов из объекта, возвращаемого в результате поиска:

Рисунок 6.1. Структура данных, возвращаемая методом as_struct()
возвращает указанный элемент
Sentry = $searchob]->entry($entrynum);
действует подобно shift() в Perl для списка элементов Sentry = $searchobj->shift_entry;
действет подобно рор() в Perl для списка элементов Sentry = $searchobj->pop_entry;
возвращает все элементы в виде списка ©entries = $searcnobj->entries;
После того как получен объект элемента, можно применить один из указанных методов (табл. 6.4).
Методы элементов Net LDAP
Таблица 6.4. Методы элементов Net::LDAP| Вызов метода | Возвращает |
| $entry->get($attrname) $entry->attributes() |
Значение атрибута в указанном элементе Список имен атрибутов для этого элемента |
Svalue = $searchobj->entry(1)->get(cn)
Теперь, когда вы умеете получать доступ к отдельным атрибутам и значениям, возвращаемым в результате поиска, посмотрим, как поместить подобные данные в каталог сервера.
Методы изменения элементов в Mozilla LDAP
Таблица 6.5. Методы изменения элементов в Mozilla::LDAP| Метод | действие |
| $entry->addValue($attrname, Sattrvalue) | Добавляет указанное значение заданному атрибуту в указанном элементе. |
| $entry-> removeValue($attrname! Sattrvalue) | Удаляет указанное значение для заданного атрибута указанного элемента. Если это значение единственное для атрибута, то удаляется и весь атрибут. |
| $entry-> setValue($attrname, $attrvalue1,...) | Изменяет значения указанного атрибута в заданное значение или значения. |
| $entry-> rerTTOve(Sattrname) | Удаляет указанный атрибут (вместе со значениями) из элемента. |
Применим эти методы для глобального поиска и замены. Рассмотрим такой сценарий: один из отделов вашей компании переводят из Бостона в Индиану. Эта программа изменит все элементы, местоположением которых является Бостон:
use Mozilla::LDAP::Conn;
Sserver = $ARGV[0];
Sport = getservbyname("ldap","tcp") || "389";
Sbasedn = "dc=ccs,dc=hogwarts,dc=edu";
Sscope = "sub";
Srootdn = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$pw = "secret";
№
неанонимное соединение $c = new Mozilla;:LDAP::
Conn(Sserver,Sport.Srootdn,$pw);
die "Невозможно соединиться с сервером
$server\n" unless $c;
tt
обратите внимание, что мы запрашиваем как можно меньше информации для ускорения поиска Sentry = $c->search($Pasedn, Sscope, "(l=Boston)", 1, ");
die "Ошибка поиска;". $c->getErrorStnng().
"\n" if $c->gettrrorCode();
if ($entry){ . - -while(Sentry)!
$entry->removeVali;e("l". "Boston");
$entry->addValue("l", "Indiana");
$c->update($entry);
die 'Ошибка при обновлении:" .
$c->getErrorString() . "\n"
if $c-'getErrorCode(); Sentry = $c->nextEntry(); }; }
$c->close();
Для изменения элементов в Net: : LDAP применяется другой подход. В нем все только что рассмотренные методы модуля Mozilla:. LDAP объединены в одном «суперметоде» modify(). Параметры, передаваемые этому методу, и определяют его функциональность (табл. 6.6).
Методы для изменения элементов в Net LDAP
Таблица 6.6. Методы для изменения элементов в Net::LDAP| Параметр | Действие |
| add => {Sattrname => Sattrvalue} | Добавляет указанный элемент с заданным значением. |
| add => {Sattrname => [$attrvalue1, $attrvalue2. . . ]} | Добавляет указанный атрибут с заданным набором значений. |
| delete => {Sattrname => Sattrvalue} I | Удаляет указанный атрибут с заданным значением. |
| delete => {Sattrname => []} delete => [Sattrnamel, $attrname2. . . ] replace => {Sattrname => Sattrvalue} |
Удаляет атрибут или набор атрибутов независимо от их значений. Действует, как add, только заменяет текущее значение указанного атрибута. Если Sattrvalue является ссылкой на пустой анонимный список ([]), метод становится синонимом для приведенной выше операции удаления. |
Можно объединять несколько таких параметров в одном и том же вызове modify, но это представляет собой потенциальную проблему. Когда modify вызывается с набором параметров, например, так:
$c->modify($dn, replace => {'!' => "Medfora"},
add => {'1' =N "Boston"). add => {'1 => "Cambridge"});
нет никаких гарантий, что указанные операции добавления будут выполняться после замены. Если необходимо, чтобы операции выполнялись в определенном порядке, можно применять синтаксис, подобный только что рассмотренному. Вместо использования набора дискретных параметров можно передать единственный массив, содержащий очередь команд. Вот как это работает: Tiod:fy() принимает параметр changes, значение которого- список. Данный список считается набором пар. Первая половина пары - это операция, которую необходимо выполнить, вторая половина - ссылка на анонимный массив, содержащий данные для этой операции. Например, если мы хотим гарантировать, что операции из предыдущего фрагмента кода выполнятся в нужном порядке, то можем написать:
$c->Tiodify($dn. changes =>
[ replace -;" ['!' => "Kedfrr'd"].
add --> ['!' => "Boston"],
add =>['!' => "Caubndge"]
]);
Внимательно посмотрите на пунктуацию: она отличается от других параметров, которые приводились раньше.
Учитывая информацию, передаваемую функции modify(), можно переписать для Net: : ЮАР предыдущую программу, меняющую Бостон на Индиану:
use Net::LDAP;
$server = $ARGV[0];
Sport = getservbynameC'ldap", "tcp") |j "389";
Sbasedn = "dc=ccs,dc=hogwarts,c!c=edu";
$scope = "sub";
Srootdn = "c!i=Manager, ou=Syste'ns, dc=ccs, dc-hogwarrs, do-edu".
$pw = "secret",
$c = new Net::LDAP($server, port => Sport) or
die "Невозможно соединиться с сервером Ssorver
$«>'\n": $c->bind(dn --> S'ootrin. password => $pw) or
die "Ошибка при соедимении; $@\n";
Ssearchob] = $c->search(base => Soasedn, fiiiei => "(l-Bosion)".
scope => $scope, attrs -s [''}.
typeso.il у => 1): dio "Ошибка поиска: ". Ssear-chonj->er! or
if (SsearchobJ){
(Sentries = $searcnopj->entries;
ОГ ( aPI't ' .OS ) {
Собираем все вместе
Теперь, когда нам известны все основные LDAP-функции, напишем несколько небольших сценариев для системного администрирования. Мы импортируем базу данных машин из главы 5 «Службы имен TCP/IP» на сервер LDAP и затем сгенерируем некую полезную информацию, основываясь на LDAP-запросах. Вот пара выдержек из этого простого файла (для того только, чтобы напомнить вам формат):
name: shimmer
address: 192.168.1.11
aliases: shim shimmy shimmydoodles
owner: David Davis
department: software
building: main
room: 909
manufacturer: Sun
model: UltraGO
name: bendir address: 192.168.1,3 aliases:
ben bendoodles owner: Cindy Coltrane department:
IT building: west room: 143
manufacturer: Apple model: 7500/100
Первое, что нужно сделать, приготовить сервер каталогов для приема этих данных. Мы будем использовать нестандартные атрибуты, так что придется обновить схему сервера. Различные серверы выполняют это по-разному. Например, сервер каталогов Netscape имеет симпатичную графическую консоль Directory Server Console для подобных изменений. Другие серверы требуют внесения изменений в текстовые конфигурационные файлы. Работая с OpenLDAP, можно использовать нечто подобное в файле, включенном основным конфигурационным файлом для определения собственных пользовательских классов объектов для машины:
objectclass machine requires-:
,-• п ullOrtS
a-:a;;es building, room.
manufacturer, model
После того как сервер настроен нужным образом, можно подумать об импортировании данных. Один из вариантов - провести загрузку большой единой операцией с помощью LDIF. Если приведенный выше отрывок из базы данных напомнил вам о формате LDIF, значит, вы на правильном пути. Эта схожесть упрощает преобразование. Тем не менее, нужно остерегаться ловушек:
Продолжающиеся строки
В нашей базе данных нет элементов, значения которых занимали бы несколько строк, иначе следовало бы убедиться, что вывод удовлетворяет стандарту LDIF. Стандарт LDIF требует, чтобы все длинные строки начинались строго с одного пробела.
Разделители элементов
Между элементами в базе данных в качестве разделителя используется симпатичная последовательность -=-. Два разделителя строк (т. е. пустая строка) должны находиться между элементами LDIF, так что нужно будет удалить эту последовательность из вводимых данных.
Разделители атрибутов
В настоящее время в наших данных есть только один атрибут с несколькими значениями: aliases (псевдонимы). LDIF обрабатывает многозначные атрибуты, перечисляя каждое значение на отдельной строке. Если встретится несколько атрибутов, то понадобится специальный код, печатающий для каждого значения отдельную строку. Если бы не эта особенность, программа, преобразующая наш формат в LDIF, представляла бы собой одну строку кода на Perl.
Но даже и с этими ловушками программа преобразования на удивление проста:
Sdatafile = "database";
Srecordsep = "-=-\n";
Ssuffix = "ou=data, ou=systems, dc=ccs. dc=hogwarts. dc=edu";
Sobjectclass = «EOC;
objectclass: top
objectclass: machine
EOC
open(DATA, Sdatafile) or aie "Невозможно открыть Sdataf ile: $''.n";
Модули Perl не работают с зги», даже если в специфики-;'!' while () {
ft выводим заголовок для каждого элемента if (/name:\s-(.-)/){
print "arr сп=$1, $suffix\n":
print Soijjoctclass;
print "en: $1\n":
next: I tt обрабатываем многозначный атрибут aliases
if (s/~aliases:\s*//){
@aliases = split:
foreach $name (@aliases){ print "aliases: $name\n";
}
next; }
ft обрабатываем конец разделителя записей if ($_ eq $recordsep){
print "\n";
next; }
ft в противном случае просто печатаем найденный атрибут print;
close(OATA);
Если выполнить эту программу, то она выведет файл LDIF,
выглядящий примерно так:
dn: cn=shimmer, ou=data, ou=systems, dc=ccs, dc=hogwarts. dc=edu
objectclass: top
objectclass: machine
en: shimmer
address: 192.168.1.11
aliases: shim
aliases: shimmy
aliases: shimmydoodles
owner: David Davis
department: software
building: main
room: 909
manufacturer: Sun
model: UltraGO
dn: cn=bendir, ou=data. Ob=systems, ac=ccs, dc=hogwarts, dc=edu
objectclass: top
objectclass: machine
en: bendir
address: 192.168. 1.3
aliases: ben aliases: bendoodles owner: Cindy Colt rant; department:
building1 west room: 143
manufacturer: Apple model: 7500/100
Имея этот LDIF-файл, можно применять одну из программ, распространяемых с сервером, для загрузки этих данных на сервер. Например, Idif2ldbm, входящий в состав обоих серверов OpenLDAP и Netscape Directory Server, считывает LDIF-файл и напрямую импортирует его в формат сервера каталогов, избавляя от необходимости проходить через LDAP. Хотя эта программа используется только при неработающем сервере, она может обеспечить самый быстрый способ загрузки большого количества данных. Если нельзя остановить сервер, можно применить только что написанную на Perl программу для чтения LDIF-файлов и передать подобный файл на LDAP-сервер.
Добавим еще один способ: вот программа, в которой пропускается промежуточный шаг создания LDIF-файла, и наши данные напрямую импортируются на LDAP-сервер:
use Net::LDAP;
use Net::LDAP::Entry;
Sdatafile = "database";
Srecordsep = "-=-":
Sserver = $ARGV[0];
Sport = getservbynameC'ldap","tcp") || "389";
Ssuffix = "ou=data, ou=systems, dc=ccs, dc=hogwarts, dc=edu";
$rootdn = "cn=Manager. o=University of Michigan, c=US";
$pw = "secret";
$c = new Net::LDAP($server.port => Sport) or
die "Невозможно соединиться с сервером Sserver: $@\n";
$c->bind(dn => Srootdn. password => Spw) or die "Ошибка при соединен;',
open(DATA,Sdatafile) or die "Невозможно открыть $datafile:$!\n";
while () {
chomp;
в
начале новой записи создаем if (,/"nare:\S'(. •)/'){
$d^="cn=$1. Ssbffix":
Sentry = new fiet: : LDAP 'E:"t'\.
$еп:г^,-аса( с *' . $i)
ne:xt i
$entry-vad
$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. Простые фильтры поиска имеют следующий вид:
где
Выполнение распространенных задач при помощи пространства имен 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
WHOISftp://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 драйвер, который заботится обо всех частностях, необходимых для соединения с сервером. В обоих случаях получается по меньшей мере трехъярусная модель:
Очень часто системные администраторы, работающие в многоплатформенном окружении, задают вопрос: «Как я могу использовать Microsoft SQL Server из Unix?» Если центральная система администрирования или наблюдения построена на Unix, то установка нового MS-SQL-сервера представляет собой трудную задачу. Я знаю три способа справиться с этой ситуацией. Второй и третий способы не зависят от SQL-сервера, поэтому даже если вы применяете не Microsoft SQL-сервер, однажды они могут вам пригодиться.
Впрочем, это грустные размышления, лучше посмотрим, как использовать 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::
Добавим несколько комментариев к приведенной программе:
Мониторинг состояния сервера
Мониторинг состояния сервераВ качестве последнего примера рассмотрим несколько способов наблюдения за состоянием SQL-сервера. Программы такого рода по своей природе похожи на службы наблюдения за сетью, уже рассмотренные в главе 5 «Службы имен TCP/IP».
Наблюдение за использованием процессорного времени на SQLсервере
Наблюдение за использованием процессорного времени на SQL-сервереВ последнем примере этой главы DBI будет выводить обновляемую раз в минуту строку состояния, содержащую информацию об использовании процессорного времени на SQL-сервере. Чтобы сделать задачу более интересной, можно из одного и того же сценария одновременно наблюдать за двумя серверами. Комментарий к сценарию последует позже:
use DBI:
Ssyadmin = "sa':
print "Пароль администратора Sybase:
chomp($sypw =
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});
Сама программа будет выполняться в вечном цикле до тех пор, пока не получит сигнал прерывания (наиболее вероятно, что это будет нажатие клавиш
Эта небольшая программа всего лишь затронула возможности наблюдения за состоянием сервера, доступные нам. Было бы несложно, взяв полученные от 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
ODBChttp://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. Интересны отличия между этим примером и предыдущим:
use Win32::ODBC;
print "Введите имя пользователя: ";
chomp($user =
print "Введите пароль для $user: "; chomp($pw =
$dsn="sysadm"; и имя источника данных, которое мы используем
# ищем доступные DSN, создаем переменную $dsn,
если она еще не существует
die "Невозможно запросить доступные DSN",Win32::ODBC::Error()."\n"
unless (%dsnavail = Win32::ODBC::DataSources());
if (Idefined $dsnavail{$dsn}) {
die "невозможно создать DSN:".Win32::ODBC::Error()."\n"
unless (Win32::ODBC::ConfigDSN(ODBC_ADD_DSN. "SQL Server", ("DSN=$dsn",
"DESCRIPTION=DSN for PeriSysAdm",
"SERVER=mssql.happy.edu". "DATABASE=master",
"NETWORK=DBMSSOCN".
библиотека сокетов TCP/IP ))); }
it
соединение с основной базой данных $dbh = new Win32: :ODBC("DSN=$dsn;UID=$iJSer:PWD=$pw: "):
die "Невозможно соединиться с DSN $dsn:".Win32:
# ищем базы данных на сервере
if (defined $dbh->Sql(q{SELECT name from sysdatabases})){
die "Невозможно послать запрос к базе данных:" .Win32: :ODBC: Error().
while ($dbh->FetchRow()){
push(@dbs, $doh->0ata("name"));
}
$dbh->DropCursor();
п
ищем пользовательские таблицы в каждой базе данных foreach $db (@dbs) {
if (defined $dbh->Sql("use $db")){
die "Невозможно изменить базу данных на $db:" .
Win32::ODBC::Error() . "\n"; >
print "---$db---\n"; @tables=(); if (defined $dbh->
Sql(q{SELECT name from sysobjects
WHERE type="U"})){ die "Невозможно запросить таблицы из $db:" .
Win32::ODBC::Error() . "\n"; } while ($dbh->FetchRow()) {
push(@tables,$dbh->Data("name")); > $dbh->DropCursor();
ft ищем информацию о полях для каждой таблицы
foreach Stable (©tables) { print "\t$table\n";
if (defined $dbh->Sql(" {call sp_columns (\'$table\')} ")){
die "Невозможно запросить поля из таблицы Stable:".
Win32::ODBC::Error() . "\n"; >
while ($dbh->FetchRow()) { @cols=();
@cols=$dbh->Data("COLUMN_NAME","TYPE.NAME","PRECISION");
print "\t\t",$cols[0]," [",$cols[1],"(",$cols[2],")]\n";
} $dbh->DropCursor();
}
}
$dbh->Close();
"SQL Server","DSN=$dsn"))
Сервер Sybase и DBI
Сервер Sybase и DBIВ этом подразделе представлен аналог для Sybase. Внимательно просмотрите программу, а после этого поговорим о некоторых существенных моментах:
use DBI;
print "Введите имя пользователя: "; chomp($user =
print "Введите пароль для $user: "; chomp($pw =
$dbh = DBl->connect('dbi:Sybase'',Suser,$pw);
die "Невозможно соединиться: $DBI::errstr\n" unless (defined $dbh);
ищем базы данных на сервере $sth = $dbh->prepare(q{SELECT name from master dbo.sysdatarases}) cr
die "Невозможно подготовить запрос к sysdatabases: ".
$db'i->er rst r . "\n", $stli->oxecute or
die "Невозможно выполнить запрос к sysdarabases: '.
$dori->errstr. "\п";
while (Sarof = $sth->fetchrow_arrayref) (
push((3dbs, $aref->[0]): }
$sth->finisn:
foreach $cm (Mbs) {
$dbh->do("USE $do") or
die "Невозможно использовать $db: ".
®tables=():
while ($агч'( - $.::;->fotchrow_arrayref) {
die "Невозможно изменить Sdb: ".
S'Jor->err-str."' n":
# ищем поля для каждой таблицы foreach Stable (tables) { print "\t$table\n";
$sth=$dbh->prepare(qq{EXEC bp_colunns Stable}) or
die "Невозможно подготовить запрос sp^columns ", $obh-:-err.v
$sth->execute or
die "Невозможно выполнить запрос sp^columns: ".$dbh->errstr.
while ($aref = $sth->fetchrow_arrayref) {
print "\t\t",$aref->[3]," [",$aref->[5],"(",
$aref->[6],")]\n": }
$sth->finish; ! }
$dbh->discohnect or warn "Невозможно отсоединиться: ".
$dbh->errstr."\n";
Вот обещанные заметные моменты:
Удобные методы DBI
Таблица 7.2. Удобные методы DBI| Название | Объединяет в себе следующие методы |
| select row_a r ray ref (Sstmnt) | prepare(Sstmnt), execute( ), fetchrow_arrayref ( ) |
| selectcol_arrayref ($stmnt) | prepare($stmnt), execute( ), (@{fetchrow_arrayref()})[0] (т. е. возвращает первое поле для каждой записи) |
| select rowar ray (Sstmnt) | prepare(Sstmnt), execute( ), fetchrow_array( ) |
die "Невозможно выполнить запрос:".$dbh~>errstr" \n";
К эти переменные получат 1-й, 2-й и 3-й столбы из SELECT
$rc = $sth->Pind_columns(\$name, \$ipaddr, \$dept):
while ($sth->fetchrow_arrayref){
tt
$name, Sipaddr и $dept автоматически получают значения из tt
результатов запроса
Учетные записи баз данных
Учетные записи баз данныхКак упоминалось раньше, администраторы баз данных вынуждены иметь дело с рядом тех же вопросов, с которыми борются системные администраторы, в частности, с поддержкой регистрационных имен и учетных записей. Например, днем на работе мы ведем занятия по программированию баз данных. Каждый студент, посещающий эти занятия, получает регистрационное имя на сервере Sybase и свою собственную (пусть и маленькую) базу данных, с которой он может работать. Вот упрощенная версия программы, которую мы используем для создания таких баз данных и регистрационных имен:
use OBI;
и ИСПОЛЬЗОВАНИЕ: syaccreate
Sadmin = 'sa';
print "Введите пароль для Sadmin: ";
chomp($pw =
$user=$ARGV[0];
# генерируем фиктивный пароль, основываясь на имени
# пользователя, записанном в обратном порядке, и дополняем его
дефисами, чтобы его длина составляла 6 символов
Sgenpass = reverse join(''. reverse split(//,$user)):
Sgenpass .= "-" x (6-length($genpass));
# вот перечень SQL-команд, которые используем
tt мы: 1) создаем базу данных на устройстве USER_DISK
с журналом регистрации в USER_LOG ft 2)
добавляем регистрационное имя для пользователя
на сервере, делая новую базу базой по умолчанию
# 3) переключаемся на вновь созданную базу данных
# 4) изменяем владельца базы данных на пользователя
(^commands = ("create database Suser on USER_DISK=5 log on USER_LOG=5", "
sp_addlogin $user,\"$genpass\",Suser". "use $user", "sp_changedbowner $user"):
Я соединяемся с сервером
$dbh = DB!->connect('dbi:Sybase:',Sadmin.$pw):
die "Невозможно соединиться: $DBI: errstr•n" unless (defined Sdbh):
$dbh->disconnect:
Поскольку эта задача заключается в выполнении ряда команд, которые не возвращают данных, можно записать их в компактном цикле, в котором вызывается повторно $dbh->ao(). Можно было бы использовать полностью идентичный сценарий для удаления этих учетных записей и баз данных, когда занятия завершатся:
use DBI;
# ИСПОЛЬЗОВАНИЕ: syacdelete
$admin = 'sa':
print "Введите пароло для Sadmin: ":
chomp($pw =
$user=$ARGV[0];
#
перечень SQL-команд, которые мы будем использовать; мы удаляем базу данных пользователя
и удаляем регистрационное имя с сервера
©commands = ("drop database $user", "sp_droplogin Suser");
# соединяемся с сервером
$dbh = OBI->connect('dbi:Sybase:',$admin,$pw);
die "Невозможно соединиться: $DBI::errstr\n" unless(defined $dbh);
# обходим в цикле массив команд, выполняя их последовательно for (©commands) {
$dbh->do($_) or die "Невозможно $_: " . $dbh->errstr . "\n": }
$dbh->disconnect or warn "Невозможно рассоединигься: " . $dbh->errstr . "\n":
Существует много функций, связанных с учетными записями, которые можно добавить в эту программу. Вот лишь некоторые идеи:
Проверка паролей
Утилита соединяется с сервером и получает список баз данных и учетных записей; затем пытается установить связь, применяя ненадежные пароли (регистрационные имена, пустые пароли, пароли по умолчанию).
Карта пользователей
Создаем список регистрационных имен и баз данных, доступных пользователям с этими регистрационными именами.
Управление паролями
Система ограничения срока действия паролей.
Perl для системного администрирования
Информация о модулях из этой главы
Информация о модулях из этой главы| Модуль | Идентификатор на CPAN | Версия |
| Mac: :Glue | CNANDOR | 0.58 |
| Win32 . : OLE (входит в состав ActiveState Perl) | JDB | 1.11 |
| Mail: : Mailer (можно найти в MailTools) | GBARR | 1.13 |
| Text::Wrap (можно найти в Text-Tabs+Wrap, также распространяется с Perl) | MUIR | 98.112902 |
| 10: '.Socket (можно найти в 10, кроме того, распространяется с Perl) | GBARR | 1.20 |
| Mail : : Internet (можно найти в MailTools) | GBARR | 1.13 |
| Mail: : Heade r (можно найти в MailTools) | GBARR | 1.13 |
| Mail: : Folder: :Mbox (можно найти в Mail: : Folder) | KJOHNSON | 0.07 |
| Socket (распространяется с Perl) | ||
| BerkeleyDB | PMQS | 0.10 |
| Net: :Tclnet | JROGERS | 3.01 |
| DB_File (распространяется с Perl) | PMQS | 1.72 |
Использование IPC специфичных для операционной системы
Использование IPC, специфичных для операционной системыВ MacOS или Windows NT можно управлять почтовым клиентом, используя IPC (Interprocess Communication, межпроцессные взаимодействия).
Я не знаю о существовании версий sendmail для MacOS, но в нем для управления почтовым клиентом можно применять AppleScript:
$to="someone\@example.com"; $from="me\@example.com"; $subject="Hi there"; $body="message body\n";
MacPerl: :DoAppleScnpt(«EOC); tell application "Eudora"
make message at end of mailbox "out"
-- 0 is the current message
set field \"from\" of message 0 to \"$frorn\"
set field \"to\" of message 0 to \"$to\"
set field \"subject\" of message 0 to \"$subject\"
set body of message 0 to \"$body\"
queue message 0
connect with sending without checking
quit
end tell EOC В этом примере запускается очень простая программа AppleScript, которая общается с почтовым клиентом Eudora. Сценарий создает новое сообщение, помещает его в очередь для отправки, а затем отдает инструкции почтовому клиенту об отправке сообщения из очереди перед выходом.
Еще один, более эффективный способ написать подобный сценарий состоит в том, чтобы использовать модуль Мае: : G1 ие, уже рассмотренный в главе 2 «Файловые системы».
use Mac::Glue ':glue ;
$e=new Mac::Glue 'Eudora';
$to="someone\@example.com"; $f rom="me\(<}>exainple. com";
$subject="Hi there"; $body="
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
$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
RCPT TO:
250
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 (или любого другого, который задан вторым необязательным аргументом). Это напоминает недавнее использование 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:Отслеживание спама Теперь когда
На некоторых сайтах ведутся локальные черные списки узлов, известных как распространители спама. Такая тактика была взята на вооружение, когда спам только начал появляться, и некоторые провайдеры отказывались принимать меры даже против самых закоренелых клиентов-спамеров. В ответ на это в основных агентах передачи почты появились механизмы, отвергающие соединения от узлов и доменов, входящих в список антисоциальных.Можно использовать такой черный список, чтобы разобраться, проходило ли сообщение через узлы, известные своим спамерством. Ясно, что в черном списке нет сервера, передавшего нам почту (иначе с ним просто не было бы установлено соединение), но любой из остальных почтовых серверов, указанный в заголовках 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
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-адрес в каком-либо из этих списков:
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