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. Max 22 chars, alnum plus \'-\' and \'_\'.')
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 vm_type = validation.validVmType(vm_type)
157 cdrom = fields.getfirst('cdrom')
158 if cdrom is not None and not CDROM.get(cdrom):
159 raise CodeError("Invalid cdrom type '%s'" % cdrom)
161 clone_from = fields.getfirst('clone_from')
162 if clone_from and clone_from != 'ice3':
163 raise CodeError("Invalid clone image '%s'" % clone_from)
165 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
166 owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
168 def create(user, fields):
169 """Handler for create requests."""
171 parsed_fields = parseCreate(user, fields)
172 machine = controls.createVm(**parsed_fields)
173 except InvalidInput, err:
177 g.clear() #Changed global state
178 d = getListDict(user)
181 for field in fields.keys():
182 setattr(d['defaults'], field, fields.getfirst(field))
184 d['new_machine'] = parsed_fields['name']
185 return templates.list(searchList=[d])
188 def getListDict(user):
189 machines = g.machines
190 checkpoint.checkpoint('Got my machines')
194 checkpoint.checkpoint('Got uptimes')
196 m.uptime = g.uptimes.get(m)
202 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
203 max_memory = validation.maxMemory(user)
204 max_disk = validation.maxDisk(user)
205 checkpoint.checkpoint('Got max mem/disk')
206 defaults = Defaults(max_memory=max_memory,
210 checkpoint.checkpoint('Got defaults')
211 def sortkey(machine):
212 return (machine.owner != user, machine.owner, machine.name)
213 machines = sorted(machines, key=sortkey)
215 cant_add_vm=validation.cantAddVm(user),
216 max_memory=max_memory,
222 cdroms=CDROM.select())
225 def listVms(user, fields):
226 """Handler for list requests."""
227 checkpoint.checkpoint('Getting list dict')
228 d = getListDict(user)
229 checkpoint.checkpoint('Got list dict')
230 return templates.list(searchList=[d])
232 def vnc(user, fields):
235 Note that due to same-domain restrictions, the applet connects to
236 the webserver, which needs to forward those requests to the xen
237 server. The Xen server runs another proxy that (1) authenticates
238 and (2) finds the correct port for the VM.
240 You might want iptables like:
242 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
243 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
244 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
245 --dport 10003 -j SNAT --to-source 18.187.7.142
246 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
247 --dport 10003 -j ACCEPT
249 Remember to enable iptables!
250 echo 1 > /proc/sys/net/ipv4/ip_forward
252 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
254 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
258 data["machine"] = machine.name
259 data["expires"] = time.time()+(5*60)
260 pickled_data = cPickle.dumps(data)
261 m = hmac.new(TOKEN_KEY, digestmod=sha)
262 m.update(pickled_data)
263 token = {'data': pickled_data, 'digest': m.digest()}
264 token = cPickle.dumps(token)
265 token = base64.urlsafe_b64encode(token)
267 status = controls.statusInfo(machine)
268 has_vnc = hasVnc(status)
274 hostname=os.environ.get('SERVER_NAME', 'localhost'),
276 return templates.vnc(searchList=[d])
278 def getHostname(nic):
279 if nic.hostname and '.' in nic.hostname:
282 return nic.machine.name + '.servers.csail.mit.edu'
287 def getNicInfo(data_dict, machine):
288 """Helper function for info, get data on nics for a machine.
290 Modifies data_dict to include the relevant data, and returns a list
291 of (key, name) pairs to display "name: data_dict[key]" to the user.
293 data_dict['num_nics'] = len(machine.nics)
294 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
295 ('nic%s_mac', 'NIC %s MAC Addr'),
296 ('nic%s_ip', 'NIC %s IP'),
299 for i in range(len(machine.nics)):
300 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
302 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
303 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
304 data_dict['nic%s_ip' % i] = machine.nics[i].ip
305 if len(machine.nics) == 1:
306 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
309 def getDiskInfo(data_dict, machine):
310 """Helper function for info, get data on disks for a machine.
312 Modifies data_dict to include the relevant data, and returns a list
313 of (key, name) pairs to display "name: data_dict[key]" to the user.
315 data_dict['num_disks'] = len(machine.disks)
316 disk_fields_template = [('%s_size', '%s size')]
318 for disk in machine.disks:
319 name = disk.guest_device_name
320 disk_fields.extend([(x % name, y % name) for x, y in
321 disk_fields_template])
322 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
325 def command(user, fields):
326 """Handler for running commands like boot and delete on a VM."""
327 back = fields.getfirst('back')
329 d = controls.commandResult(user, fields)
330 if d['command'] == 'Delete VM':
332 except InvalidInput, err:
335 #print >> sys.stderr, err
340 return templates.command(searchList=[d])
342 g.clear() #Changed global state
343 d = getListDict(user)
345 return templates.list(searchList=[d])
347 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
348 return ({'Status': '302',
349 'Location': '/info?machine_id=%d' % machine.machine_id},
350 "You shouldn't see this message.")
352 raise InvalidInput('back', back, 'Not a known back page.')
354 def modifyDict(user, fields):
356 transaction = ctx.current.create_transaction()
358 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
359 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
360 admin = validation.testAdmin(user, fields.getfirst('administrator'),
362 contact = validation.testContact(user, fields.getfirst('contact'),
364 name = validation.testName(user, fields.getfirst('name'), machine)
365 oldname = machine.name
368 memory = fields.getfirst('memory')
369 if memory is not None:
370 memory = validation.validMemory(user, memory, machine, on=False)
371 machine.memory = memory
373 disksize = validation.testDisk(user, fields.getfirst('disk'))
374 if disksize is not None:
375 disksize = validation.validDisk(user, disksize, machine)
376 disk = machine.disks[0]
377 if disk.size != disksize:
378 olddisk[disk.guest_device_name] = disksize
380 ctx.current.save(disk)
382 if owner is not None:
383 machine.owner = owner
386 if admin is not None:
387 machine.administrator = admin
388 if contact is not None:
389 machine.contact = contact
391 ctx.current.save(machine)
394 transaction.rollback()
396 for diskname in olddisk:
397 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
399 controls.renameMachine(machine, oldname, name)
400 return dict(user=user,
404 def modify(user, fields):
405 """Handler for modifying attributes of a machine."""
407 modify_dict = modifyDict(user, fields)
408 except InvalidInput, err:
410 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
412 machine = modify_dict['machine']
415 info_dict = infoDict(user, machine)
416 info_dict['err'] = err
418 for field in fields.keys():
419 setattr(info_dict['defaults'], field, fields.getfirst(field))
420 info_dict['result'] = result
421 return templates.info(searchList=[info_dict])
424 def helpHandler(user, fields):
425 """Handler for help messages."""
426 simple = fields.getfirst('simple')
427 subjects = fields.getlist('subject')
429 help_mapping = dict(paravm_console="""
430 ParaVM machines do not support local console access over VNC. To
431 access the serial console of these machines, you can SSH with Kerberos
432 to sipb-xen-console.mit.edu, using the name of the machine as your
435 HVM machines use the virtualization features of the processor, while
436 ParaVM machines use Xen's emulation of virtualization features. You
437 want an HVM virtualized machine.""",
439 Don't ask us! We're as mystified as you are.""",
441 The owner field is used to determine <a
442 href="help?subject=quotas">quotas</a>. It must be the name of a
443 locker that you are an AFS administrator of. In particular, you or an
444 AFS group you are a member of must have AFS rlidwka bits on the
445 locker. You can check who administers the LOCKER locker using the
446 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
447 href="help?subject=administrator">administrator</a>.""",
449 The administrator field determines who can access the console and
450 power on and off the machine. This can be either a user or a moira
453 Quotas are determined on a per-locker basis. Each locker may have a
454 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
457 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
458 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
459 your machine will run just fine, but the applet's display of the
460 console will suffer artifacts.
465 subjects = sorted(help_mapping.keys())
470 mapping=help_mapping)
472 return templates.help(searchList=[d])
475 def badOperation(u, e):
476 raise CodeError("Unknown operation")
478 def infoDict(user, machine):
479 status = controls.statusInfo(machine)
480 checkpoint.checkpoint('Getting status info')
481 has_vnc = hasVnc(status)
483 main_status = dict(name=machine.name,
484 memory=str(machine.memory))
488 main_status = dict(status[1:])
489 start_time = float(main_status.get('start_time', 0))
490 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
491 cpu_time_float = float(main_status.get('cpu_time', 0))
492 cputime = datetime.timedelta(seconds=int(cpu_time_float))
493 checkpoint.checkpoint('Status')
494 display_fields = """name uptime memory state cpu_weight on_reboot
495 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
496 display_fields = [('name', 'Name'),
498 ('administrator', 'Administrator'),
499 ('contact', 'Contact'),
502 ('uptime', 'uptime'),
503 ('cputime', 'CPU usage'),
506 ('state', 'state (xen format)'),
507 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
508 ('on_reboot', 'Action on VM reboot'),
509 ('on_poweroff', 'Action on VM poweroff'),
510 ('on_crash', 'Action on VM crash'),
511 ('on_xend_start', 'Action on Xen start'),
512 ('on_xend_stop', 'Action on Xen stop'),
513 ('bootloader', 'Bootloader options'),
517 machine_info['name'] = machine.name
518 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
519 machine_info['owner'] = machine.owner
520 machine_info['administrator'] = machine.administrator
521 machine_info['contact'] = machine.contact
523 nic_fields = getNicInfo(machine_info, machine)
524 nic_point = display_fields.index('NIC_INFO')
525 display_fields = (display_fields[:nic_point] + nic_fields +
526 display_fields[nic_point+1:])
528 disk_fields = getDiskInfo(machine_info, machine)
529 disk_point = display_fields.index('DISK_INFO')
530 display_fields = (display_fields[:disk_point] + disk_fields +
531 display_fields[disk_point+1:])
533 main_status['memory'] += ' MiB'
534 for field, disp in display_fields:
535 if field in ('uptime', 'cputime') and locals()[field] is not None:
536 fields.append((disp, locals()[field]))
537 elif field in machine_info:
538 fields.append((disp, machine_info[field]))
539 elif field in main_status:
540 fields.append((disp, main_status[field]))
543 #fields.append((disp, None))
545 checkpoint.checkpoint('Got fields')
548 max_mem = validation.maxMemory(user, machine, False)
549 checkpoint.checkpoint('Got mem')
550 max_disk = validation.maxDisk(user, machine)
551 defaults = Defaults()
552 for name in 'machine_id name administrator owner memory contact'.split():
553 setattr(defaults, name, getattr(machine, name))
554 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
555 checkpoint.checkpoint('Got defaults')
557 cdroms=CDROM.select(),
558 on=status is not None,
566 owner_help=helppopup("owner"),
570 def info(user, fields):
571 """Handler for info on a single VM."""
572 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
573 d = infoDict(user, machine)
574 checkpoint.checkpoint('Got infodict')
575 return templates.info(searchList=[d])
577 mapping = dict(list=listVms,
585 def printHeaders(headers):
586 for key, value in headers.iteritems():
587 print '%s: %s' % (key, value)
592 """Return the current user based on the SSL environment variables"""
593 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
596 def main(operation, user, fields):
597 start_time = time.time()
598 fun = mapping.get(operation, badOperation)
600 if fun not in (helpHandler, ):
601 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
603 checkpoint.checkpoint('Before')
604 output = fun(u, fields)
605 checkpoint.checkpoint('After')
607 headers = dict(DEFAULT_HEADERS)
608 if isinstance(output, tuple):
609 new_headers, output = output
610 headers.update(new_headers)
611 e = revertStandardError()
614 printHeaders(headers)
615 output_string = str(output)
616 checkpoint.checkpoint('output as a string')
618 print '<!-- <pre>%s</pre> -->' % checkpoint
619 except Exception, err:
620 if not fields.has_key('js'):
621 if isinstance(err, CodeError):
622 print 'Content-Type: text/html\n'
623 e = revertStandardError()
624 print error(operation, u, fields, err, e)
626 if isinstance(err, InvalidInput):
627 print 'Content-Type: text/html\n'
628 e = revertStandardError()
629 print invalidInput(operation, u, fields, err, e)
631 print 'Content-Type: text/plain\n'
632 print 'Uh-oh! We experienced an error.'
633 print 'Please email sipb-xen@mit.edu with the contents of this page.'
635 e = revertStandardError()
640 if __name__ == '__main__':
641 fields = cgi.FieldStorage()
644 operation = os.environ.get('PATH_INFO', '')
646 print "Status: 301 Moved Permanently"
647 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
650 if operation.startswith('/'):
651 operation = operation[1:]
655 if os.getenv("SIPB_XEN_PROFILE"):
657 profile.run('main(operation, u, fields)', 'log-'+operation)
659 main(operation, u, fields)