#! /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.override: 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 cannot be downloaded: %s", 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)