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 transaction.commit()
284 if REQUEST:
285 messaging.IMessageSender(self).sendToBrowser(title='Devices Moved',
286 body="Devices were moved to %s." % moveTarget)
287 REQUEST['message'] = "Devices moved to %s" % moveTarget
288 if not isinstance(REQUEST, FakeRequest):
289 REQUEST['RESPONSE'].redirect(target.getPrimaryUrlPath())
290 else:
291 if REQUEST.has_key('oneKeyValueSoInstanceIsntEmptyAndEvalToFalse'):
292 return REQUEST['message']
293 else:
294 return self.callZenScreen(REQUEST)
295
296
297 - def removeDevices(self, deviceNames=None, deleteStatus=False,
298 deleteHistory=False, deletePerf=False,REQUEST=None):
299 """
300 See IManageDevice overrides DeviceManagerBase.removeDevices
301 """
302 if not deviceNames: return self()
303 if type(deviceNames) in types.StringTypes: deviceNames = (deviceNames,)
304 for devname in deviceNames:
305 dev = self.findDevice(devname)
306 dev.deleteDevice(deleteStatus=deleteStatus,
307 deleteHistory=deleteHistory, deletePerf=deletePerf)
308 if REQUEST:
309 messaging.IMessageSender(self).sendToBrowser(
310 'Devices Deleted',
311 "Devices were deleted: %s." % ', '.join(deviceNames)
312 )
313 if REQUEST.has_key('oneKeyValueSoInstanceIsntEmptyAndEvalToFalse'):
314 return 'Devices were deleted: %s.' % ', '.join(deviceNames)
315 else:
316 return self.callZenScreen(REQUEST)
317
318
319 security.declareProtected('View', 'getEventDeviceInfo')
336
337
338 security.declareProtected('View', 'getDeviceWinInfo')
340 """
341 Return list of (devname,user,passwd,url) for each device.
342 user and passwd are used to connect via wmi.
343 """
344 ffunc = None
345 starttime = time.time()
346 if lastPoll > 0:
347 lastPoll = DateTime.DateTime(lastPoll)
348 ffunc = lambda x: x.getSnmpLastCollection() > lastPoll
349 if eventlog:
350 ffunc = lambda x: x.zWinEventlog
351 devinfo = []
352 for dev in self.getSubDevices(devfilter=ffunc):
353 if not dev.monitorDevice(): continue
354 if getattr(dev, 'zWmiMonitorIgnore', False): continue
355 user = getattr(dev,'zWinUser','')
356 passwd = getattr(dev, 'zWinPassword', '')
357 sev = getattr(dev, 'zWinEventlogMinSeverity', '')
358 devinfo.append((dev.id, str(user), str(passwd), sev, dev.absolute_url()))
359 return starttime, devinfo
360
361
363 """
364 Return a list of (devname, user, passwd, {'EvtSys':0,'Exchange':0})
365 """
366 svcinfo = []
367 allsvcs = {}
368 for s in self.getSubComponents("WinService"):
369 svcs=allsvcs.setdefault(s.hostname(),{})
370 name = s.name()
371 if type(name) == type(u''):
372 name = name.encode(s.zCollectorDecoding)
373 svcs[name] = (s.getStatus(), s.getAqProperty('zFailSeverity'))
374 for dev in self.getSubDevices():
375 if not dev.monitorDevice(): continue
376 if getattr(dev, 'zWmiMonitorIgnore', False): continue
377 svcs = allsvcs.get(dev.getId(), {})
378 if not svcs and not dev.zWinEventlog: continue
379 user = getattr(dev,'zWinUser','')
380 passwd = getattr(dev, 'zWinPassword', '')
381 svcinfo.append((dev.id, str(user), str(passwd), svcs))
382 return svcinfo
383
384
385 security.declareProtected('View', 'searchDeviceSummary')
387 """
388 Search device summary index and return device objects
389 """
390 if not query: return []
391 zcatalog = self._getCatalog()
392 if not zcatalog: return []
393 results = zcatalog({'summary':query})
394 return self._convertResultsToObj(results)
395
396
397 security.declareProtected('View', 'searchInterfaces')
399 """
400 Search interfaces index and return interface objects
401 """
402 if not query: return []
403 zcatalog = getattr(self, 'interfaceSearch', None)
404 if not zcatalog: return []
405 results = zcatalog(query)
406 return self._convertResultsToObj(results)
407
408
419
421 query = Or(MatchGlob('id', devicename),
422 Eq('getDeviceIp', devicename))
423 return self._getCatalog().evalAdvancedQuery(query)
424
426 """
427 Look up a device and return its path
428 """
429 ret = self._findDevice(devicename)
430 if not ret: return ""
431 return ret[0].getPrimaryId
432
434 """
435 Look up device in catalog and return it
436 """
437 ret = self._findDevice(devicename)
438 if ret: return ret[0].getObject()
439
441 """
442 Look up device in catalog and return it. devicename
443 must match device id exactly
444 """
445 for brains in self._getCatalog()(id=devicename):
446 dev = brains.getObject()
447 if dev.id == devicename:
448 return dev
449
451 """
452 look up device in catalog and return its pingStatus
453 """
454 dev = self.findDevice(devicename)
455 if dev: return dev.getPingStatusNumber()
456
457
459 """
460 Return generator of components, by meta_type if specified
461 """
462 zcat = self.componentSearch
463 res = zcat({'meta_type': meta_type, 'monitored': monitored})
464 for b in res:
465 try:
466 c = self.getObjByPath(b.getPrimaryId)
467 if self.checkRemotePerm("View", c):
468 yield c
469 except KeyError:
470 log.warn("bad path '%s' in index 'componentSearch'",
471 b.getPrimaryId)
472
473
474 security.declareProtected("ZenCommon", "getMonitoredComponents")
476 """
477 Return monitored components for devices within this DeviceDeviceClass
478 """
479 return self.getSubComponents()
480
481
482 security.declareProtected('View', 'getRRDTemplates')
484 """
485 Return the actual RRDTemplate instances.
486 """
487 templates = {}
488 if not context: context = self
489 mychain = aq_chain(context)
490 mychain.reverse()
491 for obj in mychain:
492 try:
493 templates.update(dict([(t.id, t) for t in obj.rrdTemplates()]))
494 except AttributeError:
495 pass
496 return templates.values()
497
498
500 """
501 Returns all available templates
502 """
503 def cmpTemplates(a, b):
504 return cmp(a.id.lower(), b.id.lower())
505 templates = self.getRRDTemplates()
506 templates.sort(cmpTemplates)
507 pdc = self.getPythonDeviceClass()
508 return [ t for t in templates
509 if issubclass(pdc, t.getTargetPythonClass()) ]
510
511
513 """
514 This will bind available templates to the zDeviceTemplates
515 """
516 return self.setZenProperty('zDeviceTemplates', ids, REQUEST)
517
531
532
534 """
535 Return all RRDTemplates at this level and below in the object tree.
536 If rrdts is provided then it must be a list of RRDTemplates which
537 will be extended with the templates from here and returned.
538
539 The original getAllRRDTemplates() method has been renamed
540 getAllRRDTemplatesPainfully(). It walks the object tree looking
541 for templates which is a very slow way of going about things.
542 The newer RRDTemplate.YieldAllRRDTemplate() method uses the
543 searchRRDTemplates catalog to speed things up dramatically.
544 YieldAllRRDTemplates is smart enough to revert to
545 getAllRRDTemplatesPainfully if the catalog is not present.
546
547 The searchRRDTemplates catalog was added in 2.2
548 """
549 if rrdts == None:
550 rrdts = []
551 rrdts.extend(RRDTemplate.YieldAllRRDTemplates(self))
552 return rrdts
553
554
556 """
557 RRDTemplate.YieldAllRRDTemplates() is probably what you want.
558 It takes advantage of the searchRRDTemplates catalog to get
559 much better performance. This method iterates over objects looking
560 for templates which is a slow, painful process.
561 """
562 if rrdts is None: rrdts = []
563 rrdts.extend(self.rrdTemplates())
564 for dev in self.devices():
565 rrdts += dev.objectValues('RRDTemplate')
566 for comps in dev.getDeviceComponents():
567 rrdts += comps.objectValues('RRDTemplate')
568 for child in self.children():
569 child.getAllRRDTemplatesPainfully(rrdts)
570 return rrdts
571
572
573 security.declareProtected('Add DMD Objects', 'manage_addRRDTemplate')
588
589
590 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
591 'manage_copyRRDTemplates')
610
611
612 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
613 'manage_pasteRRDTemplates')
651
652
653 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
654 'manage_copyAndPasteRRDTemplates')
675
676
677 security.declareProtected(ZEN_EDIT_LOCAL_TEMPLATES,
678 'manage_deleteRRDTemplates')
702
703
705 """
706 Make the catalog for device searching
707 """
708 from Products.ZCatalog.ZCatalog import manage_addZCatalog
709
710
711 manage_addZCatalog(self, self.default_catalog,
712 self.default_catalog)
713 zcat = self._getOb(self.default_catalog)
714 cat = zcat._catalog
715 for idxname in ['id',
716 'getDeviceIp','getDeviceClassPath','getProdState']:
717 cat.addIndex(idxname, makeCaseInsensitiveFieldIndex(idxname))
718 cat.addIndex('getPhysicalPath', makePathIndex('getPhysicalPath'))
719 cat.addIndex('path', makeMultiPathIndex('path'))
720 zcat.addColumn('getPrimaryId')
721 zcat.addColumn('id')
722 zcat.addColumn('path')
723
724
725 manage_addZCatalog(self, "componentSearch", "componentSearch")
726 zcat = self._getOb("componentSearch")
727 cat = zcat._catalog
728 cat.addIndex('meta_type', makeCaseInsensitiveFieldIndex('meta_type'))
729 cat.addIndex('getParentDeviceName',
730 makeCaseInsensitiveFieldIndex('getParentDeviceName'))
731 cat.addIndex('getCollectors',
732 makeCaseInsensitiveKeywordIndex('getCollectors'))
733
734
735 zcat.addIndex('monitored', 'FieldIndex')
736 zcat.addColumn('getPrimaryId')
737 zcat.addColumn('meta_type')
738
739
753
754
756 """
757 Create a new device tree with a default configuration
758 """
759 devs = self.getDmdRoot("Devices")
760 if getattr(aq_base(devs), "zSnmpCommunities", False): return
761
762
763 devs._setProperty("zPythonClass", "")
764
765
766 devs._setProperty("zProdStateThreshold", 300, type="int")
767
768
769 devs._setProperty("zIfDescription", False, type="boolean")
770
771
772 devs._setProperty("zSnmpCommunities",["public", "private"],type="lines")
773 devs._setProperty("zSnmpCommunity", "public")
774 devs._setProperty("zSnmpPort", 161, type="int")
775 devs._setProperty("zSnmpVer", "v1")
776 devs._setProperty("zSnmpTries", 2, type="int")
777 devs._setProperty("zSnmpTimeout", 2.5, type="float")
778 devs._setProperty("zSnmpSecurityName", "")
779 devs._setProperty("zSnmpAuthPassword", "")
780 devs._setProperty("zSnmpPrivPassword", "")
781 devs._setProperty("zSnmpAuthType", "")
782 devs._setProperty("zSnmpPrivType", "")
783 devs._setProperty("zRouteMapCollectOnlyLocal", False, type="boolean")
784 devs._setProperty("zRouteMapCollectOnlyIndirect", False, type="boolean")
785 devs._setProperty("zRouteMapMaxRoutes", 500, type="int")
786 devs._setProperty("zInterfaceMapIgnoreTypes", "")
787 devs._setProperty("zInterfaceMapIgnoreNames", "")
788 devs._setProperty("zFileSystemMapIgnoreTypes", [], type="lines")
789 devs._setProperty("zFileSystemMapIgnoreNames", "")
790 devs._setProperty("zFileSystemSizeOffset", 1.0, type="float")
791 devs._setProperty("zHardDiskMapMatch", "")
792 devs._setProperty("zSysedgeDiskMapIgnoreNames", "")
793 devs._setProperty("zIpServiceMapMaxPort", 1024, type="int")
794 devs._setProperty("zDeviceTemplates", ["Device"], type="lines")
795 devs._setProperty("zLocalIpAddresses", "^127|^0\.0|^169\.254|^224")
796 devs._setProperty("zLocalInterfaceNames", "^lo|^vmnet")
797
798
799
800
801
802
803
804
805 devs._setProperty("zPingInterfaceName", "")
806 devs._setProperty("zPingInterfaceDescription", "")
807
808
809 devs._setProperty("zSnmpMonitorIgnore", False, type="boolean")
810 devs._setProperty("zPingMonitorIgnore", False, type="boolean")
811 devs._setProperty("zWmiMonitorIgnore", True, type="boolean")
812 devs._setProperty("zStatusConnectTimeout", 15.0, type="float")
813
814
815 devs._setProperty("zCollectorPlugins", [], type='lines')
816 devs._setProperty("zCollectorClientTimeout", 180, type="int")
817 devs._setProperty("zCollectorDecoding", 'latin-1')
818 devs._setProperty("zCommandUsername", "")
819 devs._setProperty("zCommandPassword", "")
820 devs._setProperty("zCommandProtocol", "ssh")
821 devs._setProperty("zCommandPort", 22, type="int")
822 devs._setProperty("zCommandLoginTries", 1, type="int")
823 devs._setProperty("zCommandLoginTimeout", 10.0, type="float")
824 devs._setProperty("zCommandCommandTimeout", 10.0, type="float")
825 devs._setProperty("zCommandSearchPath", [], type="lines")
826 devs._setProperty("zCommandExistanceTest", "test -f %s")
827 devs._setProperty("zCommandPath", zenPath("libexec"))
828 devs._setProperty("zTelnetLoginRegex", "ogin:.$")
829 devs._setProperty("zTelnetPasswordRegex", "assword:")
830 devs._setProperty("zTelnetSuccessRegexList",
831 ['\$.$', '\#.$'], type="lines")
832 devs._setProperty("zTelnetEnable", False, type="boolean")
833 devs._setProperty("zTelnetEnableRegex", "assword:")
834 devs._setProperty("zTelnetTermLength", True, type="boolean")
835 devs._setProperty("zTelnetPromptTimeout", 10.0, type="float")
836 devs._setProperty("zKeyPath", "~/.ssh/id_dsa")
837 devs._setProperty("zMaxOIDPerRequest", 40, type="int")
838
839
840 devs._setProperty("zLinks", "")
841
842
843
844
845
846
847 devs._setProperty("zWinUser", "")
848 devs._setProperty("zWinPassword", "")
849 devs._setProperty("zWinEventlogMinSeverity", 2, type="int")
850 devs._setProperty("zWinEventlog", False, type="boolean")
851
852
853 devs._setProperty("zIcon", "/zport/dmd/img/icons/noicon.png")
854
855
857 """
858 Provide a set of default options for a zProperty
859
860 @param propname: zProperty name
861 @type propname: string
862 @return: list of zProperty options
863 @rtype: list
864 """
865 if propname == 'zCollectorPlugins':
866 from Products.DataCollector.Plugins import loadPlugins
867 names = [ldr.pluginName for ldr in loadPlugins(self.dmd)]
868 names.sort()
869 return names
870 if propname == 'zCommandProtocol':
871 return ['ssh', 'telnet']
872 if propname == 'zSnmpVer':
873 return ['v1', 'v2c', 'v3']
874 if propname == 'zSnmpAuthType':
875 return ['', 'MD5', 'SHA']
876 if propname == 'zSnmpPrivType':
877 return ['', 'DES', 'AES']
878 return DeviceOrganizer.zenPropertyOptions(self, propname)
879
880
882 """
883 This will result in a push of all the devices to live collectors
884
885 @param REQUEST: Zope REQUEST object
886 @type REQUEST: Zope REQUEST object
887 """
888 self._p_changed = True
889 if REQUEST:
890 messaging.IMessageSender(self).sendToBrowser(
891 'Pushed Changes',
892 'Changes to %s were pushed to collectors.' % self.id
893 )
894 return self.callZenScreen(REQUEST)
895
896
897 security.declareProtected('Change Device', 'setLastChange')
899 """
900 Set the changed datetime for this device.
901
902 @param value: changed datetime. Default is now.
903 @type value: number
904 """
905 if value is None:
906 value = time.time()
907 self._lastChange = float(value)
908
910 """
911 Define this class in terms of a description of the devices it should
912 contain and the protocol by which they would normally be monitored.
913 """
914 t = (description, protocol)
915 if not hasattr(self, 'devtypes'):
916 self._setProperty('devtypes', [], 'lines')
917 if t not in self.devtypes:
918 self.devtypes.append(t)
919 self._p_changed = True
920
922 t = (description, protocol)
923 if hasattr(self, 'devtypes'):
924 if t in self.devtypes:
925 self.devtypes.remove(t)
926 self._p_changed = True
927
928
929 InitializeClass(DeviceClass)
930