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