From 2f50ad288be1d15d46b337f6bdd44df2b8ea025c Mon Sep 17 00:00:00 2001 From: Torsten Rehn Date: Sat, 22 Jan 2011 11:50:09 -0600 Subject: Ldap: New server ldap plugin Signed-off-by: Sol Jerome --- src/lib/Server/Plugins/Ldap.py | 229 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/lib/Server/Plugins/Ldap.py diff --git a/src/lib/Server/Plugins/Ldap.py b/src/lib/Server/Plugins/Ldap.py new file mode 100644 index 000000000..4f10d8ca6 --- /dev/null +++ b/src/lib/Server/Plugins/Ldap.py @@ -0,0 +1,229 @@ +import imp +import time +import ldap +import Bcfg2.Options +import Bcfg2.Server.Plugin + +# time in seconds between retries after failed LDAP connection +RETRY_DELAY = 5 +# how many times to try reaching the LDAP server if a connection is broken +# at the very minimum, one retry is needed to handle a restarted LDAP daemon +RETRY_COUNT = 3 + +SCOPE_MAP = { + "base" : ldap.SCOPE_BASE, + "one" : ldap.SCOPE_ONELEVEL, + "sub" : ldap.SCOPE_SUBTREE, +} + +LDAP_QUERIES = [] + +def register_query(query): + LDAP_QUERIES.append(query) + +class ConfigFile(Bcfg2.Server.Plugin.FileBacked): + """ + Config file for the Ldap plugin + + The config file cannot be 'parsed' in the traditional sense as we would + need some serious type checking ugliness to just get the LdapQuery + subclasses. The alternative would be to have the user create a list with + a predefined name that contains all queries. + The approach implemented here is having the user call a registering + decorator that updates a global variable in this module. + """ + def __init__(self, filename, fam): + self.filename = filename + Bcfg2.Server.Plugin.FileBacked.__init__(self, self.filename) + fam.AddMonitor(self.filename, self) + + def Index(self): + """ + Reregisters the queries in the config file + + The config will take care of actually registering the queries, + so we just load it once and don't keep it. + """ + global LDAP_QUERIES + LDAP_QUERIES = [] + imp.load_source("ldap_cfg", self.filename) + +class Ldap(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Connector): + """ + The Ldap plugin allows adding data from an LDAP server to your metadata. + """ + name = "Ldap" + version = "$Revision: $" + experimental = True + debug_flag = True + + def __init__(self, core, datastore): + Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) + Bcfg2.Server.Plugin.Connector.__init__(self) + self.config = ConfigFile(self.data + "/config.py", core.fam) + + def debug_log(self, message, flag = None): + if (flag is None) and self.debug_flag or flag: + self.logger.error(message) + + def get_additional_data(self, metadata): + try: + data = {} + self.debug_log("LdapPlugin debug: found queries " + + str(LDAP_QUERIES)) + for QueryClass in LDAP_QUERIES: + query = QueryClass() + if query.is_applicable(metadata): + self.debug_log("LdapPlugin debug: processing query '" + + query.name + "'") + data[query.name] = query.get_result(metadata) + else: + self.debug_log("LdapPlugin debug: query '" + query.name + + "' not applicable to host '" + metadata.hostname + "'") + return data + except Exception, error_msg: + if self.debug_flag: + raise + else: + Bcfg2.Server.Plugin.logger.error("LdapPlugin error: " + + str(error_msg)) + return {} + +class LdapConnection(object): + """ + Connection to an LDAP server. + """ + def __init__(self, host = "localhost", port = 389, + binddn = None, bindpw = None): + self.host = host + self.port = port + self.binddn = binddn + self.bindpw = bindpw + self.conn = None + + def __del__(self): + if self.conn: + self.conn.unbind() + + def init_conn(self): + self.conn = ldap.initialize(self.url) + if self.binddn is not None and self.bindpw is not None: + self.conn.simple_bind_s(self.binddn, self.bindpw) + + def run_query(self, query): + result = None + for attempt in range(RETRY_COUNT + 1): + if attempt >= 1: + Bcfg2.Server.Plugin.logger.error("LdapPlugin error: " + + "LDAP server down (retry " + str(attempt) + "/" + + str(RETRY_COUNT) + ")") + try: + if not self.conn: + self.init_conn() + result = self.conn.search_s( + query.base, + SCOPE_MAP[query.scope], + query.filter, + query.attrs, + ) + break + except ldap.SERVER_DOWN: + self.conn = None + time.sleep(RETRY_DELAY) + return result + + @property + def url(self): + return "ldap://" + self.host + ":" + str(self.port) + +class LdapQuery(object): + """ + Query referencing an LdapConnection and providing several + methods for query manipulation. + """ + + name = "unknown" + base = "" + scope = "sub" + filter = "(objectClass=*)" + attrs = None + connection = None + result = None + + def __unicode__(self): + return "LdapQuery:" + self.name + + def is_applicable(self, metadata): + """ + Overrideable method to determine if the query is to be executed for + the given metadata object. + Defaults to true. + """ + return True + + def prepare_query(self, metadata): + """ + Overrideable method to alter the query based on metadata. + Defaults to doing nothing. + + In most cases, you will do something like + + self.filter = "(cn=" + metadata.hostname + ")" + + here. + """ + pass + + def process_result(self, metadata): + """ + Overrideable method to post-process the query result. + Defaults to returning the unaltered result. + """ + return self.result + + def get_result(self, metadata): + """ + Method to handle preparing, executing and processing the query. + """ + if isinstance(self.connection, LdapConnection): + self.prepare_query(metadata) + self.result = self.connection.run_query(self) + self.result = self.process_result(metadata) + return self.result + else: + Bcfg2.Server.Plugin.logger.error("LdapPlugin error: " + + "No valid connection defined for query " + str(self)) + return None + +class LdapSubQuery(LdapQuery): + """ + SubQueries are meant for internal use only and are not added + to the metadata object. They are useful for situations where + you need to run more than one query to obtain some data. + """ + def prepare_query(self, metadata, **kwargs): + """ + Overrideable method to alter the query based on metadata. + Defaults to doing nothing. + """ + pass + + def process_result(self, metadata, **kwargs): + """ + Overrideable method to post-process the query result. + Defaults to returning the unaltered result. + """ + return self.result + + def get_result(self, metadata, **kwargs): + """ + Method to handle preparing, executing and processing the query. + """ + if isinstance(self.connection, LdapConnection): + self.prepare_query(metadata, **kwargs) + self.result = self.connection.run_query(self) + return self.process_result(metadata, **kwargs) + else: + Bcfg2.Server.Plugin.logger.error("LdapPlugin error: " + + "No valid connection defined for query " + str(self)) + return None -- cgit v1.2.3-1-g7c22