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