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