Change VM creation auth failure message
[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, result=None):
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             if result:
289                 d['result'] = result
290             return d
291         index = info
292
293         @cherrypy.expose
294         @cherrypy.tools.mako(filename="/info.mako")
295         @cherrypy.tools.require_POST()
296         def modify(self, machine_id, **fields):
297             """Handler for modifying attributes of a machine."""
298             try:
299                 modify_dict = modifyDict(cherrypy.request.login,
300                                          cherrypy.request.state,
301                                          machine_id, fields)
302             except InvalidInput, err:
303                 result = None
304                 machine = validation.Validate(cherrypy.request.login,
305                                               cherrypy.request.state,
306                                               machine_id=machine_id).machine
307             else:
308                 machine = modify_dict['machine']
309                 result = 'Success!'
310                 err = None
311             info_dict = infoDict(cherrypy.request.login,
312                                  cherrypy.request.state, machine)
313             info_dict['err'] = err
314             if err:
315                 for field, value in fields.items():
316                     setattr(info_dict['defaults'], field, value)
317             info_dict['result'] = result
318             return info_dict
319
320         @cherrypy.expose
321         @cherrypy.tools.mako(filename="/vnc.mako")
322         def vnc(self, machine_id):
323             """VNC applet page"""
324             return self._vnc(machine_id)
325
326         @cherrypy.expose
327         @cherrypy.tools.response_headers(headers=[('Content-Disposition', 'attachment; filename=vnc.jnlp')])
328         @cherrypy.tools.mako(filename="/vnc_jnlp.mako", content_type="application/x-java-jnlp-file")
329         def vnc_jnlp(self, machine_id):
330             """VNC applet exposed as a Java Web Start app (JNLP file)"""
331             return self._vnc(machine_id)
332
333         def _vnc(self, machine_id):
334             """VNC applet page functionality.
335
336             Note that due to same-domain restrictions, the applet connects to
337             the webserver, which needs to forward those requests to the xen
338             server.  The Xen server runs another proxy that (1) authenticates
339             and (2) finds the correct port for the VM.
340
341             You might want iptables like:
342
343             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
344             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
345             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
346             --dport 10003 -j SNAT --to-source 18.187.7.142
347             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
348             --dport 10003 -j ACCEPT
349
350             Remember to enable iptables!
351             echo 1 > /proc/sys/net/ipv4/ip_forward
352             """
353             machine = validation.Validate(cherrypy.request.login,
354                                           cherrypy.request.state,
355                                           machine_id=machine_id).machine
356             token = controls.vnctoken(machine)
357             host = controls.listHost(machine)
358             if host:
359                 port = 10003 + [h.hostname for h in config.hosts].index(host)
360             else:
361                 port = 5900 # dummy
362
363             status = controls.statusInfo(machine)
364             has_vnc = hasVnc(status)
365
366             d = dict(on=status,
367                      has_vnc=has_vnc,
368                      machine=machine,
369                      hostname=cherrypy.request.local.name,
370                      port=port,
371                      authtoken=token)
372             return d
373
374         @cherrypy.expose
375         @cherrypy.tools.mako(filename="/command.mako")
376         @cherrypy.tools.require_POST()
377         def command(self, command_name, machine_id, **kwargs):
378             """Handler for running commands like boot and delete on a VM."""
379             back = kwargs.get('back')
380             if command_name == 'delete':
381                 back = 'list'
382             try:
383                 d = controls.commandResult(cherrypy.request.login,
384                                            cherrypy.request.state,
385                                            command_name, machine_id, kwargs)
386             except InvalidInput, err:
387                 if not back:
388                     raise
389                 print >> sys.stderr, err
390                 result = str(err)
391             else:
392                 result = 'Success!'
393                 if 'result' in d:
394                     result = d['result']
395                 if not back:
396                     return d
397             if back == 'list':
398                 cherrypy.request.state.clear() #Changed global state
399                 raise cherrypy.InternalRedirect('/list?result=%s'
400                                                 % urllib.quote(result))
401             elif back == 'info':
402                 url = cherrypy.request.base + '/machine/%d/' % machine_id
403                 if result:
404                     url += '?result='+urllib.quote(result)
405                 raise cherrypy.HTTPRedirect(url,
406                                             status=303)
407             else:
408                 raise InvalidInput('back', back, 'Not a known back page.')
409
410     machine = MachineView()
411
412
413 class Defaults:
414     """Class to store default values for fields."""
415     memory = 256
416     disk = 4.0
417     cdrom = ''
418     autoinstall = ''
419     name = ''
420     description = ''
421     administrator = ''
422     type = 'linux-hvm'
423
424     def __init__(self, max_memory=None, max_disk=None, **kws):
425         if max_memory is not None:
426             self.memory = min(self.memory, max_memory)
427         if max_disk is not None:
428             self.disk = min(self.disk, max_disk)
429         for key in kws:
430             setattr(self, key, kws[key])
431
432 def hasVnc(status):
433     """Does the machine with a given status list support VNC?"""
434     if status is None:
435         return False
436     for l in status:
437         if l[0] == 'device' and l[1][0] == 'vfb':
438             d = dict(l[1][1:])
439             return 'location' in d
440     return False
441
442
443 def getListDict(username, state):
444     """Gets the list of local variables used by list.tmpl."""
445     machines = state.machines
446     on = {}
447     has_vnc = {}
448     installing = {}
449     xmlist = state.xmlist
450     for m in machines:
451         if m not in xmlist:
452             has_vnc[m] = 'Off'
453             m.uptime = None
454         else:
455             m.uptime = xmlist[m]['uptime']
456             installing[m] = bool(xmlist[m].get('autoinstall'))
457             if xmlist[m]['console']:
458                 has_vnc[m] = True
459             elif m.type.hvm:
460                 has_vnc[m] = "WTF?"
461             else:
462                 has_vnc[m] = "ParaVM"
463     max_memory = validation.maxMemory(username, state)
464     max_disk = validation.maxDisk(username)
465     defaults = Defaults(max_memory=max_memory,
466                         max_disk=max_disk,
467                         owner=username)
468     def sortkey(machine):
469         return (machine.owner != username, machine.owner, machine.name)
470     machines = sorted(machines, key=sortkey)
471     d = dict(user=username,
472              cant_add_vm=validation.cantAddVm(username, state),
473              max_memory=max_memory,
474              max_disk=max_disk,
475              defaults=defaults,
476              machines=machines,
477              has_vnc=has_vnc,
478              installing=installing,
479              disable_creation=False)
480     return d
481
482 def getHostname(nic):
483     """Find the hostname associated with a NIC.
484
485     XXX this should be merged with the similar logic in DNS and DHCP.
486     """
487     if nic.hostname:
488         hostname = nic.hostname
489     elif nic.machine:
490         hostname = nic.machine.name
491     else:
492         return None
493     if '.' in hostname:
494         return hostname
495     else:
496         return hostname + '.' + config.dns.domains[0]
497
498 def getNicInfo(data_dict, machine):
499     """Helper function for info, get data on nics for a machine.
500
501     Modifies data_dict to include the relevant data, and returns a list
502     of (key, name) pairs to display "name: data_dict[key]" to the user.
503     """
504     data_dict['num_nics'] = len(machine.nics)
505     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
506                            ('nic%s_mac', 'NIC %s MAC Addr'),
507                            ('nic%s_ip', 'NIC %s IP'),
508                            ('nic%s_netmask', 'NIC %s Netmask'),
509                            ('nic%s_gateway', 'NIC %s Gateway'),
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         data_dict['nic%s_netmask' % i] = machine.nics[i].netmask
518         data_dict['nic%s_gateway' % i] = machine.nics[i].gateway
519         if machine.nics[i].other_ip:
520             nic_fields.append(('nic%s_other' % i, 'NIC %s Other Address' % i))
521             other = '%s/%s via %s' % (machine.nics[i].other_ip, machine.nics[i].other_netmask, machine.nics[i].other_gateway)
522             other_action = machine.nics[i].other_action
523             if other_action == 'dnat':
524                 other += " (NAT to primary IP)"
525             elif other_action == 'renumber':
526                 other += " (cold boot or renew DHCP lease to swap)"
527             elif other_action == 'renumber_dhcp':
528                 other += " (renew DHCP lease to swap)"
529             elif other_action == 'remove':
530                 other += " (will be removed at next cold boot or DHCP lease renewal)"
531             else:
532                 other += " (pending assignment)"
533             data_dict['nic%s_other' % i] = other
534     if len(machine.nics) == 1:
535         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
536     return nic_fields
537
538 def getDiskInfo(data_dict, machine):
539     """Helper function for info, get data on disks for a machine.
540
541     Modifies data_dict to include the relevant data, and returns a list
542     of (key, name) pairs to display "name: data_dict[key]" to the user.
543     """
544     data_dict['num_disks'] = len(machine.disks)
545     disk_fields_template = [('%s_size', '%s size')]
546     disk_fields = []
547     for disk in machine.disks:
548         name = disk.guest_device_name
549         disk_fields.extend([(x % name, y % name) for x, y in
550                             disk_fields_template])
551         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
552     return disk_fields
553
554 def modifyDict(username, state, machine_id, fields):
555     """Modify a machine as specified by CGI arguments.
556
557     Return a dict containing the machine that was modified.
558     """
559     olddisk = {}
560     session.begin()
561     try:
562         kws = dict((kw, fields[kw]) for kw in
563          'owner admin contact name description memory vmtype disksize'.split()
564                     if fields.get(kw))
565         kws['machine_id'] = machine_id
566         validate = validation.Validate(username, state, **kws)
567         machine = validate.machine
568         oldname = machine.name
569
570         if hasattr(validate, 'memory'):
571             machine.memory = validate.memory
572
573         if hasattr(validate, 'vmtype'):
574             machine.type = validate.vmtype
575
576         update_acl = False
577         if hasattr(validate, 'owner') and validate.owner != machine.owner:
578             machine.owner = validate.owner
579             update_acl = True
580         if hasattr(validate, 'description'):
581             machine.description = validate.description
582         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
583             machine.administrator = validate.admin
584             update_acl = True
585         if hasattr(validate, 'contact'):
586             machine.contact = validate.contact
587
588         session.add(machine)
589         session.commit()
590     except:
591         session.rollback()
592         raise
593
594     session.begin()
595     try:
596         if hasattr(validate, 'disksize'):
597             disksize = validate.disksize
598             disk = machine.disks[0]
599             if disk.size != disksize:
600                 olddisk[disk.guest_device_name] = disksize
601                 disk.size = disksize
602                 session.add(disk)
603         for diskname in olddisk:
604             controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
605         session.add(machine)
606         session.commit()
607     except:
608         session.rollback()
609         raise
610
611     session.begin()
612     try:
613         if hasattr(validate, 'name'):
614             machine.name = validate.name
615             for n in machine.nics:
616                 if n.hostname == oldname:
617                     n.hostname = validate.name
618         if hasattr(validate, 'name'):
619             controls.renameMachine(machine, oldname, validate.name)
620         session.add(machine)
621         session.commit()
622     except:
623         session.rollback()
624         raise
625
626     if update_acl:
627         cache_acls.refreshMachine(machine)
628
629     return dict(machine=machine)
630
631 def infoDict(username, state, machine):
632     """Get the variables used by info.tmpl."""
633     try:
634         status = controls.statusInfo(machine)
635     except CodeError, e:
636         # machine was shut down in between the call to listInfoDict and this
637         status = None
638     has_vnc = hasVnc(status)
639     if status is None:
640         main_status = dict(name=machine.name,
641                            memory=str(machine.memory))
642         uptime = None
643         cputime = None
644     else:
645         main_status = dict(status[1:])
646         main_status['host'] = controls.listHost(machine)
647         start_time = main_status.get('start_time')
648         if start_time is None:
649             uptime = "Still booting?"
650         else:
651             start_time = float(start_time)
652             uptime = datetime.timedelta(seconds=int(time.time()-start_time))
653         cpu_time_float = float(main_status.get('cpu_time', 0))
654         cputime = datetime.timedelta(seconds=int(cpu_time_float))
655     display_fields = [('name', 'Name'),
656                       ('description', 'Description'),
657                       ('owner', 'Owner'),
658                       ('administrator', 'Administrator'),
659                       ('contact', 'Contact'),
660                       ('type', 'Type'),
661                       'NIC_INFO',
662                       ('uptime', 'uptime'),
663                       ('cputime', 'CPU usage'),
664                       ('host', 'Hosted on'),
665                       ('memory', 'RAM'),
666                       'DISK_INFO',
667                       ('state', 'state (xen format)'),
668                       ]
669     fields = []
670     machine_info = {}
671     machine_info['name'] = machine.name
672     machine_info['description'] = machine.description
673     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
674     machine_info['owner'] = machine.owner
675     machine_info['administrator'] = machine.administrator
676     machine_info['contact'] = machine.contact
677
678     nic_fields = getNicInfo(machine_info, machine)
679     nic_point = display_fields.index('NIC_INFO')
680     display_fields = (display_fields[:nic_point] + nic_fields +
681                       display_fields[nic_point+1:])
682
683     disk_fields = getDiskInfo(machine_info, machine)
684     disk_point = display_fields.index('DISK_INFO')
685     display_fields = (display_fields[:disk_point] + disk_fields +
686                       display_fields[disk_point+1:])
687
688     renumber = False
689     for n in machine.nics:
690         if n.other_action == 'renumber_dhcp':
691             renumber = True
692
693     main_status['memory'] += ' MiB'
694     for field, disp in display_fields:
695         if field in ('uptime', 'cputime') and locals()[field] is not None:
696             fields.append((disp, locals()[field]))
697         elif field in machine_info:
698             fields.append((disp, machine_info[field]))
699         elif field in main_status:
700             fields.append((disp, main_status[field]))
701         else:
702             pass
703             #fields.append((disp, None))
704
705     max_mem = validation.maxMemory(machine.owner, state, machine, False)
706     max_disk = validation.maxDisk(machine.owner, machine)
707     defaults = Defaults()
708     for name in 'machine_id name description administrator owner memory contact'.split():
709         if getattr(machine, name):
710             setattr(defaults, name, getattr(machine, name))
711     defaults.type = machine.type.type_id
712     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
713     d = dict(user=username,
714              on=status is not None,
715              renumber=renumber,
716              machine=machine,
717              defaults=defaults,
718              has_vnc=has_vnc,
719              uptime=str(uptime),
720              ram=machine.memory,
721              max_mem=max_mem,
722              max_disk=max_disk,
723              fields = fields)
724     return d
725
726 def send_error_mail(subject, body):
727     import subprocess
728
729     to = config.web.errormail
730     mail = """To: %s
731 From: root@%s
732 Subject: %s
733
734 %s
735 """ % (to, config.web.hostname, subject, body)
736     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
737                          stdin=subprocess.PIPE)
738     p.stdin.write(mail)
739     p.stdin.close()
740     p.wait()
741
742 random.seed() #sigh