ajaxterm!
[invirt/packages/invirt-web.git] / code / main.py
1 #!/usr/bin/python
2 """Main CGI script for web interface"""
3
4 import base64
5 import cPickle
6 import cgi
7 import datetime
8 import hmac
9 import os
10 import random
11 import sha
12 import sys
13 import time
14 import urllib
15 import socket
16 import cherrypy
17 from cherrypy import _cperror
18 from StringIO import StringIO
19
20 def printError():
21     """Revert stderr to stdout, and print the contents of stderr"""
22     if isinstance(sys.stderr, StringIO):
23         print revertStandardError()
24
25 if __name__ == '__main__':
26     import atexit
27     atexit.register(printError)
28
29 import validation
30 import cache_acls
31 from webcommon import State
32 import controls
33 from getafsgroups import getAfsGroupMembers
34 from invirt import database
35 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
36 from invirt.config import structs as config
37 from invirt.common import InvalidInput, CodeError
38
39 from view import View, revertStandardError
40 import ajaxterm
41
42
43 static_dir = os.path.join(os.path.dirname(__file__), 'static')
44 InvirtStatic = cherrypy.tools.staticdir.handler(
45     root=static_dir,
46     dir=static_dir,
47     section='/static')
48
49 class InvirtUnauthWeb(View):
50     static = InvirtStatic
51
52     @cherrypy.expose
53     @cherrypy.tools.mako(filename="/unauth.mako")
54     def index(self):
55         return {'simple': True}
56
57 class InvirtWeb(View):
58     def __init__(self):
59         super(self.__class__,self).__init__()
60         connect()
61         self._cp_config['tools.require_login.on'] = True
62         self._cp_config['tools.catch_stderr.on'] = True
63         self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
64                                                  'from invirt import database']
65         self._cp_config['request.error_response'] = self.handle_error
66
67     static = InvirtStatic
68
69     @cherrypy.expose
70     @cherrypy.tools.mako(filename="/invalid.mako")
71     def invalidInput(self):
72         """Print an error page when an InvalidInput exception occurs"""
73         err = cherrypy.request.prev.params["err"]
74         emsg = cherrypy.request.prev.params["emsg"]
75         d = dict(err_field=err.err_field,
76                  err_value=str(err.err_value), stderr=emsg,
77                  errorMessage=str(err))
78         return d
79
80     @cherrypy.expose
81     @cherrypy.tools.mako(filename="/error.mako")
82     def error(self):
83         """Print an error page when an exception occurs"""
84         op = cherrypy.request.prev.path_info
85         username = cherrypy.request.login
86         err = cherrypy.request.prev.params["err"]
87         emsg = cherrypy.request.prev.params["emsg"]
88         traceback = cherrypy.request.prev.params["traceback"]
89         d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
90                  errorMessage=str(err), stderr=emsg, traceback=traceback)
91         error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
92         details = error_raw.render(**d)
93         exclude = config.web.errormail_exclude
94         if username not in exclude and '*' not in exclude:
95             send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
96                             details)
97         d['details'] = details
98         return d
99
100     def __getattr__(self, name):
101         if name in ("admin", "overlord"):
102             if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
103                 raise InvalidInput('username', cherrypy.request.login,
104                                    'Not in admin group %s.' % config.adminacl)
105             cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
106             return self
107         else:
108             return super(InvirtWeb, self).__getattr__(name)
109
110     def handle_error(self):
111         err = sys.exc_info()[1]
112         if isinstance(err, InvalidInput):
113             cherrypy.request.params['err'] = err
114             cherrypy.request.params['emsg'] = revertStandardError()
115             raise cherrypy.InternalRedirect('/invalidInput')
116         if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
117             cherrypy.request.params['err'] = err
118             cherrypy.request.params['emsg'] = revertStandardError()
119             cherrypy.request.params['traceback'] = _cperror.format_exc()
120             raise cherrypy.InternalRedirect('/error')
121         # fall back to cherrypy default error page
122         cherrypy.HTTPError(500).set_response()
123
124     @cherrypy.expose
125     @cherrypy.tools.mako(filename="/list.mako")
126     def list(self, result=None):
127         """Handler for list requests."""
128         d = getListDict(cherrypy.request.login, cherrypy.request.state)
129         if result is not None:
130             d['result'] = result
131         return d
132     index=list
133
134     @cherrypy.expose
135     @cherrypy.tools.mako(filename="/help.mako")
136     def help(self, subject=None, simple=False):
137         """Handler for help messages."""
138
139         help_mapping = {
140             'Autoinstalls': """
141 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
142 ParaVM.  You can access the resulting system by logging into the <a
143 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
144 with your Kerberos tickets; there is no root password so sshd will
145 refuse login.</p>
146
147 <p>Under the covers, the autoinstaller uses our own patched version of
148 xen-create-image, which is a tool based on debootstrap.  If you log
149 into the serial console while the install is running, you can watch
150 it.
151 """,
152             'ParaVM Console': """
153 ParaVM machines do not support local console access over VNC.  To
154 access the serial console of these machines, you can SSH with Kerberos
155 to %s, using the name of the machine as your
156 username.""" % config.console.hostname,
157             'HVM/ParaVM': """
158 HVM machines use the virtualization features of the processor, while
159 ParaVM machines rely on a modified kernel to communicate directly with
160 the hypervisor.  HVMs support boot CDs of any operating system, and
161 the VNC console applet.  The three-minute autoinstaller produces
162 ParaVMs.  ParaVMs typically are more efficient, and always support the
163 <a href="help?subject=ParaVM+Console">console server</a>.</p>
164
165 <p>More details are <a
166 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
167 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
168 (which you can skip by using the autoinstaller to begin with.)</p>
169
170 <p>We recommend using a ParaVM when possible and an HVM when necessary.
171 """,
172             'CPU Weight': """
173 Don't ask us!  We're as mystified as you are.""",
174             'Owner': """
175 The owner field is used to determine <a
176 href="help?subject=Quotas">quotas</a>.  It must be the name of a
177 locker that you are an AFS administrator of.  In particular, you or an
178 AFS group you are a member of must have AFS rlidwka bits on the
179 locker.  You can check who administers the LOCKER locker using the
180 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
181 href="help?subject=Administrator">administrator</a>.""",
182             'Administrator': """
183 The administrator field determines who can access the console and
184 power on and off the machine.  This can be either a user or a moira
185 group.""",
186             'Quotas': """
187 Quotas are determined on a per-locker basis.  Each locker may have a
188 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
189 active machines.""",
190             'Console': """
191 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
192 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
193 your machine will run just fine, but the applet's display of the
194 console will suffer artifacts.
195 """,
196             'Windows': """
197 <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>
198 <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.
199 """
200             }
201
202         if not subject:
203             subject = sorted(help_mapping.keys())
204         if not isinstance(subject, list):
205             subject = [subject]
206
207         return dict(simple=simple,
208                     subjects=subject,
209                     mapping=help_mapping)
210     help._cp_config['tools.require_login.on'] = False
211
212     def parseCreate(self, fields):
213         kws = dict([(kw, fields[kw]) for kw in
214          'name description owner memory disksize vmtype cdrom autoinstall'.split()
215                     if fields[kw]])
216         validate = validation.Validate(cherrypy.request.login,
217                                        cherrypy.request.state,
218                                        strict=True, **kws)
219         return dict(contact=cherrypy.request.login, name=validate.name,
220                     description=validate.description, memory=validate.memory,
221                     disksize=validate.disksize, owner=validate.owner,
222                     machine_type=getattr(validate, 'vmtype', Defaults.type),
223                     cdrom=getattr(validate, 'cdrom', None),
224                     autoinstall=getattr(validate, 'autoinstall', None))
225
226     @cherrypy.expose
227     @cherrypy.tools.mako(filename="/list.mako")
228     @cherrypy.tools.require_POST()
229     def create(self, **fields):
230         """Handler for create requests."""
231         try:
232             parsed_fields = self.parseCreate(fields)
233             machine = controls.createVm(cherrypy.request.login,
234                                         cherrypy.request.state, **parsed_fields)
235         except InvalidInput, err:
236             pass
237         else:
238             err = None
239         cherrypy.request.state.clear() #Changed global state
240         d = getListDict(cherrypy.request.login, cherrypy.request.state)
241         d['err'] = err
242         if err:
243             for field, value in fields.items():
244                 setattr(d['defaults'], field, value)
245         else:
246             d['new_machine'] = parsed_fields['name']
247         return d
248
249     @cherrypy.expose
250     @cherrypy.tools.mako(filename="/helloworld.mako")
251     def helloworld(self, **kwargs):
252         return {'request': cherrypy.request, 'kwargs': kwargs}
253     helloworld._cp_config['tools.require_login.on'] = False
254
255     @cherrypy.expose
256     def errortest(self):
257         """Throw an error, to test the error-tracing mechanisms."""
258         print >>sys.stderr, "look ma, it's a stderr"
259         raise RuntimeError("test of the emergency broadcast system")
260
261     class MachineView(View):
262         def __getattr__(self, name):
263             """Synthesize attributes to allow RESTful URLs like
264             /machine/13/info. This is hairy. CherryPy 3.2 adds a
265             method called _cp_dispatch that allows you to explicitly
266             handle URLs that can't be mapped, and it allows you to
267             rewrite the path components and continue processing.
268
269             This function gets the next path component being resolved
270             as a string. _cp_dispatch will get an array of strings
271             representing any subsequent path components as well."""
272
273             try:
274                 cherrypy.request.params['machine_id'] = int(name)
275                 return self
276             except ValueError:
277                 return None
278
279         @cherrypy.expose
280         @cherrypy.tools.mako(filename="/info.mako")
281         def info(self, machine_id):
282             """Handler for info on a single VM."""
283             machine = validation.Validate(cherrypy.request.login,
284                                           cherrypy.request.state,
285                                           machine_id=machine_id).machine
286             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
287             return d
288         index = info
289
290         @cherrypy.expose
291         @cherrypy.tools.mako(filename="/info.mako")
292         @cherrypy.tools.require_POST()
293         def modify(self, machine_id, **fields):
294             """Handler for modifying attributes of a machine."""
295             try:
296                 modify_dict = modifyDict(cherrypy.request.login,
297                                          cherrypy.request.state,
298                                          machine_id, fields)
299             except InvalidInput, err:
300                 result = None
301                 machine = validation.Validate(cherrypy.request.login,
302                                               cherrypy.request.state,
303                                               machine_id=machine_id).machine
304             else:
305                 machine = modify_dict['machine']
306                 result = 'Success!'
307                 err = None
308             info_dict = infoDict(cherrypy.request.login,
309                                  cherrypy.request.state, machine)
310             info_dict['err'] = err
311             if err:
312                 for field, value in fields.items():
313                     setattr(info_dict['defaults'], field, value)
314             info_dict['result'] = result
315             return info_dict
316
317         @cherrypy.expose
318         @cherrypy.tools.mako(filename="/vnc.mako")
319         def vnc(self, machine_id):
320             """VNC applet page.
321
322             Note that due to same-domain restrictions, the applet connects to
323             the webserver, which needs to forward those requests to the xen
324             server.  The Xen server runs another proxy that (1) authenticates
325             and (2) finds the correct port for the VM.
326
327             You might want iptables like:
328
329             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
330             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
331             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
332             --dport 10003 -j SNAT --to-source 18.187.7.142
333             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
334             --dport 10003 -j ACCEPT
335
336             Remember to enable iptables!
337             echo 1 > /proc/sys/net/ipv4/ip_forward
338             """
339             machine = validation.Validate(cherrypy.request.login,
340                                           cherrypy.request.state,
341                                           machine_id=machine_id).machine
342             token = controls.vnctoken(machine)
343             host = controls.listHost(machine)
344             if host:
345                 port = 10003 + [h.hostname for h in config.hosts].index(host)
346             else:
347                 port = 5900 # dummy
348
349             status = controls.statusInfo(machine)
350             has_vnc = hasVnc(status)
351
352             d = dict(on=status,
353                      has_vnc=has_vnc,
354                      machine=machine,
355                      hostname=cherrypy.request.local.name,
356                      port=port,
357                      authtoken=token)
358             return d
359
360         @cherrypy.expose
361         @cherrypy.tools.mako(filename="/command.mako")
362         @cherrypy.tools.require_POST()
363         def command(self, command_name, machine_id, **kwargs):
364             """Handler for running commands like boot and delete on a VM."""
365             back = kwargs.get('back')
366             if command_name == 'delete':
367                 back = 'list'
368             try:
369                 d = controls.commandResult(cherrypy.request.login,
370                                            cherrypy.request.state,
371                                            command_name, machine_id, kwargs)
372             except InvalidInput, err:
373                 if not back:
374                     raise
375                 print >> sys.stderr, err
376                 result = str(err)
377             else:
378                 result = 'Success!'
379                 if not back:
380                     return d
381             if back == 'list':
382                 cherrypy.request.state.clear() #Changed global state
383                 raise cherrypy.InternalRedirect('/list?result=%s'
384                                                 % urllib.quote(result))
385             elif back == 'info':
386                 raise cherrypy.HTTPRedirect(cherrypy.request.base
387                                             + '/machine/%d/' % machine_id,
388                                             status=303)
389             else:
390                 raise InvalidInput('back', back, 'Not a known back page.')
391
392         atmulti = ajaxterm.Multiplex()
393         atsessions = {}
394
395         @cherrypy.expose
396         @cherrypy.tools.mako(filename="/terminal.mako")
397         def terminal(self, machine_id):
398             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
399
400             status = controls.statusInfo(machine)
401             has_vnc = hasVnc(status)
402
403             d = dict(on=status,
404                      has_vnc=has_vnc,
405                      machine=machine,
406                      hostname=cherrypy.request.local.name)
407             return d
408
409         @cherrypy.expose
410         def at(self, machine_id, k=None, c=0):
411             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
412             if machine_id in self.atsessions:
413                 term = self.atsessions[machine_id]
414             else:
415                 print >>sys.stderr, "spawning new session for terminal to ",machine_id
416                 term = self.atsessions[machine_id] = self.atmulti.create(
417                     ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
418                     )
419             if k:
420                 self.atmulti.proc_write(term,k)
421             time.sleep(0.002)
422             dump=self.atmulti.dump(term,c)
423             cherrypy.response.headers['Content-Type']='text/xml'
424             if isinstance(dump,str):
425                 return dump
426             else:
427                 del self.atsessions[machine_id]
428                 return '<?xml version="1.0"?><idem></idem>'
429
430     machine = MachineView()
431
432
433 class Defaults:
434     """Class to store default values for fields."""
435     memory = 256
436     disk = 4.0
437     cdrom = ''
438     autoinstall = ''
439     name = ''
440     description = ''
441     administrator = ''
442     type = 'linux-hvm'
443
444     def __init__(self, max_memory=None, max_disk=None, **kws):
445         if max_memory is not None:
446             self.memory = min(self.memory, max_memory)
447         if max_disk is not None:
448             self.disk = min(self.disk, max_disk)
449         for key in kws:
450             setattr(self, key, kws[key])
451
452 def hasVnc(status):
453     """Does the machine with a given status list support VNC?"""
454     if status is None:
455         return False
456     for l in status:
457         if l[0] == 'device' and l[1][0] == 'vfb':
458             d = dict(l[1][1:])
459             return 'location' in d
460     return False
461
462
463 def getListDict(username, state):
464     """Gets the list of local variables used by list.tmpl."""
465     machines = state.machines
466     on = {}
467     has_vnc = {}
468     installing = {}
469     xmlist = state.xmlist
470     for m in machines:
471         if m not in xmlist:
472             has_vnc[m] = 'Off'
473             m.uptime = None
474         else:
475             m.uptime = xmlist[m]['uptime']
476             installing[m] = bool(xmlist[m].get('autoinstall'))
477             if xmlist[m]['console']:
478                 has_vnc[m] = True
479             elif m.type.hvm:
480                 has_vnc[m] = "WTF?"
481             else:
482                 has_vnc[m] = "ParaVM"
483     max_memory = validation.maxMemory(username, state)
484     max_disk = validation.maxDisk(username)
485     defaults = Defaults(max_memory=max_memory,
486                         max_disk=max_disk,
487                         owner=username)
488     def sortkey(machine):
489         return (machine.owner != username, machine.owner, machine.name)
490     machines = sorted(machines, key=sortkey)
491     d = dict(user=username,
492              cant_add_vm=validation.cantAddVm(username, state),
493              max_memory=max_memory,
494              max_disk=max_disk,
495              defaults=defaults,
496              machines=machines,
497              has_vnc=has_vnc,
498              installing=installing)
499     return d
500
501 def getHostname(nic):
502     """Find the hostname associated with a NIC.
503
504     XXX this should be merged with the similar logic in DNS and DHCP.
505     """
506     if nic.hostname:
507         hostname = nic.hostname
508     elif nic.machine:
509         hostname = nic.machine.name
510     else:
511         return None
512     if '.' in hostname:
513         return hostname
514     else:
515         return hostname + '.' + config.dns.domains[0]
516
517 def getNicInfo(data_dict, machine):
518     """Helper function for info, get data on nics for a machine.
519
520     Modifies data_dict to include the relevant data, and returns a list
521     of (key, name) pairs to display "name: data_dict[key]" to the user.
522     """
523     data_dict['num_nics'] = len(machine.nics)
524     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
525                            ('nic%s_mac', 'NIC %s MAC Addr'),
526                            ('nic%s_ip', 'NIC %s IP'),
527                            ]
528     nic_fields = []
529     for i in range(len(machine.nics)):
530         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
531         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
532         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
533         data_dict['nic%s_ip' % i] = machine.nics[i].ip
534     if len(machine.nics) == 1:
535         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
536     return nic_fields
537
538 def getDiskInfo(data_dict, machine):
539     """Helper function for info, get data on disks for a machine.
540
541     Modifies data_dict to include the relevant data, and returns a list
542     of (key, name) pairs to display "name: data_dict[key]" to the user.
543     """
544     data_dict['num_disks'] = len(machine.disks)
545     disk_fields_template = [('%s_size', '%s size')]
546     disk_fields = []
547     for disk in machine.disks:
548         name = disk.guest_device_name
549         disk_fields.extend([(x % name, y % name) for x, y in
550                             disk_fields_template])
551         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
552     return disk_fields
553
554 def modifyDict(username, state, machine_id, fields):
555     """Modify a machine as specified by CGI arguments.
556
557     Return a dict containing the machine that was modified.
558     """
559     olddisk = {}
560     session.begin()
561     try:
562         kws = dict([(kw, fields[kw]) for kw in
563          'owner admin contact name description memory vmtype disksize'.split()
564                     if fields[kw]])
565         kws['machine_id'] = machine_id
566         validate = validation.Validate(username, state, **kws)
567         machine = validate.machine
568         oldname = machine.name
569
570         if hasattr(validate, 'memory'):
571             machine.memory = validate.memory
572
573         if hasattr(validate, 'vmtype'):
574             machine.type = validate.vmtype
575
576         if hasattr(validate, 'disksize'):
577             disksize = validate.disksize
578             disk = machine.disks[0]
579             if disk.size != disksize:
580                 olddisk[disk.guest_device_name] = disksize
581                 disk.size = disksize
582                 session.save_or_update(disk)
583
584         update_acl = False
585         if hasattr(validate, 'owner') and validate.owner != machine.owner:
586             machine.owner = validate.owner
587             update_acl = True
588         if hasattr(validate, 'name'):
589             machine.name = validate.name
590             for n in machine.nics:
591                 if n.hostname == oldname:
592                     n.hostname = validate.name
593         if hasattr(validate, 'description'):
594             machine.description = validate.description
595         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
596             machine.administrator = validate.admin
597             update_acl = True
598         if hasattr(validate, 'contact'):
599             machine.contact = validate.contact
600
601         session.save_or_update(machine)
602         if update_acl:
603             cache_acls.refreshMachine(machine)
604         session.commit()
605     except:
606         session.rollback()
607         raise
608     for diskname in olddisk:
609         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
610     if hasattr(validate, 'name'):
611         controls.renameMachine(machine, oldname, validate.name)
612     return dict(machine=machine)
613
614 def infoDict(username, state, machine):
615     """Get the variables used by info.tmpl."""
616     status = controls.statusInfo(machine)
617     has_vnc = hasVnc(status)
618     if status is None:
619         main_status = dict(name=machine.name,
620                            memory=str(machine.memory))
621         uptime = None
622         cputime = None
623     else:
624         main_status = dict(status[1:])
625         main_status['host'] = controls.listHost(machine)
626         start_time = float(main_status.get('start_time', 0))
627         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
628         cpu_time_float = float(main_status.get('cpu_time', 0))
629         cputime = datetime.timedelta(seconds=int(cpu_time_float))
630     display_fields = [('name', 'Name'),
631                       ('description', 'Description'),
632                       ('owner', 'Owner'),
633                       ('administrator', 'Administrator'),
634                       ('contact', 'Contact'),
635                       ('type', 'Type'),
636                       'NIC_INFO',
637                       ('uptime', 'uptime'),
638                       ('cputime', 'CPU usage'),
639                       ('host', 'Hosted on'),
640                       ('memory', 'RAM'),
641                       'DISK_INFO',
642                       ('state', 'state (xen format)'),
643                       ]
644     fields = []
645     machine_info = {}
646     machine_info['name'] = machine.name
647     machine_info['description'] = machine.description
648     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
649     machine_info['owner'] = machine.owner
650     machine_info['administrator'] = machine.administrator
651     machine_info['contact'] = machine.contact
652
653     nic_fields = getNicInfo(machine_info, machine)
654     nic_point = display_fields.index('NIC_INFO')
655     display_fields = (display_fields[:nic_point] + nic_fields +
656                       display_fields[nic_point+1:])
657
658     disk_fields = getDiskInfo(machine_info, machine)
659     disk_point = display_fields.index('DISK_INFO')
660     display_fields = (display_fields[:disk_point] + disk_fields +
661                       display_fields[disk_point+1:])
662
663     main_status['memory'] += ' MiB'
664     for field, disp in display_fields:
665         if field in ('uptime', 'cputime') and locals()[field] is not None:
666             fields.append((disp, locals()[field]))
667         elif field in machine_info:
668             fields.append((disp, machine_info[field]))
669         elif field in main_status:
670             fields.append((disp, main_status[field]))
671         else:
672             pass
673             #fields.append((disp, None))
674
675     max_mem = validation.maxMemory(machine.owner, state, machine, False)
676     max_disk = validation.maxDisk(machine.owner, machine)
677     defaults = Defaults()
678     for name in 'machine_id name description administrator owner memory contact'.split():
679         if getattr(machine, name):
680             setattr(defaults, name, getattr(machine, name))
681     defaults.type = machine.type.type_id
682     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
683     d = dict(user=username,
684              on=status is not None,
685              machine=machine,
686              defaults=defaults,
687              has_vnc=has_vnc,
688              uptime=str(uptime),
689              ram=machine.memory,
690              max_mem=max_mem,
691              max_disk=max_disk,
692              fields = fields)
693     return d
694
695 def send_error_mail(subject, body):
696     import subprocess
697
698     to = config.web.errormail
699     mail = """To: %s
700 From: root@%s
701 Subject: %s
702
703 %s
704 """ % (to, config.web.hostname, subject, body)
705     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
706                          stdin=subprocess.PIPE)
707     p.stdin.write(mail)
708     p.stdin.close()
709     p.wait()
710
711 random.seed() #sigh