Checkout the current HEAD when building
[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     return d
464
465 def getHostname(nic):
466     """Find the hostname associated with a NIC.
467
468     XXX this should be merged with the similar logic in DNS and DHCP.
469     """
470     if nic.hostname:
471         hostname = nic.hostname
472     elif nic.machine:
473         hostname = nic.machine.name
474     else:
475         return None
476     if '.' in hostname:
477         return hostname
478     else:
479         return hostname + '.' + config.dns.domains[0]
480
481 def getNicInfo(data_dict, machine):
482     """Helper function for info, get data on nics for a machine.
483
484     Modifies data_dict to include the relevant data, and returns a list
485     of (key, name) pairs to display "name: data_dict[key]" to the user.
486     """
487     data_dict['num_nics'] = len(machine.nics)
488     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
489                            ('nic%s_mac', 'NIC %s MAC Addr'),
490                            ('nic%s_ip', 'NIC %s IP'),
491                            ]
492     nic_fields = []
493     for i in range(len(machine.nics)):
494         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
495         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
496         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
497         data_dict['nic%s_ip' % i] = machine.nics[i].ip
498     if len(machine.nics) == 1:
499         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
500     return nic_fields
501
502 def getDiskInfo(data_dict, machine):
503     """Helper function for info, get data on disks for a machine.
504
505     Modifies data_dict to include the relevant data, and returns a list
506     of (key, name) pairs to display "name: data_dict[key]" to the user.
507     """
508     data_dict['num_disks'] = len(machine.disks)
509     disk_fields_template = [('%s_size', '%s size')]
510     disk_fields = []
511     for disk in machine.disks:
512         name = disk.guest_device_name
513         disk_fields.extend([(x % name, y % name) for x, y in
514                             disk_fields_template])
515         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
516     return disk_fields
517
518 def modifyDict(username, state, machine_id, fields):
519     """Modify a machine as specified by CGI arguments.
520
521     Return a dict containing the machine that was modified.
522     """
523     olddisk = {}
524     session.begin()
525     try:
526         kws = dict((kw, fields[kw]) for kw in
527          'owner admin contact name description memory vmtype disksize'.split()
528                     if fields.get(kw))
529         kws['machine_id'] = machine_id
530         validate = validation.Validate(username, state, **kws)
531         machine = validate.machine
532         oldname = machine.name
533
534         if hasattr(validate, 'memory'):
535             machine.memory = validate.memory
536
537         if hasattr(validate, 'vmtype'):
538             machine.type = validate.vmtype
539
540         update_acl = False
541         if hasattr(validate, 'owner') and validate.owner != machine.owner:
542             machine.owner = validate.owner
543             update_acl = True
544         if hasattr(validate, 'description'):
545             machine.description = validate.description
546         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
547             machine.administrator = validate.admin
548             update_acl = True
549         if hasattr(validate, 'contact'):
550             machine.contact = validate.contact
551
552         session.save_or_update(machine)
553         session.commit()
554     except:
555         session.rollback()
556         raise
557
558     session.begin()
559     try:
560         if hasattr(validate, 'disksize'):
561             disksize = validate.disksize
562             disk = machine.disks[0]
563             if disk.size != disksize:
564                 olddisk[disk.guest_device_name] = disksize
565                 disk.size = disksize
566                 session.save_or_update(disk)
567         for diskname in olddisk:
568             controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
569         session.save_or_update(machine)
570         session.commit()
571     except:
572         session.rollback()
573         raise
574
575     session.begin()
576     try:
577         if hasattr(validate, 'name'):
578             machine.name = validate.name
579             for n in machine.nics:
580                 if n.hostname == oldname:
581                     n.hostname = validate.name
582         if hasattr(validate, 'name'):
583             controls.renameMachine(machine, oldname, validate.name)
584         session.save_or_update(machine)
585         session.commit()
586     except:
587         session.rollback()
588         raise
589
590     if update_acl:
591         cache_acls.refreshMachine(machine)
592
593     return dict(machine=machine)
594
595 def infoDict(username, state, machine):
596     """Get the variables used by info.tmpl."""
597     status = controls.statusInfo(machine)
598     has_vnc = hasVnc(status)
599     if status is None:
600         main_status = dict(name=machine.name,
601                            memory=str(machine.memory))
602         uptime = None
603         cputime = None
604     else:
605         main_status = dict(status[1:])
606         main_status['host'] = controls.listHost(machine)
607         start_time = float(main_status.get('start_time', 0))
608         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
609         cpu_time_float = float(main_status.get('cpu_time', 0))
610         cputime = datetime.timedelta(seconds=int(cpu_time_float))
611     display_fields = [('name', 'Name'),
612                       ('description', 'Description'),
613                       ('owner', 'Owner'),
614                       ('administrator', 'Administrator'),
615                       ('contact', 'Contact'),
616                       ('type', 'Type'),
617                       'NIC_INFO',
618                       ('uptime', 'uptime'),
619                       ('cputime', 'CPU usage'),
620                       ('host', 'Hosted on'),
621                       ('memory', 'RAM'),
622                       'DISK_INFO',
623                       ('state', 'state (xen format)'),
624                       ]
625     fields = []
626     machine_info = {}
627     machine_info['name'] = machine.name
628     machine_info['description'] = machine.description
629     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
630     machine_info['owner'] = machine.owner
631     machine_info['administrator'] = machine.administrator
632     machine_info['contact'] = machine.contact
633
634     nic_fields = getNicInfo(machine_info, machine)
635     nic_point = display_fields.index('NIC_INFO')
636     display_fields = (display_fields[:nic_point] + nic_fields +
637                       display_fields[nic_point+1:])
638
639     disk_fields = getDiskInfo(machine_info, machine)
640     disk_point = display_fields.index('DISK_INFO')
641     display_fields = (display_fields[:disk_point] + disk_fields +
642                       display_fields[disk_point+1:])
643
644     main_status['memory'] += ' MiB'
645     for field, disp in display_fields:
646         if field in ('uptime', 'cputime') and locals()[field] is not None:
647             fields.append((disp, locals()[field]))
648         elif field in machine_info:
649             fields.append((disp, machine_info[field]))
650         elif field in main_status:
651             fields.append((disp, main_status[field]))
652         else:
653             pass
654             #fields.append((disp, None))
655
656     max_mem = validation.maxMemory(machine.owner, state, machine, False)
657     max_disk = validation.maxDisk(machine.owner, machine)
658     defaults = Defaults()
659     for name in 'machine_id name description administrator owner memory contact'.split():
660         if getattr(machine, name):
661             setattr(defaults, name, getattr(machine, name))
662     defaults.type = machine.type.type_id
663     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
664     d = dict(user=username,
665              on=status is not None,
666              machine=machine,
667              defaults=defaults,
668              has_vnc=has_vnc,
669              uptime=str(uptime),
670              ram=machine.memory,
671              max_mem=max_mem,
672              max_disk=max_disk,
673              fields = fields)
674     return d
675
676 def send_error_mail(subject, body):
677     import subprocess
678
679     to = config.web.errormail
680     mail = """To: %s
681 From: root@%s
682 Subject: %s
683
684 %s
685 """ % (to, config.web.hostname, subject, body)
686     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
687                          stdin=subprocess.PIPE)
688     p.stdin.write(mail)
689     p.stdin.close()
690     p.wait()
691
692 random.seed() #sigh