1 #! /usr/bin/python
2 # $Id: roundup-admin,v 1.7 2001-07-30 01:28:46 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
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.
177 '''
178 classname = args[0]
179 cl = db.getclass(classname)
181 # look up the linked-to class and get the nodeid that has the value
182 propname, value = args[1:].split('=')
183 propcl = cl[propname].classname
184 nodeid = propcl.lookup(value)
186 # now do the find
187 # TODO: handle the -c option
188 print cl.find(propname, nodeid)
189 return 0
191 def do_spec(db, args):
192 '''Usage: spec classname
193 Show the properties for a classname.
195 This lists the properties for a given class.
196 '''
197 classname = args[0]
198 cl = db.getclass(classname)
199 for key, value in cl.properties.items():
200 print '%s: %s'%(key, value)
202 def do_create(db, args):
203 '''Usage: create classname property=value ...
204 Create a new entry of a given class.
206 This creates a new entry of the given class using the property
207 name=value arguments provided on the command line after the "create"
208 command.
209 '''
210 classname = args[0]
211 cl = db.getclass(classname)
212 props = {}
213 properties = cl.getprops()
214 for prop in args[1:]:
215 key, value = prop.split('=')
216 type = properties[key]
217 if type.isStringType:
218 props[key] = value
219 elif type.isDateType:
220 props[key] = date.Date(value)
221 elif type.isIntervalType:
222 props[key] = date.Interval(value)
223 elif type.isLinkType:
224 props[key] = value
225 elif type.isMultilinkType:
226 props[key] = value.split(',')
227 print apply(cl.create, (), props)
228 return 0
230 def do_list(db, args):
231 '''Usage: list classname [property]
232 List the instances of a class.
234 Lists all instances of the given class along. If the property is not
235 specified, the "label" property is used. The label property is tried
236 in order: the key, "name", "title" and then the first property,
237 alphabetically.
238 '''
239 db = instance.open()
240 classname = args[0]
241 cl = db.getclass(classname)
242 if len(args) > 1:
243 key = args[1]
244 else:
245 key = cl.labelprop()
246 # TODO: handle the -c option
247 for nodeid in cl.list():
248 value = cl.get(nodeid, key)
249 print "%4s: %s"%(nodeid, value)
250 return 0
252 def do_history(db, args):
253 '''Usage: history designator
254 Show the history entries of a designator.
256 Lists the journal entries for the node identified by the designator.
257 '''
258 classname, nodeid = roundupdb.splitDesignator(args[0])
259 # TODO: handle the -c option
260 print db.getclass(classname).history(nodeid)
261 return 0
263 def do_retire(db, args):
264 '''Usage: retire designator[,designator]*
265 Retire the node specified by designator.
267 This action indicates that a particular node is not to be retrieved by
268 the list or find commands, and its key value may be re-used.
269 '''
270 designators = string.split(args[0], ',')
271 for designator in designators:
272 classname, nodeid = roundupdb.splitDesignator(designator)
273 db.getclass(classname).retire(nodeid)
274 return 0
276 def db_freshen(db, args):
277 '''Usage: freshen
278 Freshen an existing instance. **do not use
280 This action should generally not be used. It reads in an instance
281 database and writes it again. In the future, is may also update
282 instance code to account for changes in templates. It's probably wise
283 not to use it anyway. Until we're sure it won't break things...
284 '''
285 for classname, cl in db.classes.items():
286 properties = cl.properties.keys()
287 for nodeid in cl.list():
288 node = {}
289 for name in properties:
290 node[name] = cl.get(nodeid, name)
291 db.setnode(classname, nodeid, node)
292 return 0
294 def figureCommands():
295 d = {}
296 for k, v in globals().items():
297 if k[:3] == 'do_':
298 d[k[3:]] = v
299 return d
301 def main():
302 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
304 # handle command-line args
305 instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
306 name = password = ''
307 if os.environ.has_key('ROUNDUP_LOGIN'):
308 l = os.environ['ROUNDUP_LOGIN'].split(':')
309 name = l[0]
310 if len(l) > 1:
311 password = l[1]
312 comma_sep = 0
313 for opt, arg in opts:
314 if opt == '-h':
315 usage()
316 return 0
317 if opt == '-i':
318 instance_home = arg
319 if opt == '-u':
320 l = arg.split(':')
321 name = l[0]
322 if len(l) > 1:
323 password = l[1]
324 if opt == '-c':
325 comma_sep = 1
327 # figure the command
328 if not args:
329 usage('No command specified')
330 return 1
331 command = args[0]
333 # handle help now
334 if command == 'help':
335 if len(args)>1:
336 command = figureCommands().get(args[1], None)
337 if not command:
338 usage('no such command "%s"'%args[1])
339 return 1
340 print command.__doc__
341 return 0
342 usage()
343 return 0
344 if command == 'morehelp':
345 moreusage()
346 return 0
348 # make sure we have an instance_home
349 while not instance_home:
350 instance_home = raw_input('Enter instance home: ').strip()
352 # before we open the db, we may be doing an init
353 if command == 'init':
354 return do_init(instance_home, args)
356 # open the database
357 if command in ('create', 'set', 'retire'):
358 while not name:
359 name = raw_input('Login name: ')
360 while not password:
361 password = getpass.getpass(' password: ')
363 # get the instance
364 path, instance = os.path.split(instance_home)
365 sys.path.insert(0, path)
366 try:
367 instance = __import__(instance)
368 finally:
369 del sys.path[0]
371 command = figureCommands().get(command, None)
373 # not a valid command
374 if command is None:
375 usage()
376 return 1
378 db = instance.open(name or 'admin')
379 try:
380 return command(db, args[1:])
381 finally:
382 db.close()
384 return 1
387 if __name__ == '__main__':
388 sys.exit(main())
390 #
391 # $Log: not supported by cvs2svn $
392 # Revision 1.6 2001/07/30 00:57:51 richard
393 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
394 # better internal structure. It's just BETTER. :)
395 #
396 # Revision 1.5 2001/07/30 00:04:48 richard
397 # Made the "init" prompting more friendly.
398 #
399 # Revision 1.4 2001/07/29 07:01:39 richard
400 # Added vim command to all source so that we don't get no steenkin' tabs :)
401 #
402 # Revision 1.3 2001/07/23 08:45:28 richard
403 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
404 # workable instance_home set up :)
405 # _and_ anydbm has had its first test :)
406 #
407 # Revision 1.2 2001/07/23 08:20:44 richard
408 # Moved over to using marshal in the bsddb and anydbm backends.
409 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
410 # retired - mod hyperdb.Class.list() so it lists retired nodes)
411 #
412 # Revision 1.1 2001/07/23 03:46:48 richard
413 # moving the bin files to facilitate out-of-the-boxness
414 #
415 # Revision 1.1 2001/07/22 11:15:45 richard
416 # More Grande Splite stuff
417 #
418 #
419 # vim: set filetype=python ts=4 sw=4 et si