Вам когда-нибудь приходилось парсить и программно вносить изменения в чужие конфигурационные файлы? А в файлы с ненормальными форматами вроде того, что у 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
Автор: hardex