2c6430da6b7a9e25732421184614a261e7466530
[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
15 print 'Content-Type: text/html\n'
16 sys.stderr = sys.stdout
17 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
18
19 from Cheetah.Template import Template
20 from sipb_xen_database import *
21 import random
22
23 class MyException(Exception):
24     pass
25
26 # ... and stolen from xend/uuid.py
27 def randomUUID():
28     """Generate a random UUID."""
29
30     return [ random.randint(0, 255) for _ in range(0, 16) ]
31
32 def uuidToString(u):
33     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
34                      "%02x" * 6]) % tuple(u)
35
36 def maxMemory(user):
37     return 256
38
39 def maxDisk(user):
40     return 10.0
41
42 def haveAccess(user, machine):
43     return True
44
45 def error(op, user, fields, err):
46     d = dict(op=op, user=user, errorMessage=str(err))
47     print Template(file='error.tmpl', searchList=d);
48
49 def validMachineName(name):
50     """Check that name is valid for a machine name"""
51     if not name:
52         return False
53     charset = string.ascii_letters + string.digits + '-_'
54     if name[0] in '-_' or len(name) > 22:
55         return False
56     return all(x in charset for x in name)
57
58 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
59     """Kinit with a given username and keytab"""
60
61     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username])
62     e = p.wait()
63     if e:
64         raise MyException("Error %s in kinit" % e)
65
66 def checkKinit():
67     """If we lack tickets, kinit."""
68     p = subprocess.Popen(['klist', '-s'])
69     if p.wait():
70         kinit()
71
72 def remctl(*args, **kws):
73     """Perform a remctl and return the output.
74
75     kinits if necessary, and outputs errors to stderr.
76     """
77     checkKinit()
78     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
79                          + list(args),
80                          stdout=subprocess.PIPE,
81                          stderr=subprocess.PIPE)
82     if kws.get('err'):
83         return p.stdout.read(), p.stderr.read()
84     if p.wait():
85         print >> sys.stderr, 'ERROR on remctl ', args
86         print >> sys.stderr, p.stderr.read()
87     return p.stdout.read()
88
89 def makeDisks():
90     """Update the lvm partitions to include all disks in the database."""
91     remctl('web', 'lvcreate')
92
93 def bootMachine(machine, cdtype):
94     """Boot a machine with a given boot CD.
95
96     If cdtype is None, give no boot cd.  Otherwise, it is the string
97     id of the CD (e.g. 'gutsy_i386')
98     """
99     if cdtype is not None:
100         remctl('web', 'vmboot', machine.name,
101                cdtype)
102     else:
103         remctl('web', 'vmboot', machine.name)
104
105 def registerMachine(machine):
106     """Register a machine to be controlled by the web interface"""
107     remctl('web', 'register', machine.name)
108
109 def parseStatus(s):
110     """Parse a status string into nested tuples of strings.
111
112     s = output of xm list --long <machine_name>
113     """
114     values = re.split('([()])', s)
115     stack = [[]]
116     for v in values[2:-2]: #remove initial and final '()'
117         if not v:
118             continue
119         v = v.strip()
120         if v == '(':
121             stack.append([])
122         elif v == ')':
123             stack[-2].append(stack[-1])
124             stack.pop()
125         else:
126             if not v:
127                 continue
128             stack[-1].extend(v.split())
129     return stack[-1]
130
131 def statusInfo(machine):
132     value_string, err_string = remctl('list-long', machine.name, err=True)
133     if 'Unknown command' in err_string:
134         raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
135     elif 'does not exist' in err_string:
136         return None
137     elif err_string:
138         raise MyException("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
139     status = parseStatus(value_string)
140     return status
141
142 def hasVnc(status):
143     if status is None:
144         return False
145     for l in status:
146         if l[0] == 'device' and l[1][0] == 'vfb':
147             d = dict(l[1][1:])
148             return 'location' in d
149     return False
150
151 def createVm(user, name, memory, disk, is_hvm, cdrom):
152     # put stuff in the table
153     transaction = ctx.current.create_transaction()
154     try:
155         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
156         id = res.fetchone()[0]
157         machine = Machine()
158         machine.machine_id = id
159         machine.name = name
160         machine.memory = memory
161         machine.owner = user.username
162         machine.contact = user.email
163         machine.uuid = uuidToString(randomUUID())
164         machine.boot_off_cd = True
165         machine_type = Type.get_by(hvm=is_hvm)
166         machine.type_id = machine_type.type_id
167         ctx.current.save(machine)
168         disk = Disk(machine.machine_id, 
169                     'hda', disk)
170         open = NIC.select_by(machine_id=None)
171         if not open: #No IPs left!
172             return "No IP addresses left!  Contact sipb-xen-dev@mit.edu"
173         nic = open[0]
174         nic.machine_id = machine.machine_id
175         nic.hostname = name
176         ctx.current.save(nic)    
177         ctx.current.save(disk)
178         transaction.commit()
179     except:
180         transaction.rollback()
181         raise
182     makeDisks()
183     registerMachine(machine)
184     # tell it to boot with cdrom
185     bootMachine(machine, cdrom)
186
187     return machine
188
189 def create(user, fields):
190     name = fields.getfirst('name')
191     if not validMachineName(name):
192         raise MyException("Invalid name '%s'" % name)
193     name = name.lower()
194
195     if Machine.get_by(name=name):
196         raise MyException("A machine named '%s' already exists" % name)
197     
198     memory = fields.getfirst('memory')
199     try:
200         memory = int(memory)
201         if memory <= 0:
202             raise ValueError
203     except ValueError:
204         raise MyException("Invalid memory amount")
205     if memory > maxMemory(user):
206         raise MyException("Too much memory requested")
207     
208     disk = fields.getfirst('disk')
209     try:
210         disk = float(disk)
211         disk = int(disk * 1024)
212         if disk <= 0:
213             raise ValueError
214     except ValueError:
215         raise MyException("Invalid disk amount")
216     if disk > maxDisk(user):
217         raise MyException("Too much disk requested")
218     
219     vm_type = fields.getfirst('vmtype')
220     if vm_type not in ('hvm', 'paravm'):
221         raise MyException("Invalid vm type '%s'"  % vm_type)    
222     is_hvm = (vm_type == 'hvm')
223
224     cdrom = fields.getfirst('cdrom')
225     if cdrom is not None and not CDROM.get(cdrom):
226         raise MyException("Invalid cdrom type '%s'" % cdrom)    
227     
228     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
229     if isinstance(machine, basestring):
230         raise MyException(machine)
231     d = dict(user=user,
232              machine=machine)
233     print Template(file='create.tmpl',
234                    searchList=d);
235
236 def listVms(user, fields):
237     machines = Machine.select()
238     status = statusInfo(machines)
239     has_vnc = {}
240     for m in machines:
241         on[m.name] = status[m.name] is not None
242         has_vnc[m.name] = hasVnc(status[m.name])
243     d = dict(user=user,
244              maxmem=maxMemory(user),
245              maxdisk=maxDisk(user),
246              machines=machines,
247              status=status,
248              has_vnc=has_vnc,
249              cdroms=CDROM.select())
250     print Template(file='list.tmpl', searchList=d)
251
252 def testMachineId(user, machineId, exists=True):
253     if machineId is None:
254         raise MyException("No machine ID specified")
255     try:
256         machineId = int(machineId)
257     except ValueError:
258         raise MyException("Invalid machine ID '%s'" % machineId)
259     machine = Machine.get(machineId)
260     if exists and machine is None:
261         raise MyException("No such machine ID '%s'" % machineId)
262     if not haveAccess(user, machine):
263         raise MyException("No access to machine ID '%s'" % machineId)
264     return machine
265
266 def vnc(user, fields):
267     """VNC applet page.
268
269     Note that due to same-domain restrictions, the applet connects to
270     the webserver, which needs to forward those requests to the xen
271     server.  The Xen server runs another proxy that (1) authenticates
272     and (2) finds the correct port for the VM.
273
274     You might want iptables like:
275
276     -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 
277     -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 
278     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
279     """
280     machine = testMachineId(user, fields.getfirst('machine_id'))
281     #XXX fix
282     
283     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
284
285     data = {}
286     data["user"] = user
287     data["machine"]=machine
288     data["expires"]=time.time()+(5*60)
289     pickledData = cPickle.dumps(data)
290     m = hmac.new(TOKEN_KEY, digestmod=sha)
291     m.update(pickledData)
292     token = {'data': pickledData, 'digest': m.digest()}
293     token = cPickle.dumps(token)
294     token = base64.urlsafe_b64encode(token)
295     
296     d = dict(user=user,
297              machine=machine,
298              hostname=os.environ.get('SERVER_NAME', 'localhost'),
299              authtoken=token)
300     print Template(file='vnc.tmpl',
301                    searchList=d)
302
303 def info(user, fields):
304     machine = testMachineId(user, fields.getfirst('machine_id'))
305     d = dict(user=user,
306              machine=machine)
307     print Template(file='info.tmpl',
308                    searchList=d)
309
310 mapping = dict(list=listVms,
311                vnc=vnc,
312                info=info,
313                create=create)
314
315 if __name__ == '__main__':
316     fields = cgi.FieldStorage()
317     class C:
318         username = "moo"
319         email = 'moo@cow.com'
320     u = C()
321     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
322     operation = os.environ.get('PATH_INFO', '')
323     if not operation:
324         pass
325         #XXX do redirect
326
327     if operation.startswith('/'):
328         operation = operation[1:]
329     if not operation:
330         operation = 'list'
331     
332     fun = mapping.get(operation, 
333                       lambda u, e:
334                           error(operation, u, e,
335                                 "Invalid operation '%'" % operation))
336     try:
337         fun(u, fields)
338     except MyException, err:
339         error(operation, u, fields, err)