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