Debian, Dojo, Django, Python

Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

Мой Telegram-канал

Писать посты для Blogger мне стало сложно. Сначала я хотел завести свой блог на отдельном доменном имени. Зарегистрировал его, оплатил хостинг, написал часть кода на Django... и сгорел. Немного подумав, я начал искать способы сохранять ценную информацию и делиться ей с читателями с как можно меньшими усилиями, чтобы не надо было поддерживать функциональность, читать документацию хостинга и т. д. В итоге остановился на канале в Telegram. Подписаться можно здесь. Там я пишу про то, как использовать EMACS, если ты технический писатель (а я уже почти год работаю им).

DGrid: обработка onDoubleClick на строке

Разбор

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

Посмотрев код библиотеки, я обнаружил, что у класса Grid есть методы row() и cell(), возвращающие информацию о строке и ячейке соответственно при передаче в качестве параметра события, которое произошло в таблице. Главная сложность была в определении того, на что ставить обработчик. Понятно, что для тысяч строк нельзя вешать onClick() на каждую строку. В этом случае на контейнер вешают один единственный обработчик, который смотрит на то, от кого пришло событие, и уже тогда что-то делает. Всё оказалось не так уж сложно.

Расширение класса
define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dgrid/Grid"
], function(declare, lang, Grid){
    return declare(Grid, {
        onDblClick: function(){
            /* Функция-пустышка, которая должна быть заменена на настоящую в классах,
               унаследованных от этого. Тут должна быть какая-то работа с переданным
               объектом, состоящим из трех полей:
               - id      - id записи, если есть
               - data    - полный объект из связанного с таблицей Store
               - element - DOM-элемент, на котором случился двойной клик               */
        },

        _onDblClick: function(event){
            /* Вызываем onDblClick, передав "чистый" объект с параметрами. Обычно тут 
               разработчики Dojo Toolkit рекомендуют дополнительно вызывать on.emit с
               необходимыми параметрами. На случай, если кто-то ещё должен слушать 
               события, происходящие в таблице, и как-то на них реагировать           */
               this.onDblClick(this.row(event));
        }, // _onDblClick

        postCreate: function(){
            // Установка обработчиков на построенную DOM-модель таблицы
            this.inherited(arguments);

            this.on(".dgrid-content .dgrid-row:dblclick", lang.hitch(this, "_onDblClick"));
        } // postCreate
    }); // declare
}); //define

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

Комментарии в исходном коде

Устал искать, в каком месте удалил лишнюю скобку. Устал определять по отступу, где закончился цикл или if. Поэтому сейчас пишу код примерно так:

Расстановка комментариев
define([
    /*
     * Главное меню приложения.
     * Обработчики событий должны устанавливаться в классе Application.
     */
    "dojo/_base/array", // array.forEach в одном месте
    "dojo/_base/declare",
    "dijit/DropDownMenu",
    "dijit/MenuBar",
    "dijit/MenuBarItem",
    "dijit/MenuItem",
    "dijit/PopupMenuBarItem"
], function(
    array,
    declare,
    DropDownMenu
    MenuBar,
    MenuBarItem,
    MenuItem,
    PopupMenuBarItem
){
    // Создает пункт главного меню с DropDown'ом
    function createMenuBarItem(label){
        return new PopupMenuBarItem({
            label: label,
            popup: new DropDownMenu({})
        });
    } // createMenuBarItem
    
    return declare(MenuBar, {
    	
        buildRendering: function(){
         this.inherited(arguments);
         
         // Какой-то код...
         
         this.fileMenu = createMenuBarItem("Файл");
         this.editMenu = createMenuBarItem("Правка");
         this.dictsMenu = createMenuBarItem("Справочники");
         
         array.forEach([
             this.fileMenu,
             this.editMenu,
             this.dictsMenu
         ], this.addChild, this); // forEach
        } // buildRendering
    }); // declare
}); // define

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

Debian Buster, SecureBoot и Nvidia RTX 2070

Условия задачи

Дано:

  • Ошибка Xorg в логах: May 7 16:21:42 darktower kernel: [ 5.572986] Lockdown: ioperm is restricted; see https://wiki.debian.org/SecureBoot
  • Debian 10 Booster с подключенными репозиториями non-free и contrib.
  • Права root
  • Видеокарта NVidia RTX 2070
  • Компьютер с включенной в настройках материнской платы опцией SecureBoot (EFI)

