воскресенье, 24 сентября 2017 г.

Решение проблемы с Open vSwitch после обновления Debian до Stretch

В одной из своих прошлых заметок Установка и настройка Open vSwitch в Debian я писал о настройке Open vSwitch. Тогда я воспользовался информацией из статьи Boot integration of the Openvswitch in Ubuntu.

Через некоторое время после выхода Deban Stretch, я решил обновить систему до него. После обновления столкнулся с проблемой: при загрузке системы происходит задержка при настройке сети и запуске демона Open vSwitch, после чего загрузка продолжается, но в загрузившейся системе оказываются настроенными только интерфейсы, не связанные с Open vSwitch.

Исправить проблему помогла всё та же статья Boot integration of the Openvswitch in Ubuntu, где была приведён юнит-файл systemd со следующим содержимым:
[Unit]
Description=Open vSwitch Internal Unit
PartOf=openvswitch-switch.service

DefaultDependencies=no

After=apparmor.service local-fs.target systemd-tmpfiles-setup.service

Wants=network-pre.target openvswitch-switch.service
Before=network-pre.target openvswitch-switch.service

[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=-/etc/default/openvswitch-switch
ExecStart=/usr/share/openvswitch/scripts/ovs-ctl start \
          --system-id=random $OVS_CTL_OPTS
ExecStop=/usr/share/openvswitch/scripts/ovs-ctl stop
Этот текст нужно поместить в файл /lib/systemd/system/openvswitch-nonetwork.service

Затем нужно добавить в файл /etc/default/openvswitch-switch вот такую опцию:
OVS_CTL_OPTS='--delete-bridges'
Однако этого оказалось не достаточно.

Затем нужно создать ещё один юнит-файл вот с таким содержимым:
[Unit]
Description=Open vSwitch
After=network.target openvswitch-nonetwork.service
Requires=openvswitch-nonetwork.service

[Service]
Type=oneshot
ExecStart=/bin/true
ExecStop=/bin/true
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
Этот текст нужно поместить в файл /lib/systemd/system/openvswitch-switch.service

Теперь нужно чтобы systemd перечитал обновления в своей конфигурации. Сделаем это при помощи следующей команды:
# systemctl daemon-reload
Теперь можно включить созданные нами сервис-файлы:
# systemctl enable /lib/systemd/system/openvswitch-nonetwork.service
# systemctl enable openvswitch-switch.service
Теперь можно перезагрузить систему и проверить, правильно ли настроилась сеть после перезагрузки.

Меня всё меньше радуют тенденции в новых релизах Debian и в Linux в целом. Я бы не возмущался, если бы лоббисты systemd, протащившие его в систему, заменили бы всё так, чтобы ничего не сломалось. А то получается, что старое сломали, а новое не работает как положено. В самих юнит-файлах по-прежнему используются shell-скрипты для запуска и остановки сервиса - достаточно посмотреть на значения опций ExecStart и ExecStop. Раньше было достаточно уметь писать shell-скрипты для того, чтобы запустить сервис, правильно подготовив для него окружение, и остановить сервис, убрав за ним мусор. Сейчас же получается, что shell-скрипты по-прежнему нужно знать, но ещё нужно знать и опции юнит-файлов systemd. Подумываю о FreeBSD, где знания об ipfw, rc.conf и системе портов в той или иной мере актуальны до сих пор, хотя система и развивается. Настораживает только то, что в последнее время даже самые ярые сторонники FreeBSD стали всё чаще использовать Ubuntu.

Использованные материалы:

воскресенье, 17 сентября 2017 г.

SSL-сертификат для ejabberd

Перевод: Ejabberd SSL Certificate
Автор: Реми ван Элст (Remy van Elst)

Примечания переводчика:
Как и в случае со статьёй "Заметки о Dockerfile", сейчас не помню - с чего вдруг я решил перевести эту статью.
Сейчас смотрю и недоумеваю от количества воды в ней. Ну да ладно, "пусть будет". Возможно я решил её перевести для комплекта с предыдущим переводом про настройку ejabberd.

Содержание

  • Введение
  • Создание запроса на подпись сертификата
  • Создание сертификата для ejabberd
  • Установка сертификата в ejabberd
В этом руководстве показано, как настроить SSL-сертификат для использования с ejabberd. Руководство охватывает вопросы создания запроса на подпись сертификата - Certificate Signing Request, подготовку сертификата для использования в ejabberd и установку сертификата.

В этом руководстве подразумевается использование уже настроенного ejabberd. Руководство протестировано в Debian и Ubuntu, но должно подойти для любого сервера ejabberd.

Введение

Для получения SSL-сертификата для сервера ejabberd нужно несколько вещей:
  • Создать запрос на подпись сертификата - Certificate Signing Request (CSR) и приватный ключ,
  • Передать запрос на подпись сертификата в удостоверяющий центр, дать ему подписать его и забрать оттуда готовый сертификат.
  • Объединить сертификат, приватный ключ (и цепочку сертификатов) в PEM-файл, совместимый с ejabberd,
  • Установить сертификат в ejabberd.
При помощи сертификата можно защитить XMPP-подключения и содержимое переговоров. Таким образом другим становится значительно сложнее прослушивать переговоры. В сочетании с включенным протоколом OTR это позволит создать сверхзащищённый канал для переговоров.

Создание запроса на подпись сертификата

Создадим каталог для хранения всех необходимых файлов и перейдём в него:
mkdir -p ~/Certificates/xmpp
cd ~/Certificates/xmpp
Теперь воспользуемся OpenSSL для создания приватного ключа и запроса на подпись сертификата - CSR. При помощи первой команды это можно сделать интерактивно, а при помощи второй - не интерактивно. Убедитесь, что задали правильное значение в поле Общее имя - Common Name (CN). В нём должен быть указан URL XMPP-сервера:

Интерактивная команда:
openssl req -nodes -newkey rsa:2048 -keyout private.key -out CSR.csr
Не интерактивная команда:
openssl req -nodes -newkey rsa:2048 -keyout private.key -out CSR.csr -subj "/C=NL/ST=State/L=City/O=Company Name/OU=Department/CN=chat.example.org"
В результате работы команды появятся два файла: CSR.csr и private.key. Теперь нужно передать запрос на подпись сертификата - CSR в удостоверяющий центр. Это может быть любой удостоверяющий центр. У меня есть успешный опыт использования Xolphin, но это могут быть другие удостоверяющие центры, например, Digicert и Verisign.

Как только вы передадите запрос на подпись сертификата и получите сертификат, можно продолжать дальше.

Создание сертификата для ejabberd

Как только у вас появятся все необходимые файлы (приватный ключ, сертификат, цепочка сертификатов), поместите их в каталог и продолжайте. Нужно объединить эти файлы в один файл ejabberd.pem.

Они должны располагаться в следующем порядке:
  • Приватный ключ,
  • Сертификат,
  • Цепочка сертификатов.
Замените имена файлов в следующей команде на ваши и создайте PEM-файл:
cat private.key >> ejabberd.pem
cat certificate.pem >> ejabberd.pem
cat chain-1.pem >> ejabberd.pem
cat chain-2.pem >> ejabberd.pem
Если всё сделано, то продолжим.

Установка сертификата в ejabberd

Скопируйте сертификат на все ваши серверы ejabberd:
scp ejabberd.pem user@srv1.example.org:
Поместите сертификат в каталог /etc/ejabberd:
cp ejabberd.pem /etc/ejabberd/ejabberd.pem
Теперь отредактируем файл конфигурации ejabberd так, чтобы он указывал на новый сертификат:
vim /etc/ejabberd/ejabberd.cfg
Проверьте, что файл конфигурации указывает на сертификат. При необходимости исправьте:
[...]
{listen, [
  {5222, ejabberdc2s, [ {access, c2s}, {shaper, c2sshaper}, {maxstanzasize, 65536},
         starttls, {certfile, "/etc/ejabberd/ejabberd.pem"}]},
[...]
{s2susestarttls, true}.
{s2s_certfile, "/etc/ejabberd/ejabberd.pem"}.
[...]
После этого перезапустите ejabberd:
/etc/init.d/ejabberd restart
Теперь можно воспользоваться любым XMPP-клиентом, чтобы подключиться к SSL/TLS и убедиться, что он работает.

воскресенье, 10 сентября 2017 г.

Настройка ejabberd для обмена мгновенными сообщениями по протоколу XMPP - своя альтернатива Google Talk Hangouts

Перевод: Set up a federated XMPP Chat Network with ejabberd, your own Google Talk Hangouts alternative
Автор: Реми ван Эст (Remy van Elst)

Примечания переводчика:
В оригинальной статье часто использовалось слово "федерализация" для того, чтобы подчеркнуть, что протокол XMPP не предполагает наличие единых серверов для всей сети, как это происходит в случае ICQ, WhatsApp, Viber или Telegram. XMPP предусматривает возможность создания множества сетей, у каждой из которых есть свои собственные серверы. Серверы каждой из сетей могут взаимодействовать с серверами других сетей на равноправных началах. То есть по аналогии с унитарными и федеральными государствами, ICQ можно назвать унитарной сетью, а XMPP можно назвать федеральной сетью. В переводе эти слова убраны, т.к. они не общеприняты и только запутывают. В качестве компенсации я добавил это примечание.

Содержание

  • Зачем настраивать собственный сервер XMPP
  • Информация
  • Установка одиночного/ведущего узла ejabberd
    • Установка ejabberd
    • Настройка ejabberd
  • Кластеризация ejabberd
    • Подготовка ведущего узла
    • Подготовка ведомых узлов
    • Ошибки при кластеризации
  • Записи DNS SRV
  • Заключительное тестирование
В этом руководстве показано, как установить сервер ejabberd для обмена мгновенными сообщениями. В нём рассказывается о базовом одноузловом сервере ejabberd, а также о настройке кластера ejabberd. Руководство включает в себя примеры ошибок и записей DNS SRV. Для установки собственного сервера XMPP можно воспользоваться услугами хостинга Inception Hosting VPS. Я пользуюсь их услугами и на мой взгляд этот хостинг очень стабильный, обладает высокой производительностью и низкими ценами.

Зачем настраивать собственный сервер XMPP

Существует несколько причин для настройки собственного сервера XMPP.

Может быть вы пользовались сервисом Google Talk или, как он теперь называется, Hangouts. Не так давно сервис Google прекратил поддерживать совместимость с XMPP. Если у вас есть контакты не на gmail, можно продолжать общаться с ними. Можно по-прежнему использовать открытый протокол, поддержка которого широко распространена и не станет частью закрытого программного и аппаратного обеспечения Google.

Возможно также, что вы хотите получить больший контроль за историей переписки. Выключите историю переписки ejabberd и воспользуйтесь протоколом OTR, который позволит обеспечить вам полную тайну переписки (и Perfect Forward Secrecy - совершенно прямую секретность).

А может быть вы хотите пользоваться многопротокольными приложениями для обмена мгновенными сообщениями, такими как Pidgin, Psi+, Empathy, Adium, iChat/Messages или Miranda IM. На Android можно использовать Xabber, Beem или OneTeam. Знаете ли вы, что большие компании, такие как Facebook, WhatsApp и Google, используют (или использовали ранее) XMPP в качестве основного протокола для обмена мгновенными сообщениями?

Или можете быть вы - системный администратор, которому нужна локальная система для обмена мгновенными сообщениями. У одного из моих клиентов есть кластер ejabberd, состоящий из 4 виртуальных машин с Debian 7 (по 2 гигабайта оперативной памяти на каждой). Кластер располагается на трёх площадках и в одном дата-центре, и обслуживает 12000 пользователей, из которых обычно одновременно подключено 6000.

XMPP - это прекрасный расширяемый протокол, дополнительную информацию о котором можно найти здесь: https://en.wikipedia.org/wiki/XMPP

Информация

Эта статья проверена на Debian 7, Ubuntu 12.04 и 10.04, на OS X 10.8 Server. Все использовавшиеся серверы ejabberd были установлены через пакетный менеджер - либо через apt, либо через порты. Описанная конфигурация не проверялась, но также должна работать в Windows Server 2012 с ejabberd, собранном из исходных текстов на языке Erlang.

В статье используется домен example.org и сервер chat.example.org в качестве доменного имени XMPP-сервера. В разделе про кластеризацию используются серверы srv1.example.org и srv2.example.org. При настройке замените эти значения на ваши собственные.

Установка одиночного/ведущего узла ejabberd

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

Установка ejabberd

Это просто - для установки ejabberd воспользуйтесь пакетным менеджером:
apt-get install ejabberd
Также нужно установить несколько зависимостей среды поддержки Erlang.

Настройка ejabberd

Приступим к настройке сервиса ejabberd. Для начала остановим его:
/etc/init.d/ejabberd stop
Теперь запустите текстовый редактор для редактирования файлов конфигурации. Конфигурация ejabberd - это конфигурация на языке Erlang, поэтому комментарии начинаются не с #, а с %%. Также каждая опция в файле конфигурации завершается точкой (.).
vim /etc/ejabberd/ejabberd.cfg
Сначала добавим домен для обмена мгновенными сообщениями:
{hosts, ["example.org"]}.
Если нужно больше доменов, можно добавить и их следующим образом:
{hosts, ["sparklingclouds.nl", "raymii.org", "sparklingnetwork.nl"]}.
Эти доменные имена не являются именами серверов.

Далее объявим пользователя-администратора:
{acl, admin, {user, "remy", "example.org"}}.
remy соответствует части до символа @ в идентификаторе XMPP, а example.org соответствует части после этого символа. Если вам нужно больше пользователей-администраторов - добавьте дополнительные строки ACL.

Теперь если вы хотите разрешить регистрироваться через XMPP-клиента, включите встроенную функцию регистрации:
{access, register, [{allow, all}]}.
Если используется аутентификацию по данным из MySQL или LDAP, тогда возможность регистрации нужно отключить.

Мне нравится пользоваться общими списками контактов с группами списков контактов. Некоторые из моих клиентов используют общий список контактов для всех, так что никто не может добавить контакты, но могут видеть всех подключенных пользователей. Для этого нужно включить modsharedroster:
%% Сделайте это в блоке modules
{mod_shared_roster,[]},
Если файл конфигурации вас устраивает, сохраните его и перезапустите ejabberd:
/etc/init.d/ejabberd restart
Теперь для проверки нашей конфигурации нужно зарегистрировать пользователя. Если функция встроенной регистрации была включена, можно воспользоваться XMPP-клиентом. Если же функция встроенной регистрации была выключена - воспользуйтесь командой ejabberdctl:
ejabberdctl register remy example.org 'passw0rd'
Теперь проверим учётную запись, воспользовавшись клиентом XMPP, таким как Pidgin, Psi+ или Empathy. Если вам удалось подключиться, можно продолжать настройку. Если же не удалось - проверьте журналы ejabberd, настройки пакетного фильтра и тому подобное, чтобы устранить проблему.

Кластеризация ejabberd

Отметим, прежде чем приступать к кластеризации ejabberd, вам нужен правильно работающий ведущий узел. Если ведущий узел не работает, то сначала исправьте его.

Важно: используемые вами модули должны быть одинаковыми на каждом из узлов кластера. Если вы используете аутентификацию LDAP/MySQL или shared_roster, или особые настройки MUC, или отправку сообщений не подключенным в данный момент пользователям, то при кластеризации эти настройки не будут действовать, пока не выставить их на всех узлах.

Теперь давайте приступим. Сначала займёмся настройкой ведущего узла, а затем перейдём к настройке ведомых узлов.

Подготовка ведущего узла

Остановим сервер на ведущем узле и отредактируем файл /etc/default/ejabberd:
vim /etc/default/ejabberd
Раскомментируем опцию с именем узла и заменим её значение на полное доменное имя узла:
ERLANG_NODE=ejabberd@srv1.example.org
Затем добавим внешний (публичный) IP-адрес, указав его как кортеж - с запятыми вместо точек:
INET_DIST_INTERFACE={20,30,10,5}
Если ejabberd будет использоваться только в локальной сети, тогда укажите первичный адрес сетевой карты.

Мы собираемся удалить все таблицы mnesia. Они будут пересозданы при перезапуске ejabberd. Это проще, чем менять сами данные mnesia. Не делайте этого на уже настроенном узле, не сняв резервную копию с cookie-файла Erlang.

Для начала создадим резервную копию cookie-файла Erlang:
cp /var/lib/ejabberd/.erlang.cookie ~/
Затем удалим базу данных mnesia:
rm /var/lib/ejabberd/*
Теперь восстановим cookie-файл Erlang:
cp ~/.erlang.cookie /var/lib/ejabberd/.erlang.cookie
Чтобы удостовериться, что все процессы erlang были остановлены, завершите все процессы пользователя ejabberd принудительно. Делать это не обязательно, но диспетчер процессов epmd всё ещё может продолжать работать:
killall -u ejabberd
И теперь снова запустим ejabberd:
/etc/init.d/ejabberd start
Если после этого удалось подключиться и обмениваться сообщениями, тогда перейдите к следующей части - к настройке ведомых узлов.

Подготовка ведомых узлов

Сначала ведомые узлы нужно настроить так, как описано в первой части этой статьи. Для этого можно скопировать файлы конфигурации с ведущего узла.

Остановим сервер ejabberd:
/etc/init.d/ejabberd stop
Остановим сервер ejabberd на ведомом узле и отредактируем файл /etc/default/ejabberd:
vim /etc/default/ejabberd
Раскомментируем опцию с именем узла и заменим её значение на полное доменное имя узла:
ERLANG_NODE=ejabberd@srv2.example.org
Затем добавим внешний (публичный) IP-адрес, указав его как кортеж - с запятыми вместо точек:
INET_DIST_INTERFACE={30,40,20,6}
Если ejabberd будет использоваться только в локальной сети, тогда укажите первичный адрес сетевой карты.

Теперь удалим все таблицы mnesia:
rm /var/lib/ejabberd/*
Скопируем cookie-файл с ведущего узла ejabberd при помощи cat и vim или через scp:
# На ведущем узле
cat /var/lib/ejabberd/.erlang.cookie
HFHHGYYEHF362GG1GF

# На ведомом узле
echo "HFHHGYYEHF362GG1GF" > /var/lib/ejabberd/.erlang.cookie
chown ejabberd:ejabberd /var/lib/ejabberd/.erlang.cookie
Теперь приступим к компиляции модуля easy_cluster на Erlang. Это очень маленький модуль, который добавляет к оболочке Erlang команду для более простого добавления кластера. Вместо этих команд в оболочке Erlang можно выполнить сами Erlang-функции в отладочной оболочке, но я считаю, что модуль удобнее и его использование уменьшает вероятность ошибок:
vim /usr/lib/ejabberd/ebin/easy_cluster.erl
Добавьте следующее содержимое:
-module(easy_cluster).

-export([test_node/1,join/1]).

test_node(MasterNode) ->
    case net_adm:ping(MasterNode) of 'pong' ->
        io:format("server is reachable.~n"); %% Сервер доступен
    _ ->
        io:format("server could NOT be reached.~n") %% Сервер не доступен
    end.

join(MasterNode) ->
    application:stop(ejabberd),
    mnesia:stop(),
    mnesia:delete_schema([node()]),
    mnesia:start(),
    mnesia:change_config(extra_db_nodes, [MasterNode]),
    mnesia:change_table_copy_type(schema, node(), disc_copies),
    application:start(ejabberd).
Сохраните его и скомпилируйте в работающий Erlang-модуль:
cd /usr/lib/ejabberd/ebin/
erlc easy_cluster.erl
Теперь проверим, что компиляция была успешной:
ls | grep easy_cluster.beam
Если вы увидели файл, значит компиляция завершилась успешно. Дополнительную информацию по модулю можно найти здесь: https://github.com/chadillac/ejabberd-easy_cluster/

Теперь приступим к присоединению узла кластера к ведущему узлу. Убедитесь что ведущий узел запущен и работает. Также удостоверьтесь, что cookie-файлы Erlang синхронизированы.

На ведомом узле запустите интерактивную оболочку ejabberd:
/etc/init.d/ejabberd live
Эта команда запустит оболочку Erlang, после чего оболочка начнёт выводить информацию. Когда она прекратит вывод, можно нажать Enter и получить приглашение. Введите следующую команду, чтобы проверить, что ведущий узел достижим:
easy_cluster:test_node('ejabberd@srv1.example.org').
Вы должны получить сообщение "server is reachable" - "Сервер доступен". Если это так, то продолжим.

Введём следующую команду, чтобы действительно присоединить узел:
easy_cluster:join('ejabberd@srv1.example.org').
Вот пример вывода при успешной проверке и подсоединении узла:
/etc/init.d/ejabberd live
*******************************************************
* To quit, press Ctrl-g then enter q and press Return *
*******************************************************

Erlang R15B01 (erts-5.9.1) [source] [async-threads:0] [kernel-poll:false]

Eshell V5.9.1 (abort with ^G)

=INFO REPORT==== 10-Jun-2013::20:38:15 ===
I(<0.39.0>:cyrsasl_digest:44) : FQDN used to check DIGEST-MD5 SASL authentication: "srv2.example.org"

=INFO REPORT==== 10-Jun-2013::20:38:15 ===
I(<0.576.0>:ejabberd_listener:166) : Reusing listening port for 5222

=INFO REPORT==== 10-Jun-2013::20:38:15 ===
I(<0.577.0>:ejabberd_listener:166) : Reusing listening port for 5269

=INFO REPORT==== 10-Jun-2013::20:38:15 ===
I(<0.578.0>:ejabberd_listener:166) : Reusing listening port for 5280

=INFO REPORT==== 10-Jun-2013::20:38:15 ===
I(<0.39.0>:ejabberd_app:72) : ejabberd 2.1.10 is started in the node 'ejabberd@srv2.example.org'
easy_cluster:test_node('ejabberd@srv1.example.org').
server is reachable.
ok
(ejabberd@srv2.example.org)2> easy_cluster:join('ejabberd@srv1.example.org').

=INFO REPORT==== 10-Jun-2013::20:38:51 ===
I(<0.39.0>:ejabberd_app:89) : ejabberd 2.1.10 is stopped in the node 'ejabberd@srv2.example.org'

=INFO REPORT==== 10-Jun-2013::20:38:51 ===
    application: ejabberd
    exited: stopped
    type: temporary

=INFO REPORT==== 10-Jun-2013::20:38:51 ===
    application: mnesia
    exited: stopped
    type: permanent

=INFO REPORT==== 10-Jun-2013::20:38:52 ===
I(<0.628.0>:cyrsasl_digest:44) : FQDN used to check DIGEST-MD5 SASL authentication: "srv2.example.org"

=INFO REPORT==== 10-Jun-2013::20:38:53 ===
I(<0.1026.0>:ejabberd_listener:166) : Reusing listening port for 5222

=INFO REPORT==== 10-Jun-2013::20:38:53 ===
I(<0.1027.0>:ejabberd_listener:166) : Reusing listening port for 5269

=INFO REPORT==== 10-Jun-2013::20:38:53 ===
I(<0.1028.0>:ejabberd_listener:166) : Reusing listening port for 5280
ok
(ejabberd@srv2.example.org)3>

=INFO REPORT==== 10-Jun-2013::20:38:53 ===
I(<0.628.0>:ejabberd_app:72) : ejabberd 2.1.10 is started in the node 'ejabberd@srv2.example.org'
Покиньте оболочку Erlang дважды нажав Ctrl+C. Теперь остановите ejabberd и запустите его снова:
/etc/init.d/ejabberd restart
Теперь в административном веб-интерфейсе можно проверить, что узел успешно присоединился к кластеру: http://srv1.example.org:5280/admin/nodes/
Узлы ejabberd
Если будут отображены другие узлы, значит всё готово. Если нет, тогда убедитесь, что выполнили каждый шаг и обратитесь к разделу ниже с описанием решений проблем.

Повторите описанные выше шаги для каждого из узлов, которые нужно добавить. Вы можете добавить столько узлов, сколько захотите.

Ошибки при кластеризации

В процессе настройки кластера могут произойти ошибки. Ниже перечислены примечания по встречавшимся мне ошибкам.
  • Перезапуск ejabberd не приводит к перезапуску epmd (демона Erlang)
    • избыточное решение: killall -u ejabberd
  • ejabberd возвращает ошибки с именем узла
    • убедитесь в правильности настройки имени узла (имя узла - srv1.example.com).
  • ejabberd возвращает ошибки несогласованности базы данных
    • создайте резервную копию cookie-файла Erlang (/var/lib/ejabberd/.erlang.cookie), а затем удалите содержимое каталога /var/lib/ejabberd, чтобы mnesia перестроила таблицы.
  • ejabberd сообщает "Попытка подключения от запрещённого узла" - "Connection attempt from disallowed node"
    • убедитесь в корректности cookie-файла Erlang (/var/lib/ejabberd/.erlang.cookie). Перед вставкой содержимого в редакторе vim перейдите в режим вставки...

Записи DNS SRV

Записи DNS SRV используются XMPP-клиентами и другими XMPP-серверами для поиска правильного адреса сервера. Например, Алиса настраивает своего XMPP-клиента на адрес alice@example.org. Её клиент ищет запись SRV и узнаёт, что сервер для обмена мгновенными сообщениями находится по адресу chat.example.org. Боб настраивает своего клиента на адрес bob@bobsbussiness.com и добавляет Алису в список контактов. XMPP-сервер домена bobsbussiness.com ищет запись SRV и узнаёт, что он должен установить подключение типа сервер-к-серверу по адресу chat.example.org, чтобы дать Бобу возможность переписываться с Алисой.

Конфигурация BIND 9 будет выглядеть следующим образом:
; XMPP
_xmpp-client._tcp IN SRV 5 0 5222 chat.example.org.
_xmpp-server._tcp IN SRV 5 0 5269 chat.example.org.
_jabber._tcp IN SRV 5 0 5269 chat.example.org.
Основные записи SRV указывают порты для подключения клиентов и для подключений типа сервер-к-серверу, а третья запись - это устаревший формат записей Jabber. Если используется хостинг DNS, введите эти записи в панели администрирования или посоветуйтесь с технической поддержкой хостинга.

Для проверки правильности SRV-записей можно воспользоваться командой dig:
dig _xmpp-client._tcp.example.org SRV
dig _xmpp-server._tcp.example.org SRV
Или если на вашем компьютере установлена операционная система Windows, тогда воспользуйтесь nslookup:
nslookup -querytype=SRV _xmpp-client._tcp.example.org
nslookup -querytype=SRV _xmpp-server._tcp.example.org
Если результат будет похожим на приведённый ниже, значит всё настроено верно:
;; QUESTION SECTION:
;_xmpp-client._tcp.raymii.org. IN SRV
;; ANSWER SECTION:
_xmpp-client._tcp.raymii.org. 3600 IN SRV 5 0 5222 chat.raymii.org.
На самом же деле в моём случае у chat.raymii.org имеется несколько A-записей:
;; ADDITIONAL SECTION:
chat.raymii.org. 3600 IN A 84.200.77.167
chat.raymii.org. 3600 IN A 205.185.117.74
chat.raymii.org. 3600 IN A 205.185.124.11
Но если был настроен только один узел, то это будет либо запись CNAME, либо одна запись A/AAAA.

Заключительное тестирование

Чтобы протестировать, что всё работает, можно добавить в список контактов XMPP-бота Duck Duck Go. Если вам без проблем удалось добавить его и поговорить с ним, то значит всё было сделано верно. Адрес бота - im@ddg.gg.

воскресенье, 3 сентября 2017 г.

Командная строка OpenSSL: Корневой и промежуточный удостоверяющий центры, включая OCSP, CRL и отзыв сертификатов

Перевод: OpenSSL command line Root and Intermediate CA including OCSP, CRL and revocation
Автор: Реми ван Элст (Remy van Elst)

Содержание:

  • Корневой удостоверяющий центр
  • Создание промежуточного удостоверяющего центра 1
  • Настройка промежуточного удостоверяющего центра 1
  • Создание конечных пользовательских сертификатов
  • Проверка сертификата
Это короткие и неаккуратные заметки по созданию удостоверяющего центра, промежуточных удостоверяющих центров и конечных сертификатов при помощи OpenSSL. Сертификаты содержат информацию об OCSP - протоколе интерактивного статуса сертификата, CRL - списке отозванных сертификатов, информацию об удостоверяющем центре-эмитенте, назначении сертификата и сроке его годности.

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

Корневой удостоверяющий центр

Создадим и переместим в каталог корневой удостоверяющий центр:
mkdir ~/SSLCA/root/
cd ~/SSLCA/root/
Создадим ключ SHA-256 RSA длиной 8192 бит для нашего корневого удостоверяющего центра:
openssl genrsa -aes256 -out rootca.key 8192
Пример вывода:
Generating RSA private key, 8192 bit long modulus # Создание приватного ключа, с модулем длиной 8192 бит
.........++
....................................................................................................................++
e is 65537 (0x10001) # e равно 65537 (0x10001)
Если хотите защитить этот ключ паролем, добавьте опцию -passout pass:пароль или -passout file:файл_с_паролем.

Создадим самозаверенный сертификат удостоверяющего центра ca.crt. Нужно предоставить идентификационные данные вашего корневого удостоверяющего центра:
openssl req -sha256 -new -x509 -days 1826 -key rootca.key -out rootca.crt
Пример вывода:
You are about to be asked to enter information that will be incorporated     # У вас будет запрошена информация, которая будет вставлена
into your certificate request.                                               # в ваш запрос сертификата.
What you are about to enter is what is called a Distinguished Name or a DN.  # То, что вы введёте, называется Distinquised Name - Отличительное Имя или DN.
There are quite a few fields but you can leave some blank                    # Далее следует несколько полей, но некоторые из них можно не заполнять
For some fields there will be a default value,                               # У некоторых полей есть значение по умолчанию,
If you enter '.', the field will be left blank.                              # Если ввести '.', то поле останется пустым.
-----
Country Name (2 letter code) [AU]:NL                                         # Название страны (двухбуквенный код) [AU]:NL
State or Province Name (full name) [Some-State]:Zuid Holland                 # Название штата или провинции (полное название) [Некий-штат]:Южная Голландия
Locality Name (eg, city) []:Rotterdam                                        # Название местности (например, город) []:Роттердам
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Sparkling Network # Название организации (например, компания) [ООО Интернет-виджеты]:Искрящая сеть
Organizational Unit Name (eg, section) []:Sparkling CA                       # Название подразделения (например, отдел) []:Искрящий удостоверяющий центр
Common Name (e.g. server FQDN or YOUR name) []:Sparkling Root CA             # Общее имя (например, полное доменное имя сервера или ваше имя) []:Искрящий корневой удостоверяющий центр
Email Address []:                                                            # Адрес электронной почты []:
Создадим несколько файлов, в которых удостоверяющий центр будет хранить серийные номера:
touch certindex
echo 1000 > certserial
echo 1000 > crlnumber
Создадим файл конфигурации удостоверяющего центра. Этот файл содержит заглушки для конечных точек CRL - списка отозванных сертификатов и OCSP - протокола интерактивного статуса сертификата.
# vim ca.conf
[ca]
default_ca = myca

[crl_ext]
issuerAltName=issuer:copy
authorityKeyIdentifier=keyid:always

[myca]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/rootca.crt
database = $dir/certindex
private_key = $dir/rootca.key
serial = $dir/certserial
default_days = 730
default_md = sha1
policy = myca_policy
x509_extensions = myca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 730

[myca_policy]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationalUnitName = optional

[myca_extensions]
basicConstraints = critical,CA:TRUE
keyUsage = critical,any
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
keyUsage = digitalSignature,keyEncipherment,cRLSign,keyCertSign
extendedKeyUsage = serverAuth
crlDistributionPoints = @crl_section
subjectAltName = @alt_names
authorityInfoAccess = @ocsp_section

[v3_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,any
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
keyUsage = digitalSignature,keyEncipherment,cRLSign,keyCertSign
extendedKeyUsage = serverAuth
crlDistributionPoints = @crl_section
subjectAltName = @alt_names
authorityInfoAccess = @ocsp_section

[alt_names]
DNS.0 = Sparkling Intermidiate CA 1 # DNS.0 = Искрящий промежуточный удостоверяющий центр 1
DNS.1 = Sparkling CA Intermidiate 1 # DNS.1 = Искрящий удостоверяющий центр промежуточный 1

[crl_section]
URI.0 = http://pki.sparklingca.com/SparklingRoot.crl
URI.1 = http://pki.backup.com/SparklingRoot.crl

[ocsp_section]
caIssuers;URI.0 = http://pki.sparklingca.com/SparklingRoot.crt
caIssuers;URI.1 = http://pki.backup.com/SparklingRoot.crt
OCSP;URI.0 = http://pki.sparklingca.com/ocsp/
OCSP;URI.1 = http://pki.backup.com/ocsp/
Если нужно задать определённые даты начала или завершения действия сертификата, добавьте в секцию [myca] следующие строки:
# формат: ГГГГММДДЧЧММСС
default_enddate = 20191222035911
default_startdate = 20181222035911

Создание промежуточного удостоверяющего центра 1

Создание приватного ключа промежуточного удостоверяющего центра:
openssl genrsa -out intermediate1.key 4096
Создание CSR - запроса на подписание сертификата промежуточного удостоверяющего центра 1:
openssl req -new -sha256 -key intermediate1.key -out intermediate1.csr
Пример вывода:
You are about to be asked to enter information that will be incorporated     # У вас будет запрошена информация, которая будет вставлена
into your certificate request.                                               # в ваш запрос сертификата.
What you are about to enter is what is called a Distinguished Name or a DN.  # То, что вы введёте, называется Distinquised Name - Отличительное Имя или DN.
There are quite a few fields but you can leave some blank                    # Далее следует несколько полей, но некоторые из них можно не заполнять
For some fields there will be a default value,                               # У некоторых полей есть значение по умолчанию,
If you enter '.', the field will be left blank.                              # Если ввести '.', то поле останется пустым.
-----
Country Name (2 letter code) [AU]:NL                                         # Название страны (двухбуквенный код) [AU]:NL
State or Province Name (full name) [Some-State]:Zuid Holland                 # Название штата или провинции (полное название) [Некий-штат]:Южная Голландия
Locality Name (eg, city) []:Rotterdam                                        # Название местности (например, город) []:Роттердам
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Sparkling Network # Название организации (например, компания) [ООО Интернет-виджеты]:Искрящая сеть
Organizational Unit Name (eg, section) []:Sparkling CA                       # Название подразделения (например, отдел) []:Искрящий удостоверяющий центр
Common Name (e.g. server FQDN or YOUR name) []:Sparkling Intermediate CA     # Общее имя (например, полное доменное имя сервера или ваше имя) []:Искрящий промежуточный удостоверяющий центр
Email Address []:                                                            # Адрес электронной почты []:

Please enter the following 'extra' attributes                                # Пожалуйста, введите следующие дополнительные атрибуты,
to be sent with your certificate request                                     # которые будут отправлены в составе запроса сертификата
A challenge password []:                                                     # Пароль вызова []:
An optional company name []:                                                 # Не обязательное название компании []:
Удостоверьтесь, что Общее Имя (Common Name - CN) промежуточного сертификата отличается от корневого.

Подпишем корневым удостоверяющим центром запрос на подписание сертификата промежуточного удостоверяющего центра:
openssl ca -batch -config ca.conf -notext -in intermediate1.csr -out intermediate1.crt
Пример вывода:
Using configuration from ca.conf                                         # Используется конфигурация из ca.conf
Check that the request matches the signature                             # Проверка соответствия подписи запросу
Signature ok                                                             # Подпись в порядке
The Subject's Distinguished Name is as follows                           # Содержимое Отличительного Имени показано ниже
countryName :PRINTABLE:'NL'
stateOrProvinceName :ASN.1 12:'Zuid Holland'
localityName :ASN.1 12:'Rotterdam'
organizationName :ASN.1 12:'Sparkling Network'
organizationalUnitName:ASN.1 12:'Sparkling CA'
commonName :ASN.1 12:'Sparkling Intermediate CA'
Certificate is to be certified until Mar 30 15:07:43 2017 GMT (730 days) # Сертификат был удостоверен до 30 марта 2017 года 15:07:43 по Гринвичу (730 дней)

Write out database with 1 new entries                                    # В базу данных записана 1 новая запись
Data Base Updated                                                        # База данных обновлена
Создадим CRL - список отозванных сертификатов в форматах PEM и DER:
openssl ca -config ca.conf -gencrl -keyfile rootca.key -cert rootca.crt -out rootca.crl.pem
openssl crl -inform PEM -in rootca.crl.pem -outform DER -out rootca.crl
Создавайте CRL - список отозванных сертификатов после каждого подписания сертификата удостоверяющим центром.

Если когда-нибудь понадобится отозвать этот промежуточный сертификат:
openssl ca -config ca.conf -revoke intermediate1.crt -keyfile rootca.key -cert rootca.crt

Настройка промежуточного удостоверяющего центра 1

Создадим новый каталог и переместим промежуточный удостоверяющий центр:
mkdir ~/SSLCA/intermediate1/
cd ~/SSLCA/intermediate1/
Скопируем сертификат промежуточного удостоверяющего центра и ключ из каталога корневого удостоверяющего центра:
cp ~/SSLCA/root/intermediate1.key ./
cp ~/SSLCA/root/intermediate1.crt ./
Создадим файлы индекса:
touch certindex
echo 1000 > certserial
echo 1000 > crlnumber
Создадим новый файл ca.conf:
# vim ca.conf
[ca]
default_ca = myca

[crl_ext]
issuerAltName=issuer:copy
authorityKeyIdentifier=keyid:always

[myca]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/intermediate1.crt
database = $dir/certindex
private_key = $dir/intermediate1.key
serial = $dir/certserial
default_days = 365
default_md = sha1
policy = myca_policy
x509_extensions = myca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 365

[myca_policy]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationalUnitName = optional

[myca_extensions]
basicConstraints = critical,CA:FALSE
keyUsage = critical,any
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = @crl_section
subjectAltName = @alt_names
authorityInfoAccess = @ocsp_section

[alt_names]
DNS.0 = example.com
DNS.1 = example.org

[crl_section]
URI.0 = http://pki.sparklingca.com/SparklingIntermidiate1.crl
URI.1 = http://pki.backup.com/SparklingIntermidiate1.crl

[ocsp_section]
caIssuers;URI.0 = http://pki.sparklingca.com/SparklingIntermediate1.crt
caIssuers;URI.1 = http://pki.backup.com/SparklingIntermediate1.crt
OCSP;URI.0 = http://pki.sparklingca.com/ocsp/
OCSP;URI.1 = http://pki.backup.com/ocsp/
Отредактируйте секцию [alt_names] так, чтобы она содержала необходимые альтернативные имена. Если альтернативные имена не нужны, то удалите эту секцию и строку subjectAltName = @alt_names.

Если нужно задать определённые даты начала или завершения действия сертификата, добавьте в секцию [myca] следующие строки:
# формат: ГГГГММДДЧЧММСС
default_enddate = 20191222035911
default_startdate = 20181222035911
Создадим пустой CRL - список отозванных сертификатов в форматах PEM и DER:
openssl ca -config ca.conf -gencrl -keyfile rootca.key -cert rootca.crt -out rootca.crl.pem
openssl crl -inform PEM -in rootca.crl.pem -outform DER -out rootca.crl

Создание конечных пользовательских сертификатов

Воспользуемся новым промежуточным удостоверяющим центром для создания конечного пользовательского сертификата. Повторим эти шаги для каждого конечного пользовательского сертификата, который нужно подписать этим удостоверяющим центром.
mkdir enduser-certs
Создадим приватный ключ конечного пользователя:
openssl genrsa -out enduser-certs/enduser-example.com.key 4096
Создадим CSR - запрос на подписание сертификата конечного пользователя:
openssl req -new -sha256 -key enduser-certs/enduser-example.com.key -out enduser-certs/enduser-example.com.csr
Пример вывода:
You are about to be asked to enter information that will be incorporated    # У вас будет запрошена информация, которая будет вставлена
into your certificate request.                                              # в ваш запрос сертификата.
What you are about to enter is what is called a Distinguished Name or a DN. # То, что вы введёте, называется Distinquised Name - Отличительное Имя или DN.
There are quite a few fields but you can leave some blank                   # Далее следует несколько полей, но некоторые из них можно не заполнять
For some fields there will be a default value,                              # У некоторых полей есть значение по умолчанию,
If you enter '.', the field will be left blank.                             # Если ввести '.', то поле останется пустым.
-----
Country Name (2 letter code) [AU]:NL                                        # Название страны (двухбуквенный код) [AU]:NL
State or Province Name (full name) [Some-State]:Noord Holland               # Название штата или провинции (полное название) [Некий-штат]:Северная Голландия
Locality Name (eg, city) []:Amsterdam                                       # Название местности (например, город) []:Амстердам
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Example Inc      # Название организации (например, компания) [ООО Интернет-виджеты]:АО Пример
Organizational Unit Name (eg, section) [например, отдел]:IT Dept            # Название подразделения (например, отдел) []:Отдел информационных технологий
Common Name (e.g. server FQDN or YOUR name) []:example.com                  # Общее имя (например, полное доменное имя сервера или ваше имя) []:example.com
Email Address []:                                                           # Адрес электронной почты []:

Please enter the following 'extra' attributes                               # Пожалуйста, введите следующие дополнительные атрибуты,
to be sent with your certificate request # которые будут отправлены в составе запроса сертификата
A challenge password []: # Пароль вызова []:
An optional company name []: # Не обязательное название компании []:
Подпишем CSR - запрос на подписание сертификата конечного пользователя в промежуточном удостоверяющем центре 1:
openssl ca -batch -config ca.conf -notext -in enduser-certs/enduser-example.com.csr -out enduser-certs/enduser-example.com.crt
Пример вывода:
Using configuration from ca.conf                                         # Используется конфигурация из ca.conf
Check that the request matches the signature                             # Проверка соответствия подписи запросу
Signature ok                                                             # Подпись в порядке
The Subject's Distinguished Name is as follows                           # Содержимое Отличительного Имени показано ниже
countryName :PRINTABLE:'NL'
stateOrProvinceName :ASN.1 12:'Noord Holland'
localityName :ASN.1 12:'Amsterdam'
organizationName :ASN.1 12:'Example Inc'
organizationalUnitName:ASN.1 12:'IT Dept'
commonName :ASN.1 12:'example.com'
Certificate is to be certified until Mar 30 15:18:26 2016 GMT (730 days) # Сертификат был удостоверен до 30 марта 2016 года 15:18:26 по Гринвичу (365 дней)

Write out database with 1 new entries                                    # В базу данных записана 1 новая запись
Data Base Updated                                                        # База данных обновлена
Создадим CRL - список отозванных сертификатов в форматах PEM и DER:
openssl ca -config ca.conf -gencrl -keyfile intermediate1.key -cert intermediate1.crt -out intermediate1.crl.pem
openssl crl -inform PEM -in intermediate1.crl.pem -outform DER -out intermediate1.crl
Создавайте CRL - список отозванных сертификатов после каждого подписания сертификата удостоверяющим центром.

Если когда-то понадобится отозвать этот сертификат конечного пользователя:
openssl ca -config ca.conf -revoke enduser-certs/enduser-example.com.crt -keyfile intermediate1.key -cert intermediate1.crt
Пример вывода:
Using configuration from ca.conf # Используется конфигурация из ca.conf
Revoking Certificate 1000.       # Отзыв сертификата 1000.
Data Base Updated                # База данных обновлена
Создадим файл с цепочкой сертификатов, соединив вместе сертификаты корневого и промежуточного удостоверяющего центра 1.
cat ../root/rootca.crt intermediate1.crt > enduser-certs/enduser-example.com.chain
Отправим следующие файлы конечному пользователю:
  • enduser-example.com.crt
  • enduser-example.com.key
  • enduser-example.com.chain
Вы также можете дать конечному пользователю возможность предоставить его собственный CSR - запрос на подписание сертификата и просто отправить ему файл .crt. Не удаляйте сертификат с сервера, в противном случае его нельзя будет отозвать.

Проверка сертификата

Вы можете проверить сертификат конечного пользователя по цепочке при помощи следующей команды:
openssl verify -CAfile enduser-certs/enduser-example.com.chain enduser-certs/enduser-example.com.crt
enduser-certs/enduser-example.com.crt: OK
Вы также можете проверить его по CRL - списку отозванных сертификатов. Сначала соедините вместе CRL в формате PEM и цепочку:
cat ../root/rootca.crt intermediate1.crt intermediate1.crl.pem > enduser-certs/enduser-example.com.crl.chain
Проверим сертификат:
openssl verify -crl_check -CAfile enduser-certs/enduser-example.com.crl.chain enduser-certs/enduser-example.com.crt
Если не отозван, будет выведено:
enduser-certs/enduser-example.com.crt: OK
Если отозван, будет выведено:
enduser-certs/enduser-example.com.crt: CN = example.com, ST = Noord Holland, C = NL, O = Example Inc, OU = IT Dept
error 23 at 0 depth lookup:certificate revoked                                                                     # ошибка 23 на глубине просмотра 0:сертификат отозван

воскресенье, 27 августа 2017 г.

Краткое руководство по GPG

Перевод: GPG Quickstart
Автор: Эндрю Бикхоф (Andrew Beekhof)

Оказалось кстати, что мне нужно было одновременно освежить знания о GPG и обновить ключи. Я собрал свой опыт (и источники) в тексте ниже, на случай если это окажется кому-то полезным:

Подготовка

Следующие настройки обеспечат, чтобы все ключи, создаваемые в дальнейшем, строго соответствовали стандартам 2013 года. Поместите эти настройки в файл ~/.gnupg/gpg.conf:
# Если все адресаты поддерживают несколько алгоритмов хэширования, выбирать самый надёжный из них:
personal-digest-preferences SHA512 SHA384 SHA256 SHA224
# Предпочтения для новых ключей должны учитывать приоритет алгоритмов, в соответствии с их надёжностью:
default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 BZIP2 ZLIB ZIP Uncompressed
# При создании сертификата OpenPGP использовать наиболее надёжный алгоритм хэширования,
# а не алгоритм SHA1, который используется по умолчанию:
cert-digest-algo SHA512
Следующая порция настроек не обязательна, но полезна для улучшения вывода команд gpg в различных ситуациях - в частности, для защиты от подделки. Их тоже нужно поместить в ~/.gnupg/gpg.conf:
# При выводе сертификатов показывать идентификатор пользователя, выделенный из ключей:
fixed-list-mode
# Длинные идентификаторы ключей более защищены от коллизий, чем короткие идентификаторы ключей
# (можно легко создать ключ с любым желаемым коротким идентификатором ключа):
keyid-format 0xlong
# Если вы пользуетесь графической средой (и даже если не пользуетесь ей), вам нужно использовать агента
# (похожие аргументы в пользу этого приведены по ссылке https://www.debian-administration.org/users/dkg/weblog/64):
use-agent
# Взглянув на идентификатор пользователя, вы всегда должны знать, что по мнению gpg он является доверенным,
# т.к. ключ присутствует в вашем брелоке:
verify-options show-uid-validity
list-options show-uid-validity
# Включить недвусмысленный индикатор ключа, которым сделана подпись
# (см. http://thread.gmane.org/gmane.mail.notmuch.general/3721/focus=7234):
sig-notation issuer-fpr@notations.openpgp.fifthhorseman.net=%g

Создание нового ключа

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

Я воспользовался инструкциями из статьи Анны Гереро (Ana Guerrero), которые были основой текущего руководства Debian, но выбрал тип ключа по умолчанию в соответствии со стандартами 2013 года:
  1. Запустите gpg --gen-key
  2. Выберите 1: RSA и RSA (по умолчанию).
  3. Выберите размер ключа больше 2048.
  4. Задайте срок действия ключа - 2-5 лет (обоснование).
  5. НЕ указывайте комментарий для идентификатора пользователя (объяснение).

Добавление дополнительных идентификаторов пользователя и настройки по умолчанию

С этого момента мой брелок gpg --list-keys выглядит следующим образом:
pub 4096R/0x726724204C644D83 2013-06-24
uid               [ultimate] Andrew Beekhof <andrew@beekhof.net>
sub 4096R/0xC88100891A418A6B 2013-06-24 [expires: 2015-06-24]
Как и у большинства людей, у меня есть несколько адресов электронной почты и я хочу использовать GPG и с ними тоже. Поэтому сейчас самое время добавить их к ключу. Для этого нужно воспользоваться командой gpg --edit-key. У Анны есть хороший пример добавления идентификаторов пользователей и настройки предпочтений. Просто поищите в её инструкциях текст "Add other UID" - "Добавить другой идентификатор пользователя".

Отдельные подключи для шифрования и подписи

Общепринято использовать отдельные ключи для подписывания и шифрования.
Коротко: бывает нужно что-то зашифровать, не подписывая, поскольку подписание может повлечь за собой юридически последствия. Также существует вероятность, что подписанные сообщения могут быть использованы для взлома зашифрованных данных.
По умолчанию gpg создаёт подключ для шифрования, но я воспользуюсь руководством Debian по субключам, чтобы создать ещё один для подписания (вместо того, чтобы использовать секретный мастер-ключ).
Это позволит сделать ваш секретный мастер-ключ ещё более защищённым, избегая его повседневного использования.
Идея заключается в том, чтобы сначала создать копию и сохранить её в ещё более безопасном месте так, что если подключ (и компьютер, где он находится) будут взломаны, мастер-ключ оставался бы в безопасности и им бы всё равно можно было бы отозвать подключи и создать новые.

Подписание нового ключа старым

Если ваш ключ старый, то им можно подписать новый ключ. Это даст знать всем, кто доверяет старому ключу, что новый ключ законный и поэтому ему можно доверять.
Вернёмся снова к советам Анны. Делается это так:
gpg --default-key СТАРЫЙ-КЛЮЧ --sign-key НОВЫЙ-КЛЮЧ
В моём случае команда будет такой:
gpg --default-key 0xEC3584EFD449E59A --sign-key 0x726724204C644D83

Отправка новых ключей на сервер

Сообщим людям, что они могут проверять вашу подпись и отправлять вам зашифрованные сообщения:
gpg --send-key 0x726724204C644D83

Отзыв старых идентификаторов пользователя

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

Чтобы сделать это с моим старым ключом, я воспользуюсь инструкциями из списка рассылки gnupg.

При поиске старого ключа всё выглядит по-прежнему:
pub 1024D/D449E59A 2007-07-20 Andrew Beekhof <beekhof@mac.com>
                              Andrew Beekhof <abeekhof@suse.de>
                              Andrew Beekhof <beekhof@gmail.com>
                              Andrew Beekhof <andrew@beekhof.net>
                              Andrew Beekhof <abeekhof@novell.com>
        Fingerprint=E5F5 BEFC 781F 3637 774F C1F8 EC35 84EF D449 E59A
Но если заглянуть в детали ключа, можно увидеть, что адреса в Novell/SuSE в данное время отмечены красными символами revok.
pub 1024D/D449E59A 2007-07-20
        Fingerprint=E5F5 BEFC 781F 3637 774F C1F8 EC35 84EF D449 E59A

uid Andrew Beekhof <beekhof@mac.com>
sig  sig3 D449E59A 2007-07-20 __________ __________ [selfsig]

uid Andrew Beekhof <abeekhof@suse.de>
sig  sig3 D449E59A 2007-07-20 __________ __________ [selfsig]
sig revok D449E59A 2013-06-24 __________ __________ [selfsig]
...
Таким образом другие люди, имеющие копию gpg, узнают, что больше не нужно использовать этот адрес. Вот почему важно периодически обновлять свои ключи.

Отзыв старых ключей

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

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

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

Осиротевшие ключи, которые не могут быть отозваны, продолжают оставаться действующими для любого, кто захочет отправить вам защищённое сообщение - хорошая причина задать более раннюю дату окончания действия ключа!

Вот как выглядит один из моих старых отозванных ключей:
pub 1024D/DABA170E 2004-10-11 *** KEY REVOKED *** [not verified]
                              Andrew Beekhof (SuSE VPN Access) <andrew@beekhof.net>
        Fingerprint=9A53 9DBB CF73 AB8F B57B 730A 3279 4AE9 DABA 170E

Конечный результат

Мой новый ключ:
pub 4096R/4C644D83 2013-06-24 Andrew Beekhof <andrew@beekhof.net>
                              Andrew Beekhof <beekhof@mac.com>
                              Andrew Beekhof <abeekhof@redhat.com>
Fingerprint=C503 7BA2 D013 6342 44C0 122C 7267 2420 4C64 4D83

Заключение

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

воскресенье, 20 августа 2017 г.

Заметки о Dockerfile

Перевод: Dockerfile Notes
Автор: Соня Гамильтон (Sonia Hamilton)

Примечания переводчика:
Искал однажды что-то в интернете и вышел на блог этой авторши. Сейчас уже не помню почему, добавил этот материал в список того, что было бы неплохо перевести. Сейчас смотрю и недоумеваю - зачем мне это понадобилось? Ничего лучше фразы "чтоб было" в голову не пришло. Ну пусть будет.
Несколько заметок о Dockerfile'ах Docker.

Команды

  • MAINTAINER - автор, адрес электронной почты и т.п.
  • FROM - базовый образ, например Ubuntu.
  • CMD и ENTRYPOINT. Любая из них по отдельности может быть использована для указания исполняемого файла по умолчанию. Если используются обе, то CMD добавляется к ENTRYPOINT для получения командной строки контейнера. Любая из них может быть заменена при запуске docker. Однако CMD легче - она просто берёт остаток команды запуска docker (как "$*" в сценариях оболочки) - разрешая образу docker почувствовать себя в роли выполняемого файла, запущенного через "обёртку".
  • RUN - установочные команды. В режиме разработки разделите команды RUN, так что слои кэшировались и собирались быстрее. В рабочем режиме лучше воспользоваться цепочкой команд RUN, разделённых знаками && или точкой с запятой, поскольку несколько отдельных команд RUN создают несколько слоёв.
  • COPY и ADD. COPY копирует файлы в образ. ADD делает то же самое, но также может разархивировать файл или скачать его по ссылке. Избегайте использования команды ADD, поскольку она обладает излишне сложным поведением.
  • VOLUME - постоянный каталог на родительской файловой системе внутри /var/lib/docker/volumes. Для получения ожидаемого поведения (общий каталог для родительской системы и контейнера) воспользуйтесь командой docker run -v /var/tmp/foo:/var/tmp/foo.
  • ENV, WORKDIR и USER - переменные окружения, текущий каталог и идентификатор пользователя для команд CMD/ENTRYPOINT.
  • EXPOSE - открыть сетевой порт. Воспользуйтесь форматом nn, а не nn:mm, который позволяет пользователю указать публичный порт при помощи опции -p. Порт, открытый при помощи команды EXPOSE может быть автоматически отображён при помощи опции -P.

Сборка, запуск, выполнение

# Собрать образ и пометить его, использовать текущий каталог в качестве контекста
docker build -t="soniah/foo:1.0" .
# Запустить, автоматически отобразить порты. В режиме разработки опустите -D - стандартный поток
# вывода будет выводиться на экран, а контейнер автоматически остановится при нажатии Ctrl-C.
docker run -P sonia/foo
# Получить интерактивную оболочку
docker exec -it random_name bash
# Удалить все старые образы, за исключением базовых образов Ubuntu
docker rmi -f `docker images | tail -n +2 | grep -v 'ubuntu' | awk {'print $3'}`
# Удалить все контейнеры, включая остановленные контейнеры
docker rm -f `docker ps --no-trunc -aq`

Смотрите также

воскресенье, 13 августа 2017 г.

Карго-культ микросервисов

Перевод: The microservices cargo cult
Автор: Ставрос Корокитакис (Stavros Korokithakis)

"Всё сделано правильно. Форма совершенна. Он выглядит точно так же, как и раньше. Но он не работает."

Микросервисы - это чудо. Мы знаем это, потому что об этом говорят все последние истории успеха. Новости наполнены историями о том, как люди берут огромную монолитную систему, разбивают её, добавляют веб-API и наслаждаются всеми выгодами.

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

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

Карго-культы

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

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

Достоинства

Давайте начнём с рассмотрения достоинств.
  • Масштабируемость: Это главное. Поскольку микросервисы малы и самодостаточны, их можно разместить на выделенном сервере, если это будет необходимо. Можно поделить данные между экземплярами, распределять между ними нагрузку и настраивать любым способом, который имеет значение для вашего приложения.
    Вы также можете принять решение хранить данные в каждом из сервисов, воспользовавшись таким хранилищем данных (и даже языком программирования или технологией), которое лучше подходит в данном конкретном случае.
  • Более понятная архитектура: Каждый сервис имеет чётко определённые границы, которые обычно неприкосновенны. Больше нельзя обращаться к приватным данным лишь потому, что это удобно. Каждый сервис теперь скрывается за собственным API и не доступен иным образом.
  • Независимое развёртывание: Поскольку каждый сервис отделён, при необходимости его легко можно развернуть даже несколько раз за день. Это может способствовать увеличению времени бесперебойной работы.
  • Меньшая кодовая база: Поскольку код небольшой, в нём проще разобраться. Назначение каждого сервиса и его интерфейс чётко определены, что позволяет кому-нибудь быстрее прочитать код и разобраться в нём, а стало быть - быстрее изменять, расширять и поддерживать его.

Недостатки

  • Сложность: Сразу на порядок увеличивается количество работы, выполняемой серверами. Между ними появляются промежуточные прослойки, работающие через сеть, что на порядок увеличивает общее количество развёртываний и нагрузку на администраторов, а количество сервисов, требующих мониторинга, увеличивается во много десятков раз.
    Также появляется масса дополнительного кода, выполняющего сериализацию и десериализацию данных, передаваемых между сервисами. И хотя обычно его суммарный объём не велик, ничего хорошего в этом нет.
  • Накладные расходы: Все эти разнообразные хранилища данных, преобразования данных и сетевые вызовы не обходятся даром. Лично мне встречалось замедление порядка 1000% из-за перехода на микросервисы (да, в десять раз медленнее).
  • Сегрегация данных: Поскольку все данные теперь хранятся в разрозненных хранилищах данных, нужно следить за их согласованностью. Если в монолитной системе можно было обойтись простым каскадным удалением, то теперь нужна сложная симфония зависимостей, вызовов и перепроверок. Это очень похоже на использование не реляционного хранилища данных. Если у вас есть опыт работы не реляционными хранилищами и вы успешно с ними справлялись, то проблем возникнуть не должно.

Критическая оценка

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

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

Стоит ли вам использовать микросервисы?

Из описанного выше следует, что небольшие проекты не смогут извлечь пользы из микросервисной архитектуры. Однако, они всё же могут извлечь пользу из более чёткой архитектуры и меньшей кодовой базы, верно?

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

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

Простой способ принятия решения

Чтобы помочь вам сориентироваться в лабиринте вопросов и доводов при принятии решения - нужны ли вам микросервисы, я создал соответствующую диаграмму. Вот она:

Диаграмма для принятия решений, нужны ли вам микросервисы

Эта диаграмма ответит на все ваши вопросы о микросервисах.

Это немного щекотно.

Заключение

Не начинайте с микросервисов, это сложный способ, который вначале не требуется.

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

Истинно глаголю вам: не начинайте с микросервисов лишь потому что так делают крутые парни. Это сложный подход, который поначалу не нужен. Наслаждайтесь простотой разработки и гибкостью развёртывания, которые даёт одно приложение. Когда ваш бизнес окрепнет и вырастет, и вы не сможете найти достаточно мощный сервер для приложения, только тогда выделяйте части из общей инфраструктуры в отдельные сервисы, соединяя их через HTTP или через очередь сообщений.

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

Примечания переводчика
Сам довольно скептически отношусь к идее микросервисов, по многим причинам.

Во-первых, идея микросервисов основывается на веб-технологиях, но программирование не ограничивается одним только вебом, а за пределами веба имеется более общий подход, который можно назвать "распределённые системы".

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

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

воскресенье, 6 августа 2017 г.

Bottle - полный веб-стек без Django

Перевод: Bottle, full stack without Django
Автор: Тьяго Авелино (Thiago Avelino)

Эта заметка в блоге основана на лекции, которую я прочитал здесь, в Бразилии. Посмотрите слайды!

Веб-микрофреймворк Bottle

Bottle - это веб-микрофреймворк, совместимый с WSGI, который зависит только от стандартной библиотеки Python и совместим с Python версий 2.6, 2.7, 3.2, 3.3 и 3.4. Весь исходный текст фреймворка умещается в одном файле. Он был создан Марселем Хеллкэмпом (Marcel Hellkamp - @defnull) и поддерживается сообществом, образовавшимся вокруг этого фреймворка.

Django - это прагматичный фреймворк для быстрой веб-разработки, который написан на Python, использует стандарт MTV (model-template-view - модель-шаблон-представление). Изначально он был создан как система для управления сайтом журнала в городе Лоуренс, в Канзасе. Стал проектом с открытым исходным кодом, был опубликован под лицензией BSD в 2005 году. Название Django фреймворк получил в честь джазового музыканта Джанго Рейнхардта. Django стал очень известен благодаря поставке с "батарейкам", то есть благодаря нескольким библиотекам, добавленным к основному коду фреймворка для упрощения разработки. Вместе с фреймворком эти библиотеки сформировали так называемый "полный стек".

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

Хорошо ли, если вместе с веб-фреймворком поставляются "батарейки"? Если вы согласны использовать всё, что даёт вам фреймворк, то ответ - да. Однако, не все веб-проекты одинаковы.

В большинстве проектов используется не более 80% возможностей Django. В тех случаях, когда используется не более 50% возможностей, мы вынуждены платить за все, что было предусмотрено архитектурой Django. То есть теряем в производительности, потому что в Django имеется множество модулей, которые не используются, но по-прежнему продолжают работать. Когда же мы используем микрофреймворк, мы берём на себя роль архитектора приложения. Поскольку у нас нет предопределённой архитектуры, нам нужно время, чтобы определить архитектуру приложения.

Все пакеты Python, которые имеются в библиотеке Django, в микрофреймворке могут быть заменены!

SQLAlchemy

SQLAlchemy существовал до Django (да, до Django) и начиная с 2005 года появилась команда, которая стала заниматься разработкой ORM. Команда же разработчиков Django занимается одновременно и разработкой фреймворка и разработкой ORM. Я думаю, что не стоит говорить о том, что результат работы специалистов обычно бывает лучше, чем результат работы универсалов.

Структура модели:
class Entity(Base):
    __tablename__ = 'entity'
    id = Column(Integer, Sequence('id_seq'), primary_key=True)
    name = Column(String(50))

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return "<Entity('%d', '%s')>" % (self.id, self.name)

WTForms

Обходное решение для тех, кто не использует Django и кому нужно работать с формами - это WTForms, которые были созданы в 2008 году и поддержка которых продолжается до сих пор!

Структура формы:
class UserForm(Form):
    name = TextField(validators=[DataRequired(), Length(max=100)])
    email = TextField(validators=[DataRequired(), Length(max=255)])

Шаблонизатор

Jinja2 - это современный удобный для дизайнеров язык шаблонизации для Python, который был создан по образцу шаблонов Django. Он быстрый, широко используется и может быть дополнительно защищён изоляцией рабочего окружения.

Структура шаблона:
<title>{% block title %}{% endblock %}</title>
<ul>
{% for user in users %}
  <li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>

Миграция

Использование Alembic начинается с создания среды миграции. Это каталог сценариев, которые относятся к отдельному приложению. Среда миграции создаётся единожды, а затем поддерживается совместно с исходным кодом самого приложения.

Структура миграции:
revision = '1975ea83b712'
down_revision = None

from alembic import op
import sqlalchemy as sa

def upgrade():
    pass

def downgrade():
    pass
Как создать обновление и откат:
def upgrade():
    op.create_table(
        'account',
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('name', sa.String(50), nullable=False),
        sa.Column('description', sa.Unicode(200)),
    )

def downgrade():
    op.drop_table('account')
Структура модификации таблицы:
"""
$ alembic revision -m "Add a column"
"""

revision = 'ae1027a6acf'
down_revision = '1975ea83b712'

from alembic import op
import sqlalchemy as sa

def upgrade():
    op.add_column('account', sa.Column('last_transaction_date', sa.DateTime))

def downgrade():
    op.drop_column('account', 'last_transaction_date')

Заключение

Здесь было продемонстрировано всё, что можно найти в стеке Django. Я писал эту заметку для не для того, чтобы принизить Django. Я лишь показал, что существуют другие полностековые решения для разработки приложений. Многие люди используют Django, не понимая экосистемы Python. В наше время Django предоставляет множество готовых решений, что заставляет некоторых разработчиков лениться и не наращивать мастерство в проектировании архитектуры приложения.

Помогайте Bottle. Мы - растущее сообщество. Чтобы внести свой вклад в код Bottle, обратитесь к списку открытых задач. В случае сомнений можно обратиться в список рассылки или в IRC-канал.

ПРИСОЕДИНЯЙТЕСЬ

Примечания переводчика:
Считаю, что в статье тема не раскрыта. Не понятно, какая такая особенная архитектура приложения имелась в виду,
которая никак не сочетается с Django. Перечислено несколько средств, которые в чём-то заменяют средства, имеющиеся в Django. Без сомнений, SQLAlchemy заткнёт за пояс джанговский ORM. Безусловно, Jinja2 - очень быстрый шаблонизатор, быстрее джанговского. Но стоит иметь в виду, что в Django все эти средства глубоко интегрированы друг с другом.

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

Другой пример - в Django миграции можно сгенерировать автоматически. Автоматическая генерация даже умеет определять переименованные поля. А если же автоматика не сработала корректно, то автоматически сгенерированный код миграции можно подправить и руками.

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

Я сам в своих проектах использую Django, но для некоторых задач он действительно избыточен. Например, в одной из прошлых своих заметок я писал Тайловый сервер на основе Python, Mapnik и Bottle. Денис Рыков, материалами которого я воспользовался, тоже писал тайловый сервер, воспользовавшись фреймворком Bottle. Для себя я решил, что если приложение не работает с собственной базой данных, а берёт информацию из сторонних источников и занимается лишь её преобразованием, или выполняет какие-то действия по требованию, то лучше использовать Bottle, т.к. в нём нет избыточных возможностей, а сам он обладает минимумом зависимостей и работает с любой версией Python.

воскресенье, 30 июля 2017 г.

Посторение высокоуровневого API запросов: правильный способ использования ORM Django

Перевод: Building a higher-level query API: the right way to use Django's ORM
Автор: Джэми Мэтьюз (Jamie Matthews)

Примечания переводчика
Поначалу может показаться, что в этой статье рассматривается ещё один способ избавления от "магических чисел", который был рассмотрен в статье Правильная обработка choices в полях моделей Django. В конце перевода я покажу, чем это решение отличается от уже рассмотренного и как его можно применить в реальных проектах.
Эта статья основана на обсуждении в группе пользователей Python города Брайтон (Brighton Python User Group) 10 апреля 2012 года.

Аннотация

В этой статье я хочу показать, что использование низкоуровневых методов запросов ORM Django (filter, order_by и т.п.) прямо в представлении обычно является плохой практикой. Вместо этого лучше построить собственный проблемно-ориентированный API запросов на уровне модели, которой принадлежит бизнес-логика. Сделать это в Django не особо просто, но глубоко погрузившись во внутренности ORM, всё же можно найти для этого несколько приемлемых способов.

Обзор

При написании приложений Django мы привыкли добавлять методы к моделям для изоляции бизнес-логики и сокрытия деталей реализации. Этот подход кажется совершенно естественным и он действительно свободно используется во встроенных приложениях Django:
>>> from django.contrib.auth.models import User
>>> user = User.objects.get(pk=5)
>>> user.set_password('super-sekrit')
>>> user.save()
Здесь set_password - это метод, определённый в модели django.contrib.auth.models.User, который скрывает детали реализации хэширования пароля. В наглядном виде этот код выглядит примерно следующим образом:
from django.contrib.auth.hashers import make_password

class User(models.Model):

    # здесь находятся поля модели...

    def set_password(self, raw_password):
        self.password = make_password(raw_password)
Мы построили проблемно-ориентированный API поверх инструментов для обобщённого низкоуровневого объектно-реляционного отображения, которые предоставляются Django. Это основа проблемного-ориентированного подхода: мы увеличиваем количество уровней абстракции, делая менее явным код, взаимодействующий с API. В результате код получается более устойчивым, пригодным для повторного использования и (самое важное) более наглядным.

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

Учебное приложение: список дел

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

Вот файл models.py из нашего приложения:
from django.db import models

PRIORITY_CHOICES = [(1, 'High'), (2, 'Low')]

class Todo(models.Model):
    content = models.CharField(max_length=100)
    is_done = models.BooleanField(default=False)
    owner = models.ForeignKey('auth.User')
    priority = models.IntegerField(choices=PRIORITY_CHOICES,
                                   default=1)
Теперь давайте посмотрим, какие запросы к этим данным мы могли бы выполнить. Допустим, мы создаём представление для просмотра списка дел из нашего приложения. Мы хотим отобразить все незавершённые дела с высоким приоритетом, существующие в настоящее время у вошедшего пользователя. Вот наш первоначальный вариант кода:
def dashboard(request):

    todos = Todo.objects.filter(
        owner=request.user
    ).filter(
        is_done=False
    ).filter(
        priority=1
    )

    return render(request, 'todos/list.html', {
        'todos': todos,
    })
Да, я знаю, что этом можно записать проще: request.user.todo_set.filter(is_done=False, priority=1). Но напоминаю: это просто пример!

Почему это плохо?

  • Во-первых, это многословно. Семь строк (в зависимости от того, как вы предпочитаете расставлять переводы строк в цепочках вызовов методов) просто чтобы получить интересующие нас строки. Конечно, это всего-лишь пример. В настоящем приложении код обращения к ORM может быть гораздо более сложным.
  • Здесь наблюдается утечка деталей реализации. Код, который взаимодействует с моделью, должен знать, что здесь существует свойство по имени is_done и это BooleanField. Если вы поменяете реализацию (возможно вам захочется заменить булево поле is_done на поле статуса, которое может принимать одно из нескольких значений), то код сломается.
  • Он не прозрачен - его смысл не понятен с первого взгляда.
  • Наконец, потенциально он будет повторяться. Представьте, что появилось новое требование: написать управляющую команду, которая будет вызываться через планировщик задач каждую неделю и отсылать всем пользователям список незавершённых дел с высоким приоритетом. Вам придётся скопировать и вставить эти семь строк в новый скрипт. Это не соответствует принципу DRY - Do not repeat yourself - не повторяйся.
Давайте подведём итоги: использование низкоуровневых методов ORM прямо в представлении обычно является плохой практикой.

Ну хорошо, а как же это можно улучшить?

Менеджеры запросов и объекты запросов

Перед рассмотрением вариантов решений ненадолго отвлечёмся, чтобы пояснить некоторые понятия.

В Django есть две тесно связанные конструкции, относящиеся к операциям над таблицами: менеджеры запросов и объекты запросов.

Менеджер запросов (экземпляр django.db.models.manager.Manager) описывается как "интерфейс, через который осуществляются операции с моделями Django в базе данных". Менеджер модели - это шлюз к функциональности ORM для доступа к таблицам (экземпляры моделей обычно предоставляют функциональность для доступа к одной строке таблицы). Каждый класс модели предоставляет менеджер по умолчанию, который называется objects.

Объект запроса (django.db.models.query.QuerySet) представляет "коллекцию объектов из базы данных". Это абстракция с отложенным выполнением вычисленного запроса SELECT. Эта абстракция может быть отфильтрована, упорядочена и использована для ограничения или изменения набора строк, который она представляет. Она отвечает за создание и манипулирование экземплярами django.db.models.sql.query.Query, которые преобразуются в настоящие SQL-запросы к одному из поддерживаемых типов нижележащих баз данных.

Уф. Запутались? Хотя разницу между менеджерами запросов и объектами запросов легко объяснить тем, кто хорошо знаком со внутренностями ORM, она не кажется очевидной, особенно новичкам.

Эта путаница усугубляется тем, что знакомый API менеджеров на самом деле немного не такой, каким кажется...

API менеджера - это обман

Методы объектов запросов можно объединять в цепочки. Каждый вызов метода объекта запроса (например filter) возвращает клонированную версию исходного объекта запроса, готового к вызову другого метода. Этот естественный интерфейс - часть прекрасного ORM Django.

Но на самом деле Model.objects - это менеджер запросов (а не объект запроса), что создаёт проблемы: нам нужно начать нашу цепочку методов вызовов с objects, но продолжение цепочки даст в результате объект запроса.

И как же эта проблема решается в коде самого Django? Итак, обман API объясняется: все методы объекта запроса повторно реализуются в менеджере запросов. Версии этих методов из менеджера запросов просто транслируются в только что созданный объект запроса через self.get_query_set():
class Manager(object):

    # Пропускаем служебные вещи...

    def get_query_set(self):
        return QuerySet(self.model, using=self._db)

    def all(self):
        return self.get_query_set()

    def count(self):
        return self.get_query_set().count()

    def filter(self, *args, **kwargs):
        return self.get_query_set().filter(*args, **kwargs)

    # и так далее сто с лишним строк...
Чтобы увидеть весь этот ужас, загляните в исходный код класса Manager.

Мы скоро вернёмся к этому хитромудрому API...

Возвращаемся к списку дел

И так, давайте вернёмся к решению нашей проблемы прояснения нашего беспорядочного API запросов. В документации Django рекомендуется следующий подход: определить собственные подклассы Manager и присоединить их к нужным моделям.

Можно даже добавить в модель несколько дополнительных менеджеров или можно переопределить objects, оставив один менеджер, но добавив к нему собственные методы.

Давайте попробуем каждый из этих подходов в приложении со списком дел.

Подход 1: несколько собственных менеджеров

class IncompleteTodoManager(models.Manager):
    def get_query_set(self):
        return super(TodoManager, self).get_query_set().filter(is_done=False)

class HighPriorityTodoManager(models.Manager):
    def get_query_set(self):
        return super(TodoManager, self).get_query_set().filter(priority=1)

class Todo(models.Model):
    content = models.CharField(max_length=100)
    # здесь следуют прочие поля...

    objects = models.Manager() # менеджер по умолчанию

    # присоединяем собственные менеджеры:
    incomplete = models.IncompleteTodoManager()
    high_priority = models.HighPriorityTodoManager()
Реализованное здесь API выглядит следующим образом:
>>> Todo.incomplete.all()
>>> Todo.high_priority.all()
К несчастью, этот подход порождает несколько больших проблем.
  • Реализация очень многословная. Нужно определить отдельный класс для каждого кусочка функциональности.
  • Создаётся беспорядок в пространстве имён модели. Разработчики Django привыкли использовать Model.objects в качестве шлюза к таблицам. Это пространство имён, под которым собираются все операции на уровне таблиц. Было бы неприятно расстаться с этим чётким соглашением.
  • Настоящая беда: нельзя пользоваться цепочками фильтров. Нельзя комбинировать менеджеров: для получения незавершённых и высокоприоритетных дел нужно вернуться к низкоуровневому коду ORM: воспользоваться либо Todo.incomplete.filter(priority=1), либо Todo.high_priority.filter(is_done=False).
Я думаю, что минусы полностью перевешивают достоинства этого подхода и создание нескольких менеджеров модели в большинстве случаев является плохой практикой.

Подход 2: методы менеджера

Что ж, давайте попробуем другой подход, разрешённый в Django: собственный менеджер с несколькими методами.
class TodoManager(models.Manager):
    def incomplete(self):
        return self.filter(is_done=False)

    def high_priority(self):
        return self.filter(priority=1)

class Todo(models.Model):
    content = models.CharField(max_length=100)
    # здесь следуют прочие поля...

    objects = TodoManager()
Теперь наш API выглядит следующим образом:
>>> Todo.objects.incomplete()
>>> Todo.objects.high_priority()
Гораздо лучше. Здесь меньше кода (есть только одно определение класса) и методы запроса размещаются в пространстве имён внутри менеджера objects.

Однако, такие запросы по-прежнему нельзя объединять в цепочки. Todo.objects.incomplete() вернёт обычный объект запроса, поэтому нельзя написать Todo.objects.incomplete().high_priority(). Нам по-прежнему придётся писать Todo.objects.incomplete().filter(is_done=False). Не годится.

Подход 3: собственный объект запроса

Теперь мы на неизведанной территории. Этого нельзя найти в документации Django...
class TodoQuerySet(models.query.QuerySet):
    def incomplete(self):
        return self.filter(is_done=False)

    def high_priority(self):
        return self.filter(priority=1)

class TodoManager(models.Manager):
    def get_query_set(self):
        return TodoQuerySet(self.model, using=self._db)

class Todo(models.Model):
    content = models.CharField(max_length=100)
    # здесь следуют прочие поля...

    objects = TodoManager()
Вот как это выглядит при использовании:
>>> Todo.objects.get_query_set().incomplete()
>>> Todo.objects.get_query_set().high_priority()
>>> # или
>>> Todo.objects.all().incomplete()
>>> Todo.objects.all().high_priority()
Мы почти на месте! Здесь не больше кода, чем в подходе 2, имеются те же достоинства, а кроме того (барабанная дробь) - можно использовать цепочки!
>>> Todo.objects.all().incomplete().high_priority()
Однако, совершенство ещё не достигнуто. Собственный менеджер -
ничего более, чем шаблонная заготовка. И этот all() выглядит как нарост, который надоедает набирать. Но важнее то, что он всё запутывает - из-за него код выглядит странно.

Подход 3a: копируем Django, транслируем всё

Теперь пригодится наше обсуждение "API менеджера - это обман": мы знаем, как исправить эту проблему. Мы просто переопределим все методы из объекта запросов в нашем менеджере, транслируя их обратно в наш объект запросов:
class TodoQuerySet(models.query.QuerySet):
    def incomplete(self):
        return self.filter(is_done=False)

    def high_priority(self):
        return self.filter(priority=1)

class TodoManager(models.Manager):
    def get_query_set(self):
        return TodoQuerySet(self.model, using=self._db)

    def incomplete(self):
        return self.get_query_set().incomplete()

    def high_priority(self):
        return self.get_query_set().high_priority()
Мы получаем в точности тот API, какой нам и нужен:
>>> Todo.objects.incomplete().high_priority() # Ура!
Однако код получился многословным и он не соответствует принципу DRY. Каждый раз, когда нужно добавить новый метод в объект запросов или поменять сигнатуру существующего метода, нужно не забыть сделать такие же изменения в менеджере. В противном случае этот метод не будет работать правильно. Похоже на источник будущих проблем.

Подход 3b: django-model-utils

Python - динамический язык. Можно ли избежать повторения шаблонных заготовок? Это возможно при помощи небольшого стороннего приложения, которое называется django-model-utils. Просто запустите pip install django-model-utils, а затем...
from model_utils.managers import PassThroughManager

class TodoQuerySet(models.query.QuerySet):
    def incomplete(self):
        return self.filter(is_done=False)

    def high_priority(self):
        return self.filter(priority=1)

    class Todo(models.Model):
        content = models.CharField(max_length=100)
        # здесь следуют прочие поля...

        objects = PassThroughManager.for_queryset_class(TodoQuerySet)()
Вот так намного приятнее. Мы просто определили наш собственный подкласс объекта запросов, как и в прошлый раз, и добавили его в нашу модель через класс PassThroughManager, который имеется в django-model-utils.

PassThroughManager работает благодаря реализации метода __getattr__, который перехватывает вызовы несуществующих методов и автоматически передаёт их в объект запроса. Он выполняет несколько проверок для предотвращения бесконечной рекурсии при обращении к некоторым свойствам. Именно поэтому я рекомендую использовать испытанную реализацию, предоставляемую django-model-utils, а не пытаться накропать её собственноручно.

Чем это поможет?

Помните код представления, который был приведён выше?
def dashboard(request):

    todos = Todo.objects.filter(
        owner=request.user
    ).filter(
        is_done=False
    ).filter(
        priority=1
    )

    return render(request, 'todos/list.html', {
        'todos': todos,
    })
После небольшой доработки его можно привести к следующему виду:
def dashboard(request):
    todos = Todo.objects.for_user(
        request.user
    ).incomplete().high_priority()

    return render(request, 'todos/list.html', {
        'todos': todos,
    })
Я думаю вы согласитесь, что эта вторая версия намного проще и нагляднее, чем первая?

Может ли помочь Django?

В списке рассылки django-dev обсуждались способы упростить решение рассмотренной проблемы. По итогам обсуждения была заведена заявка. Захари Воз (Zachary Voase) предложил следующее:
class TodoManager(models.Manager):

    @models.querymethod
    def incomplete(query):
        return query.filter(is_done=False)
Это определение задекорированного метода incomplete может волшебным образом сделать его доступным сразу в менеджере и объекте запросов.

Лично я не совсем согласен с идеей использования декоратора. Он слегка затуманивает детали и не выглядит изящным. Я нутром чую, что добавление методов в подкласс объектов запросов (а не в подкласс менеджера) - это лучший, более простой подход.

Можно было бы пойти и дальше. Вернувшись назад и заново продумав проектные решения API Django с нуля, может быть удалось бы найти настоящие, более глубокие улучшения. Можно ли стереть различия между менеджерами и объектами запросов? Или по крайней мере прояснить эти различия?

Я убеждён, что если бы была предпринята такая переработка, то она должна была бы появиться в Django 2.0 или в последующих версиях.

Итак, напомним:

Использование низкоуровневого кода запросов ORM в представлениях и других высокоуровневых частях приложения обычно является плохой практикой. Вместо этого, создав собственное API объектов запросов и присоединив его к модели при помощи PassThroughManager из django-model-utils, мы получаем следующие выгоды:
  • Код получается более компактным и устойчивым.
  • Уменьшаются повторы, увеличивается уровень абстракции.
  • Бизнес-логика помещается на уровень модели, к которой она и принадлежит.
Благодарю за чтение!

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

Дополнение переводчика
Теперь я покажу обещанный мной способ использования описанных в этой статье идей на примере реального проекта. Собственно, эту статью я нашел именно потому, что мне был нужен способ добавить собственный метод в объект запроса. Этот пример хорош ещё и потому, что в нём одновременно используются идеи из прошлой статьи Правильная обработка choices в полях моделей Django и из этой статьи.

Имеется модель, которая содержит настройки менеджера SNMP. Напоминаю, что менеджер - это программа, которая занимается опросом оборудования по SNMP. Настройки на оборудовании - это настройки агента SNMP. Настройки агента сложнее, потому что содержат список сообществ и пользователей, имеющих доступ к оборудованию по SNMP, тип доступа - только чтение или чтение-запись, и ветку дерева OID'ов, к которой относится описываемый доступ - так называемые SNMP View. Так вот, сейчас мы рассматриваем только настройки менеджера SNMP.

Модель так и называется - SNMP и первоначально описывается следующим образом:
class SNMP(models.Model):
    VERSION_1 = 1
    VERSION_2C = 2
    VERSION_3 = 3
    VERSION = (
        (VERSION_1, 'SNMPv1'),
        (VERSION_2C, 'SNMPv2c'),
        (VERSION_3, 'SNMPv3'),
    )

    V3_SECURITY_LEVEL_NO_AUTH_NO_PRIV = 0
    V3_SECURITY_LEVEL_AUTH_NO_PRIV = 1
    V3_SECURITY_LEVEL_AUTH_PRIV = 2
    V3_SECURITY_LEVEL = (
        (V3_SECURITY_LEVEL_NO_AUTH_NO_PRIV, 'noAuthNoPriv'),
        (V3_SECURITY_LEVEL_AUTH_NO_PRIV, 'authNoPriv'),
        (V3_SECURITY_LEVEL_AUTH_PRIV, 'authPriv'),
    )

    V3_AUTH_PROTOCOL_MD5 = 0
    V3_AUTH_PROTOCOL_SHA = 1
    V3_AUTH_PROTOCOL = (
        (V3_AUTH_PROTOCOL_MD5, 'MD5'),
        (V3_AUTH_PROTOCOL_SHA, 'SHA'),
    )

    V3_PRIV_PROTOCOL_DES = 0
    V3_PRIV_PROTOCOL_AES = 1
    V3_PRIV_PROTOCOL = (
        (V3_PRIV_PROTOCOL_DES, 'DES'),
        (V3_PRIV_PROTOCOL_AES, 'AES'),
    )

    DEFAULT_PORT = 161

    snmp_port = models.IntegerField(u'SNMP-порт', blank=True, default=DEFAULT_PORT)
    snmp_version = models.PositiveIntegerField(u'Версия SNMP', choices=VERSION, default=VERSION_1)
    snmp_community = models.CharField(u'SNMP-сообщество', max_length=255, blank=True, default='')
    snmpv3_contextname = models.CharField(u'SNMP-контекст безопасности', max_length=255, blank=True, default='')
    snmpv3_securityname = models.CharField(u'SNMP-имя безопасности', max_length=255, blank=True, default='')
    snmpv3_securitylevel = models.PositiveIntegerField(u'SNMP-уровень безопасности', choices=V3_SECURITY_LEVEL, default=V3_SECURITY_LEVEL_NO_AUTH_NO_PRIV)
    snmpv3_authprotocol = models.PositiveIntegerField(u'SNMP-протокол аутентификации', choices=V3_AUTH_PROTOCOL, default=V3_AUTH_PROTOCOL_MD5)
    snmpv3_authpassphrase = models.CharField(u'SNMP-пароль аутентификации', max_length=255, blank=True, default='')
    snmpv3_privprotocol = models.PositiveIntegerField(u'SNMP-протокол безопасности', choices=V3_PRIV_PROTOCOL, default=V3_PRIV_PROTOCOL_DES) 
    snmpv3_privpassphrase = models.CharField(u'SNMP-пароль безопасности', max_length=255, blank=True, default='')

    class Meta:
        unique_together = (('snmp_port', 'snmp_version', 'snmp_community', 'snmpv3_contextname', 'snmpv3_securityname', 'snmpv3_securitylevel', 'snmpv3_authprotocol', 'snmpv3_authpassphrase', 'snmpv3_privprotocol', 'snmpv3_privpassphrase'),)
        verbose_name = u'Настройки SNMP'
        verbose_name_plural = u'Настройки SNMP'
Все остальные поля, не имеющие отношения к рассматриваемому примеру, были пропущены.

Суть в том, что в зависимости от версии SNMP используются настройки, хранящиеся в разных полях. В случае SNMP версий 1 и 2c используется только поле snmp_community. В случае третьей версии SNMP как минимум используются ещё и поля snmpv3_contextname, snmpv3_securityname и snmpv3_securitylevel.

В зависимости от значения поля snmpv3_securitylevel могут использоваться ещё 4 поля. Если snmpv3_securitylevel соответствует noAuthNoPriv, то дополнительные поля не используются. Если snmpv3_securitylevel соответствует authNoPriv, то дополнительно используются поля snmpv3_authprotocol и snmpv3_authpassphrase. Если snmpv3_securitylevel соответствует authPriv, то используются поля snmpv3_authprotocol, snmpv3_authpassphrase, а так же snmpv3_privprotocol и snmpv3_privpassphrase.

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

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

Можно немного упростить задачу и воспользоваться решением, описанным в рассматриваемой статье - сделать у объекта SNMP ещё один метод, который будет оставлять в выборке из таблицы только те объекты, которые соответствуют эталонному объекту SNMP. В моём случае это решение располагается перед описанием модели SNMP и выглядит следующим образом:
class SNMPQuerySet(models.query.QuerySet):
    def like(self, snmp):
        qs = self.filter(snmp_port=snmp.snmp_port,
                         snmp_version=snmp.snmp_version)

        if snmp.snmp_version in (self.model.VERSION_1, self.model.VERSION_2C):
            qs = qs.filter(snmp_community=snmp.snmp_community)

        elif snmp.snmp_version == self.model.VERSION_3:
            qs = qs.filter(snmpv3_contextname=snmp.snmpv3_contextname,
                           snmpv3_securitylevel=snmp.snmpv3_securitylevel)

            if snmp.snmpv3_securitylevel == self.model.V3_SECURITY_LEVEL_AUTH_NO_PRIV:
                qs = qs.filter(snmpv3_authprotocol=snmp.snmpv3_authprotocol,
                               snmpv3_authpassphrase=snmp.snmpv3_authpassphrase)
            elif snmp.snmpv3_securitylevel == self.model.V3_SECURITY_LEVEL_AUTH_PRIV:
                qs = qs.filter(snmpv3_authprotocol=snmp.snmpv3_authprotocol,
                               snmpv3_authpassphrase=snmp.snmpv3_authpassphrase,
                               snmpv3_privprotocol=snmp.snmpv3_privprotocol,
                               snmpv3_privpassphrase=snmp.snmpv3_privpassphrase)
        return qs

class SNMPManager(models.Manager):
    def get_queryset(self):
        return SNMPQuerySet(self.model, using=self._db)

    def like(self, snmp):
        return self.get_queryset().like(snmp)
Теперь остаётся только добавить менеджер запросов SNMPManager в качестве менеджера по умолчанию в модель SNMP. Добавим перед строчкой "class Meta:" всего одну строку:
objects = SNMPManager()
После описанной доработки модели, можно действовать следующим образом:
# Получаем настройки SNMP от пользователя
user_snmp_settings = ...

# Ищем подобные настройки в таблице
found_snmp_settings = SNMP.objects.like(user_snmp_settings).first()

# Если настройки уже есть, используем их в последующих операциях
if found_snmp_settings:
    user_snmp_settings = found_snmp_settings
# В противном случае добавляем в таблицу настройки, полученные от пользователя
else:
    user_snmp_settings.save()

# Дальше используем user_snmp_settings
...
Стоит ли пользоваться этим методом или можно решить эту задачу изящнее каким-то другим способом - решать вам. Я хотел лишь сообщить о существовании подобного приёма и описать, как им можно воспользоваться.