Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Puppet

Глава 18 из книги "Архитектура приложений с открытым исходным кодом", том 2.
Оригинал: Puppet
Автор: Luke Kanies
Перевод: А.Панин

18.3. Анализ компонентов

Агент (Agent)

Первым компонентом, с которым вы можете столкнуться при запуске Puppet, является процесс agent. Традиционно он начинал работу после запуска отдельного исполняемого файла с именем puppetd, но в версии 2.6 мы приняли решение о сокращении количества исполняемых файлов до одного, поэтому на данный момент он может быть запущен с помощью команды puppet agent, по аналогии с тем, как работает Git. Сам по себе агент реализует небольшое количество функций; в первую очередь это функции управления конфигурацией и код, реализующий описанные выше аспекты работы на стороне клиента.

Инструмент сбора фактов (Facter)

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

Внешний классификатор узла (External Node Classifier)

Первым компонентом, с которым мы столкнемся на стороне сервера, является внешний классификатор узла или ENC. Этот компонент принимает имя узла и возвращает простую структуру данных, содержащую высокоуровневую спецификацию конфигурации для данного узла. Внешний классификатор узла обычно является отдельной службой или приложением: это сделано для взаимодействия с другим проектом с открытым исходным кодом, таким, как Puppet Dashboard или Foreman, или для интеграции с существующими хранилищами данных, такими, как LDAP. Целью этого компонента является установление того, к каким функциональным классам принадлежит заданный узел и того, какие параметры должны быть использованы для конфигурации этих классов. Например, заданный узел может принадлежать классам debian и webserver и иметь параметр datacenter со значением atlanta.

Следует отметить, что для версии 2.7 Puppet компонент ENC не является обязательным; вместо его использования пользователи могут непосредственно описать конфигурации узлов с помощью кода Puppet. Поддержка компонента ENC была добавлена примерно через 2 года после выпуска первого релиза Puppet, так как мы поняли, что процесс классификации узлов фундаментально отличается от процесса их конфигурации, что делает более осмысленным разделение инструментов для решения этих задач, нежели расширение функций языка для поддержки возможности решения обоих задач. ENC всегда является рекомендуемым компонентом и на некотором этапе развития проекта станет необходимым (в тот момент, когда в составе Puppet будет поставляться достаточно удобный для использования компонент и это требование не будет усложнять работу).

После того, как сервер получает классификационную информацию от внешнего классификатора узла и системную информацию от инструмента сбора фактов (посредством агента), он добавляет эту информацию в объект Node и передает его компилятору.

Компилятор (Compiler)

Как упоминалось ранее, в составе Puppet реализован специальный язык для описания конфигураций систем. В реальности компилятор этого языка состоит из трех частей: похожая на Yacc система разбора, генерации и лексического анализа кода; группа классов, используемая для построения нашего дерева абстрактного синтаксического анализа (Abstract Syntax Tree - AST); и класс компилятора Compiler, который управляет взаимодействием всех этих классов, а также представляет API для этой части системы.

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

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

Так как мы используем в Puppet дерево абстрактного синтаксического анализа, каждое объявление из набора грамматических конструкций Puppet преобразуется в экземпляр класса дерева синтаксического анализа (т.е., Puppet::Parser::AST::Statement) вместо непосредственного выполнения соответствующего действия и эти экземпляры классов AST компонуются в форме дерева по мере сокращения дерева грамматических конструкций. Это дерево абстрактного синтаксического анализа позволяет улучшить производительность в том случае, если на единственном сервере будут компилироваться конфигурации для многих сторонних узлов, так как становятся возможными однократный разбор кода и многократная компиляция. Также у нас появляется возможность выполнять обзор внутренних структур дерева абстрактного синтаксического анализа, что позволяет получить информацию и возможности, которыми мы не обладали бы в случае непосредственного разбора кода без преобразований.

В начале развития проекта Puppet было доступно только несколько подходящих примеров методов построения дерева абстрактного синтаксического анализа, поэтому используемый метод прошел множество этапов эволюционного развития и в итоге мы, по-видимому, пришли к его относительно уникальной версии. Вместо создания одного дерева абстрактного синтаксического анализа для всей конфигурации, мы создаем множество небольших деревьев, доступ к которым осуществляется на основе имен. Например, этот код:
class ssh {
    package { ssh: ensure => present }
}

