85eee36243e2607bbeb6bd644c025b5d231506b0
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())
441 #
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.
462 #
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).
470 #
471 # Revision 1.19 2001/10/01 06:40:43 richard
472 # made do_get have the args in the correct order
473 #
474 # Revision 1.18 2001/09/18 22:58:37 richard
475 #
476 # Added some more help to roundu-admin
477 #
478 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
479 # added missing 'import' statements.
480 #
481 # Revision 1.16 2001/08/12 06:32:36 richard
482 # using isinstance(blah, Foo) now instead of isFooType
483 #
484 # Revision 1.15 2001/08/07 00:24:42 richard
485 # stupid typo
486 #
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.
490 #
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.
494 #
495 # Revision 1.12 2001/08/03 01:28:33 richard
496 # Used the much nicer load_package, pointed out by Steve Majewski.
497 #
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.
502 #
503 # Revision 1.10 2001/07/30 08:12:17 richard
504 # Added time logging and file uploading to the templates.
505 #
506 # Revision 1.9 2001/07/30 03:52:55 richard
507 # init help now lists templates and backends
508 #
509 # Revision 1.8 2001/07/30 02:37:07 richard
510 # Freshen is really broken. Commented out.
511 #
512 # Revision 1.7 2001/07/30 01:28:46 richard
513 # Bugfixes
514 #
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. :)
518 #
519 # Revision 1.5 2001/07/30 00:04:48 richard
520 # Made the "init" prompting more friendly.
521 #
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 :)
524 #
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 :)
529 #
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)
534 #
535 # Revision 1.1 2001/07/23 03:46:48 richard
536 # moving the bin files to facilitate out-of-the-boxness
537 #
538 # Revision 1.1 2001/07/22 11:15:45 richard
539 # More Grande Splite stuff
540 #
541 #
542 # vim: set filetype=python ts=4 sw=4 et si