Code

42ec53237c89da289b53b70df6e4d4d8f1e5047e
[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.25 2001-10-10 04:12:32 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 try:
28     import csv
29 except ImportError:
30     csv = None
31 from roundup import date, roundupdb, init, password
32 import roundup.instance
34 def usage(message=''):
35     if message: message = 'Problem: '+message+'\n'
36     commands = []
37     for command in figureCommands().values():
38         h = command.__doc__.split('\n')[0]
39         commands.append(h[7:])
40     commands.sort()
41     print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
43 Commands:
44  %s
45 Help:
46  roundup-admin -h
47  roundup-admin help                       -- this help
48  roundup-admin help <command>             -- command-specific help
49  roundup-admin morehelp                   -- even more detailed help
50 Options:
51  -i instance home  -- specify the issue tracker "home directory" to administer
52  -u                -- the user[:password] to use for commands
53  -c                -- when outputting lists of data, just comma-separate them'''%(
54 message, '\n '.join(commands))
56 def moreusage(message=''):
57     usage(message)
58     print '''
59 All commands (except help) require an instance specifier. This is just the path
60 to the roundup instance you're working with. A roundup instance is where 
61 roundup keeps the database and configuration file that defines an issue
62 tracker. It may be thought of as the issue tracker's "home directory". It may
63 be specified in the environment variable ROUNDUP_INSTANCE or on the command
64 line as "-i instance".
66 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
68 Property values are represented as strings in command arguments and in the
69 printed results:
70  . Strings are, well, strings.
71  . Date values are printed in the full date format in the local time zone, and
72    accepted in the full format or any of the partial formats explained below.
73  . Link values are printed as node designators. When given as an argument,
74    node designators and key strings are both accepted.
75  . Multilink values are printed as lists of node designators joined by commas.
76    When given as an argument, node designators and key strings are both
77    accepted; an empty string, a single node, or a list of nodes joined by
78    commas is accepted.
80 When multiple nodes are specified to the roundup get or roundup set
81 commands, the specified properties are retrieved or set on all the listed
82 nodes. 
84 When multiple results are returned by the roundup get or roundup find
85 commands, they are printed one per line (default) or joined by commas (with
86 the -c) option. 
88 Where the command changes data, a login name/password is required. The
89 login may be specified as either "name" or "name:password".
90  . ROUNDUP_LOGIN environment variable
91  . the -u command-line option
92 If either the name or password is not supplied, they are obtained from the
93 command-line. 
95 Date format examples:
96   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
97   "2000-04-17" means <Date 2000-04-17.00:00:00>
98   "01-25" means <Date yyyy-01-25.00:00:00>
99   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
100   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
101   "14:25" means <Date yyyy-mm-dd.19:25:00>
102   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
103   "." means "right now"
105 Command help:
106 '''
107     for name, command in figureCommands().items():
108         print '%s:'%name
109         print '   ',command.__doc__
111 def do_init(instance_home, args):
112     '''Usage: init [template [backend [admin password]]]
113     Initialise a new Roundup instance.
115     The command will prompt for the instance home directory (if not supplied
116     through INSTANCE_HOME or the -i option. The template, backend and admin
117     password may be specified on the command-line as arguments, in that order.
118     '''
119     # select template
120     import roundup.templates
121     templates = roundup.templates.listTemplates()
122     template = len(args) > 1 and args[1] or ''
123     if template not in templates:
124         print 'Templates:', ', '.join(templates)
125     while template not in templates:
126         template = raw_input('Select template [extended]: ').strip()
127         if not template:
128             template = 'extended'
130     import roundup.backends
131     backends = roundup.backends.__all__
132     backend = len(args) > 2 and args[2] or ''
133     if backend not in backends:
134         print 'Back ends:', ', '.join(backends)
135     while backend not in backends:
136         backend = raw_input('Select backend [anydbm]: ').strip()
137         if not backend:
138             backend = 'anydbm'
139     if len(args) > 3:
140         adminpw = confirm = args[3]
141     else:
142         adminpw = ''
143         confirm = 'x'
144     while adminpw != confirm:
145         adminpw = getpass.getpass('Admin Password: ')
146         confirm = getpass.getpass('       Confirm: ')
147     init.init(instance_home, template, backend, adminpw)
148     return 0
151 def do_get(db, args):
152     '''Usage: get property designator[,designator]*
153     Get the given property of one or more designator(s).
155     Retrieves the property value of the nodes specified by the designators.
156     '''
157     propname = args[0]
158     designators = string.split(args[1], ',')
159     # TODO: handle the -c option
160     for designator in designators:
161         classname, nodeid = roundupdb.splitDesignator(designator)
162         print db.getclass(classname).get(nodeid, propname)
163     return 0
166 def do_set(db, args):
167     '''Usage: set designator[,designator]* propname=value ...
168     Set the given property of one or more designator(s).
170     Sets the property to the value for all designators given.
171     '''
172     from roundup import hyperdb
174     designators = string.split(args[0], ',')
175     props = {}
176     for prop in args[1:]:
177         key, value = prop.split('=')
178         props[key] = value
179     for designator in designators:
180         classname, nodeid = roundupdb.splitDesignator(designator)
181         cl = db.getclass(classname)
182         properties = cl.getprops()
183         for key, value in props.items():
184             type =  properties[key]
185             if isinstance(type, hyperdb.String):
186                 continue
187             elif isinstance(type, hyperdb.Password):
188                 props[key] = password.Password(value)
189             elif isinstance(type, hyperdb.Date):
190                 props[key] = date.Date(value)
191             elif isinstance(type, hyperdb.Interval):
192                 props[key] = date.Interval(value)
193             elif isinstance(type, hyperdb.Link):
194                 props[key] = value
195             elif isinstance(type, hyperdb.Multilink):
196                 props[key] = value.split(',')
197         apply(cl.set, (nodeid, ), props)
198     return 0
200 def do_find(db, args):
201     '''Usage: find classname propname=value ...
202     Find the nodes of the given class with a given property value.
204     Find the nodes of the given class with a given property value. The
205     value may be either the nodeid of the linked node, or its key value.
206     '''
207     classname = args[0]
208     cl = db.getclass(classname)
210     # look up the linked-to class and get the nodeid that has the value
211     propname, value = args[1].split('=')
212     num_re = re.compile('^\d+$')
213     if num_re.match(value):
214         nodeid = value
215     else:
216         propcl = cl.properties[propname].classname
217         propcl = db.getclass(propcl)
218         nodeid = propcl.lookup(value)
220     # now do the find
221     # TODO: handle the -c option
222     print cl.find(**{propname: nodeid})
223     return 0
225 def do_spec(db, args):
226     '''Usage: spec classname
227     Show the properties for a classname.
229     This lists the properties for a given class.
230     '''
231     classname = args[0]
232     cl = db.getclass(classname)
233     keyprop = cl.getkey()
234     for key, value in cl.properties.items():
235         if keyprop == key:
236             print '%s: %s (key property)'%(key, value)
237         else:
238             print '%s: %s'%(key, value)
240 def do_create(db, args):
241     '''Usage: create classname property=value ...
242     Create a new entry of a given class.
244     This creates a new entry of the given class using the property
245     name=value arguments provided on the command line after the "create"
246     command.
247     '''
248     from roundup import hyperdb
250     classname = args[0]
251     cl = db.getclass(classname)
252     props = {}
253     properties = cl.getprops(protected = 0)
254     if len(args) == 1:
255         # ask for the properties
256         for key, value in properties.items():
257             if key == 'id': continue
258             name = value.__class__.__name__
259             if isinstance(value , hyperdb.Password):
260                 again = None
261                 while value != again:
262                     value = getpass.getpass('%s (Password): '%key.capitalize())
263                     again = getpass.getpass('   %s (Again): '%key.capitalize())
264                     if value != again: print 'Sorry, try again...'
265                 if value:
266                     props[key] = value
267             else:
268                 value = raw_input('%s (%s): '%(key.capitalize(), name))
269                 if value:
270                     props[key] = value
271     else:
272         # use the args
273         for prop in args[1:]:
274             key, value = prop.split('=')
275             props[key] = value 
277     # convert types
278     for key in props.keys():
279         type =  properties[key]
280         if isinstance(type, hyperdb.Date):
281             props[key] = date.Date(value)
282         elif isinstance(type, hyperdb.Interval):
283             props[key] = date.Interval(value)
284         elif isinstance(type, hyperdb.Password):
285             props[key] = password.Password(value)
286         elif isinstance(type, hyperdb.Multilink):
287             props[key] = value.split(',')
289     if cl.getkey() and not props.has_key(cl.getkey()):
290         print "You must provide the '%s' property."%cl.getkey()
291     else:
292         print apply(cl.create, (), props)
294     return 0
296 def do_list(db, args):
297     '''Usage: list classname [property]
298     List the instances of a class.
300     Lists all instances of the given class along. If the property is not
301     specified, the  "label" property is used. The label property is tried
302     in order: the key, "name", "title" and then the first property,
303     alphabetically.
304     '''
305     classname = args[0]
306     cl = db.getclass(classname)
307     if len(args) > 1:
308         key = args[1]
309     else:
310         key = cl.labelprop()
311     # TODO: handle the -c option
312     for nodeid in cl.list():
313         value = cl.get(nodeid, key)
314         print "%4s: %s"%(nodeid, value)
315     return 0
317 def do_history(db, args):
318     '''Usage: history designator
319     Show the history entries of a designator.
321     Lists the journal entries for the node identified by the designator.
322     '''
323     classname, nodeid = roundupdb.splitDesignator(args[0])
324     # TODO: handle the -c option
325     print db.getclass(classname).history(nodeid)
326     return 0
328 def do_retire(db, args):
329     '''Usage: retire designator[,designator]*
330     Retire the node specified by designator.
332     This action indicates that a particular node is not to be retrieved by
333     the list or find commands, and its key value may be re-used.
334     '''
335     designators = string.split(args[0], ',')
336     for designator in designators:
337         classname, nodeid = roundupdb.splitDesignator(designator)
338         db.getclass(classname).retire(nodeid)
339     return 0
341 def do_export(db, args):
342     '''Usage: export class[,class] destination_dir
343     Export the database to CSV files by class in the given directory.
345     This action exports the current data from the database into
346     comma-separated files that are placed in the nominated destination
347     directory. The journals are not exported.
348     '''
349     if len(args) < 2:
350         print do_export.__doc__
351         return 1
352     classes = string.split(args[0], ',')
353     dir = args[1]
355     # use the csv parser if we can - it's faster
356     if csv is not None:
357         p = csv.parser()
359     # do all the classes specified
360     for classname in classes:
361         cl = db.getclass(classname)
362         f = open(os.path.join(dir, classname+'.csv'), 'w')
363         f.write(string.join(cl.properties.keys(), ',') + '\n')
365         # all nodes for this class
366         for nodeid in cl.list():
367             if csv is not None:
368                s = p.join(map(str, cl.getnode(nodeid).values(protected=0)))
369                f.write(s + '\n')
370             else:
371                l = []
372                # escape the individual entries to they're valid CSV
373                for entry in map(str, cl.getnode(nodeid).values(protected=0)):
374                   if '"' in entry:
375                       entry = '""'.join(entry.split('"'))
376                   if ',' in entry:
377                       entry = '"%s"'%entry
378                   l.append(entry)
379                f.write(','.join(l) + '\n')
380     return 0
382 def do_import(db, args):
383     '''Usage: import class file
384     Import the contents of the CSV file as new nodes for the given class.
386     The file must define the same properties as the class (including having
387     a "header" line with those property names.) The new nodes are added to
388     the existing database - if you want to create a new database using the
389     imported data, then create a new database (or, tediously, retire all
390     the old data.)
391     '''
392     if len(args) < 2:
393         print do_export.__doc__
394         return 1
395     if csv is None:
396         print 'Sorry, you need the csv module to use this function.'
397         print 'Get it from: http://www.object-craft.com.au/projects/csv/'
398         return 1
400     from roundup import hyperdb
402     # ensure that the properties and the CSV file headings match
403     cl = db.getclass(args[0])
404     f = open(args[1])
405     p = csv.parser()
406     file_props = p.parse(f.readline())
407     props = cl.properties.keys()
408     m = file_props[:]
409     m.sort()
410     props.sort()
411     if m != props:
412         print do_export.__doc__
413         print "\n\nFile doesn't define the same properties"
414         return 1
416     # loop through the file and create a node for each entry
417     n = range(len(props))
418     while 1:
419         line = f.readline()
420         if not line: break
422         # parse lines until we get a complete entry
423         while 1:
424             l = p.parse(line)
425             if l: break
427         # make the new node's property map
428         d = {}
429         for i in n:
430             value = l[i]
431             key = file_props[i]
432             type = cl.properties[key]
433             if isinstance(type, hyperdb.Date):
434                 value = date.Date(value)
435             elif isinstance(type, hyperdb.Interval):
436                 value = date.Interval(value)
437             elif isinstance(type, hyperdb.Password):
438                 pwd = password.Password()
439                 pwd.unpack(value)
440                 value = pwd
441             elif isinstance(type, hyperdb.Multilink):
442                 value = value.split(',')
443             d[key] = value
445         # and create the new node
446         apply(cl.create, (), d)
447     return 0
449 def do_freshen(db, args):
450     '''Usage: freshen
451     Freshen an existing instance.  **DO NOT USE**
453     This currently kills databases!!!!
455     This action should generally not be used. It reads in an instance
456     database and writes it again. In the future, is may also update
457     instance code to account for changes in templates. It's probably wise
458     not to use it anyway. Until we're sure it won't break things...
459     '''
460 #    for classname, cl in db.classes.items():
461 #        properties = cl.properties.items()
462 #        for nodeid in cl.list():
463 #            node = {}
464 #            for name, type in properties:
465 # isinstance(               if type, hyperdb.Multilink):
466 #                    node[name] = cl.get(nodeid, name, [])
467 #                else:
468 #                    node[name] = cl.get(nodeid, name, None)
469 #                db.setnode(classname, nodeid, node)
470     return 1
472 def figureCommands():
473     d = {}
474     for k, v in globals().items():
475         if k[:3] == 'do_':
476             d[k[3:]] = v
477     return d
479 def printInitOptions():
480     import roundup.templates
481     templates = roundup.templates.listTemplates()
482     print 'Templates:', ', '.join(templates)
483     import roundup.backends
484     backends = roundup.backends.__all__
485     print 'Back ends:', ', '.join(backends)
487 def main():
488     opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
490     # handle command-line args
491     instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
492     name = password = ''
493     if os.environ.has_key('ROUNDUP_LOGIN'):
494         l = os.environ['ROUNDUP_LOGIN'].split(':')
495         name = l[0]
496         if len(l) > 1:
497             password = l[1]
498     comma_sep = 0
499     for opt, arg in opts:
500         if opt == '-h':
501             usage()
502             return 0
503         if opt == '-i':
504             instance_home = arg
505         if opt == '-c':
506             comma_sep = 1
508     # figure the command
509     if not args:
510         usage('No command specified')
511         return 1
512     command = args[0]
514     # handle help now
515     if command == 'help':
516         if len(args)>1:
517             command = figureCommands().get(args[1], None)
518             if not command:
519                 usage('no such command "%s"'%args[1])
520                 return 1
521             print command.__doc__
522             if args[1] == 'init':
523                 printInitOptions()
524             return 0
525         usage()
526         return 0
527     if command == 'morehelp':
528         moreusage()
529         return 0
531     # make sure we have an instance_home
532     while not instance_home:
533         instance_home = raw_input('Enter instance home: ').strip()
535     # before we open the db, we may be doing an init
536     if command == 'init':
537         return do_init(instance_home, args)
539     function = figureCommands().get(command, None)
541     # not a valid command
542     if function is None:
543         usage('Unknown command "%s"'%command)
544         return 1
546     # get the instance
547     instance = roundup.instance.open(instance_home)
548     db = instance.open('admin')
550     if len(args) < 2:
551         print function.__doc__
552         return 1
554     # do the command
555     try:
556         return function(db, args[1:])
557     finally:
558         db.close()
560     return 1
563 if __name__ == '__main__':
564     sys.exit(main())
567 # $Log: not supported by cvs2svn $
568 # Revision 1.24  2001/10/10 03:54:57  richard
569 # Added database importing and exporting through CSV files.
570 # Uses the csv module from object-craft for exporting if it's available.
571 # Requires the csv module for importing.
573 # Revision 1.23  2001/10/09 23:36:25  richard
574 # Spit out command help if roundup-admin command doesn't get an argument.
576 # Revision 1.22  2001/10/09 07:25:59  richard
577 # Added the Password property type. See "pydoc roundup.password" for
578 # implementation details. Have updated some of the documentation too.
580 # Revision 1.21  2001/10/05 02:23:24  richard
581 #  . roundup-admin create now prompts for property info if none is supplied
582 #    on the command-line.
583 #  . hyperdb Class getprops() method may now return only the mutable
584 #    properties.
585 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
586 #    now support anonymous user access (read-only, unless there's an
587 #    "anonymous" user, in which case write access is permitted). Login
588 #    handling has been moved into cgi_client.Client.main()
589 #  . The "extended" schema is now the default in roundup init.
590 #  . The schemas have had their page headings modified to cope with the new
591 #    login handling. Existing installations should copy the interfaces.py
592 #    file from the roundup lib directory to their instance home.
593 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
594 #    Ping - has been removed.
595 #  . Fixed a whole bunch of places in the CGI interface where we should have
596 #    been returning Not Found instead of throwing an exception.
597 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
598 #    an item now throws an exception.
600 # Revision 1.20  2001/10/04 02:12:42  richard
601 # Added nicer command-line item adding: passing no arguments will enter an
602 # interactive more which asks for each property in turn. While I was at it, I
603 # fixed an implementation problem WRT the spec - I wasn't raising a
604 # ValueError if the key property was missing from a create(). Also added a
605 # protected=boolean argument to getprops() so we can list only the mutable
606 # properties (defaults to yes, which lists the immutables).
608 # Revision 1.19  2001/10/01 06:40:43  richard
609 # made do_get have the args in the correct order
611 # Revision 1.18  2001/09/18 22:58:37  richard
613 # Added some more help to roundu-admin
615 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
616 # added missing 'import' statements.
618 # Revision 1.16  2001/08/12 06:32:36  richard
619 # using isinstance(blah, Foo) now instead of isFooType
621 # Revision 1.15  2001/08/07 00:24:42  richard
622 # stupid typo
624 # Revision 1.14  2001/08/07 00:15:51  richard
625 # Added the copyright/license notice to (nearly) all files at request of
626 # Bizar Software.
628 # Revision 1.13  2001/08/05 07:44:13  richard
629 # Instances are now opened by a special function that generates a unique
630 # module name for the instances on import time.
632 # Revision 1.12  2001/08/03 01:28:33  richard
633 # Used the much nicer load_package, pointed out by Steve Majewski.
635 # Revision 1.11  2001/08/03 00:59:34  richard
636 # Instance import now imports the instance using imp.load_module so that
637 # we can have instance homes of "roundup" or other existing python package
638 # names.
640 # Revision 1.10  2001/07/30 08:12:17  richard
641 # Added time logging and file uploading to the templates.
643 # Revision 1.9  2001/07/30 03:52:55  richard
644 # init help now lists templates and backends
646 # Revision 1.8  2001/07/30 02:37:07  richard
647 # Freshen is really broken. Commented out.
649 # Revision 1.7  2001/07/30 01:28:46  richard
650 # Bugfixes
652 # Revision 1.6  2001/07/30 00:57:51  richard
653 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
654 # better internal structure. It's just BETTER. :)
656 # Revision 1.5  2001/07/30 00:04:48  richard
657 # Made the "init" prompting more friendly.
659 # Revision 1.4  2001/07/29 07:01:39  richard
660 # Added vim command to all source so that we don't get no steenkin' tabs :)
662 # Revision 1.3  2001/07/23 08:45:28  richard
663 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
664 # workable instance_home set up :)
665 # _and_ anydbm has had its first test :)
667 # Revision 1.2  2001/07/23 08:20:44  richard
668 # Moved over to using marshal in the bsddb and anydbm backends.
669 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
670 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
672 # Revision 1.1  2001/07/23 03:46:48  richard
673 # moving the bin files to facilitate out-of-the-boxness
675 # Revision 1.1  2001/07/22 11:15:45  richard
676 # More Grande Splite stuff
679 # vim: set filetype=python ts=4 sw=4 et si