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.ZenUtils.snmp import SnmpV1Config, SnmpV2cConfig
38 from Products.ZenUtils.snmp import SnmpAgentDiscoverer
39 from Products.ZenModel.Exceptions import NoIPAddress
40 from Products.ZenEvents.ZenEventClasses import Status_Snmp
41 from Products.ZenEvents.Event import Info
42 from Products.ZenStatus.AsyncPing import Ping
43 from Products.ZenHub.PBDaemon import FakeRemote, PBDaemon
44 from Products.ZenHub.services import DiscoverService, ModelerService
45 unused(DiscoverService, ModelerService)
46
47
48 from twisted.internet.defer import succeed
49 from twisted.python.failure import Failure
50 from twisted.internet import reactor
51 from twisted.names.error import DNSNameError
52
53
55 """
56 Scan networks and routes looking for devices to add to the ZODB
57 """
58
59 initialServices = PBDaemon.initialServices + ['DiscoverService']
60 name = 'zendisc'
61 scanned = 0
62
64 """
65 Initalizer
66
67 @param single: collect from a single device?
68 @type single: boolean
69 """
70 ZenModeler.__init__(self, single )
71 if not self.options.useFileDescriptor:
72 self.openPrivilegedPort('--ping')
73 self.discovered = []
74 sock = None
75 if self.options.useFileDescriptor:
76 sock = int(self.options.useFileDescriptor)
77 self.ping = Ping(self.options.tries,
78 self.options.timeout,
79 sock=sock)
80
81
83 """
84 Get the DiscoverService
85
86 @return: a DiscoverService from zenhub
87 @rtype: function
88 """
89 return self.services.get('DiscoverService', FakeRemote())
90
91
93 """
94 Ping all ips, create entries in the network if necessary.
95
96 @param nets: list of networks to discover
97 @type nets: list
98 @return: successful result is a list of IPs that were added
99 @rtype: Twisted deferred
100 """
101 def inner(driver):
102 """
103 Twisted driver class to iterate through devices
104
105 @param driver: Zenoss driver
106 @type driver: Zenoss driver
107 @return: successful result is a list of IPs that were added
108 @rtype: Twisted deferred
109 """
110 ips = []
111 goodCount = 0
112
113 for net in nets:
114 if self.options.subnets and len(net.children()) > 0:
115 continue
116 if not getattr(net, "zAutoDiscover", False):
117 self.log.info(
118 "Skipping network %s because zAutoDiscover is False"
119 % net.getNetworkName())
120 continue
121 self.log.info("Discover network '%s'", net.getNetworkName())
122 yield NJobs(self.options.chunkSize,
123 self.ping.ping,
124 net.fullIpList()).start()
125 results = driver.next()
126 goodips = [
127 v.ipaddr for v in results if not isinstance(v, Failure)]
128 badips = [
129 v.value.ipaddr for v in results if isinstance(v, Failure)]
130 goodCount += len(goodips)
131 self.log.debug("Got %d good IPs and %d bad IPs",
132 len(goodips), len(badips))
133 yield self.config().callRemote('pingStatus',
134 net,
135 goodips,
136 badips,
137 self.options.resetPtr,
138 self.options.addInactive)
139 ips += driver.next()
140 self.log.info("Discovered %s active ips", goodCount)
141
142 yield succeed(ips)
143 driver.next()
144
145 d = drive(inner)
146 return d
147
149 """
150 Ping all IPs in the range and create devices for the ones that come
151 back.
152
153 @param ranges: list of ranges to discover
154 @type ranges: list
155 """
156 if isinstance(self.options.range, basestring):
157 self.options.range = [self.options.range]
158
159
160 if (isinstance(self.options.range, list) and
161 self.options.range[0].find(",") > -1):
162 self.options.range = [n.strip() for n in
163 self.options.range[0].split(',')]
164 ips = []
165 goodCount = 0
166 for iprange in self.options.range:
167
168 ips.extend(parse_iprange(iprange))
169 yield NJobs(self.options.chunkSize,
170 self.ping.ping,
171 ips).start()
172 results = driver.next()
173 goodips = [v.ipaddr for v in results if not isinstance(v, Failure)]
174 badips = [v.value.ipaddr for v in results if isinstance(v, Failure)]
175 goodCount += len(goodips)
176 self.log.debug("Got %d good IPs and %d bad IPs",
177 len(goodips), len(badips))
178 yield self.discoverDevices(goodips)
179 yield succeed("Discovered %d active IPs" % goodCount)
180 driver.next()
181
182
184 """
185 Discover all default routers based on DMD configuration.
186
187 @param rootdev: device root in DMD
188 @type rootdev: device class
189 @param seenips: list of IP addresses
190 @type seenips: list of strings
191 @return: Twisted/Zenoss Python iterable
192 @rtype: Python iterable
193 """
194 if not seenips:
195 seenips = []
196
197 def inner(driver):
198 """
199 Twisted driver class to iterate through devices
200
201 @param driver: Zenoss driver
202 @type driver: Zenoss driver
203 @return: successful result is a list of IPs that were added
204 @rtype: Twisted deferred
205 """
206 yield self.config().callRemote('followNextHopIps', rootdev.id)
207 for ip in driver.next():
208 if ip in seenips:
209 continue
210 self.log.info("device '%s' next hop '%s'", rootdev.id, ip)
211 seenips.append(ip)
212 yield self.discoverDevice(ip, devicepath="/Network/Router")
213 router = driver.next()
214 if not router:
215 continue
216 yield self.discoverRouters(router, seenips)
217 driver.next()
218
219 return drive(inner)
220
221
223 """
224 Send a 'device discovered' event through zenhub
225
226 @param ip: IP addresses
227 @type ip: strings
228 @param dev: remote device name
229 @type dev: device object
230 @param sev: severity
231 @type sev: integer
232 """
233 devname = comp = ip
234 if dev:
235 devname = dev.id
236 msg = "'Discovered device name '%s' for ip '%s'" % (devname, ip)
237 evt = dict(device=devname,ipAddress=ip,eventKey=ip,
238 component=comp,eventClass=Status_Snmp,
239 summary=msg, severity=sev,
240 agent="Discover")
241 self.sendEvent(evt)
242
243
244 - def discoverDevices(self,
245 ips,
246 devicepath="/Discovered",
247 prodState=1000):
248 """
249 Discover devices by active ips that are not associated with a device.
250
251 @param ips: list of IP addresses
252 @type ips: list of strings
253 @param devicepath: where in the DMD to put any discovered devices
254 @type devicepath: string
255 @param prodState: production state (see Admin Guide for a description)
256 @type prodState: integer
257 @return: Twisted/Zenoss Python iterable
258 @rtype: Python iterable
259 """
260 def discoverDevice(ip):
261 """
262 Discover a particular device
263 NB: Wrapper around self.discoverDevice()
264
265 @param ip: IP address
266 @type ip: string
267 @return: Twisted/Zenoss Python iterable
268 @rtype: Python iterable
269 """
270 return self.discoverDevice(ip, devicepath, prodState)
271
272 return NJobs(self.options.parallel, discoverDevice, ips).start()
273
274
276 """
277 Scan a device for ways of naming it: PTR DNS record or a SNMP name
278
279 @param ip: IP address
280 @type ip: string
281 @param devicePath: where in the DMD to put any discovered devices
282 @type devicePath: string
283 @param deviceSnmpCommunities: Optional list of SNMP community strings
284 to try, overriding those set on the device class
285 @type deviceSnmpCommunities: list
286 @return: result is None or a tuple containing
287 (community, port, version, snmp name)
288 @rtype: deferred: Twisted deferred
289 """
290 from pynetsnmp.twistedsnmp import AgentProxy
291
292 def inner(driver):
293 """
294 Twisted driver class to iterate through devices
295
296 @param driver: Zenoss driver
297 @type driver: Zenoss driver
298 @return: successful result is a list of IPs that were added
299 @rtype: Twisted deferred
300 """
301 self.log.debug("Doing SNMP lookup on device %s", ip)
302 yield self.config().callRemote('getSnmpConfig', devicePath)
303 communities, port, version, timeout, retries = driver.next()
304
305
306
307 if deviceSnmpCommunities is not None:
308 communities = deviceSnmpCommunities
309
310
311
312 communities.reverse()
313
314 configs = []
315 for i, community in enumerate(communities):
316 configs.append(SnmpV1Config(
317 ip, weight=i, port=port, timeout=timeout,
318 retries=retries, community=community))
319 configs.append(SnmpV2cConfig(
320 ip, weight=i+100, port=port, timeout=timeout,
321 retries=retries, community=community))
322
323 yield SnmpAgentDiscoverer().findBestConfig(configs)
324 driver.next()
325 self.log.debug("Finished SNMP lookup on device %s", ip)
326
327 return drive(inner)
328
329
330 - def discoverDevice(self, ip, devicepath="/Discovered", prodState=1000):
331 """
332 Discover a device based on its IP address.
333
334 @param ip: IP address
335 @type ip: string
336 @param devicepath: where in the DMD to put any discovered devices
337 @type devicepath: string
338 @param prodState: production state (see Admin Guide for a description)
339 @type prodState: integer
340 @return: Twisted/Zenoss Python iterable
341 @rtype: Python iterable
342 """
343 self.scanned += 1
344 if self.options.maxdevices:
345 if self.scanned >= self.options.maxdevices:
346 self.log.info("Limit of %d devices reached" %
347 self.options.maxdevices)
348 return succeed(None)
349
350 def inner(driver):
351 """
352 Twisted driver class to iterate through devices
353
354 @param driver: Zenoss driver
355 @type driver: Zenoss driver
356 @return: successful result is a list of IPs that were added
357 @rtype: Twisted deferred
358 @todo: modularize this function (130+ lines is ridiculous)
359 """
360 try:
361 kw = dict(deviceName=ip,
362 discoverProto=None,
363 devicePath=devicepath,
364 performanceMonitor=self.options.monitor)
365
366
367 if self.options.job:
368 yield self.config().callRemote('getJobProperties',
369 self.options.job)
370 job_props = driver.next()
371 if job_props is not None:
372
373 kw['zProperties'] = job_props.get('zProperties', {})
374
375
376
377
378
379
380 snmpDeviceInfo = None
381
382
383 if not self.options.nosnmp:
384 self.log.debug("Scanning device with address %s", ip)
385 snmpCommunities = kw.get('zProperties', {}).get(
386 'zSnmpCommunities', None)
387 yield self.findRemoteDeviceInfo(ip, devicepath,
388 snmpCommunities)
389 snmp_config = driver.next()
390 if snmp_config:
391 if snmp_config.sysName:
392 kw['deviceName'] = snmp_config.sysName
393
394 if snmp_config.version:
395 kw['zSnmpVer'] = snmp_config.version
396
397 if snmp_config.port:
398 kw['zSnmpPort'] = snmp_config.port
399
400 if snmp_config.community:
401 kw['zSnmpCommunity'] = snmp_config.community
402
403
404
405
406 elif self.options.zSnmpStrictDiscovery:
407 self.log.info('zSnmpStrictDiscovery is True. ' +
408 'Not creating device for %s.'
409 % ip )
410 return
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
686 """
687 Called by Twisted once a connection has been established.
688 """
689 d = self.configure()
690 d.addCallback(self.startDiscovery)
691 d.addErrback(self.reportError)
692
693
694
709
710
712 """
713 Execute a script that will auto allocate devices into their
714 Device Classes
715
716 @param device: device object
717 @type device: device object
718 @return: Device class path to put the new device
719 @rtype: string
720 @todo: make it actually work
721 """
722 self.log.debug("trying to auto-allocate device %s" % device.id )
723 if not device:
724 return
725 script = getattr(device, "zAutoAllocateScript", None)
726 self.log.debug("no auto-allocation script found")
727 if script:
728 import string
729 script = string.join(script, "\n")
730 self.log.debug("using script\n%s" % script)
731 try:
732 compile(script, "zAutoAllocateScript", "exec")
733 except:
734 self.log.error("zAutoAllocateScript contains error")
735 return
736 vars = {'dev': device, 'log': self.log}
737 try:
738 exec(script, vars)
739 except:
740 self.log.error(
741 "error executing zAutoAllocateScript:\n%s" % script)
742 return vars.get('devicePath', None)
743 return
744
745
747 """
748 Command-line option builder for optparse
749 """
750 ZenModeler.buildOptions(self)
751 self.parser.add_option('--net', dest='net', action="append",
752 help="Discover all device on this network")
753 self.parser.add_option('--range', dest='range', action='append',
754 help="Discover all IPs in this range")
755 self.parser.add_option('--deviceclass', dest='deviceclass',
756 default="/Discovered",
757 help="Default device class for discovered devices")
758 self.parser.add_option('--prod_state', dest='productionState',
759 default=1000,
760 help="Initial production state for discovered devices")
761 self.parser.add_option('--remodel', dest='remodel',
762 action="store_true", default=False,
763 help="Remodel existing objects")
764 self.parser.add_option('--routers', dest='routersonly',
765 action="store_true", default=False,
766 help="Only discover routers")
767 self.parser.add_option('--tries', dest='tries', default=1, type="int",
768 help="How many ping tries")
769 self.parser.add_option('--timeout', dest='timeout',
770 default=2, type="float",
771 help="ping timeout in seconds")
772 self.parser.add_option('--chunk', dest='chunkSize',
773 default=10, type="int",
774 help="number of in flight ping packets")
775 self.parser.add_option('--snmp-missing', dest='snmpMissing',
776 action="store_true", default=False,
777 help="Send an event if SNMP is not found on the device")
778 self.parser.add_option('--add-inactive', dest='addInactive',
779 action="store_true", default=False,
780 help="Add all IPs found, even if they are unresponsive")
781 self.parser.add_option('--reset-ptr', dest='resetPtr',
782 action="store_true", default=False,
783 help="Reset all ip PTR records")
784 self.parser.add_option('--no-snmp', dest='nosnmp',
785 action="store_true", default=False,
786 help="Skip SNMP discovery on found IP addresses")
787 self.parser.add_option('--subnets', dest='subnets',
788 action="store_true", default=False,
789 help="Recurse into subnets for discovery")
790 self.parser.add_option('--useFileDescriptor',
791 dest='useFileDescriptor', default=None,
792 help="Use the given (privileged) file descriptor for ping")
793 self.parser.add_option('--assign-devclass-script', dest='autoAllocate',
794 action="store_true", default=False,
795 help="have zendisc auto allocate devices after discovery")
796 self.parser.add_option('--walk', dest='walk', action='store_true',
797 default=False,
798 help="Walk the route tree, performing discovery on all networks")
799 self.parser.add_option('--max-devices', dest='maxdevices',
800 default=0,
801 type='int',
802 help="Collect a maximum number of devices. Default is no limit.")
803 self.parser.add_option('--snmp-strict-discovery',
804 dest='zSnmpStrictDiscovery',
805 action="store_true", default=False,
806 help="Only add devices that can be modeled via snmp." )
807 self.parser.add_option('--prefer-snmp-naming',
808 dest='zPreferSnmpNaming',
809 action="store_true", default=False,
810 help="Prefer snmp name to dns name when modeling via snmp." )
811
812
813 self.parser.add_option('--job', dest='job', help=SUPPRESS_HELP )
814
815
816
817 if __name__ == "__main__":
818 d = ZenDisc()
819 d.processOptions()
820 reactor.run = d.reactorLoop
821 d.run()
822