Updated help text with formatting changes.
[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             'Owner': """
175 The owner field is used to determine <a
176 href="help?subject=Quotas">quotas</a>.  It must be the name of a
177 locker that you are an AFS administrator of.  In particular, you or an
178 AFS group you are a member of must have AFS rlidwka bits on the
179 locker.  You can check who administers the LOCKER locker using the
180 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
181 href="help?subject=Administrator">administrator</a>.""",
182             'Administrator': """
183 The administrator field determines who can access the console and
184 power on and off the machine.  This can be either a user or a moira
185 group.""",
186             'Quotas': """
187 Quotas are determined on a per-locker basis.  Each locker may have a
188 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
189 active machines.""",
190             'Console': """
191 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
192 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
193 your machine will run just fine, but the applet's display of the
194 console will suffer artifacts.
195 """,
196             'Windows': """
197 <strong>Windows 7:</strong> The Windows 7 image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-7.txt">the licensing agreement</a> for details. The installer requires 512 MiB RAM and at least 15 GiB disk space (20 GiB or more recommended).<br>
198 <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>
199 <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.
200 """
201             }
202
203         if not subject:
204             subject = sorted(help_mapping.keys())
205         if not isinstance(subject, list):
206             subject = [subject]
207
208         return dict(simple=simple,
209                     subjects=subject,
210                     mapping=help_mapping)
211     help._cp_config['tools.require_login.on'] = False
212
213     def parseCreate(self, fields):
214         kws = dict([(kw, fields[kw]) for kw in
215          'name description owner memory disksize vmtype cdrom autoinstall'.split()
216                     if fields[kw]])
217         validate = validation.Validate(cherrypy.request.login,
218                                        cherrypy.request.state,
219                                        strict=True, **kws)
220         return dict(contact=cherrypy.request.login, name=validate.name,
221                     description=validate.description, memory=validate.memory,
222                     disksize=validate.disksize, owner=validate.owner,
223                     machine_type=getattr(validate, 'vmtype', Defaults.type),
224                     cdrom=getattr(validate, 'cdrom', None),
225                     autoinstall=getattr(validate, 'autoinstall', None))
226
227     @cherrypy.expose
228     @cherrypy.tools.mako(filename="/list.mako")
229     @cherrypy.tools.require_POST()
230     def create(self, **fields):
231         """Handler for create requests."""
232         try:
233             parsed_fields = self.parseCreate(fields)
234             machine = controls.createVm(cherrypy.request.login,
235                                         cherrypy.request.state, **parsed_fields)
236         except InvalidInput, err:
237             pass
238         else:
239             err = None
240         cherrypy.request.state.clear() #Changed global state
241         d = getListDict(cherrypy.request.login, cherrypy.request.state)
242         d['err'] = err
243         if err:
244             for field, value in fields.items():
245                 setattr(d['defaults'], field, value)
246         else:
247             d['new_machine'] = parsed_fields['name']
248         return d
249
250     @cherrypy.expose
251     @cherrypy.tools.mako(filename="/helloworld.mako")
252     def helloworld(self, **kwargs):
253         return {'request': cherrypy.request, 'kwargs': kwargs}
254     helloworld._cp_config['tools.require_login.on'] = False
255
256     @cherrypy.expose
257     def errortest(self):
258         """Throw an error, to test the error-tracing mechanisms."""
259         print >>sys.stderr, "look ma, it's a stderr"
260         raise RuntimeError("test of the emergency broadcast system")
261
262     class MachineView(View):
263         def __getattr__(self, name):
264             """Synthesize attributes to allow RESTful URLs like
265             /machine/13/info. This is hairy. CherryPy 3.2 adds a
266             method called _cp_dispatch that allows you to explicitly
267             handle URLs that can't be mapped, and it allows you to
268             rewrite the path components and continue processing.
269
270             This function gets the next path component being resolved
271             as a string. _cp_dispatch will get an array of strings
272             representing any subsequent path components as well."""
273
274             try:
275                 cherrypy.request.params['machine_id'] = int(name)
276                 return self
277             except ValueError:
278                 return None
279
280         @cherrypy.expose
281         @cherrypy.tools.mako(filename="/info.mako")
282         def info(self, machine_id):
283             """Handler for info on a single VM."""
284             machine = validation.Validate(cherrypy.request.login,
285                                           cherrypy.request.state,
286                                           machine_id=machine_id).machine
287             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
288             return d
289         index = info
290
291         @cherrypy.expose
292         @cherrypy.tools.mako(filename="/info.mako")
293         @cherrypy.tools.require_POST()
294         def modify(self, machine_id, **fields):
295             """Handler for modifying attributes of a machine."""
296             try:
297                 modify_dict = modifyDict(cherrypy.request.login,
298                                          cherrypy.request.state,
299                                          machine_id, fields)
300             except InvalidInput, err:
301                 result = None
302                 machine = validation.Validate(cherrypy.request.login,
303                                               cherrypy.request.state,
304                                               machine_id=machine_id).machine
305             else:
306                 machine = modify_dict['machine']
307                 result = 'Success!'
308                 err = None
309             info_dict = infoDict(cherrypy.request.login,
310                                  cherrypy.request.state, machine)
311             info_dict['err'] = err
312             if err:
313                 for field, value in fields.items():
314                     setattr(info_dict['defaults'], field, value)
315             info_dict['result'] = result
316             return info_dict
317
318         @cherrypy.expose
319         @cherrypy.tools.mako(filename="/vnc.mako")
320         def vnc(self, machine_id):
321             """VNC applet page.
322
323             Note that due to same-domain restrictions, the applet connects to
324             the webserver, which needs to forward those requests to the xen
325             server.  The Xen server runs another proxy that (1) authenticates
326             and (2) finds the correct port for the VM.
327
328             You might want iptables like:
329
330             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
331             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
332             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
333             --dport 10003 -j SNAT --to-source 18.187.7.142
334             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
335             --dport 10003 -j ACCEPT
336
337             Remember to enable iptables!
338             echo 1 > /proc/sys/net/ipv4/ip_forward
339             """
340             machine = validation.Validate(cherrypy.request.login,
341                                           cherrypy.request.state,
342                                           machine_id=machine_id).machine
343             token = controls.vnctoken(machine)
344             host = controls.listHost(machine)
345             if host:
346                 port = 10003 + [h.hostname for h in config.hosts].index(host)
347             else:
348                 port = 5900 # dummy
349
350             status = controls.statusInfo(machine)
351             has_vnc = hasVnc(status)
352
353             d = dict(on=status,
354                      has_vnc=has_vnc,
355                      machine=machine,
356                      hostname=cherrypy.request.local.name,
357                      port=port,
358                      authtoken=token)
359             return d
360
361         @cherrypy.expose
362         @cherrypy.tools.mako(filename="/command.mako")
363         @cherrypy.tools.require_POST()
364         def command(self, command_name, machine_id, **kwargs):
365             """Handler for running commands like boot and delete on a VM."""
366             back = kwargs.get('back')
367             if command_name == 'delete':
368                 back = 'list'
369             try:
370                 d = controls.commandResult(cherrypy.request.login,
371                                            cherrypy.request.state,
372                                            command_name, machine_id, kwargs)
373             except InvalidInput, err:
374                 if not back:
375                     raise
376                 print >> sys.stderr, err
377                 result = str(err)
378             else:
379                 result = 'Success!'
380                 if not back:
381                     return d
382             if back == 'list':
383                 cherrypy.request.state.clear() #Changed global state
384                 raise cherrypy.InternalRedirect('/list?result=%s'
385                                                 % urllib.quote(result))
386             elif back == 'info':
387                 raise cherrypy.HTTPRedirect(cherrypy.request.base
388                                             + '/machine/%d/' % machine_id,
389                                             status=303)
390             else:
391                 raise InvalidInput('back', back, 'Not a known back page.')
392
393     machine = MachineView()
394
395
396 class Defaults:
397     """Class to store default values for fields."""
398     memory = 256
399     disk = 4.0
400     cdrom = ''
401     autoinstall = ''
402     name = ''
403     description = ''
404     administrator = ''
405     type = 'linux-hvm'
406
407     def __init__(self, max_memory=None, max_disk=None, **kws):
408         if max_memory is not None:
409             self.memory = min(self.memory, max_memory)
410         if max_disk is not None:
411             self.disk = min(self.disk, max_disk)
412         for key in kws:
413             setattr(self, key, kws[key])
414
415 def hasVnc(status):
416     """Does the machine with a given status list support VNC?"""
417     if status is None:
418         return False
419     for l in status:
420         if l[0] == 'device' and l[1][0] == 'vfb':
421             d = dict(l[1][1:])
422             return 'location' in d
423     return False
424
425
426 def getListDict(username, state):
427     """Gets the list of local variables used by list.tmpl."""
428     machines = state.machines
429     on = {}
430     has_vnc = {}
431     installing = {}
432     xmlist = state.xmlist
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             installing[m] = bool(xmlist[m].get('autoinstall'))
440             if xmlist[m]['console']:
441                 has_vnc[m] = True
442             elif m.type.hvm:
443                 has_vnc[m] = "WTF?"
444             else:
445                 has_vnc[m] = "ParaVM"
446     max_memory = validation.maxMemory(username, state)
447     max_disk = validation.maxDisk(username)
448     defaults = Defaults(max_memory=max_memory,
449                         max_disk=max_disk,
450                         owner=username)
451     def sortkey(machine):
452         return (machine.owner != username, machine.owner, machine.name)
453     machines = sorted(machines, key=sortkey)
454     d = dict(user=username,
455              cant_add_vm=validation.cantAddVm(username, state),
456              max_memory=max_memory,
457              max_disk=max_disk,
458              defaults=defaults,
459              machines=machines,
460              has_vnc=has_vnc,
461              installing=installing,
462              disable_creation=False)
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 = main_status.get('start_time')
608         if start_time is None:
609             uptime = "Still booting?"
610         else:
611             start_time = float(start_time)
612             uptime = datetime.timedelta(seconds=int(time.time()-start_time))
613         cpu_time_float = float(main_status.get('cpu_time', 0))
614         cputime = datetime.timedelta(seconds=int(cpu_time_float))
615     display_fields = [('name', 'Name'),
616                       ('description', 'Description'),
617                       ('owner', 'Owner'),
618                       ('administrator', 'Administrator'),
619                       ('contact', 'Contact'),
620                       ('type', 'Type'),
621                       'NIC_INFO',
622                       ('uptime', 'uptime'),
623                       ('cputime', 'CPU usage'),
624                       ('host', 'Hosted on'),
625                       ('memory', 'RAM'),
626                       'DISK_INFO',
627                       ('state', 'state (xen format)'),
628                       ]
629     fields = []
630     machine_info = {}
631     machine_info['name'] = machine.name
632     machine_info['description'] = machine.description
633     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
634     machine_info['owner'] = machine.owner
635     machine_info['administrator'] = machine.administrator
636     machine_info['contact'] = machine.contact
637
638     nic_fields = getNicInfo(machine_info, machine)
639     nic_point = display_fields.index('NIC_INFO')
640     display_fields = (display_fields[:nic_point] + nic_fields +
641                       display_fields[nic_point+1:])
642
643     disk_fields = getDiskInfo(machine_info, machine)
644     disk_point = display_fields.index('DISK_INFO')
645     display_fields = (display_fields[:disk_point] + disk_fields +
646                       display_fields[disk_point+1:])
647
648     main_status['memory'] += ' MiB'
649     for field, disp in display_fields:
650         if field in ('uptime', 'cputime') and locals()[field] is not None:
651             fields.append((disp, locals()[field]))
652         elif field in machine_info:
653             fields.append((disp, machine_info[field]))
654         elif field in main_status:
655             fields.append((disp, main_status[field]))
656         else:
657             pass
658             #fields.append((disp, None))
659
660     max_mem = validation.maxMemory(machine.owner, state, machine, False)
661     max_disk = validation.maxDisk(machine.owner, machine)
662     defaults = Defaults()
663     for name in 'machine_id name description administrator owner memory contact'.split():
664         if getattr(machine, name):
665             setattr(defaults, name, getattr(machine, name))
666     defaults.type = machine.type.type_id
667     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
668     d = dict(user=username,
669              on=status is not None,
670              machine=machine,
671              defaults=defaults,
672              has_vnc=has_vnc,
673              uptime=str(uptime),
674              ram=machine.memory,
675              max_mem=max_mem,
676              max_disk=max_disk,
677              fields = fields)
678     return d
679
680 def send_error_mail(subject, body):
681     import subprocess
682
683     to = config.web.errormail
684     mail = """To: %s
685 From: root@%s
686 Subject: %s
687
688 %s
689 """ % (to, config.web.hostname, subject, body)
690     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
691                          stdin=subprocess.PIPE)
692     p.stdin.write(mail)
693     p.stdin.close()
694     p.wait()
695
696 random.seed() #sigh