Проблема в том, что в ряде случаев видеокарта может не запускаться, потому что драйверы не подписаны. Для подписания драйверов следует выполнить действия, описанные в Wiki Debian / SecureBoot.

В моём случае пришлось выполнить следующую последовательность действий:

Установка драйвера Nvidia, исходных кодов ядра и mokutil

Установка драйвера
apt-get update
apt-get install nvidia-driver nvidia-xconfig linux-headers-amd64 mokutil
Пакет Предназначение
nvidia-driver Проприетарный драйвер Nvidia с поддержкой нужных моделей видеокарт.
nvidia-xconfig Утилита конфигурирования параметров Xorg для поддержки им нужных драйверов.
linux-headers-amd64 Исходные коды установленного ядра.
mokutil Утилита для работы с ключами EFI

Генерация ключа EFI, его установка и подписывание драйверов.

Теперь действуем ровно так, как написано в руководстве Debian'а:

Генерация ключа и подписывание драйверов
# Генерация ключей и импорт в EFI
cd  /root
openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER -out MOK.der -nodes -days 36500 -subj "/CN=NVidia RTX 2070 key/"
mokutil --import MOK.der

# Подписывание драйверов Nvidia
cd /lib/modules/4.19.0-8-amd64/updates/dkms/
/usr/lib/linux-kbuild-4.19/scripts/sign-file sha256 /root/MOK.priv /root/MOK.der nvidia-current-drm.ko 
/usr/lib/linux-kbuild-4.19/scripts/sign-file sha256 /root/MOK.priv /root/MOK.der nvidia-current.ko 
/usr/lib/linux-kbuild-4.19/scripts/sign-file sha256 /root/MOK.priv /root/MOK.der nvidia-current-modeset.ko 
/usr/lib/linux-kbuild-4.19/scripts/sign-file sha256 /root/MOK.priv /root/MOK.der nvidia-current-uvm.ko 

# Перенастройка Xorg под nvidia
cd /etc/X11
rm xorg.conf
nvidia-xconfig

Следует обратить внимание на путь, в котором расположены исходные модули ядра nvidia и скрипт для подписывания драйверов - они могут быть другими.

Перезагрузка

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

Автоматизация подписывания ключей.

Можно автоматизировать процесс по максимуму, если ручной выбор исходников ядра переключить на работу с результатами команды uname -r и поместить всё в файл скрипта:

/root/sign_nvidia_drivers.sh
#!/bin/sh

SIGN=/usr/lib/linux-kbuild-4.19/scripts/sign-file
MOK=/root/MOK.priv
DER=/root/MOK.der

cd /lib/modules/$(uname -r)/updates/dkms/

$SIGN sha256 $MOK $DER nvidia-current-drm.ko
$SIGN sha256 $MOK $DER nvidia-current.ko
$SIGN sha256 $MOK $DER nvidia-current-modeset.ko
$SIGN sha256 $MOK $DER nvidia-current-uvm.ko

Его нужно запускать всякий раз при обновлении драйвера Nvidia или ядра.

Настройка Exim4 / Dovecot в Astra Linux SE 1.5

Настройка почты в Astra Linux 1.5 ALD

Исходные данные и задачи

Необходимо настроить сервер электронной почты, работающий в рамках домена Astra Linux 1.5 SE. Исходные данные:

Параметр Значение
Имя домена local.net
FQDN сервера server.local.net
IP-адрес сервера 192.168.0.1
Маска сети: 255.255.255.0

Настройка сети

Отключаем графический менеджер управления сетевыми подключениями wicd.

Отключение wicd
chkconfig wicd off

Прописываем настройки сети в файле /etc/network/interfaces

/etc/network/interfaces
auto lo
iface lo inet loopback

auto eth0
allow-hotplug eth0
iface eth0 inet static
    address 192.168.0.1
    netmask 255.255.255.0
    gateway 192.168.0.1
    network 192.168.0.0
    broadcast 192.168.0.255
    dns-nameservers 192.168.0.1
    dns-search local.lan

После этого нужно выполнить перезапуск демона сети:

Перезапуск демона сети
service networking stop && service networking start

В документации к демону networking написано, что команда restart является устаревшей и работает не так, как ожидается.

