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

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

Removed the Javascript, since I realized that

  • The way it was was worthless
  • The architecture has serious trouble supporting useful js
  • Property svn:executable set to *
File size: 37.6 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    try:
498        parsed_fields = parseCreate(user, fields)
499        machine = createVm(**parsed_fields)
500    except InvalidInput, err:
501        pass
502    else:
503        err = None
504    g.clear() #Changed global state
505    d = getListDict(user)
506    d['err'] = err
507    if err:
508        for field in fields.keys():
509            setattr(d['defaults'], field, fields.getfirst(field))
510    else:
511        d['new_machine'] = parsed_fields['name']
512    return Template(file='list.tmpl', searchList=[d])
513
514
515def getListDict(user):
516    machines = [m for m in Machine.select() if haveAccess(user, m)]   
517    on = {}
518    has_vnc = {}
519    on = g.uptimes
520    for m in machines:
521        m.uptime = g.uptimes.get(m)
522        if not on[m]:
523            has_vnc[m] = 'Off'
524        elif m.type.hvm:
525            has_vnc[m] = True
526        else:
527            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
528    #     for m in machines:
529    #         status = statusInfo(m)
530    #         on[m.name] = status is not None
531    #         has_vnc[m.name] = hasVnc(status)
532    max_memory = maxMemory(user)
533    max_disk = maxDisk(user)
534    defaults = Defaults(max_memory=max_memory,
535                        max_disk=max_disk,
536                        cdrom='gutsy-i386')
537    d = dict(user=user,
538             cant_add_vm=cantAddVm(user),
539             max_memory=max_memory,
540             max_disk=max_disk,
541             defaults=defaults,
542             machines=machines,
543             has_vnc=has_vnc,
544             uptimes=g.uptimes,
545             cdroms=CDROM.select())
546    return d
547
548def listVms(user, fields):
549    """Handler for list requests."""
550    d = getListDict(user)
551    return Template(file='list.tmpl', searchList=[d])
552           
553def testMachineId(user, machineId, exists=True):
554    """Parse, validate and check authorization for a given machineId.
555
556    If exists is False, don't check that it exists.
557    """
558    if machineId is None:
559        raise CodeError("No machine ID specified")
560    try:
561        machineId = int(machineId)
562    except ValueError:
563        raise CodeError("Invalid machine ID '%s'" % machineId)
564    machine = Machine.get(machineId)
565    if exists and machine is None:
566        raise CodeError("No such machine ID '%s'" % machineId)
567    if machine is not None and not haveAccess(user, machine):
568        raise CodeError("No access to machine ID '%s'" % machineId)
569    return machine
570
571def vnc(user, fields):
572    """VNC applet page.
573
574    Note that due to same-domain restrictions, the applet connects to
575    the webserver, which needs to forward those requests to the xen
576    server.  The Xen server runs another proxy that (1) authenticates
577    and (2) finds the correct port for the VM.
578
579    You might want iptables like:
580
581    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
582      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
583    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
584      --dport 10003 -j SNAT --to-source 18.187.7.142
585    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
586      --dport 10003 -j ACCEPT
587
588    Remember to enable iptables!
589    echo 1 > /proc/sys/net/ipv4/ip_forward
590    """
591    machine = testMachineId(user, fields.getfirst('machine_id'))
592   
593    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
594
595    data = {}
596    data["user"] = user.username
597    data["machine"] = machine.name
598    data["expires"] = time.time()+(5*60)
599    pickled_data = cPickle.dumps(data)
600    m = hmac.new(TOKEN_KEY, digestmod=sha)
601    m.update(pickled_data)
602    token = {'data': pickled_data, 'digest': m.digest()}
603    token = cPickle.dumps(token)
604    token = base64.urlsafe_b64encode(token)
605   
606    status = statusInfo(machine)
607    has_vnc = hasVnc(status)
608   
609    d = dict(user=user,
610             on=status,
611             has_vnc=has_vnc,
612             machine=machine,
613             hostname=os.environ.get('SERVER_NAME', 'localhost'),
614             authtoken=token)
615    return Template(file='vnc.tmpl', searchList=[d])
616
617def getNicInfo(data_dict, machine):
618    """Helper function for info, get data on nics for a machine.
619
620    Modifies data_dict to include the relevant data, and returns a list
621    of (key, name) pairs to display "name: data_dict[key]" to the user.
622    """
623    data_dict['num_nics'] = len(machine.nics)
624    nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
625                           ('nic%s_mac', 'NIC %s MAC Addr'),
626                           ('nic%s_ip', 'NIC %s IP'),
627                           ]
628    nic_fields = []
629    for i in range(len(machine.nics)):
630        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
631        data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
632                                           '.servers.csail.mit.edu')
633        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
634        data_dict['nic%s_ip' % i] = machine.nics[i].ip
635    if len(machine.nics) == 1:
636        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
637    return nic_fields
638
639def getDiskInfo(data_dict, machine):
640    """Helper function for info, get data on disks for a machine.
641
642    Modifies data_dict to include the relevant data, and returns a list
643    of (key, name) pairs to display "name: data_dict[key]" to the user.
644    """
645    data_dict['num_disks'] = len(machine.disks)
646    disk_fields_template = [('%s_size', '%s size')]
647    disk_fields = []
648    for disk in machine.disks:
649        name = disk.guest_device_name
650        disk_fields.extend([(x % name, y % name) for x, y in 
651                            disk_fields_template])
652        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
653    return disk_fields
654
655def deleteVM(machine):
656    """Delete a VM."""
657    remctl('control', machine.name, 'destroy', err=True)
658    transaction = ctx.current.create_transaction()
659    delete_disk_pairs = [(machine.name, d.guest_device_name) 
660                         for d in machine.disks]
661    try:
662        for nic in machine.nics:
663            nic.machine_id = None
664            nic.hostname = None
665            ctx.current.save(nic)
666        for disk in machine.disks:
667            ctx.current.delete(disk)
668        ctx.current.delete(machine)
669        transaction.commit()
670    except:
671        transaction.rollback()
672        raise
673    for mname, dname in delete_disk_pairs:
674        remctl('web', 'lvremove', mname, dname)
675    unregisterMachine(machine)
676
677def commandResult(user, fields):
678    print >> sys.stderr, time.time()-start_time
679    machine = testMachineId(user, fields.getfirst('machine_id'))
680    action = fields.getfirst('action')
681    cdrom = fields.getfirst('cdrom')
682    print >> sys.stderr, time.time()-start_time
683    if cdrom is not None and not CDROM.get(cdrom):
684        raise CodeError("Invalid cdrom type '%s'" % cdrom)   
685    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
686                      'Delete VM'):
687        raise CodeError("Invalid action '%s'" % action)
688    if action == 'Reboot':
689        if cdrom is not None:
690            out, err = remctl('control', machine.name, 'reboot', cdrom,
691                              err=True)
692        else:
693            out, err = remctl('control', machine.name, 'reboot',
694                              err=True)
695        if err:
696            if re.match("Error: Domain '.*' does not exist.", err):
697                raise InvalidInput("action", "reboot", 
698                                   "Machine is not on")
699            else:
700                print >> sys.stderr, 'Error on reboot:'
701                print >> sys.stderr, err
702                raise CodeError('ERROR on remctl')
703               
704    elif action == 'Power on':
705        if maxMemory(user) < machine.memory:
706            raise InvalidInput('action', 'Power on',
707                               "You don't have enough free RAM quota "
708                               "to turn on this machine.")
709        bootMachine(machine, cdrom)
710    elif action == 'Power off':
711        out, err = remctl('control', machine.name, 'destroy', err=True)
712        if err:
713            if re.match("Error: Domain '.*' does not exist.", err):
714                raise InvalidInput("action", "Power off", 
715                                   "Machine is not on.")
716            else:
717                print >> sys.stderr, 'Error on power off:'
718                print >> sys.stderr, err
719                raise CodeError('ERROR on remctl')
720    elif action == 'Shutdown':
721        out, err = remctl('control', machine.name, 'shutdown', err=True)
722        if err:
723            if re.match("Error: Domain '.*' does not exist.", err):
724                raise InvalidInput("action", "Shutdown", 
725                                   "Machine is not on.")
726            else:
727                print >> sys.stderr, 'Error on Shutdown:'
728                print >> sys.stderr, err
729                raise CodeError('ERROR on remctl')
730    elif action == 'Delete VM':
731        deleteVM(machine)
732    print >> sys.stderr, time.time()-start_time
733
734    d = dict(user=user,
735             command=action,
736             machine=machine)
737    return d
738
739def command(user, fields):
740    """Handler for running commands like boot and delete on a VM."""
741    back = fields.getfirst('back')
742    try:
743        d = commandResult(user, fields)
744        if d['command'] == 'Delete VM':
745            back = 'list'
746    except InvalidInput, err:
747        if not back:
748            raise
749        print >> sys.stderr, err
750        result = None
751    else:
752        result = 'Success!'
753        if not back:
754            return Template(file='command.tmpl', searchList=[d])
755    if back == 'list':
756        g.clear() #Changed global state
757        d = getListDict(user)
758        d['result'] = result
759        return Template(file='list.tmpl', searchList=[d])
760    elif back == 'info':
761        machine = testMachineId(user, fields.getfirst('machine_id'))
762        d = infoDict(user, machine)
763        d['result'] = result
764        return Template(file='info.tmpl', searchList=[d])
765    else:
766        raise InvalidInput('back', back, 'Not a known back page.')
767
768def testAdmin(user, admin, machine):
769    if admin in (None, machine.administrator):
770        return None
771    if admin == user.username:
772        return admin
773    if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
774        return admin
775    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
776                                  'athena.mit.edu'):
777        return 'system:'+admin
778    return admin
779    #raise InvalidInput('administrator', admin,
780    #                   'You must control the group you move it to.')
781   
782def testOwner(user, owner, machine):
783    if owner in (None, machine.owner):
784        return None
785    value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
786    if value == True:
787        return owner
788    raise InvalidInput('owner', owner, value)
789
790def testContact(user, contact, machine=None):
791    if contact in (None, machine.contact):
792        return None
793    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
794        raise InvalidInput('contact', contact, "Not a valid email.")
795    return contact
796
797def testDisk(user, disksize, machine=None):
798    return disksize
799
800def testName(user, name, machine=None):
801    if name in (None, machine.name):
802        return None
803    if not Machine.select_by(name=name):
804        return name
805    raise InvalidInput('name', name, "Name is already taken.")
806
807def testHostname(user, hostname, machine):
808    for nic in machine.nics:
809        if hostname == nic.hostname:
810            return hostname
811    # check if doesn't already exist
812    if NIC.select_by(hostname=hostname):
813        raise InvalidInput('hostname', hostname,
814                           "Already exists")
815    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
816        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
817                           "must only use number, letters, and dashes.")
818    return hostname
819
820def modifyDict(user, fields):
821    olddisk = {}
822    transaction = ctx.current.create_transaction()
823    try:
824        machine = testMachineId(user, fields.getfirst('machine_id'))
825        owner = testOwner(user, fields.getfirst('owner'), machine)
826        admin = testAdmin(user, fields.getfirst('administrator'), machine)
827        contact = testContact(user, fields.getfirst('contact'), machine)
828        hostname = testHostname(owner, fields.getfirst('hostname'), machine)
829        name = testName(user, fields.getfirst('name'), machine)
830        oldname = machine.name
831        command = "modify"
832
833        memory = fields.getfirst('memory')
834        if memory is not None:
835            memory = validMemory(user, memory, machine, on=False)
836            machine.memory = memory
837 
838        disksize = testDisk(user, fields.getfirst('disk'))
839        if disksize is not None:
840            disksize = validDisk(user, disksize, machine)
841            disk = machine.disks[0]
842            if disk.size != disksize:
843                olddisk[disk.guest_device_name] = disksize
844                disk.size = disksize
845                ctx.current.save(disk)
846       
847        # XXX first NIC gets hostname on change? 
848        # Interface doesn't support more.
849        for nic in machine.nics[:1]:
850            nic.hostname = hostname
851            ctx.current.save(nic)
852
853        if owner is not None:
854            machine.owner = owner
855        if name is not None:
856            machine.name = name
857        if admin is not None:
858            machine.administrator = admin
859        if contact is not None:
860            machine.contact = contact
861           
862        ctx.current.save(machine)
863        transaction.commit()
864    except:
865        transaction.rollback()
866        raise
867    for diskname in olddisk:
868        remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
869    if name is not None:
870        for disk in machine.disks:
871            remctl("web", "lvrename", oldname, disk.guest_device_name, name)
872        remctl("web", "moveregister", oldname, name)
873    return dict(user=user,
874                command=command,
875                machine=machine)
876   
877def modify(user, fields):
878    """Handler for modifying attributes of a machine."""
879    try:
880        modify_dict = modifyDict(user, fields)
881    except InvalidInput, err:
882        result = None
883        machine = testMachineId(user, fields.getfirst('machine_id'))
884    else:
885        machine = modify_dict['machine']
886        result='Success!'
887        err = None
888    info_dict = infoDict(user, machine)
889    info_dict['err'] = err
890    if err:
891        for field in fields.keys():
892            setattr(info_dict['defaults'], field, fields.getfirst(field))
893    info_dict['result'] = result
894    return Template(file='info.tmpl', searchList=[info_dict])
895   
896
897def helpHandler(user, fields):
898    """Handler for help messages."""
899    simple = fields.getfirst('simple')
900    subjects = fields.getlist('subject')
901   
902    help_mapping = dict(paravm_console="""
903ParaVM machines do not support console access over VNC.  To access
904these machines, you either need to boot with a liveCD and ssh in or
905hope that the sipb-xen maintainers add support for serial consoles.""",
906                        hvm_paravm="""
907HVM machines use the virtualization features of the processor, while
908ParaVM machines use Xen's emulation of virtualization features.  You
909want an HVM virtualized machine.""",
910                        cpu_weight="""
911Don't ask us!  We're as mystified as you are.""",
912                        owner="""
913The owner field is used to determine <a
914href="help?subject=quotas">quotas</a>.  It must be the name of a
915locker that you are an AFS administrator of.  In particular, you or an
916AFS group you are a member of must have AFS rlidwka bits on the
917locker.  You can check see who administers the LOCKER locker using the
918command 'fs la /mit/LOCKER' on Athena.)  See also <a
919href="help?subject=administrator">administrator</a>.""",
920                        administrator="""
921The administrator field determines who can access the console and
922power on and off the machine.  This can be either a user or a moira
923group.""",
924                        quotas="""
925Quotas are determined on a per-locker basis.  Each quota may have a
926maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
927active machines."""
928                   )
929   
930    if not subjects:
931        subjects = sorted(help_mapping.keys())
932       
933    d = dict(user=user,
934             simple=simple,
935             subjects=subjects,
936             mapping=help_mapping)
937   
938    return Template(file="help.tmpl", searchList=[d])
939   
940
941def badOperation(u, e):
942    raise CodeError("Unknown operation")
943
944def infoDict(user, machine):
945    status = statusInfo(machine)
946    has_vnc = hasVnc(status)
947    if status is None:
948        main_status = dict(name=machine.name,
949                           memory=str(machine.memory))
950        uptime = None
951        cputime = None
952    else:
953        main_status = dict(status[1:])
954        start_time = float(main_status.get('start_time', 0))
955        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
956        cpu_time_float = float(main_status.get('cpu_time', 0))
957        cputime = datetime.timedelta(seconds=int(cpu_time_float))
958    display_fields = """name uptime memory state cpu_weight on_reboot
959     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
960    display_fields = [('name', 'Name'),
961                      ('owner', 'Owner'),
962                      ('administrator', 'Administrator'),
963                      ('contact', 'Contact'),
964                      ('type', 'Type'),
965                      'NIC_INFO',
966                      ('uptime', 'uptime'),
967                      ('cputime', 'CPU usage'),
968                      ('memory', 'RAM'),
969                      'DISK_INFO',
970                      ('state', 'state (xen format)'),
971                      ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
972                      ('on_reboot', 'Action on VM reboot'),
973                      ('on_poweroff', 'Action on VM poweroff'),
974                      ('on_crash', 'Action on VM crash'),
975                      ('on_xend_start', 'Action on Xen start'),
976                      ('on_xend_stop', 'Action on Xen stop'),
977                      ('bootloader', 'Bootloader options'),
978                      ]
979    fields = []
980    machine_info = {}
981    machine_info['name'] = machine.name
982    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
983    machine_info['owner'] = machine.owner
984    machine_info['administrator'] = machine.administrator
985    machine_info['contact'] = machine.contact
986
987    nic_fields = getNicInfo(machine_info, machine)
988    nic_point = display_fields.index('NIC_INFO')
989    display_fields = (display_fields[:nic_point] + nic_fields + 
990                      display_fields[nic_point+1:])
991
992    disk_fields = getDiskInfo(machine_info, machine)
993    disk_point = display_fields.index('DISK_INFO')
994    display_fields = (display_fields[:disk_point] + disk_fields + 
995                      display_fields[disk_point+1:])
996   
997    main_status['memory'] += ' MB'
998    for field, disp in display_fields:
999        if field in ('uptime', 'cputime') and locals()[field] is not None:
1000            fields.append((disp, locals()[field]))
1001        elif field in machine_info:
1002            fields.append((disp, machine_info[field]))
1003        elif field in main_status:
1004            fields.append((disp, main_status[field]))
1005        else:
1006            pass
1007            #fields.append((disp, None))
1008    max_mem = maxMemory(user, machine)
1009    max_disk = maxDisk(user, machine)
1010    defaults=Defaults()
1011    for name in 'machine_id name administrator owner memory contact'.split():
1012        setattr(defaults, name, getattr(machine, name))
1013    if machine.nics:
1014        defaults.hostname = machine.nics[0].hostname
1015    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
1016    d = dict(user=user,
1017             cdroms=CDROM.select(),
1018             on=status is not None,
1019             machine=machine,
1020             defaults=defaults,
1021             has_vnc=has_vnc,
1022             uptime=str(uptime),
1023             ram=machine.memory,
1024             max_mem=max_mem,
1025             max_disk=max_disk,
1026             owner_help=helppopup("owner"),
1027             fields = fields)
1028    return d
1029
1030def info(user, fields):
1031    """Handler for info on a single VM."""
1032    machine = testMachineId(user, fields.getfirst('machine_id'))
1033    d = infoDict(user, machine)
1034    return Template(file='info.tmpl', searchList=[d])
1035
1036mapping = dict(list=listVms,
1037               vnc=vnc,
1038               command=command,
1039               modify=modify,
1040               info=info,
1041               create=create,
1042               help=helpHandler)
1043
1044def printHeaders(headers):
1045    for key, value in headers.iteritems():
1046        print '%s: %s' % (key, value)
1047    print
1048
1049
1050def getUser():
1051    """Return the current user based on the SSL environment variables"""
1052    if 'SSL_CLIENT_S_DN_Email' in os.environ:
1053        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
1054        return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
1055    else:
1056        return User('moo', 'nobody')
1057
1058if __name__ == '__main__':
1059    start_time = time.time()
1060    fields = cgi.FieldStorage()
1061    u = getUser()
1062    g = Global(u)
1063    operation = os.environ.get('PATH_INFO', '')
1064    if not operation:
1065        print "Status: 301 Moved Permanently"
1066        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
1067        sys.exit(0)
1068
1069    if operation.startswith('/'):
1070        operation = operation[1:]
1071    if not operation:
1072        operation = 'list'
1073
1074
1075
1076    fun = mapping.get(operation, badOperation)
1077
1078    if fun not in (helpHandler, ):
1079        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
1080    try:
1081        output = fun(u, fields)
1082
1083        headers = dict(default_headers)
1084        if isinstance(output, tuple):
1085            new_headers, output = output
1086            headers.update(new_headers)
1087
1088        e = revertStandardError()
1089        if e:
1090            output.addError(e)
1091        printHeaders(headers)
1092        print output
1093    except Exception, err:
1094        if not fields.has_key('js'):
1095            if isinstance(err, CodeError):
1096                print 'Content-Type: text/html\n'
1097                e = revertStandardError()
1098                print error(operation, u, fields, err, e)
1099                sys.exit(1)
1100            if isinstance(err, InvalidInput):
1101                print 'Content-Type: text/html\n'
1102                e = revertStandardError()
1103                print invalidInput(operation, u, fields, err, e)
1104                sys.exit(1)
1105        print 'Content-Type: text/plain\n'
1106        print 'Uh-oh!  We experienced an error.'
1107        print 'Please email sipb-xen@mit.edu with the contents of this page.'
1108        print '----'
1109        e = revertStandardError()
1110        print e
1111        print '----'
1112        raise
Note: See TracBrowser for help on using the repository browser.