summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/Ldap.py
blob: 553ddbc47a307cd90ff74000c32a64da209c51f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import imp
import logging
import sys
import time
import traceback
import Bcfg2.Server.Plugin

logger = logging.getLogger('Bcfg2.Plugins.Ldap')

try:
    import ldap
except ImportError:
    logger.error("Unable to load ldap module. Is python-ldap installed?")
    raise ImportError

# 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):
        self.filename = filename
        Bcfg2.Server.Plugin.FileBacked.__init__(self, self.filename)
        self.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"
    experimental = True
    debug_flag = False

    def __init__(self, core):
        Bcfg2.Server.Plugin.Plugin.__init__(self, core)
        Bcfg2.Server.Plugin.Connector.__init__(self)
        self.config = ConfigFile(self.data + "/config.py")

    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):
        query = None
        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:
            if hasattr(query, "name"):
                logger.error("LdapPlugin error: " +
                             "Exception during processing of query named '" +
                             str(query.name) +
                             "', query results will be empty" +
                             " and may cause bind failures")
            for line in traceback.format_exception(sys.exc_info()[0],
                                                   sys.exc_info()[1],
                                                   sys.exc_info()[2]):
                logger.error("LdapPlugin error: " +
                             line.replace("\n", ""))
            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:
                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.replace("\\", "\\\\"),
                    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:
            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:
            logger.error("LdapPlugin error: " +
                         "No valid connection defined for query " + str(self))
            return None