воскресенье, 22 ноября 2015 г.

Разбор уязвимости DNS Каминского

Оригинал: Understanding Kaminsky's DNS Bug
Автор: Кори Райт (Cory Wright)

За последние дни всплыли подробности о природе уязвимости DNS, о которой Дэн Камински объявил две недели назад. Да, она настолько крупная и страшная, как и сообщалось.

Как вам, может быть, известно, Каминский согласовал объявление со многими крупными разработчиками программного обеспечения и обещал не раскрывать способ эксплуатации уязвимости до 6 августа, когда состоится ежегодная конференция специалистов по компьютерной безопасности Black Hat (чёрная шляпа) в Лас-Вегасе. У провайдеров было 30 дней, чтобы исправить их системы и защитить своих пользователей. Каминский также попросил членов сообщества по компьютерной безопасности воздержаться от публикации подробностей проблемы, пока не будет опубликовано официальное заявление. Возможно он попросил слишком многого, потому что уже через 13 дней проблема стала достоянием общественности.

Теперь стали известны подробности, а сам Каминский предоставил комментарии. Давайте изучим проблему и попытаемся понять, насколько она серьёзная.

Основы DNS

Для начала, нам нужно понять основы обмена данными DNS. Обычно имеется три действующих стороны:
  • ваш компьютер,
  • рекурсивные серверы DNS вашего провайдера, и
  • авторитативные DNS-серверы веб-сайта.
Авторитативные серверы DNS публикуют IP-адреса доменных имён. Рекурсивные серверы DNS общаются с авторитативными серверами DNS, чтобы найти адреса опубликованных ими доменных имён. Ваш компьютер общается только с рекурсивными серверами DNS, которые ищут для вас адрес доменного имени. Ваш компьютер и рекурсивные серверы DNS вашего провайдера представляют собой две разновидности клиентов, которые запрашивают информацию извне. Конечно, это упрощённое представление процесса, но для наших целей этого достаточно.

Давайте рассмотрим типичные DNS-запрос и ответ. Для этого мы воспользуемся доменным именем example.com и парой выдуманных серверов DNS. (Ненужные фрагменты вывода dig вырезаны).
$ dig @ns1.example.com www.example.com
;; ANSWER SECTION/РАЗДЕЛ ОТВЕТА/:
www.example.com. 120 IN A 192.168.1.10
;; AUTHORITY SECTION/АВТОРИТЕТНЫЙ РАЗДЕЛ/:
example.com. 86400 IN NS ns1.example.com.
example.com. 86400 IN NS ns2.example.com.
;; ADDITIONAL SECTION/ДОПОЛНИТЕЛЬНЫЙ РАЗДЕЛ/:
ns1.example.com. 604800 IN A 192.168.2.20
ns2.example.com. 604800 IN A 192.168.3.30
Здесь мы обратились к авторитетному серверу DNS, ns1.example.com, и спросили у него адрес www.example.com. Как можно видеть, ответ содержит IP-адрес www.example.com наряду с двумя другими наборами записей - авторитетными и дополнительными. Авторитетный раздел содержит список авторитативных серверов DNS для домена из запроса. Дополнительный раздел содержит IP-адреса этих серверов. То, что вся эта информация возвращается в одном ответе, имеет непосредственное отношение к недавней уязвимости.

Проверка зоны ответственности

Рассмотрим следующую аналогию. Предположим, что мы с вами путешествуем по скоростной автомагистрали и наша машина ломается. Я спрашиваю у вас телефонный номер ближайшей мастерской, а вы отвечаете: "Я не знаю их номер телефона. Вам нужно позвонить им, чтобы его найти." Но как узнать номер мастерской, если её номер ещё не известен?

Теперь представим ту же ситуацию применительно к DNS. Мы хотим найти адрес www.example.com, поэтому мы просим у корневого сервера DNS список DNS-серверов для .com. Корневой сервер DNS даёт нам список DNS-серверов .com, из которого мы берём один и спрашиваем у него список DNS-серверов для example.com. Если серверы .com просто ответят, что это ns1.example.com и ns2.example.com, мы окажемся в тупике. Мы хотели найти информацию об example.com, но по дороге узнали, что должны обратиться за ответом к DNS-серверам example.com. Чтобы решить проблему курицы и яйца, в дополнительном разделе предоставляются две записи "A", которые являются недостающим звеном. Они называются связующими записями.

Связующие записи - это обычные записи типа "A", которые предоставляются вместе с ответом. В ответе выше вполне могли оказаться посторонние записи:
$ dig @ns1.example.com www.example.com
;; ANSWER SECTION/РАЗДЕЛ ОТВЕТА/:
www.example.com. 120 IN A 192.168.1.10
;; AUTHORITY SECTION/АВТОРИТЕТНЫЙ РАЗДЕЛ/:
example.com. 86400 IN NS ns1.example.com.
example.com. 86400 IN NS ns2.example.com.
;; ADDITIONAL SECTION/ДОПОЛНИТЕЛЬНЫЙ РАЗДЕЛ/:
ns1.example.com. 604800 IN A 192.168.2.20
ns2.example.com. 604800 IN A 192.168.3.30
www.linuxjournal.com. 43200 IN A 66.240.243.113
Заметили дополнительную строку в конце? Мы запросили информацию о чём-то в домене example.com, но подлый сервер добавил в ответ информацию о www.linuxjournal.com. Этот сервер действительно имеет право отвечать на DNS-запросы к linuxjournal.com? Чтобы определить, принимать или не принимать эти дополнительные записи, клиенты используют приём, который называется проверкой зоны ответственности. Это просто означает, что игнорируются любые записи не из этого же домена. Если мы попросили информацию о ftp.example.com, то из дополнительного раздела мы примем информацию только об example.com.

Начиная с 1997 года, почти все современные клиенты DNS используют проверку зоны ответственности, чтобы защититься от этой разновидности атаки заражения кэша.

UDP и идентификаторы запросов

Большая часть трафика DNS отправляется поверх UDP, который является протоколом без установки соединения. Это означает, что клиент (ваш компьютер или рекурсивный сервер DNS) отправляет запрос и просто ждёт, когда кто-нибудь ответит. Обычно несколько запросов DNS выполняются одновременно, поэтому клиенту нужен способ сопоставить отправленные запросы с полученными ответами. Для этого каждый запрос снабжается числом от 0 до 65536, которое называется идентификатором запроса. Сервер всегда отправляет ответ с тем же идентификатором запроса, какой он получил в запросе.

Существовали эксплойты, угадывающие идентификатор запроса. Раньше клиенты просто увеличивали идентификатор запроса, поэтому угадать его было предельно просто. После появления эксплойтов, большинство DNS-клиентов начали использовать в качестве идентификаторов запросов псевдослучайные числа. Поскольку идентификатор запроса - это 16-битное число, чисел для выбора не так уж много (всего 65536). Исследователи в области компьютерной безопасности продемонстрировали методы предсказания этих случайных чисел.

Эксплойт

После того, как мы разобрались с основами, перейдём к эксплойту Каминского.

Представим, что клиент запрашивает IP-адрес doesnotexist.example.com. Атакующий отправляет ответ, который выглядит следующим образом:
$ dig doesnotexist.example.com
;; ANSWER SECTION/РАЗДЕЛ ОТВЕТА/:
doesnotexist.example.com. 120 IN A 10.10.10.10
;; AUTHORITY SECTION/АВТОРИТЕТНЫЙ РАЗДЕЛ/:
example.com. 86400 IN NS www.example.com.
;; ADDITIONAL SECTION/ДОПОЛНИТЕЛЬНЫЙ РАЗДЕЛ/:
www.example.com. 604800 IN A 10.10.10.20
Атакующий пытается обмануть клиента, заставив его поверить, что www.example.com теперь находится за 10.10.10.20 и это нужно запомнить на 604800 секунд (7 дней). Проверка зоны ответственности проходит успешно, поскольку домены в авторитетном и дополнительном разделах являются доменами из запроса. Однако вспомним, что клиент примет ответ только с тем идентификатором, который был отправлен в запросе. Поскольку в большинстве случаев трафик передаётся через UDP, ничто не мешает атакующим наводнить ответами клиента. Но отправка ответов на несуществующие запросы бессмысленна. Поэтому нужно чтобы ответ атакующего был получен до настоящего ответа.

Но атакующий не может просто угадать отправленные запросы. Чтобы не угадывать запросы, атакующий может настроить веб-страницу с большим количеством картинок, указывающих на различные домены. Вот так:
<img src="http://aaaa.example.com/image.jpg"/>
<img src="http://aaab.example.com/image.jpg"/>
<img src="http://aaac.example.com/image.jpg"/>
Когда браузер попытается отобразить эту страницу, он попросит клиента DNS найти адреса aaaa.example.com, aaab.example.com и так далее, пока пока не будут найдены адреса для всех 1000 картинок. По ходу поиска он будет отправлять запросы с разными идентификаторами запросов, от 1 до 65535. Если атакующий будет постоянно отправлять ответы с одним и тем же идентификатором запроса, например - 12345, в конечном итоге этот идентификатор совпадёт с идентификатором из одного из настоящих запросов и будет принят. (Для наглядности можно представить, что атакующий отправляет обратно одновременно 65536 ответов с разными идентификаторами запросов, и тогда уж точно один из ответов будет принят.)

А как же проверка зоны ответственности? Вспомним, что атакующий не контролирует example.com, он просто отправляет свои собственные ответы клиенту DNS, чтобы поддельные ответы прошли проверку зоны ответственности. Ответы могут выглядеть следующим образом:
;; ANSWER SECTION/РАЗДЕЛ ЗАПРОСА/:
aaaa.example.com. 120 IN A 10.10.10.10
;; AUTHORITY SECTION/АВТОРИТЕТНЫЙ РАЗДЕЛ/:
example.com. 86400 IN NS www.example.com.
;; ADDITIONAL SECTION/ДОПОЛНИТЕЛЬНЫЙ РАЗДЕЛ/:
www.example.com. 604800 IN A 10.10.10.20
Атакующий отправил ответ вашему клиенту DNS и указал ему на 7 дней сохранить 10.10.10.20 в качестве IP-адреса www.example.com. Все последующие DNS-запросы будут отправляться по этому адресу, потому что запись NS тоже попадёт в кэш.

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

До выпуска недавних исправлений уязвимости все клиенты провайдера (Comcast, AT&T, Verizon и т.п.) оказались бы уязвимыми, если один из них посетил бы веб-страницу атакующего. Рекурсивные DNS-серверы провайдера могли бы слать весь трафик www.bankofamerica.com на IP-адрес, который управляется кем-то, обладающим сомнительной репутацией.

