Пользователь
–0,1
рейтинг
29 июля 2013 в 21:17

Разработка → Редактирование конфигов в Python



Вам когда-нибудь приходилось парсить и программно вносить изменения в чужие конфигурационные файлы? А в файлы с ненормальными форматами вроде того, что у NSD или BIND9? А если формат предусматривает переносы строк, смысловые отступы и сохранение комментариев, задача быстро покидает категорию тривиальных.

Вот почему я делюсь с вами библиотекой python-reconfigure.


Библиотека предоставляет object mapping между текстом конфиг-файла и python-объектами.
Reconfigure никогда не «ломает» файлы, и не стесняется незнакомых блоков и опций внутри, а также сохраняет комментарии.

Сразу перейдем к примеру:

>>> from reconfigure.configs import FSTabConfig
>>> from reconfigure.items.fstab import FilesystemData
>>>
>>> config = FSTabConfig(path='/etc/fstab')
>>> config.load()
>>> print config.tree
{
    "filesystems": [
        {
            "passno": "0",
            "device": "proc",
            "mountpoint": "/proc",
            "freq": "0",
            "type": "proc",
            "options": "nodev,noexec,nosuid"
        },
        {
            "passno": "1",
            "device": "UUID=dfccef1e-d46c-45b8-969d-51391898c55e",
            "mountpoint": "/",
            "freq": "0",
            "type": "ext4",
            "options": "errors=remount-ro"
        }
    ]
}
>>> tmpfs = FilesystemData()
>>> tmpfs.mountpoint = '/srv/cache'
>>> tmpfs.type = 'tmpfs'
>>> tmpfs.device = 'none'
>>> config.tree.filesystems.append(tmpfs)
>>> config.save()
>>> quit()
$ cat /etc/fstab
proc    /proc   proc    nodev,noexec,nosuid     0       0
UUID=dfccef1e-d46c-45b8-969d-51391898c55e / ext4 errors=remount-ro 0 1
none    /srv/cache      tmpfs   none    0       0


Reconfigure — модульная система, и классы *Config скрывают некоторую внутреннюю логику.
Рассмотрим, как предыдущий пример работает «под капотом».
Сначала текст файла преобразуется парсером в абстрактное синтаксическое дерево.

>>> from reconfigure.parsers import SSVParser
>>> from reconfigure.builders import BoundBuilder
>>> content = open('/etc/fstab').read()
>>> syntax_tree = SSVParser().parse(content)
>>> syntax_tree
<reconfigure.nodes.RootNode object at 0x7f1319eeec50>
>>> print syntax_tree
(None)
        (line)
                (token)
                        value = proc
                (token)
                        value = /proc
                (token)
                        value = proc
                (token)
                        value = nodev,noexec,nosuid
                (token)
                        value = 0
                (token)
                        value = 0
        (line)
                (token)
                        value = UUID=83810b56-ef4b-44de-85c8-58dc589aef48
                (token)
                        value = /
                (token)
                        value = ext4
                (token)
                        value = errors=remount-ro
                (token)
                        value = 0
                (token)
                        value = 1


Затем, класс-строитель (Builder) создает обычные python-объекты и привязывает их к синтаксическому дереву.

>>> builder = BoundBuilder(FSTabData)
>>> data_tree = builder.build(syntax_tree)
>>> print data_tree
{
    "filesystems": [
        {
            "passno": "0",
            "device": "proc",
            "mountpoint": "/proc",
            "freq": "0",Ц
            "type": "proc",
            "options": "nodev,noexec,nosuid"
        },
        {
            "passno": "1",
            "device": "UUID=83810b56-ef4b-44de-85c8-58dc589aef48",
            "mountpoint": "/",
            "freq": "0",
            "type": "ext4",
            "options": "errors=remount-ro"
        }
    ]
}


На самом деле, созданные объекты — это proxy-классы, все поля которых являются свойствами, и при изменении изменяют значения в синтаксическом дереве.

>>> syntax_tree.children[0]
<reconfigure.nodes.Node object at 0x7f51c63b9f10>
>>> print syntax_tree.children[0]
(line)
        (token)
                value = proc
        (token)
                value = /proc
        (token)
                value = proc
        (token)
                value = nodev,noexec,nosuid
        (token)
                value = 0
        (token)
                value = 0

>>> data_tree.filesystems[0].options += ',rw'
>>> print syntax_tree.children[0]
(line)
        (token)
                value = proc
        (token)
                value = /proc
        (token)
                value = proc
        (token)
                value = nodev,noexec,nosuid,rw
        (token)
                value = 0
        (token)
                value = 0


