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