summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-01-18 23:32:05 +0000
committerGerrit Code Review <review@openstack.org>2012-01-18 23:32:05 +0000
commit2ff3e0eca25a28f248e9cdc8149869003b818651 (patch)
treea7a4b082fecf2313eabfd4ab1d4b141c0b150bd7
parentb2a5efcd050c98ef5c09db02fa06e75481d8e20e (diff)
parent709ee50e09f341037cfbfdcfcff7eb064c2ef2b5 (diff)
downloadnova-2ff3e0eca25a28f248e9cdc8149869003b818651.tar.gz
nova-2ff3e0eca25a28f248e9cdc8149869003b818651.tar.xz
nova-2ff3e0eca25a28f248e9cdc8149869003b818651.zip
Merge "Added an LDAP/PowerDNS driver"
-rw-r--r--nova/network/ldapdns.py352
-rw-r--r--nova/tests/test_network.py76
2 files changed, 428 insertions, 0 deletions
diff --git a/nova/network/ldapdns.py b/nova/network/ldapdns.py
new file mode 100644
index 000000000..a7031bc12
--- /dev/null
+++ b/nova/network/ldapdns.py
@@ -0,0 +1,352 @@
+# Copyright 2012 Andrew Bogott for the Wikimedia Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import ldap
+import os
+import shutil
+import tempfile
+import time
+
+from nova.auth import fakeldap
+from nova import exception
+from nova import flags
+from nova import log as logging
+
+
+LOG = logging.getLogger("nova.network.manager")
+
+flags.DEFINE_string('ldap_dns_url',
+ 'ldap://ldap.example.com:389',
+ 'URL for ldap server which will store dns entries')
+flags.DEFINE_string('ldap_dns_user',
+ 'uid=admin,ou=people,dc=example,dc=org',
+ 'user for ldap DNS')
+flags.DEFINE_string('ldap_dns_password',
+ 'password',
+ 'password for ldap DNS')
+flags.DEFINE_string('ldap_dns_soa_hostmaster',
+ 'hostmaster@example.org',
+ 'Hostmaster for ldap dns driver Statement of Authority')
+flags.DEFINE_multistring('ldap_dns_servers',
+ '[dns.example.org]',
+ 'DNS Servers for ldap dns driver')
+flags.DEFINE_string('ldap_dns_base_dn',
+ 'ou=hosts,dc=example,dc=org',
+ 'Base DN for DNS entries in ldap')
+flags.DEFINE_string('ldap_dns_soa_refresh',
+ '1800',
+ 'Refresh interval (in seconds) for ldap dns driver '
+ 'Statement of Authority')
+flags.DEFINE_string('ldap_dns_soa_retry',
+ '3600',
+ 'Retry interval (in seconds) for ldap dns driver '
+ 'Statement of Authority')
+flags.DEFINE_string('ldap_dns_soa_expiry',
+ '86400',
+ 'Expiry interval (in seconds) for ldap dns driver '
+ 'Statement of Authority')
+flags.DEFINE_string('ldap_dns_soa_minimum',
+ '7200',
+ 'Minimum interval (in seconds) for ldap dns driver '
+ 'Statement of Authority')
+
+
+# Importing ldap.modlist breaks the tests for some reason,
+# so this is an abbreviated version of a function from
+# there.
+def create_modlist(newattrs):
+ modlist = []
+ for attrtype in newattrs.keys():
+ modlist.append((attrtype, newattrs[attrtype]))
+ return modlist
+
+
+class DNSEntry(object):
+
+ def __init__(self, ldap_object):
+ """ldap_object is an instance of ldap.LDAPObject.
+ It should already be initialized and bound before
+ getting passed in here."""
+ self.lobj = ldap_object
+ self.ldap_tuple = None
+ self.qualified_domain = None
+
+ @classmethod
+ def _get_tuple_for_domain(cls, lobj, domain):
+ entry = lobj.search_s(flags.FLAGS.ldap_dns_base_dn, ldap.SCOPE_SUBTREE,
+ "(associatedDomain=%s)" % domain)
+ if not entry:
+ return None
+ if len(entry) > 1:
+ LOG.warn("Found multiple matches for domain %s.\n%s" %
+ (domain, entry))
+ return entry[0]
+
+ def _set_tuple(self, tuple):
+ self.ldap_tuple = tuple
+
+ def _qualify(self, name):
+ return "%s.%s" % (name, self.qualified_domain)
+
+ def _dequalify(self, name):
+ z = ".%s" % self.qualified_domain
+ if name.endswith(z):
+ dequalified = name[0:name.rfind(z)]
+ else:
+ LOG.warn("Unable to dequalify. %s is not in %s.\n" % (name, zone))
+ dequalified = None
+
+ return dequalified
+
+ def _dn(self):
+ return self.ldap_tuple[0]
+ dn = property(_dn)
+
+ def _rdn(self):
+ return self.dn.partition(',')[0]
+ rdn = property(_rdn)
+
+
+class DomainEntry(DNSEntry):
+
+ @classmethod
+ def _soa(cls):
+ date = time.strftime("%Y%m%d%H%M%S")
+ soa = "%s %s %s %s %s %s %s" % (
+ flags.FLAGS.ldap_dns_servers[0],
+ flags.FLAGS.ldap_dns_soa_hostmaster,
+ date,
+ flags.FLAGS.ldap_dns_soa_refresh,
+ flags.FLAGS.ldap_dns_soa_retry,
+ flags.FLAGS.ldap_dns_soa_expiry,
+ flags.FLAGS.ldap_dns_soa_minimum)
+ return soa
+
+ @classmethod
+ def create_domain(cls, lobj, fqdomain):
+ """Create a new domain entry, and return an object that wraps it."""
+ entry = cls._get_tuple_for_domain(lobj, fqdomain)
+ if entry:
+ raise exception.FloatingIpDNSExists(name=fqdomain, zone="")
+
+ newdn = "dc=%s,%s" % (fqdomain, flags.FLAGS.ldap_dns_base_dn)
+ attrs = {'objectClass': ['domainrelatedobject', 'dnsdomain',
+ 'domain', 'dcobject', 'top'],
+ 'sOARecord': [cls._soa()],
+ 'associatedDomain': [fqdomain],
+ 'dc': fqdomain}
+ lobj.add_s(newdn, create_modlist(attrs))
+ return DomainEntry(lobj, fqdomain)
+
+ def __init__(self, ldap_object, domain):
+ super(DomainEntry, self).__init__(ldap_object)
+ entry = self._get_tuple_for_domain(self.lobj, domain)
+ if not entry:
+ raise exception.NotFound()
+ self._set_tuple(entry)
+ assert(entry[1]['associatedDomain'][0] == domain)
+ self.qualified_domain = domain
+
+ def delete(self):
+ """Delete the domain that this entry refers to."""
+ entries = self.lobj.search_s(self.dn,
+ ldap.SCOPE_SUBTREE,
+ '(aRecord=*)')
+ for entry in entries:
+ self.lobj.delete_s(entry[0])
+
+ self.lobj.delete_s(self.dn)
+
+ def update_soa(self):
+ mlist = [(ldap.MOD_REPLACE, 'sOARecord', self._soa())]
+ self.lobj.modify_s(self.dn, mlist)
+
+ def subentry_with_name(self, name):
+ entry = self.lobj.search_s(self.dn, ldap.SCOPE_SUBTREE,
+ "(associatedDomain=%s.%s)" %
+ (name, self.qualified_domain))
+ if entry:
+ return HostEntry(self, entry[0])
+ else:
+ return None
+
+ def subentries_with_ip(self, ip):
+ entries = self.lobj.search_s(self.dn, ldap.SCOPE_SUBTREE,
+ "(aRecord=%s)" % ip)
+ objs = []
+ for entry in entries:
+ if 'associatedDomain' in entry[1]:
+ objs.append(HostEntry(self, entry))
+
+ return objs
+
+ def add_entry(self, name, address):
+ if self.subentry_with_name(name):
+ raise exception.FloatingIpDNSExists(name=name,
+ zone=self.qualified_domain)
+
+ entries = self.subentries_with_ip(address)
+ if entries:
+ # We already have an ldap entry for this IP, so we just
+ # need to add the new name.
+ existingdn = entries[0].dn
+ self.lobj.modify_s(existingdn, [(ldap.MOD_ADD,
+ 'associatedDomain',
+ self._qualify(name))])
+ return self.subentry_with_name(name)
+ else:
+ # We need to create an entirely new entry.
+ newdn = "dc=%s,%s" % (name, self.dn)
+ attrs = {'objectClass': ['domainrelatedobject', 'dnsdomain',
+ 'domain', 'dcobject', 'top'],
+ 'aRecord': [address],
+ 'associatedDomain': [self._qualify(name)],
+ 'dc': name}
+ self.lobj.add_s(newdn, create_modlist(attrs))
+ return self.subentry_with_name(name)
+ self.update_soa()
+
+ def remove_entry(self, name):
+ entry = self.subentry_with_name(name)
+ if not entry:
+ raise exception.NotFound()
+ entry.remove_name(name)
+ self.update_soa()
+
+
+class HostEntry(DNSEntry):
+
+ def __init__(self, parent, tuple):
+ super(HostEntry, self).__init__(parent.lobj)
+ self.parent_entry = parent
+ self._set_tuple(tuple)
+ self.qualified_domain = parent.qualified_domain
+
+ def remove_name(self, name):
+ names = self.ldap_tuple[1]['associatedDomain']
+ if not names:
+ raise exception.NotFound()
+ if len(names) > 1:
+ # We just have to remove the requested domain.
+ self.lobj.modify_s(self.dn, [(ldap.MOD_DELETE, 'associatedDomain',
+ self._qualify(name))])
+ if (self.rdn[1] == name):
+ # We just removed the rdn, so we need to move this entry.
+ names.remove(self._qualify(name))
+ newrdn = "dc=%s" % self._dequalify(names[0])
+ self.lobj.modrdn_s(self.dn, newrdn)
+ else:
+ # We should delete the entire record.
+ self.lobj.delete_s(self.dn)
+
+ def modify_address(self, name, address):
+ names = self.ldap_tuple[1]['associatedDomain']
+ if not names:
+ raise exception.NotFound()
+ if len(names) == 1:
+ self.lobj.modify_s(self.dn, [(ldap.MOD_REPLACE, 'aRecord',
+ [address])])
+ else:
+ self.remove_name(name)
+ parent.add_entry(name, address)
+
+ def _names(self):
+ names = []
+ for domain in self.ldap_tuple[1]['associatedDomain']:
+ names.append(self._dequalify(domain))
+ return names
+ names = property(_names)
+
+ def _ip(self):
+ ip = self.ldap_tuple[1]['aRecord'][0]
+ return ip
+ ip = property(_ip)
+
+ def _parent(self):
+ return self.parent_entry
+ parent = property(_parent)
+
+
+class LdapDNS(object):
+ """Driver for PowerDNS using ldap as a back end.
+
+ This driver assumes ldap-method=strict, with all domains
+ in the top-level, aRecords only."""
+
+ def __init__(self):
+ self.lobj = ldap.initialize(flags.FLAGS.ldap_dns_url)
+ self.lobj.simple_bind_s(flags.FLAGS.ldap_dns_user,
+ flags.FLAGS.ldap_dns_password)
+
+ def get_zones(self):
+ return flags.FLAGS.floating_ip_dns_zones
+
+ def create_entry(self, name, address, type, dnszone):
+ if type.lower() != 'a':
+ raise exception.InvalidInput(_("This driver only supports "
+ "type 'a' entries."))
+
+ dEntry = DomainEntry(self.lobj, dnszone)
+ dEntry.add_entry(name, address)
+
+ def delete_entry(self, name, dnszone):
+ dEntry = DomainEntry(self.lobj, dnszone)
+ dEntry.remove_entry(name)
+
+ def get_entries_by_address(self, address, dnszone):
+ try:
+ dEntry = DomainEntry(self.lobj, dnszone)
+ except exception.NotFound:
+ return []
+ entries = dEntry.subentries_with_ip(address)
+ names = []
+ for entry in entries:
+ names.extend(entry.names)
+ return names
+
+ def get_entries_by_name(self, name, dnszone):
+ try:
+ dEntry = DomainEntry(self.lobj, dnszone)
+ except exception.NotFound:
+ return []
+ nEntry = dEntry.subentry_with_name(name)
+ if nEntry:
+ return [nEntry.ip]
+
+ def modify_address(self, name, address, dnszone):
+ dEntry = DomainEntry(self.lobj, dnszone)
+ nEntry = dEntry.subentry_with_name(name)
+ nEntry.modify_address(name, address)
+
+ def create_domain(self, fqdomain):
+ DomainEntry.create_domain(self.lobj, fqdomain)
+
+ def delete_domain(self, fqdomain):
+ dEntry = DomainEntry(self.lobj, fqdomain)
+ dEntry.delete()
+
+ def delete_dns_file(self):
+ LOG.warn("This shouldn't be getting called except during testing.")
+ pass
+
+
+class FakeLdapDNS(LdapDNS):
+ """For testing purposes, a DNS driver backed with a fake ldap driver."""
+ def __init__(self):
+ self.lobj = fakeldap.FakeLDAP()
+ attrs = {'objectClass': ['domainrelatedobject', 'dnsdomain',
+ 'domain', 'dcobject', 'top'],
+ 'associateddomain': ['root'],
+ 'dc': 'root'}
+ self.lobj.add_s(flags.FLAGS.ldap_dns_base_dn, create_modlist(attrs))
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 351876a41..aa8a869aa 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -1478,3 +1478,79 @@ class InstanceDNSTestCase(test.TestCase):
self.network.delete_dns_domain, self.context,
domain1)
self.network.delete_dns_domain(context_admin, domain1)
+
+
+zone1 = "example.org"
+zone2 = "example.com"
+
+
+class LdapDNSTestCase(test.TestCase):
+ """Tests nova.network.ldapdns.LdapDNS"""
+ def setUp(self):
+ super(LdapDNSTestCase, self).setUp()
+ temp = utils.import_object('nova.network.ldapdns.FakeLdapDNS')
+ self.driver = temp
+ self.driver.create_domain(zone1)
+ self.driver.create_domain(zone2)
+
+ def tearDown(self):
+ super(LdapDNSTestCase, self).tearDown()
+ self.driver.delete_domain(zone1)
+ self.driver.delete_domain(zone2)
+
+ def test_ldap_dns_zones(self):
+ flags.FLAGS.floating_ip_dns_zones = [zone1, zone2]
+
+ zones = self.driver.get_zones()
+ self.assertEqual(len(zones), 2)
+ self.assertEqual(zones[0], zone1)
+ self.assertEqual(zones[1], zone2)
+
+ def test_ldap_dns_create_conflict(self):
+ address1 = "10.10.10.11"
+ name1 = "foo"
+ name2 = "bar"
+
+ self.driver.create_entry(name1, address1, "A", zone1)
+
+ self.assertRaises(exception.FloatingIpDNSExists,
+ self.driver.create_entry,
+ name1, address1, "A", zone1)
+
+ def test_ldap_dns_create_and_get(self):
+ address1 = "10.10.10.11"
+ name1 = "foo"
+ name2 = "bar"
+ entries = self.driver.get_entries_by_address(address1, zone1)
+ self.assertFalse(entries)
+
+ self.driver.create_entry(name1, address1, "A", zone1)
+ self.driver.create_entry(name2, address1, "A", zone1)
+ entries = self.driver.get_entries_by_address(address1, zone1)
+ self.assertEquals(len(entries), 2)
+ self.assertEquals(entries[0], name1)
+ self.assertEquals(entries[1], name2)
+
+ entries = self.driver.get_entries_by_name(name1, zone1)
+ self.assertEquals(len(entries), 1)
+ self.assertEquals(entries[0], address1)
+
+ def test_ldap_dns_delete(self):
+ address1 = "10.10.10.11"
+ name1 = "foo"
+ name2 = "bar"
+
+ self.driver.create_entry(name1, address1, "A", zone1)
+ self.driver.create_entry(name2, address1, "A", zone1)
+ entries = self.driver.get_entries_by_address(address1, zone1)
+ self.assertEquals(len(entries), 2)
+
+ self.driver.delete_entry(name1, zone1)
+ entries = self.driver.get_entries_by_address(address1, zone1)
+ LOG.debug("entries: %s" % entries)
+ self.assertEquals(len(entries), 1)
+ self.assertEquals(entries[0], name2)
+
+ self.assertRaises(exception.NotFound,
+ self.driver.delete_entry,
+ name1, zone1)