2 """Main CGI script for web interface"""
14 from StringIO import StringIO
17 def revertStandardError():
18 """Move stderr to stdout, and return the contents of the old stderr."""
20 if not isinstance(errio, StringIO):
22 sys.stderr = sys.stdout
27 """Revert stderr to stdout, and print the contents of stderr"""
28 if isinstance(sys.stderr, StringIO):
29 print revertStandardError()
31 if __name__ == '__main__':
33 atexit.register(printError)
34 sys.stderr = StringIO()
36 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
39 from Cheetah.Template import Template
40 from sipb_xen_database import Machine, CDROM, ctx, connect
42 from webcommon import InvalidInput, CodeError, g
47 self.start_time = time.time()
50 def checkpoint(self, s):
51 self.checkpoints.append((s, time.time()))
54 return ('Timing info:\n%s\n' %
55 '\n'.join(['%s: %s' % (d, t - self.start_time) for
56 (d, t) in self.checkpoints]))
58 checkpoint = Checkpoint()
62 """Return HTML code for a (?) link to a specified help topic"""
63 return ('<span class="helplink"><a href="help?subject=' + subj +
64 '&simple=true" target="_blank" ' +
65 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
67 def makeErrorPre(old, addition):
71 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
73 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
75 Template.helppopup = staticmethod(helppopup)
79 """Class to store a dictionary that will be converted to JSON"""
80 def __init__(self, **kws):
88 return simplejson.dumps(self.data)
90 def addError(self, text):
91 """Add stderr text to be displayed on the website."""
93 makeErrorPre(self.data.get('err'), text)
96 """Class to store default values for fields."""
102 def __init__(self, max_memory=None, max_disk=None, **kws):
103 if max_memory is not None:
104 self.memory = min(self.memory, max_memory)
105 if max_disk is not None:
106 self.max_disk = min(self.disk, max_disk)
108 setattr(self, key, kws[key])
112 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
114 def error(op, user, fields, err, emsg):
115 """Print an error page when a CodeError occurs"""
116 d = dict(op=op, user=user, errorMessage=str(err),
118 return templates.error(searchList=[d])
120 def invalidInput(op, user, fields, err, emsg):
121 """Print an error page when an InvalidInput exception occurs"""
122 d = dict(op=op, user=user, err_field=err.err_field,
123 err_value=str(err.err_value), stderr=emsg,
124 errorMessage=str(err))
125 return templates.invalid(searchList=[d])
128 """Does the machine with a given status list support VNC?"""
132 if l[0] == 'device' and l[1][0] == 'vfb':
134 return 'location' in d
137 def parseCreate(user, fields):
138 name = fields.getfirst('name')
139 if not validation.validMachineName(name):
140 raise InvalidInput('name', name, 'You must provide a machine name.')
143 if Machine.get_by(name=name):
144 raise InvalidInput('name', name,
145 "Name already exists.")
147 owner = validation.testOwner(user, fields.getfirst('owner'))
149 memory = fields.getfirst('memory')
150 memory = validation.validMemory(user, memory, on=True)
152 disk = fields.getfirst('disk')
153 disk = validation.validDisk(user, disk)
155 vm_type = fields.getfirst('vmtype')
156 if vm_type not in ('hvm', 'paravm'):
157 raise CodeError("Invalid vm type '%s'" % vm_type)
158 is_hvm = (vm_type == 'hvm')
160 cdrom = fields.getfirst('cdrom')
161 if cdrom is not None and not CDROM.get(cdrom):
162 raise CodeError("Invalid cdrom type '%s'" % cdrom)
163 return dict(contact=user, name=name, memory=memory, disk=disk,
164 owner=owner, is_hvm=is_hvm, cdrom=cdrom)
166 def create(user, fields):
167 """Handler for create requests."""
169 parsed_fields = parseCreate(user, fields)
170 machine = controls.createVm(**parsed_fields)
171 except InvalidInput, err:
175 g.clear() #Changed global state
176 d = getListDict(user)
179 for field in fields.keys():
180 setattr(d['defaults'], field, fields.getfirst(field))
182 d['new_machine'] = parsed_fields['name']
183 return templates.list(searchList=[d])
186 def getListDict(user):
187 machines = [m for m in Machine.select()
188 if validation.haveAccess(user, m)]
189 checkpoint.checkpoint('Got my machines')
193 checkpoint.checkpoint('Got uptimes')
195 m.uptime = g.uptimes.get(m)
201 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
202 max_memory = validation.maxMemory(user)
203 max_disk = validation.maxDisk(user)
204 checkpoint.checkpoint('Got max mem/disk')
205 defaults = Defaults(max_memory=max_memory,
209 checkpoint.checkpoint('Got defaults')
211 cant_add_vm=validation.cantAddVm(user),
212 max_memory=max_memory,
218 cdroms=CDROM.select())
221 def listVms(user, fields):
222 """Handler for list requests."""
223 checkpoint.checkpoint('Getting list dict')
224 d = getListDict(user)
225 checkpoint.checkpoint('Got list dict')
226 return templates.list(searchList=[d])
228 def vnc(user, fields):
231 Note that due to same-domain restrictions, the applet connects to
232 the webserver, which needs to forward those requests to the xen
233 server. The Xen server runs another proxy that (1) authenticates
234 and (2) finds the correct port for the VM.
236 You might want iptables like:
238 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
239 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
240 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
241 --dport 10003 -j SNAT --to-source 18.187.7.142
242 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
243 --dport 10003 -j ACCEPT
245 Remember to enable iptables!
246 echo 1 > /proc/sys/net/ipv4/ip_forward
248 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
250 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
254 data["machine"] = machine.name
255 data["expires"] = time.time()+(5*60)
256 pickled_data = cPickle.dumps(data)
257 m = hmac.new(TOKEN_KEY, digestmod=sha)
258 m.update(pickled_data)
259 token = {'data': pickled_data, 'digest': m.digest()}
260 token = cPickle.dumps(token)
261 token = base64.urlsafe_b64encode(token)
263 status = controls.statusInfo(machine)
264 has_vnc = hasVnc(status)
270 hostname=os.environ.get('SERVER_NAME', 'localhost'),
272 return templates.vnc(searchList=[d])
274 def getNicInfo(data_dict, machine):
275 """Helper function for info, get data on nics for a machine.
277 Modifies data_dict to include the relevant data, and returns a list
278 of (key, name) pairs to display "name: data_dict[key]" to the user.
280 data_dict['num_nics'] = len(machine.nics)
281 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
282 ('nic%s_mac', 'NIC %s MAC Addr'),
283 ('nic%s_ip', 'NIC %s IP'),
286 for i in range(len(machine.nics)):
287 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
289 data_dict['nic%s_hostname' % i] = (machine.name +
290 '.servers.csail.mit.edu')
291 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
292 data_dict['nic%s_ip' % i] = machine.nics[i].ip
293 if len(machine.nics) == 1:
294 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
297 def getDiskInfo(data_dict, machine):
298 """Helper function for info, get data on disks for a machine.
300 Modifies data_dict to include the relevant data, and returns a list
301 of (key, name) pairs to display "name: data_dict[key]" to the user.
303 data_dict['num_disks'] = len(machine.disks)
304 disk_fields_template = [('%s_size', '%s size')]
306 for disk in machine.disks:
307 name = disk.guest_device_name
308 disk_fields.extend([(x % name, y % name) for x, y in
309 disk_fields_template])
310 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
313 def command(user, fields):
314 """Handler for running commands like boot and delete on a VM."""
315 back = fields.getfirst('back')
317 d = controls.commandResult(user, fields)
318 if d['command'] == 'Delete VM':
320 except InvalidInput, err:
323 print >> sys.stderr, err
328 return templates.command(searchList=[d])
330 g.clear() #Changed global state
331 d = getListDict(user)
333 return templates.list(searchList=[d])
335 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
336 d = infoDict(user, machine)
338 return templates.info(searchList=[d])
341 ('back', back, 'Not a known back page.')
343 def modifyDict(user, fields):
345 transaction = ctx.current.create_transaction()
347 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
348 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
349 admin = validation.testAdmin(user, fields.getfirst('administrator'),
351 contact = validation.testContact(user, fields.getfirst('contact'),
353 name = validation.testName(user, fields.getfirst('name'), machine)
354 oldname = machine.name
357 memory = fields.getfirst('memory')
358 if memory is not None:
359 memory = validation.validMemory(user, memory, machine, on=False)
360 machine.memory = memory
362 disksize = validation.testDisk(user, fields.getfirst('disk'))
363 if disksize is not None:
364 disksize = validation.validDisk(user, disksize, machine)
365 disk = machine.disks[0]
366 if disk.size != disksize:
367 olddisk[disk.guest_device_name] = disksize
369 ctx.current.save(disk)
371 if owner is not None:
372 machine.owner = owner
375 if admin is not None:
376 machine.administrator = admin
377 if contact is not None:
378 machine.contact = contact
380 ctx.current.save(machine)
383 transaction.rollback()
385 for diskname in olddisk:
386 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
388 controls.renameMachine(machine, oldname, name)
389 return dict(user=user,
393 def modify(user, fields):
394 """Handler for modifying attributes of a machine."""
396 modify_dict = modifyDict(user, fields)
397 except InvalidInput, err:
399 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
401 machine = modify_dict['machine']
404 info_dict = infoDict(user, machine)
405 info_dict['err'] = err
407 for field in fields.keys():
408 setattr(info_dict['defaults'], field, fields.getfirst(field))
409 info_dict['result'] = result
410 return templates.info(searchList=[info_dict])
413 def helpHandler(user, fields):
414 """Handler for help messages."""
415 simple = fields.getfirst('simple')
416 subjects = fields.getlist('subject')
418 help_mapping = dict(paravm_console="""
419 ParaVM machines do not support console access over VNC. To access
420 these machines, you either need to boot with a liveCD and ssh in or
421 hope that the sipb-xen maintainers add support for serial consoles.""",
423 HVM machines use the virtualization features of the processor, while
424 ParaVM machines use Xen's emulation of virtualization features. You
425 want an HVM virtualized machine.""",
427 Don't ask us! We're as mystified as you are.""",
429 The owner field is used to determine <a
430 href="help?subject=quotas">quotas</a>. It must be the name of a
431 locker that you are an AFS administrator of. In particular, you or an
432 AFS group you are a member of must have AFS rlidwka bits on the
433 locker. You can check see who administers the LOCKER locker using the
434 command 'fs la /mit/LOCKER' on Athena.) See also <a
435 href="help?subject=administrator">administrator</a>.""",
437 The administrator field determines who can access the console and
438 power on and off the machine. This can be either a user or a moira
441 Quotas are determined on a per-locker basis. Each quota may have a
442 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
447 subjects = sorted(help_mapping.keys())
452 mapping=help_mapping)
454 return templates.help(searchList=[d])
457 def badOperation(u, e):
458 raise CodeError("Unknown operation")
460 def infoDict(user, machine):
461 status = controls.statusInfo(machine)
462 checkpoint.checkpoint('Getting status info')
463 has_vnc = hasVnc(status)
465 main_status = dict(name=machine.name,
466 memory=str(machine.memory))
470 main_status = dict(status[1:])
471 start_time = float(main_status.get('start_time', 0))
472 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
473 cpu_time_float = float(main_status.get('cpu_time', 0))
474 cputime = datetime.timedelta(seconds=int(cpu_time_float))
475 checkpoint.checkpoint('Status')
476 display_fields = """name uptime memory state cpu_weight on_reboot
477 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
478 display_fields = [('name', 'Name'),
480 ('administrator', 'Administrator'),
481 ('contact', 'Contact'),
484 ('uptime', 'uptime'),
485 ('cputime', 'CPU usage'),
488 ('state', 'state (xen format)'),
489 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
490 ('on_reboot', 'Action on VM reboot'),
491 ('on_poweroff', 'Action on VM poweroff'),
492 ('on_crash', 'Action on VM crash'),
493 ('on_xend_start', 'Action on Xen start'),
494 ('on_xend_stop', 'Action on Xen stop'),
495 ('bootloader', 'Bootloader options'),
499 machine_info['name'] = machine.name
500 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
501 machine_info['owner'] = machine.owner
502 machine_info['administrator'] = machine.administrator
503 machine_info['contact'] = machine.contact
505 nic_fields = getNicInfo(machine_info, machine)
506 nic_point = display_fields.index('NIC_INFO')
507 display_fields = (display_fields[:nic_point] + nic_fields +
508 display_fields[nic_point+1:])
510 disk_fields = getDiskInfo(machine_info, machine)
511 disk_point = display_fields.index('DISK_INFO')
512 display_fields = (display_fields[:disk_point] + disk_fields +
513 display_fields[disk_point+1:])
515 main_status['memory'] += ' MiB'
516 for field, disp in display_fields:
517 if field in ('uptime', 'cputime') and locals()[field] is not None:
518 fields.append((disp, locals()[field]))
519 elif field in machine_info:
520 fields.append((disp, machine_info[field]))
521 elif field in main_status:
522 fields.append((disp, main_status[field]))
525 #fields.append((disp, None))
527 checkpoint.checkpoint('Got fields')
530 max_mem = validation.maxMemory(user, machine, False)
531 checkpoint.checkpoint('Got mem')
532 max_disk = validation.maxDisk(user, machine)
533 defaults = Defaults()
534 for name in 'machine_id name administrator owner memory contact'.split():
535 setattr(defaults, name, getattr(machine, name))
536 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
537 checkpoint.checkpoint('Got defaults')
539 cdroms=CDROM.select(),
540 on=status is not None,
548 owner_help=helppopup("owner"),
552 def info(user, fields):
553 """Handler for info on a single VM."""
554 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
555 d = infoDict(user, machine)
556 checkpoint.checkpoint('Got infodict')
557 return templates.info(searchList=[d])
559 mapping = dict(list=listVms,
567 def printHeaders(headers):
568 for key, value in headers.iteritems():
569 print '%s: %s' % (key, value)
574 """Return the current user based on the SSL environment variables"""
575 if 'SSL_CLIENT_S_DN_Email' in os.environ:
576 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
581 def main(operation, user, fields):
582 start_time = time.time()
583 fun = mapping.get(operation, badOperation)
585 if fun not in (helpHandler, ):
586 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
588 checkpoint.checkpoint('Before')
589 output = fun(u, fields)
590 checkpoint.checkpoint('After')
592 headers = dict(DEFAULT_HEADERS)
593 if isinstance(output, tuple):
594 new_headers, output = output
595 headers.update(new_headers)
596 e = revertStandardError()
599 printHeaders(headers)
600 output_string = str(output)
601 checkpoint.checkpoint('output as a string')
603 print '<pre>%s</pre>' % checkpoint
604 except Exception, err:
605 if not fields.has_key('js'):
606 if isinstance(err, CodeError):
607 print 'Content-Type: text/html\n'
608 e = revertStandardError()
609 print error(operation, u, fields, err, e)
611 if isinstance(err, InvalidInput):
612 print 'Content-Type: text/html\n'
613 e = revertStandardError()
614 print invalidInput(operation, u, fields, err, e)
616 print 'Content-Type: text/plain\n'
617 print 'Uh-oh! We experienced an error.'
618 print 'Please email sipb-xen@mit.edu with the contents of this page.'
620 e = revertStandardError()
625 if __name__ == '__main__':
626 fields = cgi.FieldStorage()
629 operation = os.environ.get('PATH_INFO', '')
631 print "Status: 301 Moved Permanently"
632 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
635 if operation.startswith('/'):
636 operation = operation[1:]
640 #main(operation, u, fields)
642 profile.run('main(operation, u, fields)', 'log-'+operation)