Використання домішок (mix-in) у Django Class-based Views (CBV)

Нещодавно я повернувся до активного використання Django. І зразу до версії 1.5, в якій вже немає generic view у вигляді функцій, їх повністю витіснили Class Based Views. Спочтаку просто трохи розгубився, але коли дійшов до домішок (mix-in) став точнісінько як левеня у дуже влучному жарті від #soyeahdjango.

Me trying to figure out what a mix-in is

Me trying to figure out what a mix-in is

Тож давайте розберемося, що це за домішки і куди їх підмішують. Mixin — це підхід суть якого полягає у створенні класів, що реалізують допоміжну функціональність, яка може бути корисна об'єктам різних класів (не поєднаним спільним пращуром). Або іншими словами, додає функціональність, не втучаючись у "родовід", що дуже корисно, якщо класи зі сторонньої бібліотеки і внести зміни у верхніх щаблях ієрархії складно/неможливо. Такі класи-домішки потім використовуються при множинному наслідуванні, щоб надати додаткові властивості до успадкованих від батьківського.

Для того щоб розібратись з домішками на практиці, візьмемо таку задачу: перевірка чи є користувач власніком цього об'єкту при виконанні будь яких операцій. Декоратор permission_required для цього не підходить, а писати перевірку в кожному view окремо — суперечить принципу DRY.

Тренуватись будемо на двох моделях, пов'язаних зв'язком один до багатьох:

class Wishlist(models.Model):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=300)
    private = models.BooleanField(default=True)


class WishlistItem(models.Model):
    wishlist = models.ForeignKey(Wishlist)
    name = models.CharField(max_length=300)
    link = models.URLField(max_length=1024, blank=True, null=True)
    note = models.TextField(blank=True, null=True)

Спочатку подивимось на набір view-ів для Wishlist:

class WishlistOwnershipCheckMixin(object):
    def get_object(self, *args, **kwargs):
        object = super(WishlistOwnershipCheckMixin, self).get_object(*args, **kwargs)
        if self.request.user != object.user:
            raise Http404
        return object


class WishlistDetailView(WishlistOwnershipCheckMixin, DetailView):
    model = Wishlist


class WishlistCreate(CreateView):
    form_class = WishlistForm
    model = Wishlist

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super(WishlistCreate, self).form_valid(form)


class WishlistUpdate(WishlistOwnershipCheckMixin, UpdateView):
    form_class = WishlistForm
    model = Wishlist


class WishlistDelete(WishlistOwnershipCheckMixin, DeleteView):
    model = Wishlist
    success_url = reverse_lazy('wishlists')

Клас WishlistOwnershipCheckMixin не має батьківсього класу, але має метод get_object(), який у свою чергу через super() викликає метод get_object() батьківсього класу. Тепер, замість того щоб перевизначати метод get_object() всім трьом класам (WishlistDetailView, WishlistUpdate, WishlistDelete), ми просто успадкоємо його від класу-домішки WishlistOwnershipCheckMixin.

Важливо!!! Пам'ятайте, що через певну плутанину з super(), домішку яка перевизначає метод класу з яким змішується потрібно вказувати першою у списку.