Реальность эксплойта

Да, эксплойт реален и серьёзен. Крикет Ли, признанный эксперт и автор широкой известной книги "DNS и BIND" издательства O'Reilly, предположил, что это может быть самой значительной проблемой безопасности DNS в истории Интернета, а большинство других экспертов с ним согласились. Дэн Камински упомянул, что обмануть систему можно менее чем за 10 секунд. Это означает, что Дэн мог в считанные секунды заполучить контроль над вашим банковским счётом, электронной почтой, учётной записью на ebay или чем угодно, чем вы активно пользуетесь. И вы ничего не смогли бы с этим поделать. Каждый из тысяч или сотен тысяч пользователей вашего провайдера мог поставить под угрозу всех остальных клиентов, включая вас. Вот почему было так важно, чтобы эти системы были вовремя исправлены.

Перейдите на веб-сайт Дэна Камински, DoxPara Research, и щёлкните по кнопке "Check my DNS" ("Проверить мой DNS") справа, чтобы проверить, безопасен ли ваш провайдер. Если он всё ещё уязвим, вам стоит подумать о временном использовании OpenDNS.

воскресенье, 8 ноября 2015 г.

Переводы страниц руководства на manpages.stupin.su

Ранее, в заметке Легкий перевод страниц руководства с помощью po4a, я уже упоминал о своём проекте переводов страниц руководства manpages.ylsoftware.com. Теперь этот проект переехал на адрес manpages.stupin.su.

На старом сайте проекта практически все переводы соответствуют версиям страниц руководства, поставляемых в Debian Lenny. Долгое время я пытался обновить переводы, но каждый раз не успевал закончить обновление до выхода очередного релиза. К добавлению были готовы переводы из новых пакетов, которые я собирался представить одновременно с обновлением остальных переводов. И вот наконец я успел выполнить обновление до выхода очередного релиза Debian. В значительной мере этот релиз состоялся благодаря Вячеславу Чертову, который выполнил перевод страниц руководства из пакета traceroute, а также сконвертировал имеющийся в сети перевод страницы руководства screen и обновил его.

К обновлению приурочены несколько нововведений. Во-первых, сайт сменил адрес. Во-вторых, с него убраны устаревшие разделы, в том числе раздел с переводами из Dragonfly BSD. В-третьих, репозиторий теперь генерируется при помощи очень удобного инструмента - aptly. И, наконец, вишенка на торте - на сайте появился favicon :)

Подробнее о релизе можно прочитать в новости на сайте: manpages.stupin.su.

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

IPSec между Debian и MikroTik

Коллеги с прошлой работы обратились за помощью. В одном удалённом офисе начались проблемы со связью, исправить которые провайдер не смог. В результате этот офис подключили к другому провайдеру. С предыдущим провайдером использовался DSL-модем, а компьютеры удалённого офиса подключались к центральному офису через индивидуальные подключения PPTP. Новый провайдер предоставляет услуги по Ethernet и поэтому DSL-модем нужно было заменить на что-то другое. В качестве замены они выбрали маршрутизатор MikroTik RB951G-2HnD, планируя заменить PPTP-подключения от каждого компьютера на общий туннель IPSec.



Не вдаваясь в детали, скажу что в центральном офисе установлен сервер под управлением Debian Squeeze, с которым и нужно было объединить MikroTik туннелем IPSec. На самом деле офис этот тоже не совсем центральный и имеется большой набор ресурсов, который доступен через каналы в вышестоящие локальные сети. Список используемых удалённым офисом ресурсов непостоянен, а потому стандартная настройка IPSec с шифрованием трафика между заранее известными сетями нам не подойдёт. В данном случае нужно считать, что за центральным офисом как бы находится сеть 0.0.0.0/0. В удалённом же офисе используется сеть 192.168.81.240/28.

Если изобразить это схематично, то получится такая схема:
[0.0.0.0/0] --- 192.168.81.1 debian 11.11.11.11 ==== 22.22.22.22 mikrotik 192.168.81.241 --- [192.168.81.240/28]
1. Хождение по мукам

Этот раздел можно безболезненно пропустить. Здесь я просто дал волю своей графомании и описал, какой ценой мне достался этот рецепт. Можете считать меня неудачником :)

Как ни странно, но сходу настроить даже тестовую конфигурацию мне не удалось. Туннель упорно не хотел подниматься и трафик не шёл. Стало понятно, что взять эту задачу нахрапом не получится. Маршрутизатор отдали мне на растерзание домой. Дома у меня есть коммутатор с поддержкой VLAN и компьютер, на котором я думал настроить необходимое количество виртуальных машин и соединить всё это между собой, собрав этакий виртуальный стенд.

В первый выходной я потратил на всё это безобразие около 9 часов и всё-таки поднял туннель, соединив внешний интерфейс маршрутизатора с внешним интерфейсом виртуальной машины под управлением Debian:
debian 11.11.11.11 === 11.11.11.12 mikrotik
Я, правда, так и не понял, чем настроенная конфигурация отличалась от той, которую я пытался настраивать первоначально. И так много времени ушло в этот раз скорее не на саму настройку, а на подготовительные работы - проброс VLAN, поиск нормально работающей системы для запуска виртуальных машин (воспользовался VirtualBox), её правильную настройку (нужно было собрать дополнительные модули для ядра Linux), скачивание и установку в виртуальную машину Debian Squeeze. Дополнительное время ушло на тупление с мостовым интерфейсом, который я настроить-настроил, а поднять забыл.

Ещё некоторое время было потрачено из-за моей невнимательности, когда IPSec стал требовать вдруг шифрования трафика и на локальном интерфейсе, в результате чего я потерял управление и мне пришлось сбрасывать MikroTik и настраивать его снова. Зато я узнал о существовании "безопасного режима" в RouterOS.

Наконец, после установки первоначального туннеля я потратил ещё некоторое время на шлифование конфигурации.

Во второй выходной я уже попытался воссоздать будущие условия работы маршрутизатора более точно. Сначала настроил такую схему, с дополнительной виртуалкой, которая изображала сеть провайдера и маршрутизировала трафик между Debian и MikroTik'ом:
debian 11.11.11.11 ==(провайдер)== 22.22.22.22 mikrotik
Далее я настроил ещё две виртуальные машины, каждая из которых изображала компьютер в офисе. Одна виртуальная машина изображала компьютер в центральном офисе, а вторая - в удалённом офисе. Получилась уже такая схема:
[192.168.80.113/24] --- 192.168.81.1 debian 11.11.11.11 ==(провайдер)== 22.22.22.22 mikrotik 192.168.81.241 --- [192.168.81.242/28]
Далее я потратил ещё некоторое время на тестирование настроек при включенном NAT на Debian. Дело в том, что NAT изменяет адрес отправителя (что ожидаемо - для этого он и предназначен). Из-за этого пакет с изменившимися IP-адресами отправителя и получателя может не попасть под правила IPSec и уйти не в удалённый офис через туннель IPSec, а уйти прямо в сеть провайдера безо всякого шифрования. Этот момент тоже нужно учитывать, чем я и занялся.

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

2. Настройка Debian

Для настройки туннеля я выбрал алгоритм шифрования blowfish, алгоритм хэширования sha1 и группу Дифи-Хеллмана, исходя из желания достичь наибольшей защищённости, поддерживаемой программным обеспечением с обеих сторон туннеля.

Перед настройкой укажу на особенность настраиваемого IPSec-туннеля. У этого туннеля нет IP-адресов конечных точек внутри туннеля. Фактически, IPSec хватает пакеты перед выходом из сетевого интерфейса, выполняет шифрование пакета и кладёт в IP-пакет, в котором указаны "белые" IP-адреса обеих сторон туннеля. Чтобы этот процесс не зациклился и пакеты не продолжали шифроваться и вкладываться снова и снова, IPSec'у чётко указываются правила, пакеты с какими IP-адресами необходимо подвергать обработке. Из-за такой особенности кажется весьма необычным, что пакеты с локальными IP-адресами маршрутизируются прямо через провайдерскую сеть. Очень важно понимать эту особенность, т.к. в противном случае при настройке IPSec можно натурально сойти с ума :)

Устанавливаем необходимые пакеты:
# apt-get install ipsec-tools racoon
Прописываем настройки racoon в файле /etc/racoon/racoon.conf:
remote 22.22.22.22 {
        nat_traversal off;
        exchange_mode main;
        proposal {
                encryption_algorithm blowfish;
                hash_algorithm sha1;
                authentication_method pre_shared_key;
                dh_group modp6144;
        }
}

sainfo anonymous {
      pfs_group modp6144;
      lifetime time 1 hour;
      encryption_algorithm blowfish;
      authentication_algorithm hmac_sha1;
      compression_algorithm deflate;
}
При помощи утилиты pwgen из одноимённого пакета генерируем случайный будущий общий ключ:
$ pwgen 32
Помещаем ключ в файл /etc/racoon/psk.txt:
22.22.22.22 generated_psk_sequence
Настроим ipsec, отредактировав файл /etc/ipsec-tools.conf
flush;
spdflush;

spdadd 192.168.81.240/28 192.168.81.240/28 any -P out none;

spdadd 0.0.0.0/0 192.168.81.240/28 any -P out ipsec
        esp/tunnel/11.11.11.11-22.22.22.22/require;

spdadd 192.168.81.240/28 192.168.81.240/28 any -P in none;

spdadd 192.168.81.240/28 0.0.0.0/0 any -P in ipsec
        esp/tunnel/22.22.22.22-11.11.11.11/require;
Запустим настроенные демоны:
# /etc/init.d/setkey start
# /etc/init.d/racoon start
Добавляем маршруты в удалённую локальную сеть через внешний интерфейс:
# ip route add to 192.168.81.240/28 via 11.11.11.11 src 11.11.11.11
Добавляем правила в пакетный фильтр для прохождения трафика IPSec:
# iptables -A INPUT -i eth0 -m udp -p udp -s 22.22.22.22 --dport 500 -j ACCEPT
# iptables -A INPUT -i eth0 -p ah -s 22.22.22.22 -j ACCEPT
# iptables -A INPUT -i eth0 -p esp -s 22.22.22.22 -j ACCEPT
Добавляем правила в пакетный фильтр для трафика из сети удалённого офиса к серверу Debian:
# iptables -A INPUT -i eth0 -s 192.168.81.240/28 -p tcp -m multiport --dport 25,53,80,110,143,3128 -j ACCEPT
# iptables -A INPUT -i eth0 -s 192.168.81.240/28 -p udp -m udp --dport 53 -j ACCEPT
Если на внешнем интерфейсе осуществляется трансляция адресов, то сеть, доступную через туннель IPSec, нужно исключить из обработки. Ниже приведены два правила - первое исключает сеть из обработки, второе осуществляет трансляцию адресов остального трафика:
# iptables -t nat -I POSTROUTING -o eth0 -d 192.168.81.240/28 -j ACCEPT
# iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 11.11.11.11
3. Настройка MikroTik

