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