illumium.org

Главная › Блоги › Блог kayo

NixOS: Пишем свои модули конфигурации.

kayo — Пнд, 11/05/2015 - 23:44

Это продолжение цикла статей об операционной системе с чисто функциональной сборкой и конфигурацией NixOS. Мы уже научились устанавливать и настраивать её тут. Настало время сделать нечто бо́льшее, нечто действительно продвинутое, то ради чего, собственно, и стоит использовать именно такую ОС. Настало время написать свои собственные модули конфигурации. В этой статье мы реализуем простой модуль конфигурации, который настраивает доступ пользователей к базам данных MySQL MariaDB.

Введение

Итак, что же такое модули конфигурации, и как они организованы изнутри. Инструменты NixOS вроде nixos-rebuild, nix-env или nixos-option, вычисляют конфигурацию при загрузке. Проще говоря, загружается модуль nixos, который в свою очередь загружает модуль nixpkgs, глобальную конфигурацию и т.д. В результате получается некий набор с атрибутами options, config, system и др. Причём набор этот ленивый, то есть вычисляется по мере запроса его атрибутов инструментарием NixOS. Например, утилита nixos-option запрашивает информацию из полей options и config, именно оттуда она узнаёт о том, какие поддерживаются опции конфигурации, какие имеют значения по-умолчанию и актуальные на данный момент.

Что-то я всё говорю и по глазам вижу, что вы мне не верите, поэтому давайте посмотрим и убедимся. Существует замечательная штука nix-repl, как легко понять из названия, позволяет читать, вычислять и печатать nix выражения в интерактивном режиме. По-умолчанию она не установлена, поэтому ставим сами. Итак, давайте смотреть:

[root@nixie:/etc/nixos]# nix-repl
Welcome to Nix version 1.8. Type :? for help.

nix-repl> import <nixos>
«lambda»

# Как легко заметить, возвращается не набор атрибутов а функция.
# Поэтому выполним эту функцию, передав пустой набор атрибутов,
# и пометим результат синонимом "n".
nix-repl> n = import <nixos> {}

# Теперь если ввести этот синоним с точкой и нажать TAB,
# то мы увидим имена объявленных полей.
nix-repl> n.
n.config            n.nixFallback       n.options
n.system            n.vm                n.vmWithBootLoader

# Так можно исследовать любые поля, например:
nix-repl> n.options.services.openssh.enable 
{ _type = "option";
  declarations = [ ... ];
  default = false;
  definitions = [ ... ];
  description = "Whether to enable the OpenSSH secure shell daemon, which
allows secure remote logins.";
  files = [ ... ];
  isDefined = true;
  loc = [ ... ];
  options = [ ... ];
  type = { ... };
  value = true; }

# Видим много интересного,
# а ещё больше интересного не видим

Итак, как же устроена декларативная чисто функциональная конфигурация. Любой модуль конфигурации условно состоит из двух частей:

  • Интерфейс
  • Реализация

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

Конфигуратор для пользователей и баз данных

Итак, задача следующая: Есть некие пользователи, которые идентифицируются именами и паролями, они должны иметь доступ к некоторым базам данных в DBMS MariaDB. Причём привилегии каждого пользователя по отношению к каждой базе могут быть различными. Это типичная задача для всяких панелей управления серверами, которую каждый разработчик решает по принципу: кто во что горазд. Однако, NixOS предлагает универсальный инструментарий для решения всех подобных задач.

Итак, сперва определимся со структурой:

{
  # Наша конфигурация
  mysql = {
    # Настройки суперпользователя,
    # который будет настраивать доступ
    super = {
      # Имя пользователя
      username = "root";
      # Пароль пользователя
      password = "rootPassword";
    };
    # Настройки пользователей,
    # для которых нужно настроить доступ
    users = {
      # Конфигурация для пользователя
      someUser = {
        # Пароль пользователя
        password = "securePassword";
        # Базы данных с привилегиями
        databases = {
          someDb = "somePriv";
          # ...
        };
      };
      # ...
    };
  };
}

Интерфейс

Первое, что нужно сделать: определить интерфейс модуля конфигурации:

