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

# Source Code for Module Products.ZenWin.zenwin

      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 Windows services. Retrieved status is 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  from Products.ZenCollector.tasks import SimpleTaskFactory,\
     42                                          SimpleTaskSplitter,\
     43                                          TaskStates
     44  from Products.ZenEvents.ZenEventClasses import Error, Clear, Status_WinService, Status_Wmi
     45  from Products.ZenModel.WinServiceClass import STARTMODE_AUTO
     46  from Products.ZenUtils.observable import ObservableMixin
     47  from Products.ZenWin.WMIClient import WMIClient
     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.zenwin")
     62
     63  # Create an implementation of the ICollectorPreferences interface so that the
     64  # ZenCollector framework can configure itself from our preferences.
     65 -class ZenWinPreferences(object):
     66      zope.interface.implements(ICollectorPreferences)
     67
     68 -    def __init__(self):
     69          """
     70          Construct a new ZenWinPreferences instance and provide default
     71          values for needed attributes.
     72          """
     73          self.collectorName = "zenwin"
     74          self.defaultRRDCreateCommand = None
     75          self.cycleInterval = 5 * 60 # seconds
     76          self.configCycleInterval = 20 # minutes
     77          self.options = None
     78
     79          # the configurationService attribute is the fully qualified class-name
     80          # of our configuration service that runs within ZenHub
     81          self.configurationService = 'Products.ZenWin.services.WinServiceConfig'
     82
     83          self.wmibatchSize = 10
     84          self.wmiqueryTimeout = 1000
     85
     86 -    def buildOptions(self, parser):
     87          parser.add_option('--debug', dest='debug', default=False,
     88                                 action='store_true',
     89                                 help='Increase logging verbosity.')
     90          parser.add_option('--proxywmi', dest='proxywmi',
     91                                 default=False, action='store_true',
     92                                 help='Use a process proxy to avoid long-term blocking'
     93                                 )
     94          parser.add_option('--queryTimeout', dest='queryTimeout',
     95                                 default=None, type='int',
     96                                 help='The number of milliseconds to wait for ' + \
     97                                      'WMI query to respond. Overrides the ' + \
     98                                      'server settings.')
     99          parser.add_option('--batchSize', dest='batchSize',
    100                                 default=None, type='int',
    101                                 help='Number of data objects to retrieve in a ' +
    102                                      'single WMI query.')
    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
    115 -class ZenWinTask(ObservableMixin):
    116      zope.interface.implements(IScheduledTask)
    117
    118      STATE_WMIC_CONNECT = 'WMIC_CONNECT'
    119      STATE_WMIC_QUERY = 'WMIC_QUERY'
    120      STATE_WMIC_PROCESS = 'WMIC_PROCESS'
    121      STATE_WATCHER_CONNECT = 'WATCHER_CONNECT'
    122      STATE_WATCHER_QUERY = 'WATCHER_QUERY'
    123      STATE_WATCHER_PROCESS = 'WATCHER_PROCESS'
    124
    125      # windows service states from wmi queries (lowercased)
    126      RUNNING = "running"
    127      STOPPED = "stopped"
    128
    129 -    def __init__(self,
    130                   deviceId,
    131                   taskName,
    132                   scheduleIntervalSeconds,
    133                   taskConfig):
    134          """
    135          Construct a new task instance to watch for Windows Event Log changes
    136          for the specified device.
    137
    138          @param deviceId: the Zenoss deviceId to watch
    139          @type deviceId: string
    140          @param taskName: the unique identifier for this task
    141          @type taskName: string
    142          @param scheduleIntervalSeconds: the interval at which this task will be
    143                 collected
    144          @type scheduleIntervalSeconds: int
    145          @param taskConfig: the configuration for this task
    146          """
    147          super(ZenWinTask, self).__init__()
    148
    149          self.name = taskName
    150          self.configId = deviceId
    151          self.interval = scheduleIntervalSeconds
    152          self.state = TaskStates.STATE_IDLE
    153
    154          self._taskConfig = taskConfig
    155          self._devId = deviceId
    156          self._manageIp = self._taskConfig.manageIp
    157
    158          self._eventService = zope.component.queryUtility(IEventService)
    159          self._preferences = zope.component.queryUtility(ICollectorPreferences,
    160                                                          "zenwin")
    161
    162          # if the user hasn't specified the batchSize or queryTimeout as command
    163          # options then use whatever has been specified in the collector
    164          # preferences
    165          # TODO: convert these to zProperties
    166          self._batchSize = self._preferences.options.batchSize
    167          if not self._batchSize:
    168              self._batchSize = self._preferences.wmibatchSize
    169          self._queryTimeout = self._preferences.options.queryTimeout
    170          if not self._queryTimeout:
    171              self._queryTimeout = self._preferences.wmiqueryTimeout
    172
    173          self._wmic = None # the WMIClient
    174          self._watcher = None
    175          self._reset()
    176
    177 -    def _reset(self):
    178          """
    179          Reset the WMI client and notification query watcher connection to the
    180          device, if they are presently active.
    181          """
    182          if self._wmic:
    183              self._wmic.close()
    184          self._wmic = None
    185          if self._watcher:
    186              self._watcher.close()
    187          self._watcher = None
    188
    189 -    def _finished(self, result):
    190          """
    191          Callback activated when the task is complete so that final statistics
    192          on the collection can be displayed.
    193          """
    194          if not isinstance(result, Failure):
    195              log.debug("Device %s [%s] scanned successfully",
    196                        self._devId, self._manageIp)
    197          else:
    198              log.debug("Device %s [%s] scanned failed, %s",
    199                        self._devId, self._manageIp, result.getErrorMessage())
    200
    201          # give the result to the rest of the callback/errchain so that the
    202          # ZenCollector framework can keep track of the success/failure rate
    203          return result
    204
    205 -    def _failure(self, result):
    206          """
    207          Errback for an unsuccessful asynchronous connection or collection
    208          request.
    209          """
    210          err = result.getErrorMessage()
    211          log.error("Unable to scan device %s: %s", self._devId, err)
    212
    213          self._reset()
    214
    215          summary = """
    216              Could not read Windows services (%s). Check your
    217              username/password settings and verify network connectivity.
    218              """ % err
    219
    220          self._eventService.sendEvent(dict(
    221              summary=summary,
    222              component='zenwin',
    223              eventClass=Status_Wmi,
    224              device=self._devId,
    225              severity=Error,
    226              ))
    227
    228          # give the result to the rest of the errback chain
    229          return result
    230
    231 -    def _sendWinServiceEvent(self, name, summary, severity):
    232          event = {'summary': summary,
    233                   'eventClass': Status_WinService,
    234                   'device': self._devId,
    235                   'severity': severity,
    236                   'agent': 'zenwin',
    237                   'component': name,
    238                   'eventGroup': 'StatusTest'}
    239          self._eventService.sendEvent(event)
    240
    241 -    def _handleResult(self, name, state, startMode):
    242          """
    243          Handle a result from the wmi query. Results from both the initial WMI
    244          client query and the watcher's notification query are processed by
    245          this method. Log running and stopped transitions. Send an event if the
    246          service is monitored.
    247          """
    248          state = state.lower()
    249          summary = "Windows service '%s' is %s" % (name, state)
    250          logLevel = logging.DEBUG
    251          if name in self._taskConfig.services:
    252              was_running, stoppedSeverity, oldStartMode, monitoredStartModes = \
    253                  self._taskConfig.services[name]
    254
    255              running = (state == self.RUNNING)
    256              service_was_important = (oldStartMode in monitoredStartModes)
    257              service_is_important = (startMode in monitoredStartModes)
    258
    259              logLevel = logging.INFO
    260              if service_is_important:
    261                  if running:
    262                      self._sendWinServiceEvent(name, summary, Clear)
    263                  else:
    264                      self._sendWinServiceEvent(name, summary, stoppedSeverity)
    265                      logLevel = logging.CRITICAL
    266              else:
    267                  # if a down service was changed from important to unimportant,
    268                  # emit a clear event
    269                  if service_was_important and not running:
    270                      self._sendWinServiceEvent(name, summary, Clear)
    271
    272              self._taskConfig.services[name] = (
    273                  running, stoppedSeverity, startMode, monitoredStartModes)
    274
    275          log.log(logLevel, '%s on %s', summary, self._devId)
    276
    277 -    def _collectSuccessful(self, results):
    278          """
    279          Callback for a successful fetch of services from the remote device.
    280          """
    281          self.state = ZenWinTask.STATE_WATCHER_PROCESS
    282
    283          log.debug("Successful collection from %s [%s], results=%s",
    284                    self._devId, self._manageIp, results)
    285
    286          # make a local copy of monitored services list
    287          services = self._taskConfig.services.copy()
    288          if results:
    289              for result in [r.targetInstance for r in results]:
    290                  if result.state:
    291                      if result.name in services:
    292                          # remove service from local copy
    293                          del services[result.name]
    294                      self._handleResult(
    295                          result.name, result.state, result.startmode)
    296          # send events for the services that did not show up in results
    297          for name, data in services.items():
    298              running, failSeverity, startMode, monitoredStartModes = data
    299              if running:
    300                  state = self.RUNNING
    301              else:
    302                  state = self.STOPPED
    303              self._handleResult(name, state, startMode)
    304          if results:
    305              # schedule another immediate collection so that we'll keep eating
    306              # events as long as they are ready for us; using callLater ensures
    307              # it goes to the end of the immediate work-queue so that other
    308              # events get processing time
    309              log.debug("Queuing another fetch for %s [%s]",
    310                        self._devId, self._manageIp)
    311              d = defer.Deferred()
    312              reactor.callLater(0, d.callback, None)
    313              d.addCallback(self._collectCallback)
    314              return d
    315
    316 -    def _deviceUp(self, result):
    317          msg = 'WMI connection to %s up.' % self._devId
    318          self._eventService.sendEvent(dict(
    319              summary=msg,
    320              eventClass=Status_Wmi,
    321              device=self._devId,
    322              severity=Clear,
    323              component='zenwin'))
    324          return result
    325
    326 -    def _collectCallback(self, result):
    327          """
    328          Callback called after a connect or previous collection so that another
    329          collection can take place.
    330          """
    331          log.debug("Polling for events from %s [%s]",
    332                    self._devId, self._manageIp)
    333
    334          self.state = ZenWinTask.STATE_WATCHER_QUERY
    335          d = self._watcher.getEvents(self._queryTimeout, self._batchSize)
    336          d.addCallbacks(self._collectSuccessful, self._failure)
    337          d.addCallbacks(self._deviceUp)
    338          return d
    339
    340 -    def _connectCallback(self, result):
    341          """
    342          Callback called after a successful connect to the remote Windows device.
    343          """
    344          log.debug("Connected to %s [%s]", self._devId, self._manageIp)
    345
    346 -    def _connectWatcher(self, result):
    347          self.state = ZenWinTask.STATE_WMIC_PROCESS
    348          for service in result['query']:
    349              self._handleResult(service.name, service.state, service.startmode)
    350          self._wmic.close()
    351          self._wmic = None
    352          self.state = ZenWinTask.STATE_WATCHER_CONNECT
    353          wql = "SELECT * FROM __InstanceModificationEvent WITHIN 5 "\
    354                "WHERE TargetInstance ISA 'Win32_Service'"
    355          self._watcher = Watcher(self._taskConfig, wql)
    356          return self._watcher.connect()
    357
    358 -    def _initialQuery(self, result):
    359          self.state = ZenWinTask.STATE_WMIC_QUERY
    360          wql = "SELECT Name, State, StartMode FROM Win32_Service"
    361          d = self._wmic.query({'query': wql})
    362          d.addCallback(self._connectWatcher)
    363          return d
    364
    365 -    def _connect(self):
    366          """
    367          Called when a connection needs to be created to the remote Windows
    368          device.
    369          """
    370          log.debug("Connecting to %s [%s]", self._devId, self._manageIp)
    371          self.state = ZenWinTask.STATE_WMIC_CONNECT
    372          self._wmic = WMIClient(self._taskConfig)
    373          d = self._wmic.connect()
    374          d.addCallback(self._initialQuery)
    375          return d
    376
    377 -    def cleanup(self):
    378          return self._reset()
    379
    380 -    def doTask(self):
    381          log.debug("Scanning device %s [%s]", self._devId, self._manageIp)
    382
    383          # see if we need to connect first before doing any collection
    384          if not self._watcher:
    385              d = self._connect()
    386              d.addCallbacks(self._connectCallback, self._failure)
    387          else:
    388              # since we don't need to bother connecting, we'll just create an
    389              # empty deferred and have it run immediately so the collect callback
    390              # will be fired off
    391              d = defer.Deferred()
    392              reactor.callLater(0, d.callback, None)
    393
    394          # try collecting events after a successful connect, or if we're already
    395          # connected
    396          d.addCallback(self._collectCallback)
    397
    398          # Add the _finished callback to be called in both success and error
    399          # scenarios. While we don't need final error processing in this task,
    400          # it is good practice to catch any final errors for diagnostic purposes.
    401          d.addBoth(self._finished)
    402
    403          # returning a Deferred will keep the framework from assuming the task
    404          # is done until the Deferred actually completes
    405          return d
    406
    407
    408  #
    409  # Collector Daemon Main entry point
    410  #
    411  if __name__ == '__main__':
    412      myPreferences = ZenWinPreferences()
    413      myTaskFactory = SimpleTaskFactory(ZenWinTask)
    414      myTaskSplitter = SimpleTaskSplitter(myTaskFactory)
    415      daemon = CollectorDaemon(myPreferences, myTaskSplitter)
    416      daemon.run()
    417