1
2
3
4
5
6
7
8
9
10
11 __doc__ = """zenbatchload
12
13 zenbatchload loads a list of devices read from a file.
14 """
15
16 import sys
17 import re
18 from traceback import format_exc
19 import socket
20
21 import Globals
22 from DateTime import DateTime
23 from ZODB.POSException import ConflictError
24 from ZODB.transact import transact
25 from zope.component import getUtility
26
27 from zExceptions import BadRequest
28
29 from ZPublisher.Converters import type_converters
30 from Products.ZenModel.interfaces import IDeviceLoader
31 from Products.ZenUtils.ZCmdBase import ZCmdBase
32 from Products.ZenModel.Device import Device
33 from Products.ZenRelations.ZenPropertyManager import iszprop
34 from Products.ZenModel.ZenModelBase import iscustprop
35 from Products.ZenEvents.ZenEventClasses import Change_Add
36
37 from zenoss.protocols.protobufs.zep_pb2 import SEVERITY_INFO, SEVERITY_ERROR
41 """
42 Base class wrapping around dmd.DeviceLoader
43 """
44
45 sample_configs = """#
46 # Example zenbatchloader device file
47 #
48 # This file is formatted with one entry per line, like this:
49 #
50 # /Devices/device_class_name Python-expression
51 # hostname Python-expression
52 #
53 # For organizers (ie the /Devices path), the Python-expression
54 # is used to define defaults to be used for devices listed
55 # after the organizer. The defaults that can be specified are:
56 #
57 # * loader arguments (use the --show_options flag to show these)
58 # * zPropertie (from a device, use the More -> zProperties
59 # menu option to see the available ones.)
60 #
61 # NOTE: new zProperties *cannot* be created through this file
62 #
63 # The Python-expression is used to create a dictionary of settings.
64 # device_settings = eval( 'dict(' + python-expression + ')' )
65 #
66
67
68 # Setting locations
69 /Locations/Canada address="Canada"
70
71 /Locations/Canada/Alberta address="Alberta, Canada"
72
73 /Locations/Canada/Alberta/Calgary address="Calgary, Alberta, Canada"
74
75
76 # If no organizer is specified at the beginning of the file,
77 # defaults to the /Devices/Discovered device class.
78 device0 comments="A simple device"
79 # All settings must be seperated by a comma.
80 device1 comments="A simple device", zSnmpCommunity='blue', zSnmpVer='v1'
81
82 # Notes for this file:
83 # * Oraganizer names *must* start with '/'
84 #
85 /Devices/Server/Linux zSnmpPort=1543
86 # Python strings can use either ' or " -- there's no difference.
87 # As a special case, it is also possible to specify the IP address
88 linux_device1 setManageIp='10.10.10.77', zSnmpCommunity='blue', zSnmpVer="v2c"
89 # A '\' at the end of the line allows you to place more
90 # expressions on a new line. Don't forget the comma...
91 linux_device2 zLinks="<a href='http://example.org'>Support site</a>", \
92 zTelnetEnable=True, \
93 zTelnetPromptTimeout=15.3
94
95 # A new organizer drops all previous settings, and allows
96 # for new ones to be used. Settings do not span files.
97 /Devices/Server/Windows zWinUser="administrator", zWinPassword='fred'
98 # Bind templates
99 windows_device1 zDeviceTemplates=[ 'Device', 'myTemplate' ]
100 # Override the default from the organizer setting.
101 windows_device2 zWinUser="administrator", zWinPassword='thomas'
102
103 # Apply other settings to the device
104 settingsDevice setManageIp='10.10.10.77', setLocation="123 Elm Street", \
105 setSystems=['/mySystems'], setPerformanceMonitor='remoteCollector1', \
106 setHWSerialNumber="abc123456789", setGroups=['/myGroup'], \
107 setHWProduct=('myproductName','manufacturer'), setOSProduct=('OS Name','manufacturer')
108
109 # If the device or device class contains a space, then it must be quoted (either ' or ")
110 "/Server/Windows/WMI/Active Directory/2008"
111
112 # Now, what if we have a device that isn't really a device, and requires
113 # a special loader?
114 # The 'loader' setting requires a registered utility, and 'loader_arg_keys' is
115 # a list from which any other settings will be passed into the loader callable.
116 #
117 # Here is a commmented-out example of how a VMware endpoint might be added:
118 #
119 #/Devices/VMware loader='vmware', loader_arg_keys=['host', 'username', 'password', 'useSsl', 'id']
120 #esxwin2 id='esxwin2', host='esxwin2.zenoss.loc', username='testuser', password='password', useSsl=True
121
122 # Apply custom schema properties (c-properties) to a device
123 windows_device7 cDateTest='2010/02/28'
124
125 """
126
128 ZCmdBase.__init__(self, *args, **kwargs)
129 self.defaults = {}
130
131 self.loader = self.dmd.DeviceLoader.loadDevice
132
133 self.fqdn = socket.getfqdn()
134 self.baseEvent = dict(
135 device=self.fqdn,
136 component='',
137 agent='zenbatchload',
138 monitor='localhost',
139 manager=self.fqdn,
140 severity=SEVERITY_ERROR,
141
142
143
144 eventClass=Change_Add,
145 )
146
147
148 self.loader_args = dict.fromkeys( self.loader.func_code.co_varnames )
149 unsupportable_args = [
150 'REQUEST', 'device', 'self', 'xmlrpc', 'e', 'handler',
151 ]
152 for opt in unsupportable_args:
153 if opt in self.loader_args:
154 del self.loader_args[opt]
155
157 """
158 Read through all of the files listed as arguments and
159 return a list of device entries.
160
161 @parameter args: list of filenames (uses self.args is this is None)
162 @type args: list of strings
163 @return: list of device specifications
164 @rtype: list of dictionaries
165 """
166 if args is None:
167 args = self.args
168
169 device_list = []
170 for filename in args:
171 if filename.strip() != '':
172 try:
173 data = open(filename,'r').readlines()
174 except IOError:
175 msg = "Unable to open the file '%s'" % filename
176 self.reportException(msg)
177 continue
178
179 temp_dev_list = self.parseDevices(data)
180 if temp_dev_list:
181 device_list += temp_dev_list
182
183 return device_list
184
186 """
187 Apply zProperty settings (if any) to the device.
188
189 @parameter device: device to modify
190 @type device: DMD device object
191 @parameter device_specs: device creation dictionary
192 @type device_specs: dictionary
193 """
194 self.log.debug( "Applying zProperties..." )
195
196
197 dev_zprops = dict( device.zenPropertyItems() )
198
199 for zprop, value in device_specs.items():
200 self.log.debug( "Evaluating zProperty <%s -> %s> on %s" % (zprop, value, device.id) )
201 if not iszprop(zprop):
202 self.log.debug( "Evaluating zProperty <%s -> %s> on %s: not iszprop()" % (zprop, value, device.id) )
203 continue
204
205 if zprop in dev_zprops:
206 try:
207 self.log.debug( "Setting zProperty <%s -> %s> on %s (currently set to %s)" % (
208 zprop, value, device.id, getattr(device, zprop, 'notset')) )
209 device.setZenProperty(zprop, value)
210 except BadRequest:
211 self.log.warn( "Object %s zproperty %s is invalid or duplicate" % (
212 device.titleOrId(), zprop) )
213 except Exception, ex:
214 self.log.warn( "Object %s zproperty %s not set (%s)" % (
215 device.titleOrId(), zprop, ex) )
216 self.log.debug( "Set zProperty <%s -> %s> on %s (now set to %s)" % (
217 zprop, value, device.id, getattr(device, zprop, 'notset')) )
218 else:
219 self.log.warn( "The zproperty %s doesn't exist in %s" % (
220 zprop, device_specs.get('deviceName', device.id)))
221
223 """
224 Custom schema properties
225 """
226 self.log.debug( "Applying custom schema properties..." )
227 dev_cprops = device.custPropertyMap()
228
229 for cprop, value in device_specs.items():
230 if not iscustprop(cprop):
231 continue
232
233 matchProps = [prop for prop in dev_cprops if prop['id'] == cprop]
234 if matchProps:
235 ctype = matchProps[0]['type']
236 if ctype == 'password':
237 ctype = 'string'
238 if ctype in type_converters and value:
239 value = type_converters[ctype](value)
240 device.setZenProperty( cprop, value)
241 else:
242 self.log.warn( "The cproperty %s doesn't exist in %s" % (
243 cprop, device_specs.get('deviceName', device.id)))
244
246 location = device_specs.get('setLocation')
247 if location:
248 self.addLGSOrganizer('Locations', (location,) )
249
250 systems = device_specs.get('setSystems')
251 if systems:
252 if not isinstance(systems, list) and not isinstance(systems, tuple):
253 systems = (systems,)
254 self.addLGSOrganizer('Systems', systems)
255
256 groups = device_specs.get('setGroups')
257 if groups:
258 if not isinstance(groups, list) and not isinstance(groups, tuple):
259 groups = (groups,)
260 self.addLGSOrganizer('Groups', groups)
261
279
280 @transact
282 """
283 Add any organizers as required, and apply zproperties to them.
284 """
285 path = device_specs.get('devicePath')
286 baseOrg = path.split('/', 2)[1]
287 base = getattr(self.dmd, baseOrg, None)
288 if base is None:
289 self.log.error("The base of path %s (%s) does not exist -- skipping",
290 baseOrg, path)
291 return
292
293 try:
294 org = base.getDmdObj(path)
295 except KeyError:
296 try:
297 self.log.info("Creating organizer %s", path)
298 @transact
299 def inner():
300 base.manage_addOrganizer(path)
301 inner()
302 org = base.getDmdObj(path)
303 except IOError, ex:
304 self.log.error("Unable to create organizer! Is Rabbit up and configured correctly?")
305 sys.exit(1)
306 self.applyZProps(org, device_specs)
307 self.applyOtherProps(org, device_specs)
308
310 """
311 Apply non-zProperty settings (if any) to the device.
312
313 @parameter device: device to modify
314 @type device: DMD device object
315 @parameter device_specs: device creation dictionary
316 @type device_specs: dictionary
317 """
318 self.log.debug( "Applying other properties..." )
319 internalVars = [
320 'deviceName', 'devicePath', 'comments', 'loader', 'loader_arg_keys',
321 ]
322 @transact
323 def setNamedProp(org, name, description):
324 setattr(org, name, description)
325
326 for functor, value in device_specs.items():
327 if iszprop(functor) or iscustprop(functor) or functor in internalVars:
328 continue
329
330
331 if functor in ('description', 'address'):
332 if hasattr(device, functor):
333 setNamedProp(device, functor, value)
334 continue
335
336 try:
337 self.log.debug("For %s, calling device.%s(%s)",
338 device.id, functor, value)
339 func = getattr(device, functor, None)
340 if func is None or not callable(func):
341 self.log.warn("The function '%s' for device %s is not found.",
342 functor, device.id)
343 elif isinstance(value, (list, tuple)):
344
345 try:
346 func(*value)
347 except TypeError:
348 func(value)
349 else:
350 func(value)
351 except ConflictError:
352 raise
353 except Exception:
354 msg = "Device %s device.%s(%s) failed" % (device.id, functor, value)
355 self.reportException(msg, device.id)
356
358 """
359 It's up to the loader now to figure out what's going on.
360
361 @parameter loader: device loader
362 @type loader: callable
363 @parameter device_specs: device entries
364 @type device_specs: dictionary
365 """
366 argKeys = device_specs.get('loader_arg_keys', [])
367 loader_args = {}
368 for key in argKeys:
369 if key in device_specs:
370 loader_args[key] = device_specs[key]
371
372 result = loader().load_device(self.dmd, **loader_args)
373
374
375
376 if isinstance(result, Device):
377 return result
378 return None
379
381 """
382 Read the input and process the devices
383 * create the device entry
384 * set zproperties
385 * set custom schema properties
386 * model the device
387
388 @parameter device_list: list of device entries
389 @type device_list: list of dictionaries
390 """
391
392 def transactional(f):
393 return f if self.options.nocommit else transact(f)
394
395 processed = {'total':0, 'errors':0}
396
397 @transactional
398 def _process(device_specs):
399
400 self.dmd.zport._p_jar.sync()
401
402 loaderName = device_specs.get('loader')
403 if loaderName is not None:
404 try:
405 orgName = device_specs['devicePath']
406 organizer = self.dmd.getObjByPath('dmd' + orgName)
407 deviceLoader = getUtility(IDeviceLoader, loaderName, organizer)
408 devobj = self.runLoader(deviceLoader, device_specs)
409 except ConflictError:
410 raise
411 except Exception:
412 devName = device_specs.get('device_specs', 'Unkown Device')
413 msg = "Ignoring device loader issue for %s" % devName
414 self.reportException(msg, devName, specs=str(device_specs))
415 processed['errors'] += 1
416 return
417 else:
418 devobj = self.getDevice(device_specs)
419 deviceLoader = None
420
421 if devobj is None:
422 if deviceLoader is not None:
423 processed['total'] += 1
424 else:
425 self.addAllLGSOrganizers(device_specs)
426 self.applyZProps(devobj, device_specs)
427 self.applyCustProps(devobj, device_specs)
428 self.applyOtherProps(devobj, device_specs)
429
430 return devobj
431
432 @transactional
433 def _snmp_community(device_specs, devobj):
434
435 if 'zSnmpCommunity' not in device_specs:
436 self.log.debug('Discovering SNMP version and community')
437 devobj.manage_snmpCommunity()
438
439 @transactional
440 def _model(devobj):
441 try:
442 devobj.collectDevice(setlog=self.options.showModelOutput)
443 except ConflictError:
444 raise
445 except Exception, ex:
446 msg = "Modeling error for %s" % devobj.id
447 self.reportException(msg, devobj.id, exception=str(ex))
448 processed['errors'] += 1
449 processed['total'] += 1
450
451 for device_specs in device_list:
452 devobj = _process(device_specs)
453
454
455
456 if devobj and not self.options.nocommit and not self.options.nomodel:
457 _snmp_community(device_specs, devobj)
458 _model(devobj)
459
460 self.reportResults(processed, len(device_list))
461
463 """
464 Report exceptions back to the the event console
465 """
466 self.log.exception(msg)
467 if not self.options.nocommit:
468 evt = self.baseEvent.copy()
469 evt.update(dict(
470 summary=msg,
471 traceback=format_exc()
472 ))
473 evt.update(kwargs)
474 if devName:
475 evt['device'] = devName
476 self.dmd.ZenEventManager.sendEvent(evt)
477
479 """
480 Report the success + total counts from loading devices.
481 """
482 msg = "Modeled %d of %d devices, with %d errors" % (
483 processed['total'], totalDevices, processed['errors'] )
484 self.log.info(msg)
485
486 if not self.options.nocommit:
487 evt = self.baseEvent.copy()
488 evt.update(dict(
489 severity=SEVERITY_INFO,
490 summary=msg,
491 modeled=processed['total'],
492 errors=processed['errors'],
493 total=totalDevices,
494 ))
495 self.dmd.ZenEventManager.sendEvent(evt)
496
498 """
499 Report that we added a new device.
500 """
501 if not self.options.nocommit:
502 evt = self.baseEvent.copy()
503 evt.update(dict(
504 severity=SEVERITY_INFO,
505 summary= "Added new device %s" % deviceName
506 ))
507 self.dmd.ZenEventManager.sendEvent(evt)
508
510 """
511 Find or create the specified device
512
513 @parameter device_specs: device creation dictionary
514 @type device_specs: dictionary
515 @return: device or None
516 @rtype: DMD device object
517 """
518 if 'deviceName' not in device_specs:
519 return None
520 name = device_specs['deviceName']
521 devobj = self.dmd.Devices.findDevice(name)
522 if devobj is not None:
523 self.log.info("Found existing device %s" % name)
524 return devobj
525
526 specs = {}
527 for key in self.loader_args:
528 if key in device_specs:
529 specs[key] = device_specs[key]
530
531 try:
532 self.log.info("Creating device %s" % name)
533
534
535 specs['discoverProto'] = 'none'
536
537 self.loader(**specs)
538 devobj = self.dmd.Devices.findDevice(name)
539 if devobj is None:
540 self.log.error("Unable to find newly created device %s -- skipping" \
541 % name)
542 else:
543 self.notifyNewDeviceCreated(name)
544
545 except Exception:
546 msg = "Unable to load %s -- skipping" % name
547 self.reportException(msg, name)
548
549 return devobj
550
552 """
553 Add our command-line options to the basics
554 """
555 ZCmdBase.buildOptions(self)
556
557 self.parser.add_option('--show_options',
558 dest="show_options", default=False,
559 action="store_true",
560 help="Show the various options understood by the loader")
561
562 self.parser.add_option('--sample_configs',
563 dest="sample_configs", default=False,
564 action="store_true",
565 help="Show an example configuration file.")
566
567 self.parser.add_option('--showModelOutput',
568 dest="showModelOutput", default=True,
569 action="store_false",
570 help="Show modelling activity")
571
572 self.parser.add_option('--nocommit',
573 dest="nocommit", default=False,
574 action="store_true",
575 help="Don't commit changes to the ZODB. Use for verifying config file.")
576
577 self.parser.add_option('--nomodel',
578 dest="nomodel", default=False,
579 action="store_true",
580 help="Don't model the remote devices. Must be able to commit changes.")
581
583 """
584 From the list of strings in rawDevices, construct a list
585 of device dictionaries, ready to load into Zenoss.
586
587 @parameter data: list of strings representing device entries
588 @type data: list of strings
589 @return: list of parsed device entries
590 @rtype: list of dictionaries
591 """
592 if not data:
593 return []
594
595 comment = re.compile(r'^\s*#.*')
596
597 defaults = {'devicePath':"/Discovered" }
598 finalList = []
599 i = 0
600 while i < len(data):
601 line = data[i]
602 line = re.sub(comment, '', line).strip()
603 if line == '':
604 i += 1
605 continue
606
607
608 while line[-1] == '\\' and i < len(data):
609 i += 1
610 line = line[:-1] + data[i]
611 line = re.sub(comment, '', line).strip()
612
613 if line[0] == '/' or line[1] == '/':
614 defaults = self.parseDeviceEntry(line, {})
615 if defaults is None:
616 defaults = {'devicePath':"/Discovered" }
617 else:
618 defaults['devicePath'] = defaults['deviceName']
619 del defaults['deviceName']
620 self.addOrganizer(defaults)
621
622 else:
623 configs = self.parseDeviceEntry(line, defaults)
624 if configs:
625 finalList.append(configs)
626 i += 1
627
628 return finalList
629
630 - def parseDeviceEntry(self, line, defaults):
631 """
632 Build a dictionary of properties from one line's input
633
634 @parameter line: string containing one device's info
635 @type line: string
636 @parameter defaults: dictionary of default settings
637 @type defaults: dictionary
638 @return: parsed device entry
639 @rtype: dictionary
640 """
641 options = []
642
643 if line[0] in ["'", '"']:
644 delim = line[0]
645 eom = line.find(delim, 1)
646 if eom == -1:
647 self.log.error("While reading name, unable to parse" \
648 " the entry for %s -- skipping", line )
649 return None
650 name = line[1:eom]
651 options = line[eom+1:]
652
653 else:
654 options = line.split(None, 1)
655 name = options.pop(0)
656 if options:
657 options = options.pop(0)
658
659 configs = defaults.copy()
660 configs['deviceName'] = name
661
662 if options:
663 try:
664
665 evalString = 'dict(' + options + '\n)'
666 configs.update(eval(evalString))
667 except:
668 self.log.error( "Unable to parse the entry for %s -- skipping" % name )
669 self.log.error( "Raw string: %s" % options )
670 return None
671
672 return configs
673
674
675 if __name__=='__main__':
676 batchLoader = BatchDeviceLoader()
677 if batchLoader.options.show_options:
678 print "Options = %s" % sorted( batchLoader.loader_args.keys() )
679 help(batchLoader.loader)
680 sys.exit(0)
681
682 if batchLoader.options.sample_configs:
683 print batchLoader.sample_configs
684 sys.exit(0)
685
686 device_list = batchLoader.loadDeviceList()
687 if not device_list:
688 batchLoader.log.warn("No device entries found to load.")
689 sys.exit(1)
690
691 batchLoader.processDevices(device_list)
692 sys.exit(0)
693