1
2
3
4
5
6
7
8
9
10
11
12
13
14 __doc__="""DeviceClass
15 The primary organizer of device objects, managing zProperties and
16 their acquisition.
17 """
18
19 import types
20 import time
21 import transaction
22 import logging
23 log = logging.getLogger('zen.DeviceClass')
24
25 import DateTime
26 from Globals import DTMLFile
27 from Globals import InitializeClass
28 from Acquisition import aq_base, aq_chain
29 from AccessControl import ClassSecurityInfo
30 from AccessControl import Permissions as permissions
31
32 from Products.AdvancedQuery import MatchGlob, Or, Eq
33 from Products.CMFCore.utils import getToolByName
34
35 from Products.ZenModel.ZenossSecurity import *
36 from Products.ZenRelations.RelSchema import *
37 from Products.ZenUtils.Search import makeCaseInsensitiveFieldIndex
38 from Products.ZenUtils.Search import makeCaseInsensitiveKeywordIndex
39 from Products.ZenUtils.Search import makePathIndex, makeMultiPathIndex
40 from Products.ZenUtils.Utils import importClass, zenPath
41 from Products.ZenWidgets import messaging
42
43 from Products.ZenUtils.FakeRequest import FakeRequest
44
45 import RRDTemplate
46 from DeviceOrganizer import DeviceOrganizer
47 from ZenPackable import ZenPackable
48 from TemplateContainer import TemplateContainer
49
50 _marker = "__MARKER___"
51
58
59
60 addDeviceClass = DTMLFile('dtml/addDeviceClass',globals())
61
62
63 -class DeviceClass(DeviceOrganizer, ZenPackable, TemplateContainer):
64 """
65 DeviceClass is a device organizer that manages the primary classification
66 of device objects within the Zenoss system. It manages properties
67 that are inherited through acquisition that modify the behavior of
68 many different sub systems within Zenoss.
69 It also handles the creation of new devices in the system.
70 """
71
72
73 dmdRootName = "Devices"
74
75 manageDeviceSearch = DTMLFile('dtml/manageDeviceSearch',globals())
76 manageDeviceSearchResults = DTMLFile('dtml/manageDeviceSearchResults',
77 globals())
78
79 portal_type = meta_type = event_key = "DeviceClass"
80
81 default_catalog = 'deviceSearch'
82
83 _properties = DeviceOrganizer._properties + (
84 {'id':'devtypes', 'type':'lines', 'mode':'w'},
85 )
86
87 _relations = DeviceOrganizer._relations + ZenPackable._relations + \
88 TemplateContainer._relations + (
89 ("devices", ToManyCont(ToOne,"Products.ZenModel.Device","deviceClass")),
90 )
91
92
93 factory_type_information = (
94 {
95 'id' : 'DeviceClass',
96 'meta_type' : 'DeviceClass',
97 'description' : """Base class for all devices""",
98 'icon' : 'DeviceClass_icon.gif',
99 'product' : 'ZenModel',
100 'factory' : 'manage_addDeviceClass',
101 'immediate_view' : 'deviceOrganizerStatus',
102 'actions' :
103 (
104 { 'name' : 'Classes'
105 , 'action' : 'deviceOrganizerStatus'
106 , 'permissions' : ( permissions.view, )
107 },
108 { 'name' : 'Events'
109 , 'action' : 'viewEvents'
110 , 'permissions' : ( permissions.view, )
111 },
112 { 'name' : 'zProperties'
113 , 'action' : 'zPropertyEdit'
114 , 'permissions' : (permissions.view,)
115 },
116 { 'name' : 'Templates'
117 , 'action' : 'perfConfig'
118 , 'permissions' : ('Manage DMD',)
119 },
120 )
121 },
122 )
123
124 security = ClassSecurityInfo()
125
127 """
128 Return a list of all device paths that have the Python class pyclass
129
130 @param pyclass: Python class (default is this class)
131 @type pyclass: Python class
132 @return: list of device paths
133 @rtype: list of strings
134 """
135 dcnames = []
136 if pyclass == None:
137 pyclass = self.getPythonDeviceClass()
138 dclass = self.getDmdRoot("Devices")
139 for orgname in dclass.getOrganizerNames():
140 org = dclass.getOrganizer(orgname)
141 if pyclass == org.getPythonDeviceClass():
142 dcnames.append(orgname)
143 dcnames.sort(lambda a, b: cmp(a.lower(), b.lower()))
144 return dcnames
145
146 deviceMoveTargets = getPeerDeviceClassNames
147 childMoveTargets = getPeerDeviceClassNames
148
149
151 """
152 Create an instance based on its location in the device tree
153 walk up the primary aq path looking for a python instance class that
154 matches the name of the closest node in the device tree.
155
156 @param id: id in DMD path
157 @type id: string
158 @return: new device object
159 @rtype: device object
160 """
161 pyClass = self.getPythonDeviceClass()
162 dev = pyClass(id)
163 self.devices._setObject(id, dev)
164 return self.devices._getOb(id)
165
166
168 """
169 Return the Python class object to be used for device instances in this
170 device class. This is done by walking up the aq_chain of a deviceclass
171 to find a node that has the same name as a Python class or has an
172 attribute named zPythonClass that matches a Python class.
173
174 @return: device class
175 @rtype: device class
176 """
177 from Device import Device
178 cname = getattr(self, "zPythonClass", None)
179 if cname:
180 try:
181 return importClass(cname)
182 except ImportError:
183 log.exception("Unable to import class " + cname)
184 return Device
185
186
187 - def moveDevices(self, moveTarget, deviceNames=None, REQUEST=None):
188 """
189 Override default moveDevices because this is a contained relation.
190 If the Python class bound to a DeviceClass is different we convert to
191 the new Python class adding / removing relationships as needed.
192
193 @param moveTarget: organizer in DMD path
194 @type moveTarget: string
195 @param deviceNames: devices to move
196 @type deviceNames: list of stringa
197 @param REQUEST: Zope REQUEST object
198 @type REQUEST: Zope REQUEST object
199 """
200 if not moveTarget or not deviceNames: return self()
201 target = self.getDmdRoot(self.dmdRootName).getOrganizer(moveTarget)
202 if type(deviceNames) == types.StringType: deviceNames = (deviceNames,)
203 for devname in deviceNames:
204 dev = self.findDeviceExact(devname)
205 if not dev: continue
206 source = dev.deviceClass().primaryAq()
207 if dev.__class__ != target.getPythonDeviceClass():
208 import StringIO
209 from Products.ZenRelations.ImportRM import NoLoginImportRM
210
211 def switchClass(o, module, klass):
212 """
213 Create an XML string representing the module in a
214 new class.
215
216 @param o: file-type object
217 @type o: file-type object
218 @param module: location in DMD
219 @type module: string
220 @param klass: class name
221 @type klass: string
222 @return: XML representation of the class
223 @rtype: string
224 """
225 o.seek(0)
226 l = o.readline()
227 al = l[1:-2].split()
228 for i in range(len(al)):
229 if al[i].startswith('module'):
230 al[i] = "module='%s'" % module
231 elif al[i].startswith('class'):
232 al[i] = "class='%s'" % klass
233 nl = "<" + " ".join(al) + ">\n"
234 o.seek(0)
235 nf = ["<objects>", nl]
236 nf.extend(o.readlines()[1:])
237 nf.append('</objects>')
238 return StringIO.StringIO("".join(nf))
239
240 def devExport(d, module, klass):
241 """
242 Create an XML string representing the device d
243 at the DMD location module of type klass.
244
245 @param module: location in DMD
246 @type module: string
247 @param klass: class name
248 @type klass: string
249 @return: XML representation of the class
250 @rtype: string
251 """
252 o = StringIO.StringIO()
253 d.exportXml(o)
254 return switchClass(o, module, klass)
255
256 def devImport(xmlfile):
257 """
258 Load a new device from a file.
259
260 @param xmlfile: file type object
261 @type xmlfile: file type object
262 """
263 im = NoLoginImportRM(target.devices)
264 im.loadObjectFromXML(xmlfile)
265
266 module = target.zPythonClass
267 if module:
268 klass = target.zPythonClass.split('.')[-1]
269 else:
270 module = 'Products.ZenModel.Device'
271 klass = 'Device'
272 xmlfile = devExport(dev, module,klass)
273 source.devices._delObject(devname)
274 devImport(xmlfile)
275 else:
276 dev._operation = 1
277 source.devices._delObject(devname)
278 target.devices._setObject(devname, dev)
279 dev = target.devices._getOb(devname)
280 dev.setLastChange()
281 dev.setAdminLocalRoles()
282 dev.index_object()
283 if REQUEST:
284 messaging.IMessageSender(self).sendToBrowser(title='Devices Moved',
285 body="Devices were moved to %s." % moveTarget)
286 REQUEST['message'] = "Devices moved to %s" % moveTarget
287 if not isinstance(REQUEST, FakeRequest):
288 REQUEST['RESPONSE'].redirect(target.getPrimaryUrlPath())
289 else:
290 if REQUEST.has_key('oneKeyValueSoInstanceIsntEmptyAndEvalToFalse'):
291 return REQUEST['message']
292 else:
293 return self.callZenScreen(REQUEST)
294
295
296 - def removeDevices(self, deviceNames=None, deleteStatus=False,
297 deleteHistory=False, deletePerf=False,REQUEST=None):
298 """
299 See IManageDevice overrides DeviceManagerBase.removeDevices
300 """
301 if not deviceNames: return self()
302 if type(deviceNames) in types.StringTypes: deviceNames = (deviceNames,)
303 for devname in deviceNames:
304 dev = self.findDevice(devname)
305 dev.deleteDevice(deleteStatus=deleteStatus,
306 deleteHistory=deleteHistory, deletePerf=deletePerf)
307 if REQUEST:
308 messaging.IMessageSender(self).sendToBrowser(
309 'Devices Deleted',
310 "Devices were deleted: %s." % ', '.join(deviceNames)
311 )
312 if REQUEST.has_key('oneKeyValueSoInstanceIsntEmptyAndEvalToFalse'):
313 return 'Devices were deleted: %s.' % ', '.join(deviceNames)
314 else:
315 return self.callZenScreen(REQUEST)
316
317
318 security.declareProtected('View', 'getEventDeviceInfo')
335
336
337 security.declareProtected('View', 'getDeviceWinInfo')
339 """
340 Return list of (devname,user,passwd,url) for each device.
341 user and passwd are used to connect via wmi.
342 """
343 ffunc = None
344 starttime = time.time()
345 if lastPoll > 0:
346 lastPoll = DateTime.DateTime(lastPoll)
347 ffunc = lambda x: x.getSnmpLastCollection() > lastPoll
348 if eventlog:
349 ffunc = lambda x: x.zWinEventlog
350 devinfo = []
351 for dev in self.getSubDevices(devfilter=ffunc):
352 if not dev.monitorDevice(): continue
353 if getattr(dev, 'zWmiMonitorIgnore', False): continue
354 user = getattr(dev,'zWinUser','')
355 passwd = getattr(dev, 'zWinPassword', '')
356 sev = getattr(dev, 'zWinEventlogMinSeverity', '')
357 devinfo.append((dev.id, str(user), str(passwd), sev, dev.absolute_url()))
358 return starttime, devinfo
359
360
362 """
363 Return a list of (devname, user, passwd, {'EvtSys':0,'Exchange':0})
364 """
365 svcinfo = []
366 allsvcs = {}
367 for s in self.getSubComponents("WinService"):
368 svcs=allsvcs.setdefault(s.hostname(),{})
369 name = s.name()
370 if type(name) == type(u''):
371 name = name.encode(s.zCollectorDecoding)
372 svcs[name] = (s.getStatus(), s.getAqProperty('zFailSeverity'))
373 for dev in self.getSubDevices():
374 if not dev.monitorDevice(): continue
375 if getattr(dev, 'zWmiMonitorIgnore', False): continue
376 svcs = allsvcs.get(dev.getId(), {})
377 if not svcs and not dev.zWinEventlog: continue
378 user = getattr(dev,'zWinUser','')
379 passwd = getattr(dev, 'zWinPassword', '')
380 svcinfo.append((dev.id, str(user), str(passwd), svcs))
381 return svcinfo
382
383
384 security.declareProtected('View', 'searchDeviceSummary')
386 """
387 Search device summary index and return device objects
388 """
389 if not query: return []
390 zcatalog = self._getCatalog()
391 if not zcatalog: return []
392 results = zcatalog({'summary':query})
393 return self._convertResultsToObj(results)
394
395
396 security.declareProtected('View', 'searchInterfaces')
398 """
399 Search interfaces index and return interface objects
400 """
401 if not query: return []
402 zcatalog = getattr(self, 'interfaceSearch', None)
403 if not zcatalog: return []
404 results = zcatalog(query)
405 return self._convertResultsToObj(results)
406
407
418
420 query = Or(MatchGlob('id', devicename),
421 Eq('getDeviceIp', devicename))
422 return self._getCatalog().evalAdvancedQuery(query)
423
425 """
426 Look up a device and return its path
427 """
428 ret = self._findDevice(devicename)
429 if not ret: return ""
430 return ret[0].getPrimaryId
431
433 """
434 Look up device in catalog and return it
435 """
436 ret = self._findDevice(devicename)
437 if ret: return ret[0].getObject()
438
440 """
441 Look up device in catalog and return it. devicename
442 must match device id exactly
443 """
444 for brains in self._getCatalog()(id=devicename):
445 dev = brains.getObject()
446 if dev.id == devicename:
447 return dev
448
450 """
451 look up device in catalog and return its pingStatus
452 """
453 dev = self.findDevice(devicename)
454 if dev: return dev.getPingStatusNumber()
455
456
458 """
459 Return generator of components, by meta_type if specified
460 """
461 zcat = self.componentSearch
462 res = zcat({'meta_type': meta_type, 'monitored': monitored})
463 for b in res:
464 try:
465 c = self.getObjByPath(b.getPrimaryId)
466 if self.checkRemotePerm("View", c):
467 yield c
468 except KeyError:
469 log.warn("bad path '%s' in index 'componentSearch'",
470 b.getPrimaryId)
471
472
473 security.declareProtected("ZenCommon", "getMonitoredComponents")
475 """
476 Return monitored components for devices within this DeviceDeviceClass
477 """
478 return self.getSubComponents()
479
480
481 security.declareProtected('View', 'getRRDTemplates')
483 """
484 Return the actual RRDTemplate instances.
485 """
486 templates = {}
487 if not context: context = self
488 mychain = aq_chain(context)
489 mychain.reverse()
490 for obj in mychain:
491 try:
492 templates.update(dict([(t.id, t) for t in obj.rrdTemplates()]))
493 except AttributeError:
494 pass
495 return templates.values()
496
497
499 """
500 Returns all available templates
501 """
502 def cmpTemplates(a, b):
503 return cmp(a.id.lower(), b.id.lower())
504 templates = self.getRRDTemplates()
505 templates.sort(cmpTemplates)
506 pdc = self.getPythonDeviceClass()
507 return [ t for t in templates
508 if issubclass(pdc, t.getTargetPythonClass()) ]
509
510
512 """
513 This will bind available templates to the zDeviceTemplates
514 """
515 return self.setZenProperty('zDeviceTemplates', ids, REQUEST)
516
530
531
533 """
534 Return all RRDTemplates at this level and below in the object tree.
535 If rrdts is provided then it must be a list of RRDTemplates which
536 will be extended with the templates from here and returned.
537
538 The original getAllRRDTemplates() method has been renamed
539 getAllRRDTemplatesPainfully(). It walks the object tree looking
540 for templates which is a very slow way of going about things.
541 The newer RRDTemplate.YieldAllRRDTemplate() method uses the
542 searchRRDTemplates catalog to speed things up dramatically.
543 YieldAllRRDTemplates is smart enough to revert to
544 getAllRRDTemplatesPainfully if the catalog is not present.
545
546 The searchRRDTemplates catalog was added in 2.2
547 """
548 if rrdts == None:
549 rrdts = []
550 rrdts.extend(RRDTemplate.YieldAllRRDTemplates(self))
551 return rrdts
552
553
555 """
556 RRDTemplate.YieldAllRRDTemplates() is probably what you want.
557 It takes advantage of the searchRRDTemplates catalog to get
558 much better performance. This method iterates over objects looking
559 for templates which is a slow, painful process.
560 """
561 if rrdts is None: rrdts = []
562 rrdts.extend(self.rrdTemplates())
563 for dev in self.devices():
564 rrdts += dev.objectValues('RRDTemplate')
565 for comps in dev.getDeviceComponents():
566 rrdts += comps.objectValues('RRDTemplate')
567 for child in self.children():
568 child.getAllRRDTemplatesPainfully(rrdts)
569 return rrdts
570
571
572 security.declareProtected('Add DMD Objects', 'manage_addRRDTemplate')
587
588
589 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
590 'manage_copyRRDTemplates')
609
610
611 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
612 'manage_pasteRRDTemplates')
650
651
652 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
653 'manage_copyAndPasteRRDTemplates')
674
675
676 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
677 'manage_deleteRRDTemplates')
701
702
704 """
705 Make the catalog for device searching
706 """
707 from Products.ZCatalog.ZCatalog import manage_addZCatalog
708
709
710 manage_addZCatalog(self, self.default_catalog,
711 self.default_catalog)
712 zcat = self._getOb(self.default_catalog)
713 cat = zcat._catalog
714 for idxname in ['id',
715 'getDeviceIp','getDeviceClassPath','getProdState']:
716 cat.addIndex(idxname, makeCaseInsensitiveFieldIndex(idxname))
717 cat.addIndex('getPhysicalPath', makePathIndex('getPhysicalPath'))
718 cat.addIndex('path', makeMultiPathIndex('path'))
719 zcat.addColumn('getPrimaryId')
720 zcat.addColumn('id')
721 zcat.addColumn('path')
722
723
724 manage_addZCatalog(self, "componentSearch", "componentSearch")
725 zcat = self._getOb("componentSearch")
726 cat = zcat._catalog
727 cat.addIndex('meta_type', makeCaseInsensitiveFieldIndex('meta_type'))
728 cat.addIndex('getParentDeviceName',
729 makeCaseInsensitiveFieldIndex('getParentDeviceName'))
730 cat.addIndex('getCollectors',
731 makeCaseInsensitiveKeywordIndex('getCollectors'))
732
733
734 zcat.addIndex('monitored', 'FieldIndex')
735 zcat.addColumn('getPrimaryId')
736 zcat.addColumn('meta_type')
737
738
752
753
755 """
756 Create a new device tree with a default configuration
757 """
758 devs = self.getDmdRoot("Devices")
759 if getattr(aq_base(devs), "zSnmpCommunities", False): return
760
761
762 devs._setProperty("zPythonClass", "")
763
764
765 devs._setProperty("zProdStateThreshold", 300, type="int")
766
767
768 devs._setProperty("zIfDescription", False, type="boolean")
769
770
771 devs._setProperty("zSnmpCommunities",["public", "private"],type="lines")
772 devs._setProperty("zSnmpCommunity", "public")
773 devs._setProperty("zSnmpPort", 161, type="int")
774 devs._setProperty("zSnmpVer", "v1")
775 devs._setProperty("zSnmpTries", 2, type="int")
776 devs._setProperty("zSnmpTimeout", 2.5, type="float")
777 devs._setProperty("zSnmpSecurityName", "")
778 devs._setProperty("zSnmpAuthPassword", "")
779 devs._setProperty("zSnmpPrivPassword", "")
780 devs._setProperty("zSnmpAuthType", "")
781 devs._setProperty("zSnmpPrivType", "")
782 devs._setProperty("zRouteMapCollectOnlyLocal", False, type="boolean")
783 devs._setProperty("zRouteMapCollectOnlyIndirect", False, type="boolean")
784 devs._setProperty("zRouteMapMaxRoutes", 500, type="int")
785 devs._setProperty("zInterfaceMapIgnoreTypes", "")
786 devs._setProperty("zInterfaceMapIgnoreNames", "")
787 devs._setProperty("zFileSystemMapIgnoreTypes", [], type="lines")
788 devs._setProperty("zFileSystemMapIgnoreNames", "")
789 devs._setProperty("zFileSystemSizeOffset", 1.0, type="float")
790 devs._setProperty("zHardDiskMapMatch", "")
791 devs._setProperty("zSysedgeDiskMapIgnoreNames", "")
792 devs._setProperty("zIpServiceMapMaxPort", 1024, type="int")
793 devs._setProperty("zDeviceTemplates", ["Device"], type="lines")
794 devs._setProperty("zLocalIpAddresses", "^127|^0\.0|^169\.254|^224")
795 devs._setProperty("zLocalInterfaceNames", "^lo|^vmnet")
796
797
798
799
800
801
802
803
804 devs._setProperty("zPingInterfaceName", "")
805 devs._setProperty("zPingInterfaceDescription", "")
806
807
808 devs._setProperty("zSnmpMonitorIgnore", False, type="boolean")
809 devs._setProperty("zPingMonitorIgnore", False, type="boolean")
810 devs._setProperty("zWmiMonitorIgnore", True, type="boolean")
811 devs._setProperty("zStatusConnectTimeout", 15.0, type="float")
812
813
814 devs._setProperty("zCollectorPlugins", [], type='lines')
815 devs._setProperty("zCollectorClientTimeout", 180, type="int")
816 devs._setProperty("zCollectorDecoding", 'latin-1')
817 devs._setProperty("zCommandUsername", "")
818 devs._setProperty("zCommandPassword", "")
819 devs._setProperty("zCommandProtocol", "ssh")
820 devs._setProperty("zCommandPort", 22, type="int")
821 devs._setProperty("zCommandLoginTries", 1, type="int")
822 devs._setProperty("zCommandLoginTimeout", 10.0, type="float")
823 devs._setProperty("zCommandCommandTimeout", 10.0, type="float")
824 devs._setProperty("zCommandSearchPath", [], type="lines")
825 devs._setProperty("zCommandExistanceTest", "test -f %s")
826 devs._setProperty("zCommandPath", zenPath("libexec"))
827 devs._setProperty("zTelnetLoginRegex", "ogin:.$")
828 devs._setProperty("zTelnetPasswordRegex", "assword:")
829 devs._setProperty("zTelnetSuccessRegexList",
830 ['\$.$', '\#.$'], type="lines")
831 devs._setProperty("zTelnetEnable", False, type="boolean")
832 devs._setProperty("zTelnetEnableRegex", "assword:")
833 devs._setProperty("zTelnetTermLength", True, type="boolean")
834 devs._setProperty("zTelnetPromptTimeout", 10.0, type="float")
835 devs._setProperty("zKeyPath", "~/.ssh/id_dsa")
836 devs._setProperty("zMaxOIDPerRequest", 40, type="int")
837
838
839 devs._setProperty("zLinks", "")
840
841
842
843
844
845
846 devs._setProperty("zWinUser", "")
847 devs._setProperty("zWinPassword", "")
848 devs._setProperty("zWinEventlogMinSeverity", 2, type="int")
849 devs._setProperty("zWinEventlog", False, type="boolean")
850
851
852 devs._setProperty("zIcon", "/zport/dmd/img/icons/noicon.png")
853
854
856 """
857 Provide a set of default options for a zProperty
858
859 @param propname: zProperty name
860 @type propname: string
861 @return: list of zProperty options
862 @rtype: list
863 """
864 if propname == 'zCollectorPlugins':
865 from Products.DataCollector.Plugins import loadPlugins
866 names = [ldr.pluginName() for ldr in loadPlugins(self.dmd)]
867 names.sort()
868 return names
869 if propname == 'zCommandProtocol':
870 return ['ssh', 'telnet']
871 if propname == 'zSnmpVer':
872 return ['v1', 'v2c', 'v3']
873 if propname == 'zSnmpAuthType':
874 return ['', 'MD5', 'SHA']
875 if propname == 'zSnmpPrivType':
876 return ['', 'DES', 'AES']
877 return DeviceOrganizer.zenPropertyOptions(self, propname)
878
879
881 """
882 This will result in a push of all the devices to live collectors
883
884 @param REQUEST: Zope REQUEST object
885 @type REQUEST: Zope REQUEST object
886 """
887 self._p_changed = True
888 if REQUEST:
889 messaging.IMessageSender(self).sendToBrowser(
890 'Pushed Changes',
891 'Changes to %s were pushed to collectors.' % self.id
892 )
893 return self.callZenScreen(REQUEST)
894
895
896 security.declareProtected('Change Device', 'setLastChange')
898 """
899 Set the changed datetime for this device.
900
901 @param value: changed datetime. Default is now.
902 @type value: number
903 """
904 if value is None:
905 value = time.time()
906 self._lastChange = float(value)
907
909 """
910 Define this class in terms of a description of the devices it should
911 contain and the protocol by which they would normally be monitored.
912 """
913 t = (description, protocol)
914 if not hasattr(self, 'devtypes'):
915 self._setProperty('devtypes', [], 'lines')
916 if t not in self.devtypes:
917 self.devtypes.append(t)
918 self._p_changed = True
919
921 t = (description, protocol)
922 if hasattr(self, 'devtypes'):
923 if t in self.devtypes:
924 self.devtypes.remove(t)
925 self._p_changed = True
926
927
928 InitializeClass(DeviceClass)
929