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)
34 sys.stderr = StringIO()
36 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
39 from Cheetah.Template import Template
40 import sipb_xen_database
41 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
44 from webcommon import InvalidInput, CodeError, State
49 self.start_time = time.time()
52 def checkpoint(self, s):
53 self.checkpoints.append((s, time.time()))
56 return ('Timing info:\n%s\n' %
57 '\n'.join(['%s: %s' % (d, t - self.start_time) for
58 (d, t) in self.checkpoints]))
60 checkpoint = Checkpoint()
63 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
66 """Return HTML code for a (?) link to a specified help topic"""
67 return ('<span class="helplink"><a href="help?' +
68 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
69 +'" target="_blank" ' +
70 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
72 def makeErrorPre(old, addition):
76 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
78 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
80 Template.sipb_xen_database = sipb_xen_database
81 Template.helppopup = staticmethod(helppopup)
85 """Class to store a dictionary that will be converted to JSON"""
86 def __init__(self, **kws):
94 return simplejson.dumps(self.data)
96 def addError(self, text):
97 """Add stderr text to be displayed on the website."""
99 makeErrorPre(self.data.get('err'), text)
102 """Class to store default values for fields."""
110 def __init__(self, max_memory=None, max_disk=None, **kws):
111 if max_memory is not None:
112 self.memory = min(self.memory, max_memory)
113 if max_disk is not None:
114 self.max_disk = min(self.disk, max_disk)
116 setattr(self, key, kws[key])
120 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
122 def error(op, username, fields, err, emsg):
123 """Print an error page when a CodeError occurs"""
124 d = dict(op=op, user=username, errorMessage=str(err),
126 return templates.error(searchList=[d])
128 def invalidInput(op, username, fields, err, emsg):
129 """Print an error page when an InvalidInput exception occurs"""
130 d = dict(op=op, user=username, err_field=err.err_field,
131 err_value=str(err.err_value), stderr=emsg,
132 errorMessage=str(err))
133 return templates.invalid(searchList=[d])
136 """Does the machine with a given status list support VNC?"""
140 if l[0] == 'device' and l[1][0] == 'vfb':
142 return 'location' in d
145 def parseCreate(username, state, fields):
146 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
147 validate = validation.Validate(username, state, strict=True, **kws)
148 return dict(contact=username, name=validate.name, memory=validate.memory,
149 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
150 cdrom=getattr(validate, 'cdrom', None),
151 clone_from=getattr(validate, 'clone_from', None))
153 def create(username, state, fields):
154 """Handler for create requests."""
156 parsed_fields = parseCreate(username, state, fields)
157 machine = controls.createVm(username, state, **parsed_fields)
158 except InvalidInput, err:
162 state.clear() #Changed global state
163 d = getListDict(username, state)
166 for field in fields.keys():
167 setattr(d['defaults'], field, fields.getfirst(field))
169 d['new_machine'] = parsed_fields['name']
170 return templates.list(searchList=[d])
173 def getListDict(username, state):
174 """Gets the list of local variables used by list.tmpl."""
175 checkpoint.checkpoint('Starting')
176 machines = state.machines
177 checkpoint.checkpoint('Got my machines')
180 xmlist = state.xmlist
181 checkpoint.checkpoint('Got uptimes')
182 can_clone = 'ice3' not in state.xmlist_raw
188 m.uptime = xmlist[m]['uptime']
189 if xmlist[m]['console']:
194 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
195 max_memory = validation.maxMemory(username, state)
196 max_disk = validation.maxDisk(username)
197 checkpoint.checkpoint('Got max mem/disk')
198 defaults = Defaults(max_memory=max_memory,
202 checkpoint.checkpoint('Got defaults')
203 def sortkey(machine):
204 return (machine.owner != username, machine.owner, machine.name)
205 machines = sorted(machines, key=sortkey)
206 d = dict(user=username,
207 cant_add_vm=validation.cantAddVm(username, state),
208 max_memory=max_memory,
216 def listVms(username, state, fields):
217 """Handler for list requests."""
218 checkpoint.checkpoint('Getting list dict')
219 d = getListDict(username, state)
220 checkpoint.checkpoint('Got list dict')
221 return templates.list(searchList=[d])
223 def vnc(username, state, fields):
226 Note that due to same-domain restrictions, the applet connects to
227 the webserver, which needs to forward those requests to the xen
228 server. The Xen server runs another proxy that (1) authenticates
229 and (2) finds the correct port for the VM.
231 You might want iptables like:
233 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
234 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
235 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
236 --dport 10003 -j SNAT --to-source 18.187.7.142
237 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
238 --dport 10003 -j ACCEPT
240 Remember to enable iptables!
241 echo 1 > /proc/sys/net/ipv4/ip_forward
243 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
245 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
248 data["user"] = username
249 data["machine"] = machine.name
250 data["expires"] = time.time()+(5*60)
251 pickled_data = cPickle.dumps(data)
252 m = hmac.new(TOKEN_KEY, digestmod=sha)
253 m.update(pickled_data)
254 token = {'data': pickled_data, 'digest': m.digest()}
255 token = cPickle.dumps(token)
256 token = base64.urlsafe_b64encode(token)
258 status = controls.statusInfo(machine)
259 has_vnc = hasVnc(status)
261 d = dict(user=username,
265 hostname=os.environ.get('SERVER_NAME', 'localhost'),
267 return templates.vnc(searchList=[d])
269 def getHostname(nic):
270 """Find the hostname associated with a NIC.
272 XXX this should be merged with the similar logic in DNS and DHCP.
274 if nic.hostname and '.' in nic.hostname:
277 return nic.machine.name + '.xvm.mit.edu'
282 def getNicInfo(data_dict, machine):
283 """Helper function for info, get data on nics for a machine.
285 Modifies data_dict to include the relevant data, and returns a list
286 of (key, name) pairs to display "name: data_dict[key]" to the user.
288 data_dict['num_nics'] = len(machine.nics)
289 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
290 ('nic%s_mac', 'NIC %s MAC Addr'),
291 ('nic%s_ip', 'NIC %s IP'),
294 for i in range(len(machine.nics)):
295 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
297 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
298 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
299 data_dict['nic%s_ip' % i] = machine.nics[i].ip
300 if len(machine.nics) == 1:
301 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
304 def getDiskInfo(data_dict, machine):
305 """Helper function for info, get data on disks for a machine.
307 Modifies data_dict to include the relevant data, and returns a list
308 of (key, name) pairs to display "name: data_dict[key]" to the user.
310 data_dict['num_disks'] = len(machine.disks)
311 disk_fields_template = [('%s_size', '%s size')]
313 for disk in machine.disks:
314 name = disk.guest_device_name
315 disk_fields.extend([(x % name, y % name) for x, y in
316 disk_fields_template])
317 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
320 def command(username, state, fields):
321 """Handler for running commands like boot and delete on a VM."""
322 back = fields.getfirst('back')
324 d = controls.commandResult(username, state, fields)
325 if d['command'] == 'Delete VM':
327 except InvalidInput, err:
330 print >> sys.stderr, err
335 return templates.command(searchList=[d])
337 state.clear() #Changed global state
338 d = getListDict(username, state)
340 return templates.list(searchList=[d])
342 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
343 return ({'Status': '302',
344 'Location': '/info?machine_id=%d' % machine.machine_id},
345 "You shouldn't see this message.")
347 raise InvalidInput('back', back, 'Not a known back page.')
349 def modifyDict(username, state, fields):
350 """Modify a machine as specified by CGI arguments.
352 Return a list of local variables for modify.tmpl.
355 transaction = ctx.current.create_transaction()
357 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
358 validate = validation.Validate(username, state, **kws)
359 machine = validate.machine
360 oldname = machine.name
362 if hasattr(validate, 'memory'):
363 machine.memory = validate.memory
365 if hasattr(validate, 'vmtype'):
366 machine.type = validate.vmtype
368 if hasattr(validate, 'disksize'):
369 disksize = validate.disksize
370 disk = machine.disks[0]
371 if disk.size != disksize:
372 olddisk[disk.guest_device_name] = disksize
374 ctx.current.save(disk)
377 if hasattr(validate, 'owner') and validate.owner != machine.owner:
378 machine.owner = validate.owner
380 if hasattr(validate, 'name'):
382 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
383 machine.administrator = validate.admin
385 if hasattr(validate, 'contact'):
386 machine.contact = validate.contact
388 ctx.current.save(machine)
390 print >> sys.stderr, machine, machine.administrator
391 cache_acls.refreshMachine(machine)
394 transaction.rollback()
396 for diskname in olddisk:
397 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
398 if hasattr(validate, 'name'):
399 controls.renameMachine(machine, oldname, validate.name)
400 return dict(user=username,
404 def modify(username, state, fields):
405 """Handler for modifying attributes of a machine."""
407 modify_dict = modifyDict(username, state, fields)
408 except InvalidInput, err:
410 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
412 machine = modify_dict['machine']
415 info_dict = infoDict(username, machine)
416 info_dict['err'] = err
418 for field in fields.keys():
419 setattr(info_dict['defaults'], field, fields.getfirst(field))
420 info_dict['result'] = result
421 return templates.info(searchList=[info_dict])
424 def helpHandler(username, state, fields):
425 """Handler for help messages."""
426 simple = fields.getfirst('simple')
427 subjects = fields.getlist('subject')
429 help_mapping = {'ParaVM Console': """
430 ParaVM machines do not support local console access over VNC. To
431 access the serial console of these machines, you can SSH with Kerberos
432 to console.xvm.mit.edu, using the name of the machine as your
435 HVM machines use the virtualization features of the processor, while
436 ParaVM machines use Xen's emulation of virtualization features. You
437 want an HVM virtualized machine.""",
439 Don't ask us! We're as mystified as you are.""",
441 The owner field is used to determine <a
442 href="help?subject=Quotas">quotas</a>. It must be the name of a
443 locker that you are an AFS administrator of. In particular, you or an
444 AFS group you are a member of must have AFS rlidwka bits on the
445 locker. You can check who administers the LOCKER locker using the
446 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
447 href="help?subject=Administrator">administrator</a>.""",
449 The administrator field determines who can access the console and
450 power on and off the machine. This can be either a user or a moira
453 Quotas are determined on a per-locker basis. Each locker may have a
454 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
457 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
458 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
459 your machine will run just fine, but the applet's display of the
460 console will suffer artifacts.
465 subjects = sorted(help_mapping.keys())
467 d = dict(user=username,
470 mapping=help_mapping)
472 return templates.help(searchList=[d])
475 def badOperation(u, s, e):
476 """Function called when accessing an unknown URI."""
477 raise CodeError("Unknown operation")
479 def infoDict(username, state, machine):
480 """Get the variables used by info.tmpl."""
481 status = controls.statusInfo(machine)
482 checkpoint.checkpoint('Getting status info')
483 has_vnc = hasVnc(status)
485 main_status = dict(name=machine.name,
486 memory=str(machine.memory))
490 main_status = dict(status[1:])
491 start_time = float(main_status.get('start_time', 0))
492 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
493 cpu_time_float = float(main_status.get('cpu_time', 0))
494 cputime = datetime.timedelta(seconds=int(cpu_time_float))
495 checkpoint.checkpoint('Status')
496 display_fields = """name uptime memory state cpu_weight on_reboot
497 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
498 display_fields = [('name', 'Name'),
500 ('administrator', 'Administrator'),
501 ('contact', 'Contact'),
504 ('uptime', 'uptime'),
505 ('cputime', 'CPU usage'),
508 ('state', 'state (xen format)'),
509 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
510 ('on_reboot', 'Action on VM reboot'),
511 ('on_poweroff', 'Action on VM poweroff'),
512 ('on_crash', 'Action on VM crash'),
513 ('on_xend_start', 'Action on Xen start'),
514 ('on_xend_stop', 'Action on Xen stop'),
515 ('bootloader', 'Bootloader options'),
519 machine_info['name'] = machine.name
520 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
521 machine_info['owner'] = machine.owner
522 machine_info['administrator'] = machine.administrator
523 machine_info['contact'] = machine.contact
525 nic_fields = getNicInfo(machine_info, machine)
526 nic_point = display_fields.index('NIC_INFO')
527 display_fields = (display_fields[:nic_point] + nic_fields +
528 display_fields[nic_point+1:])
530 disk_fields = getDiskInfo(machine_info, machine)
531 disk_point = display_fields.index('DISK_INFO')
532 display_fields = (display_fields[:disk_point] + disk_fields +
533 display_fields[disk_point+1:])
535 main_status['memory'] += ' MiB'
536 for field, disp in display_fields:
537 if field in ('uptime', 'cputime') and locals()[field] is not None:
538 fields.append((disp, locals()[field]))
539 elif field in machine_info:
540 fields.append((disp, machine_info[field]))
541 elif field in main_status:
542 fields.append((disp, main_status[field]))
545 #fields.append((disp, None))
547 checkpoint.checkpoint('Got fields')
550 max_mem = validation.maxMemory(machine.owner, state, machine, False)
551 checkpoint.checkpoint('Got mem')
552 max_disk = validation.maxDisk(machine.owner, machine)
553 defaults = Defaults()
554 for name in 'machine_id name administrator owner memory contact'.split():
555 setattr(defaults, name, getattr(machine, name))
556 defaults.type = machine.type.type_id
557 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
558 checkpoint.checkpoint('Got defaults')
559 d = dict(user=username,
560 on=status is not None,
568 owner_help=helppopup("Owner"),
572 def info(username, state, fields):
573 """Handler for info on a single VM."""
574 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
575 d = infoDict(username, state, machine)
576 checkpoint.checkpoint('Got infodict')
577 return templates.info(searchList=[d])
579 def unauthFront(_, _2, fields):
580 """Information for unauth'd users."""
581 return templates.unauth(searchList=[{'simple' : True}])
583 mapping = dict(list=listVms,
592 def printHeaders(headers):
593 """Print a dictionary as HTTP headers."""
594 for key, value in headers.iteritems():
595 print '%s: %s' % (key, value)
599 def getUser(environ):
600 """Return the current user based on the SSL environment variables"""
601 email = environ.get('SSL_CLIENT_S_DN_Email', None)
604 if not email.endswith('@MIT.EDU'):
609 def __init__(self, environ, start_response):
610 self.environ = environ
611 self.start = start_response
613 self.username = getUser(environ)
614 self.state = State(self.username)
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 os.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__':