other_action message fixups
[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                            ('nic%s_netmask', 'NIC %s Netmask'),
492                            ('nic%s_gateway', 'NIC %s Gateway'),
493                            ]
494     nic_fields = []
495     for i in range(len(machine.nics)):
496         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
497         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
498         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
499         data_dict['nic%s_ip' % i] = machine.nics[i].ip
500         data_dict['nic%s_netmask' % i] = machine.nics[i].netmask
501         data_dict['nic%s_gateway' % i] = machine.nics[i].gateway
502         if machine.nics[i].other_ip:
503             nic_fields.append(('nic%s_other' % i, 'NIC %s Other Address' % i))
504             other = '%s/%s via %s' % (machine.nics[i].other_ip, machine.nics[i].other_netmask, machine.nics[i].other_gateway)
505             other_action = machine.nics[i].other_action
506             if other_action == 'dnat':
507                 other += " (NAT to primary IP)"
508             elif other_action == 'renumber':
509                 other += " (cold boot or renew DHCP lease to swap)"
510             elif other_action == 'renumber_dhcp':
511                 other += " (renew DHCP lease to swap)"
512             elif other_action == 'remove':
513                 other += " (will be removed at next cold boot or DHCP lease renewal)"
514             else:
515                 other += " (pending assignment)"
516             data_dict['nic%s_other' % i] = other
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.get(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         update_acl = False
560         if hasattr(validate, 'owner') and validate.owner != machine.owner:
561             machine.owner = validate.owner
562             update_acl = True
563         if hasattr(validate, 'description'):
564             machine.description = validate.description
565         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
566             machine.administrator = validate.admin
567             update_acl = True
568         if hasattr(validate, 'contact'):
569             machine.contact = validate.contact
570
571         session.add(machine)
572         session.commit()
573     except:
574         session.rollback()
575         raise
576
577     session.begin()
578     try:
579         if hasattr(validate, 'disksize'):
580             disksize = validate.disksize
581             disk = machine.disks[0]
582             if disk.size != disksize:
583                 olddisk[disk.guest_device_name] = disksize
584                 disk.size = disksize
585                 session.add(disk)
586         for diskname in olddisk:
587             controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
588         session.add(machine)
589         session.commit()
590     except:
591         session.rollback()
592         raise
593
594     session.begin()
595     try:
596         if hasattr(validate, 'name'):
597             machine.name = validate.name
598             for n in machine.nics:
599                 if n.hostname == oldname:
600                     n.hostname = validate.name
601         if hasattr(validate, 'name'):
602             controls.renameMachine(machine, oldname, validate.name)
603         session.add(machine)
604         session.commit()
605     except:
606         session.rollback()
607         raise
608
609     if update_acl:
610         cache_acls.refreshMachine(machine)
611
612     return dict(machine=machine)
613
614 def infoDict(username, state, machine):
615     """Get the variables used by info.tmpl."""
616     try:
617         status = controls.statusInfo(machine)
618     except CodeError, e:
619         # machine was shut down in between the call to listInfoDict and this
620         status = None
621     has_vnc = hasVnc(status)
622     if status is None:
623         main_status = dict(name=machine.name,
624                            memory=str(machine.memory))
625         uptime = None
626         cputime = None
627     else:
628         main_status = dict(status[1:])
629         main_status['host'] = controls.listHost(machine)
630         start_time = main_status.get('start_time')
631         if start_time is None:
632             uptime = "Still booting?"
633         else:
634             start_time = float(start_time)
635             uptime = datetime.timedelta(seconds=int(time.time()-start_time))
636         cpu_time_float = float(main_status.get('cpu_time', 0))
637         cputime = datetime.timedelta(seconds=int(cpu_time_float))
638     display_fields = [('name', 'Name'),
639                       ('description', 'Description'),
640                       ('owner', 'Owner'),
641                       ('administrator', 'Administrator'),
642                       ('contact', 'Contact'),
643                       ('type', 'Type'),
644                       'NIC_INFO',
645                       ('uptime', 'uptime'),
646                       ('cputime', 'CPU usage'),
647                       ('host', 'Hosted on'),
648                       ('memory', 'RAM'),
649                       'DISK_INFO',
650                       ('state', 'state (xen format)'),
651                       ]
652     fields = []
653     machine_info = {}
654     machine_info['name'] = machine.name
655     machine_info['description'] = machine.description
656     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
657     machine_info['owner'] = machine.owner
658     machine_info['administrator'] = machine.administrator
659     machine_info['contact'] = machine.contact
660
661     nic_fields = getNicInfo(machine_info, machine)
662     nic_point = display_fields.index('NIC_INFO')
663     display_fields = (display_fields[:nic_point] + nic_fields +
664                       display_fields[nic_point+1:])
665
666     disk_fields = getDiskInfo(machine_info, machine)
667     disk_point = display_fields.index('DISK_INFO')
668     display_fields = (display_fields[:disk_point] + disk_fields +
669                       display_fields[disk_point+1:])
670
671     main_status['memory'] += ' MiB'
672     for field, disp in display_fields:
673         if field in ('uptime', 'cputime') and locals()[field] is not None:
674             fields.append((disp, locals()[field]))
675         elif field in machine_info:
676             fields.append((disp, machine_info[field]))
677         elif field in main_status:
678             fields.append((disp, main_status[field]))
679         else:
680             pass
681             #fields.append((disp, None))
682
683     max_mem = validation.maxMemory(machine.owner, state, machine, False)
684     max_disk = validation.maxDisk(machine.owner, machine)
685     defaults = Defaults()
686     for name in 'machine_id name description administrator owner memory contact'.split():
687         if getattr(machine, name):
688             setattr(defaults, name, getattr(machine, name))
689     defaults.type = machine.type.type_id
690     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
691     d = dict(user=username,
692              on=status is not None,
693              machine=machine,
694              defaults=defaults,
695              has_vnc=has_vnc,
696              uptime=str(uptime),
697              ram=machine.memory,
698              max_mem=max_mem,
699              max_disk=max_disk,
700              fields = fields)
701     return d
702
703 def send_error_mail(subject, body):
704     import subprocess
705
706     to = config.web.errormail
707     mail = """To: %s
708 From: root@%s
709 Subject: %s
710
711 %s
712 """ % (to, config.web.hostname, subject, body)
713     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
714                          stdin=subprocess.PIPE)
715     p.stdin.write(mail)
716     p.stdin.close()
717     p.wait()
718
719 random.seed() #sigh