1
2
3
4
5
6
7
8
9
10
11 __doc__ = """zendisc
12 Scan networks and routes looking for devices to add to the ZODB
13 """
14 import socket
15
16
17
18
19
20 try:
21 import pysamba.twisted.reactor
22 except ImportError:
23 pass
24
25 from ipaddr import IPAddress
26
27
28 from icmpecho.Ping import Ping4, Ping6
29
30 import Globals
31 from optparse import SUPPRESS_HELP
32
33 from Products.DataCollector.zenmodeler import ZenModeler
34 from Products.ZenUtils.Exceptions import ZentinelException
35 from Products.ZenUtils.Utils import unused
36 from Products.ZenUtils.Driver import drive
37 from Products.ZenUtils.IpUtil import asyncNameLookup, isip, parse_iprange, \
38 getHostByName, ipunwrap
39 from Products.ZenUtils.NJobs import NJobs
40 from Products.ZenUtils.snmp import SnmpV1Config, SnmpV2cConfig
41 from Products.ZenUtils.snmp import SnmpAgentDiscoverer
42 from Products.ZenModel.Exceptions import NoIPAddress
43 from Products.ZenEvents.ZenEventClasses import Status_Snmp
44 from Products.ZenEvents.Event import Info
45 from Products.ZenStatus.PingService import PingService
46 from Products.ZenHub.PBDaemon import FakeRemote, PBDaemon
47 from Products.ZenHub.services import DiscoverService, ModelerService
48 from Products.ZenHub.services.DiscoverService import JobPropertiesProxy
49 unused(DiscoverService, ModelerService, JobPropertiesProxy)
50
51
52
53 from twisted.internet.defer import succeed
54 from twisted.python.failure import Failure
55 from twisted.internet import reactor
56 from twisted.names.error import DNSNameError
57
58
60 """
61 Scan networks and routes looking for devices to add to the ZODB
62 """
63
64 initialServices = PBDaemon.initialServices + ['DiscoverService']
65 name = 'zendisc'
66 scanned = 0
67
94
96 """
97 Given an IP address, return a deferred that pings the address.
98 """
99 self.log.debug("Using ipaddr module to convert %s" % ip)
100 ipObj = IPAddress(ip)
101
102 if ipObj.version == 6:
103 if self._pinger6 is None:
104 retval = Failure()
105 else:
106 retval = self._pinger6.ping(ip)
107 else:
108 if self._pinger4 is None:
109 retval = Failure()
110 else:
111 retval = self._pinger4.ping(ip)
112
113 return retval
114
116 """
117 Get the DiscoverService
118
119 @return: a DiscoverService from zenhub
120 @rtype: function
121 """
122 return self.services.get('DiscoverService', FakeRemote())
123
124
126 """
127 Ping all ips, create entries in the network if necessary.
128
129 @param nets: list of networks to discover
130 @type nets: list
131 @return: successful result is a list of IPs that were added
132 @rtype: Twisted deferred
133 """
134 def inner(driver):
135 """
136 Twisted driver class to iterate through devices
137
138 @param driver: Zenoss driver
139 @type driver: Zenoss driver
140 @return: successful result is a list of IPs that were added
141 @rtype: Twisted deferred
142 """
143 ips = []
144 goodCount = 0
145
146 for net in nets:
147 if self.options.subnets and len(net.children()) > 0:
148 continue
149 if not getattr(net, "zAutoDiscover", False):
150 self.log.info(
151 "Skipping network %s because zAutoDiscover is False"
152 % net.getNetworkName())
153 continue
154 self.log.info("Discover network '%s'", net.getNetworkName())
155 yield NJobs(self.options.chunkSize,
156 self.ping,
157 net.fullIpList()).start()
158 results = driver.next()
159 goodips = [
160 v.ipaddr for v in results if not isinstance(v, Failure)]
161 badips = [
162 v.value.ipaddr for v in results if isinstance(v, Failure)]
163 goodCount += len(goodips)
164 self.log.debug("Got %d good IPs and %d bad IPs",
165 len(goodips), len(badips))
166 yield self.config().callRemote('pingStatus',
167 net,
168 goodips,
169 badips,
170 self.options.resetPtr,
171 self.options.addInactive)
172 ips += driver.next()
173 self.log.info("Discovered %s active ips", goodCount)
174
175 yield succeed(ips)
176 driver.next()
177
178 d = drive(inner)
179 return d
180
182 """
183 Ping all IPs in the range and create devices for the ones that come
184 back.
185
186 @param ranges: list of ranges to discover
187 @type ranges: list
188 """
189 if isinstance(self.options.range, basestring):
190 self.options.range = [self.options.range]
191
192
193 if (isinstance(self.options.range, list) and
194 self.options.range[0].find(",") > -1):
195 self.options.range = [n.strip() for n in
196 self.options.range[0].split(',')]
197 ips = []
198 goodCount = 0
199 for iprange in self.options.range:
200
201 ips.extend(parse_iprange(iprange))
202 yield NJobs(self.options.chunkSize,
203 self.ping,
204 ips).start()
205 results = driver.next()
206 goodips = [v.ipaddr for v in results if not isinstance(v, Failure)]
207 badips = [v.value.ipaddr for v in results if isinstance(v, Failure)]
208 goodCount += len(goodips)
209 self.log.debug("Got %d good IPs and %d bad IPs",
210 len(goodips), len(badips))
211 yield self.discoverDevices(goodips)
212 yield succeed("Discovered %d active IPs" % goodCount)
213 driver.next()
214
215
217 """
218 Discover all default routers based on DMD configuration.
219
220 @param rootdev: device root in DMD
221 @type rootdev: device class
222 @param seenips: list of IP addresses
223 @type seenips: list of strings
224 @return: Twisted/Zenoss Python iterable
225 @rtype: Python iterable
226 """
227 if not seenips:
228 seenips = []
229
230 def inner(driver):
231 """
232 Twisted driver class to iterate through devices
233
234 @param driver: Zenoss driver
235 @type driver: Zenoss driver
236 @return: successful result is a list of IPs that were added
237 @rtype: Twisted deferred
238 """
239 yield self.config().callRemote('followNextHopIps', rootdev.id)
240 for ip in driver.next():
241 if ip in seenips:
242 continue
243 self.log.info("device '%s' next hop '%s'", rootdev.id, ip)
244 seenips.append(ip)
245 yield self.discoverDevice(ip, devicepath="/Network/Router")
246 router = driver.next()
247 if not router:
248 continue
249 yield self.discoverRouters(router, seenips)
250 driver.next()
251
252 return drive(inner)
253
254
256 """
257 Send a 'device discovered' event through zenhub
258
259 @param ip: IP addresses
260 @type ip: strings
261 @param dev: remote device name
262 @type dev: device object
263 @param sev: severity
264 @type sev: integer
265 """
266 devname = comp = ip
267 if dev:
268 devname = dev.id
269 msg = "'Discovered device name '%s' for ip '%s'" % (devname, ip)
270 evt = dict(device=devname,ipAddress=ip,eventKey=ip,
271 component=comp,eventClass=Status_Snmp,
272 summary=msg, severity=sev,
273 agent="Discover")
274 self.sendEvent(evt)
275
276
277 - def discoverDevices(self,
278 ips,
279 devicepath="/Discovered",
280 prodState=1000):
281 """
282 Discover devices by active ips that are not associated with a device.
283
284 @param ips: list of IP addresses
285 @type ips: list of strings
286 @param devicepath: where in the DMD to put any discovered devices
287 @type devicepath: string
288 @param prodState: production state (see Admin Guide for a description)
289 @type prodState: integer
290 @return: Twisted/Zenoss Python iterable
291 @rtype: Python iterable
292 """
293 def discoverDevice(ip):
294 """
295 Discover a particular device
296 NB: Wrapper around self.discoverDevice()
297
298 @param ip: IP address
299 @type ip: string
300 @return: Twisted/Zenoss Python iterable
301 @rtype: Python iterable
302 """
303 return self.discoverDevice(ip, devicepath, prodState)
304
305 return NJobs(self.options.parallel, discoverDevice, ips).start()
306
307
309 """
310 Scan a device for ways of naming it: PTR DNS record or a SNMP name
311
312 @param ip: IP address
313 @type ip: string
314 @param devicePath: where in the DMD to put any discovered devices
315 @type devicePath: string
316 @param deviceSnmpCommunities: Optional list of SNMP community strings
317 to try, overriding those set on the device class
318 @type deviceSnmpCommunities: list
319 @return: result is None or a tuple containing
320 (community, port, version, snmp name)
321 @rtype: deferred: Twisted deferred
322 """
323 from pynetsnmp.twistedsnmp import AgentProxy
324
325 def inner(driver):
326 """
327 Twisted driver class to iterate through devices
328
329 @param driver: Zenoss driver
330 @type driver: Zenoss driver
331 @return: successful result is a list of IPs that were added
332 @rtype: Twisted deferred
333 """
334 self.log.debug("findRemoteDeviceInfo.inner: Doing SNMP lookup on device %s", ip)
335 yield self.config().callRemote('getSnmpConfig', devicePath)
336 communities, port, version, timeout, retries = driver.next()
337 self.log.debug("findRemoteDeviceInfo.inner: override acquired community strings")
338
339
340 if deviceSnmpCommunities is not None:
341 communities = deviceSnmpCommunities
342
343
344
345 communities.reverse()
346
347 configs = []
348 for i, community in enumerate(communities):
349 configs.append(SnmpV1Config(
350 ip, weight=i, port=port, timeout=timeout,
351 retries=retries, community=community))
352 configs.append(SnmpV2cConfig(
353 ip, weight=i+100, port=port, timeout=timeout,
354 retries=retries, community=community))
355
356 yield SnmpAgentDiscoverer().findBestConfig(configs)
357 driver.next()
358 self.log.debug("Finished SNMP lookup on device %s", ip)
359
360 return drive(inner)
361
362
363 - def discoverDevice(self, ip, devicepath="/Discovered", prodState=1000):
364 """
365 Discover a device based on its IP address.
366
367 @param ip: IP address
368 @type ip: string
369 @param devicepath: where in the DMD to put any discovered devices
370 @type devicepath: string
371 @param prodState: production state (see Admin Guide for a description)
372 @type prodState: integer
373 @return: Twisted/Zenoss Python iterable
374 @rtype: Python iterable
375 """
376 self.scanned += 1
377 if self.options.maxdevices:
378 if self.scanned >= self.options.maxdevices:
379 self.log.info("Limit of %d devices reached" %
380 self.options.maxdevices)
381 return succeed(None)
382
383 def inner(driver):
384 """
385 Twisted driver class to iterate through devices
386
387 @param driver: Zenoss driver
388 @type driver: Zenoss driver
389 @return: successful result is a list of IPs that were added
390 @rtype: Twisted deferred
391 @todo: modularize this function (130+ lines is ridiculous)
392 """
393 try:
394 kw = dict(deviceName=ip,
395 discoverProto=None,
396 devicePath=devicepath,
397 performanceMonitor=self.options.monitor,
398 productionState=prodState)
399
400
401 if self.options.job:
402 yield self.config().callRemote('getJobProperties',
403 self.options.job)
404 job_props = driver.next()
405 if job_props is not None:
406
407 kw['zProperties'] = getattr(job_props, 'zProperties', {})
408
409
410
411
412
413
414 snmpDeviceInfo = None
415
416
417 if not self.options.nosnmp:
418 self.log.debug("Scanning device with address %s", ip)
419 snmpCommunities = kw.get('zProperties', {}).get(
420 'zSnmpCommunities', None)
421 yield self.findRemoteDeviceInfo(ip, devicepath,
422 snmpCommunities)
423 snmp_config = driver.next()
424 if snmp_config:
425 if snmp_config.sysName:
426 kw['deviceName'] = snmp_config.sysName
427
428 if snmp_config.version:
429 kw['zSnmpVer'] = snmp_config.version
430
431 if snmp_config.port:
432 kw['zSnmpPort'] = snmp_config.port
433
434 if snmp_config.community:
435 kw['zSnmpCommunity'] = snmp_config.community
436
437
438
439
440 elif self.options.zSnmpStrictDiscovery:
441 self.log.info('zSnmpStrictDiscovery is True. ' +
442 'Not creating device for %s.'
443 % ip )
444 return
445
446
447
448
449
450
451
452
453
454
455 if self.options.zPreferSnmpNaming and \
456 not isip( kw['deviceName'] ):
457
458
459 pass
460 elif self.options.device and not isip(self.options.device):
461 kw['deviceName'] = self.options.device
462 else:
463
464
465 yield asyncNameLookup(ip)
466 try:
467 kw.update(dict(deviceName=driver.next()))
468 except Exception, ex:
469 self.log.debug("Failed to lookup %s (%s)" % (ip, ex))
470
471
472
473 forceDiscovery = bool(self.options.device)
474
475
476
477 yield self.config().callRemote('createDevice', ipunwrap(ip),
478 force=forceDiscovery, **kw)
479
480 result = driver.next()
481 self.log.debug("ZenDisc.discoverDevice.inner: got result from remote_createDevice")
482 if isinstance(result, Failure):
483 raise ZentinelException(result.value)
484 dev, created = result
485
486
487
488
489 if not dev:
490 self.log.info("IP '%s' on no auto-discover, skipping",ip)
491 return
492 else:
493
494 if not created and not dev.temp_device:
495
496
497 if not self.options.remodel:
498 self.log.info("Found IP '%s' on device '%s';"
499 " skipping discovery", ip, dev.id)
500 if self.options.device:
501 self.setExitCode(3)
502 yield succeed(dev)
503 driver.next()
504 return
505 else:
506
507 self.log.info("IP '%s' on device '%s' remodel",
508 ip, dev.id)
509 self.sendDiscoveredEvent(ip, dev)
510
511
512
513 if not self.options.nosnmp:
514 self.discovered.append(dev.id)
515 yield succeed(dev)
516 driver.next()
517 except ZentinelException, e:
518 self.log.exception(e)
519 evt = dict(device=ip,
520 component=ip,
521 ipAddress=ip,
522 eventKey=ip,
523 eventClass=Status_Snmp,
524 summary=str(e),
525 severity=Info,
526 agent="Discover")
527 if self.options.snmpMissing:
528 self.sendEvent(evt)
529 except Exception, e:
530 self.log.exception("Failed device discovery for '%s'", ip)
531
532 else:
533 yield self.config().callRemote('succeedDiscovery', dev.id)
534 driver.next()
535
536
537 yield succeed(dev)
538 driver.next()
539
540 self.log.debug("Finished scanning device with address %s", ip)
541
542 return drive(inner)
543
544
546 """
547 Twisted driver class to iterate through networks
548
549 @param driver: Zenoss driver
550 @type driver: Zenoss driver
551 @return: successful result is a list of IPs that were added
552 @rtype: Twisted deferred
553 """
554
555
556 if isinstance(self.options.net, basestring):
557 self.options.net = [self.options.net]
558
559
560 if isinstance(self.options.net, (list,tuple)) and ',' in self.options.net[0]:
561 self.options.net = [
562 n.strip() for n in self.options.net[0].split(',')
563 ]
564 count = 0
565 devices = []
566 if not self.options.net:
567 yield self.config().callRemote('getDefaultNetworks')
568 self.options.net = driver.next()
569
570 if not self.options.net:
571 self.log.warning("No networks configured")
572 return
573
574 for net in self.options.net:
575 try:
576 yield self.config().callRemote('getNetworks',
577 net,
578 self.options.subnets)
579 nets = driver.next()
580 if not nets:
581 self.log.warning("No networks found for %s" % (net,))
582 continue
583 yield self.discoverIps(nets)
584 ips = driver.next()
585 devices += ips
586 count += len(ips)
587 except Exception, ex:
588 self.log.exception("Error performing net discovery on %s", ex)
589 def discoverDevice(ip):
590 """
591 Discover a particular device
592 NB: Wrapper around self.discoverDevice()
593
594 @param ip: IP address
595 @type ip: string
596 @return: Twisted/Zenoss Python iterable
597 @rtype: Python iterable
598 """
599 return self.discoverDevice(ip,
600 self.options.deviceclass,
601 self.options.productionState)
602 yield NJobs(self.options.parallel, discoverDevice, devices).start()
603 yield succeed("Discovered %d devices" % count)
604 driver.next()
605
606
608 """
609 Display the results that we've obtained
610
611 @param results: what we've discovered
612 @type results: string
613 """
614 if isinstance(results, Failure):
615 self.log.error("Error: %s", results)
616 else:
617 self.log.info("Result: %s", results)
618 self.main()
619
620
622 """
623 Add a device to the system by name or IP.
624
625 @param driver: driver object
626 @type driver: Twisted/Zenoss object
627 @return: Twisted deferred
628 @rtype: Twisted deferred
629 """
630 deviceName = self.options.device
631 self.log.info("Looking for %s" % deviceName)
632 ip = None
633 if isip(ipunwrap(deviceName)):
634 ip = ipunwrap(deviceName)
635 else:
636 try:
637
638
639 self.log.debug("getHostByName")
640 ip = getHostByName(deviceName)
641 except socket.error:
642 ip = ""
643 if not ip:
644 raise NoIPAddress("No IP found for name %s" % deviceName)
645 else:
646 self.log.debug("Found IP %s for device %s" % (ip, deviceName))
647 yield self.config().callRemote('getDeviceConfig', [deviceName])
648 me, = driver.next() or [None]
649 if not me or me.temp_device or self.options.remodel:
650 yield self.discoverDevice(ip,
651 devicepath=self.options.deviceclass,
652 prodState=self.options.productionState)
653 yield succeed("Discovered device %s." % deviceName)
654 driver.next()
655
656
658 """
659 Python iterable to go through discovery
660
661 @return: Twisted deferred
662 @rtype: Twisted deferred
663 """
664 myname = socket.getfqdn()
665 self.log.debug("My hostname = %s", myname)
666 myip = None
667 try:
668 myip = getHostByName(myname)
669 self.log.debug("My IP address = %s", myip)
670 except (socket.error, DNSNameError):
671 raise SystemExit("Failed lookup of my IP for name %s", myname)
672
673 yield self.config().callRemote('getDeviceConfig', [myname])
674 me, = driver.next() or [None]
675 if not me or self.options.remodel:
676 yield self.discoverDevice(myip,
677 devicepath=self.options.deviceclass,
678 prodState=self.options.productionState)
679 me = driver.next()
680 if not me:
681 raise SystemExit("SNMP discover of self '%s' failed" % myname)
682 if not myip:
683 myip = me.manageIp
684 if not myip:
685 raise SystemExit("Can't find my IP for name %s" % myname)
686
687 yield self.discoverRouters(me, [myip])
688
689 driver.next()
690 if self.options.routersonly:
691 self.log.info("Only routers discovered, skipping ping sweep.")
692 else:
693 yield self.config().callRemote('getSubNetworks')
694 yield self.discoverIps(driver.next())
695 ips = driver.next()
696 if not self.options.nosnmp:
697 yield self.discoverDevices(ips)
698 driver.next()
699
700
702 """
703 Our device list comes from our list of newly discovered devices
704
705 @return: list of discovered devices
706 @rtype: Twisted succeed() object
707 """
708 return succeed(self.discovered)
709
710
712 """
713 Called by Twisted once a connection has been established.
714 """
715 d = self.configure()
716 d.addCallback(self.startDiscovery)
717 d.addErrback(self.reportError)
718
719
720
735
737 """
738 Command-line option builder for optparse
739 """
740 ZenModeler.buildOptions(self)
741 self.parser.add_option('--net', dest='net', action="append",
742 help="Discover all device on this network")
743 self.parser.add_option('--range', dest='range', action='append',
744 help="Discover all IPs in this range")
745 self.parser.add_option('--deviceclass', dest='deviceclass',
746 default="/Discovered",
747 help="Default device class for discovered devices")
748 self.parser.add_option('--prod_state', dest='productionState',
749 default=1000, type='int',
750 help="Initial production state for discovered devices")
751 self.parser.add_option('--remodel', dest='remodel',
752 action="store_true", default=False,
753 help="Remodel existing objects")
754 self.parser.add_option('--routers', dest='routersonly',
755 action="store_true", default=False,
756 help="Only discover routers")
757 self.parser.add_option('--tries', dest='tries', default=1, type="int",
758 help="How many ping tries")
759 self.parser.add_option('--timeout', dest='timeout',
760 default=2, type="float",
761 help="ping timeout in seconds")
762 self.parser.add_option('--chunk', dest='chunkSize',
763 default=10, type="int",
764 help="Number of in-flight ping packets")
765 self.parser.add_option('--snmp-missing', dest='snmpMissing',
766 action="store_true", default=False,
767 help="Send an event if SNMP is not found on the device")
768 self.parser.add_option('--add-inactive', dest='addInactive',
769 action="store_true", default=False,
770 help="Add all IPs found, even if they are unresponsive")
771 self.parser.add_option('--reset-ptr', dest='resetPtr',
772 action="store_true", default=False,
773 help="Reset all IP PTR records")
774 self.parser.add_option('--no-snmp', dest='nosnmp',
775 action="store_true", default=False,
776 help="Skip SNMP discovery on found IP addresses")
777 self.parser.add_option('--subnets', dest='subnets',
778 action="store_true", default=False,
779 help="Recurse into subnets for discovery")
780 self.parser.add_option('--walk', dest='walk', action='store_true',
781 default=False,
782 help="Walk the route tree, performing discovery on all networks")
783 self.parser.add_option('--max-devices', dest='maxdevices',
784 default=0,
785 type='int',
786 help="Collect a maximum number of devices. Default is no limit.")
787 self.parser.add_option('--snmp-strict-discovery',
788 dest='zSnmpStrictDiscovery',
789 action="store_true", default=False,
790 help="Only add devices that can be modeled via SNMP." )
791 self.parser.add_option('--prefer-snmp-naming',
792 dest='zPreferSnmpNaming',
793 action="store_true", default=False,
794 help="Prefer SNMP name to DNS name when modeling via SNMP." )
795
796
797 self.parser.add_option('--job', dest='job', help=SUPPRESS_HELP )
798
799
800
801 if __name__ == "__main__":
802 d = ZenDisc()
803 d.processOptions()
804 reactor.run = d.reactorLoop
805 d.run()
806