source: package_branches/invirt-web/ajaxterm-rebased/code/main.py @ 2761

Last change on this file since 2761 was 2761, checked in by broder, 15 years ago

Use browser-based dupe suppression, so multiple clients can connect to the same terminal and not miss updates

  • Property svn:executable set to *
File size: 27.9 KB
Line 
1#!/usr/bin/python
2"""Main CGI script for web interface"""
3
4from __future__ import with_statement
5
6import base64
7import cPickle
8import cgi
9import datetime
10import hmac
11import os
12import random
13import sha
14import sys
15import threading
16import time
17import urllib
18import socket
19import cherrypy
20from cherrypy import _cperror
21from StringIO import StringIO
22
23def printError():
24    """Revert stderr to stdout, and print the contents of stderr"""
25    if isinstance(sys.stderr, StringIO):
26        print revertStandardError()
27
28if __name__ == '__main__':
29    import atexit
30    atexit.register(printError)
31
32import validation
33import cache_acls
34from webcommon import State
35import controls
36from getafsgroups import getAfsGroupMembers
37from invirt import database
38from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
39from invirt.config import structs as config
40from invirt.common import InvalidInput, CodeError
41
42from view import View, revertStandardError
43import ajaxterm
44
45
46static_dir = os.path.join(os.path.dirname(__file__), 'static')
47InvirtStatic = cherrypy.tools.staticdir.handler(
48    root=static_dir,
49    dir=static_dir,
50    section='/static')
51
52class InvirtUnauthWeb(View):
53    static = InvirtStatic
54
55    @cherrypy.expose
56    @cherrypy.tools.mako(filename="/unauth.mako")
57    def index(self):
58        return {'simple': True}
59
60class InvirtWeb(View):
61    def __init__(self):
62        super(self.__class__,self).__init__()
63        connect()
64        self._cp_config['tools.require_login.on'] = True
65        self._cp_config['tools.catch_stderr.on'] = True
66        self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
67                                                 'from invirt import database']
68        self._cp_config['request.error_response'] = self.handle_error
69
70    static = InvirtStatic
71
72    @cherrypy.expose
73    @cherrypy.tools.mako(filename="/invalid.mako")
74    def invalidInput(self):
75        """Print an error page when an InvalidInput exception occurs"""
76        err = cherrypy.request.prev.params["err"]
77        emsg = cherrypy.request.prev.params["emsg"]
78        d = dict(err_field=err.err_field,
79                 err_value=str(err.err_value), stderr=emsg,
80                 errorMessage=str(err))
81        return d
82
83    @cherrypy.expose
84    @cherrypy.tools.mako(filename="/error.mako")
85    def error(self):
86        """Print an error page when an exception occurs"""
87        op = cherrypy.request.prev.path_info
88        username = cherrypy.request.login
89        err = cherrypy.request.prev.params["err"]
90        emsg = cherrypy.request.prev.params["emsg"]
91        traceback = cherrypy.request.prev.params["traceback"]
92        d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
93                 errorMessage=str(err), stderr=emsg, traceback=traceback)
94        error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
95        details = error_raw.render(**d)
96        exclude = config.web.errormail_exclude
97        if username not in exclude and '*' not in exclude:
98            send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
99                            details)
100        d['details'] = details
101        return d
102
103    def __getattr__(self, name):
104        if name in ("admin", "overlord"):
105            if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
106                raise InvalidInput('username', cherrypy.request.login,
107                                   'Not in admin group %s.' % config.adminacl)
108            cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
109            return self
110        else:
111            return super(InvirtWeb, self).__getattr__(name)
112
113    def handle_error(self):
114        err = sys.exc_info()[1]
115        if isinstance(err, InvalidInput):
116            cherrypy.request.params['err'] = err
117            cherrypy.request.params['emsg'] = revertStandardError()
118            raise cherrypy.InternalRedirect('/invalidInput')
119        if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
120            cherrypy.request.params['err'] = err
121            cherrypy.request.params['emsg'] = revertStandardError()
122            cherrypy.request.params['traceback'] = _cperror.format_exc()
123            raise cherrypy.InternalRedirect('/error')
124        # fall back to cherrypy default error page
125        cherrypy.HTTPError(500).set_response()
126
127    @cherrypy.expose
128    @cherrypy.tools.mako(filename="/list.mako")
129    def list(self, result=None):
130        """Handler for list requests."""
131        d = getListDict(cherrypy.request.login, cherrypy.request.state)
132        if result is not None:
133            d['result'] = result
134        return d
135    index=list
136
137    @cherrypy.expose
138    @cherrypy.tools.mako(filename="/help.mako")
139    def help(self, subject=None, simple=False):
140        """Handler for help messages."""
141
142        help_mapping = {
143            'Autoinstalls': """
144The autoinstaller builds a minimal Debian or Ubuntu system to run as a
145ParaVM.  You can access the resulting system by logging into the <a
146href="help?simple=true&subject=ParaVM+Console">serial console server</a>
147with your Kerberos tickets; there is no root password so sshd will
148refuse login.</p>
149
150<p>Under the covers, the autoinstaller uses our own patched version of
151xen-create-image, which is a tool based on debootstrap.  If you log
152into the serial console while the install is running, you can watch
153it.
154""",
155            'ParaVM Console': """
156ParaVM machines do not support local console access over VNC.  To
157access the serial console of these machines, you can SSH with Kerberos
158to %s, using the name of the machine as your
159username.""" % config.console.hostname,
160            'HVM/ParaVM': """
161HVM machines use the virtualization features of the processor, while
162ParaVM machines rely on a modified kernel to communicate directly with
163the hypervisor.  HVMs support boot CDs of any operating system, and
164the VNC console applet.  The three-minute autoinstaller produces
165ParaVMs.  ParaVMs typically are more efficient, and always support the
166<a href="help?subject=ParaVM+Console">console server</a>.</p>
167
168<p>More details are <a
169href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
170wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
171(which you can skip by using the autoinstaller to begin with.)</p>
172
173<p>We recommend using a ParaVM when possible and an HVM when necessary.
174""",
175            'CPU Weight': """
176Don't ask us!  We're as mystified as you are.""",
177            'Owner': """
178The owner field is used to determine <a
179href="help?subject=Quotas">quotas</a>.  It must be the name of a
180locker that you are an AFS administrator of.  In particular, you or an
181AFS group you are a member of must have AFS rlidwka bits on the
182locker.  You can check who administers the LOCKER locker using the
183commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
184href="help?subject=Administrator">administrator</a>.""",
185            'Administrator': """
186The administrator field determines who can access the console and
187power on and off the machine.  This can be either a user or a moira
188group.""",
189            'Quotas': """
190Quotas are determined on a per-locker basis.  Each locker may have a
191maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
192active machines.""",
193            'Console': """
194<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
195setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
196your machine will run just fine, but the applet's display of the
197console will suffer artifacts.
198""",
199            'Windows': """
200<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>
201<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.
202"""
203            }
204
205        if not subject:
206            subject = sorted(help_mapping.keys())
207        if not isinstance(subject, list):
208            subject = [subject]
209
210        return dict(simple=simple,
211                    subjects=subject,
212                    mapping=help_mapping)
213    help._cp_config['tools.require_login.on'] = False
214
215    def parseCreate(self, fields):
216        kws = dict([(kw, fields[kw]) for kw in
217         'name description owner memory disksize vmtype cdrom autoinstall'.split()
218                    if fields[kw]])
219        validate = validation.Validate(cherrypy.request.login,
220                                       cherrypy.request.state,
221                                       strict=True, **kws)
222        return dict(contact=cherrypy.request.login, name=validate.name,
223                    description=validate.description, memory=validate.memory,
224                    disksize=validate.disksize, owner=validate.owner,
225                    machine_type=getattr(validate, 'vmtype', Defaults.type),
226                    cdrom=getattr(validate, 'cdrom', None),
227                    autoinstall=getattr(validate, 'autoinstall', None))
228
229    @cherrypy.expose
230    @cherrypy.tools.mako(filename="/list.mako")
231    @cherrypy.tools.require_POST()
232    def create(self, **fields):
233        """Handler for create requests."""
234        try:
235            parsed_fields = self.parseCreate(fields)
236            machine = controls.createVm(cherrypy.request.login,
237                                        cherrypy.request.state, **parsed_fields)
238        except InvalidInput, err:
239            pass
240        else:
241            err = None
242        cherrypy.request.state.clear() #Changed global state
243        d = getListDict(cherrypy.request.login, cherrypy.request.state)
244        d['err'] = err
245        if err:
246            for field, value in fields.items():
247                setattr(d['defaults'], field, value)
248        else:
249            d['new_machine'] = parsed_fields['name']
250        return d
251
252    @cherrypy.expose
253    @cherrypy.tools.mako(filename="/helloworld.mako")
254    def helloworld(self, **kwargs):
255        return {'request': cherrypy.request, 'kwargs': kwargs}
256    helloworld._cp_config['tools.require_login.on'] = False
257
258    @cherrypy.expose
259    def errortest(self):
260        """Throw an error, to test the error-tracing mechanisms."""
261        print >>sys.stderr, "look ma, it's a stderr"
262        raise RuntimeError("test of the emergency broadcast system")
263
264    class MachineView(View):
265        def __getattr__(self, name):
266            """Synthesize attributes to allow RESTful URLs like
267            /machine/13/info. This is hairy. CherryPy 3.2 adds a
268            method called _cp_dispatch that allows you to explicitly
269            handle URLs that can't be mapped, and it allows you to
270            rewrite the path components and continue processing.
271
272            This function gets the next path component being resolved
273            as a string. _cp_dispatch will get an array of strings
274            representing any subsequent path components as well."""
275
276            try:
277                cherrypy.request.params['machine_id'] = int(name)
278                return self
279            except ValueError:
280                return None
281
282        @cherrypy.expose
283        @cherrypy.tools.mako(filename="/info.mako")
284        def info(self, machine_id):
285            """Handler for info on a single VM."""
286            machine = validation.Validate(cherrypy.request.login,
287                                          cherrypy.request.state,
288                                          machine_id=machine_id).machine
289            d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
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
325            Note that due to same-domain restrictions, the applet connects to
326            the webserver, which needs to forward those requests to the xen
327            server.  The Xen server runs another proxy that (1) authenticates
328            and (2) finds the correct port for the VM.
329
330            You might want iptables like:
331
332            -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
333            --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
334            -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
335            --dport 10003 -j SNAT --to-source 18.187.7.142
336            -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
337            --dport 10003 -j ACCEPT
338
339            Remember to enable iptables!
340            echo 1 > /proc/sys/net/ipv4/ip_forward
341            """
342            machine = validation.Validate(cherrypy.request.login,
343                                          cherrypy.request.state,
344                                          machine_id=machine_id).machine
345            token = controls.vnctoken(machine)
346            host = controls.listHost(machine)
347            if host:
348                port = 10003 + [h.hostname for h in config.hosts].index(host)
349            else:
350                port = 5900 # dummy
351
352            status = controls.statusInfo(machine)
353            has_vnc = hasVnc(status)
354
355            d = dict(on=status,
356                     has_vnc=has_vnc,
357                     machine=machine,
358                     hostname=cherrypy.request.local.name,
359                     port=port,
360                     authtoken=token)
361            return d
362
363        @cherrypy.expose
364        @cherrypy.tools.mako(filename="/command.mako")
365        @cherrypy.tools.require_POST()
366        def command(self, command_name, machine_id, **kwargs):
367            """Handler for running commands like boot and delete on a VM."""
368            back = kwargs.get('back')
369            if command_name == 'delete':
370                back = 'list'
371            try:
372                d = controls.commandResult(cherrypy.request.login,
373                                           cherrypy.request.state,
374                                           command_name, machine_id, kwargs)
375            except InvalidInput, err:
376                if not back:
377                    raise
378                print >> sys.stderr, err
379                result = str(err)
380            else:
381                result = 'Success!'
382                if not back:
383                    return d
384            if back == 'list':
385                cherrypy.request.state.clear() #Changed global state
386                raise cherrypy.InternalRedirect('/list?result=%s'
387                                                % urllib.quote(result))
388            elif back == 'info':
389                raise cherrypy.HTTPRedirect(cherrypy.request.base
390                                            + '/machine/%d/' % machine_id,
391                                            status=303)
392            else:
393                raise InvalidInput('back', back, 'Not a known back page.')
394
395        atmulti = ajaxterm.Multiplex()
396        atsessions = {}
397        atsessions_lock = threading.Lock()
398
399        @cherrypy.expose
400        @cherrypy.tools.mako(filename="/terminal.mako")
401        def terminal(self, machine_id):
402            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
403
404            status = controls.statusInfo(machine)
405            has_vnc = hasVnc(status)
406
407            d = dict(on=status,
408                     has_vnc=has_vnc,
409                     machine=machine,
410                     hostname=cherrypy.request.local.name)
411            return d
412
413        @cherrypy.expose
414        @cherrypy.tools.require_POST()
415        @cherrypy.tools.gzip()
416        def at(self, machine_id, k=None, c=0, h=None):
417            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
418            with self.atsessions_lock:
419                if machine_id in self.atsessions:
420                    term = self.atsessions[machine_id]
421                else:
422                    print >>sys.stderr, "spawning new session for terminal to ",machine_id
423                    term = self.atmulti.create(
424                        ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
425                        )
426                    # Clear out old sessions when fd is reused
427                    for key in self.atsessions:
428                        if self.atsessions[key] == term:
429                            del self.atsessions[key]
430                    self.atsessions[machine_id] = term
431                if k:
432                    self.atmulti.proc_write(term,k)
433                time.sleep(0.002)
434                dump=self.atmulti.dump(term,c,h)
435                cherrypy.response.headers['Content-Type']='text/xml'
436                if isinstance(dump,str):
437                    return dump
438                else:
439                    print "Removing session for", machine_id
440                    del self.atsessions[machine_id]
441                    return '<?xml version="1.0"?><idem></idem>'
442
443    machine = MachineView()
444
445
446class Defaults:
447    """Class to store default values for fields."""
448    memory = 256
449    disk = 4.0
450    cdrom = ''
451    autoinstall = ''
452    name = ''
453    description = ''
454    administrator = ''
455    type = 'linux-hvm'
456
457    def __init__(self, max_memory=None, max_disk=None, **kws):
458        if max_memory is not None:
459            self.memory = min(self.memory, max_memory)
460        if max_disk is not None:
461            self.disk = min(self.disk, max_disk)
462        for key in kws:
463            setattr(self, key, kws[key])
464
465def hasVnc(status):
466    """Does the machine with a given status list support VNC?"""
467    if status is None:
468        return False
469    for l in status:
470        if l[0] == 'device' and l[1][0] == 'vfb':
471            d = dict(l[1][1:])
472            return 'location' in d
473    return False
474
475
476def getListDict(username, state):
477    """Gets the list of local variables used by list.tmpl."""
478    machines = state.machines
479    on = {}
480    has_vnc = {}
481    installing = {}
482    xmlist = state.xmlist
483    for m in machines:
484        if m not in xmlist:
485            has_vnc[m] = 'Off'
486            m.uptime = None
487        else:
488            m.uptime = xmlist[m]['uptime']
489            installing[m] = bool(xmlist[m].get('autoinstall'))
490            if xmlist[m]['console']:
491                has_vnc[m] = True
492            elif m.type.hvm:
493                has_vnc[m] = "WTF?"
494            else:
495                has_vnc[m] = "ParaVM"
496    max_memory = validation.maxMemory(username, state)
497    max_disk = validation.maxDisk(username)
498    defaults = Defaults(max_memory=max_memory,
499                        max_disk=max_disk,
500                        owner=username)
501    def sortkey(machine):
502        return (machine.owner != username, machine.owner, machine.name)
503    machines = sorted(machines, key=sortkey)
504    d = dict(user=username,
505             cant_add_vm=validation.cantAddVm(username, state),
506             max_memory=max_memory,
507             max_disk=max_disk,
508             defaults=defaults,
509             machines=machines,
510             has_vnc=has_vnc,
511             installing=installing)
512    return d
513
514def getHostname(nic):
515    """Find the hostname associated with a NIC.
516
517    XXX this should be merged with the similar logic in DNS and DHCP.
518    """
519    if nic.hostname:
520        hostname = nic.hostname
521    elif nic.machine:
522        hostname = nic.machine.name
523    else:
524        return None
525    if '.' in hostname:
526        return hostname
527    else:
528        return hostname + '.' + config.dns.domains[0]
529
530def getNicInfo(data_dict, machine):
531    """Helper function for info, get data on nics for a machine.
532
533    Modifies data_dict to include the relevant data, and returns a list
534    of (key, name) pairs to display "name: data_dict[key]" to the user.
535    """
536    data_dict['num_nics'] = len(machine.nics)
537    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
538                           ('nic%s_mac', 'NIC %s MAC Addr'),
539                           ('nic%s_ip', 'NIC %s IP'),
540                           ]
541    nic_fields = []
542    for i in range(len(machine.nics)):
543        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
544        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
545        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
546        data_dict['nic%s_ip' % i] = machine.nics[i].ip
547    if len(machine.nics) == 1:
548        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
549    return nic_fields
550
551def getDiskInfo(data_dict, machine):
552    """Helper function for info, get data on disks for a machine.
553
554    Modifies data_dict to include the relevant data, and returns a list
555    of (key, name) pairs to display "name: data_dict[key]" to the user.
556    """
557    data_dict['num_disks'] = len(machine.disks)
558    disk_fields_template = [('%s_size', '%s size')]
559    disk_fields = []
560    for disk in machine.disks:
561        name = disk.guest_device_name
562        disk_fields.extend([(x % name, y % name) for x, y in
563                            disk_fields_template])
564        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
565    return disk_fields
566
567def modifyDict(username, state, machine_id, fields):
568    """Modify a machine as specified by CGI arguments.
569
570    Return a dict containing the machine that was modified.
571    """
572    olddisk = {}
573    session.begin()
574    try:
575        kws = dict([(kw, fields[kw]) for kw in
576         'owner admin contact name description memory vmtype disksize'.split()
577                    if fields[kw]])
578        kws['machine_id'] = machine_id
579        validate = validation.Validate(username, state, **kws)
580        machine = validate.machine
581        oldname = machine.name
582
583        if hasattr(validate, 'memory'):
584            machine.memory = validate.memory
585
586        if hasattr(validate, 'vmtype'):
587            machine.type = validate.vmtype
588
589        if hasattr(validate, 'disksize'):
590            disksize = validate.disksize
591            disk = machine.disks[0]
592            if disk.size != disksize:
593                olddisk[disk.guest_device_name] = disksize
594                disk.size = disksize
595                session.save_or_update(disk)
596
597        update_acl = False
598        if hasattr(validate, 'owner') and validate.owner != machine.owner:
599            machine.owner = validate.owner
600            update_acl = True
601        if hasattr(validate, 'name'):
602            machine.name = validate.name
603            for n in machine.nics:
604                if n.hostname == oldname:
605                    n.hostname = validate.name
606        if hasattr(validate, 'description'):
607            machine.description = validate.description
608        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
609            machine.administrator = validate.admin
610            update_acl = True
611        if hasattr(validate, 'contact'):
612            machine.contact = validate.contact
613
614        session.save_or_update(machine)
615        if update_acl:
616            cache_acls.refreshMachine(machine)
617        session.commit()
618    except:
619        session.rollback()
620        raise
621    for diskname in olddisk:
622        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
623    if hasattr(validate, 'name'):
624        controls.renameMachine(machine, oldname, validate.name)
625    return dict(machine=machine)
626
627def infoDict(username, state, machine):
628    """Get the variables used by info.tmpl."""
629    status = controls.statusInfo(machine)
630    has_vnc = hasVnc(status)
631    if status is None:
632        main_status = dict(name=machine.name,
633                           memory=str(machine.memory))
634        uptime = None
635        cputime = None
636    else:
637        main_status = dict(status[1:])
638        main_status['host'] = controls.listHost(machine)
639        start_time = float(main_status.get('start_time', 0))
640        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
641        cpu_time_float = float(main_status.get('cpu_time', 0))
642        cputime = datetime.timedelta(seconds=int(cpu_time_float))
643    display_fields = [('name', 'Name'),
644                      ('description', 'Description'),
645                      ('owner', 'Owner'),
646                      ('administrator', 'Administrator'),
647                      ('contact', 'Contact'),
648                      ('type', 'Type'),
649                      'NIC_INFO',
650                      ('uptime', 'uptime'),
651                      ('cputime', 'CPU usage'),
652                      ('host', 'Hosted on'),
653                      ('memory', 'RAM'),
654                      'DISK_INFO',
655                      ('state', 'state (xen format)'),
656                      ]
657    fields = []
658    machine_info = {}
659    machine_info['name'] = machine.name
660    machine_info['description'] = machine.description
661    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
662    machine_info['owner'] = machine.owner
663    machine_info['administrator'] = machine.administrator
664    machine_info['contact'] = machine.contact
665
666    nic_fields = getNicInfo(machine_info, machine)
667    nic_point = display_fields.index('NIC_INFO')
668    display_fields = (display_fields[:nic_point] + nic_fields +
669                      display_fields[nic_point+1:])
670
671    disk_fields = getDiskInfo(machine_info, machine)
672    disk_point = display_fields.index('DISK_INFO')
673    display_fields = (display_fields[:disk_point] + disk_fields +
674                      display_fields[disk_point+1:])
675
676    main_status['memory'] += ' MiB'
677    for field, disp in display_fields:
678        if field in ('uptime', 'cputime') and locals()[field] is not None:
679            fields.append((disp, locals()[field]))
680        elif field in machine_info:
681            fields.append((disp, machine_info[field]))
682        elif field in main_status:
683            fields.append((disp, main_status[field]))
684        else:
685            pass
686            #fields.append((disp, None))
687
688    max_mem = validation.maxMemory(machine.owner, state, machine, False)
689    max_disk = validation.maxDisk(machine.owner, machine)
690    defaults = Defaults()
691    for name in 'machine_id name description administrator owner memory contact'.split():
692        if getattr(machine, name):
693            setattr(defaults, name, getattr(machine, name))
694    defaults.type = machine.type.type_id
695    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
696    d = dict(user=username,
697             on=status is not None,
698             machine=machine,
699             defaults=defaults,
700             has_vnc=has_vnc,
701             uptime=str(uptime),
702             ram=machine.memory,
703             max_mem=max_mem,
704             max_disk=max_disk,
705             fields = fields)
706    return d
707
708def send_error_mail(subject, body):
709    import subprocess
710
711    to = config.web.errormail
712    mail = """To: %s
713From: root@%s
714Subject: %s
715
716%s
717""" % (to, config.web.hostname, subject, body)
718    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
719                         stdin=subprocess.PIPE)
720    p.stdin.write(mail)
721    p.stdin.close()
722    p.wait()
723
724random.seed() #sigh
Note: See TracBrowser for help on using the repository browser.