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 as published by
     10  # 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 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          addNTLMv2Option(parser)
     99
    100 -    def postStartup(self):
    101          # turn on low-level pysamba debug logging if requested
    102          logseverity = self.options.logseverity
    103          if logseverity <= 5:
    104              pysamba.library.DEBUGLEVEL.value = 99
    105
    106          # force NTLMv2 authentication if requested
    107          setNTLMv2Auth(self.options)
    108
    109          # add our collector's custom statistics
    110          statService = zope.component.queryUtility(IStatisticsService)
    111          statService.addStatistic("events", "COUNTER")
    112
    113  #
    114  # Create an implementation of the IScheduledTask interface that will perform
    115  # the actual collection work needed by this collector. In this case, we will
    116  # scan Windows devices for changes to the Windows event log using a WMI
    117  # notification query. These queries are open-ended queries that wait until data
    118  # has been added to the WMI class specified in the query. This task will poll
    119  # for any changed events with a small timeout period before returning to an
    120  # idle state and trying again at the next collection interval.
    121  #
    122  # TODO: this is a timing bug with this approach where we can lose events in the
    123  # following scenarios:
    124  #   1. Anytime the daemon is shutdown and restarted.
    125  #   2. Anytime we reset our WMI connection and create a new one.
    126  #
    127 -class ZenEventLogTask(ObservableMixin):
    128      """
    129      A scheduled task that watches the event log on a single Windows device.
    130      """
    131      zope.interface.implements(IScheduledTask)
    132
    133      EVENT_LOG_NOTIFICATION_QUERY = """
    134          SELECT * FROM __InstanceCreationEvent
    135          WHERE TargetInstance ISA 'Win32_NTLogEvent'
    136            AND TargetInstance.EventType <= %d
    137          """
    138
    139      STATE_CONNECTING = 'CONNECTING'
    140      STATE_POLLING = 'POLLING'
    141      STATE_PROCESSING = 'PROCESSING'
    142
    143 -    def __init__(self,
    144                   deviceId,
    145                   taskName,
    146                   scheduleIntervalSeconds,
    147                   taskConfig):
    148          """
    149          Construct a new task instance to watch for Windows Event Log changes
    150          for the specified device.
    151
    152          @param deviceId: the Zenoss deviceId to watch
    153          @type deviceId: string
    154          @param taskName: the unique identifier for this task
    155          @type taskName: string
    156          @param scheduleIntervalSeconds: the interval at which this task will be
    157                 collected
    158          @type scheduleIntervalSeconds: int
    159          @param taskConfig: the configuration for this task
    160          """
    161          super(ZenEventLogTask, self).__init__()
    162
    163          self.name = taskName
    164          self.configId = deviceId
    165          self.interval = scheduleIntervalSeconds
    166          self.state = TaskStates.STATE_IDLE
    167
    168          self._taskConfig = taskConfig
    169          self._devId = deviceId
    170          self._manageIp = self._taskConfig.manageIp
    171
    172          # Create the actual query that will be used based upon the template and
    173          # the devices's  zWinEventlogMinSeverity zProperty. If this zProperty
    174          # changes then the task will be deleted and a new one created, so it
    175          # is okay to do so here in the constructor.
    176          self._wmiQuery = ZenEventLogTask.EVENT_LOG_NOTIFICATION_QUERY % \
    177              int(self._taskConfig.zWinEventlogMinSeverity)
    178
    179          self._eventService = zope.component.queryUtility(IEventService)
    180          self._statService = zope.component.queryUtility(IStatisticsService)
    181          self._preferences = zope.component.queryUtility(ICollectorPreferences,
    182                                                          "zeneventlog")
    183
    184          # if the user hasn't specified the batchSize or queryTimeout as command
    185          # options then use whatever has been specified in the collector
    186          # preferences
    187          # TODO: convert these to zProperties
    188          self._batchSize = self._preferences.options.batchSize
    189          if not self._batchSize:
    190              self._batchSize = self._preferences.wmibatchSize
    191          self._queryTimeout = self._preferences.options.queryTimeout
    192          if not self._queryTimeout:
    193              self._queryTimeout = self._preferences.wmiqueryTimeout
    194
    195          self._watcher = None
    196          self._reset()
    197
    198 -    def _reset(self):
    199          """
    200          Reset the WMI notification query watcher connection to the device, if
    201          one is presently active.
    202          """
    203          if self._watcher:
    204              self._watcher.close()
    205          self._watcher = None
    206
    207 -    def _makeEvent(self, lrec):
    208          """
    209          Put event in the queue to be sent to the ZenEventManager.
    210
    211          @param lrec: log record
    212          @type lrec: log record object
    213          @return: dictionary with event keys and values
    214          @rtype: dictionary
    215          """
    216          lrec = lrec.targetinstance
    217          evtkey = '%s_%s' % (lrec.sourcename, lrec.eventcode)
    218          sev = Debug
    219          if lrec.eventtype == 1:
    220              sev = Error  # error
    221          elif lrec.eventtype == 2:
    222              sev = Warning  # warning
    223          elif lrec.eventtype in (3, 4, 5):
    224              sev = Info  # information, security audit success & failure
    225
    226          log.debug( "---- log record info --------------" )
    227          for item in dir(lrec):
    228              if item[0] == '_':
    229                  continue
    230              log.debug("%s = %s"  % (item, getattr(lrec, item, '')))
    231          log.debug( "---- log record info --------------" )
    232
    233          ts= lrec.timegenerated
    234          try:
    235              date_ts = '/'.join( [ ts[0:4], ts[4:6], ts[6:8] ])
    236              time_ts = ':'.join( [ts[8:10], ts[10:12], ts[12:14] ])
    237              ts = date_ts + ' ' + time_ts
    238          except:
    239              pass
    240
    241          event_message = str(lrec.message).strip()
    242          if not event_message or event_message == 'None':
    243              event_message = "Message text from Windows not available." + \
    244                              "  See source system's event log."
    245
    246          evt = dict(
    247              device=self._devId,
    248              eventClassKey=evtkey,
    249              eventGroup=lrec.logfile,
    250              component=lrec.sourcename,
    251              ntevid=lrec.eventcode,
    252              summary=event_message,
    253              agent='zeneventlog',
    254              severity=sev,
    255              monitor=self._preferences.options.monitor,
    256              user=lrec.user,
    257              categorystring=lrec.categorystring,
    258              originaltime=ts,
    259              computername=lrec.computername,
    260              eventidentifier=lrec.eventidentifier,
    261              )
    262          log.debug("Device:%s msg:'%s'", self._devId, lrec.message)
    263          return evt
    264
    265 -    def _finished(self, result):
    266          """
    267          Callback activated when the task is complete so that final statistics
    268          on the collection can be displayed.
    269          """
    270          if not isinstance(result, Failure):
    271              log.debug("Device %s [%s] scanned successfully, %d events processed",
    272                        self._devId, self._manageIp, self._eventsFetched)
    273              stat = self._statService.getStatistic("events")
    274              stat.value += self._eventsFetched
    275          else:
    276              log.debug("Device %s [%s] scanned failed, %s",
    277                        self._devId, self._manageIp, result.getErrorMessage())
    278
    279          # give the result to the rest of the callback/errchain so that the
    280          # ZenCollector framework can keep track of the success/failure rate
    281          return result
    282
    283 -    def _failure(self, result):
    284          """
    285          Errback for an unsuccessful asynchronous connection or collection
    286          request.
    287          """
    288          err = result.getErrorMessage()
    289          log.error("Unable to scan device %s: %s", self._devId, err)
    290
    291          self._reset()
    292
    293          summary = """
    294              Could not read the Windows event log (%s). Check your
    295              username/password settings and verify network connectivity.
    296              """ % err
    297
    298          self._eventService.sendEvent(dict(
    299              summary=summary,
    300              component='zeneventlog',
    301              eventClass=Status_Wmi,
    302              device=self._devId,
    303              severity=Error,
    304              agent='zeneventlog',
    305              ))
    306
    307          # give the result to the rest of the errback chain
    308          return result
    309
    310 -    def _collectSuccessful(self, result):
    311          """
    312          Callback for a successful fetch of events from the remote device.
    313          """
    314          self.state = ZenEventLogTask.STATE_PROCESSING
    315
    316          log.debug("Successful collection from %s [%s], result=%s",
    317                    self._devId, self._manageIp, result)
    318
    319          events = result
    320          if events:
    321              # process all of the fetched events
    322              for logRecord in events:
    323                  self._eventsFetched += 1
    324                  # TODO: figure out how to post this state on the cycle interval
    325                  self._eventService.sendEvent(self._makeEvent(logRecord))
    326
    327              # schedule another immediate collection so that we'll keep eating
    328              # events as long as they are ready for us; using callLater ensures
    329              # it goes to the end of the immediate work-queue so that other
    330              # events get processing time
    331              log.debug("Queuing another fetch for %s [%s]",
    332                        self._devId, self._manageIp)
    333              d = defer.Deferred()
    334              reactor.callLater(0, d.callback, None)
    335              d.addCallback(self._collectCallback)
    336              return d
    337
    338 -    def _collectCallback(self, result):
    339          """
    340          Callback called after a connect or previous collection so that another
    341          collection can take place.
    342          """
    343          log.debug("Polling for events from %s [%s]",
    344                    self._devId, self._manageIp)
    345
    346          self.state = ZenEventLogTask.STATE_POLLING
    347          d = self._watcher.getEvents(self._queryTimeout, self._batchSize)
    348          d.addCallbacks(self._collectSuccessful, self._failure)
    349          return d
    350
    351 -    def _connectCallback(self, result):
    352          """
    353          Callback called after a successful connect to the remote Windows device.
    354          """
    355          log.debug("Connected to %s [%s]", self._devId, self._manageIp)
    356
    357 -    def _connect(self):
    358          """
    359          Called when a connection needs to be created to the remote Windows
    360          device.
    361          """
    362          log.debug("Connecting to %s [%s]", self._devId, self._manageIp)
    363
    364          self.state = ZenEventLogTask.STATE_CONNECTING
    365          self._watcher = Watcher(self._taskConfig, self._wmiQuery)
    366          return self._watcher.connect()
    367
    368 -    def cleanup(self):
    369          return self._reset()
    370
    371 -    def doTask(self):
    372          log.debug("Scanning device %s [%s]", self._devId, self._manageIp)
    373
    374          self._eventsFetched = 0
    375
    376          # see if we need to connect first before doing any collection
    377          if not self._watcher:
    378              d = self._connect()
    379              d.addCallbacks(self._connectCallback, self._failure)
    380          else:
    381              # since we don't need to bother connecting, we'll just create an
    382              # empty deferred and have it run immediately so the collect callback
    383              # will be fired off
    384              d = defer.Deferred()
    385              reactor.callLater(0, d.callback, None)
    386
    387          # try collecting events after a successful connect, or if we're already
    388          # connected
    389          d.addCallback(self._collectCallback)
    390
    391          # Add the _finished callback to be called in both success and error
    392          # scenarios. While we don't need final error processing in this task,
    393          # it is good practice to catch any final errors for diagnostic purposes.
    394          d.addBoth(self._finished)
    395
    396          # returning a Deferred will keep the framework from assuming the task
    397          # is done until the Deferred actually completes
    398          return d
    399
    400  #
    401  # Collector Daemon Main entry point
    402  #
    403  if __name__ == '__main__':
    404      myPreferences = ZenEventLogPreferences()
    405
    406      myTaskFactory = SimpleTaskFactory(ZenEventLogTask)
    407      myTaskSplitter = SimpleTaskSplitter(myTaskFactory)
    408      daemon = CollectorDaemon(myPreferences, myTaskSplitter)
    409      daemon.run()
    410