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')
38 from Cheetah.Template import Template
39 from sipb_xen_database import Machine, CDROM, ctx, connect
41 from webcommon import InvalidInput, CodeError, g
45 """Return HTML code for a (?) link to a specified help topic"""
46 return ('<span class="helplink"><a href="help?subject=' + subj +
47 '&simple=true" target="_blank" ' +
48 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
50 def makeErrorPre(old, addition):
54 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
56 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
58 Template.helppopup = staticmethod(helppopup)
62 """Class to store a dictionary that will be converted to JSON"""
63 def __init__(self, **kws):
71 return simplejson.dumps(self.data)
73 def addError(self, text):
74 """Add stderr text to be displayed on the website."""
76 makeErrorPre(self.data.get('err'), text)
79 """Class to store default values for fields."""
85 def __init__(self, max_memory=None, max_disk=None, **kws):
86 if max_memory is not None:
87 self.memory = min(self.memory, max_memory)
88 if max_disk is not None:
89 self.max_disk = min(self.disk, max_disk)
91 setattr(self, key, kws[key])
95 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
97 def error(op, user, fields, err, emsg):
98 """Print an error page when a CodeError occurs"""
99 d = dict(op=op, user=user, errorMessage=str(err),
101 return Template(file='error.tmpl', searchList=[d])
103 def invalidInput(op, user, fields, err, emsg):
104 """Print an error page when an InvalidInput exception occurs"""
105 d = dict(op=op, user=user, err_field=err.err_field,
106 err_value=str(err.err_value), stderr=emsg,
107 errorMessage=str(err))
108 return Template(file='invalid.tmpl', searchList=[d])
111 """Does the machine with a given status list support VNC?"""
115 if l[0] == 'device' and l[1][0] == 'vfb':
117 return 'location' in d
120 def parseCreate(user, fields):
121 name = fields.getfirst('name')
122 if not validation.validMachineName(name):
123 raise InvalidInput('name', name, 'You must provide a machine name.')
126 if Machine.get_by(name=name):
127 raise InvalidInput('name', name,
128 "Name already exists.")
130 owner = validation.testOwner(user, fields.getfirst('owner'))
132 memory = fields.getfirst('memory')
133 memory = validation.validMemory(user, memory, on=True)
135 disk = fields.getfirst('disk')
136 disk = validation.validDisk(user, disk)
138 vm_type = fields.getfirst('vmtype')
139 if vm_type not in ('hvm', 'paravm'):
140 raise CodeError("Invalid vm type '%s'" % vm_type)
141 is_hvm = (vm_type == 'hvm')
143 cdrom = fields.getfirst('cdrom')
144 if cdrom is not None and not CDROM.get(cdrom):
145 raise CodeError("Invalid cdrom type '%s'" % cdrom)
146 return dict(contact=user, name=name, memory=memory, disk=disk,
147 owner=owner, is_hvm=is_hvm, cdrom=cdrom)
149 def create(user, fields):
150 """Handler for create requests."""
152 parsed_fields = parseCreate(user, fields)
153 machine = controls.createVm(**parsed_fields)
154 except InvalidInput, err:
158 g.clear() #Changed global state
159 d = getListDict(user)
162 for field in fields.keys():
163 setattr(d['defaults'], field, fields.getfirst(field))
165 d['new_machine'] = parsed_fields['name']
166 return Template(file='list.tmpl', searchList=[d])
169 def getListDict(user):
170 machines = [m for m in Machine.select()
171 if validation.haveAccess(user, m)]
176 m.uptime = g.uptimes.get(m)
182 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
183 max_memory = validation.maxMemory(user)
184 max_disk = validation.maxDisk(user)
185 defaults = Defaults(max_memory=max_memory,
190 cant_add_vm=validation.cantAddVm(user),
191 max_memory=max_memory,
197 cdroms=CDROM.select())
200 def listVms(user, fields):
201 """Handler for list requests."""
202 d = getListDict(user)
203 return Template(file='list.tmpl', searchList=[d])
205 def vnc(user, fields):
208 Note that due to same-domain restrictions, the applet connects to
209 the webserver, which needs to forward those requests to the xen
210 server. The Xen server runs another proxy that (1) authenticates
211 and (2) finds the correct port for the VM.
213 You might want iptables like:
215 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
216 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
217 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
218 --dport 10003 -j SNAT --to-source 18.187.7.142
219 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
220 --dport 10003 -j ACCEPT
222 Remember to enable iptables!
223 echo 1 > /proc/sys/net/ipv4/ip_forward
225 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
227 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
231 data["machine"] = machine.name
232 data["expires"] = time.time()+(5*60)
233 pickled_data = cPickle.dumps(data)
234 m = hmac.new(TOKEN_KEY, digestmod=sha)
235 m.update(pickled_data)
236 token = {'data': pickled_data, 'digest': m.digest()}
237 token = cPickle.dumps(token)
238 token = base64.urlsafe_b64encode(token)
240 status = controls.statusInfo(machine)
241 has_vnc = hasVnc(status)
247 hostname=os.environ.get('SERVER_NAME', 'localhost'),
249 return Template(file='vnc.tmpl', searchList=[d])
251 def getNicInfo(data_dict, machine):
252 """Helper function for info, get data on nics for a machine.
254 Modifies data_dict to include the relevant data, and returns a list
255 of (key, name) pairs to display "name: data_dict[key]" to the user.
257 data_dict['num_nics'] = len(machine.nics)
258 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
259 ('nic%s_mac', 'NIC %s MAC Addr'),
260 ('nic%s_ip', 'NIC %s IP'),
263 for i in range(len(machine.nics)):
264 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
266 data_dict['nic%s_hostname' % i] = (machine.name +
267 '.servers.csail.mit.edu')
268 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
269 data_dict['nic%s_ip' % i] = machine.nics[i].ip
270 if len(machine.nics) == 1:
271 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
274 def getDiskInfo(data_dict, machine):
275 """Helper function for info, get data on disks 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_disks'] = len(machine.disks)
281 disk_fields_template = [('%s_size', '%s size')]
283 for disk in machine.disks:
284 name = disk.guest_device_name
285 disk_fields.extend([(x % name, y % name) for x, y in
286 disk_fields_template])
287 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
290 def command(user, fields):
291 """Handler for running commands like boot and delete on a VM."""
292 back = fields.getfirst('back')
294 d = controls.commandResult(user, fields)
295 if d['command'] == 'Delete VM':
297 except InvalidInput, err:
300 print >> sys.stderr, err
305 return Template(file='command.tmpl', searchList=[d])
307 g.clear() #Changed global state
308 d = getListDict(user)
310 return Template(file='list.tmpl', searchList=[d])
312 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
313 d = infoDict(user, machine)
315 return Template(file='info.tmpl', searchList=[d])
317 raise InvalidInput('back', back, 'Not a known back page.')
319 def modifyDict(user, fields):
321 transaction = ctx.current.create_transaction()
323 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
324 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
325 admin = validation.testAdmin(user, fields.getfirst('administrator'),
327 contact = validation.testContact(user, fields.getfirst('contact'),
329 name = validation.testName(user, fields.getfirst('name'), machine)
330 oldname = machine.name
333 memory = fields.getfirst('memory')
334 if memory is not None:
335 memory = validation.validMemory(user, memory, machine, on=False)
336 machine.memory = memory
338 disksize = validation.testDisk(user, fields.getfirst('disk'))
339 if disksize is not None:
340 disksize = validation.validDisk(user, disksize, machine)
341 disk = machine.disks[0]
342 if disk.size != disksize:
343 olddisk[disk.guest_device_name] = disksize
345 ctx.current.save(disk)
347 if owner is not None:
348 machine.owner = owner
351 if admin is not None:
352 machine.administrator = admin
353 if contact is not None:
354 machine.contact = contact
356 ctx.current.save(machine)
359 transaction.rollback()
361 for diskname in olddisk:
362 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
364 controls.renameMachine(machine, oldname, name)
365 return dict(user=user,
369 def modify(user, fields):
370 """Handler for modifying attributes of a machine."""
372 modify_dict = modifyDict(user, fields)
373 except InvalidInput, err:
375 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
377 machine = modify_dict['machine']
380 info_dict = infoDict(user, machine)
381 info_dict['err'] = err
383 for field in fields.keys():
384 setattr(info_dict['defaults'], field, fields.getfirst(field))
385 info_dict['result'] = result
386 return Template(file='info.tmpl', searchList=[info_dict])
389 def helpHandler(user, fields):
390 """Handler for help messages."""
391 simple = fields.getfirst('simple')
392 subjects = fields.getlist('subject')
394 help_mapping = dict(paravm_console="""
395 ParaVM machines do not support console access over VNC. To access
396 these machines, you either need to boot with a liveCD and ssh in or
397 hope that the sipb-xen maintainers add support for serial consoles.""",
399 HVM machines use the virtualization features of the processor, while
400 ParaVM machines use Xen's emulation of virtualization features. You
401 want an HVM virtualized machine.""",
403 Don't ask us! We're as mystified as you are.""",
405 The owner field is used to determine <a
406 href="help?subject=quotas">quotas</a>. It must be the name of a
407 locker that you are an AFS administrator of. In particular, you or an
408 AFS group you are a member of must have AFS rlidwka bits on the
409 locker. You can check see who administers the LOCKER locker using the
410 command 'fs la /mit/LOCKER' on Athena.) See also <a
411 href="help?subject=administrator">administrator</a>.""",
413 The administrator field determines who can access the console and
414 power on and off the machine. This can be either a user or a moira
417 Quotas are determined on a per-locker basis. Each quota may have a
418 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
423 subjects = sorted(help_mapping.keys())
428 mapping=help_mapping)
430 return Template(file="help.tmpl", searchList=[d])
433 def badOperation(u, e):
434 raise CodeError("Unknown operation")
436 def infoDict(user, machine):
437 status = controls.statusInfo(machine)
438 has_vnc = hasVnc(status)
440 main_status = dict(name=machine.name,
441 memory=str(machine.memory))
445 main_status = dict(status[1:])
446 start_time = float(main_status.get('start_time', 0))
447 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
448 cpu_time_float = float(main_status.get('cpu_time', 0))
449 cputime = datetime.timedelta(seconds=int(cpu_time_float))
450 display_fields = """name uptime memory state cpu_weight on_reboot
451 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
452 display_fields = [('name', 'Name'),
454 ('administrator', 'Administrator'),
455 ('contact', 'Contact'),
458 ('uptime', 'uptime'),
459 ('cputime', 'CPU usage'),
462 ('state', 'state (xen format)'),
463 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
464 ('on_reboot', 'Action on VM reboot'),
465 ('on_poweroff', 'Action on VM poweroff'),
466 ('on_crash', 'Action on VM crash'),
467 ('on_xend_start', 'Action on Xen start'),
468 ('on_xend_stop', 'Action on Xen stop'),
469 ('bootloader', 'Bootloader options'),
473 machine_info['name'] = machine.name
474 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
475 machine_info['owner'] = machine.owner
476 machine_info['administrator'] = machine.administrator
477 machine_info['contact'] = machine.contact
479 nic_fields = getNicInfo(machine_info, machine)
480 nic_point = display_fields.index('NIC_INFO')
481 display_fields = (display_fields[:nic_point] + nic_fields +
482 display_fields[nic_point+1:])
484 disk_fields = getDiskInfo(machine_info, machine)
485 disk_point = display_fields.index('DISK_INFO')
486 display_fields = (display_fields[:disk_point] + disk_fields +
487 display_fields[disk_point+1:])
489 main_status['memory'] += ' MiB'
490 for field, disp in display_fields:
491 if field in ('uptime', 'cputime') and locals()[field] is not None:
492 fields.append((disp, locals()[field]))
493 elif field in machine_info:
494 fields.append((disp, machine_info[field]))
495 elif field in main_status:
496 fields.append((disp, main_status[field]))
499 #fields.append((disp, None))
500 max_mem = validation.maxMemory(user, machine)
501 max_disk = validation.maxDisk(user, machine)
502 defaults = Defaults()
503 for name in 'machine_id name administrator owner memory contact'.split():
504 setattr(defaults, name, getattr(machine, name))
505 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
507 cdroms=CDROM.select(),
508 on=status is not None,
516 owner_help=helppopup("owner"),
520 def info(user, fields):
521 """Handler for info on a single VM."""
522 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
523 d = infoDict(user, machine)
524 return Template(file='info.tmpl', searchList=[d])
526 mapping = dict(list=listVms,
534 def printHeaders(headers):
535 for key, value in headers.iteritems():
536 print '%s: %s' % (key, value)
541 """Return the current user based on the SSL environment variables"""
542 if 'SSL_CLIENT_S_DN_Email' in os.environ:
543 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
548 def main(operation, user, fields):
549 fun = mapping.get(operation, badOperation)
551 if fun not in (helpHandler, ):
552 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
554 output = fun(u, fields)
556 headers = dict(DEFAULT_HEADERS)
557 if isinstance(output, tuple):
558 new_headers, output = output
559 headers.update(new_headers)
561 e = revertStandardError()
564 printHeaders(headers)
566 except Exception, err:
567 if not fields.has_key('js'):
568 if isinstance(err, CodeError):
569 print 'Content-Type: text/html\n'
570 e = revertStandardError()
571 print error(operation, u, fields, err, e)
573 if isinstance(err, InvalidInput):
574 print 'Content-Type: text/html\n'
575 e = revertStandardError()
576 print invalidInput(operation, u, fields, err, e)
578 print 'Content-Type: text/plain\n'
579 print 'Uh-oh! We experienced an error.'
580 print 'Please email sipb-xen@mit.edu with the contents of this page.'
582 e = revertStandardError()
587 if __name__ == '__main__':
588 start_time = time.time()
589 fields = cgi.FieldStorage()
592 operation = os.environ.get('PATH_INFO', '')
594 print "Status: 301 Moved Permanently"
595 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
598 if operation.startswith('/'):
599 operation = operation[1:]
603 main(operation, u, fields)