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 start_time = float(main_status.get('start_time', 0))
495 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
496 cpu_time_float = float(main_status.get('cpu_time', 0))
497 cputime = datetime.timedelta(seconds=int(cpu_time_float))
498 checkpoint.checkpoint('Status')
499 display_fields = """name uptime memory state cpu_weight on_reboot
500 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
501 display_fields = [('name', 'Name'),
502 ('description', 'Description'),
504 ('administrator', 'Administrator'),
505 ('contact', 'Contact'),
508 ('uptime', 'uptime'),
509 ('cputime', 'CPU usage'),
512 ('state', 'state (xen format)'),
513 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
514 ('on_reboot', 'Action on VM reboot'),
515 ('on_poweroff', 'Action on VM poweroff'),
516 ('on_crash', 'Action on VM crash'),
517 ('on_xend_start', 'Action on Xen start'),
518 ('on_xend_stop', 'Action on Xen stop'),
519 ('bootloader', 'Bootloader options'),
523 machine_info['name'] = machine.name
524 machine_info['description'] = machine.description
525 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
526 machine_info['owner'] = machine.owner
527 machine_info['administrator'] = machine.administrator
528 machine_info['contact'] = machine.contact
530 nic_fields = getNicInfo(machine_info, machine)
531 nic_point = display_fields.index('NIC_INFO')
532 display_fields = (display_fields[:nic_point] + nic_fields +
533 display_fields[nic_point+1:])
535 disk_fields = getDiskInfo(machine_info, machine)
536 disk_point = display_fields.index('DISK_INFO')
537 display_fields = (display_fields[:disk_point] + disk_fields +
538 display_fields[disk_point+1:])
540 main_status['memory'] += ' MiB'
541 for field, disp in display_fields:
542 if field in ('uptime', 'cputime') and locals()[field] is not None:
543 fields.append((disp, locals()[field]))
544 elif field in machine_info:
545 fields.append((disp, machine_info[field]))
546 elif field in main_status:
547 fields.append((disp, main_status[field]))
550 #fields.append((disp, None))
552 checkpoint.checkpoint('Got fields')
555 max_mem = validation.maxMemory(machine.owner, state, machine, False)
556 checkpoint.checkpoint('Got mem')
557 max_disk = validation.maxDisk(machine.owner, machine)
558 defaults = Defaults()
559 for name in 'machine_id name description administrator owner memory contact'.split():
560 setattr(defaults, name, getattr(machine, name))
561 defaults.type = machine.type.type_id
562 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
563 checkpoint.checkpoint('Got defaults')
564 d = dict(user=username,
565 on=status is not None,
573 owner_help=helppopup("Owner"),
577 def info(username, state, path, fields):
578 """Handler for info on a single VM."""
579 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
580 d = infoDict(username, state, machine)
581 checkpoint.checkpoint('Got infodict')
582 return templates.info(searchList=[d])
584 def unauthFront(_, _2, _3, fields):
585 """Information for unauth'd users."""
586 return templates.unauth(searchList=[{'simple' : True}])
588 def overlord(username, state, path, fields):
590 return ({'Status': '303 See Other',
591 'Location': 'overlord/'},
592 "You shouldn't see this message.")
593 if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
594 raise InvalidInput('username', username, 'Not an overlord.')
595 newstate = State(username, overlord=True)
596 newstate.environ = state.environ
597 return handler(username, newstate, path, fields)
599 def throwError(_, __, ___, ____):
600 """Throw an error, to test the error-tracing mechanisms."""
601 raise RuntimeError("test of the emergency broadcast system")
603 mapping = dict(list=listVms,
612 errortest=throwError)
614 def printHeaders(headers):
615 """Print a dictionary as HTTP headers."""
616 for key, value in headers.iteritems():
617 print '%s: %s' % (key, value)
620 def send_error_mail(subject, body):
625 From: root@xvm.mit.edu
629 """ % (to, subject, body)
630 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
635 def show_error(op, username, fields, err, emsg, traceback):
636 """Print an error page when an exception occurs"""
637 d = dict(op=op, user=username, fields=fields,
638 errorMessage=str(err), stderr=emsg, traceback=traceback)
639 details = templates.error_raw(searchList=[d])
640 if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
641 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
643 d['details'] = details
644 return templates.error(searchList=[d])
646 def getUser(environ):
647 """Return the current user based on the SSL environment variables"""
648 return environ.get('REMOTE_USER', None)
650 def handler(username, state, path, fields):
651 operation, path = pathSplit(path)
654 print 'Starting', operation
655 fun = mapping.get(operation, badOperation)
656 return fun(username, state, path, fields)
659 def __init__(self, environ, start_response):
660 self.environ = environ
661 self.start = start_response
663 self.username = getUser(environ)
664 self.state = State(self.username)
665 self.state.environ = environ
670 start_time = time.time()
671 sipb_xen_database.clear_cache()
672 sys.stderr = StringIO()
673 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
674 operation = self.environ.get('PATH_INFO', '')
676 self.start("301 Moved Permanently", [('Location', './')])
678 if self.username is None:
682 checkpoint.checkpoint('Before')
683 output = handler(self.username, self.state, operation, fields)
684 checkpoint.checkpoint('After')
686 headers = dict(DEFAULT_HEADERS)
687 if isinstance(output, tuple):
688 new_headers, output = output
689 headers.update(new_headers)
690 e = revertStandardError()
692 if isinstance(output, basestring):
693 sys.stderr = StringIO()
695 print >> sys.stderr, x
696 print >> sys.stderr, 'XXX'
697 print >> sys.stderr, e
700 output_string = str(output)
701 checkpoint.checkpoint('output as a string')
702 except Exception, err:
703 if not fields.has_key('js'):
704 if isinstance(err, InvalidInput):
705 self.start('200 OK', [('Content-Type', 'text/html')])
706 e = revertStandardError()
707 yield str(invalidInput(operation, self.username, fields,
711 self.start('500 Internal Server Error',
712 [('Content-Type', 'text/html')])
713 e = revertStandardError()
714 s = show_error(operation, self.username, fields,
715 err, e, traceback.format_exc())
718 status = headers.setdefault('Status', '200 OK')
719 del headers['Status']
720 self.start(status, headers.items())
722 if fields.has_key('timedebug'):
723 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
726 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
730 from flup.server.fcgi_fork import WSGIServer
731 WSGIServer(constructor()).run()
733 if __name__ == '__main__':