Внимание! Указанные ниже команды приведены для примера. Будьте внимательны и не копируйте их прямо в командную строку. Прежде чем выполнять любые примеры команд remove для начала проверьте при помощи команды print, что вы собрались удалить. Внимательно проверяйте IP-адреса и вообще - делайте что-то только если вы чётко представляете, зачем вы это делаете.

Добавляем новый адрес на локальном интерфейсе:
/ip address add interface=ether2-master-local address=192.168.81.241/28
Удаляем старый адрес 192.168.88.1, который был настроен на мостовом интерфейсе в конфигурации по умолчанию:
/ip address remove numbers=0
Переподключаемся на новый адрес, удаляем мостовой интерфейс:
/interface bridge remove numbers=0
Удаляем настройки DHCP-клиента на внешнем интерфейсе:
/ip dhcp-client remove numbers=0
Настраиваем новый внешний адрес:
/ip address add interface=ether1-gateway address=22.22.22.22/25
Настраиваем маршрут по умолчанию:
/ip route add gateway=22.22.22.1
Удаляем настройки NAT:
/ip firewal nat remove numbers=0
Удаляем правила пакетного фильтра:
/ip firewall filter remove numbers=0,1,2,3,4,5
Отключаем доступ к устройству по всем протоколам, кроме ssh:
/ip service disable numbers=0,1,2,5,6,7
Отключаем MAC-telnet (телнет с подключением по MAC-адресу, а не по IP-адресу):
/tool mac-server disable numbers=1,2,3,4,5,6
Теперь переходим к собственно настройке IPSec.

Настраиваем предпочитаемые алгоритмы аутентификации, шифрования и обмена ключами:
/ip ipsec proposal

set default auth-algorithms=sha1 \
enc-algorithms=blowfish \
lifetime=1h \
pfs-group=modp6144
Настраиваем политику, какой трафик шифровать:
/ip ipsec policy

add src-address=192.168.81.240/28 dst-address=192.168.81.240/28 \
sa-src-address=22.22.22.22 sa-dst-address=11.11.11.11 \
tunnel=no action=none

add src-address=192.168.81.240/28 dst-address=0.0.0.0/0 \
sa-src-address=22.22.22.22 sa-dst-address=11.11.11.11 \
tunnel=yes action=encrypt proposal=default
Настраиваем, с кем нужно установить соединение IPSec:
/ip ipsec peer

add address=11.11.11.11/32 local-address=22.22.22.22 port=500 \
auth-method=pre-shared-key secret="generated_psk_sequence" dh-group=modp6144 \
enc-algorithm=blowfish hash-algorithm=sha1 \
lifetime=1h nat-traversal=no
4. Использованные материалы

воскресенье, 27 сентября 2015 г.

NUT и APC Smart-UPS 1500VA

Благодаря Курбан-байраму на этой неделе появилось немного времени попробовать разобраться с ошибкой сегментации памяти в драйвере usbhid-ups из системы NUT.

Настройка драйвера NUT

Ставим nut-server и nut-client из репозитория:
# apt-get install nut-server nut-client
На странице руководства usbhid-ups описаны настройки, позволяющие привязать драйвер к строго определённому устройству. Можно указать настройки vendor, vendorid, product, productid, bus, serial.

Ищем идентификатор производителя и модели нашего ИБП при помощи lsusb. У меня ИБП в выводе этой программы выглядит так:
Bus 004 Device 006: ID 051d:0002 American Power Conversion Uninterruptible Power Supply
Теперь при помощи команды lsusb -vd 051d:0002 посмотрим другую информацию об ИБП. Я нашёл там серийный номер - он полезен, если к компьютеру подключено два ИБП одной и той же модели.

Используя полученную информацию, настроим драйвер в файле /etc/nut/ups.conf:
[apc1500]
        driver = usbhid-ups
        port = auto
        vendorid = 051d
        productid = 0002
        serial = AS0508120261
Теперь драйвер можно попробовать запустить вручную следующей командой:
# /lib/nut/usbhid-ups -a apc1500
В Debian Jessie программа завершается ошибкой сегментации. В Debian Stretch эта ошибка уже исправлена, поэтому в случае Debian Jessie можно сразу перейти к разделу "Исправление прав доступа к устройству" ниже.

Поиск неисправности в драйвере NUT

Найти ошибку мне удалось с помощью программы ltrace. Запустим драйвер под управлением этой программы:
# ltrace /lib/nut/usbhid-ups -a apc1500
Последние строчки вывода ltrace выглядят следующим образом:
strlen("Back-UPS ES 525")                                                                        = 15
strncmp(nil, "Back-UPS ES 525", 15 <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
Как видно, ошибка сегментации происходит потому, что происходит попытка сравнить строки. Первый аргумент функции сравнения строк strncmp имеет значение NULL. Используя это значение как указатель на область памяти, функция пытается обратиться по адресу, которого нет в адресном пространстве программы. Модуль управления памяти (MMU) генерирует прерывание о недоступности страницы, управление передаётся ядру операционной системы. Операционная система ищет затребованную память в каталоге страниц перемещённых в раздел подкачки и в списке файлов, отображаемых в память. Страницу не удаётся найти ни там ни там и ядро операционной системы завершает программу.

Исправление неисправности

Скачаем и распакуем исходные тексты системы NUT:
# cd /root
# apt-get source nut-server
Перейдём в каталог с распакованными файлами и поищем в них строку "Back-UPS ES 525":
# cd nut-2.7.2
# grep -R "Back-UPS ES 525" *
drivers/apc-hid.c: /* Back-UPS ES 525 overflows on ReportID 0x0c
drivers/apc-hid.c: "Back-UPS ES 525",
Строка используется в файле drivers/apc-hid.c. Откроем его и посмотрим, можно ли исправить проблему. Проблема находится в функции general_apc_check:
static void *general_apc_check(USBDevice_t *device)
{
        int i = 0;

        /* Some models of Back-UPS overflow on some ReportID.
         * This results in some data not being exposed and IO errors on
         * WIN32, causing endless reconnection or driver's failure */

        while( tweak_max_report[i] != NULL ) {
                if(!strncmp(device->Product, tweak_max_report[i],
                        strlen(tweak_max_report[i]))) {
                        max_report_size = 1;
                        return NULL;
                }
                i++;
        }
        return NULL;
}
То есть ошибка сегментации памяти происходит из-за попытки обратиться к области памяти, на которую указывает device->Product. Исправим это, пропуская цикл, если значение этого указателя равно NULL:
static void *general_apc_check(USBDevice_t *device)
{
        int i = 0;

        /* Some models of Back-UPS overflow on some ReportID.
         * This results in some data not being exposed and IO errors on
         * WIN32, causing endless reconnection or driver's failure */

        if (device->Product == NULL) {
                return NULL;
        }
        while( tweak_max_report[i] != NULL ) {
                if(!strncmp(device->Product, tweak_max_report[i],
                        strlen(tweak_max_report[i]))) {
                        max_report_size = 1;
                        return NULL;
                }
                i++;
        }
        return NULL;
}
Узаконим это изменение. Во-первых, внесём новую запись в changelog:
# dch -i
В открывшемся редакторе добавляем запись:
nut (2.7.2-4.1) UNRELEASED; urgency=medium

  * Fixed segmentation fault in function general_apc_check

 -- Vladimir Stupin <vladimir@stupin.su>  Thu, 24 Sep 2015 13:48:20 +0500

Теперь фиксируем изменения в исходном тексте при помощи команды:
# dpkg-source --commit
Патч назовём general_apc_check_segfault_fixed, в заголовок впишем комментарии к патчу:
Description: Fixed segmentation fault in function general_apc_check
 Fixed segmentation fault in function general_apc_check
 .
 nut (2.7.2-4.1) UNRELEASED; urgency=medium
 .
   * Fixed segmentation fault in function general_apc_check
Author: Vladimir Stupin <vladimir@stupin.su>

Осталось собрать новые пакеты. Установим зависимости, необходимые для сборки, а затем соберём пакеты:
# apt-get build-dep nut-server
# dpkg-buildpackage -us -uc -rfakeroot
В каталоге выше, на одном уровне с nut-2.7.2 появятся новые файлы:
libups-nut-perl_2.7.2-4.1_all.deb
nut_2.7.2-4.1_all.deb
nut_2.7.2-4.1_amd64.changes
nut_2.7.2-4.1.debian.tar.xz
nut_2.7.2-4.1.dsc
nut_2.7.2.orig.tar.gz
nut-cgi_2.7.2-4.1_amd64.deb
nut-client_2.7.2-4.1_amd64.deb
nut-doc_2.7.2-4.1_all.deb
nut-ipmi_2.7.2-4.1_amd64.deb
nut-monitor_2.7.2-4.1_all.deb
nut-powerman-pdu_2.7.2-4.1_amd64.deb
nut-server_2.7.2-4.1_amd64.deb
nut-snmp_2.7.2-4.1_amd64.deb
nut-xml_2.7.2-4.1_amd64.deb
python-nut_2.7.2-4.1_all.deb
Установим собранные нами пакеты nut-server и nut-client:
# dpkg -i nut-server_2.7.2-4.1_amd64.deb nut-client_2.7.2-4.1_amd64.deb
Попробуем запустить исправленный драйвер:
# /lib/nut/usbhid-ups -a apc1500
На этот раз ошибки сегментации нет, а драйвер самостоятельно завершает работу, сообщая об ошибке:
Can't claim USB device [051d:0002]: could not detach kernel driver from interface 0: Operation not permitted

Исправление прав доступа к устройству

Как выяснилось, дело в правах доступа к файлу устройства /dev/bus/usb/004/007, под которым доступен ИБП. К этому устройству должен иметь доступ пользователь или группа nut. Применим решение, найденное в обсуждении Trouble starting Network UPS Tools with a Eaton 3S UPS. Для этого создадим файл /etc/udev/rules.d/90-nut-ups.rules со следующим содержимым:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="051d", ATTR{idProduct}=="0002", MODE="0660", GROUP="nut"
Чтобы настройки вступили в силу, перезагрузим udevd:
# /etc/init.d/udev reload
Теперь отсоединим USB-кабель, которым ИБП соединён с компьютером, и вставим его обратно. Права доступа должны исправиться.