# Это модуль конфигурации mysql-configuration.nix
# он должен вернуть функцию, возвращающую набор атрибутов
{lib, ...}:
# Делаем видимыми определения из lib,
# такие как: mkOption, types и другие
with lib;
{
  # Это интерфейс модуля
  # он определяет опции конфигурации
  options = {
    # Настройки суперпользователя
    mysql.super = {
      # Функция mkOption создаёт опцию
      username = mkOption {
        # Значение по-умолчанию
        default = "root";
        # Образец
        example = "superuser";
        # Описание для пользователей
        description = "The name of superuser.";
        # Тип опции
        type = types.str;
      };
      password = mkOption {
        example = "superpass";
        description = "The password of superuser.";
        type = types.str;
      };
    };
    mysql.users = mkOption {
      default = {};
      # Функция literalExample позволяет задать
      # образец в виде обычной строки.
      # Она полезна для композитных опций.
      example = literalExample ''
        {
          someUser = {
            password = "securepass";
            databases = { someSiteDb = "w"; };
          };
        };
      '';
      description = "MySQL users and databases configuration.";
      # Тип этой опции композитный
      # В данном случае это произвольный
      # набор атрибутов со значениями
      # типа подмодуля.
      type = types.attrsOf (types.submodule ({name, config, ...}: {
        # Здесь мы определяем что у нас будет
        # внутри комплексного типа
        # name — это имя атрибута в наборе
        # в нашем случае — имя пользователя
        # config — текущие значения опций
        options = {
          # Чтобы было проще разбирать конфиг
          # мы продублировали имя в виде поля
          username = mkOption {
            # Значение по-умолчанию берём из
            # имени атрибута
            default = name;
            example = "someUser";
            description = "The name of user.";
            type = types.str;
          };
          password = mkOption {
            example = "securepass";
            description = "The password of user.";
            type = types.str;
          };
          databases = mkOption {
            default = {};
            example = literalExample ''
              {
                someDb = "w";
              };
            '';
            description = "The pairs of databases and permissions.";
            # Набор баз данных — это тоже композитный тип
            # с перечислениями в качестве значений
            type = types.attrsOf (types.enum ["r" "w" "a"]);
          };
        };
      }));
    };
  };
}

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

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

  • unspecified — нечто аморфное
  • bool — логический тип
  • int — целочисленный тип
  • str — строковый тип
  • path — путь в системе
  • package — пакет

Вот примеры обёрток для строк:

  • seperatedString "разделитель" — список строк, разделённых "разделителем", преобразуемый в строку
  • lines — список строк, разделённых символом новой строки
  • commas — список строк, разделённых запятыми
  • envVar — список строк, разделённых двоеточием

Они удобны тем, что позволяют задавать значение опции в виде списка, а получать как собранную строку. Например, если объявить список путей к библиотекам с типом vVar, задаваться опция будет как список строк ["/usr/lib1" "usr/lib2"], а читаться как строка "/usr/lib1:/usr/lib2".

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

  • uniq <тип> — следит за тем, чтобы значения были уникальными. Полезно при указании таких вещей, как имена пользователей в системе или точки монтирования, чтобы не позволить создать разных пользователей с одни именем или смонтировать два устройства в один и тот же каталог.
  • nullOr <тип> — позволяет специальное значение null. Удобно для необязательных опций без значений по-умолчанию.
  • enum [<значение> <значение>...] — одно из заданного набора значений.
  • either <тип1> <тип2> — один из двух типов. Если надо больше, то можно указать в качестве одного из типов either.
  • addCheck <тип> <функция проверки> — добавляет пользовательскую функцию поверки значения к произвольному типу.

Композитные типы позволяют нам создавать вложенные структуры в виде наборов атрибутов или в виде списков или и то и другое:

  • attrsOf <тип_значений> — набор атрибутов со значениями заданного типа.
  • listOf <тип_значений> — список значения данного типа.
  • loaOf <тип_значений> — набор атрибутов или список значений.
  • submodule <опции> — подмодуль конфигурации.

Принцип конфигурирования состоит в том, что все значения объединяются некоторым образом, который определяется типом опции. Так, к примеру, реализована возможность определять скрипты, исполняемые перед и после запуска системных сервисов. Значения, заданные в разных местах просто объединяются в одно, таким образом выполняются все определённые действия. Однако принципы объединения могут быть различны в зависимости от характера использования.

Интересно работает третий композитный тип, фактически он полиморфный. Пользователю даётся свобода выбора способа задания наборов: список или пары ключ-значение. Как это устроено наглядно видно в конфигурации точек монтирования файловых систем. Пользователь может задать список наборов атрибутов [ { точка_устройство_и_опции_монтирования } ... ] для точек монтирования или же набор пар { точка_монтирования = { устройство_и_опции_монтирования } ... }.

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

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

{config, lib, ...}:
with builtins; # Потребуется функция attrNames
with lib;
let
  # Потребуются текущие значения опций
  cfg = config.mysql;
