2 """Main CGI script for web interface"""
15 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')
39 from Cheetah.Template import Template
40 import sipb_xen_database
41 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
44 from webcommon import InvalidInput, CodeError, g
49 self.start_time = time.time()
52 def checkpoint(self, s):
53 self.checkpoints.append((s, time.time()))
56 return ('Timing info:\n%s\n' %
57 '\n'.join(['%s: %s' % (d, t - self.start_time) for
58 (d, t) in self.checkpoints]))
60 checkpoint = Checkpoint()
63 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
66 """Return HTML code for a (?) link to a specified help topic"""
67 return ('<span class="helplink"><a href="help?' +
68 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
69 +'" target="_blank" ' +
70 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
72 def makeErrorPre(old, addition):
76 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
78 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
80 Template.sipb_xen_database = sipb_xen_database
81 Template.helppopup = staticmethod(helppopup)
85 """Class to store a dictionary that will be converted to JSON"""
86 def __init__(self, **kws):
94 return simplejson.dumps(self.data)
96 def addError(self, text):
97 """Add stderr text to be displayed on the website."""
99 makeErrorPre(self.data.get('err'), text)
102 """Class to store default values for fields."""
110 def __init__(self, max_memory=None, max_disk=None, **kws):
111 if max_memory is not None:
112 self.memory = min(self.memory, max_memory)
113 if max_disk is not None:
114 self.max_disk = min(self.disk, max_disk)
116 setattr(self, key, kws[key])
120 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
122 def error(op, user, fields, err, emsg):
123 """Print an error page when a CodeError occurs"""
124 d = dict(op=op, user=user, errorMessage=str(err),
126 return templates.error(searchList=[d])
128 def invalidInput(op, user, fields, err, emsg):
129 """Print an error page when an InvalidInput exception occurs"""
130 d = dict(op=op, user=user, err_field=err.err_field,
131 err_value=str(err.err_value), stderr=emsg,
132 errorMessage=str(err))
133 return templates.invalid(searchList=[d])
136 """Does the machine with a given status list support VNC?"""
140 if l[0] == 'device' and l[1][0] == 'vfb':
142 return 'location' in d
145 def parseCreate(user, fields):
146 name = fields.getfirst('name')
147 if not validation.validMachineName(name):
148 raise InvalidInput('name', name, 'You must provide a machine name. Max 22 chars, alnum plus \'-\' and \'_\'.')
151 if Machine.get_by(name=name):
152 raise InvalidInput('name', name,
153 "Name already exists.")
155 owner = validation.testOwner(user, fields.getfirst('owner'))
157 memory = fields.getfirst('memory')
158 memory = validation.validMemory(owner, memory, on=True)
160 disk_size = fields.getfirst('disk')
161 disk_size = validation.validDisk(owner, disk_size)
163 vm_type = fields.getfirst('vmtype')
164 vm_type = validation.validVmType(vm_type)
166 cdrom = fields.getfirst('cdrom')
167 if cdrom is not None and not CDROM.get(cdrom):
168 raise CodeError("Invalid cdrom type '%s'" % cdrom)
170 clone_from = fields.getfirst('clone_from')
171 if clone_from and clone_from != 'ice3':
172 raise CodeError("Invalid clone image '%s'" % clone_from)
174 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
175 owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
177 def create(user, fields):
178 """Handler for create requests."""
180 parsed_fields = parseCreate(user, fields)
181 machine = controls.createVm(**parsed_fields)
182 except InvalidInput, err:
186 g.clear() #Changed global state
187 d = getListDict(user)
190 for field in fields.keys():
191 setattr(d['defaults'], field, fields.getfirst(field))
193 d['new_machine'] = parsed_fields['name']
194 return templates.list(searchList=[d])
197 def getListDict(user):
198 """Gets the list of local variables used by list.tmpl."""
199 machines = g.machines
200 checkpoint.checkpoint('Got my machines')
204 checkpoint.checkpoint('Got uptimes')
206 m.uptime = g.uptimes.get(m)
212 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
213 max_memory = validation.maxMemory(user)
214 max_disk = validation.maxDisk(user)
215 checkpoint.checkpoint('Got max mem/disk')
216 defaults = Defaults(max_memory=max_memory,
220 checkpoint.checkpoint('Got defaults')
221 def sortkey(machine):
222 return (machine.owner != user, machine.owner, machine.name)
223 machines = sorted(machines, key=sortkey)
225 cant_add_vm=validation.cantAddVm(user),
226 max_memory=max_memory,
234 def listVms(user, fields):
235 """Handler for list requests."""
236 checkpoint.checkpoint('Getting list dict')
237 d = getListDict(user)
238 checkpoint.checkpoint('Got list dict')
239 return templates.list(searchList=[d])
241 def vnc(user, fields):
244 Note that due to same-domain restrictions, the applet connects to
245 the webserver, which needs to forward those requests to the xen
246 server. The Xen server runs another proxy that (1) authenticates
247 and (2) finds the correct port for the VM.
249 You might want iptables like:
251 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
252 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
253 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
254 --dport 10003 -j SNAT --to-source 18.187.7.142
255 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
256 --dport 10003 -j ACCEPT
258 Remember to enable iptables!
259 echo 1 > /proc/sys/net/ipv4/ip_forward
261 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
263 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
267 data["machine"] = machine.name
268 data["expires"] = time.time()+(5*60)
269 pickled_data = cPickle.dumps(data)
270 m = hmac.new(TOKEN_KEY, digestmod=sha)
271 m.update(pickled_data)
272 token = {'data': pickled_data, 'digest': m.digest()}
273 token = cPickle.dumps(token)
274 token = base64.urlsafe_b64encode(token)
276 status = controls.statusInfo(machine)
277 has_vnc = hasVnc(status)
283 hostname=os.environ.get('SERVER_NAME', 'localhost'),
285 return templates.vnc(searchList=[d])
287 def getHostname(nic):
288 """Find the hostname associated with a NIC.
290 XXX this should be merged with the similar logic in DNS and DHCP.
292 if nic.hostname and '.' in nic.hostname:
295 return nic.machine.name + '.xvm.mit.edu'
300 def getNicInfo(data_dict, machine):
301 """Helper function for info, get data on nics for a machine.
303 Modifies data_dict to include the relevant data, and returns a list
304 of (key, name) pairs to display "name: data_dict[key]" to the user.
306 data_dict['num_nics'] = len(machine.nics)
307 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
308 ('nic%s_mac', 'NIC %s MAC Addr'),
309 ('nic%s_ip', 'NIC %s IP'),
312 for i in range(len(machine.nics)):
313 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
315 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
316 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
317 data_dict['nic%s_ip' % i] = machine.nics[i].ip
318 if len(machine.nics) == 1:
319 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
322 def getDiskInfo(data_dict, machine):
323 """Helper function for info, get data on disks for a machine.
325 Modifies data_dict to include the relevant data, and returns a list
326 of (key, name) pairs to display "name: data_dict[key]" to the user.
328 data_dict['num_disks'] = len(machine.disks)
329 disk_fields_template = [('%s_size', '%s size')]
331 for disk in machine.disks:
332 name = disk.guest_device_name
333 disk_fields.extend([(x % name, y % name) for x, y in
334 disk_fields_template])
335 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
338 def command(user, fields):
339 """Handler for running commands like boot and delete on a VM."""
340 back = fields.getfirst('back')
342 d = controls.commandResult(user, fields)
343 if d['command'] == 'Delete VM':
345 except InvalidInput, err:
348 #print >> sys.stderr, err
353 return templates.command(searchList=[d])
355 g.clear() #Changed global state
356 d = getListDict(user)
358 return templates.list(searchList=[d])
360 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
361 return ({'Status': '302',
362 'Location': '/info?machine_id=%d' % machine.machine_id},
363 "You shouldn't see this message.")
365 raise InvalidInput('back', back, 'Not a known back page.')
367 def modifyDict(user, fields):
368 """Modify a machine as specified by CGI arguments.
370 Return a list of local variables for modify.tmpl.
373 transaction = ctx.current.create_transaction()
375 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
376 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
377 admin = validation.testAdmin(user, fields.getfirst('administrator'),
379 contact = validation.testContact(user, fields.getfirst('contact'),
381 name = validation.testName(user, fields.getfirst('name'), machine)
382 oldname = machine.name
385 memory = fields.getfirst('memory')
386 if memory is not None:
387 memory = validation.validMemory(user, memory, machine, on=False)
388 machine.memory = memory
390 vm_type = validation.validVmType(fields.getfirst('vmtype'))
391 if vm_type is not None:
392 machine.type = vm_type
394 disksize = validation.testDisk(user, fields.getfirst('disk'))
395 if disksize is not None:
396 disksize = validation.validDisk(user, disksize, machine)
397 disk = machine.disks[0]
398 if disk.size != disksize:
399 olddisk[disk.guest_device_name] = disksize
401 ctx.current.save(disk)
404 if owner is not None and owner != machine.owner:
405 machine.owner = owner
409 if admin is not None and admin != machine.administrator:
410 machine.administrator = admin
412 if contact is not None:
413 machine.contact = contact
415 ctx.current.save(machine)
417 cache_acls.refreshMachine(machine)
420 transaction.rollback()
422 for diskname in olddisk:
423 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
425 controls.renameMachine(machine, oldname, name)
426 return dict(user=user,
430 def modify(user, fields):
431 """Handler for modifying attributes of a machine."""
433 modify_dict = modifyDict(user, fields)
434 except InvalidInput, err:
436 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
438 machine = modify_dict['machine']
441 info_dict = infoDict(user, machine)
442 info_dict['err'] = err
444 for field in fields.keys():
445 setattr(info_dict['defaults'], field, fields.getfirst(field))
446 info_dict['result'] = result
447 return templates.info(searchList=[info_dict])
450 def helpHandler(user, fields):
451 """Handler for help messages."""
452 simple = fields.getfirst('simple')
453 subjects = fields.getlist('subject')
455 help_mapping = dict(paravm_console="""
456 ParaVM machines do not support local console access over VNC. To
457 access the serial console of these machines, you can SSH with Kerberos
458 to sipb-xen-console.mit.edu, using the name of the machine as your
461 HVM machines use the virtualization features of the processor, while
462 ParaVM machines use Xen's emulation of virtualization features. You
463 want an HVM virtualized machine.""",
465 Don't ask us! We're as mystified as you are.""",
467 The owner field is used to determine <a
468 href="help?subject=quotas">quotas</a>. It must be the name of a
469 locker that you are an AFS administrator of. In particular, you or an
470 AFS group you are a member of must have AFS rlidwka bits on the
471 locker. You can check who administers the LOCKER locker using the
472 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
473 href="help?subject=administrator">administrator</a>.""",
475 The administrator field determines who can access the console and
476 power on and off the machine. This can be either a user or a moira
479 Quotas are determined on a per-locker basis. Each locker may have a
480 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
483 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
484 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
485 your machine will run just fine, but the applet's display of the
486 console will suffer artifacts.
491 subjects = sorted(help_mapping.keys())
496 mapping=help_mapping)
498 return templates.help(searchList=[d])
501 def badOperation(u, e):
502 """Function called when accessing an unknown URI."""
503 raise CodeError("Unknown operation")
505 def infoDict(user, machine):
506 """Get the variables used by info.tmpl."""
507 status = controls.statusInfo(machine)
508 checkpoint.checkpoint('Getting status info')
509 has_vnc = hasVnc(status)
511 main_status = dict(name=machine.name,
512 memory=str(machine.memory))
516 main_status = dict(status[1:])
517 start_time = float(main_status.get('start_time', 0))
518 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
519 cpu_time_float = float(main_status.get('cpu_time', 0))
520 cputime = datetime.timedelta(seconds=int(cpu_time_float))
521 checkpoint.checkpoint('Status')
522 display_fields = """name uptime memory state cpu_weight on_reboot
523 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
524 display_fields = [('name', 'Name'),
526 ('administrator', 'Administrator'),
527 ('contact', 'Contact'),
530 ('uptime', 'uptime'),
531 ('cputime', 'CPU usage'),
534 ('state', 'state (xen format)'),
535 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
536 ('on_reboot', 'Action on VM reboot'),
537 ('on_poweroff', 'Action on VM poweroff'),
538 ('on_crash', 'Action on VM crash'),
539 ('on_xend_start', 'Action on Xen start'),
540 ('on_xend_stop', 'Action on Xen stop'),
541 ('bootloader', 'Bootloader options'),
545 machine_info['name'] = machine.name
546 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
547 machine_info['owner'] = machine.owner
548 machine_info['administrator'] = machine.administrator
549 machine_info['contact'] = machine.contact
551 nic_fields = getNicInfo(machine_info, machine)
552 nic_point = display_fields.index('NIC_INFO')
553 display_fields = (display_fields[:nic_point] + nic_fields +
554 display_fields[nic_point+1:])
556 disk_fields = getDiskInfo(machine_info, machine)
557 disk_point = display_fields.index('DISK_INFO')
558 display_fields = (display_fields[:disk_point] + disk_fields +
559 display_fields[disk_point+1:])
561 main_status['memory'] += ' MiB'
562 for field, disp in display_fields:
563 if field in ('uptime', 'cputime') and locals()[field] is not None:
564 fields.append((disp, locals()[field]))
565 elif field in machine_info:
566 fields.append((disp, machine_info[field]))
567 elif field in main_status:
568 fields.append((disp, main_status[field]))
571 #fields.append((disp, None))
573 checkpoint.checkpoint('Got fields')
576 max_mem = validation.maxMemory(user, machine, False)
577 checkpoint.checkpoint('Got mem')
578 max_disk = validation.maxDisk(user, machine)
579 defaults = Defaults()
580 for name in 'machine_id name administrator owner memory contact type'.split():
581 setattr(defaults, name, getattr(machine, name))
582 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
583 checkpoint.checkpoint('Got defaults')
585 on=status is not None,
593 owner_help=helppopup("owner"),
597 def info(user, fields):
598 """Handler for info on a single VM."""
599 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
600 d = infoDict(user, machine)
601 checkpoint.checkpoint('Got infodict')
602 return templates.info(searchList=[d])
604 def unauthFront(_, fields):
605 """Information for unauth'd users."""
606 return templates.unauth(searchList=[{'simple' : True}])
608 mapping = dict(list=listVms,
617 def printHeaders(headers):
618 """Print a dictionary as HTTP headers."""
619 for key, value in headers.iteritems():
620 print '%s: %s' % (key, value)
625 """Return the current user based on the SSL environment variables"""
626 email = os.environ.get('SSL_CLIENT_S_DN_Email', None)
629 return email.split("@")[0]
631 def main(operation, user, fields):
632 start_time = time.time()
633 fun = mapping.get(operation, badOperation)
635 if fun not in (helpHandler, ):
636 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
638 checkpoint.checkpoint('Before')
639 output = fun(u, fields)
640 checkpoint.checkpoint('After')
642 headers = dict(DEFAULT_HEADERS)
643 if isinstance(output, tuple):
644 new_headers, output = output
645 headers.update(new_headers)
646 e = revertStandardError()
649 printHeaders(headers)
650 output_string = str(output)
651 checkpoint.checkpoint('output as a string')
653 print '<!-- <pre>%s</pre> -->' % checkpoint
654 except Exception, err:
655 if not fields.has_key('js'):
656 if isinstance(err, CodeError):
657 print 'Content-Type: text/html\n'
658 e = revertStandardError()
659 print error(operation, u, fields, err, e)
661 if isinstance(err, InvalidInput):
662 print 'Content-Type: text/html\n'
663 e = revertStandardError()
664 print invalidInput(operation, u, fields, err, e)
666 print 'Content-Type: text/plain\n'
667 print 'Uh-oh! We experienced an error.'
668 print 'Please email sipb-xen@mit.edu with the contents of this page.'
670 e = revertStandardError()
675 if __name__ == '__main__':
676 fields = cgi.FieldStorage()
679 operation = os.environ.get('PATH_INFO', '')
681 print "Status: 301 Moved Permanently"
682 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
688 if operation.startswith('/'):
689 operation = operation[1:]
693 if os.getenv("SIPB_XEN_PROFILE"):
695 profile.run('main(operation, u, fields)', 'log-'+operation)
697 main(operation, u, fields)