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
42 from webcommon import InvalidInput, CodeError, g
47 self.start_time = time.time()
50 def checkpoint(self, s):
51 self.checkpoints.append((s, time.time()))
54 return ('Timing info:\n%s\n' %
55 '\n'.join(['%s: %s' % (d, t - self.start_time) for
56 (d, t) in self.checkpoints]))
58 checkpoint = Checkpoint()
62 """Return HTML code for a (?) link to a specified help topic"""
63 return ('<span class="helplink"><a href="help?subject=' + subj +
64 '&simple=true" target="_blank" ' +
65 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
67 def makeErrorPre(old, addition):
71 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
73 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
75 Template.sipb_xen_database = sipb_xen_database
76 Template.helppopup = staticmethod(helppopup)
80 """Class to store a dictionary that will be converted to JSON"""
81 def __init__(self, **kws):
89 return simplejson.dumps(self.data)
91 def addError(self, text):
92 """Add stderr text to be displayed on the website."""
94 makeErrorPre(self.data.get('err'), text)
97 """Class to store default values for fields."""
102 def __init__(self, max_memory=None, max_disk=None, **kws):
103 self.type = Type.get('linux-hvm')
104 if max_memory is not None:
105 self.memory = min(self.memory, max_memory)
106 if max_disk is not None:
107 self.max_disk = min(self.disk, max_disk)
109 setattr(self, key, kws[key])
113 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
115 def error(op, user, fields, err, emsg):
116 """Print an error page when a CodeError occurs"""
117 d = dict(op=op, user=user, errorMessage=str(err),
119 return templates.error(searchList=[d])
121 def invalidInput(op, user, fields, err, emsg):
122 """Print an error page when an InvalidInput exception occurs"""
123 d = dict(op=op, user=user, err_field=err.err_field,
124 err_value=str(err.err_value), stderr=emsg,
125 errorMessage=str(err))
126 return templates.invalid(searchList=[d])
129 """Does the machine with a given status list support VNC?"""
133 if l[0] == 'device' and l[1][0] == 'vfb':
135 return 'location' in d
138 def parseCreate(user, fields):
139 name = fields.getfirst('name')
140 if not validation.validMachineName(name):
141 raise InvalidInput('name', name, 'You must provide a machine name. Max 22 chars, alnum plus \'-\' and \'_\'.')
144 if Machine.get_by(name=name):
145 raise InvalidInput('name', name,
146 "Name already exists.")
148 owner = validation.testOwner(user, fields.getfirst('owner'))
150 memory = fields.getfirst('memory')
151 memory = validation.validMemory(owner, memory, on=True)
153 disk_size = fields.getfirst('disk')
154 disk_size = validation.validDisk(owner, disk_size)
156 vm_type = fields.getfirst('vmtype')
157 vm_type = validation.validVmType(vm_type)
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, machine_type=vm_type, 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 """Gets the list of local variables used by list.tmpl."""
192 machines = g.machines
193 checkpoint.checkpoint('Got my machines')
197 checkpoint.checkpoint('Got uptimes')
199 m.uptime = g.uptimes.get(m)
205 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
206 max_memory = validation.maxMemory(user)
207 max_disk = validation.maxDisk(user)
208 checkpoint.checkpoint('Got max mem/disk')
209 defaults = Defaults(max_memory=max_memory,
213 checkpoint.checkpoint('Got defaults')
214 def sortkey(machine):
215 return (machine.owner != user, machine.owner, machine.name)
216 machines = sorted(machines, key=sortkey)
218 cant_add_vm=validation.cantAddVm(user),
219 max_memory=max_memory,
225 cdroms=CDROM.select())
228 def listVms(user, fields):
229 """Handler for list requests."""
230 checkpoint.checkpoint('Getting list dict')
231 d = getListDict(user)
232 checkpoint.checkpoint('Got list dict')
233 return templates.list(searchList=[d])
235 def vnc(user, fields):
238 Note that due to same-domain restrictions, the applet connects to
239 the webserver, which needs to forward those requests to the xen
240 server. The Xen server runs another proxy that (1) authenticates
241 and (2) finds the correct port for the VM.
243 You might want iptables like:
245 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
246 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
247 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
248 --dport 10003 -j SNAT --to-source 18.187.7.142
249 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
250 --dport 10003 -j ACCEPT
252 Remember to enable iptables!
253 echo 1 > /proc/sys/net/ipv4/ip_forward
255 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
257 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
261 data["machine"] = machine.name
262 data["expires"] = time.time()+(5*60)
263 pickled_data = cPickle.dumps(data)
264 m = hmac.new(TOKEN_KEY, digestmod=sha)
265 m.update(pickled_data)
266 token = {'data': pickled_data, 'digest': m.digest()}
267 token = cPickle.dumps(token)
268 token = base64.urlsafe_b64encode(token)
270 status = controls.statusInfo(machine)
271 has_vnc = hasVnc(status)
277 hostname=os.environ.get('SERVER_NAME', 'localhost'),
279 return templates.vnc(searchList=[d])
281 def getHostname(nic):
282 """Find the hostname associated with a NIC.
284 XXX this should be merged with the similar logic in DNS and DHCP.
286 if nic.hostname and '.' in nic.hostname:
289 return nic.machine.name + '.servers.csail.mit.edu'
294 def getNicInfo(data_dict, machine):
295 """Helper function for info, get data on nics for a machine.
297 Modifies data_dict to include the relevant data, and returns a list
298 of (key, name) pairs to display "name: data_dict[key]" to the user.
300 data_dict['num_nics'] = len(machine.nics)
301 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
302 ('nic%s_mac', 'NIC %s MAC Addr'),
303 ('nic%s_ip', 'NIC %s IP'),
306 for i in range(len(machine.nics)):
307 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
309 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
310 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
311 data_dict['nic%s_ip' % i] = machine.nics[i].ip
312 if len(machine.nics) == 1:
313 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
316 def getDiskInfo(data_dict, machine):
317 """Helper function for info, get data on disks for a machine.
319 Modifies data_dict to include the relevant data, and returns a list
320 of (key, name) pairs to display "name: data_dict[key]" to the user.
322 data_dict['num_disks'] = len(machine.disks)
323 disk_fields_template = [('%s_size', '%s size')]
325 for disk in machine.disks:
326 name = disk.guest_device_name
327 disk_fields.extend([(x % name, y % name) for x, y in
328 disk_fields_template])
329 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
332 def command(user, fields):
333 """Handler for running commands like boot and delete on a VM."""
334 back = fields.getfirst('back')
336 d = controls.commandResult(user, fields)
337 if d['command'] == 'Delete VM':
339 except InvalidInput, err:
342 #print >> sys.stderr, err
347 return templates.command(searchList=[d])
349 g.clear() #Changed global state
350 d = getListDict(user)
352 return templates.list(searchList=[d])
354 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
355 return ({'Status': '302',
356 'Location': '/info?machine_id=%d' % machine.machine_id},
357 "You shouldn't see this message.")
359 raise InvalidInput('back', back, 'Not a known back page.')
361 def modifyDict(user, fields):
362 """Modify a machine as specified by CGI arguments.
364 Return a list of local variables for modify.tmpl.
367 transaction = ctx.current.create_transaction()
369 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
370 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
371 admin = validation.testAdmin(user, fields.getfirst('administrator'),
373 contact = validation.testContact(user, fields.getfirst('contact'),
375 name = validation.testName(user, fields.getfirst('name'), machine)
376 oldname = machine.name
379 memory = fields.getfirst('memory')
380 if memory is not None:
381 memory = validation.validMemory(user, memory, machine, on=False)
382 machine.memory = memory
384 vm_type = validation.validVmType(fields.getfirst('vmtype'))
385 if vm_type is not None:
386 machine.type = vm_type
388 disksize = validation.testDisk(user, fields.getfirst('disk'))
389 if disksize is not None:
390 disksize = validation.validDisk(user, disksize, machine)
391 disk = machine.disks[0]
392 if disk.size != disksize:
393 olddisk[disk.guest_device_name] = disksize
395 ctx.current.save(disk)
397 if owner is not None:
398 machine.owner = owner
401 if admin is not None:
402 machine.administrator = admin
403 if contact is not None:
404 machine.contact = contact
406 ctx.current.save(machine)
409 transaction.rollback()
411 for diskname in olddisk:
412 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
414 controls.renameMachine(machine, oldname, name)
415 return dict(user=user,
419 def modify(user, fields):
420 """Handler for modifying attributes of a machine."""
422 modify_dict = modifyDict(user, fields)
423 except InvalidInput, err:
425 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
427 machine = modify_dict['machine']
430 info_dict = infoDict(user, machine)
431 info_dict['err'] = err
433 for field in fields.keys():
434 setattr(info_dict['defaults'], field, fields.getfirst(field))
435 info_dict['result'] = result
436 return templates.info(searchList=[info_dict])
439 def helpHandler(user, fields):
440 """Handler for help messages."""
441 simple = fields.getfirst('simple')
442 subjects = fields.getlist('subject')
444 help_mapping = dict(paravm_console="""
445 ParaVM machines do not support local console access over VNC. To
446 access the serial console of these machines, you can SSH with Kerberos
447 to sipb-xen-console.mit.edu, using the name of the machine as your
450 HVM machines use the virtualization features of the processor, while
451 ParaVM machines use Xen's emulation of virtualization features. You
452 want an HVM virtualized machine.""",
454 Don't ask us! We're as mystified as you are.""",
456 The owner field is used to determine <a
457 href="help?subject=quotas">quotas</a>. It must be the name of a
458 locker that you are an AFS administrator of. In particular, you or an
459 AFS group you are a member of must have AFS rlidwka bits on the
460 locker. You can check who administers the LOCKER locker using the
461 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
462 href="help?subject=administrator">administrator</a>.""",
464 The administrator field determines who can access the console and
465 power on and off the machine. This can be either a user or a moira
468 Quotas are determined on a per-locker basis. Each locker may have a
469 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
472 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
473 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
474 your machine will run just fine, but the applet's display of the
475 console will suffer artifacts.
480 subjects = sorted(help_mapping.keys())
485 mapping=help_mapping)
487 return templates.help(searchList=[d])
490 def badOperation(u, e):
491 """Function called when accessing an unknown URI."""
492 raise CodeError("Unknown operation")
494 def infoDict(user, machine):
495 """Get the variables used by info.tmpl."""
496 status = controls.statusInfo(machine)
497 checkpoint.checkpoint('Getting status info')
498 has_vnc = hasVnc(status)
500 main_status = dict(name=machine.name,
501 memory=str(machine.memory))
505 main_status = dict(status[1:])
506 start_time = float(main_status.get('start_time', 0))
507 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
508 cpu_time_float = float(main_status.get('cpu_time', 0))
509 cputime = datetime.timedelta(seconds=int(cpu_time_float))
510 checkpoint.checkpoint('Status')
511 display_fields = """name uptime memory state cpu_weight on_reboot
512 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
513 display_fields = [('name', 'Name'),
515 ('administrator', 'Administrator'),
516 ('contact', 'Contact'),
519 ('uptime', 'uptime'),
520 ('cputime', 'CPU usage'),
523 ('state', 'state (xen format)'),
524 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
525 ('on_reboot', 'Action on VM reboot'),
526 ('on_poweroff', 'Action on VM poweroff'),
527 ('on_crash', 'Action on VM crash'),
528 ('on_xend_start', 'Action on Xen start'),
529 ('on_xend_stop', 'Action on Xen stop'),
530 ('bootloader', 'Bootloader options'),
534 machine_info['name'] = machine.name
535 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
536 machine_info['owner'] = machine.owner
537 machine_info['administrator'] = machine.administrator
538 machine_info['contact'] = machine.contact
540 nic_fields = getNicInfo(machine_info, machine)
541 nic_point = display_fields.index('NIC_INFO')
542 display_fields = (display_fields[:nic_point] + nic_fields +
543 display_fields[nic_point+1:])
545 disk_fields = getDiskInfo(machine_info, machine)
546 disk_point = display_fields.index('DISK_INFO')
547 display_fields = (display_fields[:disk_point] + disk_fields +
548 display_fields[disk_point+1:])
550 main_status['memory'] += ' MiB'
551 for field, disp in display_fields:
552 if field in ('uptime', 'cputime') and locals()[field] is not None:
553 fields.append((disp, locals()[field]))
554 elif field in machine_info:
555 fields.append((disp, machine_info[field]))
556 elif field in main_status:
557 fields.append((disp, main_status[field]))
560 #fields.append((disp, None))
562 checkpoint.checkpoint('Got fields')
565 max_mem = validation.maxMemory(user, machine, False)
566 checkpoint.checkpoint('Got mem')
567 max_disk = validation.maxDisk(user, machine)
568 defaults = Defaults()
569 for name in 'machine_id name administrator owner memory contact type'.split():
570 setattr(defaults, name, getattr(machine, name))
571 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
572 checkpoint.checkpoint('Got defaults')
574 cdroms=CDROM.select(),
575 on=status is not None,
583 owner_help=helppopup("owner"),
587 def info(user, fields):
588 """Handler for info on a single VM."""
589 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
590 d = infoDict(user, machine)
591 checkpoint.checkpoint('Got infodict')
592 return templates.info(searchList=[d])
594 mapping = dict(list=listVms,
602 def printHeaders(headers):
603 """Print a dictionary as HTTP headers."""
604 for key, value in headers.iteritems():
605 print '%s: %s' % (key, value)
610 """Return the current user based on the SSL environment variables"""
611 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
614 def main(operation, user, fields):
615 start_time = time.time()
616 fun = mapping.get(operation, badOperation)
618 if fun not in (helpHandler, ):
619 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
621 checkpoint.checkpoint('Before')
622 output = fun(u, fields)
623 checkpoint.checkpoint('After')
625 headers = dict(DEFAULT_HEADERS)
626 if isinstance(output, tuple):
627 new_headers, output = output
628 headers.update(new_headers)
629 e = revertStandardError()
632 printHeaders(headers)
633 output_string = str(output)
634 checkpoint.checkpoint('output as a string')
636 print '<!-- <pre>%s</pre> -->' % checkpoint
637 except Exception, err:
638 if not fields.has_key('js'):
639 if isinstance(err, CodeError):
640 print 'Content-Type: text/html\n'
641 e = revertStandardError()
642 print error(operation, u, fields, err, e)
644 if isinstance(err, InvalidInput):
645 print 'Content-Type: text/html\n'
646 e = revertStandardError()
647 print invalidInput(operation, u, fields, err, e)
649 print 'Content-Type: text/plain\n'
650 print 'Uh-oh! We experienced an error.'
651 print 'Please email sipb-xen@mit.edu with the contents of this page.'
653 e = revertStandardError()
658 if __name__ == '__main__':
659 fields = cgi.FieldStorage()
662 operation = os.environ.get('PATH_INFO', '')
664 print "Status: 301 Moved Permanently"
665 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
668 if operation.startswith('/'):
669 operation = operation[1:]
673 if os.getenv("SIPB_XEN_PROFILE"):
675 profile.run('main(operation, u, fields)', 'log-'+operation)
677 main(operation, u, fields)