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 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
123 'error on %s for %s: %s\n\n%s\n'
124 % (op, username, err, emsg))
125 d = dict(op=op, user=username, fields=fields,
126 errorMessage=str(err), stderr=emsg, traceback=traceback)
127 return templates.error(searchList=[d])
129 def invalidInput(op, username, fields, err, emsg):
130 """Print an error page when an InvalidInput exception occurs"""
131 d = dict(op=op, user=username, err_field=err.err_field,
132 err_value=str(err.err_value), stderr=emsg,
133 errorMessage=str(err))
134 return templates.invalid(searchList=[d])
137 """Does the machine with a given status list support VNC?"""
141 if l[0] == 'device' and l[1][0] == 'vfb':
143 return 'location' in d
146 def parseCreate(username, state, fields):
147 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
148 validate = validation.Validate(username, state, strict=True, **kws)
149 return dict(contact=username, name=validate.name, memory=validate.memory,
150 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
151 cdrom=getattr(validate, 'cdrom', None),
152 clone_from=getattr(validate, 'clone_from', None))
154 def create(username, state, fields):
155 """Handler for create requests."""
157 parsed_fields = parseCreate(username, state, fields)
158 machine = controls.createVm(username, state, **parsed_fields)
159 except InvalidInput, err:
163 state.clear() #Changed global state
164 d = getListDict(username, state)
167 for field in fields.keys():
168 setattr(d['defaults'], field, fields.getfirst(field))
170 d['new_machine'] = parsed_fields['name']
171 return templates.list(searchList=[d])
174 def getListDict(username, state):
175 """Gets the list of local variables used by list.tmpl."""
176 checkpoint.checkpoint('Starting')
177 machines = state.machines
178 checkpoint.checkpoint('Got my machines')
181 xmlist = state.xmlist
182 checkpoint.checkpoint('Got uptimes')
183 can_clone = 'ice3' not in state.xmlist_raw
189 m.uptime = xmlist[m]['uptime']
190 if xmlist[m]['console']:
195 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
196 max_memory = validation.maxMemory(username, state)
197 max_disk = validation.maxDisk(username)
198 checkpoint.checkpoint('Got max mem/disk')
199 defaults = Defaults(max_memory=max_memory,
203 checkpoint.checkpoint('Got defaults')
204 def sortkey(machine):
205 return (machine.owner != username, machine.owner, machine.name)
206 machines = sorted(machines, key=sortkey)
207 d = dict(user=username,
208 cant_add_vm=validation.cantAddVm(username, state),
209 max_memory=max_memory,
217 def listVms(username, state, fields):
218 """Handler for list requests."""
219 checkpoint.checkpoint('Getting list dict')
220 d = getListDict(username, state)
221 checkpoint.checkpoint('Got list dict')
222 return templates.list(searchList=[d])
224 def vnc(username, state, fields):
227 Note that due to same-domain restrictions, the applet connects to
228 the webserver, which needs to forward those requests to the xen
229 server. The Xen server runs another proxy that (1) authenticates
230 and (2) finds the correct port for the VM.
232 You might want iptables like:
234 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
235 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
236 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
237 --dport 10003 -j SNAT --to-source 18.187.7.142
238 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
239 --dport 10003 -j ACCEPT
241 Remember to enable iptables!
242 echo 1 > /proc/sys/net/ipv4/ip_forward
244 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
246 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
249 data["user"] = username
250 data["machine"] = machine.name
251 data["expires"] = time.time()+(5*60)
252 pickled_data = cPickle.dumps(data)
253 m = hmac.new(TOKEN_KEY, digestmod=sha)
254 m.update(pickled_data)
255 token = {'data': pickled_data, 'digest': m.digest()}
256 token = cPickle.dumps(token)
257 token = base64.urlsafe_b64encode(token)
259 status = controls.statusInfo(machine)
260 has_vnc = hasVnc(status)
262 d = dict(user=username,
266 hostname=state.environ.get('SERVER_NAME', 'localhost'),
268 return templates.vnc(searchList=[d])
270 def getHostname(nic):
271 """Find the hostname associated with a NIC.
273 XXX this should be merged with the similar logic in DNS and DHCP.
275 if nic.hostname and '.' in nic.hostname:
278 return nic.machine.name + '.xvm.mit.edu'
283 def getNicInfo(data_dict, machine):
284 """Helper function for info, get data on nics for a machine.
286 Modifies data_dict to include the relevant data, and returns a list
287 of (key, name) pairs to display "name: data_dict[key]" to the user.
289 data_dict['num_nics'] = len(machine.nics)
290 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
291 ('nic%s_mac', 'NIC %s MAC Addr'),
292 ('nic%s_ip', 'NIC %s IP'),
295 for i in range(len(machine.nics)):
296 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
298 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
299 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
300 data_dict['nic%s_ip' % i] = machine.nics[i].ip
301 if len(machine.nics) == 1:
302 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
305 def getDiskInfo(data_dict, machine):
306 """Helper function for info, get data on disks for a machine.
308 Modifies data_dict to include the relevant data, and returns a list
309 of (key, name) pairs to display "name: data_dict[key]" to the user.
311 data_dict['num_disks'] = len(machine.disks)
312 disk_fields_template = [('%s_size', '%s size')]
314 for disk in machine.disks:
315 name = disk.guest_device_name
316 disk_fields.extend([(x % name, y % name) for x, y in
317 disk_fields_template])
318 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
321 def command(username, state, fields):
322 """Handler for running commands like boot and delete on a VM."""
323 back = fields.getfirst('back')
325 d = controls.commandResult(username, state, fields)
326 if d['command'] == 'Delete VM':
328 except InvalidInput, err:
331 print >> sys.stderr, err
336 return templates.command(searchList=[d])
338 state.clear() #Changed global state
339 d = getListDict(username, state)
341 return templates.list(searchList=[d])
343 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
344 return ({'Status': '303 See Other',
345 'Location': '/info?machine_id=%d' % machine.machine_id},
346 "You shouldn't see this message.")
348 raise InvalidInput('back', back, 'Not a known back page.')
350 def modifyDict(username, state, fields):
351 """Modify a machine as specified by CGI arguments.
353 Return a list of local variables for modify.tmpl.
356 transaction = ctx.current.create_transaction()
358 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
359 validate = validation.Validate(username, state, **kws)
360 machine = validate.machine
361 oldname = machine.name
363 if hasattr(validate, 'memory'):
364 machine.memory = validate.memory
366 if hasattr(validate, 'vmtype'):
367 machine.type = validate.vmtype
369 if hasattr(validate, 'disksize'):
370 disksize = validate.disksize
371 disk = machine.disks[0]
372 if disk.size != disksize:
373 olddisk[disk.guest_device_name] = disksize
375 ctx.current.save(disk)
378 if hasattr(validate, 'owner') and validate.owner != machine.owner:
379 machine.owner = validate.owner
381 if hasattr(validate, 'name'):
382 machine.name = validate.name
383 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
384 machine.administrator = validate.admin
386 if hasattr(validate, 'contact'):
387 machine.contact = validate.contact
389 ctx.current.save(machine)
391 print >> sys.stderr, machine, machine.administrator
392 cache_acls.refreshMachine(machine)
395 transaction.rollback()
397 for diskname in olddisk:
398 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
399 if hasattr(validate, 'name'):
400 controls.renameMachine(machine, oldname, validate.name)
401 return dict(user=username,
405 def modify(username, state, fields):
406 """Handler for modifying attributes of a machine."""
408 modify_dict = modifyDict(username, state, fields)
409 except InvalidInput, err:
411 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
413 machine = modify_dict['machine']
416 info_dict = infoDict(username, state, machine)
417 info_dict['err'] = err
419 for field in fields.keys():
420 setattr(info_dict['defaults'], field, fields.getfirst(field))
421 info_dict['result'] = result
422 return templates.info(searchList=[info_dict])
425 def helpHandler(username, state, fields):
426 """Handler for help messages."""
427 simple = fields.getfirst('simple')
428 subjects = fields.getlist('subject')
430 help_mapping = {'ParaVM Console': """
431 ParaVM machines do not support local console access over VNC. To
432 access the serial console of these machines, you can SSH with Kerberos
433 to console.xvm.mit.edu, using the name of the machine as your
436 HVM machines use the virtualization features of the processor, while
437 ParaVM machines use Xen's emulation of virtualization features. You
438 want an HVM virtualized machine.""",
440 Don't ask us! We're as mystified as you are.""",
442 The owner field is used to determine <a
443 href="help?subject=Quotas">quotas</a>. It must be the name of a
444 locker that you are an AFS administrator of. In particular, you or an
445 AFS group you are a member of must have AFS rlidwka bits on the
446 locker. You can check who administers the LOCKER locker using the
447 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
448 href="help?subject=Administrator">administrator</a>.""",
450 The administrator field determines who can access the console and
451 power on and off the machine. This can be either a user or a moira
454 Quotas are determined on a per-locker basis. Each locker may have a
455 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
458 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
459 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
460 your machine will run just fine, but the applet's display of the
461 console will suffer artifacts.
466 subjects = sorted(help_mapping.keys())
468 d = dict(user=username,
471 mapping=help_mapping)
473 return templates.help(searchList=[d])
476 def badOperation(u, s, e):
477 """Function called when accessing an unknown URI."""
478 raise CodeError("Unknown operation")
480 def infoDict(username, state, machine):
481 """Get the variables used by info.tmpl."""
482 status = controls.statusInfo(machine)
483 checkpoint.checkpoint('Getting status info')
484 has_vnc = hasVnc(status)
486 main_status = dict(name=machine.name,
487 memory=str(machine.memory))
491 main_status = dict(status[1:])
492 start_time = float(main_status.get('start_time', 0))
493 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
494 cpu_time_float = float(main_status.get('cpu_time', 0))
495 cputime = datetime.timedelta(seconds=int(cpu_time_float))
496 checkpoint.checkpoint('Status')
497 display_fields = """name uptime memory state cpu_weight on_reboot
498 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
499 display_fields = [('name', 'Name'),
501 ('administrator', 'Administrator'),
502 ('contact', 'Contact'),
505 ('uptime', 'uptime'),
506 ('cputime', 'CPU usage'),
509 ('state', 'state (xen format)'),
510 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
511 ('on_reboot', 'Action on VM reboot'),
512 ('on_poweroff', 'Action on VM poweroff'),
513 ('on_crash', 'Action on VM crash'),
514 ('on_xend_start', 'Action on Xen start'),
515 ('on_xend_stop', 'Action on Xen stop'),
516 ('bootloader', 'Bootloader options'),
520 machine_info['name'] = machine.name
521 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
522 machine_info['owner'] = machine.owner
523 machine_info['administrator'] = machine.administrator
524 machine_info['contact'] = machine.contact
526 nic_fields = getNicInfo(machine_info, machine)
527 nic_point = display_fields.index('NIC_INFO')
528 display_fields = (display_fields[:nic_point] + nic_fields +
529 display_fields[nic_point+1:])
531 disk_fields = getDiskInfo(machine_info, machine)
532 disk_point = display_fields.index('DISK_INFO')
533 display_fields = (display_fields[:disk_point] + disk_fields +
534 display_fields[disk_point+1:])
536 main_status['memory'] += ' MiB'
537 for field, disp in display_fields:
538 if field in ('uptime', 'cputime') and locals()[field] is not None:
539 fields.append((disp, locals()[field]))
540 elif field in machine_info:
541 fields.append((disp, machine_info[field]))
542 elif field in main_status:
543 fields.append((disp, main_status[field]))
546 #fields.append((disp, None))
548 checkpoint.checkpoint('Got fields')
551 max_mem = validation.maxMemory(machine.owner, state, machine, False)
552 checkpoint.checkpoint('Got mem')
553 max_disk = validation.maxDisk(machine.owner, machine)
554 defaults = Defaults()
555 for name in 'machine_id name administrator owner memory contact'.split():
556 setattr(defaults, name, getattr(machine, name))
557 defaults.type = machine.type.type_id
558 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
559 checkpoint.checkpoint('Got defaults')
560 d = dict(user=username,
561 on=status is not None,
569 owner_help=helppopup("Owner"),
573 def info(username, state, fields):
574 """Handler for info on a single VM."""
575 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
576 d = infoDict(username, state, machine)
577 checkpoint.checkpoint('Got infodict')
578 return templates.info(searchList=[d])
580 def unauthFront(_, _2, fields):
581 """Information for unauth'd users."""
582 return templates.unauth(searchList=[{'simple' : True}])
584 def throwError(_, __, ___):
585 """Throw an error, to test the error-tracing mechanisms."""
586 raise CodeError("test of the emergency broadcast system")
588 mapping = dict(list=listVms,
596 errortest=throwError)
598 def printHeaders(headers):
599 """Print a dictionary as HTTP headers."""
600 for key, value in headers.iteritems():
601 print '%s: %s' % (key, value)
604 def send_error_mail(subject, body):
609 From: root@xvm.mit.edu
613 """ % (to, subject, body)
614 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
619 def getUser(environ):
620 """Return the current user based on the SSL environment variables"""
621 email = environ.get('SSL_CLIENT_S_DN_Email', None)
624 if not email.endswith('@MIT.EDU'):
629 def __init__(self, environ, start_response):
630 self.environ = environ
631 self.start = start_response
633 self.username = getUser(environ)
634 self.state = State(self.username)
635 self.state.environ = environ
638 sys.stderr = StringIO()
639 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
640 operation = self.environ.get('PATH_INFO', '')
642 self.start("301 Moved Permanently", [('Location',
643 self.environ['SCRIPT_NAME']+'/')])
645 if self.username is None:
647 if operation.startswith('/'):
648 operation = operation[1:]
651 print 'Starting', operation
653 start_time = time.time()
654 fun = mapping.get(operation, badOperation)
656 checkpoint.checkpoint('Before')
657 output = fun(self.username, self.state, fields)
658 checkpoint.checkpoint('After')
660 headers = dict(DEFAULT_HEADERS)
661 if isinstance(output, tuple):
662 new_headers, output = output
663 headers.update(new_headers)
665 e = revertStandardError()
667 if isinstance(output, basestring):
668 sys.stderr = StringIO()
670 print >> sys.stderr, x
671 print >> sys.stderr, 'XXX'
672 print >> sys.stderr, e
675 output_string = str(output)
676 checkpoint.checkpoint('output as a string')
677 except Exception, err:
679 if not fields.has_key('js'):
680 if isinstance(err, CodeError):
681 self.start('500 Internal Server Error', [('Content-Type', 'text/html')])
682 e = revertStandardError()
683 s = error(operation, self.username, fields,
684 err, e, traceback.format_exc())
687 if isinstance(err, InvalidInput):
688 self.start('200 OK', [('Content-Type', 'text/html')])
689 e = revertStandardError()
690 yield str(invalidInput(operation, self.username, fields, err, e))
692 self.start('500 Internal Server Error', [('Content-Type', 'text/plain')])
693 send_error_mail('xvm error: %s' % (err,),
694 '%s\n' % (traceback.format_exc(),))
695 yield '''Uh-oh! We experienced an error.
696 Sorry about that. We've gotten mail about it.
698 Feel free to poke us at xvm@mit.edu if this bug is
699 consistently biting you and we don't seem to be fixing it.
701 In case you're curious, the gory details are here.
706 ----''' % (str(err), traceback.format_exc())
707 status = headers.setdefault('Status', '200 OK')
708 del headers['Status']
709 self.start(status, headers.items())
711 if fields.has_key('timedebug'):
712 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
715 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
719 from flup.server.fcgi_fork import WSGIServer
720 WSGIServer(constructor()).run()
722 if __name__ == '__main__':