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, **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, **parsed_fields)
158 except InvalidInput, err:
162 state.clear() #Changed global state
163 d = getListDict(username)
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)
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 print >> sys.stderr, machine, machine.administrator, kws['admin']
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'):
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, 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, e):
477 """Function called when accessing an unknown URI."""
478 raise CodeError("Unknown operation")
480 def infoDict(username, 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, 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 mapping = dict(list=listVms,
593 def printHeaders(headers):
594 """Print a dictionary as HTTP headers."""
595 for key, value in headers.iteritems():
596 print '%s: %s' % (key, value)
600 def getUser(environ):
601 """Return the current user based on the SSL environment variables"""
602 email = environ.get('SSL_CLIENT_S_DN_Email', None)
605 if not email.endswith('@MIT.EDU'):
609 def main(operation, username, state, fields):
610 start_time = time.time()
611 fun = mapping.get(operation, badOperation)
613 if fun not in (helpHandler, ):
614 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
616 checkpoint.checkpoint('Before')
617 output = fun(username, state, fields)
618 checkpoint.checkpoint('After')
620 headers = dict(DEFAULT_HEADERS)
621 if isinstance(output, tuple):
622 new_headers, output = output
623 headers.update(new_headers)
624 e = revertStandardError()
626 if isinstance(output, basestring):
627 sys.stderr = StringIO()
629 print >> sys.stderr, x
630 print >> sys.stderr, 'XXX'
631 print >> sys.stderr, e
634 printHeaders(headers)
635 output_string = str(output)
636 checkpoint.checkpoint('output as a string')
638 if fields.has_key('timedebug'):
639 print '<pre>%s</pre>' % cgi.escape(checkpoint)
640 except Exception, err:
641 if not fields.has_key('js'):
642 if isinstance(err, CodeError):
643 print 'Content-Type: text/html\n'
644 e = revertStandardError()
645 print error(operation, state.username, fields, err, e)
647 if isinstance(err, InvalidInput):
648 print 'Content-Type: text/html\n'
649 e = revertStandardError()
650 print invalidInput(operation, state.username, fields, err, e)
652 print 'Content-Type: text/plain\n'
653 print 'Uh-oh! We experienced an error.'
654 print 'Please email xvm-dev@mit.edu with the contents of this page.'
656 e = revertStandardError()
661 if __name__ == '__main__':
662 fields = cgi.FieldStorage()
664 if fields.has_key('sqldebug'):
666 logging.basicConfig()
667 logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
668 logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.INFO)
670 username = getUser(os.environ)
671 state.username = username
672 operation = os.environ.get('PATH_INFO', '')
674 print "Status: 301 Moved Permanently"
675 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
679 if operation.startswith('/'):
680 operation = operation[1:]
684 if os.getenv("SIPB_XEN_PROFILE"):
686 profile.run('main(operation, username, state, fields)', 'log-'+operation)
688 main(operation, username, state, fields)