From e6c68e999358a323500aac9680c4aa8ca76a18cc Mon Sep 17 00:00:00 2001 From: Martin Kosek Date: Tue, 12 Jul 2011 09:11:00 +0200 Subject: Add DNS record modification command The DNS record plugin does not support modification of a record. One can only add A type addresses to a DNS record or remove the current ones. To actually change a DNS record value it has to be removed and then added with a desired value. This patch adds a new DNS plugin command "dnsrecord-mod" which enables user to: - modify a DNS record value (note than DNS record can hold multiple values and those will be overwritten) - remove a DNS record when an empty value is passed New tests for this new command have been added to the CLI test suite. https://fedorahosted.org/freeipa/ticket/1137 --- API.txt | 44 ++++++++++++ VERSION | 2 +- ipalib/plugins/dns.py | 127 +++++++++++++++++++++++------------ tests/test_xmlrpc/test_dns_plugin.py | 48 ++++++++++++- 4 files changed, 176 insertions(+), 45 deletions(-) diff --git a/API.txt b/API.txt index c1765394e..418599c41 100644 --- a/API.txt +++ b/API.txt @@ -681,6 +681,50 @@ output: Output('summary', (, ), 'User-friendly output: ListOfEntries('result', (, ), Gettext('A list of LDAP entries', domain='ipa', localedir=None)) output: Output('count', , 'Number of entries returned') output: Output('truncated', , 'True if not all results were returned') +command: dnsrecord_mod +args: 2,37,3 +arg: Str('dnszoneidnsname', cli_name='dnszone', label=Gettext('Zone name', domain='ipa', localedir=None), query=True, required=True) +arg: Str('idnsname', attribute=True, cli_name='name', label=Gettext('Record name', domain='ipa', localedir=None), multivalue=False, primary_key=True, query=True, required=True) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui', flags=['no_output']) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui', flags=['no_output']) +option: Str('version?', exclude='webui', flags=['no_option', 'no_output']) +option: List('arecord?', _validate_ipaddr, attribute=True, cli_name='a_rec', label='A record', multivalue=True) +option: List('aaaarecord?', _validate_ipaddr, attribute=True, cli_name='aaaa_rec', label='AAAA record', multivalue=True) +option: List('a6record?', attribute=True, cli_name='a6_rec', label='A6 record', multivalue=True) +option: List('afsdbrecord?', attribute=True, cli_name='afsdb_rec', label='AFSDB record', multivalue=True) +option: List('aplrecord?', _validate_ipnet, attribute=True, cli_name='apl_rec', label='APL record', multivalue=True) +option: List('certrecord?', attribute=True, cli_name='cert_rec', label='CERT record', multivalue=True) +option: List('cnamerecord?', attribute=True, cli_name='cname_rec', label='CNAME record', multivalue=True) +option: List('dhcidrecord?', attribute=True, cli_name='dhcid_rec', label='DHCID record', multivalue=True) +option: List('dlvrecord?', attribute=True, cli_name='dlv_rec', label='DLV record', multivalue=True) +option: List('dnamerecord?', attribute=True, cli_name='dname_rec', label='DNAME record', multivalue=True) +option: List('dnskeyrecord?', attribute=True, cli_name='dnskey_rec', label='DNSKEY record', multivalue=True) +option: List('dsrecord?', attribute=True, cli_name='ds_rec', label='DS record', multivalue=True) +option: List('hiprecord?', attribute=True, cli_name='hip_rec', label='HIP record', multivalue=True) +option: List('ipseckeyrecord?', attribute=True, cli_name='ipseckey_rec', label='IPSECKEY record', multivalue=True) +option: List('keyrecord?', attribute=True, cli_name='key_rec', label='KEY record', multivalue=True) +option: List('kxrecord?', attribute=True, cli_name='kx_rec', label='KX record', multivalue=True) +option: List('locrecord?', attribute=True, cli_name='loc_rec', label='LOC record', multivalue=True) +option: List('mxrecord?', _validate_mx, attribute=True, cli_name='mx_rec', label='MX record', multivalue=True) +option: List('naptrrecord?', _validate_naptr, attribute=True, cli_name='naptr_rec', label='NAPTR record', multivalue=True) +option: List('nsrecord?', attribute=True, cli_name='ns_rec', label='NS record', multivalue=True) +option: List('nsecrecord?', attribute=True, cli_name='nsec_rec', label='NSEC record', multivalue=True) +option: List('nsec3record?', attribute=True, cli_name='nsec3_rec', label='NSEC3 record', multivalue=True) +option: List('nsec3paramrecord?', attribute=True, cli_name='nsec3param_rec', label='NSEC3PARAM record', multivalue=True) +option: List('ptrrecord?', attribute=True, cli_name='ptr_rec', label='PTR record', multivalue=True) +option: List('rrsigrecord?', attribute=True, cli_name='rrsig_rec', label='RRSIG record', multivalue=True) +option: List('rprecord?', attribute=True, cli_name='rp_rec', label='RP record', multivalue=True) +option: List('sigrecord?', attribute=True, cli_name='sig_rec', label='SIG record', multivalue=True) +option: List('spfrecord?', attribute=True, cli_name='spf_rec', label='SPF record', multivalue=True) +option: List('srvrecord?', _validate_srv, attribute=True, cli_name='srv_rec', label='SRV record', multivalue=True) +option: List('sshfprecord?', attribute=True, cli_name='sshfp_rec', label='SSHFP record', multivalue=True) +option: List('tarecord?', attribute=True, cli_name='ta_rec', label='TA record', multivalue=True) +option: List('tkeyrecord?', attribute=True, cli_name='tkey_rec', label='TKEY record', multivalue=True) +option: List('tsigrecord?', attribute=True, cli_name='tsig_rec', label='TSIG record', multivalue=True) +option: List('txtrecord?', attribute=True, cli_name='txt_rec', label='TXT record', multivalue=True) +output: Output('summary', (, ), 'User-friendly description of action performed') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('value', , "The primary_key value of the entry, e.g. 'jdoe' for a user") command: dnsrecord_show args: 2,4,3 arg: Str('dnszoneidnsname', cli_name='dnszone', label=Gettext('Zone name', domain='ipa', localedir=None), query=True, required=True) diff --git a/VERSION b/VERSION index e4f79dcb3..b4b706689 100644 --- a/VERSION +++ b/VERSION @@ -79,4 +79,4 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=7 +IPA_API_VERSION_MINOR=8 diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index 2bba446d0..c868b73b7 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -234,10 +234,13 @@ def zone_is_reverse(zone_name): return False -def has_cli_options(entry, no_option_msg): +def has_cli_options(entry, no_option_msg, allow_empty_attrs=False): entry = dict((t, entry.get(t, [])) for t in _record_attributes) - numattr = reduce(lambda x,y: x+y, - map(lambda x: len(x), [ v for v in entry.values() if v is not None ])) + if allow_empty_attrs: + numattr = len(entry) + else: + numattr = reduce(lambda x,y: x+y, + map(lambda x: len(x), [ v for v in entry.values() if v is not None ])) if numattr == 0: raise errors.OptionError(no_option_msg) return entry @@ -544,6 +547,43 @@ class dnsrecord(LDAPObject): ), ) + def _nsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): + if options.get('force', False): + return dn + + for ns in options['nsrecord']: + is_ns_rec_resolvable(ns) + return dn + + def _ptrrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): + components = dn.split(',',2) + addr = components[0].split('=')[1] + zone = components[1].split('=')[1] + zone_len = 0 + for valid_zone in _valid_reverse_zones: + if zone.find(valid_zone) != -1: + zone = zone.replace(valid_zone,'') + zone_name = valid_zone + zone_len = _valid_reverse_zones[valid_zone] + + if not zone_len: + allowed_zones = ', '.join(_valid_reverse_zones) + raise errors.ValidationError(name='cn', + error=unicode(_('Reverse zone for PTR record should be a sub-zone of one the following fully qualified domains: %s') % allowed_zones)) + + ip_addr_comp_count = len(addr.split('.')) + len(zone.split('.')) + if ip_addr_comp_count != zone_len: + raise errors.ValidationError(name='cn', + error=unicode(_('Reverse zone %s requires exactly %d IP address components, %d given') + % (zone_name, zone_len, ip_addr_comp_count))) + + for ptr in options['ptrrecord']: + if not ptr.endswith('.'): + raise errors.ValidationError(name='ptr-rec', + error=unicode(_('PTR record \'%s\' is not fully qualified (check traling \'.\')') % ptr)) + + return dn + def is_pkey_zone_record(self, *keys): idnsname = keys[-1] if idnsname == str(_dns_zone_record) or idnsname == ('%s.' % keys[-2]): @@ -732,43 +772,6 @@ class dnsrecord_add(LDAPCreate, dnsrecord_cmd_w_record_options): has_cli_options(options, self.no_option_msg) return super(dnsrecord_add, self).args_options_2_entry(*keys, **options) - def _nsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - if options.get('force', False): - return dn - - for ns in options['nsrecord']: - is_ns_rec_resolvable(ns) - return dn - - def _ptrrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - components = dn.split(',',2) - addr = components[0].split('=')[1] - zone = components[1].split('=')[1] - zone_len = 0 - for valid_zone in _valid_reverse_zones: - if zone.find(valid_zone) != -1: - zone = zone.replace(valid_zone,'') - zone_name = valid_zone - zone_len = _valid_reverse_zones[valid_zone] - - if not zone_len: - allowed_zones = ', '.join(_valid_reverse_zones) - raise errors.ValidationError(name='cn', - error=unicode(_('Reverse zone for PTR record should be a sub-zone of one the following fully qualified domains: %s') % allowed_zones)) - - ip_addr_comp_count = len(addr.split('.')) + len(zone.split('.')) - if ip_addr_comp_count != zone_len: - raise errors.ValidationError(name='cn', - error=unicode(_('Reverse zone %s requires exactly %d IP address components, %d given') - % (zone_name, zone_len, ip_addr_comp_count))) - - for ptr in options['ptrrecord']: - if not ptr.endswith('.'): - raise errors.ValidationError(name='ptr-rec', - error=unicode(_('PTR record \'%s\' is not fully qualified (check traling \'.\')') % ptr)) - - return dn - def interactive_prompt_callback(self, kw): for param in kw.keys(): if param in _record_attributes: @@ -790,8 +793,8 @@ class dnsrecord_add(LDAPCreate, dnsrecord_cmd_w_record_options): def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): for rtype in options: rtype_cb = '_%s_pre_callback' % rtype - if hasattr(self, rtype_cb): - dn = getattr(self, rtype_cb)(ldap, dn, entry_attrs, *keys, **options) + if hasattr(self.obj, rtype_cb): + dn = getattr(self.obj, rtype_cb)(ldap, dn, entry_attrs, *keys, **options) return dn @@ -807,6 +810,46 @@ class dnsrecord_add(LDAPCreate, dnsrecord_cmd_w_record_options): api.register(dnsrecord_add) +class dnsrecord_mod(dnsrecord_mod_record): + """ + Modify a DNS resource record. + """ + no_option_msg = 'No options to modify a specific record provided.' + + def update_old_entry_callback(self, entry_attrs, old_entry_attrs): + for (a, v) in entry_attrs.iteritems(): + if not isinstance(v, (list, tuple)): + v = [v] + old_entry_attrs.setdefault(a, []) + if v or v is None: # overwrite the old entry + old_entry_attrs[a] = v + print "DNSRECORD_MOD::update_old_entry_callback: old:", old_entry_attrs + print "DNSRECORD_MOD::update_old_entry_callback: new:", entry_attrs + + def record_options_2_entry(self, **options): + entries = dict((t, options.get(t, [])) for t in _record_attributes) + return has_cli_options(entries, self.no_option_msg, True) + + def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): + for rtype in options: + rtype_cb = '_%s_pre_callback' % rtype + if hasattr(self.obj, rtype_cb): + dn = getattr(self.obj, rtype_cb)(ldap, dn, entry_attrs, *keys, **options) + print "DNSRECORD_MOD::pre_callback: rtype_cb:", rtype_cb + + return dn + + def post_callback(self, keys, entry_attrs): + print "DNSRECORD_MOD::post_callback:", entry_attrs + if not self.obj.is_pkey_zone_record(*keys): + for a in _record_attributes: + if a in entry_attrs and entry_attrs[a]: + return + return self.obj.methods.delentry(*keys) + +api.register(dnsrecord_mod) + + class dnsrecord_delentry(LDAPDelete): """ Delete DNS record entry. diff --git a/tests/test_xmlrpc/test_dns_plugin.py b/tests/test_xmlrpc/test_dns_plugin.py index b994a2383..4a149db2e 100644 --- a/tests/test_xmlrpc/test_dns_plugin.py +++ b/tests/test_xmlrpc/test_dns_plugin.py @@ -364,7 +364,7 @@ class test_dns(Declarative): dict( - desc='Add A record to %r in zone %r' % (dnszone1, dnsres1), + desc='Add A record to %r in zone %r' % (dnsres1, dnszone1), command=('dnsrecord_add', [dnszone1, dnsres1], {'arecord': u'10.10.0.1'}), expected={ 'value': dnsres1, @@ -380,7 +380,7 @@ class test_dns(Declarative): dict( - desc='Remove A record from %r in zone %r' % (dnszone1, dnsres1), + desc='Remove A record from %r in zone %r' % (dnsres1, dnszone1), command=('dnsrecord_del', [dnszone1, dnsres1], {'arecord': u'127.0.0.1'}), expected={ 'value': dnsres1, @@ -393,6 +393,50 @@ class test_dns(Declarative): ), + dict( + desc='Add AAAA record to %r in zone %r using dnsrecord_mod' % (dnsres1, dnszone1), + command=('dnsrecord_mod', [dnszone1, dnsres1], {'aaaarecord': u'::1'}), + expected={ + 'value': dnsres1, + 'summary': None, + 'result': { + 'idnsname': [dnsres1], + 'arecord': [u'10.10.0.1'], + 'aaaarecord': [u'::1'], + }, + }, + ), + + + dict( + desc='Modify AAAA record in %r in zone %r' % (dnsres1, dnszone1), + command=('dnsrecord_mod', [dnszone1, dnsres1], {'aaaarecord': u'ff02::1'}), + expected={ + 'value': dnsres1, + 'summary': None, + 'result': { + 'idnsname': [dnsres1], + 'arecord': [u'10.10.0.1'], + 'aaaarecord': [u'ff02::1'], + }, + }, + ), + + + dict( + desc='Remove AAAA record from %r in zone %r using dnsrecord_mod' % (dnsres1, dnszone1), + command=('dnsrecord_mod', [dnszone1, dnsres1], {'aaaarecord': u''}), + expected={ + 'value': dnsres1, + 'summary': None, + 'result': { + 'idnsname': [dnsres1], + 'arecord': [u'10.10.0.1'], + }, + }, + ), + + dict( desc='Delete record %r in zone %r' % (dnsres1, dnszone1), command=('dnsrecord_del', [dnszone1, dnsres1], {'del_all': True }), -- cgit