f235c53f1474efc62a9d421fefa0515ab76dc3a7
[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.cells[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         def __getattr__(self, name):
252             """Synthesize attributes to allow RESTful URLs like
253             /machine/13/info. This is hairy. CherryPy 3.2 adds a
254             method called _cp_dispatch that allows you to explicitly
255             handle URLs that can't be mapped, and it allows you to
256             rewrite the path components and continue processing.
257
258             This function gets the next path component being resolved
259             as a string. _cp_dispatch will get an array of strings
260             representing any subsequent path components as well."""
261
262             try:
263                 cherrypy.request.params['machine_id'] = int(name)
264                 return self
265             except ValueError:
266                 return None
267
268         @cherrypy.expose
269         @cherrypy.tools.mako(filename="/info.mako")
270         def info(self, machine_id):
271             """Handler for info on a single VM."""
272             machine = validation.Validate(cherrypy.request.login,
273                                           cherrypy.request.state,
274                                           machine_id=machine_id).machine
275             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
276             checkpoint.checkpoint('Got infodict')
277             return d
278         index = info
279
280         @cherrypy.expose
281         @cherrypy.tools.mako(filename="/info.mako")
282         @cherrypy.tools.require_POST()
283         def modify(self, machine_id, **fields):
284             """Handler for modifying attributes of a machine."""
285             try:
286                 modify_dict = modifyDict(cherrypy.request.login,
287                                          cherrypy.request.state,
288                                          machine_id, fields)
289             except InvalidInput, err:
290                 result = None
291                 machine = validation.Validate(cherrypy.request.login,
292                                               cherrypy.request.state,
293                                               machine_id=machine_id).machine
294             else:
295                 machine = modify_dict['machine']
296                 result = 'Success!'
297                 err = None
298             info_dict = infoDict(cherrypy.request.login,
299                                  cherrypy.request.state, machine)
300             info_dict['err'] = err
301             if err:
302                 for field, value in fields.items():
303                     setattr(info_dict['defaults'], field, value)
304             info_dict['result'] = result
305             return info_dict
306
307         @cherrypy.expose
308         @cherrypy.tools.mako(filename="/vnc.mako")
309         def vnc(self, machine_id):
310             """VNC applet page.
311
312             Note that due to same-domain restrictions, the applet connects to
313             the webserver, which needs to forward those requests to the xen
314             server.  The Xen server runs another proxy that (1) authenticates
315             and (2) finds the correct port for the VM.
316
317             You might want iptables like:
318
319             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
320             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
321             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
322             --dport 10003 -j SNAT --to-source 18.187.7.142
323             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
324             --dport 10003 -j ACCEPT
325
326             Remember to enable iptables!
327             echo 1 > /proc/sys/net/ipv4/ip_forward
328             """
329             machine = validation.Validate(cherrypy.request.login,
330                                           cherrypy.request.state,
331                                           machine_id=machine_id).machine
332             token = controls.vnctoken(machine)
333             host = controls.listHost(machine)
334             if host:
335                 port = 10003 + [h.hostname for h in config.hosts].index(host)
336             else:
337                 port = 5900 # dummy
338
339             status = controls.statusInfo(machine)
340             has_vnc = hasVnc(status)
341
342             d = dict(on=status,
343                      has_vnc=has_vnc,
344                      machine=machine,
345                      hostname=cherrypy.request.local.name,
346                      port=port,
347                      authtoken=token)
348             return d
349
350         @cherrypy.expose
351         @cherrypy.tools.mako(filename="/command.mako")
352         @cherrypy.tools.require_POST()
353         def command(self, command_name, machine_id, **kwargs):
354             """Handler for running commands like boot and delete on a VM."""
355             back = kwargs.get('back')
356             try:
357                 d = controls.commandResult(cherrypy.request.login,
358                                            cherrypy.request.state,
359                                            command_name, machine_id, kwargs)
360                 if d['command'] == 'Delete VM':
361                     back = 'list'
362             except InvalidInput, err:
363                 if not back:
364                     raise
365                 print >> sys.stderr, err
366                 result = str(err)
367             else:
368                 result = 'Success!'
369                 if not back:
370                     return d
371             if back == 'list':
372                 cherrypy.request.state.clear() #Changed global state
373                 raise cherrypy.InternalRedirect('/list?result=%s'
374                                                 % urllib.quote(result))
375             elif back == 'info':
376                 raise cherrypy.HTTPRedirect(cherrypy.request.base
377                                             + '/machine/%d/' % machine_id,
378                                             status=303)
379             else:
380                 raise InvalidInput('back', back, 'Not a known back page.')
381
382     machine = MachineView()
383
384 class Checkpoint:
385     def __init__(self):
386         self.start_time = time.time()
387         self.checkpoints = []
388
389     def checkpoint(self, s):
390         self.checkpoints.append((s, time.time()))
391
392     def __str__(self):
393         return ('Timing info:\n%s\n' %
394                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
395                            (d, t) in self.checkpoints]))
396
397 checkpoint = Checkpoint()
398
399 class Defaults:
400     """Class to store default values for fields."""
401     memory = 256
402     disk = 4.0
403     cdrom = ''
404     autoinstall = ''
405     name = ''
406     description = ''
407     administrator = ''
408     type = 'linux-hvm'
409
410     def __init__(self, max_memory=None, max_disk=None, **kws):
411         if max_memory is not None:
412             self.memory = min(self.memory, max_memory)
413         if max_disk is not None:
414             self.disk = min(self.disk, max_disk)
415         for key in kws:
416             setattr(self, key, kws[key])
417
418 def hasVnc(status):
419     """Does the machine with a given status list support VNC?"""
420     if status is None:
421         return False
422     for l in status:
423         if l[0] == 'device' and l[1][0] == 'vfb':
424             d = dict(l[1][1:])
425             return 'location' in d
426     return False
427
428
429 def getListDict(username, state):
430     """Gets the list of local variables used by list.tmpl."""
431     checkpoint.checkpoint('Starting')
432     machines = state.machines
433     checkpoint.checkpoint('Got my machines')
434     on = {}
435     has_vnc = {}
436     installing = {}
437     xmlist = state.xmlist
438     checkpoint.checkpoint('Got uptimes')
439     for m in machines:
440         if m not in xmlist:
441             has_vnc[m] = 'Off'
442             m.uptime = None
443         else:
444             m.uptime = xmlist[m]['uptime']
445             if xmlist[m]['console']:
446                 has_vnc[m] = True
447             elif m.type.hvm:
448                 has_vnc[m] = "WTF?"
449             else:
450                 has_vnc[m] = "ParaVM"
451             if xmlist[m].get('autoinstall'):
452                 installing[m] = True
453             else:
454                 installing[m] = False
455     max_memory = validation.maxMemory(username, state)
456     max_disk = validation.maxDisk(username)
457     checkpoint.checkpoint('Got max mem/disk')
458     defaults = Defaults(max_memory=max_memory,
459                         max_disk=max_disk,
460                         owner=username)
461     checkpoint.checkpoint('Got defaults')
462     def sortkey(machine):
463         return (machine.owner != username, machine.owner, machine.name)
464     machines = sorted(machines, key=sortkey)
465     d = dict(user=username,
466              cant_add_vm=validation.cantAddVm(username, state),
467              max_memory=max_memory,
468              max_disk=max_disk,
469              defaults=defaults,
470              machines=machines,
471              has_vnc=has_vnc,
472              installing=installing)
473     return d
474
475 def getHostname(nic):
476     """Find the hostname associated with a NIC.
477
478     XXX this should be merged with the similar logic in DNS and DHCP.
479     """
480     if nic.hostname:
481         hostname = nic.hostname
482     elif nic.machine:
483         hostname = nic.machine.name
484     else:
485         return None
486     if '.' in hostname:
487         return hostname
488     else:
489         return hostname + '.' + config.dns.domains[0]
490
491 def getNicInfo(data_dict, machine):
492     """Helper function for info, get data on nics for a machine.
493
494     Modifies data_dict to include the relevant data, and returns a list
495     of (key, name) pairs to display "name: data_dict[key]" to the user.
496     """
497     data_dict['num_nics'] = len(machine.nics)
498     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
499                            ('nic%s_mac', 'NIC %s MAC Addr'),
500                            ('nic%s_ip', 'NIC %s IP'),
501                            ]
502     nic_fields = []
503     for i in range(len(machine.nics)):
504         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
505         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
506         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
507         data_dict['nic%s_ip' % i] = machine.nics[i].ip
508     if len(machine.nics) == 1:
509         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
510     return nic_fields
511
512 def getDiskInfo(data_dict, machine):
513     """Helper function for info, get data on disks for a machine.
514
515     Modifies data_dict to include the relevant data, and returns a list
516     of (key, name) pairs to display "name: data_dict[key]" to the user.
517     """
518     data_dict['num_disks'] = len(machine.disks)
519     disk_fields_template = [('%s_size', '%s size')]
520     disk_fields = []
521     for disk in machine.disks:
522         name = disk.guest_device_name
523         disk_fields.extend([(x % name, y % name) for x, y in
524                             disk_fields_template])
525         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
526     return disk_fields
527
528 def modifyDict(username, state, machine_id, fields):
529     """Modify a machine as specified by CGI arguments.
530
531     Return a dict containing the machine that was modified.
532     """
533     olddisk = {}
534     session.begin()
535     try:
536         kws = dict([(kw, fields[kw]) for kw in
537          'owner admin contact name description memory vmtype disksize'.split()
538                     if fields[kw]])
539         kws['machine_id'] = machine_id
540         validate = validation.Validate(username, state, **kws)
541         machine = validate.machine
542         oldname = machine.name
543
544         if hasattr(validate, 'memory'):
545             machine.memory = validate.memory
546
547         if hasattr(validate, 'vmtype'):
548             machine.type = validate.vmtype
549
550         if hasattr(validate, 'disksize'):
551             disksize = validate.disksize
552             disk = machine.disks[0]
553             if disk.size != disksize:
554                 olddisk[disk.guest_device_name] = disksize
555                 disk.size = disksize
556                 session.save_or_update(disk)
557
558         update_acl = False
559         if hasattr(validate, 'owner') and validate.owner != machine.owner:
560             machine.owner = validate.owner
561             update_acl = True
562         if hasattr(validate, 'name'):
563             machine.name = validate.name
564             for n in machine.nics:
565                 if n.hostname == oldname:
566                     n.hostname = validate.name
567         if hasattr(validate, 'description'):
568             machine.description = validate.description
569         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
570             machine.administrator = validate.admin
571             update_acl = True
572         if hasattr(validate, 'contact'):
573             machine.contact = validate.contact
574
575         session.save_or_update(machine)
576         if update_acl:
577             cache_acls.refreshMachine(machine)
578         session.commit()
579     except:
580         session.rollback()
581         raise
582     for diskname in olddisk:
583         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
584     if hasattr(validate, 'name'):
585         controls.renameMachine(machine, oldname, validate.name)
586     return dict(machine=machine)
587
588 def infoDict(username, state, machine):
589     """Get the variables used by info.tmpl."""
590     status = controls.statusInfo(machine)
591     checkpoint.checkpoint('Getting status info')
592     has_vnc = hasVnc(status)
593     if status is None:
594         main_status = dict(name=machine.name,
595                            memory=str(machine.memory))
596         uptime = None
597         cputime = None
598     else:
599         main_status = dict(status[1:])
600         main_status['host'] = controls.listHost(machine)
601         start_time = float(main_status.get('start_time', 0))
602         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
603         cpu_time_float = float(main_status.get('cpu_time', 0))
604         cputime = datetime.timedelta(seconds=int(cpu_time_float))
605     checkpoint.checkpoint('Status')
606     display_fields = [('name', 'Name'),
607                       ('description', 'Description'),
608                       ('owner', 'Owner'),
609                       ('administrator', 'Administrator'),
610                       ('contact', 'Contact'),
611                       ('type', 'Type'),
612                       'NIC_INFO',
613                       ('uptime', 'uptime'),
614                       ('cputime', 'CPU usage'),
615                       ('host', 'Hosted on'),
616                       ('memory', 'RAM'),
617                       'DISK_INFO',
618                       ('state', 'state (xen format)'),
619                       ]
620     fields = []
621     machine_info = {}
622     machine_info['name'] = machine.name
623     machine_info['description'] = machine.description
624     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
625     machine_info['owner'] = machine.owner
626     machine_info['administrator'] = machine.administrator
627     machine_info['contact'] = machine.contact
628
629     nic_fields = getNicInfo(machine_info, machine)
630     nic_point = display_fields.index('NIC_INFO')
631     display_fields = (display_fields[:nic_point] + nic_fields +
632                       display_fields[nic_point+1:])
633
634     disk_fields = getDiskInfo(machine_info, machine)
635     disk_point = display_fields.index('DISK_INFO')
636     display_fields = (display_fields[:disk_point] + disk_fields +
637                       display_fields[disk_point+1:])
638
639     main_status['memory'] += ' MiB'
640     for field, disp in display_fields:
641         if field in ('uptime', 'cputime') and locals()[field] is not None:
642             fields.append((disp, locals()[field]))
643         elif field in machine_info:
644             fields.append((disp, machine_info[field]))
645         elif field in main_status:
646             fields.append((disp, main_status[field]))
647         else:
648             pass
649             #fields.append((disp, None))
650
651     checkpoint.checkpoint('Got fields')
652
653
654     max_mem = validation.maxMemory(machine.owner, state, machine, False)
655     checkpoint.checkpoint('Got mem')
656     max_disk = validation.maxDisk(machine.owner, machine)
657     defaults = Defaults()
658     for name in 'machine_id name description administrator owner memory contact'.split():
659         if getattr(machine, name):
660             setattr(defaults, name, getattr(machine, name))
661     defaults.type = machine.type.type_id
662     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
663     checkpoint.checkpoint('Got defaults')
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