2 """Main CGI script for web interface"""
14 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')
38 from Cheetah.Template import Template
39 from sipb_xen_database import Machine, CDROM, ctx, connect
41 from webcommon import InvalidInput, CodeError, g
45 """Return HTML code for a (?) link to a specified help topic"""
46 return ('<span class="helplink"><a href="help?subject=' + subj +
47 '&simple=true" target="_blank" ' +
48 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
51 """User class (sort of useless, I admit)"""
52 def __init__(self, username, email):
53 self.username = username
56 def makeErrorPre(old, addition):
60 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
62 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
64 Template.helppopup = staticmethod(helppopup)
68 """Class to store a dictionary that will be converted to JSON"""
69 def __init__(self, **kws):
77 return simplejson.dumps(self.data)
79 def addError(self, text):
80 """Add stderr text to be displayed on the website."""
82 makeErrorPre(self.data.get('err'), text)
85 """Class to store default values for fields."""
91 def __init__(self, max_memory=None, max_disk=None, **kws):
92 if max_memory is not None:
93 self.memory = min(self.memory, max_memory)
94 if max_disk is not None:
95 self.max_disk = min(self.disk, max_disk)
97 setattr(self, key, kws[key])
101 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
103 def error(op, user, fields, err, emsg):
104 """Print an error page when a CodeError occurs"""
105 d = dict(op=op, user=user, errorMessage=str(err),
107 return Template(file='error.tmpl', searchList=[d])
109 def invalidInput(op, user, fields, err, emsg):
110 """Print an error page when an InvalidInput exception occurs"""
111 d = dict(op=op, user=user, err_field=err.err_field,
112 err_value=str(err.err_value), stderr=emsg,
113 errorMessage=str(err))
114 return Template(file='invalid.tmpl', searchList=[d])
117 """Does the machine with a given status list support VNC?"""
121 if l[0] == 'device' and l[1][0] == 'vfb':
123 return 'location' in d
126 def parseCreate(user, fields):
127 name = fields.getfirst('name')
128 if not validation.validMachineName(name):
129 raise InvalidInput('name', name, 'You must provide a machine name.')
132 if Machine.get_by(name=name):
133 raise InvalidInput('name', name,
134 "Name already exists.")
136 memory = fields.getfirst('memory')
137 memory = validation.validMemory(user, memory, on=True)
139 disk = fields.getfirst('disk')
140 disk = validation.validDisk(user, disk)
142 vm_type = fields.getfirst('vmtype')
143 if vm_type not in ('hvm', 'paravm'):
144 raise CodeError("Invalid vm type '%s'" % vm_type)
145 is_hvm = (vm_type == 'hvm')
147 cdrom = fields.getfirst('cdrom')
148 if cdrom is not None and not CDROM.get(cdrom):
149 raise CodeError("Invalid cdrom type '%s'" % cdrom)
150 return dict(user=user, name=name, memory=memory, disk=disk,
151 is_hvm=is_hvm, cdrom=cdrom)
153 def create(user, fields):
154 """Handler for create requests."""
156 parsed_fields = parseCreate(user, fields)
157 machine = controls.createVm(**parsed_fields)
158 except InvalidInput, err:
162 g.clear() #Changed global state
163 d = getListDict(user)
166 for field in fields.keys():
167 setattr(d['defaults'], field, fields.getfirst(field))
169 d['new_machine'] = parsed_fields['name']
170 return Template(file='list.tmpl', searchList=[d])
173 def getListDict(user):
174 machines = [m for m in Machine.select()
175 if validation.haveAccess(user, m)]
180 m.uptime = g.uptimes.get(m)
186 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
187 max_memory = validation.maxMemory(user)
188 max_disk = validation.maxDisk(user)
189 defaults = Defaults(max_memory=max_memory,
193 cant_add_vm=validation.cantAddVm(user),
194 max_memory=max_memory,
200 cdroms=CDROM.select())
203 def listVms(user, fields):
204 """Handler for list requests."""
205 d = getListDict(user)
206 return Template(file='list.tmpl', searchList=[d])
208 def vnc(user, fields):
211 Note that due to same-domain restrictions, the applet connects to
212 the webserver, which needs to forward those requests to the xen
213 server. The Xen server runs another proxy that (1) authenticates
214 and (2) finds the correct port for the VM.
216 You might want iptables like:
218 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
219 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
220 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
221 --dport 10003 -j SNAT --to-source 18.187.7.142
222 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
223 --dport 10003 -j ACCEPT
225 Remember to enable iptables!
226 echo 1 > /proc/sys/net/ipv4/ip_forward
228 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
230 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
233 data["user"] = user.username
234 data["machine"] = machine.name
235 data["expires"] = time.time()+(5*60)
236 pickled_data = cPickle.dumps(data)
237 m = hmac.new(TOKEN_KEY, digestmod=sha)
238 m.update(pickled_data)
239 token = {'data': pickled_data, 'digest': m.digest()}
240 token = cPickle.dumps(token)
241 token = base64.urlsafe_b64encode(token)
243 status = controls.statusInfo(machine)
244 has_vnc = hasVnc(status)
250 hostname=os.environ.get('SERVER_NAME', 'localhost'),
252 return Template(file='vnc.tmpl', searchList=[d])
254 def getNicInfo(data_dict, machine):
255 """Helper function for info, get data on nics for a machine.
257 Modifies data_dict to include the relevant data, and returns a list
258 of (key, name) pairs to display "name: data_dict[key]" to the user.
260 data_dict['num_nics'] = len(machine.nics)
261 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
262 ('nic%s_mac', 'NIC %s MAC Addr'),
263 ('nic%s_ip', 'NIC %s IP'),
266 for i in range(len(machine.nics)):
267 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
269 data_dict['nic%s_hostname' % i] = (machine.name +
270 '.servers.csail.mit.edu')
271 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
272 data_dict['nic%s_ip' % i] = machine.nics[i].ip
273 if len(machine.nics) == 1:
274 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
277 def getDiskInfo(data_dict, machine):
278 """Helper function for info, get data on disks for a machine.
280 Modifies data_dict to include the relevant data, and returns a list
281 of (key, name) pairs to display "name: data_dict[key]" to the user.
283 data_dict['num_disks'] = len(machine.disks)
284 disk_fields_template = [('%s_size', '%s size')]
286 for disk in machine.disks:
287 name = disk.guest_device_name
288 disk_fields.extend([(x % name, y % name) for x, y in
289 disk_fields_template])
290 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
293 def command(user, fields):
294 """Handler for running commands like boot and delete on a VM."""
295 back = fields.getfirst('back')
297 d = controls.commandResult(user, fields)
298 if d['command'] == 'Delete VM':
300 except InvalidInput, err:
303 print >> sys.stderr, err
308 return Template(file='command.tmpl', searchList=[d])
310 g.clear() #Changed global state
311 d = getListDict(user)
313 return Template(file='list.tmpl', searchList=[d])
315 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
316 d = infoDict(user, machine)
318 return Template(file='info.tmpl', searchList=[d])
320 raise InvalidInput('back', back, 'Not a known back page.')
322 def modifyDict(user, fields):
324 transaction = ctx.current.create_transaction()
326 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
327 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
328 admin = validation.testAdmin(user, fields.getfirst('administrator'),
330 contact = validation.testContact(user, fields.getfirst('contact'),
332 name = validation.testName(user, fields.getfirst('name'), machine)
333 oldname = machine.name
336 memory = fields.getfirst('memory')
337 if memory is not None:
338 memory = validation.validMemory(user, memory, machine, on=False)
339 machine.memory = memory
341 disksize = validation.testDisk(user, fields.getfirst('disk'))
342 if disksize is not None:
343 disksize = validation.validDisk(user, disksize, machine)
344 disk = machine.disks[0]
345 if disk.size != disksize:
346 olddisk[disk.guest_device_name] = disksize
348 ctx.current.save(disk)
350 if owner is not None:
351 machine.owner = owner
354 if admin is not None:
355 machine.administrator = admin
356 if contact is not None:
357 machine.contact = contact
359 ctx.current.save(machine)
362 transaction.rollback()
364 for diskname in olddisk:
365 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
367 controls.renameMachine(machine, oldname, name)
368 return dict(user=user,
372 def modify(user, fields):
373 """Handler for modifying attributes of a machine."""
375 modify_dict = modifyDict(user, fields)
376 except InvalidInput, err:
378 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
380 machine = modify_dict['machine']
383 info_dict = infoDict(user, machine)
384 info_dict['err'] = err
386 for field in fields.keys():
387 setattr(info_dict['defaults'], field, fields.getfirst(field))
388 info_dict['result'] = result
389 return Template(file='info.tmpl', searchList=[info_dict])
392 def helpHandler(user, fields):
393 """Handler for help messages."""
394 simple = fields.getfirst('simple')
395 subjects = fields.getlist('subject')
397 help_mapping = dict(paravm_console="""
398 ParaVM machines do not support console access over VNC. To access
399 these machines, you either need to boot with a liveCD and ssh in or
400 hope that the sipb-xen maintainers add support for serial consoles.""",
402 HVM machines use the virtualization features of the processor, while
403 ParaVM machines use Xen's emulation of virtualization features. You
404 want an HVM virtualized machine.""",
406 Don't ask us! We're as mystified as you are.""",
408 The owner field is used to determine <a
409 href="help?subject=quotas">quotas</a>. It must be the name of a
410 locker that you are an AFS administrator of. In particular, you or an
411 AFS group you are a member of must have AFS rlidwka bits on the
412 locker. You can check see who administers the LOCKER locker using the
413 command 'fs la /mit/LOCKER' on Athena.) See also <a
414 href="help?subject=administrator">administrator</a>.""",
416 The administrator field determines who can access the console and
417 power on and off the machine. This can be either a user or a moira
420 Quotas are determined on a per-locker basis. Each quota may have a
421 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
426 subjects = sorted(help_mapping.keys())
431 mapping=help_mapping)
433 return Template(file="help.tmpl", searchList=[d])
436 def badOperation(u, e):
437 raise CodeError("Unknown operation")
439 def infoDict(user, machine):
440 status = controls.statusInfo(machine)
441 has_vnc = hasVnc(status)
443 main_status = dict(name=machine.name,
444 memory=str(machine.memory))
448 main_status = dict(status[1:])
449 start_time = float(main_status.get('start_time', 0))
450 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
451 cpu_time_float = float(main_status.get('cpu_time', 0))
452 cputime = datetime.timedelta(seconds=int(cpu_time_float))
453 display_fields = """name uptime memory state cpu_weight on_reboot
454 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
455 display_fields = [('name', 'Name'),
457 ('administrator', 'Administrator'),
458 ('contact', 'Contact'),
461 ('uptime', 'uptime'),
462 ('cputime', 'CPU usage'),
465 ('state', 'state (xen format)'),
466 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
467 ('on_reboot', 'Action on VM reboot'),
468 ('on_poweroff', 'Action on VM poweroff'),
469 ('on_crash', 'Action on VM crash'),
470 ('on_xend_start', 'Action on Xen start'),
471 ('on_xend_stop', 'Action on Xen stop'),
472 ('bootloader', 'Bootloader options'),
476 machine_info['name'] = machine.name
477 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
478 machine_info['owner'] = machine.owner
479 machine_info['administrator'] = machine.administrator
480 machine_info['contact'] = machine.contact
482 nic_fields = getNicInfo(machine_info, machine)
483 nic_point = display_fields.index('NIC_INFO')
484 display_fields = (display_fields[:nic_point] + nic_fields +
485 display_fields[nic_point+1:])
487 disk_fields = getDiskInfo(machine_info, machine)
488 disk_point = display_fields.index('DISK_INFO')
489 display_fields = (display_fields[:disk_point] + disk_fields +
490 display_fields[disk_point+1:])
492 main_status['memory'] += ' MiB'
493 for field, disp in display_fields:
494 if field in ('uptime', 'cputime') and locals()[field] is not None:
495 fields.append((disp, locals()[field]))
496 elif field in machine_info:
497 fields.append((disp, machine_info[field]))
498 elif field in main_status:
499 fields.append((disp, main_status[field]))
502 #fields.append((disp, None))
503 max_mem = validation.maxMemory(user, machine)
504 max_disk = validation.maxDisk(user, machine)
505 defaults = Defaults()
506 for name in 'machine_id name administrator owner memory contact'.split():
507 setattr(defaults, name, getattr(machine, name))
508 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
510 cdroms=CDROM.select(),
511 on=status is not None,
519 owner_help=helppopup("owner"),
523 def info(user, fields):
524 """Handler for info on a single VM."""
525 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
526 d = infoDict(user, machine)
527 return Template(file='info.tmpl', searchList=[d])
529 mapping = dict(list=listVms,
537 def printHeaders(headers):
538 for key, value in headers.iteritems():
539 print '%s: %s' % (key, value)
544 """Return the current user based on the SSL environment variables"""
545 if 'SSL_CLIENT_S_DN_Email' in os.environ:
546 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
547 return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
549 return User('moo', 'nobody')
551 def main(operation, user, fields):
552 fun = mapping.get(operation, badOperation)
554 if fun not in (helpHandler, ):
555 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
557 output = fun(u, fields)
559 headers = dict(DEFAULT_HEADERS)
560 if isinstance(output, tuple):
561 new_headers, output = output
562 headers.update(new_headers)
564 e = revertStandardError()
567 printHeaders(headers)
569 except Exception, err:
570 if not fields.has_key('js'):
571 if isinstance(err, CodeError):
572 print 'Content-Type: text/html\n'
573 e = revertStandardError()
574 print error(operation, u, fields, err, e)
576 if isinstance(err, InvalidInput):
577 print 'Content-Type: text/html\n'
578 e = revertStandardError()
579 print invalidInput(operation, u, fields, err, e)
581 print 'Content-Type: text/plain\n'
582 print 'Uh-oh! We experienced an error.'
583 print 'Please email sipb-xen@mit.edu with the contents of this page.'
585 e = revertStandardError()
590 if __name__ == '__main__':
591 start_time = time.time()
592 fields = cgi.FieldStorage()
595 operation = os.environ.get('PATH_INFO', '')
597 print "Status: 301 Moved Permanently"
598 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
601 if operation.startswith('/'):
602 operation = operation[1:]
606 main(operation, u, fields)