РОМАН ЗУСИ
Python в администрировании сервера: почему бы и нет?
Стандартных решений не существует. Каждый системный администратор рано или поздно начинает писать свои скрипты, которые облегчают его работу, избавляя от рутины. Для автоматизации в UNIX-системах традиционно применяются командные оболочки (типа bash или ksh, разновидностей оболочек достаточно много) и язык Perl. Причем, следуя философии UNIX, эти оболочки используют для решения проблемы целый набор инструментов, выполняющих небольшие частные задачи: ls, wc, sort, grep, diff, tar, ...
Обычно простые задачи выполняются в командной оболочке запуcком соответствующих инструментов и организацией потока данных. Для более сложных задач требуются и более сложные инструменты, например, awk, sed или даже Perl. Так принято.
Однако хотелось бы обратить внимание системных администраторов на такой сценарный (скриптовый) язык как Python. Этот язык, благодаря своим хорошим качествам, о которых поговорим далее, уверенно завоевывает популярность, в том числе для задач системного администрирования. Например, именно его применяет Red Hat в инструменте под названием anaconda для обеспечения начальной установки своего дистрибутива Linux.
Конечно, системные администраторы – натуры весьма консервативные, и потому я решил написать эту статью, показывающую, что Python действительно имеет преимущества по сравнению с языком Perl и оболочками при решении как повседневных, так и одноразовых задач.
Python – интерпретируемый язык с развитыми высокоуровневыми структурами данных, имеющий все необходимое для вызова функций POSIX-совместимых систем. Впрочем, Python является многоплатформенным языком, так что его можно с успехом использовать и, к примеру, в среде Windows. Однако не буду долго говорить о происхождении и синтаксисе языка: об этом заинтересованный читатель узнает на http://python.ru или из книги Сузи Р.А. Python. – СПб.: БХВ-Петербург, 2002; а сразу перейду к делу.
Начнем с небольшого примера, в котором нам требуется установить права и принадлежность файлов, чтобы имена совпадали с именами пользователей в системе (для простоты будем считать, что имена пользователей доступны из файла /etc/passwd). Подобная задача может возникнуть, например, в каталоге с почтовыми ящиками, где каждый ящик должен принадлежать соответствующему его названию пользователю.
#!/usr/bin/python
import os, string
users = {}
for line in open("/etc/passwd").readlines():
rec = string.split(line, ":")
users[rec[0]] = int(rec[2]), int(rec[3]) # uid и gid
for file in os.listdir("."):
try:
uid, gid = users[file]
except:
print "Сирота: ", file
uid, gid = 0, 0 # root
os.chmod(file, 0600)
os.chown(file, uid, gid)
В самом начале мы импортируем модуль для работы с функциями ОС (os) и работы со строками (string). Перед началом цикла по строкам файла /etc/passwd словарь users пуст. В цикле по строкам файла /etc/passwd мы делаем следующее. Именем rec обозначаем кортеж значений записи passwd-файла. Мы знаем, что первое поле этой записи – имя пользователя. Имя пользователя и станет ключом в словаре users. В качестве значения для данного ключа мы берем приведенные к целому типу поля 2 и 3, соответствующие идентификаторам пользователя и группы. Таким образом в первом цикле формируется отображение имени пользователя и его идентификаторов. Во втором цикле, по именам файлов в текущем каталоге, пытаемся (try) найти владельца файла, обращаясь к словарю users. Если это не удается, мы пишем на стандартный вывод соответствующую диагностику. В этом случае владельцем файла станет root. Последние две команды, думается, ясны и без комментариев.
В приведенном на листинге примере были использованы очевидные решения, не требующие особого знания стандартной библиотеки Python.
Для любознательных укажем и второй путь решения, в котором используются «правильные» средства:
#!/usr/bin/python
import os, pwd, glob
default = pwd.getpwnam("root")
for file in glob.glob("*"):
try:
rec = pwd.getpwnam(file)
except:
print "Сирота: ", file
rec = default
os.chmod(file, 0600)
os.chown(file, rec[2], rec[3])
Здесь удалось добиться некоторого сокращения программы за счет использования функции getpwnam модуля pwd стандартной библиотеки Python. Здесь также применена функция glob из одноименного модуля для получения списка файлов.
Для сравнения приведен неправильный пример подобной же программы, написанный в оболочке bash.
#!/bin/bash
# Не работает, если имена файлов содержат точку или пробел
for file in *
do
chown $file.$file $file || chown root.root $file
chmod 600 $file
done
Конечно, на языке командной оболочки программа получилась раза в два короче. Однако в ней таится неприятная неожиданность: она работает некорректно для файлов, содержащих в имени точку, пробел или дефис в начале!
Я более чем уверен, что программу можно переписать, экранировав символы должным образом. Но тем не менее, зная Python, можно быстрее написать скрипт, решающий ту же задачу без скользких текстовых подстановок.
Из приведенных примеров уже видна особенность языка Python, не всеми воспринимаемая хорошо: для выделения фрагментов кода в составных операторах используется единообразный отступ. Тем самым интерпретатор Python требует от программиста визуально выделять структуру программы, что положительно сказывается на читаемости кода. И это немаловажно, ведь написанный скрипт может пригодиться для похожей задачи.
В следующем примере мы рассмотрим еще одну часто возникающую задачу: проверка работоспособности POP3-сервиса и отправка сообщения электронной почты в случае неудачи. Заметьте, что приведенный пример одинаково хорошо подходит и для UNIX, и для NT.
#!/usr/bin/python
import smtplib, poplib
try:
p = poplib.POP3("mymail")
p.quit()
except:
error = "connection"
try:
s = SMTP("othermail")
s.sendmail("admin@mymail", "admin@othermail",
"""From: admin@mymail
To: admin@othermail
Subject: POP3 down!!!
Please, restart pop3 at mymail.
""")
s.quit()
except:
print "нет связи"
С помощью конструктора объекта POP3-соединения из модуля poplib мы пытаемся установить соединение с POP3-сервером mymail. Если это не удается, мы должны отправить о происшествии письмо на admin@othermail. В этом нам поможет модуль smtplib и конструктор класса SMTP. Если и это не удается, просто выводится «нет связи». Заметьте, как легко в Python ввести многострочный текст.
Надеемся, вы как минимум заинтересовались предлагаемым инструментом.
Таблица 1 поможет быстро найти аналоги распространенных команд среди функций стандартной библиотеки языка Python. Перед использованием функции достаточно импортировать соответствующий модуль.
Таблица 1
Команда UNIX
|
Функция Python
|
cal месяц год
|
calendar.monthcalendar(год, месяц)
|
cd каталог
|
os.chdir("каталог")
|
chmod режим путь
|
os.chmod("путь", режим)
|
chown uid.gid путь
|
os.chown("путь", uid, gid)
|
cp файл путь
|
shutil.copy("файл", "путь")
|
date
|
time.localtime(time.time()) и другие функции модуля time
|
diff
|
функции модуля difflib
|
echo $перем
|
print "%(перем)s" % vars()
|
echo текст
|
print "текст"
|
exit N
|
sys.exit(N)
|
fetchmail
|
модуль poplib
|
ftp
|
модуль ftplib
|
grep, egrep
|
для работы с регулярными выражениями служит модуль re
|
gzip
|
модуль gzip
|
ln -s путь1 путь2
|
os.symlink("файл1", "файл2")
|
ln файл1 файл2
|
os.link("файл1", "файл2")
|
ls -l файл
|
os.stat("файл")
|
ls каталог
|
os.listdir("каталог"), glob.glob("каталог")
|
man тема
|
help("тема") или help(объект)
|
mkdir путь
|
os.mkdir(путь)
|
mv путь1 путь2
|
os.rename("путь1", "путь2")
|
nslookup хост
|
socket.gethostbyname("хост") и другие функции этого модуля
|
pwd
|
os.getcwd()
|
rm -R каталог
|
shutil.rmtree("каталог")
|
rm файл
|
os.unlink("файл") или os.remove("файл")
|
sendmail
|
модуль smtplib
|
sort
|
список.sort()
|
wget и т.п.
|
модули httplib, urllib, urllib2, urlparse
|
zip
|
модуль zipfile
|
команда
|
os.system("команда")
|
команда |
|
f = os.popen("команда", "r")
|
| команда
|
f = os.popen("команда", "w")
|
< файл
|
f = open("файл", "r"); f.read(); f.close()
|
> файл
|
f = open("файл", "r"); f.write(...); f.close()
|
Настало время разобрать более сложный пример – программу для анализа логов почтового сервера Sendmail. Наверное, не нужно долго объяснять, что для обеспечения бесперебойной работы сервера и требуемого качества обслуживания просто необходимо вовремя выявлять аномалии в его работе и пресекать попытки злоупотребления сервисом. Одна из основных проблем – массовые несанкционированные рассылки, или попросту спам. И проблема эта не только в том, чтобы уберечь пользователей сервера от спама, но и вовремя заметить попытки применения пользователями программ массовой рассылки.
Я почти уверен, что на рынке имеются готовые инструменты для решения этой проблемы. Даже очень возможно, что нужные программы есть в свободном распространении. Однако простые средства во многих случаях можно написать и самому, руководствуясь своим опытом и, возможно, знанием местной специфики. Тем более, что это не требует много времени.
Разбираемый ниже пример не претендует на полноту решения проблемы спама, он решает частную задачу. Мы будем отслеживать лишь попытки массовых рассылок, в которых на один «mail from» следуют несколько «rcpt to». Конечно, программу легко адаптировать и для другой ситуации, агрегируя данные по отправителям, получателям и так далее, получая портрет ситуации на почтовом сервере. Следует заметить, что глядя на «сырые» логи Sendmail не всегда можно увидеть проблему. Во всяком случае, для этого требуется некоторое напряжение глаз.
Наша небольшая программа на Python будет собирать данные о сеансах и показывать только сеансы с большим числом получателей. В логе эта информация сильно размазана, поэтому простого применения инструмента вроде grep здесь оказывается недостаточно.
Накапливаемые данные мы будем сохранять в файлах-хэшах (аналогичных тем, в которых хранятся, например, псевдонимы), благо в Python есть для этого нужные модули. (Конечно, в Python можно использовать и полновесные базы данных с языком запросов SQL, однако это непринципиально).
Задача распадается на две: сбор данных из лога в базу данных и выборка из базы данных.
#!/usr/bin/python
"""Анализ лога Sendmail"""
import os, re, sys, anydbm, time
server_name = "mail" # имя сервера, фигурирующее в логах
def createdb(file):
"""Создание БД"""
try: os.unlink(file) # удаляем старую БД
except: pass # игнорируем исключения
return anydbm.open(file, "c")
# ключ: значение
f = createdb("from.db") # ид сессии: отправитель
t = createdb("to.db") # ид сессии: получатели
r = createdb("relay.db") # ид сессии: почтовый хост
d = createdb("date.db") # время и ид сессии: ид сессии
''' Примеры четырех характерных случаев:
Aug 11 20:24:25 mail sendmail[4720]: g7BGOHY2004720: from=<tegeran@mail.ru>, size=103655, class="0", nrcpts=1, msgid=<000c01c2414e$d175ad80$3075a8c0@awx.ru>, proto=SMTP, daemon=MTA, relay=[212.28.127.12]
Aug 11 20:24:25 mail sendmail[4720]: g7BGOHY2004720: to=<font@twogo.ru>, delay=00:00:07, pri=133655, stat=Headers too large (32768 max)'''
Aug 11 04:02:31 mail sendmail[17058]: g7B02UY2017058: from=<gluck@subscribe.ru>, size=33977, class="0", nrcpts=1, msgid=<20020811030027_hk_=2087=top=n=n_@subscribe:news.listsoft.lnx>, proto=SMTP, daemon=MTA, relay=relay1.aport.ru [194.67.18.127]
Aug 11 07:18:20 mail sendmail[28329]: g7B3ICY2028329: <nouser@twogo.ru>... User unknown'''
# характерные части регулярных выражений (префикс, хост и адрес)
P = "(?P<date>.{15}) %(server_name)s sendmail\[[0-9]+\]: (?P<session>[^:]+): " % vars()
R = "relay=(?P<relay>.*(?:\[(?P<relay_ip>.+?)\])?)"
A = "\<?(?P<addr>[^(),>]+)\>?"
# регулярные выражения, соответствующие разным случаям
log1_re = re.compile(r"%(P)s(?P<direc>to)=%(A)s, (.*), stat=(?P<stat>.*)" % vars())
log2_re = re.compile(r"%(P)s(?P<direc>from)=%(A)s, size=(?P<size>[0-9]+), .*, %(R)s" % vars())
log3_re = re.compile(r"%(P)s(?P<direc>from)=%(A)s, .*, %(R)s" % vars())
log4_re = re.compile(r"%(P)s%(A)s\.\.\. (?P<stat>User unknown)" % vars())
input_file = open(sys.argv[1], "r") # открываем файл с логом
while 1:
line = input_file.readline()
if not line: # обнаружен конец файла: выход из цикла
break
# сравниваем строку лога с шаблонами
m = log1_re.match(line) or log2_re.match(line) or log3_re.match(line) or log4_re.match(line)
if m: # если хоть один шаблон сработал:
found = m.groupdict() # получаем словарь групп результата
date, session_id, addr = found["date"], found["session"], found["addr"]
direc = found.get("direc", "to") # по умолчанию "to"
stat = found.get("stat", "") # по умолчанию - пустая строка
if direc == "to" and stat[:4] != "Sent" and stat != "User unknown":
continue # такие сообщения не интересуют
if direc == "to": # в зависимости от направления
if t.has_key(session_id):
t[session_id] = t[session_id] + addr + ";"
else:
t[session_id] = addr + ";"
else:
f[session_id] = addr
r[session_id] = found.get("relay_ip", "") or found.get("relay", "")
d[date + session_id] = session_id
input_file.close()
f.close(); t.close(); r.close(); d.close()
Построчно читаем из файла с логами и, в зависимости от сработавшего шаблона, записываем в заранее открытые базы данных. Обратите внимание, что база date.db использует в качестве ключей как дату, так и идентификатор сессии. Последний делает ключ уникальным.
Заметим, что в Python логические операции or и and работают несколько необычно. Любой объект имеет так называемое истинностное значение. Нули, пустые последовательности, объект None считаются «ложью», а остальные объекты – истиной. В результате операции A or B возвращается объект A, если он «истинен», а в противном случае – объект B. Именно поэтому имени m будет соответствовать результат первого успешного сравнения.
Python оперирует такими высокоуровневыми объектами, как список и словарь. Словарь задает отображение между ключами и значениями. В частности, found является словарем, отображающим имя найденной в строке лога группы и значения этой группы. Имена групп в регулярных выражениях задаются через (?P<имя> ...). Взять из словаря значение по ключу можно не только с помощью словарь[ключ], но и с помощью метода get(). В последнем случае можно задать значение «по умолчанию», то есть значение, которое метод get() возвратит в случае отсутствия ключа в словаре.
Стоит заметить, что работа с базами данных использует тот же синтаксис, что и работа со словарями (об этом позаботились разработчики модуля anydbm).
Итак, мы поместили информацию из лога Sendmail в более удобный для обработки вид – в базы данных (хэши). Следующая программа - пример одного из обработчиков, которые мы теперь можем применить.
Для нашей цели (выявление чрезмерных списков рассылки) данные удобно отсортировать по времени. Это легко сделать с ключами из базы date.db. По сути мы считываем все ключи этой базы в память (метод keys()) и сортируем их по возрастанию.
#!/usr/bin/python
"""Анализируем собранные данные"""
import re, sys, anydbm, string
# открываем базы
f = anydbm.open("from.db", "r")
t = anydbm.open("to.db", "r")
r = anydbm.open("relay.db", "r")
d = anydbm.open("date.db", "r")
# показывать только случаи отправления >= LIM получателям
try:
LIM = int(sys.argv[1])
except:
LIM = 4
dkeys = d.keys() # получаем все ключи в один список
# сортируем (фактически, по времени). Пренебрегаем сменой месяцев
dkeys.sort()
good_guys_re = re.compile(".*(gluck@subscribe.ru|-errors@maillist.ru|@lists.cityline.ru|@host4.list.ru)\Z")
def our_filter(x):
"""Функция для фильтрации получателей"""
return "@" in x
for date_sess_id in dkeys:
i = d[date_sess_id] # находим идентификатор сессии
try:
recps = string.split(t[i], ";") # список получателей
# берем только адреса с @
recps = filter(our_filter, recps)
sender = f[i] # отправитель
# показываем
if len(recps) >= LIM and not good_guys_re.match(sender):
relay = r[i]
dte = date_sess_id[:15] # первые 15 символов -- дата
print f[i], relay, dte, "->\n ", string.join(recps, "\n ")
except: # если возникли ошибки, пропускаем
pass
f.close(); t.close(); r.close()
Здесь особо следует отметить функцию filter(). Эта встроенная функция применяет некоторую функцию (аргумент 1) к списку (аргумент 2). В нашем случае используется our_filter, которая возвращает «истину» в случае, когда ее аргумент содержит «@».
Составленная программа далека от совершенства, однако ее легко доработать в любом нужном направлении: добавить возможность обработки нескольких файлов логов сразу; обрабатывать ситуации, когда несколько получателей указаны в одной строке лога; изменить правила фильтрации, изменить способ агрегирования данных (например, агрегировать по именам получателей, именам отправителей, адресам почтовых хостов); сделать из программы демона, непрерывно следящего за логами и т.п.
Составляя наши программы, мы выразили логику анализа логов почтового сервера и почти не отвлекались на обдумывание применяемых конструкций языка программирования и конструирование типов данных.
Программисты на Python утверждают, что на этом языке можно писать «со скоростью мысли», то есть, почти все время оставаясь в предметной области. Если вы этому не верите, попробуйте переписать приведенные программы с использованием стандартного Си или Java. С другой стороны, приведенную задачу обычно решают с помощью языка Perl. К сожалению, Perl вносит очень много синтаксического мусора, что подчас значительно затеняет логику программы.
Основными преимуществами Python являются, на мой взгляд:
- удобный и легко читаемый синтаксис;
- многоплатформенность;
- поддержка основных системных и сетевых протоколов и форматов;
- большой набор библиотек;
- свободное распространение;
- дружелюбная поддержка в телеконференции comp.lang.python;
- надежный и устойчивый интерпретатор.
Все это делает Python отличным инструментом в работе системного администратора.