воскресенье, 28 апреля 2013 г.

VPN-подключение к Уфанет с помощью xl2tpd

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

Настройка в общих чертах совпадает с описанной в VPN-подключение к Уфанет и локальные ресурсы через Ethernet.

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

Для начала установим необходимые пакеты:
# apt-get install resolvconf iproute bind9-host
Забегая вперёд, хочу сказать, что в процессе настройки xl2tpd я столкнулся с одной проблемой - значение опции ipparam, настроенное в файле /etc/ppp/options.l2tp, игнорировалось им и вместо него подставлялся IP-адрес VPN-сервера 10.8.0.1.

Можно понять, почему так происходит, если посмотреть вывод команды:
# systemctl status xl2tpd.service
В списке опций pppd опция ipparam с IP-адресом идёт после опции file, поэтому перекрывает определённое в файле опций. Это видно по строчке:
/usr/sbin/pppd passive nodetach : name 111111 debug file /etc/ppp/options.l2tp ipparam 10.8.0.1 /dev/pts/0
Поиски подобных проблем в интернете приводят на страницу: обновил xl2tpd пропал ipparam.

Чтобы посмотреть на пресловутый кусок кода, заходим на сайт компании-разработчика xl2tpd: http://xelerance.com, переходим в разделы меню Services -> Other Software -> xl2tpd и попадаем на страницу http://www.xelerance.com/services/software/xl2tpd/, где сообщается, что проект располагается по адресу https://github.com/xelerance/xl2tpd.

Если посмотреть исходники файла control.c, то можно увидеть несколько фрагментов, похожих на этот:
if (c->lac->pppoptfile[0])
            {
                po = add_opt (po, "file");
                po = add_opt (po, c->lac->pppoptfile);
            }
        };
        if (c->lac->pass_peer)
        {
            po = add_opt (po, "ipparam");
            po = add_opt (po, IPADDY (t->peer.sin_addr));
        }
Для того, чтобы значения опций из файла имели приоритет над определёнными автоматически, нужно переместить блок if так, чтобы он обрабатывался раньше предыдущего блока (который оканчивается на точку с запятой, которые в этом случае излишни).

Чтобы исправить это, качаем исходники для сборки пакета:
# apt-get source xl2tpd
# vim xl2tpd-1.3.1+dfsg/control.c
Переносим пары строк, добавляющих опцию ipparam выше блоков, в которых добавляется опция file.

Готовим исходники для сборки своей версии deb-пакета:
# cd xl2tpd-1.3.1+dfsg/
# dch -i
Приводим верхнюю запись в changelog к следующему виду:
xl2tpd (1.3.1+dfsg-1.1) UNRELEASED; urgency=low

  * Non-maintainer upload.
  * Moved "ipparam" option detection before adding "file" option.

 -- Vladimir Stupin   Sat, 27 Apr 2013 13:35:45 +0600
И выходим из редактора.

Теперь осталось собрать и установить новый пакет (перед сборкой мне потребовалось поставить дополнительный пакет-зависимость):
# apt-get install libpcap0.8-dev
# dpkg-buildpackage -B -us -uc -rfakeroot
# cd ..
# dpkg -i xl2tpd_1.3.1+dfsg-1.1_i386.deb
Теперь всё готово для того, чтобы начать настройку. Скачать получившийся пакет можно здесь: http://stupin.su/files/xl2tpd_1.3.1_dfsg-1.1_i386.deb.

Не стесняясь самоплагиата, помещу сюда описание процедуры настройки интерфейса Ethernet, расстановку приоритетов DNS-серверов и настройки маршрутизации по политикам (полиси-роутинг).

2. Настроим сетевые интерфейсы

Настроим локальный петлевой интерфейс, Ethernet-интерфейс Уфанет, и автоматическое подключение внешнего VPN-соединения.

В файл /etc/network/interfaces пропишем следующие настройки:
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
Добавляем в файл конфигурации DHCP-клиента /etc/dhcp3/dhclient.conf новую секцию, связанную с интерфейсом eth0:
interface "eth0" {
  script "/root/bin/dhclient-ufanet";
}
Вам может понадобиться заменить в файлах инициализации интерфейс eth0 (выше он выделен жирным шрифтом) на тот, к которому подключен Ethernet-кабель Уфанет.

Копируем сценарий, выполняющийся по умолчанию, в каталог /root/bin/ под именем dhclient-ufanet:
# mkdir /root/bin/
# cp /sbin/dhclient-script /root/bin/dhclient-ufanet
Заменяем в скопированном сценарии строчки следующего вида в двух местах:
for router in $new_routers; do
    route add default dev $interface gw $router $metric_arg
