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