42ec53237c89da289b53b70df6e4d4d8f1e5047e
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())
566 #
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.
572 #
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.
575 #
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.
579 #
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.
599 #
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).
607 #
608 # Revision 1.19 2001/10/01 06:40:43 richard
609 # made do_get have the args in the correct order
610 #
611 # Revision 1.18 2001/09/18 22:58:37 richard
612 #
613 # Added some more help to roundu-admin
614 #
615 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
616 # added missing 'import' statements.
617 #
618 # Revision 1.16 2001/08/12 06:32:36 richard
619 # using isinstance(blah, Foo) now instead of isFooType
620 #
621 # Revision 1.15 2001/08/07 00:24:42 richard
622 # stupid typo
623 #
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.
627 #
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.
631 #
632 # Revision 1.12 2001/08/03 01:28:33 richard
633 # Used the much nicer load_package, pointed out by Steve Majewski.
634 #
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.
639 #
640 # Revision 1.10 2001/07/30 08:12:17 richard
641 # Added time logging and file uploading to the templates.
642 #
643 # Revision 1.9 2001/07/30 03:52:55 richard
644 # init help now lists templates and backends
645 #
646 # Revision 1.8 2001/07/30 02:37:07 richard
647 # Freshen is really broken. Commented out.
648 #
649 # Revision 1.7 2001/07/30 01:28:46 richard
650 # Bugfixes
651 #
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. :)
655 #
656 # Revision 1.5 2001/07/30 00:04:48 richard
657 # Made the "init" prompting more friendly.
658 #
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 :)
661 #
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 :)
666 #
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)
671 #
672 # Revision 1.1 2001/07/23 03:46:48 richard
673 # moving the bin files to facilitate out-of-the-boxness
674 #
675 # Revision 1.1 2001/07/22 11:15:45 richard
676 # More Grande Splite stuff
677 #
678 #
679 # vim: set filetype=python ts=4 sw=4 et si