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