diff options
| author | Jenkins <jenkins@review.openstack.org> | 2012-01-18 23:32:05 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2012-01-18 23:32:05 +0000 |
| commit | 2ff3e0eca25a28f248e9cdc8149869003b818651 (patch) | |
| tree | a7a4b082fecf2313eabfd4ab1d4b141c0b150bd7 | |
| parent | b2a5efcd050c98ef5c09db02fa06e75481d8e20e (diff) | |
| parent | 709ee50e09f341037cfbfdcfcff7eb064c2ef2b5 (diff) | |
| download | nova-2ff3e0eca25a28f248e9cdc8149869003b818651.tar.gz nova-2ff3e0eca25a28f248e9cdc8149869003b818651.tar.xz nova-2ff3e0eca25a28f248e9cdc8149869003b818651.zip | |
Merge "Added an LDAP/PowerDNS driver"
| -rw-r--r-- | nova/network/ldapdns.py | 352 | ||||
| -rw-r--r-- | nova/tests/test_network.py | 76 |
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) |