done
на блок:
for router in $new_routers; do
  for dns in $new_domain_name_servers; do
    ip route add $dns/32 dev $interface via $router $metric_arg
    vpn=`host -t A pptp.ufanet.ru $dns | cut -d" " -f4`
    ip route add $vpn/32 dev $interface via $router $metric_arg
    ip route add default dev $interface src $new_ip_address via $router $metric_arg table lufanet
    ip rule add from $new_ip_address table lufanet
  done
done
3. Настроим приоритеты DNS-серверов

DNS-серверы на интерфейсе ppp должны быть приоритетнее DNS-серверов на интерфейсе Ethernet, поэтому в файле /etc/resolvconf/interface-order строчку ppp* поместим перед eth*.

Файл /etc/resolvconf/interface-order примет следующий вид:
# interface-order(5)
lo.inet*
lo.dnsmasq
lo.pdnsd
lo.!(pdns|pdns-recursor)
lo
tun*
tap*
ppp*
eth*
ath*
wlan*
*
4. Создадим таблицы маршрутизации

Добавим две таблицы с именами lunlim и iunlim, которые будут содержать маршруты ответственные за работу ответного трафика соединений устанавливаемых снаружи.

В файл /etc/iproute2/rt_tables приведём к следующему виду:
#
# reserved values
#
255     local
254     main
253     default
0       unspec
#
# local
#
#1      inr.ruhep

201     ufanet
202     lufanet
5. Подготовим скрипты для управления маршрутами через VPN-подключение

Создадим и пропишем в файл /etc/ppp/ip-up.d/route следующее:
#!/bin/sh

case "$PPP_IPPARAM" in
        ufanet)

        ip route add default dev $PPP_IFACE src $PPP_LOCAL table main

        ip route add default dev $PPP_IFACE src $PPP_LOCAL table ufanet
        ip rule add from $PPP_LOCAL table ufanet
        ;;

        *)
        echo "No PPP_IPPARAM defined"
        ;;
esac
Создадим парный файл /etc/ppp/ip-down.d/route и пропишем в него следующее:
#!/bin/sh

case "$PPP_IPPARAM" in
        ufanet)

        ip route del default dev $PPP_IFACE src $PPP_LOCAL table main

        ip route del default dev $PPP_IFACE src $PPP_LOCAL table ufanet
        ip rule del from $PPP_LOCAL table ufanet
        ;;

        *)
        echo "No PPP_IPPARAM defined"
        ;;
esac
Не забываем проставить у файлов флаг выполнимости:
# chmod +x /etc/ppp/ip-up.d/route
# chmod +x /etc/ppp/ip-down.d/route
6. Настройка xl2tpd

В файл конфигурации /etc/xl2tpd/xl2tdp.conf пропишем следующие настройки:
[global]
access control = yes

[lac ufanet]
lns = vpn.ufanet.ru
redial = yes
redial timeout = 10
require chap = yes
require authentication = no
ppp debug = yes
pppoptfile = /etc/ppp/options.l2tp
require pap = no
autodial = yes
name = 111111
Создаём файл /etc/ppp/options.l2tp с настройками для демона pppd:
unit 0
remotename vpn.ufanet.ru
ipparam ufanet
connect /bin/true
mru 1460
mtu 1460
nodeflate
nobsdcomp
persist
maxfail 0
nopcomp
noaccomp
noauth
noproxyarp
name 111111
Теперь нужно создать файл /etc/xl2tpd/l2tp-secrets с паролем для подключения:
111111          vpn.ufanet.ru   password
Во всех трёх файлах вместо отмеченных жирным шрифтом цифр нужно указать ваш логин для подключения, а в последнем файле выделенное жирным шрифтом слово password - заменить на ваш пароль.

Теперь, в принципе, всё готово для установки подключения. Достаточно перезапустить сеть и запустить демон xl2tpd:
# /etc/init.d/networking stop
# /etc/init.d/networking start
# /etc/init.d/xl2tpd start
Для автоматического запуска xl2tpd в процессе загрузки можно выполнить следующую команду:
# update-rc.d xl2tpd defaults
Я же на этом не остановился и, поскольку на домашнем компьютере использую systemd, написал service-файл.

7. Настройка systemd для запуска xl2tpd

По умолчанию systemd использует скрипты из каталога /etc/init.d, если для них нет service-файла с таким-же именем. Попробуем создать service-файл /etc/systemd/system/xl2tpd.service со следующим содержимым:
[Unit]
Description=layer 2 tunelling protocol daemon xl2tpd
After=network.target

