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.18 2001-09-18 22:58:37 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 [classic]: ').strip()
123 if not template:
124 template = 'classic'
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 designators = string.split(args[0], ',')
154 propname = 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 for key, value in cl.properties.items():
228 print '%s: %s'%(key, value)
230 def do_create(db, args):
231 '''Usage: create classname property=value ...
232 Create a new entry of a given class.
234 This creates a new entry of the given class using the property
235 name=value arguments provided on the command line after the "create"
236 command.
237 '''
238 from roundup import hyperdb
240 classname = args[0]
241 cl = db.getclass(classname)
242 props = {}
243 properties = cl.getprops()
244 for prop in args[1:]:
245 key, value = prop.split('=')
246 type = properties[key]
247 if isinstance(type, hyperdb.String):
248 props[key] = value
249 elif isinstance(type, hyperdb.Date):
250 props[key] = date.Date(value)
251 elif isinstance(type, hyperdb.Interval):
252 props[key] = date.Interval(value)
253 elif isinstance(type, hyperdb.Link):
254 props[key] = value
255 elif isinstance(type, hyperdb.Multilink):
256 props[key] = value.split(',')
257 print apply(cl.create, (), props)
258 return 0
260 def do_list(db, args):
261 '''Usage: list classname [property]
262 List the instances of a class.
264 Lists all instances of the given class along. If the property is not
265 specified, the "label" property is used. The label property is tried
266 in order: the key, "name", "title" and then the first property,
267 alphabetically.
268 '''
269 classname = args[0]
270 cl = db.getclass(classname)
271 if len(args) > 1:
272 key = args[1]
273 else:
274 key = cl.labelprop()
275 # TODO: handle the -c option
276 for nodeid in cl.list():
277 value = cl.get(nodeid, key)
278 print "%4s: %s"%(nodeid, value)
279 return 0
281 def do_history(db, args):
282 '''Usage: history designator
283 Show the history entries of a designator.
285 Lists the journal entries for the node identified by the designator.
286 '''
287 classname, nodeid = roundupdb.splitDesignator(args[0])
288 # TODO: handle the -c option
289 print db.getclass(classname).history(nodeid)
290 return 0
292 def do_retire(db, args):
293 '''Usage: retire designator[,designator]*
294 Retire the node specified by designator.
296 This action indicates that a particular node is not to be retrieved by
297 the list or find commands, and its key value may be re-used.
298 '''
299 designators = string.split(args[0], ',')
300 for designator in designators:
301 classname, nodeid = roundupdb.splitDesignator(designator)
302 db.getclass(classname).retire(nodeid)
303 return 0
305 def do_freshen(db, args):
306 '''Usage: freshen
307 Freshen an existing instance. **DO NOT USE**
309 This currently kills databases!!!!
311 This action should generally not be used. It reads in an instance
312 database and writes it again. In the future, is may also update
313 instance code to account for changes in templates. It's probably wise
314 not to use it anyway. Until we're sure it won't break things...
315 '''
316 # for classname, cl in db.classes.items():
317 # properties = cl.properties.items()
318 # for nodeid in cl.list():
319 # node = {}
320 # for name, type in properties:
321 # isinstance( if type, hyperdb.Multilink):
322 # node[name] = cl.get(nodeid, name, [])
323 # else:
324 # node[name] = cl.get(nodeid, name, None)
325 # db.setnode(classname, nodeid, node)
326 return 1
328 def figureCommands():
329 d = {}
330 for k, v in globals().items():
331 if k[:3] == 'do_':
332 d[k[3:]] = v
333 return d
335 def printInitOptions():
336 import roundup.templates
337 templates = roundup.templates.listTemplates()
338 print 'Templates:', ', '.join(templates)
339 import roundup.backends
340 backends = roundup.backends.__all__
341 print 'Back ends:', ', '.join(backends)
343 def main():
344 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
346 # handle command-line args
347 instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
348 name = password = ''
349 if os.environ.has_key('ROUNDUP_LOGIN'):
350 l = os.environ['ROUNDUP_LOGIN'].split(':')
351 name = l[0]
352 if len(l) > 1:
353 password = l[1]
354 comma_sep = 0
355 for opt, arg in opts:
356 if opt == '-h':
357 usage()
358 return 0
359 if opt == '-i':
360 instance_home = arg
361 if opt == '-u':
362 l = arg.split(':')
363 name = l[0]
364 if len(l) > 1:
365 password = l[1]
366 if opt == '-c':
367 comma_sep = 1
369 # figure the command
370 if not args:
371 usage('No command specified')
372 return 1
373 command = args[0]
375 # handle help now
376 if command == 'help':
377 if len(args)>1:
378 command = figureCommands().get(args[1], None)
379 if not command:
380 usage('no such command "%s"'%args[1])
381 return 1
382 print command.__doc__
383 if args[1] == 'init':
384 printInitOptions()
385 return 0
386 usage()
387 return 0
388 if command == 'morehelp':
389 moreusage()
390 return 0
392 # make sure we have an instance_home
393 while not instance_home:
394 instance_home = raw_input('Enter instance home: ').strip()
396 # before we open the db, we may be doing an init
397 if command == 'init':
398 return do_init(instance_home, args)
400 # open the database
401 if command in ('create', 'set', 'retire', 'freshen'):
402 while not name:
403 name = raw_input('Login name: ')
404 while not password:
405 password = getpass.getpass(' password: ')
407 # get the instance
408 instance = roundup.instance.open(instance_home)
410 function = figureCommands().get(command, None)
412 # not a valid command
413 if function is None:
414 usage('Unknown command "%s"'%command)
415 return 1
417 db = instance.open(name or 'admin')
418 try:
419 return function(db, args[1:])
420 finally:
421 db.close()
423 return 1
426 if __name__ == '__main__':
427 sys.exit(main())
429 #
430 # $Log: not supported by cvs2svn $
431 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
432 # added missing 'import' statements.
433 #
434 # Revision 1.16 2001/08/12 06:32:36 richard
435 # using isinstance(blah, Foo) now instead of isFooType
436 #
437 # Revision 1.15 2001/08/07 00:24:42 richard
438 # stupid typo
439 #
440 # Revision 1.14 2001/08/07 00:15:51 richard
441 # Added the copyright/license notice to (nearly) all files at request of
442 # Bizar Software.
443 #
444 # Revision 1.13 2001/08/05 07:44:13 richard
445 # Instances are now opened by a special function that generates a unique
446 # module name for the instances on import time.
447 #
448 # Revision 1.12 2001/08/03 01:28:33 richard
449 # Used the much nicer load_package, pointed out by Steve Majewski.
450 #
451 # Revision 1.11 2001/08/03 00:59:34 richard
452 # Instance import now imports the instance using imp.load_module so that
453 # we can have instance homes of "roundup" or other existing python package
454 # names.
455 #
456 # Revision 1.10 2001/07/30 08:12:17 richard
457 # Added time logging and file uploading to the templates.
458 #
459 # Revision 1.9 2001/07/30 03:52:55 richard
460 # init help now lists templates and backends
461 #
462 # Revision 1.8 2001/07/30 02:37:07 richard
463 # Freshen is really broken. Commented out.
464 #
465 # Revision 1.7 2001/07/30 01:28:46 richard
466 # Bugfixes
467 #
468 # Revision 1.6 2001/07/30 00:57:51 richard
469 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
470 # better internal structure. It's just BETTER. :)
471 #
472 # Revision 1.5 2001/07/30 00:04:48 richard
473 # Made the "init" prompting more friendly.
474 #
475 # Revision 1.4 2001/07/29 07:01:39 richard
476 # Added vim command to all source so that we don't get no steenkin' tabs :)
477 #
478 # Revision 1.3 2001/07/23 08:45:28 richard
479 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
480 # workable instance_home set up :)
481 # _and_ anydbm has had its first test :)
482 #
483 # Revision 1.2 2001/07/23 08:20:44 richard
484 # Moved over to using marshal in the bsddb and anydbm backends.
485 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
486 # retired - mod hyperdb.Class.list() so it lists retired nodes)
487 #
488 # Revision 1.1 2001/07/23 03:46:48 richard
489 # moving the bin files to facilitate out-of-the-boxness
490 #
491 # Revision 1.1 2001/07/22 11:15:45 richard
492 # More Grande Splite stuff
493 #
494 #
495 # vim: set filetype=python ts=4 sw=4 et si