74a61169c6c5f9ef61d165cbc8b6e7db0c292ff9
[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.contact = user.email
299         machine.uuid = uuidToString(randomUUID())
300         machine.boot_off_cd = True
301         machine_type = Type.get_by(hvm=is_hvm)
302         machine.type_id = machine_type.type_id
303         ctx.current.save(machine)
304         disk = Disk(machine.machine_id, 
305                     'hda', disk)
306         open = NIC.select_by(machine_id=None)
307         if not open: #No IPs left!
308             raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
309         nic = open[0]
310         nic.machine_id = machine.machine_id
311         nic.hostname = name
312         ctx.current.save(nic)    
313         ctx.current.save(disk)
314         transaction.commit()
315     except:
316         transaction.rollback()
317         raise
318     registerMachine(machine)
319     makeDisks(machine)
320     # tell it to boot with cdrom
321     bootMachine(machine, cdrom)
322
323     return machine
324
325 def validMemory(user, memory, machine=None, on=True):
326     """Parse and validate limits for memory for a given user and machine.
327
328     on is whether the memory must be valid after the machine is
329     switched on.
330     """
331     try:
332         memory = int(memory)
333         if memory < MIN_MEMORY_SINGLE:
334             raise ValueError
335     except ValueError:
336         raise InvalidInput('memory', memory, 
337                            "Minimum %s MB" % MIN_MEMORY_SINGLE)
338     if memory > maxMemory(user, machine, on):
339         raise InvalidInput('memory', memory,
340                            'Maximum %s MB' % maxMemory(user, machine))
341     return memory
342
343 def validDisk(user, disk, machine=None):
344     """Parse and validate limits for disk for a given user and machine."""
345     try:
346         disk = float(disk)
347         if disk > maxDisk(user, machine):
348             raise InvalidInput('disk', disk,
349                                "Maximum %s G" % maxDisk(user, machine))
350         disk = int(disk * 1024)
351         if disk < MIN_DISK_SINGLE * 1024:
352             raise ValueError
353     except ValueError:
354         raise InvalidInput('disk', disk,
355                            "Minimum %s GB" % MIN_DISK_SINGLE)
356     return disk
357
358 def create(user, fields):
359     """Handler for create requests."""
360     name = fields.getfirst('name')
361     if not validMachineName(name):
362         raise InvalidInput('name', name)
363     name = name.lower()
364
365     if Machine.get_by(name=name):
366         raise InvalidInput('name', name,
367                            "Already exists")
368     
369     memory = fields.getfirst('memory')
370     memory = validMemory(user, memory, on=True)
371     
372     disk = fields.getfirst('disk')
373     disk = validDisk(user, disk)
374
375     vm_type = fields.getfirst('vmtype')
376     if vm_type not in ('hvm', 'paravm'):
377         raise CodeError("Invalid vm type '%s'"  % vm_type)    
378     is_hvm = (vm_type == 'hvm')
379
380     cdrom = fields.getfirst('cdrom')
381     if cdrom is not None and not CDROM.get(cdrom):
382         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
383     
384     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
385     d = dict(user=user,
386              machine=machine)
387     return Template(file='create.tmpl',
388                    searchList=[d, global_dict]);
389
390 def listVms(user, fields):
391     """Handler for list requests."""
392     machines = [m for m in Machine.select() if haveAccess(user, m)]    
393     on = {}
394     has_vnc = {}
395     on = g.uptimes
396     for m in machines:
397         if not on[m]:
398             has_vnc[m] = 'Off'
399         elif m.type.hvm:
400             has_vnc[m] = True
401         else:
402             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
403     #     for m in machines:
404     #         status = statusInfo(m)
405     #         on[m.name] = status is not None
406     #         has_vnc[m.name] = hasVnc(status)
407     max_mem=maxMemory(user)
408     max_disk=maxDisk(user)
409     d = dict(user=user,
410              can_add_vm=canAddVm(user),
411              max_mem=max_mem,
412              max_disk=max_disk,
413              default_mem=max_mem,
414              default_disk=min(4.0, max_disk),
415              machines=machines,
416              has_vnc=has_vnc,
417              uptimes=g.uptimes,
418              cdroms=CDROM.select())
419     return Template(file='list.tmpl', searchList=[d, global_dict])
420
421 def testMachineId(user, machineId, exists=True):
422     """Parse, validate and check authorization for a given machineId.
423
424     If exists is False, don't check that it exists.
425     """
426     if machineId is None:
427         raise CodeError("No machine ID specified")
428     try:
429         machineId = int(machineId)
430     except ValueError:
431         raise CodeError("Invalid machine ID '%s'" % machineId)
432     machine = Machine.get(machineId)
433     if exists and machine is None:
434         raise CodeError("No such machine ID '%s'" % machineId)
435     if machine is not None and not haveAccess(user, machine):
436         raise CodeError("No access to machine ID '%s'" % machineId)
437     return machine
438
439 def vnc(user, fields):
440     """VNC applet page.
441
442     Note that due to same-domain restrictions, the applet connects to
443     the webserver, which needs to forward those requests to the xen
444     server.  The Xen server runs another proxy that (1) authenticates
445     and (2) finds the correct port for the VM.
446
447     You might want iptables like:
448
449     -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 
450     -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 
451     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
452
453     Remember to enable iptables!
454     echo 1 > /proc/sys/net/ipv4/ip_forward
455     """
456     machine = testMachineId(user, fields.getfirst('machine_id'))
457     
458     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
459
460     data = {}
461     data["user"] = user.username
462     data["machine"]=machine.name
463     data["expires"]=time.time()+(5*60)
464     pickledData = cPickle.dumps(data)
465     m = hmac.new(TOKEN_KEY, digestmod=sha)
466     m.update(pickledData)
467     token = {'data': pickledData, 'digest': m.digest()}
468     token = cPickle.dumps(token)
469     token = base64.urlsafe_b64encode(token)
470     
471     status = statusInfo(machine)
472     has_vnc = hasVnc(status)
473     
474     d = dict(user=user,
475              on=status,
476              has_vnc=has_vnc,
477              machine=machine,
478              hostname=os.environ.get('SERVER_NAME', 'localhost'),
479              authtoken=token)
480     return Template(file='vnc.tmpl',
481                    searchList=[d, global_dict])
482
483 def getNicInfo(data_dict, machine):
484     """Helper function for info, get data on nics for a machine.
485
486     Modifies data_dict to include the relevant data, and returns a list
487     of (key, name) pairs to display "name: data_dict[key]" to the user.
488     """
489     data_dict['num_nics'] = len(machine.nics)
490     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
491                            ('nic%s_mac', 'NIC %s MAC Addr'),
492                            ('nic%s_ip', 'NIC %s IP'),
493                            ]
494     nic_fields = []
495     for i in range(len(machine.nics)):
496         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
497         data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
498         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
499         data_dict['nic%s_ip' % i] = machine.nics[i].ip
500     if len(machine.nics) == 1:
501         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
502     return nic_fields
503
504 def getDiskInfo(data_dict, machine):
505     """Helper function for info, get data on disks for a machine.
506
507     Modifies data_dict to include the relevant data, and returns a list
508     of (key, name) pairs to display "name: data_dict[key]" to the user.
509     """
510     data_dict['num_disks'] = len(machine.disks)
511     disk_fields_template = [('%s_size', '%s size')]
512     disk_fields = []
513     for disk in machine.disks:
514         name = disk.guest_device_name
515         disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
516         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
517     return disk_fields
518
519 def deleteVM(machine):
520     """Delete a VM."""
521     remctl('destroy', machine.name, err=True)
522     transaction = ctx.current.create_transaction()
523     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
524     try:
525         for nic in machine.nics:
526             nic.machine_id = None
527             nic.hostname = None
528             ctx.current.save(nic)
529         for disk in machine.disks:
530             ctx.current.delete(disk)
531         ctx.current.delete(machine)
532         transaction.commit()
533     except:
534         transaction.rollback()
535         raise
536     for mname, dname in delete_disk_pairs:
537         remctl('web', 'lvremove', mname, dname)
538     unregisterMachine(machine)
539
540 def command(user, fields):
541     """Handler for running commands like boot and delete on a VM."""
542     print >> sys.stderr, time.time()-start_time
543     machine = testMachineId(user, fields.getfirst('machine_id'))
544     action = fields.getfirst('action')
545     cdrom = fields.getfirst('cdrom')
546     print >> sys.stderr, time.time()-start_time
547     if cdrom is not None and not CDROM.get(cdrom):
548         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
549     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
550         raise CodeError("Invalid action '%s'" % action)
551     if action == 'Reboot':
552         if cdrom is not None:
553             remctl('reboot', machine.name, cdrom)
554         else:
555             remctl('reboot', machine.name)
556     elif action == 'Power on':
557         if maxMemory(user) < machine.memory:
558             raise InvalidInput('action', 'Power on',
559                                "You don't have enough free RAM quota to turn on this machine")
560         bootMachine(machine, cdrom)
561     elif action == 'Power off':
562         remctl('destroy', machine.name)
563     elif action == 'Shutdown':
564         remctl('shutdown', machine.name)
565     elif action == 'Delete VM':
566         deleteVM(machine)
567     print >> sys.stderr, time.time()-start_time
568
569     d = dict(user=user,
570              command=action,
571              machine=machine)
572     return Template(file="command.tmpl", searchList=[d, global_dict])
573
574 def testOwner(user, owner, machine=None):
575     if owner == machine.owner:   #XXX What do we do when you lose access to the locker?
576         return owner
577     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
578     if value == True:
579         return owner
580     raise InvalidInput('owner', owner, value)
581
582 def testContact(user, contact, machine=None):
583     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
584         raise InvalidInput('contact', contact, "Not a valid email")
585     return contact
586
587 def testDisk(user, disksize, machine=None):
588     return disksize
589
590 def testName(user, name, machine=None):
591     if name is None:
592         return None
593     if not Machine.select_by(name=name):
594         return name
595     if name == machine.name:
596         return name
597     raise InvalidInput('name', name, "Already taken")
598
599 def testHostname(user, hostname, machine):
600     for nic in machine.nics:
601         if hostname == nic.hostname:
602             return hostname
603     # check if doesn't already exist
604     if NIC.select_by(hostname=hostname):
605         raise InvalidInput('hostname', hostname,
606                            "Already exists")
607     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
608         raise InvalidInput('hostname', hostname, "Not a valid hostname; must only use number, letters, and dashes.")
609     return hostname
610
611 def modify(user, fields):
612     """Handler for modifying attributes of a machine."""
613
614     olddisk = {}
615     transaction = ctx.current.create_transaction()
616     try:
617         machine = testMachineId(user, fields.getfirst('machine_id'))
618         owner = testOwner(user, fields.getfirst('owner'), machine)
619         contact = testContact(user, fields.getfirst('contact'))
620         hostname = testHostname(owner, fields.getfirst('hostname'),
621                                 machine)
622         name = testName(user, fields.getfirst('name'), machine)
623         oldname = machine.name
624         command="modify"
625
626         memory = fields.getfirst('memory')
627         if memory is not None:
628             memory = validMemory(user, memory, machine, on=False)
629             machine.memory = memory
630  
631         disksize = testDisk(user, fields.getfirst('disk'))
632         if disksize is not None:
633             disksize = validDisk(user, disksize, machine)
634             disk = machine.disks[0]
635             if disk.size != disksize:
636                 olddisk[disk.guest_device_name] = disksize
637                 disk.size = disksize
638                 ctx.current.save(disk)
639         
640         # XXX first NIC gets hostname on change?  Interface doesn't support more.
641         for nic in machine.nics[:1]:
642             nic.hostname = hostname
643             ctx.current.save(nic)
644
645         if owner is not None and owner != machine.owner:
646             machine.owner = owner
647         if name is not None and name != machine.name:
648             machine.name = name
649             
650         ctx.current.save(machine)
651         transaction.commit()
652     except:
653         transaction.rollback()
654         raise
655     for diskname in olddisk:
656         remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
657     if name is not None and name != oldname:
658         for disk in machine.disks:
659             if oldname != name:
660                 remctl("web", "lvrename", oldname, disk.guest_device_name, name)
661         remctl("web", "moveregister", oldname, name)
662     d = dict(user=user,
663              command=command,
664              machine=machine)
665     return Template(file="command.tmpl", searchList=[d, global_dict])    
666
667
668 def help(user, fields):
669     """Handler for help messages."""
670     simple = fields.getfirst('simple')
671     subjects = fields.getlist('subject')
672     
673     mapping = dict(paravm_console="""
674 ParaVM machines do not support console access over VNC.  To access
675 these machines, you either need to boot with a liveCD and ssh in or
676 hope that the sipb-xen maintainers add support for serial consoles.""",
677                    hvm_paravm="""
678 HVM machines use the virtualization features of the processor, while
679 ParaVM machines use Xen's emulation of virtualization features.  You
680 want an HVM virtualized machine.""",
681                    cpu_weight="""Don't ask us!  We're as mystified as you are.""",
682                    owner="""The Owner must be the name of a locker that you are an AFS
683 administrator of.  In particular, you or an AFS group you are a member
684 of must have AFS rlidwka bits on the locker.  You can check see who
685 administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
686 Athena.)""")
687     
688     d = dict(user=user,
689              simple=simple,
690              subjects=subjects,
691              mapping=mapping)
692     
693     return Template(file="help.tmpl", searchList=[d, global_dict])
694     
695
696 def info(user, fields):
697     """Handler for info on a single VM."""
698     machine = testMachineId(user, fields.getfirst('machine_id'))
699     status = statusInfo(machine)
700     has_vnc = hasVnc(status)
701     if status is None:
702         main_status = dict(name=machine.name,
703                            memory=str(machine.memory))
704         uptime=None
705         cputime=None
706     else:
707         main_status = dict(status[1:])
708         start_time = float(main_status.get('start_time', 0))
709         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
710         cpu_time_float = float(main_status.get('cpu_time', 0))
711         cputime = datetime.timedelta(seconds=int(cpu_time_float))
712     display_fields = """name uptime memory state cpu_weight on_reboot 
713      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
714     display_fields = [('name', 'Name'),
715                       ('owner', 'Owner'),
716                       ('contact', 'Contact'),
717                       ('type', 'Type'),
718                       'NIC_INFO',
719                       ('uptime', 'uptime'),
720                       ('cputime', 'CPU usage'),
721                       ('memory', 'RAM'),
722                       'DISK_INFO',
723                       ('state', 'state (xen format)'),
724                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
725                       ('on_reboot', 'Action on VM reboot'),
726                       ('on_poweroff', 'Action on VM poweroff'),
727                       ('on_crash', 'Action on VM crash'),
728                       ('on_xend_start', 'Action on Xen start'),
729                       ('on_xend_stop', 'Action on Xen stop'),
730                       ('bootloader', 'Bootloader options'),
731                       ]
732     fields = []
733     machine_info = {}
734     machine_info['name'] = machine.name
735     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
736     machine_info['owner'] = machine.owner
737     machine_info['contact'] = machine.contact
738
739     nic_fields = getNicInfo(machine_info, machine)
740     nic_point = display_fields.index('NIC_INFO')
741     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
742
743     disk_fields = getDiskInfo(machine_info, machine)
744     disk_point = display_fields.index('DISK_INFO')
745     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
746     
747     main_status['memory'] += ' MB'
748     for field, disp in display_fields:
749         if field in ('uptime', 'cputime') and locals()[field] is not None:
750             fields.append((disp, locals()[field]))
751         elif field in machine_info:
752             fields.append((disp, machine_info[field]))
753         elif field in main_status:
754             fields.append((disp, main_status[field]))
755         else:
756             pass
757             #fields.append((disp, None))
758     max_mem = maxMemory(user, machine)
759     max_disk = maxDisk(user, machine)
760     d = dict(user=user,
761              cdroms=CDROM.select(),
762              on=status is not None,
763              machine=machine,
764              has_vnc=has_vnc,
765              uptime=str(uptime),
766              ram=machine.memory,
767              max_mem=max_mem,
768              max_disk=max_disk,
769              owner_help=helppopup("owner"),
770              fields = fields)
771     return Template(file='info.tmpl',
772                    searchList=[d, global_dict])
773
774 mapping = dict(list=listVms,
775                vnc=vnc,
776                command=command,
777                modify=modify,
778                info=info,
779                create=create,
780                help=help)
781
782 if __name__ == '__main__':
783     start_time = time.time()
784     fields = cgi.FieldStorage()
785     class User:
786         username = "moo"
787         email = 'moo@cow.com'
788     u = User()
789     g = Global(u)
790     if 'SSL_CLIENT_S_DN_Email' in os.environ:
791         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
792         u.username = username
793         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
794     else:
795         u.username = 'moo'
796         u.email = 'nobody'
797     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
798     operation = os.environ.get('PATH_INFO', '')
799 #    print 'Content-Type: text/plain\n'
800 #    print operation
801     if not operation:
802         print "Status: 301 Moved Permanently"
803         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
804         sys.exit(0)
805
806     if operation.startswith('/'):
807         operation = operation[1:]
808     if not operation:
809         operation = 'list'
810
811     def badOperation(u, e):
812         raise CodeError("Unknown operation")
813
814     fun = mapping.get(operation, badOperation)
815     if fun not in (help, ):
816         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
817     try:
818         output = fun(u, fields)
819         print 'Content-Type: text/html\n'
820         sys.stderr.seek(0)
821         e = sys.stderr.read()
822         sys.stderr=sys.stdout
823         if e:
824             output = str(output)
825             output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
826         print output
827     except CodeError, 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 error(operation, u, fields, err, e)
833     except InvalidInput, err:
834         print 'Content-Type: text/html\n'
835         sys.stderr.seek(0)
836         e = sys.stderr.read()
837         sys.stderr=sys.stdout
838         print invalidInput(operation, u, fields, err, e)
839     except:
840         print 'Content-Type: text/plain\n'
841         sys.stderr.seek(0)
842         e = sys.stderr.read()
843         print e
844         print '----'
845         sys.stderr = sys.stdout
846         raise