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.17 2001-08-28 05:58:33 anthonybaxter 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
42 Help:
43 roundup-admin -h
44 roundup-admin help
45 -- this help
46 roundup-admin help <command>
47 -- command-specific help
48 roundup-admin morehelp
49 -- even more detailed help
51 '''%(message, '\n '.join(commands))
53 def moreusage(message=''):
54 usage(message)
55 print '''
56 All commands (except help) require an instance specifier. This is just the path
57 to the roundup instance you're working with. It may be specified in the
58 environment variable ROUNDUP_INSTANCE or on the command line as "-i instance".
60 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
62 Property values are represented as strings in command arguments and in the
63 printed results:
64 . Strings are, well, strings.
65 . Date values are printed in the full date format in the local time zone, and
66 accepted in the full format or any of the partial formats explained below.
67 . Link values are printed as node designators. When given as an argument,
68 node designators and key strings are both accepted.
69 . Multilink values are printed as lists of node designators joined by commas.
70 When given as an argument, node designators and key strings are both
71 accepted; an empty string, a single node, or a list of nodes joined by
72 commas is accepted.
74 When multiple nodes are specified to the roundup get or roundup set
75 commands, the specified properties are retrieved or set on all the listed
76 nodes.
78 When multiple results are returned by the roundup get or roundup find
79 commands, they are printed one per line (default) or joined by commas (with
80 the -c) option.
82 Where the command changes data, a login name/password is required. The
83 login may be specified as either "name" or "name:password".
84 . ROUNDUP_LOGIN environment variable
85 . the -u command-line option
86 If either the name or password is not supplied, they are obtained from the
87 command-line.
89 Date format examples:
90 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
91 "2000-04-17" means <Date 2000-04-17.00:00:00>
92 "01-25" means <Date yyyy-01-25.00:00:00>
93 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
94 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
95 "14:25" means <Date yyyy-mm-dd.19:25:00>
96 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
97 "." means "right now"
99 Command help:
100 '''
101 for name, command in figureCommands().items():
102 print '%s:'%name
103 print ' ',command.__doc__
105 def do_init(instance_home, args):
106 '''Usage: init [template [backend [admin password]]]
107 Initialise a new Roundup instance.
109 The command will prompt for the instance home directory (if not supplied
110 through INSTANCE_HOME or the -i option. The template, backend and admin
111 password may be specified on the command-line as arguments, in that order.
112 '''
113 # select template
114 import roundup.templates
115 templates = roundup.templates.listTemplates()
116 template = len(args) > 1 and args[1] or ''
117 if template not in templates:
118 print 'Templates:', ', '.join(templates)
119 while template not in templates:
120 template = raw_input('Select template [classic]: ').strip()
121 if not template:
122 template = 'classic'
124 import roundup.backends
125 backends = roundup.backends.__all__
126 backend = len(args) > 2 and args[2] or ''
127 if backend not in backends:
128 print 'Back ends:', ', '.join(backends)
129 while backend not in backends:
130 backend = raw_input('Select backend [anydbm]: ').strip()
131 if not backend:
132 backend = 'anydbm'
133 if len(args) > 3:
134 adminpw = confirm = args[3]
135 else:
136 adminpw = ''
137 confirm = 'x'
138 while adminpw != confirm:
139 adminpw = getpass.getpass('Admin Password: ')
140 confirm = getpass.getpass(' Confirm: ')
141 init.init(instance_home, template, backend, adminpw)
142 return 0
145 def do_get(db, args):
146 '''Usage: get property designator[,designator]*
147 Get the given property of one or more designator(s).
149 Retrieves the property value of the nodes specified by the designators.
150 '''
151 designators = string.split(args[0], ',')
152 propname = args[1]
153 # TODO: handle the -c option
154 for designator in designators:
155 classname, nodeid = roundupdb.splitDesignator(designator)
156 print db.getclass(classname).get(nodeid, propname)
157 return 0
160 def do_set(db, args):
161 '''Usage: set designator[,designator]* propname=value ...
162 Set the given property of one or more designator(s).
164 Sets the property to the value for all designators given.
165 '''
166 from roundup import hyperdb
168 designators = string.split(args[0], ',')
169 props = {}
170 for prop in args[1:]:
171 key, value = prop.split('=')
172 props[key] = value
173 for designator in designators:
174 classname, nodeid = roundupdb.splitDesignator(designator)
175 cl = db.getclass(classname)
176 properties = cl.getprops()
177 for key, value in props.items():
178 type = properties[key]
179 if isinstance(type, hyperdb.String):
180 continue
181 elif isinstance(type, hyperdb.Date):
182 props[key] = date.Date(value)
183 elif isinstance(type, hyperdb.Interval):
184 props[key] = date.Interval(value)
185 elif isinstance(type, hyperdb.Link):
186 props[key] = value
187 elif isinstance(type, hyperdb.Multilink):
188 props[key] = value.split(',')
189 apply(cl.set, (nodeid, ), props)
190 return 0
192 def do_find(db, args):
193 '''Usage: find classname propname=value ...
194 Find the nodes of the given class with a given property value.
196 Find the nodes of the given class with a given property value. The
197 value may be either the nodeid of the linked node, or its key value.
198 '''
199 classname = args[0]
200 cl = db.getclass(classname)
202 # look up the linked-to class and get the nodeid that has the value
203 propname, value = args[1].split('=')
204 num_re = re.compile('^\d+$')
205 if num_re.match(value):
206 nodeid = value
207 else:
208 propcl = cl.properties[propname].classname
209 propcl = db.getclass(propcl)
210 nodeid = propcl.lookup(value)
212 # now do the find
213 # TODO: handle the -c option
214 print cl.find(**{propname: nodeid})
215 return 0
217 def do_spec(db, args):
218 '''Usage: spec classname
219 Show the properties for a classname.
221 This lists the properties for a given class.
222 '''
223 classname = args[0]
224 cl = db.getclass(classname)
225 for key, value in cl.properties.items():
226 print '%s: %s'%(key, value)
228 def do_create(db, args):
229 '''Usage: create classname property=value ...
230 Create a new entry of a given class.
232 This creates a new entry of the given class using the property
233 name=value arguments provided on the command line after the "create"
234 command.
235 '''
236 from roundup import hyperdb
238 classname = args[0]
239 cl = db.getclass(classname)
240 props = {}
241 properties = cl.getprops()
242 for prop in args[1:]:
243 key, value = prop.split('=')
244 type = properties[key]
245 if isinstance(type, hyperdb.String):
246 props[key] = value
247 elif isinstance(type, hyperdb.Date):
248 props[key] = date.Date(value)
249 elif isinstance(type, hyperdb.Interval):
250 props[key] = date.Interval(value)
251 elif isinstance(type, hyperdb.Link):
252 props[key] = value
253 elif isinstance(type, hyperdb.Multilink):
254 props[key] = value.split(',')
255 print apply(cl.create, (), props)
256 return 0
258 def do_list(db, args):
259 '''Usage: list classname [property]
260 List the instances of a class.
262 Lists all instances of the given class along. If the property is not
263 specified, the "label" property is used. The label property is tried
264 in order: the key, "name", "title" and then the first property,
265 alphabetically.
266 '''
267 classname = args[0]
268 cl = db.getclass(classname)
269 if len(args) > 1:
270 key = args[1]
271 else:
272 key = cl.labelprop()
273 # TODO: handle the -c option
274 for nodeid in cl.list():
275 value = cl.get(nodeid, key)
276 print "%4s: %s"%(nodeid, value)
277 return 0
279 def do_history(db, args):
280 '''Usage: history designator
281 Show the history entries of a designator.
283 Lists the journal entries for the node identified by the designator.
284 '''
285 classname, nodeid = roundupdb.splitDesignator(args[0])
286 # TODO: handle the -c option
287 print db.getclass(classname).history(nodeid)
288 return 0
290 def do_retire(db, args):
291 '''Usage: retire designator[,designator]*
292 Retire the node specified by designator.
294 This action indicates that a particular node is not to be retrieved by
295 the list or find commands, and its key value may be re-used.
296 '''
297 designators = string.split(args[0], ',')
298 for designator in designators:
299 classname, nodeid = roundupdb.splitDesignator(designator)
300 db.getclass(classname).retire(nodeid)
301 return 0
303 def do_freshen(db, args):
304 '''Usage: freshen
305 Freshen an existing instance. **DO NOT USE**
307 This currently kills databases!!!!
309 This action should generally not be used. It reads in an instance
310 database and writes it again. In the future, is may also update
311 instance code to account for changes in templates. It's probably wise
312 not to use it anyway. Until we're sure it won't break things...
313 '''
314 # for classname, cl in db.classes.items():
315 # properties = cl.properties.items()
316 # for nodeid in cl.list():
317 # node = {}
318 # for name, type in properties:
319 # isinstance( if type, hyperdb.Multilink):
320 # node[name] = cl.get(nodeid, name, [])
321 # else:
322 # node[name] = cl.get(nodeid, name, None)
323 # db.setnode(classname, nodeid, node)
324 return 1
326 def figureCommands():
327 d = {}
328 for k, v in globals().items():
329 if k[:3] == 'do_':
330 d[k[3:]] = v
331 return d
333 def printInitOptions():
334 import roundup.templates
335 templates = roundup.templates.listTemplates()
336 print 'Templates:', ', '.join(templates)
337 import roundup.backends
338 backends = roundup.backends.__all__
339 print 'Back ends:', ', '.join(backends)
341 def main():
342 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
344 # handle command-line args
345 instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
346 name = password = ''
347 if os.environ.has_key('ROUNDUP_LOGIN'):
348 l = os.environ['ROUNDUP_LOGIN'].split(':')
349 name = l[0]
350 if len(l) > 1:
351 password = l[1]
352 comma_sep = 0
353 for opt, arg in opts:
354 if opt == '-h':
355 usage()
356 return 0
357 if opt == '-i':
358 instance_home = arg
359 if opt == '-u':
360 l = arg.split(':')
361 name = l[0]
362 if len(l) > 1:
363 password = l[1]
364 if opt == '-c':
365 comma_sep = 1
367 # figure the command
368 if not args:
369 usage('No command specified')
370 return 1
371 command = args[0]
373 # handle help now
374 if command == 'help':
375 if len(args)>1:
376 command = figureCommands().get(args[1], None)
377 if not command:
378 usage('no such command "%s"'%args[1])
379 return 1
380 print command.__doc__
381 if args[1] == 'init':
382 printInitOptions()
383 return 0
384 usage()
385 return 0
386 if command == 'morehelp':
387 moreusage()
388 return 0
390 # make sure we have an instance_home
391 while not instance_home:
392 instance_home = raw_input('Enter instance home: ').strip()
394 # before we open the db, we may be doing an init
395 if command == 'init':
396 return do_init(instance_home, args)
398 # open the database
399 if command in ('create', 'set', 'retire', 'freshen'):
400 while not name:
401 name = raw_input('Login name: ')
402 while not password:
403 password = getpass.getpass(' password: ')
405 # get the instance
406 instance = roundup.instance.open(instance_home)
408 function = figureCommands().get(command, None)
410 # not a valid command
411 if function is None:
412 usage('Unknown command "%s"'%command)
413 return 1
415 db = instance.open(name or 'admin')
416 try:
417 return function(db, args[1:])
418 finally:
419 db.close()
421 return 1
424 if __name__ == '__main__':
425 sys.exit(main())
427 #
428 # $Log: not supported by cvs2svn $
429 # Revision 1.16 2001/08/12 06:32:36 richard
430 # using isinstance(blah, Foo) now instead of isFooType
431 #
432 # Revision 1.15 2001/08/07 00:24:42 richard
433 # stupid typo
434 #
435 # Revision 1.14 2001/08/07 00:15:51 richard
436 # Added the copyright/license notice to (nearly) all files at request of
437 # Bizar Software.
438 #
439 # Revision 1.13 2001/08/05 07:44:13 richard
440 # Instances are now opened by a special function that generates a unique
441 # module name for the instances on import time.
442 #
443 # Revision 1.12 2001/08/03 01:28:33 richard
444 # Used the much nicer load_package, pointed out by Steve Majewski.
445 #
446 # Revision 1.11 2001/08/03 00:59:34 richard
447 # Instance import now imports the instance using imp.load_module so that
448 # we can have instance homes of "roundup" or other existing python package
449 # names.
450 #
451 # Revision 1.10 2001/07/30 08:12:17 richard
452 # Added time logging and file uploading to the templates.
453 #
454 # Revision 1.9 2001/07/30 03:52:55 richard
455 # init help now lists templates and backends
456 #
457 # Revision 1.8 2001/07/30 02:37:07 richard
458 # Freshen is really broken. Commented out.
459 #
460 # Revision 1.7 2001/07/30 01:28:46 richard
461 # Bugfixes
462 #
463 # Revision 1.6 2001/07/30 00:57:51 richard
464 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
465 # better internal structure. It's just BETTER. :)
466 #
467 # Revision 1.5 2001/07/30 00:04:48 richard
468 # Made the "init" prompting more friendly.
469 #
470 # Revision 1.4 2001/07/29 07:01:39 richard
471 # Added vim command to all source so that we don't get no steenkin' tabs :)
472 #
473 # Revision 1.3 2001/07/23 08:45:28 richard
474 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
475 # workable instance_home set up :)
476 # _and_ anydbm has had its first test :)
477 #
478 # Revision 1.2 2001/07/23 08:20:44 richard
479 # Moved over to using marshal in the bsddb and anydbm backends.
480 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
481 # retired - mod hyperdb.Class.list() so it lists retired nodes)
482 #
483 # Revision 1.1 2001/07/23 03:46:48 richard
484 # moving the bin files to facilitate out-of-the-boxness
485 #
486 # Revision 1.1 2001/07/22 11:15:45 richard
487 # More Grande Splite stuff
488 #
489 #
490 # vim: set filetype=python ts=4 sw=4 et si