[Service]
ExecStart=/usr/sbin/xl2tpd -D -c /etc/xl2tpd/xl2tpd.conf -s /etc/xl2tpd/l2tp-secrets -p /run/xl2tpd/xl2tpd.pid -C /run/xl2tpd/l2tp-control
ExecStop=/usr/sbin/xl2tpd-control -c /run/xl2tpd/l2tp-control disconnect ufanet
Type=simple
PIDFile=/var/run/xl2tpd/xl2tpd.pid

[Install]
WantedBy=multi-user.target
Для запуска xl2tpd понадобится создать каталог /run/xl2tpd. Для этого создадим файл /etc/tmpfiles.d/xl2tpd.conf с настройками для создания каталога:
d /run/xl2tpd 0755 root root -
Теперь можно сообщить systemd об изменении настроек:
# systemctl daemon-reload
Сообщить systemd о том, что нужно запускать xl2tpd.service при загрузке системы:
# systemctl enable xl2tpd.service
И запустить xl2tpd средствами systemd:
# systemctl start xl2tpd.service
У этого service-файла есть одна недоработка, которую я не смог побороть: правильное завершение процесса xl2tpd. После завершения сервиса, systemd назначает сервису статус ошибки.

Использованные материалы:
1. Настройка VPN (протокол L2TP) на примере провайдера Корбина Телеком,
2. обновил xl2tpd пропал ipparam.

P.S. Работает не стабильно, поэтому вернулся обратно, на PPTP.

воскресенье, 21 апреля 2013 г.

Контекст запроса Flask

Перевод статьи: The Request Context

Этот документ описывает поведение Flask 0.7, которое в основном совпадает со старым, но имеет некоторые небольшие отличия.

Рекомендуем сначала прочитать главу Контекст приложения.

Подробнее о локальных объектах контекста

Представим, что имеется служебная функция, которая возвращает URL, на который нужно перенаправить пользователя. Представим, что всегда нужно перенаправлять на URL из параметра next или на страницу, с которой перешли на текущую страницу, или на страницу-индекс:
from flask import request, url_for

def redirect_url():
    return request.args.get('next') or \
    request.referrer or \
    url_for('index')
Можно заметить, что функция обращается к объекту запроса. Если попытаться запустить её из оболочки Python, будет выброшено исключение:
>>> redirect_url()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'request'
В этом есть определённый смысл, потому что в данный момент нет запроса, к которому мы пытаемся получить доступ. Итак, нам нужно создать запрос и связать его с текущим контекстом. Создадим RequestContext с помощью метода test_request_context:
>>> ctx = app.test_request_context('/?next=http://example.com/')
Этот контекст можно использовать одним из двух способов - используя выражение with или вызвав методы push() и pop():
>>> ctx.push()
После чего можно работать с объектом запроса:
>>> redirect_url()
u'http://example.com/'
И так до тех пор, пока вы не вызовете pop:
>>> ctx.pop()
Поскольку контекст запроса изнутри представляет собой элемент стека, можно добавлять и вынимать его из стека множество раз. Это очень полезно для реализации таких функций, как внутреннее перенаправление.

За более подробной информацией об использовании контекста запроса из интерактивной оболочки Python, обратитесь к главе Работа с оболочкой.

Как работает контекст

Если посмотреть изнутри на то, как работает приложение Flask WSGI, можно обнаружить фрагмент кода, который выглядит очень похожим на следующий:
def wsgi_app(self, environ):
    with self.request_context(environ):
        try:
            response = self.full_dispatch_request()
        except Exception, e:
            response = self.make_response(self.handle_exception(e))
        return response(environ, start_response)
Метод request_context() возвращает новый объект RequestContext и использует его в выражении with для связывания контекста. Всё, что будет вызвано из этого потока, начиная с этой точки и до конца выражения with, будет иметь доступ к глобальному объекту запроса (flask.request и т.п.).

Контекст запроса изнутри работает как стек: на самом верху стека находится текущий активный запрос. push() добавляет контекст на верхушку стека, а pop() вынимает его из стека. При изъятии также вызываются функции teardown_request() приложения.

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

Функции обратного вызова и ошибки

Что случится, если произойдёт ошибка во время обработки запроса во Flask? Частично это поведение изменилось в версии 0.7, потому что желательно знать, что на самом деле произошло. Новое поведение очень простое:
  1. Перед каждым запросом выполняются функции before_request(). Если одна из этих функций вернула ответ, другие функции не выполняются. Однако, в любом случае, это значение трактуется как значение, возвращённое представлением.
  2. Если функции before_request() не вернули ответ, обработка запроса прекращается и он передаётся в подходящую функцию представления, которая может вернуть ответ.
  3. Значение, возвращённое из функции представления, преобразуется в настоящий объект ответа и обрабатывается функциями after_request(), которые могут заменить его целиком или отредактировать.
  4. В конце запроса выполняются функции teardown_request(). Это происходит независимо от того, было ли выброшено необработанное исключение, были ли вызваны функции before_request(), или произошло всё сразу (например, в тестовом окружении обработка функций обратного вызова before_request() иногда может быть отключена).
