pax_global_header00006660000000000000000000000064125362711370014521gustar00rootroot0000000000000052 comment=49a208904b95b042e3bc90c377aecc1a39cc9d58 reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/000077500000000000000000000000001253627113700210335ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/.gitignore000066400000000000000000000003231253627113700230210ustar00rootroot00000000000000*.pyc *.pyo .DS_Store *~ *.sublime-workspace .coverage debian/python-reconfigure* debian/changelog dist/*.rpm dist/*.deb dist/*.gz *.egg* build/* debian/files debian/*stamp* docs/build nosetests* .idea *.whl reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/.travis.yml000066400000000000000000000001651253627113700231460ustar00rootroot00000000000000language: python python: - "3.3" - "2.7" - "2.6" install: "pip install -r requirements.txt" script: nosetests reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/Makefile000066400000000000000000000042411253627113700224740ustar00rootroot00000000000000PYTHON=`which python` DESTDIR=/ BUILDIR=$(CURDIR)/debian/reconfigure RPMTOPDIR=$(CURDIR)/build PROJECT=reconfigure DEBPROJECT=python-reconfigure VERSION=`python -c "from reconfigure import __version__; print __version__"` PREFIX=/usr PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = DOCBUILDDIR = docs/build DOCSOURCEDIR = docs/source ALLSPHINXOPTS = -d $(DOCBUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(DOCSOURCEDIR) all: build: doc: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DOCBUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." cdoc: rm -rf $(DOCBUILDDIR)/* make doc install: $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) --prefix $(PREFIX) rpm: build tgz rm -rf dist/*.rpm cat dist/$(PROJECT).spec.in | sed s/__VERSION__/$(VERSION)/g > $(PROJECT).spec mkdir -p build/SOURCES || true cp dist/$(PROJECT)*.tar.gz build/SOURCES rpmbuild --define '_topdir $(RPMTOPDIR)' -bb $(PROJECT).spec mv build/RPMS/noarch/$(PROJECT)*.rpm dist rm $(PROJECT).spec deb: build tgz rm -rf dist/*.deb cat debian/changelog.in | sed s/__VERSION__/$(VERSION)/g | sed "s/__DATE__/$(DATE)/g" > debian/changelog cp dist/$(PROJECT)*.tar.gz .. rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(DEBPROJECT)_$$1\.orig\.tar\.gz/' ../* dpkg-buildpackage -b -rfakeroot -us -uc mv ../$(DEBPROJECT)*.deb dist/ rm ../$(DEBPROJECT)*.orig.tar.gz rm ../$(DEBPROJECT)*.changes rm debian/changelog upload-deb: deb scp dist/*.deb root@ajenti.org:/srv/repo/ng/debian ssh root@ajenti.org /srv/repo/rebuild-debian.sh upload-rpm: rpm scp dist/*.rpm root@ajenti.org:/srv/repo/ng/centos/6 ssh root@ajenti.org /srv/repo/rebuild-centos.sh upload-rpm7: rpm scp dist/*.rpm root@ajenti.org:/srv/repo/ng/centos/7 ssh root@ajenti.org /srv/repo/rebuild-centos7.sh upload-tgz: tgz $(PYTHON) setup.py sdist bdist_wheel upload tgz: build rm dist/*.tar.gz || true $(PYTHON) setup.py sdist clean: $(PYTHON) setup.py clean rm -rf $(DOCBUILDDIR)/* rm -rf build/ debian/$(PROJECT)* debian/*stamp* debian/files MANIFEST *.egg-info find . -name '*.pyc' -delete test: nosetests -v reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/README.rst000066400000000000000000000106741253627113700225320ustar00rootroot00000000000000==================================================== Reconfigure - Python object mapping for config files ==================================================== .. image:: https://travis-ci.org/Eugeny/reconfigure.png `Browse API on SourceGraph `_ ---------- Quickstart ---------- :: >>> 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 This is actually a shortcut to:: >>> from reconfigure.parsers import SSVParser >>> from reconfigure.builders import BoundBuilder >>> from reconfigure.items.fstab import FSTabData >>> content = open('/etc/fstab').read() >>> syntax_tree = SSVParser().parse(content) >>> syntax_tree >>> 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 = 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" } ] } Parsers and builders can be paired in almost any possible combination. Reconfigure can be easily extended with your own parsers and builders - read the docs! Supported configs: * 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``) Included parsers: * 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``) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/000077500000000000000000000000001253627113700222555ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/changelog.in000077700000000000000000000000001253627113700271042../docs/CHANGELOGustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/compat000066400000000000000000000000021253627113700234530ustar00rootroot000000000000008 reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/control000066400000000000000000000007321253627113700236620ustar00rootroot00000000000000Source: python-reconfigure Section: python Priority: optional Maintainer: Eugeny Pankov Build-Depends: debhelper (>=8.0.0), python-support (>= 0.6), cdbs (>= 0.4.49) XS-Python-Version: >=2.6 Standards-Version: 3.9.1 Package: python-reconfigure Architecture: all Homepage: http://eugeny.github.com/reconfigure XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, python-chardet Description: Simple config file management library reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/copyright000077700000000000000000000000001253627113700266412../docs/COPYRIGHTustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/postinst000066400000000000000000000000261253627113700240610ustar00rootroot00000000000000#!/bin/sh #DEBHELPER# reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/prerm000066400000000000000000000000271253627113700233240ustar00rootroot00000000000000#!/bin/sh #DEBHELPER# reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/pyversions000066400000000000000000000000051253627113700244140ustar00rootroot000000000000002.6- reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/rules000077500000000000000000000003651253627113700233410ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- DEB_PYTHON_SYSTEM := pysupport include /usr/share/cdbs/1/rules/debhelper.mk include /usr/share/cdbs/1/class/python-distutils.mk clean:: rm -rf build build-stamp configure-stamp build/ MANIFEST dh_clean reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/debian/watch000066400000000000000000000000121253627113700232770ustar00rootroot00000000000000version=3 reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/dist/000077500000000000000000000000001253627113700217765ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/dist/reconfigure.spec.in000066400000000000000000000015061253627113700255710ustar00rootroot00000000000000%define name reconfigure %define version __VERSION__ %define unmangled_version __VERSION__ %define release 1 Summary: The server administration panel Name: %{name} Version: %{version} Release: %{release} Source0: %{name}-%{version}.tar.gz License: LGPLv3 Group: Development/Libraries BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot Prefix: %{_prefix} BuildArch: noarch Vendor: Eugene Pankov Url: http://ajenti.org/ requires: python-chardet %description Easy config file management %prep %setup -n %{name}-%{unmangled_version} -n %{name}-%{unmangled_version} %build python setup.py build %install python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --prefix=/usr %clean rm -rf $RPM_BUILD_ROOT %files -f INSTALLED_FILES %defattr(-,root,root) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/000077500000000000000000000000001253627113700217635ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/CHANGELOG000066400000000000000000000002211253627113700231700ustar00rootroot00000000000000python-reconfigure (__VERSION__) UNRELEASED; urgency=low * Initial release. -- Eugeny Pankov Thu, 7 Feb 2013 00:12:00 +0300 reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/COPYRIGHT000066400000000000000000000002031253627113700232510ustar00rootroot00000000000000Copyright (c) 2012-2013 The Ajenti Team * Eugeny Pankov & contributors LICENSED UNDER LGPLv3 See LICENSE. reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/LICENSE000066400000000000000000000167431253627113700230030ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/Makefile000066400000000000000000000107621253627113700234310ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Ajenti.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Ajenti.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Ajenti" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Ajenti" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/000077500000000000000000000000001253627113700232635ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/conf.py000066400000000000000000000020711253627113700245620ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys import os sys.path.insert(0, os.path.abspath('../..')) extensions = ['sphinx.ext.autodoc', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] # 'sphinx.ext.intersphinx'] templates_path = ['_templates'] source_suffix = '.rst' #source_encoding = 'utf-8-sig' master_doc = 'index' project = u'Reconfigure' copyright = u'2013, Eugeny Pankov' version = '1.0' release = '1.0a1' exclude_patterns = [] add_function_parentheses = True #pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- #html_theme = 'air' #html_theme_options = {} #html_theme_path = ['../../../sphinx-themes'] html_title = 'Reconfigure documentation' html_short_title = 'Reconfigure docs' #html_logo = None #html_favicon = None html_static_path = ['_static'] htmlhelp_basename = 'Reconfiguredoc' # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/000077500000000000000000000000001253627113700242135ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/architecture.rst000066400000000000000000000002271253627113700274300ustar00rootroot00000000000000Architecture ************ .. toctree:: :maxdepth: 2 architecture/trees architecture/bound architecture/components architecture/config reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/architecture/000077500000000000000000000000001253627113700266755ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/architecture/bound.rst000066400000000000000000000101201253627113700305300ustar00rootroot00000000000000.. _Bound data: Bound Data ********** Bound data (:class:`reconfigure.items.bound.BoundData`) is a special class that can be subclassed and stuffed with properties, which will act as proxies to an underlying :ref:`Node tree`. This can be confusing, so let's go with an example:: >>> from reconfigure.nodes import Node, PropertyNode >>> from reconfigure.items.bound import BoundData >>> >>> node = Node('test') >>> node.append(PropertyNode('name', 'Alice')) >>> node.append(PropertyNode('age', '25')) >>> node.append(PropertyNode('gender', 'f')) >>> print node (test) name = Alice age = 25 gender = f Here we have a very simple :ref:`Node tree `. Note that all values are ``str`` and the ``gender`` is coded in a single character (we have probably parsed this tree from some .ini file). Now let's define a BoundData class:: >>> class HumanData (BoundData): ... pass ... >>> HumanData.bind_property('name', 'name') >>> HumanData.bind_property('age', 'age', getter=int, setter=str) >>> HumanData.bind_property('gender', 'gender', ... getter=lambda x: 'Male' if x == 'm' else 'Female', ... setter=lambda x: 'm' if x == 'Male' else 'f') >>> human = HumanData(node) >>> human <__main__.MyData object at 0x114ddd0> >>> print human { "gender": "Female", "age": 25, "name": "Alice" } First, we've defined our ``BoundData`` subclass. Then, we have defined three properties in it: * ``name`` is the simplest property, it's directly bound to "name" child ``PropertyNode`` * ``age`` also has a getter and setter. These are invoked when the property is read or written. In this case, we use ``int()`` to parse a number from the node tree and ``str()`` to stringify it when writing back. * ``gender`` is similar to ``age`` but has more complex getter and setter that transform "m" and "f" to a human-readable description. When the properties are mutated, the modifications are applied to Node tree immediately and vice versa:: >>> human.age 25 >>> human.age = 30 >>> node.get('age').value '30' >>> node.get('age').value = 27 >>> human.age 27 Using collections ================= Let's try a more complex node tree:: >>> nodes = Node('', ... Node('Alice', ... PropertyNode('Phone', '1234-56-78') ... ), ... Node('Bob', ... PropertyNode('Phone', '8765-43-21') ... ) ... ) >>> print nodes () (Alice) Phone = 1234-56-78 (Bob) Phone = 8765-43-21 Bound data classes:: >>> class PersonData (BoundData): ... def template(self, name, phone): ... return Node(name, ... PropertyNode('Phone', phone) ... ) ... >>> class PhonebookData (BoundData): ... pass ... >>> PersonData.bind_property('Phone', 'phone') >>> PersonData.bind_name('name') >>> >>> PhonebookData.bind_collection('entries', item_class=PersonData) >>> >>> phonebook = PhonebookData(nodes) >>> print phonebook { "entries": [ { "phone": "1234-56-78", "name": "Alice" }, { "phone": "8765-43-21", "name": "Bob" } ] } Here, ``bind_collection`` method is used to create a collection property from child nodes. ``item_class`` class will be used to wrap these nodes. Alternatively, you can employ :class:`reconfigure.items.bound.BoundDictionary` class to create a dict-like property:: >>> PhonebookData.bind_collection('entries', collection_class=BoundDictionary, item_class=PersonData, key=lambda x: x.name) >>> print phonebook { "entries": { "Bob": { "phone": "8765-43-21", "name": "Bob" }, "Alice": { "phone": "1234-56-78", "name": "Alice" } } }reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/architecture/components.rst000066400000000000000000000104231253627113700316140ustar00rootroot00000000000000Components ********** .. _parser: Parsers ======= Parsers are :class:`reconfigure.parsers.BaseParser` subclasses which transform :ref:`raw config content ` into :ref:`node trees ` and vice versa Making your own parser is as easy as subclassing :class:`reconfigure.parsers.BaseParser` and overriding ``parse`` and ``stringify`` methods. .. _includer: Includers ========= Includers are used to handle the "include" directives in config files. Includers assemble the config file by finding the included files and parsing them and attaching them to the :ref:`node tree ` of the main config. Reconfigure keeps track of which node belongs to which file by setting ``origin`` attribute on the included nodes Example of includer in action: >>> from reconfigure.parsers import * >>> from reconfigure.includers import * >>> parser = IniFileParser() >>> includer = SupervisorIncluder(parser) >>> nodes = parser.parse(open('/etc/supervisor/supervisord.conf').read()) >>> print nodes (None) (unix_http_server) file = /var/run//supervisor.sock ((the path to the socket file)) chmod = 0700 (sockef file mode (default 0700)) (supervisord) logfile = /var/log/supervisor/supervisord.log ((main log file;default $CWD/supervisord.log)) pidfile = /var/run/supervisord.pid ((supervisord pidfile;default supervisord.pid)) childlogdir = /var/log/supervisor (('AUTO' child log dir, default $TEMP)) (rpcinterface:supervisor) supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface (supervisorctl) serverurl = unix:///var/run//supervisor.sock (use a unix:// URL for a unix socket) (include) files = /etc/supervisor/conf.d/*.conf Note the "include" node in the end. Now we'll run an includer over this tree:: >>> nodes = includer.compose('/etc/supervisor/supervisord.conf', nodes) >>> print nodes (None) (unix_http_server) file = /var/run//supervisor.sock ((the path to the socket file)) chmod = 0700 (sockef file mode (default 0700)) (supervisord) logfile = /var/log/supervisor/supervisord.log ((main log file;default $CWD/supervisord.log)) pidfile = /var/run/supervisord.pid ((supervisord pidfile;default supervisord.pid)) childlogdir = /var/log/supervisor (('AUTO' child log dir, default $TEMP)) (rpcinterface:supervisor) supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface (supervisorctl) serverurl = unix:///var/run//supervisor.sock (use a unix:// URL for a unix socket) /etc/supervisor/conf.d/*.conf (program:test) command = cat Note how the include directive has turned into a junction point (:class:`reconfigure.nodes.IncludeNode`) and content of included files was parsed and attached. Calling ``decompose`` method will split the tree back into separate files: >>> includer.decompose(nodes) { '/etc/supervisor/conf.d/1.conf': , '/etc/supervisor/supervisord.conf': } Writing your own includer ------------------------- If you're up to writing a custom includer, take a look at :class:`reconfigure.includers.AutoIncluder`. It already implements the tree-walking and attachment logic, so you only need to implement two methods: * ``is_include(node)``: should check if the ``node`` is an include directive for this file format, and if it is, return a glob (wildcard) or path to the included files * ``remove_include(include_node)``: given an :class:`reconfigure.nodes.IncludeNode`, should transform it back into file-format-specific include directive and return it (as a :ref:`node tree ` chunk) .. _builder: Builders ======== Builders transform :ref:`node trees ` into :ref:`data trees `. To write your own builder, subclass :class:`reconfigure.builders.BaseBuilder` and override ``build`` and ``unbuild`` methods.reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/architecture/config.rst000066400000000000000000000022371253627113700307000ustar00rootroot00000000000000.. _reconfig: Reconfig objects **************** :class:`reconfigure.config.Reconfig` objects are pre-set pipelines connecting :ref:`Parsers `, :ref:`Includers ` and :ref:`Builders ` Reconfigure comes with many Reconfig objects out-of-the-box - see :ref:`reconfigure.configs` Writing your Reconfig subclass ============================== Use the following pattern:: class Config (Reconfig): """ """ def __init__(self, **kwargs): k = { 'parser': (), 'includer': (), 'builder': BoundBuilder(), } k.update(kwargs) Reconfig.__init__(self, **k) Example:: class SupervisorConfig (Reconfig): """ ``/etc/supervisor/supervisord.conf`` """ def __init__(self, **kwargs): k = { 'parser': IniFileParser(), 'includer': SupervisorIncluder(), 'builder': BoundBuilder(SupervisorData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/architecture/trees.rst000066400000000000000000000144611253627113700305570ustar00rootroot00000000000000Trees ***** Reconfigure operates with three types of data: * Raw config text * Syntax tree * Data tree .. _raw-config: Config text =========== This is a raw content, as read from the config file. It is fed to :ref:`Parsers ` to produce the :ref:`Syntax trees`. .. _node-tree: Syntax trees ========== Syntax tree is an object tree built from :class:`reconfigure.nodes.Node` objects, representing the syntax structure of the file. This is very similar to Abstract Syntax Trees. Syntax trees are produced by :ref:`Parser` classes. Example:: >>> text = open('/etc/samba/smb.conf').read() >>> text '#\n# Sample configuration file for the Samba suite for Debian GNU/Linux.\ ... >>> from reconfigure.parsers import IniFileParser >>> parser = IniFileParser() >>> node_tree = parser.parse(text) >>> print node_tree (None) (global) workgroup = WORKGROUP server string = %h server (Samba, Ubuntu) dns proxy = no log file = /var/log/samba/log.%m max log size = 1000 syslog = 0 panic action = /usr/share/samba/panic-action %d encrypt passwords = true passdb backend = tdbsam obey pam restrictions = yes unix password sync = yes passwd program = /usr/bin/passwd %u passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* . pam password change = yes map to guest = bad user usershare allow guests = yes (printers) comment = All Printers browseable = no path = /var/spool/samba printable = yes guest ok = no read only = yes create mask = 0700 >>> node_tree >>> node_tree.children[0] >>> node_tree.children[0].name 'global' >>> node_tree.children[0].children[0] >>> node_tree.children[0].children[0].name 'workgroup' >>> node_tree.children[0].children[0].value 'WORKGROUP' :class:`reconfigure.nodes.Node` reference page contains more information on how to manipulate node trees. Parsers work both ways - you can call ``stringify()`` and get the text representation back. Even more, you can feed the node tree to *another* parser and get the config in other format:: >>> from reconfigure.parsers import JsonParser >>> json_parser = JsonParser() >>> json_parser.stringify(node_tree) >>> print json_parser.stringify(node_tree) { "global": { "encrypt passwords": "true", "pam password change": "yes", "passdb backend": "tdbsam", "passwd program": "/usr/bin/passwd %u", ... }, "print$": { "comment": "Printer Drivers", "path": "/var/lib/samba/printers", "read only": "yes", ... Syntax trees might look useful to you, but they are not nearly as cool as :ref:`Data trees ` .. _data-tree: Data trees ========== Data tree represents the actual, meaningful ideas stored in the config. Straight to example:: >>> from reconfigure.builders import BoundBuilder >>> from reconfigure.items.samba import SambaData >>> builder = BoundBuilder(SambaData) >>> data_tree = builder.build(node_tree) >>> data_tree { "global": { "server_string": "%h server (Samba, Ubuntu)", "workgroup": "WORKGROUP", "interfaces": "", "bind_interfaces_only": true, "security": "user", "log_file": "/var/log/samba/log.%m" }, "shares": [ { "comment": "All Printers", "browseable": false, "create_mask": "0700", "name": "printers", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/spool/samba" }, { "comment": "Printer Drivers", "browseable": true, "create_mask": "0744", "name": "print$", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/lib/samba/printers" } ] } >>> data_tree.shares >>> [_.path for _ in data_tree.shares] ['/var/spool/samba', '/var/lib/samba/printers'] Data trees may consist of any Python objects, but the common approach is to use :ref:`Bound data` Data trees can be manipulated as you wish:: >>> from reconfigure.items.samba import ShareData >>> share = ShareData() >>> share.path = '/home/user' >>> share.comment = 'New share' >>> data_tree.shares.append(share) >>> data_tree { .... "shares": [ { "comment": "All Printers", "browseable": false, "create_mask": "0700", "name": "printers", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/spool/samba" }, { "comment": "Printer Drivers", "browseable": true, "create_mask": "0744", "name": "print$", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/lib/samba/printers" }, { "comment": "New share", "browseable": true, "create_mask": "0744", "name": "share", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/home/user" } ] After you're done with the modifications, the data tree must be converted back to the node tree:: >>> node_tree = builder.unbuild(data_tree) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/docs/quickstart.rst000066400000000000000000000054071253627113700271450ustar00rootroot00000000000000Quickstart ========== Adding lines to ``fstab``:: >>> 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 Changing Samba settings:: >>> from reconfigure.configs import SambaConfig >>> config = SambaConfig(path='/etc/samba/smb.conf') >>> config.load() >>> print config.tree.shares [ { "comment": "All Printers", "browseable": false, "create_mask": "0700", "name": "printers", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/spool/samba" }, { "comment": "Printer Drivers", "browseable": true, "create_mask": "0744", "name": "print$", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/lib/samba/printers" } ] >>> config.tree.shares[0].guest_ok = True >>> print config.tree.shares [ { "comment": "All Printers", "browseable": false, "create_mask": "0700", "name": "printers", "directory_mask": "0755", "read_only": true, "guest_ok": true, "path": "/var/spool/samba" }, { "comment": "Printer Drivers", "browseable": true, "create_mask": "0744", "name": "print$", "directory_mask": "0755", "read_only": true, "guest_ok": false, "path": "/var/lib/samba/printers" } ] >>> config.save() reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/index.rst000066400000000000000000000022461253627113700251300ustar00rootroot00000000000000.. Reconfigure documentation master file, created by sphinx-quickstart on Mon Aug 15 15:18:35 2011. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Reconfigure's documentation! ======================================= Links: ------ * `Source at GitHub `_ * Questions? `Email me `_ * `PyPI `_ `Browse API on SourceGraph `_ .. image:: https://sourcegraph.com/api/repos/github.com/Eugeny/reconfigure/badges/funcs.png .. image:: https://sourcegraph.com/api/repos/github.com/Eugeny/reconfigure/badges/status.png Contents: --------- .. toctree:: :maxdepth: 2 docs/quickstart docs/architecture API Reference: -------------- .. toctree:: :maxdepth: 2 ref/reconfigure.configs ref/reconfigure.parsers ref/reconfigure.nodes ref/reconfigure.includers ref/reconfigure.builders ref/reconfigure.items.bound Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/000077500000000000000000000000001253627113700240375ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/reconfigure.builders.rst000066400000000000000000000001601253627113700307060ustar00rootroot00000000000000reconfigure.builders ******************** .. automodule:: reconfigure.builders :members: :undoc-members: reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/reconfigure.configs.rst000066400000000000000000000002071253627113700305270ustar00rootroot00000000000000.. _reconfigure.configs: reconfigure.configs ******************* .. automodule:: reconfigure.configs :members: :undoc-members: reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/reconfigure.includers.rst000066400000000000000000000001631253627113700310700ustar00rootroot00000000000000reconfigure.includers ********************* .. automodule:: reconfigure.includers :members: :undoc-members: reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/reconfigure.items.bound.rst000066400000000000000000000001711253627113700313260ustar00rootroot00000000000000reconfigure.items.bound *********************** .. automodule:: reconfigure.items.bound :members: :undoc-members: reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/reconfigure.nodes.rst000066400000000000000000000001471253627113700302120ustar00rootroot00000000000000reconfigure.nodes ***************** .. automodule:: reconfigure.nodes :members: :undoc-members: reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/docs/source/ref/reconfigure.parsers.rst000066400000000000000000000001551253627113700305600ustar00rootroot00000000000000reconfigure.parsers ******************* .. automodule:: reconfigure.parsers :members: :undoc-members: reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure.sublime-project000066400000000000000000000000551253627113700263710ustar00rootroot00000000000000{ "folders": [ { "path": "." } ] } reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/000077500000000000000000000000001253627113700233435ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/__init__.py000066400000000000000000000000271253627113700254530ustar00rootroot00000000000000__version__ = "0.1.74" reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/builders/000077500000000000000000000000001253627113700251545ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/builders/__init__.py000066400000000000000000000003321253627113700272630ustar00rootroot00000000000000""" Builders are used to convert Node Tree to Data Tree """ from reconfigure.builders.base import BaseBuilder from reconfigure.builders.bound import BoundBuilder __all__ = [ 'BaseBuilder', 'BoundBuilder', ] reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/builders/base.py000066400000000000000000000005401253627113700264370ustar00rootroot00000000000000class BaseBuilder (object): """ A base class for builders """ def build(self, tree): """ :param tree: :class:`reconfigure.nodes.Node` tree :returns: Data tree """ def unbuild(self, tree): """ :param tree: Data tree :returns: :class:`reconfigure.nodes.Node` tree """ reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/builders/bound.py000066400000000000000000000007011253627113700266330ustar00rootroot00000000000000from reconfigure.builders.base import BaseBuilder class BoundBuilder (BaseBuilder): """ A builder that uses :class:`reconfigure.items.bound.BoundData` to build stuff :param root_class: a ``BoundData`` class that used as processing root """ def __init__(self, root_class): self.root_class = root_class def build(self, nodetree): return self.root_class(nodetree) def unbuild(self, tree): pass reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/builders/bound_tests.py000066400000000000000000000032451253627113700300630ustar00rootroot00000000000000from reconfigure.items.bound import BoundData from reconfigure.nodes import Node, PropertyNode import unittest class BoundDataTest (unittest.TestCase): def test_bind_property(self): class TestBoundData (BoundData): pass TestBoundData.bind_property('prop', 'dataprop', getter=lambda x: 'd' + x, setter=lambda x: x[1:]) n = Node('name', children=[ PropertyNode('prop', 'value') ]) d = TestBoundData(n) self.assertEqual(d.dataprop, 'dvalue') d.dataprop = 'dnew' self.assertEqual(d.dataprop, 'dnew') self.assertEqual(n.get('prop').value, 'new') def test_bind_collection(self): class TestBoundData (BoundData): pass class TestChildData (BoundData): def template(self): return Node('', children=[PropertyNode('value', None)]) TestBoundData.bind_collection('items', item_class=TestChildData, selector=lambda x: x.name != 'test') TestChildData.bind_property('value', 'value') n = Node('name', children=[ Node('1', children=[PropertyNode('value', 1)]), Node('2', children=[PropertyNode('value', 2)]), Node('test', children=[PropertyNode('value', 3)]), Node('3', children=[PropertyNode('value', 3)]), ]) d = TestBoundData(n) self.assertEqual(d.items[0].value, 1) self.assertEqual(len(d.items), 3) c = TestChildData() c.value = 4 d.items.append(c) self.assertEqual(len(d.items), 4) self.assertEqual(d.items[-1].value, 4) if __name__ == '__main__': unittest.main() reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/000077500000000000000000000000001253627113700247735ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/__init__.py000066400000000000000000000031001253627113700270760ustar00rootroot00000000000000""" Configs are ready-to-use objects that link together Parsers, Includers and Builders to provide direct conversion between config files and Data tree. """ from reconfigure.configs.base import Reconfig from reconfigure.configs.ajenti import AjentiConfig from reconfigure.configs.bind9 import BIND9Config from reconfigure.configs.crontab import CrontabConfig from reconfigure.configs.ctdb import CTDBConfig, CTDBNodesConfig, CTDBPublicAddressesConfig from reconfigure.configs.csf import CSFConfig from reconfigure.configs.dhcpd import DHCPDConfig from reconfigure.configs.exports import ExportsConfig from reconfigure.configs.fstab import FSTabConfig from reconfigure.configs.group import GroupConfig from reconfigure.configs.hosts import HostsConfig from reconfigure.configs.iptables import IPTablesConfig from reconfigure.configs.netatalk import NetatalkConfig from reconfigure.configs.nsd import NSDConfig from reconfigure.configs.passwd import PasswdConfig from reconfigure.configs.resolv import ResolvConfig from reconfigure.configs.samba import SambaConfig from reconfigure.configs.squid import SquidConfig from reconfigure.configs.supervisor import SupervisorConfig __all__ = [ 'Reconfig', 'AjentiConfig', 'BIND9Config', 'CrontabConfig', 'CTDBConfig', 'CTDBNodesConfig', 'CTDBPublicAddressesConfig', 'DHCPDConfig', 'ExportsConfig', 'FSTabConfig', 'GroupConfig', 'HostsConfig', 'IPTablesConfig', 'NetatalkConfig', 'NSDConfig', 'PasswdConfig', 'ResolvConfig', 'SambaConfig', 'SquidConfig', 'SupervisorConfig', ] reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/ajenti.py000066400000000000000000000006451253627113700266240ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import JsonParser from reconfigure.builders import BoundBuilder from reconfigure.items.ajenti import AjentiData class AjentiConfig (Reconfig): def __init__(self, **kwargs): k = { 'parser': JsonParser(), 'builder': BoundBuilder(AjentiData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/base.py000066400000000000000000000052771253627113700262720ustar00rootroot00000000000000import chardet import six import sys class Reconfig (object): """ Basic config class. Derivatives normally only need to override the constructor. Config data is loaded either from ``path`` or from ``content`` :param parser: overrides the Parser instance :param includer: overrides the Includer instance :param builder: overrides the Builder instance :param path: config file path. Not compatible with ``content`` :param content: config file content. Not compatible with ``path`` """ def __init__(self, parser=None, includer=None, builder=None, path=None, content=None): self.parser = parser self.builder = builder self.includer = includer if self.includer is not None: if not self.includer.parser: self.includer.parser = self.parser if path: self.origin = path self.content = None else: self.origin = None self.content = content def load(self): """ Loads the config data, parses and builds it. Sets ``tree`` attribute to point to Data tree. """ if self.origin: self.content = open(self.origin, 'r').read() self.encoding = 'utf8' if (six.PY3 and isinstance(self.content, bytes)) or \ (six.PY2 and isinstance(self.content, str)): try: self.content = self.content.decode('utf8') except (UnicodeDecodeError, AttributeError): self.encoding = chardet.detect(self.content)['encoding'] self.content = self.content.decode(self.encoding) self.nodetree = self.parser.parse(self.content) if self.includer is not None: self.nodetree = self.includer.compose(self.origin, self.nodetree) if self.builder is not None: self.tree = self.builder.build(self.nodetree) return self def save(self): """ Unbuilds, stringifies and saves the config. If the config was loaded from string, returns ``{ origin: data }`` dict """ tree = self.tree if self.builder is not None: nodetree = self.builder.unbuild(tree) or self.nodetree if self.includer is not None: nodetree = self.includer.decompose(nodetree) else: nodetree = {self.origin: nodetree} result = {} for k in nodetree: v = self.parser.stringify(nodetree[k]) if self.encoding != 'utf8': v = v.encode(self.encoding) result[k or self.origin] = v if self.origin is not None: for k in result: open(k, 'w').write(result[k]) return result reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/bind9.py000066400000000000000000000010371253627113700263530ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import BIND9Parser from reconfigure.includers import BIND9Includer from reconfigure.builders import BoundBuilder from reconfigure.items.bind9 import BIND9Data class BIND9Config (Reconfig): """ ``named.conf`` """ def __init__(self, **kwargs): k = { 'parser': BIND9Parser(), 'includer': BIND9Includer(), 'builder': BoundBuilder(BIND9Data), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/crontab.py000066400000000000000000000006571253627113700270050ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import CrontabParser from reconfigure.builders import BoundBuilder from reconfigure.items.crontab import CrontabData class CrontabConfig (Reconfig): def __init__(self, **kwargs): k = { 'parser': CrontabParser(), 'builder': BoundBuilder(CrontabData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/csf.py000066400000000000000000000007031253627113700261200ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import ShellParser from reconfigure.builders import BoundBuilder from reconfigure.items.csf import CSFData class CSFConfig (Reconfig): """ ``CSF main config`` """ def __init__(self, **kwargs): k = { 'parser': ShellParser(), 'builder': BoundBuilder(CSFData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/ctdb.py000066400000000000000000000022071253627113700262620ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import IniFileParser from reconfigure.parsers import SSVParser from reconfigure.builders import BoundBuilder from reconfigure.items.ctdb import CTDBData, NodesData, PublicAddressesData class CTDBConfig (Reconfig): """ ``CTDB main config`` """ def __init__(self, **kwargs): k = { 'parser': IniFileParser(sectionless=True), 'builder': BoundBuilder(CTDBData), } k.update(kwargs) Reconfig.__init__(self, **k) class CTDBNodesConfig (Reconfig): """ ``CTDB node list file`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(), 'builder': BoundBuilder(NodesData), } k.update(kwargs) Reconfig.__init__(self, **k) class CTDBPublicAddressesConfig (Reconfig): """ ``CTDB public address list file`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(separator=' '), 'builder': BoundBuilder(PublicAddressesData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/dhcpd.py000066400000000000000000000007011253627113700264250ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import NginxParser from reconfigure.builders import BoundBuilder from reconfigure.items.dhcpd import DHCPDData class DHCPDConfig (Reconfig): """ ``DHCPD`` """ def __init__(self, **kwargs): k = { 'parser': NginxParser(), 'builder': BoundBuilder(DHCPDData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/exports.py000066400000000000000000000007221253627113700270520ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import ExportsParser from reconfigure.builders import BoundBuilder from reconfigure.items.exports import ExportsData class ExportsConfig (Reconfig): """ ``/etc/fstab`` """ def __init__(self, **kwargs): k = { 'parser': ExportsParser(), 'builder': BoundBuilder(ExportsData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/fstab.py000066400000000000000000000007021253627113700264430ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import SSVParser from reconfigure.builders import BoundBuilder from reconfigure.items.fstab import FSTabData class FSTabConfig (Reconfig): """ ``/etc/fstab`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(), 'builder': BoundBuilder(FSTabData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/group.py000066400000000000000000000007221253627113700265020ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import SSVParser from reconfigure.builders import BoundBuilder from reconfigure.items.group import GroupsData class GroupConfig (Reconfig): """ ``/etc/group`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(separator=':'), 'builder': BoundBuilder(GroupsData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/hosts.py000066400000000000000000000007021253627113700265040ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import SSVParser from reconfigure.builders import BoundBuilder from reconfigure.items.hosts import HostsData class HostsConfig (Reconfig): """ ``/etc/hosts`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(), 'builder': BoundBuilder(HostsData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/iptables.py000066400000000000000000000007641253627113700271570ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import IPTablesParser from reconfigure.builders import BoundBuilder from reconfigure.items.iptables import IPTablesData class IPTablesConfig (Reconfig): """ ``iptables-save`` and ``iptables-restore`` """ def __init__(self, **kwargs): k = { 'parser': IPTablesParser(), 'builder': BoundBuilder(IPTablesData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/netatalk.py000066400000000000000000000007321253627113700271520ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import IniFileParser from reconfigure.builders import BoundBuilder from reconfigure.items.netatalk import NetatalkData class NetatalkConfig (Reconfig): """ Netatalk afp.conf """ def __init__(self, **kwargs): k = { 'parser': IniFileParser(), 'builder': BoundBuilder(NetatalkData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/nsd.py000066400000000000000000000007071253627113700261350ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import NSDParser from reconfigure.builders import BoundBuilder from reconfigure.items.nsd import NSDData class NSDConfig (Reconfig): """ ``NSD DNS server nsd.conf`` """ def __init__(self, **kwargs): k = { 'parser': NSDParser(), 'builder': BoundBuilder(NSDData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/passwd.py000066400000000000000000000007251253627113700266520ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import SSVParser from reconfigure.builders import BoundBuilder from reconfigure.items.passwd import PasswdData class PasswdConfig (Reconfig): """ ``/etc/passwd`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(separator=':'), 'builder': BoundBuilder(PasswdData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/resolv.py000066400000000000000000000007271253627113700266650ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import SSVParser from reconfigure.builders import BoundBuilder from reconfigure.items.resolv import ResolvData class ResolvConfig (Reconfig): """ ``/etc/resolv.conf`` """ def __init__(self, **kwargs): k = { 'parser': SSVParser(maxsplit=1), 'builder': BoundBuilder(ResolvData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/samba.py000066400000000000000000000006471253627113700264370ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import IniFileParser from reconfigure.builders import BoundBuilder from reconfigure.items.samba import SambaData class SambaConfig (Reconfig): def __init__(self, **kwargs): k = { 'parser': IniFileParser(), 'builder': BoundBuilder(SambaData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/squid.py000066400000000000000000000006431253627113700264750ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import SquidParser from reconfigure.builders import BoundBuilder from reconfigure.items.squid import SquidData class SquidConfig (Reconfig): def __init__(self, **kwargs): k = { 'parser': SquidParser(), 'builder': BoundBuilder(SquidData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/configs/supervisor.py000066400000000000000000000011301253627113700275610ustar00rootroot00000000000000from reconfigure.configs.base import Reconfig from reconfigure.parsers import IniFileParser from reconfigure.includers import SupervisorIncluder from reconfigure.builders import BoundBuilder from reconfigure.items.supervisor import SupervisorData class SupervisorConfig (Reconfig): """ ``/etc/supervisor/supervisord.conf`` """ def __init__(self, **kwargs): k = { 'parser': IniFileParser(), 'includer': SupervisorIncluder(), 'builder': BoundBuilder(SupervisorData), } k.update(kwargs) Reconfig.__init__(self, **k) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/000077500000000000000000000000001253627113700253335ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/__init__.py000066400000000000000000000006201253627113700274420ustar00rootroot00000000000000from reconfigure.includers.base import BaseIncluder from reconfigure.includers.auto import AutoIncluder from reconfigure.includers.bind9 import BIND9Includer from reconfigure.includers.nginx import NginxIncluder from reconfigure.includers.supervisor import SupervisorIncluder __all__ = [ 'BaseIncluder', 'AutoIncluder', 'BIND9Includer', 'NginxIncluder', 'SupervisorIncluder', ] reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/auto.py000066400000000000000000000052461253627113700266640ustar00rootroot00000000000000from reconfigure.includers.base import BaseIncluder from reconfigure.nodes import * import glob import os class AutoIncluder (BaseIncluder): """ This base includer automatically walks the node tree and loads the include files from ``IncludeNode.files`` properties. ``files`` is supposed to contain absolute path, relative path or a shell wildcard. """ def compose(self, origin, tree): self.compose_rec(origin, origin, tree) return tree def compose_rec(self, root, origin, node): if not node.origin: node.origin = origin for child in node.children: self.compose_rec(root, origin, child) for child in node.children: spec = self.is_include(child) if spec: files = spec if node.origin and not files.startswith('/'): files = os.path.join(os.path.split(root)[0], files) if '*' in files or '.' in files: files = glob.glob(files) else: files = [files] for file in files: if file in self.content_map: content = self.content_map[file] else: content = open(file, 'r').read() subtree = self.parser.parse(content) node.children.extend(subtree.children) self.compose_rec(root, file, subtree) node.children[node.children.index(child)] = IncludeNode(spec) def decompose(self, tree): result = {} result[tree.origin] = self.decompose_rec(tree, result) return result def decompose_rec(self, node, result): for child in node.children: if child.__class__ == IncludeNode: replacement = self.remove_include(child) if replacement: node.children[node.children.index(child)] = replacement else: if child.origin is None: child.origin = node.origin if child.origin != node.origin: node.children.remove(child) result.setdefault(child.origin, RootNode()).children.append(self.decompose_rec(child, result)) else: self.decompose_rec(child, result) return node def is_include(self, node): """ Should return whether the node is an include node and return file pattern glob if it is """ def remove_include(self, node): """ Shoud transform :class:`reconfigure.nodes.IncludeNode` into a normal Node to be stringified into the file """ reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/base.py000066400000000000000000000014761253627113700266270ustar00rootroot00000000000000class BaseIncluder (object): # pragma: no cover """ A base includer class :param parser: Parser instance that was used to parse the root config file :param content_map: a dict that overrides config content for specific paths """ def __init__(self, parser=None, content_map={}): self.parser = parser self.content_map = content_map def compose(self, origin, tree): """ Should locate the include nodes in the Node tree, replace them with :class:`reconfigure.nodes.IncludeNode`, parse the specified include files and append them to tree, with correct node ``origin`` attributes """ def decompose(self, origin, tree): """ Should detach the included subtrees from the Node tree and return a ``{ origin: content-node-tree }`` dict. """ reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/bind9.py000066400000000000000000000005651253627113700267200ustar00rootroot00000000000000from reconfigure.includers.auto import AutoIncluder from reconfigure.nodes import PropertyNode class BIND9Includer (AutoIncluder): def is_include(self, node): if isinstance(node, PropertyNode) and node.name == 'include': return node.value.strip('"') def remove_include(self, node): return PropertyNode('include', '"%s"' % node.files) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/nginx.py000066400000000000000000000005411253627113700270300ustar00rootroot00000000000000from reconfigure.includers.auto import AutoIncluder from reconfigure.nodes import PropertyNode class NginxIncluder (AutoIncluder): def is_include(self, node): if isinstance(node, PropertyNode) and node.name == 'include': return node.value def remove_include(self, node): return PropertyNode('include', node.files) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/includers/supervisor.py000066400000000000000000000005601253627113700301270ustar00rootroot00000000000000from reconfigure.includers.auto import AutoIncluder from reconfigure.nodes import Node, PropertyNode class SupervisorIncluder (AutoIncluder): def is_include(self, node): if node.name == 'include': return node.get('files').value def remove_include(self, node): return Node('include', children=[PropertyNode('files', node.files)]) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/000077500000000000000000000000001253627113700244645ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/__init__.py000066400000000000000000000000141253627113700265700ustar00rootroot00000000000000__all__ = []reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/ajenti.py000066400000000000000000000032711253627113700263130ustar00rootroot00000000000000import json from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData, BoundDictionary class AjentiData (BoundData): pass class HttpData (BoundData): pass class SSLData (BoundData): pass class UserData (BoundData): def template(self): return Node( 'unnamed', PropertyNode('configs', {}), PropertyNode('password', ''), PropertyNode('permissions', []), ) class ConfigData (BoundData): def template(self): return PropertyNode('', '{}') AjentiData.bind_property('authentication', 'authentication') AjentiData.bind_property('language', 'language') AjentiData.bind_property('installation_id', 'installation_id') AjentiData.bind_property('enable_feedback', 'enable_feedback') AjentiData.bind_child('http_binding', lambda x: x.get('bind'), item_class=HttpData) AjentiData.bind_child('ssl', lambda x: x.get('ssl'), item_class=SSLData) AjentiData.bind_collection('users', path=lambda x: x.get('users'), item_class=UserData, collection_class=BoundDictionary, key=lambda x: x.name) HttpData.bind_property('host', 'host') HttpData.bind_property('port', 'port') SSLData.bind_property('certificate_path', 'certificate_path') SSLData.bind_property('enable', 'enable') ConfigData.bind_name('name') UserData.bind_name('name') UserData.bind_property('email', 'email') UserData.bind_property('password', 'password') UserData.bind_property('permissions', 'permissions') UserData.bind_collection('configs', lambda x: x.get('configs'), item_class=ConfigData, collection_class=BoundDictionary, key=lambda x: x.name) ConfigData.bind_attribute('value', 'data', getter=json.loads, setter=json.dumps) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/bind9.py000066400000000000000000000013141253627113700260420ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class BIND9Data (BoundData): pass class ZoneData (BoundData): def template(self): return Node( 'zone', PropertyNode('type', 'master'), PropertyNode('file', 'db.example.com'), parameter='"example.com"', ) quote = lambda x: '"%s"' % x unquote = lambda x: x.strip('"') BIND9Data.bind_collection('zones', selector=lambda x: x.name == 'zone', item_class=ZoneData) ZoneData.bind_attribute('parameter', 'name', getter=unquote, setter=quote) ZoneData.bind_property('type', 'type') ZoneData.bind_property('file', 'file', getter=unquote, setter=quote) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/bound.py000066400000000000000000000244231253627113700261520ustar00rootroot00000000000000import json class BoundCollection (object): """ Binds a list-like object to a set of nodes :param node: target node (its children will be bound) :param item_class: :class:`BoundData` class for items :param selector: ``lambda x: bool``, used to filter out a subset of nodes """ def __init__(self, node, item_class, selector=lambda x: True): self.node = node self.selector = selector self.item_class = item_class self.data = [] self.rebuild() def rebuild(self): """ Discards cached collection and rebuilds it from the nodes """ del self.data[:] for node in self.node.children: if self.selector(node): self.data.append(self.item_class(node)) def to_dict(self): return [x.to_dict() if hasattr(x, 'to_dict') else x for x in self] def to_json(self): return json.dumps(self.to_dict(), indent=4) def __str__(self): return self.to_json() def __iter__(self): return self.data.__iter__() def __getitem__(self, index): return self.data.__getitem__(index) def __len__(self): return len(self.data) def __contains__(self, item): return item in self.data def append(self, item): self.node.append(item._node) self.data.append(item) def remove(self, item): self.node.remove(item._node) self.data.remove(item) def insert(self, index, item): self.node.children.insert(index, item._node) self.data.insert(index, item) def pop(self, index): d = self[index] self.remove(d) return d class BoundDictionary (BoundCollection): """ Binds a dict-like object to a set of nodes. Accepts same params as :class:`BoundCollection` plus ``key`` :param key: ``lambda value: object``, is used to get key for value in the collection """ def __init__(self, key=None, **kwargs): self.key = key BoundCollection.__init__(self, **kwargs) def rebuild(self): BoundCollection.rebuild(self) self.rebuild_dict() def rebuild_dict(self): self.datadict = dict((self.key(x), x) for x in self.data) def to_dict(self): return dict((k, x.to_dict() if hasattr(x, 'to_dict') else x) for k, x in self.items()) def __getitem__(self, key): self.rebuild_dict() return self.datadict[key] def __setitem__(self, key, value): self.rebuild_dict() if not key in self: self.append(value) self.datadict[key] = value self.rebuild_dict() def __contains__(self, key): self.rebuild_dict() return key in self.datadict def __iter__(self): self.rebuild_dict() return self.datadict.__iter__() def iteritems(self): self.rebuild_dict() return self.datadict.items() items = iteritems def setdefault(self, k, v): if not k in self: self[k] = v self.append(v) return self[k] def values(self): self.rebuild_dict() return self.data def update(self, other): for k, v in other.items(): self[k] = v def pop(self, key): if key in self: self.remove(self[key]) class BoundData (object): """ Binds itself to a node. ``bind_*`` classmethods should be called on module-level, after subclass declaration. :param node: all bindings will be relative to this node :param kwargs: if ``node`` is ``None``, ``template(**kwargs)`` will be used to create node tree fragment """ def __init__(self, node=None, **kwargs): if node is None: node = self.template(**kwargs) self._node = node def template(self, **kwargs): """ Override to create empty objects. :returns: a :class:`reconfigure.nodes.Node` tree that will be used as a template for new BoundData instance """ return None def to_dict(self): res_dict = {} for attr_key in self.__class__.__dict__: if attr_key in self.__class__._bound: attr_value = getattr(self, attr_key) if isinstance(attr_value, BoundData): res_dict[attr_key] = attr_value.to_dict() elif isinstance(attr_value, BoundCollection): res_dict[attr_key] = attr_value.to_dict() else: res_dict[attr_key] = attr_value return res_dict def to_json(self): return json.dumps(self.to_dict(), indent=4) def __str__(self): return self.to_json() @classmethod def bind(cls, data_property, getter, setter): """ Creates an arbitrary named property in the class with given getter and setter. Not usually used directly. :param data_property: property name :param getter: ``lambda: object``, property getter :param setter: ``lambda value: None``, property setter """ if not hasattr(cls, '_bound'): cls._bound = [] cls._bound.append(data_property) setattr(cls, data_property, property(getter, setter)) @classmethod def bind_property(cls, node_property, data_property, default=None, \ default_remove=[], \ path=lambda x: x, getter=lambda x: x, setter=lambda x: x): """ Binds the value of a child :class:`reconfigure.node.PropertyNode` to a property :param node_property: ``PropertyNode``'s ``name`` :param data_property: property name to be created :param default: default value of the property (is ``PropertyNode`` doesn't exist) :param default_remove: if setting a value contained in default_remove, the target property is removed :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. :param getter: ``lambda object: object``, used to transform value when getting :param setter: ``lambda object: object``, used to transform value when setting """ def pget(self): prop = path(self._node).get(node_property) if prop is not None: return getter(prop.value) else: return default def pset(self, value): if setter(value) in default_remove: node = path(self._node).get(node_property) if node is not None: path(self._node).remove(node) else: path(self._node).set_property(node_property, setter(value)) cls.bind(data_property, pget, pset) @classmethod def bind_attribute(cls, node_attribute, data_property, default=None, \ path=lambda x: x, getter=lambda x: x, setter=lambda x: x): """ Binds the value of node object's attribute to a property :param node_attribute: ``Node``'s attribute name :param data_property: property name to be created :param default: default value of the property (is ``PropertyNode`` doesn't exist) :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. :param getter: ``lambda object: object``, used to transform value when getting :param setter: ``lambda object: object``, used to transform value when setting """ def pget(self): prop = getattr(path(self._node), node_attribute) if prop is not None: return getter(prop) else: return getter(default) def pset(self, value): setattr(path(self._node), node_attribute, setter(value)) cls.bind(data_property, pget, pset) @classmethod def bind_collection(cls, data_property, path=lambda x: x, selector=lambda x: True, item_class=None, \ collection_class=BoundCollection, **kwargs): """ Binds the subset of node's children to a collection property :param data_property: property name to be created :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. :param selector: ``lambda Node: bool``, can be used to filter out a subset of child nodes :param item_class: a :class:`BoundData` subclass to be used for collection items :param collection_class: a :class:`BoundCollection` subclass to be used for collection property itself """ def pget(self): if not hasattr(self, '__' + data_property): setattr(self, '__' + data_property, collection_class( node=path(self._node), item_class=item_class, selector=selector, **kwargs ) ) return getattr(self, '__' + data_property) cls.bind(data_property, pget, None) @classmethod def bind_name(cls, data_property, getter=lambda x: x, setter=lambda x: x): """ Binds the value of node's ``name`` attribute to a property :param data_property: property name to be created :param getter: ``lambda object: object``, used to transform value when getting :param setter: ``lambda object: object``, used to transform value when setting """ def pget(self): return getter(self._node.name) def pset(self, value): self._node.name = setter(value) cls.bind(data_property, pget, pset) @classmethod def bind_child(cls, data_property, path=lambda x: x, item_class=None): """ Directly binds a child Node to a BoundData property :param data_property: property name to be created :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. :param item_class: a :class:`BoundData` subclass to be used for the property value """ def pget(self): if not hasattr(self, '__' + data_property): setattr(self, '__' + data_property, item_class( path(self._node), ) ) return getattr(self, '__' + data_property) cls.bind(data_property, pget, None) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/crontab.py000066400000000000000000000040261253627113700264700ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class CrontabData(BoundData): """Data class for crontab configs""" pass class CrontabNormalTaskData(BoundData): fields = ['minute', 'hour', 'day_of_month', 'month', 'day_of_week', 'command'] def describe(self): return ' '.join(getattr(self, x) for x in self.fields) def template(self, **kwargs): return Node('normal_task', children=[ PropertyNode('minute', '0'), PropertyNode('hour', '0'), PropertyNode('day_of_month', '1'), PropertyNode('month', '1'), PropertyNode('day_of_week', '1'), PropertyNode('command', 'false') ]) class CrontabSpecialTaskData(BoundData): fields = ['special', 'command'] def template(self, **kwargs): return Node('special_task', children=[ PropertyNode('special', '@reboot'), PropertyNode('command', 'false') ]) class CrontabEnvSettingData(BoundData): fields = ['name', 'value'] def template(self, **kwargs): return Node('env_setting', children=[ PropertyNode('name', 'ENV_NAME'), PropertyNode('value', 'ENV_VALUE') ]) def bind_for_fields(bound_data_class): for field in bound_data_class.fields: bound_data_class.bind_property(field, field) CrontabData.bind_collection('normal_tasks', selector=lambda x: x.name == 'normal_task', item_class=CrontabNormalTaskData) bind_for_fields(CrontabNormalTaskData) CrontabNormalTaskData.bind_attribute('comment', 'comment') CrontabData.bind_collection('env_settings', selector=lambda x: x.name == 'env_setting', item_class=CrontabEnvSettingData) bind_for_fields(CrontabEnvSettingData) CrontabEnvSettingData.bind_attribute('comment', 'comment') CrontabData.bind_collection('special_tasks', selector=lambda x: x.name == 'special_task', item_class=CrontabSpecialTaskData) bind_for_fields(CrontabSpecialTaskData) CrontabSpecialTaskData.bind_attribute('comment', 'comment') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/csf.py000066400000000000000000000014761253627113700256210ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData from reconfigure.items.util import onezero_getter, onezero_setter class CSFData (BoundData): pass CSFData.bind_property('TESTING', 'testing', getter=onezero_getter, setter=onezero_setter) CSFData.bind_property('IPV6', 'ipv6', getter=onezero_getter, setter=onezero_setter) CSFData.bind_property('TCP_IN', 'tcp_in') CSFData.bind_property('TCP_OUT', 'tcp_out') CSFData.bind_property('UDP_IN', 'udp_in') CSFData.bind_property('UDP_OUT', 'udp_out') CSFData.bind_property('TCP6_IN', 'tcp6_in') CSFData.bind_property('TCP6_OUT', 'tcp6_out') CSFData.bind_property('UDP6_IN', 'udp6_in') CSFData.bind_property('UDP6_OUT', 'udp6_out') CSFData.bind_property('ETH_DEVICE', 'eth_device') CSFData.bind_property('ETH6_DEVICE', 'eth6_device') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/ctdb.py000066400000000000000000000040001253627113700257440ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData from reconfigure.items.util import yn_getter, yn_setter class CTDBData (BoundData): pass CTDBData.bind_property('CTDB_RECOVERY_LOCK', 'recovery_lock_file', path=lambda x: x.get(None)) CTDBData.bind_property('CTDB_PUBLIC_INTERFACE', 'public_interface', path=lambda x: x.get(None)) CTDBData.bind_property('CTDB_PUBLIC_ADDRESSES', 'public_addresses_file', default='/etc/ctdb/public_addresses', path=lambda x: x.get(None)) CTDBData.bind_property( 'CTDB_MANAGES_SAMBA', 'manages_samba', path=lambda x: x.get(None), getter=yn_getter, setter=yn_setter) CTDBData.bind_property('CTDB_NODES', 'nodes_file', default='/etc/ctdb/nodes', path=lambda x: x.get(None)) CTDBData.bind_property('CTDB_LOGFILE', 'log_file', path=lambda x: x.get(None)) CTDBData.bind_property('CTDB_DEBUGLEVEL', 'debug_level', default='2', path=lambda x: x.get(None)) CTDBData.bind_property('CTDB_PUBLIC_NETWORK', 'public_network', default='', path=lambda x: x.get(None)) CTDBData.bind_property('CTDB_PUBLIC_GATEWAY', 'public_gateway', default='', path=lambda x: x.get(None)) class NodesData (BoundData): pass class NodeData (BoundData): def template(self): return Node('line', children=[ Node('token', children=[PropertyNode('value', '127.0.0.1')]), ]) NodesData.bind_collection('nodes', item_class=NodeData) NodeData.bind_property('value', 'address', path=lambda x: x.children[0]) class PublicAddressesData (BoundData): pass class PublicAddressData (BoundData): def template(self): return Node('line', children=[ Node('token', children=[PropertyNode('value', '127.0.0.1')]), Node('token', children=[PropertyNode('value', 'eth0')]), ]) PublicAddressesData.bind_collection('addresses', item_class=PublicAddressData) PublicAddressData.bind_property('value', 'address', path=lambda x: x.children[0]) PublicAddressData.bind_property('value', 'interface', path=lambda x: x.children[1]) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/dhcpd.py000066400000000000000000000020741253627113700261230ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class DHCPDData (BoundData): pass class SubnetData (BoundData): def template(self): return Node( 'subnet', parameter='192.168.0.0 netmask 255.255.255.0', ) class RangeData (BoundData): def template(self): return PropertyNode('range', '192.168.0.1 192.168.0.100') class OptionData (BoundData): def template(self): return PropertyNode('option', '') DHCPDData.bind_collection('subnets', selector=lambda x: x.name == 'subnet', item_class=SubnetData) SubnetData.bind_attribute('parameter', 'name') SubnetData.bind_collection('subnets', selector=lambda x: x.name == 'subnet', item_class=SubnetData) SubnetData.bind_collection('ranges', selector=lambda x: x.name == 'range', item_class=RangeData) RangeData.bind_attribute('value', 'range') OptionData.bind_attribute('value', 'value') for x in [DHCPDData, SubnetData]: x.bind_collection('options', selector=lambda x: x.name == 'option', item_class=OptionData) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/exports.py000066400000000000000000000013441253627113700265440ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class ExportsData (BoundData): pass class ExportData (BoundData): def template(self): return Node( '/', Node('clients') ) class ClientData (BoundData): def template(self): return Node( 'localhost', PropertyNode('options', '') ) ExportsData.bind_collection('exports', item_class=ExportData) ExportData.bind_name('name') ExportData.bind_attribute('comment', 'comment', default='') ExportData.bind_collection('clients', path=lambda x: x['clients'], item_class=ClientData) ClientData.bind_name('name') ClientData.bind_property('options', 'options') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/fstab.py000066400000000000000000000017241253627113700261410ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class FSTabData (BoundData): pass class FilesystemData (BoundData): fields = ['device', 'mountpoint', 'type', 'options', 'freq', 'passno'] def template(self): return Node('line', children=[ Node('token', children=[PropertyNode('value', 'none')]), Node('token', children=[PropertyNode('value', 'none')]), Node('token', children=[PropertyNode('value', 'auto')]), Node('token', children=[PropertyNode('value', 'defaults,rw')]), Node('token', children=[PropertyNode('value', '0')]), Node('token', children=[PropertyNode('value', '0')]), ]) FSTabData.bind_collection('filesystems', item_class=FilesystemData) for i in range(0, len(FilesystemData.fields)): path = lambda i: lambda x: x.children[i] FilesystemData.bind_property('value', FilesystemData.fields[i], path=path(i)) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/group.py000066400000000000000000000012621253627113700261730ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class GroupsData (BoundData): pass class GroupData (BoundData): fields = ['name', 'password', 'gid', 'users'] def template(self): return Node( 'line', *[ Node('token', children=[ PropertyNode('value', '') ]) for x in GroupData.fields ] ) GroupsData.bind_collection('groups', item_class=GroupData) for i in range(0, len(GroupData.fields)): path = lambda i: lambda x: x.children[i] GroupData.bind_property('value', GroupData.fields[i], path=path(i)) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/hosts.py000066400000000000000000000014501253627113700261760ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class HostsData (BoundData): pass class HostData (BoundData): def template(self): return Node('line', children=[ Node('token', children=[PropertyNode('value', '127.0.0.1')]), Node('token', children=[PropertyNode('value', 'localhost')]), ]) class AliasData (BoundData): def template(self): return Node() HostsData.bind_collection('hosts', item_class=HostData) HostData.bind_property('value', 'address', path=lambda x: x.children[0]) HostData.bind_property('value', 'name', path=lambda x: x.children[1]) HostData.bind_collection('aliases', item_class=AliasData, selector=lambda x: x.parent.indexof(x) > 1) AliasData.bind_property('value', 'name') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/iptables.py000066400000000000000000000072531253627113700266500ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class IPTablesData (BoundData): pass class TableData (BoundData): def template(self): return Node('custom') class ChainData (BoundData): def template(self): return Node( 'CUSTOM', PropertyNode('default', '-'), ) class RuleData (BoundData): def template(self): return Node( 'append', Node( 'option', Node('argument', PropertyNode('value', 'ACCEPT')), PropertyNode('negative', False), PropertyNode('name', 'j'), ) ) @property def summary(self): return ' '.join(( ('! ' if x.negative else '') + ('-' if len(x.name) == 1 else '--') + x.name + ' ' + ' '.join(a.value for a in x.arguments)) for x in self.options ) def verify(self): protocol_option = None for option in self.options: if option.name in ['p', 'protocol']: self.options.remove(option) self.options.insert(0, option) protocol_option = option for option in self.options: if 'port' in option.name: if not protocol_option: protocol_option = OptionData.create('protocol') self.options.insert(0, protocol_option) def get_option(self, *names): for name in names: for option in self.options: if option.name == name: return option class OptionData (BoundData): templates = { 'protocol': ['protocol', ['tcp']], 'match': ['match', ['multiport']], 'source': ['source', ['127.0.0.1']], 'mac-source': ['mac-source', ['00:00:00:00:00:00']], 'destination': ['destination', ['127.0.0.1']], 'in-interface': ['in-interface', ['lo']], 'out-interface': ['out-interface', ['lo']], 'source-port': ['source-port', ['80']], 'source-ports': ['source-ports', ['80,443']], 'destination-port': ['destination-port', ['80']], 'destination-ports': ['destination-ports', ['80,443']], 'state': ['state', ['NEW']], 'reject-with': ['reject-with', ['icmp-net-unreachable']], 'custom': ['name', ['value']], } @staticmethod def create(template_id): t = OptionData.templates[template_id] return OptionData(Node( 'option', *( [Node('argument', PropertyNode('value', x)) for x in t[1]] + [PropertyNode('negative', False)] + [PropertyNode('name', t[0])] ) )) @staticmethod def create_destination(): return OptionData(Node( 'option', Node('argument', PropertyNode('value', 'ACCEPT')), PropertyNode('negative', False), PropertyNode('name', 'j'), )) class ArgumentData (BoundData): pass IPTablesData.bind_collection('tables', item_class=TableData) TableData.bind_collection('chains', item_class=ChainData) TableData.bind_name('name') ChainData.bind_property('default', 'default') ChainData.bind_collection('rules', selector=lambda x: x.name == 'append', item_class=RuleData) ChainData.bind_name('name') RuleData.bind_collection('options', item_class=OptionData) RuleData.bind_attribute('comment', 'comment') OptionData.bind_property('name', 'name') OptionData.bind_property('negative', 'negative') OptionData.bind_collection('arguments', selector=lambda x: x.name == 'argument', item_class=ArgumentData) ArgumentData.bind_property('value', 'value') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/netatalk.py000066400000000000000000000025601253627113700266440ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData from reconfigure.items.util import yn_getter, yn_setter class NetatalkData (BoundData): pass class GlobalData (BoundData): pass class ShareData (BoundData): fields = ['path', 'appledouble', 'valid users', 'cnid scheme', 'ea', 'password', 'file perm', 'directory perm'] defaults = ['', 'ea', '', 'dbd', 'none', '', '', ''] def template(self): return Node( 'share', *[PropertyNode(x, y) for x, y in zip(ShareData.fields, ShareData.defaults)] ) NetatalkData.bind_child('global', lambda x: x.get('Global'), item_class=GlobalData) NetatalkData.bind_collection('shares', selector=lambda x: x.name != 'Global', item_class=ShareData) GlobalData.bind_property('afp port', 'afp_port', default='548') GlobalData.bind_property('cnid listen', 'cnid_listen', default='localhost:4700') GlobalData.bind_property('uam list', 'uam_list', default='uams_dhx.so,uams_dhx2.so') GlobalData.bind_property( 'zeroconf', 'zeroconf', default=True, getter=yn_getter, setter=yn_setter) ShareData.bind_name('name') ShareData.bind_attribute('comment', 'comment', path=lambda x: x.get('path'), default='') for f, d in zip(ShareData.fields, ShareData.defaults): ShareData.bind_property(f, f.replace(' ', '_'), default=d, default_remove=[d, None]) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/nsd.py000066400000000000000000000012031253627113700256160ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class NSDData (BoundData): pass class ZoneData (BoundData): def template(self): return Node( 'zone', PropertyNode('name', '"example.com"'), PropertyNode('file', '"example.com.zone"'), ) quote = lambda x: '"%s"' % x unquote = lambda x: x.strip('"') NSDData.bind_collection('zones', selector=lambda x: x.name == 'zone', item_class=ZoneData) ZoneData.bind_property('name', 'name', getter=unquote, setter=quote) ZoneData.bind_property('zonefile', 'file', getter=unquote, setter=quote) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/passwd.py000066400000000000000000000013051253627113700263360ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class PasswdData (BoundData): pass class UserData (BoundData): fields = ['name', 'password', 'uid', 'gid', 'comment', 'home', 'shell'] def template(self): return Node( 'line', *[ Node('token', children=[ PropertyNode('value', '') ]) for x in UserData.fields ] ) PasswdData.bind_collection('users', item_class=UserData) for i in range(0, len(UserData.fields)): path = lambda i: lambda x: x.children[i] UserData.bind_property('value', UserData.fields[i], path=path(i)) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/resolv.py000066400000000000000000000011131253627113700263440ustar00rootroot00000000000000from 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]) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/samba.py000066400000000000000000000045161253627113700261270ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData from reconfigure.items.util import yn_getter, yn_setter class SambaData (BoundData): pass class GlobalData (BoundData): pass class ShareData (BoundData): fields = [ 'comment', 'path', 'guest ok', 'browseable', 'create mask', 'directory mask', 'read only', 'follow symlinks', 'wide links', 'fstype', 'write list', 'veto files', 'force create mode', 'force directory mode', 'dfree command', 'force user', 'force group', 'valid users', 'read list', 'dfree cache time', 'oplocks', 'locking', 'preopen:names', 'preopen:num_bytes', 'preopen:helpers', 'preopen:queuelen', 'vfs objects', 'recycle:repository', 'recycle:keeptree', 'recycle:exclude', ] defaults = [ '', '', 'no', 'yes', '0744', '0755', 'yes', 'yes', 'no', 'NTFS', '', '', '000', '000', '', '', '', '', '', '', 'yes', 'yes', '', '', '', '', '', '', 'no', '', ] default_values = [ '', '', False, True, '0744', '0755', True, True, False, '', '', '', '000', '000', '', '', '', '', '', '', True, True, '', '', '', '', '', '', False, '', ] def template(self): return Node( 'share', *[PropertyNode(x, y) for x, y in zip(ShareData.fields, ShareData.defaults)] ) SambaData.bind_child('global', lambda x: x.get('global'), item_class=GlobalData) SambaData.bind_collection('shares', selector=lambda x: x.name != 'global', item_class=ShareData) GlobalData.bind_property('workgroup', 'workgroup', default='') GlobalData.bind_property('server string', 'server_string', default='') GlobalData.bind_property('interfaces', 'interfaces', default='') GlobalData.bind_property( 'bind interfaces only', 'bind_interfaces_only', default=True, getter=yn_getter, setter=yn_setter) GlobalData.bind_property('log file', 'log_file', default='') GlobalData.bind_property('security', 'security', default='user') ShareData.bind_name('name') for f, d in zip(ShareData.fields, ShareData.default_values): if d not in [True, False]: ShareData.bind_property(f, f.replace(' ', '_'), default=d) else: ShareData.bind_property( f, f.replace(' ', '_'), default=d, getter=yn_getter, setter=yn_setter) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/squid.py000066400000000000000000000050021253627113700261600ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData class SquidData (BoundData): pass class ACLData (BoundData): def template(self, name=None, *args): children = [PropertyNode('1', name)] index = 2 for arg in args: children += [PropertyNode(str(index), arg)] index += 1 return Node( 'line', PropertyNode('name', 'acl'), Node( 'arguments', *children ) ) def describe(self): return ' '.join(x.value for x in self.options) class HTTPAccessData (BoundData): def template(self): return Node( 'line', PropertyNode('name', 'http_access'), Node('arguments', PropertyNode('1', '')) ) class HTTPPortData (BoundData): def template(self): return Node( 'line', PropertyNode('name', 'http_port'), Node('arguments', PropertyNode('1', '3128')) ) class HTTPSPortData (BoundData): def template(self): return Node( 'line', PropertyNode('name', 'https_port'), Node('arguments', PropertyNode('1', '3128')) ) class ArgumentData (BoundData): def template(self): return PropertyNode('value', 'none') def __bind_by_name(cls, prop, name, itemcls): cls.bind_collection( prop, selector=lambda x: x.get('name').value == name, item_class=itemcls ) __bind_by_name(SquidData, 'acl', 'acl', ACLData) __bind_by_name(SquidData, 'http_access', 'http_access', HTTPAccessData) __bind_by_name(SquidData, 'http_port', 'http_port', HTTPPortData) __bind_by_name(SquidData, 'https_port', 'https_port', HTTPSPortData) def __bind_first_arg(cls, prop): cls.bind_attribute('value', prop, path=lambda x: x.get('arguments').children[0]) def __bind_other_args(cls, prop, itemcls): cls.bind_collection( prop, path=lambda x: x.get('arguments'), selector=lambda x: x.parent.children.index(x) > 0, item_class=itemcls ) __bind_first_arg(ACLData, 'name') __bind_other_args(ACLData, 'options', ArgumentData) __bind_first_arg(HTTPAccessData, 'mode') __bind_other_args(HTTPAccessData, 'options', ArgumentData) __bind_first_arg(HTTPPortData, 'port') __bind_other_args(HTTPPortData, 'options', ArgumentData) __bind_first_arg(HTTPSPortData, 'port') __bind_other_args(HTTPSPortData, 'options', ArgumentData) ArgumentData.bind_attribute('value', 'value') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/supervisor.py000066400000000000000000000021741253627113700272630ustar00rootroot00000000000000from reconfigure.nodes import Node, PropertyNode from reconfigure.items.bound import BoundData from reconfigure.items.util import tf_getter, tf_setter class SupervisorData (BoundData): pass class ProgramData (BoundData): fields = ['command', 'autostart', 'autorestart', 'startsecs', 'startretries', \ 'user', 'directory', 'umask', 'environment', 'stopasgroup', 'killasgroup'] def template(self): return Node('program:new', PropertyNode('command', 'false'), ) SupervisorData.bind_collection('programs', item_class=ProgramData, selector=lambda x: x.name.startswith('program:')) ProgramData.bind_name('name', getter=lambda x: x[8:], setter=lambda x: 'program:%s' % x) ProgramData.bind_attribute('comment', 'comment') for i in range(0, len(ProgramData.fields)): ProgramData.bind_property(ProgramData.fields[i], ProgramData.fields[i], default_remove=[None, '']) ProgramData.bind_property('stopasgroup', 'stopasgroup', default_remove=[False], getter=tf_getter, setter=tf_setter) ProgramData.bind_property('killasgroup', 'killasgroup', default_remove=[False], getter=tf_getter, setter=tf_setter) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/items/util.py000066400000000000000000000003561253627113700260170ustar00rootroot00000000000000yn_getter = lambda x: x == 'yes' yn_setter = lambda x: 'yes' if x else 'no' tf_getter = lambda x: x == 'true' tf_setter = lambda x: 'true' if x else 'false' onezero_getter = lambda x: x == '1' onezero_setter = lambda x: '1' if x else '0' reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/nodes.py000066400000000000000000000116461253627113700250350ustar00rootroot00000000000000class Node (object): """ A base node class for the Node Tree. This class represents a named container node. """ def __init__(self, name=None, *args, **kwargs): """ :param name: Node name :param *args: Children :param comment: Node comment string :param origin: Node's source location (usually path to the file) """ self.name = name self.origin = None self.children = [] for node in list(args) + kwargs.pop('children', []): self.append(node) self.comment = kwargs.pop('comment', None) self.__dict__.update(kwargs) def __str__(self): s = '(%s)' % self.name if self.comment: s += ' (%s)' % self.comment s += '\n' for child in self.children: if child.origin != self.origin: s += '\t@%s\n' % child.origin s += '\n'.join('\t' + x for x in str(child).splitlines()) + '\n' return s def __hash__(self): return sum(hash(x) for x in [self.name, self.origin, self.comment] + self.children) def __eq__(self, other): if other is None: return False return \ self.name == other.name and \ self.comment == other.comment and \ self.origin == other.origin and \ set(self.children) == set(other.children) def __iter__(self): return iter(self.children) def __len__(self): return len(self.children) def __nonzero__(self): return True def __getitem__(self, key): if type(key) in (int, slice): return self.children[key] return self.get(key) def __setitem__(self, key, value): if type(key) is int: self.children[key] = value self.set_property(key, value) def __contains__(self, item): return item in self.children def indexof(self, node): """ :returns: index of the node in the children array or ``None`` if it's not a child """ if node in self.children: return self.children.index(node) else: return None def get(self, name, default=None): """ :returns: a child node by its name or ``default`` """ for child in self.children: if child.name == name: return child if default: self.append(default) return default def get_all(self, name): """ :returns: list of child nodes with supplied ``name`` """ return [n for n in self.children if n.name == name] def append(self, node): if not node.origin: node.origin = self.origin self.children.append(node) node.parent = self def remove(self, node): self.children.remove(node) def replace(self, name, node=None): """ Replaces the child nodes by ``name`` :param node: replacement node or list of nodes :: n.append(Node('a')) n.append(Node('a')) n.replace('a', None) assert(len(n.get_all('a')) == 0) """ if name: self.children = [c for c in self.children if c.name != name] if node is not None: if type(node) == list: for n in node: self.children.append(n) else: self.children.append(node) def set_property(self, name, value): """ Creates or replaces a child :class:`PropertyNode` by name. """ node = self.get(name) if node is None: node = PropertyNode(name, value) self.append(node) node.value = value return self class RootNode (Node): """ A special node class that indicates tree root """ class PropertyNode (Node): """ A node that serves as a property of its parent node. """ def __init__(self, name, value, comment=None): """ :param name: Property name :param value: Property value """ Node.__init__(self, name, comment=comment) self.value = value def __eq__(self, other): if other is None: return False return \ Node.__eq__(self, other) and \ self.value == other.value def __hash__(self): return Node.__hash__(self) + hash(self.value) def __str__(self): s = '%s = %s' % (self.name, self.value) if self.comment: s += ' (%s)' % self.comment return s class IncludeNode (Node): """ A node that indicates a junction point between two config files """ def __init__(self, files): """ :param files: an includer-dependent config location specifier """ Node.__init__(self) self.name = '' self.files = files def __str__(self): return ' %s' % self.files reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/000077500000000000000000000000001253627113700250225ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/__init__.py000066400000000000000000000015261253627113700271370ustar00rootroot00000000000000from reconfigure.parsers.base import BaseParser from reconfigure.parsers.bind9 import BIND9Parser from reconfigure.parsers.exports import ExportsParser from reconfigure.parsers.ini import IniFileParser from reconfigure.parsers.iptables import IPTablesParser from reconfigure.parsers.jsonparser import JsonParser from reconfigure.parsers.nginx import NginxParser from reconfigure.parsers.nsd import NSDParser from reconfigure.parsers.shell import ShellParser from reconfigure.parsers.ssv import SSVParser from reconfigure.parsers.squid import SquidParser from reconfigure.parsers.crontab import CrontabParser __all__ = [ 'BaseParser', 'BIND9Parser', 'CrontabParser', 'ExportsParser', 'IniFileParser', 'IPTablesParser', 'JsonParser', 'NginxParser', 'NSDParser', 'ShellParser', 'SSVParser', 'SquidParser', ] reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/base.py000066400000000000000000000006711253627113700263120ustar00rootroot00000000000000class BaseParser (object): # pragma: no cover """ A base parser class """ def parse(self, content): """ :param content: string config content :returns: a :class:`reconfigure.nodes.Node` tree """ return None def stringify(self, tree): """ :param tree: a :class:`reconfigure.nodes.Node` tree :returns: string config content """ return None reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/bind9.py000066400000000000000000000020131253627113700263750ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers.nginx import NginxParser class BIND9Parser (NginxParser): """ A parser for named.conf """ tokens = [ (r"(acl|key|masters|server|trusted-keys|managed-keys|controls|logging|lwres|options|view|zone|channel|category|listen-on|search|avoid-v4-udp-ports|avoid-v6-udp-ports|blackhole|listen-on|listen-on-v6|allow-recursion|allow-recursion-on|sortlist|topology|rrset-order|dual-stack-servers|disable-algorithms|dns64|forwarders|rrset-order|update-policy|also-notify|allow-notify|rate-limit)\s+?([^\s{}]*\s*)*{", lambda s, t: ('section_start', t)), (r"\#.*?\n", lambda s, t: ('comment', t)), (r"//.*?\n", lambda s, t: ('comment', t)), (r"/\*.*?\*/", lambda s, t: ('comment', t)), (r"((([^\s{};#]+)|({\s*([^\s{};#]+;\s*)*}))\s*?)+;", lambda s, t: ('option', t)), (r"\s", lambda s, t: 'whitespace'), (r"$^", lambda s, t: 'newline'), (r"\};", lambda s, t: 'section_end'), ] token_section_end = '};' reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/crontab.py000066400000000000000000000066401253627113700270320ustar00rootroot00000000000000from reconfigure.nodes import RootNode, Node, PropertyNode from reconfigure.parsers import BaseParser class CrontabParser(BaseParser): def __init__(self, remove_comments=False): self.remove_comments = remove_comments def parse(self, content): root = RootNode() lines = [l.strip() for l in content.splitlines() if l] comment = None for line in lines: if line.startswith('#'): comment = '\n'.join([comment, line]) if comment else line[1:] continue elif line.startswith('@'): special, command = line.split(' ', 1) node = Node('special_task', comment=comment) node.append(PropertyNode('special', special)) node.append(PropertyNode('command', command)) else: split_line = line.split(' ', 5) if len(split_line) <= 3 and '=' in line: name, value = [n.strip() for n in line.split('=')] if not name: continue node = Node('env_setting', comment=comment) node.append(PropertyNode('name', name)) node.append(PropertyNode('value', value)) elif len(split_line) == 6: node = Node('normal_task', comment=comment) node.append(PropertyNode('minute', split_line[0])) node.append(PropertyNode('hour', split_line[1])) node.append(PropertyNode('day_of_month', split_line[2])) node.append(PropertyNode('month', split_line[3])) node.append(PropertyNode('day_of_week', split_line[4])) node.append(PropertyNode('command', split_line[5])) else: continue root.append(node) comment = None root.comment = comment return root def stringify(self, tree): result_lines = [] stringify_func = { 'special_task': self.stringify_special_task, 'env_setting': self.stringify_env_setting, 'normal_task': self.stringify_normal_task, } for node in tree: if isinstance(node, Node): string_line = stringify_func.get(node.name, lambda x: '')(node) if node.comment: result_lines.append('#' + node.comment) result_lines.append(string_line) return '\n'.join([line for line in result_lines if line]) def stringify_special_task(self, node): special_node = node.get('special') command_node = node.get('command') if isinstance(special_node, PropertyNode) and isinstance(command_node, PropertyNode): return ' '.join([special_node.value, command_node.value]) return '' def stringify_env_setting(self, node): name = node.get('name') value = node.get('value') if isinstance(name, PropertyNode) and isinstance(value, PropertyNode): return ' = '.join([name.value, value.value]) return '' def stringify_normal_task(self, node): if all([isinstance(child, PropertyNode) for child in node.children]): values_list = [str(pr_node.value).strip() for pr_node in node.children if pr_node.value] if len(values_list) == 6: return ' '.join(values_list) return '' reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/exports.py000066400000000000000000000032571253627113700271070ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser from reconfigure.parsers.ssv import SSVParser class ExportsParser (BaseParser): """ A parser for NFS' /etc/exports """ def __init__(self, *args, **kwargs): BaseParser.__init__(self, *args, **kwargs) self.inner = SSVParser(continuation='\\') def parse(self, content): tree = self.inner.parse(content) root = RootNode() for export in tree: export_node = Node(export[0].get('value').value.strip('"')) export_node.comment = export.comment clients_node = Node('clients') export_node.append(clients_node) root.append(export_node) for client in export[1:]: s = client.get('value').value name = s.split('(')[0] options = '' if '(' in s: options = s.split('(', 1)[1].rstrip(')') client_node = Node(name) client_node.set_property('options', options) clients_node.append(client_node) return root def stringify(self, tree): root = RootNode() for export in tree: export_node = Node('line', comment=export.comment) export_node.append(Node('token', PropertyNode('value', '"%s"' % export.name))) for client in export['clients']: s = client.name if client['options'].value: s += '(%s)' % client['options'].value export_node.append(Node('token', PropertyNode('value', s))) root.append(export_node) return self.inner.stringify(root) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/ini.py000066400000000000000000000053721253627113700261620ustar00rootroot00000000000000import six from reconfigure.nodes import * from reconfigure.parsers import BaseParser from reconfigure.parsers.iniparse import INIConfig try: from StringIO import StringIO except ImportError: from io import StringIO class IniFileParser (BaseParser): """ A parser for standard ``.ini`` config files. :param sectionless: if ``True``, allows a section-less attributes appear in the beginning of file """ def __init__(self, sectionless=False, nullsection='__default__'): self.sectionless = sectionless self.nullsection = nullsection def _get_comment(self, container): c = container.contents[0].comment return c.strip() if c else None def _set_comment(self, container, comment): if comment: container.contents[0].comment = comment container.contents[0].comment_separator = ';' def parse(self, content): content = '\n'.join(filter(None, [x.strip() for x in content.splitlines()])) if self.sectionless: content = '[' + self.nullsection + ']\n' + content data = StringIO(content) cp = INIConfig(data, optionxformvalue=lambda x: x) root = RootNode() for section in cp: name = section if self.sectionless and section == self.nullsection: name = None section_node = Node(name) section_node.comment = self._get_comment(cp[section]._lines[0]) for option in cp[section]: if option in cp[section]._options: node = PropertyNode(option, cp[section][option]) node.comment = self._get_comment(cp[section]._options[option]) section_node.children.append(node) root.children.append(section_node) return root def stringify(self, tree): cp = INIConfig() for section in tree.children: if self.sectionless and section.name is None: sectionname = self.nullsection else: sectionname = section.name cp._new_namespace(sectionname) for option in section.children: if not isinstance(option, PropertyNode): raise TypeError('Third level nodes should be PropertyNodes') cp[sectionname][option.name] = option.value if option.comment: self._set_comment(cp[sectionname]._options[option.name], option.comment) if hasattr(cp[sectionname], '_lines'): self._set_comment(cp[sectionname]._lines[0], section.comment) data = (str if six.PY3 else unicode)(cp) + u'\n' if self.sectionless: data = data.replace('[' + self.nullsection + ']\n', '') return data reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iniparse/000077500000000000000000000000001253627113700266345ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iniparse/__init__.py000066400000000000000000000030731253627113700307500ustar00rootroot00000000000000# Copyright (c) 2001, 2002, 2003 Python Software Foundation # Copyright (c) 2004-2008 Paramjit Oberoi # Copyright (c) 2007 Tim Lauridsen # All Rights Reserved. See LICENSE-PSF & LICENSE for details. from reconfigure.parsers.iniparse.ini import INIConfig, change_comment_syntax from reconfigure.parsers.iniparse.config import BasicConfig, ConfigNamespace from reconfigure.parsers.iniparse.compat import RawConfigParser, ConfigParser, SafeConfigParser from reconfigure.parsers.iniparse.utils import tidy try: from ConfigParser import DuplicateSectionError, \ NoSectionError, NoOptionError, \ InterpolationMissingOptionError, \ InterpolationDepthError, \ InterpolationSyntaxError, \ DEFAULTSECT, MAX_INTERPOLATION_DEPTH except ImportError: from configparser import DuplicateSectionError, \ NoSectionError, NoOptionError, \ InterpolationMissingOptionError, \ InterpolationDepthError, \ InterpolationSyntaxError, \ DEFAULTSECT, MAX_INTERPOLATION_DEPTH __all__ = [ 'BasicConfig', 'ConfigNamespace', 'INIConfig', 'tidy', 'change_comment_syntax', 'RawConfigParser', 'ConfigParser', 'SafeConfigParser', 'DuplicateSectionError', 'NoSectionError', 'NoOptionError', 'InterpolationMissingOptionError', 'InterpolationDepthError', 'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH', ]reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iniparse/compat.py000066400000000000000000000307401253627113700304750ustar00rootroot00000000000000# Copyright (c) 2001, 2002, 2003 Python Software Foundation # Copyright (c) 2004-2008 Paramjit Oberoi # All Rights Reserved. See LICENSE-PSF & LICENSE for details. """Compatibility interfaces for ConfigParser Interfaces of ConfigParser, RawConfigParser and SafeConfigParser should be completely identical to the Python standard library versions. Tested with the unit tests included with Python-2.3.4 The underlying INIConfig object can be accessed as cfg.data """ import re try: from ConfigParser import DuplicateSectionError, \ NoSectionError, NoOptionError, \ InterpolationMissingOptionError, \ InterpolationDepthError, \ InterpolationSyntaxError, \ DEFAULTSECT, MAX_INTERPOLATION_DEPTH # These are imported only for compatiability. # The code below does not reference them directly. from ConfigParser import Error, InterpolationError, \ MissingSectionHeaderError, ParsingError except ImportError: from configparser import DuplicateSectionError, \ NoSectionError, NoOptionError, \ InterpolationMissingOptionError, \ InterpolationDepthError, \ InterpolationSyntaxError, \ DEFAULTSECT, MAX_INTERPOLATION_DEPTH # These are imported only for compatiability. # The code below does not reference them directly. from configparser import Error, InterpolationError, \ MissingSectionHeaderError, ParsingError import reconfigure.parsers.iniparse.ini class RawConfigParser(object): def __init__(self, defaults=None, dict_type=dict): if dict_type != dict: raise ValueError('Custom dict types not supported') self.data = ini.INIConfig(defaults=defaults, optionxformsource=self) def optionxform(self, optionstr): return optionstr.lower() def defaults(self): d = {} secobj = self.data._defaults for name in secobj._options: d[name] = secobj._compat_get(name) return d def sections(self): """Return a list of section names, excluding [DEFAULT]""" return list(self.data) def add_section(self, section): """Create a new section in the configuration. Raise DuplicateSectionError if a section by the specified name already exists. Raise ValueError if name is DEFAULT or any of its case-insensitive variants. """ # The default section is the only one that gets the case-insensitive # treatment - so it is special-cased here. if section.lower() == "default": raise ValueError('Invalid section name: %s' % section) if self.has_section(section): raise DuplicateSectionError(section) else: self.data._new_namespace(section) def has_section(self, section): """Indicate whether the named section is present in the configuration. The DEFAULT section is not acknowledged. """ return (section in self.data) def options(self, section): """Return a list of option names for the given section name.""" if section in self.data: return list(self.data[section]) else: raise NoSectionError(section) def read(self, filenames): """Read and parse a filename or a list of filenames. Files that cannot be opened are silently ignored; this is designed so that you can specify a list of potential configuration file locations (e.g. current directory, user's home directory, systemwide directory), and all existing configuration files in the list will be read. A single filename may also be given. """ files_read = [] if isinstance(filenames, basestring): filenames = [filenames] for filename in filenames: try: fp = open(filename) except IOError: continue files_read.append(filename) self.data._readfp(fp) fp.close() return files_read def readfp(self, fp, filename=None): """Like read() but the argument must be a file-like object. The `fp' argument must have a `readline' method. Optional second argument is the `filename', which if not given, is taken from fp.name. If fp has no `name' attribute, `' is used. """ self.data._readfp(fp) def get(self, section, option, vars=None): if not self.has_section(section): raise NoSectionError(section) if vars is not None and option in vars: value = vars[option] sec = self.data[section] if option in sec: return sec._compat_get(option) else: raise NoOptionError(option, section) def items(self, section): if section in self.data: ans = [] for opt in self.data[section]: ans.append((opt, self.get(section, opt))) return ans else: raise NoSectionError(section) def getint(self, section, option): return int(self.get(section, option)) def getfloat(self, section, option): return float(self.get(section, option)) _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False} def getboolean(self, section, option): v = self.get(section, option) if v.lower() not in self._boolean_states: raise ValueError('Not a boolean: %s' % v) return self._boolean_states[v.lower()] def has_option(self, section, option): """Check for the existence of a given option in a given section.""" if section in self.data: sec = self.data[section] else: raise NoSectionError(section) return (option in sec) def set(self, section, option, value): """Set an option.""" if section in self.data: self.data[section][option] = value else: raise NoSectionError(section) def write(self, fp): """Write an .ini-format representation of the configuration state.""" fp.write(str(self.data)) def remove_option(self, section, option): """Remove an option.""" if section in self.data: sec = self.data[section] else: raise NoSectionError(section) if option in sec: del sec[option] return 1 else: return 0 def remove_section(self, section): """Remove a file section.""" if not self.has_section(section): return False del self.data[section] return True class ConfigDict(object): """Present a dict interface to a ini section.""" def __init__(self, cfg, section, vars): self.cfg = cfg self.section = section self.vars = vars def __getitem__(self, key): try: return RawConfigParser.get(self.cfg, self.section, key, self.vars) except (NoOptionError, NoSectionError): raise KeyError(key) class ConfigParser(RawConfigParser): def get(self, section, option, raw=False, vars=None): """Get an option value for a given section. All % interpolations are expanded in the return values, based on the defaults passed into the constructor, unless the optional argument `raw' is true. Additional substitutions may be provided using the `vars' argument, which must be a dictionary whose contents overrides any pre-existing defaults. The section DEFAULT is special. """ if section != DEFAULTSECT and not self.has_section(section): raise NoSectionError(section) option = self.optionxform(option) value = RawConfigParser.get(self, section, option, vars) if raw: return value else: d = ConfigDict(self, section, vars) return self._interpolate(section, option, value, d) def _interpolate(self, section, option, rawval, vars): # do the string interpolation value = rawval depth = MAX_INTERPOLATION_DEPTH while depth: # Loop through this until it's done depth -= 1 if "%(" in value: try: value = value % vars except KeyError as e: raise InterpolationMissingOptionError( option, section, rawval, e.args[0]) else: break if value.find("%(") != -1: raise InterpolationDepthError(option, section, rawval) return value def items(self, section, raw=False, vars=None): """Return a list of tuples with (name, value) for each option in the section. All % interpolations are expanded in the return values, based on the defaults passed into the constructor, unless the optional argument `raw' is true. Additional substitutions may be provided using the `vars' argument, which must be a dictionary whose contents overrides any pre-existing defaults. The section DEFAULT is special. """ if section != DEFAULTSECT and not self.has_section(section): raise NoSectionError(section) if vars is None: options = list(self.data[section]) else: options = [] for x in self.data[section]: if x not in vars: options.append(x) options.extend(vars.keys()) if "__name__" in options: options.remove("__name__") d = ConfigDict(self, section, vars) if raw: return [(option, d[option]) for option in options] else: return [(option, self._interpolate(section, option, d[option], d)) for option in options] class SafeConfigParser(ConfigParser): _interpvar_re = re.compile(r"%\(([^)]+)\)s") _badpercent_re = re.compile(r"%[^%]|%$") def set(self, section, option, value): if not isinstance(value, basestring): raise TypeError("option values must be strings") # check for bad percent signs: # first, replace all "good" interpolations tmp_value = self._interpvar_re.sub('', value) # then, check if there's a lone percent sign left m = self._badpercent_re.search(tmp_value) if m: raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, m.start())) ConfigParser.set(self, section, option, value) def _interpolate(self, section, option, rawval, vars): # do the string interpolation L = [] self._interpolate_some(option, L, rawval, section, vars, 1) return ''.join(L) _interpvar_match = re.compile(r"%\(([^)]+)\)s").match def _interpolate_some(self, option, accum, rest, section, map, depth): if depth > MAX_INTERPOLATION_DEPTH: raise InterpolationDepthError(option, section, rest) while rest: p = rest.find("%") if p < 0: accum.append(rest) return if p > 0: accum.append(rest[:p]) rest = rest[p:] # p is no longer used c = rest[1:2] if c == "%": accum.append("%") rest = rest[2:] elif c == "(": m = self._interpvar_match(rest) if m is None: raise InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest) var = m.group(1) rest = rest[m.end():] try: v = map[var] except KeyError: raise InterpolationMissingOptionError( option, section, rest, var) if "%" in v: self._interpolate_some(option, accum, v, section, map, depth + 1) else: accum.append(v) else: raise InterpolationSyntaxError( option, section, "'%' must be followed by '%' or '(', found: " + repr(rest))reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iniparse/config.py000066400000000000000000000201621253627113700304540ustar00rootroot00000000000000class ConfigNamespace(object): """Abstract class representing the interface of Config objects. A ConfigNamespace is a collection of names mapped to values, where the values may be nested namespaces. Values can be accessed via container notation - obj[key] - or via dotted notation - obj.key. Both these access methods are equivalent. To minimize name conflicts between namespace keys and class members, the number of class members should be minimized, and the names of all class members should start with an underscore. Subclasses must implement the methods for container-like access, and this class will automatically provide dotted access. """ # Methods that must be implemented by subclasses def _getitem(self, key): return NotImplementedError(key) def __setitem__(self, key, value): raise NotImplementedError(key, value) def __delitem__(self, key): raise NotImplementedError(key) def __iter__(self): return NotImplementedError() def _new_namespace(self, name): raise NotImplementedError(name) def __contains__(self, key): try: self._getitem(key) except KeyError: return False return True # Machinery for converting dotted access into container access, # and automatically creating new sections/namespaces. # # To distinguish between accesses of class members and namespace # keys, we first call object.__getattribute__(). If that succeeds, # the name is assumed to be a class member. Otherwise it is # treated as a namespace key. # # Therefore, member variables should be defined in the class, # not just in the __init__() function. See BasicNamespace for # an example. def __getitem__(self, key): try: return self._getitem(key) except KeyError: return Undefined(key, self) def __getattr__(self, name): try: return self._getitem(name) except KeyError: if name.startswith('__') and name.endswith('__'): raise AttributeError return Undefined(name, self) def __setattr__(self, name, value): try: object.__getattribute__(self, name) object.__setattr__(self, name, value) except AttributeError: self.__setitem__(name, value) def __delattr__(self, name): try: object.__getattribute__(self, name) object.__delattr__(self, name) except AttributeError: self.__delitem__(name) # During unpickling, Python checks if the class has a __setstate__ # method. But, the data dicts have not been initialised yet, which # leads to _getitem and hence __getattr__ raising an exception. So # we explicitly impement default __setstate__ behavior. def __setstate__(self, state): self.__dict__.update(state) class Undefined(object): """Helper class used to hold undefined names until assignment. This class helps create any undefined subsections when an assignment is made to a nested value. For example, if the statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet. """ def __init__(self, name, namespace): object.__setattr__(self, 'name', name) object.__setattr__(self, 'namespace', namespace) def __setattr__(self, name, value): obj = self.namespace._new_namespace(self.name) obj[name] = value def __setitem__(self, name, value): obj = self.namespace._new_namespace(self.name) obj[name] = value # ---- Basic implementation of a ConfigNamespace class BasicConfig(ConfigNamespace): """Represents a hierarchical collection of named values. Values are added using dotted notation: >>> n = BasicConfig() >>> n.x = 7 >>> n.name.first = 'paramjit' >>> n.name.last = 'oberoi' ...and accessed the same way, or with [...]: >>> n.x 7 >>> n.name.first 'paramjit' >>> n.name.last 'oberoi' >>> n['x'] 7 >>> n['name']['first'] 'paramjit' Iterating over the namespace object returns the keys: >>> l = list(n) >>> l.sort() >>> l ['name', 'x'] Values can be deleted using 'del' and printed using 'print'. >>> n.aaa = 42 >>> del n.x >>> print n aaa = 42 name.first = paramjit name.last = oberoi Nested namepsaces are also namespaces: >>> isinstance(n.name, ConfigNamespace) True >>> print n.name first = paramjit last = oberoi >>> sorted(list(n.name)) ['first', 'last'] Finally, values can be read from a file as follows: >>> from StringIO import StringIO >>> sio = StringIO(''' ... # comment ... ui.height = 100 ... ui.width = 150 ... complexity = medium ... have_python ... data.secret.password = goodness=gracious me ... ''') >>> n = BasicConfig() >>> n._readfp(sio) >>> print n complexity = medium data.secret.password = goodness=gracious me have_python ui.height = 100 ui.width = 150 """ # this makes sure that __setattr__ knows this is not a namespace key _data = None def __init__(self): self._data = {} def _getitem(self, key): return self._data[key] def __setitem__(self, key, value): self._data[key] = value def __delitem__(self, key): del self._data[key] def __iter__(self): return iter(self._data) def __str__(self, prefix=''): lines = [] keys = self._data.keys() keys.sort() for name in keys: value = self._data[name] if isinstance(value, ConfigNamespace): lines.append(value.__str__(prefix='%s%s.' % (prefix,name))) else: if value is None: lines.append('%s%s' % (prefix, name)) else: lines.append('%s%s = %s' % (prefix, name, value)) return '\n'.join(lines) def _new_namespace(self, name): obj = BasicConfig() self._data[name] = obj return obj def _readfp(self, fp): while True: line = fp.readline() if not line: break line = line.strip() if not line: continue if line[0] == '#': continue data = line.split('=', 1) if len(data) == 1: name = line value = None else: name = data[0].strip() value = data[1].strip() name_components = name.split('.') ns = self for n in name_components[:-1]: if n in ns: ns = ns[n] if not isinstance(ns, ConfigNamespace): raise TypeError('value-namespace conflict', n) else: ns = ns._new_namespace(n) ns[name_components[-1]] = value # ---- Utility functions def update_config(target, source): """Imports values from source into target. Recursively walks the ConfigNamespace and inserts values into the ConfigNamespace. For example: >>> n = BasicConfig() >>> n.playlist.expand_playlist = True >>> n.ui.display_clock = True >>> n.ui.display_qlength = True >>> n.ui.width = 150 >>> print n playlist.expand_playlist = True ui.display_clock = True ui.display_qlength = True ui.width = 150 >>> from iniparse import ini >>> i = ini.INIConfig() >>> update_config(i, n) >>> print i [playlist] expand_playlist = True [ui] display_clock = True display_qlength = True width = 150 """ for name in source: value = source[name] if isinstance(value, ConfigNamespace): if name in target: myns = target[name] if not isinstance(myns, ConfigNamespace): raise TypeError('value-namespace conflict') else: myns = target._new_namespace(name) update_config(myns, value) else: target[name] = value reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iniparse/ini.py000066400000000000000000000504751253627113700300000ustar00rootroot00000000000000from __future__ import unicode_literals """Access and/or modify INI files * Compatiable with ConfigParser * Preserves order of sections & options * Preserves comments/blank lines/etc * More conveninet access to data Example: >>> from StringIO import StringIO >>> sio = StringIO('''# configure foo-application ... [foo] ... bar1 = qualia ... bar2 = 1977 ... [foo-ext] ... special = 1''') >>> cfg = INIConfig(sio) >>> print cfg.foo.bar1 qualia >>> print cfg['foo-ext'].special 1 >>> cfg.foo.newopt = 'hi!' >>> cfg.baz.enabled = 0 >>> print cfg # configure foo-application [foo] bar1 = qualia bar2 = 1977 newopt = hi! [foo-ext] special = 1 [baz] enabled = 0 """ # An ini parser that supports ordered sections/options # Also supports updates, while preserving structure # Backward-compatiable with ConfigParser import re try: from ConfigParser import DEFAULTSECT, ParsingError, MissingSectionHeaderError except ImportError: from configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError from reconfigure.parsers.iniparse import config class LineType(object): line = None def __init__(self, line=None): if line is not None: self.line = line.strip('\n') # Return the original line for unmodified objects # Otherwise construct using the current attribute values def __str__(self): if self.line is not None: return self.line else: return self.to_string() # If an attribute is modified after initialization # set line to None since it is no longer accurate. def __setattr__(self, name, value): if hasattr(self,name): self.__dict__['line'] = None self.__dict__[name] = value def to_string(self): raise Exception('This method must be overridden in derived classes') class SectionLine(LineType): regex = re.compile(r'^\[' r'(?P[^]]+)' r'\]\s*' r'((?P;|#)(?P.*))?$') def __init__(self, name, comment=None, comment_separator=None, comment_offset=-1, line=None): super(SectionLine, self).__init__(line) self.name = name self.comment = comment self.comment_separator = comment_separator self.comment_offset = comment_offset def to_string(self): out = '[' + self.name + ']' if self.comment is not None: # try to preserve indentation of comments out = (out+' ').ljust(self.comment_offset) out = out + self.comment_separator + self.comment return out def parse(cls, line): m = cls.regex.match(line.rstrip()) if m is None: return None return cls(m.group('name'), m.group('comment'), m.group('csep'), m.start('csep'), line) parse = classmethod(parse) class OptionLine(LineType): def __init__(self, name, value, separator='=', comment=None, comment_separator=None, comment_offset=-1, line=None): super(OptionLine, self).__init__(line) self.name = name self.value = value self.separator = separator self.comment = comment self.comment_separator = comment_separator self.comment_offset = comment_offset def to_string(self): out = '%s%s%s' % (self.name, self.separator, self.value) if self.comment is not None: # try to preserve indentation of comments out = (out+' ').ljust(self.comment_offset) out = out + self.comment_separator + self.comment return out regex = re.compile(r'^(?P[^=\s[][^=]*)' r'(?P[=]\s*)' r'(?P.*)$') def parse(cls, line): m = cls.regex.match(line.rstrip()) if m is None: return None name = m.group('name').rstrip() value = m.group('value') sep = m.group('name')[len(name):] + m.group('sep') # comments are not detected in the regex because # ensuring total compatibility with ConfigParser # requires that: # option = value ;comment // value=='value' # option = value;1 ;comment // value=='value;1 ;comment' # # Doing this in a regex would be complicated. I # think this is a bug. The whole issue of how to # include ';' in the value needs to be addressed. # Also, '#' doesn't mark comments in options... coff = value.find(';') if coff != -1 and value[coff-1].isspace(): comment = value[coff+1:] csep = value[coff] value = value[:coff].rstrip() coff = m.start('value') + coff else: comment = None csep = None coff = -1 return cls(name, value, sep, comment, csep, coff, line) parse = classmethod(parse) def change_comment_syntax(comment_chars='%;#', allow_rem=False): comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars) regex = r'^(?P[%s]' % comment_chars if allow_rem: regex += '|[rR][eE][mM]' regex += r')(?P.*)$' CommentLine.regex = re.compile(regex) class CommentLine(LineType): regex = re.compile(r'^(?P[;#]|[rR][eE][mM])' r'(?P.*)$') def __init__(self, comment='', separator='#', line=None): super(CommentLine, self).__init__(line) self.comment = comment self.separator = separator def to_string(self): return self.separator + self.comment def parse(cls, line): m = cls.regex.match(line.rstrip()) if m is None: return None return cls(m.group('comment'), m.group('csep'), line) parse = classmethod(parse) class EmptyLine(LineType): # could make this a singleton def to_string(self): return '' value = property(lambda _: '') def parse(cls, line): if line.strip(): return None return cls(line) parse = classmethod(parse) class ContinuationLine(LineType): regex = re.compile(r'^\s+(?P.*)$') def __init__(self, value, value_offset=None, line=None): super(ContinuationLine, self).__init__(line) self.value = value if value_offset is None: value_offset = 8 self.value_offset = value_offset def to_string(self): return ' '*self.value_offset + self.value def parse(cls, line): m = cls.regex.match(line.rstrip()) if m is None: return None return cls(m.group('value'), m.start('value'), line) parse = classmethod(parse) class LineContainer(object): def __init__(self, d=None): self.contents = [] self.orgvalue = None if d: if isinstance(d, list): self.extend(d) else: self.add(d) def add(self, x): self.contents.append(x) def extend(self, x): for i in x: self.add(i) def get_name(self): return self.contents[0].name def set_name(self, data): self.contents[0].name = data def get_value(self): if self.orgvalue is not None: return self.orgvalue elif len(self.contents) == 1: return self.contents[0].value else: return '\n'.join([('%s' % x.value) for x in self.contents if not isinstance(x, CommentLine)]) def set_value(self, data): self.orgvalue = data lines = ('%s' % data).split('\n') # If there is an existing ContinuationLine, use its offset value_offset = None for v in self.contents: if isinstance(v, ContinuationLine): value_offset = v.value_offset break # Rebuild contents list, preserving initial OptionLine self.contents = self.contents[0:1] self.contents[0].value = lines[0] del lines[0] for line in lines: if line.strip(): self.add(ContinuationLine(line, value_offset)) else: self.add(EmptyLine()) name = property(get_name, set_name) value = property(get_value, set_value) def __str__(self): s = [x.__str__() for x in self.contents] return '\n'.join(s) def finditer(self, key): for x in self.contents[::-1]: if hasattr(x, 'name') and x.name==key: yield x def find(self, key): for x in self.finditer(key): return x raise KeyError(key) def _make_xform_property(myattrname, srcattrname=None): private_attrname = myattrname + 'value' private_srcname = myattrname + 'source' if srcattrname is None: srcattrname = myattrname def getfn(self): srcobj = getattr(self, private_srcname) if srcobj is not None: return getattr(srcobj, srcattrname) else: return getattr(self, private_attrname) def setfn(self, value): srcobj = getattr(self, private_srcname) if srcobj is not None: setattr(srcobj, srcattrname, value) else: setattr(self, private_attrname, value) return property(getfn, setfn) class INISection(config.ConfigNamespace): _lines = None _options = None _defaults = None _optionxformvalue = None _optionxformsource = None _compat_skip_empty_lines = set() def __init__(self, lineobj, defaults = None, optionxformvalue=None, optionxformsource=None): self._lines = [lineobj] self._defaults = defaults self._optionxformvalue = optionxformvalue self._optionxformsource = optionxformsource self._options = {} _optionxform = _make_xform_property('_optionxform') def _compat_get(self, key): # identical to __getitem__ except that _compat_XXX # is checked for backward-compatible handling if key == '__name__': return self._lines[-1].name if self._optionxform: key = self._optionxform(key) try: value = self._options[key].value del_empty = key in self._compat_skip_empty_lines except KeyError: if self._defaults and key in self._defaults._options: value = self._defaults._options[key].value del_empty = key in self._defaults._compat_skip_empty_lines else: raise if del_empty: value = re.sub('\n+', '\n', value) return value def _getitem(self, key): if key == '__name__': return self._lines[-1].name if self._optionxform: key = self._optionxform(key) try: return self._options[key].value except KeyError: if self._defaults and key in self._defaults._options: return self._defaults._options[key].value else: raise def __setitem__(self, key, value): if self._optionxform: xkey = self._optionxform(key) else: xkey = key if xkey in self._compat_skip_empty_lines: self._compat_skip_empty_lines.remove(xkey) if xkey not in self._options: # create a dummy object - value may have multiple lines obj = LineContainer(OptionLine(key, '')) self._lines[-1].add(obj) self._options[xkey] = obj # the set_value() function in LineContainer # automatically handles multi-line values self._options[xkey].value = value def __delitem__(self, key): if self._optionxform: key = self._optionxform(key) if key in self._compat_skip_empty_lines: self._compat_skip_empty_lines.remove(key) for l in self._lines: remaining = [] for o in l.contents: if isinstance(o, LineContainer): n = o.name if self._optionxform: n = self._optionxform(n) if key != n: remaining.append(o) else: remaining.append(o) l.contents = remaining del self._options[key] def __iter__(self): d = set() for l in self._lines: for x in l.contents: if isinstance(x, LineContainer): if self._optionxform: ans = self._optionxform(x.name) else: ans = x.name if ans not in d: yield ans d.add(ans) if self._defaults: for x in self._defaults: if x not in d: yield x d.add(x) def _new_namespace(self, name): raise Exception('No sub-sections allowed', name) def make_comment(line): return CommentLine(line.rstrip('\n')) def readline_iterator(f): """iterate over a file by only using the file object's readline method""" have_newline = False while True: line = f.readline() if not line: if have_newline: yield "" return if line.endswith('\n'): have_newline = True else: have_newline = False yield line def lower(x): return x.lower() class INIConfig(config.ConfigNamespace): _data = None _sections = None _defaults = None _optionxformvalue = None _optionxformsource = None _sectionxformvalue = None _sectionxformsource = None _parse_exc = None _bom = False def __init__(self, fp=None, defaults=None, parse_exc=True, optionxformvalue=lower, optionxformsource=None, sectionxformvalue=None, sectionxformsource=None): self._data = LineContainer() self._parse_exc = parse_exc self._optionxformvalue = optionxformvalue self._optionxformsource = optionxformsource self._sectionxformvalue = sectionxformvalue self._sectionxformsource = sectionxformsource self._sections = {} if defaults is None: defaults = {} self._defaults = INISection(LineContainer(), optionxformsource=self) for name, value in defaults.items(): self._defaults[name] = value if fp is not None: self._readfp(fp) _optionxform = _make_xform_property('_optionxform', 'optionxform') _sectionxform = _make_xform_property('_sectionxform', 'optionxform') def _getitem(self, key): if key == DEFAULTSECT: return self._defaults if self._sectionxform: key = self._sectionxform(key) return self._sections[key] def __setitem__(self, key, value): raise Exception('Values must be inside sections', key, value) def __delitem__(self, key): if self._sectionxform: key = self._sectionxform(key) for line in self._sections[key]._lines: self._data.contents.remove(line) del self._sections[key] def __iter__(self): d = set() d.add(DEFAULTSECT) for x in self._data.contents: if isinstance(x, LineContainer): if x.name not in d: yield x.name d.add(x.name) def _new_namespace(self, name): if self._data.contents: self._data.add(EmptyLine()) obj = LineContainer(SectionLine(name)) self._data.add(obj) if self._sectionxform: name = self._sectionxform(name) if name in self._sections: ns = self._sections[name] ns._lines.append(obj) else: ns = INISection(obj, defaults=self._defaults, optionxformsource=self) self._sections[name] = ns return ns def __str__(self): if self._bom: fmt = '\ufeff%s' else: fmt = '%s' return fmt % self._data.__str__() __unicode__ = __str__ _line_types = [EmptyLine, CommentLine, SectionLine, OptionLine, ContinuationLine] def _parse(self, line): for linetype in self._line_types: lineobj = linetype.parse(line) if lineobj: return lineobj else: # can't parse line return None def _readfp(self, fp): cur_section = None cur_option = None cur_section_name = None cur_option_name = None pending_lines = [] pending_empty_lines = False try: fname = fp.name except AttributeError: fname = '' linecount = 0 exc = None line = None for line in readline_iterator(fp): # Check for BOM on first line if linecount == 0: if line[0] == '\ufeff': line = line[1:] self._bom = True lineobj = self._parse(line) linecount += 1 if not cur_section and not isinstance(lineobj, (CommentLine, EmptyLine, SectionLine)): if self._parse_exc: raise MissingSectionHeaderError(fname, linecount, line) else: lineobj = make_comment(line) if lineobj is None: if self._parse_exc: if exc is None: exc = ParsingError(fname) exc.append(linecount, line) lineobj = make_comment(line) if isinstance(lineobj, ContinuationLine): if cur_option: if pending_lines: cur_option.extend(pending_lines) pending_lines = [] if pending_empty_lines: optobj._compat_skip_empty_lines.add(cur_option_name) pending_empty_lines = False cur_option.add(lineobj) else: # illegal continuation line - convert to comment if self._parse_exc: if exc is None: exc = ParsingError(fname) exc.append(linecount, line) lineobj = make_comment(line) if isinstance(lineobj, OptionLine): if pending_lines: cur_section.extend(pending_lines) pending_lines = [] pending_empty_lines = False cur_option = LineContainer(lineobj) cur_section.add(cur_option) if self._optionxform: cur_option_name = self._optionxform(cur_option.name) else: cur_option_name = cur_option.name if cur_section_name == DEFAULTSECT: optobj = self._defaults else: optobj = self._sections[cur_section_name] optobj._options[cur_option_name] = cur_option if isinstance(lineobj, SectionLine): self._data.extend(pending_lines) pending_lines = [] pending_empty_lines = False cur_section = LineContainer(lineobj) self._data.add(cur_section) cur_option = None cur_option_name = None if cur_section.name == DEFAULTSECT: self._defaults._lines.append(cur_section) cur_section_name = DEFAULTSECT else: if self._sectionxform: cur_section_name = self._sectionxform(cur_section.name) else: cur_section_name = cur_section.name if cur_section_name not in self._sections: self._sections[cur_section_name] = \ INISection(cur_section, defaults=self._defaults, optionxformsource=self) else: self._sections[cur_section_name]._lines.append(cur_section) if isinstance(lineobj, (CommentLine, EmptyLine)): pending_lines.append(lineobj) if isinstance(lineobj, EmptyLine): pending_empty_lines = True self._data.extend(pending_lines) if line and line[-1]=='\n': self._data.add(EmptyLine()) if exc: raise exc reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iniparse/utils.py000066400000000000000000000024601253627113700303500ustar00rootroot00000000000000from reconfigure.parsers.iniparse import compat from reconfigure.parsers.iniparse.ini import LineContainer, EmptyLine def tidy(cfg): """Clean up blank lines. This functions makes the configuration look clean and handwritten - consecutive empty lines and empty lines at the start of the file are removed, and one is guaranteed to be at the end of the file. """ if isinstance(cfg, compat.RawConfigParser): cfg = cfg.data cont = cfg._data.contents i = 1 while i < len(cont): if isinstance(cont[i], LineContainer): tidy_section(cont[i]) i += 1 elif (isinstance(cont[i-1], EmptyLine) and isinstance(cont[i], EmptyLine)): del cont[i] else: i += 1 # Remove empty first line if cont and isinstance(cont[0], EmptyLine): del cont[0] # Ensure a last line if cont and not isinstance(cont[-1], EmptyLine): cont.append(EmptyLine()) def tidy_section(lc): cont = lc.contents i = 1 while i < len(cont): if (isinstance(cont[i-1], EmptyLine) and isinstance(cont[i], EmptyLine)): del cont[i] else: i += 1 # Remove empty first line if len(cont) > 1 and isinstance(cont[1], EmptyLine): del cont[1]reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/iptables.py000066400000000000000000000056441253627113700272100ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser class IPTablesParser (BaseParser): """ A parser for ``iptables`` configuration as produced by ``iptables-save`` """ def parse(self, content): content = filter(None, [x.strip() for x in content.splitlines() if not x.startswith('#')]) root = RootNode() cur_table = None chains = {} for l in content: if l.startswith('*'): cur_table = Node(l[1:]) chains = {} root.append(cur_table) elif l.startswith(':'): name = l[1:].split()[0] node = Node(name) node.set_property('default', l.split()[1]) chains[name] = node cur_table.append(node) else: comment = None if '#' in l: l, comment = l.split('#') comment = comment.strip() tokens = l.split() if tokens[0] == '-A': tokens.pop(0) node = Node('append') node.comment = comment chain = tokens.pop(0) chains[chain].append(node) while tokens: token = tokens.pop(0) option = Node('option') option.set_property('negative', token == '!') if token == '!': token = tokens.pop(0) option.set_property('name', token.strip('-')) while tokens and not tokens[0].startswith('-') and tokens[0] != '!': option.append(Node('argument', PropertyNode('value', tokens.pop(0)))) node.append(option) return root def stringify(self, tree): data = '' for table in tree.children: data += '*%s\n' % table.name for chain in table.children: data += ':%s %s [0:0]\n' % (chain.name, chain.get('default').value) for chain in table.children: for item in chain.children: if item.name == 'append': data += '-A %s %s%s\n' % ( chain.name, ' '.join( ('! ' if o.get('negative').value else '') + ('--' if len(o.get('name').value) > 1 else '-') + o.get('name').value + ' ' + ' '.join(a.get('value').value for a in o.children if a.name == 'argument') for o in item.children if o.name == 'option' ), ' # %s' % item.comment if item.comment else '' ) data += 'COMMIT\n' return data reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/jsonparser.py000066400000000000000000000017751253627113700275740ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser import json class JsonParser (BaseParser): """ A parser for JSON files (using ``json`` module) """ def parse(self, content): node = RootNode() self.load_node_rec(node, json.loads(content)) return node def load_node_rec(self, node, json): for k in sorted(json.keys()): v = json[k] if isinstance(v, dict): child = Node(k) node.children.append(child) self.load_node_rec(child, v) else: node.children.append(PropertyNode(k, v)) def stringify(self, tree): return json.dumps(self.save_node_rec(tree), indent=4) def save_node_rec(self, node): r = {} for child in node.children: if isinstance(child, PropertyNode): r[child.name] = child.value else: r[child.name] = self.save_node_rec(child) return r reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/nginx.py000066400000000000000000000060351253627113700265230ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser import re class NginxParser (BaseParser): """ A parser for nginx configs """ tokens = [ (r"[\w_]+\s*?.*?{", lambda s, t: ('section_start', t)), (r"[\w_]+?.+?;", lambda s, t: ('option', t)), (r"\s", lambda s, t: 'whitespace'), (r"$^", lambda s, t: 'newline'), (r"\#.*?\n", lambda s, t: ('comment', t)), (r"\}", lambda s, t: 'section_end'), ] token_comment = '#' token_section_end = '}' def parse(self, content): scanner = re.Scanner(self.tokens) tokens, remainder = scanner.scan(' '.join(filter(None, content.split(' ')))) if remainder: raise Exception('Invalid tokens: %s. Tokens: %s' % (remainder, tokens)) node = RootNode() node.parameter = None node_stack = [] next_comment = None while len(tokens) > 0: token = tokens[0] tokens = tokens[1:] if token in ['whitespace', 'newline']: continue if token == 'section_end': node = node_stack.pop() if token[0] == 'comment': if not next_comment: next_comment = '' else: next_comment += '\n' next_comment += token[1].strip('#/*').strip() if token[0] == 'option': if ' ' in token[1] and not token[1][0] in ['"', "'"]: k, v = token[1].split(None, 1) else: v = token[1] k = '' prop = PropertyNode(k.strip(), v[:-1].strip()) prop.comment = next_comment next_comment = None node.children.append(prop) if token[0] == 'section_start': line = token[1][:-1].strip().split(None, 1) + [None] section = Node(line[0]) section.parameter = line[1] section.comment = next_comment next_comment = None node_stack += [node] node.children.append(section) node = section return node def stringify(self, tree): return ''.join(self.stringify_rec(node) for node in tree.children) def stringify_rec(self, node): if isinstance(node, PropertyNode): if node.name: s = '%s %s;\n' % (node.name, node.value) else: s = '%s;\n' % (node.value) elif isinstance(node, IncludeNode): s = 'include %s;\n' % (node.files) else: result = '\n%s %s {\n' % (node.name, node.parameter or '') for child in node.children: result += '\n'.join('\t' + x for x in self.stringify_rec(child).splitlines()) + '\n' result += self.token_section_end + '\n' s = result if node.comment: s = ''.join(self.token_comment + ' %s\n' % l for l in node.comment.splitlines()) + s return s reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/nsd.py000066400000000000000000000031341253627113700261610ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser class NSDParser (BaseParser): """ A parser for NSD DNS server nsd.conf file """ def parse(self, content): lines = content.splitlines() root = RootNode() last_comment = None node = root for line in lines: line = line.strip() if line: if line.startswith('#'): c = line.strip('#').strip() if last_comment: last_comment += '\n' + c else: last_comment = c continue key, value = line.split(':') value = value.strip() key = key.strip() if key in ['server', 'zone', 'key']: node = Node(key, comment=last_comment) root.append(node) else: node.append(PropertyNode(key, value, comment=last_comment)) last_comment = None return root def stringify_comment(self, line, comment): if comment: return ''.join('# %s\n' % x for x in comment.splitlines()) + line return line def stringify(self, tree): r = '' for node in tree.children: r += self.stringify_comment(node.name + ':', node.comment) + '\n' for subnode in node.children: l = '%s: %s' % (subnode.name, subnode.value) r += self.stringify_comment(l, subnode.comment) + '\n' r += '\n' return r reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/shell.py000066400000000000000000000040321253627113700265020ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser class ShellParser (BaseParser): """ A parser for shell scripts with variables """ def __init__(self, *args, **kwargs): self.comment = '#' self.continuation = '\\' BaseParser.__init__(self, *args, **kwargs) def parse(self, content): rawlines = content.splitlines() lines = [] while rawlines: l = rawlines.pop(0).strip() while self.continuation and rawlines and l.endswith(self.continuation): l = l[:-len(self.continuation)] l += rawlines.pop(0) lines.append(l) root = RootNode() last_comment = None for line in lines: line = line.strip() if line: if line.startswith(self.comment): c = line.strip(self.comment).strip() if last_comment: last_comment += '\n' + c else: last_comment = c continue if len(line) == 0: continue name, value = line.split('=', 1) comment = None if '#' in value: value, comment = value.split('#', 1) last_comment = (last_comment or '') + comment.strip() node = PropertyNode(name.strip(), value.strip().strip('"')) if last_comment: node.comment = last_comment last_comment = None root.append(node) return root def stringify(self, tree): r = '' for node in tree.children: if node.comment and '\n' in node.comment: r += '\n' + ''.join('# %s\n' % x for x in node.comment.splitlines()) r += '%s="%s"' % (node.name, node.value) if node.comment and not '\n' in node.comment: r += ' # %s' % node.comment r += '\n' return r reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/squid.py000066400000000000000000000034111253627113700265200ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser class SquidParser (BaseParser): """ A parser for Squid configs """ def parse(self, content): lines = filter(None, [x.strip() for x in content.splitlines()]) root = RootNode() last_comment = None for line in lines: line = line.strip() if line.startswith('#'): c = line.strip('#').strip() if last_comment: last_comment += '\n' + c else: last_comment = c continue if len(line) == 0: continue tokens = line.split() node = Node('line', Node('arguments')) if last_comment: node.comment = last_comment last_comment = None index = 0 for token in tokens: if token.startswith('#'): node.comment = ' '.join(tokens[tokens.index(token):])[1:].strip() break if index == 0: node.set_property('name', token) else: node.get('arguments').set_property(str(index), token) index += 1 root.append(node) return root def stringify(self, tree): r = '' for node in tree.children: if node.comment and '\n' in node.comment: r += ''.join('%s %s\n' % ('#', x) for x in node.comment.splitlines()) r += node.get('name').value + ' ' + ' '.join(x.value for x in node.get('arguments').children) if node.comment and not '\n' in node.comment: r += ' # %s' % node.comment r += '\n' return r reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/parsers/ssv.py000066400000000000000000000053451253627113700262160ustar00rootroot00000000000000from reconfigure.nodes import * from reconfigure.parsers import BaseParser class SSVParser (BaseParser): """ A parser for files containing space-separated value (notably, ``/etc/fstab`` and friends) :param separator: separator character, defaults to whitespace :param maxsplit: max number of tokens per line, defaults to infinity :param comment: character denoting comments :param continuation: line continuation character, None to disable """ def __init__(self, separator=None, maxsplit=-1, comment='#', continuation=None, *args, **kwargs): self.separator = separator self.maxsplit = maxsplit self.comment = comment self.continuation = continuation BaseParser.__init__(self, *args, **kwargs) def parse(self, content): rawlines = content.splitlines() lines = [] while rawlines: l = rawlines.pop(0).strip() while self.continuation and rawlines and l.endswith(self.continuation): l = l[:-len(self.continuation)] l += rawlines.pop(0) lines.append(l) root = RootNode() last_comment = None for line in lines: line = line.strip() if line: if line.startswith(self.comment): c = line.strip(self.comment).strip() if last_comment: last_comment += '\n' + c else: last_comment = c continue if len(line) == 0: continue tokens = line.split(self.separator, self.maxsplit) node = Node('line') if last_comment: node.comment = last_comment last_comment = None for token in tokens: if token.startswith(self.comment): node.comment = ' '.join(tokens[tokens.index(token):])[1:].strip() break node.append(Node( name='token', children=[ PropertyNode(name='value', value=token) ] )) root.append(node) return root def stringify(self, tree): r = '' for node in tree.children: if node.comment and '\n' in node.comment: r += ''.join('%s %s\n' % (self.comment, x) for x in node.comment.splitlines()) r += (self.separator or '\t').join(x.get('value').value for x in node.children) if node.comment and not '\n' in node.comment: r += ' # %s' % node.comment r += '\n' return r reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/000077500000000000000000000000001253627113700245055ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/__init__.py000066400000000000000000000000001253627113700266040ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/000077500000000000000000000000001253627113700261355ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/__init__.py000066400000000000000000000000001253627113700302340ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/ajenti_tests.py000066400000000000000000000023631253627113700312070ustar00rootroot00000000000000import json from reconfigure.configs import AjentiConfig from reconfigure.tests.configs.base_test import BaseConfigTest class AjentiConfigTest (BaseConfigTest): sources = { None: """{ "authentication": false, "bind": { "host": "0.0.0.0", "port": 8000 }, "language": null, "enable_feedback": true, "installation_id": null, "users": { "test": { "configs": { "a": "{}" }, "password": "sha512", "permissions": [ "section:Dash" ] } }, "ssl": { "enable": false, "certificate_path": "" } } """ } result = { 'authentication': False, 'enable_feedback': True, 'installation_id': None, 'language': None, 'http_binding': {'host': '0.0.0.0', 'port': 8000}, 'ssl': {'certificate_path': '', 'enable': False}, 'users': {'test': { 'configs': {'a': {'data': {}, 'name': 'a'}}, 'email': None, 'name': 'test', 'password': 'sha512', 'permissions': ['section:Dash'] }} } config = AjentiConfig stringify_filter = staticmethod(lambda x: json.loads(str(x))) del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/base_test.py000066400000000000000000000022451253627113700304630ustar00rootroot00000000000000import unittest import json class BaseConfigTest (unittest.TestCase): sources = "" result = None config = None config_kwargs = {} stringify_filter = staticmethod(lambda x: x.split()) def test_config(self): if not self.config: return self.maxDiff = None config = self.config(content=self.sources[None], **self.config_kwargs) if config.includer: config.includer.content_map = self.sources config.load() #print config.tree._node #print 'RESULT', config.tree.to_dict() #print 'SOURCE', self.__class__.result #self.assertTrue(self.__class__.result== config.tree.to_dict()) a, b = self.__class__.result, config.tree.to_dict() if a != b: print('SOURCE: %s\nGENERATED: %s\n' % (json.dumps(a, indent=4), json.dumps(b, indent=4))) self.assertEquals(a, b) result = config.save() s_filter = self.__class__.stringify_filter #print s_filter(result[None]) for k, v in result.items(): self.assertEquals( s_filter(self.__class__.sources[k]), s_filter(v) ) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/bind9_tests.py000066400000000000000000000007231253627113700307400ustar00rootroot00000000000000from reconfigure.configs import BIND9Config from reconfigure.tests.configs.base_test import BaseConfigTest class BIND9ConfigTest (BaseConfigTest): sources = { None: """ zone "asd" { type master; file "/file"; }; """ } result = { "zones": [ { "type": "master", "name": "asd", "file": "/file" } ] } config = BIND9Config del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/crontab_tests.py000066400000000000000000000023331253627113700313620ustar00rootroot00000000000000from reconfigure.configs import CrontabConfig from reconfigure.tests.configs.base_test import BaseConfigTest class CrontabConfigTest (BaseConfigTest): sources = { None: """#comment line * * * * * date @reboot ls -al 1 * 0 1 2 date -s NAME = TEST""" } result = { 'normal_tasks': [ { 'minute': '*', 'hour': '*', 'day_of_month': '*', 'month': '*', 'day_of_week': '*', 'command': 'date', 'comment': 'comment line' }, { 'minute': '1', 'hour': '*', 'day_of_month': '0', 'month': '1', 'day_of_week': '2', 'command': 'date -s', 'comment': None, }, ], 'special_tasks': [ { 'special': '@reboot', 'command': 'ls -al', 'comment': None, } ], 'env_settings': [ { 'name': 'NAME', 'value': 'TEST', 'comment': None } ] } config = CrontabConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/csf_tests.py000066400000000000000000000020301253627113700304770ustar00rootroot00000000000000from reconfigure.configs import CSFConfig from reconfigure.tests.configs.base_test import BaseConfigTest class CSFConfigTest (BaseConfigTest): sources = { None: """ TESTING="1" TCP_IN="20,21,22,25,53,80,110,143,443,465,587,993,995" TCP_OUT="20,21,22,25,53,80,110,113,443" UDP_IN="20,21,53" UDP_OUT="20,21,53,113,123" IPV6="0" TCP6_IN="20,21,22,25,53,80,110,143,443,465,587,993,995" TCP6_OUT="20,21,22,25,53,80,110,113,443" UDP6_IN="20,21,53" UDP6_OUT="20,21,53,113,123" ETH_DEVICE="" ETH6_DEVICE="" """ } result = { "tcp6_out": "20,21,22,25,53,80,110,113,443", "testing": True, "eth_device": "", "tcp_in": "20,21,22,25,53,80,110,143,443,465,587,993,995", "tcp6_in": "20,21,22,25,53,80,110,143,443,465,587,993,995", "udp6_in": "20,21,53", "tcp_out": "20,21,22,25,53,80,110,113,443", "udp6_out": "20,21,53,113,123", "ipv6": False, "udp_in": "20,21,53", "eth6_device": "", "udp_out": "20,21,53,113,123" } config = CSFConfig reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/ctdb_tests.py000066400000000000000000000033131253627113700306450ustar00rootroot00000000000000from reconfigure.configs import CTDBConfig, CTDBNodesConfig, CTDBPublicAddressesConfig from reconfigure.tests.configs.base_test import BaseConfigTest class CTDBNodesConfigTest (BaseConfigTest): sources = { None: """10.10.1.1 10.10.1.2 """ } result = { 'nodes': [ { 'address': '10.10.1.1', }, { 'address': '10.10.1.2', }, ] } config = CTDBNodesConfig class CTDBPublicAddressesConfigTest (BaseConfigTest): sources = { None: """10.10.1.1 eth0 10.10.1.2 eth1 """ } result = { 'addresses': [ { 'address': '10.10.1.1', 'interface': 'eth0', }, { 'address': '10.10.1.2', 'interface': 'eth1', }, ] } config = CTDBPublicAddressesConfig class CTDBConfigTest (BaseConfigTest): sources = { None: """CTDB_RECOVERY_LOCK="/dadoscluster/ctdb/storage" CTDB_PUBLIC_INTERFACE=eth0 CTDB_PUBLIC_ADDRESSES=/etc/ctdb/public_addresses CTDB_MANAGES_SAMBA=yes CTDB_NODES=/etc/ctdb/nodes CTDB_LOGFILE=/var/log/log.ctdb CTDB_DEBUGLEVEL=2 CTDB_PUBLIC_NETWORK="10.0.0.0/24" CTDB_PUBLIC_GATEWAY="10.0.0.9" """ } result = { "recovery_lock_file": "\"/dadoscluster/ctdb/storage\"", "public_interface": "eth0", "public_addresses_file": "/etc/ctdb/public_addresses", "nodes_file": "/etc/ctdb/nodes", "debug_level": "2", "public_gateway": "\"10.0.0.9\"", "public_network": "\"10.0.0.0/24\"", "log_file": "/var/log/log.ctdb", "manages_samba": True } config = CTDBConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/dhcpd_tests.py000066400000000000000000000021101253627113700310050ustar00rootroot00000000000000from reconfigure.configs import DHCPDConfig from reconfigure.tests.configs.base_test import BaseConfigTest class DHCPDConfigTest (BaseConfigTest): sources = { None: """ default-lease-time 600; max-lease-time 7200; subnet 10.17.224.0 netmask 255.255.255.0 { option routers rtr-224.example.org; range 10.0.29.10 10.0.29.230; } shared-network 224-29 { subnet 10.17.224.0 netmask 255.255.255.0 { option routers rtr-224.example.org; } pool { deny members of "foo"; range 10.0.29.10 10.0.29.230; } } """ } result = { "subnets": [ { "ranges": [ { "range": "10.0.29.10 10.0.29.230" } ], "subnets": [], "name": "10.17.224.0 netmask 255.255.255.0", "options": [ { "value": "routers rtr-224.example.org" } ] } ], "options": [] } config = DHCPDConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/exports_tests.py000066400000000000000000000021271253627113700314370ustar00rootroot00000000000000from reconfigure.configs import ExportsConfig from reconfigure.tests.configs.base_test import BaseConfigTest class ExportsConfigTest (BaseConfigTest): sources = { None: """ "/another/exported/directory" 192.168.0.3(rw,sync) \ 192.168.0.4(ro) # test "/one" 192.168.0.1 # comment """ } result = { "exports": [ { "comment": "test", "name": '/another/exported/directory', "clients": [ { "name": "192.168.0.3", "options": "rw,sync" }, { "name": "192.168.0.4", "options": "ro" } ] }, { "comment": "comment", "name": '/one', "clients": [ { "name": "192.168.0.1", "options": "" } ] } ] } config = ExportsConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/fstab_tests.py000066400000000000000000000014301253627113700310260ustar00rootroot00000000000000from reconfigure.configs import FSTabConfig from reconfigure.tests.configs.base_test import BaseConfigTest class FSTabConfigTest (BaseConfigTest): sources = { None: """fs1\tmp1\text\trw\t1\t2 fs2\tmp2\tauto\tnone\t0\t0 """ } result = { 'filesystems': [ { 'device': 'fs1', 'mountpoint': 'mp1', 'type': 'ext', 'options': 'rw', 'freq': '1', 'passno': '2' }, { 'device': 'fs2', 'mountpoint': 'mp2', 'type': 'auto', 'options': 'none', 'freq': '0', 'passno': '0' }, ] } config = FSTabConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/group_tests.py000066400000000000000000000011531253627113700310650ustar00rootroot00000000000000from reconfigure.configs import GroupConfig from reconfigure.tests.configs.base_test import BaseConfigTest class GroupConfigTest (BaseConfigTest): sources = { None: """sys:x:3: adm:x:4:eugeny """ } result = { 'groups': [ { 'name': 'sys', 'password': 'x', 'gid': '3', 'users': '', }, { 'name': 'adm', 'password': 'x', 'gid': '4', 'users': 'eugeny', }, ] } config = GroupConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/hosts_tests.py000066400000000000000000000015371253627113700310770ustar00rootroot00000000000000from reconfigure.configs import HostsConfig from reconfigure.tests.configs.base_test import BaseConfigTest class FSTabConfigTest (BaseConfigTest): sources = { None: """a1 h1 a2 a3 a4 a5 h2 a6 h3 a7 """ } result = { 'hosts': [ { 'address': 'a1', 'name': 'h1', 'aliases': [ {'name': 'a2'}, {'name': 'a3'}, {'name': 'a4'}, ] }, { 'address': 'a5', 'aliases': [], 'name': 'h2', }, { 'address': 'a6', 'name': 'h3', 'aliases': [ {'name': 'a7'}, ] }, ] } config = HostsConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/iptables_tests.py000066400000000000000000000072071253627113700315420ustar00rootroot00000000000000from reconfigure.configs import IPTablesConfig from reconfigure.tests.configs.base_test import BaseConfigTest class IPTablesConfigTest (BaseConfigTest): sources = { None: '''*filter :INPUT ACCEPT [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT ! -s 202.54.1.2/32 -j DROP -A INPUT -m state --state NEW,ESTABLISHED -j ACCEPT # test COMMIT ''' } result = { 'tables': [ { 'chains': [ { 'default': 'ACCEPT', 'rules': [ { 'options': [ { 'arguments': [ { 'value': '202.54.1.2/32' } ], 'negative': True, 'name': 's' }, { 'arguments': [ { 'value': 'DROP' } ], 'negative': False, 'name': 'j' } ], 'comment': None, }, { 'options': [ { 'arguments': [ { 'value': 'state' } ], 'negative': False, 'name': 'm' }, { 'arguments': [ { 'value': 'NEW,ESTABLISHED' } ], 'negative': False, 'name': 'state' }, { 'arguments': [ { 'value': 'ACCEPT' } ], 'negative': False, 'name': 'j' } ], 'comment': 'test', } ], 'name': 'INPUT' }, { 'default': 'DROP', 'rules': [], 'name': 'FORWARD' }, { 'default': 'ACCEPT', 'rules': [], 'name': 'OUTPUT' } ], 'name': 'filter' } ] } config = IPTablesConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/netatalk_tests.py000066400000000000000000000017001253627113700315320ustar00rootroot00000000000000from reconfigure.configs import NetatalkConfig from reconfigure.tests.configs.base_test import BaseConfigTest class NetatalkConfigTest (BaseConfigTest): sources = { None: """ [Global] afp port=123 [test] path=/home ;comment valid users=root ea=sys file perm=0755 """ } result = { "global": { "zeroconf": True, "cnid_listen": "localhost:4700", "uam_list": 'uams_dhx.so,uams_dhx2.so', "afp_port": "123", }, "shares": [ { "comment": "comment", "appledouble": "ea", "name": "test", "ea": "sys", "valid_users": "root", "cnid_scheme": "dbd", "path": "/home", "password": '', "file_perm": '0755', "directory_perm": '', } ] } config = NetatalkConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/nsd_tests.py000066400000000000000000000007671253627113700305270ustar00rootroot00000000000000from reconfigure.configs import NSDConfig from reconfigure.tests.configs.base_test import BaseConfigTest class NSDConfigTest (BaseConfigTest): sources = { None: """ zone: name: "example.net" zonefile: "example.net.signed.zone" notify-retry: 5 """ } result = { "zones": [ { "name": "example.net", "file": "example.net.signed.zone" } ] } config = NSDConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/passwd_tests.py000066400000000000000000000016371253627113700312410ustar00rootroot00000000000000from reconfigure.configs import PasswdConfig from reconfigure.tests.configs.base_test import BaseConfigTest class PasswdConfigTest (BaseConfigTest): sources = { None: """backup:x:34:34:backup:/var/backups:/bin/sh list:x:38:38:Mailing List Manager:/var/list:/bin/sh """ } result = { 'users': [ { 'name': 'backup', 'password': 'x', 'uid': '34', 'gid': '34', 'comment': 'backup', 'home': '/var/backups', 'shell': '/bin/sh' }, { 'name': 'list', 'password': 'x', 'uid': '38', 'gid': '38', 'comment': 'Mailing List Manager', 'home': '/var/list', 'shell': '/bin/sh' }, ] } config = PasswdConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/resolv_tests.py000066400000000000000000000011411253627113700312400ustar00rootroot00000000000000from reconfigure.configs import ResolvConfig from reconfigure.tests.configs.base_test import BaseConfigTest class ResolvConfigTest (BaseConfigTest): sources = { None: """nameserver 1 domain 2 search 3 5 """ } result = { 'items': [ { 'name': 'nameserver', 'value': '1', }, { 'name': 'domain', 'value': '2', }, { 'name': 'search', 'value': '3 5', }, ] } config = ResolvConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/samba_tests.py000066400000000000000000000065511253627113700310230ustar00rootroot00000000000000from reconfigure.configs import SambaConfig from reconfigure.tests.configs.base_test import BaseConfigTest class SambaConfigTest (BaseConfigTest): sources = { None: """ [global] workgroup=WORKGROUP server string=%h server (Samba, Ubuntu) interfaces=127.0.0.0/8 eth0 bind interfaces only=yes log file=/var/log/samba/log.%m security=user [homes] comment=Home Directories browseable=no preopen:names=/*.frm/ preopen:num_bytes=123 preopen:helpers=2 preopen:queuelen=20 [profiles] comment=Users profiles path=/home/samba/profiles guest ok=no browseable=no create mask=0600 directory mask=0700 """ } result = { "global": { "server_string": "%h server (Samba, Ubuntu)", "workgroup": "WORKGROUP", "interfaces": "127.0.0.0/8 eth0", "bind_interfaces_only": True, "security": "user", "log_file": "/var/log/samba/log.%m" }, "shares": [ { "name": "homes", "comment": "Home Directories", "browseable": False, "create_mask": "0744", "directory_mask": "0755", 'follow_symlinks': True, "read_only": True, "guest_ok": False, "path": "", 'wide_links': False, "fstype": "", "force_create_mode": "000", "force_directory_mode": "000", "veto_files": "", "write_list": "", "dfree_command": "", "force_group": "", "force_user": "", "valid_users": "", "read_list": "", "dfree_cache_time": "", "oplocks": True, "locking": True, "preopen:queuelen": "20", "preopen:names": "/*.frm/", "preopen:num_bytes": "123", "preopen:helpers": "2", "vfs_objects": "", "recycle:keeptree": False, "recycle:repository": "", "recycle:exclude": '', }, { "name": "profiles", "comment": "Users profiles", "browseable": False, "create_mask": "0600", "directory_mask": "0700", 'follow_symlinks': True, "read_only": True, "guest_ok": False, "path": "/home/samba/profiles", 'wide_links': False, "fstype": "", "force_create_mode": "000", "force_directory_mode": "000", "veto_files": "", "write_list": "", "dfree_command": "", "force_group": "", "force_user": "", "valid_users": "", "read_list": "", "dfree_cache_time": "", "oplocks": True, "locking": True, "preopen:queuelen": "", "preopen:names": "", "preopen:num_bytes": "", "preopen:helpers": "", "vfs_objects": "", "recycle:keeptree": False, "recycle:repository": "", "recycle:exclude": '', } ] } config = SambaConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/squid_tests.py000066400000000000000000000026271253627113700310650ustar00rootroot00000000000000from reconfigure.configs import SquidConfig from reconfigure.tests.configs.base_test import BaseConfigTest class SquidConfigTest (BaseConfigTest): sources = { None: """acl manager proto cache_object acl SSL_ports port 443 http_access deny CONNECT !SSL_ports http_port 3128 """ } result = { "http_access": [ { "mode": "deny", "options": [ { "value": "CONNECT" }, { "value": "!SSL_ports" } ] } ], "http_port": [ { "options": [], "port": "3128" } ], "https_port": [], "acl": [ { "name": "manager", "options": [ { "value": "proto" }, { "value": "cache_object" } ] }, { "name": "SSL_ports", "options": [ { "value": "port" }, { "value": "443" } ] } ] } config = SquidConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/configs/supervisor_tests.py000066400000000000000000000016721253627113700321600ustar00rootroot00000000000000from reconfigure.configs import SupervisorConfig from reconfigure.tests.configs.base_test import BaseConfigTest class SupervisorConfigTest (BaseConfigTest): sources = { None: """[unix_http_server] file=/var/run//supervisor.sock ;comment chmod=0700 [include] files=test""", 'test': """[program:test1] command=cat stopasgroup=true """ } result = { "programs": [ { "comment": None, "autorestart": None, "name": "test1", "startsecs": None, "umask": None, "environment": None, "command": "cat", "user": None, "startretries": None, "directory": None, "autostart": None, "stopasgroup": True, "killasgroup": None, } ] } config = SupervisorConfig del BaseConfigTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/includers/000077500000000000000000000000001253627113700264755ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/includers/__init__.py000066400000000000000000000000001253627113700305740ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/includers/nginx_tests.py000066400000000000000000000014741253627113700314220ustar00rootroot00000000000000#coding: utf8 import unittest from reconfigure.parsers import NginxParser from reconfigure.includers import NginxIncluder class IncludersTest (unittest.TestCase): def test_compose_decompose(self): content = """ sec1 { p1 1; include test; } """ content2 = """ sec2 { p2 2; } """ parser = NginxParser() includer = NginxIncluder(parser=parser, content_map={'test': content2}) tree = parser.parse(content) tree = includer.compose(None, tree) self.assertTrue(len(tree.children[0].children) == 3) treemap = includer.decompose(tree) self.assertTrue(len(treemap.keys()) == 2) self.assertTrue(treemap['test'].children[0].name == 'sec2') reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/000077500000000000000000000000001253627113700261645ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/__init__.py000066400000000000000000000000001253627113700302630ustar00rootroot00000000000000reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/base_test.py000066400000000000000000000015251253627113700305120ustar00rootroot00000000000000import unittest class BaseParserTest (unittest.TestCase): source = "" parsed = None parser = None @property def stringified(self): return self.source def test_parse(self): if not self.__class__.parser: return nodetree = self.parser.parse(self.__class__.source) if self.__class__.parsed != nodetree: print('TARGET: %s\n\nPARSED: %s' % (self.__class__.parsed, nodetree)) self.assertEquals(self.__class__.parsed, nodetree) def test_stringify(self): if not self.__class__.parser: return unparsed = self.parser.stringify(self.__class__.parsed) a, b = self.stringified, unparsed if a.split() != b.split(): print('SOURCE: %s\n\nGENERATED: %s' % (a, b)) self.assertEquals(a.split(), b.split()) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/bind9_tests.py000066400000000000000000000121241253627113700307650ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import BIND9Parser from reconfigure.nodes import * class BIND9ParserTest (BaseParserTest): parser = BIND9Parser() source = """p1 asd; key { s1p1 asd; /*s1p2 wqe;*/ zone test { ::1; s2p1 qwe; }; }; """ @property def stringified(self): return """ p1 asd; key { s1p1 asd; # s1p2 wqe; zone test { ::1; s2p1 qwe; }; }; """ parsed = RootNode( None, PropertyNode('p1', 'asd'), Node( 'key', PropertyNode('s1p1', 'asd'), Node( 'zone', PropertyNode('', '::1'), PropertyNode('s2p1', 'qwe'), parameter='test', comment='s1p2 wqe;', ), parameter=None, ) ) del BaseParserTest import unittest class BIND9ParserHangTest (unittest.TestCase): source = """ controls { inet * port 953 allow { 5.231.55.113; 127.0.0.1; } keys { "rndc-key"; }; }; options { listen-on port 53 { 127.0.0.1; }; listen-on-v6 port 53 { ::1; }; directory "/var/named"; dump-file "/var/named/data/cache_dump.db"; statistics-file "/var/named/data/named_stats.txt"; memstatistics-file "/var/named/data/named_mem_stats.txt"; allow-query { localhost; }; /* - If you are building an AUTHORITATIVE DNS server, do NOT enable recursion. - If you are building a RECURSIVE (caching) DNS server, you need to enable recursion. - If your recursive DNS server has a public IP address, you MUST enable access control to limit queries to your legitimate users. Failing to do so will cause your server to become part of large scale DNS amplification attacks. Implementing BCP38 within your network would greatly reduce such attack surface */ recursion yes; dnssec-enable yes; dnssec-validation yes; dnssec-lookaside auto; /* Path to ISC DLV key */ bindkeys-file "/etc/named.iscdlv.key"; managed-keys-directory "/var/named/dynamic"; pid-file "/run/named/named.pid"; session-keyfile "/run/named/session.key"; }; logging { channel default_debug { file "data/named.run"; severity dynamic; }; }; zone "." IN { type hint; file "named.ca"; }; zone "localhost.localdomain" IN { type master; file "named.localhost"; allow-update { none; }; }; zone "localhost" IN { type master; file "named.localhost"; allow-update { none; }; }; zone "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" IN { type master; file "named.loopback"; allow-update { none; }; }; zone "1.0.0.127.in-addr.arpa" IN { type master; file "named.loopback"; allow-update { none; }; }; zone "0.in-addr.arpa" IN { type master; file "named.empty"; allow-update { none; }; }; managed-keys { # DNSKEY for the root zone. # Updates are published on root-dnssec-announce@icann.org . initial-key 257 3 8 "AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0="; }; managed-keys { # ISC DLV: See https://www.isc.org/solutions/dlv for details. # NOTE: This key is activated by setting "dnssec-lookaside auto;" # in named.conf. dlv.isc.org. initial-key 257 3 5 "BEAAAAPHMu/5onzrEE7z1egmhg/WPO0+juoZrW3euWEn4MxDCE1+lLy2 brhQv5rN32RKtMzX6Mj70jdzeND4XknW58dnJNPCxn8+jAGl2FZLK8t+ 1uq4W+nnA3qO2+DL+k6BD4mewMLbIYFwe0PG73Te9fZ2kJb56dhgMde5 ymX4BI/oQ+cAK50/xvJv00Frf8kw6ucMTwFlgPe+jnGxPPEmHAte/URk Y62ZfkLoBAADLHQ9IrS2tryAe7mbBZVcOwIeU/Rw/mRx/vwwMCTgNboM QKtUdvNXDrYJDSHZws3xiRXF1Rf+al9UmZfSav/4NWLKjHzpT59k/VSt TDN0YUuWrBNh"; # ROOT KEY: See https://data.iana.org/root-anchors/root-anchors.xml # for current trust anchor information. # NOTE: This key is activated by setting "dnssec-validation auto;" # in named.conf. . initial-key 257 3 8 "AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0="; }; """ def test_hang(self): BIND9Parser().parse(self.source) reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/crontab_tests.py000066400000000000000000000037701253627113700314170ustar00rootroot00000000000000from reconfigure.parsers import CrontabParser from reconfigure.nodes import RootNode, Node, PropertyNode from reconfigure.tests.parsers.base_test import BaseParserTest class CrontabParserTest (BaseParserTest): parser = CrontabParser() source = '\n'.join(['#comment line', '* * * * * date', '@reboot ls -al', '1 * 0 1 2 date -s', 'NAME = TEST', ]) parsed = RootNode(None, children=[ Node('normal_task', comment='comment line', children=[ PropertyNode('minute', '*'), PropertyNode('hour', '*'), PropertyNode('day_of_month', '*'), PropertyNode('month', '*'), PropertyNode('day_of_week', '*'), PropertyNode('command', 'date'), ] ), Node('special_task', children=[ PropertyNode('special', '@reboot'), PropertyNode('command', 'ls -al'), ] ), Node('normal_task', children=[ PropertyNode('minute', '1'), PropertyNode('hour', '*'), PropertyNode('day_of_month', '0'), PropertyNode('month', '1'), PropertyNode('day_of_week', '2'), PropertyNode('command', 'date -s'), ] ), Node('env_setting', children=[ PropertyNode('name', 'NAME'), PropertyNode('value', 'TEST'), ] ), ] ) # bad_source = '\n'.join(['* * * * dd', #Wrong line # ' = FAIL', #wrong line # ]) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/exports_tests.py000066400000000000000000000022621253627113700314660ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import ExportsParser from reconfigure.nodes import * class ExportsParserTest (BaseParserTest): parser = ExportsParser() source = """ "/another/exported/directory" 192.168.0.3(rw,sync) \ 192.168.0.4(ro) # comment /one 192.168.0.1 """ parsed = RootNode( None, Node( '/another/exported/directory', Node( 'clients', Node( '192.168.0.3', PropertyNode('options', 'rw,sync') ), Node( '192.168.0.4', PropertyNode('options', 'ro') ), ), ), Node( '/one', Node( 'clients', Node( '192.168.0.1', PropertyNode('options', '') ), ), comment='comment' ) ) @property def stringified(self): return """"/another/exported/directory"\t192.168.0.3(rw,sync)\t192.168.0.4(ro) "/one"\t192.168.0.1\t# comment """ del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/ini_tests.py000066400000000000000000000011371253627113700305410ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import IniFileParser from reconfigure.nodes import * class IniParserTest (BaseParserTest): parser = IniFileParser(sectionless=True) source = """a=b [section1] ;section comment s1p1=asd ;comment 2 s1p2=123 """ parsed = RootNode(None, Node(None, PropertyNode('a', 'b'), ), Node('section1', PropertyNode('s1p1', 'asd', comment='comment 2'), PropertyNode('s1p2', '123'), comment='section comment' ), ) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/iptables_tests.py000066400000000000000000000041151253627113700315640ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import IPTablesParser from reconfigure.nodes import * class IPTablesParserTest (BaseParserTest): parser = IPTablesParser() source = """*filter :INPUT ACCEPT [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT ! -s 202.54.1.2/32 -j DROP # test -A INPUT -m state --state NEW,ESTABLISHED -j ACCEPT COMMIT """ parsed = RootNode(None, Node('filter', Node('INPUT', PropertyNode('default', 'ACCEPT'), Node('append', Node('option', Node('argument', PropertyNode('value', '202.54.1.2/32')), PropertyNode('negative', True), PropertyNode('name', 's') ), Node('option', Node('argument', PropertyNode('value', 'DROP')), PropertyNode('negative', False), PropertyNode('name', 'j') ), comment='test' ), Node('append', Node('option', Node('argument', PropertyNode('value', 'state')), PropertyNode('negative', False), PropertyNode('name', 'm') ), Node('option', Node('argument', PropertyNode('value', 'NEW,ESTABLISHED')), PropertyNode('negative', False), PropertyNode('name', 'state') ), Node('option', Node('argument', PropertyNode('value', 'ACCEPT')), PropertyNode('negative', False), PropertyNode('name', 'j') ), ), ), Node('FORWARD', PropertyNode('default', 'DROP'), ), Node('OUTPUT', PropertyNode('default', 'ACCEPT'), ), ) ) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/jsonparser_tests.py000066400000000000000000000013411253627113700321450ustar00rootroot00000000000000import json from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import JsonParser from reconfigure.nodes import * class JsonParserTest (BaseParserTest): parser = JsonParser() source = """{ "p2": 123, "s1": { "s1p1": "qwerty" } } """ parsed = RootNode(None, PropertyNode('p2', 123), Node('s1', PropertyNode('s1p1', 'qwerty'), ), ) def test_stringify(self): unparsed = self.parser.stringify(self.__class__.parsed) a, b = self.stringified, unparsed if json.loads(a) != json.loads(b): print('SOURCE: %s\n\nGENERATED: %s' % (a, b)) self.assertEquals(a, b) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/nginx_tests.py000066400000000000000000000013351253627113700311050ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import NginxParser from reconfigure.nodes import * class NginxParserTest (BaseParserTest): parser = NginxParser() source = """p1 asd; sec { s1p1 asd; s1p2 wqe; # test sec2 test { s2p1 qwe; } } """ parsed = RootNode( None, PropertyNode('p1', 'asd'), Node( 'sec', PropertyNode('s1p1', 'asd'), PropertyNode('s1p2', 'wqe'), Node( 'sec2', PropertyNode('s2p1', 'qwe'), parameter='test', comment='test', ), parameter=None, ) ) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/nsd_tests.py000066400000000000000000000010601253627113700305410ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import NSDParser from reconfigure.nodes import * class BIND9ParserTest (BaseParserTest): parser = NSDParser() source = """# asd server: ip4-only: no key: name: "mskey" """ parsed = RootNode( None, Node( 'server', PropertyNode('ip4-only', 'no'), comment='asd' ), Node( 'key', PropertyNode('name', '"mskey"'), ) ) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/shell_tests.py000066400000000000000000000010171253627113700310660ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import ShellParser from reconfigure.nodes import * class ShellParserTest (BaseParserTest): parser = ShellParser() source = """ # The following # otherwise they PORTS_pop3d="110,995" PORTS_htpasswd="80,443" # b """ parsed = RootNode( None, PropertyNode('PORTS_pop3d', '110,995', comment='The following\notherwise they'), PropertyNode('PORTS_htpasswd', '80,443', comment='b'), ) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/squid_tests.py000066400000000000000000000012261253627113700311060ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import SquidParser from reconfigure.nodes import * class SquidParserTest (BaseParserTest): parser = SquidParser() source = """# line1 # long comment a\tbc efgh # line2 """ parsed = RootNode(None, Node('line', PropertyNode('name', 'a'), Node('arguments', PropertyNode('1', 'bc'), ), comment='line1\nlong comment', ), Node('line', PropertyNode('name', 'efgh'), Node('arguments'), comment='line2', ), ) del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/reconfigure/tests/parsers/ssv_tests.py000066400000000000000000000015301253627113700305720ustar00rootroot00000000000000from reconfigure.tests.parsers.base_test import BaseParserTest from reconfigure.parsers import SSVParser from reconfigure.nodes import * class SSVParserTest (BaseParserTest): parser = SSVParser(continuation='\\') source = """# line1 # long comment a\tbc\\ \tdef efgh # line2 """ parsed = RootNode( None, Node( 'line', Node('token', PropertyNode('value', 'a')), Node('token', PropertyNode('value', 'bc')), Node('token', PropertyNode('value', 'def')), comment='line1\nlong comment', ), Node( 'line', Node('token', PropertyNode('value', 'efgh')), comment='line2', ), ) @property def stringified(self): return """# line1 # long comment a\tbc\tdef efgh # line2 """ del BaseParserTest reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/requirements.txt000066400000000000000000000000211253627113700243100ustar00rootroot00000000000000chardet nose six reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/setup.cfg000066400000000000000000000000341253627113700226510ustar00rootroot00000000000000[bdist_wheel] universal = 1 reconfigure-49a208904b95b042e3bc90c377aecc1a39cc9d58/setup.py000066400000000000000000000010661253627113700225500ustar00rootroot00000000000000#!/usr/bin/env python from distutils.core import setup from setuptools import find_packages from reconfigure import __version__ setup( name='reconfigure', version=__version__, install_requires=[ 'chardet', 'six', ], description='An ORM for config files', license='LGPLv3', author='Eugeny Pankov', author_email='e@ajenti.org', url='http://ajenti.org/', classifiers=[ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", ], packages=find_packages(exclude=['*test*']), )