создает новое дерево абстрактного синтаксического анализа, содержащее одни экземпляр класса Puppet::Parser::AST::Resource и сохраняет это дерево под именем "ssh" в хэш-таблице для всех классов этого определенного окружения. (Я не буду рассматривать подробности реализации других связанных с классами конструкций, так как эта информация не является обязательной для продолжения данного описания).

При наличии дерева абстрактного синтаксического анализа и объекта описания узла Node (полученного от внешнего классификатора узла), компилятор может выбрать классы, описанные в рамках данного объекта (конечно же, если таковые имеются), найти и обработать их. В ходе этой обработки компилятор занимается построением дерева пространств действия переменных; каждый класс получает свое собственное пространство действия переменных, которое объединяется с пространством создающего его класса. Основной принцип создания динамических пространств действия переменных в рамках Puppet: если одни класс включает в себя другой класс, то включенный класс может напрямую работать с переменными включившего его в свой состав класса. Такое поведение всегда было кошмаром для разработчиков, поэтому мы прорабатывали пути избавления от этой возможности.

Дерево пространств действия переменных (Scope tree) является временным и уничтожается после завершения компиляции, но в ходе компиляции также постепенно формируется один артефакт. Мы называем этот артефакт каталогом (Catalog) и он является всего лишь графом, представляющим ресурсы и их взаимодействия. В этом каталоге не сохраняются никакие описания переменных, управляющих структур или вызовов функций; все, что там хранится - это необработанные данные, которые могут быть достаточно просто сконвертированы в форматы JSON, YAML или в любые другие.

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

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

Транзакция (Transaction)

После того, как формирование каталога завершено (будем считать, что ошибки не произошло), он будет передан с помощью транзакции. В системе с разделенными клиентом и сервером, транзакция выполняется на стороне клиента, который принимает каталог, используя для этого протокол HTTP, как показано на Рисунке 18.2.

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

Транзакция используется для выполнения относительно простой задачи: эта задача заключается в обходе графа, фактически представляющего различные взаимоотношения компонентов и проверке того, что состояние каждого из ресурсов было синхронизировано. Как было сказано выше, в ходе транзакции приходится преобразовывать вершины графа содержащихся ресурсов (т.е., вершины, указывающие, например, на то, что класс Class[ssh] содержит пакет Package[ssh] и службу Service[sshd]) в вершины графа зависимостей (т.е., в вершины, указывающие на то, что служба Service[sshd] зависит от пакета Package[ssh]), после чего выполняется стандартная топологическая сортировка графа с выбором каждого из ресурсов по очереди.

В отношении заданного ресурса мы осуществляем простой процесс обработки, состоящий из трех шагов: получение текущего состояния этого ресурса, сравнение его с желаемым состоянием и осуществление любых изменений, необходимых для устранения расхождений. Например, при использовании следующего кода:
file { "/etc/motd":
    ensure => file,
    content => "Welcome to the machine",
    mode => 644
}

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

Этот процесс изменения состояния системы фактически производится средствами простого класса ResourceHarness, в рамках которого полностью описан интерфейс между классами транзакции Transcation и ресурса Resource. Данный подход позволяет снизить количество соединений между классами и упрощает внесение изменений в самостоятельные классы.

Уровень абстракции ресурсов (Resource Abstraction Layer)

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

Уровень абстракции ресурсов был первым компонентом, созданным в рамках проекта Puppet, и, в отличие от языка, он точно описывает то, что может сделать пользователь. Задачей этого компонента является определение назначения ресурса и способа выполнения работы в рамках системы с использованием ресурсов, а язык Puppet специально создан для указания ресурсов с использованием модели, понятной уровню абстракции ресурсов. Поэтому это также наиболее важный компонент системы, который сложнее всего изменить. Существует огромное количество вещей, которые нам хотелось бы изменить в рамках уровня абстракции ресурсов и мы уже реализовали множество критических улучшений данного компонента в течение многих лет (наиболее важным из них было добавление классов Providers), но, несмотря на это, большой объем работы в рамках этого компонента придется выполнить в будущем.

