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