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