Code

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