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