c18cd9fcd82bcd664946d505f6611accb559d432
[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         @cherrypy.tools.gzip()
411         def at(self, machine_id, k=None, c=0, force=0):
412             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
413             if machine_id in self.atsessions:
414                 term = self.atsessions[machine_id]
415             else:
416                 print >>sys.stderr, "spawning new session for terminal to ",machine_id
417                 term = self.atsessions[machine_id] = self.atmulti.create(
418                     ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
419                     )
420             if k:
421                 self.atmulti.proc_write(term,k)
422             time.sleep(0.002)
423             dump=self.atmulti.dump(term,c,int(force))
424             cherrypy.response.headers['Content-Type']='text/xml'
425             if isinstance(dump,str):
426                 return dump
427             else:
428                 print "Removing session for", machine_id
429                 del self.atsessions[machine_id]
430                 return '<?xml version="1.0"?><idem></idem>'
431
432     machine = MachineView()
433
434
435 class Defaults:
436     """Class to store default values for fields."""
437     memory = 256
438     disk = 4.0
439     cdrom = ''
440     autoinstall = ''
441     name = ''
442     description = ''
443     administrator = ''
444     type = 'linux-hvm'
445
446     def __init__(self, max_memory=None, max_disk=None, **kws):
447         if max_memory is not None:
448             self.memory = min(self.memory, max_memory)
449         if max_disk is not None:
450             self.disk = min(self.disk, max_disk)
451         for key in kws:
452             setattr(self, key, kws[key])
453
454 def hasVnc(status):
455     """Does the machine with a given status list support VNC?"""
456     if status is None:
457         return False
458     for l in status:
459         if l[0] == 'device' and l[1][0] == 'vfb':
460             d = dict(l[1][1:])
461             return 'location' in d
462     return False
463
464
465 def getListDict(username, state):
466     """Gets the list of local variables used by list.tmpl."""
467     machines = state.machines
468     on = {}
469     has_vnc = {}
470     installing = {}
471     xmlist = state.xmlist
472     for m in machines:
473         if m not in xmlist:
474             has_vnc[m] = 'Off'
475             m.uptime = None
476         else:
477             m.uptime = xmlist[m]['uptime']
478             installing[m] = bool(xmlist[m].get('autoinstall'))
479             if xmlist[m]['console']:
480                 has_vnc[m] = True
481             elif m.type.hvm:
482                 has_vnc[m] = "WTF?"
483             else:
484                 has_vnc[m] = "ParaVM"
485     max_memory = validation.maxMemory(username, state)
486     max_disk = validation.maxDisk(username)
487     defaults = Defaults(max_memory=max_memory,
488                         max_disk=max_disk,
489                         owner=username)
490     def sortkey(machine):
491         return (machine.owner != username, machine.owner, machine.name)
492     machines = sorted(machines, key=sortkey)
493     d = dict(user=username,
494              cant_add_vm=validation.cantAddVm(username, state),
495              max_memory=max_memory,
496              max_disk=max_disk,
497              defaults=defaults,
498              machines=machines,
499              has_vnc=has_vnc,
500              installing=installing)
501     return d
502
503 def getHostname(nic):
504     """Find the hostname associated with a NIC.
505
506     XXX this should be merged with the similar logic in DNS and DHCP.
507     """
508     if nic.hostname:
509         hostname = nic.hostname
510     elif nic.machine:
511         hostname = nic.machine.name
512     else:
513         return None
514     if '.' in hostname:
515         return hostname
516     else:
517         return hostname + '.' + config.dns.domains[0]
518
519 def getNicInfo(data_dict, machine):
520     """Helper function for info, get data on nics for a machine.
521
522     Modifies data_dict to include the relevant data, and returns a list
523     of (key, name) pairs to display "name: data_dict[key]" to the user.
524     """
525     data_dict['num_nics'] = len(machine.nics)
526     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
527                            ('nic%s_mac', 'NIC %s MAC Addr'),
528                            ('nic%s_ip', 'NIC %s IP'),
529                            ]
530     nic_fields = []
531     for i in range(len(machine.nics)):
532         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
533         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
534         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
535         data_dict['nic%s_ip' % i] = machine.nics[i].ip
536     if len(machine.nics) == 1:
537         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
538     return nic_fields
539
540 def getDiskInfo(data_dict, machine):
541     """Helper function for info, get data on disks for a machine.
542
543     Modifies data_dict to include the relevant data, and returns a list
544     of (key, name) pairs to display "name: data_dict[key]" to the user.
545     """
546     data_dict['num_disks'] = len(machine.disks)
547     disk_fields_template = [('%s_size', '%s size')]
548     disk_fields = []
549     for disk in machine.disks:
550         name = disk.guest_device_name
551         disk_fields.extend([(x % name, y % name) for x, y in
552                             disk_fields_template])
553         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
554     return disk_fields
555
556 def modifyDict(username, state, machine_id, fields):
557     """Modify a machine as specified by CGI arguments.
558
559     Return a dict containing the machine that was modified.
560     """
561     olddisk = {}
562     session.begin()
563     try:
564         kws = dict([(kw, fields[kw]) for kw in
565          'owner admin contact name description memory vmtype disksize'.split()
566                     if fields[kw]])
567         kws['machine_id'] = machine_id
568         validate = validation.Validate(username, state, **kws)
569         machine = validate.machine
570         oldname = machine.name
571
572         if hasattr(validate, 'memory'):
573             machine.memory = validate.memory
574
575         if hasattr(validate, 'vmtype'):
576             machine.type = validate.vmtype
577
578         if hasattr(validate, 'disksize'):
579             disksize = validate.disksize
580             disk = machine.disks[0]
581             if disk.size != disksize:
582                 olddisk[disk.guest_device_name] = disksize
583                 disk.size = disksize
584                 session.save_or_update(disk)
585
586         update_acl = False
587         if hasattr(validate, 'owner') and validate.owner != machine.owner:
588             machine.owner = validate.owner
589             update_acl = True
590         if hasattr(validate, 'name'):
591             machine.name = validate.name
592             for n in machine.nics:
593                 if n.hostname == oldname:
594                     n.hostname = validate.name
595         if hasattr(validate, 'description'):
596             machine.description = validate.description
597         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
598             machine.administrator = validate.admin
599             update_acl = True
600         if hasattr(validate, 'contact'):
601             machine.contact = validate.contact
602
603         session.save_or_update(machine)
604         if update_acl:
605             cache_acls.refreshMachine(machine)
606         session.commit()
607     except:
608         session.rollback()
609         raise
610     for diskname in olddisk:
611         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
612     if hasattr(validate, 'name'):
613         controls.renameMachine(machine, oldname, validate.name)
614     return dict(machine=machine)
615
616 def infoDict(username, state, machine):
617     """Get the variables used by info.tmpl."""
618     status = controls.statusInfo(machine)
619     has_vnc = hasVnc(status)
620     if status is None:
621         main_status = dict(name=machine.name,
622                            memory=str(machine.memory))
623         uptime = None
624         cputime = None
625     else:
626         main_status = dict(status[1:])
627         main_status['host'] = controls.listHost(machine)
628         start_time = float(main_status.get('start_time', 0))
629         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
630         cpu_time_float = float(main_status.get('cpu_time', 0))
631         cputime = datetime.timedelta(seconds=int(cpu_time_float))
632     display_fields = [('name', 'Name'),
633                       ('description', 'Description'),
634                       ('owner', 'Owner'),
635                       ('administrator', 'Administrator'),
636                       ('contact', 'Contact'),
637                       ('type', 'Type'),
638                       'NIC_INFO',
639                       ('uptime', 'uptime'),
640                       ('cputime', 'CPU usage'),
641                       ('host', 'Hosted on'),
642                       ('memory', 'RAM'),
643                       'DISK_INFO',
644                       ('state', 'state (xen format)'),
645                       ]
646     fields = []
647     machine_info = {}
648     machine_info['name'] = machine.name
649     machine_info['description'] = machine.description
650     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
651     machine_info['owner'] = machine.owner
652     machine_info['administrator'] = machine.administrator
653     machine_info['contact'] = machine.contact
654
655     nic_fields = getNicInfo(machine_info, machine)
656     nic_point = display_fields.index('NIC_INFO')
657     display_fields = (display_fields[:nic_point] + nic_fields +
658                       display_fields[nic_point+1:])
659
660     disk_fields = getDiskInfo(machine_info, machine)
661     disk_point = display_fields.index('DISK_INFO')
662     display_fields = (display_fields[:disk_point] + disk_fields +
663                       display_fields[disk_point+1:])
664
665     main_status['memory'] += ' MiB'
666     for field, disp in display_fields:
667         if field in ('uptime', 'cputime') and locals()[field] is not None:
668             fields.append((disp, locals()[field]))
669         elif field in machine_info:
670             fields.append((disp, machine_info[field]))
671         elif field in main_status:
672             fields.append((disp, main_status[field]))
673         else:
674             pass
675             #fields.append((disp, None))
676
677     max_mem = validation.maxMemory(machine.owner, state, machine, False)
678     max_disk = validation.maxDisk(machine.owner, machine)
679     defaults = Defaults()
680     for name in 'machine_id name description administrator owner memory contact'.split():
681         if getattr(machine, name):
682             setattr(defaults, name, getattr(machine, name))
683     defaults.type = machine.type.type_id
684     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
685     d = dict(user=username,
686              on=status is not None,
687              machine=machine,
688              defaults=defaults,
689              has_vnc=has_vnc,
690              uptime=str(uptime),
691              ram=machine.memory,
692              max_mem=max_mem,
693              max_disk=max_disk,
694              fields = fields)
695     return d
696
697 def send_error_mail(subject, body):
698     import subprocess
699
700     to = config.web.errormail
701     mail = """To: %s
702 From: root@%s
703 Subject: %s
704
705 %s
706 """ % (to, config.web.hostname, subject, body)
707     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
708                          stdin=subprocess.PIPE)
709     p.stdin.write(mail)
710     p.stdin.close()
711     p.wait()
712
713 random.seed() #sigh