1
0
mirror of https://github.com/oskar456/dzonegit.git synced 2024-05-11 05:55:41 +00:00

8 Commits

Author SHA1 Message Date
Ondřej Caletka
27ae5ff210 version 0.3 2018-08-10 15:49:29 +02:00
Ondřej Caletka
331df2a4ec Add Experimental filters for smudging/cleaning the SOA serial 2018-08-10 15:48:02 +02:00
Ondřej Caletka
1f79f52b1a Allow wildcards in zone blacklists and whitelists 2018-08-10 12:51:21 +02:00
Ondřej Caletka
9a521350d3 Drop dzonegit multi-call executable 2018-08-10 12:30:51 +02:00
Ondřej Caletka
5988fd005e version 0.2 2018-07-23 14:05:25 +02:00
Ondřej Caletka
7e6376ffb2 Add template wildcard matches 2018-07-23 14:04:46 +02:00
Ondřej Caletka
c3a181be14 Fix Knot DNS template example 2018-07-19 16:38:48 +02:00
Ondřej Caletka
6f23c066bc Add Travis CI config 2018-07-19 10:50:06 +02:00
5 changed files with 153 additions and 33 deletions

13
.travis.yml Normal file
View 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

View File

@@ -24,7 +24,7 @@ Requirements
------------
- Python 3.5+
- `named-compilezone(8)`_ (part of BIND9 package)
- `named-compilezone(8)`_ (part of `bind9utils` package)
- git
@@ -84,20 +84,27 @@ All boolean options default to *False*.
*dzonegit.zoneblacklist*
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
``post-receive`` hook generates configuration.
``post-receive`` hook generates configuration. Wildcards can be used as
well, see `JSON template`_ below.
*dzonegit.zonewhitelist*
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,
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
-------------
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
JSON file. It is possible to define a zone-specific options, for instance for
changing DNSSEC parameters per zone.
template. All keys are optional but please make sure the file is a valid JSON
file. It is possible to define a zone-specific options, for instance for
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:
@@ -112,11 +119,14 @@ Valid keys are:
*defaultvar*
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*
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:
@@ -138,12 +148,14 @@ Example JSON template for Knot DNS
.. code-block:: json
{
"header": "# Managed by dzonegit, do not edit.\n",
"header": "# Managed by dzonegit, do not edit.\nzone:",
"footer": "",
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
"item": " - domain: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
"defaultvar": "template: default",
"zonevars": {
"example.com": "template: signed"
"example.com": "template: signed",
"*.cz": "template: czdomains",
"*.in-addr.arpa": "template: ipv4reverse"
}
}

View File

