1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 __doc__ = """ImportRM
17 Import RelationshipManager objects into a Zope database
18 This provides support methods used by the Python xml.sax library to
19 parse and construct an XML document.
20
21 Descriptions of the XML document format can be found in the
22 Developers Guide.
23 """
24
25 import sys
26 import os
27 import types
28 import transaction
29 from DateTime import DateTime
30 from xml.sax import make_parser, saxutils
31 from xml.sax.handler import ContentHandler
32
33 from Acquisition import aq_base
34 from zExceptions import NotFound
35
36 from Products.ZenUtils.ZCmdBase import ZCmdBase
37 from Products.ZenUtils.Utils import importClass
38 from Products.ZenUtils.Utils import getObjByPath
39
40 from Products.ZenRelations.Exceptions import *
41
42
43 -class ImportRM(ZCmdBase, ContentHandler):
44 """
45 Wrapper module to interface between Zope and the Python SAX XML library.
46 The xml.sax.parse() calls different routines depending on what it finds.
47
48 A simple example of a valid XML file can be found in the objects.xml file
49 for a ZenPack.
50
51 <?xml version="1.0"?>
52 <objects>
53 <!-- ('', 'zport', 'dmd', 'Devices', 'rrdTemplates', 'HelloWorld') -->
54 <object id='/zport/dmd/Devices/rrdTemplates/HelloWorld' module='Products.ZenModel.RRDTemplate' class='RRDTemplate'>
55 <property type="text" id="description" mode="w" > This is the glorious description that shows up when we click on our RRD template </property>
56 <tomanycont id='datasources'>
57 <object id='hello' module='Products.ZenModel.BasicDataSource' class='BasicDataSource'>
58 <property select_variable="sourcetypes" type="selection" id="sourcetype" mode="w" > SNMP </property>
59 <property type="boolean" id="enabled" mode="w" > True </property>
60 <property type="string" id="eventClass" mode="w" > /Cmd/Fail </property>
61 <property type="int" id="severity" mode="w" > 3 </property>
62 <property type="int" id="cycletime" mode="w" > 300 </property>
63 <property type="boolean" id="usessh" mode="w" > False </property>
64 <tomanycont id='datapoints'>
65 <object id='hello' module='Products.ZenModel.RRDDataPoint' class='RRDDataPoint'>
66 <property select_variable="rrdtypes" type="selection" id="rrdtype" mode="w" > GAUGE </property>
67 <property type="boolean" id="isrow" mode="w" > True </property>
68 </object>
69 </tomanycont>
70 </object>
71
72 <!-- snip -->
73
74 </objects>
75 """
76 rootpath = ''
77 skipobj = 0
78
79 - def __init__(self, noopts=0, app=None, keeproot=False):
80 """
81 Initializer
82
83 @param noopts: don't use sys.argv for command-line options
84 @type noopts: boolean
85 @param app: app
86 @type app: object
87 @param keeproot: keeproot
88 @type keeproot: boolean
89 """
90 ZCmdBase.__init__(self, noopts, app, keeproot)
91 ContentHandler.__init__(self)
92
94 """
95 Return the bottom object in the stack
96
97 @return:
98 @rtype: object
99 """
100 return self.objstack[-1]
101
103 """
104 Convert all attributes to string values
105
106 @param attrs: (key,val) pairs
107 @type attrs: list
108 @return: same list, but with all values as strings
109 @rtype: list
110 """
111 myattrs = {}
112 for (key, val) in attrs.items():
113 myattrs[key] = str(val)
114 return myattrs
115
117 """
118 Function called when the parser finds the starting element
119 in the XML file.
120
121 @param name: name of the element
122 @type name: string
123 @param attrs: list of (key, value) tuples
124 @type attrs: list
125 """
126 ignoredElements = [ 'objects' ]
127 attrs = self.cleanattrs(attrs)
128 if self.skipobj > 0:
129 self.skipobj += 1
130 return
131
132 self.log.debug('tag %s, context %s, line %s' % (
133 name, self.context().id, self._locator.getLineNumber() ))
134
135 if name == 'object':
136
137 obj = self.createObject(attrs)
138
139 if obj is None:
140 formattedAttrs = ''
141 for key, value in attrs.items():
142 formattedAttrs += ' * %s: %s\n' % (key, value)
143 raise Exception('Unable to create object using the following '
144 'attributes:\n%s' % formattedAttrs)
145
146 if not self.options.noindex and hasattr(aq_base(obj),
147 'reIndex') and not self.rootpath:
148 self.rootpath = obj.getPrimaryId()
149
150 self.objstack.append(obj)
151
152 elif name == 'tomanycont' or name == 'tomany':
153 nextobj = self.context()._getOb(attrs['id'], None)
154 if nextobj is None:
155 self.skipobj = 1
156 return
157 else:
158 self.objstack.append(nextobj)
159 elif name == 'toone':
160 relname = attrs.get('id')
161 self.log.debug('toone %s, on object %s', relname,
162 self.context().id)
163 rel = getattr(aq_base(self.context()), relname, None)
164 if rel is None:
165 return
166 objid = attrs.get('objid')
167 self.addLink(rel, objid)
168 elif name == 'link':
169 self.addLink(self.context(), attrs['objid'])
170 elif name == 'property':
171 self.curattrs = attrs
172 elif name in ignoredElements:
173 pass
174 else:
175 self.log.warning( "Ignoring an unknown XML element type: %s" % name )
176
178 """
179 Function called when the parser finds the starting element
180 in the XML file.
181
182 @param name: name of the ending element
183 @type name: string
184 """
185 ignoredElements = [ 'toone', 'link' ]
186 if self.skipobj > 0:
187 self.skipobj -= 1
188 return
189
190 if name in ('object', 'tomany', 'tomanycont'):
191 obj = self.objstack.pop()
192 if hasattr(aq_base(obj), 'index_object'):
193 obj.index_object()
194 if self.rootpath == obj.getPrimaryId():
195 self.log.info('Calling reIndex %s', obj.getPrimaryId())
196 obj.reIndex()
197 self.rootpath = ''
198
199 elif name == 'objects':
200 self.log.info('End loading objects')
201 self.log.info('Processing links')
202 self.processLinks()
203 if not self.options.noCommit:
204 self.commit()
205 self.log.info('Loaded %d objects into the ZODB database'
206 % self.objectnumber)
207 else:
208 self.log.info('Would have created %d objects in the ZODB database'
209 % self.objectnumber)
210
211 elif name == 'property':
212 self.setProperty(self.context(), self.curattrs,
213 self.charvalue)
214
215
216 self.charvalue = ''
217
218 elif name in ignoredElements:
219 pass
220 else:
221 self.log.warning( "Ignoring an unknown XML element type: %s" % name )
222
224 """
225 Called by xml.sax.parse() with data found in an element
226 eg <object>my characters stuff</object>
227
228 Note that this can be called character by character.
229
230 @param chars: chars
231 @type chars: string
232 """
233 self.charvalue += saxutils.unescape(chars)
234
236 """
237 Create an object and set it into its container
238
239 @param attrs: attrs
240 @type attrs: string
241 @return: newly created object
242 @rtype: object
243 """
244
245 id = attrs.get('id')
246 obj = None
247 try:
248 if id.startswith('/'):
249 obj = getObjByPath(self.app, id)
250 else:
251 obj = self.context()._getOb(id)
252 except (KeyError, AttributeError, NotFound):
253 pass
254
255 if obj is None:
256 klass = importClass(attrs.get('module'), attrs.get('class'))
257 if id.find('/') > -1:
258 (contextpath, id) = os.path.split(id)
259 try:
260 pathobj = getObjByPath(self.context(), contextpath)
261 except (KeyError, AttributeError, NotFound):
262 self.log.warn( "Unable to find context path %s (line %s ?) for %s" % (
263 contextpath, self._locator.getLineNumber(), id ))
264 if not self.options.noCommit:
265 self.log.warn( "Not committing any changes" )
266 self.options.noCommit = True
267 return None
268 self.objstack.append(pathobj)
269 obj = klass(id)
270 self.context()._setObject(obj.id, obj)
271 obj = self.context()._getOb(obj.id)
272 self.objectnumber += 1
273 if self.objectnumber % 5000 == 0:
274 transaction.savepoint()
275 self.log.debug('Added object %s to database'
276 % obj.getPrimaryId())
277 else:
278 self.log.debug('Object %s already exists -- skipping' % id)
279 return obj
280
282 """
283 Set the value of a property on an object.
284
285 @param obj: obj
286 @type obj: string
287 @param attrs: attrs
288 @type attrs: string
289 @param value: value
290 @type value: string
291 @return:
292 @rtype:
293 """
294 name = attrs.get('id')
295 proptype = attrs.get('type')
296 setter = attrs.get('setter', None)
297 self.log.debug('Setting object %s attr %s type %s value %s'
298 % (obj.id, name, proptype, value))
299 linenum = self._locator.getLineNumber()
300
301
302 value = value.strip()
303 try:
304 value = str(value)
305 except UnicodeEncodeError:
306 self.log.warn('UnicodeEncodeError at line %s while attempting' % linenum + \
307 ' str(%s) for object %s attribute %s -- ignoring' % (
308 obj.id, name, proptype, value))
309
310
311 if proptype == 'selection':
312 try:
313 firstElement = getattr(obj, name)[0]
314 if type(firstElement) in types.StringTypes:
315 proptype = 'string'
316 except (TypeError, IndexError):
317 self.log.warn("Error at line %s when trying to " % linenum + \
318 " use (%s) as the value for object %s attribute %s -- assuming a string"
319 % (obj.id, name, proptype, value))
320 proptype = 'string'
321
322 if proptype == 'date':
323 try:
324 value = float(value)
325 except ValueError:
326 pass
327 value = DateTime(value)
328
329 elif proptype != 'string' and proptype != 'text':
330 try:
331 value = eval(value)
332 except SyntaxError:
333 self.log.debug("Non-fatal SyntaxError at line %s while eval'ing '%s'" % (
334 linenum, value) )
335
336
337 if not obj.hasProperty(name):
338 obj._setProperty(name, value, type=proptype, setter=setter)
339 else:
340 obj._updateProperty(name, value)
341
343 """
344 Build list of links to form after all objects have been created
345 make sure that we don't add other side of a bidirectional relation
346
347 @param rel: relationship object
348 @type rel: relation object
349 @param objid: objid
350 @type objid: string
351 """
352 self.links.append((rel.getPrimaryId(), objid))
353
355 """
356 Walk through all the links that we saved and link them up
357 """
358 for (relid, objid) in self.links:
359 try:
360 self.log.debug('Linking relation %s to object %s',
361 relid, objid)
362 rel = getObjByPath(self.app, relid)
363 obj = getObjByPath(self.app, objid)
364 if not rel.hasobject(obj):
365 rel.addRelation(obj)
366 except:
367 self.log.critical('Failed linking relation %s to object %s' % (
368 relid, objid))
369
371 """
372 Command-line options specific to this command
373 """
374 ZCmdBase.buildOptions(self)
375 self.parser.add_option('-i', '--infile', dest='infile',
376 help='Input file for import. Default is stdin'
377 )
378 self.parser.add_option('--noindex', dest='noindex',
379 action='store_true', default=False,
380 help='Do not try to index the freshly loaded objects.'
381 )
382 self.parser.add_option(
383 '-n',
384 '--noCommit',
385 dest='noCommit',
386 action='store_true',
387 default=0,
388 help='Do not store changes to the DMD (ie for debugging purposes)',
389 )
390
392 """
393 This method can be used to load data for the root of Zenoss (default
394 behavior) or it can be used to operate on a specific point in the
395 Zenoss hierarchy (ZODB).
396
397 Upon loading the XML file to be processed, the content of the XML file
398 is handled by the methods in this class when called by xml.sax.parse().
399
400 Reads from a file if xmlfile is specified, otherwise reads
401 from the command-line option --infile. If no files are found from
402 any of these places, read from standard input.
403
404 @param xmlfile: Name of XML file to load, or file-like object
405 @type xmlfile: string or file-like object
406 """
407 self.objstack = [self.app]
408 self.links = []
409 self.objectnumber = 0
410 self.charvalue = ''
411 if xmlfile and type(xmlfile) in types.StringTypes:
412 self.infile = open(xmlfile)
413 elif hasattr(xmlfile, 'read'):
414 self.infile = xmlfile
415 elif self.options.infile:
416 self.infile = open(self.options.infile)
417 else:
418 self.infile = sys.stdin
419 parser = make_parser()
420 parser.setContentHandler(self)
421 try:
422 parser.parse(self.infile)
423 finally:
424 self.infile.close()
425
427 """
428 The default behavior of loadObjectFromXML() will be to use the Zope
429 app object, and thus operatate on the whole of Zenoss.
430 """
431 self.loadObjectFromXML()
432
434 """
435 Wrapper around the Zope database commit()
436 """
437 trans = transaction.get()
438 trans.note('Import from file %s using %s'
439 % (self.options.infile, self.__class__.__name__))
440 trans.commit()
441
442
444 """
445 SpoofedOptions
446 """
447
449 self.infile = ''
450 self.noCommit = True
451 self.noindex = True
452
453
455 """
456 An ImportRM that does not call the __init__ method on ZCmdBase
457 """
458
460 """
461 Initializer
462
463 @param app: app
464 @type app: string
465 """
466 self.app = app
467 ContentHandler.__init__(self)
468 import logging
469 self.log = logging.getLogger('zen.ImportRM')
470 self.options = SpoofedOptions()
471
472
473 if __name__ == '__main__':
474 im = ImportRM()
475 im.loadDatabase()
476