#! /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 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 -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)