diff --git a/ipalib/errors.py b/ipalib/errors.py index 22138ab01..86cd60d11 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -1310,6 +1310,23 @@ class MutuallyExclusiveError(ExecutionError): format = _('%(reason)s') +class NonFatalError(ExecutionError): + """ + **4303** Raised when part of an operation succeeds and the part that failed isn't critical. + + For example: + + >>> raise NonFatalError(reason=u'The host was added but the DNS update failed') + Traceback (most recent call last): + ... + NonFatalError: The host was added but the DNS update failed + + """ + + errno = 4303 + format = _('%(reason)s') + + ############################################################################## # 5000 - 5999: Generic errors diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index a3e6c1eeb..6f3959bcb 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -90,6 +90,18 @@ _record_types = ( u'SRV', u'TXT', ) +# mapping from attribute to resource record type +_attribute_types = dict( + arecord=u'A', aaaarecord=u'AAAA', a6record=u'A6', + afsdbrecord=u'AFSDB', certrecord=u'CERT', cnamerecord=u'CNAME', + dnamerecord=u'DNAME', dsrecord=u'DS', hinforecord=u'HINFO', + keyrecord=u'KEY', kxrecord=u'KX', locrecord='LOC', + mdrecord=u'MD', minforecord=u'MINFO', mxrecord=u'MX', + naptrrecord=u'NAPTR', nsrecord=u'NS', nsecrecord=u'NSEC', + ntxtrecord=u'NTXT', ptrrecord=u'PTR', rrsigrecord=u'RRSIG', + sshfprecord=u'SSHFP', srvrecord=u'SRV', txtrecord=u'TXT', +) + # supported DNS classes, IN = internet, rest is almost never used _record_classes = (u'IN', u'CS', u'CH', u'HS') @@ -137,6 +149,7 @@ def dns_container_exists(ldap): except errors.NotFound: raise errors.NotFound(reason=_('DNS is not configured')) + return True class dns(Object): """DNS zone/SOA record object.""" diff --git a/ipalib/plugins/host.py b/ipalib/plugins/host.py index 2e77dd5f0..9d3a2a9a9 100644 --- a/ipalib/plugins/host.py +++ b/ipalib/plugins/host.py @@ -81,10 +81,12 @@ from ipalib.plugins.service import split_principal from ipalib.plugins.service import validate_certificate from ipalib.plugins.service import normalize_certificate from ipalib.plugins.service import set_certificate_attrs +from ipalib.plugins.dns import dns_container_exists, _attribute_types from ipalib import _, ngettext from ipalib import x509 from ipapython.ipautil import ipa_generate_password from ipalib.request import context +from ipaserver.install.bindinstance import get_reverse_zone import base64 import nss.nss as nss @@ -130,6 +132,15 @@ host_output_params = ( ) ) +def validate_ipaddr(ugettext, ipaddr): + """ + Verify that we have either an IPv4 or IPv6 address. + """ + if not util.validate_ipaddr(ipaddr): + return _('invalid IP address') + return None + + class host(LDAPObject): """ Host object. @@ -245,10 +256,39 @@ class host_add(LDAPCreate): Flag('force', doc=_('force host name even if not in DNS'), ), + Str('ipaddr?', validate_ipaddr, + doc=_('Add the host to DNS with this IP address'), + ), ) def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - if not options.get('force', False): + if 'ipaddr' in options and dns_container_exists(ldap): + parts = keys[-1].split('.') + domain = unicode('.'.join(parts[1:])) + result = api.Command['dns_find']()['result'] + match = False + for zone in result: + if domain == zone['idnsname'][0]: + match = True + break + if not match: + raise errors.NotFound(reason=_('DNS zone %(zone)s not found' % dict(zone=domain))) + revzone, revname = get_reverse_zone(options['ipaddr']) + # Verify that our reverse zone exists + match = False + for zone in result: + if revzone == zone['idnsname'][0]: + match = True + break + if not match: + raise errors.NotFound(reason=_('Reverse DNS zone %(zone)s not found' % dict(zone=revzone))) + try: + reverse = api.Command['dns_find_rr'](revzone, revname) + if reverse['count'] > 0: + raise errors.DuplicateEntry(message=u'This IP address is already assigned.') + except errors.NotFound: + pass + if not options.get('force', False) and not 'ipaddr' in options: util.validate_host_dns(self.log, keys[-1]) if 'locality' in entry_attrs: entry_attrs['l'] = entry_attrs['locality'] @@ -275,6 +315,29 @@ class host_add(LDAPCreate): return dn def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + exc = None + try: + if 'ipaddr' in options and dns_container_exists(ldap): + parts = keys[-1].split('.') + domain = unicode('.'.join(parts[1:])) + if ':' in options['ipaddr']: + type = u'AAAA' + else: + type = u'A' + try: + api.Command['dns_add_rr'](domain, parts[0], type, options['ipaddr']) + except errors.EmptyModlist: + # the entry already exists and matches + pass + revzone, revname = get_reverse_zone(options['ipaddr']) + try: + api.Command['dns_add_rr'](revzone, revname, u'PTR', keys[-1]+'.') + except errors.EmptyModlist: + # the entry already exists and matches + pass + del options['ipaddr'] + except Exception, e: + exc = e if options.get('random', False): try: entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) @@ -282,6 +345,8 @@ class host_add(LDAPCreate): # On the off-chance some other extension deletes this from the # context, don't crash. pass + if exc: + raise errors.NonFatalError(reason=_('The host was added but the DNS update failed with: %(exc)s' % dict(exc=exc))) set_certificate_attrs(entry_attrs) return dn @@ -296,6 +361,13 @@ class host_del(LDAPDelete): msg_summary = _('Deleted host "%(value)s"') member_attributes = ['managedby'] + takes_options = LDAPCreate.takes_options + ( + Flag('updatedns?', + doc=_('Remove entries from DNS'), + default=False, + ), + ) + def pre_callback(self, ldap, dn, *keys, **options): # If we aren't given a fqdn, find it if validate_host(None, keys[-1]) is not None: @@ -318,6 +390,53 @@ class host_del(LDAPDelete): (service, hostname, realm) = split_principal(principal) if hostname.lower() == fqdn: api.Command['service_del'](principal) + updatedns = options.get('updatedns', False) + if updatedns: + try: + updatedns = dns_container_exists(ldap) + except errors.NotFound: + updatedns = False + + if updatedns: + # Remove DNS entries + parts = fqdn.split('.') + domain = unicode('.'.join(parts[1:])) + result = api.Command['dns_find']()['result'] + match = False + for zone in result: + if domain == zone['idnsname'][0]: + match = True + break + if not match: + raise errors.NotFound(reason=_('DNS zone %(zone)s not found' % dict(zone=domain))) + raise e + # Get all forward resources for this host + records = api.Command['dns_find_rr'](domain, parts[0])['result'] + for record in records: + if 'arecord' in record: + ipaddr = record['arecord'][0] + self.debug('deleting ipaddr %s' % ipaddr) + revzone, revname = get_reverse_zone(ipaddr) + try: + api.Command['dns_del_rr'](revzone, revname, u'PTR', fqdn+'.') + except errors.NotFound: + pass + try: + api.Command['dns_del_rr'](domain, parts[0], u'A', ipaddr) + except errors.NotFound: + pass + else: + # Try to delete all other record types too + for attr in _attribute_types: + if attr != 'arecord' and attr in record: + for i in xrange(len(record[attr])): + if (record[attr][i].endswith(parts[0]) or + record[attr][i].endswith(fqdn+'.')): + api.Command['dns_del_rr'](domain, + record['idnsname'][0], + _attribute_types[attr], record[attr][i]) + break + (dn, entry_attrs) = ldap.get_entry(dn, ['usercertificate']) if 'usercertificate' in entry_attrs: cert = normalize_certificate(entry_attrs.get('usercertificate')[0]) diff --git a/ipalib/util.py b/ipalib/util.py index 1803e65ab..0f84b7975 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -170,3 +170,18 @@ def isvalid_base64(data): return False else: return True + +def validate_ipaddr(ipaddr): + """ + Check to see if the given IP address is a valid IPv4 or IPv6 address. + + Returns True or False + """ + try: + socket.inet_pton(socket.AF_INET, ipaddr) + except socket.error: + try: + socket.inet_pton(socket.AF_INET6, ipaddr) + except socket.error: + return False + return True