Code

Added the Password property type. See "pydoc roundup.password" for
[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.22 2001-10-09 07:25:59 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, password
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.Password):
184                 props[key] = password.Password(value)
185             elif isinstance(type, hyperdb.Date):
186                 props[key] = date.Date(value)
187             elif isinstance(type, hyperdb.Interval):
188                 props[key] = date.Interval(value)
189             elif isinstance(type, hyperdb.Link):
190                 props[key] = value
191             elif isinstance(type, hyperdb.Multilink):
192                 props[key] = value.split(',')
193         apply(cl.set, (nodeid, ), props)
194     return 0
196 def do_find(db, args):
197     '''Usage: find classname propname=value ...
198     Find the nodes of the given class with a given property value.
200     Find the nodes of the given class with a given property value. The
201     value may be either the nodeid of the linked node, or its key value.
202     '''
203     classname = args[0]
204     cl = db.getclass(classname)
206     # look up the linked-to class and get the nodeid that has the value
207     propname, value = args[1].split('=')
208     num_re = re.compile('^\d+$')
209     if num_re.match(value):
210         nodeid = value
211     else:
212         propcl = cl.properties[propname].classname
213         propcl = db.getclass(propcl)
214         nodeid = propcl.lookup(value)
216     # now do the find
217     # TODO: handle the -c option
218     print cl.find(**{propname: nodeid})
219     return 0
221 def do_spec(db, args):
222     '''Usage: spec classname
223     Show the properties for a classname.
225     This lists the properties for a given class.
226     '''
227     classname = args[0]
228     cl = db.getclass(classname)
229     keyprop = cl.getkey()
230     for key, value in cl.properties.items():
231         if keyprop == key:
232             print '%s: %s (key property)'%(key, value)
233         else:
234             print '%s: %s'%(key, value)
236 def do_create(db, args, pretty_re=re.compile(r'<roundup\.hyperdb\.(.*)>')):
237     '''Usage: create classname property=value ...
238     Create a new entry of a given class.
240     This creates a new entry of the given class using the property
241     name=value arguments provided on the command line after the "create"
242     command.
243     '''
244     from roundup import hyperdb
246     classname = args[0]
247     cl = db.getclass(classname)
248     props = {}
249     properties = cl.getprops(protected = 0)
250     if len(args) == 1:
251         # ask for the properties
252         for key, value in properties.items():
253             if key == 'id': continue
254             m = pretty_re.match(str(value))
255             if m:
256                 value = m.group(1)
257             value = raw_input('%s (%s): '%(key.capitalize(), value))
258             if value:
259                 props[key] = value
260     else:
261         # use the args
262         for prop in args[1:]:
263             key, value = prop.split('=')
264             props[key] = value 
266     # convert types
267     for key in props.keys():
268         type =  properties[key]
269         if isinstance(type, hyperdb.Date):
270             props[key] = date.Date(value)
271         elif isinstance(type, hyperdb.Interval):
272             props[key] = date.Interval(value)
273         elif isinstance(type, hyperdb.Multilink):
274             props[key] = value.split(',')
276     if cl.getkey() and not props.has_key(cl.getkey()):
277         print "You must provide the '%s' property."%cl.getkey()
278     else:
279         print apply(cl.create, (), props)
281     return 0
283 def do_list(db, args):
284     '''Usage: list classname [property]
285     List the instances of a class.
287     Lists all instances of the given class along. If the property is not
288     specified, the  "label" property is used. The label property is tried
289     in order: the key, "name", "title" and then the first property,
290     alphabetically.
291     '''
292     classname = args[0]
293     cl = db.getclass(classname)
294     if len(args) > 1:
295         key = args[1]
296     else:
297         key = cl.labelprop()
298     # TODO: handle the -c option
299     for nodeid in cl.list():
300         value = cl.get(nodeid, key)
301         print "%4s: %s"%(nodeid, value)
302     return 0
304 def do_history(db, args):
305     '''Usage: history designator
306     Show the history entries of a designator.
308     Lists the journal entries for the node identified by the designator.
309     '''
310     classname, nodeid = roundupdb.splitDesignator(args[0])
311     # TODO: handle the -c option
312     print db.getclass(classname).history(nodeid)
313     return 0
315 def do_retire(db, args):
316     '''Usage: retire designator[,designator]*
317     Retire the node specified by designator.
319     This action indicates that a particular node is not to be retrieved by
320     the list or find commands, and its key value may be re-used.
321     '''
322     designators = string.split(args[0], ',')
323     for designator in designators:
324         classname, nodeid = roundupdb.splitDesignator(designator)
325         db.getclass(classname).retire(nodeid)
326     return 0
328 def do_freshen(db, args):
329     '''Usage: freshen
330     Freshen an existing instance.  **DO NOT USE**
332     This currently kills databases!!!!
334     This action should generally not be used. It reads in an instance
335     database and writes it again. In the future, is may also update
336     instance code to account for changes in templates. It's probably wise
337     not to use it anyway. Until we're sure it won't break things...
338     '''
339 #    for classname, cl in db.classes.items():
340 #        properties = cl.properties.items()
341 #        for nodeid in cl.list():
342 #            node = {}
343 #            for name, type in properties:
344 # isinstance(               if type, hyperdb.Multilink):
345 #                    node[name] = cl.get(nodeid, name, [])
346 #                else:
347 #                    node[name] = cl.get(nodeid, name, None)
348 #                db.setnode(classname, nodeid, node)
349     return 1
351 def figureCommands():
352     d = {}
353     for k, v in globals().items():
354         if k[:3] == 'do_':
355             d[k[3:]] = v
356     return d
358 def printInitOptions():
359     import roundup.templates
360     templates = roundup.templates.listTemplates()
361     print 'Templates:', ', '.join(templates)
362     import roundup.backends
363     backends = roundup.backends.__all__
364     print 'Back ends:', ', '.join(backends)
366 def main():
367     opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
369     # handle command-line args
370     instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
371     name = password = ''
372     if os.environ.has_key('ROUNDUP_LOGIN'):
373         l = os.environ['ROUNDUP_LOGIN'].split(':')
374         name = l[0]
375         if len(l) > 1:
376             password = l[1]
377     comma_sep = 0
378     for opt, arg in opts:
379         if opt == '-h':
380             usage()
381             return 0
382         if opt == '-i':
383             instance_home = arg
384         if opt == '-c':
385             comma_sep = 1
387     # figure the command
388     if not args:
389         usage('No command specified')
390         return 1
391     command = args[0]
393     # handle help now
394     if command == 'help':
395         if len(args)>1:
396             command = figureCommands().get(args[1], None)
397             if not command:
398                 usage('no such command "%s"'%args[1])
399                 return 1
400             print command.__doc__
401             if args[1] == 'init':
402                 printInitOptions()
403             return 0
404         usage()
405         return 0
406     if command == 'morehelp':
407         moreusage()
408         return 0
410     # make sure we have an instance_home
411     while not instance_home:
412         instance_home = raw_input('Enter instance home: ').strip()
414     # before we open the db, we may be doing an init
415     if command == 'init':
416         return do_init(instance_home, args)
418     function = figureCommands().get(command, None)
420     # not a valid command
421     if function is None:
422         usage('Unknown command "%s"'%command)
423         return 1
425     # get the instance
426     instance = roundup.instance.open(instance_home)
427     db = instance.open('admin')
429     # do the command
430     try:
431         return function(db, args[1:])
432     finally:
433         db.close()
435     return 1
438 if __name__ == '__main__':
439     sys.exit(main())
442 # $Log: not supported by cvs2svn $
443 # Revision 1.21  2001/10/05 02:23:24  richard
444 #  . roundup-admin create now prompts for property info if none is supplied
445 #    on the command-line.
446 #  . hyperdb Class getprops() method may now return only the mutable
447 #    properties.
448 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
449 #    now support anonymous user access (read-only, unless there's an
450 #    "anonymous" user, in which case write access is permitted). Login
451 #    handling has been moved into cgi_client.Client.main()
452 #  . The "extended" schema is now the default in roundup init.
453 #  . The schemas have had their page headings modified to cope with the new
454 #    login handling. Existing installations should copy the interfaces.py
455 #    file from the roundup lib directory to their instance home.
456 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
457 #    Ping - has been removed.
458 #  . Fixed a whole bunch of places in the CGI interface where we should have
459 #    been returning Not Found instead of throwing an exception.
460 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
461 #    an item now throws an exception.
463 # Revision 1.20  2001/10/04 02:12:42  richard
464 # Added nicer command-line item adding: passing no arguments will enter an
465 # interactive more which asks for each property in turn. While I was at it, I
466 # fixed an implementation problem WRT the spec - I wasn't raising a
467 # ValueError if the key property was missing from a create(). Also added a
468 # protected=boolean argument to getprops() so we can list only the mutable
469 # properties (defaults to yes, which lists the immutables).
471 # Revision 1.19  2001/10/01 06:40:43  richard
472 # made do_get have the args in the correct order
474 # Revision 1.18  2001/09/18 22:58:37  richard
476 # Added some more help to roundu-admin
478 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
479 # added missing 'import' statements.
481 # Revision 1.16  2001/08/12 06:32:36  richard
482 # using isinstance(blah, Foo) now instead of isFooType
484 # Revision 1.15  2001/08/07 00:24:42  richard
485 # stupid typo
487 # Revision 1.14  2001/08/07 00:15:51  richard
488 # Added the copyright/license notice to (nearly) all files at request of
489 # Bizar Software.
491 # Revision 1.13  2001/08/05 07:44:13  richard
492 # Instances are now opened by a special function that generates a unique
493 # module name for the instances on import time.
495 # Revision 1.12  2001/08/03 01:28:33  richard
496 # Used the much nicer load_package, pointed out by Steve Majewski.
498 # Revision 1.11  2001/08/03 00:59:34  richard
499 # Instance import now imports the instance using imp.load_module so that
500 # we can have instance homes of "roundup" or other existing python package
501 # names.
503 # Revision 1.10  2001/07/30 08:12:17  richard
504 # Added time logging and file uploading to the templates.
506 # Revision 1.9  2001/07/30 03:52:55  richard
507 # init help now lists templates and backends
509 # Revision 1.8  2001/07/30 02:37:07  richard
510 # Freshen is really broken. Commented out.
512 # Revision 1.7  2001/07/30 01:28:46  richard
513 # Bugfixes
515 # Revision 1.6  2001/07/30 00:57:51  richard
516 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
517 # better internal structure. It's just BETTER. :)
519 # Revision 1.5  2001/07/30 00:04:48  richard
520 # Made the "init" prompting more friendly.
522 # Revision 1.4  2001/07/29 07:01:39  richard
523 # Added vim command to all source so that we don't get no steenkin' tabs :)
525 # Revision 1.3  2001/07/23 08:45:28  richard
526 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
527 # workable instance_home set up :)
528 # _and_ anydbm has had its first test :)
530 # Revision 1.2  2001/07/23 08:20:44  richard
531 # Moved over to using marshal in the bsddb and anydbm backends.
532 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
533 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
535 # Revision 1.1  2001/07/23 03:46:48  richard
536 # moving the bin files to facilitate out-of-the-boxness
538 # Revision 1.1  2001/07/22 11:15:45  richard
539 # More Grande Splite stuff
542 # vim: set filetype=python ts=4 sw=4 et si