Package Products :: Package ZenStatus :: Package nmap :: Module NmapPingTask |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenStatus.nmap.NmapPingTask

      1  ##############################################################################
      2  #
      3  # Copyright (C) Zenoss, Inc. 2011, all rights reserved.
      4  #
      5  # This content is made available according to terms specified in
      6  # License.zenoss under the directory where your Zenoss product is installed.
      7  #
      8  ##############################################################################
      9
     10
     11  __doc__ = """NmapPingTask
     12
     13  Pings a all devices in the current device list.
     14  """
     15
     16  import logging
     17  log = logging.getLogger("zen.NmapPingTask")
     18  import tempfile
     19  import subprocess
     20  from twisted.internet import utils
     21  from twisted.internet import task as twistedTask
     22  from twisted.internet import defer
     23  from twisted.internet import reactor
     24  import os.path
     25  from cStringIO import StringIO
     26  import stat
     27
     28  import Globals
     29  from zope import interface
     30  from zope import component
     31  from Products import ZenCollector
     32  from Products.ZenCollector.tasks import BaseTask, TaskStates
     33  from Products.ZenCollector import interfaces
     34  from Products.ZenCollector.tasks import SimpleTaskFactory
     35  from Products.ZenUtils.Utils import zenPath
     36  from Products.ZenEvents import ZenEventClasses
     37  from zenoss.protocols.protobufs import zep_pb2 as events
     38
     39  # imports from within ZenStatus
     40  from Products.ZenStatus import PingTask
     41  from Products.ZenStatus.ping.CmdPingTask import CmdPingTask
     42  from PingResult import PingResult, parseNmapXmlToDict
     43  from Products.ZenStatus.PingCollectionPreferences import PingCollectionPreferences
     44  from Products.ZenStatus.interfaces import IPingTaskFactory
     45  from Products.ZenStatus import nmap
     46
     47  _NMAP_BINARY = zenPath("bin/nmap")
     48
     49
     50  _CLEAR = 0
     51  _CRITICAL = 5
     52  _WARNING = 3
     53
     54  # amount of IPs/events to process before giving time to the reactor
     55  _SENDEVENT_YIELD_INTERVAL = 100  # should always be >= 1
     56
     57  # twisted.callLater has trouble with sys.maxint as call interval,
     58  # just use a big interval, 100 years
     59  _NEVER_INTERVAL = 60 * 60 * 24 * 365 * 100
     60
     61 -class NmapPingCollectionPreferences(PingCollectionPreferences):
     62
     63 -    def postStartup(self):
     64          """
     65          Hook in to application startup and start background NmapPingTask.
     66          """
     67          daemon = component.getUtility(interfaces.ICollector)
     68          task = NmapPingTask (
     69              'NmapPingTask',
     70              'NmapPingTask',
     71              taskConfig=daemon._prefs
     72          )
     73          daemon._scheduler.addTask(task, now=True)
     74
     75 -class NPingTaskFactory(object):
     76      """
     77      A Factory to create PingTasks that do not run. This allows NmapPingTask
     78      to use the created PingTasks as placeholders for configuration.
     79      """
     80      interface.implements(IPingTaskFactory)
     81
     82 -    def __init__(self):
     83          self.reset()
     84
     85 -    def build(self):
     86          # Task spliter will gurantee every task has exactly one monitoredIp
     87          if self.config.monitoredIps[0].ipVersion == 6:
     88              # nmap does not support IPV6 ping/traceroute, use CmdPing
     89              log.debug("Creating an IPv6 task: %s", self.config.monitoredIps[0].ip)
     90              task = CmdPingTask(
     91                  self.name,
     92                  self.configId,
     93                  self.interval,
     94                  self.config,
     95              )
     96          else:
     97              log.debug("Creating an IPv4 task: %s", self.config.monitoredIps[0].ip)
     98              task = PingTask(
     99                  self.name,
    100                  self.configId,
    101                  self.interval,
    102                  self.config,
    103              )
    104              # don't run the tasks, they are used for storing config
    105              task.pauseOnScheduled = True
    106              task.interval = _NEVER_INTERVAL
    107          return task
    108
    109 -    def reset(self):
    110          self.name = None
    111          self.configId = None
    112          self.interval = None
    113          self.config = None
    114
    115 -class NmapPingTask(BaseTask):
    116      interface.implements(ZenCollector.interfaces.IScheduledTask)
    117      """
    118      NmapPingTask pings all PingTasks using using nmap.
    119      """
    120
    121 -    def __init__(self,
    122                   taskName, configId,
    123                   scheduleIntervalSeconds=60,
    124                   taskConfig=None):
    125          """
    126          @param deviceId: the Zenoss deviceId to watch
    127          @type deviceId: string
    128          @param taskName: the unique identifier for this task
    129          @type taskName: string
    130          @param scheduleIntervalSeconds: the interval at which this task will be
    131                 collected
    132          @type scheduleIntervalSeconds: int
    133          @param taskConfig: the configuration for this task
    134          """
    135          super(NmapPingTask, self).__init__(
    136                   taskName, configId,
    137                   scheduleIntervalSeconds, taskConfig=None
    138                )
    139
    140          # Needed for interface
    141          self.name = taskName
    142          self.configId = configId
    143          self.state = TaskStates.STATE_IDLE
    144          self.interval = scheduleIntervalSeconds
    145
    146          if taskConfig is None:
    147              raise TypeError("taskConfig cannot be None")
    148          self._preferences = taskConfig
    149
    150          self._daemon = component.getUtility(ZenCollector.interfaces.ICollector)
    151          self._dataService = component.queryUtility(ZenCollector.interfaces.IDataService)
    152          self._eventService = component.queryUtility(ZenCollector.interfaces.IEventService)
    153
    154          self._pings = 0
    155          self._nmapPresent = False    # assume nmap is not present at startup
    156          self._nmapIsSuid = False     # assume nmap is not SUID at startup
    157          self.collectorName = self._daemon._prefs.collectorName
    158
    159
    160 -    def _detectNmap(self):
    161          """
    162          Detect that nmap is present.
    163          """
    164          # if nmap has already been detected, do not detect it again
    165          if self._nmapPresent:
    166              return
    167          self._nmapPresent = os.path.exists(_NMAP_BINARY)
    168
    169          if self._nmapPresent == False:
    170              raise nmap.NmapNotFound()
    171          self._sendNmapMissing()
    172
    173 -    def _sendNmapMissing(self):
    174          """
    175          Send/Clear event to show that nmap is present/missing.
    176          """
    177          if self._nmapPresent:
    178              msg = "nmap was found"
    179              severity = _CLEAR
    180          else:
    181              msg = "nmap was NOT found at %r " % _NMAP_BINARY
    182              severity = _CRITICAL
    183          evt = dict(
    184              device=self.collectorName,
    185              eventClass=ZenEventClasses.Status_Ping,
    186              eventGroup='Ping',
    187              eventKey="nmap_missing",
    188              severity=severity,
    189              summary=msg,
    190          )
    191          self._eventService.sendEvent(evt)
    192
    193 -    def _detectNmapIsSuid(self):
    194          """
    195          Detect that nmap is set SUID
    196          """
    197          if self._nmapPresent and self._nmapIsSuid == False:
    198              # get attributes for nmap binary
    199              attribs = os.stat(_NMAP_BINARY)
    200              # find out if it is SUID and owned by root
    201              self._nmapIsSuid = (attribs.st_uid == 0) and (attribs.st_mode & stat.S_ISUID)
    202              if self._nmapIsSuid is False:
    203                  raise nmap.NmapNotSuid()
    204          self._sendNmapNotSuid()  # send a clear
    205
    206 -    def _sendNmapNotSuid(self):
    207          """
    208          Send/Clear event to show that nmap is set SUID.
    209          """
    210          resolution = None
    211          if self._nmapIsSuid:
    212              msg = "nmap is set SUID"
    213              severity = _CLEAR
    214          else:
    215              msg = "nmap is NOT SUID: %s" % _NMAP_BINARY
    216              try:
    217                 import socket
    218                 cmd = subprocess.check_output('echo "chown root.`id -gn` $ZENHOME/bin/nmap && chmod u+s $ZENHOME/bin/nmap"', shell=True).strip()
    219                 resolution = "Log on to %s and execute the following as root: %s" % (socket.getfqdn(), cmd)
    220              except Exception as ex:
    221                 log.exception("There was an error generating a help message related to sudo nmap.")
    222              severity = _CRITICAL
    223          evt = dict(
    224              device=self.collectorName,
    225              eventClass=ZenEventClasses.Status_Ping,
    226              eventGroup='Ping',
    227              eventKey="nmap_suid",
    228              severity=severity,
    229              summary=msg,
    230          )
    231          if resolution:
    232              evt['resolution'] = resolution
    233          self._eventService.sendEvent(evt)
    234
    235 -    def _nmapExecution(self, ex=None):
    236          """
    237          Send/Clear event to show that nmap is executed properly.
    238          """
    239          if ex is None:
    240              msg = "nmap executed correctly"
    241              severity = _CLEAR
    242          else:
    243              msg = "nmap did not execute correctly: %s" % ex
    244              severity = _CRITICAL
    245          evt = dict(
    246              device=self.collectorName,
    247              eventClass=ZenEventClasses.Status_Ping,
    248              eventGroup='Ping',
    249              eventKey="nmap_execution",
    250              severity=severity,
    251              summary=msg,
    252          )
    253          self._eventService.sendEvent(evt)
    254
    255      @defer.inlineCallbacks
    256 -    def _executeNmapCmd(self, inputFileFilename, traceroute=False, outputType='xml'):
    257          """
    258          Execute nmap and return it's output.
    259          """
    260          args = []
    261          args.extend(["-iL", inputFileFilename])  # input file
    262          args.append("-sn")               # don't port scan the hosts
    263          args.append("-PE")               # use ICMP echo
    264          args.append("-n")                # don't resolve hosts internally
    265          args.append("--privileged")      # assume we can open raw socket
    266          args.append("--send-ip")         # don't allow ARP responses
    267
    268          # give up on a host after spending too much time on it
    269          args.extend(["--initial-rtt-timeout", "%.1fs" % self._preferences.pingTimeOut])
    270          args.extend(["--min-rtt-timeout", "%.1fs" % self._preferences.pingTimeOut])
    271          args.extend(["--max-retries", "0"])
    272
    273          if traceroute:
    274              args.append("--traceroute")
    275
    276          if outputType == 'xml':
    277              args.extend(["-oX", '-']) # outputXML to stdout
    278          else:
    279              raise ValueError("Unsupported nmap output type: %s" % outputType)
    280
    281          # execute nmap
    282          if log.isEnabledFor(logging.DEBUG):
    283              log.debug("executing nmap %s", " ".join(args))
    284          out, err, exitCode = yield utils.getProcessOutputAndValue(
    285              _NMAP_BINARY, args)
    286
    287          if exitCode != 0:
    288              input = open(inputFileFilename).read()
    289              log.debug("input file: %s", input)
    290              log.debug("stdout: %s", out)
    291              log.debug("stderr: %s", err)
    292              raise nmap.NmapExecutionError(
    293                  exitCode=exitCode, stdout=out, stderr=err, args=args)
    294
    295          try:
    296              nmapResults = parseNmapXmlToDict(StringIO(out))
    297              defer.returnValue(nmapResults)
    298          except Exception as e:
    299              input = open(inputFileFilename).read()
    300              log.debug("input file: %s", input)
    301              log.debug("stdout: %s", out)
    302              log.debug("stderr: %s", err)
    303              log.exception(e)
    304              raise nmap.NmapExecutionError(
    305                  exitCode=exitCode, stdout=out, stderr=err, args=args)
    306
    307      @defer.inlineCallbacks
    308 -    def doTask(self):
    309          """
    310          BatchPingDevices !
    311          """
    312          log.debug('---- BatchPingDevices ----')
    313
    314          if self.interval != self._daemon._prefs.pingCycleInterval:
    315              log.info("Changing ping interval from %r to %r ",
    316                  self.interval,
    317                  self._daemon._prefs.pingCycleInterval,
    318              )
    319              self.interval = self._daemon._prefs.pingCycleInterval
    320
    321          try:
    322              self._detectNmap()        # will clear nmap_missing
    323              self._detectNmapIsSuid()  # will clear nmap_suid
    324              yield self._batchPing()   # will clear nmap_execution
    325              self._pings += 1
    326
    327          except nmap.NmapNotFound:
    328              self._sendNmapMissing()
    329          except nmap.NmapNotSuid:
    330              self._sendNmapNotSuid()
    331          except nmap.NmapExecutionError as ex:
    332              self._nmapExecution(ex)
    333
    334
    335 -    def _getPingTasks(self):
    336          """
    337          Iterate the daemons task list and find PingTask tasks that are IPV4.
    338          """
    339          tasks = self._daemon._scheduler._tasks
    340          pingTasks = {}
    341          for configName, task in tasks.iteritems():
    342              if isinstance(task.task, PingTask):
    343                  if task.task.config.ipVersion == 4:
    344                      pingTasks[configName] = task.task
    345          return pingTasks
    346
    347      @defer.inlineCallbacks
    348 -    def _batchPing(self):
    349          """
    350          Find the IPs, ping/traceroute, parse, correlate, and send events.
    351          """
    352          # find all devices to Ping
    353          ipTasks = self._getPingTasks()
    354          if len(ipTasks) == 0:
    355              log.debug("No ips to ping!")
    356              raise StopIteration() # exit this generator
    357
    358          with tempfile.NamedTemporaryFile(prefix='zenping_nmap_') as tfile:
    359              ips = []
    360              for taskName, ipTask in ipTasks.iteritems():
    361                  ips.append(ipTask.config.ip)
    362                  ipTask.resetPingResult() # clear out previous run's results
    363              ips.sort()
    364              for ip in ips:
    365                  tfile.write("%s\n" % ip)
    366              tfile.flush()
    367
    368              # ping up to self._preferences.pingTries
    369              tracerouteInterval = self._daemon.options.tracerouteInterval
    370
    371              # determine if traceroute needs to run
    372              doTraceroute = False
    373              if tracerouteInterval > 0:
    374                  if self._pings == 0 or (self._pings % tracerouteInterval) == 0:
    375                      doTraceroute = True # try to traceroute on next ping
    376
    377              import time
    378              for attempt in range(0, self._daemon._prefs.pingTries):
    379
    380                  start = time.time()
    381                  results = yield self._executeNmapCmd(tfile.name, doTraceroute)
    382                  elapsed = time.time() - start
    383                  log.debug("Nmap execution took %f seconds", elapsed)
    384
    385                  # only do traceroute on the first ping attempt, if at all
    386                  doTraceroute = False
    387
    388                  # record the results!
    389                  for taskName, ipTask in ipTasks.iteritems():
    390                      ip = ipTask.config.ip
    391                      if ip in results:
    392                          result = results[ip]
    393                          ipTask.logPingResult(result)
    394                      else:
    395                          # received no result, log as down
    396                          ipTask.logPingResult(PingResult(ip, isUp=False))
    397
    398              downTasks = {}
    399              i = 0
    400              for taskName, ipTask in ipTasks.iteritems():
    401                  i += 1
    402                  if ipTask.isUp:
    403                      log.debug("%s is up!", ipTask.config.ip)
    404                      ipTask.sendPingUp()
    405                      ipTask.storeResults()
    406                  else:
    407                      log.debug("%s is down", ipTask.config.ip)
    408                      downTasks[ipTask.config.ip] = ipTask
    409                      ipTask.storeResults()
    410
    411                  # give time to reactor to send events if necessary
    412                  if i % _SENDEVENT_YIELD_INTERVAL:
    413                      yield twistedTask.deferLater(reactor, 0, lambda: None)
    414
    415              yield self._correlate(downTasks)
    416              self._nmapExecution()
    417
    418      @defer.inlineCallbacks
    419 -    def _correlate(self, downTasks):
    420          """
    421          Correlate ping down events.
    422
    423          This simple correlator will take a list of PingTasks that are in the
    424          down state. It loops through the list and the last known trace route
    425          for each of the ip's. For every hop in the traceroute (starting from the
    426          collector to the ip in question), the hop's ip is searched for in
    427          downTasks. If it's found, then this collector was also monitoring the
    428          source of the problem.
    429
    430          Note: this does not take in to account multiple routes to the ip in
    431          question. It uses only the last known traceroute as given by nmap which
    432          will not have routing loops and hosts that block icmp.
    433          """
    434
    435          # for every down ipTask
    436          i = 0
    437          for currentIp, ipTask in downTasks.iteritems():
    438              i += 1
    439              # walk the hops in the traceroute
    440              for hop in ipTask.trace:
    441                  # if a hop.ip alog the traceroute is in our list of down ips
    442                  # and that hop.ip is not the currentIp then
    443                  if hop.ip in downTasks and hop.ip != currentIp:
    444                      # we found our root cause!
    445                      rootCause = downTasks[hop.ip]
    446                      ipTask.sendPingDown(rootCause=rootCause)
    447                      break
    448              else:
    449                  # no root cause found
    450                  ipTask.sendPingDown()
    451
    452              # give time to reactor to send events if necessary
    453              if i % _SENDEVENT_YIELD_INTERVAL:
    454                  yield twistedTask.deferLater(reactor, 0, lambda: None, )
    455
    456          # TODO: we could go a step further and ping all the ips along the last good
    457          # traceroute to give some insight as to where the problem may lie
    458
    459 -    def displayStatistics(self):
    460          """
    461          Called by the collector framework scheduler, and allows us to
    462          see how each task is doing.
    463          """
    464          return ''
    465