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