Создание системы учетных записей для работы с пользователями
Теперь, в достаточной мере познакомившись с информацией о пользователях, мы можем перейти к вопросу администрирования учетных записей. Вместо того чтобы просто привести список подпрограмм и функций Perl, необходимых для добавления и удаления пользователей, я хочу рассмотреть предмет разговора на другом уровне, рассказывая об этих операциях в широком контексте. В оставшейся части этой главы мы попытаемся написать скелет системы учетных записей, которая работает с пользователями как в NT, так и в Unix.
Наша система учетных записей будет состоять из четырех частей: пользовательского интерфейса, хранилища данных, сценариев обработки (в Microsoft это назвали бы «бизнес-логикой») и низкоуровневых библиотечных вызовов. В организации процесса они работают все вместе.
Запросы поступают в систему через пользовательский интерфейс и помещаются в файл «очереди добавления учетных записей» для обработки. Мы будем называть ее просто «очередью добавления». Сценарии обработки читают эту очередь, создают необходимые учетные записи и сохраняют информацию о созданных учетных записях в отдельной базе данных. Этот процесс отвечает за добавление пользователей в систему.
Процесс удаления пользователя подобен только что описанному. Пользовательский интерфейс создает «очереди удаления». Второй сценарий обработки читает эту очередь, удаляет пользователей из системы и обновляет центральную базу данных.
Данные операции разделены по концептуально различным группам, благодаря чему
достигается максимальная гибкость при необходимости что-либо изменить. Например, желание сменить систему баз данных, потребует только других вызовов низкоуровневой библиотеки. Точно так же, для включения дополнительных шагов в процесс добавления пользователей (к примеру, провести сравнение с базой данных отдела кадров) придется изменить только сценарий обработки. Нам-
нем с рассмотрения первого компонента - пользовательского интерфейса, применяемого для создания первоначальной очереди учетных записей. Мы будем использовать простой текстовый интерфейс для запроса параметров учетной записи, поскольку строим только костяк системы:
sub Collectlnformation{
П список полей приводится только для наглядности. На самом
# деле его надо хранить в центральном конфигурационном
в файле
my ^fields = qw{login fullname id type password};
my Krecord;
foreach my $field (©fields){
print "Please enter $field: ";
chomp($record{$field} = <STDIN>);
}
$record{status}="to_be_created"; return \%record;
}
В этой подпрограмме создается список, состоящий из различных полей учетной записи пользователя. Как уже говорилось в комментариях, этот список упоминается в коде программы только для краткости. Хорошим стилем проектирования программного обеспечения было бы чтение списка имен полей из дополнительного конфигурационного файла.
После того как список создан, подпрограмма рассматривает его в цикле и запрашивает значение для каждого поля. Каждое значение затем сохраняется в хэше. После получения ответов на все вопросы ссылка на этот хэш возвращается для последующей обработки. Наш следующий шаг - записать информацию в очередь добавления. Перед тем как посмотреть на этот код, мы должны рассказать о хранилище данных и форматах данных, используемых в нашей системе учетных записей.
База данных
Центральная часть любой системы учетных записей - это база данных. Некоторые администраторы используют только файл /etc/ pass-wd или базу данных SAM для хранения записей о пользователях системы, но такое решение часто оказывается недальновидным. Помимо информации, о которой мы уже говорили, в отдельной базе данных можно хранить метаданные о каждой учетной записи: например, дату создания учетной записи, срок ее действия, номера телефонов пользователей и прочие сведения. Когда появляется такая база данных, ее можно применять не только для работы с учетными записями. Она годится для создания списков рассылки, служб LDAP и индексации веб-страниц пользователей.
Почему настоящие системные администраторы создают системы учетных записей
Системные администраторы делятся на две категории: ремесленники и архитекторы. Ремесленники большую часть своего времени проводят в не посредственном контакте с подробностями внутреннего устройства ОС. Они знают множество тайн об аппаратном и программном обеспечении, которое они администрируют. Если что-то идет не так, как надо, они знают, какую использовать команду, файл, или какой «гаечный ключ» нужно применить. Талантливые ремесленники могут поразить вас способностью определить и исправить неполадки, находясь даже в соседней комнате от «проблемной» машины.
Архитекторы же тратят время, осматривая компьютерные пространства с высоты. Они мыслят более абстрактно, решая, как сформировать более сложные системы из отдельных частей. Архитекторы озабочены вопросами масштабируемости, расширяемости и повторного использования.
Администраторы обоих типов вносят важный вклад в системное администрирование. Я больше всего уважаю системных администраторов, которые могут быть ремесленниками, но при этом предпочитают действовать как архитекторы. Они решают проблему, а потом определяют, какие изменения в системе можно сделать, чтобы избежать повторения ошибки в дальнейшем. Они думают о том, как даже маленькие усилия с их стороны могут послужить для дальнейшего выигрыша.
Отлично действующее компьютерное окружение требует, чтобы архитекторы работали с ремесленниками в тесном взаимодействии. Ремесленники больше всего полезны при работе в рамках, созданных архитекторами. В автомобильном мире ремесленники нужны для сборки и ремонта машин. Но ремесленники расчитывают на то, что проектировщики машин разрабатывают трудно ломаемые и быстро ремонтируемые автомобили. Чтобы хорошо выполнять свою работу, им нужна инфраструктура, напоминающая сборочный цех, инструкция по эксплуатации и канал поставок запасных частей. Если архитектор хорошо выполняет свою работу, работа ремесленника становится проще.
Какое отношение это имеет к предмету нашего обсуждения? Что ж, вероятно, ремесленники будут применять имеющиеся в операционной системе инструменты для работы с пользователями. Они даже могут пойти дальше и написать небольшие сценарии, упрощающие такие задачи, как добавление пользователей, Архитектор, посмотрев на эту проблему, тут же начнет создавать
систему ведения учетных записей. Архитектор задумается над такими вопросами:
Упоминание о создании отдельной базы данных заставляет некоторых нервничать. Они думают так: «Теперь мне нужно покупать действительно дорогую коммерческую базу данных, отдельный компьютер, на котором она будет работать, и нанимать администратора баз данных». Если у вас в системе тысячи или десятки тысяч учетных записей, с которыми необходимо работать, - да, вам понадобится все это (хотя можно обойтись и некоммерческими базами данных, такими как Postgres и MySQL). В этом случае переходите к главе 7 «Администрирование баз данных SQL», чтобы подробно узнать о работе с подобными базами данных в Perl.
Но когда в этой главе я говорю база данных, то употребляю этот термин в самом широком смысле слова. Плоские файлы вполне подойдут в нашем случае. Пользователи Windows даже могут работать с файлами баз данных Access (например database.mdb). В целях переносимости в этом разделе для различных создаваемых компонентов мы будем использовать простые текстовые базы данных. Но чтобы это было более интересным, базы данных будут в формате XML. Если вы никогда раньше не имели дела с XML, пожалуйста, потратьте немного времени и ознакомьтесь с приложением С «Восьмиминутное руководство по XML».
Почему XML? У XML есть несколько свойств, которые делают его хорошим выбором
для подобных файлов и других конфигурационных файлов системного администрирования:
Энно Дерксена (Enno Derksen). Анализатор, даже не проверяющий синтаксис, все-таки способен найти много ошибок, если он проверяет формат документа.
Мы будем использовать текстовые файлы в XML-формате для основного файла, в котором хранятся учетные записи, и для очереди добавления/удаления.
И в этом случае вы увидите, что правило TMTOWTDI по-прежнему действует. Для каждой операции с XML, которая нам понадобится, мы рассмотрим или, по крайней мере, упомянем несколько способов ее выполнения. Обычно, собирая подобную систему, лучше ограничить число реализованных опций, а действуя таким образом, вы сможете понять, какие возможности программирования существуют при работе с 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);
# записываем открывающий тег для каждой записи <account> $w->startTag("account");
# записываем открывающие/закрывающие внутренние теги и
# данные в <account>
foreach my $field (keys %{$record}){
print $fh "\n\t";
$w->startTag($field);
$w->characters($$record{$field});
$w->endTag;
}
print $fh "\n";
# записываем закрывающий тег для каждой записи <account>
$w->endTag;
$w->end;
$fh->close();
}
Теперь можно использовать всего лишь одну строчку, чтобы получить данные и записать их в файл очереди:
&AppondAcco:jntXML($addqueue. ^Collect In formation);
Вот что получается в результате работы этой подпрограммы:
<account>
<login>bobf</]ogin>
<fullname>Boh Fate'/fiil lr.ame>
<id>24-9057</iri>
<type>staff</type>
<password>passwora</password>
<status>to_be_created</5tatus>
</account>
Да, мы храним пароли открытым текстом. Это очень плохая идея, и даже в случае с нашей демонстрационной системой стоит дважды подумать, прежде чем ее использовать. В настоящей системе учетных записей надо либо шифровать пароль перед тем, как помещать его в очередь, либо вообще не хранить его там.
Функция AppendAccountXML() будет применяться еще раз, когда мы захотим записать данные в очередь удаления и в нашу базу данных учетных записей.
Использование модуля XML: :Writer в подпрограмме AppendAccountXMLO имеет несколько преимуществ:
Чтение кода 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, чтобы обозначить начало новой записи (например, открывающий тег <account>). В этом случае удаляются значения из существующего кэша. Во всех остальных случаях мы возвращаемся, ничего не выполняя:
sub Text {
my $ce = $_[0]->current_element();
$record{$ce}=$_ unless ($ce eq "account");
На этот раз мы используем &Text() для заполнения кэша %record. Как и предыдущая функция, она тоже получает два параметра при вызове: ссылку на объект и «накопленный неразмеченный текст», который анализатор нашел между последним открывающим и закрывающим тегом. Для определения элемента, в котором мы находимся, используется метод current_element(). В соответствии с документацией по XML::Parser: :Expat этот метод «возвращает имя внутреннего элемента, открытого в данный момент». То обстоятельство, что имя текущего элемента не «account», гарантирует нам, что мы находимся внутри одного из подэлементов в <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("<queue>".Squeuecontents."</queue>");
Этот фрагмент кода, вероятно, заставил вас приподнять бровь, а то и обе. В первых двух строчках мы открываем файл очереди и считываем его содержимое в скалярную переменную Squeuecontents. Третья строка могла бы показаться понятной, если бы не забавный аргумент, переданный функции parse(). Почему мы считываем содержимое файла очереди и заключаем его в теги XML вместо того, чтобы перейти к его анализу?
А потому, что это хак. И надо сказать - не плохой. И вот почему эти «фокусы» необходимы для анализа нескольких элементов <account> в одном файле очереди.
Каждый XML-документ по определению должен иметь корневой элемент (root element).
Этот элемент служит контейнером для всего документа; все остальные элементы являются его подэлементами. XML-анализатор ожидает, что первый встреченный им тег будет открывающим тегом корневого элемента документа, а последний тег - закрывающим тегом этого элемента. XML-документы, не соответствующие этой структуре, не считаются корректными (well-formed).
Попытка смоделировать очередь в XML заставит нас призадуматься. Если ничего не сделать, то первым тегом, найденным в файле, будет <account>. Все будет работать нормально до тех пор, пока анализатор не встретит закрывающий тег </account> для этой записи. В этот момент анализатор завершит свою работу, даже если в очереди есть другие записи, потому что он посчитает, что дошел до конца документа.
Мы без труда могли бы добавить открывающий тег (<que^e>) в начало очереди, но что делать с закрывающим тегом? Закрывающий тег корневого элемента всегда должен быть расположен в самом конце документа (и не иначе), а сделать это не просто, учитывая, что мы собираемся постоянно добавлять записи в этот файл.
Можно было бы (но это довольно неприятно) достигать конца файла при помощи функции seek(), а затем двигаться назад (опять же при помощи seok()) и остановиться прямо перед последним закрывающим тегом. Затем мы могли бы записать нашу новую запись перед этим тегом, оставляя закрывающий тег в самом конце данных. Риск повредить данные (что, если мы перейдем не в ту позицию?) должен предостеречь вас от использования этого метода. Кроме того, этот метод сложно применять, если вы не можете точно определить конец файла, например, при чтении данных XML по сетевому соединению. В подобных случаях, вероятно, стоит буферизовать поток данных, чтобы можно было вернуться к концу данных после завершения соединения.
Метод, показанный в предыдущем примере и предлагающий добавлять пару корневых тегов к существующим данным, может показаться хаком, но выглядит он гораздо элегантнее, чем другие решения. Впрочем, вернемся к более приятной теме.
Чтение 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("<queue>".Squejecontents."</qjeue>"):
Содержимое Squeue мы выводим подобным образом:
prim Da la : : Dumper->Di;
mp( [Sqi.euc ] ["uueuo" ])
Теперь это ссылка на данные, найденные в файле очереди, сохраненные в виде хэшей, ключами которого являются элементы <id>.
Мы используем именно такие ключи потому, что XML: : Simple позволяет распознавать в данных конкретные теги, выделяя их среди других в процессе преобразования. Если мы отключим эту возможность:
Squeue = XMLin("<queue>".$queuecontents."</queue>",keyattr=>[]);
то получим ссылку на хэш, где единственное значение является ссылкой на анонимный массив.
Такая структура данных не очень полезна. Этот параметр можно определять по собственному желанию:
$queue = XMLin("<queue>".$qucuecontents."</queue>",keyattr => ["login"]):
Замечательно? Теперь мы можем удалить элементы из очереди в памяти, после того как обработаем их всего в одной строке:
# например, Slogin - "bobf"; delete $queue->{account){$logi");
Если мы хотим изменить значение, перед тем как записать его на диск (скажем, мы работаем с нашей основной базой данных), то это тоже просто сделать:
# например, $login="wendyf"; $field="status"
$queue->{account}{$login}{$field}="created";
Создание XML-данных при помощи XML::Simple
Упоминание «записать его на диск» возвращает нас обратно к методу создания XML-данных, который мы обещали показать. Вторая функция из XML: : Simple принимает ссылку на структуру данных и генерирует XML-данные: rootname определяет имя корневого элемента, мы могли бы использовать XMLdecl. чтобы добавить объявление XML print XMLout($queue, rootname =>"queue"),
В результате получаем (отступы сделаны для удобства чтения):
<queue> <account name="bobf" type="staff"
password="password" status="to_be_created"
fullname="Bob Fate" id="24-9C57" />
<account nanie="wendyf" type="faculty"
password="password"
status="to_be_created"
fullname="Wendy Fate" id="50-9057" />
</queue>
Мы получили отличный XML-код, но его формат несколько отличается от формата наших файлов с данными. Данные о каждой учетной записи представлены в виде атрибутов одного элемента <account> </ accojnt>, a не в виде вложенных элементов. В XML: :Simple есть несколько правил, руководствуясь которыми, он преобразовывает структуры данных. Два из них можно сформулировать так (а остальные можно найти в документации): «отдельные значения преобразуются в XML-атрибуты», а «ссылки на анонимные массивы преобразуются во вложенные XML-элементы».
Чтобы получить «верный» XML-документ («верный» означает «в том же стиле и того же формата, что и наши файлы данных»).
Кошмар, не правда ли? Но у нас есть варианты для выбора. Мы можем:
Squeue = XMLin("<queue>",Squeuecontents."</queue>",
forcearray=>1, keyattr => [""]):
Но если мы перекроим способ чтения данных, чтобы упростить запись, то потеряем семантику кэшей, упрощающих поиск и обработку данных.
Вариант номер 3 кажется более разумным, так что последуем ему. Вот подпрограмма, которая принимает одну структуру данных (рис. 3.5), и преобразует ее в другую структуру данных (рис. 3.6). Объяснение примера будет приведено позже:
sub TransforuiForWrite{ my $queueref = shift;
my Stoplevel = scalar each %$queueref;
foreach my $user (keys %{$queueref->{$toplevel}}){
my %innerhash =map {$_, [$queueref->
{$toplevel}{$user}{$J] }
keys %<$queueref->{Stoplevel}{$user}};
$innerhash{'login'} = [$user];
push @outputarray, \%innerhash; }
Soutputref = { Stoplevei => \@outnui.array};
return $outputref:
}
Теперь подробно рассмотрим подпрограмму TrarsformForWate().
Если вы сравните две структуры (рис. 3.5, рис. 3.6), то заметите в них кое-что общее: это внешний хэш, ключом которого в обоих случаях является account. В следующей строке видно, как получить имя этого ключа, запрашивая первый ключ из хэша, на который указывает $que-ueref:
my Stoplevel = scalar each :6$i;eref:
Интересно взглянуть на закулисную сторону создания этой структуры данных:
my %innernabh =
mар {$_. [$queuer-ef-><$toplevel){$use'-}{$_ M 1
keys %{$queueref->{$toplevelf{$user}};
В этом отрывке кода мы используем функцию "iap(), чтобы обойти все ключи, найденные во внутреннем хэше для каждой записи (т. е. login, type, password и status). Ключи возвращаются в такой строке:
keys %{$queueref->{$toplevel}{$user}};
Просматривая ключи, можно с помощью тар вернуть два значения для каждого из них: сам ключ и ссылку на анонимный массив, содержащий его значение:
шар {$_, [$queueref->{$topleve]}{$user}{$_n }
Список, возвращаемый тар(), выглядит так:
(login,[bobf], type,[staff], password,[password]...)
Он имеет формат ключ-значение, где значения хранятся как элементы анонимного массива. Этот список можно присвоить хэшу %innerhash, чтобы заполнить внутреннюю хэш-таблицу для получаемой структуры данных (my %innerhash =). Кроме того, к хэшу следует добавить ключ login, соответствующий рассматриваемому пользователю:
$innerhash{'login'} = [$user];
Структура данных, которую мы пытаемся создать, - это список подобных хэшей, поэтому после того как будет создан и определен внутренний хэш, необходимо добавить ссылку на него в конец списка, т. к. он и представляет получаемую структуру данных:
push @outputarray. \%innerhash:
Такую процедуру следует повторить для каждого ключа login из первоначальной структуры данных (один на каждую запись об учетной записи). После того как это будет сделано, у нас появится список ссылок на хэши в той форме, которая нам нужна. Мы создаем анонимный хэш с ключом, совпадающим с внешним ключом из первоначальной структуры данных, и значением, равным нашему списку хэшей. Ссылку на этот анонимный хэш можно возвратить обратно вызывающей программе. Вот и все:
Soutputref = { Stoplevel => \SO'..tputarray}: return Soutputre?:
Теперь, располагая &TransforrnForWrite(), мы можем написать программу для чтения, записи наших данных и работы с ними:
Squeue = XMLin("<queue>".$queuecontents."</queue>",keyattr => ["login"]);
print OUTPLITFILE XMLojt(Ti ansfonnFor Write($queuu), г ootname => "queue");
Записанные и прочитанные данные будут иметь один и тот же формат.
Перед тем как закрыть тему чтения и записи данных, следует избавиться от несоответствий:
1. Внимательные читатели, наверное, заметили, что одновременное использование XML: : Write г и XML: : Simple в одной и той же программе для записи данных в очередь может оказаться непростым делом. Если записывать данные при помощи XML: : Simple, то они будут вложены в корневой элемент по умолчанию. Если же применять XML: : Write г (или просто операторы print) для записи данных, вложения не произойдет, т. е. нам придется опять прибегнуть к хаку "<queue>". Squeuecontents. "</queue>". Возникает неудачный уровень синхронизации чтения-записи между программами, анализирующими и записывающими данные в XML-формате.
Чтобы избежать этой проблемы, надо будет использовать продвинутую возможность модуля XML: .'Simple: если XMLoutO передать параметр rootname с пустым значением или значением undef, то возвращаются данные в XML-формате без корневого элемента. В большинстве случаев так поступать не следует, потому что в результате образуется неправильный (синтаксически) документ, который невозможно проанализировать. Наша программа позволяет этим методом воспользоваться, но такую возможность не стоит применять необдуманно.
2. И хотя в примере этого нет, мы должны быть готовы к обработке ошибок анализа. Если файл содержит синтаксически неверные данные, то анализатор не справится и прекратит работу (согласно спецификации XML), остановив при этом и всю программу в случае, если вы не примете мер предосторожности. Самый распространенный способ справиться с этим из Perl - заключить оператор анализа в eval() и затем проверить содержимое переменной $@ после завершения работы анализатора. Например:
eval {$p->parse("<queue>".Squeuecontents."</queue>")};
if ($@) { сделать что-то для обработки ошибки перед выходом.. };
Другим решением было бы применение известного модуля из разряда XML: : Checker, т. к. он обрабатывает ошибки разбора аккуратнее.
Низкоуровневая библиотека компонентов
Теперь, когда мы умеем отследить данные на всех этапах, включая то, как они получаются, записываются, читаются и хранятся, можно перейти к рассмотрению их использования глубоко в недрах нашей системы учетных записей. Мы собираемся исследовать код, который действительно создает и удаляет пользователей. Ключевой момент этого раздела заключается в создании библиотеки повторно используемых компонентов. Чем лучше вам удастся разбить систему учетных записей на подпрограммы, тем проще будет внести лишь небольшие изменения, когда придет время переходить на другую операционную систему или что-либо менять. Это предупреждение может показаться ненужным, но единственное, что остается постоянным в системном администрировании, - это постоянные изменения.
Подпрограммы для создания и удаления учетных записей в 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 <accountname>
изменит пароль для этой учетной записи.
Звучит просто, но тут затаилась проблема. Команда passwd запрашивает пароль у пользователя. Она принимает меры предосторожности, чтобы убедиться, что общается с настоящим пользователем, взаимодействуя напрямую с его терминалом. В результате следующий код работать не будет:
и такой код РАБОТАТЬ НЕ БУДЕТ open(PW,"|passwd Saccount")
print PW $olcipasswd, "\n"; print PW Snewpasswd,"\n";
На этот раз мы должны быть искуснее, чем обычно; нам нужно как-то заставить команду passed думать, что она имеет дело с человеком, а не программой на Perl. Этого можно достичь, если использовать модуль Expect.pm, написанный Остином Шутцом (Austin Schutz), - ведь он устанавливает псевдотерминал (pty), внутри которого выполняется другая программа. Expect.pm основан на известной Tel-программе Expect Дона Либеса (Don Libes). Этот модуль входит в семейство модулей, взаимодействующих с программами. В главе 6 мы рассмотрим его близкого «родственника», модуль Net: :Telnet Джея Роджерса (Jay Rogers).
Эти модули действуют в соответствии со следующей моделью: они ждут вывода программы, посылают ей на ввод данные, ждут ответа, посылают некоторые данные и т. д. Приведенная ниже программа запускает команду passed в псевдотерминале и ждет до тех пор, пока та запросит пароль. Поддержание «разговора» с passwd не должно требовать усилий:
use Expect;
sub InitUnixPasswd {
my (Saccount,Spasswd) = @_;
ft вернуть объект
my $pobj - Expect->spawn($passwdex, Saccount);
die "Unable to spawn $passwdex:$!\n" unless (defined SpoDj):
it не выводить данные на стандартный вывод (т. е.
# работать молча)
$pobj->log_stdout(0);
# Подождать запроса на ввод пароля и запроса на повторение
# пароля, ответить. $pobj->expect(10,"New password: ");
и Linux иногда выводит подсказки раньше, чем он готов к вводу,
print $pob] "$passwd\r"
$pobi->expect(10, "Re-enter new password: "); print $pob] "$passwd\r";
it работает"
Sresul: = (defined ($pobj-~ expectdO.
"successfully changeo")) ? "" : "password crarac.
failed"): в закрываем обьект, ждем 15 секунд, пока процесс завершится
$pobj-'suft_close();
return $resuJ t.:
}
Модуль Expect.pm очень хорошо подходит для этой подпрограммы, но стоит отметить, что он годится для куда более сложных операций. Подробную информацию можно найти в документации и руководстве по модулю Expect.pm.
Подпрограммы для создания и удаления учетных записей в Windows NT/2000
Процесс создания и удаления учетных записей в Windows NT/2000 несколько проще, чем в Unix, поскольку стандартные вызовы API для этой операции существуют в NT. Как и в Unix, мы могли бы вызвать внешнюю программу, чтобы выполнить подобную работу (например, вездесущую команду net с ключом USERS/ADD), но проще использовать API-вызовы из многочисленных модулей, о некоторых из которых мы уже говорили. Функции для создания учетных записей есть, например, в Win32::NetAdmin, Win32: :UserAdmin, Win32API::Net и Win32::Lanman. Пользователям Windows 2000 лучше ознакомиться с материалом по ADSI в главе 6.
Выбор одного из этих модулей, в основном, дело вкуса. Чтобы разобраться в отличиях между ними, рассмотрим существующие вызовы для создания пользователей. Эти вызовы описаны в документации Network Management SDK на
http://msdn.microsoft. com (если вы ничего не можете найти, поищите «NetUserAdd»). NetllserAdd() и другие вызовы принимают в качестве параметра информационный уровень данных. Например, если информационный уровень равен 1, структура данных на С, передаваемая вызову для создания пользователя, выглядит так:
typedef struct JJSER_INFO_1 {
LPWSTR usri1_name;
LPWSTR usri1_oassworc';
DWORD usril_passwora_age:
DWORD usril_oriv:
LPWSTR usril_home_dir;
LPWSTR usri1_comment;
DWORD usri1_flags:
LPWSTR usri1_script_pat!i:
}
Если используется информационный уровень, равный 2, структура значительно расширится:
typedef struct _UbER_INrG;
LPWSTR |
usn2_name; |
LPWSTP |
lisri? password: |
DWORD |
usri2_password_age: |
DWORD |
usn2_priv: |
LPWSTR |
Lisri2_home_dir; |
LPWSTR |
usri2_conwient : |
DWORD |
usri2_flags; |
LPWSTR |
usri2_scnpt_path; |
DWORD |
usri2_auth_f lags; |
LPWSTR |
usri2_fiJll_name: |
LPWSTR |
usri2_usr_comment; |
LPWSTR |
usri2_parms: |
LPWSTR |
usri2_workstations: |
DWORD |
usri2_last_logon; |
DWORD |
usn2_last_logoff ; |
DWORD |
usri2_acct_expires; |
DWORD |
usri2_max_storage; |
DWORD |
usri2_units_per_week; |
PBYTE |
usri2_logon_hours; |
DWORD |
usri2_bad_pw^count; |
DWORD |
usri2_num_logons; |
LPWSTR |
usri2_logon_server; |
DWORD |
usri2_country_code; |
DWORD |
usri2_code_page; |
He обязательно много знать об этих параметрах или даже вообще о С, чтобы понять, что при изменении уровня увеличивается количество информации, которое можно передать при создании пользователя. Кроме того, каждый последующий уровень является надмножеством предыдущего.
Какое это имеет отношение к Perl? Каждый упомянутый модуль требует принять два решения:
Модули Win32API: :Net и Win32: :UserAdmin позволяют программисту выбрать информационный уровень. Win32; : NetAdmin и Win32: : Lanrnan этого не делают. Из всех этих модулей Win32; :NetAdmin применяет наименьшее число параметров; в частности, вы не можете определить поле на этапе создания пользователя. Если вы решите применять модуль Win32; :NetAcmin, вам, скорее всего, придется дополнить его вызовами из другого модуля, чтобы установить те параметры, которые он устанавливать не позволяет. Если вы остановитесь на комбинации Win32 : : NetAarn.in и Win32 : : AaminMisc, вам стоит обратиться к многократно упомянутой книге Рота, поскольку это отличный справочник по модулю Win32: : NetAdmin, по которому нет достаточного количества документации.
Теперь читателю должно быть понятно, почему выбор модуля - это де ло личных предпочтений. Хорошей стратегией было бы сначала решить, какие параметры важны для вас, а затем найти модуль, который их поддерживает. Для наших демонстрационных подпрограмм мы выбираем модуль Win32: : Lanman. Вот какой код можно применить для создания и удаления пользователей в нашей системе учетных записей:
use Win32: :Lanman; tt для создания учетной записи
use Win32::Perms; # для установки прав на домашний каталог
$homeNTdirs = "\\\\homeserver\\home"; # корневой каталог
# домашних каталогов
sub CreateNTAccount{
my ($account,$record) = @_;
П создаем учетную запись на локальной машине
# (т. е., первый параметр пустой)
$result = Win32::Lanman::NetUserAdd("",
{'name' => Saccount,
'password' => $record->{password},
'home_dir' => "$homeNTdirs\\$account",
'full_name' => $record->{fullname}});
return Win32::Lanman::6etLastError() unless ($result);
добавляем в нужную ЛОКАЛЬНУЮ группу
(предварительно мы Я получаем SID учетной записи)
# Мы считаем, что имя группы совпадает с типом учетной
die "SID lookup error:
".Win32::Lanman::6etLastError()."\n"
unless (Win32: :Lanman: :LsalookupNames("", [$account],
\@info)); $result = Win32::Lanman::NetLocalGroupAddMember("",
$record->{type), ${$info[0]){sid»;
return Win32::Lanman::GetLastError() unless (Sresult);
# создаем домашний каталог
mkdir "$homeNTdirs\\$account",0777 or
return "Unable to make honedir:$!";
№ устанавливаем ACL и владельца каталога
$acl = new Win32::Perms("$homeNTdirs\\$account");
$acl->0wner($account);
# мы предоставляем пользователю полный контроль за
# каталогом и всеми файлами, которые будут в нем созданы
# (потому и два различных вызова)
DIRECTORY | СОНТШЕВ_ШЕИТ_АСЕ);
$acl->Allow($account, FULL, -
FILE|OBJECT_INHERIT_ACE|IMHERIT_ONLY_ACE);
$result = $acl->Set(); $acl->Close();
return($result ? "" : Sresult); }
Программа для удаления пользователей выглядит так:
use Win32: iLanman;
для удаления учетной записи
use File::Path;
для рекурсивного удаления каталогов
sub DeleteNTAccount{
my($account,$record) = @_;
# удаляем пользователя только из ЛОКАЛЬНЫХ групп.
Если мы № хотим удалить их и из глобальных групп, мы можем убрать
слово "Local" из двух вызовов Win32::Lanman::NetUser
(например, NetUserGetGroups)
die "SID lookup error: ".Win32::Lanman::GetLastError()."\n"
unless (Win32::Lanman::LsaLookupNames("",
[Saccount], \@info));
Win32::Lanman::NetUserGetLocalGroups($server, Saccount, ",
\@groups); foreach $group (@groups){
print "Removing user from local group ".
$group->{name}."...";
print(Win32::Lanman::NetLocalGroupDelMember("",
$group->{name}, ${$info[0]}{sid})?
"succeeded\n" : "FAILED\n"); }
tt удалить эту учетную запись с локальной машины
(т. е., перый параметр пустой) Sresult = Win32::Lanman::NetUserDel("", Saccount);
return Win32::Lanman::GetLastError() if ($result);
удалить домашний каталог и его содержимое
Sresult = rmtree("$homeNTdirs\\$account",0,1);
rmtree возвращает число удаленных файлов, так что если мы
удалили более нуля элементов, то скорее всего все прошло 8 успешно return Sresult;
}
Заметьте, для удаления домашнего каталога здесь используется переносимый модуль File: :Path. Если бы мы хотели сделать что-то специфичное для Win32, например, переместить домашний каталог в корзину, то могли бы сделать это при помощи модуля Win32: :File Op Йенды Крыники (Jenda Krynicky), который можно найти на http://jen da.krynicky.cz/. В таком случае мы применили бы Wi n32: ; F: 1еОр и изменили бы строку, включающую rmtrc-e(), на:
# удалим каталог в корзину, потенциально подтверждая
# действие пользователем, если для этой учетной записи
# необходимо подтверждать такие операции
$result = Recycle("$homeNTdirs\\$account");
В данном модуле есть функция Delete(), которая выполняет то же, что и rmtree() менее переносимым (правда, более быстрым) способом.
Сценарии
Теперь, когда мы разобрались с базой данных, самое время написать сценарии для выполнения периодических или каждодневных действий, необходимых при системном администрировании. Эти сценарии построены на низкоуровневой библиотеке компонентов (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("<queue>".Squeuecontents."</queue>",
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("<queue>".Squeuecontents."</queue>",
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("<queue>".$queuecontents."</queue>",
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("<maindb>".Sdbcontents. "</maindb>",
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("<maindb>
".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.
http://www.procmail.org. Процедура установки procmail принимает усиленные меры, чтобы определить безопасные стратегии блокировки для используемой файловой системы, lockfile делает именно то, что можно ожидать, глядя на ее название, скрывая при этом основные сложности.
Мы завершаем наш разговор об администрировании пользователей и о том, как можно перевести эти операции на другой уровень, применив подход архитектора. В этой главе мы уделили особое внимание началу и концу жизненного цикла учетной записи. В следующей главе мы поговорим о том, что делают пользователи между этими двумя моментами.