class Configuration (object): def __init__ (self, configurations, text=False): self.api_encoder = environment.settings().api.encoder self.fifo = environment.settings().api.file self.logger = Logger() self._configurations = configurations self.error = Error() self.tokens = Tokeniser(self.error,self.logger) self.neighbor = ParseNeighbor(self.error) self.family = ParseFamily(self.error) self.route = ParseRoute(self.error) self.flow = ParseFlow(self.error,self.logger) self.l2vpn = ParseL2VPN(self.error) self.process = ParseProcess(self.error) self._dispatch_neighbor = { 'description': self.neighbor.description, 'router-id': self.neighbor.router_id, 'host-name': self.neighbor.hostname, 'domain-name': self.neighbor.domainname, 'local-address': self.neighbor.ip, 'local-as': self.neighbor.asn, 'peer-as': self.neighbor.asn, 'passive': self.neighbor.passive, 'listen': self.neighbor.listen, 'hold-time': self.neighbor.holdtime, 'md5': self.neighbor.md5, 'ttl-security': self.neighbor.ttl, 'group-updates': self.neighbor.groupupdate, 'adj-rib-out': self.neighbor.adjribout, 'auto-flush': self.neighbor.autoflush, } self._dispatch_family = { 'ipv4': self.family.ipv4, 'ipv6': self.family.ipv6, 'l2vpn': self.family.l2vpn, 'minimal': self.family.minimal, 'all': self.family.all, } self._dispatch_capability = { # deprecated 'route-refresh': self.neighbor.capability.refresh, 'graceful-restart': self.neighbor.capability.gracefulrestart, 'multi-session': self.neighbor.capability.multisession, 'add-path': self.neighbor.capability.addpath, 'aigp': self.neighbor.capability.aigp, 'operational': self.neighbor.capability.operational, 'add-path': self.neighbor.capability.addpath, 'asn4': self.neighbor.capability.asn4, } self._dispatch_route = { 'origin': self.route.origin, 'as-path': self.route.aspath, # For legacy with version 2.0.x 'as-sequence': self.route.aspath, 'med': self.route.med, 'aigp': self.route.aigp, 'next-hop': self.route.next_hop, 'local-preference': self.route.local_preference, 'atomic-aggregate': self.route.atomic_aggregate, 'aggregator': self.route.aggregator, 'path-information': self.route.path_information, 'originator-id': self.route.originator_id, 'cluster-list': self.route.cluster_list, 'split': self.route.split, 'label': self.route.label, 'rd': self.route.rd, 'route-distinguisher': self.route.rd, 'watchdog': self.route.watchdog, # withdrawn is here to not break legacy code 'withdraw': self.route.withdraw, 'withdrawn': self.route.withdraw, 'name': self.route.name, 'community': self.route.community, 'extended-community': self.route.extended_community, 'attribute': self.route.generic_attribute, } self._dispatch_flow = { 'rd': self.route.rd, 'route-distinguisher': self.route.rd, 'next-hop': self.flow.next_hop, 'source': self.flow.source, 'source-ipv4': self.flow.source, 'destination': self.flow.destination, 'destination-ipv4': self.flow.destination, 'port': self.flow.anyport, 'source-port': self.flow.source_port, 'destination-port': self.flow.destination_port, 'protocol': self.flow.protocol, 'next-header': self.flow.next_header, 'tcp-flags': self.flow.tcp_flags, 'icmp-type': self.flow.icmp_type, 'icmp-code': self.flow.icmp_code, 'fragment': self.flow.fragment, 'dscp': self.flow.dscp, 'traffic-class': self.flow.traffic_class, 'packet-length': self.flow.packet_length, 'flow-label': self.flow.flow_label, 'accept': self.flow.accept, 'discard': self.flow.discard, 'rate-limit': self.flow.rate_limit, 'redirect': self.flow.redirect, 'redirect-to-nexthop': self.flow.redirect_next_hop, 'copy': self.flow.copy, 'mark': self.flow.mark, 'action': self.flow.action, 'community': self.route.community, 'extended-community': self.route.extended_community, } self._dispatch_vpls = { 'endpoint': self.l2vpn.vpls_endpoint, 'offset': self.l2vpn.vpls_offset, 'size': self.l2vpn.vpls_size, 'base': self.l2vpn.vpls_base, 'origin': self.route.origin, 'as-path': self.route.aspath, 'med': self.route.med, 'next-hop': self.route.next_hop, 'local-preference': self.route.local_preference, 'originator-id': self.route.originator_id, 'cluster-list': self.route.cluster_list, 'rd': self.route.rd, 'route-distinguisher': self.route.rd, 'withdraw': self.route.withdraw, 'withdrawn': self.route.withdraw, 'name': self.route.name, 'community': self.route.community, 'extended-community': self.route.extended_community, } self._clear() def _clear (self): self.processes = {} self.neighbors = {} self._neighbors = {} self.error.clear() self._scope = [] self._location = ['root'] self.tokens.clear() self.error.clear() self.neighbor.clear() self.family.clear() self.route.clear() self.flow.clear() self.l2vpn.clear() self.process.clear() # Public Interface def reload (self): try: return self._reload() except KeyboardInterrupt: return self.error.set('configuration reload aborted by ^C or SIGINT') except Exception: # unhandled configuration parsing issue raise def _reload (self): # taking the first configuration available (FIFO buffer) fname = self._configurations.pop(0) self.process.configuration(fname) self._configurations.append(fname) # storing the routes associated with each peer so we can find what changed backup_changes = {} for neighbor in self._neighbors: backup_changes[neighbor] = self._neighbors[neighbor].changes # clearing the current configuration to be able to re-parse it self._clear() if not self.tokens.set_file(fname): return False # parsing the configurtion r = False while not self.tokens.finished: r = self._dispatch( self._scope,'configuration', ['group','neighbor'], [] ) if r is False: break # handling possible parsing errors if r not in [True,None]: return self.error.set("\nsyntax error in section %s\nline %d: %s\n\n%s" % (self._location[-1],self.tokens.number,' '.join(self.tokens.line),self.error)) # parsing was sucessful, assigning the result self.neighbors = self._neighbors # installing in the neighbor what was its previous routes so we can # add/withdraw what need to be for neighbor in self.neighbors: self.neighbors[neighbor].backup_changes = backup_changes.get(neighbor,[]) # we are not really running the program, just want to .... if environment.settings().debug.route: from exabgp.configuration.check import check_message if check_message(self.neighbors,environment.settings().debug.route): sys.exit(0) sys.exit(1) # we are not really running the program, just want check the configuration validity if environment.settings().debug.selfcheck: from exabgp.configuration.check import check_neighbor if check_neighbor(self.neighbors): sys.exit(0) sys.exit(1) return True # XXX: FIXME: move this from here to the reactor (or whatever will manage command from user later) def change_to_peers (self, change, peers): result = True for neighbor in self.neighbors: if neighbor in peers: if change.nlri.family() in self.neighbors[neighbor].families(): self.neighbors[neighbor].rib.outgoing.insert_announced(change) else: self.logger.configuration('the route family is not configured on neighbor','error') result = False return result # XXX: FIXME: move this from here to the reactor (or whatever will manage command from user later) def eor_to_peers (self, family, peers): result = False for neighbor in self.neighbors: if neighbor in peers: result = True self.neighbors[neighbor].eor.append(family) return result # XXX: FIXME: move this from here to the reactor (or whatever will manage command from user later) def operational_to_peers (self, operational, peers): result = True for neighbor in self.neighbors: if neighbor in peers: if operational.family() in self.neighbors[neighbor].families(): if operational.name == 'ASM': self.neighbors[neighbor].asm[operational.family()] = operational self.neighbors[neighbor].messages.append(operational) else: self.logger.configuration('the route family is not configured on neighbor','error') result = False return result # XXX: FIXME: move this from here to the reactor (or whatever will manage command from user later) def refresh_to_peers (self, refresh, peers): result = True for neighbor in self.neighbors: if neighbor in peers: family = (refresh.afi,refresh.safi) if family in self.neighbors[neighbor].families(): self.neighbors[neighbor].refresh.append(refresh.__class__(refresh.afi,refresh.safi)) else: result = False return result # Tokenisation def number (self): return self._number # Flow control ...................... # name is not used yet but will come really handy if we have name collision :D def _dispatch (self, scope, name, multi, single, location=None): if location: self._location = location self.flow.clear() try: tokens = self.tokens.next() except IndexError: return self.error.set('configuration file incomplete (most likely missing })') self.logger.configuration("parsing | %-13s | '%s'" % (name,"' '".join(tokens))) end = tokens[-1] if multi and end == '{': self._location.append(tokens[0]) return self._multi_line(scope,name,tokens[:-1],multi) if single and end == ';': return self._single_line(scope,name,tokens[:-1],single) if end == '}': if len(self._location) == 1: return self.error.set('closing too many parenthesis') self._location.pop(-1) return None return False def _multi_line (self, scope, name, tokens, valid): command = tokens[0] if valid and command not in valid: return self.error.set('option %s in not valid here' % command) if name == 'configuration': if command == 'neighbor': if self._multi_neighbor(scope,tokens[1:]): return self._make_neighbor(scope) return False if command == 'group': if len(tokens) != 2: return self.error.set('syntax: group <name> { <options> }') return self._multi_group(scope,tokens[1]) if name == 'group': if command == 'neighbor': if self._multi_neighbor(scope,tokens[1:]): return self._make_neighbor(scope) return False if command == 'static': return self._multi_static(scope,tokens[1:]) if command == 'flow': return self._multi_flow(scope,tokens[1:]) if command == 'l2vpn': return self._multi_l2vpn(scope,tokens[1:]) if command == 'process': return self._multi_process(scope,tokens[1:]) if command == 'family': return self._multi_family(scope,tokens[1:]) if command == 'capability': return self._multi_capability(scope,tokens[1:]) if command == 'operational': return self._multi_operational(scope,tokens[1:]) if name == 'neighbor': if command == 'static': return self._multi_static(scope,tokens[1:]) if command == 'flow': return self._multi_flow(scope,tokens[1:]) if command == 'l2vpn': return self._multi_l2vpn(scope,tokens[1:]) if command == 'process': return self._multi_process(scope,tokens[1:]) if command == 'family': return self._multi_family(scope,tokens[1:]) if command == 'capability': return self._multi_capability(scope,tokens[1:]) if command == 'operational': return self._multi_operational(scope,tokens[1:]) if name == 'static': if command == 'route': if self._multi_static_route(scope,tokens[1:]): return self.route.check_static_route(scope) return False if name == 'flow': if command == 'route': if self._multi_flow_route(scope,tokens[1:]): return self._check_flow_route(scope) return False if name == 'l2vpn': if command == 'vpls': if self._multi_l2vpn_vpls(scope,tokens[1:]): return self._check_l2vpn_vpls(scope) return False if name == 'flow-route': if command == 'match': if self._multi_match(scope,tokens[1:]): return True return False if command == 'then': if self._multi_then(scope,tokens[1:]): return True return False if name == 'process': if command in ['send','receive']: if self._multi_api(scope,command,tokens[1:]): return True return False return False def _single_line (self, scope, name, tokens, valid): command = tokens[0] if valid and command not in valid: return self.error.set('invalid keyword "%s"' % command) elif name == 'route': if command in self._dispatch_route: if command in ('rd','route-distinguisher'): return self._dispatch_route[command](scope,tokens[1:],SAFI.mpls_vpn) else: return self._dispatch_route[command](scope,tokens[1:]) elif name == 'l2vpn': if command in self._dispatch_vpls: if command in ('rd','route-distinguisher'): return self._dispatch_vpls[command](scope,tokens[1:],SAFI.vpls) else: return self._dispatch_vpls[command](scope,tokens[1:]) elif name == 'flow-route': if command in self._dispatch_flow: if command in ('rd','route-distinguisher'): return self._dispatch_flow[command](scope,tokens[1:],SAFI.flow_vpn) else: return self._dispatch_flow[command](scope,tokens[1:]) elif name == 'flow-match': if command in self._dispatch_flow: return self._dispatch_flow[command](scope,tokens[1:]) elif name == 'flow-then': if command in self._dispatch_flow: return self._dispatch_flow[command](scope,tokens[1:]) if name in ('neighbor','group'): if command in self._dispatch_neighbor: return self._dispatch_neighbor[command](scope,command,tokens[1:]) elif name == 'family': if command in self._dispatch_family: return self._dispatch_family[command](scope,tokens[1:]) elif name == 'capability': if command in self._dispatch_capability: return self._dispatch_capability[command](scope,command,tokens[1:]) elif name == 'process': if command == 'run': return self.process.run(scope,'process-run',tokens[1:]) if command == 'encoder': return self.process.encoder(scope,'encoder',tokens[1:]) if command == 'neighbor-changes': return self.process.command(scope,'neighbor-changes',tokens[1:]) elif name in ['send','receive']: # process / send if command in ['packets','parsed','consolidate']: return self.process.command(scope,'%s-%s' % (name,command),tokens[1:]) for message in Message.CODE.MESSAGES: if command == message.SHORT: return self.process.command(scope,'%s-%d' % (name,message),tokens[1:]) elif name == 'static': if command == 'route': return self._single_static_route(scope,tokens[1:]) elif name == 'l2vpn': if command == 'vpls': return self._single_l2vpn_vpls(scope,tokens[1:]) elif name == 'operational': if command == 'asm': return self._single_operational_asm(scope,tokens[1]) # it does not make sense to have adm return False # Programs used to control exabgp def _multi_process (self, scope, tokens): while True: r = self._dispatch( scope,'process', ['send','receive'], [ 'run','encoder', 'neighbor-changes', ] ) if r is False: return False if r is None: break name = tokens[0] if len(tokens) >= 1 else 'conf-only-%s' % str(time.time())[-6:] self.processes.setdefault(name,{})['neighbor'] = scope[-1]['peer-address'] if 'peer-address' in scope[-1] else '*' for key in ['neighbor-changes',]: self.processes[name][key] = scope[-1].pop(key,False) for direction in ['send','receive']: for action in ['packets','parsed','consolidate']: key = '%s-%s' % (direction,action) self.processes[name][key] = scope[-1].pop(key,False) for message in Message.CODE.MESSAGES: key = '%s-%d' % (direction,message) self.processes[name][key] = scope[-1].pop(key,False) run = scope[-1].pop('process-run','') if run: if len(tokens) != 1: return self.error.set(self._str_process_error) self.processes[name]['encoder'] = scope[-1].get('encoder','') or self.api_encoder self.processes[name]['run'] = run return True elif len(tokens): return self.error.set(self._str_process_error) # Limit the AFI/SAFI pair announced to peers def _multi_family (self, scope, tokens): # we know all the families we should use scope[-1]['families'] = [] while True: r = self._dispatch( scope,'family', [], self._dispatch_family.keys() ) if r is False: return False if r is None: break self.family.clear() return True # capacity def _multi_capability (self, scope, tokens): # we know all the families we should use while True: r = self._dispatch( scope,'capability', [], self._dispatch_capability.keys() ) if r is False: return False if r is None: break return True # route grouping with watchdog # Group Neighbor def _multi_group (self, scope, address): scope.append({}) while True: r = self._dispatch( scope,'group', [ 'static','flow','l2vpn', 'neighbor','process','family', 'capability','operational' ], self._dispatch_neighbor.keys() ) if r is False: return False if r is None: scope.pop(-1) return True def _make_neighbor (self, scope): # we have local_scope[-2] as the group template and local_scope[-1] as the peer specific if len(scope) > 1: for key,content in scope[-2].iteritems(): if key not in scope[-1]: scope[-1][key] = deepcopy(content) elif key == 'announce': scope[-1][key].extend(scope[-2][key]) neighbor = Neighbor() for local_scope in scope: value = local_scope.get('router-id','') if value: neighbor.router_id = value value = local_scope.get('peer-address','') if value: neighbor.peer_address = value value = local_scope.get('local-address','') if value: neighbor.local_address = value value = local_scope.get('local-as','') if value: neighbor.local_as = value value = local_scope.get('peer-as','') if value: neighbor.peer_as = value value = local_scope.get('passive',False) if value: neighbor.passive = value value = local_scope.get('listen',0) if value: neighbor.listen = value value = local_scope.get('hold-time','') if value: neighbor.hold_time = value neighbor.host_name = local_scope.get('host-name',hostname()) neighbor.domain_name = local_scope.get('domain-name',domainname()) neighbor.changes = local_scope.get('announce',[]) messages = local_scope.get('operational-message',[]) # we want to have a socket for the cli if self.fifo: _cli_name = 'CLI' self.processes[_cli_name] = { 'neighbor': '*', 'encoder': 'json', 'run': [sys.executable, sys.argv[0]], 'neighbor-changes': False, 'receive-consolidate': False, 'receive-packets': False, 'receive-parsed': False, 'send-consolidate': False, 'send-packets': False, 'send-parsed': False, } for direction in ['send','receive']: for message in [ Message.CODE.NOTIFICATION, Message.CODE.OPEN, Message.CODE.KEEPALIVE, Message.CODE.UPDATE, Message.CODE.ROUTE_REFRESH, Message.CODE.OPERATIONAL ]: self.processes[_cli_name]['%s-%d' % (direction,message)] = False for name in self.processes.keys(): process = self.processes[name] neighbor.api.set('neighbor-changes',process.get('neighbor-changes',False)) for direction in ['send','receive']: for option in ['packets','consolidate','parsed']: neighbor.api.set_value(direction,option,process.get('%s-%s' % (direction,option),False)) for message in [ Message.CODE.NOTIFICATION, Message.CODE.OPEN, Message.CODE.KEEPALIVE, Message.CODE.UPDATE, Message.CODE.ROUTE_REFRESH, Message.CODE.OPERATIONAL ]: neighbor.api.set_message(direction,message,process.get('%s-%d' % (direction,message),False)) if not neighbor.router_id: neighbor.router_id = neighbor.local_address local_scope = scope[-1] neighbor.description = local_scope.get('description','') neighbor.md5 = local_scope.get('md5',None) neighbor.ttl = local_scope.get('ttl-security',None) neighbor.group_updates = local_scope.get('group-updates',None) neighbor.route_refresh = local_scope.get('route-refresh',0) neighbor.graceful_restart = local_scope.get('graceful-restart',0) if neighbor.graceful_restart is None: # README: Should it be a subclass of int ? neighbor.graceful_restart = int(neighbor.hold_time) neighbor.multisession = local_scope.get('multi-session',False) neighbor.operational = local_scope.get('capa-operational',False) neighbor.add_path = local_scope.get('add-path',0) neighbor.flush = local_scope.get('auto-flush',True) neighbor.adjribout = local_scope.get('adj-rib-out',True) neighbor.asn4 = local_scope.get('asn4',True) neighbor.aigp = local_scope.get('aigp',None) if neighbor.route_refresh and not neighbor.adjribout: return self.error.set('incomplete option route-refresh and no adj-rib-out') # XXX: check that if we have any message, we have parsed/packets # XXX: and vice-versa missing = neighbor.missing() if missing: return self.error.set('incomplete neighbor, missing %s' % missing) if neighbor.local_address.afi != neighbor.peer_address.afi: return self.error.set('local-address and peer-address must be of the same family') if neighbor.peer_address.ip in self._neighbors: return self.error.set('duplicate peer definition %s' % neighbor.peer_address.ip) openfamilies = local_scope.get('families','everything') # announce every family we known if neighbor.multisession and openfamilies == 'everything': # announce what is needed, and no more, no need to have lots of TCP session doing nothing _families = set() for change in neighbor.changes: _families.add((change.nlri.afi,change.nlri.safi)) families = list(_families) elif openfamilies in ('all','everything'): families = NLRI.known_families() # only announce what you have as routes elif openfamilies == 'minimal': _families = set() for change in neighbor.changes: _families.add((change.nlri.afi,change.nlri.safi)) families = list(_families) else: families = openfamilies # check we are not trying to announce routes without the right MP announcement for family in neighbor.families(): if family not in families: afi,safi = family return self.error.set('Trying to announce a route of type %s,%s when we are not announcing the family to our peer' % (afi,safi)) # add the families to the list of families known initial_families = list(neighbor.families()) for family in families: if family not in initial_families : # we are modifying the data used by .families() here neighbor.add_family(family) if neighbor.group_updates is None: neighbor.group_updates = True def _init_neighbor (neighbor): families = neighbor.families() for change in neighbor.changes: if change.nlri.family() in families: # This add the family to neighbor.families() neighbor.rib.outgoing.insert_announced_watchdog(change) for message in messages: if message.family() in families: if message.name == 'ASM': neighbor.asm[message.family()] = message else: neighbor.messages.append(message) self._neighbors[neighbor.name()] = neighbor # create one neighbor object per family for multisession if neighbor.multisession and len(neighbor.families()) > 1: for family in neighbor.families(): # XXX: FIXME: Ok, it works but it takes LOTS of memory .. m_neighbor = deepcopy(neighbor) m_neighbor.make_rib() m_neighbor.rib.outgoing.families = [family] _init_neighbor(m_neighbor) else: neighbor.make_rib() _init_neighbor(neighbor) # display configuration for line in str(neighbor).split('\n'): self.logger.configuration(line) self.logger.configuration("\n") # ... scope.pop(-1) return True def _multi_neighbor (self, scope, tokens): if len(tokens) != 1: return self.error.set('syntax: neighbor <ip> { <options> }') address = tokens[0] scope.append({}) try: scope[-1]['peer-address'] = IP.create(address) except (IndexError,ValueError,socket.error): return self.error.set('"%s" is not a valid IP address' % address) while True: r = self._dispatch( scope,'neighbor', [ 'static','flow','l2vpn', 'process','family','capability','operational' ], [ 'description','router-id','local-address','local-as','peer-as', 'host-name','domain-name', 'passive','listen','hold-time','add-path','graceful-restart','md5', 'ttl-security','multi-session','group-updates','asn4','aigp', 'auto-flush','adj-rib-out' ] ) # XXX: THIS SHOULD ALLOW CAPABILITY AND NOT THE INDIVIDUAL SUB KEYS if r is False: return False if r is None: return True # Group Static ................ def _multi_static (self, scope, tokens): if len(tokens) != 0: return self.error.set('syntax: static { route; route; ... }') while True: r = self._dispatch( scope,'static', ['route',], ['route',] ) if r is False: return False if r is None: return True # Group Route ........ def _split_last_route (self, scope): # if the route does not need to be broken in smaller routes, return change = scope[-1]['announce'][-1] if Attribute.CODE.INTERNAL_SPLIT not in change.attributes: return True # ignore if the request is for an aggregate, or the same size mask = change.nlri.mask split = change.attributes[Attribute.CODE.INTERNAL_SPLIT] if mask >= split: return True # get a local copy of the route change = scope[-1]['announce'].pop(-1) # calculate the number of IP in the /<size> of the new route increment = pow(2,(len(change.nlri.packed)*8) - split) # how many new routes are we going to create from the initial one number = pow(2,split - change.nlri.mask) # convert the IP into a integer/long ip = 0 for c in change.nlri.packed: ip <<= 8 ip += ord(c) afi = change.nlri.afi safi = change.nlri.safi # Really ugly klass = change.nlri.__class__ if klass is INET: path_info = change.nlri.path_info elif klass is MPLS: path_info = None labels = change.nlri.labels rd = change.nlri.rd # packed and not pack() but does not matter atm, it is an IP not a NextHop nexthop = change.nlri.nexthop.packed change.nlri.mask = split change.nlri = None # generate the new routes for _ in range(number): # update ip to the next route, this recalculate the "ip" field of the Inet class nlri = klass(afi,safi,pack_int(afi,ip,split),split,nexthop,OUT.ANNOUNCE,path_info) if klass is MPLS: nlri.labels = labels nlri.rd = rd # next ip ip += increment # save route scope[-1]['announce'].append(Change(nlri,change.attributes)) return True def _multi_static_route (self, scope, tokens): if len(tokens) != 1: return self.error.set(self.route.syntax) if not self.route.insert_static_route(scope,tokens): return False while True: r = self._dispatch( scope,'route', [], [ 'next-hop','origin','as-path','as-sequence','med','aigp', 'local-preference','atomic-aggregate','aggregator', 'path-information','community','originator-id','cluster-list', 'extended-community','split','label','rd','route-distinguisher', 'watchdog','withdraw','attribute' ] ) if r is False: return False if r is None: return self._split_last_route(scope) def _single_static_route (self, scope, tokens): if len(tokens) < 3: return False if not self.route.insert_static_route(scope,tokens): return False while len(tokens): command = tokens.pop(0) if command in ('withdraw','withdrawn'): if self.route.withdraw(scope,tokens): continue return False if len(tokens) < 1: return False if command in self._dispatch_route: if command in ('rd','route-distinguisher'): if self._dispatch_route[command](scope,tokens,SAFI.nlri_mpls): continue else: if self._dispatch_route[command](scope,tokens): continue else: return False return False if not self.route.check_static_route(scope): return False return self._split_last_route(scope) def _single_l2vpn_vpls (self, scope, tokens): # TODO: actual length?(like rd+lb+bo+ve+bs+rd; 14 or so) if len(tokens) < 10: return False if not self._insert_l2vpn_vpls(scope,tokens): return False while len(tokens): command = tokens.pop(0) if len(tokens) < 1: return False if command in self._dispatch_vpls: if command in ('rd','route-distinguisher'): if self._dispatch_vpls[command](scope,tokens,SAFI.vpls): continue else: if self._dispatch_vpls[command](scope,tokens): continue else: return False return False if not self._check_l2vpn_vpls(scope): return False return True # VPLS def _multi_l2vpn (self, scope, tokens): if len(tokens) != 0: return self.error.set(self.l2vpn.syntax) while True: r = self._dispatch( scope,'l2vpn', ['vpls',], ['vpls',] ) if r is False: return False if r is None: break return True def _insert_l2vpn_vpls (self, scope, tokens=None): try: attributes = Attributes() change = Change(VPLS(None,None,None,None,None),attributes) except ValueError: return self.error.set(self.l2vpn.syntax) if 'announce' not in scope[-1]: scope[-1]['announce'] = [] scope[-1]['announce'].append(change) return True def _multi_l2vpn_vpls (self, scope, tokens): if len(tokens) > 1: return self.error.set(self.l2vpn.syntax) if not self._insert_l2vpn_vpls(scope): return False while True: r = self._dispatch( scope,'l2vpn', [], [ 'next-hop','origin','as-path','med','local-preference', 'community','originator-id','cluster-list','extended-community', 'rd','route-distinguisher','withdraw', 'endpoint','offset', 'size','base' ] ) if r is False: return False if r is None: break return True # Group Flow ........ def _multi_flow (self, scope, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) while True: r = self._dispatch( scope,'flow', ['route',], [] ) if r is False: return False if r is None: break return True def _insert_flow_route (self, scope, tokens=None): if self.flow.state != 'out': return self.error.set(self.flow.syntax) self.flow.state = 'match' try: attributes = Attributes() attributes[Attribute.CODE.EXTENDED_COMMUNITY] = ExtendedCommunities() flow = Change(Flow(),attributes) except ValueError: return self.error.set(self.flow.syntax) if 'announce' not in scope[-1]: scope[-1]['announce'] = [] scope[-1]['announce'].append(flow) return True def _check_flow_route (self, scope): self.logger.configuration('warning: no check on flows are implemented') return True def _check_l2vpn_vpls (self, scope): nlri = scope[-1]['announce'][-1].nlri if nlri.ve is None: raise ValueError(self._str_vpls_bad_enpoint) if nlri.base is None: raise ValueError(self._str_vpls_bad_label) if nlri.offset is None: raise ValueError(self._str_vpls_bad_offset) if nlri.size is None: raise ValueError(self._str_vpls_bad_size) if nlri.base > (0xFFFFF - nlri.size): # 20 bits, 3 bytes raise ValueError(self._str_vpls_bad_label) return True def _multi_flow_route (self, scope, tokens): if len(tokens) > 1: return self.error.set(self.flow.syntax) if not self._insert_flow_route(scope): return False while True: r = self._dispatch( scope,'flow-route', ['match','then'], ['rd','route-distinguisher','next-hop'] ) if r is False: return False if r is None: break if self.flow.state != 'out': return self.error.set(self.flow.syntax) return True # .......................................... def _multi_match (self, scope, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) if self.flow.state != 'match': return self.error.set(self.flow.syntax) self.flow.state = 'then' while True: r = self._dispatch( scope,'flow-match', [], [ 'source','destination', 'source-ipv4','destination-ipv4', 'port','source-port','destination-port', 'protocol','next-header','tcp-flags','icmp-type','icmp-code', 'fragment','dscp','traffic-class','packet-length','flow-label' ] ) if r is False: return False if r is None: break return True def _multi_then (self, scope, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) if self.flow.state != 'then': return self.error.set(self.flow.syntax) self.flow.state = 'out' while True: r = self._dispatch( scope,'flow-then', [], [ 'accept','discard','rate-limit', 'redirect','copy','redirect-to-nexthop', 'mark','action', 'community','extended-community' ] ) if r is False: return False if r is None: break return True # .......................................... def _multi_api (self, scope, direction, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) while True: r = self._dispatch( scope,direction, [], [ 'packets','parsed','consolidate', 'notification','open','keepalive', 'update','refresh','operational' ] ) if r is False: return False if r is None: break return True # Group Operational ................ def _multi_operational (self, scope, tokens): if len(tokens) != 0: return self.error.set('syntax: operational { command; command; ... }') while True: r = self._dispatch( scope,'operational', [], ['asm',] ) if r is False: return False if r is None: return True def _single_operational_asm (self, scope, value): return self._single_operational(Advisory.ASM,scope,['afi','safi','advisory'],value) def _single_operational (self, klass, scope, parameters, value): def utf8 (string): return string.encode('utf-8')[1:-1] convert = { 'afi': AFI.value, 'safi': SAFI.value, 'sequence': int, 'counter': long, 'advisory': utf8 } def valid (_): return True def u32 (_): return int(_) <= 0xFFFFFFFF def u64 (_): return long(_) <= 0xFFFFFFFFFFFFFFFF def advisory (_): return len(_.encode('utf-8')) <= MAX_ADVISORY + 2 # the two quotes validate = { 'afi': AFI.value, 'safi': SAFI.value, 'sequence': u32, 'counter': u64, } number = len(parameters)*2 tokens = formated(value).split(' ',number-1) if len(tokens) != number: return self.error.set('invalid operational syntax, wrong number of arguments') return False data = {} while tokens and parameters: command = tokens.pop(0).lower() value = tokens.pop(0) if command == 'router-id': if isipv4(value): data['routerid'] = RouterID(value) else: return self.error.set('invalid operational value for %s' % command) return False continue expected = parameters.pop(0) if command != expected: return self.error.set('invalid operational syntax, unknown argument %s' % command) return False if not validate.get(command,valid)(value): return self.error.set('invalid operational value for %s' % command) return False data[command] = convert[command](value) if tokens or parameters: return self.error.set('invalid advisory syntax, missing argument(s) %s' % ', '.join(parameters)) return False if 'routerid' not in data: data['routerid'] = None if 'operational-message' not in scope[-1]: scope[-1]['operational-message'] = [] # iterate on each family for the peer if multiprotocol is set. scope[-1]['operationa-messagel'].append(klass(**data)) return True
class Configuration (object): def __init__ (self, configurations, text=False): self.api_encoder = environment.settings().api.encoder self.logger = Logger() self._configurations = configurations self.error = Error() self.tokens = Tokeniser(self.error,self.logger) self.neighbor = ParseNeighbor(self.error,self.logger) self.family = ParseFamily(self.error) self.process = ParseProcess(self.error) self.route = ParseRoute(self.error) self.flow = ParseFlow(self.error,self.logger) self.l2vpn = ParseL2VPN(self.error) self.operational = ParseOperational(self.error) self._tree = { 'configuration': { 'neighbor': (self._multi_neighbor,self.neighbor.make), 'group': (self._multi_group,true), }, 'group': { 'neighbor': (self._multi_neighbor,self.neighbor.make), 'static': (self._multi_static,true), 'flow': (self._multi_flow,true), 'l2vpn': (self._multi_l2vpn,true), 'process': (self._multi_process,true), 'family': (self._multi_family,true), 'capability': (self._multi_capability,true), 'operational': (self._multi_operational,true), }, 'neighbor': { 'static': (self._multi_static,true), 'flow': (self._multi_flow,true), 'l2vpn': (self._multi_l2vpn,true), 'process': (self._multi_process,true), 'family': (self._multi_family,true), 'capability': (self._multi_capability,true), 'operational': (self._multi_operational,true), }, 'static': { 'route': (self._multi_static_route,self.route.check_static_route), }, 'flow': { 'route': (self._multi_flow_route,self.flow.check_flow), }, 'l2vpn': { 'vpls': (self._multi_l2vpn_vpls,self.l2vpn.check_vpls), }, 'flow-route': { 'match': (self._multi_match,true), 'then': (self._multi_then,true), }, 'process': { 'send': (self._multi_api,true), 'receive': (self._multi_api,true), } } self._command = { 'group': { 'description': self.neighbor.description, 'router-id': self.neighbor.router_id, 'host-name': self.neighbor.hostname, 'domain-name': self.neighbor.domainname, 'local-address': self.neighbor.ip, 'local-as': self.neighbor.asn, 'peer-as': self.neighbor.asn, 'passive': self.neighbor.passive, 'listen': self.neighbor.listen, 'hold-time': self.neighbor.holdtime, 'md5': self.neighbor.md5, 'ttl-security': self.neighbor.ttl, 'group-updates': self.neighbor.groupupdate, 'adj-rib-out': self.neighbor.adjribout, 'auto-flush': self.neighbor.autoflush, }, 'neighbor': { 'description': self.neighbor.description, 'router-id': self.neighbor.router_id, 'host-name': self.neighbor.hostname, 'domain-name': self.neighbor.domainname, 'local-address': self.neighbor.ip, 'local-as': self.neighbor.asn, 'peer-as': self.neighbor.asn, 'passive': self.neighbor.passive, 'listen': self.neighbor.listen, 'hold-time': self.neighbor.holdtime, 'md5': self.neighbor.md5, 'ttl-security': self.neighbor.ttl, 'group-updates': self.neighbor.groupupdate, 'adj-rib-out': self.neighbor.adjribout, 'auto-flush': self.neighbor.autoflush, }, 'capability': { 'route-refresh': self.neighbor.capability.refresh, 'graceful-restart': self.neighbor.capability.gracefulrestart, 'multi-session': self.neighbor.capability.multisession, 'add-path': self.neighbor.capability.addpath, 'aigp': self.neighbor.capability.aigp, 'operational': self.neighbor.capability.operational, 'add-path': self.neighbor.capability.addpath, 'asn4': self.neighbor.capability.asn4, }, 'process': { 'run': self.process.run, 'encoder': self.process.encoder, 'neighbor-changes': self.process.command, }, 'family': { 'ipv4': self.family.ipv4, 'ipv6': self.family.ipv6, 'l2vpn': self.family.l2vpn, 'minimal': self.family.minimal, 'all': self.family.all, }, 'static': { 'route': self.route.static, }, 'l2vpn': { 'vpls': self.l2vpn.vpls, }, 'operational': { 'asm': self.operational.asm, # it makes no sense to have adm or others }, 'static-route': self.route.command, # 'inet-route': { # 'mpls-route': { 'l2vpn-vpls': self.l2vpn.command, 'flow-route': { 'rd': self.route.rd, 'route-distinguisher': self.route.rd, 'next-hop': self.flow.next_hop, }, 'flow-match': { 'source': self.flow.source, 'source-ipv4': self.flow.source, 'destination': self.flow.destination, 'destination-ipv4': self.flow.destination, 'port': self.flow.anyport, 'source-port': self.flow.source_port, 'destination-port': self.flow.destination_port, 'protocol': self.flow.protocol, 'next-header': self.flow.next_header, 'tcp-flags': self.flow.tcp_flags, 'icmp-type': self.flow.icmp_type, 'icmp-code': self.flow.icmp_code, 'fragment': self.flow.fragment, 'dscp': self.flow.dscp, 'traffic-class': self.flow.traffic_class, 'packet-length': self.flow.packet_length, 'flow-label': self.flow.flow_label, }, 'flow-then': { 'accept': self.flow.accept, 'discard': self.flow.discard, 'rate-limit': self.flow.rate_limit, 'redirect': self.flow.redirect, 'redirect-to-nexthop': self.flow.redirect_next_hop, 'copy': self.flow.copy, 'mark': self.flow.mark, 'action': self.flow.action, 'community': self.route.community, 'extended-community': self.route.extended_community, }, 'send': { 'parsed': self.process.command, 'packets': self.process.command, 'consolidate': self.process.command, 'open': self.process.command, 'update': self.process.command, 'notification': self.process.command, 'keepalive': self.process.command, 'refresh': self.process.command, 'operational': self.process.command, }, 'receive': { 'parsed': self.process.command, 'packets': self.process.command, 'consolidate': self.process.command, 'open': self.process.command, 'update': self.process.command, 'notification': self.process.command, 'keepalive': self.process.command, 'refresh': self.process.command, 'operational': self.process.command, }, } self._clear() self.processes = {} self._scope = [] self._location = ['root'] def _clear (self): self.processes = {} self._scope = [] self._location = ['root'] self.tokens.clear() self.error.clear() self.neighbor.clear() self.family.clear() self.process.clear() self.route.clear() self.flow.clear() self.l2vpn.clear() self.operational.clear() # Public Interface def reload (self): try: return self._reload() except KeyboardInterrupt: return self.error.set('configuration reload aborted by ^C or SIGINT') except Exception: # unhandled configuration parsing issue raise def _reload (self): # taking the first configuration available (FIFO buffer) fname = self._configurations.pop(0) self.process.configuration(fname) self._configurations.append(fname) # clearing the current configuration to be able to re-parse it self._clear() if not self.tokens.set_file(fname): return False # parsing the configuration r = False while not self.tokens.finished: r = self._dispatch( self._scope,'root','configuration', self._tree['configuration'].keys(), [] ) if r is False: break # handling possible parsing errors if r not in [True,None]: # making sure nothing changed self.neighbor.cancel() return self.error.set( "\n" "syntax error in section %s\n" "line %d: %s\n" "\n%s" % ( self._location[-1], self.tokens.number, ' '.join(self.tokens.line), str(self.error) ) ) # installing in the neighbor the API routes self.neighbor.complete() # we are not really running the program, just want to .... if environment.settings().debug.route: from exabgp.configuration.current.check import check_message if check_message(self.neighbor.neighbors,environment.settings().debug.route): sys.exit(0) sys.exit(1) # we are not really running the program, just want check the configuration validity if environment.settings().debug.selfcheck: from exabgp.configuration.current.check import check_neighbor if check_neighbor(self.neighbor.neighbors): sys.exit(0) sys.exit(1) return True # name is not used yet but will come really handy if we have name collision :D def _dispatch (self, scope, name, command, multi, single, location=None): if location: self._location = location self.flow.clear() try: tokens = self.tokens.next() except IndexError: return self.error.set('configuration file incomplete (most likely missing })') self.logger.configuration("parsing | %-13s | '%s'" % (command,"' '".join(tokens))) end = tokens[-1] if multi and end == '{': self._location.append(tokens[0]) return self._multi_line(scope,command,tokens[1],tokens[:-1],multi) if single and end == ';': return self.run(scope,command,tokens[1],tokens[:-1],single) if end == '}': if len(self._location) == 1: return self.error.set('closing too many parenthesis') self._location.pop(-1) return None return False def _multi (self, tree, scope, name, command, tokens, valid): command = tokens[0] if valid and command not in valid: return self.error.set('option %s in not valid here' % command) if name not in tree: return self.error.set('option %s is not allowed here' % name) run, validate = tree[name].get(command,(false,false)) if not run(scope,name,command,tokens[1:]): return False if not validate(scope,self): return False return True def _multi_line (self, scope, name, command, tokens, valid): return self._multi(self._tree,scope,name,command,tokens,valid) # Programs used to control exabgp def _multi_process (self, scope, name, command, tokens): while True: r = self._dispatch( scope,name,'process', ['send','receive'], [ 'run','encoder', 'neighbor-changes', ] ) if r is False: return False if r is None: break name = tokens[0] if len(tokens) >= 1 else 'conf-only-%s' % str(time.time())[-6:] self.processes.setdefault(name,{})['neighbor'] = scope[-1]['peer-address'] if 'peer-address' in scope[-1] else '*' for key in ['neighbor-changes',]: self.processes[name][key] = scope[-1].pop(key,False) for direction in ['send','receive']: for action in ['packets','parsed','consolidate']: key = '%s-%s' % (direction,action) self.processes[name][key] = scope[-1].pop(key,False) for message in Message.CODE.MESSAGES: key = '%s-%d' % (direction,message) self.processes[name][key] = scope[-1].pop(key,False) run = scope[-1].pop('run','') if run: if len(tokens) != 1: return self.error.set(self.process.syntax) self.processes[name]['encoder'] = scope[-1].get('encoder','') or self.api_encoder self.processes[name]['run'] = run return True elif len(tokens): return self.error.set(self.process.syntax) # Limit the AFI/SAFI pair announced to peers def _multi_family (self, scope, name, command, tokens): # we know all the families we should use scope[-1]['families'] = [] while True: r = self._dispatch( scope,name,'family', [], self._command['family'].keys() ) if r is False: return False if r is None: break self.family.clear() return True # capacity def _multi_capability (self, scope, name, command, tokens): # we know all the families we should use while True: r = self._dispatch( scope,name,'capability', [], self._command['capability'].keys() ) if r is False: return False if r is None: break return True # route grouping with watchdog # Group Neighbor def _multi_group (self, scope, name, command, address): # if len(tokens) != 2: # return self.error.set('syntax: group <name> { <options> }') scope.append({}) while True: r = self._dispatch( scope,name,'group', [ 'static','flow','l2vpn', 'neighbor','process','family', 'capability','operational' ], self._command['neighbor'].keys() ) if r is False: return False if r is None: scope.pop(-1) return True def _multi_neighbor (self, scope, name, command, tokens): if len(tokens) != 1: return self.error.set('syntax: neighbor <ip> { <options> }') address = tokens[0] scope.append({}) try: scope[-1]['peer-address'] = IP.create(address) except (IndexError,ValueError,socket.error): return self.error.set('"%s" is not a valid IP address' % address) while True: r = self._dispatch( scope,name,'neighbor', [ 'static','flow','l2vpn', 'process','family','capability','operational' ], self._command['neighbor'] ) # XXX: THIS SHOULD ALLOW CAPABILITY AND NOT THE INDIVIDUAL SUB KEYS if r is False: return False if r is None: return True # Group Static ................ def _multi_static (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set('syntax: static { route; route; ... }') while True: r = self._dispatch( scope,name,'static', ['route',], ['route',] ) if r is False: return False if r is None: return True # Group Route ........ def _multi_static_route (self, scope, name, command, tokens): if len(tokens) != 1: return self.error.set(self.route.syntax) if not self.route.insert_static_route(scope,name,command,tokens): return False while True: r = self._dispatch( scope,name,'static-route', self._command['static-route'].keys(), self._command['static-route'].keys() ) if r is False: return False if r is None: return self.route.make_split(scope) def _multi_l2vpn (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set(self.l2vpn.syntax) while True: r = self._dispatch( scope,name,'l2vpn', ['vpls',], ['vpls',] ) if r is False: return False if r is None: break return True def _multi_l2vpn_vpls (self, scope, name, command, tokens): if len(tokens) > 1: return self.error.set(self.l2vpn.syntax) if not self.l2vpn.insert_vpls(scope,name,command,tokens): return False while True: r = self._dispatch( scope,name,'l2vpn-vpls', self._command['l2vpn-vpls'].keys(), self._command['l2vpn-vpls'].keys() ) if r is False: return False if r is None: break return True def _multi_flow (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) while True: r = self._dispatch( scope,name,'flow', ['route',], [] ) if r is False: return False if r is None: break return True def _insert_flow_route (self, scope, name, command, tokens=None): if self.flow.state != 'out': return self.error.set(self.flow.syntax) self.flow.state = 'match' try: attributes = Attributes() attributes[Attribute.CODE.EXTENDED_COMMUNITY] = ExtendedCommunities() flow = Change(Flow(),attributes) except ValueError: return self.error.set(self.flow.syntax) if 'announce' not in scope[-1]: scope[-1]['announce'] = [] scope[-1]['announce'].append(flow) return True def _multi_flow_route (self, scope, name, command, tokens): if len(tokens) > 1: return self.error.set(self.flow.syntax) if not self._insert_flow_route(scope,name,command): return False while True: r = self._dispatch( scope,name,'flow-route', ['match','then'], ['rd','route-distinguisher','next-hop'] ) if r is False: return False if r is None: break if self.flow.state != 'out': return self.error.set(self.flow.syntax) return True # .......................................... def _multi_match (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) if self.flow.state != 'match': return self.error.set(self.flow.syntax) self.flow.state = 'then' while True: r = self._dispatch( scope,name,'flow-match', [], [ 'source','destination', 'source-ipv4','destination-ipv4', 'port','source-port','destination-port', 'protocol','next-header','tcp-flags','icmp-type','icmp-code', 'fragment','dscp','traffic-class','packet-length','flow-label' ] ) if r is False: return False if r is None: break return True def _multi_then (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set(self.flow.syntax) if self.flow.state != 'then': return self.error.set(self.flow.syntax) self.flow.state = 'out' while True: r = self._dispatch( scope,name,'flow-then', [], [ 'accept','discard','rate-limit', 'redirect','copy','redirect-to-nexthop', 'mark','action', 'community','extended-community' ] ) if r is False: return False if r is None: break return True # .......................................... def _multi_api (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set('api issue') while True: r = self._dispatch( scope,name,command, [], self._command[command].keys() ) if r is False: return False if r is None: break return True # Group Operational ................ def _multi_operational (self, scope, name, command, tokens): if len(tokens) != 0: return self.error.set('syntax: operational { command; command; ... }') while True: r = self._dispatch( scope,name,command, [], self._command[command].keys() ) if r is False: return False if r is None: return True def run (self, scope, name, comamnd, tokens, valid): command = tokens[0] if valid and command not in valid: return self.error.set('invalid keyword "%s"' % command) family = { 'static-route': { 'rd': SAFI.mpls_vpn, 'route-distinguisher': SAFI.mpls_vpn, }, 'l2vpn-vpls': { 'rd': SAFI.vpls, 'route-distinguisher': SAFI.vpls, }, 'flow-route': { 'rd': SAFI.flow_vpn, 'route-distinguisher': SAFI.flow_vpn, } } if name in self._command: if command in self._command[name]: if command in family.get(name,{}): return self._command[name][command](scope,name,command,tokens[1:],family[name][command]) return self._command[name][command](scope,name,command,tokens[1:]) return self.error.set('command not known %s' % command)