Python 2.4 support
[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
16 sys.stderr = sys.stdout
17 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
18
19 from Cheetah.Template import Template
20 from sipb_xen_database import *
21 import random
22
23 class MyException(Exception):
24     pass
25
26 def helppopup(subj):
27     return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
28
29
30 global_dict = {}
31 global_dict['helppopup'] = helppopup
32
33
34 # ... and stolen from xend/uuid.py
35 def randomUUID():
36     """Generate a random UUID."""
37
38     return [ random.randint(0, 255) for _ in range(0, 16) ]
39
40 def uuidToString(u):
41     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
42                      "%02x" * 6]) % tuple(u)
43
44 def maxMemory(user, machine=None):
45     return 256
46
47 def maxDisk(user, machine=None):
48     return 10.0
49
50 def haveAccess(user, machine):
51     if user.username == 'moo':
52         return True
53     return machine.owner == user.username
54
55 def error(op, user, fields, err):
56     d = dict(op=op, user=user, errorMessage=str(err))
57     print Template(file='error.tmpl', searchList=[d, global_dict]);
58
59 def validMachineName(name):
60     """Check that name is valid for a machine name"""
61     if not name:
62         return False
63     charset = string.ascii_letters + string.digits + '-_'
64     if name[0] in '-_' or len(name) > 22:
65         return False
66     for x in name:
67         if x not in charset:
68             return False
69     return True
70
71 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
72     """Kinit with a given username and keytab"""
73
74     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
75                          stderr=subprocess.PIPE)
76     e = p.wait()
77     if e:
78         raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
79
80 def checkKinit():
81     """If we lack tickets, kinit."""
82     p = subprocess.Popen(['klist', '-s'])
83     if p.wait():
84         kinit()
85
86 def remctl(*args, **kws):
87     """Perform a remctl and return the output.
88
89     kinits if necessary, and outputs errors to stderr.
90     """
91     checkKinit()
92     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
93                          + list(args),
94                          stdout=subprocess.PIPE,
95                          stderr=subprocess.PIPE)
96     if kws.get('err'):
97         return p.stdout.read(), p.stderr.read()
98     if p.wait():
99         print >> sys.stderr, 'ERROR on remctl ', args
100         print >> sys.stderr, p.stderr.read()
101     return p.stdout.read()
102
103 def makeDisks():
104     """Update the lvm partitions to include all disks in the database."""
105     remctl('web', 'lvcreate')
106
107 def bootMachine(machine, cdtype):
108     """Boot a machine with a given boot CD.
109
110     If cdtype is None, give no boot cd.  Otherwise, it is the string
111     id of the CD (e.g. 'gutsy_i386')
112     """
113     if cdtype is not None:
114         remctl('web', 'vmboot', machine.name,
115                cdtype)
116     else:
117         remctl('web', 'vmboot', machine.name)
118
119 def registerMachine(machine):
120     """Register a machine to be controlled by the web interface"""
121     remctl('web', 'register', machine.name)
122
123 def unregisterMachine(machine):
124     """Unregister a machine to not be controlled by the web interface"""
125     remctl('web', 'unregister', machine.name)
126
127 def parseStatus(s):
128     """Parse a status string into nested tuples of strings.
129
130     s = output of xm list --long <machine_name>
131     """
132     values = re.split('([()])', s)
133     stack = [[]]
134     for v in values[2:-2]: #remove initial and final '()'
135         if not v:
136             continue
137         v = v.strip()
138         if v == '(':
139             stack.append([])
140         elif v == ')':
141             if len(stack[-1]) == 1:
142                 stack[-1].append('')
143             stack[-2].append(stack[-1])
144             stack.pop()
145         else:
146             if not v:
147                 continue
148             stack[-1].extend(v.split())
149     return stack[-1]
150
151 def getUptimes(machines):
152     """Return a dictionary mapping machine names to uptime strings"""
153     value_string = remctl('web', 'listvms')
154     lines = value_string.splitlines()
155     d = {}
156     for line in lines[1:]:
157         lst = line.split()
158         name, id = lst[:2]
159         uptime = ' '.join(lst[2:])
160         d[name] = uptime
161     return d
162
163 def statusInfo(machine):
164     """Return the status list for a given machine.
165
166     Gets and parses xm list --long
167     """
168     value_string, err_string = remctl('list-long', machine.name, err=True)
169     if 'Unknown command' in err_string:
170         raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
171     elif 'does not exist' in err_string:
172         return None
173     elif err_string:
174         raise MyException("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
175     status = parseStatus(value_string)
176     return status
177
178 def hasVnc(status):
179     """Does the machine with a given status list support VNC?"""
180     if status is None:
181         return False
182     for l in status:
183         if l[0] == 'device' and l[1][0] == 'vfb':
184             d = dict(l[1][1:])
185             return 'location' in d
186     return False
187
188 def createVm(user, name, memory, disk, is_hvm, cdrom):
189     """Create a VM and put it in the database"""
190     # put stuff in the table
191     transaction = ctx.current.create_transaction()
192     try:
193         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
194         id = res.fetchone()[0]
195         machine = Machine()
196         machine.machine_id = id
197         machine.name = name
198         machine.memory = memory
199         machine.owner = user.username
200         machine.contact = user.email
201         machine.uuid = uuidToString(randomUUID())
202         machine.boot_off_cd = True
203         machine_type = Type.get_by(hvm=is_hvm)
204         machine.type_id = machine_type.type_id
205         ctx.current.save(machine)
206         disk = Disk(machine.machine_id, 
207                     'hda', disk)
208         open = NIC.select_by(machine_id=None)
209         if not open: #No IPs left!
210             return "No IP addresses left!  Contact sipb-xen-dev@mit.edu"
211         nic = open[0]
212         nic.machine_id = machine.machine_id
213         nic.hostname = name
214         ctx.current.save(nic)    
215         ctx.current.save(disk)
216         transaction.commit()
217     except:
218         transaction.rollback()
219         raise
220     makeDisks()
221     registerMachine(machine)
222     # tell it to boot with cdrom
223     bootMachine(machine, cdrom)
224
225     return machine
226
227 def validMemory(user, memory, machine=None):
228     try:
229         memory = int(memory)
230         if memory <= 0:
231             raise ValueError
232     except ValueError:
233         raise MyException("Invalid memory amount")
234     if memory > maxMemory(user, machine):
235         raise MyException("Too much memory requested")
236     return memory
237
238 def validDisk(user, disk, machine=None):
239     try:
240         disk = float(disk)
241         if disk > maxDisk(user, machine):
242             raise MyException("Too much disk requested")
243         disk = int(disk * 1024)
244         if disk <= 0:
245             raise ValueError
246     except ValueError:
247         raise MyException("Invalid disk amount")
248     return disk
249
250 def create(user, fields):
251     name = fields.getfirst('name')
252     if not validMachineName(name):
253         raise MyException("Invalid name '%s'" % name)
254     name = user.username + '_' + name.lower()
255
256     if Machine.get_by(name=name):
257         raise MyException("A machine named '%s' already exists" % name)
258     
259     memory = fields.getfirst('memory')
260     memory = validMemory(user, memory)
261     
262     disk = fields.getfirst('disk')
263     disk = validDisk(user, disk)
264
265     vm_type = fields.getfirst('vmtype')
266     if vm_type not in ('hvm', 'paravm'):
267         raise MyException("Invalid vm type '%s'"  % vm_type)    
268     is_hvm = (vm_type == 'hvm')
269
270     cdrom = fields.getfirst('cdrom')
271     if cdrom is not None and not CDROM.get(cdrom):
272         raise MyException("Invalid cdrom type '%s'" % cdrom)    
273     
274     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
275     if isinstance(machine, basestring):
276         raise MyException(machine)
277     d = dict(user=user,
278              machine=machine)
279     print Template(file='create.tmpl',
280                    searchList=[d, global_dict]);
281
282 def listVms(user, fields):
283     machines = [m for m in Machine.select() if haveAccess(user, m)]    
284     on = {}
285     has_vnc = {}
286     uptimes = getUptimes(machines)
287     on = uptimes
288     for m in machines:
289         if not on.get(m.name):
290             has_vnc[m.name] = 'Off'
291         elif m.type.hvm:
292             has_vnc[m.name] = True
293         else:
294             has_vnc[m.name] = "ParaVM"+helppopup("paravm_console")
295     #     for m in machines:
296     #         status = statusInfo(m)
297     #         on[m.name] = status is not None
298     #         has_vnc[m.name] = hasVnc(status)
299     d = dict(user=user,
300              maxmem=maxMemory(user),
301              maxdisk=maxDisk(user),
302              machines=machines,
303              has_vnc=has_vnc,
304              uptimes=uptimes,
305              cdroms=CDROM.select())
306     print Template(file='list.tmpl', searchList=[d, global_dict])
307
308 def testMachineId(user, machineId, exists=True):
309     if machineId is None:
310         raise MyException("No machine ID specified")
311     try:
312         machineId = int(machineId)
313     except ValueError:
314         raise MyException("Invalid machine ID '%s'" % machineId)
315     machine = Machine.get(machineId)
316     if exists and machine is None:
317         raise MyException("No such machine ID '%s'" % machineId)
318     if not haveAccess(user, machine):
319         raise MyException("No access to machine ID '%s'" % machineId)
320     return machine
321
322 def vnc(user, fields):
323     """VNC applet page.
324
325     Note that due to same-domain restrictions, the applet connects to
326     the webserver, which needs to forward those requests to the xen
327     server.  The Xen server runs another proxy that (1) authenticates
328     and (2) finds the correct port for the VM.
329
330     You might want iptables like:
331
332     -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 
333     -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 
334     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
335     """
336     machine = testMachineId(user, fields.getfirst('machine_id'))
337     #XXX fix
338     
339     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
340
341     data = {}
342     data["user"] = user.username
343     data["machine"]=machine.name
344     data["expires"]=time.time()+(5*60)
345     pickledData = cPickle.dumps(data)
346     m = hmac.new(TOKEN_KEY, digestmod=sha)
347     m.update(pickledData)
348     token = {'data': pickledData, 'digest': m.digest()}
349     token = cPickle.dumps(token)
350     token = base64.urlsafe_b64encode(token)
351     
352     d = dict(user=user,
353              machine=machine,
354              hostname=os.environ.get('SERVER_NAME', 'localhost'),
355              authtoken=token)
356     print Template(file='vnc.tmpl',
357                    searchList=[d, global_dict])
358
359 def getNicInfo(data_dict, machine):
360     data_dict['num_nics'] = len(machine.nics)
361     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
362                            ('nic%s_mac', 'NIC %s MAC Addr'),
363                            ('nic%s_ip', 'NIC %s IP'),
364                            ]
365     nic_fields = []
366     for i in range(len(machine.nics)):
367         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
368         data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
369         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
370         data_dict['nic%s_ip' % i] = machine.nics[i].ip
371     if len(machine.nics) == 1:
372         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
373     return nic_fields
374
375 def getDiskInfo(data_dict, machine):
376     data_dict['num_disks'] = len(machine.disks)
377     disk_fields_template = [('%s_size', '%s size')]
378     disk_fields = []
379     for disk in machine.disks:
380         name = disk.guest_device_name
381         disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
382         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
383     return disk_fields
384
385 def deleteVM(machine):
386     transaction = ctx.current.create_transaction()
387     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
388     try:
389         for nic in machine.nics:
390             nic.machine_id = None
391             nic.hostname = None
392             ctx.current.save(nic)
393         for disk in machine.disks:
394             ctx.current.delete(disk)
395         ctx.current.delete(machine)
396         transaction.commit()
397     except:
398         transaction.rollback()
399         raise
400     for mname, dname in delete_disk_pairs:
401         remctl('web', 'lvremove', mname, dname)
402     unregisterMachine(machine)
403
404 def command(user, fields):
405     print time.time()-start_time
406     machine = testMachineId(user, fields.getfirst('machine_id'))
407     action = fields.getfirst('action')
408     cdrom = fields.getfirst('cdrom')
409     print time.time()-start_time
410     if cdrom is not None and not CDROM.get(cdrom):
411         raise MyException("Invalid cdrom type '%s'" % cdrom)    
412     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
413         raise MyException("Invalid action '%s'" % action)
414     if action == 'Reboot':
415         if cdrom is not None:
416             remctl('reboot', machine.name, cdrom)
417         else:
418             remctl('reboot', machine.name)
419     elif action == 'Power on':
420         bootMachine(machine, cdrom)
421     elif action == 'Power off':
422         remctl('destroy', machine.name)
423     elif action == 'Shutdown':
424         remctl('shutdown', machine.name)
425     elif action == 'Delete VM':
426         deleteVM(machine)
427     print time.time()-start_time
428
429     d = dict(user=user,
430              command=action,
431              machine=machine)
432     print Template(file="command.tmpl", searchList=[d, global_dict])
433         
434 def modify(user, fields):
435     machine = testMachineId(user, fields.getfirst('machine_id'))
436     
437 def help(user, fields):
438     simple = fields.getfirst('simple')
439     subjects = fields.getlist('subject')
440     
441     mapping = dict(paravm_console="""
442 ParaVM machines do not support console access over VNC.  To access
443 these machines, you either need to boot with a liveCD and ssh in or
444 hope that the sipb-xen maintainers add support for serial consoles.""",
445                    hvm_paravm="""
446 HVM machines use the virtualization features of the processor, while
447 ParaVM machines use Xen's emulation of virtualization features.  You
448 want an HVM virtualized machine.""",
449                    cpu_weight="""Don't ask us!  We're as mystified as you are.""")
450     
451     d = dict(user=user,
452              simple=simple,
453              subjects=subjects,
454              mapping=mapping)
455     
456     print Template(file="help.tmpl", searchList=[d, global_dict])
457     
458
459 def info(user, fields):
460     machine = testMachineId(user, fields.getfirst('machine_id'))
461     status = statusInfo(machine)
462     has_vnc = hasVnc(status)
463     if status is None:
464         main_status = dict(name=machine.name,
465                            memory=str(machine.memory))
466     else:
467         main_status = dict(status[1:])
468     start_time = float(main_status.get('start_time', 0))
469     uptime = datetime.timedelta(seconds=int(time.time()-start_time))
470     cpu_time_float = float(main_status.get('cpu_time', 0))
471     cputime = datetime.timedelta(seconds=int(cpu_time_float))
472     display_fields = """name uptime memory state cpu_weight on_reboot 
473      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
474     display_fields = [('name', 'Name'),
475                       ('owner', 'Owner'),
476                       ('contact', 'Contact'),
477                       ('type', 'Type'),
478                       'NIC_INFO',
479                       ('uptime', 'uptime'),
480                       ('cputime', 'CPU usage'),
481                       ('memory', 'RAM'),
482                       'DISK_INFO',
483                       ('state', 'state (xen format)'),
484                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
485                       ('on_reboot', 'Action on VM reboot'),
486                       ('on_poweroff', 'Action on VM poweroff'),
487                       ('on_crash', 'Action on VM crash'),
488                       ('on_xend_start', 'Action on Xen start'),
489                       ('on_xend_stop', 'Action on Xen stop'),
490                       ('bootloader', 'Bootloader options'),
491                       ]
492     fields = []
493     machine_info = {}
494     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
495     machine_info['owner'] = machine.owner
496     machine_info['contact'] = machine.contact
497
498     nic_fields = getNicInfo(machine_info, machine)
499     nic_point = display_fields.index('NIC_INFO')
500     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
501
502     disk_fields = getDiskInfo(machine_info, machine)
503     disk_point = display_fields.index('DISK_INFO')
504     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
505     
506     main_status['memory'] += ' MB'
507     for field, disp in display_fields:
508         if field in ('uptime', 'cputime'):
509             fields.append((disp, locals()[field]))
510         elif field in main_status:
511             fields.append((disp, main_status[field]))
512         elif field in machine_info:
513             fields.append((disp, machine_info[field]))
514         else:
515             pass
516             #fields.append((disp, None))
517
518     d = dict(user=user,
519              cdroms=CDROM.select(),
520              on=status is not None,
521              machine=machine,
522              has_vnc=has_vnc,
523              uptime=str(uptime),
524              ram=machine.memory,
525              maxmem=maxMemory(user, machine),
526              maxdisk=maxDisk(user, machine),
527              fields = fields)
528     print Template(file='info.tmpl',
529                    searchList=[d, global_dict])
530
531 mapping = dict(list=listVms,
532                vnc=vnc,
533                command=command,
534                modify=modify,
535                info=info,
536                create=create,
537                help=help)
538
539 if __name__ == '__main__':
540     start_time = time.time()
541     fields = cgi.FieldStorage()
542     class User:
543         username = "moo"
544         email = 'moo@cow.com'
545     u = User()
546     if 'SSL_CLIENT_S_DN_Email' in os.environ:
547         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
548         u.username = username
549         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
550     else:
551         u.username = 'nobody'
552         u.email = None
553     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
554     operation = os.environ.get('PATH_INFO', '')
555     #print 'Content-Type: text/plain\n'
556     #print operation
557     if not operation:
558         print "Status: 301 Moved Permanently"
559         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
560         sys.exit(0)
561     print 'Content-Type: text/html\n'
562
563     if operation.startswith('/'):
564         operation = operation[1:]
565     if not operation:
566         operation = 'list'
567     
568     fun = mapping.get(operation, 
569                       lambda u, e:
570                           error(operation, u, e,
571                                 "Invalid operation '%s'" % operation))
572     if fun not in (help, ):
573         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
574     try:
575         fun(u, fields)
576     except MyException, err:
577         error(operation, u, fields, err)