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