Setup hosting for static resources in the InvirtWeb and
[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 {'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         if name in ("admin", "overlord"):
101             if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
102                 raise InvalidInput('username', cherrypy.request.login,
103                                    'Not in admin group %s.' % config.adminacl)
104             cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
105             return self
106         else:
107             return super(InvirtWeb, self).__getattr__(name)
108
109     def handle_error(self):
110         err = sys.exc_info()[1]
111         if isinstance(err, InvalidInput):
112             cherrypy.request.params['err'] = err
113             cherrypy.request.params['emsg'] = revertStandardError()
114             raise cherrypy.InternalRedirect('/invalidInput')
115         if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
116             cherrypy.request.params['err'] = err
117             cherrypy.request.params['emsg'] = revertStandardError()
118             cherrypy.request.params['traceback'] = _cperror.format_exc()
119             raise cherrypy.InternalRedirect('/error')
120         # fall back to cherrypy default error page
121         cherrypy.HTTPError(500).set_response()
122
123     @cherrypy.expose
124     @cherrypy.tools.mako(filename="/list.mako")
125     def list(self, result=None):
126         """Handler for list requests."""
127         checkpoint.checkpoint('Getting list dict')
128         d = getListDict(cherrypy.request.login, cherrypy.request.state)
129         if result is not None:
130             d['result'] = result
131         checkpoint.checkpoint('Got list dict')
132         return d
133     index=list
134
135     @cherrypy.expose
136     @cherrypy.tools.mako(filename="/help.mako")
137     def help(self, subject=None, simple=False):
138         """Handler for help messages."""
139
140         help_mapping = {
141             'Autoinstalls': """
142 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
143 ParaVM.  You can access the resulting system by logging into the <a
144 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
145 with your Kerberos tickets; there is no root password so sshd will
146 refuse login.</p>
147
148 <p>Under the covers, the autoinstaller uses our own patched version of
149 xen-create-image, which is a tool based on debootstrap.  If you log
150 into the serial console while the install is running, you can watch
151 it.
152 """,
153             'ParaVM Console': """
154 ParaVM machines do not support local console access over VNC.  To
155 access the serial console of these machines, you can SSH with Kerberos
156 to %s, using the name of the machine as your
157 username.""" % config.console.hostname,
158             'HVM/ParaVM': """
159 HVM machines use the virtualization features of the processor, while
160 ParaVM machines rely on a modified kernel to communicate directly with
161 the hypervisor.  HVMs support boot CDs of any operating system, and
162 the VNC console applet.  The three-minute autoinstaller produces
163 ParaVMs.  ParaVMs typically are more efficient, and always support the
164 <a href="help?subject=ParaVM+Console">console server</a>.</p>
165
166 <p>More details are <a
167 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
168 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
169 (which you can skip by using the autoinstaller to begin with.)</p>
170
171 <p>We recommend using a ParaVM when possible and an HVM when necessary.
172 """,
173             'CPU Weight': """
174 Don't ask us!  We're as mystified as you are.""",
175             'Owner': """
176 The owner field is used to determine <a
177 href="help?subject=Quotas">quotas</a>.  It must be the name of a
178 locker that you are an AFS administrator of.  In particular, you or an
179 AFS group you are a member of must have AFS rlidwka bits on the
180 locker.  You can check who administers the LOCKER locker using the
181 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
182 href="help?subject=Administrator">administrator</a>.""",
183             'Administrator': """
184 The administrator field determines who can access the console and
185 power on and off the machine.  This can be either a user or a moira
186 group.""",
187             'Quotas': """
188 Quotas are determined on a per-locker basis.  Each locker may have a
189 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
190 active machines.""",
191             'Console': """
192 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
193 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
194 your machine will run just fine, but the applet's display of the
195 console will suffer artifacts.
196 """,
197             'Windows': """
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             checkpoint.checkpoint('Got infodict')
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 class Checkpoint:
397     def __init__(self):
398         self.start_time = time.time()
399         self.checkpoints = []
400
401     def checkpoint(self, s):
402         self.checkpoints.append((s, time.time()))
403
404     def __str__(self):
405         return ('Timing info:\n%s\n' %
406                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
407                            (d, t) in self.checkpoints]))
408
409 checkpoint = Checkpoint()
410
411 class Defaults:
412     """Class to store default values for fields."""
413     memory = 256
414     disk = 4.0
415     cdrom = ''
416     autoinstall = ''
417     name = ''
418     description = ''
419     administrator = ''
420     type = 'linux-hvm'
421
422     def __init__(self, max_memory=None, max_disk=None, **kws):
423         if max_memory is not None:
424             self.memory = min(self.memory, max_memory)
425         if max_disk is not None:
426             self.disk = min(self.disk, max_disk)
427         for key in kws:
428             setattr(self, key, kws[key])
429
430 def hasVnc(status):
431     """Does the machine with a given status list support VNC?"""
432     if status is None:
433         return False
434     for l in status:
435         if l[0] == 'device' and l[1][0] == 'vfb':
436             d = dict(l[1][1:])
437             return 'location' in d
438     return False
439
440
441 def getListDict(username, state):
442     """Gets the list of local variables used by list.tmpl."""
443     checkpoint.checkpoint('Starting')
444     machines = state.machines
445     checkpoint.checkpoint('Got my machines')
446     on = {}
447     has_vnc = {}
448     installing = {}
449     xmlist = state.xmlist
450     checkpoint.checkpoint('Got uptimes')
451     for m in machines:
452         if m not in xmlist:
453             has_vnc[m] = 'Off'
454             m.uptime = None
455         else:
456             m.uptime = xmlist[m]['uptime']
457             installing[m] = bool(xmlist[m].get('autoinstall'))
458             if xmlist[m]['console']:
459                 has_vnc[m] = True
460             elif m.type.hvm:
461                 has_vnc[m] = "WTF?"
462             else:
463                 has_vnc[m] = "ParaVM"
464     max_memory = validation.maxMemory(username, state)
465     max_disk = validation.maxDisk(username)
466     checkpoint.checkpoint('Got max mem/disk')
467     defaults = Defaults(max_memory=max_memory,
468                         max_disk=max_disk,
469                         owner=username)
470     checkpoint.checkpoint('Got defaults')
471     def sortkey(machine):
472         return (machine.owner != username, machine.owner, machine.name)
473     machines = sorted(machines, key=sortkey)
474     d = dict(user=username,
475              cant_add_vm=validation.cantAddVm(username, state),
476              max_memory=max_memory,
477              max_disk=max_disk,
478              defaults=defaults,
479              machines=machines,
480              has_vnc=has_vnc,
481              installing=installing)
482     return d
483
484 def getHostname(nic):
485     """Find the hostname associated with a NIC.
486
487     XXX this should be merged with the similar logic in DNS and DHCP.
488     """
489     if nic.hostname:
490         hostname = nic.hostname
491     elif nic.machine:
492         hostname = nic.machine.name
493     else:
494         return None
495     if '.' in hostname:
496         return hostname
497     else:
498         return hostname + '.' + config.dns.domains[0]
499
500 def getNicInfo(data_dict, machine):
501     """Helper function for info, get data on nics for a machine.
502
503     Modifies data_dict to include the relevant data, and returns a list
504     of (key, name) pairs to display "name: data_dict[key]" to the user.
505     """
506     data_dict['num_nics'] = len(machine.nics)
507     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
508                            ('nic%s_mac', 'NIC %s MAC Addr'),
509                            ('nic%s_ip', 'NIC %s IP'),
510                            ]
511     nic_fields = []
512     for i in range(len(machine.nics)):
513         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
514         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
515         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
516         data_dict['nic%s_ip' % i] = machine.nics[i].ip
517     if len(machine.nics) == 1:
518         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
519     return nic_fields
520
521 def getDiskInfo(data_dict, machine):
522     """Helper function for info, get data on disks for a machine.
523
524     Modifies data_dict to include the relevant data, and returns a list
525     of (key, name) pairs to display "name: data_dict[key]" to the user.
526     """
527     data_dict['num_disks'] = len(machine.disks)
528     disk_fields_template = [('%s_size', '%s size')]
529     disk_fields = []
530     for disk in machine.disks:
531         name = disk.guest_device_name
532         disk_fields.extend([(x % name, y % name) for x, y in
533                             disk_fields_template])
534         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
535     return disk_fields
536
537 def modifyDict(username, state, machine_id, fields):
538     """Modify a machine as specified by CGI arguments.
539
540     Return a dict containing the machine that was modified.
541     """
542     olddisk = {}
543     session.begin()
544     try:
545         kws = dict([(kw, fields[kw]) for kw in
546          'owner admin contact name description memory vmtype disksize'.split()
547                     if fields[kw]])
548         kws['machine_id'] = machine_id
549         validate = validation.Validate(username, state, **kws)
550         machine = validate.machine
551         oldname = machine.name
552
553         if hasattr(validate, 'memory'):
554             machine.memory = validate.memory
555
556         if hasattr(validate, 'vmtype'):
557             machine.type = validate.vmtype
558
559         if hasattr(validate, 'disksize'):
560             disksize = validate.disksize
561             disk = machine.disks[0]
562             if disk.size != disksize:
563                 olddisk[disk.guest_device_name] = disksize
564                 disk.size = disksize
565                 session.save_or_update(disk)
566
567         update_acl = False
568         if hasattr(validate, 'owner') and validate.owner != machine.owner:
569             machine.owner = validate.owner
570             update_acl = True
571         if hasattr(validate, 'name'):
572             machine.name = validate.name
573             for n in machine.nics:
574                 if n.hostname == oldname:
575                     n.hostname = validate.name
576         if hasattr(validate, 'description'):
577             machine.description = validate.description
578         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
579             machine.administrator = validate.admin
580             update_acl = True
581         if hasattr(validate, 'contact'):
582             machine.contact = validate.contact
583
584         session.save_or_update(machine)
585         if update_acl:
586             cache_acls.refreshMachine(machine)
587         session.commit()
588     except:
589         session.rollback()
590         raise
591     for diskname in olddisk:
592         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
593     if hasattr(validate, 'name'):
594         controls.renameMachine(machine, oldname, validate.name)
595     return dict(machine=machine)
596
597 def infoDict(username, state, machine):
598     """Get the variables used by info.tmpl."""
599     status = controls.statusInfo(machine)
600     checkpoint.checkpoint('Getting status info')
601     has_vnc = hasVnc(status)
602     if status is None:
603         main_status = dict(name=machine.name,
604                            memory=str(machine.memory))
605         uptime = None
606         cputime = None
607     else:
608         main_status = dict(status[1:])
609         main_status['host'] = controls.listHost(machine)
610         start_time = float(main_status.get('start_time', 0))
611         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
612         cpu_time_float = float(main_status.get('cpu_time', 0))
613         cputime = datetime.timedelta(seconds=int(cpu_time_float))
614     checkpoint.checkpoint('Status')
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     checkpoint.checkpoint('Got fields')
661
662
663     max_mem = validation.maxMemory(machine.owner, state, machine, False)
664     checkpoint.checkpoint('Got mem')
665     max_disk = validation.maxDisk(machine.owner, machine)
666     defaults = Defaults()
667     for name in 'machine_id name description administrator owner memory contact'.split():
668         if getattr(machine, name):
669             setattr(defaults, name, getattr(machine, name))
670     defaults.type = machine.type.type_id
671     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
672     checkpoint.checkpoint('Got defaults')
673     d = dict(user=username,
674              on=status is not None,
675              machine=machine,
676              defaults=defaults,
677              has_vnc=has_vnc,
678              uptime=str(uptime),
679              ram=machine.memory,
680              max_mem=max_mem,
681              max_disk=max_disk,
682              fields = fields)
683     return d
684
685 def send_error_mail(subject, body):
686     import subprocess
687
688     to = config.web.errormail
689     mail = """To: %s
690 From: root@%s
691 Subject: %s
692
693 %s
694 """ % (to, config.web.hostname, subject, body)
695     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
696                          stdin=subprocess.PIPE)
697     p.stdin.write(mail)
698     p.stdin.close()
699     p.wait()
700
701 random.seed() #sigh