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

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

Full error handling

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