Ось так просто ми додали перевірку права власності до стандартних CBV. Але давайте подумаємо, чи можна використати нашу домішку для інших (як правило пов'язаних моделей), чи доведеться таки для кожної моделі і її CRUD view-ів писати свої домішки.

Якщо уважно подивитись на метод get_object(), то побачимо що у ньому є тільки одна жорстка залежність — наявність поля user у моделі з якою працюємо. WishlistItem пов'язаний з Wishlist, тож його user нам і потрібен. Найкращим способом додати його прямо до WishlistItem (щоб не звертатись до нього object.wishlist.user) мені здалося створення динамічного поля (property). Напишемо це з використанням lambda:

class WishlistItem(models.Model):
    """Wishist item model"""

    wishlist = models.ForeignKey(Wishlist)
    name = models.CharField(max_length=300)
    link = models.URLField(max_length=1024, blank=True, null=True)
    note = models.TextField(blank=True, null=True)
    user = property(lambda self: self.wishlist.user)

Тепер нашу домішку для перевірки права власності можна підмішати і до view-ів моделі WishlistItem:

class WishlistItemFormMixin(object):
    """
    Sets success_url to wishlist
    """
    def get_success_url(self):
        return reverse_lazy("wishlist", kwargs={'pk': self.object.wishlist.id, })


class WishlistItemCreate(WishlistItemFormMixin, CreateView):
    form_class = WishlistItemForm
    model = WishlistItem

    def dispatch(self, request, *args, **kwargs):
        self.wishlist = get_object_or_404(Wishlist, pk=self.kwargs['wishlist_pk'])
        if self.wishlist.user != request.user:
            raise Http404
        return super(WishlistItemCreate, self).dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        form.instance.wishlist = self.wishlist
        return super(WishlistItemCreate, self).form_valid(form)


class WishlistItemUpdate(WishlistItemFormMixin, WishlistOwnershipCheckMixin, UpdateView):
    form_class = WishlistItemForm
    model = WishlistItem


class WishlistItemDelete(WishlistItemFormMixin, WishlistOwnershipCheckMixin, DeleteView):
    model = WishlistItem

О! А ось і ще одна домішка — WishlistItemFormMixin, вона виправляє success_url (всього для двох view-ів, але то я на радостях, що освоїв домішки).

Ось така штука домішки, ось так їх можна використовувати у Django-вських CBV. Дякую за увагу!


Зберігання резервних копій в амазонівсьокому льодовику (Amazon Glacier)

Піддався і я модним тенденціям, переношу свою господарку в амазонівські хмари (AWS). Статичні сайти — на S3, домени — на Route 53, поштарку — на EC2. Все це вже працює пару тижнів, політ нормальний. Але тепер необхідно налаштувати резервне копіювання, і почну я з даних EC2-шної машинки.

Для зберігання резервних копій ми будемо використовувати найбільш підходящий для цього сервіс — Льодовик (Glacier). Нічого дивного, його саме для цього і проектували і коштує він зовсім не дорого (на момент написання статті 0,01$ за 1 Гб на місяць).

Коли я тільки збирався налаштовувати цю господарку, я знав що у Glacier є тільки суворий API, який дозволяє оперувати даними як KEY-BLOB. Але виявилось, що амазонці не стоять на місці і інтегрували його з S3, а значить писати своє чудисько для запихання даних не потрібно, підійдуть вже опановані інструменти для роботи з S3.

Ну, S3 так S3, потрібно створити відро (bucket) спеціально для резервних копій. При виборі регіону врахуйте наступне: якщо будете бекапитись з EC2, то логічно вибрати той же регіон, якщо резервні копії будуть завантажуватись з поза меж AWS, то вибирайте найближчий. Думаю, для більшості моїх читачів це буде "Ireland".

Тепер потрібно налаштувати переміщення файлів з S3 у Glacier. Для цього йдемо до налаштувань відра і дивимось у розділ "Lifecycle":

AWS Lifecycle

Додаємо правило, яке назвали "Freazing backups", встановлюємо галочку "Apply to Entire Bucket", адже це відро ми створили тільки для резервних копій. Додаємо "Transition" – переміщення у Glacier через 1 день (нема сенсу довго зберігати резервну копію на дорожчому S3). Додаємо "Expiration" - видалення об'єктів після 32-х днів (Glacier дешевий, але не безкоштовний).

AWS Lifecycle Rule

Після того як відро створено і "замерзання" налаштоване, потрібно створити користувача і надати йому відповідні права доступу. Для цього в консолі керування AWS переходимо до розділу IAM. Процедура створення користувача дуже проста, тільки не забудьте зберегти ключі, вони нам знадобляться при конфігуруванні s3cmd. А ще, я рекомендую зробити для програм і скриптів групу, наприклад "Programms", і помістити нашого користуваа туди.

Коли користувача створено, виділимо його та перейдемо на вкладку "Permissions" (внизу). Щоб надати цьому користувачу права доступу до відра, натисіть "Attach User Policy". Оскільки нам потрібно дотати дозвіл на одну операцію з одним об'єктом, вибираємо "Policy Generator". Вибираємо сервіс до якого потрібно надати доступ (Amazon S3) і дію "PutObject".

AWS Permission

В полі "Amazon Resource Name" вводимо ARN нашого відерка:

arn:aws:s3:::moby.backup/*

Все, тиснемо "Add Statement" і "Continue". Бачимо нашу політику у форматі JSON, натискаємо "Apply Policy". Доступ налаштовано.

На цьому підготовка відра і льодовика закінчена.

Тепер треба організувати запихання наших резервих копій в S3. Для цього скористаємося утилітою s3cmd. Установка і налаштування:

pip install s3cmd
... skip ...
s3cmd --configure

Конфігуратор спитає у вас ключі (Access Key і Secret Key), "Encryption password" вводити не потрібно (цією конфігурацією користуватиметься скрипт), бажано включити HTTPS. Воно само одразу перевірить конфігурацію, якщо все гаразд перейдемо до підготовки резервних копій.

Мені необхідно зробити резервні копії критичної інформації поштового сервера, а це:

  • конфігурація;
  • база даних;
  • пошта.

Пишемо для цього простий bash-скрипт s3backup.sh:

#!/bin/bash

DST=/tmp/s3-backup # Тимчасовий каталог для резервних копій

DBUSER=root # Користувач БД
DBPASS=db_p@ssw0rd # Пароль від БД !!! Погано, тому пильнуйте права на скрипт !!!

DATE=$(date +%Y-%m-%d)

mkdir ${DST}

# Configs
/bin/tar -czf ${DST}/etc-${DATE}.tar.gz -C / etc

# Mail
/bin/tar -czf ${DST}/mail-${DATE}.tar.gz -C / var/mail_virtual

# MySQL
for db in $(echo 'show databases;' | mysql -s -u ${DBUSER} -p${DBPASS}) ; do
    mysqldump --opt --single-transaction -u ${DBUSER} -p${DBPASS} $db | gzip -c > ${DST}/mysql-${db}-${DATE}.sql.gz
done

# S3 sync
s3cmd put ${DST}/* s3://<назва відра>/

rm -rf ${DST}

Обережно!!! Скрипт може містити логін/пароль до БД і скоріш за все супер-користувача :(, тому ОБОВ'ЯЗКОВО встановіть на скрипт права 7000

Дописуємо у crontab виконання скрипта раз на добу у зручний час. От і все, маємо резервні копії даних за 32-а дні.

Бережіть себе і свої дані!


Магія unicode у psycopg2

При написанні простенького веб-сервісу на Python/Django з доступом напряму до PostgreSQL, я зіткнувся з дивною поведінкою. Коли код запускався на тестовому сервері Django (manage.py runserver), psycopg2 вертав рядки як 'str' у кодуванні UTF-8. А коли запускався через WSGI під Apache2, то unicode. Я спочатку не вникав, чому так, але правити код на сервері після кожного оновлення, коли полізуть помилки, це не діло.

Маю підозру, що Django-вський сервер звертає увагу на # -*- coding: utf-8 -*-, тоді як mod_wsgi має це оголошення в носі, або навпаки.

Коли вже не знав що робити, як завжди допомогла документація. Там пишуть, що в Python 2 ви скоріш за все захочете завжди отримувати unicode. Так, я такий - хочу unicode. Для цього, одразу після імпорту psycopg2, задайте йому глобальні налаштування приведення типів, ось так:

import psycopg2

psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)

І не сушіть собі більше голову, у вас завжди буде unicode :)


"Системні" налаштування Django-проекту

Цікавий підхід до організації файлу налаштувань Django-проекту (settings.py).

Виносимо певні змінні конфігурації "за дужки", може бути корисним для використання спільних опцій для кількох проектів або спрощення процедури оновлення проекту (локальні налаштування за межами каталогу проекту).

### settings.py

import os

ENVIRONMENT_SETTING_FILE = '/etc/django.myproject.settings'

### this will load all environment file settings in here

execfile(ENVIRONMENT_SETTING_FILE)

### all common settings

### ...

Завантажння з ISO-образу на USB Flash

Прикро, але досі більшість виробників "заліза" випускають свої діагностичні утиліти для найпоширенішої ОС. А ще прикріше, коли ці утиліти під нею не працюють, як сталося у моєму випадку: SeaTools for Windows ніяк не хотіли запускатись.

Насправді все не так погано, Seagate знає про існування інших систем, і на цей випадок зробив SeaTools for DOS, які можна завантажити у вигляді завантажувального ISO-образу. Новий виток "розваг" чекає на вас, якщо комп'ютер не має CD/DVD або під рукою не виявиться "болванки", на яку можна було б записати образ. Як завжди, у скрутній ситуації нас виручить Linux.

Тут закінчується лірика і починається інструкція по запису ISO-образу на USB Flash у такий спосіб, щоб з нього можна було завантажитись. !!! Файлова система на Flash-накопичувачі повинна бути FAT23

Якщо раптом у системі не встановлено пакунок syslinux:

# apt-get install syslinux

Копіюємо MBR із завантажувачем:

# cat /usr/lib/syslinux/mbr.bin /dev/sdX

! MBR копіюється саме на пристрій, а не розділ, наприклад, /dev/sdc

Встановлюємо SYSLINUX на розділ:

# syslinux /dev/sdXY

! А тут уже розділ, наприклад, /dev/sdc1

Монтуємо Flash-накопичувач, якщо він не примонтувався автоматично і копіюємо файли в його корінь:

# cp /usr/lib/syslinux/memdisk /media/flash
# cp ~/downloads/SeaToolsDOS223ALL.ISO /media/flash/

Також у корінь вашого Flash-накопичувача потрібно помістити конфігураційний файл syslinux.cfg з таким вмістом:

DEFAULT SeaTools

LABEL SeaTools

  LINUX memdisk

  INITRD SeaToolsDOS223ALL.ISO

  APPEND iso

Все! Тепер можна завантажуватись з цього накопичувача і користуватись утилітами.

Цей спосіб навряд чи підійде для запису ISO-образів установочних дисків ОС Windows, але усілякі "Utility" і "Firmware" працюють.

По матеріалам статті Preparing a bootable SeaTools USB drive in Fedora від Felix Kaechele.


SSH прості хитрощі

Інформація здебільшого для новачків, але й у мене руки дійшли до цього тільки зараз.

Є багато засобів захисту SSH від тупих атак "китайських хакерів", найочевидніший – прикрити фаєрволом від "світу", залишивши доступ тільки з відомих довірених мереж/вузлів. Але такий варіант не завжди підходить, наприклад, є машини які використовуються у якості "точок входу" (на випадок коли немає можливості підключитись по VPN). Стійкі паролі досить надійно захищають від грубих атак, але армія роботів все одно спробує переконати сервер що пароль "Мао*Дзедун", їхні спроби засмічують журнали, що в будь-якому разі не є добре.

Найпростіший спосіб направити цю армію по хибному шляху, перенести службу SSH на інший нестандартний порт, наприклад, tcp/227 або будь-який інший не задіяний для стандартних служб.

!!! Будьте обережні, особливо якщо сервер віддалений, при зміні налаштувань не "зарубайте" доступ самому собі. Для цього виконуйте зміни у наступному порядку:

  • до конфігурації фаєрволу додайте правило, яке дозволить з'єднання по новому нестандартному порту;
  • змініть порт у налаштуваннях SSHd;
  • перевірте з'єднання по новому порту;
  • і тільки тепер заберіть правило, яке дозволяло з'єднання по стандартному порту.

Але це тільки пів справи, ви надурили армію роботів, але й собі створили незручності, для підключення до сервера вам доведеться вказувати додаткові параметри:

~$ ssh -p 227 server.example.com

Справжні спеціалісти не люблять дурної роботи, тому ми навчимо ssh-клієнт пам'ятати у якого сервер який порт використовується. Для цього допишіть у файл ~/.ssh/config ось такі рядки:

Host server.example.com

Port 227

Після цього вам не потрібно буде кожного разу вказувати порт вручну.

Це не єдина зручність яку дозволяє організувати конфігураційний файл ssh, ви можете створювати псевдоніми для вузлів, автоматично підставляти нестандартне ім'я користувача, тощо.

Host dev

HostName developer.example.com

User vasyadev

Port 227

IdentityFile developer_key

Почитайте man ssh_config. Що краще ви знаєте свій інструмент, то більш ефективною стає ваша робота.


408 - Підступний Chrome

Хорошою практикою є періодичний перегляд журналів на серверах, там можна знайти багато цікавого (симптоми виходу з ладу обладнання, хакерські потуги, помилки конфігурації, тощо).

Віднедавна я почав помічати на кількох web-серверах (apache2) помилку 408 "Request timeout". Причому кількість помилок поступово зростала. "Фізичний зміст" помилки - браузер встановив з'єднання з сервером, але не надіслав запит протягом відведеного на це часу, тоді сервер закриває з'єднання з помилкою 408.

Перші пояснення, які мені вдалося знайти на форумах, зводилися до (D)DoS атак або злих хробаків. Обидві гіпотези майже зразу спростувалися: сервіси глибоко внутрішні - DDoS-у взятись нема звідки; майже половина машин, які засвітилися з такою помилкою - це робочі станції локальної мережі з ОС Ununtu, які обслуговуються кваліфікованими адміністраторами.

Але другий підхід до цієї проблеми (лишити її так я не міг, Logwatch щодня про неї нагадував) навів мене на справжнього винуватця, ним виявився браузер Chrome від "корпорації добра". Для зменшення часу відкривання сторінок, ця зараза має "фічу" speculative pre-connect, тобто про всяк випадок відкриває з'єднання заздалегідь, сподіваючись що від сервера ще щось знадобиться і тоді треба буде тільки послати запит. На цю "фічу" завели "баг репорт" :)

На цьому місці я зміг посміхнутись і вирішити залишити все як є, навантаження на сервери не велике, Chrome-ів мало. Але для серверів з більшим навантаженням може знадобитись зменшити значення Timeout у конфігурації Apache, щоб зменшити імовірність вичерпання максимальної кількості одночасно підключених клієнтів (MaxClients).

Дуже сподіваюся, що скоро вони таки «improve the accuracy of our preconnect target»


Використовуємо "резервне" місце на ext3/ext4

Сьогодні напишу про дуже просту штуку, але я про неї не зразу згадав, тому можливо і вам буде цікаво.

Є маленький сервер, який використовується для резервного копіювання з допомогою Bacula. Для зберігання резервних копій у нього окремий розділ (515.4 GB) з ext4. Одного дня перестало вистачати місця для зберігання резервних копій протягом 30-днів, забракло якихось 20GB. Я вже заходився підключати новий диск, більший. Аж раптом згадав...

...що при створенні файлових систем ext3/ext4, по замовчуванню 5% резервується на випадок переповнення диску. Ці блоки доступні лише для користувача root, щоб він міг увійти на систему з переповненим диском. Це дуже завбачливо, але є кілька "але":

  • 5% це резонна величина для маленьких дисків/розділів, але для 1 TB - це 51 GB;

  • для успішного входу супер-користувача місце потрібно на / і /tmp, на інших розділах (/home, наприклад) цього не треба;

  • місце резервується для входу супер-користувача, але у більшості сучасних систем для входу використовується звичайний користувач, який потім підвищує свої привілеї, припускаю що резервування не допоможе зайти на таку систему, коли у неї переповниться диск

Тому, діючи дуже обережно, ми зменшимо кількість зарезервованих блоків або відключимо резервування, змінивши налаштування ФС.

Подивимось скільки ж ми "втрачаємо":

# tune2fs -l /dev/sdc5 | grep -i block
Block count:              125828608
Reserved block count:     6291430
Free blocks:              488281
... skip ...
Block size:               4096
... skip ...

Як бачимо, 6291430*4096=25.769.697.280 байт дискового простору "гуляє". Для зміни кількості зарезервованих блоків використовується та ж команда, тільки з опцією -m:

# tune2fs -m <скільки відсотків резервувати> /dev/sdb5

Щоб повністю відключити резервування вкажіть кількість відсотків - 0:

# tune2fs -m 0 /dev/sdс5
tune2fs 1.41.11 (14-Mar-2010)
Setting reserved blocks percentage to 0% (0 blocks)
# tune2fs -l /dev/sdc5 | grep -i block
Block count:              125828608
Reserved block count:     0
Free blocks:              6779711
... skip ...
Block size:               4096
... skip ...

"Звільнилася" купа місця :)

Увага! Зовсім не потрібно бігти відключати резервування на всіх-всіх-всіх розділах, при невисокій ціні на жорсткі диски, 5% не така висока ціна за додаткову надійність. Тому міняйте налаштування резервування блоків тільки тоді, коли точно знаєте що робите.

Я би просто полінувався міняти, але морока зі зміною розділів була більшою ;) Про LVM там теж вчасно не згадали :)


Перенаправлення запитів з HTTP на HTTPS

Часто виникала необхідність зробити автоматичне перенаправлення всіх запитів з http на https, а головне зробити це максимально простим способом і не влазячи в код програми.

А робиться це наступним чином, в потрібному (найчастіше webroot) каталозі розмішується .htaccess з наступним вмістом:

RewriteEngine OnRewriteCond %{HTTPS} offRewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}

PostgreSQL schema diff

Я з 2008-го року мріяв про інструмент який шукає різниці між схемами БД, а виявляється є такий. Зустрічайте - Another PostgreSQL Diff Tool (apgdiff)!

apgdiff є в репозиторіях Ubuntu. Дуже дивно як я його раніше не знайшов.