152cd75682cb24e765a8abe6fde935c77f75f311
[invirt/packages/invirt-web.git] / templates / main.py
1 #!/usr/bin/python
2
3 import sys
4 import cgi
5 import os
6 import string
7 import subprocess
8 import re
9 import time
10 import cPickle
11 import base64
12 import sha
13 import hmac
14 import datetime
15
16 sys.stderr = sys.stdout
17 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
18
19 from Cheetah.Template import Template
20 from sipb_xen_database import *
21 import random
22
23 class MyException(Exception):
24     """Base class for my exceptions"""
25     pass
26
27 class InvalidInput(MyException):
28     """Exception for user-provided input is invalid but maybe in good faith.
29
30     This would include setting memory to negative (which might be a
31     typo) but not setting an invalid boot CD (which requires bypassing
32     the select box).
33     """
34     pass
35
36 class CodeError(MyException):
37     """Exception for internal errors or bad faith input."""
38     pass
39
40
41
42 def helppopup(subj):
43     """Return HTML code for a (?) link to a specified help topic"""
44     return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
45
46
47 global_dict = {}
48 global_dict['helppopup'] = helppopup
49
50
51 # ... and stolen from xend/uuid.py
52 def randomUUID():
53     """Generate a random UUID."""
54
55     return [ random.randint(0, 255) for _ in range(0, 16) ]
56
57 def uuidToString(u):
58     """Turn a numeric UUID to a hyphen-seperated one."""
59     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
60                      "%02x" * 6]) % tuple(u)
61
62 MAX_MEMORY_TOTAL = 512
63 MAX_MEMORY_SINGLE = 256
64 MIN_MEMORY_SINGLE = 16
65 MAX_DISK_TOTAL = 50
66 MAX_DISK_SINGLE = 50
67 MIN_DISK_SINGLE = 0.1
68 MAX_VMS_TOTAL = 10
69 MAX_VMS_ACTIVE = 4
70
71 def getMachinesByOwner(owner):
72     """Return the machines owned by a given owner."""
73     return Machine.select_by(owner=owner)
74
75 def maxMemory(user, machine=None, on=None):
76     """Return the maximum memory for a machine or a user.
77
78     If machine is None, return the memory available for a new 
79     machine.  Else, return the maximum that machine can have.
80
81     on is a dictionary from machines to booleans, whether a machine is
82     on.  If None, it is recomputed. XXX make this global?
83     """
84
85     machines = getMachinesByOwner(user.username)
86     if on is None:
87         on = getUptimes(machines)
88     active_machines = [x for x in machines if on[x]]
89     mem_usage = sum([x.memory for x in active_machines if x != machine])
90     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
91
92 def maxDisk(user, machine=None):
93     machines = getMachinesByOwner(user.username)
94     disk_usage = sum([sum([y.size for y in x.disks])
95                       for x in machines if x != machine])
96     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
97
98 def canAddVm(user, on=None):
99     machines = getMachinesByOwner(user.username)
100     if on is None:
101         on = getUptimes(machines)
102     active_machines = [x for x in machines if on[x]]
103     return (len(machines) < MAX_VMS_TOTAL and
104             len(active_machines) < MAX_VMS_ACTIVE)
105
106 def haveAccess(user, machine):
107     """Return whether a user has access to a machine"""
108     if user.username == 'moo':
109         return True
110     return machine.owner == user.username
111
112 def error(op, user, fields, err):
113     """Print an error page when a CodeError occurs"""
114     d = dict(op=op, user=user, errorMessage=str(err))
115     print Template(file='error.tmpl', searchList=[d, global_dict]);
116
117 def validMachineName(name):
118     """Check that name is valid for a machine name"""
119     if not name:
120         return False
121     charset = string.ascii_letters + string.digits + '-_'
122     if name[0] in '-_' or len(name) > 22:
123         return False
124     for x in name:
125         if x not in charset:
126             return False
127     return True
128
129 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
130     """Kinit with a given username and keytab"""
131
132     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
133                          stderr=subprocess.PIPE)
134     e = p.wait()
135     if e:
136         raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
137
138 def checkKinit():
139     """If we lack tickets, kinit."""
140     p = subprocess.Popen(['klist', '-s'])
141     if p.wait():
142         kinit()
143
144 def remctl(*args, **kws):
145     """Perform a remctl and return the output.
146
147     kinits if necessary, and outputs errors to stderr.
148     """
149     checkKinit()
150     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
151                          + list(args),
152                          stdout=subprocess.PIPE,
153                          stderr=subprocess.PIPE)
154     if kws.get('err'):
155         p.wait()
156         return p.stdout.read(), p.stderr.read()
157     if p.wait():
158         raise CodeError('ERROR on remctl %s: %s' %
159                           (args, p.stderr.read()))
160     return p.stdout.read()
161
162 def makeDisks():
163     """Update the lvm partitions to include all disks in the database."""
164     remctl('web', 'lvcreate')
165
166 def bootMachine(machine, cdtype):
167     """Boot a machine with a given boot CD.
168
169     If cdtype is None, give no boot cd.  Otherwise, it is the string
170     id of the CD (e.g. 'gutsy_i386')
171     """
172     if cdtype is not None:
173         remctl('web', 'vmboot', machine.name,
174                cdtype)
175     else:
176         remctl('web', 'vmboot', machine.name)
177
178 def registerMachine(machine):
179     """Register a machine to be controlled by the web interface"""
180     remctl('web', 'register', machine.name)
181
182 def unregisterMachine(machine):
183     """Unregister a machine to not be controlled by the web interface"""
184     remctl('web', 'unregister', machine.name)
185
186 def parseStatus(s):
187     """Parse a status string into nested tuples of strings.
188
189     s = output of xm list --long <machine_name>
190     """
191     values = re.split('([()])', s)
192     stack = [[]]
193     for v in values[2:-2]: #remove initial and final '()'
194         if not v:
195             continue
196         v = v.strip()
197         if v == '(':
198             stack.append([])
199         elif v == ')':
200             if len(stack[-1]) == 1:
201                 stack[-1].append('')
202             stack[-2].append(stack[-1])
203             stack.pop()
204         else:
205             if not v:
206                 continue
207             stack[-1].extend(v.split())
208     return stack[-1]
209
210 def getUptimes(machines):
211     """Return a dictionary mapping machine names to uptime strings"""
212     value_string = remctl('web', 'listvms')
213     lines = value_string.splitlines()
214     d = {}
215     for line in lines[1:]:
216         lst = line.split()
217         name, id = lst[:2]
218         uptime = ' '.join(lst[2:])
219         d[name] = uptime
220     ans = {}
221     for m in machines:
222         ans[m] = d.get(m.name)
223     return ans
224
225 def statusInfo(machine):
226     """Return the status list for a given machine.
227
228     Gets and parses xm list --long
229     """
230     value_string, err_string = remctl('list-long', machine.name, err=True)
231     if 'Unknown command' in err_string:
232         raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
233     elif 'does not exist' in err_string:
234         return None
235     elif err_string:
236         raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
237     status = parseStatus(value_string)
238     return status
239
240 def hasVnc(status):
241     """Does the machine with a given status list support VNC?"""
242     if status is None:
243         return False
244     for l in status:
245         if l[0] == 'device' and l[1][0] == 'vfb':
246             d = dict(l[1][1:])
247             return 'location' in d
248     return False
249
250 def createVm(user, name, memory, disk, is_hvm, cdrom):
251     """Create a VM and put it in the database"""
252     # put stuff in the table
253     transaction = ctx.current.create_transaction()
254     try:
255         if memory > maxMemory(user):
256             raise InvalidInput("Too much memory requested")
257         if disk > maxDisk(user) * 1024:
258             raise InvalidInput("Too much disk requested")
259         if not canAddVm(user):
260             raise InvalidInput("Too many VMs requested")
261         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
262         id = res.fetchone()[0]
263         machine = Machine()
264         machine.machine_id = id
265         machine.name = name
266         machine.memory = memory
267         machine.owner = user.username
268         machine.contact = user.email
269         machine.uuid = uuidToString(randomUUID())
270         machine.boot_off_cd = True
271         machine_type = Type.get_by(hvm=is_hvm)
272         machine.type_id = machine_type.type_id
273         ctx.current.save(machine)
274         disk = Disk(machine.machine_id, 
275                     'hda', disk)
276         open = NIC.select_by(machine_id=None)
277         if not open: #No IPs left!
278             raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
279         nic = open[0]
280         nic.machine_id = machine.machine_id
281         nic.hostname = name
282         ctx.current.save(nic)    
283         ctx.current.save(disk)
284         transaction.commit()
285     except:
286         transaction.rollback()
287         raise
288     registerMachine(machine)
289     makeDisks()
290     # tell it to boot with cdrom
291     bootMachine(machine, cdrom)
292
293     return machine
294
295 def validMemory(user, memory, machine=None):
296     """Parse and validate limits for memory for a given user and machine."""
297     try:
298         memory = int(memory)
299         if memory < MIN_MEMORY_SINGLE:
300             raise ValueError
301     except ValueError:
302         raise InvalidInput("Invalid memory amount; must be at least %s MB" %
303                           MIN_MEMORY_SINGLE)
304     if memory > maxMemory(user, machine):
305         raise InvalidInput("Too much memory requested")
306     return memory
307
308 def validDisk(user, disk, machine=None):
309     """Parse and validate limits for disk for a given user and machine."""
310     try:
311         disk = float(disk)
312         if disk > maxDisk(user, machine):
313             raise InvalidInput("Too much disk requested")
314         disk = int(disk * 1024)
315         if disk < MIN_DISK_SINGLE * 1024:
316             raise ValueError
317     except ValueError:
318         raise InvalidInput("Invalid disk amount; minimum is %s GB" %
319                           MIN_DISK_SINGLE)
320     return disk
321
322 def create(user, fields):
323     """Handler for create requests."""
324     name = fields.getfirst('name')
325     if not validMachineName(name):
326         raise InvalidInput("Invalid name '%s'" % name)
327     name = user.username + '_' + name.lower()
328
329     if Machine.get_by(name=name):
330         raise InvalidInput("A machine named '%s' already exists" % name)
331     
332     memory = fields.getfirst('memory')
333     memory = validMemory(user, memory)
334     
335     disk = fields.getfirst('disk')
336     disk = validDisk(user, disk)
337
338     vm_type = fields.getfirst('vmtype')
339     if vm_type not in ('hvm', 'paravm'):
340         raise CodeError("Invalid vm type '%s'"  % vm_type)    
341     is_hvm = (vm_type == 'hvm')
342
343     cdrom = fields.getfirst('cdrom')
344     if cdrom is not None and not CDROM.get(cdrom):
345         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
346     
347     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
348     d = dict(user=user,
349              machine=machine)
350     print Template(file='create.tmpl',
351                    searchList=[d, global_dict]);
352
353 def listVms(user, fields):
354     """Handler for list requests."""
355     machines = [m for m in Machine.select() if haveAccess(user, m)]    
356     on = {}
357     has_vnc = {}
358     uptimes = getUptimes(machines)
359     on = uptimes
360     for m in machines:
361         if not on[m]:
362             has_vnc[m] = 'Off'
363         elif m.type.hvm:
364             has_vnc[m] = True
365         else:
366             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
367     #     for m in machines:
368     #         status = statusInfo(m)
369     #         on[m.name] = status is not None
370     #         has_vnc[m.name] = hasVnc(status)
371     max_mem=maxMemory(user, on=on)
372     max_disk=maxDisk(user)
373     d = dict(user=user,
374              can_add_vm=canAddVm(user, on=on),
375              max_mem=max_mem,
376              max_disk=max_disk,
377              default_mem=max_mem,
378              default_disk=min(4.0, max_disk),
379              machines=machines,
380              has_vnc=has_vnc,
381              uptimes=uptimes,
382              cdroms=CDROM.select())
383     print Template(file='list.tmpl', searchList=[d, global_dict])
384
385 def testMachineId(user, machineId, exists=True):
386     """Parse, validate and check authorization for a given machineId.
387
388     If exists is False, don't check that it exists.
389     """
390     if machineId is None:
391         raise CodeError("No machine ID specified")
392     try:
393         machineId = int(machineId)
394     except ValueError:
395         raise CodeError("Invalid machine ID '%s'" % machineId)
396     machine = Machine.get(machineId)
397     if exists and machine is None:
398         raise CodeError("No such machine ID '%s'" % machineId)
399     if machine is not None and not haveAccess(user, machine):
400         raise CodeError("No access to machine ID '%s'" % machineId)
401     return machine
402
403 def vnc(user, fields):
404     """VNC applet page.
405
406     Note that due to same-domain restrictions, the applet connects to
407     the webserver, which needs to forward those requests to the xen
408     server.  The Xen server runs another proxy that (1) authenticates
409     and (2) finds the correct port for the VM.
410
411     You might want iptables like:
412
413     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
414     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142 
415     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
416
417     Remember to enable iptables!
418     echo 1 > /proc/sys/net/ipv4/ip_forward
419     """
420     machine = testMachineId(user, fields.getfirst('machine_id'))
421     
422     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
423
424     data = {}
425     data["user"] = user.username
426     data["machine"]=machine.name
427     data["expires"]=time.time()+(5*60)
428     pickledData = cPickle.dumps(data)
429     m = hmac.new(TOKEN_KEY, digestmod=sha)
430     m.update(pickledData)
431     token = {'data': pickledData, 'digest': m.digest()}
432     token = cPickle.dumps(token)
433     token = base64.urlsafe_b64encode(token)
434     
435     d = dict(user=user,
436              machine=machine,
437              hostname=os.environ.get('SERVER_NAME', 'localhost'),
438              authtoken=token)
439     print Template(file='vnc.tmpl',
440                    searchList=[d, global_dict])
441
442 def getNicInfo(data_dict, machine):
443     """Helper function for info, get data on nics for a machine.
444
445     Modifies data_dict to include the relevant data, and returns a list
446     of (key, name) pairs to display "name: data_dict[key]" to the user.
447     """
448     data_dict['num_nics'] = len(machine.nics)
449     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
450                            ('nic%s_mac', 'NIC %s MAC Addr'),
451                            ('nic%s_ip', 'NIC %s IP'),
452                            ]
453     nic_fields = []
454     for i in range(len(machine.nics)):
455         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
456         data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
457         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
458         data_dict['nic%s_ip' % i] = machine.nics[i].ip
459     if len(machine.nics) == 1:
460         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
461     return nic_fields
462
463 def getDiskInfo(data_dict, machine):
464     """Helper function for info, get data on disks for a machine.
465
466     Modifies data_dict to include the relevant data, and returns a list
467     of (key, name) pairs to display "name: data_dict[key]" to the user.
468     """
469     data_dict['num_disks'] = len(machine.disks)
470     disk_fields_template = [('%s_size', '%s size')]
471     disk_fields = []
472     for disk in machine.disks:
473         name = disk.guest_device_name
474         disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
475         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
476     return disk_fields
477
478 def deleteVM(machine):
479     """Delete a VM."""
480     transaction = ctx.current.create_transaction()
481     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
482     try:
483         for nic in machine.nics:
484             nic.machine_id = None
485             nic.hostname = None
486             ctx.current.save(nic)
487         for disk in machine.disks:
488             ctx.current.delete(disk)
489         ctx.current.delete(machine)
490         transaction.commit()
491     except:
492         transaction.rollback()
493         raise
494     for mname, dname in delete_disk_pairs:
495         remctl('web', 'lvremove', mname, dname)
496     unregisterMachine(machine)
497
498 def command(user, fields):
499     """Handler for running commands like boot and delete on a VM."""
500     print time.time()-start_time
501     machine = testMachineId(user, fields.getfirst('machine_id'))
502     action = fields.getfirst('action')
503     cdrom = fields.getfirst('cdrom')
504     print time.time()-start_time
505     if cdrom is not None and not CDROM.get(cdrom):
506         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
507     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
508         raise CodeError("Invalid action '%s'" % action)
509     if action == 'Reboot':
510         if cdrom is not None:
511             remctl('reboot', machine.name, cdrom)
512         else:
513             remctl('reboot', machine.name)
514     elif action == 'Power on':
515         if maxMemory(user) < machine.memory:
516             raise InvalidInput("You don't have enough free RAM quota")
517         bootMachine(machine, cdrom)
518     elif action == 'Power off':
519         remctl('destroy', machine.name)
520     elif action == 'Shutdown':
521         remctl('shutdown', machine.name)
522     elif action == 'Delete VM':
523         deleteVM(machine)
524     print time.time()-start_time
525
526     d = dict(user=user,
527              command=action,
528              machine=machine)
529     print Template(file="command.tmpl", searchList=[d, global_dict])
530         
531 def modify(user, fields):
532     """Handler for modifying attributes of a machine."""
533     #XXX not written yet
534     machine = testMachineId(user, fields.getfirst('machine_id'))
535     
536 def help(user, fields):
537     """Handler for help messages."""
538     simple = fields.getfirst('simple')
539     subjects = fields.getlist('subject')
540     
541     mapping = dict(paravm_console="""
542 ParaVM machines do not support console access over VNC.  To access
543 these machines, you either need to boot with a liveCD and ssh in or
544 hope that the sipb-xen maintainers add support for serial consoles.""",
545                    hvm_paravm="""
546 HVM machines use the virtualization features of the processor, while
547 ParaVM machines use Xen's emulation of virtualization features.  You
548 want an HVM virtualized machine.""",
549                    cpu_weight="""Don't ask us!  We're as mystified as you are.""")
550     
551     d = dict(user=user,
552              simple=simple,
553              subjects=subjects,
554              mapping=mapping)
555     
556     print Template(file="help.tmpl", searchList=[d, global_dict])
557     
558
559 def info(user, fields):
560     """Handler for info on a single VM."""
561     machine = testMachineId(user, fields.getfirst('machine_id'))
562     status = statusInfo(machine)
563     has_vnc = hasVnc(status)
564     if status is None:
565         main_status = dict(name=machine.name,
566                            memory=str(machine.memory))
567     else:
568         main_status = dict(status[1:])
569     start_time = float(main_status.get('start_time', 0))
570     uptime = datetime.timedelta(seconds=int(time.time()-start_time))
571     cpu_time_float = float(main_status.get('cpu_time', 0))
572     cputime = datetime.timedelta(seconds=int(cpu_time_float))
573     display_fields = """name uptime memory state cpu_weight on_reboot 
574      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
575     display_fields = [('name', 'Name'),
576                       ('owner', 'Owner'),
577                       ('contact', 'Contact'),
578                       ('type', 'Type'),
579                       'NIC_INFO',
580                       ('uptime', 'uptime'),
581                       ('cputime', 'CPU usage'),
582                       ('memory', 'RAM'),
583                       'DISK_INFO',
584                       ('state', 'state (xen format)'),
585                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
586                       ('on_reboot', 'Action on VM reboot'),
587                       ('on_poweroff', 'Action on VM poweroff'),
588                       ('on_crash', 'Action on VM crash'),
589                       ('on_xend_start', 'Action on Xen start'),
590                       ('on_xend_stop', 'Action on Xen stop'),
591                       ('bootloader', 'Bootloader options'),
592                       ]
593     fields = []
594     machine_info = {}
595     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
596     machine_info['owner'] = machine.owner
597     machine_info['contact'] = machine.contact
598
599     nic_fields = getNicInfo(machine_info, machine)
600     nic_point = display_fields.index('NIC_INFO')
601     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
602
603     disk_fields = getDiskInfo(machine_info, machine)
604     disk_point = display_fields.index('DISK_INFO')
605     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
606     
607     main_status['memory'] += ' MB'
608     for field, disp in display_fields:
609         if field in ('uptime', 'cputime'):
610             fields.append((disp, locals()[field]))
611         elif field in main_status:
612             fields.append((disp, main_status[field]))
613         elif field in machine_info:
614             fields.append((disp, machine_info[field]))
615         else:
616             pass
617             #fields.append((disp, None))
618     max_mem = maxMemory(user, machine)
619     max_disk = maxDisk(user, machine)
620     d = dict(user=user,
621              cdroms=CDROM.select(),
622              on=status is not None,
623              machine=machine,
624              has_vnc=has_vnc,
625              uptime=str(uptime),
626              ram=machine.memory,
627              max_mem=max_mem,
628              max_disk=max_disk,
629              fields = fields)
630     print Template(file='info.tmpl',
631                    searchList=[d, global_dict])
632
633 mapping = dict(list=listVms,
634                vnc=vnc,
635                command=command,
636                modify=modify,
637                info=info,
638                create=create,
639                help=help)
640
641 if __name__ == '__main__':
642     start_time = time.time()
643     fields = cgi.FieldStorage()
644     class User:
645         username = "moo"
646         email = 'moo@cow.com'
647     u = User()
648     if 'SSL_CLIENT_S_DN_Email' in os.environ:
649         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
650         u.username = username
651         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
652     else:
653         u.username = 'moo'
654         u.email = 'nobody'
655     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
656     operation = os.environ.get('PATH_INFO', '')
657     #print 'Content-Type: text/plain\n'
658     #print operation
659     if not operation:
660         print "Status: 301 Moved Permanently"
661         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
662         sys.exit(0)
663     print 'Content-Type: text/html\n'
664
665     if operation.startswith('/'):
666         operation = operation[1:]
667     if not operation:
668         operation = 'list'
669     
670     fun = mapping.get(operation, 
671                       lambda u, e:
672                           error(operation, u, e,
673                                 "Invalid operation '%s'" % operation))
674     if fun not in (help, ):
675         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
676     try:
677         fun(u, fields)
678     except CodeError, err:
679         error(operation, u, fields, err)
680     except InvalidInput, err:
681         error(operation, u, fields, err)