Правила привязки элементов дерева к полям классов задаются в классах *Data.
Пример привязок для данных файла /etc/resolv.conf:

from reconfigure.nodes import Node, PropertyNode
from reconfigure.items.bound import BoundData


class ResolvData (BoundData):
    pass


class ItemData (BoundData):
    def template(self):
        return Node('line', children=[
            Node('token', children=[PropertyNode('value', 'nameserver')]),
            Node('token', children=[PropertyNode('value', '8.8.8.8')]),
        ])


ResolvData.bind_collection('items', item_class=ItemData)
ItemData.bind_property('value', 'name', path=lambda x: x.children[0])
ItemData.bind_property('value', 'value', path=lambda x: x.children[1])


Содержимое, синтаксическое и дерево данных этого файла:

>>> print open('/etc/resolv.conf').read()
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 127.0.0.1

>>> print syntax_tree
(None)
        (line) (Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
        DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN)
                (token)
                        value = nameserver
                (token)
                        value = 127.0.0.1

>>> print data_tree
{
    "items": [
        {
            "name": "nameserver", 
            "value": "127.0.0.1"
        }
    ]
}



Кроме того, Reconfigure осведомлена о наличии include-директив в некоторых файлах, и запоминает, что в каком файле находилось.

Reconfigure легко расширить собственными парсерами, builder'ами и includer'ами.

В настоящий момент Reconfigure — это сердце Ajenti 1.0 Beta, но об этом в следующий раз :)

