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