И так, что же происходит в случае ошибки? В рабочем режиме неотловленные исключения приводят к тому, что обработчик выводит сообщение об ошибке 500 на сервере. В режиме разработки, однако, приложение не обрабатывает исключение и передаёт его наверх, серверу WSGI. Таким образом, средства интерактивной отладки могут предоставить информацию для отладки.

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

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

Функции обратного вызова teardown_request

Функции обратного вызова teardown_request() - это особые функции обратного вызова, которые выполняются отдельно. Строго говоря, они не зависят от действительной обработки запроса и связаны с жизненным циклом объекта RequestContext. Когда контекст запроса вынимается из стека, вызываются функции teardown_request().

Это важно знать, если жизнь контекста запроса будет удлинена при использовании клиента для тестировании с помощью выражения with или при использовании контекста запроса из командной строки:
with app.test_client() as client:
    resp = client.get('/foo')
    # Функции teardown_request() ещё не вызываются в этом месте
    # не смотря на то, что получен объект ответа

# Только когда код достигнет этой точки, функции teardown_request()
# будут вызваны. Ещё это может произойти, если произойдёт
# переключение на другой запрос из клиента для тестирования

В этом можно убедиться, воспользовавшись командной строкой:
>>> app = Flask(__name__)
>>> @app.teardown_request
... def teardown_request(exception=None):
... print 'это запущено после запроса'
...
>>> ctx = app.test_request_context()
>>> ctx.push()
>>> ctx.pop()
это запущено после запроса
>>>
Учтите, что функции обратного вызова выполняются всегда, независимо от того, были ли выполнены функции обратного вызова before_request() и произошло ли исключение. Некоторые части системы тестирования могут также создавать временный контекст без вызова обработчиков before_request(). Убедитесь, что ваши обработчики teardown_request() в таких случаях никогда не приводят к ошибкам.

Замечания о посредниках

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

В большинстве случаев об этом не стоит беспокоиться, но существует несколько исключительных случаев, когда хорошо знать, что объект на самом деле является посредником:
  • Объекты-посредники не подделывают наследуемые типы, поэтому если понадобится провести проверки над реальным экземпляром объекта, это можно сделать это над экземпляром, который доступен через посредника (см. ниже _get_current_object).
  • Если ссылка на объект имеет значение (например, для отправки сигналов).
Если нужно получить доступ к объекту, доступному через посредника, можно воспользоваться методом the _get_current_object():
app = current_app._get_current_object()
my_signal.send(app)
Защита контекста при ошибках

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

Начиная со Flask 0.7 имеется возможность управлять этим поведением при помощи настройки параметра конфигурации PRESERVE_CONTEXT_ON_EXCEPTION. По умолчанию он связан с настройкой DEBUG. Если приложение находится в отладочном режиме, то контекст защищается, а если в рабочем, то - нет.

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

Примечания переводчика

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

воскресенье, 14 апреля 2013 г.

Контекст приложения Flask

Перевод статьи: The Application Context

Новинка версии 0.9

Одно из проектных решений Flask заключается в том, что есть два разных "состояния", в которых выполняется код. Состояние настройки приложения, которое подразумевается на уровне модуля. Оно наступает в момент, когда создаётся экземпляр объекта Flask, и заканчивается когда поступает первый запрос. Пока приложение находится в этом состоянии, верно следующее:
  • программист может безопасно менять объект.
  • запросы ещё не обрабатывались.
  • у вас имеется ссылка на объект приложения, чтобы изменить его, нет необходимости пользоваться каким-либо посредником для того, чтобы получить ссылку на созданный или изменяемый объект приложения.
Напротив, во время обработки запроса верны следующие правила:
  • пока активен запрос, объекты локального контекста (flask.request и другие) указывают на текущий запрос.
  • любой код может в любое время может заполучить эти объекты.
Есть и третье состояние, которое располагается между ними. Иногда можно работать с приложением так же, как и во время обработки запроса, просто в этот момент нет активного запроса. Например, вы можете работать с интерактивной оболочкой Python и взаимодействовать с приложением или запустить приложение из командной строки.

Контекст приложения - это то, чем управляет локальный контекст current_app.

