Changeset 2737 for trunk/packages/invirt-web/code/main.py
- Timestamp:
- Dec 20, 2009, 11:27:10 PM (15 years ago)
- Location:
- trunk/packages/invirt-web
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/packages/invirt-web
-
Property
svn:mergeinfo
set to
False
/package_branches/invirt-web/cherrypy-rebased merged eligible
-
Property
svn:mergeinfo
set to
False
-
trunk/packages/invirt-web/code/main.py
r2217 r2737 7 7 import datetime 8 8 import hmac 9 import os 9 10 import random 10 11 import sha 11 import simplejson12 12 import sys 13 13 import time 14 14 import urllib 15 15 import socket 16 import cherrypy 17 from cherrypy import _cperror 16 18 from StringIO import StringIO 17 18 def revertStandardError():19 """Move stderr to stdout, and return the contents of the old stderr."""20 errio = sys.stderr21 if not isinstance(errio, StringIO):22 return ''23 sys.stderr = sys.stdout24 errio.seek(0)25 return errio.read()26 19 27 20 def printError(): … … 34 27 atexit.register(printError) 35 28 36 import templates37 from Cheetah.Template import Template38 29 import validation 39 30 import cache_acls … … 46 37 from invirt.common import InvalidInput, CodeError 47 38 48 def pathSplit(path): 49 if path.startswith('/'): 50 path = path[1:] 51 i = path.find('/') 52 if i == -1: 53 i = len(path) 54 return path[:i], path[i:] 55 56 class Checkpoint: 39 from view import View, revertStandardError 40 41 42 static_dir = os.path.join(os.path.dirname(__file__), 'static') 43 InvirtStatic = cherrypy.tools.staticdir.handler( 44 root=static_dir, 45 dir=static_dir, 46 section='/static') 47 48 class InvirtUnauthWeb(View): 49 static = InvirtStatic 50 51 @cherrypy.expose 52 @cherrypy.tools.mako(filename="/unauth.mako") 53 def index(self): 54 return {'simple': True} 55 56 class InvirtWeb(View): 57 57 def __init__(self): 58 self.start_time = time.time() 59 self.checkpoints = [] 60 61 def checkpoint(self, s): 62 self.checkpoints.append((s, time.time())) 63 64 def __str__(self): 65 return ('Timing info:\n%s\n' % 66 '\n'.join(['%s: %s' % (d, t - self.start_time) for 67 (d, t) in self.checkpoints])) 68 69 checkpoint = Checkpoint() 70 71 def jquote(string): 72 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'" 73 74 def helppopup(subj): 75 """Return HTML code for a (?) link to a specified help topic""" 76 return ('<span class="helplink"><a href="help?' + 77 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true'))) 78 +'" target="_blank" ' + 79 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>') 80 81 def makeErrorPre(old, addition): 82 if addition is None: 83 return 84 if old: 85 return old[:-6] + '\n----\n' + str(addition) + '</pre>' 86 else: 87 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>' 88 89 Template.database = database 90 Template.config = config 91 Template.helppopup = staticmethod(helppopup) 92 Template.err = None 93 94 class JsonDict: 95 """Class to store a dictionary that will be converted to JSON""" 96 def __init__(self, **kws): 97 self.data = kws 98 if 'err' in kws: 99 err = kws['err'] 100 del kws['err'] 101 self.addError(err) 102 103 def __str__(self): 104 return simplejson.dumps(self.data) 105 106 def addError(self, text): 107 """Add stderr text to be displayed on the website.""" 108 self.data['err'] = \ 109 makeErrorPre(self.data.get('err'), text) 58 super(self.__class__,self).__init__() 59 connect() 60 self._cp_config['tools.require_login.on'] = True 61 self._cp_config['tools.catch_stderr.on'] = True 62 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config', 63 'from invirt import database'] 64 self._cp_config['request.error_response'] = self.handle_error 65 66 static = InvirtStatic 67 68 @cherrypy.expose 69 @cherrypy.tools.mako(filename="/invalid.mako") 70 def invalidInput(self): 71 """Print an error page when an InvalidInput exception occurs""" 72 err = cherrypy.request.prev.params["err"] 73 emsg = cherrypy.request.prev.params["emsg"] 74 d = dict(err_field=err.err_field, 75 err_value=str(err.err_value), stderr=emsg, 76 errorMessage=str(err)) 77 return d 78 79 @cherrypy.expose 80 @cherrypy.tools.mako(filename="/error.mako") 81 def error(self): 82 """Print an error page when an exception occurs""" 83 op = cherrypy.request.prev.path_info 84 username = cherrypy.request.login 85 err = cherrypy.request.prev.params["err"] 86 emsg = cherrypy.request.prev.params["emsg"] 87 traceback = cherrypy.request.prev.params["traceback"] 88 d = dict(op=op, user=username, fields=cherrypy.request.prev.params, 89 errorMessage=str(err), stderr=emsg, traceback=traceback) 90 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako") 91 details = error_raw.render(**d) 92 exclude = config.web.errormail_exclude 93 if username not in exclude and '*' not in exclude: 94 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err), 95 details) 96 d['details'] = details 97 return d 98 99 def __getattr__(self, name): 100 if name in ("admin", "overlord"): 101 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell): 102 raise InvalidInput('username', cherrypy.request.login, 103 'Not in admin group %s.' % config.adminacl) 104 cherrypy.request.state = State(cherrypy.request.login, isadmin=True) 105 return self 106 else: 107 return super(InvirtWeb, self).__getattr__(name) 108 109 def handle_error(self): 110 err = sys.exc_info()[1] 111 if isinstance(err, InvalidInput): 112 cherrypy.request.params['err'] = err 113 cherrypy.request.params['emsg'] = revertStandardError() 114 raise cherrypy.InternalRedirect('/invalidInput') 115 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params: 116 cherrypy.request.params['err'] = err 117 cherrypy.request.params['emsg'] = revertStandardError() 118 cherrypy.request.params['traceback'] = _cperror.format_exc() 119 raise cherrypy.InternalRedirect('/error') 120 # fall back to cherrypy default error page 121 cherrypy.HTTPError(500).set_response() 122 123 @cherrypy.expose 124 @cherrypy.tools.mako(filename="/list.mako") 125 def list(self, result=None): 126 """Handler for list requests.""" 127 d = getListDict(cherrypy.request.login, cherrypy.request.state) 128 if result is not None: 129 d['result'] = result 130 return d 131 index=list 132 133 @cherrypy.expose 134 @cherrypy.tools.mako(filename="/help.mako") 135 def help(self, subject=None, simple=False): 136 """Handler for help messages.""" 137 138 help_mapping = { 139 'Autoinstalls': """ 140 The autoinstaller builds a minimal Debian or Ubuntu system to run as a 141 ParaVM. You can access the resulting system by logging into the <a 142 href="help?simple=true&subject=ParaVM+Console">serial console server</a> 143 with your Kerberos tickets; there is no root password so sshd will 144 refuse login.</p> 145 146 <p>Under the covers, the autoinstaller uses our own patched version of 147 xen-create-image, which is a tool based on debootstrap. If you log 148 into the serial console while the install is running, you can watch 149 it. 150 """, 151 'ParaVM Console': """ 152 ParaVM machines do not support local console access over VNC. To 153 access the serial console of these machines, you can SSH with Kerberos 154 to %s, using the name of the machine as your 155 username.""" % config.console.hostname, 156 'HVM/ParaVM': """ 157 HVM machines use the virtualization features of the processor, while 158 ParaVM machines rely on a modified kernel to communicate directly with 159 the hypervisor. HVMs support boot CDs of any operating system, and 160 the VNC console applet. The three-minute autoinstaller produces 161 ParaVMs. ParaVMs typically are more efficient, and always support the 162 <a href="help?subject=ParaVM+Console">console server</a>.</p> 163 164 <p>More details are <a 165 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the 166 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM 167 (which you can skip by using the autoinstaller to begin with.)</p> 168 169 <p>We recommend using a ParaVM when possible and an HVM when necessary. 170 """, 171 'CPU Weight': """ 172 Don't ask us! We're as mystified as you are.""", 173 'Owner': """ 174 The owner field is used to determine <a 175 href="help?subject=Quotas">quotas</a>. It must be the name of a 176 locker that you are an AFS administrator of. In particular, you or an 177 AFS group you are a member of must have AFS rlidwka bits on the 178 locker. You can check who administers the LOCKER locker using the 179 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a 180 href="help?subject=Administrator">administrator</a>.""", 181 'Administrator': """ 182 The administrator field determines who can access the console and 183 power on and off the machine. This can be either a user or a moira 184 group.""", 185 'Quotas': """ 186 Quotas are determined on a per-locker basis. Each locker may have a 187 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4 188 active machines.""", 189 'Console': """ 190 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try 191 setting <tt>fb=false</tt> to disable the framebuffer. If you don't, 192 your machine will run just fine, but the applet's display of the 193 console will suffer artifacts. 194 """, 195 'Windows': """ 196 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br> 197 <strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one, or visit <a href="http://msca.mit.edu/">http://msca.mit.edu/</a> if you are staff/faculty to request one. 198 """ 199 } 200 201 if not subject: 202 subject = sorted(help_mapping.keys()) 203 if not isinstance(subject, list): 204 subject = [subject] 205 206 return dict(simple=simple, 207 subjects=subject, 208 mapping=help_mapping) 209 help._cp_config['tools.require_login.on'] = False 210 211 def parseCreate(self, fields): 212 kws = dict([(kw, fields[kw]) for kw in 213 'name description owner memory disksize vmtype cdrom autoinstall'.split() 214 if fields[kw]]) 215 validate = validation.Validate(cherrypy.request.login, 216 cherrypy.request.state, 217 strict=True, **kws) 218 return dict(contact=cherrypy.request.login, name=validate.name, 219 description=validate.description, memory=validate.memory, 220 disksize=validate.disksize, owner=validate.owner, 221 machine_type=getattr(validate, 'vmtype', Defaults.type), 222 cdrom=getattr(validate, 'cdrom', None), 223 autoinstall=getattr(validate, 'autoinstall', None)) 224 225 @cherrypy.expose 226 @cherrypy.tools.mako(filename="/list.mako") 227 @cherrypy.tools.require_POST() 228 def create(self, **fields): 229 """Handler for create requests.""" 230 try: 231 parsed_fields = self.parseCreate(fields) 232 machine = controls.createVm(cherrypy.request.login, 233 cherrypy.request.state, **parsed_fields) 234 except InvalidInput, err: 235 pass 236 else: 237 err = None 238 cherrypy.request.state.clear() #Changed global state 239 d = getListDict(cherrypy.request.login, cherrypy.request.state) 240 d['err'] = err 241 if err: 242 for field, value in fields.items(): 243 setattr(d['defaults'], field, value) 244 else: 245 d['new_machine'] = parsed_fields['name'] 246 return d 247 248 @cherrypy.expose 249 @cherrypy.tools.mako(filename="/helloworld.mako") 250 def helloworld(self, **kwargs): 251 return {'request': cherrypy.request, 'kwargs': kwargs} 252 helloworld._cp_config['tools.require_login.on'] = False 253 254 @cherrypy.expose 255 def errortest(self): 256 """Throw an error, to test the error-tracing mechanisms.""" 257 print >>sys.stderr, "look ma, it's a stderr" 258 raise RuntimeError("test of the emergency broadcast system") 259 260 class MachineView(View): 261 def __getattr__(self, name): 262 """Synthesize attributes to allow RESTful URLs like 263 /machine/13/info. This is hairy. CherryPy 3.2 adds a 264 method called _cp_dispatch that allows you to explicitly 265 handle URLs that can't be mapped, and it allows you to 266 rewrite the path components and continue processing. 267 268 This function gets the next path component being resolved 269 as a string. _cp_dispatch will get an array of strings 270 representing any subsequent path components as well.""" 271 272 try: 273 cherrypy.request.params['machine_id'] = int(name) 274 return self 275 except ValueError: 276 return None 277 278 @cherrypy.expose 279 @cherrypy.tools.mako(filename="/info.mako") 280 def info(self, machine_id): 281 """Handler for info on a single VM.""" 282 machine = validation.Validate(cherrypy.request.login, 283 cherrypy.request.state, 284 machine_id=machine_id).machine 285 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine) 286 return d 287 index = info 288 289 @cherrypy.expose 290 @cherrypy.tools.mako(filename="/info.mako") 291 @cherrypy.tools.require_POST() 292 def modify(self, machine_id, **fields): 293 """Handler for modifying attributes of a machine.""" 294 try: 295 modify_dict = modifyDict(cherrypy.request.login, 296 cherrypy.request.state, 297 machine_id, fields) 298 except InvalidInput, err: 299 result = None 300 machine = validation.Validate(cherrypy.request.login, 301 cherrypy.request.state, 302 machine_id=machine_id).machine 303 else: 304 machine = modify_dict['machine'] 305 result = 'Success!' 306 err = None 307 info_dict = infoDict(cherrypy.request.login, 308 cherrypy.request.state, machine) 309 info_dict['err'] = err 310 if err: 311 for field, value in fields.items(): 312 setattr(info_dict['defaults'], field, value) 313 info_dict['result'] = result 314 return info_dict 315 316 @cherrypy.expose 317 @cherrypy.tools.mako(filename="/vnc.mako") 318 def vnc(self, machine_id): 319 """VNC applet page. 320 321 Note that due to same-domain restrictions, the applet connects to 322 the webserver, which needs to forward those requests to the xen 323 server. The Xen server runs another proxy that (1) authenticates 324 and (2) finds the correct port for the VM. 325 326 You might want iptables like: 327 328 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \ 329 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 330 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \ 331 --dport 10003 -j SNAT --to-source 18.187.7.142 332 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \ 333 --dport 10003 -j ACCEPT 334 335 Remember to enable iptables! 336 echo 1 > /proc/sys/net/ipv4/ip_forward 337 """ 338 machine = validation.Validate(cherrypy.request.login, 339 cherrypy.request.state, 340 machine_id=machine_id).machine 341 token = controls.vnctoken(machine) 342 host = controls.listHost(machine) 343 if host: 344 port = 10003 + [h.hostname for h in config.hosts].index(host) 345 else: 346 port = 5900 # dummy 347 348 status = controls.statusInfo(machine) 349 has_vnc = hasVnc(status) 350 351 d = dict(on=status, 352 has_vnc=has_vnc, 353 machine=machine, 354 hostname=cherrypy.request.local.name, 355 port=port, 356 authtoken=token) 357 return d 358 359 @cherrypy.expose 360 @cherrypy.tools.mako(filename="/command.mako") 361 @cherrypy.tools.require_POST() 362 def command(self, command_name, machine_id, **kwargs): 363 """Handler for running commands like boot and delete on a VM.""" 364 back = kwargs.get('back') 365 if command_name == 'delete': 366 back = 'list' 367 try: 368 d = controls.commandResult(cherrypy.request.login, 369 cherrypy.request.state, 370 command_name, machine_id, kwargs) 371 except InvalidInput, err: 372 if not back: 373 raise 374 print >> sys.stderr, err 375 result = str(err) 376 else: 377 result = 'Success!' 378 if not back: 379 return d 380 if back == 'list': 381 cherrypy.request.state.clear() #Changed global state 382 raise cherrypy.InternalRedirect('/list?result=%s' 383 % urllib.quote(result)) 384 elif back == 'info': 385 raise cherrypy.HTTPRedirect(cherrypy.request.base 386 + '/machine/%d/' % machine_id, 387 status=303) 388 else: 389 raise InvalidInput('back', back, 'Not a known back page.') 390 391 machine = MachineView() 392 110 393 111 394 class Defaults: … … 117 400 name = '' 118 401 description = '' 402 administrator = '' 119 403 type = 'linux-hvm' 120 404 … … 126 410 for key in kws: 127 411 setattr(self, key, kws[key]) 128 129 130 131 DEFAULT_HEADERS = {'Content-Type': 'text/html'}132 133 def invalidInput(op, username, fields, err, emsg):134 """Print an error page when an InvalidInput exception occurs"""135 d = dict(op=op, user=username, err_field=err.err_field,136 err_value=str(err.err_value), stderr=emsg,137 errorMessage=str(err))138 return templates.invalid(searchList=[d])139 412 140 413 def hasVnc(status): … … 148 421 return False 149 422 150 def parseCreate(username, state, fields):151 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])152 validate = validation.Validate(username, state, strict=True, **kws)153 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,154 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),155 cdrom=getattr(validate, 'cdrom', None),156 autoinstall=getattr(validate, 'autoinstall', None))157 158 def create(username, state, path, fields):159 """Handler for create requests."""160 try:161 parsed_fields = parseCreate(username, state, fields)162 machine = controls.createVm(username, state, **parsed_fields)163 except InvalidInput, err:164 pass165 else:166 err = None167 state.clear() #Changed global state168 d = getListDict(username, state)169 d['err'] = err170 if err:171 for field in fields.keys():172 setattr(d['defaults'], field, fields.getfirst(field))173 else:174 d['new_machine'] = parsed_fields['name']175 return templates.list(searchList=[d])176 177 423 178 424 def getListDict(username, state): 179 425 """Gets the list of local variables used by list.tmpl.""" 180 checkpoint.checkpoint('Starting')181 426 machines = state.machines 182 checkpoint.checkpoint('Got my machines')183 427 on = {} 184 428 has_vnc = {} 429 installing = {} 185 430 xmlist = state.xmlist 186 checkpoint.checkpoint('Got uptimes')187 can_clone = 'ice3' not in state.xmlist_raw188 431 for m in machines: 189 432 if m not in xmlist: … … 192 435 else: 193 436 m.uptime = xmlist[m]['uptime'] 437 installing[m] = bool(xmlist[m].get('autoinstall')) 194 438 if xmlist[m]['console']: 195 439 has_vnc[m] = True … … 197 441 has_vnc[m] = "WTF?" 198 442 else: 199 has_vnc[m] = "ParaVM" +helppopup("ParaVM Console")443 has_vnc[m] = "ParaVM" 200 444 max_memory = validation.maxMemory(username, state) 201 445 max_disk = validation.maxDisk(username) 202 checkpoint.checkpoint('Got max mem/disk')203 446 defaults = Defaults(max_memory=max_memory, 204 447 max_disk=max_disk, 205 448 owner=username) 206 checkpoint.checkpoint('Got defaults')207 449 def sortkey(machine): 208 450 return (machine.owner != username, machine.owner, machine.name) … … 215 457 machines=machines, 216 458 has_vnc=has_vnc, 217 can_clone=can_clone)459 installing=installing) 218 460 return d 219 220 def listVms(username, state, path, fields):221 """Handler for list requests."""222 checkpoint.checkpoint('Getting list dict')223 d = getListDict(username, state)224 checkpoint.checkpoint('Got list dict')225 return templates.list(searchList=[d])226 227 def vnc(username, state, path, fields):228 """VNC applet page.229 230 Note that due to same-domain restrictions, the applet connects to231 the webserver, which needs to forward those requests to the xen232 server. The Xen server runs another proxy that (1) authenticates233 and (2) finds the correct port for the VM.234 235 You might want iptables like:236 237 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \238 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003239 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \240 --dport 10003 -j SNAT --to-source 18.187.7.142241 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \242 --dport 10003 -j ACCEPT243 244 Remember to enable iptables!245 echo 1 > /proc/sys/net/ipv4/ip_forward246 """247 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine248 249 token = controls.vnctoken(machine)250 host = controls.listHost(machine)251 if host:252 port = 10003 + [h.hostname for h in config.hosts].index(host)253 else:254 port = 5900 # dummy255 256 status = controls.statusInfo(machine)257 has_vnc = hasVnc(status)258 259 d = dict(user=username,260 on=status,261 has_vnc=has_vnc,262 machine=machine,263 hostname=state.environ.get('SERVER_NAME', 'localhost'),264 port=port,265 authtoken=token)266 return templates.vnc(searchList=[d])267 461 268 462 def getHostname(nic): … … 319 513 return disk_fields 320 514 321 def command(username, state, path, fields): 322 """Handler for running commands like boot and delete on a VM.""" 323 back = fields.getfirst('back') 324 try: 325 d = controls.commandResult(username, state, fields) 326 if d['command'] == 'Delete VM': 327 back = 'list' 328 except InvalidInput, err: 329 if not back: 330 raise 331 print >> sys.stderr, err 332 result = err 333 else: 334 result = 'Success!' 335 if not back: 336 return templates.command(searchList=[d]) 337 if back == 'list': 338 state.clear() #Changed global state 339 d = getListDict(username, state) 340 d['result'] = result 341 return templates.list(searchList=[d]) 342 elif back == 'info': 343 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine 344 return ({'Status': '303 See Other', 345 'Location': 'info?machine_id=%d' % machine.machine_id}, 346 "You shouldn't see this message.") 347 else: 348 raise InvalidInput('back', back, 'Not a known back page.') 349 350 def modifyDict(username, state, fields): 515 def modifyDict(username, state, machine_id, fields): 351 516 """Modify a machine as specified by CGI arguments. 352 517 353 Return a list of local variables for modify.tmpl.518 Return a dict containing the machine that was modified. 354 519 """ 355 520 olddisk = {} 356 521 session.begin() 357 522 try: 358 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()]) 523 kws = dict([(kw, fields[kw]) for kw in 524 'owner admin contact name description memory vmtype disksize'.split() 525 if fields[kw]]) 526 kws['machine_id'] = machine_id 359 527 validate = validation.Validate(username, state, **kws) 360 528 machine = validate.machine … … 403 571 if hasattr(validate, 'name'): 404 572 controls.renameMachine(machine, oldname, validate.name) 405 return dict(user=username, 406 command="modify", 407 machine=machine) 408 409 def modify(username, state, path, fields): 410 """Handler for modifying attributes of a machine.""" 411 try: 412 modify_dict = modifyDict(username, state, fields) 413 except InvalidInput, err: 414 result = None 415 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine 416 else: 417 machine = modify_dict['machine'] 418 result = 'Success!' 419 err = None 420 info_dict = infoDict(username, state, machine) 421 info_dict['err'] = err 422 if err: 423 for field in fields.keys(): 424 setattr(info_dict['defaults'], field, fields.getfirst(field)) 425 info_dict['result'] = result 426 return templates.info(searchList=[info_dict]) 427 428 429 def helpHandler(username, state, path, fields): 430 """Handler for help messages.""" 431 simple = fields.getfirst('simple') 432 subjects = fields.getlist('subject') 433 434 help_mapping = { 435 'Autoinstalls': """ 436 The autoinstaller builds a minimal Debian or Ubuntu system to run as a 437 ParaVM. You can access the resulting system by logging into the <a 438 href="help?simple=true&subject=ParaVM+Console">serial console server</a> 439 with your Kerberos tickets; there is no root password so sshd will 440 refuse login.</p> 441 442 <p>Under the covers, the autoinstaller uses our own patched version of 443 xen-create-image, which is a tool based on debootstrap. If you log 444 into the serial console while the install is running, you can watch 445 it. 446 """, 447 'ParaVM Console': """ 448 ParaVM machines do not support local console access over VNC. To 449 access the serial console of these machines, you can SSH with Kerberos 450 to %s, using the name of the machine as your 451 username.""" % config.console.hostname, 452 'HVM/ParaVM': """ 453 HVM machines use the virtualization features of the processor, while 454 ParaVM machines rely on a modified kernel to communicate directly with 455 the hypervisor. HVMs support boot CDs of any operating system, and 456 the VNC console applet. The three-minute autoinstaller produces 457 ParaVMs. ParaVMs typically are more efficient, and always support the 458 <a href="help?subject=ParaVM+Console">console server</a>.</p> 459 460 <p>More details are <a 461 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the 462 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM 463 (which you can skip by using the autoinstaller to begin with.)</p> 464 465 <p>We recommend using a ParaVM when possible and an HVM when necessary. 466 """, 467 'CPU Weight': """ 468 Don't ask us! We're as mystified as you are.""", 469 'Owner': """ 470 The owner field is used to determine <a 471 href="help?subject=Quotas">quotas</a>. It must be the name of a 472 locker that you are an AFS administrator of. In particular, you or an 473 AFS group you are a member of must have AFS rlidwka bits on the 474 locker. You can check who administers the LOCKER locker using the 475 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a 476 href="help?subject=Administrator">administrator</a>.""", 477 'Administrator': """ 478 The administrator field determines who can access the console and 479 power on and off the machine. This can be either a user or a moira 480 group.""", 481 'Quotas': """ 482 Quotas are determined on a per-locker basis. Each locker may have a 483 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4 484 active machines.""", 485 'Console': """ 486 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try 487 setting <tt>fb=false</tt> to disable the framebuffer. If you don't, 488 your machine will run just fine, but the applet's display of the 489 console will suffer artifacts. 490 """, 491 'Windows': """ 492 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br> 493 <strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one. 494 """ 495 } 496 497 if not subjects: 498 subjects = sorted(help_mapping.keys()) 499 500 d = dict(user=username, 501 simple=simple, 502 subjects=subjects, 503 mapping=help_mapping) 504 505 return templates.help(searchList=[d]) 506 507 508 def badOperation(u, s, p, e): 509 """Function called when accessing an unknown URI.""" 510 return ({'Status': '404 Not Found'}, 'Invalid operation.') 573 return dict(machine=machine) 511 574 512 575 def infoDict(username, state, machine): 513 576 """Get the variables used by info.tmpl.""" 514 577 status = controls.statusInfo(machine) 515 checkpoint.checkpoint('Getting status info')516 578 has_vnc = hasVnc(status) 517 579 if status is None: … … 527 589 cpu_time_float = float(main_status.get('cpu_time', 0)) 528 590 cputime = datetime.timedelta(seconds=int(cpu_time_float)) 529 checkpoint.checkpoint('Status')530 591 display_fields = [('name', 'Name'), 531 592 ('description', 'Description'), … … 541 602 'DISK_INFO', 542 603 ('state', 'state (xen format)'), 543 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),544 604 ] 545 605 fields = [] … … 574 634 #fields.append((disp, None)) 575 635 576 checkpoint.checkpoint('Got fields')577 578 579 636 max_mem = validation.maxMemory(machine.owner, state, machine, False) 580 checkpoint.checkpoint('Got mem')581 637 max_disk = validation.maxDisk(machine.owner, machine) 582 638 defaults = Defaults() 583 639 for name in 'machine_id name description administrator owner memory contact'.split(): 584 setattr(defaults, name, getattr(machine, name)) 640 if getattr(machine, name): 641 setattr(defaults, name, getattr(machine, name)) 585 642 defaults.type = machine.type.type_id 586 643 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.) 587 checkpoint.checkpoint('Got defaults')588 644 d = dict(user=username, 589 645 on=status is not None, … … 595 651 max_mem=max_mem, 596 652 max_disk=max_disk, 597 owner_help=helppopup("Owner"),598 653 fields = fields) 599 654 return d 600 601 def info(username, state, path, fields):602 """Handler for info on a single VM."""603 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine604 d = infoDict(username, state, machine)605 checkpoint.checkpoint('Got infodict')606 return templates.info(searchList=[d])607 608 def unauthFront(_, _2, _3, fields):609 """Information for unauth'd users."""610 return templates.unauth(searchList=[{'simple' : True,611 'hostname' : socket.getfqdn()}])612 613 def admin(username, state, path, fields):614 if path == '':615 return ({'Status': '303 See Other',616 'Location': 'admin/'},617 "You shouldn't see this message.")618 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):619 raise InvalidInput('username', username,620 'Not in admin group %s.' % config.adminacl)621 newstate = State(username, isadmin=True)622 newstate.environ = state.environ623 return handler(username, newstate, path, fields)624 625 def throwError(_, __, ___, ____):626 """Throw an error, to test the error-tracing mechanisms."""627 raise RuntimeError("test of the emergency broadcast system")628 629 mapping = dict(list=listVms,630 vnc=vnc,631 command=command,632 modify=modify,633 info=info,634 create=create,635 help=helpHandler,636 unauth=unauthFront,637 admin=admin,638 overlord=admin,639 errortest=throwError)640 641 def printHeaders(headers):642 """Print a dictionary as HTTP headers."""643 for key, value in headers.iteritems():644 print '%s: %s' % (key, value)645 print646 655 647 656 def send_error_mail(subject, body): … … 661 670 p.wait() 662 671 663 def show_error(op, username, fields, err, emsg, traceback): 664 """Print an error page when an exception occurs""" 665 d = dict(op=op, user=username, fields=fields, 666 errorMessage=str(err), stderr=emsg, traceback=traceback) 667 details = templates.error_raw(searchList=[d]) 668 exclude = config.web.errormail_exclude 669 if username not in exclude and '*' not in exclude: 670 send_error_mail('xvm error on %s for %s: %s' % (op, username, err), 671 details) 672 d['details'] = details 673 return templates.error(searchList=[d]) 674 675 def getUser(environ): 676 """Return the current user based on the SSL environment variables""" 677 user = environ.get('REMOTE_USER') 678 if user is None: 679 return 680 681 if environ.get('AUTH_TYPE') == 'Negotiate': 682 # Convert the krb5 principal into a krb4 username 683 if not user.endswith('@%s' % config.kerberos.realm): 684 return 685 else: 686 return user.split('@')[0].replace('/', '.') 687 else: 688 return user 689 690 def handler(username, state, path, fields): 691 operation, path = pathSplit(path) 692 if not operation: 693 operation = 'list' 694 print 'Starting', operation 695 fun = mapping.get(operation, badOperation) 696 return fun(username, state, path, fields) 697 698 class App: 699 def __init__(self, environ, start_response): 700 self.environ = environ 701 self.start = start_response 702 703 self.username = getUser(environ) 704 self.state = State(self.username) 705 self.state.environ = environ 706 707 random.seed() #sigh 708 709 def __iter__(self): 710 start_time = time.time() 711 database.clear_cache() 712 sys.stderr = StringIO() 713 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ) 714 operation = self.environ.get('PATH_INFO', '') 715 if not operation: 716 self.start("301 Moved Permanently", [('Location', './')]) 717 return 718 if self.username is None: 719 operation = 'unauth' 720 721 try: 722 checkpoint.checkpoint('Before') 723 output = handler(self.username, self.state, operation, fields) 724 checkpoint.checkpoint('After') 725 726 headers = dict(DEFAULT_HEADERS) 727 if isinstance(output, tuple): 728 new_headers, output = output 729 headers.update(new_headers) 730 e = revertStandardError() 731 if e: 732 if hasattr(output, 'addError'): 733 output.addError(e) 734 else: 735 # This only happens on redirects, so it'd be a pain to get 736 # the message to the user. Maybe in the response is useful. 737 output = output + '\n\nstderr:\n' + e 738 output_string = str(output) 739 checkpoint.checkpoint('output as a string') 740 except Exception, err: 741 if not fields.has_key('js'): 742 if isinstance(err, InvalidInput): 743 self.start('200 OK', [('Content-Type', 'text/html')]) 744 e = revertStandardError() 745 yield str(invalidInput(operation, self.username, fields, 746 err, e)) 747 return 748 import traceback 749 self.start('500 Internal Server Error', 750 [('Content-Type', 'text/html')]) 751 e = revertStandardError() 752 s = show_error(operation, self.username, fields, 753 err, e, traceback.format_exc()) 754 yield str(s) 755 return 756 status = headers.setdefault('Status', '200 OK') 757 del headers['Status'] 758 self.start(status, headers.items()) 759 yield output_string 760 if fields.has_key('timedebug'): 761 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint)) 762 763 def constructor(): 764 connect() 765 return App 766 767 def main(): 768 from flup.server.fcgi_fork import WSGIServer 769 WSGIServer(constructor()).run() 770 771 if __name__ == '__main__': 772 main() 672 random.seed() #sigh
Note: See TracChangeset
for help on using the changeset viewer.