1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import datetime
18 import functools
19 import inspect
20 import logging
21 import sys
22 import traceback
23 from pprint import pformat
24
25 import pymongo
26 from pymongo.bson import BSON
27 from pymongo.son import SON
28
29 from pulp.server.auth import auth
30 from pulp.server.api.base import BaseApi
31 from pulp.server import config
32 from pulp.server.crontab import CronTab
33 from pulp.server.db.connection import get_object_db
34 from pulp.server.db.model import Event
35
36
37
38 _objdb = get_object_db('events', ['id'], ['timestamp', 'principal', 'api'])
39
40
41 _log = logging.getLogger('auditing')
46 """
47 Class for method inspection.
48 """
50 """
51 @type method: unbound class instance method
52 @param method: method to build spec of
53 @type params: list of str's or None
54 @param params: ordered list of method parameters of interest,
55 None means all parameters are of interest
56 """
57 assert params is None or 'self' not in params
58
59 self.method = method.__name__
60
61
62 spec = inspect.getargspec(method)
63
64 args = list(spec[0])
65
66 if 'self' in args:
67 args.remove('self')
68
69 self.__param_to_index = dict((a, i + 1) for i, a in
70 enumerate(args)
71 if params is None or a in params)
72
73 self.params = params if params is not None else list(args)
74
75 defaults = spec[3]
76 if defaults:
77 self.__param_defaults = dict((a, d) for a, d in
78 zip(args[0 - len(defaults):], defaults)
79 if params is None or a in params)
80 else:
81 self.__param_defaults = {}
82
84 """
85 Return the api class name for the given instance method positional
86 arguments.
87 @type args: list
88 @param args: positional arguments of an api instance method
89 @return: name of api class
90 """
91 assert args
92 api_obj = args[0]
93 if not isinstance(api_obj, BaseApi):
94 return 'Unknown API'
95 return api_obj.__class__.__name__
96
98 """
99 Return an audit-friendly representation of a value.
100 @type value: any
101 @param value: parameter value
102 @return: string representing the value
103 """
104 if isinstance(value, basestring):
105 return value
106 if not isinstance(value, (dict, BSON, SON)):
107 return repr(value)
108 if 'id' in value:
109 return '<id: %s>' % value['id']
110 elif '_id' in value:
111 return '<_id: %s>' % str(value['_id'])
112 return '%s instance' % str(type(value))
113
115 """
116 Grep through passed in arguments and keyword arguments and return a list
117 of values corresponding to the parameters of interest.
118 @type args: list or tuple
119 @param args: positional arguments
120 @type kwargs: dict
121 @param kwargs: keyword arguments
122 @return: list of (paramter, value) for parameters of interest
123 """
124 values = []
125 for p in self.params:
126 i = self.__param_to_index[p]
127 if i < len(args):
128 values.append(self.audit_repr(args[i]))
129 else:
130 value = kwargs.get(p, self.__param_defaults.get(p, 'Unknown Parameter'))
131 values.append(self.audit_repr(value))
132 return zip(self.params, values)
133
134
135 -def audit(params=None, record_result=False):
136 """
137 API class instance method decorator meant to log calls that constitute
138 events on pulp's model instances.
139 Any call to a decorated method will both record the event in the database
140 and log it to a special log file.
141
142 A decorated method may have an optional keyword argument, 'principal',
143 passed in that represents the user or other entity making the call.
144 This optional keyword argument is not passed to the underlying method,
145 unless the pass_principal flag is True.
146
147 @type params: list or tuple of str's or None
148 @param params: list of names of parameters to record the values of,
149 None records all parameters
150 @type record_result: bool
151 @param record_result: whether or not to record the result
152 """
153 def _audit_decorator(method):
154
155 inspector = MethodInspector(method, params)
156
157 @functools.wraps(method)
158 def _audit(*args, **kwargs):
159
160
161 def _record_event():
162 _objdb.insert(event, safe=False, check_keys=False)
163 _log.info('[%s] %s called %s.%s on %s' %
164 (event.timestamp,
165 unicode(principal),
166 api,
167 inspector.method,
168 param_values_str))
169
170
171 principal = auth.get_principal()
172 api = inspector.api_name(args)
173 param_values = inspector.param_values(args, kwargs)
174 param_values_str = ', '.join('%s: %s' % (p, v) for p, v in param_values)
175 action = '%s.%s: %s' % (api,
176 inspector.method,
177 param_values_str)
178 event = Event(principal,
179 action,
180 api,
181 inspector.method,
182 param_values)
183
184
185 try:
186 result = method(*args, **kwargs)
187 except Exception, e:
188 event.exception = pformat(e)
189 exc = sys.exc_info()
190 event.traceback = ''.join(traceback.format_exception(*exc))
191 _record_event()
192 raise
193 else:
194 event.result = inspector.audit_repr(result) if record_result else None
195 _record_event()
196 return result
197
198 return _audit
199
200 return _audit_decorator
201
202
203
204 -def events(spec=None, fields=None, limit=None, errors_only=False):
205 """
206 Query function that returns events according to the pymongo spec.
207 The results are sorted by timestamp into descending order.
208 @type spec: dict or pymongo.son.SON instance
209 @param spec: pymongo spec for filtering events
210 @type fields: list or tuple of str
211 @param fields: iterable of fields to include from each document
212 @type limit: int or None
213 @param limit: limit the number of results, None means no limit
214 @type errors_only: bool
215 @param errors_only: if True, only return events that match the spec and have
216 an exception associated with them, otherwise return all
217 events that match spec
218 @return: list of events containing fields and matching spec
219 """
220 assert isinstance(spec, (dict, BSON, SON)) or spec is None
221 assert isinstance(fields, (list, tuple)) or fields is None
222 if errors_only:
223 spec = spec or {}
224 spec['exception'] = {'$ne': None}
225 events_ = _objdb.find(spec=spec, fields=fields)
226 if limit is not None:
227 events_.limit(limit)
228 events_.sort('timestamp', pymongo.DESCENDING)
229 return list(events_)
230
231
232 -def events_on_api(api, fields=None, limit=None, errors_only=False):
233 """
234 Return all recorded events for a given api.
235 @type api: str
236 @param api: name of the api
237 @type fields: list or tuple of str
238 @param fields: iterable of fields to include from each document
239 @type limit: int or None
240 @param limit: limit the number of results, None means no limit
241 @type errors_only: bool
242 @param errors_only: if True, only return events that match the spec and have
243 an exception associated with them, otherwise return all
244 events that match spec
245 @return: list of events for the given api containing fields
246 """
247 return events({'api': api}, fields, limit, errors_only)
248
251 """
252 Return all recorded events for a given principal (caller).
253 @type principal: model object or dict
254 @param principal: principal that triggered the event (i.e. User instance)
255 @type fields: list or tuple of str
256 @param fields: iterable of fields to include from each document
257 @type limit: int or None
258 @param limit: limit the number of results, None means no limit
259 @type errors_only: bool
260 @param errors_only: if True, only return events that match the spec and have
261 an exception associated with them, otherwise return all
262 events that match spec
263 @return: list of events for the given principal containing fields
264 """
265 return events({'principal': unicode(principal)}, fields, limit, errors_only)
266
270 """
271 Return all events in a given time range.
272 @type lower_bound: datetime.datetime instance or None
273 @param lower_bound: lower time bound, None = oldest in db
274 @type fields: list or tuple of str
275 @param fields: iterable of fields to include from each document
276 @type limit: int or None
277 @param limit: limit the number of results, None means no limit
278 @type errors_only: bool
279 @param errors_only: if True, only return events that match the spec and have
280 an exception associated with them, otherwise return all
281 events that match spec
282 @return: list of events in the given time range containing fields
283 """
284 assert isinstance(lower_bound, datetime.datetime) or lower_bound is None
285 assert isinstance(upper_bound, datetime.datetime) or upper_bound is None
286 timestamp_range = {}
287 if lower_bound is not None:
288 timestamp_range['$gt'] = lower_bound
289 if upper_bound is not None:
290 timestamp_range['$lt'] = upper_bound
291 spec = {'timestamp': timestamp_range} if timestamp_range else None
292 return events(spec, fields, limit, errors_only)
293
295 """
296 Return all the events that occurred in the last time delta from now.
297 @type delta: datetime.timedelta instance
298 @param delta: length of time frame to return events from
299 @type fields: list or tuple of str
300 @param fields: iterable of fields to include from each document
301 @type limit: int or None
302 @param limit: limit the number of results, None means no limit
303 @type errors_only: bool
304 @param errors_only: if True, only return events that match the spec and have
305 an exception associated with them, otherwise return all
306 events that match spec
307 @return: list of events in the given length of time containing fields
308 """
309 assert isinstance(delta, datetime.timedelta)
310 now = datetime.datetime.now()
311 lower_bound = now - delta
312 return events({'timestamp': {'$gt': lower_bound}}, fields, limit, errors_only)
313
316 """
317 Reaper function that removes all events older than the passed in time delta
318 from the database.
319 @type delta: dateteime.timedelta instance or None
320 @param delta: length of time from current time to keep events,
321 None means don't keep any events
322 @return: the number of events removed from the database
323 """
324 spec = None
325 if delta is not None:
326 now = datetime.datetime.now()
327 spec = {'timestamp': {'$lt': now - delta}}
328 count = _objdb.find(spec).count()
329 _objdb.remove(spec, safe=False)
330 return count
331
338 """
339 Check to see that the cull auditing events crontab entry exists, and add it
340 if it doesn't.
341 """
342 tab = CronTab()
343 cmd = 'python %s' % __file__
344 if tab.find_command(cmd):
345 return
346 schedule = '0,30 * * * *'
347 entry = tab.new(cmd, 'cull auditing events')
348 entry.parse('%s %s' % (schedule, cmd))
349 tab.write()
350 _log.info('Added crontab entry for culling events')
351
354 """
355 Check to see that the cull auditing events crontab entry exists, and remove
356 it if it does.
357 """
358 tab = CronTab()
359 cmd = 'python %s' % __file__
360 if not tab.find_command(cmd):
361 return
362 tab.remove_all(cmd)
363 tab.write()
364
367 """
368 Get the configured auditing lifeteime as a datetime.timedelta instance.
369 @return: dateteime.timedelta instance
370 """
371 days = config.config.getint('auditing', 'lifetime')
372 return datetime.timedelta(days=days)
373
374
375
376 _check_crontab()
377
378 if __name__ == '__main__':
379 lifetime = _get_lifetime()
380 cull_events(lifetime)
381