source: package_branches/invirt-web/cherrypy-rebased/code/main.py @ 2708

Last change on this file since 2708 was 2708, checked in by broder, 14 years ago

tighten a bit of code

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