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, g
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, user, fields, err, emsg):
123 """Print an error page when a CodeError occurs"""
124 d = dict(op=op, user=user, errorMessage=str(err),
126 return templates.error(searchList=[d])
128 def invalidInput(op, user, fields, err, emsg):
129 """Print an error page when an InvalidInput exception occurs"""
130 d = dict(op=op, user=user, 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(user, fields):
146 name = fields.getfirst('name')
147 if not validation.validMachineName(name):
148 raise InvalidInput('name', name, 'You must provide a machine name. Max 22 chars, alnum plus \'-\' and \'_\'.')
151 if Machine.get_by(name=name):
152 raise InvalidInput('name', name,
153 "Name already exists.")
155 owner = validation.testOwner(user, fields.getfirst('owner'))
157 memory = fields.getfirst('memory')
158 memory = validation.validMemory(owner, memory, on=True)
160 disk_size = fields.getfirst('disk')
161 disk_size = validation.validDisk(owner, disk_size)
163 vm_type = fields.getfirst('vmtype')
164 vm_type = validation.validVmType(vm_type)
166 cdrom = fields.getfirst('cdrom')
167 if cdrom is not None and not CDROM.get(cdrom):
168 raise CodeError("Invalid cdrom type '%s'" % cdrom)
170 clone_from = fields.getfirst('clone_from')
171 if clone_from and clone_from != 'ice3':
172 raise CodeError("Invalid clone image '%s'" % clone_from)
174 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
175 owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
177 def create(user, fields):
178 """Handler for create requests."""
180 parsed_fields = parseCreate(user, fields)
181 machine = controls.createVm(**parsed_fields)
182 except InvalidInput, err:
186 g.clear() #Changed global state
187 d = getListDict(user)
190 for field in fields.keys():
191 setattr(d['defaults'], field, fields.getfirst(field))
193 d['new_machine'] = parsed_fields['name']
194 return templates.list(searchList=[d])
197 def getListDict(user):
198 """Gets the list of local variables used by list.tmpl."""
199 checkpoint.checkpoint('Starting')
200 machines = g.machines
201 checkpoint.checkpoint('Got my machines')
205 checkpoint.checkpoint('Got uptimes')
206 can_clone = 'ice3' not in g.xmlist_raw
212 m.uptime = xmlist[m]['uptime']
213 if xmlist[m]['console']:
218 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
219 max_memory = validation.maxMemory(user)
220 max_disk = validation.maxDisk(user)
221 checkpoint.checkpoint('Got max mem/disk')
222 defaults = Defaults(max_memory=max_memory,
226 checkpoint.checkpoint('Got defaults')
227 def sortkey(machine):
228 return (machine.owner != user, machine.owner, machine.name)
229 machines = sorted(machines, key=sortkey)
231 cant_add_vm=validation.cantAddVm(user),
232 max_memory=max_memory,
240 def listVms(user, fields):
241 """Handler for list requests."""
242 checkpoint.checkpoint('Getting list dict')
243 d = getListDict(user)
244 checkpoint.checkpoint('Got list dict')
245 return templates.list(searchList=[d])
247 def vnc(user, fields):
250 Note that due to same-domain restrictions, the applet connects to
251 the webserver, which needs to forward those requests to the xen
252 server. The Xen server runs another proxy that (1) authenticates
253 and (2) finds the correct port for the VM.
255 You might want iptables like:
257 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
258 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
259 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
260 --dport 10003 -j SNAT --to-source 18.187.7.142
261 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
262 --dport 10003 -j ACCEPT
264 Remember to enable iptables!
265 echo 1 > /proc/sys/net/ipv4/ip_forward
267 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
269 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
273 data["machine"] = machine.name
274 data["expires"] = time.time()+(5*60)
275 pickled_data = cPickle.dumps(data)
276 m = hmac.new(TOKEN_KEY, digestmod=sha)
277 m.update(pickled_data)
278 token = {'data': pickled_data, 'digest': m.digest()}
279 token = cPickle.dumps(token)
280 token = base64.urlsafe_b64encode(token)
282 status = controls.statusInfo(machine)
283 has_vnc = hasVnc(status)
289 hostname=os.environ.get('SERVER_NAME', 'localhost'),
291 return templates.vnc(searchList=[d])
293 def getHostname(nic):
294 """Find the hostname associated with a NIC.
296 XXX this should be merged with the similar logic in DNS and DHCP.
298 if nic.hostname and '.' in nic.hostname:
301 return nic.machine.name + '.xvm.mit.edu'
306 def getNicInfo(data_dict, machine):
307 """Helper function for info, get data on nics for a machine.
309 Modifies data_dict to include the relevant data, and returns a list
310 of (key, name) pairs to display "name: data_dict[key]" to the user.
312 data_dict['num_nics'] = len(machine.nics)
313 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
314 ('nic%s_mac', 'NIC %s MAC Addr'),
315 ('nic%s_ip', 'NIC %s IP'),
318 for i in range(len(machine.nics)):
319 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
321 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
322 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
323 data_dict['nic%s_ip' % i] = machine.nics[i].ip
324 if len(machine.nics) == 1:
325 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
328 def getDiskInfo(data_dict, machine):
329 """Helper function for info, get data on disks for a machine.
331 Modifies data_dict to include the relevant data, and returns a list
332 of (key, name) pairs to display "name: data_dict[key]" to the user.
334 data_dict['num_disks'] = len(machine.disks)
335 disk_fields_template = [('%s_size', '%s size')]
337 for disk in machine.disks:
338 name = disk.guest_device_name
339 disk_fields.extend([(x % name, y % name) for x, y in
340 disk_fields_template])
341 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
344 def command(user, fields):
345 """Handler for running commands like boot and delete on a VM."""
346 back = fields.getfirst('back')
348 d = controls.commandResult(user, fields)
349 if d['command'] == 'Delete VM':
351 except InvalidInput, err:
354 #print >> sys.stderr, err
359 return templates.command(searchList=[d])
361 g.clear() #Changed global state
362 d = getListDict(user)
364 return templates.list(searchList=[d])
366 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
367 return ({'Status': '302',
368 'Location': '/info?machine_id=%d' % machine.machine_id},
369 "You shouldn't see this message.")
371 raise InvalidInput('back', back, 'Not a known back page.')
373 def modifyDict(user, fields):
374 """Modify a machine as specified by CGI arguments.
376 Return a list of local variables for modify.tmpl.
379 transaction = ctx.current.create_transaction()
381 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
382 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
383 admin = validation.testAdmin(user, fields.getfirst('administrator'),
385 contact = validation.testContact(user, fields.getfirst('contact'),
387 name = validation.testName(user, fields.getfirst('name'), machine)
388 oldname = machine.name
391 memory = fields.getfirst('memory')
392 if memory is not None:
393 memory = validation.validMemory(user, memory, machine, on=False)
394 machine.memory = memory
396 vm_type = validation.validVmType(fields.getfirst('vmtype'))
397 if vm_type is not None:
398 machine.type = vm_type
400 disksize = validation.testDisk(user, fields.getfirst('disk'))
401 if disksize is not None:
402 disksize = validation.validDisk(user, disksize, machine)
403 disk = machine.disks[0]
404 if disk.size != disksize:
405 olddisk[disk.guest_device_name] = disksize
407 ctx.current.save(disk)
410 if owner is not None and owner != machine.owner:
411 machine.owner = owner
415 if admin is not None and admin != machine.administrator:
416 machine.administrator = admin
418 if contact is not None:
419 machine.contact = contact
421 ctx.current.save(machine)
423 cache_acls.refreshMachine(machine)
426 transaction.rollback()
428 for diskname in olddisk:
429 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
431 controls.renameMachine(machine, oldname, name)
432 return dict(user=user,
436 def modify(user, fields):
437 """Handler for modifying attributes of a machine."""
439 modify_dict = modifyDict(user, fields)
440 except InvalidInput, err:
442 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
444 machine = modify_dict['machine']
447 info_dict = infoDict(user, machine)
448 info_dict['err'] = err
450 for field in fields.keys():
451 setattr(info_dict['defaults'], field, fields.getfirst(field))
452 info_dict['result'] = result
453 return templates.info(searchList=[info_dict])
456 def helpHandler(user, fields):
457 """Handler for help messages."""
458 simple = fields.getfirst('simple')
459 subjects = fields.getlist('subject')
461 help_mapping = {'ParaVM Console': """
462 ParaVM machines do not support local console access over VNC. To
463 access the serial console of these machines, you can SSH with Kerberos
464 to console.xvm.mit.edu, using the name of the machine as your
467 HVM machines use the virtualization features of the processor, while
468 ParaVM machines use Xen's emulation of virtualization features. You
469 want an HVM virtualized machine.""",
471 Don't ask us! We're as mystified as you are.""",
473 The owner field is used to determine <a
474 href="help?subject=Quotas">quotas</a>. It must be the name of a
475 locker that you are an AFS administrator of. In particular, you or an
476 AFS group you are a member of must have AFS rlidwka bits on the
477 locker. You can check who administers the LOCKER locker using the
478 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
479 href="help?subject=Administrator">administrator</a>.""",
481 The administrator field determines who can access the console and
482 power on and off the machine. This can be either a user or a moira
485 Quotas are determined on a per-locker basis. Each locker may have a
486 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
489 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
490 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
491 your machine will run just fine, but the applet's display of the
492 console will suffer artifacts.
497 subjects = sorted(help_mapping.keys())
502 mapping=help_mapping)
504 return templates.help(searchList=[d])
507 def badOperation(u, e):
508 """Function called when accessing an unknown URI."""
509 raise CodeError("Unknown operation")
511 def infoDict(user, machine):
512 """Get the variables used by info.tmpl."""
513 status = controls.statusInfo(machine)
514 checkpoint.checkpoint('Getting status info')
515 has_vnc = hasVnc(status)
517 main_status = dict(name=machine.name,
518 memory=str(machine.memory))
522 main_status = dict(status[1:])
523 start_time = float(main_status.get('start_time', 0))
524 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
525 cpu_time_float = float(main_status.get('cpu_time', 0))
526 cputime = datetime.timedelta(seconds=int(cpu_time_float))
527 checkpoint.checkpoint('Status')
528 display_fields = """name uptime memory state cpu_weight on_reboot
529 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
530 display_fields = [('name', 'Name'),
532 ('administrator', 'Administrator'),
533 ('contact', 'Contact'),
536 ('uptime', 'uptime'),
537 ('cputime', 'CPU usage'),
540 ('state', 'state (xen format)'),
541 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
542 ('on_reboot', 'Action on VM reboot'),
543 ('on_poweroff', 'Action on VM poweroff'),
544 ('on_crash', 'Action on VM crash'),
545 ('on_xend_start', 'Action on Xen start'),
546 ('on_xend_stop', 'Action on Xen stop'),
547 ('bootloader', 'Bootloader options'),
551 machine_info['name'] = machine.name
552 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
553 machine_info['owner'] = machine.owner
554 machine_info['administrator'] = machine.administrator
555 machine_info['contact'] = machine.contact
557 nic_fields = getNicInfo(machine_info, machine)
558 nic_point = display_fields.index('NIC_INFO')
559 display_fields = (display_fields[:nic_point] + nic_fields +
560 display_fields[nic_point+1:])
562 disk_fields = getDiskInfo(machine_info, machine)
563 disk_point = display_fields.index('DISK_INFO')
564 display_fields = (display_fields[:disk_point] + disk_fields +
565 display_fields[disk_point+1:])
567 main_status['memory'] += ' MiB'
568 for field, disp in display_fields:
569 if field in ('uptime', 'cputime') and locals()[field] is not None:
570 fields.append((disp, locals()[field]))
571 elif field in machine_info:
572 fields.append((disp, machine_info[field]))
573 elif field in main_status:
574 fields.append((disp, main_status[field]))
577 #fields.append((disp, None))
579 checkpoint.checkpoint('Got fields')
582 max_mem = validation.maxMemory(user, machine, False)
583 checkpoint.checkpoint('Got mem')
584 max_disk = validation.maxDisk(user, machine)
585 defaults = Defaults()
586 for name in 'machine_id name administrator owner memory contact'.split():
587 setattr(defaults, name, getattr(machine, name))
588 defaults.type = machine.type.type_id
589 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
590 checkpoint.checkpoint('Got defaults')
592 on=status is not None,
600 owner_help=helppopup("Owner"),
604 def info(user, fields):
605 """Handler for info on a single VM."""
606 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
607 d = infoDict(user, machine)
608 checkpoint.checkpoint('Got infodict')
609 return templates.info(searchList=[d])
611 def unauthFront(_, fields):
612 """Information for unauth'd users."""
613 return templates.unauth(searchList=[{'simple' : True}])
615 mapping = dict(list=listVms,
624 def printHeaders(headers):
625 """Print a dictionary as HTTP headers."""
626 for key, value in headers.iteritems():
627 print '%s: %s' % (key, value)
632 """Return the current user based on the SSL environment variables"""
633 email = os.environ.get('SSL_CLIENT_S_DN_Email', None)
636 return email.split("@")[0]
638 def main(operation, user, fields):
639 start_time = time.time()
640 fun = mapping.get(operation, badOperation)
642 if fun not in (helpHandler, ):
643 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
645 checkpoint.checkpoint('Before')
646 output = fun(u, fields)
647 checkpoint.checkpoint('After')
649 headers = dict(DEFAULT_HEADERS)
650 if isinstance(output, tuple):
651 new_headers, output = output
652 headers.update(new_headers)
653 e = revertStandardError()
656 printHeaders(headers)
657 output_string = str(output)
658 checkpoint.checkpoint('output as a string')
660 if fields.has_key('timedebug'):
661 print '<pre>%s</pre>' % checkpoint
662 except Exception, err:
663 if not fields.has_key('js'):
664 if isinstance(err, CodeError):
665 print 'Content-Type: text/html\n'
666 e = revertStandardError()
667 print error(operation, u, fields, err, e)
669 if isinstance(err, InvalidInput):
670 print 'Content-Type: text/html\n'
671 e = revertStandardError()
672 print invalidInput(operation, u, fields, err, e)
674 print 'Content-Type: text/plain\n'
675 print 'Uh-oh! We experienced an error.'
676 print 'Please email xvm-dev@mit.edu with the contents of this page.'
678 e = revertStandardError()
683 if __name__ == '__main__':
684 fields = cgi.FieldStorage()
686 if fields.has_key('sqldebug'):
688 logging.basicConfig()
689 logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
690 logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.INFO)
694 operation = os.environ.get('PATH_INFO', '')
696 print "Status: 301 Moved Permanently"
697 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
703 if operation.startswith('/'):
704 operation = operation[1:]
708 if os.getenv("SIPB_XEN_PROFILE"):
710 profile.run('main(operation, u, fields)', 'log-'+operation)
712 main(operation, u, fields)