Настройка сервера NUT

Откроем файл с настройками NUT-сервера /etc/nut/upsd.conf и впишем в него строчку:
LISTEN 127.0.0.1 3493
Откроем файл со списком пользователей NUT-сервера /etc/nut/upsd.users:
[admin]
        password = admin_password
        actions = SET
        instcmds = ALL

[upsmon]
        password = upsmon_password
        upsmon master
Откроем файл /etc/nut/nut.conf и заменим режим работы NUT-сервера с none на standalone:
MODE=standalone
Теперь включим и запустим NUT-сервер:
# systemctl enable nut-server.service
# systemctl start nut-server.service
Запустился ли NUT-сервер, можно посмотреть при помощи следующей команды:
# systemctl status nut-server.service
Настройка клиента мониторинга NUT

Простейший клиент позволяет просматривать текущие параметры ИБП:
$ upsc apc1500
Посмотреть значение какого-то одного конкретного параметра можно указав дополнительно его имя:
$ upsc apc1500 ups.beeper.status
Кроме того, имеется более сложный клиент, который позволяет не только просматривать настройки, но и выполнять команды. Список доступных команд можно увидеть вот так:
$ upscmd -l -u admin -p admin_password apc1500
Например, вот так можно отключить на ИБП звуковой сигнал:
$ upscmd -u admin -p admin_password apc1500 beeper.disable
Можно убедиться в том, что звуковой сигнал действительно отключен:
$ upsc apc1500 ups.beeper.status
Кроме поддержки моделей ИБП разных производителей, NUT выгодно отличается от apcupsd именно возможностью выполнять команды на ИБП. Можно, например, с помощью соответствующей команды протестировать всю процедуру отключения системы.

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

# Команда, которую нужно выполнить перед пропаданием питания от ИБП
SHUTDOWNCMD "/sbin/shutdown -h +0"
Команда нужна именно такая, чтобы компьютер включился при подаче питания. Для этого в BIOS выставляется соответствующая настройка, которая либо всегда при появлении электричества подаёт его на блок питания, либо помнит последнее состояние и подаёт питание, если до пропадания электричества компьютер был включен. Если вам интересно узнать назначение остальных настроек, можете почитать одну из моих прошлых заметок NUT и Eaton Powerware 5110, в которой система NUT разобрана досконально.

Осталось включить и запустить клиента мониторинга NUT:
# systemctl enable ups-monitor.service
# systemctl start ups-monitor.service
Так же, как и в случае с NUT-сервером, можно проверить состояние клиента мониторинга:
# systemctl status ups-monitor.service
Настройка Zabbix

Предполагается, что на компьютере уже установлен и настроен Zabbix-агент. Добавим в конфигурацию агента "пользовательский параметр". Сделать этом можно либо напрямую отредактировав файл /etc/zabbix/zabbix_agentd.conf, либо создав новый файл в каталоге /etc/zabbix/zabbix_agentd.d/ специально для этого пользовательского параметра. Впишем строчку:
UserParameter=nut[*],/bin/upsc $1@$2 $3 2>/dev/null
Перезапустим Zabbix-агента, чтобы новые настройки вступили в силу:
# systemctl restart zabbix-agent.service
Я подготовил два варианта шаблонов - один с элементами данных "Zabbix-агент", а второй - с элементами данных "Zabbix-агент (активный)".

Поскольку к NUT можно подключить несколько ИБП, в шаблонах предусмотрены макросы, в которых указывается конкретный ИБП, данные с которого нужно снимать. Если ИБП один, можно присоединить шаблон прямо к узлу Zabbix, соответствующему компьютеру, к которому подключен ИБП. Если ИБП несколько, то удобнее будет создать отдельные узлы Zabbix, соответствующие каждому из наблюдаемых ИБП. Макросы узла Zabbix выглядят вот так:

Снимаемые с ИБП данные выглядят следующим образом:

Наконец, в шаблоне имеются следующие триггеры:

Дополнительно на узле Zabbix, соответствующем компьютеру, к которому подключены ИБП, можно поставить на контроль наличие работающих процессов upsd и upsmon.

Примеры использования графического клиента NUT и настройки веб-клиента NUT можно посмотреть в прошлой заметке, посвящённой настройке NUT: NUT и Eaton Powerware 5110.

воскресенье, 20 сентября 2015 г.

apcupsd и APC Smart-UPS 1500VA

В прошлом я уже подключал ИБП к компьютеру и воспользовался тогда для контроля за состоянием ИБП системой NUT, о чём и написал в заметке NUT и Eaton Powerware 5110. В этот раз мне понадобилось подключить к компьютеру ИБП APC Smart-UPS 1500VA. Попробовал по старой памяти настроить для этого NUT, но драйвер usbhid-ups, работающий с этим ИБП через интерфейс USB, при запуске завершался ошибкой сегментации памяти. При этом XFCE издевательски выводил уведомление о полной зарядке батареи в правом верхнем углу экрана, общаясь с ИБП через шину dbus и демон upowerd.

Попробовал воспользоваться переходником USB-RS232 (COM-порт поддерживается драйвером apcsmart), но после подключения к компьютеру ИБП пискнул и отключился вместе со всей нагрузкой. Собственно, можно было ожидать чего-то подобного, т.к. в конфигурации драйвера apcsmart есть не один вариант кабеля RS232. Стало понятно, что переходник на USB, по всей видимости, не поможет.

Я знал о существовании apcupsd, но пользоваться им не хотелось из-за его специализированности на ИБП только одного производителя. Но тут деваться стало некуда и я решил всё-таки настроить его.

Настройка apcupsd

Итак, перво-наперво, установим apcupsd:
# apt-get install apcupsd
Открываем файл /etc/apcupsd/apcupsd.conf и редактируем, выставляя следующие настройки:
UPSCABLE usb
UPSTYPE usb
DEVICE

POLLTIME 10 # По умолчанию 60

BATTERYLEVEL 0 # По умолчанию 5%
MINUTES 3
TIMEOUT 0

BEEPSTATE N # Отключаем писк ИБП
apcupsd выключает компьютер при наступлении одного из условий:
  • уровень заряда батареи в процентах упал до значения меньше указанного в BATTERYLEVEL,
  • расчётное время работы от батареи в минутах стало меньше значения, указанного в MINUTES,
  • время непрерывной работы от батареи в минутах превысило значение, указанное в TIMEOUT.
Если какая-либо из этих настроек имеет значение 0, она не учитывается при принятии решения о выключении компьютера.

Теперь откроем файл /etc/default/apcupsd и включим демон, вписав в файл настройку:
ISCONFIGURED=yes
Теперь можно включить и запустить демона через systemd:
# systemctl enable apcupsd.service
# systemctl start apcupsd.service
Поскольку мы не меняли сетевые настройки, демон запустится и будет ожидать подключений на TCP-порту 3551 на локальном IP-адресе 127.0.0.1.

Узнать состояние ИБП можно с помощью программы-клиента apcaccess. Можно запускать её и от имени обычного пользователя, ведь для установки сетевого подключения не нужно обладать особыми правами, но стоит учитывать, что программа лежит в каталоге /sbin, поэтому для обычного пользователя доступна только при указании полного пути:
$ /sbin/apcaccess
Программа выводит различные данные ИБП в виде списка имён параметров и их значений. У программы есть опция -u, отключающая отображение единиц измерения. С помощью опции -p можно вывести значение только одного параметра, указав после опции имя параметра. Воспользуемся этим чтобы наблюдать за состоянием ИБП при помощи системы мониторинга Zabbix.

Настройка Zabbix

Предполагается, что на компьютере уже установлен и настроен Zabbix-агент. Добавим в конфигурацию агента "пользовательский параметр". Сделать этом можно либо напрямую отредактировав файл /etc/zabbix/zabbix_agentd.conf, либо создав новый файл в каталоге /etc/zabbix/zabbix_agentd.d/ специально для этого пользовательского параметра. Впишем строчку:
UserParameter=ups[*],/sbin/apcaccess -u -p $1
Перезапустим Zabbix-агента, чтобы новые настройки вступили в силу:
# systemctl restart zabbix-agent.service
Я подготовил два варианта шаблонов - один с элементами данных "Zabbix-агент", а второй - с элементами данных "Zabbix-агент (активный)".

Фрагмент страницы истории:


Фрагмент шаблона со списком триггеров:

Состав триггеров и приоритеты соответствуют моим нуждам, вы можете настроить их по-другому.

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

воскресенье, 30 августа 2015 г.

php5-fpm и chroot

В php5-fpm имеется множество разных средств обеспечения безопасности веб-приложений. Во-первых, каждое приложение можно запустить в отдельном пуле, от имени отдельного системного пользователя. Во-вторых, можно зафиксировать некоторые настройки php при помощи директив php_admin_value и php_admin_flag. В-третьих, можно запретить веб-приложению использовать определённые функции php при помощи директивы php_admin_value[disable_functions]. В-четвёртых, при помощи директивы php_admin_value[open_basedir] можно ограничить список каталогов, в пределах которых будет работать веб-приложение.

Для параноиков же предусмотрен ещё один способ защиты - запуск веб-приложения внутри chroot-среды. Этот режим ценен тем, что позволяет защитить систему от взломщиков, пытающихся воспользоваться локальными уязвимостями, то есть уязвимостями установленного в системе программного обеспечения. В идеале, в chroot-среде должен иметься только необходимый для работы веб-приложения минимум файлов, программ и библиотек. Уязвимость в программе внутри chroot-окружения не окажет никакого влияния на файлы за его пределами. Также хорошо бы было защититься от сетевой активности, не характерной для штатной работы веб-приложения. Например, от возможности отправлять куда-то спам. Но это уже можно попробовать реализовать при помощи модуля owner для iptables. А вот если уязвимости есть в ядре системы, то воспользоваться ими, по идее, можно даже из chroot-среды.

Для поиска проблем при работе веб-приложений в chroot-среде неоценимую помощь мне оказали две программы: strace и ltrace. Первая позволяет следить за системными вызовами, выполняемыми отслеживаемым процессом, а вторая - за вызовами библиотечных функций. Мне оказалось достаточным знать о всего двух опциях этих программ:
  • -p - позволяет указать идентификатор процесса, за которым нужно следить. Эту опцию можно указать несколько раз, тогда будут отслеживаться системные вызовы (или вызовы библиотечных функций) из всех указанных процессов.
  • -f - позволяет отслеживать системные вызовы (или вызовы библиотечных функций) порождённых процессов.
