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