pax_global_header00006660000000000000000000000064140017154470014515gustar00rootroot0000000000000052 comment=d3483634067021bf48ebe544b0175c9656124d56 ags-slc-localzone-d348363/000077500000000000000000000000001400171544700153125ustar00rootroot00000000000000ags-slc-localzone-d348363/.gitignore000066400000000000000000000020411400171544700172770ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject *.npy *.pkl ags-slc-localzone-d348363/CHANGELOG.rst000066400000000000000000000007611400171544700173370ustar00rootroot00000000000000v0.9.8 ------ - Support dnspython 2.1. v0.9.7 ------ - Support dnspython 2. v0.9.6 ------ - Minor documentation change for PyPI. v0.9.5 ------ - Support dnspython 1.16.0. - Add tox config. v0.9.4 ------ - Improve error handling. - Correct and improve documentation. - Create additional tests. v0.9.3 ------ - Remove deprecated option from `open` call and use defaults. v0.9.2 ------ - Add Python 2.7 support. - Beta release. v0.9.1 ------ - Alpha release. v0.0.1 ------ - Origin! (ha, ha) ags-slc-localzone-d348363/LICENSE000066400000000000000000000026751400171544700163310ustar00rootroot00000000000000Copyright (C) 2018 Andrew Grant Spencer Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ags-slc-localzone-d348363/MANIFEST.in000066400000000000000000000000331400171544700170440ustar00rootroot00000000000000include README.rst LICENSE ags-slc-localzone-d348363/README.rst000066400000000000000000000031401400171544700167770ustar00rootroot00000000000000.. image:: https://localzone.iomaestro.com/_images/localzone.png :align: center :width: 100px :height: 100px :alt: Project link: localzone (calzone image by sobinsergey from the Noun Project) :target: https://localzone.iomaestro.com A low-calorie library for managing DNS zones ============================================ .. code:: python import localzone with localzone.manage("db.example.com") as z: r = z.add_record("greeting", "TXT", "hello, world!") r.name # the record name, i.e. "greeting" r.rdtype # the record type, i.e. "TXT" r.content # the record content, i.e. "hello," "world!" Powered by `dnspython `_. Features -------- - A simple API focused on managing resource records in local zone files - Support for almost all resource record types - Auto-save and auto-serial - Built for automation Installing localzone -------------------- .. code-block:: shell $ pip install localzone Raison d'être ------------- Comprehensive, low-level DNS toolkits can be cumbersome for the more common zone management tasks--especially those related to making simple changes to zone records. They can also come with a steep learning curve. Enter localzone: a simple library for managing DNS zones. While `localzone` may be a low-calorie library, it's stuffed full of everything that a hungry hostmaster needs. License ------- - BSD - Calzone image by sobinsergey from the Noun Project Where did the calories go? The likely `suspect `_. ags-slc-localzone-d348363/docs/000077500000000000000000000000001400171544700162425ustar00rootroot00000000000000ags-slc-localzone-d348363/docs/Makefile000066400000000000000000000011041400171544700176760ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)ags-slc-localzone-d348363/docs/_static/000077500000000000000000000000001400171544700176705ustar00rootroot00000000000000ags-slc-localzone-d348363/docs/_static/favicon.ico000066400000000000000000000353561400171544700220250ustar00rootroot0000000000000000 %6  % h6(0` $qq6688T'88'T(88(5885588558855885588558855885588,5888o5885885885555VfeefV,65545o"4<opopc55GaaG5ll5^^5<opopczz5"'~~'55xx55WW55bb55 7SS7 55555555550055kk55pp55oo55oo55oo55oo5(oo(T'oo'Tppkk66???( @ vv)~~)0(~~(0m3(~~(3mo5(~~(5oo5(~~(5oo5(~~(5oo5(~~(5oo5(~~(%To5(~~(%66.o5)~~)o5&&o5 >II> Tqpf^*.eHQQ:5o@@5o#rr#5oHQQ:XX5o^*cc5oo5EE5oo5 QuuQ 5oo55oo55oo55oo5225oo5PP5oo5QQ5oo5PP5om3PP3m0QQ0PP99~~~~~~~~~~~~~~~~~~>|>|>|>|>|>|>|>|>??(  wwiyyiyyyyyyHZww##Zo33#vv#--66**55i55i00cccsags-slc-localzone-d348363/docs/_static/localzone-light.svg000066400000000000000000000125701400171544700235110ustar00rootroot00000000000000 ags-slc-localzone-d348363/docs/_static/localzone.png000066400000000000000000000144261400171544700223730ustar00rootroot00000000000000PNG  IHDR,,N~GtEXtSoftwareAdobe ImageReadyqe<PLTEOOO RRR}}}+++lll%%%FFFWWW>>>''' إLLL:::111HHH 尰瞞###tttpppkkkPPPbbbaaahhhKKK444(((  ʤ222߫쀀yyynnneee<<~>D 'R?:ogX'ͅ]YqxM`)De\+;J.}w۠S_ XܟCۧP-A\k9: G@M}aEcs崷3E?}&_V}:«: L1hX>VyN}阧?ԣ8,^K%`9qXL^~ѩ]핆k@ %جї G9@ ;B/A=Xzlȸ٠2ڋFRw:z^gZA!'֯=nce>{ĺk6Uu>%(x=ny-G/S1 =?]]+۶ȹ4E˷c}رwGYΩT:[bim\LA;rXY):?%Fk* oAbBِ|QR{;x[U]VMVޠ.J1餖l,̰*}2v,ůGebXtQ {ºjq!ߏS{7;}Ge\#*ZS6E!bufS"r-ړ"/7ӓx DeV\`ݱR >P1B vYr?nx( ZQB!vUt}-5rqZDaG(f[Oӑ j -(О[I[\SqauǞzVo&k:T)~VoRMJS0SHa!_"GRoeH9;r 2,=}J{JX('ƝQh+6g *}FeaudeG;jfhzT|)AK޿0܎628oV݉FJ ?*0 wn kWxJ;D`cڢqv ܥN:sBb vjaյ ,O0֋Ք_#}*7$M4̿*Tbvh %\ҩv=*l/,KO\y+VyMAl哶Sn:S,'?=}A7T}w/M֑ $y&o_k4[(;j }=\*}GX)\W]O{{KUB,w|dQ$ _T:kwe롖y-kT&rIeG=WmK_$N۫pԞ }fDNg:od=O H$I.Jt(E+@3N؛=~"zt%vvgeGR=J rc>: m09Mb 1nx[KA%pdSS]PWsRjjo9XF>JGtb|eƷ\ !>*g"OFҊZ )"Q׭U-AbNT7r3hgeK2oiݏw"Ǔj rewn&t~Tф݆kRkY gjGK%úZ2,h'|tIA䈑?ȍ)}\ ,?uk3x`UajT,r>"#ձ$]l*=ZPj:?X.1*Ć#NJ1r{F6|nK^!g)O}]Ex{cne%1o,_#ϰ;uF2?.X|0QVp#nnqYqɗyb;#{/?{sDgbUDy;-A=ɇbz7N>9Gdlb'yLSQ&ץIbgZ͆w-_C i* bV]#FMcFjo.S+}DW|  Ū2=J8Pi~r|Z ̃uw|UK% 8`S>厍_  gWpI=ViͮG* Eqk ՌU:VXl9Ns%C>-IJg??'}.H3.bf7IŰ?-q;*Ⓣª vKZ,h1`Ԇv6pG9=D2\= %x4Rda]Rt=JKOMP))'6֏ ُr4LG;r,CL/ͰԜ?+l-&:jeh Щ!r0!dhD?sy^f3Y:W2[erxfi-.3l8,eY/^LSp[ѻ3 ոr+ 3~zR^^KAOk9M /'3aM+#jx{Du}ŭ_u޵+~%s1kg93aBC5CѶDtgrFa\0^.MLXg6źȁ>rb,όarIuMW6*~}؜m=}&-yt *]s\]}z-wq OB6 xMe{  ,Hzn@ɸ D¾v #7P{Lj=@s1>?P"ƩꞱ1/|orS_;n~{(9}b 8V +,X` `,X `,X `,X` `,X `,X `,X` `,X `,X `,X` `,X `,X `,X` `,X `,?qwWIENDB`ags-slc-localzone-d348363/docs/_static/localzone.svg000066400000000000000000000124361400171544700224050ustar00rootroot00000000000000 ags-slc-localzone-d348363/docs/changelog.rst000066400000000000000000000000751400171544700207250ustar00rootroot00000000000000========= Changelog ========= .. include:: ../CHANGELOG.rst ags-slc-localzone-d348363/docs/conf.py000066400000000000000000000142011400171544700175370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys loc = os.path.abspath(os.path.dirname(__file__)) module_name = "localzone" module_loc = os.path.join(loc, "..") sys.path.insert(0, module_loc) about = {} with open(os.path.join(module_loc, module_name, "__version__.py")) as f: exec(f.read(), about) # -- Project information ----------------------------------------------------- project = 'localzone' copyright = '2018, Andrew Grant Spencer' author = 'Andrew Grant Spencer' # The short X.Y version version = about["__version__"] # The full version, including alpha/beta/rc tags release = about["__version__"] # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = "_static/localzone-light.svg" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { 'canonical_url': '/', } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} html_context = { "display_github": True, # Integrate GitHub "github_user": "ags-slc", # Username "github_repo": "localzone", # Repo name "github_version": "master", # Version "conf_py_path": "/docs/", # Path in the checkout to the docs root } # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'localzonedoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'localzone.tex', 'localzone Documentation', 'Andrew Grant Spencer', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'localzone', 'localzone Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'localzone', 'localzone Documentation', author, 'localzone', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True ags-slc-localzone-d348363/docs/index.rst000066400000000000000000000034241400171544700201060ustar00rootroot00000000000000.. localzone documentation master file, created by sphinx-quickstart on Thu Nov 1 16:04:50 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. include:: logo.rst A low-calorie library for managing DNS zones ============================================ .. code:: python import localzone with localzone.manage("db.example.com") as z: r = z.add_record("greeting", "TXT", "hello, world!") r.name # the record name, i.e. "greeting" r.rdtype # the record type, i.e. "TXT" r.content # the record content, i.e. "hello," "world!" Powered by `dnspython `_. Features -------- - A simple API focused on managing resource records in local zone files - Support for almost all resource record types - Auto-save and auto-serial - Built for automation Installing localzone -------------------- .. code-block:: shell $ pip install localzone Raison d'être ------------- Comprehensive, low-level DNS toolkits can be cumbersome for the more common zone management tasks--especially those related to making simple changes to zone records. They can also come with a steep learning curve. Enter localzone: a simple library for managing DNS zones. While `localzone` may be a low-calorie library, it's stuffed full of everything that a hungry hostmaster needs. License ------- - BSD - Calzone image by sobinsergey from the Noun Project Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. toctree:: :maxdepth: 2 :caption: Documentation: :hidden: modules changelog Where did the calories go? The likely `suspect `_. ags-slc-localzone-d348363/docs/localzone.rst000066400000000000000000000011221400171544700207560ustar00rootroot00000000000000localzone package ================= Submodules ---------- localzone.context module ------------------------ .. automodule:: localzone.context :members: :undoc-members: :show-inheritance: localzone.models module ----------------------- .. automodule:: localzone.models :members: :undoc-members: :show-inheritance: localzone.util module --------------------- .. automodule:: localzone.util :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: localzone :members: :undoc-members: :show-inheritance: ags-slc-localzone-d348363/docs/logo.rst000066400000000000000000000003471400171544700177400ustar00rootroot00000000000000:orphan: .. image:: _static/localzone.svg :align: center :width: 100px :height: 100px :alt: Project link: localzone (calzone image by sobinsergey from the Noun Project) :target: https://localzone.iomaestro.com ags-slc-localzone-d348363/docs/modules.rst000066400000000000000000000001001400171544700204330ustar00rootroot00000000000000localzone ========= .. toctree:: :maxdepth: 4 localzone ags-slc-localzone-d348363/localzone/000077500000000000000000000000001400171544700173005ustar00rootroot00000000000000ags-slc-localzone-d348363/localzone/__init__.py000066400000000000000000000022631400171544700214140ustar00rootroot00000000000000# # You have entered the localzone. # """ The `localzone` DNS Library ~~~~~~~~~~~~~~~~~~~~~~~~~~~ A simple DNS library, written in Python, for managing zone files. Basic usage: >>> import localzone >>> with localzone.manage("db.example.com") as z: ... r = z.add_record("greeting", "TXT", "hello, world!") ... r.name # the record name, i.e. 'greeting' ... r.rdtype # the record type, i.e. 'TXT' ... r.content # the record content, i.e. '"hello," "world!"' ... Print the zone's resource records: >>> import localzone >>> with localzone.manage("db.example.com") as z: ... print(*z.records, sep="\\n") ... @ 3600 IN SOA ns username 2007120710 86400 7200 2419200 3600 @ 3600 IN NS ns @ 3600 IN NS ns.somewhere.example. @ 3600 IN MX 10 mail @ ... or: >>> import localzone >>> with localzone.manage("db.example.com") as z: >>> for r in z.records: >>> print(r) ... The full documentation is available at . :copyright: (c) 2018 by Andrew Grant Spencer. :license: BSD, see LICENSE for more details. """ from .context import manage, load from .models import Zone, Record ags-slc-localzone-d348363/localzone/__version__.py000066400000000000000000000005311400171544700221320ustar00rootroot00000000000000VERSION = (0, 9, 8) __title__ = "localzone" __description__ = "A simple library for managing DNS zones." __url__ = "https://localzone.iomaestro.com" __version__ = ".".join(map(str, VERSION)) __author__ = "Andrew Grant Spencer" __author_email__ = "ags@iomaestro.com" __license__ = "BSD" __copyright__ = "Copyright (C) 2018 Andrew Grant Spencer" ags-slc-localzone-d348363/localzone/context.py000066400000000000000000000054771400171544700213530ustar00rootroot00000000000000""" localzone.context ~~~~~~~~~~~~~~~~~ This module implements the methods for localzone context management. :copyright: (c) 2018 Andrew Grant Spencer :license: BSD, see LICENSE for more details. """ from contextlib import contextmanager import dns.name import dns.rdataclass import dns.tokenizer import dns.zone try: # The api for the zonefile reader was exposed in dnspython 2.1 from dns.zonefile import Reader DNSPYTHON21 = True except ImportError: from dns.zone import _MasterReader DNSPYTHON21 = False from .models import Zone @contextmanager def manage(filename, origin=None, autosave=False): """ A context manager that yields a :class:`Zone ` object. :param filename: The path to the zone's master file. :type filename: string :param origin: (optional) The zone's origin domain :type origin: string :param autosave: (optional) controls whether or not the zone's master file is written to disk upon exit :type autosave: bool :return: :class:`Zone ` object :rtype: localzone.models.Zone """ if origin: # perform basic validation/sanitization on origin origin = dns.name.from_text(origin).to_text() zone = load(filename, origin) try: yield zone finally: if autosave: zone.save() def load(filename, origin=None): """ Read a master zone file and construct a :class:`Zone ` object. :param filename: The path to the zone's master file. :type filename: string :param origin: (optional) The zone's origin domain :type origin: string :return: :class:`Zone ` object :rtype: localzone.Zone """ with open(filename) as text: tok = dns.tokenizer.Tokenizer(text, filename) if DNSPYTHON21: zone = Zone( origin, dns.rdataclass.IN, relativize=True, ) with zone.writer() as txn: reader = Reader( tok, rdclass=dns.rdataclass.IN, txn=txn, allow_include=True, ) reader.read() else: reader = _MasterReader( tok, origin=origin, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, allow_include=True, check_origin=True, ) reader.read() zone = reader.zone # TODO: remember that any method using the zone.filename property should # have it passed as a parameter zone._filename = filename # starting with dnspython v1.16.0, use default_ttl try: zone._ttl = reader.default_ttl except AttributeError: zone._ttl = reader.ttl return zone ags-slc-localzone-d348363/localzone/models.py000066400000000000000000000274031400171544700211430ustar00rootroot00000000000000""" localzone.models ~~~~~~~~~~~~~~~~ This module contains the primary objects that power localzone. :copyright: (c) 2018 Andrew Grant Spencer :license: BSD, see LICENSE for more details. """ from collections import namedtuple from time import strftime, localtime, time from dns.zone import Zone as DNSZone import dns.name import dns.rdata import dns.rdataclass import dns.rdatatype from .util import checksum class Zone(DNSZone): """ Initialize a :class:`Zone ` object. The Zone class extends its base class dns.zone.Zone with additional abstractions (or denormalizations) for dealing with DNS zone records. :param origin: The zone's origin. :type origin: :class:`dns.name.Name ` object or string :param rdclass: The zone's rdata class; the default is class `dns.rdataclass.IN`. :type rdclass: int :param relativize: Should the zone's names be relativized to the origin? :type relativize: bool """ def _increment_serial(self): """ Increment the zone's serial. Credit: https://bitbucket.org/chrismiles/easyzone/ """ next_serial = int(strftime("%Y%m%d00", localtime(time()))) if next_serial <= self.soa.rdata.serial: next_serial = self.soa.rdata.serial + 1 # TODO: this is cheating a little bit, since record rdata is mutable. # The immutable hashid of the SOA record will be out of sync until the # record is released. Probably not a big deal, as there is really no # reason to hold the record given the existence of the soa property. # Should this implementation be reconsidered? if hasattr(self.soa.rdata, 'replace'): content = self.soa.rdata.replace(serial=next_serial).to_text() self.update_record(self.soa.hashid, content) else: self.soa.rdata.serial = next_serial def save(self, filename=None, autoserial=True): """ Write the zone master file to disk. If `filename` is not provided, the file from which the zone was originally loaded will be written. NB: this will replace the file located at `filename`. :param filename: The location to where the zone master file will be written. :type filename: string :param autoserial: Should the zone's serial be updated automatically? :type autoserial: bool """ if not filename: filename = self.filename if autoserial: self._increment_serial() # TODO: investigate subclassing dns.zone.Zone.to_file() to support the # `$ORIGIN` and `$TTL` directives, e.g. with_origin=True, with_ttl=True. self.to_file(filename) def get_record(self, hashid): """ Get a resource record via ID. If no record is found, raise a `KeyError`. :param hashid: The record's ID. :type hashid: string :return: :class:`Record ` object :rtype: localzone.models.Record """ record = next((r for r in self.records if r.hashid == hashid), None) if not record: raise KeyError("The supplied hashid was not found in the zone") return record def get_records(self, rdtype): """ Create and return a list of each resource record in the zone matching the specified type. If rdtype is `"ANY"`, all zone records are returned. :param rdtype: The record's type. :type rdtype: string :return: list of :class:`Record ` objects :rtype: list """ result = [] for n in self.nodes: for rds in self[n]: for r in rds: if ( r.rdtype == dns.rdatatype.from_text(rdtype) or rdtype.upper() == "ANY" ): record = Record(self.origin, n, self[n], rds, r) result.append(record) return result # TODO: was a default rdtype required because of lexicon? # otherwise, remove the default. def find_record(self, rdtype="ANY", name=None, content=None): """ Create and return a list of each resource record in the zone matching the search criteria. :param rdtype: The record's type. :type rdtype: string :param name: The record's name. :type name: string :param content: The record's content. :type content: string :return: list of :class:`Record ` objects :rtype: list """ result = [] # relativize the name if name: name_obj = dns.name.from_text(name, origin=self.origin) name = name_obj.relativize(self.origin).to_text() if rdtype.upper() == "TXT" and content: # Content of record type `TXT` has enclosing quotes. See: # https://git.io/fxART # TODO: will this match for multiline records e.g. domainkeys? # Maybe we should strip quotes instead? i.e. r.content.strip('\"') content = '"%s"' % content for r in self.get_records(rdtype): if ( (r.name == name and r.content == content) or (r.name == name and not content) or (r.content == content and not name) or (not name and not content) ): result.append(r) return result def add_record(self, name, rdtype, content, rdclass="IN", ttl=None): """ Add a resource record to the zone. :param name: The record's name. :type name: string :param rdtype: The record's type, e.g. "CNAME". :type rdtype: string :param content: The record's content. :type content: string :param rdclass: The record's class. :type rdclass: string :param ttl: The record's TTL. :type ttl: ttl :return: :class:`Record ` object :rtype: localzone.models.Record """ # TODO: standardize on named params? # convert string parameters to dnspython objects name = dns.name.from_text(name, self.origin) rdclass = dns.rdataclass.from_text(rdclass) rdtype = dns.rdatatype.from_text(rdtype) # TODO: won't this always be the case? if name.is_subdomain(self.origin): name = name.relativize(self.origin) if not ttl: ttl = self.ttl # create the record data rdata = dns.rdata.from_text(rdclass, rdtype, content, origin=self.origin) # get or create the node and rdataset that will conatin the record node = self.find_node(name, create=True) rdataset = self.find_rdataset(name, rdtype, create=True) # add the new rdata to the set rdataset.add(rdata, ttl) return Record(self.origin, name, node, rdataset, rdata) def remove_record(self, hashid, cascade=True): """ Remove a resource record from the zone. A `KeyError` is raised by the `get_record()` method if the supplied `hashid` is not found in the zone. If `cascade` is `True` and the`rdataset` is empty after removing the record, the `rdataset` is also removed. If the `node` only contains the empty `rdataset`, then the `node` is removed. :param hashid: The record's ID. :type hashid: string :param cascade: (optional) Also remove the rdataset and node if empty? :type cascade: bool """ record = self.get_record(hashid) rdata = record.rdata rdataset = record.rdataset node = record.node rdataset.remove(rdata) if cascade: if not rdataset and len(node) == 1: # the node contains only an empty rdataset; remove self.delete_node(record.name) elif not rdataset: # the node contains other rdatasets; only remove empty set self.delete_rdataset(record.name, record.rdtype) def update_record(self, hashid, content): """ Update the content of a resource record. A `KeyError` is raised by the `get_record()` method if the supplied `hashid` is not found in the zone. :param hashid: The record's ID. :type hashid: string :param content: The new content of the record. :type content: string """ record = self.get_record(hashid) self.remove_record(hashid, cascade=False) return self.add_record(record.name, record.rdtype, content) @property def filename(self): return self._filename @property def ttl(self): return self._ttl @property def soa(self): """ Return the SOA record of the zone's origin. :return: :class:`Record ` object :rtype: localzone.models.Record """ return self.get_records("soa")[0] @property def records(self): """ Return a list of :class:`Record ` objects for each resource record in the zone. If the zone is very large, be aware of memory constraints. :return: list of :class:`Record ` objects :rtype: list """ return self.get_records("ANY") class Record(object): """ Initialize a :class:`Record ` object. :param origin: The record's parent domain. :type origin: :class:`dns.name.Name ` object :param name: The record's name. :type name: :class:`dns.name.Name ` object :param node: The record's node. :type node: :class:`dns.node.Node ` object :param rdataset: The record's rdataset. :type rdataset: :class:`dns.rdataset.Rdataset ` object :param rdata: The record's rdata. :type rdata: :class:`dns.rdata.Rdata ` object """ def __init__(self, origin, name, node, rdataset, rdata): RecordData = namedtuple( "RecordData", ["hashid", "origin", "name", "node", "rdataset", "rdata"] ) hashid = "" # Pre-initialize the record so that a hash id can be created. # Why not just use a dict instead? Because a tuple more clearly # communicates the nature of the interface and the immutability of # the (name, type, content) composite. self._data = RecordData(hashid, origin, name, node, rdataset, rdata) # Create the hash id and replace the tuple. hashid = self.__hash__() self._data = RecordData(hashid, origin, name, node, rdataset, rdata) def __repr__(self): s = "" return s.format(rdtype=self.rdtype, name=self.name) def __str__(self): return self.to_text() def __hash__(self): # TODO: convert to md5? return checksum(self.to_text()) def to_text(self): s = "{name} {ttl} {rdclass} {rdtype} {content}" return s.format( name=self.name, ttl=self.ttl, rdclass=self.rdclass, rdtype=self.rdtype, content=self.content, ) @property def hashid(self): return self._data.hashid @property def name(self): return self._data.name.to_text() @property def origin(self): return self._data.origin.to_text() @property def ttl(self): return self._data.rdataset.ttl @property def content(self): return self._data.rdata.to_text() @property def rdata(self): return self._data.rdata @property def rdclass(self): return dns.rdataclass.to_text(self._data.rdata.rdclass) @property def rdtype(self): return dns.rdatatype.to_text(self._data.rdata.rdtype) @property def rdataset(self): return self._data.rdataset @property def node(self): return self._data.node ags-slc-localzone-d348363/localzone/util.py000066400000000000000000000033321400171544700206300ustar00rootroot00000000000000""" localzone.util ~~~~~~~~~~~~~~ This module contains general purpose utilities used by localzone. """ from itertools import chain, repeat def group(iterable, n, padvalue=None): """ Create n-length sets from an iterable. :param iterable: An iterable object. :type iterable: iterable :param n: The number of items in the set. :type n: int :param padvalue: The value used, if necessary, to pad the last set. :type n: int :return: An iterable set. :rtype: iterator """ return zip(*[chain(iterable, repeat(padvalue, n - 1))] * n) def pack(tup): """ Packs a tuple of 8-bit integers. :param tup: Tuple of 8-bit integers. :type tup: tuple :return: The packed n-bit value, where n is len(tup) * 8. :rtype: int """ size = len(tup) result = 0 for i in range(size): result = tup[i] << (i * 8) | result return result def checksum(word, size=32): """ Implements Fletcher's checksum to create 16, 32, or 64-bit checksums. :param word: The data to checksum. :type word: string :param size: The checksum's size. :type size: int :return: Fletcher's checksum represented as a hexadecimal digest. :rtype: string """ if size not in [16, 32, 64]: raise ValueError("Valid checksum sizes are 16, 32 and 64") bits = int(size / 2) block_size = int(bits / 8) modulus = int(2 ** bits - 1) pad = 0 ordinals = map(ord, word) if size == 16: blocks = ordinals else: blocks = map(pack, group(ordinals, block_size, pad)) a = b = 0 for block in blocks: a += block b += a a %= modulus b %= modulus return format((b << bits) | a, "x").zfill(4) ags-slc-localzone-d348363/requirements.txt000066400000000000000000000000211400171544700205670ustar00rootroot00000000000000dnspython sphinx ags-slc-localzone-d348363/setup.py000066400000000000000000000100111400171544700170150ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Note: To use the 'upload' functionality of this file, you must: # $ pip install twine import io import os import sys from shutil import rmtree from setuptools import find_packages, setup, Command # Package meta-data. NAME = "localzone" DESCRIPTION = "A simple library for managing DNS zones." URL = None EMAIL = "ags@iomaestro.com" AUTHOR = "Andrew Grant Spencer" REQUIRES_PYTHON = ">=2.7.0" VERSION = None # What packages are required for this module to be executed? REQUIRED = ["dnspython"] # What packages are optional? EXTRAS = { # 'fancy feature': ['django'], } # The rest you shouldn't have to touch too much :) # ------------------------------------------------ # Except, perhaps the License and Trove Classifiers! # If you do change the License, remember to change the Trove Classifier for that! here = os.path.abspath(os.path.dirname(__file__)) # Import the README and use it as the long-description. # Note: this will only work if 'README.md' is present in your MANIFEST.in file! try: with io.open(os.path.join(here, "README.rst"), encoding="utf-8") as f: long_description = "\n" + f.read() except FileNotFoundError: long_description = DESCRIPTION # Load the package's __version__.py module as a dictionary. about = {} if not VERSION: with open(os.path.join(here, NAME, "__version__.py")) as f: exec(f.read(), about) else: about["__version__"] = VERSION if URL: about["__url__"] = URL class UploadCommand(Command): """Support setup.py upload.""" description = "Build and publish the package." user_options = [] @staticmethod def status(s): """Prints things in bold.""" print("\033[1m{0}\033[0m".format(s)) def initialize_options(self): pass def finalize_options(self): pass def run(self): try: self.status("Removing previous builds…") rmtree(os.path.join(here, "dist")) except OSError: pass self.status("Building Source and Wheel (universal) distribution…") os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable)) self.status("Uploading the package to PyPI via Twine…") os.system("twine upload dist/*") self.status("Pushing git tags…") os.system("git tag v{0}".format(about["__version__"])) os.system("git push --tags") sys.exit() # Where the magic happens: setup( name=NAME, version=about["__version__"], description=DESCRIPTION, long_description=long_description, long_description_content_type="text/x-rst", author=AUTHOR, author_email=EMAIL, python_requires=REQUIRES_PYTHON, url=about["__url__"], packages=find_packages(exclude=("tests",)), # If your package is a single module, use this instead of 'packages': # py_modules=['mypackage'], # entry_points={ # 'console_scripts': ['mycli=mymodule:cli'], # }, install_requires=REQUIRED, extras_require=EXTRAS, include_package_data=True, license="BSD", classifiers=[ # Trove classifiers # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers "Development Status :: 4 - Beta", "License :: OSI Approved :: BSD License", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: Name Service (DNS)", "Topic :: Software Development :: Libraries :: Python Modules", ], # $ setup.py publish support. cmdclass={"upload": UploadCommand}, ) ags-slc-localzone-d348363/tests/000077500000000000000000000000001400171544700164545ustar00rootroot00000000000000ags-slc-localzone-d348363/tests/__init__.py000066400000000000000000000000541400171544700205640ustar00rootroot00000000000000"""localzone test package initialization""" ags-slc-localzone-d348363/tests/test_context.py000066400000000000000000000012411400171544700215470ustar00rootroot00000000000000import pytest import localzone try: from dns.zonefile import UnknownOrigin except ImportError: from dns.zone import UnknownOrigin ZONEFILE = "tests/zonefiles/db.example.com" ORIGIN = "example.com." TTL = 3600 def test_load(): z = localzone.load(ZONEFILE, ORIGIN) assert z.filename == ZONEFILE assert z.ttl == TTL assert len(z.records) == 16 def test_load_missing_origin(): with pytest.raises(UnknownOrigin): localzone.load("tests/zonefiles/db.no-origin.com") def test_manage(): with localzone.manage(ZONEFILE, ORIGIN) as z: assert z.filename == ZONEFILE assert z.ttl == TTL assert len(z.records) == 16 ags-slc-localzone-d348363/tests/test_models.py000066400000000000000000000064231400171544700213550ustar00rootroot00000000000000from dns.rdatatype import UnknownRdatatype from dns.exception import SyntaxError as DNSSyntaxError import pytest import localzone ZONEFILE = "tests/zonefiles/db.example.com" ORIGIN = "example.com." HASHID = "dd03d449" # # readers # def test_get_all_records(): z = localzone.load(ZONEFILE, ORIGIN) records = z.get_records("ANY") assert len(records) == 16 def test_get_record(): z = localzone.load(ZONEFILE, ORIGIN) record = z.get_record(HASHID) assert record.name == "@" assert record.rdtype == "A" assert record.content == "192.0.2.1" assert record.ttl == 3600 def test_get_record_not_found(): z = localzone.load(ZONEFILE, ORIGIN) with pytest.raises(KeyError): z.get_record("deadbeef") def test_find_records_by_type(): z = localzone.load(ZONEFILE, ORIGIN) records = z.find_record("MX") assert len(records) == 3 def test_find_record_by_name(): z = localzone.load(ZONEFILE, ORIGIN) records = z.find_record("CNAME", "www") assert len(records) == 1 assert records[0].content == "@" def test_find_record_by_content(): z = localzone.load(ZONEFILE, ORIGIN) records = z.find_record("A", content="192.0.2.2") assert len(records) == 1 assert records[0].name == "ns" def test_zone_records_property(): z = localzone.load(ZONEFILE, ORIGIN) records = z.records assert len(records) == 16 # # writers # def test_zone_add_record(): with localzone.manage(ZONEFILE, ORIGIN) as z: record = z.add_record("test", "txt", "testing") assert record.hashid == "28c9e108" def test_zone_add_record_unknown_type(): with localzone.manage(ZONEFILE, ORIGIN) as z: with pytest.raises(UnknownRdatatype): z.add_record("test", "err", "testing") def test_zone_add_record_no_content(): with localzone.manage(ZONEFILE, ORIGIN) as z: with pytest.raises((AttributeError, DNSSyntaxError)): z.add_record("test", "txt", None) def test_zone_remove_record(): with localzone.manage(ZONEFILE, ORIGIN) as z: z.remove_record(HASHID) with pytest.raises(KeyError): z.get_record(HASHID) def test_zone_remove_record_not_found(): with localzone.manage(ZONEFILE, ORIGIN) as z: with pytest.raises(KeyError): z.remove_record("deadbeef") def test_zone_update_record(): with localzone.manage(ZONEFILE, ORIGIN) as z: record = z.update_record(HASHID, "192.0.2.100") assert record.hashid == "117e047a" assert record.name == "@" assert record.rdtype == "A" assert record.content == "192.0.2.100" assert record.ttl == 3600 def test_zone_update_record_not_found(): with localzone.manage(ZONEFILE, ORIGIN) as z: with pytest.raises(KeyError): z.update_record("deadbeef", "eat mor chikin") def test_zone_save(): savefile = "tests/db.example.com.saved" z = localzone.load(ZONEFILE, ORIGIN) serial = z.soa.rdata.serial record = z.update_record(HASHID, "192.0.2.100") z.save(savefile) z = localzone.load(savefile, ORIGIN) record = z.find_record("A", content="192.0.2.100")[0] assert z.soa.rdata.serial > serial assert record.name == "@" assert record.rdtype == "A" assert record.content == "192.0.2.100" assert record.ttl == 3600 ags-slc-localzone-d348363/tests/test_util.py000066400000000000000000000007241400171544700210450ustar00rootroot00000000000000import pytest from localzone.util import checksum # Test vectors WORD = "abcde" CS16 = "c8f0" CS32 = "f04fc729" CS64 = "c8c6c527646362c6" def test_checksum_16(): cs = checksum(WORD, 16) assert cs == CS16 def test_checksum_32(): cs = checksum(WORD) assert cs == CS32 def test_checksum_64(): cs = checksum(WORD, 64) assert cs == CS64 def test_checksum_invalid_size(): with pytest.raises(ValueError): cs = checksum(WORD, 128) ags-slc-localzone-d348363/tests/zonefiles/000077500000000000000000000000001400171544700204525ustar00rootroot00000000000000ags-slc-localzone-d348363/tests/zonefiles/db.example.com000066400000000000000000000031231400171544700231700ustar00rootroot00000000000000$ORIGIN example.com. ; designates the start of this zone file in the namespace $TTL 1h ; default expiration time of all resource records without their own TTL value example.com. IN SOA ns.example.com. username.example.com. ( 2007120710 1d 2h 4w 1h ) example.com. IN NS ns ; ns.example.com is a nameserver for example.com example.com. IN NS ns.somewhere.example. ; ns.somewhere.example is a backup nameserver for example.com example.com. IN MX 10 mail.example.com. ; mail.example.com is the mailserver for example.com @ IN MX 20 mail2.example.com. ; equivalent to above line, "@" represents zone origin @ IN MX 50 mail3 ; equivalent to above line, but using a relative host name example.com. IN A 192.0.2.1 ; IPv4 address for example.com IN AAAA 2001:db8:10::1 ; IPv6 address for example.com ns IN A 192.0.2.2 ; IPv4 address for ns.example.com IN AAAA 2001:db8:10::2 ; IPv6 address for ns.example.com www IN CNAME example.com. ; www.example.com is an alias for example.com wwwtest IN CNAME www ; wwwtest.example.com is another alias for www.example.com mail IN A 192.0.2.3 ; IPv4 address for mail.example.com mail2 IN A 192.0.2.4 ; IPv4 address for mail2.example.com mail3 IN A 192.0.2.5 ; IPv4 address for mail3.example.com @ IN TXT "v=spf1 mx ~all" ; SPFv1 record for example.com ags-slc-localzone-d348363/tests/zonefiles/db.no-origin.com000066400000000000000000000001711400171544700234360ustar00rootroot00000000000000islegit.com. IN SOA ns.islegit.com. ags.islegit.com. ( 2019010100 1d 2h 4w 1h ) @ IN TXT "coming2019" ags-slc-localzone-d348363/tox.ini000066400000000000000000000004771400171544700166350ustar00rootroot00000000000000# tox (https://tox.readthedocs.io/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py38 [testenv] deps = pytest commands = pytest