Package Products :: Package ZenWin :: Module zeneventlog |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenWin.zeneventlog

      1  #! /usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  # ##########################################################################
      4  #
      5  # This program is part of Zenoss Core, an open source monitoring platform.
      6  # Copyright (C) 2006-2009 Zenoss Inc.
      7  #
      8  # This program is free software; you can redistribute it and/or modify it
      9  # under the terms of the GNU General Public License version 2 or (at your
     10  # option) any later version as published by the Free Software Foundation.
     11  #
     12  # For complete information please visit: http://www.zenoss.com/oss/
     13  #
     14  # ##########################################################################
     15
     16  """
     17  This module provides a collector daemon that polls Windows devices for changes
     18  to the Windows Event Log. Retrieved events are then converted into Zenoss events
     19  and sent back to ZenHub for further processing.
     20  """
     21
     22  import logging
     23
     24  # IMPORTANT! The import of the pysamba.twisted.reactor module should come before
     25  # any other libraries that might possibly use twisted. This will ensure that
     26  # the proper WmiReactor is installed before anyone else grabs a reference to
     27  # the wrong reactor.
     28  import pysamba.twisted.reactor
     29
     30  import Globals
     31  import zope.component
     32  import zope.interface
     33
     34  from twisted.internet import defer, reactor
     35  from twisted.python.failure import Failure
     36
     37  from Products.ZenCollector.daemon import CollectorDaemon
     38  from Products.ZenCollector.interfaces import ICollectorPreferences,\
     39                                               IEventService,\
     40                                               IScheduledTask,\
     41                                               IStatisticsService
     42  from Products.ZenCollector.tasks import SimpleTaskFactory,\
     43                                          SimpleTaskSplitter,\
     44                                          TaskStates
     45  from Products.ZenEvents.ZenEventClasses import Clear, Error, Warning, Info, \
     46      Debug, Status_Wmi
     47  from Products.ZenUtils.observable import ObservableMixin
     48  from Products.ZenWin.Watcher import Watcher
     49  from Products.ZenWin.utils import addNTLMv2Option, setNTLMv2Auth
     50
     51  # We retrieve our configuration data remotely via a Twisted PerspectiveBroker
     52  # connection. To do so, we need to import the class that will be used by the
     53  # configuration service to send the data over, i.e. DeviceProxy.
     54  from Products.ZenUtils.Utils import unused
     55  from Products.ZenCollector.services.config import DeviceProxy
     56  unused(DeviceProxy)
     57
     58  #
     59  # creating a logging context for this module to use
     60  #
     61  log = logging.getLogger("zen.zeneventlog")
     62
     63
     64  # Create an implementation of the ICollectorPreferences interface so that the
     65  # ZenCollector framework can configure itself from our preferences.
     66 -class ZenEventLogPreferences(object):
     67      zope.interface.implements(ICollectorPreferences)
     68
     69 -    def __init__(self):
     70          """
     71          Constructs a new ZenEventLogPreferences instance and provide default
     72          values for needed attributes.
     73          """
     74          self.collectorName = "zeneventlog"
     75          self.defaultRRDCreateCommand = None
     76          self.cycleInterval = 5 * 60 # seconds
     77          self.configCycleInterval = 20 # minutes
     78          self.options = None
     79
     80          # the configurationService attribute is the fully qualified class-name
     81          # of our configuration service that runs within ZenHub
     82          self.configurationService = 'Products.ZenWin.services.EventLogConfig'
     83
     84          self.wmibatchSize = 10
     85          self.wmiqueryTimeout = 1000
     86
     87 -    def buildOptions(self, parser):
     88          parser.add_option('--batchSize', dest='batchSize',
     89                            default=None, type='int',
     90                            help='Number of data objects to retrieve in a ' +
     91                                 'single WMI query.')
     92
     93          parser.add_option('--queryTimeout', dest='queryTimeout',
     94                            default=None, type='int',
     95                            help='The number of milliseconds to wait for ' + \
     96                                 'WMI query to respond. Overrides the ' + \
     97                                 'server settings.')
     98
     99          parser.add_option('--testClause', dest='testClause',
    100                            default=None,
    101                            help="Override the device's zWinEventlogClause" \
    102                                 " with this string.")
    103          addNTLMv2Option(parser)
    104
    105 -    def postStartup(self):
    106          # turn on low-level pysamba debug logging if requested
    107          logseverity = self.options.logseverity
    108          if logseverity <= 5:
    109              pysamba.library.DEBUGLEVEL.value = 99
    110
    111          # force NTLMv2 authentication if requested
    112          setNTLMv2Auth(self.options)
    113
    114          # add our collector's custom statistics
    115          statService = zope.component.queryUtility(IStatisticsService)
    116          statService.addStatistic("events", "COUNTER")
    117
    118  #
    119  # Create an implementation of the IScheduledTask interface that will perform
    120  # the actual collection work needed by this collector. In this case, we will
    121  # scan Windows devices for changes to the Windows event log using a WMI
    122  # notification query. These queries are open-ended queries that wait until data
    123  # has been added to the WMI class specified in the query. This task will poll
    124  # for any changed events with a small timeout period before returning to an
    125  # idle state and trying again at the next collection interval.
    126  #
    127  # TODO: this is a timing bug with this approach where we can lose events in the
    128  # following scenarios:
    129  #   1. Anytime the daemon is shutdown and restarted.
    130  #   2. Anytime we reset our WMI connection and create a new one.
    131  #
    132 -class ZenEventLogTask(ObservableMixin):
    133      """
    134      A scheduled task that watches the event log on a single Windows device.
    135      """
    136      zope.interface.implements(IScheduledTask)
    137
    138      EVENT_LOG_NOTIFICATION_QUERY = """
    139          SELECT * FROM __InstanceCreationEvent
    140          WHERE TargetInstance ISA 'Win32_NTLogEvent'
    141            AND TargetInstance.EventType <= %d
    142          """
    143
    144      STATE_CONNECTING = 'CONNECTING'
    145      STATE_POLLING = 'POLLING'
    146      STATE_PROCESSING = 'PROCESSING'
    147
    148 -    def __init__(self,
    149                   deviceId,
    150                   taskName,
    151                   scheduleIntervalSeconds,
    152                   taskConfig):
    153          """
    154          Construct a new task instance to watch for Windows Event Log changes
    155          for the specified device.
    156
    157          @param deviceId: the Zenoss deviceId to watch
    158          @type deviceId: string
    159          @param taskName: the unique identifier for this task
    160          @type taskName: string
    161          @param scheduleIntervalSeconds: the interval at which this task will be
    162                 collected
    163          @type scheduleIntervalSeconds: int
    164          @param taskConfig: the configuration for this task
    165          """
    166          super(ZenEventLogTask, self).__init__()
    167
    168          self.name = taskName
    169          self.configId = deviceId
    170          self.interval = scheduleIntervalSeconds
    171          self.state = TaskStates.STATE_IDLE
    172
    173          self._taskConfig = taskConfig
    174          self._devId = deviceId
    175          self._manageIp = self._taskConfig.manageIp
    176
    177          self._eventService = zope.component.queryUtility(IEventService)
    178          self._statService = zope.component.queryUtility(IStatisticsService)
    179          self._preferences = zope.component.queryUtility(ICollectorPreferences,
    180                                                          "zeneventlog")
    181
    182          # if the user hasn't specified the batchSize or queryTimeout as command
    183          # options then use whatever has been specified in the collector
    184          # preferences
    185          # TODO: convert these to zProperties
    186          self._batchSize = self._preferences.options.batchSize
    187          if not self._batchSize:
    188              self._batchSize = self._preferences.wmibatchSize
    189          self._queryTimeout = self._preferences.options.queryTimeout
    190          if not self._queryTimeout:
    191              self._queryTimeout = self._preferences.wmiqueryTimeout
    192
    193          # Create the actual query that will be used based upon the template and
    194          # the devices's  zWinEventlogMinSeverity zProperty. If this zProperty
    195          # changes then the task will be deleted and a new one created, so it
    196          # is okay to do so here in the constructor.
    197          self._wmiQuery = ZenEventLogTask.EVENT_LOG_NOTIFICATION_QUERY % \
    198              int(self._taskConfig.zWinEventlogMinSeverity)
    199          testClause = self._preferences.options.testClause
    200          andClause = self._taskConfig.zWinEventlogClause
    201          if testClause is not None:
    202              if testClause.strip():
    203                  self._wmiQuery += " AND " + testClause
    204          elif andClause and andClause.strip():
    205              self._wmiQuery += " AND " + andClause
    206
    207          self._watcher = None
    208          self._reset()
    209
    210 -    def _reset(self):
    211          """
    212          Reset the WMI notification query watcher connection to the device, if
    213          one is presently active.
    214          """
    215          if self._watcher:
    216              self._watcher.close()
    217          self._watcher = None
    218
    219 -    def _makeEvent(self, lrec):
    220          """
    221          Put event in the queue to be sent to the ZenEventManager.
    222
    223          @param lrec: log record
    224          @type lrec: log record object
    225          @return: dictionary with event keys and values
    226          @rtype: dictionary
    227          """
    228          lrec = lrec.targetinstance
    229          evtkey = '%s_%s' % (lrec.sourcename, lrec.eventcode)
    230          sev = Debug
    231          if lrec.eventtype == 1:
    232              sev = Error  # error
    233          elif lrec.eventtype == 2:
    234              sev = Warning  # warning
    235          elif lrec.eventtype in (3, 4, 5):
    236              sev = Info  # information, security audit success & failure
    237
    238          log.debug( "---- log record info --------------" )
    239          for item in dir(lrec):
    240              if item[0] == '_':
    241                  continue
    242              log.debug("%s = %s"  % (item, getattr(lrec, item, '')))
    243          log.debug( "---- log record info --------------" )
    244
    245          ts= lrec.timegenerated
    246          try:
    247              date_ts = '/'.join( [ ts[0:4], ts[4:6], ts[6:8] ])
    248              time_ts = ':'.join( [ts[8:10], ts[10:12], ts[12:14] ])
    249              ts = date_ts + ' ' + time_ts
    250          except:
    251              pass
    252
    253          event_message = str(lrec.message).strip()
    254          if not event_message or event_message == 'None':
    255              event_message = "Message text from Windows not available." + \
    256                              "  See source system's event log."
    257
    258          evt = dict(
    259              device=self._devId,
    260              eventClassKey=evtkey,
    261              eventGroup=lrec.logfile,
    262              component=lrec.sourcename,
    263              ntevid=lrec.eventcode,
    264              summary=event_message,
    265              agent='zeneventlog',
    266              severity=sev,
    267              monitor=self._preferences.options.monitor,
    268              user=lrec.user,
    269              categorystring=lrec.categorystring,
    270              originaltime=ts,
    271              computername=lrec.computername,
    272              eventidentifier=lrec.eventidentifier,
    273              )
    274          log.debug("Device:%s msg:'%s'", self._devId, lrec.message)
    275          return evt
    276
    277 -    def _finished(self, result):
    278          """
    279          Callback activated when the task is complete so that final statistics
    280          on the collection can be displayed.
    281          """
    282          if not isinstance(result, Failure):
    283              log.debug("Device %s [%s] scanned successfully, %d events processed",
    284                        self._devId, self._manageIp, self._eventsFetched)
    285              stat = self._statService.getStatistic("events")
    286              stat.value += self._eventsFetched
    287          else:
    288              log.debug("Device %s [%s] scanned failed, %s",
    289                        self._devId, self._manageIp, result.getErrorMessage())
    290
    291          # give the result to the rest of the callback/errchain so that the
    292          # ZenCollector framework can keep track of the success/failure rate
    293          return result
    294
    295 -    def _failure(self, result):
    296          """
    297          Errback for an unsuccessful asynchronous connection or collection
    298          request.
    299          """
    300          err = result.getErrorMessage()
    301          log.error("Unable to scan device %s: %s", self._devId, err)
    302
    303          self._reset()
    304
    305          if 'WBEM_E_UNPARSABLE_QUERY' in err:
    306              summary = """
    307              There was an error found while processing the zWinEventlogClause
    308              query: %s
    309              """ % err
    310          else:
    311              summary = """
    312              Could not read the Windows event log (%s). Check your
    313              username/password settings and verify network connectivity.
    314              """ % err
    315
    316          self._eventService.sendEvent(dict(
    317              summary=summary,
    318              component='zeneventlog',
    319              eventClass=Status_Wmi,
    320              device=self._devId,
    321              severity=Error,
    322              agent='zeneventlog',
    323              ))
    324
    325          # give the result to the rest of the errback chain
    326          return result
    327
    328 -    def _collectSuccessful(self, result):
    329          """
    330          Callback for a successful fetch of events from the remote device.
    331          """
    332          self.state = ZenEventLogTask.STATE_PROCESSING
    333
    334          log.debug("Successful collection from %s [%s], result=%s",
    335                    self._devId, self._manageIp, result)
    336
    337          events = result
    338          if events:
    339              # process all of the fetched events
    340              for logRecord in events:
    341                  self._eventsFetched += 1
    342                  # TODO: figure out how to post this state on the cycle interval
    343                  self._eventService.sendEvent(self._makeEvent(logRecord))
    344
    345              # schedule another immediate collection so that we'll keep eating
    346              # events as long as they are ready for us; using callLater ensures
    347              # it goes to the end of the immediate work-queue so that other
    348              # events get processing time
    349              log.debug("Queuing another fetch for %s [%s]",
    350                        self._devId, self._manageIp)
    351              d = defer.Deferred()
    352              reactor.callLater(0, d.callback, None)
    353              d.addCallback(self._collectCallback)
    354              return d
    355
    356 -    def _deviceUp(self, result):
    357          msg = 'WMI connection to %s up.' % self._devId
    358          self._eventService.sendEvent(dict(
    359              summary=msg,
    360              eventClass=Status_Wmi,
    361              device=self._devId,
    362              severity=Clear,
    363              component='zeneventlog'))
    364          return result
    365
    366 -    def _collectCallback(self, result):
    367          """
    368          Callback called after a connect or previous collection so that another
    369          collection can take place.
    370          """
    371          log.debug("Polling for events from %s [%s]",
    372                    self._devId, self._manageIp)
    373
    374          self.state = ZenEventLogTask.STATE_POLLING
    375          d = self._watcher.getEvents(self._queryTimeout, self._batchSize)
    376          d.addCallbacks(self._collectSuccessful, self._failure)
    377          d.addCallbacks(self._deviceUp)
    378          return d
    379
    380 -    def _connectCallback(self, result):
    381          """
    382          Callback called after a successful connect to the remote Windows device.
    383          """
    384          log.debug("Connected to %s [%s]", self._devId, self._manageIp)
    385
    386 -    def _connect(self):
    387          """
    388          Called when a connection needs to be created to the remote Windows
    389          device.
    390          """
    391          log.debug("Connecting to %s [%s]", self._devId, self._manageIp)
    392
    393          self.state = ZenEventLogTask.STATE_CONNECTING
    394          self._watcher = Watcher(self._taskConfig, self._wmiQuery)
    395          return self._watcher.connect()
    396
    397 -    def cleanup(self):
    398          return self._reset()
    399
    400 -    def doTask(self):
    401          log.debug("Scanning device %s [%s]", self._devId, self._manageIp)
    402
    403          self._eventsFetched = 0
    404
    405          # see if we need to connect first before doing any collection
    406          if not self._watcher:
    407              d = self._connect()
    408              d.addCallbacks(self._connectCallback, self._failure)
    409          else:
    410              # since we don't need to bother connecting, we'll just create an
    411              # empty deferred and have it run immediately so the collect callback
    412              # will be fired off
    413              d = defer.Deferred()
    414              reactor.callLater(0, d.callback, None)
    415
    416          # try collecting events after a successful connect, or if we're already
    417          # connected
    418          d.addCallback(self._collectCallback)
    419
    420          # Add the _finished callback to be called in both success and error
    421          # scenarios. While we don't need final error processing in this task,
    422          # it is good practice to catch any final errors for diagnostic purposes.
    423          d.addBoth(self._finished)
    424
    425          # returning a Deferred will keep the framework from assuming the task
    426          # is done until the Deferred actually completes
    427          return d
    428
    429  #
    430  # Collector Daemon Main entry point
    431  #
    432  if __name__ == '__main__':
    433      myPreferences = ZenEventLogPreferences()
    434
    435      myTaskFactory = SimpleTaskFactory(ZenEventLogTask)
    436      myTaskSplitter = SimpleTaskSplitter(myTaskFactory)
    437      daemon = CollectorDaemon(myPreferences, myTaskSplitter)
    438      daemon.run()
    439