Необходимо в файле /etc/hosts прописать соответствие FQDN сервера и его IP-адреса, а также задать hostname

/etc/hosts
127.0.0.1    localhost
192.168.0.1 server.local.net server

192.168.0.101 arm01.local.net arm01
192.168.0.102 arm02.local.net arm02
192.168.0.103 arm03.local.net arm03

В файле /etc/hostname меняем краткое имя компьютера server на полное server.local.net, а также изменяем текущее значение системной переменной:

hostname
echo server.local.net > /etc/hostname
hostname server.local.net

Инициализация домена

Действуем согласно официальной инструкции от РусБиТех:

ald-init
ald-init init

Появится уведомление о том, что команда init уничтожит всю базу данных LDAP и Kerberos, будут остановлены и перезапущены службы, и упадет с неба большая звезда, горящая подобно светильнику, и падет на третью часть рек и на источники вод... Отвечаем утвердительно

Попросят ввести пароли для доступа к базе данных Kerberos и пароль администратора Astra Linux Directory. Лучше записать куда-нибудь, потому что понадобится ещё не раз.

Спустя какое-то время на экране появится сообщение:

Успешная инициализация
Astra Linux Directory сконфигурирована.
Сервер ALD активен.
Клиент ALD включен.

Astra Linux Directory сервер успешно инициализирован.

Установка и настройка почтовых сервисов.

Необходимо установить три пакета:

Пакет Описание
exim4-daemon-heavy Передает сообщения и умеет работать с мандатными метками.
dovecot-imapd Умеет отдавать почту клиентам.
dovecot-gssapi Умеет принимать авторизацию на сервере через Kerberos (ALD).

Отдельно отмечу, что сразу же после установки нужно будет запустить конфигурирование exim4.

Установка и конфигурирование
aptitude install exim4-daemon-heavy dovecot-imapd dovecot-gssapi -y
dpkg-reconfigure exim4-config

Процесс настройки exim4 не очень сложный, но нужно правильно ответить на несколько вопросов.

Вопрос Ответ
Общий тип почтовой конфигурации интернет-сайт; приём и отправка почты напрямую, используя SMTP
Почтовое имя системы: local.net
IP-адреса, с которых следует ожидать входящие соединения SMTP: 192.168.0.1
Другие места назначения, для которых должна приниматься почта: local.net
Домены, для которых доступна релейная передача почты: Оставляем пустым
Машины, для которых доступна релейная передача почты: Оставляем пустым
Сокращать количество DNS-запросов до минимума (дозвон по требованию)? Нет
Метод доставки локальной почты: Maildir формат в домашнем каталоге
Разделить конфигурацию на маленькие файлы? Да

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

Удаление лишнего файла
rm /var/mail/administrator

Создадим сервисы ALD для работы с почтой:

Создание сервисов ALD и ключей Kerberos
ald-admin service-add imap/server.local.lan
ald-admin sgroup-svc-add imap/server.local.lan --sgroup=mac
ald-admin sgroup-svc-add imap/server.local.lan --sgroup=mail
ald-admin service-add smtp/server.local.lan
ald-admin sgroup-svc-add smtp/server.local.lan --sgroup=mac
ald-admin sgroup-svc-add smtp/server.local.lan --sgroup=mail
ald-client update-svc-keytab imap/server.local.lan --ktfile="/var/lib/dovecot/dovecot.keytab"
ald-client update-svc-keytab smtp/server.local.lan --ktfile="/var/lib/dovecot/dovecot.keytab"

Указывая имя сервиса, важно помнить, что после слеша указывается то же самое имя сервера, что и в файле /etc/hostname. Если даже в настройках Bind вы потом укажете, что mail.local.net и smtp.local.net являются всего лишь псевдонимами для server.local.net, почта работать не будет, потому что Kerberos очень строго проверяет этот параметр.

После создания файла ключей нужно предоставить доступ к нему для dovecot:

Доступ к файлу ключей
setfacl -m u:dovecot:x /var/lib/dovecot
setfacl -m u:dovecot:r /var/lib/dovecot/dovecot.keytab

Dovecot

Выполняем настройку dovecot. Если убрать все комментарии из файлов, то получится примерно следующее (в тех файлах, где меняли):