Насколько я понял, для работы веб-приложений на php в chroot-среде достаточно добавить в неё разделяемые библиотеки libnss и минимальные настройки для них. Кроме того, нужно создать каталоги для хранения сеансов php и временных файлов. Ещё нужны файлы временных зон. Наконец, нужно устройство /dev/urandom. Возможно каким-то другим приложениям понадобится что-то ещё. Выяснить это можно при помощи упомянутых выше утилит strace и ltrace.

Для работы веб-приложений с базами данных MySQL и PostgreSQL нужно в настройках веб-приложения поменять адрес сервера localhost на 127.0.0.1, поскольку при указании localhost будет предпринята попытка подключения к Unix-сокету, которого в chroot-среде нет. При замене адреса сервера на 127.0.0.1 подключение будет осуществляться на TCP-порт.

Многие веб-приложения на PHP используют функцию mail для отправки писем с помощью sendmail или утилит, заменяющих sendmail. Чтобы отправка почты работала в chroot-окружении, нужно установить в неё альтернативу sendmail. Я попробовал воспользоваться для этих целей утилитами esmtp и msmtp. Обе утилиты мне удалось настроить для отправки почты с аутентификацией, но вот настраивать шифрование по протоколу TLS мне уже было лень, т.к. для его работы в chroot-среду нужно положить дополнительные файлы - список доверенных сертификатов и сами сертификаты.

Поскольку описание настройки chroot-среды в блоге получилось бы очень объёмным, я скомпоновал инструкции по настройке среды в скрипт, который можно взять здесь: http://stupin.su/files/php_fpm_chroot.sh

Для удобства пересоздания chroot-среды предполагается, что файлы веб-приложения будут находиться в каталоге /site. Перед созданием chroot-среды из неё удаляются каталоги bin, etc, dev, lib, lib64, tmp, usr, var. Перед запуском скрипта нужно поменять его настройки под ваши нужды и обязательно убедиться в том, что переменная CHROOT указывает не на корень вашей системы. Чем это грозит, думаю говорить не приходится.

Внутри скрипта есть следующие настройки:
  • USER - пользователь, которому будут принадлежать файлы веб-приложения, лежащего в каталоге site,
  • GROUP - группа, которой будут принадлежать файлы веб-приложения, лежащего в каталоге site,
  • UID - идентификатор пользователя, указанного в USER,
  • GID - идентификатор группы, указанной в GROUP,
  • ZONEINFO - локальная временная зона веб-приложения, например - Asia/Yekaterinburg,
  • MAILER - программа-аналог sendmail для отправки писем. Доступны варианты - esmtp, msmtp. Любое другое значение отключит настройку программы (я в таких случаях указываю значение no),
  • MAIL_SERVER - адрес SMTP-сервера,
  • MAIL_PORT - порт SMTP-сервера (чаще всего это порт 25 или 587),
  • MAIL_USER - почтовый ящик с доменом. Предполагается, что аутентификация на SMTP-сервере происходит по имени этого почтового ящика, вместе с доменом,
  • MAIL_PASSWORD - пароль для аутентификации на SMTP-сервере.
На этом всё. Готов выслушать замечания и предложения.

воскресенье, 23 августа 2015 г.

Чистка системы от устаревших и ненужных пакетов

Установим пакет, специально предназначенный для удаления ненужных пакетов:
# apt-get install debfoster
Удаляем пакеты, которые по нашему мнению не нужны. Предлагаются к выбору только те пакеты, от которых не зависят другие:
# debfoster
Создадим скрипт с именем no_repo.sh, который выведет список установленных пакетов, отсутствующих в текущих репозиториях:
#!/bin/sh

dpkg -l | awk '/^ii/ { print $2; }' \
  | while read pkg;
    do
      apt-cache policy $pkg \
        | awk -v pkg=$pkg 'BEGIN { local = 0;
                                   repo = 0; }

                           /^[ ]+[0-9]+ .*status$/ { local = 1; }

                           /^[ ]+[0-9]+ / && !/status$/ { repo = 1; }

                           END { if ((local == 1) && (repo == 0))
                                   print pkg; }'
    done
Дадим скрипту право быть запущенным:
$ chmod +x not_repo.sh

Составляем список пакетов, отсутствующих в текущих репозиториях:
$ ./not_repo.sh > not_repo

Редактируем полученный список, удаляя из него пакеты, которые нам нужны или которые были установлены вручную. Затем удаляем оставшиеся:
# apt-get purge `cat not_repo`

Составляем список удалённых пакетов, от которых остались какие-либо файлы:
$ dpkg -l | awk '$1 !~ /^ii/ { print $2; }' > removed

Редактируем полученный список, удаляя из него пакеты, файлы конфигурации и файлы данных которых нужно оставить нетронутыми. Далее удаляем оставшиеся:
# apt-get purge `cat removed`

Теперь осталось удалить пакеты, установленные автоматически и больше не нужные:
# apt-get autoremove

Результат - удалось высвободить почти гигабайт места:

воскресенье, 16 августа 2015 г.

systemd и uwsgi в режиме Emperor

Изо всех щелей рассказывают о том, какой systemd хороший. На эти рассказы даже купились в Debian, устраивали голосования одно за другим вплоть до достижения "нужного" исхода. Решение пропихнули и в Jessie теперь systemd идёт по умолчанию. Только вот в Jessie service-файлов кот наплакал и большей частью всё происходит по старинке - через те же скрипты /etc/init.d/, только теперь запускает их процесс с другим именем. Непонятно, то ли происходит саботаж этого решения, то ли на самом деле не так уж важны эти фишки systemd. А вы думали только в России бывает такое головотяпство?

Скрипт /etc/init.d/uwsgi кроме команды типа start/stop/restart опционально может принимать ещё имя экземпляра, так что с помощью этого скрипта можно было легко перезапускать отдельные экземпляры uwsgi, обслуживающие разные приложения. Теперь при попытке воспользоваться этим скриптом происходит перехват управления, в результате чего этот скрипт выполняется через systemctl. systemctl о дополнительном аргументе ничего не знает, поэтому указать нужный экземпляр uwsgi не представляется возможным. Перехват происходит через файл-хук /lib/lsb/init-functions.d/40-systemd, который используется в файле /lib/lsb/init-functions, который в свою очередь используется во всех скриптах /etc/init.d/ в Debian.

Само собой напрашивалось решение с многоэкземплярным service-файлом, который вызывался бы примерно так:
# systemctl start uwsgi@redmine.service
Но тут я вспомнил о том, что в Jessie поставляется новый uwsgi, в котором поддерживается режим Emperor. Emperor - это отдельный процесс uwsgi, который умеет заглядывать в указанный каталог и искать там файлы конфигурации экземпляров uwsgi. При появлении нового файла конфигурации он умеет самостоятельно порождать новые процессы master, которые в свою очередь порождают процессы worker. Соответственно, при пропадании файла конфигурации Emperor корректно завершает master-процесс, соответствующий пропавшему файлу конфигурации. Плюс к тому, он умеет следить за master-процессами, порождая их, если они вдруг неожиданно завершились.

Поиски документации привели меня сюда: uwsgi - systemd. В ходе экспериментов получилось рабочее решение, которым и спешу поделиться.

Для начала создадим service-файл /etc/systemd/system/uwsgi.service:
[Unit]
Description=uWSGI Emperor
After=syslog.target

[Service]
ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/emperor.ini
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target
Теперь создаём файл /etc/uwsgi/emperor.ini, который будет содержать конфигурацию "императора":
[uwsgi]

emperor = /etc/uwsgi/apps-enabled/
vassals-inherit = /etc/uwsgi/vassal-default.ini
Теперь создаём файл с настройками по умолчанию для "вассалов". За основу возьмём файл /usr/share/uwsgi/conf/default.ini, который упоминался в файле /etc/default/uwsgi. Пути к файлам придётся изменить так, чтобы для создания любого из файлов не нужно было создавать соответствующий каталог - файл взятый за основу этим грешит, но в нём это не является проблемой, т.к. файл инициализации может создавать необходимые каталоги сам. Доведя файл конфигурации вассала до состояния, пригодного для использования, получим файл /etc/uwsgi/vassal-default.ini:
[uwsgi]

autoload = true

master = true
workers = 2
no-orphans = true

pidfile = /run/uwsgi/%N.pid
socket = /run/uwsgi/%N.socket
chmod-socket = 660

logto = /var/log/uwsgi/%N.log
log-date = true

uid = www-data
gid = www-data
Есть ещё один странный момент. Чтобы экземпляры заработали, у имён плагинов нужно убрать префикс uwsgi_. Например, имя плагина плагина uwsgi_rack_ruby21 нужно сократить до rack_ruby21.

Чтобы при перезагрузке компьютера создавался каталог /run/uwsgi, создадим файл /etc/tmpfiles.d/uwsgi.conf со следующей строчкой:
d /run/uwsgi 0750 www-data www-data -
На всякий случай приведу команду, при помощи которой можно создать все каталоги, создаваемые в процессе загрузки системы:
# systemd-tmpfiles --create
Поскольку пути к сокетам поменяются, нужно подготовить пути к новым сокетам в конфигурации nginx.

Теперь останавливаем uwsgi, запущенный скриптом /etc/init.d/uwsgi, включаем использование service-файла и запускаем uwsgi уже с его помощью:
# systemctl stop uwsgi.service
# systemctl enable uwsgi.service
# systemctl start uwsgi.service
Чтобы nginx обращался к новым сокетам, размещающимся прямо в каталоге /run/uwsgi, перезапустим и его:
# systemctl restart nginx.service
И как же теперь перезагрузить/перезапустить экземпляр uwsgi? К сожалению, это не так наглядно, как со скриптом /etc/init.d/uwsgi. Для перезагрузки конфигурации используется команда с PID-файлом экземпляра:
# uwsgi --reload /run/uwsgi/redmine.pid
Перезапуск делается через остановку экземпляра, а systemd автоматически запустит его снова:
# uwsgi --stop /run/uwsgi/redmine.pid
Чтобы остановить экземпляр, нужно удалить ссылку из каталога /etc/uwsgi/apps-enabled/:
# rm /etc/uwsgi/apps-enabled/redmine.ini
Чтобы снова запустить экземпляр, нужно снова создать ссылку в каталоге /etc/uwsgi/apps-enabled/:
# ln -s /etc/uwsgi/apps-available/redmine.ini /etc/uwsgi/apps-enabled/redmine.ini
Посмотреть список запущенных процессов можно при помощи systemctl:
$ systemctl status uwsgi.service
На этом всё. Если у вас есть идеи, как сделать перезапуск экземпляров более наглядным, например при помощи многоэкземплярного service-файла, прошу делиться предложениями.

