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')
211 m.uptime = xmlist[m]['uptime']
212 if xmlist[m]['console']:
217 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
218 max_memory = validation.maxMemory(user)
219 max_disk = validation.maxDisk(user)
220 checkpoint.checkpoint('Got max mem/disk')
221 defaults = Defaults(max_memory=max_memory,
225 checkpoint.checkpoint('Got defaults')
226 def sortkey(machine):
227 return (machine.owner != user, machine.owner, machine.name)
228 machines = sorted(machines, key=sortkey)
230 cant_add_vm=validation.cantAddVm(user),
231 max_memory=max_memory,
238 def listVms(user, fields):
239 """Handler for list requests."""
240 checkpoint.checkpoint('Getting list dict')
241 d = getListDict(user)
242 checkpoint.checkpoint('Got list dict')
243 return templates.list(searchList=[d])
245 def vnc(user, fields):
248 Note that due to same-domain restrictions, the applet connects to
249 the webserver, which needs to forward those requests to the xen
250 server. The Xen server runs another proxy that (1) authenticates
251 and (2) finds the correct port for the VM.
253 You might want iptables like:
255 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
256 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
257 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
258 --dport 10003 -j SNAT --to-source 18.187.7.142
259 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
260 --dport 10003 -j ACCEPT
262 Remember to enable iptables!
263 echo 1 > /proc/sys/net/ipv4/ip_forward
265 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
267 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
271 data["machine"] = machine.name
272 data["expires"] = time.time()+(5*60)
273 pickled_data = cPickle.dumps(data)
274 m = hmac.new(TOKEN_KEY, digestmod=sha)
275 m.update(pickled_data)
276 token = {'data': pickled_data, 'digest': m.digest()}
277 token = cPickle.dumps(token)
278 token = base64.urlsafe_b64encode(token)
280 status = controls.statusInfo(machine)
281 has_vnc = hasVnc(status)
287 hostname=os.environ.get('SERVER_NAME', 'localhost'),
289 return templates.vnc(searchList=[d])
291 def getHostname(nic):
292 """Find the hostname associated with a NIC.
294 XXX this should be merged with the similar logic in DNS and DHCP.
296 if nic.hostname and '.' in nic.hostname:
299 return nic.machine.name + '.xvm.mit.edu'
304 def getNicInfo(data_dict, machine):
305 """Helper function for info, get data on nics 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_nics'] = len(machine.nics)
311 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
312 ('nic%s_mac', 'NIC %s MAC Addr'),
313 ('nic%s_ip', 'NIC %s IP'),
316 for i in range(len(machine.nics)):
317 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
319 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
320 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
321 data_dict['nic%s_ip' % i] = machine.nics[i].ip
322 if len(machine.nics) == 1:
323 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
326 def getDiskInfo(data_dict, machine):
327 """Helper function for info, get data on disks for a machine.
329 Modifies data_dict to include the relevant data, and returns a list
330 of (key, name) pairs to display "name: data_dict[key]" to the user.
332 data_dict['num_disks'] = len(machine.disks)
333 disk_fields_template = [('%s_size', '%s size')]
335 for disk in machine.disks:
336 name = disk.guest_device_name
337 disk_fields.extend([(x % name, y % name) for x, y in
338 disk_fields_template])
339 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
342 def command(user, fields):
343 """Handler for running commands like boot and delete on a VM."""
344 back = fields.getfirst('back')
346 d = controls.commandResult(user, fields)
347 if d['command'] == 'Delete VM':
349 except InvalidInput, err:
352 #print >> sys.stderr, err
357 return templates.command(searchList=[d])
359 g.clear() #Changed global state
360 d = getListDict(user)
362 return templates.list(searchList=[d])
364 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
365 return ({'Status': '302',
366 'Location': '/info?machine_id=%d' % machine.machine_id},
367 "You shouldn't see this message.")
369 raise InvalidInput('back', back, 'Not a known back page.')
371 def modifyDict(user, fields):
372 """Modify a machine as specified by CGI arguments.
374 Return a list of local variables for modify.tmpl.
377 transaction = ctx.current.create_transaction()
379 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
380 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
381 admin = validation.testAdmin(user, fields.getfirst('administrator'),
383 contact = validation.testContact(user, fields.getfirst('contact'),
385 name = validation.testName(user, fields.getfirst('name'), machine)
386 oldname = machine.name
389 memory = fields.getfirst('memory')
390 if memory is not None:
391 memory = validation.validMemory(user, memory, machine, on=False)
392 machine.memory = memory
394 vm_type = validation.validVmType(fields.getfirst('vmtype'))
395 if vm_type is not None:
396 machine.type = vm_type
398 disksize = validation.testDisk(user, fields.getfirst('disk'))
399 if disksize is not None:
400 disksize = validation.validDisk(user, disksize, machine)
401 disk = machine.disks[0]
402 if disk.size != disksize:
403 olddisk[disk.guest_device_name] = disksize
405 ctx.current.save(disk)
408 if owner is not None and owner != machine.owner:
409 machine.owner = owner
413 if admin is not None and admin != machine.administrator:
414 machine.administrator = admin
416 if contact is not None:
417 machine.contact = contact
419 ctx.current.save(machine)
421 cache_acls.refreshMachine(machine)
424 transaction.rollback()
426 for diskname in olddisk:
427 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
429 controls.renameMachine(machine, oldname, name)
430 return dict(user=user,
434 def modify(user, fields):
435 """Handler for modifying attributes of a machine."""
437 modify_dict = modifyDict(user, fields)
438 except InvalidInput, err:
440 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
442 machine = modify_dict['machine']
445 info_dict = infoDict(user, machine)
446 info_dict['err'] = err
448 for field in fields.keys():
449 setattr(info_dict['defaults'], field, fields.getfirst(field))
450 info_dict['result'] = result
451 return templates.info(searchList=[info_dict])
454 def helpHandler(user, fields):
455 """Handler for help messages."""
456 simple = fields.getfirst('simple')
457 subjects = fields.getlist('subject')
459 help_mapping = {'ParaVM Console': """
460 ParaVM machines do not support local console access over VNC. To
461 access the serial console of these machines, you can SSH with Kerberos
462 to console.xvm.mit.edu, using the name of the machine as your
465 HVM machines use the virtualization features of the processor, while
466 ParaVM machines use Xen's emulation of virtualization features. You
467 want an HVM virtualized machine.""",
469 Don't ask us! We're as mystified as you are.""",
471 The owner field is used to determine <a
472 href="help?subject=Quotas">quotas</a>. It must be the name of a
473 locker that you are an AFS administrator of. In particular, you or an
474 AFS group you are a member of must have AFS rlidwka bits on the
475 locker. You can check who administers the LOCKER locker using the
476 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
477 href="help?subject=Administrator">administrator</a>.""",
479 The administrator field determines who can access the console and
480 power on and off the machine. This can be either a user or a moira
483 Quotas are determined on a per-locker basis. Each locker may have a
484 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
487 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
488 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
489 your machine will run just fine, but the applet's display of the
490 console will suffer artifacts.
495 subjects = sorted(help_mapping.keys())
500 mapping=help_mapping)
502 return templates.help(searchList=[d])
505 def badOperation(u, e):
506 """Function called when accessing an unknown URI."""
507 raise CodeError("Unknown operation")
509 def infoDict(user, machine):
510 """Get the variables used by info.tmpl."""
511 status = controls.statusInfo(machine)
512 checkpoint.checkpoint('Getting status info')
513 has_vnc = hasVnc(status)
515 main_status = dict(name=machine.name,
516 memory=str(machine.memory))
520 main_status = dict(status[1:])
521 start_time = float(main_status.get('start_time', 0))
522 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
523 cpu_time_float = float(main_status.get('cpu_time', 0))
524 cputime = datetime.timedelta(seconds=int(cpu_time_float))
525 checkpoint.checkpoint('Status')
526 display_fields = """name uptime memory state cpu_weight on_reboot
527 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
528 display_fields = [('name', 'Name'),
530 ('administrator', 'Administrator'),
531 ('contact', 'Contact'),
534 ('uptime', 'uptime'),
535 ('cputime', 'CPU usage'),
538 ('state', 'state (xen format)'),
539 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
540 ('on_reboot', 'Action on VM reboot'),
541 ('on_poweroff', 'Action on VM poweroff'),
542 ('on_crash', 'Action on VM crash'),
543 ('on_xend_start', 'Action on Xen start'),
544 ('on_xend_stop', 'Action on Xen stop'),
545 ('bootloader', 'Bootloader options'),
549 machine_info['name'] = machine.name
550 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
551 machine_info['owner'] = machine.owner
552 machine_info['administrator'] = machine.administrator
553 machine_info['contact'] = machine.contact
555 nic_fields = getNicInfo(machine_info, machine)
556 nic_point = display_fields.index('NIC_INFO')
557 display_fields = (display_fields[:nic_point] + nic_fields +
558 display_fields[nic_point+1:])
560 disk_fields = getDiskInfo(machine_info, machine)
561 disk_point = display_fields.index('DISK_INFO')
562 display_fields = (display_fields[:disk_point] + disk_fields +
563 display_fields[disk_point+1:])
565 main_status['memory'] += ' MiB'
566 for field, disp in display_fields:
567 if field in ('uptime', 'cputime') and locals()[field] is not None:
568 fields.append((disp, locals()[field]))
569 elif field in machine_info:
570 fields.append((disp, machine_info[field]))
571 elif field in main_status:
572 fields.append((disp, main_status[field]))
575 #fields.append((disp, None))
577 checkpoint.checkpoint('Got fields')
580 max_mem = validation.maxMemory(user, machine, False)
581 checkpoint.checkpoint('Got mem')
582 max_disk = validation.maxDisk(user, machine)
583 defaults = Defaults()
584 for name in 'machine_id name administrator owner memory contact'.split():
585 setattr(defaults, name, getattr(machine, name))
586 defaults.type = machine.type.type_id
587 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
588 checkpoint.checkpoint('Got defaults')
590 on=status is not None,
598 owner_help=helppopup("Owner"),
602 def info(user, fields):
603 """Handler for info on a single VM."""
604 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
605 d = infoDict(user, machine)
606 checkpoint.checkpoint('Got infodict')
607 return templates.info(searchList=[d])
609 def unauthFront(_, fields):
610 """Information for unauth'd users."""
611 return templates.unauth(searchList=[{'simple' : True}])
613 mapping = dict(list=listVms,
622 def printHeaders(headers):
623 """Print a dictionary as HTTP headers."""
624 for key, value in headers.iteritems():
625 print '%s: %s' % (key, value)
630 """Return the current user based on the SSL environment variables"""
631 email = os.environ.get('SSL_CLIENT_S_DN_Email', None)
634 return email.split("@")[0]
636 def main(operation, user, fields):
637 start_time = time.time()
638 fun = mapping.get(operation, badOperation)
640 if fun not in (helpHandler, ):
641 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
643 checkpoint.checkpoint('Before')
644 output = fun(u, fields)
645 checkpoint.checkpoint('After')
647 headers = dict(DEFAULT_HEADERS)
648 if isinstance(output, tuple):
649 new_headers, output = output
650 headers.update(new_headers)
651 e = revertStandardError()
654 printHeaders(headers)
655 output_string = str(output)
656 checkpoint.checkpoint('output as a string')
658 if fields.has_key('timedebug'):
659 print '<pre>%s</pre>' % checkpoint
660 except Exception, err:
661 if not fields.has_key('js'):
662 if isinstance(err, CodeError):
663 print 'Content-Type: text/html\n'
664 e = revertStandardError()
665 print error(operation, u, fields, err, e)
667 if isinstance(err, InvalidInput):
668 print 'Content-Type: text/html\n'
669 e = revertStandardError()
670 print invalidInput(operation, u, fields, err, e)
672 print 'Content-Type: text/plain\n'
673 print 'Uh-oh! We experienced an error.'
674 print 'Please email xvm-dev@mit.edu with the contents of this page.'
676 e = revertStandardError()
681 if __name__ == '__main__':
682 fields = cgi.FieldStorage()
684 if fields.has_key('sqldebug'):
686 logging.basicConfig()
687 logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
688 logging.getLogger('sqlalchemy.orm.unitofwork').setLevel(logging.INFO)
692 operation = os.environ.get('PATH_INFO', '')
694 print "Status: 301 Moved Permanently"
695 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
701 if operation.startswith('/'):
702 operation = operation[1:]
706 if os.getenv("SIPB_XEN_PROFILE"):
708 profile.run('main(operation, u, fields)', 'log-'+operation)
710 main(operation, u, fields)