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