Package pulp :: Package server :: Module auditing
[hide private]
[frames] | no frames]

Source Code for Module pulp.server.auditing

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  # 
  4  # Copyright © 2010 Red Hat, Inc. 
  5  # 
  6  # This software is licensed to you under the GNU General Public License, 
  7  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  8  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  9  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
 10  # along with this software; if not, see 
 11  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 12  # 
 13  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 14  # granted to use or replicate Red Hat trademarks that are incorporated 
 15  # in this software or its documentation. 
 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  # globals --------------------------------------------------------------------- 
 37   
 38  _objdb = get_object_db('events', ['id'], ['timestamp', 'principal', 'api']) 
 39   
 40  # setup log - do not change this to __name__ 
 41  _log = logging.getLogger('auditing') 
42 43 # auditing decorator ---------------------------------------------------------- 44 45 -class MethodInspector(object):
46 """ 47 Class for method inspection. 48 """
49 - def __init__(self, method, params):
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 # returns a tuple: (args, varargs, keywords, defaults) 62 spec = inspect.getargspec(method) 63 64 args = list(spec[0]) 65 # for some reason, self is sometimes in the args, and sometimes not 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
83 - def api_name(self, args):
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
97 - def audit_repr(self, value):
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
114 - def param_values(self, args, kwargs):
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 # convenience function for recording events 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 # build up the data to record 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 # execute the wrapped method and record the results 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 # auditing api ---------------------------------------------------------------- 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
249 250 -def events_by_principal(principal, fields=None, limit=None, errors_only=False):
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
267 268 -def events_in_datetime_range(lower_bound=None, upper_bound=None, 269 fields=None, limit=None, errors_only=False):
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
294 -def events_since_delta(delta, fields=None, limit=None, errors_only=False):
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
314 315 -def cull_events(delta):
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
332 # main ------------------------------------------------------------------------ 333 334 # this module is also the crontab entry script for culling entries from the 335 # database 336 337 -def _check_crontab():
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
352 353 -def _clear_crontab():
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
365 366 -def _get_lifetime():
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 # check to see that a crontab entry exists on import or execution 376 _check_crontab() 377 # cull old auditing events from the database 378 if __name__ == '__main__': 379 lifetime = _get_lifetime() 380 cull_events(lifetime) 381