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