#!/usr/local/bin/python import os,sys import re import string import copy import pickle import logging import time from threading import Thread, Lock from subprocess import Popen, call, PIPE, STDOUT from util import * from allocate_project import allocate_project_local, allocate_project_remote from access_project import access_project from fedid import fedid, generate_fedid from authorizer import authorizer from service_error import service_error from remote_service import xmlrpc_handler, soap_handler, service_caller import httplib import tempfile from urlparse import urlparse import topdl import list_log import proxy_emulab_segment import local_emulab_segment from access import access_base # Make log messages disappear if noone configures a fedd logger class nullHandler(logging.Handler): def emit(self, record): pass fl = logging.getLogger("fedd.access") fl.addHandler(nullHandler()) class access(access_base): @staticmethod def parse_vlans(v, log=None): """ Parse a vlan parameter into a set of vlan ids. Comma separated sequences of vlan ranges are acceptable. """ # the vlans can be a single integer, a comma separated list or a # comma separated lists of dashed ranges. E.g 100 or 100,300 or # 100,300-305,400 vset = set() if v: for v in [ x.strip() for x in v.split(",") ]: try: if v.count("-") == 1: f, t = v.split("-", 1) for i in range(int(f), int(t)+1): vset.add(i) else: vset.add(int(v)) except ValueError: if log: log.warn("Invalid expression in vlan list: %s" % v) return vset def __init__(self, config=None, auth=None): """ Initializer. Pulls parameters out of the ConfigParser's access section. """ access_base.__init__(self, config, auth) self.domain = config.get("access", "domain") vlan_str = config.get("access", "vlans") self.vlans = self.parse_vlans(vlan_str) self.attrs = { } self.access = { } # State is a dict of dicts indexed by segment fedid that includes the # owners of the segment as fedids (who can manipulate it, key: owners), # the repo dir/user for the allocation (key: user), Current allocation # log (key: log), and GRI of the reservation once made (key: gri) self.log = logging.getLogger("fedd.access") set_log_level(config, "access", self.log) if config.has_option("access", "accessdb"): self.read_access(config.get("access", "accessdb")) # Add the ownership attributes to the authorizer. Note that the # indices of the allocation dict are strings, but the attributes are # fedids, so there is a conversion. self.state_lock.acquire() for k in self.state.keys(): for o in self.state[k].get('owners', []): self.auth.set_attribute(o, fedid(hexstr=k)) self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k)) # If the allocation has a vlan assigned, remove it from the # available vlans v = self.state[k].get('vlan', None) if v: self.vlans.discard(v) self.state_lock.release() self.lookup_access = self.lookup_access_base self.call_GetValue= service_caller('GetValue') self.call_SetValue= service_caller('SetValue') self.soap_services = {\ 'RequestAccess': soap_handler("RequestAccess", self.RequestAccess), 'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess), 'StartSegment': soap_handler("StartSegment", self.StartSegment), 'TerminateSegment': soap_handler("TerminateSegment", self.TerminateSegment), } self.xmlrpc_services = {\ 'RequestAccess': xmlrpc_handler('RequestAccess', self.RequestAccess), 'ReleaseAccess': xmlrpc_handler('ReleaseAccess', self.ReleaseAccess), 'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment), 'TerminateSegment': xmlrpc_handler('TerminateSegment', self.TerminateSegment), } def RequestAccess(self, req, fid): """ Handle the access request. Proxy if not for us. Parse out the fields and make the allocations or rejections if for us, otherwise, assuming we're willing to proxy, proxy the request out. """ # The dance to get into the request body if req.has_key('RequestAccessRequestBody'): req = req['RequestAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") found, match = self.lookup_access(req, fid) # keep track of what's been added allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log) aid = unicode(allocID) self.state_lock.acquire() self.state[aid] = { } self.state[aid]['user'] = found self.state[aid]['owners'] = [ fid ] self.state[aid]['vlan'] = None self.write_state() self.state_lock.release() self.auth.set_attribute(fid, allocID) self.auth.set_attribute(allocID, allocID) try: f = open("%s/%s.pem" % (self.certdir, aid), "w") print >>f, alloc_cert f.close() except EnvironmentError, e: raise service_error(service_error.internal, "Can't open %s/%s : %s" % (self.certdir, aid, e)) return { 'allocID': { 'fedid': allocID } } def ReleaseAccess(self, req, fid): # The dance to get into the request body if req.has_key('ReleaseAccessRequestBody'): req = req['ReleaseAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") # Local request try: if req['allocID'].has_key('localname'): auth_attr = aid = req['allocID']['localname'] elif req['allocID'].has_key('fedid'): aid = unicode(req['allocID']['fedid']) auth_attr = req['allocID']['fedid'] else: raise service_error(service_error.req, "Only localnames and fedids are understood") except KeyError: raise service_error(service_error.req, "Badly formed request") self.log.debug("[access] deallocation requested for %s", aid) if not self.auth.check_attribute(fid, auth_attr): self.log.debug("[access] deallocation denied for %s", aid) raise service_error(service_error.access, "Access Denied") self.state_lock.acquire() if self.state.has_key(aid): self.log.debug("Found allocation for %s" %aid) del self.state[aid] self.write_state() self.state_lock.release() # And remove the access cert cf = "%s/%s.pem" % (self.certdir, aid) self.log.debug("Removing %s" % cf) os.remove(cf) return { 'allocID': req['allocID'] } else: self.state_lock.release() raise service_error(service_error.req, "No such allocation") def extract_parameters(self, top): """ DETER's internal networking currently supports a fixed capacity link between two endpoints. Those endpoints may specify a VPN (or list or range) to use. This extracts the and vpn preferences from the segments (as attributes) and the capacity of the connection as given by the substrate. The two endpoints VLAN choices are intersected to get set of VLANs that are acceptable (no VLAN requiremnets means any is acceptable). """ segments = filter(lambda x: isinstance(x, topdl.Segment), top.elements) if len(segments) != 2 or len(top.substrates) != 1: raise service_error(service_error.req, "Requests to DRAGON must have exactlty two segments " +\ "and one substrate") vlans = set() for s in segments: v = s.get_attribute('vlans') vlans &= self.parse_vlans(v) if len(vlans) == 0: vlans = None sub = top.substrates[0] if sub.capacity: cap = int(sub.capacity.rate / 1000.0) if cap < 1: cap = 1 else: cap = 100 return cap, vlans def export_store_info(self, cf, vlan, connInfo): """ For the export requests in the connection info, install the peer names at the experiment controller via SetValue calls. """ for c in connInfo: for p in [ p for p in c.get('parameter', []) \ if p.get('type', '') == 'output']: if p.get('name', '') != 'vlan_id': self.log.error("Unknown export parameter: %s" % \ p.get('name')) continue k = p.get('key', None) surl = p.get('store', None) if surl and k: value = "%s" % vlan req = { 'name': k, 'value': value } self.call_SetValue(surl, req, cf) else: self.log.error("Bad export request: %s" % p) def StartSegment(self, req, fid): err = None # Any service_error generated after tmpdir is created rv = None # Return value from segment creation try: req = req['StartSegmentRequestBody'] topref = req['segmentdescription']['topdldescription'] except KeyError: raise service_error(server_error.req, "Badly formed request") auth_attr = req['allocID']['fedid'] aid = "%s" % auth_attr attrs = req.get('fedAttr', []) if not self.auth.check_attribute(fid, auth_attr): raise service_error(service_error.access, "Access denied") else: # See if this is a replay of an earlier succeeded StartSegment - # sometimes SSL kills 'em. If so, replay the response rather than # redoing the allocation. self.state_lock.acquire() retval = self.state[aid].get('started', None) self.state_lock.release() if retval: self.log.warning("Duplicate StartSegment for %s: " % aid + \ "replaying response") return retval certfile = "%s/%s.pem" % (self.certdir, aid) if topref: topo = topdl.Topology(**topref) else: raise service_error(service_error.req, "Request missing segmentdescription'") connInfo = req.get('connection', []) cap, vlans = self.extract_parameters(topo) # No vlans passes in, consider any vlan acceptable if not vlans: vlans = self.vlans avail = self.vlans | vlans if len(avail) != 0: vlan_no = avail.pop() self.vlans.discard(vlan_no) else: raise service_error(service_error.federant, "No vlan available") self.export_store_info(certfile, vlan_no, connInfo) # This annotation isn't strictly necessary, but may help in debugging rtopo = topo.clone() for s in rtopo.substrates: s.set_attribute('vlan', vlan_no) # Grab the log (this is some anal locking, but better safe than # sorry) self.state_lock.acquire() self.state[aid]['vlan'] = vlan_no logv = "Allocated vlan: %d" % vlan_no # It's possible that the StartSegment call gets retried (!). # if the 'started' key is in the allocation, we'll return it rather # than redo the setup. self.state[aid]['started'] = { 'allocID': req['allocID'], 'allocationLog': logv, 'segmentdescription': { 'topdldescription': rtopo.to_dict() } } retval = copy.deepcopy(self.state[aid]['started']) self.write_state() self.state_lock.release() return retval def TerminateSegment(self, req, fid): try: req = req['TerminateSegmentRequestBody'] except KeyError: raise service_error(server_error.req, "Badly formed request") auth_attr = req['allocID']['fedid'] aid = "%s" % auth_attr self.log.debug("Terminate request for %s" %aid) if not self.auth.check_attribute(fid, auth_attr): raise service_error(service_error.access, "Access denied") self.state_lock.acquire() if self.state.has_key(aid): vlan_no = self.state[aid].get('vlan', None) else: vlan_no = None self.state_lock.release() self.log.debug("Stop segment for vlan: %s" % vlan_no) if not vlan_no: raise service_error(service_error.internal, "Can't find assigfned vlan for for %s" % aid) self.vlans.add(vlan_no) self.state_lock.acquire() self.state[aid]['vlan'] = None self.state_lock.release() return { 'allocID': req['allocID'] }