/etc/dovecot/dovecot.conf
!include_try /usr/share/dovecot/protocols.d/*.protocol
protocols = imap
listen = 192.168.0.1
dict {}
!include conf.d/*.conf
!include_try local.conf
/etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = yes
auth_mechanisms = gssapi
auth_gssapi_hostname = server.local.lan
auth_krb5_keytab = /var/lib/dovecot/dovecot.keytab
!include auth-system.conf.ext

Я не могу в настройку SSL. Отключаем:

/etc/dovecot/conf.d/10-ssl.conf
ssl = no
ssl_cert = </etc/dovecot/dovecot.pem
ssl_key = </dovecot/private/dovecot.pem

Маленькая настройка для корректной работы с почтой - добавляем в файле /etc/dovecot/conf.d/10-master.conf в секцию service auth:

/etc/dovecot/conf.d/10-master.conf
# ...
service auth {
  unix_listener auth-client {
    mode = 0600
    user = Debian-exim
  }
  # ...
}

Немного упрощаем себе и пользователям жизнь, автоматически создавая на сервере каталоги под входящие, отправленные и удаленные письма при первой авторизации (параметр auto = subscribe).

/etc/dovecot/conf.d/15-mailboxes.conf
namespace inbox {
  mailbox Drafts {
    special_use = \Drafts
    auto = subscribe
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Trash {
    special_use = \Trash
    auto = subscribe
  }
  mailbox Sent {
    special_use = \Sent
    auto = subscribe
  }
}

Расположение каталогов внутри /var/mail/%username% можно изменить, сделав более удобным, если в /etc/dovecot/conf.d/10-mail.conf немного изменить параметр mail_location:

/etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:/var/mail/%u:LAYOUT=fs

Exim4

Теперь нужно сделать пару дополнительных настроек Exim4.

/etc/exim4/conf.d/auth/33_exim4-dovecot-kerberos-ald
dovecot_gssapi:
  driver = dovecot
  public_name = GSSAPI
  server_socket = /var/run/dovecot/auth-client
  server_set_id = $auth1 # В конце цифра один, а не маленькая эль

Файл /etc/exim4/conf.d/acl/30_exim4-config_check_rcpt очень большой, поэтому не привожу его полностью. В самом начале нужно добавить в секцию acl_check_rcpt 4 строки, чтобы выглядело примерно так:

Проверка авторизации
acl_check_rcpt:
  deny
    message = "Auth required"
    hosts = *:+relay_from_hosts
    !authenticated = *
  accept:
    hosts = :
    control = dkim_disable_verify
# и так далее

Проверка работоспособности

Заходим от имени пользователя ALD. Запускаем Thunderbird и создаем новую учетную запись. В параметрах IMAP- и SMTP-серверов пишем одно и то же - server.local.net. При нажатии кнопки "Проверка параметров" параметры авторизации должны измениться на Kerberos/GSSAPI. Сохраняем, перезапускаем почтовый клиент. После второго запуска должно загрузиться дерево папок с сервера. Пишем письмо сами себе, если пришло - все хорошо. Если нет - очень внимательно проверяем содержимое всех конфигурационных файлов. Я сам потерял три дня из-за того, что в одном месте пропустил всего одну строку.

Настройка GRUB2 в Debian

Зачем настраивать GRUB 2

Решил я настроить свой GRUB 2, потому что не все его параметры меня устраивают. Ну, например, уменьшить таймаут, увеличить разрешение, в конце концов, поменять фоновую картинку. К чему в итоге пришёл, написано ниже.

Все выполняемые операции требуют привилегий пользователя root.

Настройка разрешения

Первое, что нужно сделать - зайти в консоль самого GRUB'а при запуске. Для этого нужно нажать клавишу c, и, если пароль на загрузчик не установлен, сразу же осуществляется переход к командной строке. Тут вводим одну команду из двух (результат на моём Debian 9 одинаковый):

Команды для определения графических режимов
videoinfo
vbeinfo

Обе команды дают один и тот же результат - список доступных GRUB'у режимов видеоадаптера. Однако, всё не так просто. Дело в том, что при запуске загрузчика загружается видеодрайвер, НО ЭТО НЕ ТОТ ВИДЕОДРАЙВЕР, который даёт полный доступ ко всем режимам видеоадаптера. Таким образом, у меня, например, максимально доступное разрешение не соответствует параметрам монитора, т. е. тут не всё так просто.

В общем, параметры монитора я выяснил, теперь надо было подкрутить /etc/default/grub. Настраивать нужно именно этот файл, поскольку при вызове скрипта update-grub настройки будут взяты оттуда. Ниже привожу только те настройки, которые менял.

/etc/default/grub
GRUB_DEFAULT=0
GRUB_TIMEOUT=3
GRUB_GFXMODE=1280x1024x32          # Разрешение загрузочного меню GRUB
GRUB_GFXPAYLOAD_LINUX=1920x1080x32 # Передается в параметрах ядра
GRUB_BACKGROUND=/etc/alternatives/desktop-theme/grub/Hexagons-16x9.png
GRUB_DISABLE_OS_PROBER="true"

Краткое описание параметров:

GRUB_DEFAULT Пункт меню по умолчанию
GRUB_TIMEOUT Время в секундах до загрузки пункта по умолчанию
GRUB_GFXMODE Графический режим загрузчика. Можно указать так же значение auto.
GRUB_GFXPAYLOAD_LINUX Разрешение графического режима, которое загрузчик передаст ядру. Если указать правильное значение, можно запускать ОС сразу с нужным разрешением экрана.
GRUB_BACKGROUND Путь к фону загрузчика. Допускается использовать файлы в форматах jpg, png и tga. При необходимости изображение будет отмасштабировано под GRUB_GFXMODE. Если картинки не будет в указанном месте, загрузчик будет запущен в текстовом режиме, без какой-либо графики.
GRUB_DISABLE_OS_PROBER Запретить запуск утилиты osprober, собирающей информацию о других установленных ОС. Поскольку у меня Linux единственная ОС на этом компьютере, могу себе позволить сэкономить немного времени.

После настройки всех параметров обязательно нужно вызвать команду update-grub. Для тех систем, где используется GRUB2, данная команда является символической ссылкой на update-grub2.

Django, JSON и формы

Очень интересные результаты выдает Google при поиске по словам django json forms. Большая часть ссылок ведет на Stack Overflow, но там всё одно и то же. Как правило, всё сводится к тому, чтобы: 1) сменить стек технологий; 2) разобрать request.POST как словарь (это не работает); 3) передать данные каким-то другим способом.

А вот рабочее решение, проверенное в Django 1.4.22 (из-за некоторых особенностей мне сейчас приходится пользоваться именно таким старьем).

views.py
# -*- coding: utf-8 -*-

from __future__ import absolute_imports
from django.utils import simplejson as json
from django import http

from app.items import forms
from app.items import models
from api.items import serializers

from rest import response


def api_post_view(request):
    if request.method == 'POST' and request.is_ajax():
        # Ну да, всего одна строка. А вы что думали?
        raw_data = json.loads(request.POST)
        new_item_form = forms.CreateItemForm(raw_data)
        if new_item_form.is_valid():
            new_item = new_item_form.save()
            serializer = serializers.ItemReadSerializer()
            return reponse.JsonResponse(serializer.serialize(new_item))
        else:
            return response.JsonResponse(new_tem_form.errors)
    else:
        return http.HttpResponseBadRequest()

Насчет JsonResponse тоже надо пару слов сказать. В старых версиях такого класса нет, но он отлично портируется из новых. Вот код:

rest.response
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import simplejson as json
from django.http.response import HttpResponse

class JsonResponse(HttpResponse):
    """
    An HTTP response class that consumes data to be serialized to JSON.

    :param data: Data to be dumped into json. By default only ``dict`` objects
      are allowed to be passed due to a security flaw before EcmaScript 5. See
      the ``safe`` parameter for more information.
    :param encoder: Should be a json encoder class. Defaults to
      ``django.core.serializers.json.DjangoJSONEncoder``.
    :param safe: Controls if only ``dict`` objects may be serialized. Defaults
      to ``True``.
    :param json_dumps_params: A dictionary of kwargs passed to json.dumps().
    """

    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
                 json_dumps_params=None, **kwargs):
        if safe and not isinstance(data, dict):
            raise TypeError(
                'In order to allow non-dict objects to be serialized set the '
                'safe parameter to False.'
            )
        if json_dumps_params is None:
            json_dumps_params = {}
        kwargs.setdefault('content_type', 'application/json')
        data = json.dumps(data, cls=encoder, **json_dumps_params)
        super().__init__(content=data, **kwargs)