Moo!
[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 validMemory(user, memory, machine=None):
216     try:
217         memory = int(memory)
218         if memory <= 0:
219             raise ValueError
220     except ValueError:
221         raise MyException("Invalid memory amount")
222     if memory > maxMemory(user, machine):
223         raise MyException("Too much memory requested")
224     return memory
225
226 def validDisk(user, disk, machine=None):
227     try:
228         disk = float(disk)
229         if disk > maxDisk(user, machine):
230             raise MyException("Too much disk requested")
231         disk = int(disk * 1024)
232         if disk <= 0:
233             raise ValueError
234     except ValueError:
235         raise MyException("Invalid disk amount")
236     return disk
237
238 def create(user, fields):
239     name = fields.getfirst('name')
240     if not validMachineName(name):
241         raise MyException("Invalid name '%s'" % name)
242     name = user.username + '_' + name.lower()
243
244     if Machine.get_by(name=name):
245         raise MyException("A machine named '%s' already exists" % name)
246     
247     memory = fields.getfirst('memory')
248     memory = validMemory(user, memory)
249     
250     disk = fields.getfirst('disk')
251     disk = validDisk(user, disk)
252
253     vm_type = fields.getfirst('vmtype')
254     if vm_type not in ('hvm', 'paravm'):
255         raise MyException("Invalid vm type '%s'"  % vm_type)    
256     is_hvm = (vm_type == 'hvm')
257
258     cdrom = fields.getfirst('cdrom')
259     if cdrom is not None and not CDROM.get(cdrom):
260         raise MyException("Invalid cdrom type '%s'" % cdrom)    
261     
262     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
263     if isinstance(machine, basestring):
264         raise MyException(machine)
265     d = dict(user=user,
266              machine=machine)
267     print Template(file='create.tmpl',
268                    searchList=d);
269
270 def listVms(user, fields):
271     machines = Machine.select()
272     on = {}
273     has_vnc = {}
274     uptimes = getUptimes(machines)
275     on = has_vnc = uptimes
276     #     for m in machines:
277     #         status = statusInfo(m)
278     #         on[m.name] = status is not None
279     #         has_vnc[m.name] = hasVnc(status)
280     d = dict(user=user,
281              maxmem=maxMemory(user),
282              maxdisk=maxDisk(user),
283              machines=machines,
284              has_vnc=has_vnc,
285              uptimes=uptimes,
286              cdroms=CDROM.select())
287     print Template(file='list.tmpl', searchList=d)
288
289 def testMachineId(user, machineId, exists=True):
290     if machineId is None:
291         raise MyException("No machine ID specified")
292     try:
293         machineId = int(machineId)
294     except ValueError:
295         raise MyException("Invalid machine ID '%s'" % machineId)
296     machine = Machine.get(machineId)
297     if exists and machine is None:
298         raise MyException("No such machine ID '%s'" % machineId)
299     if not haveAccess(user, machine):
300         raise MyException("No access to machine ID '%s'" % machineId)
301     return machine
302
303 def vnc(user, fields):
304     """VNC applet page.
305
306     Note that due to same-domain restrictions, the applet connects to
307     the webserver, which needs to forward those requests to the xen
308     server.  The Xen server runs another proxy that (1) authenticates
309     and (2) finds the correct port for the VM.
310
311     You might want iptables like:
312
313     -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 
314     -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 
315     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
316     """
317     machine = testMachineId(user, fields.getfirst('machine_id'))
318     #XXX fix
319     
320     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
321
322     data = {}
323     data["user"] = user.username
324     data["machine"]=machine.name
325     data["expires"]=time.time()+(5*60)
326     pickledData = cPickle.dumps(data)
327     m = hmac.new(TOKEN_KEY, digestmod=sha)
328     m.update(pickledData)
329     token = {'data': pickledData, 'digest': m.digest()}
330     token = cPickle.dumps(token)
331     token = base64.urlsafe_b64encode(token)
332     
333     d = dict(user=user,
334              machine=machine,
335              hostname=os.environ.get('SERVER_NAME', 'localhost'),
336              authtoken=token)
337     print Template(file='vnc.tmpl',
338                    searchList=d)
339
340 def getNicInfo(data_dict, machine):
341     data_dict['num_nics'] = len(machine.nics)
342     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
343                            ('nic%s_mac', 'NIC %s MAC Addr'),
344                            ('nic%s_ip', 'NIC %s IP'),
345                            ]
346     nic_fields = []
347     for i in range(len(machine.nics)):
348         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
349         data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
350         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
351         data_dict['nic%s_ip' % i] = machine.nics[i].ip
352     if len(machine.nics) == 1:
353         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
354     return nic_fields
355
356 def getDiskInfo(data_dict, machine):
357     data_dict['num_disks'] = len(machine.disks)
358     disk_fields_template = [('%s_size', '%s size')]
359     disk_fields = []
360     for disk in machine.disks:
361         name = disk.guest_device_name
362         disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
363         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
364     return disk_fields
365
366 def deleteVM(machine):
367     transaction = ctx.current.create_transaction()
368     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
369     try:
370         for nic in machine.nics:
371             nic.machine_id = None
372             nic.hostname = None
373             ctx.current.save(nic)
374         for disk in machine.disks:
375             ctx.current.delete(disk)
376         ctx.current.delete(machine)
377         transaction.commit()
378     except:
379         transaction.rollback()
380         raise
381     for mname, dname in delete_disk_pairs:
382         remctl('web', 'lvremove', mname, dname)
383     unregisterMachine(machine)
384
385 def command(user, fields):
386     print time.time()-start_time
387     machine = testMachineId(user, fields.getfirst('machine_id'))
388     action = fields.getfirst('action')
389     cdrom = fields.getfirst('cdrom')
390     print time.time()-start_time
391     if cdrom is not None and not CDROM.get(cdrom):
392         raise MyException("Invalid cdrom type '%s'" % cdrom)    
393     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
394         raise MyException("Invalid action '%s'" % action)
395     if action == 'Reboot':
396         if cdrom is not None:
397             remctl('reboot', machine.name, cdrom)
398         else:
399             remctl('reboot', machine.name)
400     elif action == 'Power on':
401         bootMachine(machine, cdrom)
402     elif action == 'Power off':
403         remctl('destroy', machine.name)
404     elif action == 'Shutdown':
405         remctl('shutdown', machine.name)
406     elif action == 'Delete VM':
407         deleteVM(machine)
408     print time.time()-start_time
409
410     d = dict(user=user,
411              command=action,
412              machine=machine)
413     print Template(file="command.tmpl", searchList=d)
414         
415 def modify(user, fields):
416     machine = testMachineId(user, fields.getfirst('machine_id'))
417     
418
419 def info(user, fields):
420     machine = testMachineId(user, fields.getfirst('machine_id'))
421     status = statusInfo(machine)
422     has_vnc = hasVnc(status)
423     if status is None:
424         main_status = dict(name=machine.name,
425                            memory=str(machine.memory))
426     else:
427         main_status = dict(status[1:])
428     start_time = float(main_status.get('start_time', 0))
429     uptime = datetime.timedelta(seconds=int(time.time()-start_time))
430     cpu_time_float = float(main_status.get('cpu_time', 0))
431     cputime = datetime.timedelta(seconds=int(cpu_time_float))
432     display_fields = """name uptime memory state cpu_weight on_reboot 
433      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
434     display_fields = [('name', 'Name'),
435                       ('owner', 'Owner'),
436                       ('contact', 'Contact'),
437                       'NIC_INFO',
438                       ('uptime', 'uptime'),
439                       ('cputime', 'CPU usage'),
440                       ('memory', 'RAM'),
441                       'DISK_INFO',
442                       ('state', 'state (xen format)'),
443                       ('cpu_weight', 'CPU weight'),
444                       ('on_reboot', 'Action on VM reboot'),
445                       ('on_poweroff', 'Action on VM poweroff'),
446                       ('on_crash', 'Action on VM crash'),
447                       ('on_xend_start', 'Action on Xen start'),
448                       ('on_xend_stop', 'Action on Xen stop'),
449                       ('bootloader', 'Bootloader options'),
450                       ]
451     fields = []
452     machine_info = {}
453     machine_info['owner'] = machine.owner
454     machine_info['contact'] = machine.contact
455
456     nic_fields = getNicInfo(machine_info, machine)
457     nic_point = display_fields.index('NIC_INFO')
458     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
459
460     disk_fields = getDiskInfo(machine_info, machine)
461     disk_point = display_fields.index('DISK_INFO')
462     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
463     
464     main_status['memory'] += ' MB'
465     for field, disp in display_fields:
466         if field in ('uptime', 'cputime'):
467             fields.append((disp, locals()[field]))
468         elif field in main_status:
469             fields.append((disp, main_status[field]))
470         elif field in machine_info:
471             fields.append((disp, machine_info[field]))
472         else:
473             pass
474             #fields.append((disp, None))
475
476     d = dict(user=user,
477              cdroms=CDROM.select(),
478              on=status is not None,
479              machine=machine,
480              has_vnc=has_vnc,
481              uptime=str(uptime),
482              ram=machine.memory,
483              maxmem=maxMemory(user, machine),
484              maxdisk=maxDisk(user, machine),
485              fields = fields)
486     print Template(file='info.tmpl',
487                    searchList=d)
488
489 mapping = dict(list=listVms,
490                vnc=vnc,
491                command=command,
492                modify=modify,
493                info=info,
494                create=create)
495
496 if __name__ == '__main__':
497     start_time = time.time()
498     fields = cgi.FieldStorage()
499     class User:
500         username = "moo"
501         email = 'moo@cow.com'
502     u = User()
503     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
504     operation = os.environ.get('PATH_INFO', '')
505     if not operation:
506         pass
507         #XXX do redirect
508
509     if operation.startswith('/'):
510         operation = operation[1:]
511     if not operation:
512         operation = 'list'
513     
514     fun = mapping.get(operation, 
515                       lambda u, e:
516                           error(operation, u, e,
517                                 "Invalid operation '%s'" % operation))
518     try:
519         fun(u, fields)
520     except MyException, err:
521         error(operation, u, fields, err)