Показаны сообщения с ярлыком wheezy. Показать все сообщения
Показаны сообщения с ярлыком wheezy. Показать все сообщения

воскресенье, 1 сентября 2019 г.

Virt-manager для управления виртуальными машинами под управлением KVM и Xen

Для управления виртуальными машинам, работающими под управлением систем виртуализации KVM и Xen можно использовать программу с графическим интерфейсом, которая называется Virt-manager. Программа использует демон libvirt, установленный на компьютере с системой виртуализации. Для подключения Virt-manager к демону libvirt можно использовать SSH, что довольно удобно и безопасно. Как правило, демон SSH бывает уже установлен на компьютерах, доступных по сети, поэтому не нужно заботиться о настройке отдельного сетевого демона. Плюс к тому, SSH поддерживает аутентификацию пользователей и надёжное шифрование передаваемых данных.

Настройка демона libvirt для управления виртуальными машинами под управлением KVM (на примере Debian Stretch)

Проверим наличие аппаратной поддержки виртуализации:
$ egrep -c '(vmx|svm)' /proc/cpuinfo
Устанавливаем систему виртуализации qemu с поддержкой KVM и демон libvirt для управления виртуальными машинами, работающими под управлением KVM:
# apt-get install qemu-kvm libvirt-daemon-system
Добавляем пользователя, от имени которого будем заходить на сервер по SSH, в группу libvirt, чтобы он имел доступ к управлению виртуальными машинами:
# usermod -aG libvirt stupin
Пользователь получит доступ к Unix-сокету /var/run/libvirt/libvirt-sock

Установим netcat из OpenBSD, поддерживающий опцию -U для подключения к Unix-сокету. Это нужно для перенаправления подключения от SSH-клиента в Unix-сокет libvirt:
# apt-get install netcat-openbsd
Осталось включить и запустить демона:
# systemctl enable libvirtd
# systemctl start libvirtd

Настройка демона libvirt для управления виртуальными машинами под управлением Xen (на примере Debian Wheezy)

В моём случае Xen был развёрнут в операционной системе Debian Wheezy, где пакет с демоном libvirt называется libvirt-bin. Установим его:
# apt-get install libvirt-bin
Теперь, чтобы пользователь, от имени которого мы будем подключаться по SSH к серверу виртуализации, смог подключиться к демону libvirtd, нужно добавить его в группу libvirt:
# usermod -aG libvirt stupin
Пользователь получит доступ к Unix-сокету /var/run/libvirt/libvirt-sock

Для того, чтобы демон libvirt смог работать с системой виртуализации Xen, нужно включить HTTP-сервер, встроенный в xend. Демон libvirt будет выполнять роль посредника, транслируя входящие запросы в запросы к xend.

Настроим HTTP-сервер, ожидающий подключений на порту 8000 и адресе 127.0.0.1. Для этого нужно прописать в файл /etc/xen/xend-config.sxp следующие опции:
(xend-http-server yes)
(xend-address localhost)
(xend-port 8000)
Чтобы настройки xend вступили в силу, нужно перезапустить его (пример для Debian Wheezy):
# /etc/init.d/xen restart
Будьте осторожны - в конфигурации по умолчанию при перезапуске демона xend перезапускаются также и все виртуальные машины. Чтобы избежать этого, нужно при создании виртуальной машины прописать в её конфигурацию такие настройки:
on_xend_start = 'ignore'
on_xend_stop = 'ignore'
У меня эти настройки были прописаны в каждой виртуальной машине, поэтому я перезапускал xend без опасений.

Установка и использование Virt-manager

Устанавливаем на компьютере администратора пакет с GUI-интерфейсом для управления виртуальными машинами:
# apt-get install virt-manager
Теперь можно попробовать сделать то, ради чего всё и затевалось: подключиться к системе виртуализации из программы virt-manager. Ниже приведены снимки экрана, иллюстрирующие этот процесс.

Запуск virt-manager:

Добавление нового подключения:

Новое подключение к системе виртуализации Xen:

Настройки удалённого подключения к системе виртуализации Xen через SSH:

Подключение к системе виртуализации установлено, виден список виртуальных машин:

Теперь можно подключаться к консоли виртуальной машины и менять её настройки.

Типичные проблемы

Не установлен пакет netcat-openbsd

Не удалось подключиться к libvirt.

Для взаимодействия с удалённым узлом необходимо, чтобы версия netcat/nc поддерживала параметр -U.

Не установлен пакет libvirt-bin или libvirt-daemon-system

Не удалось подключиться к libvirt.

End of file while reading data: nc: unix connect failed: No such file or directory: Ошибка ввода/вывода

Убедитесь, что на удалённом узле запущен libvirtd.

Не настроен HTTP-сервер xend

Не удалось подключиться к libvirt.

unable to connect to 'localhost:8000': В соединении отказано

Убедитесь, что на удалённом узле запущен libvirtd.

Не установлен пакет gir1.2-spice-client-gtk-3.0

Ошибка подключения к графической консоли:
Error opening Spice console, SpiceClientGtk missing

воскресенье, 16 июня 2019 г.

Контроль количества процессов приложений uWSGI через Zabbix

Задумался как-то о том, чтобы подобрать подходящее количество рабочих процессов для каждого из приложений, работающих под управлением uwsgi. Если рабочих процессов не хватает, то приложение работает не в полную силу и часть поступающих запросов встаёт в очередь до освобождения рабочего процесса. Если же процессов слишком много, то часть из них окажутся неиспользуемыми и будут просто занимать свободную оперативную память. Чтобы задать подходящее количество рабочих процессов, нужно понять, сколько обычно их используется в действительности.

В uwsgi новых версий есть множество разных способов контролировать его работу, но в имеющейся у меня версии 1.2.3 из репозитория Debian Wheezy поддерживается не так-то много вариантов для его контроля. Я остановился на варианте с механизмом Stats Server.

Настройка сокет-файлов

Чтобы включить Stats Server для каждого из приложений, нужно добавить соответствующую опцию в конфигурацию каждого из приложений в файлах /etc/uwsgi/apps-available/. Однако, чтобы не редактировать много файлов конфигурации, можно изменить файл с настройками по умолчанию. Копируем из файла /usr/share/uwsgi/conf/default.ini настройки в файл /etc/uwsgi/uwsgi.ini и добавляем опцию:
stats = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/stats
В результате у меня получился файл с таким содержимым:
[uwsgi]
autoload = true
master = true
workers = 2
no-orphans = true
pidfile = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/pid
socket = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/socket
stats = /run/uwsgi/%(deb-confnamespace)/%(deb-confname)/stats
chmod-socket = 660
log-date = true
uid = www-data
gid = www-data
Теперь пропишем использование этого файла с настройками по умолчанию в файле /etc/default/uwsgi, прописав в него соответствующую настройку:
INHERITED_CONFIG=/etc/uwsgi/uwsgi.ini
Теперь нужно перезапустить все приложения:
# /etc/init.d/uwsgi restart
В каталогах приложений /var/run/uwsgi/app/<приложение>/ должны появиться файлы stats.

Скрипт обнаружения и опроса

К сожалению, права доступа к этому файлу точно такие же, как у файла сокета и поменять их через файл конфигурации приложения нельзя, поэтому для доступа к файлу статистики от имени пользователя zabbix придётся либо воспользоваться sudo, либо включить пользователя zabbix в группу www-data. Я воспользовался sudo. Однако, прежде чем перейти к настройкам sudo и Zabbix-агента, нужно написать скрипт, с помощью которого Zabbix-агент будет извлекать необходимые данные из сокетов статистики.

У меня получился скрипт на языке Python со следующим содержимым:
#!/usr/bin/python

import sys, os, socket, json

def discover():
    data = []
    for app in os.listdir('/run/uwsgi/app/'):
        data.append({'{#UWSGI_APP}': app})
    data = {'data': data}
    return json.dumps(data)

def read_stats(unix_socket):
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    try:
        s.connect(unix_socket)
    except socket.error:
        return None

    data = ''
    while True:
        buf = s.recv(4096)
        if len(buf) < 1:
            break
        data += buf
    s.close()

    data = data.decode('UTF-8')
    return json.loads(data)

def analyze_stats(data):
    listen_queue = data['listen_queue']
    listen_queue_errors = data['listen_queue_errors']
    
    total = 0
    idle = 0
    busy = 0
    for worker in data['workers']:
        total += 1
        if worker['status'] == 'idle':
            idle += 1
        elif worker['status'] == 'busy':
            busy += 1

    pidle = idle * 100.0 / total
    pbusy = busy * 100.0 / total

    return listen_queue, listen_queue_errors, total, idle, busy, pidle, pbusy

if __name__ == '__main__':
    if len(sys.argv) == 2:
        if sys.argv[1] == 'discover':
            print discover()
    elif len(sys.argv) == 3:
        app = sys.argv[2]
        data = read_stats('/run/uwsgi/app/%s/stats' % app)
        if data is None:
            print 'No stats'
            sys.exit()

        listen_queue, listen_queue_errors, total, idle, busy, pidle, pbusy = analyze_stats(data)
        if sys.argv[1] == 'listen_queue':
            print listen_queue
        elif sys.argv[1] == 'listen_queue_errors':
            print listen_queue_errors
        elif sys.argv[1] == 'total':
            print total
        elif sys.argv[1] == 'idle':
            print idle
        elif sys.argv[1] == 'busy':
            print busy
        elif sys.argv[1] == 'pidle':
            print pidle
        elif sys.argv[1] == 'pbusy':
            print pbusy
Я поместил этот скрипт в файл /etc/zabbix/uwsgi.py. Не забудьте поменять права доступа к файлу:
# chown root:root /etc/zabbix/uwsgi.py
# chmod ugo=rx /etc/zabbix/uwsgi.py

