Rob Crittenden wrote:
Simo Sorce wrote:
On Wed, 20 Oct 2010 10:26:08 -0400
Rob Crittenden<rcrit...@redhat.com> wrote:

Add ability to add/remove DNS records when adding/removing a host
entry.

A host in DNS must have an IP address so a valid IP address is
required when adding a host. The --force flag will be needed too
since you are adding a host that isn't in DNS.

For IPv4 it will create an A and a PTR DNS record.

IPv6 isn't quite supported yet. Some basic work in the DNS installer
is needed to get this working. Once the get_reverse_zone() returns
the right value then this should start working and create an AAAA
record and the appropriate reverse entry.

When deleting a host with the --updatedns flag it will try to remove
all records it can find in the zone for this host.

ticket 238

rob

NACK, this patch introduces a bug when trying to add the same host
multiple time with different ip address.
The second time the ipa host-ad will correctly return an error that the
host already exist yet the A record with the new address is added in
DNS. Adding records to the DNS should happen only after the host has
been successfully created.

Simo.


Ok, moved the dns_add into the post operation. It still does some amount
of validation in the preop.

I added a failsafe so that if the host add is successful but the dns add
fails it raises an error to that effect, it doesn't roll back all the
changes.

rob


Re-based patch.

rob
>From 9523f9fbdab84abef2566a35bcd6562f7908598f Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Tue, 23 Nov 2010 17:47:29 -0500
Subject: [PATCH] Add ability to add/remove DNS records when adding/removing a host entry.

A host in DNS must have an IP address so a valid IP address is required
when adding a host. The --force flag will be needed too since you are
adding a host that isn't in DNS.

For IPv4 it will create an A and a PTR DNS record.

IPv6 isn't quite supported yet. Some basic work in the DNS installer
is needed to get this working. Once the get_reverse_zone() returns the
right value then this should start working and create an AAAA record and
the appropriate reverse entry.

When deleting a host with the --updatedns flag it will try to remove all
records it can find in the zone for this host.

ticket 238
---
 ipalib/errors.py       |   17 +++++++
 ipalib/plugins/dns.py  |   13 +++++
 ipalib/plugins/host.py |  121 +++++++++++++++++++++++++++++++++++++++++++++++-
 ipalib/util.py         |   15 ++++++
 4 files changed, 165 insertions(+), 1 deletions(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index 22138ab..86cd60d 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 a3e6c1e..6f3959b 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 2e77dd5..9d3a2a9 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 1803e65..0f84b79 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
-- 
1.7.2.1

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to