Github
PYPI
Документация
DEB и RPM пакеты доступны в репозиториях Ajenti
Eugene @hardex
карма
184,0
рейтинг –0,1
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (32)

  • +5
    Вроде здорово, но такая штука должна быть покрыта тестами обязательно, иначе страшно использовать.
    Посмотрел на гитхабе, тесты вроде есть, но насколько я понял они просто проверяют валидность загрузки и сохранения, а надо бы еще тестировать редактирование и добавление новых строчек.
    • +1
      На самом деле тесты для этого есть, но они разделены по компонентам — тесты конкретных конфигов, которые вы видели, и несколько тестов конкретных операций (изменение, добавление, удаление).
  • +3
    Я делал что-то похожее, bitbucket.org/eyeofhell/pyrewriter
    Насколько я понял на практике, основная сложность — это задать грамматику для распарсиваемого файла, потому что после того, как все сделано и начинается работа с реальными конфигурационными файлами, «неожиданно» оказывается, что понимание грамматики автором библиотеки, автором самой программы и авторами конфигов к ней перпендикулярны друг другу :). С грамматикой достаточно тривиального конфига nginx я довольно долго промучился, печальную историю можно посмотреть в коммитах.

    В связи с чем вопрос — и для каких конфигурационных файлов оно знает грамматику «из коробки»?
    • +3
      Не осудите за лень, скопипащу как есть из README:

      Знает синтаксис (в скобках — название модуля):
      BIND9 config (bind9)
      Crontab (crontab)
      NFS Exports (exports)
      .ini (ini)
      iptables-save (iptables)
      nginx-like (nginx)
      squid (squid)
      nsd (nsd)
      CSV-like space-separated values (ssv)
      JSON (jsonparser)
      


      Знает грамматику:
      Ajenti (ajenti)
      BIND9 DNS (bind9)
      Crontabs (crontab)
      Samba CTDB (ctdb)
      ISC DHCPD / uDHCPD (dhcpd)
      NFS /etc/exports (exports)
      /etc/fstab (fstab)
      /etc/group (group)
      /etc/hosts (hosts)
      iptables-save dump (iptables)
      Netatalk afp.conf (netatalk)
      NSD DNS (nsd)
      /etc/passwd (passwd)
      /etc/resolv.conf (resolv)
      Samba (samba)
      Squid 3 (squid)
      Supervisord (supervisor)
      • +1
        Неплохо, всячески одобряю. А поиск и модификация в распарсеном конфиге по аналогу XPath есть? Чтобы если нужно поменять определенное поле и записать результат обратно не приходилось все дерево вручную перебирать?
        • 0
          Вручную ничего делать не нужно — достаточно вызвать config.load(), исправить нужное поле в объекте и вызвать config.save()
          Рекурсивного поиска по дереву нет, но и работа с синтаксическим деревом для конечного «пользователя» не предполагается.
          • +3
            Наверное, я не совсем верно выразился. Есть у нас, к примеру, конфиг nginx'а с 5-ю серверами (это я к примеру, не пугайтесь). И нужно нам у сервера с именем «foo» поменять настройку с именем «bar». В конфиге список серверов, у каждого список настрек. Искать кодом по загруженному дереву сильно увеличивает количество действий для выполнения тривиальных операций. Я в своем минимальном велосипеде предусмотрел поиск вида XPath — задается строчка с указанием какие элементы интересуют — в ответ получаем один или несколько объектов, которые уже можно менять.
            • +1
              Мне кажется, такая функциональность не будет являться неотемлемой частью библиотеки. Можно взять любой клон LINQ to objects и использовать есть, благо объекты — самые обыкновенные
              • 0
                Сложный вопрос. Во всех XML парсерах XPath есть — никто для этого не использует LINQ. Но в целом тоже вариант — про LINQ для питона я не знал :).
                • 0
                  XPath там есть из-за исключительно строгого синтаксиса XML :)
  • +2
    Автор, Вы меня простите, но чем Вам не нравится конфиг BIND? Один из лучших форматов конфигов, что я видел, вообще-то.
    Care to explain.
    • 0
      Он похож на конфиг nginx, но при этом отличается точками с запятой. Похож на конфиг DHCPD, но все равно отличается. И вообще по моему личному мнению, использование YAML/XML/JSON везде-везде сэкономило бы всем кучу нервов.
      • +1
        Использование любого единого подхода сэкономило бы кучу нервов.
        Переход с древнего формата bind'а на юный yaml, который появился, когда bind уже седой и с палочкой — потратил бы всем нервов.

        Следовательно, более логичным и полезным для нервов было бы переход всего-всего как раз на формат bind. Не смотря на то, что там есть точки с запятой. В XML'е вообще стрелки, и ничего же.
    • +1
      Я могу объяснить. Он лучший для человека. Но если нам нужен скрипт, который зайдет на сотню серверов и поменяет настроки bind — то корректное распарсивание становится проблемой. Конечно, ее можно решить regexp, но это порождает ряд сложностей: во-первых, regexp легко писать но сложно читать — когда через полгода нужно будет что-то поменять — придется писать заного. А во-вторых, с regexp есть некие риски — в конфиге может найтись что-то неучтенное, что «неожиданно» поматчится.
      • +4
        Вообще-то, корректному (пусть и неполному) распарсиванию конфига посвящён Bison/YACC howto. Конфиг BIND нельзя парсить регекспами.
        Как и XML.
        You can't parse [X]HTML with regex. Because HTML can't be parsed by regex. Regex is not a tool that can be used to correctly parse HTML. As I have answered in HTML-and-regex questions here so many times before, the use of regex will not allow you to consume HTML. Regular expressions are a tool that is insufficiently sophisticated to understand the constructs employed by HTML. HTML is not a regular language and hence cannot be parsed by regular expressions. Regex queries are not equipped to break down HTML into its meaningful parts. so many times but it is not getting to me. Even enhanced irregular regular expressions as used by Perl are not up to the task of parsing HTML. You will never make me crack. HTML is a language of sufficient complexity that it cannot be parsed by regular expressions. Even Jon Skeet cannot parse HTML using regular expressions. Every time you attempt to parse HTML with regular expressions, the unholy child weeps the blood of virgins, and Russian hackers pwn your webapp. Parsing HTML with regex summons tainted souls into the realm of the living. HTML and regex go together like love, marriage, and ritual infanticide. The cannot hold it is too late. The force of regex and HTML together in the same conceptual space will destroy your mind like so much watery putty. If you parse HTML with regex you are giving in to Them and their blasphemous ways which doom us all to inhuman toil for the One whose Name cannot be expressed in the Basic Multilingual Plane, he comes. HTML-plus-regexp will liquify the n​erves of the sentient whilst you observe, your psyche withering in the onslaught of horror. Rege̿̔̉x-based HTML parsers are the cancer that is killing StackOverflow it is too late it is too late we cannot be saved the trangession of a chi͡ld ensures regex will consume all living tissue (except for HTML which it cannot, as previously prophesied) dear lord help us how can anyone survive this scourge using regex to parse HTML has doomed humanity to an eternity of dread torture and security holes using regex as a tool to process HTML establishes a breach between this world and the dread realm of c͒ͪo͛ͫrrupt entities (like SGML entities, but more corrupt) a mere glimpse of the world of reg​ex parsers for HTML will ins​tantly transport a programmer's consciousness into a world of ceaseless screaming, he comes, the pestilent slithy regex-infection wil​l devour your HT​ML parser, application and existence for all time like Visual Basic only worse he comes he comes do not fi​ght he com̡e̶s, ̕h̵i​s un̨ho͞ly radiańcé destro҉ying all enli̍̈́̂̈́ghtenment, HTML tags lea͠ki̧n͘g fr̶ǫm ̡yo​͟ur eye͢s̸ ̛l̕ik͏e liq​uid pain, the song of re̸gular exp​ression parsing will exti​nguish the voices of mor​tal man from the sp​here I can see it can you see ̲͚̖͔̙î̩́t̲͎̩̱͔́̋̀ it is beautiful t​he final snuffing of the lie​s of Man ALL IS LOŚ͖̩͇̗̪̏̈́T ALL I​S LOST the pon̷y he comes he c̶̮omes he comes the ich​or permeates all MY FACE MY FACE ᵒh god no NO NOO̼O​O NΘ stop the an​*̶͑̾̾​̅ͫ͏̙̤g͇̫͛͆̾ͫ̑͆l͖͉̗̩̳̟̍ͫͥͨe̠̅s ͎a̧͈͖r̽̾̈́͒͑e n​ot rè̑ͧ̌aͨl̘̝̙̃ͤ͂̾̆ ZA̡͊͠͝LGΌ ISͮ̂҉̯͈͕̹̘̱ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘ ̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ
        Оригинал
        • +1
          Сразу видно, вы с академическим интересом к делу подходите. К сожалению, админам и разработчикам контекстная зависимость грамматики не очень интересна — им работу делать надо. И писать интерпретатор на Bison/Yacc никто не будет. А вт использовать библиотеку на python — будет. Отсюда и начинания топикстартера.

          Пассаж про регекспы не понял — я писал, что их не хотят применять из-за потенциальных проблем и поэтому используют такие штуки, как рассказал автор. Вы пересказываете мне то, что с регекспами будут проблемы. Зачем?
          • +1
            Писать его не надо, он уже написан. И причём много раз.
            Регекспами конфиг BIND распарсить принципиально нельзя. Просто нельзя и всё, тут не идёт даже о проблемах. Проблема одна — это невозможно.
    • 0
      Конфиг из ада у syslog-ng
  • +1
    И ещё. Почему в репозитории Debian-пакетов запрещены листинги? Чтобы всем удобнее было, да?
  • +1
    И опять я. Выкиньте уже наконец CDBS! Иначе zомби догонят Вас и съедят мозг :)
  • +3
    А чем оно лучше/хуже/иначе чем Augeas, к которому прилагается 160 стоковых линз (парсеров/конструкторов конфига)?
    • +1
      Обидно — когда я решал здачу модификации конфигов, я потратил довольно много времени, чтобы найти существующие решения. И, не найдя, сделал свой велосипед. А тут оказывается этих существующих решений — куча. Что-то не так с моими заклинаниями поиска в гугле O_O.

      С ходу могу сказать, что решение автора на чистом python, что позволяет его использовать на машинах с минимумом зависимостей и на всяких странных штуках с ARM процессорами или Windows RT. Augeas требует компиляции.
      • +1
        Что-то не так с моими заклинаниями поиска в гугле O_O.

        Оно просто так не гуглится =). Можно выйти по ссылкам с результатов поиска по кейворду «Configuration management» на Puppet, Chef, CFEngine и т.п., и уже оттуда — на утилиты вроде Augeas.
      • +1
        А ещё есть libconfigmodel-perl.
  • +1
    давно не видел такого хорошего Python кода. очень приятно.
  • 0
    Раз пошла такая пьянка, то подскажите такого же, но например на haskell.
    • 0
      «На» или «для»? Если второе, то есть биндинги к Augeas. Да и язык описания линз хаскелеводу страшным не покажется.
      • 0
        Конечно лучше «на», import blablabla и все готово. Но любопытны любые варианты, в жизни всякое бывает.
        • 0
          Про «на» — не знаю, биндинги к Augeas (через FFI, насколько я понял) доступны тут: trac.haskell.org/augeas/
  • 0
    я просто оставлю здесь эту ссылку…
    augeas.net/
    UPD
    Упс… уже написали. Извиняюсь
  • 0
    А детектор конфига есть?
  • 0
    А file lock предусмотрен?

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