source: trunk/web/templates/main.py @ 205

Last change on this file since 205 was 205, checked in by ecprice, 17 years ago

A monster checkin, with a variety of changes to the web
infrastructure.

Adds some support for javascript and asynchronous updates.

Also added prototype.

The interface is *really* *slow*, though.

  • Property svn:executable set to *
File size: 38.8 KB
Line 
1#!/usr/bin/python
2"""Main CGI script for web interface"""
3
4import base64
5import cPickle
6import cgi
7import datetime
8import getafsgroups
9import hmac
10import os
11import random
12import re
13import sha
14import simplejson
15import string
16import subprocess
17import sys
18import time
19from StringIO import StringIO
20
21
22def revertStandardError():
23    """Move stderr to stdout, and return the contents of the old stderr."""
24    errio = sys.stderr
25    if not isinstance(errio, StringIO):
26        return None
27    sys.stderr = sys.stdout
28    errio.seek(0)
29    return errio.read()
30
31def printError():
32    """Revert stderr to stdout, and print the contents of stderr"""
33    if isinstance(sys.stderr, StringIO):
34        print revertStandardError()
35
36if __name__ == '__main__':
37    import atexit
38    atexit.register(printError)
39    sys.stderr = StringIO()
40
41sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
42
43from Cheetah.Template import Template
44from sipb_xen_database import *
45
46class MyException(Exception):
47    """Base class for my exceptions"""
48    pass
49
50class InvalidInput(MyException):
51    """Exception for user-provided input is invalid but maybe in good faith.
52
53    This would include setting memory to negative (which might be a
54    typo) but not setting an invalid boot CD (which requires bypassing
55    the select box).
56    """
57    def __init__(self, err_field, err_value, expl=None):
58        MyException.__init__(self, expl)
59        self.err_field = err_field
60        self.err_value = err_value
61
62class CodeError(MyException):
63    """Exception for internal errors or bad faith input."""
64    pass
65
66def helppopup(subj):
67    """Return HTML code for a (?) link to a specified help topic"""
68    return ('<span class="helplink"><a href="help?subject=' + subj + 
69            '&amp;simple=true" target="_blank" ' + 
70            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
71
72class Global(object):
73    """Global state of the system, to avoid duplicate remctls to get state"""
74    def __init__(self, user):
75        self.user = user
76
77    def __get_uptimes(self):
78        if not hasattr(self, '_uptimes'):
79            self._uptimes = getUptimes(Machine.select())
80        return self._uptimes
81    uptimes = property(__get_uptimes)
82
83    def clear(self):
84        """Clear the state so future accesses reload it."""
85        for attr in ('_uptimes', ):
86            if hasattr(self, attr):
87                delattr(self, attr)
88
89g = None
90
91class User:
92    """User class (sort of useless, I admit)"""
93    def __init__(self, username, email):
94        self.username = username
95        self.email = email
96
97def makeErrorPre(old, addition):
98    if addition is None:
99        return
100    if old:
101        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
102    else:
103        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
104
105Template.helppopup = staticmethod(helppopup)
106Template.err = None
107
108class JsonDict:
109    """Class to store a dictionary that will be converted to JSON"""
110    def __init__(self, **kws):
111        self.data = kws
112        if 'err' in kws:
113            err = kws['err']
114            del kws['err']
115            self.addError(err)
116
117    def __str__(self):
118        return simplejson.dumps(self.data)
119
120    def addError(self, text):
121        """Add stderr text to be displayed on the website."""
122        self.data['err'] = \
123            makeErrorPre(self.data.get('err'), text)
124
125class Defaults:
126    """Class to store default values for fields."""
127    memory = 256
128    disk = 4.0
129    cdrom = ''
130    name = ''
131    vmtype = 'hvm'
132    def __init__(self, max_memory=None, max_disk=None, **kws):
133        if max_memory is not None:
134            self.memory = min(self.memory, max_memory)
135        if max_disk is not None:
136            self.max_disk = min(self.disk, max_disk)
137        for key in kws:
138            setattr(self, key, kws[key])
139
140
141
142default_headers = {'Content-Type': 'text/html'}
143
144# ... and stolen from xend/uuid.py
145def randomUUID():
146    """Generate a random UUID."""
147
148    return [ random.randint(0, 255) for _ in range(0, 16) ]
149
150def uuidToString(u):
151    """Turn a numeric UUID to a hyphen-seperated one."""
152    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
153                     "%02x" * 6]) % tuple(u)
154
155MAX_MEMORY_TOTAL = 512
156MAX_MEMORY_SINGLE = 256
157MIN_MEMORY_SINGLE = 16
158MAX_DISK_TOTAL = 50
159MAX_DISK_SINGLE = 50
160MIN_DISK_SINGLE = 0.1
161MAX_VMS_TOTAL = 10
162MAX_VMS_ACTIVE = 4
163
164def getMachinesByOwner(user, machine=None):
165    """Return the machines owned by the same as a machine.
166   
167    If the machine is None, return the machines owned by the same
168    user.
169    """
170    if machine:
171        owner = machine.owner
172    else:
173        owner = user.username
174    return Machine.select_by(owner=owner)
175
176def maxMemory(user, machine=None, on=True):
177    """Return the maximum memory for a machine or a user.
178
179    If machine is None, return the memory available for a new
180    machine.  Else, return the maximum that machine can have.
181
182    on is whether the machine should be turned on.  If false, the max
183    memory for the machine to change to, if it is left off, is
184    returned.
185    """
186    if not on:
187        return MAX_MEMORY_SINGLE
188    machines = getMachinesByOwner(user, machine)
189    active_machines = [x for x in machines if g.uptimes[x]]
190    mem_usage = sum([x.memory for x in active_machines if x != machine])
191    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
192
193def maxDisk(user, machine=None):
194    machines = getMachinesByOwner(user, machine)
195    disk_usage = sum([sum([y.size for y in x.disks])
196                      for x in machines if x != machine])
197    return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
198
199def cantAddVm(user):
200    machines = getMachinesByOwner(user)
201    active_machines = [x for x in machines if g.uptimes[x]]
202    if len(machines) >= MAX_VMS_TOTAL:
203        return 'You have too many VMs to create a new one.'
204    if len(active_machines) >= MAX_VMS_ACTIVE:
205        return ('You already have the maximum number of VMs turned on.  '
206                'To create more, turn one off.')
207    return False
208
209def haveAccess(user, machine):
210    """Return whether a user has adminstrative access to a machine"""
211    if user.username == 'moo':
212        return True
213    if user.username in (machine.administrator, machine.owner):
214        return True
215    if getafsgroups.checkAfsGroup(user.username, machine.administrator, 
216                                  'athena.mit.edu'): #XXX Cell?
217        return True
218    if getafsgroups.checkLockerOwner(user.username, machine.owner):
219        return True
220    return owns(user, machine)
221
222def owns(user, machine):
223    """Return whether a user owns a machine"""
224    if user.username == 'moo':
225        return True
226    return getafsgroups.checkLockerOwner(user.username, machine.owner)
227
228def error(op, user, fields, err, emsg):
229    """Print an error page when a CodeError occurs"""
230    d = dict(op=op, user=user, errorMessage=str(err),
231             stderr=emsg)
232    return Template(file='error.tmpl', searchList=[d]);
233
234def invalidInput(op, user, fields, err, emsg):
235    """Print an error page when an InvalidInput exception occurs"""
236    d = dict(op=op, user=user, err_field=err.err_field,
237             err_value=str(err.err_value), stderr=emsg,
238             errorMessage=str(err))
239    return Template(file='invalid.tmpl', searchList=[d]);
240
241def validMachineName(name):
242    """Check that name is valid for a machine name"""
243    if not name:
244        return False
245    charset = string.ascii_letters + string.digits + '-_'
246    if name[0] in '-_' or len(name) > 22:
247        return False
248    for x in name:
249        if x not in charset:
250            return False
251    return True
252
253def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
254    """Kinit with a given username and keytab"""
255
256    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
257                         stderr=subprocess.PIPE)
258    e = p.wait()
259    if e:
260        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
261
262def checkKinit():
263    """If we lack tickets, kinit."""
264    p = subprocess.Popen(['klist', '-s'])
265    if p.wait():
266        kinit()
267
268def remctl(*args, **kws):
269    """Perform a remctl and return the output.
270
271    kinits if necessary, and outputs errors to stderr.
272    """
273    checkKinit()
274    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
275                         + list(args),
276                         stdout=subprocess.PIPE,
277                         stderr=subprocess.PIPE)
278    v = p.wait()
279    if kws.get('err'):
280        return p.stdout.read(), p.stderr.read()
281    if v:
282        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
283        print >> sys.stderr, p.stderr.read()
284        raise CodeError('ERROR on remctl')
285    return p.stdout.read()
286
287def lvcreate(machine, disk):
288    """Create a single disk for a machine"""
289    remctl('web', 'lvcreate', machine.name,
290           disk.guest_device_name, str(disk.size))
291   
292def makeDisks(machine):
293    """Update the lvm partitions to add a disk."""
294    for disk in machine.disks:
295        lvcreate(machine, disk)
296
297def bootMachine(machine, cdtype):
298    """Boot a machine with a given boot CD.
299
300    If cdtype is None, give no boot cd.  Otherwise, it is the string
301    id of the CD (e.g. 'gutsy_i386')
302    """
303    if cdtype is not None:
304        remctl('control', machine.name, 'create', 
305               cdtype)
306    else:
307        remctl('control', machine.name, 'create')
308
309def registerMachine(machine):
310    """Register a machine to be controlled by the web interface"""
311    remctl('web', 'register', machine.name)
312
313def unregisterMachine(machine):
314    """Unregister a machine to not be controlled by the web interface"""
315    remctl('web', 'unregister', machine.name)
316
317def parseStatus(s):
318    """Parse a status string into nested tuples of strings.
319
320    s = output of xm list --long <machine_name>
321    """
322    values = re.split('([()])', s)
323    stack = [[]]
324    for v in values[2:-2]: #remove initial and final '()'
325        if not v:
326            continue
327        v = v.strip()
328        if v == '(':
329            stack.append([])
330        elif v == ')':
331            if len(stack[-1]) == 1:
332                stack[-1].append('')
333            stack[-2].append(stack[-1])
334            stack.pop()
335        else:
336            if not v:
337                continue
338            stack[-1].extend(v.split())
339    return stack[-1]
340
341def getUptimes(machines=None):
342    """Return a dictionary mapping machine names to uptime strings"""
343    value_string = remctl('web', 'listvms')
344    lines = value_string.splitlines()
345    d = {}
346    for line in lines:
347        lst = line.split()
348        name, id = lst[:2]
349        uptime = ' '.join(lst[2:])
350        d[name] = uptime
351    ans = {}
352    for m in machines:
353        ans[m] = d.get(m.name)
354    return ans
355
356def statusInfo(machine):
357    """Return the status list for a given machine.
358
359    Gets and parses xm list --long
360    """
361    value_string, err_string = remctl('control', machine.name, 'list-long', 
362                                      err=True)
363    if 'Unknown command' in err_string:
364        raise CodeError("ERROR in remctl list-long %s is not registered" % 
365                        (machine.name,))
366    elif 'does not exist' in err_string:
367        return None
368    elif err_string:
369        raise CodeError("ERROR in remctl list-long %s%s" % 
370                        (machine.name, err_string))
371    status = parseStatus(value_string)
372    return status
373
374def hasVnc(status):
375    """Does the machine with a given status list support VNC?"""
376    if status is None:
377        return False
378    for l in status:
379        if l[0] == 'device' and l[1][0] == 'vfb':
380            d = dict(l[1][1:])
381            return 'location' in d
382    return False
383
384def createVm(user, name, memory, disk, is_hvm, cdrom):
385    """Create a VM and put it in the database"""
386    # put stuff in the table
387    transaction = ctx.current.create_transaction()
388    try:
389        if memory > maxMemory(user):
390            raise InvalidInput('memory', memory,
391                               "Max %s" % maxMemory(user))
392        if disk > maxDisk(user) * 1024:
393            raise InvalidInput('disk', disk,
394                               "Max %s" % maxDisk(user))
395        reason = cantAddVm(user)
396        if reason:
397            raise InvalidInput('create', True, reason)
398        res = meta.engine.execute('select nextval('
399                                  '\'"machines_machine_id_seq"\')')
400        id = res.fetchone()[0]
401        machine = Machine()
402        machine.machine_id = id
403        machine.name = name
404        machine.memory = memory
405        machine.owner = user.username
406        machine.administrator = user.username
407        machine.contact = user.email
408        machine.uuid = uuidToString(randomUUID())
409        machine.boot_off_cd = True
410        machine_type = Type.get_by(hvm=is_hvm)
411        machine.type_id = machine_type.type_id
412        ctx.current.save(machine)
413        disk = Disk(machine.machine_id, 
414                    'hda', disk)
415        open_nics = NIC.select_by(machine_id=None)
416        if not open_nics: #No IPs left!
417            raise CodeError("No IP addresses left!  "
418                            "Contact sipb-xen-dev@mit.edu")
419        nic = open_nics[0]
420        nic.machine_id = machine.machine_id
421        nic.hostname = name
422        ctx.current.save(nic)   
423        ctx.current.save(disk)
424        transaction.commit()
425    except:
426        transaction.rollback()
427        raise
428    registerMachine(machine)
429    makeDisks(machine)
430    # tell it to boot with cdrom
431    bootMachine(machine, cdrom)
432
433    return machine
434
435def validMemory(user, memory, machine=None, on=True):
436    """Parse and validate limits for memory for a given user and machine.
437
438    on is whether the memory must be valid after the machine is
439    switched on.
440    """
441    try:
442        memory = int(memory)
443        if memory < MIN_MEMORY_SINGLE:
444            raise ValueError
445    except ValueError:
446        raise InvalidInput('memory', memory, 
447                           "Minimum %s MB" % MIN_MEMORY_SINGLE)
448    if memory > maxMemory(user, machine, on):
449        raise InvalidInput('memory', memory,
450                           'Maximum %s MB' % maxMemory(user, machine))
451    return memory
452
453def validDisk(user, disk, machine=None):
454    """Parse and validate limits for disk for a given user and machine."""
455    try:
456        disk = float(disk)
457        if disk > maxDisk(user, machine):
458            raise InvalidInput('disk', disk,
459                               "Maximum %s G" % maxDisk(user, machine))
460        disk = int(disk * 1024)
461        if disk < MIN_DISK_SINGLE * 1024:
462            raise ValueError
463    except ValueError:
464        raise InvalidInput('disk', disk,
465                           "Minimum %s GB" % MIN_DISK_SINGLE)
466    return disk
467
468def parseCreate(user, fields):
469    name = fields.getfirst('name')
470    if not validMachineName(name):
471        raise InvalidInput('name', name, 'You must provide a machine name.')
472    name = name.lower()
473
474    if Machine.get_by(name=name):
475        raise InvalidInput('name', name,
476                           "Name already exists.")
477   
478    memory = fields.getfirst('memory')
479    memory = validMemory(user, memory, on=True)
480   
481    disk = fields.getfirst('disk')
482    disk = validDisk(user, disk)
483
484    vm_type = fields.getfirst('vmtype')
485    if vm_type not in ('hvm', 'paravm'):
486        raise CodeError("Invalid vm type '%s'"  % vm_type)   
487    is_hvm = (vm_type == 'hvm')
488
489    cdrom = fields.getfirst('cdrom')
490    if cdrom is not None and not CDROM.get(cdrom):
491        raise CodeError("Invalid cdrom type '%s'" % cdrom)
492    return dict(user=user, name=name, memory=memory, disk=disk,
493                is_hvm=is_hvm, cdrom=cdrom)
494
495def create(user, fields):
496    """Handler for create requests."""
497    js = fields.getfirst('js')
498    try:
499        parsed_fields = parseCreate(user, fields)
500        machine = createVm(**parsed_fields)
501    except InvalidInput, err:
502        if not js:
503            raise
504    else:
505        err = None
506        if not js:
507            d = dict(user=user,
508                     machine=machine)
509            return Template(file='create.tmpl', searchList=[d])
510    g.clear() #Changed global state
511    d = getListDict(user)
512    d['err'] = err
513    if err:
514        for field in fields.keys():
515            setattr(d['defaults'], field, fields.getfirst(field))
516    else:
517        d['new_machine'] = parsed_fields['name']
518    t = Template(file='list.tmpl', searchList=[d])
519    return JsonDict(createtable=t.createTable(),
520                    machinelist=t.machineList(d['machines']))
521
522
523def getListDict(user):
524    machines = [m for m in Machine.select() if haveAccess(user, m)]   
525    on = {}
526    has_vnc = {}
527    on = g.uptimes
528    for m in machines:
529        m.uptime = g.uptimes.get(m)
530        if not on[m]:
531            has_vnc[m] = 'Off'
532        elif m.type.hvm:
533            has_vnc[m] = True
534        else:
535            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
536    #     for m in machines:
537    #         status = statusInfo(m)
538    #         on[m.name] = status is not None
539    #         has_vnc[m.name] = hasVnc(status)
540    max_memory = maxMemory(user)
541    max_disk = maxDisk(user)
542    defaults = Defaults(max_memory=max_memory,
543                        max_disk=max_disk,
544                        cdrom='gutsy-i386')
545    d = dict(user=user,
546             cant_add_vm=cantAddVm(user),
547             max_memory=max_memory,
548             max_disk=max_disk,
549             defaults=defaults,
550             machines=machines,
551             has_vnc=has_vnc,
552             uptimes=g.uptimes,
553             cdroms=CDROM.select())
554    return d
555
556def listVms(user, fields):
557    """Handler for list requests."""
558    d = getListDict(user)
559    t = Template(file='list.tmpl', searchList=[d])
560    js = fields.getfirst('js')
561    if not js:
562        return t
563    if js == 'machinelist':
564        return t.machineList(d['machines'])
565    elif js.startswith('machinerow-'):
566        request_machine_id = int(js.split('-')[1])
567        m = [x for x in d['machines'] if x.id == request_machine_id]
568        return t.machineRow(m)
569    elif js == 'createtable':
570        return t.createTable()
571           
572def testMachineId(user, machineId, exists=True):
573    """Parse, validate and check authorization for a given machineId.
574
575    If exists is False, don't check that it exists.
576    """
577    if machineId is None:
578        raise CodeError("No machine ID specified")
579    try:
580        machineId = int(machineId)
581    except ValueError:
582        raise CodeError("Invalid machine ID '%s'" % machineId)
583    machine = Machine.get(machineId)
584    if exists and machine is None:
585        raise CodeError("No such machine ID '%s'" % machineId)
586    if machine is not None and not haveAccess(user, machine):
587        raise CodeError("No access to machine ID '%s'" % machineId)
588    return machine
589
590def vnc(user, fields):
591    """VNC applet page.
592
593    Note that due to same-domain restrictions, the applet connects to
594    the webserver, which needs to forward those requests to the xen
595    server.  The Xen server runs another proxy that (1) authenticates
596    and (2) finds the correct port for the VM.
597
598    You might want iptables like:
599
600    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
601      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
602    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
603      --dport 10003 -j SNAT --to-source 18.187.7.142
604    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
605      --dport 10003 -j ACCEPT
606
607    Remember to enable iptables!
608    echo 1 > /proc/sys/net/ipv4/ip_forward
609    """
610    machine = testMachineId(user, fields.getfirst('machine_id'))
611   
612    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
613
614    data = {}
615    data["user"] = user.username
616    data["machine"] = machine.name
617    data["expires"] = time.time()+(5*60)
618    pickled_data = cPickle.dumps(data)
619    m = hmac.new(TOKEN_KEY, digestmod=sha)
620    m.update(pickled_data)
621    token = {'data': pickled_data, 'digest': m.digest()}
622    token = cPickle.dumps(token)
623    token = base64.urlsafe_b64encode(token)
624   
625    status = statusInfo(machine)
626    has_vnc = hasVnc(status)
627   
628    d = dict(user=user,
629             on=status,
630             has_vnc=has_vnc,
631             machine=machine,
632             hostname=os.environ.get('SERVER_NAME', 'localhost'),
633             authtoken=token)
634    return Template(file='vnc.tmpl', searchList=[d])
635
636def getNicInfo(data_dict, machine):
637    """Helper function for info, get data on nics for a machine.
638
639    Modifies data_dict to include the relevant data, and returns a list
640    of (key, name) pairs to display "name: data_dict[key]" to the user.
641    """
642    data_dict['num_nics'] = len(machine.nics)
643    nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
644                           ('nic%s_mac', 'NIC %s MAC Addr'),
645                           ('nic%s_ip', 'NIC %s IP'),
646                           ]
647    nic_fields = []
648    for i in range(len(machine.nics)):
649        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
650        data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
651                                           '.servers.csail.mit.edu')
652        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
653        data_dict['nic%s_ip' % i] = machine.nics[i].ip
654    if len(machine.nics) == 1:
655        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
656    return nic_fields
657
658def getDiskInfo(data_dict, machine):
659    """Helper function for info, get data on disks for a machine.
660
661    Modifies data_dict to include the relevant data, and returns a list
662    of (key, name) pairs to display "name: data_dict[key]" to the user.
663    """
664    data_dict['num_disks'] = len(machine.disks)
665    disk_fields_template = [('%s_size', '%s size')]
666    disk_fields = []
667    for disk in machine.disks:
668        name = disk.guest_device_name
669        disk_fields.extend([(x % name, y % name) for x, y in 
670                            disk_fields_template])
671        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
672    return disk_fields
673
674def deleteVM(machine):
675    """Delete a VM."""
676    remctl('control', machine.name, 'destroy', err=True)
677    transaction = ctx.current.create_transaction()
678    delete_disk_pairs = [(machine.name, d.guest_device_name) 
679                         for d in machine.disks]
680    try:
681        for nic in machine.nics:
682            nic.machine_id = None
683            nic.hostname = None
684            ctx.current.save(nic)
685        for disk in machine.disks:
686            ctx.current.delete(disk)
687        ctx.current.delete(machine)
688        transaction.commit()
689    except:
690        transaction.rollback()
691        raise
692    for mname, dname in delete_disk_pairs:
693        remctl('web', 'lvremove', mname, dname)
694    unregisterMachine(machine)
695
696def commandResult(user, fields):
697    print >> sys.stderr, time.time()-start_time
698    machine = testMachineId(user, fields.getfirst('machine_id'))
699    action = fields.getfirst('action')
700    cdrom = fields.getfirst('cdrom')
701    print >> sys.stderr, time.time()-start_time
702    if cdrom is not None and not CDROM.get(cdrom):
703        raise CodeError("Invalid cdrom type '%s'" % cdrom)   
704    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
705                      'Delete VM'):
706        raise CodeError("Invalid action '%s'" % action)
707    if action == 'Reboot':
708        if cdrom is not None:
709            out, err = remctl('control', machine.name, 'reboot', cdrom,
710                              err=True)
711        else:
712            out, err = remctl('control', machine.name, 'reboot',
713                              err=True)
714        if err:
715            if re.match("Error: Domain '.*' does not exist.", err):
716                raise InvalidInput("action", "reboot", 
717                                   "Machine is not on")
718            else:
719                print >> sys.stderr, 'Error on reboot:'
720                print >> sys.stderr, err
721                raise CodeError('ERROR on remctl')
722               
723    elif action == 'Power on':
724        if maxMemory(user) < machine.memory:
725            raise InvalidInput('action', 'Power on',
726                               "You don't have enough free RAM quota "
727                               "to turn on this machine.")
728        bootMachine(machine, cdrom)
729    elif action == 'Power off':
730        out, err = remctl('control', machine.name, 'destroy', err=True)
731        if err:
732            if re.match("Error: Domain '.*' does not exist.", err):
733                raise InvalidInput("action", "Power off", 
734                                   "Machine is not on.")
735            else:
736                print >> sys.stderr, 'Error on power off:'
737                print >> sys.stderr, err
738                raise CodeError('ERROR on remctl')
739    elif action == 'Shutdown':
740        out, err = remctl('control', machine.name, 'shutdown', err=True)
741        if err:
742            if re.match("Error: Domain '.*' does not exist.", err):
743                raise InvalidInput("action", "Shutdown", 
744                                   "Machine is not on.")
745            else:
746                print >> sys.stderr, 'Error on Shutdown:'
747                print >> sys.stderr, err
748                raise CodeError('ERROR on remctl')
749    elif action == 'Delete VM':
750        deleteVM(machine)
751    print >> sys.stderr, time.time()-start_time
752
753    d = dict(user=user,
754             command=action,
755             machine=machine)
756    return d
757
758def command(user, fields):
759    """Handler for running commands like boot and delete on a VM."""
760    js = fields.getfirst('js')
761    try:
762        d = commandResult(user, fields)
763    except InvalidInput, err:
764        if not js:
765            raise
766        result = None
767    else:
768        err = None
769        result = 'Success!'
770        if not js:
771            return Template(file='command.tmpl', searchList=[d])
772    if js == 'list':
773        g.clear() #Changed global state
774        d = getListDict(user)
775        t = Template(file='list.tmpl', searchList=[d])
776        return JsonDict(createtable=t.createTable(),
777                        machinelist=t.machineList(d['machines']),
778                        result=result,
779                        err=err)
780    elif js == 'info':
781        machine = testMachineId(user, fields.getfirst('machine_id'))
782        d = infoDict(user, machine)
783        t = Template(file='info.tmpl', searchList=[d])
784        return JsonDict(info=t.infoTable(),
785                        commands=t.commands(),
786                        modify=t.modifyForm(),
787                        result=result,
788                        err=err)
789    else:
790        raise InvalidInput('js', js, 'Not a known js type.')
791
792def testAdmin(user, admin, machine):
793    if admin in (None, machine.administrator):
794        return None
795    if admin == user.username:
796        return admin
797    if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
798        return admin
799    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
800                                  'athena.mit.edu'):
801        return 'system:'+admin
802    raise InvalidInput('administrator', admin, 
803                       'You must control the group you move it to.')
804   
805def testOwner(user, owner, machine):
806    if owner in (None, machine.owner):
807        return None
808    value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
809    if value == True:
810        return owner
811    raise InvalidInput('owner', owner, value)
812
813def testContact(user, contact, machine=None):
814    if contact in (None, machine.contact):
815        return None
816    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
817        raise InvalidInput('contact', contact, "Not a valid email.")
818    return contact
819
820def testDisk(user, disksize, machine=None):
821    return disksize
822
823def testName(user, name, machine=None):
824    if name in (None, machine.name):
825        return None
826    if not Machine.select_by(name=name):
827        return name
828    raise InvalidInput('name', name, "Name is already taken.")
829
830def testHostname(user, hostname, machine):
831    for nic in machine.nics:
832        if hostname == nic.hostname:
833            return hostname
834    # check if doesn't already exist
835    if NIC.select_by(hostname=hostname):
836        raise InvalidInput('hostname', hostname,
837                           "Already exists")
838    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
839        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
840                           "must only use number, letters, and dashes.")
841    return hostname
842
843def modifyDict(user, fields):
844    olddisk = {}
845    transaction = ctx.current.create_transaction()
846    try:
847        machine = testMachineId(user, fields.getfirst('machine_id'))
848        owner = testOwner(user, fields.getfirst('owner'), machine)
849        admin = testAdmin(user, fields.getfirst('administrator'), machine)
850        contact = testContact(user, fields.getfirst('contact'), machine)
851        hostname = testHostname(owner, fields.getfirst('hostname'), machine)
852        name = testName(user, fields.getfirst('name'), machine)
853        oldname = machine.name
854        command = "modify"
855
856        memory = fields.getfirst('memory')
857        if memory is not None:
858            memory = validMemory(user, memory, machine, on=False)
859            machine.memory = memory
860 
861        disksize = testDisk(user, fields.getfirst('disk'))
862        if disksize is not None:
863            disksize = validDisk(user, disksize, machine)
864            disk = machine.disks[0]
865            if disk.size != disksize:
866                olddisk[disk.guest_device_name] = disksize
867                disk.size = disksize
868                ctx.current.save(disk)
869       
870        # XXX first NIC gets hostname on change? 
871        # Interface doesn't support more.
872        for nic in machine.nics[:1]:
873            nic.hostname = hostname
874            ctx.current.save(nic)
875
876        if owner is not None:
877            machine.owner = owner
878        if name is not None:
879            machine.name = name
880        if admin is not None:
881            machine.administrator = admin
882        if contact is not None:
883            machine.contact = contact
884           
885        ctx.current.save(machine)
886        transaction.commit()
887    except:
888        transaction.rollback()
889        raise
890    for diskname in olddisk:
891        remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
892    if name is not None:
893        for disk in machine.disks:
894            remctl("web", "lvrename", oldname, disk.guest_device_name, name)
895        remctl("web", "moveregister", oldname, name)
896    return dict(user=user,
897                command=command,
898                machine=machine)
899   
900def modify(user, fields):
901    """Handler for modifying attributes of a machine."""
902    js = fields.getfirst('js')
903    try:
904        modify_dict = modifyDict(user, fields)
905    except InvalidInput, err:
906        if not js:
907            raise
908        result = ''
909        machine = testMachineId(user, fields.getfirst('machine_id'))
910    else:
911        machine = modify_dict['machine']
912        result='Success!'
913        err = None
914        if not js:
915            return Template(file='command.tmpl', searchList=[modify_dict])
916    info_dict = infoDict(user, machine)
917    info_dict['err'] = err
918    if err:
919        for field in fields.keys():
920            setattr(info_dict['defaults'], field, fields.getfirst(field))
921    t = Template(file='info.tmpl', searchList=[info_dict])
922    return JsonDict(info=t.infoTable(),
923                    commands=t.commands(),
924                    modify=t.modifyForm(),
925                    result=result,
926                    err=err)
927   
928
929def helpHandler(user, fields):
930    """Handler for help messages."""
931    simple = fields.getfirst('simple')
932    subjects = fields.getlist('subject')
933   
934    help_mapping = dict(paravm_console="""
935ParaVM machines do not support console access over VNC.  To access
936these machines, you either need to boot with a liveCD and ssh in or
937hope that the sipb-xen maintainers add support for serial consoles.""",
938                        hvm_paravm="""
939HVM machines use the virtualization features of the processor, while
940ParaVM machines use Xen's emulation of virtualization features.  You
941want an HVM virtualized machine.""",
942                        cpu_weight="""
943Don't ask us!  We're as mystified as you are.""",
944                        owner="""
945The owner field is used to determine <a
946href="help?subject=quotas">quotas</a>.  It must be the name of a
947locker that you are an AFS administrator of.  In particular, you or an
948AFS group you are a member of must have AFS rlidwka bits on the
949locker.  You can check see who administers the LOCKER locker using the
950command 'fs la /mit/LOCKER' on Athena.)  See also <a
951href="help?subject=administrator">administrator</a>.""",
952                        administrator="""
953The administrator field determines who can access the console and
954power on and off the machine.  This can be either a user or a moira
955group.""",
956                        quotas="""
957Quotas are determined on a per-locker basis.  Each quota may have a
958maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
959active machines."""
960                   )
961   
962    if not subjects:
963        subjects = sorted(help_mapping.keys())
964       
965    d = dict(user=user,
966             simple=simple,
967             subjects=subjects,
968             mapping=help_mapping)
969   
970    return Template(file="help.tmpl", searchList=[d])
971   
972
973def badOperation(u, e):
974    raise CodeError("Unknown operation")
975
976def infoDict(user, machine):
977    status = statusInfo(machine)
978    has_vnc = hasVnc(status)
979    if status is None:
980        main_status = dict(name=machine.name,
981                           memory=str(machine.memory))
982        uptime = None
983        cputime = None
984    else:
985        main_status = dict(status[1:])
986        start_time = float(main_status.get('start_time', 0))
987        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
988        cpu_time_float = float(main_status.get('cpu_time', 0))
989        cputime = datetime.timedelta(seconds=int(cpu_time_float))
990    display_fields = """name uptime memory state cpu_weight on_reboot
991     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
992    display_fields = [('name', 'Name'),
993                      ('owner', 'Owner'),
994                      ('administrator', 'Administrator'),
995                      ('contact', 'Contact'),
996                      ('type', 'Type'),
997                      'NIC_INFO',
998                      ('uptime', 'uptime'),
999                      ('cputime', 'CPU usage'),
1000                      ('memory', 'RAM'),
1001                      'DISK_INFO',
1002                      ('state', 'state (xen format)'),
1003                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
1004                      ('on_reboot', 'Action on VM reboot'),
1005                      ('on_poweroff', 'Action on VM poweroff'),
1006                      ('on_crash', 'Action on VM crash'),
1007                      ('on_xend_start', 'Action on Xen start'),
1008                      ('on_xend_stop', 'Action on Xen stop'),
1009                      ('bootloader', 'Bootloader options'),
1010                      ]
1011    fields = []
1012    machine_info = {}
1013    machine_info['name'] = machine.name
1014    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
1015    machine_info['owner'] = machine.owner
1016    machine_info['administrator'] = machine.administrator
1017    machine_info['contact'] = machine.contact
1018
1019    nic_fields = getNicInfo(machine_info, machine)
1020    nic_point = display_fields.index('NIC_INFO')
1021    display_fields = (display_fields[:nic_point] + nic_fields + 
1022                      display_fields[nic_point+1:])
1023
1024    disk_fields = getDiskInfo(machine_info, machine)
1025    disk_point = display_fields.index('DISK_INFO')
1026    display_fields = (display_fields[:disk_point] + disk_fields + 
1027                      display_fields[disk_point+1:])
1028   
1029    main_status['memory'] += ' MB'
1030    for field, disp in display_fields:
1031        if field in ('uptime', 'cputime') and locals()[field] is not None:
1032            fields.append((disp, locals()[field]))
1033        elif field in machine_info:
1034            fields.append((disp, machine_info[field]))
1035        elif field in main_status:
1036            fields.append((disp, main_status[field]))
1037        else:
1038            pass
1039            #fields.append((disp, None))
1040    max_mem = maxMemory(user, machine)
1041    max_disk = maxDisk(user, machine)
1042    defaults=Defaults()
1043    for name in 'machine_id name administrator owner memory contact'.split():
1044        setattr(defaults, name, getattr(machine, name))
1045    if machine.nics:
1046        defaults.hostname = machine.nics[0].hostname
1047    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
1048    d = dict(user=user,
1049             cdroms=CDROM.select(),
1050             on=status is not None,
1051             machine=machine,
1052             defaults=defaults,
1053             has_vnc=has_vnc,
1054             uptime=str(uptime),
1055             ram=machine.memory,
1056             max_mem=max_mem,
1057             max_disk=max_disk,
1058             owner_help=helppopup("owner"),
1059             fields = fields)
1060    return d
1061
1062def info(user, fields):
1063    """Handler for info on a single VM."""
1064    machine = testMachineId(user, fields.getfirst('machine_id'))
1065    d = infoDict(user, machine)
1066    return Template(file='info.tmpl', searchList=[d])
1067
1068mapping = dict(list=listVms,
1069               vnc=vnc,
1070               command=command,
1071               modify=modify,
1072               info=info,
1073               create=create,
1074               help=helpHandler)
1075
1076def printHeaders(headers):
1077    for key, value in headers.iteritems():
1078        print '%s: %s' % (key, value)
1079    print
1080
1081
1082def getUser():
1083    """Return the current user based on the SSL environment variables"""
1084    if 'SSL_CLIENT_S_DN_Email' in os.environ:
1085        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
1086        return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
1087    else:
1088        return User('moo', 'nobody')
1089
1090if __name__ == '__main__':
1091    start_time = time.time()
1092    fields = cgi.FieldStorage()
1093    u = getUser()
1094    g = Global(u)
1095    operation = os.environ.get('PATH_INFO', '')
1096    if not operation:
1097        print "Status: 301 Moved Permanently"
1098        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
1099        sys.exit(0)
1100
1101    if operation.startswith('/'):
1102        operation = operation[1:]
1103    if not operation:
1104        operation = 'list'
1105
1106
1107
1108    fun = mapping.get(operation, badOperation)
1109
1110    if fun not in (helpHandler, ):
1111        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
1112    try:
1113        output = fun(u, fields)
1114
1115        headers = dict(default_headers)
1116        if isinstance(output, tuple):
1117            new_headers, output = output
1118            headers.update(new_headers)
1119
1120        e = revertStandardError()
1121        if e:
1122            output.addError(e)
1123        printHeaders(headers)
1124        print output
1125    except Exception, err:
1126        if not fields.has_key('js'):
1127            if isinstance(err, CodeError):
1128                print 'Content-Type: text/html\n'
1129                e = revertStandardError()
1130                print error(operation, u, fields, err, e)
1131                sys.exit(1)
1132            if isinstance(err, InvalidInput):
1133                print 'Content-Type: text/html\n'
1134                e = revertStandardError()
1135                print invalidInput(operation, u, fields, err, e)
1136                sys.exit(1)
1137        print 'Content-Type: text/plain\n'
1138        print 'Uh-oh!  We experienced an error.'
1139        print 'Please email sipb-xen@mit.edu with the contents of this page.'
1140        print '----'
1141        e = revertStandardError()
1142        print e
1143        print '----'
1144        raise
Note: See TracBrowser for help on using the repository browser.