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)
34 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
37 from Cheetah.Template import Template
38 import sipb_xen_database
39 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
42 from webcommon import InvalidInput, CodeError, State
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()
61 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
64 """Return HTML code for a (?) link to a specified help topic"""
65 return ('<span class="helplink"><a href="help?' +
66 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
67 +'" target="_blank" ' +
68 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
70 def makeErrorPre(old, addition):
74 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
76 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
78 Template.sipb_xen_database = sipb_xen_database
79 Template.helppopup = staticmethod(helppopup)
83 """Class to store a dictionary that will be converted to JSON"""
84 def __init__(self, **kws):
92 return simplejson.dumps(self.data)
94 def addError(self, text):
95 """Add stderr text to be displayed on the website."""
97 makeErrorPre(self.data.get('err'), text)
100 """Class to store default values for fields."""
108 def __init__(self, max_memory=None, max_disk=None, **kws):
109 if max_memory is not None:
110 self.memory = min(self.memory, max_memory)
111 if max_disk is not None:
112 self.max_disk = min(self.disk, max_disk)
114 setattr(self, key, kws[key])
118 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
120 def error(op, username, fields, err, emsg, traceback):
121 """Print an error page when a CodeError occurs"""
122 d = dict(op=op, user=username, fields=fields,
123 errorMessage=str(err), stderr=emsg, traceback=traceback)
124 details = templates.error_raw(searchList=[d])
125 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
127 d['details'] = details
128 return templates.error(searchList=[d])
130 def invalidInput(op, username, fields, err, emsg):
131 """Print an error page when an InvalidInput exception occurs"""
132 d = dict(op=op, user=username, err_field=err.err_field,
133 err_value=str(err.err_value), stderr=emsg,
134 errorMessage=str(err))
135 return templates.invalid(searchList=[d])
138 """Does the machine with a given status list support VNC?"""
142 if l[0] == 'device' and l[1][0] == 'vfb':
144 return 'location' in d
147 def parseCreate(username, state, fields):
148 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
149 validate = validation.Validate(username, state, strict=True, **kws)
150 return dict(contact=username, name=validate.name, memory=validate.memory,
151 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
152 cdrom=getattr(validate, 'cdrom', None),
153 clone_from=getattr(validate, 'clone_from', None))
155 def create(username, state, fields):
156 """Handler for create requests."""
158 parsed_fields = parseCreate(username, state, fields)
159 machine = controls.createVm(username, state, **parsed_fields)
160 except InvalidInput, err:
164 state.clear() #Changed global state
165 d = getListDict(username, state)
168 for field in fields.keys():
169 setattr(d['defaults'], field, fields.getfirst(field))
171 d['new_machine'] = parsed_fields['name']
172 return templates.list(searchList=[d])
175 def getListDict(username, state):
176 """Gets the list of local variables used by list.tmpl."""
177 checkpoint.checkpoint('Starting')
178 machines = state.machines
179 checkpoint.checkpoint('Got my machines')
182 xmlist = state.xmlist
183 checkpoint.checkpoint('Got uptimes')
184 can_clone = 'ice3' not in state.xmlist_raw
190 m.uptime = xmlist[m]['uptime']
191 if xmlist[m]['console']:
196 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
197 max_memory = validation.maxMemory(username, state)
198 max_disk = validation.maxDisk(username)
199 checkpoint.checkpoint('Got max mem/disk')
200 defaults = Defaults(max_memory=max_memory,
204 checkpoint.checkpoint('Got defaults')
205 def sortkey(machine):
206 return (machine.owner != username, machine.owner, machine.name)
207 machines = sorted(machines, key=sortkey)
208 d = dict(user=username,
209 cant_add_vm=validation.cantAddVm(username, state),
210 max_memory=max_memory,
218 def listVms(username, state, fields):
219 """Handler for list requests."""
220 checkpoint.checkpoint('Getting list dict')
221 d = getListDict(username, state)
222 checkpoint.checkpoint('Got list dict')
223 return templates.list(searchList=[d])
225 def vnc(username, state, fields):
228 Note that due to same-domain restrictions, the applet connects to
229 the webserver, which needs to forward those requests to the xen
230 server. The Xen server runs another proxy that (1) authenticates
231 and (2) finds the correct port for the VM.
233 You might want iptables like:
235 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
236 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
237 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
238 --dport 10003 -j SNAT --to-source 18.187.7.142
239 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
240 --dport 10003 -j ACCEPT
242 Remember to enable iptables!
243 echo 1 > /proc/sys/net/ipv4/ip_forward
245 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
247 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
250 data["user"] = username
251 data["machine"] = machine.name
252 data["expires"] = time.time()+(5*60)
253 pickled_data = cPickle.dumps(data)
254 m = hmac.new(TOKEN_KEY, digestmod=sha)
255 m.update(pickled_data)
256 token = {'data': pickled_data, 'digest': m.digest()}
257 token = cPickle.dumps(token)
258 token = base64.urlsafe_b64encode(token)
260 status = controls.statusInfo(machine)
261 has_vnc = hasVnc(status)
263 d = dict(user=username,
267 hostname=state.environ.get('SERVER_NAME', 'localhost'),
269 return templates.vnc(searchList=[d])
271 def getHostname(nic):
272 """Find the hostname associated with a NIC.
274 XXX this should be merged with the similar logic in DNS and DHCP.
276 if nic.hostname and '.' in nic.hostname:
279 return nic.machine.name + '.xvm.mit.edu'
284 def getNicInfo(data_dict, machine):
285 """Helper function for info, get data on nics for a machine.
287 Modifies data_dict to include the relevant data, and returns a list
288 of (key, name) pairs to display "name: data_dict[key]" to the user.
290 data_dict['num_nics'] = len(machine.nics)
291 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
292 ('nic%s_mac', 'NIC %s MAC Addr'),
293 ('nic%s_ip', 'NIC %s IP'),
296 for i in range(len(machine.nics)):
297 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
299 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
300 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
301 data_dict['nic%s_ip' % i] = machine.nics[i].ip
302 if len(machine.nics) == 1:
303 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
306 def getDiskInfo(data_dict, machine):
307 """Helper function for info, get data on disks for a machine.
309 Modifies data_dict to include the relevant data, and returns a list
310 of (key, name) pairs to display "name: data_dict[key]" to the user.
312 data_dict['num_disks'] = len(machine.disks)
313 disk_fields_template = [('%s_size', '%s size')]
315 for disk in machine.disks:
316 name = disk.guest_device_name
317 disk_fields.extend([(x % name, y % name) for x, y in
318 disk_fields_template])
319 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
322 def command(username, state, fields):
323 """Handler for running commands like boot and delete on a VM."""
324 back = fields.getfirst('back')
326 d = controls.commandResult(username, state, fields)
327 if d['command'] == 'Delete VM':
329 except InvalidInput, err:
332 print >> sys.stderr, err
337 return templates.command(searchList=[d])
339 state.clear() #Changed global state
340 d = getListDict(username, state)
342 return templates.list(searchList=[d])
344 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
345 return ({'Status': '303 See Other',
346 'Location': '/info?machine_id=%d' % machine.machine_id},
347 "You shouldn't see this message.")
349 raise InvalidInput('back', back, 'Not a known back page.')
351 def modifyDict(username, state, fields):
352 """Modify a machine as specified by CGI arguments.
354 Return a list of local variables for modify.tmpl.
357 transaction = ctx.current.create_transaction()
359 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
360 validate = validation.Validate(username, state, **kws)
361 machine = validate.machine
362 oldname = machine.name
364 if hasattr(validate, 'memory'):
365 machine.memory = validate.memory
367 if hasattr(validate, 'vmtype'):
368 machine.type = validate.vmtype
370 if hasattr(validate, 'disksize'):
371 disksize = validate.disksize
372 disk = machine.disks[0]
373 if disk.size != disksize:
374 olddisk[disk.guest_device_name] = disksize
376 ctx.current.save(disk)
379 if hasattr(validate, 'owner') and validate.owner != machine.owner:
380 machine.owner = validate.owner
382 if hasattr(validate, 'name'):
383 machine.name = validate.name
384 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
385 machine.administrator = validate.admin
387 if hasattr(validate, 'contact'):
388 machine.contact = validate.contact
390 ctx.current.save(machine)
392 print >> sys.stderr, machine, machine.administrator
393 cache_acls.refreshMachine(machine)
396 transaction.rollback()
398 for diskname in olddisk:
399 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
400 if hasattr(validate, 'name'):
401 controls.renameMachine(machine, oldname, validate.name)
402 return dict(user=username,
406 def modify(username, state, fields):
407 """Handler for modifying attributes of a machine."""
409 modify_dict = modifyDict(username, state, fields)
410 except InvalidInput, err:
412 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
414 machine = modify_dict['machine']
417 info_dict = infoDict(username, state, machine)
418 info_dict['err'] = err
420 for field in fields.keys():
421 setattr(info_dict['defaults'], field, fields.getfirst(field))
422 info_dict['result'] = result
423 return templates.info(searchList=[info_dict])
426 def helpHandler(username, state, fields):
427 """Handler for help messages."""
428 simple = fields.getfirst('simple')
429 subjects = fields.getlist('subject')
431 help_mapping = {'ParaVM Console': """
432 ParaVM machines do not support local console access over VNC. To
433 access the serial console of these machines, you can SSH with Kerberos
434 to console.xvm.mit.edu, using the name of the machine as your
437 HVM machines use the virtualization features of the processor, while
438 ParaVM machines use Xen's emulation of virtualization features. You
439 want an HVM virtualized machine.""",
441 Don't ask us! We're as mystified as you are.""",
443 The owner field is used to determine <a
444 href="help?subject=Quotas">quotas</a>. It must be the name of a
445 locker that you are an AFS administrator of. In particular, you or an
446 AFS group you are a member of must have AFS rlidwka bits on the
447 locker. You can check who administers the LOCKER locker using the
448 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
449 href="help?subject=Administrator">administrator</a>.""",
451 The administrator field determines who can access the console and
452 power on and off the machine. This can be either a user or a moira
455 Quotas are determined on a per-locker basis. Each locker may have a
456 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
459 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
460 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
461 your machine will run just fine, but the applet's display of the
462 console will suffer artifacts.
467 subjects = sorted(help_mapping.keys())
469 d = dict(user=username,
472 mapping=help_mapping)
474 return templates.help(searchList=[d])
477 def badOperation(u, s, e):
478 """Function called when accessing an unknown URI."""
479 raise CodeError("Unknown operation")
481 def infoDict(username, state, machine):
482 """Get the variables used by info.tmpl."""
483 status = controls.statusInfo(machine)
484 checkpoint.checkpoint('Getting status info')
485 has_vnc = hasVnc(status)
487 main_status = dict(name=machine.name,
488 memory=str(machine.memory))
492 main_status = dict(status[1:])
493 start_time = float(main_status.get('start_time', 0))
494 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
495 cpu_time_float = float(main_status.get('cpu_time', 0))
496 cputime = datetime.timedelta(seconds=int(cpu_time_float))
497 checkpoint.checkpoint('Status')
498 display_fields = """name uptime memory state cpu_weight on_reboot
499 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
500 display_fields = [('name', 'Name'),
502 ('administrator', 'Administrator'),
503 ('contact', 'Contact'),
506 ('uptime', 'uptime'),
507 ('cputime', 'CPU usage'),
510 ('state', 'state (xen format)'),
511 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
512 ('on_reboot', 'Action on VM reboot'),
513 ('on_poweroff', 'Action on VM poweroff'),
514 ('on_crash', 'Action on VM crash'),
515 ('on_xend_start', 'Action on Xen start'),
516 ('on_xend_stop', 'Action on Xen stop'),
517 ('bootloader', 'Bootloader options'),
521 machine_info['name'] = machine.name
522 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
523 machine_info['owner'] = machine.owner
524 machine_info['administrator'] = machine.administrator
525 machine_info['contact'] = machine.contact
527 nic_fields = getNicInfo(machine_info, machine)
528 nic_point = display_fields.index('NIC_INFO')
529 display_fields = (display_fields[:nic_point] + nic_fields +
530 display_fields[nic_point+1:])
532 disk_fields = getDiskInfo(machine_info, machine)
533 disk_point = display_fields.index('DISK_INFO')
534 display_fields = (display_fields[:disk_point] + disk_fields +
535 display_fields[disk_point+1:])
537 main_status['memory'] += ' MiB'
538 for field, disp in display_fields:
539 if field in ('uptime', 'cputime') and locals()[field] is not None:
540 fields.append((disp, locals()[field]))
541 elif field in machine_info:
542 fields.append((disp, machine_info[field]))
543 elif field in main_status:
544 fields.append((disp, main_status[field]))
547 #fields.append((disp, None))
549 checkpoint.checkpoint('Got fields')
552 max_mem = validation.maxMemory(machine.owner, state, machine, False)
553 checkpoint.checkpoint('Got mem')
554 max_disk = validation.maxDisk(machine.owner, machine)
555 defaults = Defaults()
556 for name in 'machine_id name administrator owner memory contact'.split():
557 setattr(defaults, name, getattr(machine, name))
558 defaults.type = machine.type.type_id
559 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
560 checkpoint.checkpoint('Got defaults')
561 d = dict(user=username,
562 on=status is not None,
570 owner_help=helppopup("Owner"),
574 def info(username, state, fields):
575 """Handler for info on a single VM."""
576 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
577 d = infoDict(username, state, machine)
578 checkpoint.checkpoint('Got infodict')
579 return templates.info(searchList=[d])
581 def unauthFront(_, _2, fields):
582 """Information for unauth'd users."""
583 return templates.unauth(searchList=[{'simple' : True}])
585 def throwError(_, __, ___):
586 """Throw an error, to test the error-tracing mechanisms."""
587 raise CodeError("test of the emergency broadcast system")
589 mapping = dict(list=listVms,
597 errortest=throwError)
599 def printHeaders(headers):
600 """Print a dictionary as HTTP headers."""
601 for key, value in headers.iteritems():
602 print '%s: %s' % (key, value)
605 def send_error_mail(subject, body):
610 From: root@xvm.mit.edu
614 """ % (to, subject, body)
615 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
620 def getUser(environ):
621 """Return the current user based on the SSL environment variables"""
622 email = environ.get('SSL_CLIENT_S_DN_Email', None)
625 if not email.endswith('@MIT.EDU'):
630 def __init__(self, environ, start_response):
631 self.environ = environ
632 self.start = start_response
634 self.username = getUser(environ)
635 self.state = State(self.username)
636 self.state.environ = environ
639 sys.stderr = StringIO()
640 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
641 operation = self.environ.get('PATH_INFO', '')
643 self.start("301 Moved Permanently", [('Location',
644 self.environ['SCRIPT_NAME']+'/')])
646 if self.username is None:
648 if operation.startswith('/'):
649 operation = operation[1:]
652 print 'Starting', operation
654 start_time = time.time()
655 fun = mapping.get(operation, badOperation)
657 checkpoint.checkpoint('Before')
658 output = fun(self.username, self.state, fields)
659 checkpoint.checkpoint('After')
661 headers = dict(DEFAULT_HEADERS)
662 if isinstance(output, tuple):
663 new_headers, output = output
664 headers.update(new_headers)
666 e = revertStandardError()
668 if isinstance(output, basestring):
669 sys.stderr = StringIO()
671 print >> sys.stderr, x
672 print >> sys.stderr, 'XXX'
673 print >> sys.stderr, e
676 output_string = str(output)
677 checkpoint.checkpoint('output as a string')
678 except Exception, err:
680 if not fields.has_key('js'):
681 if isinstance(err, CodeError):
682 self.start('500 Internal Server Error', [('Content-Type', 'text/html')])
683 e = revertStandardError()
684 s = error(operation, self.username, fields,
685 err, e, traceback.format_exc())
688 if isinstance(err, InvalidInput):
689 self.start('200 OK', [('Content-Type', 'text/html')])
690 e = revertStandardError()
691 yield str(invalidInput(operation, self.username, fields, err, e))
693 self.start('500 Internal Server Error', [('Content-Type', 'text/plain')])
694 send_error_mail('xvm error: %s' % (err,),
695 '%s\n' % (traceback.format_exc(),))
696 yield '''Uh-oh! We experienced an error.
697 Sorry about that. We've gotten mail about it.
699 Feel free to poke us at xvm@mit.edu if this bug is
700 consistently biting you and we don't seem to be fixing it.
702 In case you're curious, the gory details are here.
707 ----''' % (str(err), traceback.format_exc())
708 status = headers.setdefault('Status', '200 OK')
709 del headers['Status']
710 self.start(status, headers.items())
712 if fields.has_key('timedebug'):
713 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
716 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
720 from flup.server.fcgi_fork import WSGIServer
721 WSGIServer(constructor()).run()
723 if __name__ == '__main__':