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, MachineAccess
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(owner, memory, on=True)
151 disk_size = fields.getfirst('disk')
152 disk_size = validation.validDisk(owner, 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 = g.machines
187 checkpoint.checkpoint('Got my machines')
191 checkpoint.checkpoint('Got uptimes')
193 m.uptime = g.uptimes.get(m)
199 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
200 max_memory = validation.maxMemory(user)
201 max_disk = validation.maxDisk(user)
202 checkpoint.checkpoint('Got max mem/disk')
203 defaults = Defaults(max_memory=max_memory,
207 checkpoint.checkpoint('Got defaults')
209 cant_add_vm=validation.cantAddVm(user),
210 max_memory=max_memory,
216 cdroms=CDROM.select())
219 def listVms(user, fields):
220 """Handler for list requests."""
221 checkpoint.checkpoint('Getting list dict')
222 d = getListDict(user)
223 checkpoint.checkpoint('Got list dict')
224 return templates.list(searchList=[d])
226 def vnc(user, fields):
229 Note that due to same-domain restrictions, the applet connects to
230 the webserver, which needs to forward those requests to the xen
231 server. The Xen server runs another proxy that (1) authenticates
232 and (2) finds the correct port for the VM.
234 You might want iptables like:
236 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
237 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
238 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
239 --dport 10003 -j SNAT --to-source 18.187.7.142
240 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241 --dport 10003 -j ACCEPT
243 Remember to enable iptables!
244 echo 1 > /proc/sys/net/ipv4/ip_forward
246 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
248 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
252 data["machine"] = machine.name
253 data["expires"] = time.time()+(5*60)
254 pickled_data = cPickle.dumps(data)
255 m = hmac.new(TOKEN_KEY, digestmod=sha)
256 m.update(pickled_data)
257 token = {'data': pickled_data, 'digest': m.digest()}
258 token = cPickle.dumps(token)
259 token = base64.urlsafe_b64encode(token)
261 status = controls.statusInfo(machine)
262 has_vnc = hasVnc(status)
268 hostname=os.environ.get('SERVER_NAME', 'localhost'),
270 return templates.vnc(searchList=[d])
272 def getHostname(nic):
273 if nic.hostname and '.' in nic.hostname:
276 return nic.machine.name + '.servers.csail.mit.edu'
281 def getNicInfo(data_dict, machine):
282 """Helper function for info, get data on nics for a machine.
284 Modifies data_dict to include the relevant data, and returns a list
285 of (key, name) pairs to display "name: data_dict[key]" to the user.
287 data_dict['num_nics'] = len(machine.nics)
288 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
289 ('nic%s_mac', 'NIC %s MAC Addr'),
290 ('nic%s_ip', 'NIC %s IP'),
293 for i in range(len(machine.nics)):
294 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
296 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
297 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
298 data_dict['nic%s_ip' % i] = machine.nics[i].ip
299 if len(machine.nics) == 1:
300 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
303 def getDiskInfo(data_dict, machine):
304 """Helper function for info, get data on disks for a machine.
306 Modifies data_dict to include the relevant data, and returns a list
307 of (key, name) pairs to display "name: data_dict[key]" to the user.
309 data_dict['num_disks'] = len(machine.disks)
310 disk_fields_template = [('%s_size', '%s size')]
312 for disk in machine.disks:
313 name = disk.guest_device_name
314 disk_fields.extend([(x % name, y % name) for x, y in
315 disk_fields_template])
316 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
319 def command(user, fields):
320 """Handler for running commands like boot and delete on a VM."""
321 back = fields.getfirst('back')
323 d = controls.commandResult(user, fields)
324 if d['command'] == 'Delete VM':
326 except InvalidInput, err:
329 #print >> sys.stderr, err
334 return templates.command(searchList=[d])
336 g.clear() #Changed global state
337 d = getListDict(user)
339 return templates.list(searchList=[d])
341 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
342 d = infoDict(user, machine)
344 return templates.info(searchList=[d])
346 raise InvalidInput('back', back, 'Not a known back page.')
348 def modifyDict(user, fields):
350 transaction = ctx.current.create_transaction()
352 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
353 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
354 admin = validation.testAdmin(user, fields.getfirst('administrator'),
356 contact = validation.testContact(user, fields.getfirst('contact'),
358 name = validation.testName(user, fields.getfirst('name'), machine)
359 oldname = machine.name
362 memory = fields.getfirst('memory')
363 if memory is not None:
364 memory = validation.validMemory(user, memory, machine, on=False)
365 machine.memory = memory
367 disksize = validation.testDisk(user, fields.getfirst('disk'))
368 if disksize is not None:
369 disksize = validation.validDisk(user, disksize, machine)
370 disk = machine.disks[0]
371 if disk.size != disksize:
372 olddisk[disk.guest_device_name] = disksize
374 ctx.current.save(disk)
376 if owner is not None:
377 machine.owner = owner
380 if admin is not None:
381 machine.administrator = admin
382 if contact is not None:
383 machine.contact = contact
385 ctx.current.save(machine)
388 transaction.rollback()
390 for diskname in olddisk:
391 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
393 controls.renameMachine(machine, oldname, name)
394 return dict(user=user,
398 def modify(user, fields):
399 """Handler for modifying attributes of a machine."""
401 modify_dict = modifyDict(user, fields)
402 except InvalidInput, err:
404 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
406 machine = modify_dict['machine']
409 info_dict = infoDict(user, machine)
410 info_dict['err'] = err
412 for field in fields.keys():
413 setattr(info_dict['defaults'], field, fields.getfirst(field))
414 info_dict['result'] = result
415 return templates.info(searchList=[info_dict])
418 def helpHandler(user, fields):
419 """Handler for help messages."""
420 simple = fields.getfirst('simple')
421 subjects = fields.getlist('subject')
423 help_mapping = dict(paravm_console="""
424 ParaVM machines do not support console access over VNC. To access
425 these machines, you either need to boot with a liveCD and ssh in or
426 hope that the sipb-xen maintainers add support for serial consoles.""",
428 HVM machines use the virtualization features of the processor, while
429 ParaVM machines use Xen's emulation of virtualization features. You
430 want an HVM virtualized machine.""",
432 Don't ask us! We're as mystified as you are.""",
434 The owner field is used to determine <a
435 href="help?subject=quotas">quotas</a>. It must be the name of a
436 locker that you are an AFS administrator of. In particular, you or an
437 AFS group you are a member of must have AFS rlidwka bits on the
438 locker. You can check see who administers the LOCKER locker using the
439 command 'fs la /mit/LOCKER' on Athena.) See also <a
440 href="help?subject=administrator">administrator</a>.""",
442 The administrator field determines who can access the console and
443 power on and off the machine. This can be either a user or a moira
446 Quotas are determined on a per-locker basis. Each quota may have a
447 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
452 subjects = sorted(help_mapping.keys())
457 mapping=help_mapping)
459 return templates.help(searchList=[d])
462 def badOperation(u, e):
463 raise CodeError("Unknown operation")
465 def infoDict(user, machine):
466 status = controls.statusInfo(machine)
467 checkpoint.checkpoint('Getting status info')
468 has_vnc = hasVnc(status)
470 main_status = dict(name=machine.name,
471 memory=str(machine.memory))
475 main_status = dict(status[1:])
476 start_time = float(main_status.get('start_time', 0))
477 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
478 cpu_time_float = float(main_status.get('cpu_time', 0))
479 cputime = datetime.timedelta(seconds=int(cpu_time_float))
480 checkpoint.checkpoint('Status')
481 display_fields = """name uptime memory state cpu_weight on_reboot
482 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
483 display_fields = [('name', 'Name'),
485 ('administrator', 'Administrator'),
486 ('contact', 'Contact'),
489 ('uptime', 'uptime'),
490 ('cputime', 'CPU usage'),
493 ('state', 'state (xen format)'),
494 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
495 ('on_reboot', 'Action on VM reboot'),
496 ('on_poweroff', 'Action on VM poweroff'),
497 ('on_crash', 'Action on VM crash'),
498 ('on_xend_start', 'Action on Xen start'),
499 ('on_xend_stop', 'Action on Xen stop'),
500 ('bootloader', 'Bootloader options'),
504 machine_info['name'] = machine.name
505 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
506 machine_info['owner'] = machine.owner
507 machine_info['administrator'] = machine.administrator
508 machine_info['contact'] = machine.contact
510 nic_fields = getNicInfo(machine_info, machine)
511 nic_point = display_fields.index('NIC_INFO')
512 display_fields = (display_fields[:nic_point] + nic_fields +
513 display_fields[nic_point+1:])
515 disk_fields = getDiskInfo(machine_info, machine)
516 disk_point = display_fields.index('DISK_INFO')
517 display_fields = (display_fields[:disk_point] + disk_fields +
518 display_fields[disk_point+1:])
520 main_status['memory'] += ' MiB'
521 for field, disp in display_fields:
522 if field in ('uptime', 'cputime') and locals()[field] is not None:
523 fields.append((disp, locals()[field]))
524 elif field in machine_info:
525 fields.append((disp, machine_info[field]))
526 elif field in main_status:
527 fields.append((disp, main_status[field]))
530 #fields.append((disp, None))
532 checkpoint.checkpoint('Got fields')
535 max_mem = validation.maxMemory(user, machine, False)
536 checkpoint.checkpoint('Got mem')
537 max_disk = validation.maxDisk(user, machine)
538 defaults = Defaults()
539 for name in 'machine_id name administrator owner memory contact'.split():
540 setattr(defaults, name, getattr(machine, name))
541 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
542 checkpoint.checkpoint('Got defaults')
544 cdroms=CDROM.select(),
545 on=status is not None,
553 owner_help=helppopup("owner"),
557 def info(user, fields):
558 """Handler for info on a single VM."""
559 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
560 d = infoDict(user, machine)
561 checkpoint.checkpoint('Got infodict')
562 return templates.info(searchList=[d])
564 mapping = dict(list=listVms,
572 def printHeaders(headers):
573 for key, value in headers.iteritems():
574 print '%s: %s' % (key, value)
579 """Return the current user based on the SSL environment variables"""
580 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
583 def main(operation, user, fields):
584 start_time = time.time()
585 fun = mapping.get(operation, badOperation)
587 if fun not in (helpHandler, ):
588 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
590 checkpoint.checkpoint('Before')
591 output = fun(u, fields)
592 checkpoint.checkpoint('After')
594 headers = dict(DEFAULT_HEADERS)
595 if isinstance(output, tuple):
596 new_headers, output = output
597 headers.update(new_headers)
598 e = revertStandardError()
601 printHeaders(headers)
602 output_string = str(output)
603 checkpoint.checkpoint('output as a string')
605 print '<pre>%s</pre>' % checkpoint
606 except Exception, err:
607 if not fields.has_key('js'):
608 if isinstance(err, CodeError):
609 print 'Content-Type: text/html\n'
610 e = revertStandardError()
611 print error(operation, u, fields, err, e)
613 if isinstance(err, InvalidInput):
614 print 'Content-Type: text/html\n'
615 e = revertStandardError()
616 print invalidInput(operation, u, fields, err, e)
618 print 'Content-Type: text/plain\n'
619 print 'Uh-oh! We experienced an error.'
620 print 'Please email sipb-xen@mit.edu with the contents of this page.'
622 e = revertStandardError()
627 if __name__ == '__main__':
628 fields = cgi.FieldStorage()
631 operation = os.environ.get('PATH_INFO', '')
633 print "Status: 301 Moved Permanently"
634 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
637 if operation.startswith('/'):
638 operation = operation[1:]
642 if os.getenv("SIPB_XEN_PROFILE"):
644 profile.run('main(operation, u, fields)', 'log-'+operation)
646 main(operation, u, fields)