Настройка sudo и Zabbix-агента

Теперь дадим пользователю zabbix права на запуск этого скрипта от имени пользователя root. Запускаем visudo и добавляем в файл конфигурации две строчки:
Defaults:zabbix !requiretty
zabbix ALL=(www-data:ALL) NOPASSWD:/etc/zabbix/uwsgi.py *
Отредактируем файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf:
UserParameter=uwsgi.discover,/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py discover
UserParameter=uwsgi.total[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py total $1
UserParameter=uwsgi.idle[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py idle $1
UserParameter=uwsgi.busy[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py busy $1
UserParameter=uwsgi.pidle[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py pidle $1
UserParameter=uwsgi.pbusy[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py pbusy $1
UserParameter=uwsgi.listen_queue[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py listen_queue $1
UserParameter=uwsgi.listen_queue_errors[*],/usr/bin/sudo -u www-data /etc/zabbix/uwsgi.py listen_queue_errors $1
И перезапустим Zabbix-агента, чтобы новые настройки вступили в силу:
# /etc/init.d/zabbix-agentd restart

Шаблоны Zabbix

Я подготовил два шаблона для Zabbix 3.4:
В обоих шаблонах имеется элемент данных для низкоуровневого обнаружения, который находит все имеющиеся сокет-файлы для статистики приложений. Обнаружение происходит раз в час:

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

Пример графика

Приведу пример графика с реального сервера:

Из графиков видно, что приложения maps и bottle иногда используют 100% рабочих процессов, а вот приложение ncc постоянно использует не более 30% рабочих процессов.

воскресенье, 11 марта 2018 г.

Пересборка uwsgi с поддержкой Linux Capabilities

Понадобилось мне сделать веб-приложение, которое, среди прочих функций, позволяло бы проверить, отвечает ли указанный IP-адрес на ICMP-запросы. Проблема в том, что ICMP-запросы может выполнять только пользователь root, а запускать веб-приложение от имени root мне не хотелось, потому что оно будет доступно из сети и если я допущу в нём ошибку, это может позволить злоумышленнику получить доступ к серверу, на котором работает это приложение. Не хотелось бы, чтобы взломщик сразу получил бы все карты в руки - всё же гораздо безопаснее, если он сможет хозяйничать на сервере только от имени пользователя, под которым работает веб-сервер, а не сразу от имени root.

Мне в голову сразу пришли две идеи. Первая идея - сделать отдельное небольшое веб-приложение, которое будет работать от имени пользователя root, но будет уметь только пинговать и будет доступно только на локальном петлевом интерфейсе. Основное веб-приложение будет обращаться к этому небольшому веб-приложению только для выполнения ICMP-запросов, а все остальные функции будет выполнять самостоятельно. Вторая идея - воспользоваться POSIX Capabilities. POSIX Capabilities позволяет выполнять лишь определённые функции, доступные пользователю root, не работая при этом от имени пользователя root.

Так как в качестве сервера веб-приложений я использую uwsgi, то поддержку POSIX Capabilities нужно искать в нём. И такая поддержка имеется. Её описание можно увидеть на странице Setting POSIX Capabilities

Реализовать мне пришлось обе идеи, т.к. это веб-приложение должно работать в том числе и на FreeBSD, где POSIX Capabilities не поддерживается. В случае Linux положение немного осложняется тем, что в стандартной поставке uwsgi из репозитория Debian Wheezy поддержка POSIX Capabilities отсутствует. К счастью, исправить это сравнительно просто: нужно пересобрать пакеты, предварительно установив в систему пакет libcap-dev.

Установим инструменты для сборки и нужную нам библиотеку:
# apt-get install dpkg-dev devscripts fakeroot libcap-dev
Распакуем в текущий каталог исходные тексты uwsgi, из которых можно собрать как двоичные пакеты, так и пакеты с исходными текстами:
# apt-get source uwsgi
Установим все зависимости, необходимые для сборки uwsgi:
# apt-get build-dep uwsgi
Внесём в журнал изменений пакета описание последнего изменения, которое мы добавляем. Для этого запустим команду:
# dch -i
И приведём запись о последних изменениях к следующему виду:
uwsgi (1.2.3+dfsg-5+deb7u2) UNRELEASED; urgency=low

* Compiled with libcap-dev to support linux capabilities

-- Vladimir Stupin <vladimir@stupin.su>  Mon, 11 Sep 2017 09:53:11 +0500
Теперь осталось собрать пакеты:
# cd uwsgi-1.2.3+dfsg
# dpkg-buildpackage -us -uc -rfakeroot
И можно устанавливать готовые пакеты, которые были помещены в вышестоящем каталоге:
# cd ..
# dpkg -i uwsgi_1.2.3+dfsg-5+deb7u2_amd64.deb uwsgi-core_1.2.3+dfsg-5+deb7u2_amd64.deb uwsgi-plugin-python_1.2.3+dfsg-5+deb7u2_amd64.deb
Для использования функции, нужно добавить в файл конфигурации веб-приложения, например, в моём случае это файл /etc/uwsgi/apps-available/ncc.ini, одну строчку:
cap = net_raw
P.S. Понадобилось недавно повторить все эти действия. Был уверен, что описал всё в блоге, но, неожиданно, записей не нашёл. Зато нашёл черновик, где были записаны команды по сборке, пример текста для журнала изменений и ссылка на документацию uwsgi. Повторил сборку по черновику и заодно довёл заметку до вида, пригодного к публикации.

воскресенье, 16 апреля 2017 г.

Установка Debian с флешки по SSH

Когда возникает задача настроить сервер для другого города, часто задача решается одним из способов:
  1. Сервер отправляется в филиал полностью преднастроенным, так что местным системным администраторам остаётся только установить его в стойку и подключить к сети,
  2. Один из системных администраторов из головного офиса компании отправляется в командировку в филиал, где и настраивает сервер вместе с местными системными администраторами,
  3. Местные системные администраторы сами устанавливают на имеющееся оборудование операционную систему и настраивают на сервере сеть, после чего настройку сервера продолжают системные администраторы из головного офиса.
Последний способ позволяет обойтись без расходов на транспорт - не нужно платить за перевозку преднастроенного сервера, не нужно оплачивать командировочные расходы. Однако и у этого способа имеются свои недостатки - иногда на этапе установки операционной системы бывает нужно выполнить сложную разметку дисков или подгрузить дополнительные драйверы. В подобных случаях лучшим решением может быть использование устройства IP-KVM, если оно есть в филиале. Если же такого устройства в филиале нет, то сгодиться могут разнообразные суррогаты. Один из таких суррогатов и будет рассмотрен в этой статье. Мы подготовим образ флэш-карты с инсталлятором операционной системы Debian, консоль которого после загрузки будет доступна по SSH.

Чтобы подготовить загрузочный образ, необходима уже установленная система Debian. Установим пакет debian-installer, необходимые для его сборки зависимости и пакет fakeroot:
# apt-get install dpkg-dev
# apt-get source debian-installer
# apt-get build-dep debian-installer
# apt-get install fakeroot
В текущем каталоге будет создан каталог с исходными текстами пакета debian-installer. Перейдём в этот каталог:
# cd debian-installer-20130613+deb7u3/
Откроем в редакторе файл build/pkg-lists/base и добавим в него одну строчку:
#include "network-console"
Эта строчка добавит в инсталлятор дополнительные пакеты, которые сделают возможным доступ к загруженному инсталлятору по SSH.

Создадим файл preseed.cfg и пропишем в него ответы на первые вопросы инсталлятора:
# Выбираем локаль и раскладку клавиатуры
d-i localechooser/shortlist select RU
d-i keyboard-configuration/xkb-keymap select ru
# Задаём настройки сетевой карты
d-i netcfg/enable boolean true
d-i netcfg/choose_interface select eth0
d-i netcfg/disable_autoconfig boolean true
d-i netcfg/get_ipaddress string 192.168.1.16
d-i netcfg/get_netmask string 255.255.255.0
d-i netcfg/get_gateway string 192.168.1.1
d-i netcfg/get_nameservers string 192.168.1.2 192.168.1.3
d-i netcfg/confirm_static boolean true
# Задаём имя сервера и домен
d-i netcfg/get_hostname string server
d-i netcfg/get_domain string core.ufanet.ru
# Включаем доступ к инсталлятору по SSH с логином installer и паролем installer-password
d-i anna/choose_modules string network-console
d-i preseed/early_command string anna-install network-console
d-i network-console/password password installer-password
d-i network-console/password-again password installer-password
# Подключаем сетевой репозиторий для скачивания недостающих пакетов
d-i mirror/country string manual
d-i mirror/http/hostname string mirror.ufanet.ru
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i mirror/suite string wheezy
d-i mirror/udeb/suite string wheezy
В этом файле можно сразу указать сетевые настройки будущего сервера, согласовав их с системными администраторами филилала. А можно договориться использовать одни и те же сетевые настройки только на период настройки новых серверов, после чего на готовом сервере менять настройки на постоянные.

Теперь отредактируем build/config/local и добавим в него полное имя файла preseed.cfg и кодовое имя релиза инсталлятора:
PRESEED=/root/preseed.cfg
USE_UDEBS_FROM=wheezy
Осталось только собрать образ загрузочного диска с инсталлятором. Для этого нужно перейти в каталог build и выполнить команду сборки образа:
# cd build
# fakeroot make rebuild_monolithic
Эта команда соберёт образ "small bootable CD image for network install" - "маленький загрузочный образ компакт-диска для сетевой установки". Такой образ содержит полный комплект пакетов самого инсталлятора, но устанавливаемые пакеты будет скачивать по сети.

Собранный образ можно найти в файле dest/netboot/mini.iso Остаётся только отправить этот образ системным администраторам филиала, например, по электронной почте. Дальше системные администраторы филиала должны будут записать этот образ на свободную флэшку и загрузить с неё будущий сервер. Дальнейший процесс разметки дисков, настройки учётных записей, настройки системных часов, часового пояса можно производить через SSH из головного офиса, зайдя по SSH с логином installer и указав пароль из файла preseed.cfg (в примере выше это пароль installer-password).

Полный список вариантов образов можно узнать, выполнив команду fakeroot make без указания конкретной цели сборки. Среди других вариантов для наших целей полезными могут оказаться:
  • rebuild_netboot - "tiny CD image that boots the netboot installer" - "крошечный образ компакт-диска, который загрузит установщик с сетевой загрузкой". Образ получится самым маленьким, т.к. недостающие пакеты инсталлятора будут загружаться по сети. Собранный образ можно найти в файле dest/monolithic/mini.iso,
  • rebuild_hd-media - "1 gb image (compressed) for USB memory stick" - "сжатый образ размером в 1 гигабайт для флэш-карт". Этот образ содержит в себе полный комплект пакетов инсталлятора и самые популярные устанавливаемые пакеты операционной системы. Собранный образ можно найти в файле dest/hd-media/boot.img.gz
При создании файла preseed.cfg можно ориентироваться на официальный пример файла wheezy/example-preseed.txt или брать значения опций из уже установленной где-либо системы. Для этого нужно установить в этой системе пакет debconf-utils:
# apt-get install debconf-utils
Вывести на экран значения настроек можно при помощи такой команды:
# debconf-get-selections --install
Вместо указания настроек mirror в файле preseed.cfg можно создать файл со списком репозиториев - build/sources.list.udeb.local. Например, вот такой:
deb http://mirror.ufanet.ru/debian wheezy main contrib non-free
deb http://mirror.ufanet.ru/debian wheezy-updates main contrib non-free
deb http://mirror.ufanet.ru/debian wheezy-proposed-updates main contrib non-free
deb http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free
Использованные материалы:

воскресенье, 9 апреля 2017 г.

Решение проблемы с паразитным трафиком TCP ACK

Столкнулся с необычной проблемой. Имеется Zabbix-сервер, работающий под управлением FreeBSD, и Zabbix-прокси, работающий под управлением Linux. Между ними находится NAT-шлюз, работающий под управлением FreeBSD. В такой конфигурации Zabbix-сервер и Zabbix-прокси периодически начинают забрасывать друг друга огромным количеством пустых пакетов с флагом ACK:
09:59:13.868487 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.868496 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.868498 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.868504 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.868990 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.868997 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.868999 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.869005 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869364 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.869372 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869374 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.869380 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869863 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.869870 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.869872 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.869878 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.870238 IP x.y.z.4.60272 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 3255861608], length 0
09:59:13.870245 IP x.y.z.8.10054 > x.y.z.4.60272: Flags [.], ack 4294967295, win 65535, length 0
09:59:13.870247 IP x.y.z.4.53217 > x.y.z.8.10054: Flags [.], ack 1, win 457, options [nop,nop,TS val 200464 ecr 738884814], length 0
09:59:13.870253 IP x.y.z.8.10054 > x.y.z.4.53217: Flags [.], ack 4294967295, win 65535, length 0
Тут видно два подключения с портов 60272 и 53217 на порт 10054. Интервалы времени между ответными пакетами составляют единицы микросекунд. В результате образуется взаимный шторм в десятки тысяч пакетов в секунду. Самое интересное, что остановка Zabbix-прокси не приводит к пропаданию этого трафика - пакеты продолжают сыпаться.

Имеется аналогичная конфигурация, в которой Zabbix-сервер работает под управлением Linux. То есть два Linux'а разделены NAT-шлюзом под управлением FreeBSD. В этой конфигурации подобной проблемы не наблюдается.

Как я выяснил, Zabbix тут вообще не при чём. Я нашёл описание заплатки для Linux, в которой описана эта проблема: Merge branch 'tcp_ack_loops'. Проблема проявляется, когда промежуточный шлюз вносит изменения в параметры TCP-подключения этих двух узлов - меняет номер последовательности, номер ACK или отметку времени. Один из узлов, получив такой пакет, сообщает противоположной стороне о несовпадении параметров установленного подключения, отправляя ей пакет ACK. Другая сторона получает этот пакет ACK, а параметры в нём тоже не совпадают с параметрами установленного подключения. Обе стороны начинают забрасывать друг друга ACK-пакетами, в которых сообщается, что противная сторона отправила пакет с параметрами, отличающимися от параметров установленного соединения.

В новых версиях ядра Linux должна появиться переменная net.ipv4.tcp_invalid_ratelimit, описание которой можно найти здесь - /proc/sys/net/ipv4/* Variables. Приведу перевод описания этой переменной ядра:
tcp_invalid_ratelimit - ЦЕЛОЕ

Ограничивает максимальную скорость отправки дубликатов подтверждений в ответ на входящие пакеты TCP, которые соответствуют существующему подключению, но по одной из причин считаются неправильными:
  1. номер последовательности за пределами окна,
  2. номер подтверждения за пределами окна, или
  3. ошибка проверки PAWS (Protection Against Wrapped Sequence numbers - защита от завёрнутых номеров последовательности).
Может помочь смягчить простые DoS-атаки "цикл ACK", когда неисправный хост или злоумышленник между ними может переписать поля заголовка TCP таким образом, что это заставит каждую из сторон считать, что другая отправляет неисправные сегменты TCP, что заставит каждую из сторон отправлять непрекращающийся поток дубликатов подтверждений о неисправных сегментах.

Значение 0 отключит ограничение скорости дубликатов подтверждений в ответ на неисправные сегменты; в противном случае значение указывает минимальный интервал между отправкой дубликатов подтверждений в миллисекундах.

Значение по умолчанию: 500 миллисекунд.
Обновлять ядро Linux мне не хотелось, т.к. в конфигурации Linux-FreeBSD-Linux всё работало нормально и без этой заплатки. Я стал искать обходные решения.

На правильное направление меня вывела вот эта статья - FreeBSD Tuning and Optimization. Приведу перевод фрагмента, описывающего возможные проблемы от включения функции Syncookies:
Syncookies обладают как плюсами, так и минусами. Syncookies полезны если вы находитесь под DoS-атакой, поскольку с их помощью можно отличать настоящих клиентов от компьютеров злоумышленников. Но, поскольку опции TCP из начального SYN-пакета не сохраняются в Syncookies, опции TCP не применяются к подключению, препятствуя использованию функций масштабирования окна, отметок времени или точному определению размера MSS. Поскольку при получении ответного ACK устанавливаетcя подключение, злоумышленник может попробовать наводнить машину пакетами ACK, чтобы попытаться создать подключение.

Дополнительно для переполнения получателя настоящих SYN-куки злоумышленник может добавить к пакету данные. Тогда злоумышленник сможет отправлять данные сетевому демону FreeBSD, даже используя поддельный IP-адрес отправителя. Это приведёт к тому, что FreeBSD будет обрабатывать данные, которые не обрабатывались бы, если бы Syncookies были выключены. Несмотря на то, что Syncookies полезны во время DoS-атак, мы собираемся отключить синкуки.
Я попробовал выключить на Zabbix-сервере под управлением FreeBSD использование опции Syncookies:
# sysctl -w net.inet.tcp.syncookies=0
Затем заблокировал на время исходящий трафик на Zabbix-прокси, работающем под управлением Linux:
# iptables -A OUTPUT -d x.y.z.8 -p tcp -m tcp --dport 10054 -j DROP
Выждав полминуты, снова его разблокировал:
# iptables -D OUTPUT -d x.y.z.8 -p tcp -m tcp --dport 10054 -j DROP
В результате паразитный трафик пропал.

То есть, как я понял, изменения, вносимые в TCP-пакеты промежуточным шлюзом, осуществляющим трансляцию адресов, могут приводить к тому, что обе стороны будут слать друг другу ACK-пакеты с несовпадающими параметрами. Эти ACK-пакеты сервером FreeBSD будут восприниматься как пакеты, продолжающие процесс установки нового подключения - FreeBSD посчитает, что отправитель ACK-пакета ранее отправлял ей SYN-пакет и уже получил в ответ подтверждение в виде SYN-ACK-пакета. Таким образом FreeBSD создаст новое подключение. В дальнейшем обе стороны будут обмениваться друг с другом сообщениями о получении пакета, не соответствующего тем параметрам подключения, которые обе стороны согласовали друг с другом ранее. Не претендую на полное понимание ситуации и точность объяснения, но отключение Syncookies на стороне Zabbix-сервера под управлением FreeBSD помогло избавиться от проблемы.

Чтобы Syncookies отключались после перезагрузки системы, нужно внести соответствующие настройки в файл /etc/sysctl.conf:
net.inet.tcp.syncookies=0

воскресенье, 26 марта 2017 г.

Таймаут DNS в OpenNTPd

Как-то на работе настраивал сервер для отправки в филиал. Всё настроил, начал уже отключать от сервера провода, вдруг решил ещё раз проверить какой-то файл конфигурации. Включил сервер и обнаружил, что загрузка замерла.

Стал разбираться, что же не так. Как оказалось, загрузка остановилась на этапе запуска OpenNTPd. Заметил, что сервер не был подключен к сети. Воткнул сеть - сервер стал загружаться дальше. В интернете нашёл описание этой ошибки в OpenNTPd: net-misc/openntpd - failed dns results in extended startup delay when -s option in use

Проблема заключается в том, что у OpenNTPd предусмотрен таймаут для NTP-серверов, но не предусмотрен таймаут для DNS-серверов, поэтому демон продолжает в бесконечном цикле выполнять DNS-запросы, пытаясь узнать имена NTP-серверов.

Проблему можно решить одним из способов:
  • Убрать из настроек опцию -s,
  • Прописать в конфигурацию IP-адреса NTP-серверов, а не доменные имена,
  • Пропатчить пакет патчем из этого отчёта об ошибке.
Я решил тогда пропатчить пакет. Потом я много раз пользовался этим пакетом, но описать его сборку всё забывал. Вот сейчас решил всё-таки задокументировать эту процедуру, хотя в новых версиях Debian она станет больше не нужной, т.к. в новых версиях OpenNTPd ошибка уже исправлена.

Для начала отредактируем файл /etc/apt/sources.list и добавим строчки с репозиториями deb-src с исходными текстами:
deb http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free

deb-src http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free
Установим инструменты, необходимые для сборки пакета:
# apt-get install dpkg-dev devscripts fakeroot
Установим пакет с исходными текстами openntpd:
# apt-get source openntpd
Скачаем подготовленный мной патч:
# wget http://stupin.su/files/openntpd-20080406p-dns-timeout.patch
Перейдём в каталог с исходными текстами пакета:
# cd openntpd-20080406p
Наложим скачанный патч:
# patch -Np0 < ../openntpd-20080406p-dns-timeout.patch
Установим пакеты, необходимые для сборки пакета openntpd:
# apt-get build-dep openntpd
Оформим изменения, сделанные в исходных текстах, в виде патча:
# dpkg-source --commit
В ответ на запрос имени заплатки введём dns-timeout.

Содержимое заголовка заплатки:
Description: DNS timeout added
 Added patch from https://bugs.gentoo.org/show_bug.cgi?id=493358
 .
 openntpd (20080406p-4) unstable; urgency=low
Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: <2017-02-09>

--- openntpd-20080406p.orig/ntpd.c
+++ openntpd-20080406p/ntpd.c
Теперь опишем сделанные изменения в журнале изменений:
# dch -i
Свежая запись в журнале будет выглядеть следующим образом:
openntpd (20080406p-4.1) UNRELEASED; urgency=low

  * DNS timeout added

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 09 Mar 2017 22:11:37 +0500
Соберём новый пакет с исходными текстами и двоичный пакет:
# dpkg-buildpackage -us -uc -rfakeroot
Теперь можно перейти на уровень выше и установить собранный пакет:
# cd ..
# dpkg -i openntpd_20080406p-4.1_amd64.deb
С новым пакетом пауза в процессе загрузки составляет 1 минуту 40 секунд, что вполне приемлемо, т.к. сервер в конце концов всё-таки загружается, а на фоне всего процесса загрузки эта дополнительная пауза почти не заметна.

воскресенье, 19 марта 2017 г.

Смена темы письма в Postfix

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

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

Чтобы решить эту проблему и облегчить себе работу, я решил настроить все свои серверы так, чтобы Postfix добавлял в тему отправляемого письма имя сервера.

Сделать это просто. Для начала нужно установить пакет postfix-pcre:
# apt-get install postfix-pcre
Далее, добавим в файл /etc/postfix/main.cf одну строчку:
header_checks = pcre:/etc/postfix/rewrite_subject
Теперь нужно создать файл /etc/postfix/rewrite_subject и поместить в него правило, которое будет добавлять в тему письма дополнительный текст с именем сервера. Например, вот так:
/^Subject: (.*)$/ REPLACE Subject: $1 (from server.domain.tld)
Текст "/^Subject: (.*)$/" является регулярным выражением в стиле Perl. Это регулярное выражение будет совпадать со строкой заголовка, начинающейся с текста "Subject: ". Текст "(.*)" совпадает с любой темой письма и будет запомнен в переменной $1.

Действие "REPLACE" говорит о том, что нужно произвести замену текста.

Строка "Subject: $1 (from server.domain.tld)" содержит в текст, который заменит совпавшую строчку. При этом вместо переменной $1 будет подставлено её значение. Если имя переменной не отделено от остального текста пробельными символами, тогда её имя следует записывать в виде ${1}

Итак, осталось перезапустить почтовый сервер:
# /etc/init.d/postfix restart
Проверить, добавляется ли текст к теме письма можно следующим образом:
# mail -s test box@mailserver.tld
test
.
При этом в почтовом ящике должно появиться письмо с темой "test (from server.domain.tld)".

воскресенье, 25 сентября 2016 г.

Стандарт SNMPv3 и суровая действительность USM_TIME_WINDOW

В прошлой статье Zabbix и проблемы с опросом по SNMPv3 при дублировании EngineID я описал решение первой проблемы, возникшей при внедрении SNMP третьей версии. После этого я столкнулся с ещё одной проблемой, которую исправлял в два подхода.

D-Link не соблюдает стандарт RFC3414

Дело в том, что даже после исправления EngineID на всех коммутаторах, на некоторых коммутаторах проблема повторялась. После перезагрузки коммутатора, Zabbix считал коммутатор недоступным по SNMP. Если разбираться глубже, с помощью приведённых в прошлой заметке OID'ов, то оказывается, что на коммутаторе после перезагрузки значение EngineBoots остаётся прежним или даже уменьшается! Первым делом были проверены версии прошивок - возможно эта проблема уже была исправлена и надо только обновить прошивку. Но оказалось, что прошивки на проблемных коммутаторах были самые свежие. Поискал в интернете, сталкивался ли кто-нибудь с подобной проблемой, и нашёл аж три темы, в которых открытым текстом утверждается, что эта проблема известна производителю и исправлять он её не собирается:
Поразмыслив, я решил, что можно было бы отключить проверку монотонности возрастания EngineBoots. В конце концов, лучше использовать SNMP третьей версии без защиты от повторного воспроизведения запросов, но зато с более безопасной аутентификацией и с защитой информации от подслушивания, чем вернуться к использованию SNMP второй или первой версии и передавать по сети в открытом виде и строку сообщества и сами запросы и ответы. Решено - будем править библиотеку.

Подготовка к пересборке Net-SNMP

Для начала отредактируем файл /etc/apt/sources.list и добавим строчки с репозиториями deb-src с исходными текстами:
deb http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free

deb-src http://mirror.ufanet.ru/debian/ wheezy main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian/ wheezy-proposed-updates main contrib non-free
deb-src http://mirror.ufanet.ru/debian-security wheezy/updates main contrib non-free
Обновим список доступных пакетов, чтобы можно было скачать из вновь подключенных репозиториев пакеты с исходными текстами:
# apt-get update
Скачаем и распакуем исходные тексты, а затем перейдём в каталог с распакованными исходными текстами:
$ apt-get source snmp
$ cd net-snmp-5.4.3~dfsg/
Описание исправлений, внесённых в Net-SNMP

Поиск показал, что константа USM_TIME_WINDOW со значением 150 используется только в файле snmplib/snmpusm.c. Первым делом я нашёл строчку, в которой проверяется, что значение EngineBoots должно быть больше или равно значению из кэша:
if (theirBoots == ENGINEBOOT_MAX || theirBoots > boots_uint) {
Если значение EngineBoots не изменилось, то используются значение EngineTime, сохранённое в кэше. Если значение стало больше, то новые значения EngineBoots и EngineTime сохраняются в кэш. Если значение уменьшилось, то запрос завершается ошибкой SNMPERR_USM_NOTINTIMEWINDOW.

Это строчку я исправил следующим образом:
if (theirBoots == ENGINEBOOT_MAX) {
То есть при любом изменении значения EngineBoots новое значение будет сохранено в кэш для дальнейшего использования.

После этого я проделал описанные ниже процедуры по сборке пакета, установил исправленный пакет в систему и перезапустил Zabbix. Прошла пара дней, после чего стало понятно, что это исправление учитывает не все возможные случаи. Некоторые коммутаторы после перезагрузки всё-таки переставали опрашиваться.

Детальное изучение проблемы показало, что при перезагрузке значение EngineBoots у них не изменилось, зато EngineTime становилось меньше прошлого, т.к. отсчёт времени после перезагрузки начинался с нуля. В этом случае вступал в силу другой фрагмент кода, который при равенстве полученного значения EngineBoots и имеющегося в кэше проверяет соответствие времени. Если время из ответа больше расчётного, то библиотека запоминает новое значение времени в кэше и принимает ответ. Если время из ответа отстаёт от расчётного менее чем на 150 секунд, то такой ответ тоже принимается. Если же время из ответа отстаёт от расчётного более чем на 150 секунд, то ответ отбрасывается, а запрос завершается всё той же ошибкой SNMPERR_USM_NOTINTIMEWINDOW. Этот фрагмент кода выглядит следующим образом:
/*
 * Boots is ok, see if the boots is the same but the time
 * is old.
 */
if (theirBoots == boots_uint && time_uint < theirLastTime) {
        if (time_difference > USM_TIME_WINDOW) {
                DEBUGMSGTL(("usm", "%s\n", "Message too old."));
                *error = SNMPERR_USM_NOTINTIMEWINDOW;
                return -1;
        }
        else {              /* Old, but acceptable */
                *error = SNMPERR_SUCCESS;
                return 0;
        }
}
Его я исправил следующим образом:
/*
 * Boots is ok, see if the boots is the same but the time
 * is acceptable old.
 */
if (theirBoots == boots_uint && time_uint < theirLastTime && time_difference < USM_TIME_WINDOW) {
        *error = SNMPERR_SUCCESS;
        return 0;
}
Снова собрал пакет, установил его в систему и перезапустил Zabbix. Через несколько дней стало понятно, что это исправление помогло и опрос по SNMP третьей версии теперь продолжает работать даже после перезагрузок коммутаторов, когда значение EngineBoots не меняется или меняется в меньшую сторону. Был достигнут неприятный компромисс, на который пришлось пойти ради общего увеличения безопасности системы.

Сборка исправленных пакетов Net-SNMP

Теперь уже, когда в деталях раскрыта подноготная исправлений, опишу до конца процесс сборки пакета.

Скачиваем и устанавливаем пакеты, которые используются в процессе сборки Net-SNMP:
# apt-get build-dep snmp
Скачиваем патч:
$ wget http://stupin.su/files/netsnmp543_snmpv3.diff
Тут же, если вы используете библиотеку Net-SNMP из Python, можно скачать ещё один патч, описанный в заметке Исправление Python-прослойки библиотеки Net-SNMP:
$ wget http://stupin.su/files/netsnmp543_python_client.diff
Теперь перейдём в каталог с исходными текстами и наложим наши патчи:
# cd net-snmp-5.4.3~dfsg/
# patch -Np0 --ignore-whitespace < netsnmp543_snmpv3.diff
# patch -Np0 --ignore-whitespace < netsnmp543_python_client.diff
Добавим наши патчи в будущий пакет:
# dpkg-source --commit
Укажем имя патча snmpv3-python_client. В окне редактирования информации о патче введём что-нибудь подобное следующему (вплоть до двух нижних строчек, с которых начинается сам текст патча):
Description: Fixed Python bindings and SNMPv3 time window logic
Author: Vladimir Stupin <vladimir@stupin.su>
Last-Update: <2016-09-06>
--- net-snmp-5.4.3~dfsg.orig/python/netsnmp/client_intf.c
+++ net-snmp-5.4.3~dfsg/python/netsnmp/client_intf.c
Теперь отметим изменения, внесённые в пакет:
# dch -i
В окне редактирования введём информацию о нашей версии пакета:
net-snmp (5.4.3~dfsg-2.8+deb7u2) UNRELEASED; urgency=low

* Fixed Python bindings for IPv4 parameter in SET-requests
* Fixed SNMPv3 time window logic

-- Vladimir Stupin <vladimir@stupin.su>  Tue, 06 Sep 2016 16:01:20 +0500
Осталось собрать пакеты:
# dpkg-buildpackage -us -uc -rfakeroot
Теперь можно установить необходимые пакеты в систему:
# cd ..
# dpkg -i libsnmp-base_5.4.3~dfsg-2.8+deb7u2_amd64.deb
# dpkg -i libsnmp15_5.4.3~dfsg-2.8+deb7u2_amd64.deb
# dpkg -i snmp_5.4.3~dfsg-2.8+deb7u2_amd64.deb
# dpkg -i libsnmp-python_5.4.3~dfsg-2.8+deb7u2_amd64.deb
Python и SNMPv3 в Net-SNMP

Двух пересборок пакетов, на самом деле, оказалось недостаточно. Дело в том, что модуль netsnmp для Python не работает с SNMP третьей версии. Как оказалось, при попытке выполнить из программы на Python запроса SNMP с настройками третьей версии, библиотека "молчит" - в сеть не уходит ни единый пакет. В то же время я вспомнил, что на одном из серверов под управлением FreeBSD всё работало превосходно. Сразу же в глаза бросилось различие в версиях Net-SNMP во FreeBSD на том сервере и на сервере под управлением Debian Wheezy. Во FreeBSD был установлен Net-SNMP версии 5.7.2, а в Debian - 5.4.3. Чтобы проверить свои предположения о том, что этот функционал был исправлен в версии 5.7.2, я воспользовался Net-SNMP из Debian Jessie. Набросал небольшую тестовую программку на Python, запустил - всё отработало на ура.

После этого я заменил в файле /etc/apt/sources.list репозитории deb-src с релиза Wheezy на релиз Jessie и повторил проделанные выше операции, получив таким образом пропатченный бэкпорт Net-SNMP из Jessie для Wheezy. Дело, однако, этим не ограничилось. В бэкпорте вместо библиотеки libsnmp15 была библиотека libsnmp30, а пакет libsnmp-python был переименован в python-netsnmp. К библиотеке libsnmp15 были привязаны Zabbix и PHP5. Пришлось пересобрать и их, в том числе с использованием моих патчей Zabbix, описанных в заметке Установка и настройка Zabbix 2.2.0 в Debian Wheezy.

Патчи, адаптированные для Net-SNMP версии 5.7.2.1 из Jessie, можно взять по следующим ссылкам:
Мне невероятно "везёт" с программами, которыми я пользуюсь. Или просто я слишком часто стал заходить на никем ещё не топтанные дорожки?

воскресенье, 7 февраля 2016 г.

Исправление Python-прослойки библиотеки Net-SNMP

Заметка, ссылка на которую мне может в дальнейшем понадобиться.

Из всех SNMP-модулей Python для работы с SNMP наименьшей гадостью мне показалась официальная прослойка библиотеки Net-SNMP для Python. Её разработчики не так прониклись Java-подходом, как разработчики библиотеки pysnmp, воспользоваться которой невозможно без написания трёхэтажного кода с использованием всех фабрик классов, прокси-классов и преобразования типов, родных для Python, в классы, используемые библиотекой. Библиотека pysnmp выглядит в Python совершенно чужеродно, потребляет мнго ресурсов и плохо документирована.

Прослойка библиотеки Net-SNMP для Python выглядит очень куцей, однако для моих нужд её оказывалось достаточно, пока я не попробовал с её помощью инициировать резервное копирование коммутатора. Оказалось, что в ней неправильно реализовано преобразование параметров типа IPADDR в запросах SNMP SET. Официальный патч в тот момент я не нашёл и решил попробовать исправить библиотеку самостоятельно, что мне и удалось. Официальный патч можно найти тут: Python bindings: Fix the out-of-bounds write. Я же подготовил свой патч, использование которого будет описано ниже.

Скачиваем патч:
$ wget http://stupin.su/files/netsnmp543_python_client.diff
Скачиваем и распаковываем в текущий каталог исходные тексты пакета:
# apt-get source libsnmp-python
Скачиваем и устанавливаем зависимости, необходимые для сборки пакета:
# apt-get build-dep net-snmp
Скачиваем и устанавливаем инструменты для сборки:
# apt-get install dpkg-dev
# apt-get install fakeroot
# apt-get install devscripts
Переходим в каталог с исходными текстами пакета и накладываем патч:
# cd net-snmp-5.4.3~dfsg/
# patch -Np0 --ignore-whitespace < ../netsnmp543_python_client.diff
Добавим наш патч в будущий пакет:
# dpkg-source --commit
Укажем имя патча snmp_set_fixed. В окне редактирования информации о патче введём что-нибудь подобное следующему (вплоть до двух нижних строчек, с которых начинается сам текст патча):
Description: Fixed snmp set operation in python-snmp
Fixed snmp set operation in python-snmp
 .
 net-snmp (5.4.3~dfsg-2.8+deb7u1) stable-proposed-updates; urgency=medium
 .
--- net-snmp-5.4.3~dfsg.orig/python/netsnmp/client_intf.c
+++ net-snmp-5.4.3~dfsg/python/netsnmp/client_intf.c
Теперь отметим изменения, внесённые в пакет:
# dch -i
В окне редактирования введём информацию о нашей версии пакета:
net-snmp (5.4.3~dfsg-2.8+deb7ufanet) UNRELEASED; urgency=low

  * Fixed snmp set in libsnmp-python

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 21 Jan 2016 16:58:40 +0500
Осталось собрать пакет:
# dpkg-buildpackage -us -uc -rfakeroot
Теперь его можно установить в систему:
# cd ..
# dpkg -i libsnmp-python_5.4.3~dfsg-2.8+deb7u1_amd64.deb

воскресенье, 2 ноября 2014 г.

NUT и Eaton Powerware 5110

Эта заметка будет необычно длинной и нудной :) Дело в том, что поначалу система NUT показалась мне сложной, поэтому я начал читать man'ы, файлы конфигурации и выписывать понятое. Как оказалось, всё очень просто, но не пропадать же добру? Вот и решил выложить всё, что накопилось.

NUT - это сетевая клиент-серверная система для наблюдения за источниками бесперебойного питания. Сервер NUT устанавливается непосредственно на тот компьютер, к которому подключен информационный кабель ИБП. Клиенты устанавливаются на те компьютеры, которые запитаны от ИБП или на компьютеры, с которых нужно наблюдать за состоянием ИБП.

1. Серверная часть

Установим на компьютер с ИБП серверную часть NUT:
# apt-get install nut-server
Настройки сервера NUT находятся в трёх файлах:
  • /etc/nut/ups.conf - настройки драйверов ИБП,
  • /etc/nut/upsd.conf - настройки сервера,
  • /etc/nut/upsd.users - настройки аутентификации и прав пользователей сервера.
1.1. Драйверы ИБП

В файле /etc/nut/ups.conf описываются ИБП, подключенные к этому компьютеру. Файл представляет собой INI-файл, в котором секцией является имя ИБП, а внутри секции прописываются настройки драйвера ИБП. Для начала опишу глобальные настройки:
  • chroot - не обязательный параметр, указывающий chroot-каталог, в котором будет запускаться драйвер ИБП,
  • driverpath - не обязательный параметр, указывающий путь к исполняемым файлам драйверов ИБП,
  • maxstartdelay - не обязательный параметр, значение которого можно указать локально, в секции с описанием ИБП. Указывает время, в течение которого ожидается завершение запуска драйвера ИБП. По умолчанию - 45 секунд,
  • pollinterval - не обязательный параметр, указывающий интервалы времени, через которые необходимо запрашивать у ИБП свежие данные о его состоянии.
Локальные настройки, описывающие конкретный ИБП:
  • dirver - обязательный параметр, указывающий, какую программу использовать для взаимодействия с ИБП. Найти подходящий драйвер можно в файле /usr/share/nut/driver.list,
  • port - обязательный параметр, указывающий используемый для связи с ИБП последовательный или USB-порт. Возможными значениями могут быть файлы устройств вроде /dev/ttyS0 или значение auto,
  • sdorder - если к системе подключено несколько ИБП, то этот параметр указывает очерёдность отключения этого ИБП. Чтобы не отключать ИБП, здесь можно указать -1. Значение по умолчанию - 0,
  • desc - не обязательный параметр, описывающий ИБП. Может содержать произвольный текст,
  • nolock - не обязательный флаг, указывающий не блокировать порт связи с ИБП. В этом случае другие программы смогут работать с портом, но указывать этот флаг не рекомендуется,
  • ignorelb - не обязательный флаг, предписывающий игнорировать состояние ИБП разряда батареи. Вместо этого используются данные о текущем заряде батареи и его предельно низком значении (battery.charge < battery.charge.low) или о расчётном времени работы батареи и минимально допустимом времени работы от батареи (battery.runtime < battery.runtime.low). Это позволяет вручную задать предельные значения, по достижении которых батарея ИБП будет считаться разряженной (например, override.battery.charge.low = 30 - осталось 30% заряда батареи или override.battery.runtime.low = 180 - осталось 3 минуты работы от батареи),
  • default.переменная - не обязательный параметр, который задаёт значение по умолчанию для тех переменных, данные о которых ИБП не отдаёт,
  • override.переменная - не обязательный параметр, значение которого заменяет значение, отдаваемое ИБП.
Драйвер может иметь собственные, специфичные только для него настройки и команды, о которых можно прочитать на странице руководства man выбранного драйвера. Например, у драйвера bcmxcp_usb, конфигурация которого будет приведена ниже, имеются следующие настройки и команды:
  • shutdown_delay - пауза в секундах, которую ИБП выдержит между получением команды на отключение и её выполнением. По умолчанию - 120 секунд,
  • shutdown.return - команда отключить нагрузку и снова подать питание,
  • shutdown.stayoff - команда отключить нагрузку и не включать,
  • test.battery.start - команда начать проверку батареи.
В файл /etc/nut/ups.conf пропишем настройки драйвера ИБП:
[Powerware5110]
driver = bcmxcp_usb
port = auto
desc = "UPS on computer fstu"
1.2. Сервер NUT

Теперь настроим сервер, предоставляющий информацию о состоянии ИБП клиентам:
  • MAXAGE - максимальное время, в течение которого полученные от ИБП данные считаются актуальными. По умолчанию - 15 секунд,
  • STATEPATH - каталог, в котором размещаются сокет-файлы драйверов ИБП. По умолчанию /var/run/nut,
  • MAXCONN - максимальное количество разрешённых подключений к серверу, включая драйверы, директивы LISTEN и подключенных клиентов. По умолчанию - 1024,
  • CERTFILE - если вы хотите шифровать сетевые подключения, укажите здесь путь к PEM-файлу с сертификатом и ключом сервера. По умолчанию шифрование не используется,
  • LISTEN - директива, указывающая IP-адрес и TCP-порт, на которых сервер будет ожидать запросы клиентов. Может быть указана несколько раз. Может быть указан IPv6-адрес.
Поскольку в нашем случае единственный клиент располагается на компьютере с сервером, пропишем в файл /etc/nut/upsd.conf одну директиву:
LISTEN 127.0.0.1 3493
В файле /etc/nut/upsd.users описываются пользователи, которые будут работать с сервером. Файл имеет формат INI-файла, в котором именем секции является имя пользователя, а параметры внутри секции описывают права пользователя.
  • password - параметр задаёт пароль пользователя,
  • actions - параметр описывает разрешённые пользователю действия. Может быть указан несколько раз. Всего доступно два действия: SET - возможность устанавливать значения переменных ИБП, FSD - возможность инициировать ситуацию отсутствия питания на ИБП и низкого заряда батареи, которая может инициировать завершение работы компьютеров, запитанных от ИБП,
  • instcmds - параметр, разрешающий отдавать ИБП определённые команды. Может быть указан несколько раз. Чтобы разрешить все команды, можно указать значение ALL. Чтобы узнать список доступных команд, можно воспользоваться командой "upscmd -l". Полный список всех команд и переменных, поддерживаемых системой NUT, можно посмотреть в файле /usr/share/nut/cmdvartab,
  • upsmon master - флаг описывает пользователя, от имени которого будет подключаться клиент мониторинга, работающий на том же компьютере, что и сервер (и куда подключен интерфейс взаимодействия с ИБП),
  • upsmon slave - флаг описывает пользователя, от имени которого будет подключаться клиент мониторинга, работающий на компьютере отличном от того, где запущен сервер.
Теперь настроим доступ к серверу в файле /etc/nut/upsd.users. Добавим пользователя-администратора и пользователя, от имени которого будет подключаться клиент мониторинга, запущенный на том же компьютере, что и сервер:
[admin]
password = password
actions = SET
instcmds = ALL

[upsmon]
password = password
upsmon master
Если предполагается, что к серверу будут подключаться клиенты мониторинга, работающие на других компьютерах, можно добавить ещё одного пользователя с настройкой upsmon slave.

Осталось перезапустить сервер, чтобы его настройки вступили в силу:
# /etc/init.d/nut-server restart
2. Клиентская часть

На компьютеры, запитанные от ИБП нужно поставить пакет с консольными клиентами NUT:
# apt-get install nut-client
Внутри пакета есть три консольных клиента, умеющих взаимодействовать с сервером NUT:
  • upsc - клиент, при помощи которого можно запросить текущие значения переменных на ИБП,
  • upscmd - клиент, при помощи которого можно установить значения переменных ИБП или выполнять команды,
  • upsmon - клиент, следящий за состоянием одного или нескольких ИБП, который отсылает уведомления или выполняет команду отключения компьютера. Его конфигурация находится в файле /etc/nut/upsmon.conf.
В составе клиента имеется также утилита upssched с файлом конфигурации /etc/nut/upssched.conf, которая помогает осуществлять отложенное исполнение команд. В файле конфигурации можно настроить таймер, который будет запущен при наступлении одних событий (например, когда в ИБП не поступает внешнее питание, а его батарея разрядилась) или сброшен при наступлении других событий (например, когда на ИБП вновь начало поступать внешнее питание). Когда таймер срабатывает, выполняется указанная команда.

В файле /etc/nut/nut.conf имеется единственный параметр MODE, задающий режим работы клиента upsmon. Имеется четыре режима работы системы NUT:
  • none - система выключена,
  • standalone - автономная система, в которой интерфейс управления ИБП подключен к тому же компьютеру, который запитан от ИБП. Других компьютеров, запитанных от ИБП нет,
  • netserver - система, в которой от ИБП запитано несколько компьютеров. В этом случае сервер NUT должен быть доступен по сети для остальных компьютеров,
  • netclient - система, запитанная от ИБП, интерфейс управления которым подключен к другому компьютеру.
В нашем случае подходит значение standalone, которое и пропишем в файл /etc/nut/nut.conf:
MODE=standalone
2.1. Клиент для просмотра состояния ИБП

Текущее состояние ИБП можно посмотреть при помощи следующей команды:
$ upsc Powerware5110@localhost
Текущее значение любого из параметров можно посмотреть при помощи команды с указанием имени параметра. Например, текущий статус можно посмотреть при помощи такой команды:
$ upsc Powerware5110@localhost ups.status
Возможные значения статуса:
  • OL - ИБП работает от сети,
  • OB - ИБП работает от батареи,
  • LB - батарея разряжена,
  • RB - необходима замена батареи,
  • CHRG - батарея заряжается,
  • DISCHRG - батарея разряжается,
  • BYPASS - ИБП работает в режиме питания от сети в обход батареи,
  • OVER - ИБП перегружен,
  • TRIM - ИБП отрезает повышенное напряжение сети,
  • BOOST - ИБП усиливает пониженное напряжение сети,
2.2. Клиент для записи переменных ИБП

Для выполнения команд на ИБП необходимо предоставить имя пользователя и его пароль. Например, вот так можно заставить ИБП отключить потребителей, а при появлении питания на входе снова включить их:
$ upscmd -u admin -p password Powerware5110@localhost shutdown.return
Если команда принимает дополнительные параметры (например, время в секундах), то их значения можно указать после команды ИБП, в качестве дополнительного аргумента.

2.3. Клиент мониторинга ИБП

Настроим клиента мониторинга, который наблюдает за состоянием ИБП и при необходимости выключает компьютер. Для этого впишем в файл /etc/nut/upsmon.conf следующую конфигурацию:
# Описание ИБП, за которым нужно наблюдать
MONITOR Powerware5110@localhost 1 upsmon password master

# Команда, которую нужно выполнить перед пропаданием питания от ИБП
SHUTDOWNCMD "/sbin/shutdown -h +0"
В настройке MONITOR цифра 1 означает количество блоков питания компьютера, запитанных от этого ИБП. Большинство компьютеров имеет только один блок питания. Можно указать значение 0, если компьютер не запитан от ИБП, но необходимо следить за состоянием ИБП.

Кроме указанных настроек доступны также следующие:
# Пользователь, от имени которого будет работать клиент мониторинга
# RUN_AS_USER nutmon

# Минимальное количество запитанных блоков питания компьютера, необходимых для его работы
# Например, если компьютер обладает 4 блоками питания, но для работы достаточно 2, то
# здесь нужно вписать 2. Количество блоков питания, запитанных от определённого ИБП
# указывается в настройке MONITOR
MINSUPPLIES 1

# Обычная частота опроса состояния ИБП
POLLFREQ 5

# Частота опроса состояния ИБП, если он работает от батареи
POLLFREQALERT 5

# Интервал, в течение которого команда на отключение должна быть подтверждена
# Указывается на случай, если в течение этого времени вновь появится питание
# и отключение будет отменено
HOSTSYNC 15

# Если в течение этого интервала данные от ИБП не обновлялись, ИБП помечается
# как недоступный и заряд его батареи будет считаться низким, что может
# спровоцировать отключение компьютеров, запитанных от него
DEADTIME 15
Перезапустим клиента мониторинга:
# /etc/init.d/nut-client restart
Протестировать выключение системы в случае пропадания питания и разряда батареи, можно при помощи команды:
# upsmon -c fsd
2.4. Графический клиент

Графический клиент находится в пакете nut-monitor. Установим его:
# apt-get install nut-monitor
Чтобы подключиться к ИБП, нужно указать IP-адрес или имя сервера, порт и, если нужно, имя пользователя и пароль:


После подключения можно посмотреть краткую сводку по текущему состоянию ИБП:


На отдельной вкладке можно посмотреть значение всех переменных ИБП:


Можно добавлять ИБП в закладки и переключаться между ними:


Клиента можно свернуть в трей, он примет вид батарейки:

2.5. Веб-клиент

Веб-клиент находится в пакете nut-cgi. Установим сам клиент, а также nginx и uwsgi для запуска CGI-приложений:
# apt-get install nut-cgi nginx uwsgi
Настройки веб-клиента располагаются в файлах /etc/nut/upsset.conf и /etc/nut/hosts.conf.

В первом файле нет никаких настроек. Его единственное назначение - отключить веб-интерфейс до тех пор, пока вы не ограничите в веб-сервере список узлов, с которых будет доступен этот веб-интерфейс. Когда доступ ограничен, можно раскомментировать единственную опцию из этого файла:
I_HAVE_SECURED_MY_CGI_DIRECTORY
Во втором файле находится список ИБП, состояние которых можно будет просматривать через этот веб-интерфейс. Директивы имеют следующий вид:
MONITOR myups@localhost "Local UPS"
В нашем случае в этом файле будет лишь одна директива:
MONITOR Powerware5110@localhost "Powerware5110 on my computer"
Для запуска веб-клиента NUT будем использовать uwsgi и nginx. Настроим пул процессов, обслуживающих CGI-приложение NUT в файле /etc/uwsgi/apps-available/cgi.ini:
[uwsgi]

procname = uwsgi-cgi
procname-master = uwsgi-cgi-master

plugins = cgi
cgi = /usr/lib/
cgi-allowed-ext = .cgi
cgi-timeout = 300
processes = 2
Включим использование этого приложения:
# cd /etc/uwsgi/apps-enabled/
# ln -s /etc/uwsgi/apps-available .
Запустим включенное приложение uwsgi:
# /etc/init.d/uwsgi start cgi
Теперь настроим обработку доступа к CGI-страницам в файле /etc/nginx/sites-available/default:
server {
  listen 127.0.0.1:80; 
  server_name localhost;

  root /usr/share/nut/www;
  index index.html;

  location ~ ^/cgi-bin/nut/(upsimage|upsset|upsstats)\.cgi$ {
    allow 127.0.0.1;
    deny all;
    
    include uwsgi_params;
    uwsgi_modifier1 9;
    uwsgi_pass unix:/var/run/uwsgi/app/cgi/socket;
    uwsgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/nut/$1.cgi;
  }
}
Включим использование этого сайта:
# cd /etc/nginx/sites-enabled/
# ln -s /etc/nginx/sites-available .
Запустим включенное приложение uwsgi:
# /etc/init.d/nginx reload
Директивы allow и deny в файле конфигурации nginx ограничивают доступ к веб-клиенту NUT только IP-адресом самого компьютера. Если в директиве listen указан IP-адрес 127.0.0.1, то директивы deny и allow указывать не обязательно, т.к. к веб-серверу просто неоткуда больше подключиться, кроме как с самого компьютера. Если же веб-сервер прослушивает какие-то внешние IP-адреса, то в директивах allow нужно перечислить те IP-адреса или подсети, с которых необходим доступ к веб-интерфейсу.

На странице http://localhost/cgi-bin/nut/upsstats.cgi можно увидеть таблицу с ИБП с кратким описанием текущего состояния каждого из них. В моём случае ИБП всего один:


Если в таблице ИБП перейти по ссылке в левом столбце, то можно попасть на такую вот страницу с диаграммами текущего состояния ИБП:


Если в таблице ИБП перейти по ссылке в правом столбце, то можно попасть на страницу с текущими значениями переменных ИБП:


воскресенье, 5 октября 2014 г.

Настройка почты в Redmine, обновление структуры базы данных

1. Настройка почты

Для отправки уведомлений об изменении в задачах заинтересованным лицам, в каталоге с настройками Redmine можно создать файл /etc/redmine/default/configuration.yml следующего вида:
production:
  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      address: mail.domain.tld
      port: 587
      domain: domain.tld
      authentication: :login
      user_name: redmine@domain.tld
      password: uxai5eikahf2Ik8H
Смысл настроек понятен из их названия. Существует ещё возможность отправлять почту при помощи локальной команды sendmail. Подробнее о настройках отправки почты можно почитать здесь: Настройка почты

Теперь нужно перезапустить redmine, чтобы он заново прочитал файлы конфигурации:
# /etc/init.d/uwsgi restart redmine
Теперь нужно зайти в Redmine, перейти в раздел "Администрирование" -> "Настройки":


Во вкладке "Общие" поправить поле "Имя компьютера", указав веб-адрес, по которому доступен Redmine, но без протокола и без косых черт слева и справа:


Во вкладке "Уведомления по email" нужно выбрать события, уведомления о которых нужно отсылать, указать адрес, с которого будут отправляться письма:

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

Адреса пользователей можно редактировать в их профилях.

2. Обновление структуры базы данных

Если у вас имеется база данных от старой версии Redmine, которую вы хотите развернуть на более свежей версии Redmine, то обновить структуру базы данных до более свежей версии очень просто:
# cd /usr/share/redmine
# rake db:migrate RAILS_ENV=production X_DEBIAN_SITEID=support
В Debian существует возможность легко запустить на одном компьютере несколько разных экземпляров Redmine. В переменной X_DEBIAN_SITEID можно указать имя экземпляра Redmine. Имя экземпляра задаёт имя подкаталога в каталоге /etc/redmine/. В подкаталоге экземпляра находятся файлы с настройками Redmine. Если имя экземпляра не указывать, то подразумевается экземпляр default и используются настройки из каталога /etc/redmine/default/.

В Debian даже есть возможность управлять созданием и удалением экземпляров Redmine при помощи мастера, которого можно запустить по команде dpkg-reconfigure redmine. Но я предостерегаю им пользоваться, т.к. при создании нового экземпляра он удаляет настройки уже существующего, не обращая внимания на ваш ответ о том, что вы не хотите это делать.

воскресенье, 28 сентября 2014 г.

Установка Redmine в Debian Wheezy

Эту заметку отчасти можно рассматривать как продолжение заметки Настройка nginx, php5-fpm и uwsgi. Здесь я покажу, как можно запустить Redmine под управлением uwsgi.

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

Написана эта система на языке Ruby и с использованием очень известного за пределами Ruby-мира веб-фреймворка Rails. Запустить дистрибутивный Redmine мне удалось далеко не с первого раза: сначала появлялась какая-то ошибка обработки UTF-8, которую удалось решить переключением с Ruby1.9.1 на Ruby1.8, потом я долго не мог справиться с неполной передачей данных между веб-сервером и uwsgi, которая решилась указанием специальной опции буферизации uwsgi.

Установка пакетов

Для начала установим необходимые пакеты:
# apt-get install redmine redmine-mysql ruby1.8 uwsgi uwsgi-plugin-rack-ruby1.8 nginx-full
Как уже понятно из состава установленных пакетов, в качестве СУБД будем использовать MySQL. В процессе установки запустится мастер настройки базы данных. Ниже приведены снимки экрана с его диалоговыми окнами:







Настройка uwsgi

Для запуска Redmine под управлением uwsgi создадим файл /etc/uwsgi/apps-available/redmine.ini со следующим содержимым:
[uwsgi]

master = true
procname = uwsgi-redmine
procname-master = uwsgi-redmine-master
  
plugin = uwsgi_rack_ruby18
rails = /usr/share/redmine/
env = RAILS_ENV=production
env = RAILS_RELATIVE_URL_ROOT=/redmine
processes = 4
post-buffering = 1
Описание опции post-buffering, с помощью которой удалось решить проблему с неполной передачей данных между веб-сервером и uwsgi, найденное на странице Things to know (best practices and “issues”) READ IT !!!:
If an HTTP request has a body (like a POST request generated by a form), you have to read (consume) it in your application. If you do not do this, the communication socket with your webserver may be clobbered. If you are lazy you can use the post-buffering option that will automatically read data for you. For Rack applications this is automatically enabled.
Несмотря на то, что тут написано что эта опция автоматически включается для Rack-приложений, на практике мне пришлось включить её вручную. Возможно автоматическое включение опции было добавлено в более поздних версиях uwsgi.

Переменная окружения RAILS_RELATIVE_URL_ROOT позволяет настроить Redmine для работы из подкаталога веб-сервера. Если вы запускаете Redmine на отдельном виртуальном или выделенном сервере, то эту строку указывать не нужно.

Осталось включить приложение:
# cd /etc/uwsgi/apps-enabled
# ln -s /etc/uwsgi/apps-available/redmine.ini .
# /etc/init.d/uwsgi start redmine
Настройка веб-сервера

Для начала приведу пример файла конфигурации для настройки Redmine для работы из корня сайта:
server {
  listen 80;

  server_name redmine.domain.tld;

  location / {
    alias /usr/share/redmine/public/;
    try_files $uri @redmine;
  }

  location @redmine {
    uwsgi_pass unix:/var/run/uwsgi/app/redmine/socket;
    include uwsgi_params;

    uwsgi_modifier1 7;
  }
}
Этот файл представляет собой настройки отдельного сайта, поэтому его нужно поместить в отдельный файл в каталоге для сайтов nginx. В данном случае это будет файл /etc/nginx/sites-available/redmine. Чтобы подключить его использование в nginx, нужно создать символическую ссылку:
# cd /etc/nginx/sites-enabled
# ln -s /etc/nginx/sites-available/redmine .
Осталось перезагрузить или перезапустить nginx, чтобы настройки нового сайта вступили в силу:
# /etc/init.d/nginx restart
Можно настроить Redmine для работы из подкаталога /redmine. Для этого нужно вписать следующие настройки в файл конфигурации уже существующего сайта:
location /redmine/ {
    alias /usr/share/redmine/public/;
    try_files $uri @redmine;
  }

  location @redmine {
    uwsgi_pass unix:/var/run/uwsgi/app/redmine/socket;
    include uwsgi_params;

    uwsgi_modifier1 7;
  }
Осталось перезагрузить nginx, чтобы новые настройки вступили в силу:
# /etc/init.d/nginx reload
Redmine

Теперь можно зайти в Redmine:

В системе уже создан пользователь с логином admin и таким же паролем. Не забудьте сразу же после входа поменять пароль!

воскресенье, 7 сентября 2014 г.

Тайловый сервер на основе Python, Mapnik и Bottle

В прошлом я писал серию статей о настройке тайлового сервера на основе apache2, renderd и модуля mod_tile. Недостаток этой связки, с моей точки зрения, заключается во-первых в необходимости использования apache, а во-вторых в том, что renderd и mod_tile отсутствуют в репозиториях Debian. Из-за второго недостатка нет уверенности в том, что при очередном обновлении её удастся успешно собрать и продолжить использовать.

Несколько месяцев назад мне попалась статья Дениса Рыкова Основы работы динамических TMS-сервисов, в которой приводится пример тайлового сервера, написанного на Python с использованием веб-фреймворка Bottle и библиотеки Mapnik. Статья привлекла моё внимание ещё потому, что в последних проектах я начал пользоваться фреймворком Bottle. Этот фреймворк обладает минимальным количеством зависимостей и может всё, что мне нужно от веб-фреймворков (да, мои потребности не велики), не навязывая при этом собственный стиль разработки, как это происходит, например, с Django.

Я решил воспользоваться этой статьёй для того, чтобы избавить себя от возможных проблем с используемой мной связкой на основе apache2, renderd и mod_tile.

1. Тайловый сервер

Для начала установим библиотеку Mapnik и веб-фреймворк Bottle:
# apt-get install python-mapnik2 python-bottle
Сам тайловый сервер разместим в файле /usr/local/share/tiles/main.py:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

from bottle import get, response, run, debug, app, HTTPError
import mapnik2 as mapnik
import os

from settings import TILE_WIDTH, TILE_HEIGHT, BBOX_MINX, BBOX_MINY, BBOX_MAXX, BBOX_MAXY, MAPNIK_CONFIGS, CACHE_PATH

# Сохранение тайла в файловую систему в целях кэширования
def save_tile(filename, body):
    path, name = os.path.split(filename)
    try:
        os.makedirs(path)
    except:
        pass
    file = open(filename, 'w')
    file.write(body)
    file.close() 
 
@get('/<service>/1.0.0/<layer>/<z:int>/<x:int>/<y:int>.png')
def tms(service, layer, z, x, y):
    # Считаем охват тайла на выбранном масштабном уровне
    stepx = (BBOX_MAXX - 1.0 - BBOX_MINX) / (2 ** z)
    stepy = (BBOX_MAXY - 1.0 - BBOX_MINY) / (2 ** z)

    # Выбираем охват тайла из словаря extents согласно типу запрашиваемого сервиса (tms или xyz)
    if service == 'tms':
        box = mapnik.Box2d(BBOX_MINX + x * stepx,
                           BBOX_MINY + y * stepy,
                           BBOX_MINX + (x + 1) * stepx,
                           BBOX_MINY + (y + 1) * stepy)
    elif service == 'xyz':
        box = mapnik.Box2d(BBOX_MINX + x * stepx,
                           BBOX_MAXY - (y + 1) * stepy,
                           BBOX_MINX + (x + 1) * stepx,
                           BBOX_MAXY - y * stepy)
 
    # Указываем путь, где находится файл с настройками Mapnik
    map = mapnik.Map(TILE_WIDTH, TILE_HEIGHT)
    mapnik.load_map(map, os.path.join(MAPNIK_CONFIGS, layer + '.xml'))
 
    # Нацеливаем карту на выбранный охват
    map.zoom_to_box(box)
 
    # Отрисовываем тайл
    image = mapnik.Image(map.width, map.height)
    mapnik.render(map, image)
    body = image.tostring('png')

    # Сохраняем тайл в файловой системе
    filename = os.path.join(CACHE_PATH, service, '1.0.0', layer, str(z), str(x), str(y) + '.png')
    save_tile(filename, body)

    # Передаём ответ клиенту
    response.content_type = 'image/png'
    return body

if __name__ == '__main__':
    debug(True)
    run(host = '0.0.0.0', port = 8080)
else:
    application = app()
В отдельном файле /usr/local/share/tiles/settings.py разместим настройки этого приложения:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# Размеры тайла
TILE_WIDTH = 256
TILE_HEIGHT = 256

# Размеры карты
BBOX_MINX = -20037508.0
BBOX_MINY = -20037508.0
BBOX_MAXX = 20037508.0
BBOX_MAXY = 20037508.0

# Каталог с xml-файлами с описаниями слоёв
MAPNIK_CONFIGS = '/etc/mapnik-osm-data/'
# Каталог для хранения кэша тайлов
CACHE_PATH = '/var/cache/tiles'
Таким образом, в запросе можно указывать имя слоя, тайл из которого нужно вернуть. Имя слоя преобразуется в имя XML-файла, из которого извлекается его описание. Например, слою OpenStreetMap будет соответствовать запрос http://tiles.domain.tld/xyz/1.0.0/osm/0/0/0.png, при обслуживании которого будет использован файл /etc/mapnik-osm-data/osm.xml.

Создадим каталог для кэша тайлов:
# mkdir /var/cache/tiles
Проставим права доступа к нему для веб-сервера:
# chown www-data:www-data /var/cache/tiles
2. Сервер приложений uwsgi

Установим uwsgi и плагин к нему uwsgi-plugin-python, если они ещё не установлены:
# apt-get install uwsgi uwsgi-plugin-python
Создадим файл конфигурации /etc/uwsgi/apps-available/tiles.ini:
[uwsgi]

procname = uwsgi-tiles
procname-master = uwsgi-tiles-master

chdir = /usr/local/share/tiles
#module = app:application
mount = /=main:application
plugin = python27
master = true
processes = 8
Добавим файл конфигурации в каталог включенных приложений:
# cd /etc/uwsgi/apps-enabled
# ln -s /etc/uwsgi/apps-available/tiles.ini .
Запустим новое приложение:
# /etc/init.d/uwsgi start tiles
3. Веб-сервер nginx

Установим nginx, если он ещё не установлен:
# apt-get install nginx-full
Для доступа к файлам в кэше и запросе файла у тайлового сервера при их отсутствии в кэше создадим файл конфигурации. Например, если этот веб-сервер будет использоваться только для отрисовки тайлов, можно создать файл /etc/nginx/sites-available/default со следующим содержимым:
server {
  listen 0.0.0.0:80;
#  server_name tiles.domain.tld;

  root /var/www;

  location @tiles {
    uwsgi_pass unix:/var/run/uwsgi/app/tiles/socket;
    include uwsgi_params;
  }

  location /tms/ {
    root /var/cache/tiles/;
    try_files $uri @tiles;
  }

  location /xyz/ {
    root /var/cache/tiles/;
    try_files $uri @tiles;
  }
}
Включим использование этого файла веб-сервером:
# cd /etc/nginx/sites-enabled
# ln -s /etc/nginx/sites-available/tiles .
Перезапустим nginx, чтобы его новые настройки вступили в силу:
# /etc/init.d/nginx restart
4. Очистка кэша

Осталось только настроить периодическую чистку кэша от тайлов, к которым давно не было обращений. Это позволит достичь более высокой эффективности кэша в пересчёте на его объём. Сделать это просто - можно воспользоваться обычыми для Unix инструментами - планировщиком задач cron и командой find. Добавим в файл /etc/crontab такую строчку:
30    8 * * *   www-data     find /var/cache/tiles -type f -atime 14 -delete ; find /var/cache/tiles -type d -empty -delete
Из каталога кэша тайлов будут удаляться файлы, к которым не обращались более 14 дней, а также пустые каталоги.

Хочу отметить, что рассматриваемый тайловый сервер - скорее заготовка, т.к. он, например, практически не осуществляет обработку возможных исключительных ситуаций - отсутствие файла конфигурации, отсутсвие прав доступа к каталогу тайлов и т.п. Такие исключительные ситуации в конечном итоге попадают в фреймворк Bottle, который при их возникновении просто сообщает о внутренней ошибке сервера с кодом 500.