Initial (public) commit

parents
*~
\#*#
*.pyc
# dnslines
Tool used to maintain the following kind of documentational DNS zone.
$ dig +short -t txt rtfm.arrakis.se | sort
"audiorecode"
"conntrack"
"gs-dynspace"
"gs-numbers"
"gs-tabs"
"ssh-multiplex"
"ssh-proxy"
"tsig"
$ dig +short -t txt gs-tabs.rtfm.arrakis.se
"gsettings set org.gnome.shell.app-switcher current-workspace-only true"
$ dig +short -t txt tsig.rtfm.arrakis.se
"dnssec-keygen -a hmac-sha256 -b 256 -n HOST host1-host2"
$
It's built around doing Dynamic DNS updates, and uses [dnspython][1]
under the hood.
See also https://blog.bogosity.se/2016/11/06/oneliners-in-dns/.
## Configuration
To use _dnslines_ you need to setup a dedicated dynamic zone. You will
also need to configure a TSIG key with the permissions to do updates
as well as (preferably) zone transfers.
Client side all configuration come in the form of environment variables.
### Required config
* *DNSLINES_ALG*: Algorithm used by the TSIG key
* *DNSLINES_NAME*: Name of the TSIG key
* *DNSLINES_SECRET*: The actual TSIG key
* *DNSLINES_ZONE*: Name of the zone to maintain.
### Optional config
* *DNSLINES_SERVER*: DNS Server to update against. Defaults to the SOA MNAME.
* *DNSLINES_TTL*: TTL value to set. Defaults to 300.
### Example config
export DNSLINES_ALG="hmac-sha256"
export DNSLINES_NAME="foo-bar"
export DNSLINES_SECRET="DNSS8AI9rkRaKGEcE/f70hN21zSebSPWe0hCU295LjA="
export DNSLINES_SERVER="ns-master.example.net"
export DNSLINES_ZONE="dnsdocs.example.net"
## Usage
$ dnslines --help
Usage:
dnslines --add <name> <oneliner>
dnslines --set <name> <oneliner>
dnslines --delete <name>
dnslines --reindex
dnslines --help
options:
-h, --help
Prints this help message.
-a, --add <name> <oneliner>
Adds a new oneliner. Will fail if name already exists.
-s, --set <name> <oneliner>
Sets a oneliner. Will overwrite if name already exists.
-d, --delete <name>
Deletes a oneliner.
--reindex
Rebuilds the index. Ideally never needed.
## License
dnslines is available under the [MIT license][2].
[1]: http://www.dnspython.org/
[2]: https://opensource.org/licenses/MIT
#!/usr/bin/env python3
#
# Copyright (c) 2016 Andreas Olsson <andreas@arrakis.se>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import binascii
import getopt
import os
import re
import sys
import dns.query
import dns.message
import dns.resolver
import dns.tsigkeyring
import dns.update
import dns.zone
def bailout(exitmsg):
print(exitmsg)
sys.exit(1)
def wrong_number(mode):
bailout('Wrong number of arguments to --{}, exiting.'.format(mode))
def usage():
cmd = os.path.basename(sys.argv[0])
print('Usage:')
print(' {} --add <name> <oneliner>'.format(cmd))
print(' {} --set <name> <oneliner>'.format(cmd))
print(' {} --delete <name>'.format(cmd))
print(' {} --reindex'.format(cmd))
print(' {} --help'.format(cmd))
def print_help():
tfm = '''
options:
-h, --help
Prints this help message.
-a, --add <name> <oneliner>
Adds a new oneliner. Will fail if name already exists.
-s, --set <name> <oneliner>
Sets a oneliner. Will overwrite if name already exists.
-d, --delete <name>
Deletes a oneliner.
--reindex
Rebuilds the index. Ideally never needed.
'''
usage()
print(tfm)
def getargs(commandline):
try:
opts, args = getopt.getopt(commandline, 'adhs',
['add', 'delete', 'help', 'reindex', 'set'])
except getopt.GetoptError as goe:
print(str(goe))
sys.exit(1)
modes = []
for opt, _ in opts:
if opt in ['-h', '--help']:
print_help()
sys.exit(0)
elif opt in ['-a', '--add']:
modes.append('add')
elif opt in ['-d', '--delete']:
modes.append('delete')
elif opt in ['-s', '--set']:
modes.append('set')
elif opt in ['--reindex']:
modes.append('reindex')
if len(modes) != 1:
usage()
sys.exit(1)
else:
mode = modes[0]
if mode in ['add', 'set'] and len(args) != 2:
wrong_number(mode)
elif mode == 'delete' and len(args) != 1:
wrong_number(mode)
elif mode == 'reindex' and len(args) != 0:
wrong_number(mode)
if mode in ['add', 'set', 'delete']:
args[0] = args[0].lower()
namerex = r'^[a-z0-9]+[a-z0-9_-]*[a-z0-9]+$'
if not re.match(namerex, args[0]):
bailout('Invalid oneliner name, exiting.')
return mode, args
def end_dot(entry):
if entry[-1] != '.':
entry += '.'
return entry
def get_server(zone):
try:
if os.environ['DNSLINES_SERVER']:
return end_dot(os.environ['DNSLINES_SERVER'])
except KeyError:
pass
try:
answers = dns.resolver.query(zone, dns.rdatatype.SOA)
except dns.resolver.NXDOMAIN as nxem:
bailout(str(nxem))
except dns.resolver.NoAnswer as noem:
bailout(str(noem))
return str(answers[0].mname)
def get_ttl():
try:
if os.environ['DNSLINES_TTL']:
return int(os.environ['DNSLINES_TTL'])
except KeyError:
pass
return 300
def get_required_config():
result = {}
missing = []
for option in ['zone', 'name', 'alg', 'secret']:
envname = 'DNSLINES_' + option.upper()
try:
if os.environ[envname]:
result[option] = os.environ[envname]
else:
missing.append(envname)
except KeyError:
missing.append(envname)
if missing:
print('Missing environment variable(s):')
for vari in missing:
print(' {}'.format(vari))
print('Exiting.')
sys.exit(1)
else:
result['zone'] = end_dot(result['zone'])
return result
def do_update(update, server):
try:
response = dns.query.tcp(update, server)
except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature,
NotImplementedError) as tue:
bailout('TSIG update error: {}'.format(str(tue)))
return response
def set_oneline(update, dkey, dvalue, config, add_only=False):
zone, server, ttl = config['zone'], config['server'], config['ttl']
if add_only:
update.absent(dkey, dns.rdatatype.TXT)
update.replace(dkey, ttl, dns.rdatatype.TXT, '"{}"'.format(dvalue))
update.add('@', ttl, dns.rdatatype.TXT, dkey)
response = do_update(update, server)
if response.rcode() != 0:
bailout('Failed to set {} oneliner in zone {}'.format(dkey, zone))
def delete_oneline(update, dkey, config):
zone, server = config['zone'], config['server']
update.delete(dkey, dns.rdatatype.TXT)
update.delete('@', dns.rdatatype.TXT, dkey)
response = do_update(update, server)
if response.rcode() != 0:
bailout('Failed to delete {} oneliner in zone {}'.format(dkey, zone))
def rebuild_index(update, keyring, config):
zone, server = config['zone'], config['server']
alg, ttl = config['alg'], config['ttl']
generator = dns.query.xfr(server, zone, keyring=keyring, keyalgorithm=alg)
try:
content = dns.zone.from_xfr(generator)
except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature,
NotImplementedError) as tze:
bailout('TSIG AXFR error: {}'.format(str(tze)))
except dns.exception.FormError as zte:
print('Failed to get zone transfer for {}'.format(zone))
print(str(zte))
sys.exit(1)
names = []
for dnsname, _ in content.iterate_rdatasets(dns.rdatatype.TXT):
name = dnsname.to_text()
if name != '@':
names.append(name)
update.delete('@', dns.rdatatype.TXT)
for name in names:
update.add('@', ttl, dns.rdatatype.TXT, name)
response = do_update(update, server)
if response.rcode() != 0:
bailout('Failed to rebuild the {} index.'.format(zone))
def main():
mode, args = getargs(sys.argv[1:])
config = get_required_config()
config['server'] = get_server(config['zone'])
config['ttl'] = get_ttl()
try:
keyring = dns.tsigkeyring.from_text({
config['name']: config['secret']
})
except binascii.Error as kre:
bailout('TSIG key error: {}'.format(str(kre)))
update = dns.update.Update(
config['zone'], keyring=keyring, keyalgorithm=config['alg']
)
if mode == 'set':
set_oneline(update, args[0], args[1], config)
elif mode == 'add':
set_oneline(update, args[0], args[1], config, add_only=True)
elif mode == 'delete':
delete_oneline(update, args[0], config)
elif mode == 'reindex':
rebuild_index(update, keyring, config)
if __name__ == "__main__":
main()
[MESSAGES CONTROL]
disable=missing-docstring
[REPORTS]
reports=no
[FORMAT]
max-line-length=79
[DESIGN]
max-locals=20
max-branches=15
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment