mirror of
https://github.com/oskar456/dzonegit.git
synced 2024-05-11 05:55:41 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e2ea93fb | ||
|
|
45046429d1 | ||
|
|
293bf930a1 | ||
|
|
c9026ff21b | ||
|
|
b608c25372 | ||
|
|
27ae5ff210 | ||
|
|
331df2a4ec | ||
|
|
1f79f52b1a | ||
|
|
9a521350d3 | ||
|
|
5988fd005e | ||
|
|
7e6376ffb2 | ||
|
|
c3a181be14 | ||
|
|
6f23c066bc |
13
.travis.yml
Normal file
13
.travis.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
before_install:
|
||||||
|
- sudo apt-get install -y bind9utils
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
|
- "nightly"
|
||||||
|
install:
|
||||||
|
- pip install -e .
|
||||||
|
- pip install pytest
|
||||||
|
script:
|
||||||
|
- pytest
|
||||||
|
sudo: false
|
||||||
77
README.rst
77
README.rst
@@ -17,36 +17,47 @@ Main features
|
|||||||
- enforce updating serial number when zone content is changed
|
- enforce updating serial number when zone content is changed
|
||||||
- both ``pre-commit`` and ``pre-receive``/``update`` hooks to enforce similar checks in the remote repository
|
- both ``pre-commit`` and ``pre-receive``/``update`` hooks to enforce similar checks in the remote repository
|
||||||
- ``post-receive`` hook to checkout the working copy from a bare repository, generate config snippets for various DNS server software and reload them
|
- ``post-receive`` hook to checkout the working copy from a bare repository, generate config snippets for various DNS server software and reload them
|
||||||
- only Python standard library is used
|
- only Python 3.5+ standard library is used
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- Python 3.5+
|
- Python 3.5+
|
||||||
- `named-compilezone(8)`_ (part of BIND9 package)
|
- `named-compilezone(8)`_ (part of `bind9utils` package)
|
||||||
- git
|
- git
|
||||||
|
|
||||||
|
|
||||||
Instalation and usage
|
Simple instalation (especially for workstations)
|
||||||
---------------------
|
------------------------------------------------
|
||||||
|
|
||||||
- install required dependencies
|
Since there is no other Python dependency than the standard library, you can
|
||||||
- install ``dzonegit`` package using your favourite tool (``virtualenvwrapper``,
|
simply download the `dzonegit.py` file, make it executable and rename/symlink
|
||||||
``venv``, ``pipenv``, etc.)
|
it to an appropriate hook location inside the Git repository
|
||||||
|
`.git/hooks/pre-commit`. This is especially handy for the end users not
|
||||||
|
experienced with Python packaging ecosystem.
|
||||||
|
|
||||||
|
|
||||||
|
Full instalation and usage
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
- install all the requirements
|
||||||
|
- install ``dzonegit`` Python package using your
|
||||||
|
favourite tool (``virtualenvwrapper``, ``venv``, ``pipenv``, etc.)
|
||||||
- in the local repository, create a symlink for the ``pre-commit`` hook:
|
- in the local repository, create a symlink for the ``pre-commit`` hook:
|
||||||
|
|
||||||
``$ ln -s $(which dzonegit-pre-commit) /path/to/repo/.git/hooks/pre-commit``
|
``$ ln -s $(which dzonegit-pre-commit) /path/to/repo/.git/hooks/pre-commit``
|
||||||
- on the server, install some git repository management software, preferrably Gitolite_
|
- on the server, install some git repository management software,
|
||||||
- on the server, install either ``pre-receive`` or ``update`` hook (both do the same) as
|
preferably Gitolite_
|
||||||
well as ``post-receive`` hook. See `Gitolite documentation on how to add custom hooks`_
|
- on the server, install either ``pre-receive`` or ``update`` hook
|
||||||
- on the server set up the configuration options for each repository
|
(both do the same) as well as the ``post-receive`` hook. See `Gitolite
|
||||||
|
documentation on how to add custom hooks`_
|
||||||
|
- on the server, set up the configuration options for each repository
|
||||||
|
|
||||||
Configuration options
|
Configuration options
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
All configuration options are stored in `git-config(1)`_ in section named ``dzonegit``.
|
All configuration options are stored in `git-config(1)`_ in the section
|
||||||
All boolean options default to *False*.
|
named ``dzonegit``. All boolean options default to *False*.
|
||||||
|
|
||||||
|
|
||||||
*dzonegit.ignorewhitespaceerrors*
|
*dzonegit.ignorewhitespaceerrors*
|
||||||
@@ -76,7 +87,7 @@ All boolean options default to *False*.
|
|||||||
can be provided by appending single digit from 1 to 9 to this option.
|
can be provided by appending single digit from 1 to 9 to this option.
|
||||||
|
|
||||||
*dzonegit.zonereloadcmd*
|
*dzonegit.zonereloadcmd*
|
||||||
A command to run for each zone, whose zone file has been modified. Zone
|
A command to run for each zone, where zone file has been modified. Zone
|
||||||
name is automatically appended as the last argument. Should do something
|
name is automatically appended as the last argument. Should do something
|
||||||
like ``rndc reload``. More commands can be provided by appending single digit
|
like ``rndc reload``. More commands can be provided by appending single digit
|
||||||
from 1 to 9 to this option.
|
from 1 to 9 to this option.
|
||||||
@@ -84,20 +95,27 @@ All boolean options default to *False*.
|
|||||||
*dzonegit.zoneblacklist*
|
*dzonegit.zoneblacklist*
|
||||||
Path to a text file containing list of zone names without trailing dots,
|
Path to a text file containing list of zone names without trailing dots,
|
||||||
one per line. If zone is found on the blacklist, it is ignored when
|
one per line. If zone is found on the blacklist, it is ignored when
|
||||||
``post-receive`` hook generates configuration.
|
``post-receive`` hook generates configuration. Wildcards can be used as
|
||||||
|
well, see `JSON template`_ below.
|
||||||
|
|
||||||
*dzonegit.zonewhitelist*
|
*dzonegit.zonewhitelist*
|
||||||
Path to a text file containing list of zone names without trailing dots,
|
Path to a text file containing list of zone names without trailing dots,
|
||||||
one per line. If not empty and zone is not found on the whitelist,
|
one per line. If not empty and zone is not found on the whitelist,
|
||||||
it is ignored when ``post-receive`` hook generates configuration.
|
it is ignored when ``post-receive`` hook generates configuration. Wildcards
|
||||||
|
can be used as well, see `JSON template`_ below.
|
||||||
|
|
||||||
JSON template
|
JSON template
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
The DNS server configuration snippets are generated using a simple JSON-based
|
The DNS server configuration snippets are generated using a simple JSON-based
|
||||||
template. All keys are optional but please make sure the file is a valid
|
template. All keys are optional but please make sure the file is a valid JSON
|
||||||
JSON file. It is possible to define a zone-specific options, for instance for
|
file. It is possible to define a zone-specific options, for instance for
|
||||||
changing DNSSEC parameters per zone.
|
changing DNSSEC parameters per zone. Those zone-specific options allow usage of
|
||||||
|
wildcards; if an exact match of zone name is not found, the leftmost label is
|
||||||
|
substituted with `*`. If still no match is found, the leftmost label is dropped
|
||||||
|
and the second one is again substituted with `*`. In the end, a single `*` is
|
||||||
|
checked. Only if even this key is not found, the value of *defaultvar* is used
|
||||||
|
as the zone-specific option.
|
||||||
|
|
||||||
Valid keys are:
|
Valid keys are:
|
||||||
|
|
||||||
@@ -112,19 +130,22 @@ Valid keys are:
|
|||||||
|
|
||||||
*defaultvar*
|
*defaultvar*
|
||||||
A string that would template variable ``$zonevar`` expand to if there is not
|
A string that would template variable ``$zonevar`` expand to if there is not
|
||||||
a zone-specific variable defined.
|
a zone-specific variable defined, nor any wildcard matched.
|
||||||
|
|
||||||
*zonevars*
|
*zonevars*
|
||||||
An object mapping zone names (without the final dot) to a zone-specific
|
An object mapping zone names (without the final dot) to a zone-specific
|
||||||
variable to which template variable ``$zonevar`` would expand to.
|
variable to which template variable ``$zonevar`` would expand to. Using
|
||||||
|
wildcards is possible by replacing the leftmost label with `*`. Ultimately,
|
||||||
|
a key with label `*` will match every single zone (making *defaultvar*
|
||||||
|
option litte bit pointless)
|
||||||
|
|
||||||
In the template strings, these placeholders are supported:
|
In the template strings, these placeholders are supported:
|
||||||
|
|
||||||
``$datetime``
|
``$datetime``
|
||||||
Current timestamp
|
Current date and time in human readable format
|
||||||
|
|
||||||
``$zonename``
|
``$zonename``
|
||||||
Zone name, without trailing dot
|
Zone name, without the trailing dot
|
||||||
|
|
||||||
``$zonefile``
|
``$zonefile``
|
||||||
Full path to the zone file
|
Full path to the zone file
|
||||||
@@ -138,12 +159,14 @@ Example JSON template for Knot DNS
|
|||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
{
|
{
|
||||||
"header": "# Managed by dzonegit, do not edit.\n",
|
"header": "# Managed by dzonegit, do not edit.\nzone:",
|
||||||
"footer": "",
|
"footer": "",
|
||||||
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
|
"item": " - domain: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
|
||||||
"defaultvar": "template: default",
|
"defaultvar": "template: default",
|
||||||
"zonevars": {
|
"zonevars": {
|
||||||
"example.com": "template: signed"
|
"example.com": "template: signed",
|
||||||
|
"*.cz": "template: czdomains",
|
||||||
|
"*.in-addr.arpa": "template: ipv4reverse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
dzonegit.py
63
dzonegit.py
@@ -268,6 +268,23 @@ def replace_serial(path, oldserial, newserial):
|
|||||||
path.write_text(updated)
|
path.write_text(updated)
|
||||||
|
|
||||||
|
|
||||||
|
def get_zone_wildcards(name):
|
||||||
|
""" A generator of wildcards out of a zone name.
|
||||||
|
For a DNS name, returns series of:
|
||||||
|
- the name itself
|
||||||
|
- the name with first label substitued as *
|
||||||
|
- the name with first label dropped and second substittuted as *
|
||||||
|
- ...
|
||||||
|
- single *
|
||||||
|
"""
|
||||||
|
yield name
|
||||||
|
labels = name.split(".")
|
||||||
|
while labels:
|
||||||
|
labels[0] = "*"
|
||||||
|
yield ".".join(labels)
|
||||||
|
labels.pop(0)
|
||||||
|
|
||||||
|
|
||||||
def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
|
def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
|
||||||
""" Recursively find all *.zone files and template config file using
|
""" Recursively find all *.zone files and template config file using
|
||||||
a simple JSON based template like this:
|
a simple JSON based template like this:
|
||||||
@@ -278,7 +295,9 @@ def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
|
|||||||
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
|
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
|
||||||
"defaultvar": "template: default",
|
"defaultvar": "template: default",
|
||||||
"zonevars": {
|
"zonevars": {
|
||||||
"example.com": "template: signed"
|
"example.com": "template: signed",
|
||||||
|
"*.com": "template: dotcom",
|
||||||
|
"*": "template: uberdefault"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,13 +320,16 @@ def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
|
|||||||
out.append(headertpl.substitute(mapping))
|
out.append(headertpl.substitute(mapping))
|
||||||
for f in sorted(Path(checkoutpath).glob("**/*.zone")):
|
for f in sorted(Path(checkoutpath).glob("**/*.zone")):
|
||||||
zonename = get_zone_name(f, f.read_bytes())
|
zonename = get_zone_name(f, f.read_bytes())
|
||||||
if whitelist and zonename not in whitelist:
|
if whitelist and not any(
|
||||||
|
n in whitelist
|
||||||
|
for n in get_zone_wildcards(zonename)
|
||||||
|
):
|
||||||
print(
|
print(
|
||||||
"WARNING: Ignoring zone {} - not whitelisted for "
|
"WARNING: Ignoring zone {} - not whitelisted for "
|
||||||
"this repository.".format(zonename),
|
"this repository.".format(zonename),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if zonename in blacklist:
|
if any(n in blacklist for n in get_zone_wildcards(zonename)):
|
||||||
print(
|
print(
|
||||||
"WARNING: Ignoring zone {} - blacklisted for "
|
"WARNING: Ignoring zone {} - blacklisted for "
|
||||||
"this repository.".format(zonename),
|
"this repository.".format(zonename),
|
||||||
@@ -323,7 +345,12 @@ def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
zones[zonename] = f.relative_to(checkoutpath)
|
zones[zonename] = f.relative_to(checkoutpath)
|
||||||
zonevar = zonevars[zonename] if zonename in zonevars else defaultvar
|
for name in get_zone_wildcards(zonename):
|
||||||
|
if name in zonevars:
|
||||||
|
zonevar = zonevars[name]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
zonevar = defaultvar
|
||||||
out.append(itemtpl.substitute(
|
out.append(itemtpl.substitute(
|
||||||
mapping, zonename=zonename,
|
mapping, zonename=zonename,
|
||||||
zonefile=str(f), zonevar=zonevar,
|
zonefile=str(f), zonevar=zonevar,
|
||||||
@@ -468,17 +495,25 @@ def post_receive(stdin=sys.stdin):
|
|||||||
subprocess.run(cmd)
|
subprocess.run(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_action(argv=sys.argv):
|
||||||
|
name = Path(argv[0]).name
|
||||||
|
if "pre-commit" in name:
|
||||||
|
return pre_commit
|
||||||
|
if "update" in name:
|
||||||
|
return update
|
||||||
|
if "pre-receive" in name:
|
||||||
|
return pre_receive
|
||||||
|
if "post-receive" in name:
|
||||||
|
return post_receive
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
name = Path(sys.argv[0]).name
|
action = get_action()
|
||||||
print(name)
|
if action is None and len(sys.argv) > 1:
|
||||||
if name == "pre-commit":
|
sys.argv.pop(0)
|
||||||
pre_commit()
|
action = get_action()
|
||||||
elif name == "update":
|
if action:
|
||||||
update()
|
action()
|
||||||
elif name == "pre-receive":
|
|
||||||
pre_receive()
|
|
||||||
elif name == "post-receive":
|
|
||||||
post_receive()
|
|
||||||
else:
|
else:
|
||||||
sys.exit("No valid command found")
|
sys.exit("No valid command found")
|
||||||
|
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -5,7 +5,7 @@ readme = Path(__file__).with_name("README.rst").read_text()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="dzonegit",
|
name="dzonegit",
|
||||||
version="0.1",
|
version="0.4",
|
||||||
description="Git hooks to manage a repository of DNS zones",
|
description="Git hooks to manage a repository of DNS zones",
|
||||||
long_description=readme,
|
long_description=readme,
|
||||||
long_description_content_type="text/x-rst",
|
long_description_content_type="text/x-rst",
|
||||||
@@ -19,7 +19,6 @@ setup(
|
|||||||
tests_require=["pytest"],
|
tests_require=["pytest"],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
"dzonegit = dzonegit:main",
|
|
||||||
"dzonegit-pre-commit = dzonegit:pre_commit",
|
"dzonegit-pre-commit = dzonegit:pre_commit",
|
||||||
"dzonegit-pre-receive = dzonegit:pre_receive",
|
"dzonegit-pre-receive = dzonegit:pre_receive",
|
||||||
"dzonegit-post-receive = dzonegit:post_receive",
|
"dzonegit-post-receive = dzonegit:post_receive",
|
||||||
|
|||||||
@@ -298,12 +298,14 @@ def test_template_config(git_dir):
|
|||||||
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
|
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
|
||||||
"defaultvar": "template: default",
|
"defaultvar": "template: default",
|
||||||
"zonevars": {
|
"zonevars": {
|
||||||
"example.com": "template: signed"
|
"example.com": "template: signed",
|
||||||
|
"*": "template: dummy"
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
output = dzonegit.template_config(str(git_dir), template)
|
output = dzonegit.template_config(str(git_dir), template)
|
||||||
assert output.startswith("# Managed by dzonegit")
|
assert output.startswith("# Managed by dzonegit")
|
||||||
assert " - zone: \"dummy\"\n file: \"" in output
|
assert " - zone: \"dummy\"\n file: \"" in output
|
||||||
|
assert " template: dummy" in output
|
||||||
assert output.endswith("# This is the end")
|
assert output.endswith("# This is the end")
|
||||||
output = dzonegit.template_config(
|
output = dzonegit.template_config(
|
||||||
str(git_dir),
|
str(git_dir),
|
||||||
@@ -311,9 +313,22 @@ def test_template_config(git_dir):
|
|||||||
whitelist=set("a"),
|
whitelist=set("a"),
|
||||||
)
|
)
|
||||||
assert " - zone: \"dummy\"\n file: \"" not in output
|
assert " - zone: \"dummy\"\n file: \"" not in output
|
||||||
|
output = dzonegit.template_config(
|
||||||
|
str(git_dir),
|
||||||
|
template,
|
||||||
|
blacklist=set("*"),
|
||||||
|
)
|
||||||
|
assert " - zone: \"dummy\"\n file: \"" not in output
|
||||||
|
|
||||||
|
|
||||||
def test_load_set_file(git_dir):
|
def test_load_set_file(git_dir):
|
||||||
git_dir.join("dummy").write("dummy\n\n # Comment")
|
git_dir.join("dummy").write("dummy\n\n # Comment")
|
||||||
s = dzonegit.load_set_file("dummy")
|
s = dzonegit.load_set_file("dummy")
|
||||||
assert s == {"dummy"}
|
assert s == {"dummy"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_zone_wildcards():
|
||||||
|
assert list(dzonegit.get_zone_wildcards("a.long.zone.name")) == [
|
||||||
|
"a.long.zone.name", "*.long.zone.name",
|
||||||
|
"*.zone.name", "*.name", "*",
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user