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