summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/errors.py17
-rw-r--r--ipalib/plugins/dns.py13
-rw-r--r--ipalib/plugins/host.py121
-rw-r--r--ipalib/util.py15
4 files changed, 165 insertions, 1 deletions
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