├── .github └── workflows │ └── check.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc ├── Makefile └── source │ ├── api.rst │ ├── conf.py │ ├── contributors.rst │ ├── getting_started.rst │ └── index.rst ├── pyproject.toml ├── setup.py ├── src ├── pdns │ ├── __init__.py │ └── remotebackend │ │ ├── __init__.py │ │ └── unix.py └── pipe_abi.py └── tests └── test.py /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: 3.9 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install flake8 nose 22 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 23 | - name: Lint with flake8 24 | run: | 25 | flake8 src tests 26 | - name: Test with pytest 27 | run: | 28 | nosetests -v tests 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Aki Tuomi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pdns-remotebackend-python 2 | ========================= 3 | 4 | This package is intended to help with remotebackend scripts. 5 | 6 | Installation 7 | ------------ 8 | To install, run 9 | 10 | ``` 11 | python setup.py build 12 | python setup.py install 13 | ``` 14 | 15 | Usage 16 | ----- 17 | 18 | To use, import pdns.remotebackend, and subclass Handler. This code 19 | currently supports Pipe and Unix Connector, which you can use. This version 20 | also supports pipe backend, you can signify this by setting option 'abi' to pipe. 21 | 22 | Example 23 | ------- 24 | 25 | ``` 26 | import pdns.remotebackend, pdns.remotebackend.unix 27 | import sys 28 | import io 29 | 30 | class MyHandler(pdns.remotebackend.Handler): 31 | def do_lookup(self, qname='', qtype='', **kwargs): 32 | self.result =  [] 33 | self.log.append("Handling a new DNS request") 34 | if (qname == 'test.com' and qtype == 'ANY'): 35 | self.result.append(self.record('test.com','A','127.0.0.1',ttl=300)) 36 | if (qname == 'test.com' and (qtype == 'ANY' or qtype == 'SOA')): 37 | self.result.append(self.record('test.com','A','127.0.0.1',ttl=300)) 38 | self.result.append(self.record('test.com','SOA','sns.dns.icann.org. noc.dns.icann.org. 2013073082 7200 3600 1209600 3600',ttl=300)) 39 | 40 | if __name__ == '__main__': 41 | pdns.remotebackend.PipeConnector(MyHandler).run() 42 | # or you can use 43 | # pdns.remotebackend.unix.UnixConnector(MyHandler).run() 44 | ``` 45 | 46 | Details 47 | --- 48 | The Handler class will call your class with do\_\. initialize becomes do\_initialize. The function is passed a hash with all the keys provided. 49 | Please see http://doc.powerdns.com/html/remotebackend.html for details on the API. 50 | 51 | All log messages will appear in the PowerDNS log. For the messages to appear, the PowerDNS log level needs to be set to 6 or above. 52 | 53 | Connector constructor is 54 | ``` 55 | Connector(HandlerClass, options = {}) 56 | ``` 57 | Supported options for all connectors is ttl, which defines default ttl to use if missing. For Unix Connector you can also specify path, which is path to the 58 | unix socket file to use. 59 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PowerDNSRemoteBackendhelper.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PowerDNSRemoteBackendhelper.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PowerDNSRemoteBackendhelper" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PowerDNSRemoteBackendhelper" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | **** 2 | API 3 | **** 4 | 5 | Remotebackend main module 6 | ========================= 7 | .. automodule:: pdns.remotebackend 8 | :members: 9 | 10 | Unix connector module 11 | ===================== 12 | .. automodule:: pdns.remotebackend.unix 13 | :members: 14 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PowerDNS RemoteBackend helper documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 5 11:58:49 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../../src')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'PowerDNS RemoteBackend helper' 44 | copyright = u'2013, Aki Tuomi' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.4' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.4-beta' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'PowerDNSRemoteBackendhelperdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'PowerDNSRemoteBackendhelper.tex', u'PowerDNS RemoteBackend helper Documentation', 187 | u'Aki Tuomi', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'powerdnsremotebackendhelper', u'PowerDNS RemoteBackend helper Documentation', 217 | [u'Aki Tuomi'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'PowerDNSRemoteBackendhelper', u'PowerDNS RemoteBackend helper Documentation', 231 | u'Aki Tuomi', 'PowerDNSRemoteBackendhelper', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /doc/source/contributors.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Contributors 3 | *************** 4 | 5 | The following people have contributed their valuable time and effort to improve this python package. 6 | 7 | - Preston Mason 8 | - https://github.com/cmouse/pdns-remotebackend-python/commit/45bb456ff129b366604d631cd1e87b4e36c5624c 9 | 10 | -------------------------------------------------------------------------------- /doc/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Getting started 3 | *************** 4 | 5 | Quickstart script 6 | ================= 7 | 8 | .. code-block:: python 9 | 10 | import pdns.remotebackend 11 | import sys 12 | import io 13 | 14 | class MyHandler(pdns.remotebackend.Handler): 15 | def do_lookup(self,args): 16 | if (args['qname'] == 'test.com' and args['qtype'] == 'ANY'): 17 | self.result = [] 18 | self.result.append(self.record('test.com','A','127.0.0.1',ttl=300)) 19 | if (args['qname'] == 'test.com' and (args['qtype'] == 'ANY' or args['qtype'] == 'SOA')): 20 | self.result = [] 21 | self.result.append(self.record('test.com','A','127.0.0.1',ttl=300)) 22 | self.result.append(self.record('test.com','SOA','sns.dns.icann.org noc.dns.icann.org 2013073082 7200 3600 1209600 3600',ttl=300)) 23 | 24 | pdns.remotebackend.PipeConnector(MyHandler).run() 25 | 26 | 27 | PowerDNS configuration 28 | ====================== 29 | 30 | To use the script with PowerDNS, add following lines into configuration: 31 | 32 | .. line-block:: 33 | launch=remote 34 | remote-connection-string=pipe:command=/path/to/script.py 35 | 36 | Implementing methods 37 | ==================== 38 | 39 | You can find a list of methods that are supported by remotebackend from http://doc.powerdns.com/html/remotebackend.html#remotebackend-api. To implement these, you must write a do\_ method for them. 40 | 41 | .. function::do_lookup(self, args) 42 | 43 | To return a result, you set self.result into array of returns values, True, or whatever else is expected. This will gets rendered into json. Default result is always False. You *must* set result on success. 44 | 45 | 46 | To push log messages into PowerDNS, you can add them into self.log. 47 | 48 | :samp:`self.log << "Hello, world"` 49 | 50 | Pipe backend support 51 | ==================== 52 | From version 0.2, pipe backend is also supported. The only difference in python side is that you do 53 | 54 | .. line-block:: 55 | pdns.remotebackend.PipeConnector(MyHandler, {"abi":"pipe"}).run() 56 | 57 | It is also supported for unix connector. Configure PowerDNS side as usual. It supports do_lookup and do_list methods. 58 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. PowerDNS RemoteBackend helper documentation master file, created by 2 | sphinx-quickstart on Thu Sep 5 11:58:49 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PowerDNS RemoteBackend helper's documentation! 7 | ========================================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | getting_started 15 | api 16 | contributors 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='Pdns_Remotebackend', 7 | version='0.8.1', 8 | description='Support package for PowerDNS remotebackend', 9 | long_description='This package is intended to make writing remotebackends with python easier. It provides base class for request handling and connector classes for pipe and unix connectors.', 10 | author='Aki Tuomi', 11 | author_email='cmouse@cmouse.fi', 12 | url='https://github.com/PowerDNS/pdns-remotebackend-python', 13 | download_url='https://pypi.org/project/Pdns_Remotebackend', 14 | license='MIT', 15 | platforms=['all'], 16 | packages=['pdns','pdns.remotebackend'], 17 | package_dir={'pdns.remotebackend': 'src/pdns/remotebackend', 18 | 'pdns':'src/pdns'}, 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'Intended Audience :: Developers', 22 | 'Intended Audience :: System Administrators', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python', 26 | 'Topic :: Internet :: Name Service (DNS)', 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /src/pdns/__init__.py: -------------------------------------------------------------------------------- 1 | """PowerDNS stub module""" 2 | -------------------------------------------------------------------------------- /src/pdns/remotebackend/__init__.py: -------------------------------------------------------------------------------- 1 | """PowerDNS remotebackend support module""" 2 | import json 3 | import re 4 | import sys 5 | import traceback 6 | 7 | VERSION = "0.8.1" 8 | """Module version string""" 9 | 10 | 11 | class Handler: 12 | """Base class for request handler, you have to implement at least 13 | do_lookup. 14 | Please see 15 | http://doc.powerdns.com/html/remotebackend.html#remotebackend-api 16 | for information on how to implement anything. All methods get called 17 | with hash containing the request variables.""" 18 | 19 | description = "PowerDNS python remotebackend" 20 | version = VERSION 21 | 22 | def __init__(self, options={}): 23 | """Initialize with default values""" 24 | self.log = [] 25 | """Array of log messages""" 26 | self.result = False 27 | """Result of the invocation""" 28 | self.ttl = 300 29 | """Default TTL""" 30 | self.params = {} 31 | """Any handler parameters you want to store""" 32 | self.options = options 33 | """Connector options""" 34 | 35 | if 'ttl' in self.options: 36 | self.ttl = self.options['ttl'] 37 | 38 | def record(self, qname, qtype, content, ttl=-1, auth=1): 39 | """Generate one record""" 40 | if ttl == -1: 41 | ttl = self.ttl 42 | return {'qtype': qtype, 'qname': qname, 'content': content, 43 | 'ttl': ttl, 'auth': auth} 44 | 45 | def do_initialize(self, **args): 46 | """Default handler for initialization method, stores any parameters 47 | into attribute params""" 48 | self.params = args 49 | self.log.append( 50 | "{0} version {1} initialized".format( 51 | self.description, self.version 52 | ) 53 | ) 54 | self.result = True 55 | 56 | def __enter__(self): 57 | return self 58 | 59 | def __exit__(self, exc_type, exc_value, traceback): 60 | pass 61 | 62 | 63 | class TraceWriter(object): 64 | def __init__(self, writer, tracer): 65 | self.writer = writer 66 | self.tracer = tracer 67 | 68 | def write(self, line): 69 | self.tracer.write(line) 70 | self.writer.write(line) 71 | 72 | def flush(self): 73 | self.tracer.flush() 74 | self.writer.flush() 75 | 76 | 77 | class Connector: 78 | """Connector base class for handling endless loop""" 79 | def __init__(self, klass, options={}): 80 | # initialize the handler class 81 | self.handler = klass 82 | """"Handler class""" 83 | self.options = options 84 | """Any options""" 85 | if "abi" not in self.options: 86 | self.options["abi"] = 'remote' 87 | if 'rawlog' in self.options: 88 | self.tracer = open(self.options['rawlog'], 'w') 89 | else: 90 | self.tracer = None 91 | 92 | def mainloop(self, reader, writer): 93 | """Setup basic reader/writer and start correct main loop""" 94 | 95 | if self.tracer: 96 | writer = TraceWriter(writer, self.tracer) 97 | 98 | with self.handler(options=self.options) as h: 99 | if self.options["abi"] == 'pipe': 100 | return self.mainloop3(reader, writer, h) 101 | else: 102 | return self.mainloop4(reader, writer, h) 103 | 104 | def mainloop3(self, reader, writer, h): 105 | """Reader/writer and request de/serialization for pipe backend""" 106 | # initialize 107 | line = reader.readline() 108 | if getattr(self, 'tracer', None): 109 | self.tracer.write(line) 110 | 111 | m = re.match("^HELO\t([1-4])", line) 112 | if m is not None: 113 | # simulate empty initialize 114 | h.do_initialize() 115 | writer.write( 116 | "OK\t{0} version {1} initialized\n".format( 117 | h.description, h.version 118 | ) 119 | ) 120 | writer.flush() 121 | self.abi = int(m.group(1)) 122 | else: 123 | writer.write("FAIL\n") 124 | writer.flush() 125 | while True: 126 | line = reader.readline() 127 | if line == "": 128 | return 129 | 130 | # keep track of last seen SOA for AXFR 131 | last_soa_name = None 132 | 133 | while True: 134 | line = reader.readline() 135 | if self.tracer: 136 | self.tracer.write(line) 137 | line = line.strip().split("\t") 138 | if not line: 139 | break 140 | if (len(line) < 2): 141 | break 142 | h.log = [] 143 | h.result = False 144 | method = line[0] 145 | parms = {} 146 | 147 | if method == "AXFR": 148 | parms = {"zonename": last_soa_name, "domain_id": int(line[1])} 149 | method = "do_list" 150 | elif method == "Q": 151 | parms = { 152 | "qname": line[1], 153 | "qclass": line[2], 154 | "qtype": line[3], 155 | "domain_id": int(line[4]), 156 | "zone_id": int(line[4]), 157 | "remote": line[5] 158 | } 159 | if self.abi > 1: 160 | parms["local"] = line[6] 161 | if self.abi > 2: 162 | parms["edns-subnet"] = line[7] 163 | if (line[3] == "SOA"): 164 | last_soa_name = line[1] 165 | method = "do_lookup" 166 | else: 167 | writer.write("FAIL\n") 168 | writer.flush() 169 | continue 170 | 171 | if (callable(getattr(h, method, None))): 172 | getattr(h, method)(**parms) 173 | 174 | if (len(h.log) > 0): 175 | writer.write("LOG\t{0}\n".format(h.log[0])) 176 | 177 | if not isinstance(h.result, bool): 178 | for r in h.result: 179 | if "scopeMask" not in r: 180 | r["scopeMask"] = 0 181 | if "domain_id" not in r: 182 | r["domain_id"] = int(parms["domain_id"]) 183 | if (self.abi < 3): 184 | writer.write( 185 | "DATA\t{0}\t{1}\t{2}\t{3}\t{4}\t{5}\n".format( 186 | r["qname"], "IN", r["qtype"], r["ttl"], 187 | r["domain_id"], r["content"] 188 | ) 189 | ) 190 | else: 191 | writer.write( 192 | "DATA\t{0}\t{1}\t{2}\t{3}\t{4}\t" 193 | "{5}\t{6}\t{7}\n".format( 194 | r["scopeMask"], r["auth"], r["qname"], "IN", 195 | r["qtype"], r["ttl"], r["domain_id"], 196 | r["content"] 197 | ) 198 | ) 199 | writer.write("END\n") 200 | else: 201 | writer.write("FAIL\n") 202 | 203 | writer.flush() 204 | 205 | def mainloop4(self, reader, writer, h): 206 | """Reader/writer and request de/serialization for remotebackend""" 207 | 208 | while True: 209 | line = reader.readline() 210 | if self.tracer: 211 | self.tracer.write(line) 212 | if line == "": 213 | break 214 | try: 215 | data_in = json.loads(line) 216 | method = "do_{0}".format(data_in['method'].lower()) 217 | args = {} 218 | if ('parameters' in data_in): 219 | args = data_in['parameters'] 220 | h.result = False 221 | h.log = [] 222 | if (callable(getattr(h, method, None))): 223 | getattr(h, method)(**args) 224 | writer.write(json.dumps({'result': h.result, 'log': h.log})) 225 | except ValueError: 226 | writer.write(json.dumps({'result': False, 227 | 'log': ["Cannot parse input"]})) 228 | # errors are never visible if we don't catch this exception. 229 | except BaseException: 230 | writer.write(json.dumps({'result': False, 231 | 'log': [traceback.format_exc()]})) 232 | 233 | writer.write("\n") 234 | writer.flush() 235 | 236 | 237 | class PipeConnector(Connector): 238 | """Pipe Connector""" 239 | def run(self): 240 | """Startup pipe connector with stdin/stdout as source/sink""" 241 | try: 242 | self.mainloop(sys.stdin, sys.stdout) 243 | except (KeyboardInterrupt, SystemExit): 244 | pass 245 | -------------------------------------------------------------------------------- /src/pdns/remotebackend/unix.py: -------------------------------------------------------------------------------- 1 | """Unix connector module""" 2 | 3 | import os 4 | import os.path 5 | import sys 6 | 7 | from pdns.remotebackend import Connector 8 | 9 | if sys.version_info < (3, 0): 10 | import SocketServer 11 | else: 12 | import io 13 | import socketserver 14 | SocketServer = socketserver # backward compability for python2 15 | 16 | 17 | class UnixRequestHandler(SocketServer.StreamRequestHandler, Connector): 18 | def __init__(self, *args): 19 | SocketServer.StreamRequestHandler.__init__(self, *args) 20 | Connector.__init__(self) 21 | 22 | """Class implementing unix read/write server""" 23 | def handle(self): 24 | """Handle connection""" 25 | options = self.server.rpc_options 26 | with self.server.rpc_handler(options=options) as h: 27 | if sys.version_info < (3, 0): 28 | rfile = self.rfile 29 | wfile = self.wfile 30 | else: 31 | rfile = io.TextIOWrapper( 32 | self.rfile, encoding="utf-8", newline="\n" 33 | ) 34 | wfile = io.TextIOWrapper( 35 | self.wfile, encoding="utf-8", newline="\n" 36 | ) 37 | 38 | if (options["abi"] == 'pipe'): 39 | return self.mainloop3(rfile, wfile, h) 40 | else: 41 | return self.mainloop4(rfile, wfile, h) 42 | 43 | 44 | class ThreadedUnixStreamServer(SocketServer.ThreadingMixIn, 45 | SocketServer.UnixStreamServer): 46 | pass 47 | 48 | 49 | class UnixConnector(Connector): 50 | """Connector class, which spawns a server and handler. 51 | Provide option path for constructor.""" 52 | def run(self): 53 | """Start listening in options['path'] and spawn handler per connection. 54 | Your remotebackend handler class is rebuilt between connections.""" 55 | if 'path' in self.options: 56 | path = self.options['path'] 57 | else: 58 | path = '/tmp/remotebackend.sock' 59 | if os.path.exists(path): 60 | os.remove(path) 61 | 62 | s = ThreadedUnixStreamServer(path, UnixRequestHandler, False) 63 | s.rpc_handler = self.handler 64 | s.rpc_options = self.options 65 | s.server_bind() 66 | s.server_activate() 67 | try: 68 | s.serve_forever() 69 | except (KeyboardInterrupt, SystemExit): 70 | pass 71 | os.remove(path) 72 | -------------------------------------------------------------------------------- /src/pipe_abi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pdns.remotebackend 4 | import pdns.remotebackend.unix 5 | 6 | 7 | class MyHandler(pdns.remotebackend.Handler): 8 | def do_lookup(self, qname='', qtype='', **kwargs): 9 | if (qname == 'test.com' and qtype in ('ANY', 'A')): 10 | self.result = [] 11 | self.result.append(self.record('test.com', 'A', '127.0.0.1', 12 | ttl=300)) 13 | if (qname == 'test.com' and (qtype in ('ANY', 'SOA'))): 14 | self.result = [] 15 | self.result.append( 16 | self.record('test.com', 'SOA', 17 | 'sns.dns.icann.org. noc.dns.icann.org. ' 18 | '2013073082 7200 3600 1209600 3600', 19 | ttl=300)) 20 | 21 | 22 | if __name__ == '__main__': 23 | options = {"abi": sys.argv[1]} 24 | if len(sys.argv) > 2: 25 | options['path'] = sys.argv[2] 26 | pdns.remotebackend.unix.UnixConnector(MyHandler, options).run() 27 | else: 28 | pdns.remotebackend.PipeConnector(MyHandler, options).run() 29 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import unittest 4 | import re 5 | import sys 6 | import socket 7 | import time 8 | 9 | from subprocess import PIPE, Popen 10 | 11 | 12 | class pipetest(unittest.TestCase): 13 | unix_socket = "/tmp/remotebackend.sock" 14 | 15 | def test_pipe_abi_pipe(self): 16 | sub = Popen(["/usr/bin/env", "python", "src/pipe_abi.py", "pipe"], 17 | stdin=PIPE, stdout=PIPE, stderr=sys.stderr, 18 | close_fds=True, shell=False) 19 | (writer, reader) = (sub.stdin, sub.stdout) 20 | writer.write("HELO\t1\n".encode("utf-8")) 21 | writer.flush() 22 | sub.poll() 23 | line = reader.readline().decode("utf-8") 24 | assert re.match("^OK\t", line) 25 | writer.write("Q\ttest.com\tIN\tSOA\t-1\t127.0.0.1\n".encode("utf-8")) 26 | writer.flush() 27 | line = reader.readline().decode("utf-8") 28 | print(line) 29 | assert re.match("^DATA\ttest.com\tIN\tSOA\t300\t-1\t" 30 | "sns.dns.icann.org. noc.dns.icann.org. " 31 | "2013073082 7200 3600 1209600 3600", 32 | line) 33 | writer.flush() 34 | line = reader.readline().decode("utf-8") 35 | assert re.match("^END", line) 36 | writer.write( 37 | "Q\tinvalid.test\tIN\tSOA\t-1\t127.0.0.1\n".encode("utf-8") 38 | ) 39 | writer.flush() 40 | line = reader.readline().decode("utf-8") 41 | assert re.match("^FAIL", line) 42 | sub.stdout.close() 43 | sub.stdin.close() 44 | sub.kill() 45 | sub.wait() 46 | 47 | def test_pipe_abi_remote(self): 48 | sub = Popen(["/usr/bin/env", "python", "src/pipe_abi.py", "remote"], 49 | stdin=PIPE, stdout=PIPE, stderr=sys.stderr, 50 | close_fds=True, shell=False) 51 | (writer, reader) = (sub.stdin, sub.stdout) 52 | writer.write(json.dumps({ 53 | "method": "initialize", 54 | "parameters": { 55 | "timeout": 2000 56 | } 57 | }).encode("utf-8")) 58 | writer.write("\n".encode("utf-8")) 59 | writer.flush() 60 | sub.poll() 61 | line = reader.readline().decode("utf-8") 62 | resp = json.loads(line) 63 | assert resp["result"] 64 | writer.write(json.dumps({ 65 | "method": "lookup", 66 | "parameters": { 67 | "qname": "test.com", 68 | "qtype": "SOA" 69 | } 70 | }).encode("utf-8")) 71 | writer.write("\n".encode("utf-8")) 72 | writer.flush() 73 | resp = json.loads(reader.readline().decode("utf-8")) 74 | assert resp["result"][0]["qname"] == "test.com" 75 | sub.stdout.close() 76 | sub.stdin.close() 77 | sub.kill() 78 | sub.wait() 79 | 80 | def test_pipe_abi_pipe_unix(self): 81 | sub = Popen(["/usr/bin/env", "python", "src/pipe_abi.py", "pipe", 82 | self.unix_socket]) 83 | 84 | for _ in range(20): 85 | if os.path.exists(self.unix_socket): 86 | break 87 | 88 | time.sleep(0.2) 89 | 90 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 91 | try: 92 | s.connect(self.unix_socket) 93 | 94 | s.sendall("HELO\t1\n".encode("utf-8")) 95 | line = s.recv(100).decode("utf-8") 96 | assert re.match("^OK\t", line) 97 | finally: 98 | sub.kill() 99 | sub.wait() 100 | s.close() 101 | os.unlink(self.unix_socket) 102 | 103 | 104 | if __name__ == '__main__': 105 | unittest.main() 106 | --------------------------------------------------------------------------------