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, g
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()
63 """Return HTML code for a (?) link to a specified help topic"""
64 return ('<span class="helplink"><a href="help?subject=' + subj +
65 '&simple=true" target="_blank" ' +
66 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
68 def makeErrorPre(old, addition):
72 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
74 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
76 Template.sipb_xen_database = sipb_xen_database
77 Template.helppopup = staticmethod(helppopup)
81 """Class to store a dictionary that will be converted to JSON"""
82 def __init__(self, **kws):
90 return simplejson.dumps(self.data)
92 def addError(self, text):
93 """Add stderr text to be displayed on the website."""
95 makeErrorPre(self.data.get('err'), text)
98 """Class to store default values for fields."""
104 def __init__(self, max_memory=None, max_disk=None, **kws):
105 self.type = Type.get('linux-hvm')
106 if max_memory is not None:
107 self.memory = min(self.memory, max_memory)
108 if max_disk is not None:
109 self.max_disk = min(self.disk, max_disk)
111 setattr(self, key, kws[key])
115 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
117 def error(op, user, fields, err, emsg):
118 """Print an error page when a CodeError occurs"""
119 d = dict(op=op, user=user, errorMessage=str(err),
121 return templates.error(searchList=[d])
123 def invalidInput(op, user, fields, err, emsg):
124 """Print an error page when an InvalidInput exception occurs"""
125 d = dict(op=op, user=user, err_field=err.err_field,
126 err_value=str(err.err_value), stderr=emsg,
127 errorMessage=str(err))
128 return templates.invalid(searchList=[d])
131 """Does the machine with a given status list support VNC?"""
135 if l[0] == 'device' and l[1][0] == 'vfb':
137 return 'location' in d
140 def parseCreate(user, fields):
141 name = fields.getfirst('name')
142 if not validation.validMachineName(name):
143 raise InvalidInput('name', name, 'You must provide a machine name. Max 22 chars, alnum plus \'-\' and \'_\'.')
146 if Machine.get_by(name=name):
147 raise InvalidInput('name', name,
148 "Name already exists.")
150 owner = validation.testOwner(user, fields.getfirst('owner'))
152 memory = fields.getfirst('memory')
153 memory = validation.validMemory(owner, memory, on=True)
155 disk_size = fields.getfirst('disk')
156 disk_size = validation.validDisk(owner, disk_size)
158 vm_type = fields.getfirst('vmtype')
159 vm_type = validation.validVmType(vm_type)
161 cdrom = fields.getfirst('cdrom')
162 if cdrom is not None and not CDROM.get(cdrom):
163 raise CodeError("Invalid cdrom type '%s'" % cdrom)
165 clone_from = fields.getfirst('clone_from')
166 if clone_from and clone_from != 'ice3':
167 raise CodeError("Invalid clone image '%s'" % clone_from)
169 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
170 owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
172 def create(user, fields):
173 """Handler for create requests."""
175 parsed_fields = parseCreate(user, fields)
176 machine = controls.createVm(**parsed_fields)
177 except InvalidInput, err:
181 g.clear() #Changed global state
182 d = getListDict(user)
185 for field in fields.keys():
186 setattr(d['defaults'], field, fields.getfirst(field))
188 d['new_machine'] = parsed_fields['name']
189 return templates.list(searchList=[d])
192 def getListDict(user):
193 """Gets the list of local variables used by list.tmpl."""
194 machines = g.machines
195 checkpoint.checkpoint('Got my machines')
199 checkpoint.checkpoint('Got uptimes')
201 m.uptime = g.uptimes.get(m)
207 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
208 max_memory = validation.maxMemory(user)
209 max_disk = validation.maxDisk(user)
210 checkpoint.checkpoint('Got max mem/disk')
211 defaults = Defaults(max_memory=max_memory,
215 checkpoint.checkpoint('Got defaults')
216 def sortkey(machine):
217 return (machine.owner != user, machine.owner, machine.name)
218 machines = sorted(machines, key=sortkey)
220 cant_add_vm=validation.cantAddVm(user),
221 max_memory=max_memory,
229 def listVms(user, fields):
230 """Handler for list requests."""
231 checkpoint.checkpoint('Getting list dict')
232 d = getListDict(user)
233 checkpoint.checkpoint('Got list dict')
234 return templates.list(searchList=[d])
236 def vnc(user, fields):
239 Note that due to same-domain restrictions, the applet connects to
240 the webserver, which needs to forward those requests to the xen
241 server. The Xen server runs another proxy that (1) authenticates
242 and (2) finds the correct port for the VM.
244 You might want iptables like:
246 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
247 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
248 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
249 --dport 10003 -j SNAT --to-source 18.187.7.142
250 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
251 --dport 10003 -j ACCEPT
253 Remember to enable iptables!
254 echo 1 > /proc/sys/net/ipv4/ip_forward
256 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
258 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
262 data["machine"] = machine.name
263 data["expires"] = time.time()+(5*60)
264 pickled_data = cPickle.dumps(data)
265 m = hmac.new(TOKEN_KEY, digestmod=sha)
266 m.update(pickled_data)
267 token = {'data': pickled_data, 'digest': m.digest()}
268 token = cPickle.dumps(token)
269 token = base64.urlsafe_b64encode(token)
271 status = controls.statusInfo(machine)
272 has_vnc = hasVnc(status)
278 hostname=os.environ.get('SERVER_NAME', 'localhost'),
280 return templates.vnc(searchList=[d])
282 def getHostname(nic):
283 """Find the hostname associated with a NIC.
285 XXX this should be merged with the similar logic in DNS and DHCP.
287 if nic.hostname and '.' in nic.hostname:
290 return nic.machine.name + '.servers.csail.mit.edu'
295 def getNicInfo(data_dict, machine):
296 """Helper function for info, get data on nics for a machine.
298 Modifies data_dict to include the relevant data, and returns a list
299 of (key, name) pairs to display "name: data_dict[key]" to the user.
301 data_dict['num_nics'] = len(machine.nics)
302 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
303 ('nic%s_mac', 'NIC %s MAC Addr'),
304 ('nic%s_ip', 'NIC %s IP'),
307 for i in range(len(machine.nics)):
308 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
310 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
311 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
312 data_dict['nic%s_ip' % i] = machine.nics[i].ip
313 if len(machine.nics) == 1:
314 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
317 def getDiskInfo(data_dict, machine):
318 """Helper function for info, get data on disks for a machine.
320 Modifies data_dict to include the relevant data, and returns a list
321 of (key, name) pairs to display "name: data_dict[key]" to the user.
323 data_dict['num_disks'] = len(machine.disks)
324 disk_fields_template = [('%s_size', '%s size')]
326 for disk in machine.disks:
327 name = disk.guest_device_name
328 disk_fields.extend([(x % name, y % name) for x, y in
329 disk_fields_template])
330 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
333 def command(user, fields):
334 """Handler for running commands like boot and delete on a VM."""
335 back = fields.getfirst('back')
337 d = controls.commandResult(user, fields)
338 if d['command'] == 'Delete VM':
340 except InvalidInput, err:
343 #print >> sys.stderr, err
348 return templates.command(searchList=[d])
350 g.clear() #Changed global state
351 d = getListDict(user)
353 return templates.list(searchList=[d])
355 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
356 return ({'Status': '302',
357 'Location': '/info?machine_id=%d' % machine.machine_id},
358 "You shouldn't see this message.")
360 raise InvalidInput('back', back, 'Not a known back page.')
362 def modifyDict(user, fields):
363 """Modify a machine as specified by CGI arguments.
365 Return a list of local variables for modify.tmpl.
368 transaction = ctx.current.create_transaction()
370 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
371 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
372 admin = validation.testAdmin(user, fields.getfirst('administrator'),
374 contact = validation.testContact(user, fields.getfirst('contact'),
376 name = validation.testName(user, fields.getfirst('name'), machine)
377 oldname = machine.name
380 memory = fields.getfirst('memory')
381 if memory is not None:
382 memory = validation.validMemory(user, memory, machine, on=False)
383 machine.memory = memory
385 vm_type = validation.validVmType(fields.getfirst('vmtype'))
386 if vm_type is not None:
387 machine.type = vm_type
389 disksize = validation.testDisk(user, fields.getfirst('disk'))
390 if disksize is not None:
391 disksize = validation.validDisk(user, disksize, machine)
392 disk = machine.disks[0]
393 if disk.size != disksize:
394 olddisk[disk.guest_device_name] = disksize
396 ctx.current.save(disk)
399 if owner is not None and owner != machine.owner:
400 machine.owner = owner
404 if admin is not None and admin != machine.administrator:
405 machine.administrator = admin
407 if contact is not None:
408 machine.contact = contact
410 ctx.current.save(machine)
412 cache_acls.refreshMachine(machine)
415 transaction.rollback()
417 for diskname in olddisk:
418 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
420 controls.renameMachine(machine, oldname, name)
421 return dict(user=user,
425 def modify(user, fields):
426 """Handler for modifying attributes of a machine."""
428 modify_dict = modifyDict(user, fields)
429 except InvalidInput, err:
431 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
433 machine = modify_dict['machine']
436 info_dict = infoDict(user, machine)
437 info_dict['err'] = err
439 for field in fields.keys():
440 setattr(info_dict['defaults'], field, fields.getfirst(field))
441 info_dict['result'] = result
442 return templates.info(searchList=[info_dict])
445 def helpHandler(user, fields):
446 """Handler for help messages."""
447 simple = fields.getfirst('simple')
448 subjects = fields.getlist('subject')
450 help_mapping = dict(paravm_console="""
451 ParaVM machines do not support local console access over VNC. To
452 access the serial console of these machines, you can SSH with Kerberos
453 to sipb-xen-console.mit.edu, using the name of the machine as your
456 HVM machines use the virtualization features of the processor, while
457 ParaVM machines use Xen's emulation of virtualization features. You
458 want an HVM virtualized machine.""",
460 Don't ask us! We're as mystified as you are.""",
462 The owner field is used to determine <a
463 href="help?subject=quotas">quotas</a>. It must be the name of a
464 locker that you are an AFS administrator of. In particular, you or an
465 AFS group you are a member of must have AFS rlidwka bits on the
466 locker. You can check who administers the LOCKER locker using the
467 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
468 href="help?subject=administrator">administrator</a>.""",
470 The administrator field determines who can access the console and
471 power on and off the machine. This can be either a user or a moira
474 Quotas are determined on a per-locker basis. Each locker may have a
475 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
478 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
479 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
480 your machine will run just fine, but the applet's display of the
481 console will suffer artifacts.
486 subjects = sorted(help_mapping.keys())
491 mapping=help_mapping)
493 return templates.help(searchList=[d])
496 def badOperation(u, e):
497 """Function called when accessing an unknown URI."""
498 raise CodeError("Unknown operation")
500 def infoDict(user, machine):
501 """Get the variables used by info.tmpl."""
502 status = controls.statusInfo(machine)
503 checkpoint.checkpoint('Getting status info')
504 has_vnc = hasVnc(status)
506 main_status = dict(name=machine.name,
507 memory=str(machine.memory))
511 main_status = dict(status[1:])
512 start_time = float(main_status.get('start_time', 0))
513 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
514 cpu_time_float = float(main_status.get('cpu_time', 0))
515 cputime = datetime.timedelta(seconds=int(cpu_time_float))
516 checkpoint.checkpoint('Status')
517 display_fields = """name uptime memory state cpu_weight on_reboot
518 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
519 display_fields = [('name', 'Name'),
521 ('administrator', 'Administrator'),
522 ('contact', 'Contact'),
525 ('uptime', 'uptime'),
526 ('cputime', 'CPU usage'),
529 ('state', 'state (xen format)'),
530 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
531 ('on_reboot', 'Action on VM reboot'),
532 ('on_poweroff', 'Action on VM poweroff'),
533 ('on_crash', 'Action on VM crash'),
534 ('on_xend_start', 'Action on Xen start'),
535 ('on_xend_stop', 'Action on Xen stop'),
536 ('bootloader', 'Bootloader options'),
540 machine_info['name'] = machine.name
541 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
542 machine_info['owner'] = machine.owner
543 machine_info['administrator'] = machine.administrator
544 machine_info['contact'] = machine.contact
546 nic_fields = getNicInfo(machine_info, machine)
547 nic_point = display_fields.index('NIC_INFO')
548 display_fields = (display_fields[:nic_point] + nic_fields +
549 display_fields[nic_point+1:])
551 disk_fields = getDiskInfo(machine_info, machine)
552 disk_point = display_fields.index('DISK_INFO')
553 display_fields = (display_fields[:disk_point] + disk_fields +
554 display_fields[disk_point+1:])
556 main_status['memory'] += ' MiB'
557 for field, disp in display_fields:
558 if field in ('uptime', 'cputime') and locals()[field] is not None:
559 fields.append((disp, locals()[field]))
560 elif field in machine_info:
561 fields.append((disp, machine_info[field]))
562 elif field in main_status:
563 fields.append((disp, main_status[field]))
566 #fields.append((disp, None))
568 checkpoint.checkpoint('Got fields')
571 max_mem = validation.maxMemory(user, machine, False)
572 checkpoint.checkpoint('Got mem')
573 max_disk = validation.maxDisk(user, machine)
574 defaults = Defaults()
575 for name in 'machine_id name administrator owner memory contact type'.split():
576 setattr(defaults, name, getattr(machine, name))
577 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
578 checkpoint.checkpoint('Got defaults')
580 on=status is not None,
588 owner_help=helppopup("owner"),
592 def info(user, fields):
593 """Handler for info on a single VM."""
594 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
595 d = infoDict(user, machine)
596 checkpoint.checkpoint('Got infodict')
597 return templates.info(searchList=[d])
599 mapping = dict(list=listVms,
607 def printHeaders(headers):
608 """Print a dictionary as HTTP headers."""
609 for key, value in headers.iteritems():
610 print '%s: %s' % (key, value)
615 """Return the current user based on the SSL environment variables"""
616 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
619 def main(operation, user, fields):
620 start_time = time.time()
621 fun = mapping.get(operation, badOperation)
623 if fun not in (helpHandler, ):
624 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
626 checkpoint.checkpoint('Before')
627 output = fun(u, fields)
628 checkpoint.checkpoint('After')
630 headers = dict(DEFAULT_HEADERS)
631 if isinstance(output, tuple):
632 new_headers, output = output
633 headers.update(new_headers)
634 e = revertStandardError()
637 printHeaders(headers)
638 output_string = str(output)
639 checkpoint.checkpoint('output as a string')
641 print '<!-- <pre>%s</pre> -->' % checkpoint
642 except Exception, err:
643 if not fields.has_key('js'):
644 if isinstance(err, CodeError):
645 print 'Content-Type: text/html\n'
646 e = revertStandardError()
647 print error(operation, u, fields, err, e)
649 if isinstance(err, InvalidInput):
650 print 'Content-Type: text/html\n'
651 e = revertStandardError()
652 print invalidInput(operation, u, fields, err, e)
654 print 'Content-Type: text/plain\n'
655 print 'Uh-oh! We experienced an error.'
656 print 'Please email sipb-xen@mit.edu with the contents of this page.'
658 e = revertStandardError()
663 if __name__ == '__main__':
664 fields = cgi.FieldStorage()
667 operation = os.environ.get('PATH_INFO', '')
669 print "Status: 301 Moved Permanently"
670 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
673 if operation.startswith('/'):
674 operation = operation[1:]
678 if os.getenv("SIPB_XEN_PROFILE"):
680 profile.run('main(operation, u, fields)', 'log-'+operation)
682 main(operation, u, fields)