initial checkin / version
This commit is contained in:
597
freeipa-dns.py
Executable file
597
freeipa-dns.py
Executable file
@@ -0,0 +1,597 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# freeipa-dns.py - python script to migrate and maintain DNS domains in FreeIPA
|
||||
#
|
||||
# Version 1.0, latest version, documentation and bugtracker available at:
|
||||
# https://gitlab.lindenaar.net/scripts/freeipa
|
||||
#
|
||||
# Copyright (c) 2018 Frederik Lindenaar
|
||||
#
|
||||
# This script is free software,you can redistribute and/or modify it under the
|
||||
# terms of version 3 of the GNU General Public License as published by the Free
|
||||
# Software Foundation, or (at your option) any later version of the license.
|
||||
#
|
||||
# This script is distributed in the hope that it will be useful but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, visit <http://www.gnu.org/licenses/> to download it.
|
||||
|
||||
"""
|
||||
migrate/synchronize and maintain DNS domain(s) with FreeIPA.
|
||||
|
||||
This script provides functionality not provided by FreeIPA to migrate and/or
|
||||
synchronize / maintain DNS data in FreeIPA. Currently the following commands
|
||||
are implemented:
|
||||
axfr import/synchronize a DNS zone in FreeIPA using a zone-xfer
|
||||
copy copy a DNS record in FreeIPA within or between zones
|
||||
move move a DNS record in FreeIPA from one one to another
|
||||
serial update (set) zone serial(s) in FreeIPA
|
||||
generate generate number-range DNS records/attributes in FreeIPA
|
||||
reverse-ptr create/update reverse DNS (PTR) entries in FreeIPA
|
||||
|
||||
for available commands run 'freeipa-dns.py -h and to get an overview of
|
||||
the available options for each commmand run 'freeipa-dns.py <command> -h'
|
||||
"""
|
||||
|
||||
import os, logging
|
||||
from datetime import date
|
||||
from argparse import ArgumentParser, FileType, \
|
||||
_StoreAction as StoreAction, _StoreConstAction as StoreConstAction
|
||||
|
||||
import dns.query
|
||||
import dns.zone
|
||||
from dns.rdatatype import SOA, HINFO
|
||||
from dns.exception import DNSException
|
||||
|
||||
from ipalib import api
|
||||
from ipalib.errors import PublicError, AuthenticationError, NotFound
|
||||
|
||||
|
||||
VERSION="1.0"
|
||||
PROG_NAME=os.path.splitext(os.path.basename(__file__))[0]
|
||||
PROG_VERSION=PROG_NAME + ' ' + VERSION
|
||||
LOG_FORMAT='%(levelname)s - %(message)s'
|
||||
LOG_FORMAT_FILE='%(asctime)s - ' + LOG_FORMAT
|
||||
|
||||
logger = logging.getLogger(PROG_NAME)
|
||||
|
||||
|
||||
##################[ Action to immediately set the log level ]##################
|
||||
class SetLogLevel(StoreConstAction):
|
||||
"""ArgumentParser action to set log level to provided const value"""
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
logging.getLogger(PROG_NAME).setLevel(self.const)
|
||||
|
||||
####################[ Action to immediately log to a file ]####################
|
||||
class SetLogFile(StoreAction):
|
||||
"""ArgumentParser action to log to file (sets up FileHandler accordingly)"""
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
super(SetLogFile, self).__call__(parser,namespace,values,option_string)
|
||||
formatter = logging.Formatter(LOG_FORMAT_FILE)
|
||||
handler = logging.FileHandler(values)
|
||||
handler.setFormatter(formatter)
|
||||
logger = logging.getLogger(PROG_NAME)
|
||||
logger.propagate = False
|
||||
logger.addHandler(handler)
|
||||
|
||||
def get_zone_opts(args):
|
||||
return {
|
||||
'dnsttl': None if args.ttl is None else str(args.ttl),
|
||||
'dnsdefaultttl': None if args.default_ttl is None else str(args.default_ttl),
|
||||
'idnsforwardpolicy':args.forward_policy,
|
||||
'idnsforwarders': args.forwarder,
|
||||
'idnsallowquery': ';'.join(args.allow_query)+';' if args.allow_query else None,
|
||||
'idnsallowtransfer':';'.join(args.allow_transfer)+';' if args.allow_transfer else None,
|
||||
'idnsallowsyncptr': args.create_reverse_ptr,
|
||||
}, {
|
||||
'skip_nameserver_check':args.no_ns_check,
|
||||
'skip_overlap_check': args.no_ns_check,
|
||||
}, {
|
||||
'force': args.no_ns_check,
|
||||
}
|
||||
|
||||
############################[ AXFR implementation ]############################
|
||||
def process_zone(api, dnszone, args):
|
||||
"""Process a single DNS Zone and synchronize with FreeIPA using its API"""
|
||||
domain = args.merge_zone if args.merge_zone else str(dnszone.origin)
|
||||
logger.info('processing DNS domain %s to %s', str(dnszone.origin), domain)
|
||||
soa_data = dnszone.get_rdataset(dnszone.origin, SOA)[0]
|
||||
zone_opts, add_opts, mod_opts = get_zone_opts(args)
|
||||
try:
|
||||
ipa_data = api.Command.dnszone_show(domain, all=True)['result']
|
||||
updates = { key: str(soa_value) for key, ipa_value in (
|
||||
(k, v[0]) for k,v in ipa_data.items() if k.startswith('idnssoa')
|
||||
) for soa_value in ( getattr(soa_data, key[7:]), )
|
||||
if ( int(soa_value) > int(ipa_value) if key == 'idnssoaserial'
|
||||
else str(soa_value) != str(ipa_value) )
|
||||
}
|
||||
updates.update((key, value) for key,value in zone_opts.items()
|
||||
if value is not None and not (key in ipa_data and
|
||||
value in ipa_data[key] if isinstance(value, str)
|
||||
else set(value).issubset(ipa_data.get(key))))
|
||||
if updates:
|
||||
logger.info('updating existing domain %s in FreeIPA', domain)
|
||||
logger.debug('updating domain %s with %s', domain, updates)
|
||||
api.Command.dnszone_mod(domain, **mod_opts, **updates)
|
||||
else:
|
||||
logger.debug('no updates for existing domain %s in FreeIPA', domain)
|
||||
except NotFound:
|
||||
logger.info('creating new domain %s in FreeIPA', domain)
|
||||
response = api.Command.dnszone_add(domain, **zone_opts, **add_opts, **{
|
||||
'idnssoa'+key: str(getattr(soa_data, key)) for key in [
|
||||
'mname','rname','serial','refresh','retry','expire','minimum']})
|
||||
print(response)
|
||||
|
||||
api.Command.dnsrecord_del(domain, '@',
|
||||
nsrecord=response['result']['nsrecord'])
|
||||
|
||||
recordname = lambda x: dns.rdatatype.to_text(x.rdtype).lower()+'record'
|
||||
optionchecks = tuple((key, datafield) for key, argopt, datafield in (
|
||||
('force', 'no_ns_check', 'nsrecord'),
|
||||
('a_extra_create_reverse', 'create_reverse_ptr', 'arecord'),
|
||||
('aaaa_extra_create_reverse', 'create_reverse_ptr', 'aaaarecord')
|
||||
) if getattr(args, argopt) )
|
||||
options = lambda data: { k: True for k, f in optionchecks if f in data }
|
||||
for rname, rdataset in dnszone.items():
|
||||
name = str(rname)
|
||||
dns_data = { recordname(rdatas[0]): tuple(map(str, rdatas))
|
||||
for rdatas in rdataset.rdatasets
|
||||
if rdatas[0].rdtype not in (SOA, HINFO) }
|
||||
try:
|
||||
ipa_data = api.Command.dnsrecord_show(domain, name)['result']
|
||||
dns_data={ key: value for key, value in dns_data.items() if not (
|
||||
key in ipa_data and set(value).issubset(ipa_data[key]))}
|
||||
if dns_data:
|
||||
logger.info('updating %s in domain %s in FreeIPA', name, domain)
|
||||
except NotFound:
|
||||
logger.info('adding %s to domain %s in FreeIPA', name, domain)
|
||||
finally:
|
||||
if dns_data:
|
||||
dns_data.update(options(dns_data))
|
||||
logger.debug(dns_data)
|
||||
api.Command.dnsrecord_add(domain, name, **dns_data)
|
||||
else:
|
||||
logger.debug('no updates for entry %s', name)
|
||||
|
||||
def axfr(api, args):
|
||||
for domain in args.dnszone:
|
||||
logger.info('performing zone-xfer for domain %s', domain)
|
||||
axfr_request = dns.query.xfr(args.dnsserver, domain,
|
||||
source=args.source_address, relativize=args.relativize)
|
||||
dnszone = dns.zone.from_xfr(axfr_request, relativize=args.relativize)
|
||||
process_zone(api, dnszone, args)
|
||||
|
||||
|
||||
############################[ COPY implementation ]############################
|
||||
def copy(api, args, remove=False):
|
||||
"""Copy a DNS Record in FreeIPA and optionally remove the source values"""
|
||||
src_record, src_zone = ('@', args.source_record) if args.source_zone else \
|
||||
(args.source_record, args.source_zone_name) if args.source_zone_name else \
|
||||
args.source_record.split('.', 1)
|
||||
dst_record, dst_zone = ('@', args.target_record) if args.target_zone else \
|
||||
(args.target_record, args.target_zone_name) if args.target_zone_name else \
|
||||
args.target_record.split('.', 1)
|
||||
|
||||
logger.debug('%s %s in %s to %s in %s', 'Moving' if remove else 'Copying',
|
||||
src_record, src_zone, dst_record, dst_zone)
|
||||
|
||||
src_data = { key:value for key,value in
|
||||
api.Command.dnsrecord_show(src_zone, src_record)['result'].items()
|
||||
if key.endswith('record') and
|
||||
(not args.limit_records or key[:-6].upper() in args.limit_records) }
|
||||
logger.debug("obtained source data from %s in domain %s,%s", src_record, src_zone, src_data)
|
||||
|
||||
try:
|
||||
dst_data = api.Command.dnsrecord_show(dst_zone, dst_record)['result']
|
||||
if not args.merge:
|
||||
logger.error("Destination record %s already exists in zone %s", dst_record, dst_zone)
|
||||
exit(1)
|
||||
updates = { key: value for key, value in src_data.items()
|
||||
if not (key in dst_data and set(value).issubset(dst_data[key]))
|
||||
}
|
||||
if updates:
|
||||
logger.info('updating %s in domain %s in FreeIPA', dst_record, dst_zone)
|
||||
except NotFound:
|
||||
logger.info('creating %s in domain %s in FreeIPA', dst_record, dst_zone)
|
||||
updates = src_data
|
||||
|
||||
if updates:
|
||||
logger.debug(updates)
|
||||
api.Command.dnsrecord_add(dst_zone, dst_record, **updates)['result']
|
||||
else:
|
||||
logger.debug('no updates for entry %s in domain %s', dst_record, dst_zone)
|
||||
|
||||
if remove and src_data:
|
||||
if not args.limit_records:
|
||||
logger.info('removing %s from domain %s in FreeIPA', src_record, src_zone)
|
||||
api.Command.dnsrecord_delentry(src_zone, (src_record,))
|
||||
else:
|
||||
logger.info('removing attributes %s from %s in domain %s in FreeIPA',
|
||||
args.limit_records, src_record, src_zone)
|
||||
api.Command.dnsrecord_del(src_zone, src_record, **src_data)
|
||||
|
||||
|
||||
############################[ MOVE implementation ]############################
|
||||
def move(api, args):
|
||||
"""Move a DNS Record from one DNS zone in FreeIPA to another or rename it"""
|
||||
copy(api, args, True)
|
||||
|
||||
|
||||
###########################[ SERIAL implementation ]###########################
|
||||
def serial(api, args, date=date.today()):
|
||||
"""Set the SOA serial number of a DNS zone to specified or RFC1912 value"""
|
||||
serial = int(args.serial if args.serial else '%04d%02d%02d%02d'%(date.year,
|
||||
0 if args.year_revision is not False else date.month,
|
||||
0 if args.today_revision is False else date.day,
|
||||
args.year_revision if args.year_revision else
|
||||
args.month_revision if args.month_revision else
|
||||
args.today_revision if args.today_revision else 0))
|
||||
for domain in args.dnszone:
|
||||
ipa_serial = int(
|
||||
api.Command.dnszone_show(domain)['result']['idnssoaserial'][0])
|
||||
if ipa_serial > serial and not (args.force or args.ignore_greater):
|
||||
logger.error('current serial %s of %s is greater than target (%s)',
|
||||
ipa_serial, domain, serial)
|
||||
exit(1)
|
||||
elif serial > ipa_serial or (args.force and serial != ipa_serial):
|
||||
logger.info('updating serial of %s to %s', domain, serial)
|
||||
api.Command.dnszone_mod(domain, idnssoaserial=serial)
|
||||
|
||||
|
||||
##########################[ GENERATE implementation ]##########################
|
||||
def generate(api, args):
|
||||
"""Generate DNS records / set DNS attributes based on a number range """
|
||||
records = { key: value for key in dir(args) if key.endswith('record')
|
||||
for value in (getattr(args, key),) if value is not None }
|
||||
dynamic = [k for k, v in records.items() if any(map(lambda x: '%' in x, v))]
|
||||
autoincrement = { key: {'sep':':', 'maxvalue':65535, 'base':16, 'fmt':'%x'}
|
||||
if key == 'aaaarecord' else {} for key in records.keys()
|
||||
if getattr(args, 'auto_increment_'+key[:-6]) }
|
||||
def increment(value, sep='.', maxvalue=255, base=10, fmt='%d'):
|
||||
rest, _, value = value.rpartition(sep)
|
||||
value = 1 if value == '' or value == 'None' else int(value, base) + 1
|
||||
return rest + sep + fmt % value if value < maxvalue else \
|
||||
increment(rest, sep, maxvalue, base, fmt) + sep + 1
|
||||
for number in range(args.start, args.end+1 if args.end else args.start+args.number):
|
||||
name = args.template % number if '%' in args.template else args.template
|
||||
if number > args.start:
|
||||
for key, params in autoincrement.items():
|
||||
records[key] = list(map(lambda x: x if '%' in x else \
|
||||
increment(x, **params), records[key]))
|
||||
dst_data = records.copy()
|
||||
for key in dynamic:
|
||||
dst_data[key] = list(map(lambda x: x%number if '%' in x else x, records[key]))
|
||||
try:
|
||||
ipa_data = api.Command.dnsrecord_show(args.dnszone, name)['result']
|
||||
updates = { key: value for key, value in dst_data.items()
|
||||
if not (key in ipa_data and set(value).issubset(ipa_data[key]))}
|
||||
if updates:
|
||||
logger.info('updating %s in domain %s in FreeIPA', name, args.dnszone)
|
||||
logger.debug(updates)
|
||||
api.Command.dnsrecord_mod(args.dnszone, name, **updates)
|
||||
else:
|
||||
logger.debug('no updates for entry %s in domain %s', name, args.dnszone)
|
||||
except NotFound:
|
||||
logger.info('creating %s in domain %s in FreeIPA', name, args.dnszone)
|
||||
logger.debug(updates)
|
||||
api.Command.dnsrecord_add(args.dnszone, name, **dst_data)
|
||||
|
||||
|
||||
#########################[ REVERSEPTR implementation ]#########################
|
||||
def reverseptr(api, args):
|
||||
rev_zones, ipv4_rev_zones, ipv6_rev_zones = {}, {}, {}
|
||||
logger.info('Fetching existing reverse PTR domains')
|
||||
for revzone in (zone for d in
|
||||
api.Command.dnszone_find('.arpa.',pkey_only=True)['result']
|
||||
for zone in map(str, d['idnsname']) if zone.endswith('.arpa.')):
|
||||
prefix, _, type = revzone[:-6].rpartition('.')
|
||||
if type == 'ip6':
|
||||
if not args.ipv6:
|
||||
continue
|
||||
reverse = lambda v: ''.join(reversed(v.replace('.','')))
|
||||
prefix = reverse(prefix)
|
||||
ipv6_rev_zones[prefix] = revzone
|
||||
else:
|
||||
if not args.ipv4:
|
||||
continue
|
||||
reverse = lambda v: '.'.join(reversed(v.split('.')))
|
||||
prefix = reverse(prefix) + '.'
|
||||
ipv4_rev_zones[prefix] = revzone
|
||||
if args.dnszone or args.all_zones:
|
||||
logger.debug('Loading reverse PTR domain %s for %s*',revzone,prefix)
|
||||
rev_zones[prefix] = { prefix+reverse(str(ptr)): host
|
||||
for d in api.Command.dnsrecord_find(revzone)['result']
|
||||
for ptr in d['idnsname'] for host in d.get('ptrrecord', ())}
|
||||
else:
|
||||
rev_zones[prefix] = {}
|
||||
|
||||
for revprefix in args.create_revprefix:
|
||||
try:
|
||||
if ':' in revprefix and args.ipv6:
|
||||
revtype = 'ipv6 '
|
||||
revprefix = revprefix.strip(': \t\r\n')
|
||||
revprefix = ''.join([ '%04x' % int(w,16)
|
||||
for w in revprefix.split(':',7) ])
|
||||
ipv6_rev_zones[revprefix] = revzone = \
|
||||
'.'.join(reversed(revprefix)) + '.ip6.arpa.'
|
||||
elif '.' in revprefix and args.ipv4:
|
||||
revtype = 'ipv4 '
|
||||
revprefix = revprefix.strip('. \t\r\n')
|
||||
if revprefix.count('.') > 3 or any(map(lambda x: x<0 or x>255,
|
||||
map(int, revprefix.split('.')))):
|
||||
raise ValueError('not a valid IPv4 value')
|
||||
revzone = '.'.join(reversed(revprefix.split('.'))) + '.in-addr.arpa.'
|
||||
revprefix += '.'
|
||||
ipv4_rev_zones[revprefix] = revzone
|
||||
else:
|
||||
revtype = ''
|
||||
raise ValueError('unsupported reverse zone prefix')
|
||||
except ValueError as e:
|
||||
logger.critical('Error: %sreverse zone prefix %s is not valid: %s',
|
||||
revtype, revprefix, e)
|
||||
exit(1)
|
||||
else:
|
||||
if revprefix in rev_zones:
|
||||
logger.debug('%sreverse zone %s exists', revtype, revprefix)
|
||||
else:
|
||||
logger.info('Creating %sreverse zone %s for %s*',
|
||||
revtype, revzone, revprefix)
|
||||
zone_opts, add_opts, _ = get_zone_opts(args)
|
||||
api.Command.dnszone_add(revzone, **zone_opts, **add_opts)
|
||||
rev_zones[revprefix] = {}
|
||||
|
||||
revattrs = [ ('arecord', ipv4_rev_zones, sorted(ipv4_rev_zones), str,
|
||||
lambda t,v: '.'.join(reversed(v[len(t):].split('.')))) ] if args.ipv4 else []
|
||||
if args.ipv6:
|
||||
revattrs.append( ('aaaarecord', ipv6_rev_zones, sorted(ipv6_rev_zones),
|
||||
lambda v: ''.join([ '0000' * (8-v.count(':')) if w == ''
|
||||
else '%04x' % int(w, 16) for w in v.split(':') ]),
|
||||
lambda t,v: '.'.join(reversed(v[len(t):]))) )
|
||||
excluded_zones = map(lambda x: x.strip(':.\t\r\n')+'.', args.exclude_zone)
|
||||
for zone in (z for z in ((n for d in
|
||||
api.Command.dnszone_find(pkey_only=True, idnszoneactive=True)['result']
|
||||
for n in map(str, d['idnsname'])) if args.all_zones
|
||||
else map(lambda x: x.strip(':.\t\r\n')+'.', args.dnszone))
|
||||
if not (z.endswith('in-addr.arpa.') or z.endswith('ip6.arpa.')
|
||||
or z in excluded_zones)):
|
||||
logger.info('Processing DNS zone %s', zone)
|
||||
for record in ( api.Command.dnsrecord_show(zone, host)['result']
|
||||
for host in args.host) if args.host \
|
||||
else api.Command.dnsrecord_find(zone)['result']:
|
||||
recordname = str(record['idnsname'][0])
|
||||
recordname = zone if recordname=='@' else '%s.%s'%(recordname,zone)
|
||||
for revattr,revzones,revzonelist,revconvert,revformat in revattrs:
|
||||
for ipaddr in record.get(revattr, ()):
|
||||
revaddr = revconvert(ipaddr)
|
||||
try:
|
||||
*oldrevs, revtarget = [ rz for rz in revzonelist
|
||||
if revaddr.startswith(rz) ]
|
||||
except ValueError:
|
||||
logger.debug('Skipping %s (%s): no reverse DNS zone',
|
||||
ipaddr, recordname)
|
||||
else:
|
||||
reventry = revformat(revtarget, revaddr)
|
||||
revzone = revzones[revtarget]
|
||||
currrev = rev_zones[revtarget].get(revaddr)
|
||||
if currrev == recordname:
|
||||
logger.debug('no update for %s (%s) in %s',
|
||||
reventry, recordname, revzone)
|
||||
elif currrev and not args.overwrite:
|
||||
logger.warn('not updating %s (%s) in %s pointing to'
|
||||
' %s', reventry, recordname, revzone, currrev)
|
||||
else:
|
||||
action = 'modifying' if currrev else 'adding'
|
||||
logger.info('%s %s (%s) to %s', action, reventry,
|
||||
recordname, revzone)
|
||||
getattr(api.Command, 'dnsrecord_%s' % action[:3])(
|
||||
revzone, reventry, ptrrecord=recordname)
|
||||
rev_zones[revtarget][revaddr] = recordname
|
||||
for revzone, reventry, oldentry in ( (revzones[r],
|
||||
revformat(r, revaddr), o) for r in oldrevs
|
||||
for o in (rev_zones[r].get(revaddr),) if o):
|
||||
logger.info('removing %s (%s) from %s', reventry,
|
||||
oldentry, revzone)
|
||||
api.Command.dnsrecord_delentry(revzone, reventry)
|
||||
|
||||
|
||||
##########################[ Command-line processing ]##########################
|
||||
def parse_args():
|
||||
"""Parse command line and get parameters from environment if not set"""
|
||||
|
||||
record_types = (
|
||||
('A', '4', 'IPv4 address'),
|
||||
('AAAA', '6', 'IPv6 address'),
|
||||
('CNAME', 'c', 'canonical name (alias)'),
|
||||
('MX', 'm', 'Mail Exchange (priority + server)'),
|
||||
('NS', 'N', 'Nameserver name'),
|
||||
('PTR', 'p', 'Reverse address Pointer'),
|
||||
('SRV', 'V', 'Service record (priority+port+server)'),
|
||||
('TXT', 't', 'Text record'),
|
||||
('SSHFP', 'H', 'SSH Fingerprint (priority+type+fingerprint)'),
|
||||
)
|
||||
parser = ArgumentParser(
|
||||
description='Migrate or synchronize a DNS zone in FreeIPA with DNS',
|
||||
)
|
||||
parser.add_argument('-V', '--version',action="version",version=PROG_VERSION)
|
||||
|
||||
pgroup = parser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-q', '--quiet', action=SetLogLevel, const=logging.CRITICAL,
|
||||
default=logging.CRITICAL, help='quiet (only fatal errors)')
|
||||
pgroup.add_argument('-v', '--verbose', action=SetLogLevel, const=logging.INFO,
|
||||
help='more verbose output')
|
||||
pgroup.add_argument('-d', '--debug', action=SetLogLevel, const=logging.DEBUG,
|
||||
help='debug output')
|
||||
|
||||
parser.add_argument('-l', '--logfile', action=SetLogFile,
|
||||
help='send logging output to logfile')
|
||||
|
||||
zoneoptionsparser = ArgumentParser(add_help=False)
|
||||
zoneoptionsparser.add_argument('-n', '--no-ns-check', action='store_true',
|
||||
help='force zone/record creation in case NS not in DNS')
|
||||
zoneoptionsparser.add_argument('-p', '--create-reverse-ptr', action='store_true',
|
||||
help='Enable updating reverse records for created zone(s)')
|
||||
zoneoptionsparser.add_argument('-f','--forward-policy',choices=('first','only','none'),
|
||||
default='first', help='Set forward policy for created zone(s)')
|
||||
zoneoptionsparser.add_argument('-F', '--forwarder', nargs="+",
|
||||
help='Set IP addresses to forward queries for created zone(s)')
|
||||
zoneoptionsparser.add_argument('-Q', '--allow-query', nargs="+", default=(),
|
||||
help='Set IP addresses/ranges that can query created zone(s)')
|
||||
zoneoptionsparser.add_argument('-T', '--allow-transfer', nargs="+", default=(),
|
||||
help='Set IP addresses/ranges that can transfer created zone(s)')
|
||||
zoneoptionsparser.add_argument('-t', '--ttl', type=int,
|
||||
help='Set TTL for SOA record of created zone(s)')
|
||||
zoneoptionsparser.add_argument('-D', '--default-ttl', type=int,
|
||||
help='Set default TTL for records in created zone(s)')
|
||||
|
||||
subparser = parser.add_subparsers(title='Available commands', dest='command')
|
||||
subparser.required=True
|
||||
cmdparser = subparser.add_parser('axfr', parents=[zoneoptionsparser],
|
||||
help='import or synchronize an DNS zone in FreeIPA with the '
|
||||
'result of a zone-xfer',
|
||||
description='Migrate or synchronize a DNS zone in FreeIPA with '
|
||||
'an external DNS server. Since it synchronizes data it is '
|
||||
'safe to run multiple times for a domain.',
|
||||
epilog='Please note that this uses a domain-xfer to fetch DNS '
|
||||
'zone(s) so the DNS server must allow a Zone Transfer '
|
||||
'for the domain from the host running the script for '
|
||||
'this command to work')
|
||||
cmdparser.set_defaults(func=axfr, cmdparser=cmdparser)
|
||||
cmdparser.add_argument('-s', '--source-address',
|
||||
help='perform the zone-xfr from specified address')
|
||||
pgroup = cmdparser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-r', '--relativize', action='store_true', default=True,
|
||||
help='store DNS records relative to zone origin')
|
||||
pgroup.add_argument('-a','--absolute',action='store_false',dest='relativize',
|
||||
help='store DNS records with absolute DNS domain')
|
||||
|
||||
cmdparser.add_argument('-m', '--merge-zone',
|
||||
help='merge DNSZONE(s) into MERGE_ZONE')
|
||||
cmdparser.add_argument('dnsserver', help='DNS Server to request the zone from')
|
||||
cmdparser.add_argument('dnszone', nargs='+', help='DNS Zone to synchronize')
|
||||
|
||||
commonparser = ArgumentParser(add_help=False)
|
||||
commonparser.add_argument('-m', '--merge', action='store_true',
|
||||
help='merge SRC_RECORD into DST_RECORD (default is '
|
||||
'to fail when DST_RECORD exists)')
|
||||
commonparser.add_argument('-l', '--limit-records', nargs='+',
|
||||
choices=list(map(lambda x:x[0], record_types)),
|
||||
help='only move specified record types (default is to move all)')
|
||||
pgroup = commonparser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-z', '--source-zone-name',
|
||||
help='DNS zone to move SRC_RECORD from (will use SRC_RECORD if not provided)')
|
||||
pgroup.add_argument('-Z', '--source-zone', action='store_true',
|
||||
help='move attributes from DNS zone SRC_RECORD itself')
|
||||
pgroup = commonparser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-t', '--target-zone-name',
|
||||
help='new name for SRC_RECORD in DST_RECORD')
|
||||
pgroup.add_argument('-T', '--target-zone', action='store_true',
|
||||
help='move attributes from to DNS zone SRC_RECORD itself')
|
||||
commonparser.add_argument('source_record', help='source record(s) to move')
|
||||
commonparser.add_argument('target_record', help='DNS Zone to move records to')
|
||||
|
||||
cmdparser = subparser.add_parser('copy',parents=[commonparser],
|
||||
help='Copy a DNS record in FreeIPA within or between zones')
|
||||
cmdparser.set_defaults(func=copy, cmdparser=cmdparser)
|
||||
|
||||
cmdparser = subparser.add_parser('move',parents=[commonparser],
|
||||
help='Move a DNS record in FreeIPA from one one to another')
|
||||
cmdparser.set_defaults(func=move, cmdparser=cmdparser)
|
||||
|
||||
cmdparser = subparser.add_parser('serial',
|
||||
help='update (set) zone serial(s) in FreeIPA')
|
||||
cmdparser.set_defaults(func=serial, cmdparser=cmdparser)
|
||||
pgroup = cmdparser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-f', '--force', action='store_true',
|
||||
help='force setting the SOA serial, even when smaller '
|
||||
'(default is to only update to larger value)')
|
||||
pgroup.add_argument('-i', '--ignore-greater', action='store_true',
|
||||
help='silently ignore exisging larger SOA serials '
|
||||
'(default is to abort if existing serial is larger)')
|
||||
pgroup = cmdparser.add_mutually_exclusive_group(required=True)
|
||||
generate_RFC1912 = 'generate RFC1912-format serial based on %s'
|
||||
pgroup.add_argument('-y', '--current-year', type=int,
|
||||
dest='year_revision', nargs='?', default=False,
|
||||
help=generate_RFC1912 % 'current year (YYYY######)')
|
||||
pgroup.add_argument('-m', '--current-month', type=int,
|
||||
dest='month_revision', nargs='?', default=False,
|
||||
help=generate_RFC1912 % 'current year & month (YYYYMM####)')
|
||||
pgroup.add_argument('-t', '--today', type=int,
|
||||
dest='today_revision', nargs='?', default=False,
|
||||
help=generate_RFC1912 % 'current date (YYYYMMDD##)')
|
||||
pgroup.add_argument('-s', '--serial', type=int,
|
||||
help='serial value to use (must be a 32-bit integer)')
|
||||
cmdparser.add_argument('dnszone', nargs='+',
|
||||
help='DNS Zone to update serial for')
|
||||
|
||||
cmdparser = subparser.add_parser('generate',
|
||||
help='generate number-range DNS records/attributes in FreeIPA',
|
||||
epilog='(*) %s/%d will be replaced with the sequence numnber, specify'
|
||||
' #leading zeros like with printf, i.e. %02d = 2 leading zeros')
|
||||
cmdparser.set_defaults(func=generate, cmdparser=cmdparser)
|
||||
cmdparser.add_argument('-s', '--start', type=int, default=1,
|
||||
help='start (first) value of the numner range to generate, defaults to 1')
|
||||
pgroup = cmdparser.add_mutually_exclusive_group(required=True)
|
||||
pgroup.add_argument('-e', '--end', type=int,
|
||||
help='end (last) value of the numner range to generate')
|
||||
pgroup.add_argument('-n', '--number', type=int,
|
||||
help='number of entries to generate')
|
||||
for record, option, description in record_types:
|
||||
cmdparser.add_argument('-'+option, '--'+record.lower()+'record',
|
||||
nargs='+', help='set %s for record (*)' % description)
|
||||
if option in ('4', '6'):
|
||||
cmdparser.add_argument('--auto-increment-'+record.lower(),
|
||||
action='store_true', help='Automatically increment %s values without a pattern' % description)
|
||||
cmdparser.add_argument('dnszone', help='DNS Zone to generate records in')
|
||||
cmdparser.add_argument('template', help='template for the name of the generated records (*)')
|
||||
|
||||
cmdparser = subparser.add_parser('reverse-ptr', parents=[zoneoptionsparser],
|
||||
help='create/update reverse DNS (PTR) entries in FreeIPA',
|
||||
description='Generate IPv4/IPv6 reverse PTR zones and records.')
|
||||
cmdparser.set_defaults(func=reverseptr, cmdparser=cmdparser)
|
||||
pgroup = cmdparser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-4', '--ipv4', action='store_false', dest='ipv6', default=True,
|
||||
help='process only ipv4 addresses')
|
||||
pgroup.add_argument('-6', '--ipv6', action='store_false', dest='ipv4', default=True,
|
||||
help='process only ipv6 addresses')
|
||||
cmdparser.add_argument('-o', '--override', action='store_true',
|
||||
help='overwrite existing reverse PTR records (default is to only add new mappings)')
|
||||
cmdparser.add_argument('-c', '--create_revprefix', nargs='+', default=(),
|
||||
help='create reverse zone for IPv4/IPv6 prefix '
|
||||
'(must contain either . or :, e.g. 10. 192.168 2001:0db8:85a3)')
|
||||
pgroup = cmdparser.add_mutually_exclusive_group(required=False)
|
||||
pgroup.add_argument('-a', '--all-zones', action='store_true',
|
||||
help='process all enabled zones in FreeIPA')
|
||||
pgroup.add_argument('-z', '--dnszone', nargs='+', default=(),
|
||||
help='DNS zone(s) to generate reverse pointer records for')
|
||||
cmdparser.add_argument('-H', '--host', nargs='+',
|
||||
help='DNS host(s) to generate reverse pointer records for within each DNSZONE')
|
||||
cmdparser.add_argument('-x', '--exclude-zone', nargs='+', default=(),
|
||||
help='exclude specified zones')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
###############################################################################
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(format=LOG_FORMAT)
|
||||
args = parse_args()
|
||||
|
||||
try:
|
||||
logger.debug('connecting to FreeIPA')
|
||||
if api.isdone('finalize') is False:
|
||||
api.bootstrap_with_global_options(context='api')
|
||||
api.finalize()
|
||||
api.Backend.rpcclient.connect()
|
||||
args.func(api, args)
|
||||
exit(0)
|
||||
except DNSException as e:
|
||||
logger.critical("Domain %s cannot be downloaded,%s", domain, e)
|
||||
except AuthenticationError:
|
||||
logger.critical("Unable to authenticate to FreeIPA, make sure you have a valid Kerberos ticket!")
|
||||
except PublicError as e:
|
||||
logger.critical("error while communicating with FreeIPA: %s", e)
|
||||
exit(1)
|
||||
|
||||
Reference in New Issue
Block a user