f64fe3dbefebf4fb44a7c3b46beefa906096eef4
[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         MyException.__init__(self, 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     remctl('destroy', machine.name, err=True)
515     transaction = ctx.current.create_transaction()
516     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
517     try:
518         for nic in machine.nics:
519             nic.machine_id = None
520             nic.hostname = None
521             ctx.current.save(nic)
522         for disk in machine.disks:
523             ctx.current.delete(disk)
524         ctx.current.delete(machine)
525         transaction.commit()
526     except:
527         transaction.rollback()
528         raise
529     for mname, dname in delete_disk_pairs:
530         remctl('web', 'lvremove', mname, dname)
531     unregisterMachine(machine)
532
533 def command(user, fields):
534     """Handler for running commands like boot and delete on a VM."""
535     print >> sys.stderr, time.time()-start_time
536     machine = testMachineId(user, fields.getfirst('machine_id'))
537     action = fields.getfirst('action')
538     cdrom = fields.getfirst('cdrom')
539     print >> sys.stderr, time.time()-start_time
540     if cdrom is not None and not CDROM.get(cdrom):
541         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
542     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
543         raise CodeError("Invalid action '%s'" % action)
544     if action == 'Reboot':
545         if cdrom is not None:
546             remctl('reboot', machine.name, cdrom)
547         else:
548             remctl('reboot', machine.name)
549     elif action == 'Power on':
550         if maxMemory(user) < machine.memory:
551             raise InvalidInput('action', 'Power on',
552                                "You don't have enough free RAM quota to turn on this machine")
553         bootMachine(machine, cdrom)
554     elif action == 'Power off':
555         remctl('destroy', machine.name)
556     elif action == 'Shutdown':
557         remctl('shutdown', machine.name)
558     elif action == 'Delete VM':
559         deleteVM(machine)
560     print >> sys.stderr, time.time()-start_time
561
562     d = dict(user=user,
563              command=action,
564              machine=machine)
565     return Template(file="command.tmpl", searchList=[d, global_dict])
566
567 def testOwner(user, owner, machine=None):
568     if not getafsgroups.checkLockerOwner(user.username, owner):
569         raise InvalidInput('owner', owner,
570                            "Invalid")
571     return owner
572
573 def testContact(user, contact, machine=None):
574     if contact != user.email:
575         raise InvalidInput('contact', contact,
576                            "Invalid")
577     return contact
578
579 def testDisk(user, disksize, machine=None):
580     return disksize
581
582 def testName(user, name, machine=None):
583     if Machine.select_by(name=name) == []:
584         return name
585     if name == machine.name:
586         return name
587     raise InvalidInput('name', name,
588                        "Already taken")
589
590 def testHostname(user, hostname, machine):
591     for nic in machine.nics:
592         if hostname == nic.hostname:
593             return hostname
594     # check if doesn't already exist
595     if NIC.select_by(hostname=hostname) == []:
596         return hostname
597     raise InvalidInput('hostname', hostname,
598                        "Different from before")
599
600
601 def modify(user, fields):
602     """Handler for modifying attributes of a machine."""
603     #XXX not written yet
604
605     transaction = ctx.current.create_transaction()
606     try:
607         machine = testMachineId(user, fields.getfirst('machine_id'))
608         owner = testOwner(user, fields.getfirst('owner'), machine)
609         contact = testContact(user, fields.getfirst('contact'))
610         hostname = testHostname(owner, fields.getfirst('hostname'),
611                             machine)
612         name = testName(user, fields.getfirst('name'), machine)
613         oldname = machine.name
614         command="modify"
615         olddisk = {}
616
617         memory = fields.getfirst('memory')
618         if memory is not None:
619             memory = validMemory(user, memory, machine)
620         else:
621             memory = machine.memory
622         if memory != machine.memory:
623             machine.memory = memory
624
625         disksize = testDisk(user, fields.getfirst('disk'))
626         if disksize is not None:
627             disksize = validDisk(user, disksize, machine)
628         else:
629             disksize = machine.disks[0].size
630         for disk in machine.disks:
631             olddisk[disk.guest_device_name] = disk.size
632             disk.size = disksize
633             ctx.current.save(disk)
634         
635         # XXX all NICs get same hostname on change?  Interface doesn't support more.
636         for nic in machine.nics:
637             nic.hostname = hostname
638             ctx.current.save(nic)
639
640         if owner != machine.owner:
641             machine.owner = owner
642         if name != machine.name:
643             machine.name = name
644             
645         ctx.current.save(machine)
646         transaction.commit()
647     except:
648         transaction.rollback()
649         raise
650     remctl("web", "moveregister", oldname, name)
651     for disk in machine.disks:
652         # XXX all disks get the same size on change?  Interface doesn't support more.
653         if disk.size != olddisk[disk.guest_device_name]:
654             remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size))
655         if oldname != name:
656             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
657     d = dict(user=user,
658              command=command,
659              machine=machine)
660     return Template(file="command.tmpl", searchList=[d, global_dict])    
661
662
663 def help(user, fields):
664     """Handler for help messages."""
665     simple = fields.getfirst('simple')
666     subjects = fields.getlist('subject')
667     
668     mapping = dict(paravm_console="""
669 ParaVM machines do not support console access over VNC.  To access
670 these machines, you either need to boot with a liveCD and ssh in or
671 hope that the sipb-xen maintainers add support for serial consoles.""",
672                    hvm_paravm="""
673 HVM machines use the virtualization features of the processor, while
674 ParaVM machines use Xen's emulation of virtualization features.  You
675 want an HVM virtualized machine.""",
676                    cpu_weight="""Don't ask us!  We're as mystified as you are.""",
677                    owner="""The Owner must be the name of a locker that you are an AFS
678 administrator of.  In particular, you or an AFS group you are a member
679 of must have AFS rlidwka bits on the locker.  You can check see who
680 administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
681 Athena.)""")
682     
683     d = dict(user=user,
684              simple=simple,
685              subjects=subjects,
686              mapping=mapping)
687     
688     return Template(file="help.tmpl", searchList=[d, global_dict])
689     
690
691 def info(user, fields):
692     """Handler for info on a single VM."""
693     machine = testMachineId(user, fields.getfirst('machine_id'))
694     status = statusInfo(machine)
695     has_vnc = hasVnc(status)
696     if status is None:
697         main_status = dict(name=machine.name,
698                            memory=str(machine.memory))
699         uptime=None
700         cputime=None
701     else:
702         main_status = dict(status[1:])
703         start_time = float(main_status.get('start_time', 0))
704         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
705         cpu_time_float = float(main_status.get('cpu_time', 0))
706         cputime = datetime.timedelta(seconds=int(cpu_time_float))
707     display_fields = """name uptime memory state cpu_weight on_reboot 
708      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
709     display_fields = [('name', 'Name'),
710                       ('owner', 'Owner'),
711                       ('contact', 'Contact'),
712                       ('type', 'Type'),
713                       'NIC_INFO',
714                       ('uptime', 'uptime'),
715                       ('cputime', 'CPU usage'),
716                       ('memory', 'RAM'),
717                       'DISK_INFO',
718                       ('state', 'state (xen format)'),
719                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
720                       ('on_reboot', 'Action on VM reboot'),
721                       ('on_poweroff', 'Action on VM poweroff'),
722                       ('on_crash', 'Action on VM crash'),
723                       ('on_xend_start', 'Action on Xen start'),
724                       ('on_xend_stop', 'Action on Xen stop'),
725                       ('bootloader', 'Bootloader options'),
726                       ]
727     fields = []
728     machine_info = {}
729     machine_info['name'] = machine.name
730     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
731     machine_info['owner'] = machine.owner
732     machine_info['contact'] = machine.contact
733
734     nic_fields = getNicInfo(machine_info, machine)
735     nic_point = display_fields.index('NIC_INFO')
736     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
737
738     disk_fields = getDiskInfo(machine_info, machine)
739     disk_point = display_fields.index('DISK_INFO')
740     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
741     
742     main_status['memory'] += ' MB'
743     for field, disp in display_fields:
744         if field in ('uptime', 'cputime') and locals()[field] is not None:
745             fields.append((disp, locals()[field]))
746         elif field in machine_info:
747             fields.append((disp, machine_info[field]))
748         elif field in main_status:
749             fields.append((disp, main_status[field]))
750         else:
751             pass
752             #fields.append((disp, None))
753     max_mem = maxMemory(user, machine)
754     max_disk = maxDisk(user, machine)
755     d = dict(user=user,
756              cdroms=CDROM.select(),
757              on=status is not None,
758              machine=machine,
759              has_vnc=has_vnc,
760              uptime=str(uptime),
761              ram=machine.memory,
762              max_mem=max_mem,
763              max_disk=max_disk,
764              owner_help=helppopup("owner"),
765              fields = fields)
766     return Template(file='info.tmpl',
767                    searchList=[d, global_dict])
768
769 mapping = dict(list=listVms,
770                vnc=vnc,
771                command=command,
772                modify=modify,
773                info=info,
774                create=create,
775                help=help)
776
777 if __name__ == '__main__':
778     start_time = time.time()
779     fields = cgi.FieldStorage()
780     class User:
781         username = "moo"
782         email = 'moo@cow.com'
783     u = User()
784     g = Global(u)
785     if 'SSL_CLIENT_S_DN_Email' in os.environ:
786         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
787         u.username = username
788         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
789     else:
790         u.username = 'moo'
791         u.email = 'nobody'
792     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
793     operation = os.environ.get('PATH_INFO', '')
794 #    print 'Content-Type: text/plain\n'
795 #    print operation
796     if not operation:
797         print "Status: 301 Moved Permanently"
798         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
799         sys.exit(0)
800
801     if operation.startswith('/'):
802         operation = operation[1:]
803     if not operation:
804         operation = 'list'
805
806     def badOperation(u, e):
807         raise CodeError("Unknown operation")
808
809     fun = mapping.get(operation, badOperation)
810     if fun not in (help, ):
811         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
812     try:
813         output = fun(u, fields)
814         print 'Content-Type: text/html\n'
815         sys.stderr.seek(0)
816         e = sys.stderr.read()
817         if e:
818             output = str(output)
819             output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
820         print output
821     except CodeError, err:
822         print 'Content-Type: text/html\n'
823         sys.stderr.seek(0)
824         e = sys.stderr.read()
825         sys.stderr=sys.stdout
826         print error(operation, u, fields, err, e)
827     except InvalidInput, err:
828         print 'Content-Type: text/html\n'
829         sys.stderr.seek(0)
830         e = sys.stderr.read()
831         sys.stderr=sys.stdout
832         print invalidInput(operation, u, fields, err, e)
833     except:
834         print 'Content-Type: text/plain\n'
835         sys.stderr.seek(0)
836         e = sys.stderr.read()
837         print e
838         print '----'
839         sys.stderr = sys.stdout
840         raise