Code

a29c0558262d5239a492d1e6bc9ede09a59be4a2
[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.19 2001-10-01 06:40:43 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 [classic]: ').strip()
123         if not template:
124             template = 'classic'
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     for key, value in cl.properties.items():
228         print '%s: %s'%(key, value)
230 def do_create(db, args):
231     '''Usage: create classname property=value ...
232     Create a new entry of a given class.
234     This creates a new entry of the given class using the property
235     name=value arguments provided on the command line after the "create"
236     command.
237     '''
238     from roundup import hyperdb
240     classname = args[0]
241     cl = db.getclass(classname)
242     props = {}
243     properties = cl.getprops()
244     for prop in args[1:]:
245         key, value = prop.split('=')
246         type =  properties[key]
247         if isinstance(type, hyperdb.String):
248             props[key] = value 
249         elif isinstance(type, hyperdb.Date):
250             props[key] = date.Date(value)
251         elif isinstance(type, hyperdb.Interval):
252             props[key] = date.Interval(value)
253         elif isinstance(type, hyperdb.Link):
254             props[key] = value
255         elif isinstance(type, hyperdb.Multilink):
256             props[key] = value.split(',')
257     print apply(cl.create, (), props)
258     return 0
260 def do_list(db, args):
261     '''Usage: list classname [property]
262     List the instances of a class.
264     Lists all instances of the given class along. If the property is not
265     specified, the  "label" property is used. The label property is tried
266     in order: the key, "name", "title" and then the first property,
267     alphabetically.
268     '''
269     classname = args[0]
270     cl = db.getclass(classname)
271     if len(args) > 1:
272         key = args[1]
273     else:
274         key = cl.labelprop()
275     # TODO: handle the -c option
276     for nodeid in cl.list():
277         value = cl.get(nodeid, key)
278         print "%4s: %s"%(nodeid, value)
279     return 0
281 def do_history(db, args):
282     '''Usage: history designator
283     Show the history entries of a designator.
285     Lists the journal entries for the node identified by the designator.
286     '''
287     classname, nodeid = roundupdb.splitDesignator(args[0])
288     # TODO: handle the -c option
289     print db.getclass(classname).history(nodeid)
290     return 0
292 def do_retire(db, args):
293     '''Usage: retire designator[,designator]*
294     Retire the node specified by designator.
296     This action indicates that a particular node is not to be retrieved by
297     the list or find commands, and its key value may be re-used.
298     '''
299     designators = string.split(args[0], ',')
300     for designator in designators:
301         classname, nodeid = roundupdb.splitDesignator(designator)
302         db.getclass(classname).retire(nodeid)
303     return 0
305 def do_freshen(db, args):
306     '''Usage: freshen
307     Freshen an existing instance.  **DO NOT USE**
309     This currently kills databases!!!!
311     This action should generally not be used. It reads in an instance
312     database and writes it again. In the future, is may also update
313     instance code to account for changes in templates. It's probably wise
314     not to use it anyway. Until we're sure it won't break things...
315     '''
316 #    for classname, cl in db.classes.items():
317 #        properties = cl.properties.items()
318 #        for nodeid in cl.list():
319 #            node = {}
320 #            for name, type in properties:
321 # isinstance(               if type, hyperdb.Multilink):
322 #                    node[name] = cl.get(nodeid, name, [])
323 #                else:
324 #                    node[name] = cl.get(nodeid, name, None)
325 #                db.setnode(classname, nodeid, node)
326     return 1
328 def figureCommands():
329     d = {}
330     for k, v in globals().items():
331         if k[:3] == 'do_':
332             d[k[3:]] = v
333     return d
335 def printInitOptions():
336     import roundup.templates
337     templates = roundup.templates.listTemplates()
338     print 'Templates:', ', '.join(templates)
339     import roundup.backends
340     backends = roundup.backends.__all__
341     print 'Back ends:', ', '.join(backends)
343 def main():
344     opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
346     # handle command-line args
347     instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
348     name = password = ''
349     if os.environ.has_key('ROUNDUP_LOGIN'):
350         l = os.environ['ROUNDUP_LOGIN'].split(':')
351         name = l[0]
352         if len(l) > 1:
353             password = l[1]
354     comma_sep = 0
355     for opt, arg in opts:
356         if opt == '-h':
357             usage()
358             return 0
359         if opt == '-i':
360             instance_home = arg
361         if opt == '-u':
362             l = arg.split(':')
363             name = l[0]
364             if len(l) > 1:
365                 password = l[1]
366         if opt == '-c':
367             comma_sep = 1
369     # figure the command
370     if not args:
371         usage('No command specified')
372         return 1
373     command = args[0]
375     # handle help now
376     if command == 'help':
377         if len(args)>1:
378             command = figureCommands().get(args[1], None)
379             if not command:
380                 usage('no such command "%s"'%args[1])
381                 return 1
382             print command.__doc__
383             if args[1] == 'init':
384                 printInitOptions()
385             return 0
386         usage()
387         return 0
388     if command == 'morehelp':
389         moreusage()
390         return 0
392     # make sure we have an instance_home
393     while not instance_home:
394         instance_home = raw_input('Enter instance home: ').strip()
396     # before we open the db, we may be doing an init
397     if command == 'init':
398         return do_init(instance_home, args)
400     # open the database
401     if command in ('create', 'set', 'retire', 'freshen'):
402         while not name:
403             name = raw_input('Login name: ')
404         while not password:
405             password = getpass.getpass('  password: ')
407     # get the instance
408     instance = roundup.instance.open(instance_home)
410     function = figureCommands().get(command, None)
412     # not a valid command
413     if function is None:
414         usage('Unknown command "%s"'%command)
415         return 1
417     db = instance.open(name or 'admin')
418     try:
419         return function(db, args[1:])
420     finally:
421         db.close()
423     return 1
426 if __name__ == '__main__':
427     sys.exit(main())
430 # $Log: not supported by cvs2svn $
431 # Revision 1.18  2001/09/18 22:58:37  richard
433 # Added some more help to roundu-admin
435 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
436 # added missing 'import' statements.
438 # Revision 1.16  2001/08/12 06:32:36  richard
439 # using isinstance(blah, Foo) now instead of isFooType
441 # Revision 1.15  2001/08/07 00:24:42  richard
442 # stupid typo
444 # Revision 1.14  2001/08/07 00:15:51  richard
445 # Added the copyright/license notice to (nearly) all files at request of
446 # Bizar Software.
448 # Revision 1.13  2001/08/05 07:44:13  richard
449 # Instances are now opened by a special function that generates a unique
450 # module name for the instances on import time.
452 # Revision 1.12  2001/08/03 01:28:33  richard
453 # Used the much nicer load_package, pointed out by Steve Majewski.
455 # Revision 1.11  2001/08/03 00:59:34  richard
456 # Instance import now imports the instance using imp.load_module so that
457 # we can have instance homes of "roundup" or other existing python package
458 # names.
460 # Revision 1.10  2001/07/30 08:12:17  richard
461 # Added time logging and file uploading to the templates.
463 # Revision 1.9  2001/07/30 03:52:55  richard
464 # init help now lists templates and backends
466 # Revision 1.8  2001/07/30 02:37:07  richard
467 # Freshen is really broken. Commented out.
469 # Revision 1.7  2001/07/30 01:28:46  richard
470 # Bugfixes
472 # Revision 1.6  2001/07/30 00:57:51  richard
473 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
474 # better internal structure. It's just BETTER. :)
476 # Revision 1.5  2001/07/30 00:04:48  richard
477 # Made the "init" prompting more friendly.
479 # Revision 1.4  2001/07/29 07:01:39  richard
480 # Added vim command to all source so that we don't get no steenkin' tabs :)
482 # Revision 1.3  2001/07/23 08:45:28  richard
483 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
484 # workable instance_home set up :)
485 # _and_ anydbm has had its first test :)
487 # Revision 1.2  2001/07/23 08:20:44  richard
488 # Moved over to using marshal in the bsddb and anydbm backends.
489 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
490 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
492 # Revision 1.1  2001/07/23 03:46:48  richard
493 # moving the bin files to facilitate out-of-the-boxness
495 # Revision 1.1  2001/07/22 11:15:45  richard
496 # More Grande Splite stuff
499 # vim: set filetype=python ts=4 sw=4 et si