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())
598 #
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.
603 #
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.
607 #
608 # Revision 1.25 2001/10/10 04:12:32 richard
609 # The setup.cfg file is just causing pain. Away it goes.
610 #
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.
615 #
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.
618 #
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.
622 #
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.
642 #
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).
650 #
651 # Revision 1.19 2001/10/01 06:40:43 richard
652 # made do_get have the args in the correct order
653 #
654 # Revision 1.18 2001/09/18 22:58:37 richard
655 #
656 # Added some more help to roundu-admin
657 #
658 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
659 # added missing 'import' statements.
660 #
661 # Revision 1.16 2001/08/12 06:32:36 richard
662 # using isinstance(blah, Foo) now instead of isFooType
663 #
664 # Revision 1.15 2001/08/07 00:24:42 richard
665 # stupid typo
666 #
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.
670 #
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.
674 #
675 # Revision 1.12 2001/08/03 01:28:33 richard
676 # Used the much nicer load_package, pointed out by Steve Majewski.
677 #
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.
682 #
683 # Revision 1.10 2001/07/30 08:12:17 richard
684 # Added time logging and file uploading to the templates.
685 #
686 # Revision 1.9 2001/07/30 03:52:55 richard
687 # init help now lists templates and backends
688 #
689 # Revision 1.8 2001/07/30 02:37:07 richard
690 # Freshen is really broken. Commented out.
691 #
692 # Revision 1.7 2001/07/30 01:28:46 richard
693 # Bugfixes
694 #
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. :)
698 #
699 # Revision 1.5 2001/07/30 00:04:48 richard
700 # Made the "init" prompting more friendly.
701 #
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 :)
704 #
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 :)
709 #
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)
714 #
715 # Revision 1.1 2001/07/23 03:46:48 richard
716 # moving the bin files to facilitate out-of-the-boxness
717 #
718 # Revision 1.1 2001/07/22 11:15:45 richard
719 # More Grande Splite stuff
720 #
721 #
722 # vim: set filetype=python ts=4 sw=4 et si