воскресенье, 9 августа 2015 г.

Перенос сайта Wordpress на другой домен

Не так давно возникла задача перенести блог на Wordpress на другой домен. Имелись только резервная копия базы данных и файлов. Задача тривиальная, за исключением нескольких моментов: 1. в базе данных имеется много ссылок на старый домен, 2. перенос осуществлялся с Apache на nginx + php-fpm, поэтому нужно было правильно настроить веб-сервер, 3. на старом хостинге отправлять письма можно было без аутентификации, на новом - только с аутентификацией.

На всякий случай, пройдусь полностью по всей процедуре восстановления.

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

Установим пакеты, необходимые для работы Wordpress:
# apt-get install nginx mysql-server php5-fpm php5-gd php5-mysql libphp-phpmailer
Возможно понадобится что-то ещё, но мне хватило этого.

2. Восстановление базы данных

Подключимся к только что установленному серверу MySQL:
# mysql -uroot -p mysql
Создадим базу данных blog, в которую будем восстанавливать резервную копию:
mysql> CREATE DATABASE blog CHARSET utf8;
Восстановим резервную копию:
mysql> use blog
mysql> source backup.sql
Теперь создадим пользователя, от имени которого Wordpress будет работать с базой данных:
mysql> use mysql
mysql> INSERT INTO user(user, password, host) VALUES('blog', PASSWORD('blog_password'), 'localhost');
mysql> FLUSH PRIVILEGES;
mysql> GRANT ALL ON blog.* TO blog@localhost;
mysql> FLUSH PRIVILEGES;
Теперь исправим содержимое базы данных так, чтобы все ссылки указывали на новый домен (domain.old - старый домен, domain.new - новый домен):
mysql> UPDATE wp_options SET option_value = REPLACE(option_value, 'domain.old', 'domain.new') WHERE option_name IN ('home', 'siteurl');
mysql> UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://domain.old', 'http://domain.new');
mysql> UPDATE wp_posts SET guid = REPLACE(guid, 'http://domain.old', 'http://domain.new');
mysql> UPDATE wp_posts SET pinged = REPLACE(pinged, 'domain.old', 'domain.new');
mysql> UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, 'domain.old', 'domain.new');
mysql> UPDATE wp_comments SET comment_author_url = REPLACE(comment_author_url, 'domain.old', 'domain.new');
Выйдем из клиента mysql:
mysql> \q
3. Настройка php5-fpm

Создадим новый или отредактируем имеющийся файл /etc/php5/fpm/pool.d/default.conf:
[default]

# Блог будет работать с правами пользователя www-data и группы www-data
user = www-data
group = www-data

# Этот пул php5-fpm будет ждать соединений на указанном UNIX-сокете
listen = /var/run/default.sock

# Права доступа к UNIX-сокету
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

# Динамическое количество процессов в пуле
pm = dynamic
# Максимальное количество процессов в пуле - 6
pm.max_children = 6
# При запуске php5-fpm создаётся 2 процесса в пуле
pm.start_servers = 2
# Если все процессы заняты, значит ожидается увеличение нагрузки, нужно создать ещё один процесс
pm.min_spare_servers = 1
# Если свободно больше 4 процессов, завершаем лишние
pm.max_spare_servers = 4
 
# Журнал запросов, обработанных php5-fpm
access.log = /var/log/php5-fpm.access.log

# Журнал для отладки, сюда попадают сообщения об ошибках
php_value[log_errors] = On
php_value[error_log] = /var/log/php5-fpm.error.log

# Отключаем возможность использовать URL'ы вида file.php/section/page/1
php_value[cgi.fix_pathinfo] = 0

# Страница может генерироваться максимум 30 секунд
php_value[max_execution_time] = 30
# Максимальный объём POST-запроса - 16 мегабайт
php_value[post_max_size] = 16M
# Максимальный размер вложений в POST-запросе - 8 мегабайт
php_value[upload_max_filesize] = 8M
# Выставляем местный часовой пояс
php_value[date.timezone] = Asia/Yekaterinburg
Теперь запустим php5-fpm:
# systemctl start php5-fpm.service
4. Подготовка файлов сайта

После распаковки файлов из резервной копии нужно положить их туда, где с ними будут работать nginx и php5-fpm (в нашем случае это путь /home/www01/domain.new) и выставить правильные права доступа:
# cd /home/www01/domain.new/
# chown www-data:www-data -R *
# find . -type d -exec chmod u=rwx,g=rx,o= \{\} \;
# find . -type f -exec chmod u=rw,g=r,o= \{\} \;
5. Настройка nginx

Пропишем в файл /etc/nginx/sites-available/default такие настройки:
server {
  listen 80;

  server_name domain.new;

  root /home/www01/domain.new/;
  index index.php;

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ \.php$ {
    include fastcgi.conf;
    fastcgi_pass unix:/var/run/default.sock;
  }
}
Включим использование этого файла веб-сервером:
# cd /etc/nginx/sites-enabled/
# ln /etc/nginx/sites-available/default .
И запустим сам веб-сервер:
# systemctl nginx start
6. Правка шаблонов Wordpress

Если шаблоны для сайта были разработаны или доработаны специально, нужно поискать и заменить в шаблонах возможно имеющиеся в них ссылки на старый домен. Поиск можно осуществить, например, так:
# cd /home/www/domain.new/
# grep -R domain.old *
Будут выведены имена всех файлов, в которых встречается старый домен и строчки, в которых этот домен встретился. Далее нужно последовательно открыть каждый файл и исправить в нём домен на новый.

По идее, уже сейчас сайт должен работать правильно. Однако, стоит открыть сайт в веб-браузере с активированной функцией отладки и посмотреть, не происходит ли обращений по старому адресу. Если такие обращения происходят, то скорее всего нужно исправить настройки используемой темы или какого-либо плагина Wordpress.

7. Перенаправление со старого домена

Может случиться так, что старый домен проплачен и будет работать ещё некоторое время. Чтобы не терять посетителей, пришедших на старый домен, можно перенастроить его DNS-серверы так, чтобы они указывали на новый хостинг. В таком случае в конфигурацию nginx можно внести небольшое дополнение, которое будет перенаправлять посетителей со старого адреса на новый:
server {
  listen 80;

  server_name domain.old;

  return 301 http://domain.new$request_uri;
}
Можно ещё немного подкорректировать этот фрагмент конфигурации. У многих интернет-пользователей старой закалки сложился стереотип, что имя любого сайта обязано начинаться с www. Чтобы не терять и таких посетителей, поменяем предыдущий фрагмент файла конфигурации следующим образом:
server {
  listen 80;

  server_name domain.old www.domain.old www.domain.new;

  return 301 http://domain.new$request_uri;
}
Чтобы новые настройки вступили в силу, перезагрузим веб-сервер:
# systemctl nginx reload
8. Настройка аутентификации при отправке почты

Как я уже говорил, на старом хостинге почта отправлялась без аутентификации, а на новом требуется аутентификация. Как вариант, на новом хостинге вообще может быть не настроен почтовый сервер. В таком случае всё-же можно отправлять почту, но для этого придётся аутентифицироваться на каком-либо стороннем сервере (gmail.com, yandex.ru, mail.ru и т.п.).

Для решения этой проблемы можно воспользоваться плагином для Wordpress, который называется WP SMTP. Ставим плагин и настраиваем почтовый ящик, который будет использоваться для отправки уведомлений:



На этом поставленные цели достигнуты, задача решена.

воскресенье, 2 августа 2015 г.

Проксирование запросов к PostgreSQL через PgBouncer

Введение

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

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

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

Во-вторых, каждый процесс или поток, созданный для обслуживания одного пользователя или одного запроса, держит ресурсы, даже если не осуществляет никакой активной работы. Веб-приложения, работающие по протоколу FastCGI или ему подобному (SCGI, uWSGI), не устанавливают новые подключения к базе данных каждый раз при получении нового запроса. Вместо этого подобные приложения запускаются единожды, открывают необходимые подключения и загружают необходимые ресурсы, а затем последовательно обрабатывают поступающие запросы. Такая схема работы избавляет от недостатков CGI, но порождает другой недостаток - однажды запущенное приложение FastCGI держит открытыми подключения к базе данных, даже если не обслуживает никаких запросов.

Веб-серверы Lighttpd и nginx стали популярны во многом благодаря тому, что позволили нивелировать недостаток веб-сервера Apache как сервера приложений. Apache построен по гибридной многопроцессной и многопоточной архитектуре, но поддерживает повторное использование процессов и потоков. Поэтому для Apache характерен второй недостаток. Когда есть много медленных клиентов, выходящих в интернет через модем или GPRS, Apache быстро обрабатывает запрос клиента, а потом медленно-медленно отдаёт ему результат. Пока результат отдаётся клиенту процесс или поток практически ничем не заняты, но продолжают удерживать ресурсы, которые могли бы пригодиться для обработки запросов от других пользователей.

И тут на сцене появились Lighttpd и nginx, которые сочетают в себе две функции - отдачу файлов с диска и проксирование запросов. Кстати, эти веб-серверы были не первыми, до них были boa, thttpd, mathopd, которые были построены по той же архитектуре - мультиплексирование запросов в пределах одного однопоточного процесса. Позже, для того чтобы равномерно нагружать все имеющиеся процессорные ядра, в некоторых из этих серверов (nginx, например) была добавлена возможность обработки соединений несколькими процессами. Так вот, эти веб-серверы, по сути, выполняют ровно две задачи. Первая - отдавать клиентам неизменяющиеся файлы в обход сервера приложений, вторая - быстро забрать у сервера приложения ответ, освободив тем самым ресурсы для обслуживания другого запроса, и затем отдавать этот ответ клиенту с той скоростью, с которой он готов его принимать.

Остаётся только догадываться о том, достигли бы эти серверы хоть какой-либо, не то что популярности, а вообще - известности, если бы разработчики Apache вовремя догадались встроить в него прокси. Разработчики веб-сервера Microsoft IIS до подобного решения догадались и встроили проксирование запросов в специальный драйвер http.sys.

Прошу прощения за столь долгое вступление, но оно очень полезно для того, чтобы описать, чем занимается pgbouncer и в чём его польза. Так вот, pgbouncer - это такой прокси для протокола PostgreSQL, каковым является nginx для протокола HTTP или его аналогов (FastCGI, SCGI, uWSGI). К сожалению, в случае с СУБД, мы имеем дело не с запросами без учёта состояния, а с полноценными сеансами, поэтому проксирование в данном случае является скорее трюком, нежели законным способом избавиться от описанных проблем. pgbouncer позволяет во-первых, уменьшить частоту порождения и уничтожения процессов PostgreSQL, занимающихся обслуживанием запросов пользователей, а во-вторых - уменьшить простой однажды порождённого процесса, заставляя один процесс обрабатывать запросы, которые раньше обрабатывались бы несколькими процессами.

