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)
35 from Cheetah.Template import Template
36 import sipb_xen_database
37 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
40 from webcommon import InvalidInput, CodeError, State
45 self.start_time = time.time()
48 def checkpoint(self, s):
49 self.checkpoints.append((s, time.time()))
52 return ('Timing info:\n%s\n' %
53 '\n'.join(['%s: %s' % (d, t - self.start_time) for
54 (d, t) in self.checkpoints]))
56 checkpoint = Checkpoint()
59 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
62 """Return HTML code for a (?) link to a specified help topic"""
63 return ('<span class="helplink"><a href="help?' +
64 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
65 +'" target="_blank" ' +
66 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
68 def makeErrorPre(old, addition):
72 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
74 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
76 Template.sipb_xen_database = sipb_xen_database
77 Template.helppopup = staticmethod(helppopup)
81 """Class to store a dictionary that will be converted to JSON"""
82 def __init__(self, **kws):
90 return simplejson.dumps(self.data)
92 def addError(self, text):
93 """Add stderr text to be displayed on the website."""
95 makeErrorPre(self.data.get('err'), text)
98 """Class to store default values for fields."""
107 def __init__(self, max_memory=None, max_disk=None, **kws):
108 if max_memory is not None:
109 self.memory = min(self.memory, max_memory)
110 if max_disk is not None:
111 self.max_disk = min(self.disk, max_disk)
113 setattr(self, key, kws[key])
117 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
119 def invalidInput(op, username, fields, err, emsg):
120 """Print an error page when an InvalidInput exception occurs"""
121 d = dict(op=op, user=username, 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(username, state, fields):
137 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
138 validate = validation.Validate(username, state, strict=True, **kws)
139 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
140 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
141 cdrom=getattr(validate, 'cdrom', None),
142 autoinstall=getattr(validate, 'autoinstall', None))
144 def create(username, state, fields):
145 """Handler for create requests."""
147 parsed_fields = parseCreate(username, state, fields)
148 machine = controls.createVm(username, state, **parsed_fields)
149 except InvalidInput, err:
153 state.clear() #Changed global state
154 d = getListDict(username, state)
157 for field in fields.keys():
158 setattr(d['defaults'], field, fields.getfirst(field))
160 d['new_machine'] = parsed_fields['name']
161 return templates.list(searchList=[d])
164 def getListDict(username, state):
165 """Gets the list of local variables used by list.tmpl."""
166 checkpoint.checkpoint('Starting')
167 machines = state.machines
168 checkpoint.checkpoint('Got my machines')
171 xmlist = state.xmlist
172 checkpoint.checkpoint('Got uptimes')
173 can_clone = 'ice3' not in state.xmlist_raw
179 m.uptime = xmlist[m]['uptime']
180 if xmlist[m]['console']:
185 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
186 max_memory = validation.maxMemory(username, state)
187 max_disk = validation.maxDisk(username)
188 checkpoint.checkpoint('Got max mem/disk')
189 defaults = Defaults(max_memory=max_memory,
193 checkpoint.checkpoint('Got defaults')
194 def sortkey(machine):
195 return (machine.owner != username, machine.owner, machine.name)
196 machines = sorted(machines, key=sortkey)
197 d = dict(user=username,
198 cant_add_vm=validation.cantAddVm(username, state),
199 max_memory=max_memory,
207 def listVms(username, state, fields):
208 """Handler for list requests."""
209 checkpoint.checkpoint('Getting list dict')
210 d = getListDict(username, state)
211 checkpoint.checkpoint('Got list dict')
212 return templates.list(searchList=[d])
214 def vnc(username, state, fields):
217 Note that due to same-domain restrictions, the applet connects to
218 the webserver, which needs to forward those requests to the xen
219 server. The Xen server runs another proxy that (1) authenticates
220 and (2) finds the correct port for the VM.
222 You might want iptables like:
224 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
225 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
226 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
227 --dport 10003 -j SNAT --to-source 18.187.7.142
228 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
229 --dport 10003 -j ACCEPT
231 Remember to enable iptables!
232 echo 1 > /proc/sys/net/ipv4/ip_forward
234 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
236 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
239 data["user"] = username
240 data["machine"] = machine.name
241 data["expires"] = time.time()+(5*60)
242 pickled_data = cPickle.dumps(data)
243 m = hmac.new(TOKEN_KEY, digestmod=sha)
244 m.update(pickled_data)
245 token = {'data': pickled_data, 'digest': m.digest()}
246 token = cPickle.dumps(token)
247 token = base64.urlsafe_b64encode(token)
249 status = controls.statusInfo(machine)
250 has_vnc = hasVnc(status)
252 d = dict(user=username,
256 hostname=state.environ.get('SERVER_NAME', 'localhost'),
258 return templates.vnc(searchList=[d])
260 def getHostname(nic):
261 """Find the hostname associated with a NIC.
263 XXX this should be merged with the similar logic in DNS and DHCP.
265 if nic.hostname and '.' in nic.hostname:
268 return nic.machine.name + '.xvm.mit.edu'
273 def getNicInfo(data_dict, machine):
274 """Helper function for info, get data on nics for a machine.
276 Modifies data_dict to include the relevant data, and returns a list
277 of (key, name) pairs to display "name: data_dict[key]" to the user.
279 data_dict['num_nics'] = len(machine.nics)
280 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
281 ('nic%s_mac', 'NIC %s MAC Addr'),
282 ('nic%s_ip', 'NIC %s IP'),
285 for i in range(len(machine.nics)):
286 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
288 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
289 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
290 data_dict['nic%s_ip' % i] = machine.nics[i].ip
291 if len(machine.nics) == 1:
292 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
295 def getDiskInfo(data_dict, machine):
296 """Helper function for info, get data on disks for a machine.
298 Modifies data_dict to include the relevant data, and returns a list
299 of (key, name) pairs to display "name: data_dict[key]" to the user.
301 data_dict['num_disks'] = len(machine.disks)
302 disk_fields_template = [('%s_size', '%s size')]
304 for disk in machine.disks:
305 name = disk.guest_device_name
306 disk_fields.extend([(x % name, y % name) for x, y in
307 disk_fields_template])
308 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
311 def command(username, state, fields):
312 """Handler for running commands like boot and delete on a VM."""
313 back = fields.getfirst('back')
315 d = controls.commandResult(username, state, fields)
316 if d['command'] == 'Delete VM':
318 except InvalidInput, err:
321 print >> sys.stderr, err
326 return templates.command(searchList=[d])
328 state.clear() #Changed global state
329 d = getListDict(username, state)
331 return templates.list(searchList=[d])
333 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
334 return ({'Status': '303 See Other',
335 'Location': '/info?machine_id=%d' % machine.machine_id},
336 "You shouldn't see this message.")
338 raise InvalidInput('back', back, 'Not a known back page.')
340 def modifyDict(username, state, fields):
341 """Modify a machine as specified by CGI arguments.
343 Return a list of local variables for modify.tmpl.
346 transaction = ctx.current.create_transaction()
348 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
349 validate = validation.Validate(username, state, **kws)
350 machine = validate.machine
351 oldname = machine.name
353 if hasattr(validate, 'memory'):
354 machine.memory = validate.memory
356 if hasattr(validate, 'vmtype'):
357 machine.type = validate.vmtype
359 if hasattr(validate, 'disksize'):
360 disksize = validate.disksize
361 disk = machine.disks[0]
362 if disk.size != disksize:
363 olddisk[disk.guest_device_name] = disksize
365 ctx.current.save(disk)
368 if hasattr(validate, 'owner') and validate.owner != machine.owner:
369 machine.owner = validate.owner
371 if hasattr(validate, 'name'):
372 machine.name = validate.name
373 if hasattr(validate, 'description'):
374 machine.description = validate.description
375 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
376 machine.administrator = validate.admin
378 if hasattr(validate, 'contact'):
379 machine.contact = validate.contact
381 ctx.current.save(machine)
383 print >> sys.stderr, machine, machine.administrator
384 cache_acls.refreshMachine(machine)
387 transaction.rollback()
389 for diskname in olddisk:
390 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
391 if hasattr(validate, 'name'):
392 controls.renameMachine(machine, oldname, validate.name)
393 return dict(user=username,
397 def modify(username, state, fields):
398 """Handler for modifying attributes of a machine."""
400 modify_dict = modifyDict(username, state, fields)
401 except InvalidInput, err:
403 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
405 machine = modify_dict['machine']
408 info_dict = infoDict(username, state, machine)
409 info_dict['err'] = err
411 for field in fields.keys():
412 setattr(info_dict['defaults'], field, fields.getfirst(field))
413 info_dict['result'] = result
414 return templates.info(searchList=[info_dict])
417 def helpHandler(username, state, fields):
418 """Handler for help messages."""
419 simple = fields.getfirst('simple')
420 subjects = fields.getlist('subject')
422 help_mapping = {'ParaVM Console': """
423 ParaVM machines do not support local console access over VNC. To
424 access the serial console of these machines, you can SSH with Kerberos
425 to console.xvm.mit.edu, using the name of the machine as your
428 HVM machines use the virtualization features of the processor, while
429 ParaVM machines use Xen's emulation of virtualization features. You
430 want an HVM virtualized machine.""",
432 Don't ask us! We're as mystified as you are.""",
434 The owner field is used to determine <a
435 href="help?subject=Quotas">quotas</a>. It must be the name of a
436 locker that you are an AFS administrator of. In particular, you or an
437 AFS group you are a member of must have AFS rlidwka bits on the
438 locker. You can check who administers the LOCKER locker using the
439 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
440 href="help?subject=Administrator">administrator</a>.""",
442 The administrator field determines who can access the console and
443 power on and off the machine. This can be either a user or a moira
446 Quotas are determined on a per-locker basis. Each locker may have a
447 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
450 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
451 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
452 your machine will run just fine, but the applet's display of the
453 console will suffer artifacts.
458 subjects = sorted(help_mapping.keys())
460 d = dict(user=username,
463 mapping=help_mapping)
465 return templates.help(searchList=[d])
468 def badOperation(u, s, e):
469 """Function called when accessing an unknown URI."""
470 return ({'Status': '404 Not Found'}, 'Invalid operation.')
472 def infoDict(username, state, machine):
473 """Get the variables used by info.tmpl."""
474 status = controls.statusInfo(machine)
475 checkpoint.checkpoint('Getting status info')
476 has_vnc = hasVnc(status)
478 main_status = dict(name=machine.name,
479 memory=str(machine.memory))
483 main_status = dict(status[1:])
484 start_time = float(main_status.get('start_time', 0))
485 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
486 cpu_time_float = float(main_status.get('cpu_time', 0))
487 cputime = datetime.timedelta(seconds=int(cpu_time_float))
488 checkpoint.checkpoint('Status')
489 display_fields = """name uptime memory state cpu_weight on_reboot
490 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
491 display_fields = [('name', 'Name'),
492 ('description', 'Description'),
494 ('administrator', 'Administrator'),
495 ('contact', 'Contact'),
498 ('uptime', 'uptime'),
499 ('cputime', 'CPU usage'),
502 ('state', 'state (xen format)'),
503 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
504 ('on_reboot', 'Action on VM reboot'),
505 ('on_poweroff', 'Action on VM poweroff'),
506 ('on_crash', 'Action on VM crash'),
507 ('on_xend_start', 'Action on Xen start'),
508 ('on_xend_stop', 'Action on Xen stop'),
509 ('bootloader', 'Bootloader options'),
513 machine_info['name'] = machine.name
514 machine_info['description'] = machine.description
515 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
516 machine_info['owner'] = machine.owner
517 machine_info['administrator'] = machine.administrator
518 machine_info['contact'] = machine.contact
520 nic_fields = getNicInfo(machine_info, machine)
521 nic_point = display_fields.index('NIC_INFO')
522 display_fields = (display_fields[:nic_point] + nic_fields +
523 display_fields[nic_point+1:])
525 disk_fields = getDiskInfo(machine_info, machine)
526 disk_point = display_fields.index('DISK_INFO')
527 display_fields = (display_fields[:disk_point] + disk_fields +
528 display_fields[disk_point+1:])
530 main_status['memory'] += ' MiB'
531 for field, disp in display_fields:
532 if field in ('uptime', 'cputime') and locals()[field] is not None:
533 fields.append((disp, locals()[field]))
534 elif field in machine_info:
535 fields.append((disp, machine_info[field]))
536 elif field in main_status:
537 fields.append((disp, main_status[field]))
540 #fields.append((disp, None))
542 checkpoint.checkpoint('Got fields')
545 max_mem = validation.maxMemory(machine.owner, state, machine, False)
546 checkpoint.checkpoint('Got mem')
547 max_disk = validation.maxDisk(machine.owner, machine)
548 defaults = Defaults()
549 for name in 'machine_id name description administrator owner memory contact'.split():
550 setattr(defaults, name, getattr(machine, name))
551 defaults.type = machine.type.type_id
552 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
553 checkpoint.checkpoint('Got defaults')
554 d = dict(user=username,
555 on=status is not None,
563 owner_help=helppopup("Owner"),
567 def info(username, state, fields):
568 """Handler for info on a single VM."""
569 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
570 d = infoDict(username, state, machine)
571 checkpoint.checkpoint('Got infodict')
572 return templates.info(searchList=[d])
574 def unauthFront(_, _2, fields):
575 """Information for unauth'd users."""
576 return templates.unauth(searchList=[{'simple' : True}])
578 def throwError(_, __, ___):
579 """Throw an error, to test the error-tracing mechanisms."""
580 raise RuntimeError("test of the emergency broadcast system")
582 mapping = dict(list=listVms,
590 errortest=throwError)
592 def printHeaders(headers):
593 """Print a dictionary as HTTP headers."""
594 for key, value in headers.iteritems():
595 print '%s: %s' % (key, value)
598 def send_error_mail(subject, body):
603 From: root@xvm.mit.edu
607 """ % (to, subject, body)
608 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
613 def show_error(op, username, fields, err, emsg, traceback):
614 """Print an error page when an exception occurs"""
615 d = dict(op=op, user=username, fields=fields,
616 errorMessage=str(err), stderr=emsg, traceback=traceback)
617 details = templates.error_raw(searchList=[d])
618 if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
619 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
621 d['details'] = details
622 return templates.error(searchList=[d])
624 def getUser(environ):
625 """Return the current user based on the SSL environment variables"""
626 return environ.get('REMOTE_USER', None)
629 def __init__(self, environ, start_response):
630 self.environ = environ
631 self.start = start_response
633 self.username = getUser(environ)
634 self.state = State(self.username)
635 self.state.environ = environ
638 sipb_xen_database.clear_cache()
639 sys.stderr = StringIO()
640 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
641 operation = self.environ.get('PATH_INFO', '')
643 self.start("301 Moved Permanently", [('Location',
644 self.environ['SCRIPT_NAME']+'/')])
646 if self.username is None:
648 if operation.startswith('/'):
649 operation = operation[1:]
652 print 'Starting', operation
654 start_time = time.time()
655 fun = mapping.get(operation, badOperation)
657 checkpoint.checkpoint('Before')
658 output = fun(self.username, self.state, fields)
659 checkpoint.checkpoint('After')
661 headers = dict(DEFAULT_HEADERS)
662 if isinstance(output, tuple):
663 new_headers, output = output
664 headers.update(new_headers)
665 e = revertStandardError()
667 if isinstance(output, basestring):
668 sys.stderr = StringIO()
670 print >> sys.stderr, x
671 print >> sys.stderr, 'XXX'
672 print >> sys.stderr, e
675 output_string = str(output)
676 checkpoint.checkpoint('output as a string')
677 except Exception, err:
678 if not fields.has_key('js'):
679 if isinstance(err, InvalidInput):
680 self.start('200 OK', [('Content-Type', 'text/html')])
681 e = revertStandardError()
682 yield str(invalidInput(operation, self.username, fields,
686 self.start('500 Internal Server Error',
687 [('Content-Type', 'text/html')])
688 e = revertStandardError()
689 s = show_error(operation, self.username, fields,
690 err, e, traceback.format_exc())
693 status = headers.setdefault('Status', '200 OK')
694 del headers['Status']
695 self.start(status, headers.items())
697 if fields.has_key('timedebug'):
698 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
701 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
705 from flup.server.fcgi_fork import WSGIServer
706 WSGIServer(constructor()).run()
708 if __name__ == '__main__':