2 """Main CGI script for web interface"""
15 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)
36 from Cheetah.Template import Template
37 import sipb_xen_database
38 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
41 from webcommon import InvalidInput, CodeError, State
43 from getafsgroups import getAfsGroupMembers
46 if path.startswith('/'):
51 return path[:i], path[i:]
55 self.start_time = time.time()
58 def checkpoint(self, s):
59 self.checkpoints.append((s, time.time()))
62 return ('Timing info:\n%s\n' %
63 '\n'.join(['%s: %s' % (d, t - self.start_time) for
64 (d, t) in self.checkpoints]))
66 checkpoint = Checkpoint()
69 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
72 """Return HTML code for a (?) link to a specified help topic"""
73 return ('<span class="helplink"><a href="help?' +
74 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
75 +'" target="_blank" ' +
76 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
78 def makeErrorPre(old, addition):
82 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
84 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
86 Template.sipb_xen_database = sipb_xen_database
87 Template.helppopup = staticmethod(helppopup)
91 """Class to store a dictionary that will be converted to JSON"""
92 def __init__(self, **kws):
100 return simplejson.dumps(self.data)
102 def addError(self, text):
103 """Add stderr text to be displayed on the website."""
105 makeErrorPre(self.data.get('err'), text)
108 """Class to store default values for fields."""
117 def __init__(self, max_memory=None, max_disk=None, **kws):
118 if max_memory is not None:
119 self.memory = min(self.memory, max_memory)
120 if max_disk is not None:
121 self.max_disk = min(self.disk, max_disk)
123 setattr(self, key, kws[key])
127 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
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 description owner memory disksize vmtype cdrom autoinstall'.split()])
148 validate = validation.Validate(username, state, strict=True, **kws)
149 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
150 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
151 cdrom=getattr(validate, 'cdrom', None),
152 autoinstall=getattr(validate, 'autoinstall', None))
154 def create(username, state, path, 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, path, 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, path, 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, path, 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 description 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, 'description'):
384 machine.description = validate.description
385 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
386 machine.administrator = validate.admin
388 if hasattr(validate, 'contact'):
389 machine.contact = validate.contact
391 ctx.current.save(machine)
393 print >> sys.stderr, machine, machine.administrator
394 cache_acls.refreshMachine(machine)
397 transaction.rollback()
399 for diskname in olddisk:
400 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
401 if hasattr(validate, 'name'):
402 controls.renameMachine(machine, oldname, validate.name)
403 return dict(user=username,
407 def modify(username, state, path, fields):
408 """Handler for modifying attributes of a machine."""
410 modify_dict = modifyDict(username, state, fields)
411 except InvalidInput, err:
413 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
415 machine = modify_dict['machine']
418 info_dict = infoDict(username, state, machine)
419 info_dict['err'] = err
421 for field in fields.keys():
422 setattr(info_dict['defaults'], field, fields.getfirst(field))
423 info_dict['result'] = result
424 return templates.info(searchList=[info_dict])
427 def helpHandler(username, state, path, fields):
428 """Handler for help messages."""
429 simple = fields.getfirst('simple')
430 subjects = fields.getlist('subject')
432 help_mapping = {'ParaVM Console': """
433 ParaVM machines do not support local console access over VNC. To
434 access the serial console of these machines, you can SSH with Kerberos
435 to console.xvm.mit.edu, using the name of the machine as your
438 HVM machines use the virtualization features of the processor, while
439 ParaVM machines use Xen's emulation of virtualization features. You
440 want an HVM virtualized machine.""",
442 Don't ask us! We're as mystified as you are.""",
444 The owner field is used to determine <a
445 href="help?subject=Quotas">quotas</a>. It must be the name of a
446 locker that you are an AFS administrator of. In particular, you or an
447 AFS group you are a member of must have AFS rlidwka bits on the
448 locker. You can check who administers the LOCKER locker using the
449 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
450 href="help?subject=Administrator">administrator</a>.""",
452 The administrator field determines who can access the console and
453 power on and off the machine. This can be either a user or a moira
456 Quotas are determined on a per-locker basis. Each locker may have a
457 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
460 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
461 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
462 your machine will run just fine, but the applet's display of the
463 console will suffer artifacts.
468 subjects = sorted(help_mapping.keys())
470 d = dict(user=username,
473 mapping=help_mapping)
475 return templates.help(searchList=[d])
478 def badOperation(u, s, p, e):
479 """Function called when accessing an unknown URI."""
480 return ({'Status': '404 Not Found'}, 'Invalid operation.')
482 def infoDict(username, state, machine):
483 """Get the variables used by info.tmpl."""
484 status = controls.statusInfo(machine)
485 checkpoint.checkpoint('Getting status info')
486 has_vnc = hasVnc(status)
488 main_status = dict(name=machine.name,
489 memory=str(machine.memory))
493 main_status = dict(status[1:])
494 main_status['host'] = controls.listHost(machine)
495 start_time = float(main_status.get('start_time', 0))
496 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
497 cpu_time_float = float(main_status.get('cpu_time', 0))
498 cputime = datetime.timedelta(seconds=int(cpu_time_float))
499 checkpoint.checkpoint('Status')
500 display_fields = """name uptime memory state cpu_weight on_reboot
501 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
502 display_fields = [('name', 'Name'),
503 ('description', 'Description'),
505 ('administrator', 'Administrator'),
506 ('contact', 'Contact'),
509 ('uptime', 'uptime'),
510 ('cputime', 'CPU usage'),
511 ('host', 'Hosted on'),
514 ('state', 'state (xen format)'),
515 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
516 ('on_reboot', 'Action on VM reboot'),
517 ('on_poweroff', 'Action on VM poweroff'),
518 ('on_crash', 'Action on VM crash'),
519 ('on_xend_start', 'Action on Xen start'),
520 ('on_xend_stop', 'Action on Xen stop'),
521 ('bootloader', 'Bootloader options'),
525 machine_info['name'] = machine.name
526 machine_info['description'] = machine.description
527 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
528 machine_info['owner'] = machine.owner
529 machine_info['administrator'] = machine.administrator
530 machine_info['contact'] = machine.contact
532 nic_fields = getNicInfo(machine_info, machine)
533 nic_point = display_fields.index('NIC_INFO')
534 display_fields = (display_fields[:nic_point] + nic_fields +
535 display_fields[nic_point+1:])
537 disk_fields = getDiskInfo(machine_info, machine)
538 disk_point = display_fields.index('DISK_INFO')
539 display_fields = (display_fields[:disk_point] + disk_fields +
540 display_fields[disk_point+1:])
542 main_status['memory'] += ' MiB'
543 for field, disp in display_fields:
544 if field in ('uptime', 'cputime') and locals()[field] is not None:
545 fields.append((disp, locals()[field]))
546 elif field in machine_info:
547 fields.append((disp, machine_info[field]))
548 elif field in main_status:
549 fields.append((disp, main_status[field]))
552 #fields.append((disp, None))
554 checkpoint.checkpoint('Got fields')
557 max_mem = validation.maxMemory(machine.owner, state, machine, False)
558 checkpoint.checkpoint('Got mem')
559 max_disk = validation.maxDisk(machine.owner, machine)
560 defaults = Defaults()
561 for name in 'machine_id name description administrator owner memory contact'.split():
562 setattr(defaults, name, getattr(machine, name))
563 defaults.type = machine.type.type_id
564 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
565 checkpoint.checkpoint('Got defaults')
566 d = dict(user=username,
567 on=status is not None,
575 owner_help=helppopup("Owner"),
579 def info(username, state, path, fields):
580 """Handler for info on a single VM."""
581 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
582 d = infoDict(username, state, machine)
583 checkpoint.checkpoint('Got infodict')
584 return templates.info(searchList=[d])
586 def unauthFront(_, _2, _3, fields):
587 """Information for unauth'd users."""
588 return templates.unauth(searchList=[{'simple' : True}])
590 def overlord(username, state, path, fields):
592 return ({'Status': '303 See Other',
593 'Location': 'overlord/'},
594 "You shouldn't see this message.")
595 if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
596 raise InvalidInput('username', username, 'Not an overlord.')
597 newstate = State(username, overlord=True)
598 newstate.environ = state.environ
599 return handler(username, newstate, path, fields)
601 def throwError(_, __, ___, ____):
602 """Throw an error, to test the error-tracing mechanisms."""
603 raise RuntimeError("test of the emergency broadcast system")
605 mapping = dict(list=listVms,
614 errortest=throwError)
616 def printHeaders(headers):
617 """Print a dictionary as HTTP headers."""
618 for key, value in headers.iteritems():
619 print '%s: %s' % (key, value)
622 def send_error_mail(subject, body):
627 From: root@xvm.mit.edu
631 """ % (to, subject, body)
632 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
637 def show_error(op, username, fields, err, emsg, traceback):
638 """Print an error page when an exception occurs"""
639 d = dict(op=op, user=username, fields=fields,
640 errorMessage=str(err), stderr=emsg, traceback=traceback)
641 details = templates.error_raw(searchList=[d])
642 if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
643 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
645 d['details'] = details
646 return templates.error(searchList=[d])
648 def getUser(environ):
649 """Return the current user based on the SSL environment variables"""
650 return environ.get('REMOTE_USER', None)
652 def handler(username, state, path, fields):
653 operation, path = pathSplit(path)
656 print 'Starting', operation
657 fun = mapping.get(operation, badOperation)
658 return fun(username, state, path, fields)
661 def __init__(self, environ, start_response):
662 self.environ = environ
663 self.start = start_response
665 self.username = getUser(environ)
666 self.state = State(self.username)
667 self.state.environ = environ
672 start_time = time.time()
673 sipb_xen_database.clear_cache()
674 sys.stderr = StringIO()
675 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
676 operation = self.environ.get('PATH_INFO', '')
678 self.start("301 Moved Permanently", [('Location', './')])
680 if self.username is None:
684 checkpoint.checkpoint('Before')
685 output = handler(self.username, self.state, operation, fields)
686 checkpoint.checkpoint('After')
688 headers = dict(DEFAULT_HEADERS)
689 if isinstance(output, tuple):
690 new_headers, output = output
691 headers.update(new_headers)
692 e = revertStandardError()
694 if isinstance(output, basestring):
695 sys.stderr = StringIO()
697 print >> sys.stderr, x
698 print >> sys.stderr, 'XXX'
699 print >> sys.stderr, e
702 output_string = str(output)
703 checkpoint.checkpoint('output as a string')
704 except Exception, err:
705 if not fields.has_key('js'):
706 if isinstance(err, InvalidInput):
707 self.start('200 OK', [('Content-Type', 'text/html')])
708 e = revertStandardError()
709 yield str(invalidInput(operation, self.username, fields,
713 self.start('500 Internal Server Error',
714 [('Content-Type', 'text/html')])
715 e = revertStandardError()
716 s = show_error(operation, self.username, fields,
717 err, e, traceback.format_exc())
720 status = headers.setdefault('Status', '200 OK')
721 del headers['Status']
722 self.start(status, headers.items())
724 if fields.has_key('timedebug'):
725 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
728 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
732 from flup.server.fcgi_fork import WSGIServer
733 WSGIServer(constructor()).run()
735 if __name__ == '__main__':