@@ -14,6 +14,9 @@ from pathlib import Path
from string import Template
SERIAL_SMUDGE_TAG = "DZONEGIT_DO_NOT_CHANGE_OLD_SERIAL_"
class HookException(ValueError):
"""Exception raised when there is an error in input data.
@@ -268,6 +271,23 @@ def replace_serial(path, oldserial, newserial):
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()):
""" Recursively find all *.zone files and template config file using
a simple JSON based template like this:
@@ -278,7 +298,9 @@ def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
"defaultvar": "template: default",
"zonevars": {
"example.com": "template: signed"
"example.com": "template: signed",
"*.com": "template: dotcom",
"*": "template: uberdefault"
}
}
@@ -301,13 +323,16 @@ def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
out.append(headertpl.substitute(mapping))
for f in sorted(Path(checkoutpath).glob("**/*.zone")):
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(
"WARNING: Ignoring zone {} - not whitelisted for "
"this repository.".format(zonename),
)
continue
if zonename in blacklist:
if any(n in blacklist for n in get_zone_wildcards(zonename)):
print(
"WARNING: Ignoring zone {} - blacklisted for "
"this repository.".format(zonename),
@@ -323,7 +348,12 @@ def template_config(checkoutpath, template, blacklist=set(), whitelist=set()):
)
continue
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(
mapping, zonename=zonename,
zonefile=str(f), zonevar=zonevar,
@@ -468,20 +498,30 @@ def post_receive(stdin=sys.stdin):
subprocess.run(cmd)
def main():
name = Path(sys.argv[0]).name
print(name)
if name == "pre-commit":
pre_commit()
elif name == "update":
update()
elif name == "pre-receive":
pre_receive()
elif name == "post-receive":
post_receive()
else:
sys.exit("No valid command found")
def clean_serial(stdin=sys.stdin):
""" Git filter command to replace smudged serial with an increased one. """
for line in stdin:
m = re.search(
r"(\s){}([0-9]+)(\s)".format(SERIAL_SMUDGE_TAG),
line,
)
if m:
oldserial = m.group(2)
newserial = get_increased_serial(oldserial)
line = line.replace(SERIAL_SMUDGE_TAG + oldserial, newserial)
print(line, end="")
if __name__ == "__main__":
main()
def smudge_serial(stdin=sys.stdin):
""" Git filter command to smudge SOA serial number. """
sys.stderr.write("Smudging {}\n".format(sys.argv))
zonedata = stdin.read()
if SERIAL_SMUDGE_TAG not in zonedata:
zonedata = re.sub(
r'(^.*\sSOA\s.+?\s)([0-9]+[^0-9])',
r'\g<1>{}\g<2>'.format(SERIAL_SMUDGE_TAG),
zonedata,
count=1,
flags=re.DOTALL | re.IGNORECASE | re.MULTILINE,
)
print(zonedata, end="")

View File

@@ -5,7 +5,7 @@ readme = Path(__file__).with_name("README.rst").read_text()
setup(
name="dzonegit",
version="0.1",
version="0.3",
description="Git hooks to manage a repository of DNS zones",
long_description=readme,
long_description_content_type="text/x-rst",
@@ -19,11 +19,12 @@ setup(
tests_require=["pytest"],
entry_points={
"console_scripts": [
"dzonegit = dzonegit:main",
"dzonegit-pre-commit = dzonegit:pre_commit",
"dzonegit-pre-receive = dzonegit:pre_receive",
"dzonegit-post-receive = dzonegit:post_receive",
"dzonegit-update = dzonegit:update",
"dzonegit-smudge-serial = dzonegit:smudge_serial",
"dzonegit-clean-serial = dzonegit:clean_serial",
],
},
classifiers=[

View File

@@ -298,12 +298,14 @@ def test_template_config(git_dir):
"item": " - zone: \"$zonename\"\n file: \"$zonefile\"\n $zonevar\n",
"defaultvar": "template: default",
"zonevars": {
"example.com": "template: signed"
"example.com": "template: signed",
"*": "template: dummy"
}
}"""
output = dzonegit.template_config(str(git_dir), template)
assert output.startswith("# Managed by dzonegit")
assert " - zone: \"dummy\"\n file: \"" in output
assert " template: dummy" in output
assert output.endswith("# This is the end")
output = dzonegit.template_config(
str(git_dir),
@@ -311,9 +313,61 @@ def test_template_config(git_dir):
whitelist=set("a"),
)
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):
git_dir.join("dummy").write("dummy\n\n # Comment")
s = dzonegit.load_set_file("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", "*",
]
def test_smudge_serial(capsys):
stdin = StringIO("""
@ 60 IN SOA ns hm (
60 ; serial
60 ; refresh
60 ; retry
60 ; expire
60 ; minimum
)
60 NS ns.example.org.
""")
dzonegit.smudge_serial(stdin)
stdout = capsys.readouterr().out
assert "{}60 ; serial".format(dzonegit.SERIAL_SMUDGE_TAG) in stdout
stdin = StringIO(stdout)
dzonegit.smudge_serial(stdin)
assert stdout == capsys.readouterr().out
def test_clean_serial(capsys):
stdin = StringIO("""
@ 60 IN SOA ns hm (
{}60 ; serial
60 ; refresh
60 ; retry
60 ; expire
60 ; minimum
)
60 NS ns.example.org.
""".format(dzonegit.SERIAL_SMUDGE_TAG))
dzonegit.clean_serial(stdin)
stdout = capsys.readouterr().out
assert " 61 ; serial" in stdout
stdin = StringIO(stdout)
dzonegit.clean_serial(stdin)
assert stdout == capsys.readouterr().out