Removed the Javascript, since I realized that
[invirt/packages/invirt-web.git] / templates / main.py
1 #!/usr/bin/python
2 """Main CGI script for web interface"""
3
4 import base64
5 import cPickle
6 import cgi
7 import datetime
8 import getafsgroups
9 import hmac
10 import os
11 import random
12 import re
13 import sha
14 import simplejson
15 import string
16 import subprocess
17 import sys
18 import time
19 from StringIO import StringIO
20
21
22 def revertStandardError():
23     """Move stderr to stdout, and return the contents of the old stderr."""
24     errio = sys.stderr
25     if not isinstance(errio, StringIO):
26         return None
27     sys.stderr = sys.stdout
28     errio.seek(0)
29     return errio.read()
30
31 def printError():
32     """Revert stderr to stdout, and print the contents of stderr"""
33     if isinstance(sys.stderr, StringIO):
34         print revertStandardError()
35
36 if __name__ == '__main__':
37     import atexit
38     atexit.register(printError)
39     sys.stderr = StringIO()
40
41 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
42
43 from Cheetah.Template import Template
44 from sipb_xen_database import *
45
46 class MyException(Exception):
47     """Base class for my exceptions"""
48     pass
49
50 class InvalidInput(MyException):
51     """Exception for user-provided input is invalid but maybe in good faith.
52
53     This would include setting memory to negative (which might be a
54     typo) but not setting an invalid boot CD (which requires bypassing
55     the select box).
56     """
57     def __init__(self, err_field, err_value, expl=None):
58         MyException.__init__(self, expl)
59         self.err_field = err_field
60         self.err_value = err_value
61
62 class CodeError(MyException):
63     """Exception for internal errors or bad faith input."""
64     pass
65
66 def helppopup(subj):
67     """Return HTML code for a (?) link to a specified help topic"""
68     return ('<span class="helplink"><a href="help?subject=' + subj + 
69             '&amp;simple=true" target="_blank" ' + 
70             'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
71
72 class Global(object):
73     """Global state of the system, to avoid duplicate remctls to get state"""
74     def __init__(self, user):
75         self.user = user
76
77     def __get_uptimes(self):
78         if not hasattr(self, '_uptimes'):
79             self._uptimes = getUptimes(Machine.select())
80         return self._uptimes
81     uptimes = property(__get_uptimes)
82
83     def clear(self):
84         """Clear the state so future accesses reload it."""
85         for attr in ('_uptimes', ):
86             if hasattr(self, attr):
87                 delattr(self, attr)
88
89 g = None
90
91 class User:
92     """User class (sort of useless, I admit)"""
93     def __init__(self, username, email):
94         self.username = username
95         self.email = email
96
97 def makeErrorPre(old, addition):
98     if addition is None:
99         return
100     if old:
101         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
102     else:
103         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
104
105 Template.helppopup = staticmethod(helppopup)
106 Template.err = None
107
108 class JsonDict:
109     """Class to store a dictionary that will be converted to JSON"""
110     def __init__(self, **kws):
111         self.data = kws
112         if 'err' in kws:
113             err = kws['err']
114             del kws['err']
115             self.addError(err)
116
117     def __str__(self):
118         return simplejson.dumps(self.data)
119
120     def addError(self, text):
121         """Add stderr text to be displayed on the website."""
122         self.data['err'] = \
123             makeErrorPre(self.data.get('err'), text)
124
125 class Defaults:
126     """Class to store default values for fields."""
127     memory = 256
128     disk = 4.0
129     cdrom = ''
130     name = ''
131     vmtype = 'hvm'
132     def __init__(self, max_memory=None, max_disk=None, **kws):
133         if max_memory is not None:
134             self.memory = min(self.memory, max_memory)
135         if max_disk is not None:
136             self.max_disk = min(self.disk, max_disk)
137         for key in kws:
138             setattr(self, key, kws[key])
139
140
141
142 default_headers = {'Content-Type': 'text/html'}
143
144 # ... and stolen from xend/uuid.py
145 def randomUUID():
146     """Generate a random UUID."""
147
148     return [ random.randint(0, 255) for _ in range(0, 16) ]
149
150 def uuidToString(u):
151     """Turn a numeric UUID to a hyphen-seperated one."""
152     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
153                      "%02x" * 6]) % tuple(u)
154
155 MAX_MEMORY_TOTAL = 512
156 MAX_MEMORY_SINGLE = 256
157 MIN_MEMORY_SINGLE = 16
158 MAX_DISK_TOTAL = 50
159 MAX_DISK_SINGLE = 50
160 MIN_DISK_SINGLE = 0.1
161 MAX_VMS_TOTAL = 10
162 MAX_VMS_ACTIVE = 4
163
164 def getMachinesByOwner(user, machine=None):
165     """Return the machines owned by the same as a machine.
166     
167     If the machine is None, return the machines owned by the same
168     user.
169     """
170     if machine:
171         owner = machine.owner
172     else:
173         owner = user.username
174     return Machine.select_by(owner=owner)
175
176 def maxMemory(user, machine=None, on=True):
177     """Return the maximum memory for a machine or a user.
178
179     If machine is None, return the memory available for a new 
180     machine.  Else, return the maximum that machine can have.
181
182     on is whether the machine should be turned on.  If false, the max
183     memory for the machine to change to, if it is left off, is
184     returned.
185     """
186     if not on:
187         return MAX_MEMORY_SINGLE
188     machines = getMachinesByOwner(user, machine)
189     active_machines = [x for x in machines if g.uptimes[x]]
190     mem_usage = sum([x.memory for x in active_machines if x != machine])
191     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
192
193 def maxDisk(user, machine=None):
194     machines = getMachinesByOwner(user, machine)
195     disk_usage = sum([sum([y.size for y in x.disks])
196                       for x in machines if x != machine])
197     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
198
199 def cantAddVm(user):
200     machines = getMachinesByOwner(user)
201     active_machines = [x for x in machines if g.uptimes[x]]
202     if len(machines) >= MAX_VMS_TOTAL:
203         return 'You have too many VMs to create a new one.'
204     if len(active_machines) >= MAX_VMS_ACTIVE:
205         return ('You already have the maximum number of VMs turned on.  '
206                 'To create more, turn one off.')
207     return False
208
209 def haveAccess(user, machine):
210     """Return whether a user has adminstrative access to a machine"""
211     if user.username == 'moo':
212         return True
213     if user.username in (machine.administrator, machine.owner):
214         return True
215     if getafsgroups.checkAfsGroup(user.username, machine.administrator, 
216                                   'athena.mit.edu'): #XXX Cell?
217         return True
218     if getafsgroups.checkLockerOwner(user.username, machine.owner):
219         return True
220     return owns(user, machine)
221
222 def owns(user, machine):
223     """Return whether a user owns a machine"""
224     if user.username == 'moo':
225         return True
226     return getafsgroups.checkLockerOwner(user.username, machine.owner)
227
228 def error(op, user, fields, err, emsg):
229     """Print an error page when a CodeError occurs"""
230     d = dict(op=op, user=user, errorMessage=str(err),
231              stderr=emsg)
232     return Template(file='error.tmpl', searchList=[d]);
233
234 def invalidInput(op, user, fields, err, emsg):
235     """Print an error page when an InvalidInput exception occurs"""
236     d = dict(op=op, user=user, err_field=err.err_field,
237              err_value=str(err.err_value), stderr=emsg,
238              errorMessage=str(err))
239     return Template(file='invalid.tmpl', searchList=[d]);
240
241 def validMachineName(name):
242     """Check that name is valid for a machine name"""
243     if not name:
244         return False
245     charset = string.ascii_letters + string.digits + '-_'
246     if name[0] in '-_' or len(name) > 22:
247         return False
248     for x in name:
249         if x not in charset:
250             return False
251     return True
252
253 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
254     """Kinit with a given username and keytab"""
255
256     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
257                          stderr=subprocess.PIPE)
258     e = p.wait()
259     if e:
260         raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
261
262 def checkKinit():
263     """If we lack tickets, kinit."""
264     p = subprocess.Popen(['klist', '-s'])
265     if p.wait():
266         kinit()
267
268 def remctl(*args, **kws):
269     """Perform a remctl and return the output.
270
271     kinits if necessary, and outputs errors to stderr.
272     """
273     checkKinit()
274     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
275                          + list(args),
276                          stdout=subprocess.PIPE,
277                          stderr=subprocess.PIPE)
278     v = p.wait()
279     if kws.get('err'):
280         return p.stdout.read(), p.stderr.read()
281     if v:
282         print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
283         print >> sys.stderr, p.stderr.read()
284         raise CodeError('ERROR on remctl')
285     return p.stdout.read()
286
287 def lvcreate(machine, disk):
288     """Create a single disk for a machine"""
289     remctl('web', 'lvcreate', machine.name,
290            disk.guest_device_name, str(disk.size))
291     
292 def makeDisks(machine):
293     """Update the lvm partitions to add a disk."""
294     for disk in machine.disks:
295         lvcreate(machine, disk)
296
297 def bootMachine(machine, cdtype):
298     """Boot a machine with a given boot CD.
299
300     If cdtype is None, give no boot cd.  Otherwise, it is the string
301     id of the CD (e.g. 'gutsy_i386')
302     """
303     if cdtype is not None:
304         remctl('control', machine.name, 'create', 
305                cdtype)
306     else:
307         remctl('control', machine.name, 'create')
308
309 def registerMachine(machine):
310     """Register a machine to be controlled by the web interface"""
311     remctl('web', 'register', machine.name)
312
313 def unregisterMachine(machine):
314     """Unregister a machine to not be controlled by the web interface"""
315     remctl('web', 'unregister', machine.name)
316
317 def parseStatus(s):
318     """Parse a status string into nested tuples of strings.
319
320     s = output of xm list --long <machine_name>
321     """
322     values = re.split('([()])', s)
323     stack = [[]]
324     for v in values[2:-2]: #remove initial and final '()'
325         if not v:
326             continue
327         v = v.strip()
328         if v == '(':
329             stack.append([])
330         elif v == ')':
331             if len(stack[-1]) == 1:
332                 stack[-1].append('')
333             stack[-2].append(stack[-1])
334             stack.pop()
335         else:
336             if not v:
337                 continue
338             stack[-1].extend(v.split())
339     return stack[-1]
340
341 def getUptimes(machines=None):
342     """Return a dictionary mapping machine names to uptime strings"""
343     value_string = remctl('web', 'listvms')
344     lines = value_string.splitlines()
345     d = {}
346     for line in lines:
347         lst = line.split()
348         name, id = lst[:2]
349         uptime = ' '.join(lst[2:])
350         d[name] = uptime
351     ans = {}
352     for m in machines:
353         ans[m] = d.get(m.name)
354     return ans
355
356 def statusInfo(machine):
357     """Return the status list for a given machine.
358
359     Gets and parses xm list --long
360     """
361     value_string, err_string = remctl('control', machine.name, 'list-long', 
362                                       err=True)
363     if 'Unknown command' in err_string:
364         raise CodeError("ERROR in remctl list-long %s is not registered" % 
365                         (machine.name,))
366     elif 'does not exist' in err_string:
367         return None
368     elif err_string:
369         raise CodeError("ERROR in remctl list-long %s:  %s" % 
370                         (machine.name, err_string))
371     status = parseStatus(value_string)
372     return status
373
374 def hasVnc(status):
375     """Does the machine with a given status list support VNC?"""
376     if status is None:
377         return False
378     for l in status:
379         if l[0] == 'device' and l[1][0] == 'vfb':
380             d = dict(l[1][1:])
381             return 'location' in d
382     return False
383
384 def createVm(user, name, memory, disk, is_hvm, cdrom):
385     """Create a VM and put it in the database"""
386     # put stuff in the table
387     transaction = ctx.current.create_transaction()
388     try:
389         if memory > maxMemory(user):
390             raise InvalidInput('memory', memory,
391                                "Max %s" % maxMemory(user))
392         if disk > maxDisk(user) * 1024:
393             raise InvalidInput('disk', disk,
394                                "Max %s" % maxDisk(user))
395         reason = cantAddVm(user)
396         if reason:
397             raise InvalidInput('create', True, reason)
398         res = meta.engine.execute('select nextval('
399                                   '\'"machines_machine_id_seq"\')')
400         id = res.fetchone()[0]
401         machine = Machine()
402         machine.machine_id = id
403         machine.name = name
404         machine.memory = memory
405         machine.owner = user.username
406         machine.administrator = user.username
407         machine.contact = user.email
408         machine.uuid = uuidToString(randomUUID())
409         machine.boot_off_cd = True
410         machine_type = Type.get_by(hvm=is_hvm)
411         machine.type_id = machine_type.type_id
412         ctx.current.save(machine)
413         disk = Disk(machine.machine_id, 
414                     'hda', disk)
415         open_nics = NIC.select_by(machine_id=None)
416         if not open_nics: #No IPs left!
417             raise CodeError("No IP addresses left!  "
418                             "Contact sipb-xen-dev@mit.edu")
419         nic = open_nics[0]
420         nic.machine_id = machine.machine_id
421         nic.hostname = name
422         ctx.current.save(nic)    
423         ctx.current.save(disk)
424         transaction.commit()
425     except:
426         transaction.rollback()
427         raise
428     registerMachine(machine)
429     makeDisks(machine)
430     # tell it to boot with cdrom
431     bootMachine(machine, cdrom)
432
433     return machine
434
435 def validMemory(user, memory, machine=None, on=True):
436     """Parse and validate limits for memory for a given user and machine.
437
438     on is whether the memory must be valid after the machine is
439     switched on.
440     """
441     try:
442         memory = int(memory)
443         if memory < MIN_MEMORY_SINGLE:
444             raise ValueError
445     except ValueError:
446         raise InvalidInput('memory', memory, 
447                            "Minimum %s MB" % MIN_MEMORY_SINGLE)
448     if memory > maxMemory(user, machine, on):
449         raise InvalidInput('memory', memory,
450                            'Maximum %s MB' % maxMemory(user, machine))
451     return memory
452
453 def validDisk(user, disk, machine=None):
454     """Parse and validate limits for disk for a given user and machine."""
455     try:
456         disk = float(disk)
457         if disk > maxDisk(user, machine):
458             raise InvalidInput('disk', disk,
459                                "Maximum %s G" % maxDisk(user, machine))
460         disk = int(disk * 1024)
461         if disk < MIN_DISK_SINGLE * 1024:
462             raise ValueError
463     except ValueError:
464         raise InvalidInput('disk', disk,
465                            "Minimum %s GB" % MIN_DISK_SINGLE)
466     return disk
467
468 def parseCreate(user, fields):
469     name = fields.getfirst('name')
470     if not validMachineName(name):
471         raise InvalidInput('name', name, 'You must provide a machine name.')
472     name = name.lower()
473
474     if Machine.get_by(name=name):
475         raise InvalidInput('name', name,
476                            "Name already exists.")
477     
478     memory = fields.getfirst('memory')
479     memory = validMemory(user, memory, on=True)
480     
481     disk = fields.getfirst('disk')
482     disk = validDisk(user, disk)
483
484     vm_type = fields.getfirst('vmtype')
485     if vm_type not in ('hvm', 'paravm'):
486         raise CodeError("Invalid vm type '%s'"  % vm_type)    
487     is_hvm = (vm_type == 'hvm')
488
489     cdrom = fields.getfirst('cdrom')
490     if cdrom is not None and not CDROM.get(cdrom):
491         raise CodeError("Invalid cdrom type '%s'" % cdrom)
492     return dict(user=user, name=name, memory=memory, disk=disk,
493                 is_hvm=is_hvm, cdrom=cdrom)
494
495 def create(user, fields):
496     """Handler for create requests."""
497     try:
498         parsed_fields = parseCreate(user, fields)
499         machine = createVm(**parsed_fields)
500     except InvalidInput, err:
501         pass
502     else:
503         err = None
504     g.clear() #Changed global state
505     d = getListDict(user)
506     d['err'] = err
507     if err:
508         for field in fields.keys():
509             setattr(d['defaults'], field, fields.getfirst(field))
510     else:
511         d['new_machine'] = parsed_fields['name']
512     return Template(file='list.tmpl', searchList=[d])
513
514
515 def getListDict(user):
516     machines = [m for m in Machine.select() if haveAccess(user, m)]    
517     on = {}
518     has_vnc = {}
519     on = g.uptimes
520     for m in machines:
521         m.uptime = g.uptimes.get(m)
522         if not on[m]:
523             has_vnc[m] = 'Off'
524         elif m.type.hvm:
525             has_vnc[m] = True
526         else:
527             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
528     #     for m in machines:
529     #         status = statusInfo(m)
530     #         on[m.name] = status is not None
531     #         has_vnc[m.name] = hasVnc(status)
532     max_memory = maxMemory(user)
533     max_disk = maxDisk(user)
534     defaults = Defaults(max_memory=max_memory,
535                         max_disk=max_disk,
536                         cdrom='gutsy-i386')
537     d = dict(user=user,
538              cant_add_vm=cantAddVm(user),
539              max_memory=max_memory,
540              max_disk=max_disk,
541              defaults=defaults,
542              machines=machines,
543              has_vnc=has_vnc,
544              uptimes=g.uptimes,
545              cdroms=CDROM.select())
546     return d
547
548 def listVms(user, fields):
549     """Handler for list requests."""
550     d = getListDict(user)
551     return Template(file='list.tmpl', searchList=[d])
552             
553 def testMachineId(user, machineId, exists=True):
554     """Parse, validate and check authorization for a given machineId.
555
556     If exists is False, don't check that it exists.
557     """
558     if machineId is None:
559         raise CodeError("No machine ID specified")
560     try:
561         machineId = int(machineId)
562     except ValueError:
563         raise CodeError("Invalid machine ID '%s'" % machineId)
564     machine = Machine.get(machineId)
565     if exists and machine is None:
566         raise CodeError("No such machine ID '%s'" % machineId)
567     if machine is not None and not haveAccess(user, machine):
568         raise CodeError("No access to machine ID '%s'" % machineId)
569     return machine
570
571 def vnc(user, fields):
572     """VNC applet page.
573
574     Note that due to same-domain restrictions, the applet connects to
575     the webserver, which needs to forward those requests to the xen
576     server.  The Xen server runs another proxy that (1) authenticates
577     and (2) finds the correct port for the VM.
578
579     You might want iptables like:
580
581     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
582       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
583     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
584       --dport 10003 -j SNAT --to-source 18.187.7.142 
585     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
586       --dport 10003 -j ACCEPT
587
588     Remember to enable iptables!
589     echo 1 > /proc/sys/net/ipv4/ip_forward
590     """
591     machine = testMachineId(user, fields.getfirst('machine_id'))
592     
593     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
594
595     data = {}
596     data["user"] = user.username
597     data["machine"] = machine.name
598     data["expires"] = time.time()+(5*60)
599     pickled_data = cPickle.dumps(data)
600     m = hmac.new(TOKEN_KEY, digestmod=sha)
601     m.update(pickled_data)
602     token = {'data': pickled_data, 'digest': m.digest()}
603     token = cPickle.dumps(token)
604     token = base64.urlsafe_b64encode(token)
605     
606     status = statusInfo(machine)
607     has_vnc = hasVnc(status)
608     
609     d = dict(user=user,
610              on=status,
611              has_vnc=has_vnc,
612              machine=machine,
613              hostname=os.environ.get('SERVER_NAME', 'localhost'),
614              authtoken=token)
615     return Template(file='vnc.tmpl', searchList=[d])
616
617 def getNicInfo(data_dict, machine):
618     """Helper function for info, get data on nics for a machine.
619
620     Modifies data_dict to include the relevant data, and returns a list
621     of (key, name) pairs to display "name: data_dict[key]" to the user.
622     """
623     data_dict['num_nics'] = len(machine.nics)
624     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
625                            ('nic%s_mac', 'NIC %s MAC Addr'),
626                            ('nic%s_ip', 'NIC %s IP'),
627                            ]
628     nic_fields = []
629     for i in range(len(machine.nics)):
630         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
631         data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
632                                            '.servers.csail.mit.edu')
633         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
634         data_dict['nic%s_ip' % i] = machine.nics[i].ip
635     if len(machine.nics) == 1:
636         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
637     return nic_fields
638
639 def getDiskInfo(data_dict, machine):
640     """Helper function for info, get data on disks for a machine.
641
642     Modifies data_dict to include the relevant data, and returns a list
643     of (key, name) pairs to display "name: data_dict[key]" to the user.
644     """
645     data_dict['num_disks'] = len(machine.disks)
646     disk_fields_template = [('%s_size', '%s size')]
647     disk_fields = []
648     for disk in machine.disks:
649         name = disk.guest_device_name
650         disk_fields.extend([(x % name, y % name) for x, y in 
651                             disk_fields_template])
652         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
653     return disk_fields
654
655 def deleteVM(machine):
656     """Delete a VM."""
657     remctl('control', machine.name, 'destroy', err=True)
658     transaction = ctx.current.create_transaction()
659     delete_disk_pairs = [(machine.name, d.guest_device_name) 
660                          for d in machine.disks]
661     try:
662         for nic in machine.nics:
663             nic.machine_id = None
664             nic.hostname = None
665             ctx.current.save(nic)
666         for disk in machine.disks:
667             ctx.current.delete(disk)
668         ctx.current.delete(machine)
669         transaction.commit()
670     except:
671         transaction.rollback()
672         raise
673     for mname, dname in delete_disk_pairs:
674         remctl('web', 'lvremove', mname, dname)
675     unregisterMachine(machine)
676
677 def commandResult(user, fields):
678     print >> sys.stderr, time.time()-start_time
679     machine = testMachineId(user, fields.getfirst('machine_id'))
680     action = fields.getfirst('action')
681     cdrom = fields.getfirst('cdrom')
682     print >> sys.stderr, time.time()-start_time
683     if cdrom is not None and not CDROM.get(cdrom):
684         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
685     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
686                       'Delete VM'):
687         raise CodeError("Invalid action '%s'" % action)
688     if action == 'Reboot':
689         if cdrom is not None:
690             out, err = remctl('control', machine.name, 'reboot', cdrom,
691                               err=True)
692         else:
693             out, err = remctl('control', machine.name, 'reboot',
694                               err=True)
695         if err:
696             if re.match("Error: Domain '.*' does not exist.", err):
697                 raise InvalidInput("action", "reboot", 
698                                    "Machine is not on")
699             else:
700                 print >> sys.stderr, 'Error on reboot:'
701                 print >> sys.stderr, err
702                 raise CodeError('ERROR on remctl')
703                 
704     elif action == 'Power on':
705         if maxMemory(user) < machine.memory:
706             raise InvalidInput('action', 'Power on',
707                                "You don't have enough free RAM quota "
708                                "to turn on this machine.")
709         bootMachine(machine, cdrom)
710     elif action == 'Power off':
711         out, err = remctl('control', machine.name, 'destroy', err=True)
712         if err:
713             if re.match("Error: Domain '.*' does not exist.", err):
714                 raise InvalidInput("action", "Power off", 
715                                    "Machine is not on.")
716             else:
717                 print >> sys.stderr, 'Error on power off:'
718                 print >> sys.stderr, err
719                 raise CodeError('ERROR on remctl')
720     elif action == 'Shutdown':
721         out, err = remctl('control', machine.name, 'shutdown', err=True)
722         if err:
723             if re.match("Error: Domain '.*' does not exist.", err):
724                 raise InvalidInput("action", "Shutdown", 
725                                    "Machine is not on.")
726             else:
727                 print >> sys.stderr, 'Error on Shutdown:'
728                 print >> sys.stderr, err
729                 raise CodeError('ERROR on remctl')
730     elif action == 'Delete VM':
731         deleteVM(machine)
732     print >> sys.stderr, time.time()-start_time
733
734     d = dict(user=user,
735              command=action,
736              machine=machine)
737     return d
738
739 def command(user, fields):
740     """Handler for running commands like boot and delete on a VM."""
741     back = fields.getfirst('back')
742     try:
743         d = commandResult(user, fields)
744         if d['command'] == 'Delete VM':
745             back = 'list'
746     except InvalidInput, err:
747         if not back:
748             raise
749         print >> sys.stderr, err
750         result = None
751     else:
752         result = 'Success!'
753         if not back:
754             return Template(file='command.tmpl', searchList=[d])
755     if back == 'list':
756         g.clear() #Changed global state
757         d = getListDict(user)
758         d['result'] = result
759         return Template(file='list.tmpl', searchList=[d])
760     elif back == 'info':
761         machine = testMachineId(user, fields.getfirst('machine_id'))
762         d = infoDict(user, machine)
763         d['result'] = result
764         return Template(file='info.tmpl', searchList=[d])
765     else:
766         raise InvalidInput('back', back, 'Not a known back page.')
767
768 def testAdmin(user, admin, machine):
769     if admin in (None, machine.administrator):
770         return None
771     if admin == user.username:
772         return admin
773     if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
774         return admin
775     if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
776                                   'athena.mit.edu'):
777         return 'system:'+admin
778     return admin
779     #raise InvalidInput('administrator', admin, 
780     #                   'You must control the group you move it to.')
781     
782 def testOwner(user, owner, machine):
783     if owner in (None, machine.owner):
784         return None
785     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
786     if value == True:
787         return owner
788     raise InvalidInput('owner', owner, value)
789
790 def testContact(user, contact, machine=None):
791     if contact in (None, machine.contact):
792         return None
793     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
794         raise InvalidInput('contact', contact, "Not a valid email.")
795     return contact
796
797 def testDisk(user, disksize, machine=None):
798     return disksize
799
800 def testName(user, name, machine=None):
801     if name in (None, machine.name):
802         return None
803     if not Machine.select_by(name=name):
804         return name
805     raise InvalidInput('name', name, "Name is already taken.")
806
807 def testHostname(user, hostname, machine):
808     for nic in machine.nics:
809         if hostname == nic.hostname:
810             return hostname
811     # check if doesn't already exist
812     if NIC.select_by(hostname=hostname):
813         raise InvalidInput('hostname', hostname,
814                            "Already exists")
815     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
816         raise InvalidInput('hostname', hostname, "Not a valid hostname; "
817                            "must only use number, letters, and dashes.")
818     return hostname
819
820 def modifyDict(user, fields):
821     olddisk = {}
822     transaction = ctx.current.create_transaction()
823     try:
824         machine = testMachineId(user, fields.getfirst('machine_id'))
825         owner = testOwner(user, fields.getfirst('owner'), machine)
826         admin = testAdmin(user, fields.getfirst('administrator'), machine)
827         contact = testContact(user, fields.getfirst('contact'), machine)
828         hostname = testHostname(owner, fields.getfirst('hostname'), machine)
829         name = testName(user, fields.getfirst('name'), machine)
830         oldname = machine.name
831         command = "modify"
832
833         memory = fields.getfirst('memory')
834         if memory is not None:
835             memory = validMemory(user, memory, machine, on=False)
836             machine.memory = memory
837  
838         disksize = testDisk(user, fields.getfirst('disk'))
839         if disksize is not None:
840             disksize = validDisk(user, disksize, machine)
841             disk = machine.disks[0]
842             if disk.size != disksize:
843                 olddisk[disk.guest_device_name] = disksize
844                 disk.size = disksize
845                 ctx.current.save(disk)
846         
847         # XXX first NIC gets hostname on change?  
848         # Interface doesn't support more.
849         for nic in machine.nics[:1]:
850             nic.hostname = hostname
851             ctx.current.save(nic)
852
853         if owner is not None:
854             machine.owner = owner
855         if name is not None:
856             machine.name = name
857         if admin is not None:
858             machine.administrator = admin
859         if contact is not None:
860             machine.contact = contact
861             
862         ctx.current.save(machine)
863         transaction.commit()
864     except:
865         transaction.rollback()
866         raise
867     for diskname in olddisk:
868         remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
869     if name is not None:
870         for disk in machine.disks:
871             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
872         remctl("web", "moveregister", oldname, name)
873     return dict(user=user,
874                 command=command,
875                 machine=machine)
876     
877 def modify(user, fields):
878     """Handler for modifying attributes of a machine."""
879     try:
880         modify_dict = modifyDict(user, fields)
881     except InvalidInput, err:
882         result = None
883         machine = testMachineId(user, fields.getfirst('machine_id'))
884     else:
885         machine = modify_dict['machine']
886         result='Success!'
887         err = None
888     info_dict = infoDict(user, machine)
889     info_dict['err'] = err
890     if err:
891         for field in fields.keys():
892             setattr(info_dict['defaults'], field, fields.getfirst(field))
893     info_dict['result'] = result
894     return Template(file='info.tmpl', searchList=[info_dict])
895     
896
897 def helpHandler(user, fields):
898     """Handler for help messages."""
899     simple = fields.getfirst('simple')
900     subjects = fields.getlist('subject')
901     
902     help_mapping = dict(paravm_console="""
903 ParaVM machines do not support console access over VNC.  To access
904 these machines, you either need to boot with a liveCD and ssh in or
905 hope that the sipb-xen maintainers add support for serial consoles.""",
906                         hvm_paravm="""
907 HVM machines use the virtualization features of the processor, while
908 ParaVM machines use Xen's emulation of virtualization features.  You
909 want an HVM virtualized machine.""",
910                         cpu_weight="""
911 Don't ask us!  We're as mystified as you are.""",
912                         owner="""
913 The owner field is used to determine <a
914 href="help?subject=quotas">quotas</a>.  It must be the name of a
915 locker that you are an AFS administrator of.  In particular, you or an
916 AFS group you are a member of must have AFS rlidwka bits on the
917 locker.  You can check see who administers the LOCKER locker using the
918 command 'fs la /mit/LOCKER' on Athena.)  See also <a
919 href="help?subject=administrator">administrator</a>.""",
920                         administrator="""
921 The administrator field determines who can access the console and
922 power on and off the machine.  This can be either a user or a moira
923 group.""",
924                         quotas="""
925 Quotas are determined on a per-locker basis.  Each quota may have a
926 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
927 active machines."""
928                    )
929     
930     if not subjects:
931         subjects = sorted(help_mapping.keys())
932         
933     d = dict(user=user,
934              simple=simple,
935              subjects=subjects,
936              mapping=help_mapping)
937     
938     return Template(file="help.tmpl", searchList=[d])
939     
940
941 def badOperation(u, e):
942     raise CodeError("Unknown operation")
943
944 def infoDict(user, machine):
945     status = statusInfo(machine)
946     has_vnc = hasVnc(status)
947     if status is None:
948         main_status = dict(name=machine.name,
949                            memory=str(machine.memory))
950         uptime = None
951         cputime = None
952     else:
953         main_status = dict(status[1:])
954         start_time = float(main_status.get('start_time', 0))
955         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
956         cpu_time_float = float(main_status.get('cpu_time', 0))
957         cputime = datetime.timedelta(seconds=int(cpu_time_float))
958     display_fields = """name uptime memory state cpu_weight on_reboot 
959      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
960     display_fields = [('name', 'Name'),
961                       ('owner', 'Owner'),
962                       ('administrator', 'Administrator'),
963                       ('contact', 'Contact'),
964                       ('type', 'Type'),
965                       'NIC_INFO',
966                       ('uptime', 'uptime'),
967                       ('cputime', 'CPU usage'),
968                       ('memory', 'RAM'),
969                       'DISK_INFO',
970                       ('state', 'state (xen format)'),
971                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
972                       ('on_reboot', 'Action on VM reboot'),
973                       ('on_poweroff', 'Action on VM poweroff'),
974                       ('on_crash', 'Action on VM crash'),
975                       ('on_xend_start', 'Action on Xen start'),
976                       ('on_xend_stop', 'Action on Xen stop'),
977                       ('bootloader', 'Bootloader options'),
978                       ]
979     fields = []
980     machine_info = {}
981     machine_info['name'] = machine.name
982     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
983     machine_info['owner'] = machine.owner
984     machine_info['administrator'] = machine.administrator
985     machine_info['contact'] = machine.contact
986
987     nic_fields = getNicInfo(machine_info, machine)
988     nic_point = display_fields.index('NIC_INFO')
989     display_fields = (display_fields[:nic_point] + nic_fields + 
990                       display_fields[nic_point+1:])
991
992     disk_fields = getDiskInfo(machine_info, machine)
993     disk_point = display_fields.index('DISK_INFO')
994     display_fields = (display_fields[:disk_point] + disk_fields + 
995                       display_fields[disk_point+1:])
996     
997     main_status['memory'] += ' MB'
998     for field, disp in display_fields:
999         if field in ('uptime', 'cputime') and locals()[field] is not None:
1000             fields.append((disp, locals()[field]))
1001         elif field in machine_info:
1002             fields.append((disp, machine_info[field]))
1003         elif field in main_status:
1004             fields.append((disp, main_status[field]))
1005         else:
1006             pass
1007             #fields.append((disp, None))
1008     max_mem = maxMemory(user, machine)
1009     max_disk = maxDisk(user, machine)
1010     defaults=Defaults()
1011     for name in 'machine_id name administrator owner memory contact'.split():
1012         setattr(defaults, name, getattr(machine, name))
1013     if machine.nics:
1014         defaults.hostname = machine.nics[0].hostname
1015     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
1016     d = dict(user=user,
1017              cdroms=CDROM.select(),
1018              on=status is not None,
1019              machine=machine,
1020              defaults=defaults,
1021              has_vnc=has_vnc,
1022              uptime=str(uptime),
1023              ram=machine.memory,
1024              max_mem=max_mem,
1025              max_disk=max_disk,
1026              owner_help=helppopup("owner"),
1027              fields = fields)
1028     return d
1029
1030 def info(user, fields):
1031     """Handler for info on a single VM."""
1032     machine = testMachineId(user, fields.getfirst('machine_id'))
1033     d = infoDict(user, machine)
1034     return Template(file='info.tmpl', searchList=[d])
1035
1036 mapping = dict(list=listVms,
1037                vnc=vnc,
1038                command=command,
1039                modify=modify,
1040                info=info,
1041                create=create,
1042                help=helpHandler)
1043
1044 def printHeaders(headers):
1045     for key, value in headers.iteritems():
1046         print '%s: %s' % (key, value)
1047     print
1048
1049
1050 def getUser():
1051     """Return the current user based on the SSL environment variables"""
1052     if 'SSL_CLIENT_S_DN_Email' in os.environ:
1053         username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
1054         return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
1055     else:
1056         return User('moo', 'nobody')
1057
1058 if __name__ == '__main__':
1059     start_time = time.time()
1060     fields = cgi.FieldStorage()
1061     u = getUser()
1062     g = Global(u)
1063     operation = os.environ.get('PATH_INFO', '')
1064     if not operation:
1065         print "Status: 301 Moved Permanently"
1066         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
1067         sys.exit(0)
1068
1069     if operation.startswith('/'):
1070         operation = operation[1:]
1071     if not operation:
1072         operation = 'list'
1073
1074
1075
1076     fun = mapping.get(operation, badOperation)
1077
1078     if fun not in (helpHandler, ):
1079         connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
1080     try:
1081         output = fun(u, fields)
1082
1083         headers = dict(default_headers)
1084         if isinstance(output, tuple):
1085             new_headers, output = output
1086             headers.update(new_headers)
1087
1088         e = revertStandardError()
1089         if e:
1090             output.addError(e)
1091         printHeaders(headers)
1092         print output
1093     except Exception, err:
1094         if not fields.has_key('js'):
1095             if isinstance(err, CodeError):
1096                 print 'Content-Type: text/html\n'
1097                 e = revertStandardError()
1098                 print error(operation, u, fields, err, e)
1099                 sys.exit(1)
1100             if isinstance(err, InvalidInput):
1101                 print 'Content-Type: text/html\n'
1102                 e = revertStandardError()
1103                 print invalidInput(operation, u, fields, err, e)
1104                 sys.exit(1)
1105         print 'Content-Type: text/plain\n'
1106         print 'Uh-oh!  We experienced an error.'
1107         print 'Please email sipb-xen@mit.edu with the contents of this page.'
1108         print '----'
1109         e = revertStandardError()
1110         print e
1111         print '----'
1112         raise