1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 #$Id: back_anydbm.py,v 1.34 2002-05-15 06:21:21 richard Exp $
19 '''
20 This module defines a backend that saves the hyperdatabase in a database
21 chosen by anydbm. It is guaranteed to always be available in python
22 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
23 serious bugs, and is not available)
24 '''
26 import whichdb, anydbm, os, marshal
27 from roundup import hyperdb, date
28 from blobfiles import FileStorage
29 from locking import acquire_lock, release_lock
31 #
32 # Now the database
33 #
34 class Database(FileStorage, hyperdb.Database):
35 """A database for storing records containing flexible data types.
37 Transaction stuff TODO:
38 . check the timestamp of the class file and nuke the cache if it's
39 modified. Do some sort of conflict checking on the dirty stuff.
40 . perhaps detect write collisions (related to above)?
42 """
43 def __init__(self, config, journaltag=None):
44 """Open a hyperdatabase given a specifier to some storage.
46 The 'storagelocator' is obtained from config.DATABASE.
47 The meaning of 'storagelocator' depends on the particular
48 implementation of the hyperdatabase. It could be a file name,
49 a directory path, a socket descriptor for a connection to a
50 database over the network, etc.
52 The 'journaltag' is a token that will be attached to the journal
53 entries for any edits done on the database. If 'journaltag' is
54 None, the database is opened in read-only mode: the Class.create(),
55 Class.set(), and Class.retire() methods are disabled.
56 """
57 self.config, self.journaltag = config, journaltag
58 self.dir = config.DATABASE
59 self.classes = {}
60 self.cache = {} # cache of nodes loaded or created
61 self.dirtynodes = {} # keep track of the dirty nodes by class
62 self.newnodes = {} # keep track of the new nodes by class
63 self.transactions = []
64 # ensure files are group readable and writable
65 os.umask(0002)
67 def __repr__(self):
68 return '<back_anydbm instance at %x>'%id(self)
70 #
71 # Classes
72 #
73 def __getattr__(self, classname):
74 """A convenient way of calling self.getclass(classname)."""
75 if self.classes.has_key(classname):
76 if __debug__:
77 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
78 return self.classes[classname]
79 raise AttributeError, classname
81 def addclass(self, cl):
82 if __debug__:
83 print >>hyperdb.DEBUG, 'addclass', (self, cl)
84 cn = cl.classname
85 if self.classes.has_key(cn):
86 raise ValueError, cn
87 self.classes[cn] = cl
89 def getclasses(self):
90 """Return a list of the names of all existing classes."""
91 if __debug__:
92 print >>hyperdb.DEBUG, 'getclasses', (self,)
93 l = self.classes.keys()
94 l.sort()
95 return l
97 def getclass(self, classname):
98 """Get the Class object representing a particular class.
100 If 'classname' is not a valid class name, a KeyError is raised.
101 """
102 if __debug__:
103 print >>hyperdb.DEBUG, 'getclass', (self, classname)
104 return self.classes[classname]
106 #
107 # Class DBs
108 #
109 def clear(self):
110 '''Delete all database contents
111 '''
112 if __debug__:
113 print >>hyperdb.DEBUG, 'clear', (self,)
114 for cn in self.classes.keys():
115 for dummy in 'nodes', 'journals':
116 path = os.path.join(self.dir, 'journals.%s'%cn)
117 if os.path.exists(path):
118 os.remove(path)
119 elif os.path.exists(path+'.db'): # dbm appends .db
120 os.remove(path+'.db')
122 def getclassdb(self, classname, mode='r'):
123 ''' grab a connection to the class db that will be used for
124 multiple actions
125 '''
126 if __debug__:
127 print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
128 return self._opendb('nodes.%s'%classname, mode)
130 def _opendb(self, name, mode):
131 '''Low-level database opener that gets around anydbm/dbm
132 eccentricities.
133 '''
134 if __debug__:
135 print >>hyperdb.DEBUG, '_opendb', (self, name, mode)
137 # determine which DB wrote the class file
138 db_type = ''
139 path = os.path.join(os.getcwd(), self.dir, name)
140 if os.path.exists(path):
141 db_type = whichdb.whichdb(path)
142 if not db_type:
143 raise hyperdb.DatabaseError, "Couldn't identify database type"
144 elif os.path.exists(path+'.db'):
145 # if the path ends in '.db', it's a dbm database, whether
146 # anydbm says it's dbhash or not!
147 db_type = 'dbm'
149 # new database? let anydbm pick the best dbm
150 if not db_type:
151 if __debug__:
152 print >>hyperdb.DEBUG, "_opendb anydbm.open(%r, 'n')"%path
153 return anydbm.open(path, 'n')
155 # open the database with the correct module
156 try:
157 dbm = __import__(db_type)
158 except ImportError:
159 raise hyperdb.DatabaseError, \
160 "Couldn't open database - the required module '%s'"\
161 "is not available"%db_type
162 if __debug__:
163 print >>hyperdb.DEBUG, "_opendb %r.open(%r, %r)"%(db_type, path,
164 mode)
165 return dbm.open(path, mode)
167 def _lockdb(self, name):
168 ''' Lock a database file
169 '''
170 path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
171 return acquire_lock(path)
173 #
174 # Node IDs
175 #
176 def newid(self, classname):
177 ''' Generate a new id for the given class
178 '''
179 # open the ids DB - create if if doesn't exist
180 lock = self._lockdb('_ids')
181 db = self._opendb('_ids', 'c')
182 if db.has_key(classname):
183 newid = db[classname] = str(int(db[classname]) + 1)
184 else:
185 # the count() bit is transitional - older dbs won't start at 1
186 newid = str(self.getclass(classname).count()+1)
187 db[classname] = newid
188 db.close()
189 release_lock(lock)
190 return newid
192 #
193 # Nodes
194 #
195 def addnode(self, classname, nodeid, node):
196 ''' add the specified node to its class's db
197 '''
198 if __debug__:
199 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
200 self.newnodes.setdefault(classname, {})[nodeid] = 1
201 self.cache.setdefault(classname, {})[nodeid] = node
202 self.savenode(classname, nodeid, node)
204 def setnode(self, classname, nodeid, node):
205 ''' change the specified node
206 '''
207 if __debug__:
208 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
209 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
211 # can't set without having already loaded the node
212 self.cache[classname][nodeid] = node
213 self.savenode(classname, nodeid, node)
215 def savenode(self, classname, nodeid, node):
216 ''' perform the saving of data specified by the set/addnode
217 '''
218 if __debug__:
219 print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
220 self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
222 def getnode(self, classname, nodeid, db=None, cache=1):
223 ''' get a node from the database
224 '''
225 if __debug__:
226 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
227 if cache:
228 # try the cache
229 cache_dict = self.cache.setdefault(classname, {})
230 if cache_dict.has_key(nodeid):
231 if __debug__:
232 print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
233 nodeid)
234 return cache_dict[nodeid]
236 if __debug__:
237 print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
239 # get from the database and save in the cache
240 if db is None:
241 db = self.getclassdb(classname)
242 if not db.has_key(nodeid):
243 raise IndexError, "no such %s %s"%(classname, nodeid)
245 # decode
246 res = marshal.loads(db[nodeid])
248 # reverse the serialisation
249 res = self.unserialise(classname, res)
251 # store off in the cache dict
252 if cache:
253 cache_dict[nodeid] = res
255 return res
257 def hasnode(self, classname, nodeid, db=None):
258 ''' determine if the database has a given node
259 '''
260 if __debug__:
261 print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
263 # try the cache
264 cache = self.cache.setdefault(classname, {})
265 if cache.has_key(nodeid):
266 if __debug__:
267 print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
268 return 1
269 if __debug__:
270 print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
272 # not in the cache - check the database
273 if db is None:
274 db = self.getclassdb(classname)
275 res = db.has_key(nodeid)
276 return res
278 def countnodes(self, classname, db=None):
279 if __debug__:
280 print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
281 # include the new nodes not saved to the DB yet
282 count = len(self.newnodes.get(classname, {}))
284 # and count those in the DB
285 if db is None:
286 db = self.getclassdb(classname)
287 count = count + len(db.keys())
288 return count
290 def getnodeids(self, classname, db=None):
291 if __debug__:
292 print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
293 # start off with the new nodes
294 res = self.newnodes.get(classname, {}).keys()
296 if db is None:
297 db = self.getclassdb(classname)
298 res = res + db.keys()
299 return res
302 #
303 # Files - special node properties
304 # inherited from FileStorage
306 #
307 # Journal
308 #
309 def addjournal(self, classname, nodeid, action, params):
310 ''' Journal the Action
311 'action' may be:
313 'create' or 'set' -- 'params' is a dictionary of property values
314 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
315 'retire' -- 'params' is None
316 '''
317 if __debug__:
318 print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
319 action, params)
320 self.transactions.append((self._doSaveJournal, (classname, nodeid,
321 action, params)))
323 def getjournal(self, classname, nodeid):
324 ''' get the journal for id
325 '''
326 if __debug__:
327 print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
328 # attempt to open the journal - in some rare cases, the journal may
329 # not exist
330 try:
331 db = self._opendb('journals.%s'%classname, 'r')
332 except anydbm.error, error:
333 if str(error) == "need 'c' or 'n' flag to open new db": return []
334 elif error.args[0] != 2: raise
335 return []
336 journal = marshal.loads(db[nodeid])
337 res = []
338 for entry in journal:
339 (nodeid, date_stamp, user, action, params) = entry
340 date_obj = date.Date(date_stamp)
341 res.append((nodeid, date_obj, user, action, params))
342 return res
344 def pack(self, pack_before):
345 ''' delete all journal entries before 'pack_before' '''
346 if __debug__:
347 print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
349 pack_before = pack_before.get_tuple()
351 classes = self.getclasses()
353 # TODO: factor this out to method - we're already doing it in
354 # _opendb.
355 db_type = ''
356 path = os.path.join(os.getcwd(), self.dir, classes[0])
357 if os.path.exists(path):
358 db_type = whichdb.whichdb(path)
359 if not db_type:
360 raise hyperdb.DatabaseError, "Couldn't identify database type"
361 elif os.path.exists(path+'.db'):
362 db_type = 'dbm'
364 for classname in classes:
365 db_name = 'journals.%s'%classname
366 db = self._opendb(db_name, 'w')
368 for key in db.keys():
369 journal = marshal.loads(db[key])
370 l = []
371 last_set_entry = None
372 for entry in journal:
373 (nodeid, date_stamp, self.journaltag, action,
374 params) = entry
375 if date_stamp > pack_before or action == 'create':
376 l.append(entry)
377 elif action == 'set':
378 # grab the last set entry to keep information on
379 # activity
380 last_set_entry = entry
381 if last_set_entry:
382 date_stamp = last_set_entry[1]
383 # if the last set entry was made after the pack date
384 # then it is already in the list
385 if date_stamp < pack_before:
386 l.append(last_set_entry)
387 db[key] = marshal.dumps(l)
388 if db_type == 'gdbm':
389 db.reorganize()
390 db.close()
393 #
394 # Basic transaction support
395 #
396 def commit(self):
397 ''' Commit the current transactions.
398 '''
399 if __debug__:
400 print >>hyperdb.DEBUG, 'commit', (self,)
401 # TODO: lock the DB
403 # keep a handle to all the database files opened
404 self.databases = {}
406 # now, do all the transactions
407 for method, args in self.transactions:
408 method(*args)
410 # now close all the database files
411 for db in self.databases.values():
412 db.close()
413 del self.databases
414 # TODO: unlock the DB
416 # all transactions committed, back to normal
417 self.cache = {}
418 self.dirtynodes = {}
419 self.newnodes = {}
420 self.transactions = []
422 def _doSaveNode(self, classname, nodeid, node):
423 if __debug__:
424 print >>hyperdb.DEBUG, '_doSaveNode', (self, classname, nodeid,
425 node)
427 # get the database handle
428 db_name = 'nodes.%s'%classname
429 if self.databases.has_key(db_name):
430 db = self.databases[db_name]
431 else:
432 db = self.databases[db_name] = self.getclassdb(classname, 'c')
434 # now save the marshalled data
435 db[nodeid] = marshal.dumps(self.serialise(classname, node))
437 def _doSaveJournal(self, classname, nodeid, action, params):
438 # serialise first
439 if action in ('set', 'create'):
440 params = self.serialise(classname, params)
442 # create the journal entry
443 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
444 params)
446 if __debug__:
447 print >>hyperdb.DEBUG, '_doSaveJournal', entry
449 # get the database handle
450 db_name = 'journals.%s'%classname
451 if self.databases.has_key(db_name):
452 db = self.databases[db_name]
453 else:
454 db = self.databases[db_name] = self._opendb(db_name, 'c')
456 # now insert the journal entry
457 if db.has_key(nodeid):
458 # append to existing
459 s = db[nodeid]
460 l = marshal.loads(s)
461 l.append(entry)
462 else:
463 l = [entry]
465 db[nodeid] = marshal.dumps(l)
467 def _doStoreFile(self, name, **databases):
468 # the file is currently ".tmp" - move it to its real name to commit
469 os.rename(name+".tmp", name)
471 def rollback(self):
472 ''' Reverse all actions from the current transaction.
473 '''
474 if __debug__:
475 print >>hyperdb.DEBUG, 'rollback', (self, )
476 for method, args in self.transactions:
477 # delete temporary files
478 if method == self._doStoreFile:
479 if os.path.exists(args[0]+".tmp"):
480 os.remove(args[0]+".tmp")
481 self.cache = {}
482 self.dirtynodes = {}
483 self.newnodes = {}
484 self.transactions = []
486 #
487 #$Log: not supported by cvs2svn $
488 #Revision 1.33 2002/04/24 10:38:26 rochecompaan
489 #All database files are now created group readable and writable.
490 #
491 #Revision 1.32 2002/04/15 23:25:15 richard
492 #. node ids are now generated from a lockable store - no more race conditions
493 #
494 #We're using the portalocker code by Jonathan Feinberg that was contributed
495 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
496 #
497 #Revision 1.31 2002/04/03 05:54:31 richard
498 #Fixed serialisation problem by moving the serialisation step out of the
499 #hyperdb.Class (get, set) into the hyperdb.Database.
500 #
501 #Also fixed htmltemplate after the showid changes I made yesterday.
502 #
503 #Unit tests for all of the above written.
504 #
505 #Revision 1.30 2002/02/27 03:40:59 richard
506 #Ran it through pychecker, made fixes
507 #
508 #Revision 1.29 2002/02/25 14:34:31 grubert
509 # . use blobfiles in back_anydbm which is used in back_bsddb.
510 # change test_db as dirlist does not work for subdirectories.
511 # ATTENTION: blobfiles now creates subdirectories for files.
512 #
513 #Revision 1.28 2002/02/16 09:14:17 richard
514 # . #514854 ] History: "User" is always ticket creator
515 #
516 #Revision 1.27 2002/01/22 07:21:13 richard
517 #. fixed back_bsddb so it passed the journal tests
518 #
519 #... it didn't seem happy using the back_anydbm _open method, which is odd.
520 #Yet another occurrance of whichdb not being able to recognise older bsddb
521 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
522 #process.
523 #
524 #Revision 1.26 2002/01/22 05:18:38 rochecompaan
525 #last_set_entry was referenced before assignment
526 #
527 #Revision 1.25 2002/01/22 05:06:08 rochecompaan
528 #We need to keep the last 'set' entry in the journal to preserve
529 #information on 'activity' for nodes.
530 #
531 #Revision 1.24 2002/01/21 16:33:20 rochecompaan
532 #You can now use the roundup-admin tool to pack the database
533 #
534 #Revision 1.23 2002/01/18 04:32:04 richard
535 #Rollback was breaking because a message hadn't actually been written to the file. Needs
536 #more investigation.
537 #
538 #Revision 1.22 2002/01/14 02:20:15 richard
539 # . changed all config accesses so they access either the instance or the
540 # config attriubute on the db. This means that all config is obtained from
541 # instance_config instead of the mish-mash of classes. This will make
542 # switching to a ConfigParser setup easier too, I hope.
543 #
544 #At a minimum, this makes migration a _little_ easier (a lot easier in the
545 #0.5.0 switch, I hope!)
546 #
547 #Revision 1.21 2002/01/02 02:31:38 richard
548 #Sorry for the huge checkin message - I was only intending to implement #496356
549 #but I found a number of places where things had been broken by transactions:
550 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
551 # for _all_ roundup-generated smtp messages to be sent to.
552 # . the transaction cache had broken the roundupdb.Class set() reactors
553 # . newly-created author users in the mailgw weren't being committed to the db
554 #
555 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
556 #on when I found that stuff :):
557 # . #496356 ] Use threading in messages
558 # . detectors were being registered multiple times
559 # . added tests for mailgw
560 # . much better attaching of erroneous messages in the mail gateway
561 #
562 #Revision 1.20 2001/12/18 15:30:34 rochecompaan
563 #Fixed bugs:
564 # . Fixed file creation and retrieval in same transaction in anydbm
565 # backend
566 # . Cgi interface now renders new issue after issue creation
567 # . Could not set issue status to resolved through cgi interface
568 # . Mail gateway was changing status back to 'chatting' if status was
569 # omitted as an argument
570 #
571 #Revision 1.19 2001/12/17 03:52:48 richard
572 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
573 #storing more than one file per node - if a property name is supplied,
574 #the file is called designator.property.
575 #I decided not to migrate the existing files stored over to the new naming
576 #scheme - the FileClass just doesn't specify the property name.
577 #
578 #Revision 1.18 2001/12/16 10:53:38 richard
579 #take a copy of the node dict so that the subsequent set
580 #operation doesn't modify the oldvalues structure
581 #
582 #Revision 1.17 2001/12/14 23:42:57 richard
583 #yuck, a gdbm instance tests false :(
584 #I've left the debugging code in - it should be removed one day if we're ever
585 #_really_ anal about performace :)
586 #
587 #Revision 1.16 2001/12/12 03:23:14 richard
588 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
589 #incorrectly identifies a dbm file as a dbhash file on my system. This has
590 #been submitted to the python bug tracker as issue #491888:
591 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
592 #
593 #Revision 1.15 2001/12/12 02:30:51 richard
594 #I fixed the problems with people whose anydbm was using the dbm module at the
595 #backend. It turns out the dbm module modifies the file name to append ".db"
596 #and my check to determine if we're opening an existing or new db just
597 #tested os.path.exists() on the filename. Well, no longer! We now perform a
598 #much better check _and_ cope with the anydbm implementation module changing
599 #too!
600 #I also fixed the backends __init__ so only ImportError is squashed.
601 #
602 #Revision 1.14 2001/12/10 22:20:01 richard
603 #Enabled transaction support in the bsddb backend. It uses the anydbm code
604 #where possible, only replacing methods where the db is opened (it uses the
605 #btree opener specifically.)
606 #Also cleaned up some change note generation.
607 #Made the backends package work with pydoc too.
608 #
609 #Revision 1.13 2001/12/02 05:06:16 richard
610 #. We now use weakrefs in the Classes to keep the database reference, so
611 # the close() method on the database is no longer needed.
612 # I bumped the minimum python requirement up to 2.1 accordingly.
613 #. #487480 ] roundup-server
614 #. #487476 ] INSTALL.txt
615 #
616 #I also cleaned up the change message / post-edit stuff in the cgi client.
617 #There's now a clearly marked "TODO: append the change note" where I believe
618 #the change note should be added there. The "changes" list will obviously
619 #have to be modified to be a dict of the changes, or somesuch.
620 #
621 #More testing needed.
622 #
623 #Revision 1.12 2001/12/01 07:17:50 richard
624 #. We now have basic transaction support! Information is only written to
625 # the database when the commit() method is called. Only the anydbm
626 # backend is modified in this way - neither of the bsddb backends have been.
627 # The mail, admin and cgi interfaces all use commit (except the admin tool
628 # doesn't have a commit command, so interactive users can't commit...)
629 #. Fixed login/registration forwarding the user to the right page (or not,
630 # on a failure)
631 #
632 #Revision 1.11 2001/11/21 02:34:18 richard
633 #Added a target version field to the extended issue schema
634 #
635 #Revision 1.10 2001/10/09 23:58:10 richard
636 #Moved the data stringification up into the hyperdb.Class class' get, set
637 #and create methods. This means that the data is also stringified for the
638 #journal call, and removes duplication of code from the backends. The
639 #backend code now only sees strings.
640 #
641 #Revision 1.9 2001/10/09 07:25:59 richard
642 #Added the Password property type. See "pydoc roundup.password" for
643 #implementation details. Have updated some of the documentation too.
644 #
645 #Revision 1.8 2001/09/29 13:27:00 richard
646 #CGI interfaces now spit up a top-level index of all the instances they can
647 #serve.
648 #
649 #Revision 1.7 2001/08/12 06:32:36 richard
650 #using isinstance(blah, Foo) now instead of isFooType
651 #
652 #Revision 1.6 2001/08/07 00:24:42 richard
653 #stupid typo
654 #
655 #Revision 1.5 2001/08/07 00:15:51 richard
656 #Added the copyright/license notice to (nearly) all files at request of
657 #Bizar Software.
658 #
659 #Revision 1.4 2001/07/30 01:41:36 richard
660 #Makes schema changes mucho easier.
661 #
662 #Revision 1.3 2001/07/25 01:23:07 richard
663 #Added the Roundup spec to the new documentation directory.
664 #
665 #Revision 1.2 2001/07/23 08:20:44 richard
666 #Moved over to using marshal in the bsddb and anydbm backends.
667 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
668 # retired - mod hyperdb.Class.list() so it lists retired nodes)
669 #
670 #