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 import sipb_xen_database
40 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
43 from webcommon import InvalidInput, CodeError, State
48 self.start_time = time.time()
51 def checkpoint(self, s):
52 self.checkpoints.append((s, time.time()))
55 return ('Timing info:\n%s\n' %
56 '\n'.join(['%s: %s' % (d, t - self.start_time) for
57 (d, t) in self.checkpoints]))
59 checkpoint = Checkpoint()
62 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
65 """Return HTML code for a (?) link to a specified help topic"""
66 return ('<span class="helplink"><a href="help?' +
67 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
68 +'" target="_blank" ' +
69 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
71 def makeErrorPre(old, addition):
75 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
77 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
79 Template.sipb_xen_database = sipb_xen_database
80 Template.helppopup = staticmethod(helppopup)
84 """Class to store a dictionary that will be converted to JSON"""
85 def __init__(self, **kws):
93 return simplejson.dumps(self.data)
95 def addError(self, text):
96 """Add stderr text to be displayed on the website."""
98 makeErrorPre(self.data.get('err'), text)
101 """Class to store default values for fields."""
109 def __init__(self, max_memory=None, max_disk=None, **kws):
110 if max_memory is not None:
111 self.memory = min(self.memory, max_memory)
112 if max_disk is not None:
113 self.max_disk = min(self.disk, max_disk)
115 setattr(self, key, kws[key])
119 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
121 def error(op, username, fields, err, emsg):
122 """Print an error page when a CodeError occurs"""
123 d = dict(op=op, user=username, errorMessage=str(err),
125 return templates.error(searchList=[d])
127 def invalidInput(op, username, fields, err, emsg):
128 """Print an error page when an InvalidInput exception occurs"""
129 d = dict(op=op, user=username, err_field=err.err_field,
130 err_value=str(err.err_value), stderr=emsg,
131 errorMessage=str(err))
132 return templates.invalid(searchList=[d])
135 """Does the machine with a given status list support VNC?"""
139 if l[0] == 'device' and l[1][0] == 'vfb':
141 return 'location' in d
144 def parseCreate(username, state, fields):
145 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
146 validate = validation.Validate(username, state, strict=True, **kws)
147 return dict(contact=username, name=validate.name, memory=validate.memory,
148 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
149 cdrom=getattr(validate, 'cdrom', None),
150 clone_from=getattr(validate, 'clone_from', None))
152 def create(username, state, fields):
153 """Handler for create requests."""
155 parsed_fields = parseCreate(username, state, fields)
156 machine = controls.createVm(username, state, **parsed_fields)
157 except InvalidInput, err:
161 state.clear() #Changed global state
162 d = getListDict(username, state)
165 for field in fields.keys():
166 setattr(d['defaults'], field, fields.getfirst(field))
168 d['new_machine'] = parsed_fields['name']
169 return templates.list(searchList=[d])
172 def getListDict(username, state):
173 """Gets the list of local variables used by list.tmpl."""
174 checkpoint.checkpoint('Starting')
175 machines = state.machines
176 checkpoint.checkpoint('Got my machines')
179 xmlist = state.xmlist
180 checkpoint.checkpoint('Got uptimes')
181 can_clone = 'ice3' not in state.xmlist_raw
187 m.uptime = xmlist[m]['uptime']
188 if xmlist[m]['console']:
193 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
194 max_memory = validation.maxMemory(username, state)
195 max_disk = validation.maxDisk(username)
196 checkpoint.checkpoint('Got max mem/disk')
197 defaults = Defaults(max_memory=max_memory,
201 checkpoint.checkpoint('Got defaults')
202 def sortkey(machine):
203 return (machine.owner != username, machine.owner, machine.name)
204 machines = sorted(machines, key=sortkey)
205 d = dict(user=username,
206 cant_add_vm=validation.cantAddVm(username, state),
207 max_memory=max_memory,
215 def listVms(username, state, fields):
216 """Handler for list requests."""
217 checkpoint.checkpoint('Getting list dict')
218 d = getListDict(username, state)
219 checkpoint.checkpoint('Got list dict')
220 return templates.list(searchList=[d])
222 def vnc(username, state, fields):
225 Note that due to same-domain restrictions, the applet connects to
226 the webserver, which needs to forward those requests to the xen
227 server. The Xen server runs another proxy that (1) authenticates
228 and (2) finds the correct port for the VM.
230 You might want iptables like:
232 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
233 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
234 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
235 --dport 10003 -j SNAT --to-source 18.187.7.142
236 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
237 --dport 10003 -j ACCEPT
239 Remember to enable iptables!
240 echo 1 > /proc/sys/net/ipv4/ip_forward
242 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
244 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
247 data["user"] = username
248 data["machine"] = machine.name
249 data["expires"] = time.time()+(5*60)
250 pickled_data = cPickle.dumps(data)
251 m = hmac.new(TOKEN_KEY, digestmod=sha)
252 m.update(pickled_data)
253 token = {'data': pickled_data, 'digest': m.digest()}
254 token = cPickle.dumps(token)
255 token = base64.urlsafe_b64encode(token)
257 status = controls.statusInfo(machine)
258 has_vnc = hasVnc(status)
260 d = dict(user=username,
264 hostname=state.environ.get('SERVER_NAME', 'localhost'),
266 return templates.vnc(searchList=[d])
268 def getHostname(nic):
269 """Find the hostname associated with a NIC.
271 XXX this should be merged with the similar logic in DNS and DHCP.
273 if nic.hostname and '.' in nic.hostname:
276 return nic.machine.name + '.xvm.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(username, state, fields):
320 """Handler for running commands like boot and delete on a VM."""
321 back = fields.getfirst('back')
323 d = controls.commandResult(username, state, fields)
324 if d['command'] == 'Delete VM':
326 except InvalidInput, err:
329 print >> sys.stderr, err
334 return templates.command(searchList=[d])
336 state.clear() #Changed global state
337 d = getListDict(username, state)
339 return templates.list(searchList=[d])
341 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
342 return ({'Status': '302',
343 'Location': '/info?machine_id=%d' % machine.machine_id},
344 "You shouldn't see this message.")
346 raise InvalidInput('back', back, 'Not a known back page.')
348 def modifyDict(username, state, fields):
349 """Modify a machine as specified by CGI arguments.
351 Return a list of local variables for modify.tmpl.
354 transaction = ctx.current.create_transaction()
356 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
357 validate = validation.Validate(username, state, **kws)
358 machine = validate.machine
359 oldname = machine.name
361 if hasattr(validate, 'memory'):
362 machine.memory = validate.memory
364 if hasattr(validate, 'vmtype'):
365 machine.type = validate.vmtype
367 if hasattr(validate, 'disksize'):
368 disksize = validate.disksize
369 disk = machine.disks[0]
370 if disk.size != disksize:
371 olddisk[disk.guest_device_name] = disksize
373 ctx.current.save(disk)
376 if hasattr(validate, 'owner') and validate.owner != machine.owner:
377 machine.owner = validate.owner
379 if hasattr(validate, 'name'):
381 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
382 machine.administrator = validate.admin
384 if hasattr(validate, 'contact'):
385 machine.contact = validate.contact
387 ctx.current.save(machine)
389 print >> sys.stderr, machine, machine.administrator
390 cache_acls.refreshMachine(machine)
393 transaction.rollback()
395 for diskname in olddisk:
396 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
397 if hasattr(validate, 'name'):
398 controls.renameMachine(machine, oldname, validate.name)
399 return dict(user=username,
403 def modify(username, state, fields):
404 """Handler for modifying attributes of a machine."""
406 modify_dict = modifyDict(username, state, fields)
407 except InvalidInput, err:
409 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
411 machine = modify_dict['machine']
414 info_dict = infoDict(username, state, machine)
415 info_dict['err'] = err
417 for field in fields.keys():
418 setattr(info_dict['defaults'], field, fields.getfirst(field))
419 info_dict['result'] = result
420 return templates.info(searchList=[info_dict])
423 def helpHandler(username, state, fields):
424 """Handler for help messages."""
425 simple = fields.getfirst('simple')
426 subjects = fields.getlist('subject')
428 help_mapping = {'ParaVM Console': """
429 ParaVM machines do not support local console access over VNC. To
430 access the serial console of these machines, you can SSH with Kerberos
431 to console.xvm.mit.edu, using the name of the machine as your
434 HVM machines use the virtualization features of the processor, while
435 ParaVM machines use Xen's emulation of virtualization features. You
436 want an HVM virtualized machine.""",
438 Don't ask us! We're as mystified as you are.""",
440 The owner field is used to determine <a
441 href="help?subject=Quotas">quotas</a>. It must be the name of a
442 locker that you are an AFS administrator of. In particular, you or an
443 AFS group you are a member of must have AFS rlidwka bits on the
444 locker. You can check who administers the LOCKER locker using the
445 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
446 href="help?subject=Administrator">administrator</a>.""",
448 The administrator field determines who can access the console and
449 power on and off the machine. This can be either a user or a moira
452 Quotas are determined on a per-locker basis. Each locker may have a
453 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
456 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
457 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
458 your machine will run just fine, but the applet's display of the
459 console will suffer artifacts.
464 subjects = sorted(help_mapping.keys())
466 d = dict(user=username,
469 mapping=help_mapping)
471 return templates.help(searchList=[d])
474 def badOperation(u, s, e):
475 """Function called when accessing an unknown URI."""
476 raise CodeError("Unknown operation")
478 def infoDict(username, state, machine):
479 """Get the variables used by info.tmpl."""
480 status = controls.statusInfo(machine)
481 checkpoint.checkpoint('Getting status info')
482 has_vnc = hasVnc(status)
484 main_status = dict(name=machine.name,
485 memory=str(machine.memory))
489 main_status = dict(status[1:])
490 start_time = float(main_status.get('start_time', 0))
491 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
492 cpu_time_float = float(main_status.get('cpu_time', 0))
493 cputime = datetime.timedelta(seconds=int(cpu_time_float))
494 checkpoint.checkpoint('Status')
495 display_fields = """name uptime memory state cpu_weight on_reboot
496 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
497 display_fields = [('name', 'Name'),
499 ('administrator', 'Administrator'),
500 ('contact', 'Contact'),
503 ('uptime', 'uptime'),
504 ('cputime', 'CPU usage'),
507 ('state', 'state (xen format)'),
508 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
509 ('on_reboot', 'Action on VM reboot'),
510 ('on_poweroff', 'Action on VM poweroff'),
511 ('on_crash', 'Action on VM crash'),
512 ('on_xend_start', 'Action on Xen start'),
513 ('on_xend_stop', 'Action on Xen stop'),
514 ('bootloader', 'Bootloader options'),
518 machine_info['name'] = machine.name
519 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
520 machine_info['owner'] = machine.owner
521 machine_info['administrator'] = machine.administrator
522 machine_info['contact'] = machine.contact
524 nic_fields = getNicInfo(machine_info, machine)
525 nic_point = display_fields.index('NIC_INFO')
526 display_fields = (display_fields[:nic_point] + nic_fields +
527 display_fields[nic_point+1:])
529 disk_fields = getDiskInfo(machine_info, machine)
530 disk_point = display_fields.index('DISK_INFO')
531 display_fields = (display_fields[:disk_point] + disk_fields +
532 display_fields[disk_point+1:])
534 main_status['memory'] += ' MiB'
535 for field, disp in display_fields:
536 if field in ('uptime', 'cputime') and locals()[field] is not None:
537 fields.append((disp, locals()[field]))
538 elif field in machine_info:
539 fields.append((disp, machine_info[field]))
540 elif field in main_status:
541 fields.append((disp, main_status[field]))
544 #fields.append((disp, None))
546 checkpoint.checkpoint('Got fields')
549 max_mem = validation.maxMemory(machine.owner, state, machine, False)
550 checkpoint.checkpoint('Got mem')
551 max_disk = validation.maxDisk(machine.owner, machine)
552 defaults = Defaults()
553 for name in 'machine_id name administrator owner memory contact'.split():
554 setattr(defaults, name, getattr(machine, name))
555 defaults.type = machine.type.type_id
556 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
557 checkpoint.checkpoint('Got defaults')
558 d = dict(user=username,
559 on=status is not None,
567 owner_help=helppopup("Owner"),
571 def info(username, state, fields):
572 """Handler for info on a single VM."""
573 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
574 d = infoDict(username, state, machine)
575 checkpoint.checkpoint('Got infodict')
576 return templates.info(searchList=[d])
578 def unauthFront(_, _2, fields):
579 """Information for unauth'd users."""
580 return templates.unauth(searchList=[{'simple' : True}])
582 mapping = dict(list=listVms,
591 def printHeaders(headers):
592 """Print a dictionary as HTTP headers."""
593 for key, value in headers.iteritems():
594 print '%s: %s' % (key, value)
598 def getUser(environ):
599 """Return the current user based on the SSL environment variables"""
600 email = environ.get('SSL_CLIENT_S_DN_Email', None)
603 if not email.endswith('@MIT.EDU'):
608 def __init__(self, environ, start_response):
609 self.environ = environ
610 self.start = start_response
612 self.username = getUser(environ)
613 self.state = State(self.username)
614 self.state.environ = environ
617 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
618 print >> sys.stderr, fields
619 operation = self.environ.get('PATH_INFO', '')
621 self.start("301 Moved Permanently", [('Location',
622 self.environ['SCRIPT_NAME']+'/')])
624 if self.username is None:
626 if operation.startswith('/'):
627 operation = operation[1:]
630 print 'Starting', operation
632 start_time = time.time()
633 fun = mapping.get(operation, badOperation)
635 checkpoint.checkpoint('Before')
636 output = fun(self.username, self.state, fields)
637 checkpoint.checkpoint('After')
639 headers = dict(DEFAULT_HEADERS)
640 if isinstance(output, tuple):
641 new_headers, output = output
642 headers.update(new_headers)
644 e = revertStandardError()
646 if isinstance(output, basestring):
647 sys.stderr = StringIO()
649 print >> sys.stderr, x
650 print >> sys.stderr, 'XXX'
651 print >> sys.stderr, e
654 output_string = str(output)
655 checkpoint.checkpoint('output as a string')
656 except Exception, err:
657 if not fields.has_key('js'):
658 if isinstance(err, CodeError):
659 self.start('500 Internal Server Error', [('Content-Type', 'text/html')])
660 e = revertStandardError()
661 s = error(operation, self.username, fields, err, e)
664 if isinstance(err, InvalidInput):
665 self.start('200 OK', [('Content-Type', 'text/html')])
666 e = revertStandardError()
667 yield str(invalidInput(operation, self.username, fields, err, e))
669 self.start('500 Internal Server Error', [('Content-Type', 'text/plain')])
671 yield '''Uh-oh! We experienced an error.'
672 Please email xvm-dev@mit.edu with the contents of this page.'
677 ----''' % (str(err), traceback.format_exc())
678 self.start('200 OK', headers.items())
680 if fields.has_key('timedebug'):
681 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
684 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
688 from flup.server.fcgi_fork import WSGIServer
689 WSGIServer(constructor()).run()
691 if __name__ == '__main__':