Установка и настройка pgbouncer

Итак, установим pgbouncer:
# apt-get install pgbouncer
Теперь займёмся его настройкой. Откроем файл /etc/pgbouncer/pgbouncer.ini и впишем в секцию databases следующие настройки:
zabbix = host=localhost dbname=zabbix user=zabbix
redmine_default = host=localhost dbname=redmine_default user=redmine_default
В секции pgbouncer пропишем такие настройки:
auth_type = md5
pool_mode = transaction
Первая настройка указывает, что при аутентификации pgbouncer'а на сервере будет использоваться учётная запись с хэшированным по алгоритму md5 паролем. За неимением лучших вариантов приходится пользоваться md5. Вторая настройка говорит, что в пределах одного и того же подключения к серверу должна выполняться транзакция. Вариант session не позволит нам сэкономить ресурсы на простаивающих соединениях, а лишь решит проблему с частым порождением и уничтожением процессов, происходящих при обслуживании запросов веб-интерфейсом Zabbix. Вариант statement предписывает выполнять в пределах одного и того же процесса только один запрос, но это может привести к несогласованным изменениям данных. Транзакция может включать в себя несколько запросов, которые изменяют содержимое базы данных согласованным образом. Для достижения согласованности изменений нужно чтобы при ошибке в одном из запросов ни один из запросов не изменил содержимое базы данных, а содержимое было бы изменено только в том случае, если все запросы внутри транзакции успешно выполнились.

Далее, при желании, можно настроить максимальное количество разрешённых подключений от клиентов, количество подключений в пуле, таймауты подключений в пуле и т.п. Если pgbouncer будет принимать подключения от нелокальных клиентов, нужно задать прослушиваемый IP-адрес в директиве listen_addr.

Теперь откроем файл /etc/pgbouncer/userlist.txt и впишем в него имена пользователей и их пароли, чтобы pgbouncer мог аутентифицироваться при установке подключений к СУБД и мог сам аутентифицировать подключающихся клиентов (на мой взгляд - хранить пароли ещё в одном месте весьма сомнительная затея, но разработчики, видимо, не предусмотрели возможность брать необходимые данные от клиентов и использовать их при при подключении к серверу):
"zabbix" "zabbix_password"
"redmine_default" "redmine_default_password"
Осталось разрешить запуск pgbouncer и запустить его. Для этого откроем файл /etc/default/pgbouncer, пропишем в опцию START значение 1 и запустим его:
# systemctl start pgbouncer
По умолчанию pgbouncer ожидает подключений на порту 6432, то есть номер порта на тысячу больше, чем стандартный порт PostgreSQL. В случае локальных подключений клиент умеет автоматически использовать Unix-сокет, находящийся в каталоге /var/run/postgresql/ и на этот случай pgbouncer создаёт в этом каталоге свой Unix-сокет, который содержит в своём имени номер прослушиваемого TCP-порта.

Теперь осталось перенастроить приложения на использование pgbouncer.

Перенастройка сервера Zabbix

Начнём с Zabbix-сервера. Откроем файл /etc/zabbix/zabbix_server.conf и впишем номер порта pgbouncer'а:
DBPort=6432
Перезапустим Zabbix-сервер:
# systemctl restart zabbix-server
Перенастройка веб-интерфейса Zabbix

Теперь перенастроим веб-интерфейс Zabbix. Откроем файл /etc/zabbix/web/zabbix.conf.php и пропишем номер порта:
$DB['PORT']     = '6432';
Перезапустим php5-fpm, чтобы настройки веб-приложения вступили в силу (не уверен что это необходимо):
# systemctl restart php5-fpm
Перенастройка Redmine

Теперь настала очередь Redmine. Тут всё оказалось не совсем просто. Дело в том, что в Rails-приложениях активно используются заготовленные запросы. Что это такое? Это такой шаблон запроса, в котором вместо конкретных значений указаны символы подстановки. СУБД может заранее разобрать такой запрос, пропустить через оптимизатор и составить план выполнения запроса. Затем подготовленный запрос можно использовать многократно, указывая конкретные значения, которые заменят символы подстановки из подготовленного запроса. При этом СУБД экономит время, не повторяя этапы синтаксического разбора, оптимизации и составления плана выполнения запроса, выполняя эту обработку единожды при подготовке запроса, а затем многократно используя её результаты при каждом выполнении запроса.

Оказалось, что использование подготовленных запросов можно отключить через файл /etc/redmine/default/database.yml, содержащий настройки подключения Redmine к базе данных. Соответствующую опцию я нашёл по ссылке Configuring Rails Applications, 3.14.3 Configuring a PostgreSQL Database. Итак, редактируем файл /etc/redmine/default/database.yml, прописав в нём номер порта и опцию prepared_statements:
production:
  adapter: postgresql
  database: redmine_default
  host: localhost
  port: 6432
  username: redmine_default
  password: redmine_default_password
  encoding: utf8
  prepared_statements: false
Осталось перезапустить Redmine:
# systemctl restart uwsgi

Стоит отметить, что отключение подготовленных запросов может отрицательно сказаться на производительности Redmine. Но в моём случае Redmine используется очень редко, поэтому мне важнее не улучшить его производительность, а снизить потребление ресурсов системы. Это будет достигнуто за счёт того, что pgbouncer будет устанавливать подключение к PostgreSQL только в те моменты, когда кто-то будет пользоваться Redmine. Если же им никто не пользуется, то спустя некоторое время соединение к СУБД будет закрыто и ресурсы, удерживавшиеся процессом PostgreSQL, будут возвращены системе.
Итоги

Результат произведённых действий можно лицезреть на графике, отображающем количество процессов в системе до и после внедрения pgbouncer:

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

воскресенье, 26 июля 2015 г.

Миграция Redmine с MySQL на PostgreSQL

На этот раз рассмотрим перенос данных из MySQL в PostgreSQL, пригодный, пожалуй, для любых приложений, предоставляющих выбор из этих двух СУБД.

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

Рассмотрим эту стратегию миграции на примере Redmine.

Получение схемы базы данных

Схема базы данных - это её структура, то есть таблицы, индексы, ключи, без данных.

Установим дополнительно пакет, отвечающий за работу Redmine совместно с СУБД PostgreSQL:
# apt-get install redmine-pgsql
Для начала перейдём в каталог /etc/redmine/, в котором есть подкаталоги, соответствующие экземплярам redmine:
# cd /etc/redmine/
Скопируем каталог одного из экземпляров в другой каталог, создав таким образом новый экземпляр. В данном случае копируем экземпляр default и создаём на его основе экземпляр pgsql:
# rsync -rogp default/ pgsql/
Открываем файл /etc/redmine/pgsql/database.yml и заменяем в нём имя пользователя в поле username, имя базы данных в поле database и тип базы данных в поле adapter. У меня получился такой файл:
production:
  adapter: postgresql
  database: redmine_pgsql
  host: localhost
  port:
  username: redmine_pgsql
  password: password
  encoding: utf8
Теперь нужно создать соответствующего пользователя и базу данных:
# su - postgres
$ createuser -P redmine_pgsql
$ createdb -E UTF-8 -O redmine_pgsql redmine_pgsql
$ exit
Пустая база данных и пользователь созданы. Теперь нам нужно наполнить эту базу данных структурой и первичными данными. Для этого переходим в каталог /usr/share/redmine/, в котором находится Redmine, и запускаем команду миграции экземпляра pgsql, которая сделает всё необходимое:
# cd /usr/share/redmine
# rake db:migrate RAILS_ENV=production X_DEBIAN_SITEID=pgsql
Теперь, если всё прошло успешно, создаём резервную копию этой базы данных, но без собственно данных:
# su - postgres
$ pg_dump -s -d redmine_pgsql > redmine_pgsql.sql
Теперь можно удалить этого пользователя и его базу данных, от них нам больше ничего не нужно:
$ dropdb redmine_pgsql
$ dropuser redmine_pgsql
$ exit
Теперь можно удалить и каталог с настройками экземпляра pgsql:
# cd /etc/redmine
# rm -R pgsql
Как стало ясно в процессе дальнейших попыток воспользоваться дампом, его нужно откорректировать:
  • Удалить комментарии и пустые строки, т.к. pgloader ожидает, что в каждой строчке будет указан какой-то запрос, который что-то изменяет,
  • Удалить запрос, добавляющий описание языка PL/PgSQL,
  • Удалить двойные кавычки вокруг имён полей, совпадающих с зарезервированными словами,
  • Сменить владельца базы данных с redmine_pgsql на владельца базы данных, которую будем переносить из MySQL в PostgreSQL.
Сделать это можно вот так:
$ cat redmine_pgsql.sql | sed -e 's/^--.*$//g; s/^COMMENT .*$//; /^$/d; s/"//g; s/TO redmine_pgsql/TO redmine_default/g' > redmine_default.sql
Теперь нужно поделить файл redmine_default.sql на две части. Первый должен создать структуру базы данных без ограничений и внешних ключей, а второй - добавлять их. К счастью, стандартный дамп, созданный pg_dump, можно легко поделить на нужные нам части. Первая часть заканчивается запросами вида "ALTER TABLE ONLY ... ALTER COLUMN ... SET DEFAULT ...", а вторая часть начинается запросами вида "ALTER TABLE ONLY ... ADD CONSTRAINT ...". В результате должны получиться файлы redmine_default1.sql и redmine_default2.sql

Приведу ссылки на эти файлы: redmine_default1.sql и redmine_default2.sql Они соответствуют версии Remine, поставляющейся в репозиториях Debian Jessie (версии пакетов - 3.0~20140825-5). Если у вас именно эта версия Redmine, можно пропустить весь этот раздел и сразу воспользоваться этими файлами.

Собственно перенос данных

Если pgloader ещё не установлен, установим его:
# apt-get install pgloader
Теперь создадим файл redmine_default.sql с настройками миграции:
LOAD DATABASE
  FROM mysql://redmine_default:password@localhost/redmine_default
  INTO postgresql://redmine_default:password@localhost/redmine_default

WITH include no drop,
     truncate,
     create no tables,
     create no indexes,
     no foreign keys,
     reset sequences,
     data only

SET maintenance_work_mem TO '128MB',
    work_mem to '12MB'

