2 """Main CGI script for web interface"""
14 from StringIO import StringIO
16 def revertStandardError():
17 """Move stderr to stdout, and return the contents of the old stderr."""
19 if not isinstance(errio, StringIO):
21 sys.stderr = sys.stdout
26 """Revert stderr to stdout, and print the contents of stderr"""
27 if isinstance(sys.stderr, StringIO):
28 print revertStandardError()
30 if __name__ == '__main__':
32 atexit.register(printError)
33 sys.stderr = StringIO()
35 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
38 from Cheetah.Template import Template
39 from sipb_xen_database import Machine, CDROM, ctx, connect
41 from webcommon import InvalidInput, CodeError, g
46 self.start_time = time.time()
49 def checkpoint(self, s):
50 self.checkpoints.append((s, time.time()))
53 return ('Timing info:\n%s\n' %
54 '\n'.join(['%s: %s' % (d, t - self.start_time) for
55 (d, t) in self.checkpoints]))
57 checkpoint = Checkpoint()
61 """Return HTML code for a (?) link to a specified help topic"""
62 return ('<span class="helplink"><a href="help?subject=' + subj +
63 '&simple=true" target="_blank" ' +
64 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
66 def makeErrorPre(old, addition):
70 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
72 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
74 Template.helppopup = staticmethod(helppopup)
78 """Class to store a dictionary that will be converted to JSON"""
79 def __init__(self, **kws):
87 return simplejson.dumps(self.data)
89 def addError(self, text):
90 """Add stderr text to be displayed on the website."""
92 makeErrorPre(self.data.get('err'), text)
95 """Class to store default values for fields."""
101 def __init__(self, max_memory=None, max_disk=None, **kws):
102 if max_memory is not None:
103 self.memory = min(self.memory, max_memory)
104 if max_disk is not None:
105 self.max_disk = min(self.disk, max_disk)
107 setattr(self, key, kws[key])
111 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
113 def error(op, user, fields, err, emsg):
114 """Print an error page when a CodeError occurs"""
115 d = dict(op=op, user=user, errorMessage=str(err),
117 return templates.error(searchList=[d])
119 def invalidInput(op, user, fields, err, emsg):
120 """Print an error page when an InvalidInput exception occurs"""
121 d = dict(op=op, user=user, err_field=err.err_field,
122 err_value=str(err.err_value), stderr=emsg,
123 errorMessage=str(err))
124 return templates.invalid(searchList=[d])
127 """Does the machine with a given status list support VNC?"""
131 if l[0] == 'device' and l[1][0] == 'vfb':
133 return 'location' in d
136 def parseCreate(user, fields):
137 name = fields.getfirst('name')
138 if not validation.validMachineName(name):
139 raise InvalidInput('name', name, 'You must provide a machine name.')
142 if Machine.get_by(name=name):
143 raise InvalidInput('name', name,
144 "Name already exists.")
146 owner = validation.testOwner(user, fields.getfirst('owner'))
148 memory = fields.getfirst('memory')
149 memory = validation.validMemory(user, memory, on=True)
151 disk_size = fields.getfirst('disk')
152 disk_size = validation.validDisk(user, disk_size)
154 vm_type = fields.getfirst('vmtype')
155 if vm_type not in ('hvm', 'paravm'):
156 raise CodeError("Invalid vm type '%s'" % vm_type)
157 is_hvm = (vm_type == 'hvm')
159 cdrom = fields.getfirst('cdrom')
160 if cdrom is not None and not CDROM.get(cdrom):
161 raise CodeError("Invalid cdrom type '%s'" % cdrom)
162 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
163 owner=owner, is_hvm=is_hvm, cdrom=cdrom)
165 def create(user, fields):
166 """Handler for create requests."""
168 parsed_fields = parseCreate(user, fields)
169 machine = controls.createVm(**parsed_fields)
170 except InvalidInput, err:
174 g.clear() #Changed global state
175 d = getListDict(user)
178 for field in fields.keys():
179 setattr(d['defaults'], field, fields.getfirst(field))
181 d['new_machine'] = parsed_fields['name']
182 return templates.list(searchList=[d])
185 def getListDict(user):
186 machines = [m for m in Machine.select()
187 if validation.haveAccess(user, m)]
189 # machines = Machine.select()
191 # machines = Machine.query().join('users').filter_by(user=user).all()
192 checkpoint.checkpoint('Got my machines')
196 checkpoint.checkpoint('Got uptimes')
198 m.uptime = g.uptimes.get(m)
204 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
205 max_memory = validation.maxMemory(user)
206 max_disk = validation.maxDisk(user)
207 checkpoint.checkpoint('Got max mem/disk')
208 defaults = Defaults(max_memory=max_memory,
212 checkpoint.checkpoint('Got defaults')
214 cant_add_vm=validation.cantAddVm(user),
215 max_memory=max_memory,
221 cdroms=CDROM.select())
224 def listVms(user, fields):
225 """Handler for list requests."""
226 checkpoint.checkpoint('Getting list dict')
227 d = getListDict(user)
228 checkpoint.checkpoint('Got list dict')
229 return templates.list(searchList=[d])
231 def vnc(user, fields):
234 Note that due to same-domain restrictions, the applet connects to
235 the webserver, which needs to forward those requests to the xen
236 server. The Xen server runs another proxy that (1) authenticates
237 and (2) finds the correct port for the VM.
239 You might want iptables like:
241 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
242 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
243 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
244 --dport 10003 -j SNAT --to-source 18.187.7.142
245 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
246 --dport 10003 -j ACCEPT
248 Remember to enable iptables!
249 echo 1 > /proc/sys/net/ipv4/ip_forward
251 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
253 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
257 data["machine"] = machine.name
258 data["expires"] = time.time()+(5*60)
259 pickled_data = cPickle.dumps(data)
260 m = hmac.new(TOKEN_KEY, digestmod=sha)
261 m.update(pickled_data)
262 token = {'data': pickled_data, 'digest': m.digest()}
263 token = cPickle.dumps(token)
264 token = base64.urlsafe_b64encode(token)
266 status = controls.statusInfo(machine)
267 has_vnc = hasVnc(status)
273 hostname=os.environ.get('SERVER_NAME', 'localhost'),
275 return templates.vnc(searchList=[d])
277 def getNicInfo(data_dict, machine):
278 """Helper function for info, get data on nics for a machine.
280 Modifies data_dict to include the relevant data, and returns a list
281 of (key, name) pairs to display "name: data_dict[key]" to the user.
283 data_dict['num_nics'] = len(machine.nics)
284 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
285 ('nic%s_mac', 'NIC %s MAC Addr'),
286 ('nic%s_ip', 'NIC %s IP'),
289 for i in range(len(machine.nics)):
290 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
292 data_dict['nic%s_hostname' % i] = (machine.name +
293 '.servers.csail.mit.edu')
294 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
295 data_dict['nic%s_ip' % i] = machine.nics[i].ip
296 if len(machine.nics) == 1:
297 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
300 def getDiskInfo(data_dict, machine):
301 """Helper function for info, get data on disks for a machine.
303 Modifies data_dict to include the relevant data, and returns a list
304 of (key, name) pairs to display "name: data_dict[key]" to the user.
306 data_dict['num_disks'] = len(machine.disks)
307 disk_fields_template = [('%s_size', '%s size')]
309 for disk in machine.disks:
310 name = disk.guest_device_name
311 disk_fields.extend([(x % name, y % name) for x, y in
312 disk_fields_template])
313 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
316 def command(user, fields):
317 """Handler for running commands like boot and delete on a VM."""
318 back = fields.getfirst('back')
320 d = controls.commandResult(user, fields)
321 if d['command'] == 'Delete VM':
323 except InvalidInput, err:
326 print >> sys.stderr, err
331 return templates.command(searchList=[d])
333 g.clear() #Changed global state
334 d = getListDict(user)
336 return templates.list(searchList=[d])
338 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
339 d = infoDict(user, machine)
341 return templates.info(searchList=[d])
344 ('back', back, 'Not a known back page.')
346 def modifyDict(user, fields):
348 transaction = ctx.current.create_transaction()
350 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
351 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
352 admin = validation.testAdmin(user, fields.getfirst('administrator'),
354 contact = validation.testContact(user, fields.getfirst('contact'),
356 name = validation.testName(user, fields.getfirst('name'), machine)
357 oldname = machine.name
360 memory = fields.getfirst('memory')
361 if memory is not None:
362 memory = validation.validMemory(user, memory, machine, on=False)
363 machine.memory = memory
365 disksize = validation.testDisk(user, fields.getfirst('disk'))
366 if disksize is not None:
367 disksize = validation.validDisk(user, disksize, machine)
368 disk = machine.disks[0]
369 if disk.size != disksize:
370 olddisk[disk.guest_device_name] = disksize
372 ctx.current.save(disk)
374 if owner is not None:
375 machine.owner = owner
378 if admin is not None:
379 machine.administrator = admin
380 if contact is not None:
381 machine.contact = contact
383 ctx.current.save(machine)
386 transaction.rollback()
388 for diskname in olddisk:
389 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
391 controls.renameMachine(machine, oldname, name)
392 return dict(user=user,
396 def modify(user, fields):
397 """Handler for modifying attributes of a machine."""
399 modify_dict = modifyDict(user, fields)
400 except InvalidInput, err:
402 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
404 machine = modify_dict['machine']
407 info_dict = infoDict(user, machine)
408 info_dict['err'] = err
410 for field in fields.keys():
411 setattr(info_dict['defaults'], field, fields.getfirst(field))
412 info_dict['result'] = result
413 return templates.info(searchList=[info_dict])
416 def helpHandler(user, fields):
417 """Handler for help messages."""
418 simple = fields.getfirst('simple')
419 subjects = fields.getlist('subject')
421 help_mapping = dict(paravm_console="""
422 ParaVM machines do not support console access over VNC. To access
423 these machines, you either need to boot with a liveCD and ssh in or
424 hope that the sipb-xen maintainers add support for serial consoles.""",
426 HVM machines use the virtualization features of the processor, while
427 ParaVM machines use Xen's emulation of virtualization features. You
428 want an HVM virtualized machine.""",
430 Don't ask us! We're as mystified as you are.""",
432 The owner field is used to determine <a
433 href="help?subject=quotas">quotas</a>. It must be the name of a
434 locker that you are an AFS administrator of. In particular, you or an
435 AFS group you are a member of must have AFS rlidwka bits on the
436 locker. You can check see who administers the LOCKER locker using the
437 command 'fs la /mit/LOCKER' on Athena.) See also <a
438 href="help?subject=administrator">administrator</a>.""",
440 The administrator field determines who can access the console and
441 power on and off the machine. This can be either a user or a moira
444 Quotas are determined on a per-locker basis. Each quota may have a
445 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
450 subjects = sorted(help_mapping.keys())
455 mapping=help_mapping)
457 return templates.help(searchList=[d])
460 def badOperation(u, e):
461 raise CodeError("Unknown operation")
463 def infoDict(user, machine):
464 status = controls.statusInfo(machine)
465 checkpoint.checkpoint('Getting status info')
466 has_vnc = hasVnc(status)
468 main_status = dict(name=machine.name,
469 memory=str(machine.memory))
473 main_status = dict(status[1:])
474 start_time = float(main_status.get('start_time', 0))
475 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
476 cpu_time_float = float(main_status.get('cpu_time', 0))
477 cputime = datetime.timedelta(seconds=int(cpu_time_float))
478 checkpoint.checkpoint('Status')
479 display_fields = """name uptime memory state cpu_weight on_reboot
480 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
481 display_fields = [('name', 'Name'),
483 ('administrator', 'Administrator'),
484 ('contact', 'Contact'),
487 ('uptime', 'uptime'),
488 ('cputime', 'CPU usage'),
491 ('state', 'state (xen format)'),
492 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
493 ('on_reboot', 'Action on VM reboot'),
494 ('on_poweroff', 'Action on VM poweroff'),
495 ('on_crash', 'Action on VM crash'),
496 ('on_xend_start', 'Action on Xen start'),
497 ('on_xend_stop', 'Action on Xen stop'),
498 ('bootloader', 'Bootloader options'),
502 machine_info['name'] = machine.name
503 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
504 machine_info['owner'] = machine.owner
505 machine_info['administrator'] = machine.administrator
506 machine_info['contact'] = machine.contact
508 nic_fields = getNicInfo(machine_info, machine)
509 nic_point = display_fields.index('NIC_INFO')
510 display_fields = (display_fields[:nic_point] + nic_fields +
511 display_fields[nic_point+1:])
513 disk_fields = getDiskInfo(machine_info, machine)
514 disk_point = display_fields.index('DISK_INFO')
515 display_fields = (display_fields[:disk_point] + disk_fields +
516 display_fields[disk_point+1:])
518 main_status['memory'] += ' MiB'
519 for field, disp in display_fields:
520 if field in ('uptime', 'cputime') and locals()[field] is not None:
521 fields.append((disp, locals()[field]))
522 elif field in machine_info:
523 fields.append((disp, machine_info[field]))
524 elif field in main_status:
525 fields.append((disp, main_status[field]))
528 #fields.append((disp, None))
530 checkpoint.checkpoint('Got fields')
533 max_mem = validation.maxMemory(user, machine, False)
534 checkpoint.checkpoint('Got mem')
535 max_disk = validation.maxDisk(user, machine)
536 defaults = Defaults()
537 for name in 'machine_id name administrator owner memory contact'.split():
538 setattr(defaults, name, getattr(machine, name))
539 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
540 checkpoint.checkpoint('Got defaults')
542 cdroms=CDROM.select(),
543 on=status is not None,
551 owner_help=helppopup("owner"),
555 def info(user, fields):
556 """Handler for info on a single VM."""
557 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
558 d = infoDict(user, machine)
559 checkpoint.checkpoint('Got infodict')
560 return templates.info(searchList=[d])
562 mapping = dict(list=listVms,
570 def printHeaders(headers):
571 for key, value in headers.iteritems():
572 print '%s: %s' % (key, value)
577 """Return the current user based on the SSL environment variables"""
578 if 'SSL_CLIENT_S_DN_Email' in os.environ:
579 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
584 def main(operation, user, fields):
585 start_time = time.time()
586 fun = mapping.get(operation, badOperation)
588 if fun not in (helpHandler, ):
589 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
591 checkpoint.checkpoint('Before')
592 output = fun(u, fields)
593 checkpoint.checkpoint('After')
595 headers = dict(DEFAULT_HEADERS)
596 if isinstance(output, tuple):
597 new_headers, output = output
598 headers.update(new_headers)
599 e = revertStandardError()
602 printHeaders(headers)
603 output_string = str(output)
604 checkpoint.checkpoint('output as a string')
606 print '<pre>%s</pre>' % checkpoint
607 except Exception, err:
608 if not fields.has_key('js'):
609 if isinstance(err, CodeError):
610 print 'Content-Type: text/html\n'
611 e = revertStandardError()
612 print error(operation, u, fields, err, e)
614 if isinstance(err, InvalidInput):
615 print 'Content-Type: text/html\n'
616 e = revertStandardError()
617 print invalidInput(operation, u, fields, err, e)
619 print 'Content-Type: text/plain\n'
620 print 'Uh-oh! We experienced an error.'
621 print 'Please email sipb-xen@mit.edu with the contents of this page.'
623 e = revertStandardError()
628 if __name__ == '__main__':
629 fields = cgi.FieldStorage()
632 operation = os.environ.get('PATH_INFO', '')
634 print "Status: 301 Moved Permanently"
635 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
638 if operation.startswith('/'):
639 operation = operation[1:]
643 if os.getenv("SIPB_XEN_PROFILE"):
645 profile.run('main(operation, u, fields)', 'log-'+operation)
647 main(operation, u, fields)