in {
  options = {
    # ...
    # Добавили возможность определять наборы привилегий
    mysql.perms = mkOption {
      # Используем ключевое слово rec
      # Это даёт возможность ссылаться на
      # ранее определённые атрибуты
      default = rec {
        # Только чтение
        r = ["SELECT"];
        # Чтение и запись
        w = r ++ ["INSERT" "UPDATE" "DELETE"];
        # Чтение запись и изменение структуры
        a = w ++ ["CREATE" "DROP" "ALTER" "INDEX" "REFERENCES"];
      };
      description = "Named MySQL permission sets";
      # Тип определили как произвольный набор атрибутов
      # со значениями в виде списка перечислимого типа
      type = types.attrsOf (types.listOf (types.enum [
        # Перечислили все поддерживаемые привилегии
        "SELECT" "INSERT" "UPDATE" "DELETE"
        "CREATE" "DROP" "ALTER" "INDEX" "REFERENCES"
        "ALTER ROUTINE" "CREATE ROUTINE" "CREATE TEMPORARY TABLES"
        "CREATE USER" "CREATE VIEW" "EVENT" "EXECUTE" "FILE"
        "GRANT OPTION" "LOCK TABLES" "PROCESS" "RELOAD"
        "REPLICATION CLIENT" "REPLICATION SLAVE" "SHOW DATABASES"
        "SHOW VIEW" "SHUTDOWN" "SUPER" "TRIGGER"
      ]));
    };
    mysql.users = mkOption {
      default = {};
      example = literalExample ''{
        someUser = {
          password = "securepass";
          databases = { someSiteDb = "w"; };
        };
      }'';
      description = "MySQL users and databases configuration.";
      type = types.attrsOf (types.submodule ({name, config, ...}: {
        options = {
          username = mkOption {
            default = name;
            example = "someUser";
            description = "The name of user.";
            type = types.str;
          };
          password = mkOption {
            example = "securepass";
            description = "The password of user.";
            type = types.str;
          };
          databases = mkOption {
            default = {};
            example = literalExample ''
              {
                someSiteDb = "w";
              };
            '';
            description = "The pairs of databases and permissions.";
            # Теперь возможные варианты для перечислимого
            # типа вычисляются как имена определённых
            # пользователем наборов привилегий
            type = types.attrsOf (types.enum (attrNames cfg.perms));
          };
        };
      }));
    };
  };
}

В общем-то мы вольны создавать свои типы с помощью функции mkOptionType, определённой в lib. При этом кроме собственно имени типа нам потребуется определить функцию проверки валидности значения и функции объединения значений.

Реализация

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

{config, pkgs, lib, ...}:
with builtins;
with lib;
let
  cfg = config.mysql;
  # Функция генерации кода настройки привилегий пользователя
  # Здесь у нас алхимия для корректного переконфигурирования
  setupUser = {username, password, databases}: ''
    echo "GRANT USAGE ON *.* TO '${username}' IDENTIFIED BY '${password}';"
    echo "REVOKE ALL PRIVILEGES, GRANT OPTION FROM '${username}';"
    ${concatStrings (attrValues (mapAttrs (database: access: ''
      echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
      echo "GRANT ${concatStringsSep ", " cfg.perms.${access}} ON \`${database}\`.* TO '${username}' IDENTIFIED BY '${password}';"
    '') databases))}
  '';
in {
  # интерфейс ...

  # Мы реализуем конфиг только в том случае
  # когда определен хотя бы один пользователь
  config = mkIf (0 < length (attrNames cfg.users)) {
    # Добавляем наш произвольный код
    # к после-стартовому скрипту сервиса СУБД
    systemd.services.mysql.postStart = ''
      {
        # Забираем настройки пользователей
        # и применяем к ним функцию setupUser
        # Результаты склеиваем воедино
        ${concatMapStrings setupUser (attrValues cfg.users)}
        # Не забываем сбросить кеш привилегий
        echo "FLUSH PRIVILEGES;"
          # Запускаем скрипт в клиенте
          # передавая в него опции из конфига
      } | ${config.services.mysql.package}/bin/mysql -u ${cfg.super.username} -p${cfg.super.password}
    '';

    # Настраиваем сервис
    services.mysql = {
      # Включаем
      enable = true;
      # Переопределяем пакет
      package = pkgs.mariadb;
      # Начальный пароль устанавливаем свой
      rootPassword = cfg.super.password;
      extraOptions = ''
        # Опции конфигурации сервера СУБД
      '';
    };
  };
}

Как видим, всё довольно тривиально, но мощно. Теперь необходимо добавить модуль конфигурации в любой другой модуль, включаемый в /etc/nixos/configuration.nix с использованием атрибута imports = [ ./mysql-configuration.nix ]. Если где-то в конфигурации определены пользователи, тогда при запуске nixos-rebuild switch наша реализация активируется, установится пакет MariaDB, запустится системный сервис, создадутся пользователи и базы данных, выдадутся права, и будет всем счастье.

Заключение

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

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

  • Системное администрирование
  • nixos
  • взрыв мозга
  • Бортовой журнал Иллюмиума

Отправить комментарий

Содержимое этого поля является приватным и не будет отображаться публично.
  • Доступные HTML теги: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Syntax highlight code surrounded by the {syntaxhighlighter SPEC}...{/syntaxhighlighter} tags, where SPEC is a Syntaxhighlighter options string or "class="OPTIONS" title="the title".

Подробнее о форматировании

CAPTCHA
Этот вопрос задается для того, чтобы выяснить, являетесь ли Вы человеком или представляете из себя автоматическую спам-рассылку.
 __     __   ___         __        __    _       
\ \ / / / _ \ ____ \ \ / / (_) ___
\ \ / / | | | | |_ / \ \ /\ / / | | / __|
\ V / | |_| | / / \ V V / | | \__ \
\_/ \__\_\ /___| \_/\_/ _/ | |___/
|__/
Введите код, изображенный в стиле ASCII-арт.
RSS-материал

Навигация

  • Подшивки
  • Фотоальбомы

«Иллюмиум» на якоре.

Работает на Drupal, система с открытым исходным кодом.

(L) 2010, Illumium.Org. All rights reversed ^_~