На уровне подсистемы компилятора мы создаем модели ресурсов и их типов с использованием отдельных классов (названных соответственно Puppet::Resouce и Puppet::Resource::Type). Наша цель состоит в том, чтобы использовать эти классы также на уровне абстракции ресурсов, но на сегодняшний день модели этих двух элементов (ресурс и тип) создаются с использованием единственного класса Puppet::Type. (Класс назван некорректно из-за того, что он был создан задолго до того, как мы начали использовать термин "ресурс", в то время, когда мы использовали непосредственную сериализацию структур из памяти для осуществления взаимодействия между узлами, поэтому было достаточно сложно изменить имена классов.)

Во время создания класса Puppet::Type казалось разумным размещение данных ресурса и его типа в едином классе; кроме того, ресурсы являются всего лишь экземплярами типов ресурсов. Со временем, однако, стало понятно, что отношение между ресурсом и его типом не достаточно хорошо смоделировано в понятиях традиционной структуры наследования. Например, типы ресурсов описывают то, какие параметры может иметь ресурс, а не то, принимает ли он параметры (они все их принимают). Следовательно, наш базовый класс Puppet::Type реализует на уровне классов поведение, призванное установить поведение типов ресурсов, а также поведение на уровне экземпляров классов, направленное на установление их поведения. Также его задачей является управление регистрацией и получением типов ресурсов; если вам нужен тип "user", вы можете осуществить вызов Puppet::Type.type(:user).

Это смешение типов поведения значительно затрудняет поддержку кода класса Puppet::Type. Весь класс состоит менее чем из 2,000 строк кода, но функционирует на трех уровнях - ресурсов, типов ресурсов и управления типами ресурсов, что делает его очень запутанным. Становится абсолютно понятно, из-за чего он является главной целью рефакторинга, но с помощью него не производится взаимодействия с пользователем, поэтому обычно сложно выделить ресурсы на его рефакторинг вместо непосредственной реализации новых возможностей.

Уровнем ниже класса Puppet::Type в рамках уровня абстракции ресурсов находятся классы двух основных типов, наиболее интересный из которых мы называем Providers. На начальном этапе разработки уровня абстракции ресурсов в каждом типе ресурса происходило смешение описания параметра с кодом, который реализовывал функции управления. Например, мы могли объявить параметр "content", после чего реализовать метод, с помощью которого можно будет прочитать содержимое файла, а также другой метод, с помощью которого можно будет модифицировать содержимое этого файла:
Puppet::Type.newtype(:file) do
    ...
    newproperty(:content) do
        def retrieve
            File.read(@resource[:name])
        end
        def sync
            File.open(@resource[:name], "w") { |f| f.print @resource[:content] }
        end
    end
end

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

Использование необходимой модели управления ресурсами с учетом всего их многообразия, со временем стало невозможным. Проект Puppet на данный момент поддерживает 30 типов систем управления пакетами и было бы невозможно поддерживать все их средствами единственного типа ресурса Package. Вместо этого мы реализуем понятный интерфейс для описания типа ресурса. Предоставляющие свойства классы реализуют методы установки и получения значений для всех свойств типов ресурсов, названные очевидным образом. Например, ниже приведен образец класса, предоставляющего описанное свойство:
Puppet::Type.newtype(:file) do
    newproperty(:content)
end
Puppet::Type.type(:file).provide(:posix) do
    def content
        File.read(@resource[:name])
    end
    def content=(str)
        File.open(@resource[:name], "w") { |f| f.print(str) }
    end
end

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

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

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

Создание отчетов (Reporting)

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

Эти события создаются в объекте ResourceStatus, который связан с каждым из ресурсов. Следовательно, для заданной транзакции у вас будет вся информация об использованных ресурсах и о любых произведенных изменениях наряду со всеми метаданными этих изменений, которые могут потребоваться вам.

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


Далее: Инфраструктура