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