1 #! /usr/bin/env python
2 #
3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
8 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
9 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
10 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
11 # POSSIBILITY OF SUCH DAMAGE.
12 #
13 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
15 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18 #
19 # $Id: roundup-admin,v 1.46 2001-11-21 03:40:54 richard Exp $
21 import sys
22 if int(sys.version[0]) < 2:
23 print 'Roundup requires python 2.0 or later.'
24 sys.exit(1)
26 import string, os, getpass, getopt, re, UserDict
27 try:
28 import csv
29 except ImportError:
30 csv = None
31 from roundup import date, hyperdb, roundupdb, init, password
32 import roundup.instance
34 class CommandDict(UserDict.UserDict):
35 '''Simple dictionary that lets us do lookups using partial keys.
37 Original code submitted by Engelbert Gruber.
38 '''
39 _marker = []
40 def get(self, key, default=_marker):
41 if self.data.has_key(key):
42 return [(key, self.data[key])]
43 keylist = self.data.keys()
44 keylist.sort()
45 l = []
46 for ki in keylist:
47 if ki.startswith(key):
48 l.append((ki, self.data[ki]))
49 if not l and default is self._marker:
50 raise KeyError, key
51 return l
53 class UsageError(ValueError):
54 pass
56 class AdminTool:
58 def __init__(self):
59 self.commands = CommandDict()
60 for k in AdminTool.__dict__.keys():
61 if k[:3] == 'do_':
62 self.commands[k[3:]] = getattr(self, k)
63 self.help = {}
64 for k in AdminTool.__dict__.keys():
65 if k[:5] == 'help_':
66 self.help[k[5:]] = getattr(self, k)
67 self.db = None
69 def usage(self, message=''):
70 if message: message = 'Problem: '+message+'\n\n'
71 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
73 Help:
74 roundup-admin -h
75 roundup-admin help -- this help
76 roundup-admin help <command> -- command-specific help
77 roundup-admin help all -- all available help
78 Options:
79 -i instance home -- specify the issue tracker "home directory" to administer
80 -u -- the user[:password] to use for commands
81 -c -- when outputting lists of data, just comma-separate them'''%message
82 self.help_commands()
84 def help_commands(self):
85 print 'Commands:',
86 commands = ['']
87 for command in self.commands.values():
88 h = command.__doc__.split('\n')[0]
89 commands.append(' '+h[7:])
90 commands.sort()
91 commands.append(
92 'Commands may be abbreviated as long as the abbreviation matches only one')
93 commands.append('command, e.g. l == li == lis == list.')
94 print '\n'.join(commands)
95 print
97 def help_all(self):
98 print '''
99 All commands (except help) require an instance specifier. This is just the path
100 to the roundup instance you're working with. A roundup instance is where
101 roundup keeps the database and configuration file that defines an issue
102 tracker. It may be thought of as the issue tracker's "home directory". It may
103 be specified in the environment variable ROUNDUP_INSTANCE or on the command
104 line as "-i instance".
106 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
108 Property values are represented as strings in command arguments and in the
109 printed results:
110 . Strings are, well, strings.
111 . Date values are printed in the full date format in the local time zone, and
112 accepted in the full format or any of the partial formats explained below.
113 . Link values are printed as node designators. When given as an argument,
114 node designators and key strings are both accepted.
115 . Multilink values are printed as lists of node designators joined by commas.
116 When given as an argument, node designators and key strings are both
117 accepted; an empty string, a single node, or a list of nodes joined by
118 commas is accepted.
120 When multiple nodes are specified to the roundup get or roundup set
121 commands, the specified properties are retrieved or set on all the listed
122 nodes.
124 When multiple results are returned by the roundup get or roundup find
125 commands, they are printed one per line (default) or joined by commas (with
126 the -c) option.
128 Where the command changes data, a login name/password is required. The
129 login may be specified as either "name" or "name:password".
130 . ROUNDUP_LOGIN environment variable
131 . the -u command-line option
132 If either the name or password is not supplied, they are obtained from the
133 command-line.
135 Date format examples:
136 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
137 "2000-04-17" means <Date 2000-04-17.00:00:00>
138 "01-25" means <Date yyyy-01-25.00:00:00>
139 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
140 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
141 "14:25" means <Date yyyy-mm-dd.19:25:00>
142 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
143 "." means "right now"
145 Command help:
146 '''
147 for name, command in self.commands.items():
148 print '%s:'%name
149 print ' ',command.__doc__
151 def do_help(self, args, nl_re=re.compile('[\r\n]'),
152 indent_re=re.compile(r'^(\s+)\S+')):
153 '''Usage: help topic
154 Give help about topic.
156 commands -- list commands
157 <command> -- help specific to a command
158 initopts -- init command options
159 all -- all available help
160 '''
161 topic = args[0]
163 # try help_ methods
164 if self.help.has_key(topic):
165 self.help[topic]()
166 return 0
168 # try command docstrings
169 try:
170 l = self.commands.get(topic)
171 except KeyError:
172 print 'Sorry, no help for "%s"'%topic
173 return 1
175 # display the help for each match, removing the docsring indent
176 for name, help in l:
177 lines = nl_re.split(help.__doc__)
178 print lines[0]
179 indent = indent_re.match(lines[1])
180 if indent: indent = len(indent.group(1))
181 for line in lines[1:]:
182 if indent:
183 print line[indent:]
184 else:
185 print line
186 return 0
188 def help_initopts(self):
189 import roundup.templates
190 templates = roundup.templates.listTemplates()
191 print 'Templates:', ', '.join(templates)
192 import roundup.backends
193 backends = roundup.backends.__all__
194 print 'Back ends:', ', '.join(backends)
197 def do_initialise(self, instance_home, args):
198 '''Usage: initialise [template [backend [admin password]]]
199 Initialise a new Roundup instance.
201 The command will prompt for the instance home directory (if not supplied
202 through INSTANCE_HOME or the -i option. The template, backend and admin
203 password may be specified on the command-line as arguments, in that
204 order.
206 See also initopts help.
207 '''
208 # select template
209 import roundup.templates
210 templates = roundup.templates.listTemplates()
211 template = len(args) > 1 and args[1] or ''
212 if template not in templates:
213 print 'Templates:', ', '.join(templates)
214 while template not in templates:
215 template = raw_input('Select template [classic]: ').strip()
216 if not template:
217 template = 'classic'
219 import roundup.backends
220 backends = roundup.backends.__all__
221 backend = len(args) > 2 and args[2] or ''
222 if backend not in backends:
223 print 'Back ends:', ', '.join(backends)
224 while backend not in backends:
225 backend = raw_input('Select backend [anydbm]: ').strip()
226 if not backend:
227 backend = 'anydbm'
228 if len(args) > 3:
229 adminpw = confirm = args[3]
230 else:
231 adminpw = ''
232 confirm = 'x'
233 while adminpw != confirm:
234 adminpw = getpass.getpass('Admin Password: ')
235 confirm = getpass.getpass(' Confirm: ')
236 init.init(instance_home, template, backend, adminpw)
237 return 0
240 def do_get(self, args):
241 '''Usage: get property designator[,designator]*
242 Get the given property of one or more designator(s).
244 Retrieves the property value of the nodes specified by the designators.
245 '''
246 propname = args[0]
247 designators = string.split(args[1], ',')
248 l = []
249 for designator in designators:
250 # decode the node designator
251 try:
252 classname, nodeid = roundupdb.splitDesignator(designator)
253 except roundupdb.DesignatorError, message:
254 raise UsageError, message
256 # get the class
257 try:
258 cl = self.db.getclass(classname)
259 except KeyError:
260 raise UsageError, 'invalid class "%s"'%classname
261 try:
262 if self.comma_sep:
263 l.append(cl.get(nodeid, propname))
264 else:
265 print cl.get(nodeid, propname)
266 except IndexError:
267 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
268 except KeyError:
269 raise UsageError, 'no such %s property "%s"'%(classname,
270 propname)
271 if self.comma_sep:
272 print ','.join(l)
273 return 0
276 def do_set(self, args):
277 '''Usage: set designator[,designator]* propname=value ...
278 Set the given property of one or more designator(s).
280 Sets the property to the value for all designators given.
281 '''
282 from roundup import hyperdb
284 designators = string.split(args[0], ',')
285 props = {}
286 for prop in args[1:]:
287 if prop.find('=') == -1:
288 raise UsageError, 'argument "%s" not propname=value'%prop
289 try:
290 key, value = prop.split('=')
291 except ValueError:
292 raise UsageError, 'argument "%s" not propname=value'%prop
293 props[key] = value
294 for designator in designators:
295 # decode the node designator
296 try:
297 classname, nodeid = roundupdb.splitDesignator(designator)
298 except roundupdb.DesignatorError, message:
299 raise UsageError, message
301 # get the class
302 try:
303 cl = self.db.getclass(classname)
304 except KeyError:
305 raise UsageError, 'invalid class "%s"'%classname
307 properties = cl.getprops()
308 for key, value in props.items():
309 type = properties[key]
310 if isinstance(type, hyperdb.String):
311 continue
312 elif isinstance(type, hyperdb.Password):
313 props[key] = password.Password(value)
314 elif isinstance(type, hyperdb.Date):
315 try:
316 props[key] = date.Date(value)
317 except ValueError, message:
318 raise UsageError, '"%s": %s'%(value, message)
319 elif isinstance(type, hyperdb.Interval):
320 try:
321 props[key] = date.Interval(value)
322 except ValueError, message:
323 raise UsageError, '"%s": %s'%(value, message)
324 elif isinstance(type, hyperdb.Link):
325 props[key] = value
326 elif isinstance(type, hyperdb.Multilink):
327 props[key] = value.split(',')
329 # try the set
330 try:
331 apply(cl.set, (nodeid, ), props)
332 except (TypeError, IndexError, ValueError), message:
333 raise UsageError, message
334 return 0
336 def do_find(self, args):
337 '''Usage: find classname propname=value ...
338 Find the nodes of the given class with a given link property value.
340 Find the nodes of the given class with a given link property value. The
341 value may be either the nodeid of the linked node, or its key value.
342 '''
343 classname = args[0]
344 # get the class
345 try:
346 cl = self.db.getclass(classname)
347 except KeyError:
348 raise UsageError, 'invalid class "%s"'%classname
350 # TODO: handle > 1 argument
351 # handle the propname=value argument
352 if args[1].find('=') == -1:
353 raise UsageError, 'argument "%s" not propname=value'%prop
354 try:
355 propname, value = args[1].split('=')
356 except ValueError:
357 raise UsageError, 'argument "%s" not propname=value'%prop
359 # if the value isn't a number, look up the linked class to get the
360 # number
361 num_re = re.compile('^\d+$')
362 if not num_re.match(value):
363 # get the property
364 try:
365 property = cl.properties[propname]
366 except KeyError:
367 raise UsageError, '%s has no property "%s"'%(classname,
368 propname)
370 # make sure it's a link
371 if (not isinstance(property, hyperdb.Link) and not
372 isinstance(type, hyperdb.Multilink)):
373 raise UsageError, 'You may only "find" link properties'
375 # get the linked-to class and look up the key property
376 link_class = self.db.getclass(property.classname)
377 try:
378 value = link_class.lookup(value)
379 except TypeError:
380 raise UsageError, '%s has no key property"'%link_class.classname
381 except KeyError:
382 raise UsageError, '%s has no entry "%s"'%(link_class.classname,
383 propname)
385 # now do the find
386 try:
387 if self.comma_sep:
388 print ','.join(apply(cl.find, (), {propname: value}))
389 else:
390 print apply(cl.find, (), {propname: value})
391 except KeyError:
392 raise UsageError, '%s has no property "%s"'%(classname,
393 propname)
394 except (ValueError, TypeError), message:
395 raise UsageError, message
396 return 0
398 def do_specification(self, args):
399 '''Usage: specification classname
400 Show the properties for a classname.
402 This lists the properties for a given class.
403 '''
404 classname = args[0]
405 # get the class
406 try:
407 cl = self.db.getclass(classname)
408 except KeyError:
409 raise UsageError, 'invalid class "%s"'%classname
411 # get the key property
412 keyprop = cl.getkey()
413 for key, value in cl.properties.items():
414 if keyprop == key:
415 print '%s: %s (key property)'%(key, value)
416 else:
417 print '%s: %s'%(key, value)
419 def do_create(self, args):
420 '''Usage: create classname property=value ...
421 Create a new entry of a given class.
423 This creates a new entry of the given class using the property
424 name=value arguments provided on the command line after the "create"
425 command.
426 '''
427 from roundup import hyperdb
429 classname = args[0]
431 # get the class
432 try:
433 cl = self.db.getclass(classname)
434 except KeyError:
435 raise UsageError, 'invalid class "%s"'%classname
437 # now do a create
438 props = {}
439 properties = cl.getprops(protected = 0)
440 if len(args) == 1:
441 # ask for the properties
442 for key, value in properties.items():
443 if key == 'id': continue
444 name = value.__class__.__name__
445 if isinstance(value , hyperdb.Password):
446 again = None
447 while value != again:
448 value = getpass.getpass('%s (Password): '%key.capitalize())
449 again = getpass.getpass(' %s (Again): '%key.capitalize())
450 if value != again: print 'Sorry, try again...'
451 if value:
452 props[key] = value
453 else:
454 value = raw_input('%s (%s): '%(key.capitalize(), name))
455 if value:
456 props[key] = value
457 else:
458 # use the args
459 for prop in args[1:]:
460 if prop.find('=') == -1:
461 raise UsageError, 'argument "%s" not propname=value'%prop
462 try:
463 key, value = prop.split('=')
464 except ValueError:
465 raise UsageError, 'argument "%s" not propname=value'%prop
466 props[key] = value
468 # convert types
469 for key in props.keys():
470 # get the property
471 try:
472 type = properties[key]
473 except KeyError:
474 raise UsageError, '%s has no property "%s"'%(classname, key)
476 if isinstance(type, hyperdb.Date):
477 try:
478 props[key] = date.Date(value)
479 except ValueError, message:
480 raise UsageError, '"%s": %s'%(value, message)
481 elif isinstance(type, hyperdb.Interval):
482 try:
483 props[key] = date.Interval(value)
484 except ValueError, message:
485 raise UsageError, '"%s": %s'%(value, message)
486 elif isinstance(type, hyperdb.Password):
487 props[key] = password.Password(value)
488 elif isinstance(type, hyperdb.Multilink):
489 props[key] = value.split(',')
491 # check for the key property
492 if cl.getkey() and not props.has_key(cl.getkey()):
493 raise UsageError, "you must provide the '%s' property."%cl.getkey()
495 # do the actual create
496 try:
497 print apply(cl.create, (), props)
498 except (TypeError, IndexError, ValueError), message:
499 raise UsageError, message
500 return 0
502 def do_list(self, args):
503 '''Usage: list classname [property]
504 List the instances of a class.
506 Lists all instances of the given class. If the property is not
507 specified, the "label" property is used. The label property is tried
508 in order: the key, "name", "title" and then the first property,
509 alphabetically.
510 '''
511 classname = args[0]
513 # get the class
514 try:
515 cl = self.db.getclass(classname)
516 except KeyError:
517 raise UsageError, 'invalid class "%s"'%classname
519 # figure the property
520 if len(args) > 1:
521 key = args[1]
522 else:
523 key = cl.labelprop()
525 if self.comma_sep:
526 print ','.join(cl.list())
527 else:
528 for nodeid in cl.list():
529 try:
530 value = cl.get(nodeid, key)
531 except KeyError:
532 raise UsageError, '%s has no property "%s"'%(classname, key)
533 print "%4s: %s"%(nodeid, value)
534 return 0
536 def do_table(self, args):
537 '''Usage: table classname [property[,property]*]
538 List the instances of a class in tabular form.
540 Lists all instances of the given class. If the properties are not
541 specified, all properties are displayed. By default, the column widths
542 are the width of the property names. The width may be explicitly defined
543 by defining the property as "name:width". For example::
544 roundup> table priority id,name:10
545 Id Name
546 1 fatal-bug
547 2 bug
548 3 usability
549 4 feature
550 '''
551 classname = args[0]
553 # get the class
554 try:
555 cl = self.db.getclass(classname)
556 except KeyError:
557 raise UsageError, 'invalid class "%s"'%classname
559 # figure the property names to display
560 if len(args) > 1:
561 prop_names = args[1].split(',')
562 else:
563 prop_names = cl.getprops().keys()
565 # now figure column widths
566 props = []
567 for spec in prop_names:
568 if ':' in spec:
569 try:
570 name, width = spec.split(':')
571 except (ValueError, TypeError):
572 raise UsageError, '"%s" not name:width'%spec
573 props.append((spec, int(width)))
574 else:
575 props.append((spec, len(spec)))
577 # now display the heading
578 print ' '.join([string.capitalize(name) for name, width in props])
580 # and the table data
581 for nodeid in cl.list():
582 l = []
583 for name, width in props:
584 if name != 'id':
585 try:
586 value = str(cl.get(nodeid, name))
587 except KeyError:
588 raise UsageError, '%s has no property "%s"'%(classname,
589 name)
590 else:
591 value = str(nodeid)
592 f = '%%-%ds'%width
593 l.append(f%value[:width])
594 print ' '.join(l)
595 return 0
597 def do_history(self, args):
598 '''Usage: history designator
599 Show the history entries of a designator.
601 Lists the journal entries for the node identified by the designator.
602 '''
603 try:
604 classname, nodeid = roundupdb.splitDesignator(args[0])
605 except roundupdb.DesignatorError, message:
606 raise UsageError, message
608 # TODO: handle the -c option?
609 try:
610 print self.db.getclass(classname).history(nodeid)
611 except KeyError:
612 raise UsageError, 'no such class "%s"'%classname
613 except IndexError:
614 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
615 return 0
617 def do_retire(self, args):
618 '''Usage: retire designator[,designator]*
619 Retire the node specified by designator.
621 This action indicates that a particular node is not to be retrieved by
622 the list or find commands, and its key value may be re-used.
623 '''
624 designators = string.split(args[0], ',')
625 for designator in designators:
626 try:
627 classname, nodeid = roundupdb.splitDesignator(designator)
628 except roundupdb.DesignatorError, message:
629 raise UsageError, message
630 try:
631 self.db.getclass(classname).retire(nodeid)
632 except KeyError:
633 raise UsageError, 'no such class "%s"'%classname
634 except IndexError:
635 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
636 return 0
638 def do_export(self, args):
639 '''Usage: export class[,class] destination_dir
640 Export the database to tab-separated-value files.
642 This action exports the current data from the database into
643 tab-separated-value files that are placed in the nominated destination
644 directory. The journals are not exported.
645 '''
646 if len(args) < 2:
647 print do_export.__doc__
648 return 1
649 classes = string.split(args[0], ',')
650 dir = args[1]
652 # use the csv parser if we can - it's faster
653 if csv is not None:
654 p = csv.parser(field_sep=':')
656 # do all the classes specified
657 for classname in classes:
658 try:
659 cl = self.db.getclass(classname)
660 except KeyError:
661 raise UsageError, 'no such class "%s"'%classname
662 f = open(os.path.join(dir, classname+'.csv'), 'w')
663 f.write(string.join(cl.properties.keys(), ':') + '\n')
665 # all nodes for this class
666 properties = cl.properties.items()
667 for nodeid in cl.list():
668 l = []
669 for prop, type in properties:
670 value = cl.get(nodeid, prop)
671 # convert data where needed
672 if isinstance(type, hyperdb.Date):
673 value = value.get_tuple()
674 elif isinstance(type, hyperdb.Interval):
675 value = value.get_tuple()
676 elif isinstance(type, hyperdb.Password):
677 value = str(value)
678 l.append(repr(value))
680 # now write
681 if csv is not None:
682 f.write(p.join(l) + '\n')
683 else:
684 # escape the individual entries to they're valid CSV
685 m = []
686 for entry in l:
687 if '"' in entry:
688 entry = '""'.join(entry.split('"'))
689 if ':' in entry:
690 entry = '"%s"'%entry
691 m.append(entry)
692 f.write(':'.join(m) + '\n')
693 return 0
695 def do_import(self, args):
696 '''Usage: import class file
697 Import the contents of the tab-separated-value file.
699 The file must define the same properties as the class (including having
700 a "header" line with those property names.) The new nodes are added to
701 the existing database - if you want to create a new database using the
702 imported data, then create a new database (or, tediously, retire all
703 the old data.)
704 '''
705 if len(args) < 2:
706 raise UsageError, 'Not enough arguments supplied'
707 if csv is None:
708 raise UsageError, \
709 'Sorry, you need the csv module to use this function.\n'\
710 'Get it from: http://www.object-craft.com.au/projects/csv/'
712 from roundup import hyperdb
714 # ensure that the properties and the CSV file headings match
715 try:
716 cl = self.db.getclass(classname)
717 except KeyError:
718 raise UsageError, 'no such class "%s"'%classname
719 f = open(args[1])
720 p = csv.parser(field_sep=':')
721 file_props = p.parse(f.readline())
722 props = cl.properties.keys()
723 m = file_props[:]
724 m.sort()
725 props.sort()
726 if m != props:
727 raise UsageError, 'Import file doesn\'t define the same '\
728 'properties as "%s".'%args[0]
730 # loop through the file and create a node for each entry
731 n = range(len(props))
732 while 1:
733 line = f.readline()
734 if not line: break
736 # parse lines until we get a complete entry
737 while 1:
738 l = p.parse(line)
739 if l: break
741 # make the new node's property map
742 d = {}
743 for i in n:
744 # Use eval to reverse the repr() used to output the CSV
745 value = eval(l[i])
746 # Figure the property for this column
747 key = file_props[i]
748 type = cl.properties[key]
749 # Convert for property type
750 if isinstance(type, hyperdb.Date):
751 value = date.Date(value)
752 elif isinstance(type, hyperdb.Interval):
753 value = date.Interval(value)
754 elif isinstance(type, hyperdb.Password):
755 pwd = password.Password()
756 pwd.unpack(value)
757 value = pwd
758 if value is not None:
759 d[key] = value
761 # and create the new node
762 apply(cl.create, (), d)
763 return 0
765 def run_command(self, args):
766 '''Run a single command
767 '''
768 command = args[0]
770 # handle help now
771 if command == 'help':
772 if len(args)>1:
773 self.do_help(args[1:])
774 return 0
775 self.do_help(['help'])
776 return 0
777 if command == 'morehelp':
778 self.do_help(['help'])
779 self.help_commands()
780 self.help_all()
781 return 0
783 # figure what the command is
784 try:
785 functions = self.commands.get(command)
786 except KeyError:
787 # not a valid command
788 print 'Unknown command "%s" ("help commands" for a list)'%command
789 return 1
791 # check for multiple matches
792 if len(functions) > 1:
793 print 'Multiple commands match "%s": %s'%(command,
794 ', '.join([i[0] for i in functions]))
795 return 1
796 command, function = functions[0]
798 # make sure we have an instance_home
799 while not self.instance_home:
800 self.instance_home = raw_input('Enter instance home: ').strip()
802 # before we open the db, we may be doing an init
803 if command == 'initialise':
804 return self.do_initialise(self.instance_home, args)
806 # get the instance
807 try:
808 instance = roundup.instance.open(self.instance_home)
809 except ValueError, message:
810 print "Couldn't open instance: %s"%message
811 return 1
812 self.db = instance.open('admin')
814 if len(args) < 2:
815 print function.__doc__
816 return 1
818 # do the command
819 ret = 0
820 try:
821 ret = function(args[1:])
822 except UsageError, message:
823 print 'Error: %s'%message
824 print function.__doc__
825 ret = 1
826 except:
827 import traceback
828 traceback.print_exc()
829 ret = 1
830 return ret
832 def interactive(self, ws_re=re.compile(r'\s+')):
833 '''Run in an interactive mode
834 '''
835 print 'Roundup {version} ready for input.'
836 print 'Type "help" for help.'
837 try:
838 import readline
839 except ImportError:
840 print "Note: command history and editing not available"
842 while 1:
843 try:
844 command = raw_input('roundup> ')
845 except EOFError:
846 print '.. exit'
847 return 0
848 args = ws_re.split(command)
849 if not args: continue
850 if args[0] in ('quit', 'exit'): return 0
851 self.run_command(args)
853 def main(self):
854 try:
855 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
856 except getopt.GetoptError, e:
857 self.usage(str(e))
858 return 1
860 # handle command-line args
861 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
862 name = password = ''
863 if os.environ.has_key('ROUNDUP_LOGIN'):
864 l = os.environ['ROUNDUP_LOGIN'].split(':')
865 name = l[0]
866 if len(l) > 1:
867 password = l[1]
868 self.comma_sep = 0
869 for opt, arg in opts:
870 if opt == '-h':
871 self.usage()
872 return 0
873 if opt == '-i':
874 self.instance_home = arg
875 if opt == '-c':
876 self.comma_sep = 1
878 # if no command - go interactive
879 ret = 0
880 if not args:
881 self.interactive()
882 else:
883 ret = self.run_command(args)
884 if self.db:
885 self.db.close()
886 return ret
889 if __name__ == '__main__':
890 tool = AdminTool()
891 sys.exit(tool.main())
893 #
894 # $Log: not supported by cvs2svn $
895 # Revision 1.45 2001/11/12 22:51:59 jhermann
896 # Fixed option & associated error handling
897 #
898 # Revision 1.44 2001/11/12 22:01:06 richard
899 # Fixed issues with nosy reaction and author copies.
900 #
901 # Revision 1.43 2001/11/09 22:33:28 richard
902 # More error handling fixes.
903 #
904 # Revision 1.42 2001/11/09 10:11:08 richard
905 # . roundup-admin now handles all hyperdb exceptions
906 #
907 # Revision 1.41 2001/11/09 01:25:40 richard
908 # Should parse with python 1.5.2 now.
909 #
910 # Revision 1.40 2001/11/08 04:42:00 richard
911 # Expanded the already-abbreviated "initialise" and "specification" commands,
912 # and added a comment to the command help about the abbreviation.
913 #
914 # Revision 1.39 2001/11/08 04:29:59 richard
915 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
916 # [thanks Engelbert Gruber for the inspiration]
917 #
918 # Revision 1.38 2001/11/05 23:45:40 richard
919 # Fixed newuser_action so it sets the cookie with the unencrypted password.
920 # Also made it present nicer error messages (not tracebacks).
921 #
922 # Revision 1.37 2001/10/23 01:00:18 richard
923 # Re-enabled login and registration access after lopping them off via
924 # disabling access for anonymous users.
925 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
926 # a couple of bugs while I was there. Probably introduced a couple, but
927 # things seem to work OK at the moment.
928 #
929 # Revision 1.36 2001/10/21 00:45:15 richard
930 # Added author identification to e-mail messages from roundup.
931 #
932 # Revision 1.35 2001/10/20 11:58:48 richard
933 # Catch errors in login - no username or password supplied.
934 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
935 #
936 # Revision 1.34 2001/10/18 02:16:42 richard
937 # Oops, committed the admin script with the wierd #! line.
938 # Also, made the thing into a class to reduce parameter passing.
939 # Nuked the leading whitespace from the help __doc__ displays too.
940 #
941 # Revision 1.33 2001/10/17 23:13:19 richard
942 # Did a fair bit of work on the admin tool. Now has an extra command "table"
943 # which displays node information in a tabular format. Also fixed import and
944 # export so they work. Removed freshen.
945 # Fixed quopri usage in mailgw from bug reports.
946 #
947 # Revision 1.32 2001/10/17 06:57:29 richard
948 # Interactive startup blurb - need to figure how to get the version in there.
949 #
950 # Revision 1.31 2001/10/17 06:17:26 richard
951 # Now with readline support :)
952 #
953 # Revision 1.30 2001/10/17 06:04:00 richard
954 # Beginnings of an interactive mode for roundup-admin
955 #
956 # Revision 1.29 2001/10/16 03:48:01 richard
957 # admin tool now complains if a "find" is attempted with a non-link property.
958 #
959 # Revision 1.28 2001/10/13 00:07:39 richard
960 # More help in admin tool.
961 #
962 # Revision 1.27 2001/10/11 23:43:04 richard
963 # Implemented the comma-separated printing option in the admin tool.
964 # Fixed a typo (more of a vim-o actually :) in mailgw.
965 #
966 # Revision 1.26 2001/10/11 05:03:51 richard
967 # Marked the roundup-admin import/export as experimental since they're not fully
968 # operational.
969 #
970 # Revision 1.25 2001/10/10 04:12:32 richard
971 # The setup.cfg file is just causing pain. Away it goes.
972 #
973 # Revision 1.24 2001/10/10 03:54:57 richard
974 # Added database importing and exporting through CSV files.
975 # Uses the csv module from object-craft for exporting if it's available.
976 # Requires the csv module for importing.
977 #
978 # Revision 1.23 2001/10/09 23:36:25 richard
979 # Spit out command help if roundup-admin command doesn't get an argument.
980 #
981 # Revision 1.22 2001/10/09 07:25:59 richard
982 # Added the Password property type. See "pydoc roundup.password" for
983 # implementation details. Have updated some of the documentation too.
984 #
985 # Revision 1.21 2001/10/05 02:23:24 richard
986 # . roundup-admin create now prompts for property info if none is supplied
987 # on the command-line.
988 # . hyperdb Class getprops() method may now return only the mutable
989 # properties.
990 # . Login now uses cookies, which makes it a whole lot more flexible. We can
991 # now support anonymous user access (read-only, unless there's an
992 # "anonymous" user, in which case write access is permitted). Login
993 # handling has been moved into cgi_client.Client.main()
994 # . The "extended" schema is now the default in roundup init.
995 # . The schemas have had their page headings modified to cope with the new
996 # login handling. Existing installations should copy the interfaces.py
997 # file from the roundup lib directory to their instance home.
998 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
999 # Ping - has been removed.
1000 # . Fixed a whole bunch of places in the CGI interface where we should have
1001 # been returning Not Found instead of throwing an exception.
1002 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1003 # an item now throws an exception.
1004 #
1005 # Revision 1.20 2001/10/04 02:12:42 richard
1006 # Added nicer command-line item adding: passing no arguments will enter an
1007 # interactive more which asks for each property in turn. While I was at it, I
1008 # fixed an implementation problem WRT the spec - I wasn't raising a
1009 # ValueError if the key property was missing from a create(). Also added a
1010 # protected=boolean argument to getprops() so we can list only the mutable
1011 # properties (defaults to yes, which lists the immutables).
1012 #
1013 # Revision 1.19 2001/10/01 06:40:43 richard
1014 # made do_get have the args in the correct order
1015 #
1016 # Revision 1.18 2001/09/18 22:58:37 richard
1017 #
1018 # Added some more help to roundu-admin
1019 #
1020 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
1021 # added missing 'import' statements.
1022 #
1023 # Revision 1.16 2001/08/12 06:32:36 richard
1024 # using isinstance(blah, Foo) now instead of isFooType
1025 #
1026 # Revision 1.15 2001/08/07 00:24:42 richard
1027 # stupid typo
1028 #
1029 # Revision 1.14 2001/08/07 00:15:51 richard
1030 # Added the copyright/license notice to (nearly) all files at request of
1031 # Bizar Software.
1032 #
1033 # Revision 1.13 2001/08/05 07:44:13 richard
1034 # Instances are now opened by a special function that generates a unique
1035 # module name for the instances on import time.
1036 #
1037 # Revision 1.12 2001/08/03 01:28:33 richard
1038 # Used the much nicer load_package, pointed out by Steve Majewski.
1039 #
1040 # Revision 1.11 2001/08/03 00:59:34 richard
1041 # Instance import now imports the instance using imp.load_module so that
1042 # we can have instance homes of "roundup" or other existing python package
1043 # names.
1044 #
1045 # Revision 1.10 2001/07/30 08:12:17 richard
1046 # Added time logging and file uploading to the templates.
1047 #
1048 # Revision 1.9 2001/07/30 03:52:55 richard
1049 # init help now lists templates and backends
1050 #
1051 # Revision 1.8 2001/07/30 02:37:07 richard
1052 # Freshen is really broken. Commented out.
1053 #
1054 # Revision 1.7 2001/07/30 01:28:46 richard
1055 # Bugfixes
1056 #
1057 # Revision 1.6 2001/07/30 00:57:51 richard
1058 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1059 # better internal structure. It's just BETTER. :)
1060 #
1061 # Revision 1.5 2001/07/30 00:04:48 richard
1062 # Made the "init" prompting more friendly.
1063 #
1064 # Revision 1.4 2001/07/29 07:01:39 richard
1065 # Added vim command to all source so that we don't get no steenkin' tabs :)
1066 #
1067 # Revision 1.3 2001/07/23 08:45:28 richard
1068 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1069 # workable instance_home set up :)
1070 # _and_ anydbm has had its first test :)
1071 #
1072 # Revision 1.2 2001/07/23 08:20:44 richard
1073 # Moved over to using marshal in the bsddb and anydbm backends.
1074 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1075 # retired - mod hyperdb.Class.list() so it lists retired nodes)
1076 #
1077 # Revision 1.1 2001/07/23 03:46:48 richard
1078 # moving the bin files to facilitate out-of-the-boxness
1079 #
1080 # Revision 1.1 2001/07/22 11:15:45 richard
1081 # More Grande Splite stuff
1082 #
1083 #
1084 # vim: set filetype=python ts=4 sw=4 et si