Назначение контекста приложения

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

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

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

Создание контекста приложения

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

Второй способ - это явное создание контекста при помощи метода app_context():
from flask import Flask, current_app

app = Flask(__name__)

with app.app_context():
    # Внутри этого блока current_app указывает на app.
    print current_app.name
Контекст приложения также используется функцией url_for() в случае, если было настроено значение параметра конфигурации SERVER_NAME. Это позволяет вам генерировать URL'ы даже при отсутствии запроса.

Локальность контекста

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

За дополнительной информацией по теме обратитесь к разделу Разработка расширений Flask.

Примечания переводчика

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

воскресенье, 7 апреля 2013 г.

Программа генерации учётных записей на Flask

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

В начале прошлого года я принялся за освоение Perl. Тогда я решил переписать эту веб-страницу на Perl, чтобы поближе познакомиться с модулем HTML::Template.

Теперь я вновь взял для теста эту веб-страницу и решил переписать её на Python, на этот раз для практического ознакомления с веб-фреймворком Flask.

Вспомогательные функции, занимающиеся собственно генерацией логинов и паролей, я вынес в отдельный модуль utils, дабы не перегружать пример. Основной модуль приложения располагается в файле main.py и выглядит следующим образом:
#!/usr/bin/python
# -*- coding: UTF-8 -*-

from flask import Flask, render_template, request

import utils

# Создаём новое приложение Flask
app = Flask(__name__, instance_path = '/home/stupin/flask',
                      instance_relative_config = True)

# Загружаем настройки приложения из файла конфигурации
app.config.from_pyfile('config.cfg')

# Генерация случайного пароля с использованием
# настроек из файла конфигурации
def random_password(length = app.config['PASS_LENGTH'],
                    chars = app.config['PASS_CHARS']):
    return utils.random_password(length, chars)

# Генерация логина, длина которого указана в файле конфигурации
def login(*args, **kwargs):
    if 'max_len' not in kwargs:
        kwargs['max_len'] = app.config['LOGIN_LENGTH']
    return utils.login(*args, **kwargs)

@app.route('/', methods = ['GET'])
def index_get():
    return render_template('index.html', ilogin = 0,
                                         password = random_password())

@app.route('/', methods = ['POST'])
def index_post():
    surname = request.form.get('surname', '')
    name = request.form.get('name', '')
    patronym = request.form.get('patronym', '')
    password = request.form.get('password', random_password())
    ilogin = int(request.form.get('ilogin', 0))

    # Если попросили предыдущий логин, уменьшаем номер логина на единицу
    if 'prevlogin' in request.form:
        ilogin = max(0, ilogin - 1)
    # Если попросили следующий логин, увеличиваем номер логина на единицу
    elif 'nextlogin' in request.form:
        ilogin = max(0, ilogin + 1)
    # Если попросили другой пароль, генерируем новый пароль
    elif 'nextpassword' in request.form:
        password = random_password()
    # Если попросили очистить форму, чистим
    elif 'reset' in request.form:
        surname = ''
        name = ''
        patronym = ''
        ilogin = 0

    return render_template('index.html', surname = surname,
                                         name = name,
                                         patronym = patronym,
                                         ilogin = ilogin,
                                         password = password)
                                         login = login(surname, name, patronym, ilogin),
                                         ilogin = ilogin,
                                         password = password)

# Запуск сервера, если этот модуль был запущен как программа
if __name__ == '__main__':
    app.run(debug = True, host = '127.0.0.1', port = 5000)
И содержимое файла конфигурации config.cfg:
LOGIN_LENGTH = 12
PASS_LENGTH = 8
PASS_CHARS = 'abcdefhkmnpqrstuvwxyzABCDEFHKLMNPQRSTUVWXYZ23456789'
DEBUG = False
Осталось запустить главный модуль в режиме отладки:
$ ./main.py
Получившееся приложение выглядит следующим образом:

Полный архив с приложением можно взять здесь: http://stupin.su/files/flask_account.tbz.

Этот фреймворк привлёк меня в первую очередь своей похожестью на Perl-фреймворк Dancer, который, в своё время привлёк меня лёгкостью, простотой освоения и модульностью (кстати, он очень похож на Mojolicious::Lite). Flask чуть менее модульный и потому чуть более сложный. Положительная сторона заключается в том, что меньше приходится задумываться о выборе и настройке какого-то дополнительного модуля, потому что базовые компоненты уже есть в ядре и они лучше интегрированы между собой. Есть и другие доводы, заставившие меня попробовать этот фреймворк, но до них я пока не добрался и говорить о них сейчас рано.