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

20 Commits
v0.2 ... v0.9

Author SHA1 Message Date
Ondřej Caletka
a7d693253d version 0.9 (skipping version 0.8 as it was mistakenly published before) 2018-08-23 10:46:43 +02:00
Ondřej Caletka
3777453d2f Better handling of replace serial failure. 2018-08-23 10:38:56 +02:00
Ondřej Caletka
023906177a version 0.7 2018-08-20 16:37:49 +02:00
Ondřej Caletka
e79bb901f3 $UNIXTIME doc update 2018-08-20 16:37:08 +02:00
Ondřej Caletka
f07c84aa32 Fix no reload on very first push to the repository 2018-08-20 16:33:00 +02:00
Ondřej Caletka
62e35c59d0 version 0.6 2018-08-20 15:30:45 +02:00
Ondřej Caletka
806976ca6e Add smudge filter to replace $UNIXTIME directive on checkout 2018-08-20 15:30:45 +02:00
Ondřej Caletka
17f771bca6 Support custom $UNIXTIME directive in place of SOA serial 2018-08-20 15:30:45 +02:00
Ondřej Caletka
03cf26bbbe Add allowfancynames option 2018-08-20 15:30:40 +02:00
Ondřej Caletka
9923df14b7 Fix crash of post-receive when checkout directory does not exist 2018-08-16 10:35:07 +02:00
Ondřej Caletka
8d99e86222 Travis: allow failures of the nightly build 2018-08-15 14:09:57 +02:00
Ondřej Caletka
08e2ea93fb version 0.4 2018-08-15 14:04:05 +02:00
Ondřej Caletka
45046429d1 Update docs 2018-08-15 14:03:15 +02:00
Ondřej Caletka
293bf930a1 Better multi-call support
Also supports calling by secondary argument (kind-of busybox
stylekind-of). Drop support for multicall setuptools wrapper – when
installed using setuptools, the user should call explicit function
instead.
2018-08-15 13:32:17 +02:00
Ondřej Caletka
c9026ff21b Revert "Drop dzonegit multi-call executable"
This reverts commit 9a521350d3.
Since there is no Python dependency, it may be actually handy to call
the script as is.
2018-08-15 13:17:33 +02:00
Ondřej Caletka
b608c25372 Revert "Add Experimental filters for smudging/cleaning the SOA serial"
This reverts commit 331df2a4ec.
Increasing serial during the cleaning phase does not work as expected.
Maybe the way is to smudge serial to current unix time during checkout
on the server.
2018-08-15 13:08:08 +02:00
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
5 changed files with 193 additions and 51 deletions

View File

@@ -5,6 +5,9 @@ python:
- "3.5"
- "3.6"
- "nightly"
matrix:
allow_failures:
- python: "nightly"
install:
- pip install -e .
- pip install pytest

View File

