minor bugfix, and check not reusing names.
[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     if Machine.select_by(name=name) == []:
587         return name
588     raise InvalidInput('name', name,
589                        "Already taken")
590
591 def testHostname(user, hostname, machine):
592     for nic in machine.nics:
593         if hostname == nic.hostname:
594             return hostname
595     # check if doesn't already exist
596     if NIC.select_by(hostname=hostname) == []:
597         return hostname
598     raise InvalidInput('hostname', hostname,
599                        "Different from before")
600
601
602 def modify(user, fields):
603     """Handler for modifying attributes of a machine."""
604     #XXX not written yet
605
606     transaction = ctx.current.create_transaction()
607     try:
608         machine = testMachineId(user, fields.getfirst('machine_id'))
609         owner = testOwner(user, fields.getfirst('owner'), machine)
610         contact = testContact(user, fields.getfirst('contact'))
611         hostname = testHostname(owner, fields.getfirst('hostname'),
612                             machine)
613         name = testName(user, fields.getfirst('name'))
614         oldname = machine.name
615         olddisk = {}
616
617         memory = fields.getfirst('memory')
618         if memory is not None:
619             memory = validMemory(user, memory, machine)
620         if memory != machine.memory:
621             machine.memory = memory
622
623         disksize = testDisk(user, fields.getfirst('disk'))
624         if disksize is not None:
625             disksize = validDisk(user, disksize, machine)
626         
627         for disk in machine.disks:
628             disk.size = disksize
629             olddisk[disk.guest_device_name] = disk.size
630             ctx.current.save(disk)
631         
632         # XXX all NICs get same hostname on change?  Interface doesn't support more.
633         for nic in machine.nics:
634             nic.hostname = hostname
635             ctx.current.save(nic)
636
637         if owner != machine.owner:
638             machine.owner = owner
639         if name != machine.name:
640             machine.name = name
641             
642         ctx.current.save(machine)
643         transaction.commit()
644     except:
645         transaction.rollback()
646         raise
647     remctl("web", "moveregister", oldname, name)
648     for disk in machine.disks:
649         # XXX all disks get the same size on change?  Interface doesn't support more.
650         if disk.size != olddisk[disk.guest_device_name]:
651             remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size))
652         if oldname != name:
653             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
654     d = dict(user=user,
655              command="modify",
656              machine=machine)
657     return Template(file="command.tmpl", searchList=[d, global_dict])    
658
659
660 def help(user, fields):
661     """Handler for help messages."""
662     simple = fields.getfirst('simple')
663     subjects = fields.getlist('subject')
664     
665     mapping = dict(paravm_console="""
666 ParaVM machines do not support console access over VNC.  To access
667 these machines, you either need to boot with a liveCD and ssh in or
668 hope that the sipb-xen maintainers add support for serial consoles.""",
669                    hvm_paravm="""
670 HVM machines use the virtualization features of the processor, while
671 ParaVM machines use Xen's emulation of virtualization features.  You
672 want an HVM virtualized machine.""",
673                    cpu_weight="""Don't ask us!  We're as mystified as you are.""")
674     
675     d = dict(user=user,
676              simple=simple,
677              subjects=subjects,
678              mapping=mapping)
679     
680     return Template(file="help.tmpl", searchList=[d, global_dict])
681     
682
683 def info(user, fields):
684     """Handler for info on a single VM."""
685     machine = testMachineId(user, fields.getfirst('machine_id'))
686     status = statusInfo(machine)
687     has_vnc = hasVnc(status)
688     if status is None:
689         main_status = dict(name=machine.name,
690                            memory=str(machine.memory))
691     else:
692         main_status = dict(status[1:])
693     start_time = float(main_status.get('start_time', 0))
694     uptime = datetime.timedelta(seconds=int(time.time()-start_time))
695     cpu_time_float = float(main_status.get('cpu_time', 0))
696     cputime = datetime.timedelta(seconds=int(cpu_time_float))
697     display_fields = """name uptime memory state cpu_weight on_reboot 
698      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
699     display_fields = [('name', 'Name'),
700                       ('owner', 'Owner'),
701                       ('contact', 'Contact'),
702                       ('type', 'Type'),
703                       'NIC_INFO',
704                       ('uptime', 'uptime'),
705                       ('cputime', 'CPU usage'),
706                       ('memory', 'RAM'),
707                       'DISK_INFO',
708                       ('state', 'state (xen format)'),
709                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
710                       ('on_reboot', 'Action on VM reboot'),
711                       ('on_poweroff', 'Action on VM poweroff'),
712                       ('on_crash', 'Action on VM crash'),
713                       ('on_xend_start', 'Action on Xen start'),
714                       ('on_xend_stop', 'Action on Xen stop'),
715                       ('bootloader', 'Bootloader options'),
716                       ]
717     fields = []
718     machine_info = {}
719     machine_info['name'] = machine.name
720     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
721     machine_info['owner'] = machine.owner
722     machine_info['contact'] = machine.contact
723
724     nic_fields = getNicInfo(machine_info, machine)
725     nic_point = display_fields.index('NIC_INFO')
726     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
727
728     disk_fields = getDiskInfo(machine_info, machine)
729     disk_point = display_fields.index('DISK_INFO')
730     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
731     
732     main_status['memory'] += ' MB'
733     for field, disp in display_fields:
734         if field in ('uptime', 'cputime'):
735             fields.append((disp, locals()[field]))
736         elif field in machine_info:
737             fields.append((disp, machine_info[field]))
738         elif field in main_status:
739             fields.append((disp, main_status[field]))
740         else:
741             pass
742             #fields.append((disp, None))
743     max_mem = maxMemory(user, machine)
744     max_disk = maxDisk(user, machine)
745     d = dict(user=user,
746              cdroms=CDROM.select(),
747              on=status is not None,
748              machine=machine,
749              has_vnc=has_vnc,
750              uptime=str(uptime),
751              ram=machine.memory,
752              max_mem=max_mem,
753              max_disk=max_disk,
754              fields = fields)
755     return Template(file='info.tmpl',
756                    searchList=[d, global_dict])
757
758 mapping = dict(list=listVms,
759                vnc=vnc,
760                command=command,
761                modify=modify,
762                info=info,
763                create=create,
764                help=help)
765
766 if __name__ == '__main__':
767     start_time = time.time()
768     fields = cgi.FieldStorage()
769     class User:
770         username = "moo"
771         email = 'moo@cow.com'
772     u = User()
773     g = Global(u)
774     if 'SSL_CLIENT_S_DN_Email' in os.environ:
775         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
776         u.username = username
777         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
778     else:
779         u.username = 'moo'
780         u.email = 'nobody'
781     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
782     operation = os.environ.get('PATH_INFO', '')
783 #    print 'Content-Type: text/plain\n'
784 #    print operation
785     if not operation:
786         print "Status: 301 Moved Permanently"
787         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
788         sys.exit(0)
789
790     if operation.startswith('/'):
791         operation = operation[1:]
792     if not operation:
793         operation = 'list'
794
795     def badOperation(u, e):
796         raise CodeError("Unknown operation")
797
798     fun = mapping.get(operation, badOperation)
799     if fun not in (help, ):
800         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
801     try:
802         output = fun(u, fields)
803         print 'Content-Type: text/html\n'
804         sys.stderr.seek(0)
805         e = sys.stderr.read()
806         if e:
807             output = str(output)
808             output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
809         print output
810     except CodeError, err:
811         print 'Content-Type: text/html\n'
812         sys.stderr.seek(0)
813         e = sys.stderr.read()
814         sys.stderr=sys.stdout
815         print error(operation, u, fields, err, e)
816     except InvalidInput, err:
817         print 'Content-Type: text/html\n'
818         sys.stderr.seek(0)
819         e = sys.stderr.read()
820         sys.stderr=sys.stdout
821         print invalidInput(operation, u, fields, err, e)
822     except:
823         print 'Content-Type: text/plain\n'
824         sys.stderr.seek(0)
825         e = sys.stderr.read()
826         print e
827         print '----'
828         sys.stderr = sys.stdout
829         raise