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