BEFORE LOAD EXECUTE redmine_default1.sql
Теперь отключаем Redmine, чтобы данные не менялись в процессе переноса в новую СУБД:
# /etc/init.d/uwsgi stop redmine
Открываем файл с настройками подключения к базе данных мигрируемого экземпляра default. В данном случае это файл /etc/redmine/default/database.yml, который в данный момент настроен на использование MySQL. Нужно создать точно такого же пользователя и базу данных в PostgreSQL:
# su - postgres
$ createuser -P redmine_default
$ createdb -E UTF-8 -O redmine_default redmine_default
$ exit
Запускаем миграцию:
$ pgloader redmine_default.load
После миграции открываем снова файл с настройками подключения к базе данных мигрируемого экземпляра default. Это файл /etc/redmine/default/database.yml, заменяем в нём тип базы данных с mysql2 на postgresql.

Теперь можно запустить uwsgi, чтобы Redmine вновь стал доступен для пользователей. Тестируем, всё ли правильно работает. Дальше можно перенести другие экземпляры Redmine и по окончании миграции удалить пакет Redmine для работы с MySQL:
# apt-get purge redmine-mysql

воскресенье, 19 июля 2015 г.

Чиним Redmine после обновления Debian до Jessie

После обновления Debian с Wheezy до Jessie перестал работать Redmine, настроенный по заметке Установка Redmine в Debian Wheezy.

Во-первых, поскольку в дистрибутиве Jessie вместо плагинов uwsgi_rack_ruby18 и uwsgi_rack_ruby191 имеется только плагин uwsgi_rack_ruby21, использовать теперь нужно именно его.

Во-вторых, в каталоге, на который указывала опция rails, появился файл config.ru, поэтому uwsgi предлагает воспользоваться опцией rack, которой и нужно указать этот файл. Однако, одной этой опции rack недостаточно для правильной работы, нужно ещё указать опцию chdir, указав в ней путь к каталогу, где находится приложение.

Опция post-buffering теперь не обязательна, поэтому её можно убрать.

В результате получился такой вот файл /etc/uwsgi/apps-available/redmine.ini:
[uwsgi]

procname = uwsgi-redmine
procname-master = uwsgi-redmine-master
  
plugin = uwsgi_rack_ruby21
env = RAILS_RELATIVE_URL_ROOT=/redmine
env = RAILS_ENV=production
chdir = /usr/share/redmine
rack = /usr/share/config.ru
processes = 2
Это всё было довольно быстро исправлено, но этого оказалось не достаточно. Осталась ещё одна проблема, которая заключалась в том, что redmine наотрез отказывался принимать во внимание переменную окружения RAILS_RELATIVE_URL_ROOT, которая позволяла получить доступ к приложению не через корневой каталог сервера, а поместить его в подкаталог. В данном случае этим каталогом являлся каталог /redmine/.

Вмешиваться в код приложения не хотелось, поэтому я искал другой способ исправить ситуацию. Чего я только не пробовал... В конце концов приспособил комментарий habermann24 commented on 13 May и создал файл /etc/redmine/config.ru со следующим содержимым:
require ::File.expand_path('/usr/share/redmine/config/environment', __FILE__)
map ENV['RAILS_RELATIVE_URL_ROOT'] || '/' do
  run RedmineApp::Application
end
Этот файл я указал в конфигурации /etc/uwsgi/apps-available/redmine.ini, заменив значение опции rack:
rack = /etc/redmine/config.ru
После добавления такого костыля и перезапуска uwsgi, всё заработало как положено.

К сожалению, подобные недоработки в Debian стали встречаться чаще. Может быть качество объективно падает, а может быть я чаще стал пользоваться всякими маргинальными программами и поэтому подобные недоработки просто стали мне чаще попадаться.

воскресенье, 12 июля 2015 г.

Миграция Zabbix с MySQL на PostgreSQL

Периодически замечал в iotop на своём домашнем компьютере, что самую высокую нагрузку по вводу-выводу создаёт MySQL. В интернете встречал мнения, что PostgreSQL по сравнению с MySQL более стабильно ведёт себя в условиях дефицита производительности дисковой подсистемы. Больше всего MySQL на этом компьютере нагружался Zabbix'ом, поэтому ради эксперимента решил попробовать перевести Zabbix на использование PostgreSQL.

Довольно долго я пытался экспортировать данные из MySQL при помощи штатного инструмента mysqldump в виде, пригодном для последующего экспорта в PostgreSQL. У этой утилиты имеется опция, позволяющая экспортировать в формате, совместимом с PostgreSQL. Несколько дополнительных опций, чтобы отключить директивы, отключить создание таблиц, добавить явные имена колонок в запросы INSERT, позволили получить результат с виду пригодный для импорта в PostgreSQL.

Тут, однако, возникла проблема. Данные таблиц в дампе шли в порядке, соответствюущем алфавитному порядку имён таблиц. Это вызывало проблемы с обработкой внешних ключей. Почему-то на тот момент я не догадался заглянуть в файл scheme.sql и создавал схему базы данных сразу с ограничениями и внешними ключами. Для решения этой проблемы я написал скрипт, который брал информацию о внешних ключах из базы данных information_schema и сортировал таблицы так, чтобы ссылающиеся таблицы шли после тех, на которые они ссылаются. Экспортировал данные я именно в этом порядке.

Однако когда проблема с внешними ключами была решена не самым простым путём, всплыло несколько досадных мелочей:
  • в полученном дампе внутри одинарных кавычек двойные кавычки экранировались обратным слэшем, в то время как PostgreSQL принимал их без экранирования,
  • одинарные кавычки в тех же строках тоже экранировались обратным слэшем, но PostgreSQL ждал, что они просто будут продублированы,
  • двоичные данные сохранялись в виде закавыченной последовательности байтов, в то время как PostgreSQL ждал двоичные данные в виде шестнадцатеричных цифр с префиксом 0x.
Попытки воспользоваться sed'ом для исправления недостатков не привели к успеху и я решил попробовать pgloader.

После продолжительных мучений с документацией pgloader и PostgreSQL в поисках правильной обработки зависимостей внешних ключей, я наконец-то догадался заглянуть в файл schema.sql и поделил его на две части. После этого pgloader отработал без запинки. Получившимся рецептом миграции и спешу поделиться.

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

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

Установим СУБД, если она ещё не установлена:
# apt-get install postgresql
Установим пакет pgloader, с помощью которого будем переносить содержимое базы из MySQL в PostgreSQL:
# apt-get install pgloader
Доустановим пакет для работы веб-интерфейса с СУБД PostgreSQL:
# apt-get install php5-pgsql
Чтобы новый модуль можно было использовать из PHP, перезапустим php5-fpm:
# systemctl restart php5-fpm
2. Подготовка СУБД

Правим файл конфигурации аутентификации пользователей /etc/postgresql/9.4/pg_hba.conf, заменяя первую строку на вторую:
local   all             all                                     peer
local   all             all                                     md5
По умолчанию, при подключении к UNIX-сокету, PostgreSQL определяет учётную запись, под которой работает подключившийся процесс и автоматически создаёт подключение от имени одноимённого пользователя из СУБД. Пароль при этом не запрашивается. Меняя эту строчку, мы будем требовать у подключившегося процесса явным образом указать имя пользователя СУБД и его пароль.

После этого перезапустим сервер базы данных, чтобы новые настройки вступили в силу:
# systemctl restart postgresql
Теперь из сеанса пользователя root заходим под пользователем postgres:
# su - postgres
От имени пользователя postgres создаём пользователя базы данных с именем zabbix:
$ createuser -P zabbix
Ключ -P означает, что будет запрошен пароль нового пользователя.

От имени пользователя postgres создаём саму базу данных с именем zabbix, владеть которой будет только что созданный пользователь с именем zabbix:
$ createdb -E UTF-8 -O zabbix zabbix
Опция -E UTF-8 означает, что текстовая информация в базе данных будет храниться в кодировке UTF-8, а опция -O задаёт пользователя, который будет владельцем базы данных.

3. Подготовка к переносу данных

Теперь возьмём файл database/postgresql/schema.sql, имеющийся в дистрибутиве Zabbix и поделим его на две части. В первой части будут запросы, создающие таблицы (CREATE TABLE), а во второй - создающие внешние ключи и ограничения (ALTER TABLE). Назовём эти файлы schema1.sql и schema2.sql

Создадим файл zabbix.load, содержащий настройки для переноса данных:
LOAD DATABASE
  FROM mysql://zabbix:zabbix_password@localhost/zabbix
  INTO postgresql://zabbix:zabbix_password@localhost/zabbix

WITH include no drop,
     truncate,
     create no tables,
     create no indexes,
     no foreign keys,
     reset sequences,
     data only

SET maintenance_work_mem TO '128MB',
    work_mem to '12MB'

BEFORE LOAD EXECUTE schema1.sql

AFTER LOAD EXECUTE schema2.sql;
Подготовим пакет с Zabbix-сервером, работающим с PostgreSQL или подключим репозиторий с этим пакетом. Как это сделать - решайте сами.

4. Перенос данных

Перед переносом данных остановим Zabbix-сервер:
# systemctl stop zabbix-server
Закроем доступ к веб-интерфейсу Zabbix, остановив php5-fpm (можно просто запретить доступ к веб-интерфейсу Zabbix через настройки nginx):
# systemctl stop php5-fpm
Теперь приступим к собственно переносу данных (в текущем каталоге должны быть подготовленные ранее файлы schema1.sql, schema2.sql, zabbix.load):
$ pgload zabbix.load
Пока данные переносятся, удалим старый Zabbix-сервер для MySQL:
# dpkg -r zabbix-server-mysql
Установим Zabbix-сервер для PostgreSQL :
# dpkg -i zabbix-server-pgsql_2.4.5-1+jessie_amd64.deb
Теперь, если данные уже перенеслись, можно запускать новый Zabbix-сервер:
# systemctl start zabbix-server
Отредактируем файл /etc/zabbix/web/zabbix.conf.php, заменив первую строчку на вторую:
$DB['TYPE']     = 'MYSQL';
$DB['TYPE']     = 'POSTGRESQL';
Запускаем php5-fpm (или открываем доступ к веб-интерфейсу Zabbix через настройки nginx):
# systemctl start php5-fpm
5. Проверка результата

Заглядываем в журналы Zabbix-сервера /var/log/zabbix/zabbix_server.log и заходим в веб-интерфейс Zabbix, проверяя, что всё работает нормально.

Кстати, наблюдение за нагрузкой на дисковую подсистему по такому не совсем чёткому параметру как iowait, показало, что нагрузка действительно упала: