Code

86bc4ae7441976703c121d78f222104a6edf2e93
[roundup.git] / roundup-admin
1 #! /usr/bin/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.21 2001-10-05 02:23:24 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
27 from roundup import date, roundupdb, init
28 import roundup.instance
30 def usage(message=''):
31     if message: message = 'Problem: '+message+'\n'
32     commands = []
33     for command in figureCommands().values():
34         h = command.__doc__.split('\n')[0]
35         commands.append(h[7:])
36     commands.sort()
37     print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
39 Commands:
40  %s
41 Help:
42  roundup-admin -h
43  roundup-admin help                       -- this help
44  roundup-admin help <command>             -- command-specific help
45  roundup-admin morehelp                   -- even more detailed help
46 Options:
47  -i instance home  -- specify the issue tracker "home directory" to administer
48  -u                -- the user[:password] to use for commands
49  -c                -- when outputting lists of data, just comma-separate them'''%(
50 message, '\n '.join(commands))
52 def moreusage(message=''):
53     usage(message)
54     print '''
55 All commands (except help) require an instance specifier. This is just the path
56 to the roundup instance you're working with. A roundup instance is where 
57 roundup keeps the database and configuration file that defines an issue
58 tracker. It may be thought of as the issue tracker's "home directory". It may
59 be specified in the environment variable ROUNDUP_INSTANCE or on the command
60 line as "-i instance".
62 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
64 Property values are represented as strings in command arguments and in the
65 printed results:
66  . Strings are, well, strings.
67  . Date values are printed in the full date format in the local time zone, and
68    accepted in the full format or any of the partial formats explained below.
69  . Link values are printed as node designators. When given as an argument,
70    node designators and key strings are both accepted.
71  . Multilink values are printed as lists of node designators joined by commas.
72    When given as an argument, node designators and key strings are both
73    accepted; an empty string, a single node, or a list of nodes joined by
74    commas is accepted.
76 When multiple nodes are specified to the roundup get or roundup set
77 commands, the specified properties are retrieved or set on all the listed
78 nodes. 
80 When multiple results are returned by the roundup get or roundup find
81 commands, they are printed one per line (default) or joined by commas (with
82 the -c) option. 
84 Where the command changes data, a login name/password is required. The
85 login may be specified as either "name" or "name:password".
86  . ROUNDUP_LOGIN environment variable
87  . the -u command-line option
88 If either the name or password is not supplied, they are obtained from the
89 command-line. 
91 Date format examples:
92   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
93   "2000-04-17" means <Date 2000-04-17.00:00:00>
94   "01-25" means <Date yyyy-01-25.00:00:00>
95   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
96   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
97   "14:25" means <Date yyyy-mm-dd.19:25:00>
98   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
99   "." means "right now"
101 Command help:
102 '''
103     for name, command in figureCommands().items():
104         print '%s:'%name
105         print '   ',command.__doc__
107 def do_init(instance_home, args):
108     '''Usage: init [template [backend [admin password]]]
109     Initialise a new Roundup instance.
111     The command will prompt for the instance home directory (if not supplied
112     through INSTANCE_HOME or the -i option. The template, backend and admin
113     password may be specified on the command-line as arguments, in that order.
114     '''
115     # select template
116     import roundup.templates
117     templates = roundup.templates.listTemplates()
118     template = len(args) > 1 and args[1] or ''
119     if template not in templates:
120         print 'Templates:', ', '.join(templates)
121     while template not in templates:
122         template = raw_input('Select template [extended]: ').strip()
123         if not template:
124             template = 'extended'
126     import roundup.backends
127     backends = roundup.backends.__all__
128     backend = len(args) > 2 and args[2] or ''
129     if backend not in backends:
130         print 'Back ends:', ', '.join(backends)
131     while backend not in backends:
132         backend = raw_input('Select backend [anydbm]: ').strip()
133         if not backend:
134             backend = 'anydbm'
135     if len(args) > 3:
136         adminpw = confirm = args[3]
137     else:
138         adminpw = ''
139         confirm = 'x'
140     while adminpw != confirm:
141         adminpw = getpass.getpass('Admin Password: ')
142         confirm = getpass.getpass('       Confirm: ')
143     init.init(instance_home, template, backend, adminpw)
144     return 0
147 def do_get(db, args):
148     '''Usage: get property designator[,designator]*
149     Get the given property of one or more designator(s).
151     Retrieves the property value of the nodes specified by the designators.
152     '''
153     propname = args[0]
154     designators = string.split(args[1], ',')
155     # TODO: handle the -c option
156     for designator in designators:
157         classname, nodeid = roundupdb.splitDesignator(designator)
158         print db.getclass(classname).get(nodeid, propname)
159     return 0
162 def do_set(db, args):
163     '''Usage: set designator[,designator]* propname=value ...
164     Set the given property of one or more designator(s).
166     Sets the property to the value for all designators given.
167     '''
168     from roundup import hyperdb
170     designators = string.split(args[0], ',')
171     props = {}
172     for prop in args[1:]:
173         key, value = prop.split('=')
174         props[key] = value
175     for designator in designators:
176         classname, nodeid = roundupdb.splitDesignator(designator)
177         cl = db.getclass(classname)
178         properties = cl.getprops()
179         for key, value in props.items():
180             type =  properties[key]
181             if isinstance(type, hyperdb.String):
182                 continue
183             elif isinstance(type, hyperdb.Date):
184                 props[key] = date.Date(value)
185             elif isinstance(type, hyperdb.Interval):
186                 props[key] = date.Interval(value)
187             elif isinstance(type, hyperdb.Link):
188                 props[key] = value
189             elif isinstance(type, hyperdb.Multilink):
190                 props[key] = value.split(',')
191         apply(cl.set, (nodeid, ), props)
192     return 0
194 def do_find(db, args):
195     '''Usage: find classname propname=value ...
196     Find the nodes of the given class with a given property value.
198     Find the nodes of the given class with a given property value. The
199     value may be either the nodeid of the linked node, or its key value.
200     '''
201     classname = args[0]
202     cl = db.getclass(classname)
204     # look up the linked-to class and get the nodeid that has the value
205     propname, value = args[1].split('=')
206     num_re = re.compile('^\d+$')
207     if num_re.match(value):
208         nodeid = value
209     else:
210         propcl = cl.properties[propname].classname
211         propcl = db.getclass(propcl)
212         nodeid = propcl.lookup(value)
214     # now do the find
215     # TODO: handle the -c option
216     print cl.find(**{propname: nodeid})
217     return 0
219 def do_spec(db, args):
220     '''Usage: spec classname
221     Show the properties for a classname.
223     This lists the properties for a given class.
224     '''
225     classname = args[0]
226     cl = db.getclass(classname)
227     keyprop = cl.getkey()
228     for key, value in cl.properties.items():
229         if keyprop == key:
230             print '%s: %s (key property)'%(key, value)
231         else:
232             print '%s: %s'%(key, value)
234 def do_create(db, args, pretty_re=re.compile(r'<roundup\.hyperdb\.(.*)>')):
235     '''Usage: create classname property=value ...
236     Create a new entry of a given class.
238     This creates a new entry of the given class using the property
239     name=value arguments provided on the command line after the "create"
240     command.
241     '''
242     from roundup import hyperdb
244     classname = args[0]
245     cl = db.getclass(classname)
246     props = {}
247     properties = cl.getprops(protected = 0)
248     if len(args) == 1:
249         # ask for the properties
250         for key, value in properties.items():
251             if key == 'id': continue
252             m = pretty_re.match(str(value))
253             if m:
254                 value = m.group(1)
255             value = raw_input('%s (%s): '%(key.capitalize(), value))
256             if value:
257                 props[key] = value
258     else:
259         # use the args
260         for prop in args[1:]:
261             key, value = prop.split('=')
262             props[key] = value 
264     # convert types
265     for key in props.keys():
266         type =  properties[key]
267         if isinstance(type, hyperdb.Date):
268             props[key] = date.Date(value)
269         elif isinstance(type, hyperdb.Interval):
270             props[key] = date.Interval(value)
271         elif isinstance(type, hyperdb.Multilink):
272             props[key] = value.split(',')
274     if cl.getkey() and not props.has_key(cl.getkey()):
275         print "You must provide the '%s' property."%cl.getkey()
276     else:
277         print apply(cl.create, (), props)
279     return 0
281 def do_list(db, args):
282     '''Usage: list classname [property]
283     List the instances of a class.
285     Lists all instances of the given class along. If the property is not
286     specified, the  "label" property is used. The label property is tried
287     in order: the key, "name", "title" and then the first property,
288     alphabetically.
289     '''
290     classname = args[0]
291     cl = db.getclass(classname)
292     if len(args) > 1:
293         key = args[1]
294     else:
295         key = cl.labelprop()
296     # TODO: handle the -c option
297     for nodeid in cl.list():
298         value = cl.get(nodeid, key)
299         print "%4s: %s"%(nodeid, value)
300     return 0
302 def do_history(db, args):
303     '''Usage: history designator
304     Show the history entries of a designator.
306     Lists the journal entries for the node identified by the designator.
307     '''
308     classname, nodeid = roundupdb.splitDesignator(args[0])
309     # TODO: handle the -c option
310     print db.getclass(classname).history(nodeid)
311     return 0
313 def do_retire(db, args):
314     '''Usage: retire designator[,designator]*
315     Retire the node specified by designator.
317     This action indicates that a particular node is not to be retrieved by
318     the list or find commands, and its key value may be re-used.
319     '''
320     designators = string.split(args[0], ',')
321     for designator in designators:
322         classname, nodeid = roundupdb.splitDesignator(designator)
323         db.getclass(classname).retire(nodeid)
324     return 0
326 def do_freshen(db, args):
327     '''Usage: freshen
328     Freshen an existing instance.  **DO NOT USE**
330     This currently kills databases!!!!
332     This action should generally not be used. It reads in an instance
333     database and writes it again. In the future, is may also update
334     instance code to account for changes in templates. It's probably wise
335     not to use it anyway. Until we're sure it won't break things...
336     '''
337 #    for classname, cl in db.classes.items():
338 #        properties = cl.properties.items()
339 #        for nodeid in cl.list():
340 #            node = {}
341 #            for name, type in properties:
342 # isinstance(               if type, hyperdb.Multilink):
343 #                    node[name] = cl.get(nodeid, name, [])
344 #                else:
345 #                    node[name] = cl.get(nodeid, name, None)
346 #                db.setnode(classname, nodeid, node)
347     return 1
349 def figureCommands():
350     d = {}
351     for k, v in globals().items():
352         if k[:3] == 'do_':
353             d[k[3:]] = v
354     return d
356 def printInitOptions():
357     import roundup.templates
358     templates = roundup.templates.listTemplates()
359     print 'Templates:', ', '.join(templates)
360     import roundup.backends
361     backends = roundup.backends.__all__
362     print 'Back ends:', ', '.join(backends)
364 def main():
365     opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
367     # handle command-line args
368     instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
369     name = password = ''
370     if os.environ.has_key('ROUNDUP_LOGIN'):
371         l = os.environ['ROUNDUP_LOGIN'].split(':')
372         name = l[0]
373         if len(l) > 1:
374             password = l[1]
375     comma_sep = 0
376     for opt, arg in opts:
377         if opt == '-h':
378             usage()
379             return 0
380         if opt == '-i':
381             instance_home = arg
382         if opt == '-u':
383             l = arg.split(':')
384             name = l[0]
385             if len(l) > 1:
386                 password = l[1]
387         if opt == '-c':
388             comma_sep = 1
390     # figure the command
391     if not args:
392         usage('No command specified')
393         return 1
394     command = args[0]
396     # handle help now
397     if command == 'help':
398         if len(args)>1:
399             command = figureCommands().get(args[1], None)
400             if not command:
401                 usage('no such command "%s"'%args[1])
402                 return 1
403             print command.__doc__
404             if args[1] == 'init':
405                 printInitOptions()
406             return 0
407         usage()
408         return 0
409     if command == 'morehelp':
410         moreusage()
411         return 0
413     # make sure we have an instance_home
414     while not instance_home:
415         instance_home = raw_input('Enter instance home: ').strip()
417     # before we open the db, we may be doing an init
418     if command == 'init':
419         return do_init(instance_home, args)
421     # open the database
422     if command in ('create', 'set', 'retire', 'freshen'):
423         while not name:
424             name = raw_input('Login name: ')
425         while not password:
426             password = getpass.getpass('  password: ')
428     # get the instance
429     instance = roundup.instance.open(instance_home)
431     function = figureCommands().get(command, None)
433     # not a valid command
434     if function is None:
435         usage('Unknown command "%s"'%command)
436         return 1
438     db = instance.open(name or 'admin')
439     try:
440         return function(db, args[1:])
441     finally:
442         db.close()
444     return 1
447 if __name__ == '__main__':
448     sys.exit(main())
451 # $Log: not supported by cvs2svn $
452 # Revision 1.20  2001/10/04 02:12:42  richard
453 # Added nicer command-line item adding: passing no arguments will enter an
454 # interactive more which asks for each property in turn. While I was at it, I
455 # fixed an implementation problem WRT the spec - I wasn't raising a
456 # ValueError if the key property was missing from a create(). Also added a
457 # protected=boolean argument to getprops() so we can list only the mutable
458 # properties (defaults to yes, which lists the immutables).
460 # Revision 1.19  2001/10/01 06:40:43  richard
461 # made do_get have the args in the correct order
463 # Revision 1.18  2001/09/18 22:58:37  richard
465 # Added some more help to roundu-admin
467 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
468 # added missing 'import' statements.
470 # Revision 1.16  2001/08/12 06:32:36  richard
471 # using isinstance(blah, Foo) now instead of isFooType
473 # Revision 1.15  2001/08/07 00:24:42  richard
474 # stupid typo
476 # Revision 1.14  2001/08/07 00:15:51  richard
477 # Added the copyright/license notice to (nearly) all files at request of
478 # Bizar Software.
480 # Revision 1.13  2001/08/05 07:44:13  richard
481 # Instances are now opened by a special function that generates a unique
482 # module name for the instances on import time.
484 # Revision 1.12  2001/08/03 01:28:33  richard
485 # Used the much nicer load_package, pointed out by Steve Majewski.
487 # Revision 1.11  2001/08/03 00:59:34  richard
488 # Instance import now imports the instance using imp.load_module so that
489 # we can have instance homes of "roundup" or other existing python package
490 # names.
492 # Revision 1.10  2001/07/30 08:12:17  richard
493 # Added time logging and file uploading to the templates.
495 # Revision 1.9  2001/07/30 03:52:55  richard
496 # init help now lists templates and backends
498 # Revision 1.8  2001/07/30 02:37:07  richard
499 # Freshen is really broken. Commented out.
501 # Revision 1.7  2001/07/30 01:28:46  richard
502 # Bugfixes
504 # Revision 1.6  2001/07/30 00:57:51  richard
505 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
506 # better internal structure. It's just BETTER. :)
508 # Revision 1.5  2001/07/30 00:04:48  richard
509 # Made the "init" prompting more friendly.
511 # Revision 1.4  2001/07/29 07:01:39  richard
512 # Added vim command to all source so that we don't get no steenkin' tabs :)
514 # Revision 1.3  2001/07/23 08:45:28  richard
515 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
516 # workable instance_home set up :)
517 # _and_ anydbm has had its first test :)
519 # Revision 1.2  2001/07/23 08:20:44  richard
520 # Moved over to using marshal in the bsddb and anydbm backends.
521 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
522 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
524 # Revision 1.1  2001/07/23 03:46:48  richard
525 # moving the bin files to facilitate out-of-the-boxness
527 # Revision 1.1  2001/07/22 11:15:45  richard
528 # More Grande Splite stuff
531 # vim: set filetype=python ts=4 sw=4 et si