@@ -15,9 +15,10 @@ Main features
- check if zone file compiles properly using `named-compilezone(8)`_
- autodetect zone name from file name or ``$ORIGIN`` directive
- enforce updating serial number when zone content is changed
- optional ``smudge`` filter to replace ``$UNIXTIME`` directive with current UNIX time
- 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
- only Python standard library is used
- only Python 3.5+ standard library is used
Requirements
@@ -28,30 +29,66 @@ Requirements
- git
Instalation and usage
---------------------
Simple instalation (especially for workstations)
------------------------------------------------
- install required dependencies
- install ``dzonegit`` package using your favourite tool (``virtualenvwrapper``,
``venv``, ``pipenv``, etc.)
Since there is no other Python dependency than the standard library, you can
simply download the `dzonegit.py` file, make it executable and rename/symlink
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:
``$ 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 either ``pre-receive`` or ``update`` hook (both do the same) as
well as ``post-receive`` hook. See `Gitolite documentation on how to add custom hooks`_
- on the server set up the configuration options for each repository
- on the server, install some git repository management software,
preferably Gitolite_
- on the server, install either ``pre-receive`` or ``update`` hook
(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
Support for $UNIXTIME directive
-------------------------------
If you want to use ``$UNIXTIME`` in your zone files instead of serial number,
you have to install a `smudge` filter on the server, that will replace the
directive with current unix time on every checkout. First, set up the filter
in the Git configuration:
.. code-block:: shell
$ git config --global filter.dzonegit.smudge $(which dzonegit-smudge-serial)
Then, apply the filter on all zone files using either ``.git/info/attributes``
or directly ``.gitattributes`` file inside the repository:
.. code-block::
*.zone filter=dzonegit
Configuration options
---------------------
All configuration options are stored in `git-config(1)`_ in section named ``dzonegit``.
All boolean options default to *False*.
All configuration options are stored in `git-config(1)`_ in the section
named ``dzonegit``. All boolean options default to *False*.
*dzonegit.ignorewhitespaceerrors*
Ignore white space errors in ``pre-commit`` and ``pre-receive``/``update`` hooks.
*dzonegit.allowfancynames*
In ``pre-commit`` and ``pre-receive``/``update`` hooks, do not enforce zone
file name to be similar to the name of the zone.
*dzonegit.noserialupdate*
Do not try to automatically update zone serial number if necessary.
Valid only in the ``pre-commit`` hook.
@@ -76,7 +113,7 @@ All boolean options default to *False*.
can be provided by appending single digit from 1 to 9 to this option.
*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
like ``rndc reload``. More commands can be provided by appending single digit
from 1 to 9 to this option.
@@ -84,12 +121,14 @@ 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
-------------
@@ -98,7 +137,7 @@ 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. Those zone-specific options allow usage of
wildcards; if exact match of zone name is not found, the leftmost label is
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
@@ -129,10 +168,10 @@ Valid keys are:
In the template strings, these placeholders are supported:
``$datetime``
Current timestamp
Current date and time in human readable format
``$zonename``
Zone name, without trailing dot
Zone name, without the trailing dot
``$zonefile``
Full path to the zone file

View File

@@ -83,14 +83,26 @@ def get_file_contents(path, revision=None):
return r.stdout
def compile_zone(zonename, zonedata):
def unixtime_directive(zonedata, unixtime=None):
""" Filter binary zone data. Replace $UNIXTIME with current unix time. """
if unixtime is None:
unixtime = int(time.time())
return re.sub(
br'\$UNIXTIME\b',
str(unixtime).encode("ascii"),
zonedata,
flags=re.IGNORECASE,
)
def compile_zone(zonename, zonedata, unixtime=None):
""" Compile the zone. Return tuple with results."""
CompileResults = namedtuple(
"CompileResults", "success, serial, zonehash, stderr",
)
r = subprocess.run(
["/usr/sbin/named-compilezone", "-o", "-", zonename, "/dev/stdin"],
input=zonedata,
input=unixtime_directive(zonedata, unixtime),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
@@ -173,15 +185,15 @@ def get_zone_origin(zonedata):
def get_zone_name(path, zonedata):
"""
Try to guess zone name from either filename or the first $ORIGIN.
Throw a HookException if filename and zone ORIGIN differ more than
in slashes.
Unless disabled, throw a HookException if filename and zone ORIGIN differ
more than in slashes.
"""
stemname = Path(path).stem.lower()
originname = get_zone_origin(zonedata)
if originname:
tt = str.maketrans("", "", "/_,:-+*%^&#$")
sn, on = [s.translate(tt) for s in [stemname, originname]]
if sn != on:
if sn != on and not get_config("dzonegit.allowfancynames", bool):
raise HookException(
"Zone origin {o} differs from zone file.".format(o=originname),
fname=path,
@@ -193,13 +205,14 @@ def get_zone_name(path, zonedata):
def check_updated_zones(against, revision=None, autoupdate_serial=False):
""" Check whether all updated zone files compile. """
unixtime = int(time.time())
for f in get_altered_files(against, "AMCR", revision):
if not f.suffix == ".zone":
continue
print("Checking file {f}".format(f=f))
zonedata = get_file_contents(f, revision)
zname = get_zone_name(f, zonedata)
rnew = compile_zone(zname, zonedata)
rnew = compile_zone(zname, zonedata, unixtime)
if not rnew.success:
raise HookException(
"New zone version does not compile",
@@ -208,7 +221,7 @@ def check_updated_zones(against, revision=None, autoupdate_serial=False):
try:
zonedata = get_file_contents(f, against)
zname = get_zone_name(f, zonedata)
rold = compile_zone(zname, zonedata)
rold = compile_zone(zname, zonedata, unixtime-1)
if (rold.success and rold.zonehash != rnew.zonehash and not
is_serial_increased(rold.serial, rnew.serial)):
@@ -219,9 +232,11 @@ def check_updated_zones(against, revision=None, autoupdate_serial=False):
if autoupdate_serial:
newserial = get_increased_serial(rnew.serial)
replace_serial(f, rnew.serial, newserial)
errmsg += " Serial has been automatically increased."
errmsg += " Check and recommit."
if replace_serial(f, rnew.serial, newserial):
errmsg += " Serial has been automatically increased."
errmsg += " Check and recommit."
else:
errmsg += " Autoupdate of serial number failed."
raise HookException(
errmsg,
fname=f,
@@ -264,8 +279,9 @@ def replace_serial(path, oldserial, newserial):
flags=re.DOTALL | re.IGNORECASE | re.MULTILINE,
)
if count != 1:
raise HookException("Cannot update zone serial number")
return False
path.write_text(updated)
return True
def get_zone_wildcards(name):
@@ -320,13 +336,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),
@@ -431,6 +450,7 @@ def post_receive(stdin=sys.stdin):
raise SystemExit("Checkout path not defined. Nothing to do.")
print("Checking out repository into {}".format(checkoutpath))
Path(checkoutpath).mkdir(parents=True, exist_ok=True)
subprocess.run(
["git", "checkout", "-f", "master"],
check=True,
@@ -462,7 +482,8 @@ def post_receive(stdin=sys.stdin):
if refname != "refs/heads/master":
continue
if against == "0000000000000000000000000000000000000000":
against = get_head() # Empty commit
# Empty commit
against = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
should_reconfig = [
f for f in get_altered_files(against, "ACDRU", revision)
if f.suffix == ".zone"
@@ -492,17 +513,36 @@ def post_receive(stdin=sys.stdin):
subprocess.run(cmd)
def smudge_serial(
bstdin=sys.stdin.buffer,
bstdout=sys.stdout.buffer,
unixtime=None,
):
"""Replace all $UNIXTIME directives with current unix time."""
bstdout.write(unixtime_directive(bstdin.read(), unixtime))
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
if "smudge" in name:
return smudge_serial
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()
action = get_action()
if action is None and len(sys.argv) > 1:
sys.argv.pop(0)
action = get_action()
if action:
action()
else:
sys.exit("No valid command found")

View File

@@ -5,7 +5,7 @@ readme = Path(__file__).with_name("README.rst").read_text()
setup(
name="dzonegit",
version="0.2",
version="0.9",
description="Git hooks to manage a repository of DNS zones",
long_description=readme,
long_description_content_type="text/x-rst",
@@ -19,11 +19,11 @@ 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",
],
},
classifiers=[

View File

@@ -4,7 +4,7 @@ import subprocess
import time
import datetime
import os
from io import StringIO
from io import StringIO, BytesIO
from pathlib import Path
import dzonegit
@@ -74,6 +74,33 @@ ns.example.com. 60 IN A 192.0.2.1
assert r.zonehash == r2.zonehash
def test_compile_unsmudged_zone():
testzone = b"""
$ORIGIN example.com.
@ 60 IN SOA ns hostmaster (
$UNIXTIME ; serial
3600 ; refresh (1 hour)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
60 ; minimum (1 minute)
)
60 IN NS ns
ns.example.com. 60 IN A 192.0.2.1
"""
replaced = dzonegit.unixtime_directive(testzone)
assert b"$UNIXTIME" not in replaced
r = dzonegit.compile_zone("example.com", testzone, 123456)
assert r.success
assert r.serial == str(123456)
def test_smudge_serial():
bstdin = BytesIO(b"something $UNIXTIME something")
bstdout = BytesIO()
dzonegit.smudge_serial(bstdin, bstdout, 123456)
assert b"something 123456 something" == bstdout.getvalue()
def test_is_serial_increased():
assert dzonegit.is_serial_increased(1234567890, "2018010100")
assert dzonegit.is_serial_increased("2018010100", "4018010100")
@@ -131,6 +158,8 @@ ns.example.com. 60 IN A 192.0.2.1
)
with pytest.raises(ValueError):
dzonegit.get_zone_name("zones/example.org.zone", testzone)
subprocess.call(["git", "config", "dzonegit.allowfancynames", "TRUE"])
dzonegit.get_zone_name("zones/example.org.zone", testzone)
testzone = b"""
$ORIGIN 240/28.2.0.192.in-addr.arpa.
@ 60 IN SOA ns hostmaster 1 60 60 60 60
@@ -148,12 +177,12 @@ def test_replace_serial(git_dir):
@ 60 IN SOA ns hm 1 61 60 60 60
60 NS ns.example.org.
""")
dzonegit.replace_serial(Path("dummy.zone"), "1", "60")
assert dzonegit.replace_serial(Path("dummy.zone"), "1", "60")
assert git_dir.join("dummy.zone").read() == """
@ 60 IN SOA ns hm 60 61 60 60 60
60 NS ns.example.org.
"""
dzonegit.replace_serial(Path("dummy.zone"), "60", "61")
assert dzonegit.replace_serial(Path("dummy.zone"), "60", "61")
assert git_dir.join("dummy.zone").read() == """
@ 60 IN SOA ns hm 61 61 60 60 60
60 NS ns.example.org.
@@ -168,7 +197,7 @@ def test_replace_serial(git_dir):
)
60 NS ns.example.org.
""")
dzonegit.replace_serial(Path("dummy.zone"), "60", "6000000")
assert dzonegit.replace_serial(Path("dummy.zone"), "60", "6000000")
assert git_dir.join("dummy.zone").read() == """
@ 60 IN SOA ns hm (
6000000 ; serial
@@ -179,6 +208,7 @@ def test_replace_serial(git_dir):
)
60 NS ns.example.org.
"""
assert not dzonegit.replace_serial(Path("dummy.zone"), "0", "60")
def test_check_updated_zones(git_dir):
@@ -226,6 +256,29 @@ $ORIGIN dummy.
dzonegit.check_updated_zones("HEAD", autoupdate_serial=True)
subprocess.call(["git", "add", "dummy.zone"])
dzonegit.check_updated_zones(dzonegit.get_head())
git_dir.join("dummy.zone").write("""
$ORIGIN dummy.
@ 60 IN SOA ns hm $UNIXTIME 61 60 60 60
60 NS ns.example.org.
""")
subprocess.call(["git", "add", "dummy.zone"])
dzonegit.check_updated_zones(dzonegit.get_head())
subprocess.call(["git", "commit", "-m", "dummy.zone with $UNIXTIME"])
git_dir.join("dummy.zone").write("""
$ORIGIN dummy.
@ 60 IN SOA ns hm 1 60 60 60 60
60 NS ns.example.org.
""")
subprocess.call(["git", "add", "dummy.zone"])
with pytest.raises(ValueError):
dzonegit.check_updated_zones(dzonegit.get_head())
git_dir.join("dummy.zone").write("""
$ORIGIN dummy.
@ 60 IN SOA ns hm $UNIXTIME 60 60 60 60
60 NS ns.example.org.
""")
subprocess.call(["git", "add", "dummy.zone"])
dzonegit.check_updated_zones(dzonegit.get_head())
subprocess.call(["git", "commit", "-m", "final dummy.zone"])
dzonegit.check_updated_zones("HEAD~", "HEAD")
@@ -276,17 +329,18 @@ def test_post_receive(git_dir):
git_dir.chdir()
head = dzonegit.get_head()
revisions = "{} {} refs/heads/master\n".format(
"4b825dc642cb6eb9a060e54bf8d69288fbee4904",
"0000000000000000000000000000000000000000",
head,
)
stdin = StringIO(revisions)
codir = git_dir.mkdir("co")
codir = git_dir.join("co")
subprocess.call(["git", "config", "dzonegit.checkoutpath", str(codir)])
subprocess.call([
"git", "config", "dzonegit.reconfigcmd",
"echo TEST >{}/test".format(codir),
])
dzonegit.post_receive(stdin)
dzonegit.post_receive(stdin) # Check coping with existing codir
assert codir.join("dummy.zone").check()
assert codir.join("test").read() == "TEST\n"
@@ -313,6 +367,12 @@ 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):