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 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess
41 from webcommon import InvalidInput, CodeError, g
46 self.start_time = time.time()
49 def checkpoint(self, s):
50 self.checkpoints.append((s, time.time()))
53 return ('Timing info:\n%s\n' %
54 '\n'.join(['%s: %s' % (d, t - self.start_time) for
55 (d, t) in self.checkpoints]))
57 checkpoint = Checkpoint()
61 """Return HTML code for a (?) link to a specified help topic"""
62 return ('<span class="helplink"><a href="help?subject=' + subj +
63 '&simple=true" target="_blank" ' +
64 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
66 def makeErrorPre(old, addition):
70 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
72 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
74 Template.helppopup = staticmethod(helppopup)
78 """Class to store a dictionary that will be converted to JSON"""
79 def __init__(self, **kws):
87 return simplejson.dumps(self.data)
89 def addError(self, text):
90 """Add stderr text to be displayed on the website."""
92 makeErrorPre(self.data.get('err'), text)
95 """Class to store default values for fields."""
101 def __init__(self, max_memory=None, max_disk=None, **kws):
102 if max_memory is not None:
103 self.memory = min(self.memory, max_memory)
104 if max_disk is not None:
105 self.max_disk = min(self.disk, max_disk)
107 setattr(self, key, kws[key])
111 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
113 def error(op, user, fields, err, emsg):
114 """Print an error page when a CodeError occurs"""
115 d = dict(op=op, user=user, errorMessage=str(err),
117 return templates.error(searchList=[d])
119 def invalidInput(op, user, fields, err, emsg):
120 """Print an error page when an InvalidInput exception occurs"""
121 d = dict(op=op, user=user, err_field=err.err_field,
122 err_value=str(err.err_value), stderr=emsg,
123 errorMessage=str(err))
124 return templates.invalid(searchList=[d])
127 """Does the machine with a given status list support VNC?"""
131 if l[0] == 'device' and l[1][0] == 'vfb':
133 return 'location' in d
136 def parseCreate(user, fields):
137 name = fields.getfirst('name')
138 if not validation.validMachineName(name):
139 raise InvalidInput('name', name, 'You must provide a machine name.')
142 if Machine.get_by(name=name):
143 raise InvalidInput('name', name,
144 "Name already exists.")
146 owner = validation.testOwner(user, fields.getfirst('owner'))
148 memory = fields.getfirst('memory')
149 memory = validation.validMemory(owner, memory, on=True)
151 disk_size = fields.getfirst('disk')
152 disk_size = validation.validDisk(owner, disk_size)
154 vm_type = fields.getfirst('vmtype')
155 if vm_type not in ('hvm', 'paravm'):
156 raise CodeError("Invalid vm type '%s'" % vm_type)
157 is_hvm = (vm_type == 'hvm')
159 cdrom = fields.getfirst('cdrom')
160 if cdrom is not None and not CDROM.get(cdrom):
161 raise CodeError("Invalid cdrom type '%s'" % cdrom)
163 clone_from = fields.getfirst('clone_from')
164 if clone_from and clone_from != 'ice3':
165 raise CodeError("Invalid clone image '%s'" % clone_from)
167 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
168 owner=owner, is_hvm=is_hvm, cdrom=cdrom, clone_from=clone_from)
170 def create(user, fields):
171 """Handler for create requests."""
173 parsed_fields = parseCreate(user, fields)
174 machine = controls.createVm(**parsed_fields)
175 except InvalidInput, err:
179 g.clear() #Changed global state
180 d = getListDict(user)
183 for field in fields.keys():
184 setattr(d['defaults'], field, fields.getfirst(field))
186 d['new_machine'] = parsed_fields['name']
187 return templates.list(searchList=[d])
190 def getListDict(user):
191 machines = g.machines
192 checkpoint.checkpoint('Got my machines')
196 checkpoint.checkpoint('Got uptimes')
198 m.uptime = g.uptimes.get(m)
204 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
205 max_memory = validation.maxMemory(user)
206 max_disk = validation.maxDisk(user)
207 checkpoint.checkpoint('Got max mem/disk')
208 defaults = Defaults(max_memory=max_memory,
212 checkpoint.checkpoint('Got defaults')
213 def sortkey(machine):
214 return (machine.owner != user, machine.owner, machine.name)
215 machines = sorted(machines, key=sortkey)
217 cant_add_vm=validation.cantAddVm(user),
218 max_memory=max_memory,
224 cdroms=CDROM.select())
227 def listVms(user, fields):
228 """Handler for list requests."""
229 checkpoint.checkpoint('Getting list dict')
230 d = getListDict(user)
231 checkpoint.checkpoint('Got list dict')
232 return templates.list(searchList=[d])
234 def vnc(user, fields):
237 Note that due to same-domain restrictions, the applet connects to
238 the webserver, which needs to forward those requests to the xen
239 server. The Xen server runs another proxy that (1) authenticates
240 and (2) finds the correct port for the VM.
242 You might want iptables like:
244 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
245 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
246 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
247 --dport 10003 -j SNAT --to-source 18.187.7.142
248 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
249 --dport 10003 -j ACCEPT
251 Remember to enable iptables!
252 echo 1 > /proc/sys/net/ipv4/ip_forward
254 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
256 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
260 data["machine"] = machine.name
261 data["expires"] = time.time()+(5*60)
262 pickled_data = cPickle.dumps(data)
263 m = hmac.new(TOKEN_KEY, digestmod=sha)
264 m.update(pickled_data)
265 token = {'data': pickled_data, 'digest': m.digest()}
266 token = cPickle.dumps(token)
267 token = base64.urlsafe_b64encode(token)
269 status = controls.statusInfo(machine)
270 has_vnc = hasVnc(status)
276 hostname=os.environ.get('SERVER_NAME', 'localhost'),
278 return templates.vnc(searchList=[d])
280 def getHostname(nic):
281 if nic.hostname and '.' in nic.hostname:
284 return nic.machine.name + '.servers.csail.mit.edu'
289 def getNicInfo(data_dict, machine):
290 """Helper function for info, get data on nics for a machine.
292 Modifies data_dict to include the relevant data, and returns a list
293 of (key, name) pairs to display "name: data_dict[key]" to the user.
295 data_dict['num_nics'] = len(machine.nics)
296 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
297 ('nic%s_mac', 'NIC %s MAC Addr'),
298 ('nic%s_ip', 'NIC %s IP'),
301 for i in range(len(machine.nics)):
302 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
304 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
305 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
306 data_dict['nic%s_ip' % i] = machine.nics[i].ip
307 if len(machine.nics) == 1:
308 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
311 def getDiskInfo(data_dict, machine):
312 """Helper function for info, get data on disks for a machine.
314 Modifies data_dict to include the relevant data, and returns a list
315 of (key, name) pairs to display "name: data_dict[key]" to the user.
317 data_dict['num_disks'] = len(machine.disks)
318 disk_fields_template = [('%s_size', '%s size')]
320 for disk in machine.disks:
321 name = disk.guest_device_name
322 disk_fields.extend([(x % name, y % name) for x, y in
323 disk_fields_template])
324 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
327 def command(user, fields):
328 """Handler for running commands like boot and delete on a VM."""
329 back = fields.getfirst('back')
331 d = controls.commandResult(user, fields)
332 if d['command'] == 'Delete VM':
334 except InvalidInput, err:
337 #print >> sys.stderr, err
342 return templates.command(searchList=[d])
344 g.clear() #Changed global state
345 d = getListDict(user)
347 return templates.list(searchList=[d])
349 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
350 return ({'Status': '302',
351 'Location': '/info?machine_id=%d' % machine.machine_id},
352 "You shouldn't see this message.")
354 raise InvalidInput('back', back, 'Not a known back page.')
356 def modifyDict(user, fields):
358 transaction = ctx.current.create_transaction()
360 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
361 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
362 admin = validation.testAdmin(user, fields.getfirst('administrator'),
364 contact = validation.testContact(user, fields.getfirst('contact'),
366 name = validation.testName(user, fields.getfirst('name'), machine)
367 oldname = machine.name
370 memory = fields.getfirst('memory')
371 if memory is not None:
372 memory = validation.validMemory(user, memory, machine, on=False)
373 machine.memory = memory
375 disksize = validation.testDisk(user, fields.getfirst('disk'))
376 if disksize is not None:
377 disksize = validation.validDisk(user, disksize, machine)
378 disk = machine.disks[0]
379 if disk.size != disksize:
380 olddisk[disk.guest_device_name] = disksize
382 ctx.current.save(disk)
384 if owner is not None:
385 machine.owner = owner
388 if admin is not None:
389 machine.administrator = admin
390 if contact is not None:
391 machine.contact = contact
393 ctx.current.save(machine)
396 transaction.rollback()
398 for diskname in olddisk:
399 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
401 controls.renameMachine(machine, oldname, name)
402 return dict(user=user,
406 def modify(user, fields):
407 """Handler for modifying attributes of a machine."""
409 modify_dict = modifyDict(user, fields)
410 except InvalidInput, err:
412 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
414 machine = modify_dict['machine']
417 info_dict = infoDict(user, machine)
418 info_dict['err'] = err
420 for field in fields.keys():
421 setattr(info_dict['defaults'], field, fields.getfirst(field))
422 info_dict['result'] = result
423 return templates.info(searchList=[info_dict])
426 def helpHandler(user, fields):
427 """Handler for help messages."""
428 simple = fields.getfirst('simple')
429 subjects = fields.getlist('subject')
431 help_mapping = dict(paravm_console="""
432 ParaVM machines do not support console access over VNC. To access
433 these machines, you either need to boot with a liveCD and ssh in or
434 hope that the sipb-xen maintainers add support for serial consoles.""",
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 see who administers the LOCKER locker using the
447 command '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())
471 mapping=help_mapping)
473 return templates.help(searchList=[d])
476 def badOperation(u, e):
477 raise CodeError("Unknown operation")
479 def infoDict(user, machine):
480 status = controls.statusInfo(machine)
481 checkpoint.checkpoint('Getting status info')
482 has_vnc = hasVnc(status)
484 main_status = dict(name=machine.name,
485 memory=str(machine.memory))
489 main_status = dict(status[1:])
490 start_time = float(main_status.get('start_time', 0))
491 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
492 cpu_time_float = float(main_status.get('cpu_time', 0))
493 cputime = datetime.timedelta(seconds=int(cpu_time_float))
494 checkpoint.checkpoint('Status')
495 display_fields = """name uptime memory state cpu_weight on_reboot
496 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
497 display_fields = [('name', 'Name'),
499 ('administrator', 'Administrator'),
500 ('contact', 'Contact'),
503 ('uptime', 'uptime'),
504 ('cputime', 'CPU usage'),
507 ('state', 'state (xen format)'),
508 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
509 ('on_reboot', 'Action on VM reboot'),
510 ('on_poweroff', 'Action on VM poweroff'),
511 ('on_crash', 'Action on VM crash'),
512 ('on_xend_start', 'Action on Xen start'),
513 ('on_xend_stop', 'Action on Xen stop'),
514 ('bootloader', 'Bootloader options'),
518 machine_info['name'] = machine.name
519 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
520 machine_info['owner'] = machine.owner
521 machine_info['administrator'] = machine.administrator
522 machine_info['contact'] = machine.contact
524 nic_fields = getNicInfo(machine_info, machine)
525 nic_point = display_fields.index('NIC_INFO')
526 display_fields = (display_fields[:nic_point] + nic_fields +
527 display_fields[nic_point+1:])
529 disk_fields = getDiskInfo(machine_info, machine)
530 disk_point = display_fields.index('DISK_INFO')
531 display_fields = (display_fields[:disk_point] + disk_fields +
532 display_fields[disk_point+1:])
534 main_status['memory'] += ' MiB'
535 for field, disp in display_fields:
536 if field in ('uptime', 'cputime') and locals()[field] is not None:
537 fields.append((disp, locals()[field]))
538 elif field in machine_info:
539 fields.append((disp, machine_info[field]))
540 elif field in main_status:
541 fields.append((disp, main_status[field]))
544 #fields.append((disp, None))
546 checkpoint.checkpoint('Got fields')
549 max_mem = validation.maxMemory(user, machine, False)
550 checkpoint.checkpoint('Got mem')
551 max_disk = validation.maxDisk(user, machine)
552 defaults = Defaults()
553 for name in 'machine_id name administrator owner memory contact'.split():
554 setattr(defaults, name, getattr(machine, name))
555 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
556 checkpoint.checkpoint('Got defaults')
558 cdroms=CDROM.select(),
559 on=status is not None,
567 owner_help=helppopup("owner"),
571 def info(user, fields):
572 """Handler for info on a single VM."""
573 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
574 d = infoDict(user, machine)
575 checkpoint.checkpoint('Got infodict')
576 return templates.info(searchList=[d])
578 mapping = dict(list=listVms,
586 def printHeaders(headers):
587 for key, value in headers.iteritems():
588 print '%s: %s' % (key, value)
593 """Return the current user based on the SSL environment variables"""
594 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
597 def main(operation, user, fields):
598 start_time = time.time()
599 fun = mapping.get(operation, badOperation)
601 if fun not in (helpHandler, ):
602 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
604 checkpoint.checkpoint('Before')
605 output = fun(u, fields)
606 checkpoint.checkpoint('After')
608 headers = dict(DEFAULT_HEADERS)
609 if isinstance(output, tuple):
610 new_headers, output = output
611 headers.update(new_headers)
612 e = revertStandardError()
615 printHeaders(headers)
616 output_string = str(output)
617 checkpoint.checkpoint('output as a string')
619 print '<!-- <pre>%s</pre> -->' % checkpoint
620 except Exception, err:
621 if not fields.has_key('js'):
622 if isinstance(err, CodeError):
623 print 'Content-Type: text/html\n'
624 e = revertStandardError()
625 print error(operation, u, fields, err, e)
627 if isinstance(err, InvalidInput):
628 print 'Content-Type: text/html\n'
629 e = revertStandardError()
630 print invalidInput(operation, u, fields, err, e)
632 print 'Content-Type: text/plain\n'
633 print 'Uh-oh! We experienced an error.'
634 print 'Please email sipb-xen@mit.edu with the contents of this page.'
636 e = revertStandardError()
641 if __name__ == '__main__':
642 fields = cgi.FieldStorage()
645 operation = os.environ.get('PATH_INFO', '')
647 print "Status: 301 Moved Permanently"
648 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
651 if operation.startswith('/'):
652 operation = operation[1:]
656 if os.getenv("SIPB_XEN_PROFILE"):
658 profile.run('main(operation, u, fields)', 'log-'+operation)
660 main(operation, u, fields)